Fix icon picker positioning + match input height + add overlay rule
Some checks failed
Validate / Hassfest (push) Has been cancelled
Some checks failed
Validate / Hassfest (push) Has been cancelled
- IconPicker: use position:fixed with getBoundingClientRect() for dropdown (fixes rendering at page footer instead of below button) - Match icon button height to text input (py-2 same as inputs) - CLAUDE.md: add rule about overlays requiring position:fixed with inline styles (Tailwind v4 classes unreliable in flex containers) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@ The README is the primary user-facing documentation and must accurately reflect
|
|||||||
3. Start: `cd <repo_root> && IMMICH_WATCHER_DATA_DIR=./test-data IMMICH_WATCHER_SECRET_KEY=test-secret-key-minimum-32chars nohup python -m uvicorn immich_watcher_server.main:app --host 0.0.0.0 --port 8420 > /dev/null 2>&1 &`
|
3. Start: `cd <repo_root> && IMMICH_WATCHER_DATA_DIR=./test-data IMMICH_WATCHER_SECRET_KEY=test-secret-key-minimum-32chars nohup python -m uvicorn immich_watcher_server.main:app --host 0.0.0.0 --port 8420 > /dev/null 2>&1 &`
|
||||||
4. Verify: `curl -s http://localhost:8420/api/health`
|
4. Verify: `curl -s http://localhost:8420/api/health`
|
||||||
|
|
||||||
|
**IMPORTANT**: Overlays (modals, dropdowns, pickers) MUST use `position: fixed` with inline styles and `z-index: 9999`. Tailwind CSS v4 `fixed`/`absolute` classes do NOT work reliably inside flex/overflow containers in this project. Always calculate position from `getBoundingClientRect()` for dropdowns, or use `top:0;left:0;right:0;bottom:0` for full-screen backdrops.
|
||||||
|
|
||||||
**IMPORTANT**: When the user requests it, restart the frontend dev server:
|
**IMPORTANT**: When the user requests it, restart the frontend dev server:
|
||||||
1. Kill existing process on port 5173
|
1. Kill existing process on port 5173
|
||||||
2. Start: `cd frontend && npx vite dev --port 5173 --host &`
|
2. Start: `cd frontend && npx vite dev --port 5173 --host &`
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from '$lib/i18n';
|
|
||||||
import * as mdi from '@mdi/js';
|
import * as mdi from '@mdi/js';
|
||||||
|
|
||||||
let { value = '', onselect } = $props<{
|
let { value = '', onselect } = $props<{
|
||||||
@@ -9,12 +8,11 @@
|
|||||||
|
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
let search = $state('');
|
let search = $state('');
|
||||||
|
let buttonEl: HTMLButtonElement;
|
||||||
|
let dropdownStyle = $state('');
|
||||||
|
|
||||||
// Build searchable icon list from @mdi/js exports
|
|
||||||
// Each export is like mdiAccount = "M12 4..." (SVG path)
|
|
||||||
const allIcons = Object.keys(mdi).filter(k => k.startsWith('mdi') && k !== 'default');
|
const allIcons = Object.keys(mdi).filter(k => k.startsWith('mdi') && k !== 'default');
|
||||||
|
|
||||||
// Popular icons shown first when search is empty
|
|
||||||
const popular = [
|
const popular = [
|
||||||
'mdiServer', 'mdiCamera', 'mdiImage', 'mdiVideo', 'mdiBell', 'mdiSend',
|
'mdiServer', 'mdiCamera', 'mdiImage', 'mdiVideo', 'mdiBell', 'mdiSend',
|
||||||
'mdiRobot', 'mdiHome', 'mdiStar', 'mdiHeart', 'mdiAccount', 'mdiFolder',
|
'mdiRobot', 'mdiHome', 'mdiStar', 'mdiHeart', 'mdiAccount', 'mdiFolder',
|
||||||
@@ -38,6 +36,15 @@
|
|||||||
return (mdi as any)[iconName] || '';
|
return (mdi as any)[iconName] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleOpen() {
|
||||||
|
if (!open && buttonEl) {
|
||||||
|
const rect = buttonEl.getBoundingClientRect();
|
||||||
|
dropdownStyle = `position:fixed; z-index:9999; top:${rect.bottom + 4}px; left:${rect.left}px;`;
|
||||||
|
}
|
||||||
|
open = !open;
|
||||||
|
if (!open) search = '';
|
||||||
|
}
|
||||||
|
|
||||||
function select(iconName: string) {
|
function select(iconName: string) {
|
||||||
onselect(iconName);
|
onselect(iconName);
|
||||||
open = false;
|
open = false;
|
||||||
@@ -45,37 +52,37 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="inline-block relative">
|
<div class="inline-block">
|
||||||
<button type="button" onclick={() => open = !open}
|
<button type="button" bind:this={buttonEl} onclick={toggleOpen}
|
||||||
class="flex items-center gap-1 px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)] hover:bg-[var(--color-muted)] transition-colors">
|
class="flex items-center justify-center gap-1 px-2 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)] hover:bg-[var(--color-muted)] transition-colors">
|
||||||
{#if value && getPath(value)}
|
{#if value && getPath(value)}
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d={getPath(value)} /></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d={getPath(value)} /></svg>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-[var(--color-muted-foreground)]">Icon</span>
|
<span class="text-[var(--color-muted-foreground)] text-xs">Icon</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="text-xs text-[var(--color-muted-foreground)]">▼</span>
|
<span class="text-xs text-[var(--color-muted-foreground)]">▼</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if open}
|
|
||||||
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9998;"
|
|
||||||
onclick={() => { open = false; search = ''; }}></div>
|
|
||||||
<div style="position: absolute; z-index: 9999; top: 100%; left: 0; margin-top: 0.25rem;"
|
|
||||||
class="bg-[var(--color-card)] border border-[var(--color-border)] rounded-lg shadow-lg p-3 w-72">
|
|
||||||
<input type="text" bind:value={search} placeholder="Search icons..."
|
|
||||||
class="w-full px-2 py-1 mb-2 border border-[var(--color-border)] rounded text-sm bg-[var(--color-background)]" />
|
|
||||||
<div class="grid grid-cols-8 gap-1 max-h-48 overflow-y-auto">
|
|
||||||
<!-- Clear option -->
|
|
||||||
<button type="button" onclick={() => select('')}
|
|
||||||
class="flex items-center justify-center w-8 h-8 rounded hover:bg-[var(--color-muted)] text-xs text-[var(--color-muted-foreground)]"
|
|
||||||
title="No icon">✕</button>
|
|
||||||
{#each filtered() as iconName}
|
|
||||||
<button type="button" onclick={() => select(iconName)}
|
|
||||||
class="flex items-center justify-center w-8 h-8 rounded hover:bg-[var(--color-muted)] {value === iconName ? 'bg-[var(--color-accent)]' : ''}"
|
|
||||||
title={iconName.replace('mdi', '')}>
|
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d={getPath(iconName)} /></svg>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div style="position:fixed; top:0; left:0; right:0; bottom:0; z-index:9998;" onclick={() => { open = false; search = ''; }}></div>
|
||||||
|
|
||||||
|
<div style="{dropdownStyle}" class="bg-[var(--color-card)] border border-[var(--color-border)] rounded-lg shadow-lg p-3 w-72">
|
||||||
|
<input type="text" bind:value={search} placeholder="Search icons..."
|
||||||
|
class="w-full px-2 py-1 mb-2 border border-[var(--color-border)] rounded text-sm bg-[var(--color-background)]" />
|
||||||
|
<div class="grid grid-cols-8 gap-1 max-h-48 overflow-y-auto">
|
||||||
|
<button type="button" onclick={() => select('')}
|
||||||
|
class="flex items-center justify-center w-8 h-8 rounded hover:bg-[var(--color-muted)] text-xs text-[var(--color-muted-foreground)]"
|
||||||
|
title="No icon">✕</button>
|
||||||
|
{#each filtered() as iconName}
|
||||||
|
<button type="button" onclick={() => select(iconName)}
|
||||||
|
class="flex items-center justify-center w-8 h-8 rounded hover:bg-[var(--color-muted)] {value === iconName ? 'bg-[var(--color-accent)]' : ''}"
|
||||||
|
title={iconName.replace('mdi', '')}>
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d={getPath(iconName)} /></svg>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user