release: v0.18.1 (#734)
## Thanks - [@RichKidsDev](https://github.com/RichKidsDev) for reporting the admin layout on iPhone (#732). ### Fixed - **The admin fits a phone** ([#733](https://github.com/ralksta/immich-folio/pull/733), reported in [#732](https://github.com/ralksta/immich-folio/issues/732) by [@RichKidsDev](https://github.com/RichKidsDev)). Below 860px the sidebar unfolded above the content — navigation, status and four actions — and filled most of an iPhone screen. It is now a header row with the brand and a menu button; the menu opens as one column of full-width rows and closes again on navigation, and an unread message shows as a dot on the button. Desktop is unchanged. - **The settings editor refuses prototype keys in a setting's path** ([#731](https://github.com/ralksta/immich-folio/pull/731)). All paths are written in the code, so this was not reachable; CodeQL flagged the setter after the v0.18.0 release. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ralf <rohlenbo79@gmail.com> Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
This commit is contained in:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
Releases up to and including v0.9.2 are documented in the
|
Releases up to and including v0.9.2 are documented in the
|
||||||
[GitHub releases](https://github.com/ralksta/immich-folio/releases).
|
[GitHub releases](https://github.com/ralksta/immich-folio/releases).
|
||||||
|
|
||||||
|
## [0.18.1] — 2026-09-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **The admin fits a phone**
|
||||||
|
([#733](https://github.com/ralksta/immich-folio/pull/733), reported in
|
||||||
|
[#732](https://github.com/ralksta/immich-folio/issues/732) by
|
||||||
|
[@RichKidsDev](https://github.com/RichKidsDev)). Below 860px the sidebar
|
||||||
|
unfolded above the content — navigation, status and four actions — and
|
||||||
|
filled most of an iPhone screen. It is now a header row with the brand and
|
||||||
|
a menu button; the menu opens as one column of full-width rows and closes
|
||||||
|
again on navigation, and an unread message shows as a dot on the button.
|
||||||
|
Desktop is unchanged.
|
||||||
|
|
||||||
|
- **The settings editor refuses prototype keys in a setting's path**
|
||||||
|
([#731](https://github.com/ralksta/immich-folio/pull/731)). All paths are
|
||||||
|
written in the code, so this was not reachable; CodeQL flagged the setter
|
||||||
|
after the v0.18.0 release.
|
||||||
|
|
||||||
## [0.18.0] — 2026-09-27
|
## [0.18.0] — 2026-09-27
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
A self-hosted portfolio powered by [Immich](https://immich.app). It acts as a **secure reverse proxy** between your visitors and your private Immich instance: your Immich server stays on your local network, completely invisible to the outside world, while your albums are published as a gallery you control.
|
A self-hosted portfolio powered by [Immich](https://immich.app). It acts as a **secure reverse proxy** between your visitors and your private Immich instance: your Immich server stays on your local network, completely invisible to the outside world, while your albums are published as a gallery you control.
|
||||||
|
|
||||||
**Latest: v0.18.0** — built-in contact form, privacy page and a reworked admin panel. → [What's New](#whats-new)
|
**Latest: v0.18.1** — built-in contact form, privacy page and a reworked admin panel. → [What's New](#whats-new)
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ const SIDEBAR_KEY = 'folio_admin_sidebar';
|
|||||||
export default function AdminDashboard({ onLogout, children }: Props) {
|
export default function AdminDashboard({ onLogout, children }: Props) {
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
// Phones: the sidebar folds into a header row, and the nav opens from a
|
||||||
|
// menu button (#732) — spread out above the content, it took most of an
|
||||||
|
// iPhone screen before anything could be edited.
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
useEffect(() => setMenuOpen(false), [pathname]);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// Diagnostics & Backup state
|
// Diagnostics & Backup state
|
||||||
@@ -267,7 +272,11 @@ export default function AdminDashboard({ onLogout, children }: Props) {
|
|||||||
const missingCredentials = status?.setup?.credentials === 'missing';
|
const missingCredentials = status?.setup?.credentials === 'missing';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`admin-dashboard admin-app${collapsed ? ' is-collapsed' : ''}`}>
|
<div
|
||||||
|
className={`admin-dashboard admin-app${collapsed ? ' is-collapsed' : ''}${
|
||||||
|
menuOpen ? ' menu-open' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<aside className="admin-sidebar" aria-label="Admin">
|
<aside className="admin-sidebar" aria-label="Admin">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -298,7 +307,29 @@ export default function AdminDashboard({ onLogout, children }: Props) {
|
|||||||
<span className="admin-brand-sub">Admin</span>
|
<span className="admin-brand-sub">Admin</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-menu-toggle"
|
||||||
|
onClick={() => setMenuOpen((open) => !open)}
|
||||||
|
aria-expanded={menuOpen}
|
||||||
|
aria-controls="admin-sidebar-menu"
|
||||||
|
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{menuOpen ? <path d="M6 6l12 12M18 6L6 18" /> : <path d="M4 7h16M4 12h16M4 17h16" />}
|
||||||
|
</svg>
|
||||||
|
{unread > 0 && !menuOpen && <span className="admin-menu-dot" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
<div className="admin-sidebar-menu" id="admin-sidebar-menu">
|
||||||
<nav className="admin-nav">
|
<nav className="admin-nav">
|
||||||
{NAV.map((section, i) => (
|
{NAV.map((section, i) => (
|
||||||
<div key={section.group ?? i} className="admin-nav-group">
|
<div key={section.group ?? i} className="admin-nav-group">
|
||||||
@@ -322,7 +353,10 @@ export default function AdminDashboard({ onLogout, children }: Props) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{item.badge === 'health' && health.tone === 'disconnected' && (
|
{item.badge === 'health' && health.tone === 'disconnected' && (
|
||||||
<span className={`admin-nav-dot ${health.tone}`} aria-label={health.label} />
|
<span
|
||||||
|
className={`admin-nav-dot ${health.tone}`}
|
||||||
|
aria-label={health.label}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -469,6 +503,7 @@ export default function AdminDashboard({ onLogout, children }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="admin-main admin-workspace">{children}</main>
|
<main className="admin-main admin-workspace">{children}</main>
|
||||||
|
|||||||
@@ -188,6 +188,12 @@ export default function SettingsEditor() {
|
|||||||
setSettings((s) => {
|
setSettings((s) => {
|
||||||
const copy = JSON.parse(JSON.stringify(s));
|
const copy = JSON.parse(JSON.stringify(s));
|
||||||
const parts = path.split('.');
|
const parts = path.split('.');
|
||||||
|
// Paths are literals in the section components, but a key like
|
||||||
|
// `__proto__` would write through to Object.prototype (CodeQL
|
||||||
|
// js/prototype-pollution-utility).
|
||||||
|
if (parts.some((p) => p === '__proto__' || p === 'constructor' || p === 'prototype')) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
let obj = copy;
|
let obj = copy;
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
if (!obj[parts[i]]) obj[parts[i]] = {};
|
if (!obj[parts[i]]) obj[parts[i]] = {};
|
||||||
|
|||||||
@@ -1247,3 +1247,80 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Phone header (#732) ───────────────────────────────────── */
|
||||||
|
|
||||||
|
/* On a phone the sidebar used to unfold above the content — nav, status,
|
||||||
|
and four actions — and filled most of an iPhone screen. It is now a
|
||||||
|
header row with the brand and a menu button; the rest opens on demand. */
|
||||||
|
.admin-menu-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.admin-sidebar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar .admin-brand {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu-toggle {
|
||||||
|
position: relative;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: var(--admin-radius);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--admin-text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu-toggle:focus-visible {
|
||||||
|
outline: var(--admin-focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
right: 7px;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--admin-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar-menu {
|
||||||
|
display: none;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-app.menu-open .admin-sidebar-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-app.menu-open .admin-sidebar {
|
||||||
|
max-height: 100dvh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One column: tap targets of a full row, not a wrapped cloud of links. */
|
||||||
|
.admin-nav {
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav .admin-nav-item {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-folio",
|
"name": "immich-folio",
|
||||||
"version": "0.18.0",
|
"version": "0.18.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-folio",
|
"name": "immich-folio",
|
||||||
"version": "0.18.0",
|
"version": "0.18.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-folio",
|
"name": "immich-folio",
|
||||||
"version": "0.18.0",
|
"version": "0.18.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user