Skip to content

Commit 0090656

Browse files
committed
Swap UnoCSS for unplugin-icons to restore FE hot reload
- UnoCSS's dev plugin was regenerating `virtual:uno.css` on every Svelte save, trickling through the root layout and triggering SvelteKit's TDZ crash (sveltejs/kit#15287). Root-layout HMR became unreliable across the app. - `unplugin-icons` (v23.0.1) with `@iconify-json/lucide` replaces UnoCSS. Icons are inline SVG Svelte components imported as `~icons/lucide/{name}` — tree-shaken, no runtime JS beyond the SVG itself, no virtual-CSS module in the HMR path. - Updated the three users (`ErrorPane`, `FullList`, `SelectionInfo`), sizing via explicit `width`/`height` props and coloring via a wrapping `<span>` with a scoped class (same `currentColor` mechanic as before). - Removed `unocss`, `@unocss/preset-icons`, `uno.config.ts`, the `virtual:uno.css` import, and the icon-class entries in `scripts/check-css-unused/allowlist.go`. - Added `src/unplugin-icons.d.ts` for the `~icons/*` virtual module types; registered the `Icons()` plugin in `vite.config.js` and `vitest.config.ts`; added `ignoreUnresolved: ["~icons/.+"]` to `knip.json`. - `hmr-recovery.ts` stays in place — it's defense-in-depth against any future root-layout HMR TDZ crash, not specific to UnoCSS. - Docs refreshed: `AGENTS.md`, `docs/style-guide.md` § Icons, and the three colocated `CLAUDE.md` files.
1 parent f37d7e5 commit 0090656

18 files changed

Lines changed: 132 additions & 671 deletions

File tree

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ resilience, and common pitfalls.
159159
`withGlobalTauri: true` in dev mode is a security risk.
160160
- ❌ When testing the Tauri app, DO NOT USE THE BROWSER. Use the MCP servers.
161161
- ❌ Don't ignore linter warnings — fix them or justify with a comment.
162-
- **Icons**: We use UnoCSS with the Icons preset (`@iconify-json/lucide`). Icons are pure CSS classes like
163-
`i-lucide:triangle-alert`no JS imports. See `docs/style-guide.md` § Icons for usage, sizing, coloring, and how to
164-
find new icons. When adding a new icon, also add it to `scripts/check-css-unused/allowlist.go`.
162+
- **Icons**: We use `unplugin-icons` with `@iconify-json/lucide`. Import as Svelte components from
163+
`~icons/lucide/{icon-name}` (inline SVGs, no runtime cost). See `docs/style-guide.md` § Icons for usage, sizing,
164+
coloring, and how to find new icons.
165165
- Always use CSS variables defined in `apps/desktop/src/app.css`. Stylelint catches undefined/hallucinated variables.
166166
- Never use raw `px` values for `font-size`, `border-radius`, `font-family`, or `z-index` >= 10. Use
167167
`var(--font-size-*)`, `var(--radius-*)`, `var(--font-*)`, and `var(--z-*)` tokens. Stylelint enforces this.

apps/desktop/knip.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"$schema": "https://unpkg.com/knip@5/schema.json",
33
"ignoreBinaries": ["only-allow", "rustc"],
44
"ignore": ["src/lib/tauri-commands/**"],
5+
"ignoreUnresolved": ["~icons/.+"],
56
"ignoreDependencies": [
67
"oxlint",
78
"@tauri-apps/cli",

apps/desktop/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
},
4949
"devDependencies": {
5050
"@eslint/js": "^10.0.1",
51-
"@iconify-json/lucide": "1.2.96",
51+
"@iconify-json/lucide": "1.2.102",
5252
"@playwright/test": "^1.58.2",
5353
"@srsholmes/tauri-playwright": "^0.2.1",
5454
"@sveltejs/adapter-static": "^3.0.10",
@@ -57,7 +57,6 @@
5757
"@tauri-apps/cli": "^2.10.1",
5858
"@testing-library/svelte": "^5.3.1",
5959
"@types/node": "^25.5.0",
60-
"@unocss/preset-icons": "66.6.6",
6160
"@vitest/coverage-v8": "^4.1.0",
6261
"axe-core": "^4.11.1",
6362
"eslint": "^10.1.0",
@@ -80,7 +79,7 @@
8079
"tsx": "^4.21.0",
8180
"typescript": "~5.9.3",
8281
"typescript-eslint": "^8.57.1",
83-
"unocss": "66.6.6",
82+
"unplugin-icons": "23.0.1",
8483
"vite": "^8.0.2",
8584
"vitest": "^4.1.0"
8685
}

apps/desktop/src/lib/file-explorer/CLAUDE.md

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,9 @@ and actionable error experience.
177177
Receives a `FriendlyError` struct from Rust (all content is pre-baked on the backend, the frontend doesn't do any error
178178
classification or OS-specific logic):
179179

180-
- **Title**: large text, always in accent color. UnoCSS/Lucide icon signals severity: ⚠ `i-lucide:triangle-alert` in
181-
warning color for transient, ⊘ `i-lucide:circle-alert` in error color for serious, no icon for needs-action
180+
- **Title**: large text, always in accent color. Lucide icon (via `unplugin-icons`) signals severity: ⚠
181+
`~icons/lucide/triangle-alert` in warning color for transient, ⊘ `~icons/lucide/circle-alert` in error color for
182+
serious, no icon for needs-action
182183
- **Folder path**: shown in secondary text so the user knows exactly which folder is affected
183184
- **Explanation**: rendered as markdown via `snarkdown` — plain-language description of what happened
184185
- **Suggestion**: rendered as markdown — actionable steps, often provider-specific (for example, "Open **MacDroid** and
@@ -226,20 +227,12 @@ to O(visible). See [benchmarks](../../../../../docs/notes/non-reactive-file-stor
226227

227228
## Gotchas
228229

229-
**UnoCSS content list is manually tracked.** `uno.config.ts` lists the specific files that use UnoCSS classes
230-
(`i-lucide:*` icons) so UnoCSS only watches those files during dev, not the entire `src/` tree. Without this, every file
231-
change triggers 6-7 redundant HMR updates. When adding UnoCSS classes to a new file, add that file to the
232-
`content.filesystem` array in `uno.config.ts`.
233-
234-
**UnoCSS triggers SvelteKit root-layout HMR crash.** `virtual:uno.css` regenerates on every Svelte file save. Because
235-
it's imported in the root `+layout.svelte`, Vite treats it as a root-layout change, which forces SvelteKit to rebuild
236-
the entire route tree. SvelteKit's client router (`client.js:373`, `get_navigation_result_from_branch`) crashes with
230+
**Root-layout HMR can trigger a SvelteKit TDZ crash.** When an HMR update propagates through the root `+layout.svelte`
231+
(for example, `app.css` changes), SvelteKit's client router can crash with
237232
`ReferenceError: Cannot access 'component' before initialization` — a TDZ error where a route component module hasn't
238-
finished importing during the rebuild. This is a SvelteKit bug (sveltejs/kit#15287, observed with SvelteKit 2.55.0 /
239-
Svelte 5.54.1 / Vite 8.0.2). Workaround: `import.meta.hot.accept(() => { import.meta.hot!.invalidate() })` in the root
240-
layout catches the update and triggers a clean full page reload instead of the broken HMR path. Side effect: edits to
241-
the root layout or its deps (app.css, virtual:uno.css) cause a full reload instead of hot-swap. Leaf component edits are
242-
unaffected — SvelteKit handles those fine. If sveltejs/kit#15287 gets fixed, the workaround can be removed.
233+
finished importing during the rebuild. This is a SvelteKit bug (sveltejs/kit#15287). `$lib/hmr-recovery.ts` catches the
234+
crash and forces a clean page reload. The recovery listener is imported from `+layout.ts` (a stable module that survives
235+
layout component re-evaluation). If sveltejs/kit#15287 gets fixed, the workaround can be removed.
243236

244237
## Tabs (`tabs/`)
245238

apps/desktop/src/lib/file-explorer/pane/ErrorPane.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script lang="ts">
22
import { onDestroy } from 'svelte'
3+
import IconCircleAlert from '~icons/lucide/circle-alert'
4+
import IconTriangleAlert from '~icons/lucide/triangle-alert'
35
import type { FriendlyError } from '../types'
46
import { openPrivacySettings } from '$lib/tauri-commands'
57
import { isMacOS } from '$lib/shortcuts/key-capture'
@@ -65,9 +67,9 @@
6567
<div class="content">
6668
<h2 class="title">
6769
{#if friendly.category === 'serious'}
68-
<span class="title-icon icon-error i-lucide:circle-alert" style="width: 20px; height: 20px;"></span>
70+
<span class="title-icon icon-error"><IconCircleAlert width="20" height="20" /></span>
6971
{:else if friendly.category === 'transient'}
70-
<span class="title-icon icon-warning i-lucide:triangle-alert" style="width: 20px; height: 20px;"></span>
72+
<span class="title-icon icon-warning"><IconTriangleAlert width="20" height="20" /></span>
7173
{/if}
7274
{friendly.title}
7375
</h2>

apps/desktop/src/lib/file-explorer/selection/CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ In `selection-summary` mode, directory recursive sizes are included in the size
4747
index). The `hasOnlyDirs` branch shows size triads when `totalSize > 0`; when sizes are unavailable (indexing off), it
4848
falls back to showing only dir count and percentage.
4949

50-
Stale indicator (UnoCSS/Lucide `i-lucide:hourglass` icon in accent color) appears in `selection-summary` when
51-
`isScanning()` is true and directories are selected, because dir sizes may be incomplete during scanning.
50+
Stale indicator (Lucide hourglass icon via `~icons/lucide/hourglass`, rendered in accent color) appears in
51+
`selection-summary` when `isScanning()` is true and directories are selected, because dir sizes may be incomplete during
52+
scanning.
5253

5354
Filename truncation in `file-info` mode uses the `useShortenMiddle` action with `preferBreakAt: '.'` to preserve file
5455
extensions. The action uses pretext for canvas-based measurement and a built-in ResizeObserver.

apps/desktop/src/lib/file-explorer/selection/SelectionInfo.svelte

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import IconHourglass from '~icons/lucide/hourglass'
23
import type { FileEntry, ListingStats } from '../types'
34
import {
45
buildDateTooltip,
@@ -220,8 +221,8 @@
220221
{pluralize(totalDirs, 'dir', 'dirs')}{#if totalSize === 0}
221222
selected{/if}.
222223
{#if showSelectionStale}
223-
<span class="stale-indicator" use:tooltip={'Updating index — size may change.'}
224-
><span class="i-lucide:hourglass stale-icon"></span></span
224+
<span class="stale-indicator stale-icon" use:tooltip={'Updating index — size may change.'}
225+
><IconHourglass width="12" height="12" /></span
225226
>
226227
{/if}
227228
{:else if hasFiles}
@@ -234,8 +235,8 @@
234235
&nbsp;and {formatNumber(selectedDirs)} of {formatNumber(totalDirs)}
235236
{pluralize(totalDirs, 'dir', 'dirs')}{/if}.
236237
{#if showSelectionStale}
237-
<span class="stale-indicator" use:tooltip={'Updating index — size may change.'}
238-
><span class="i-lucide:hourglass stale-icon"></span></span
238+
<span class="stale-indicator stale-icon" use:tooltip={'Updating index — size may change.'}
239+
><IconHourglass width="12" height="12" /></span
239240
>
240241
{/if}
241242
{/if}
@@ -303,7 +304,5 @@
303304
.stale-icon {
304305
/* stylelint-disable-next-line declaration-property-value-disallowed-list -- small icon indicator, not body text */
305306
color: var(--color-accent);
306-
width: 12px;
307-
height: 12px;
308307
}
309308
</style>

apps/desktop/src/lib/file-explorer/views/CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ without DOM performance issues.
2020
- **measure-brief-column-widths.ts**`measureWidestFilename()`: widest filename's pixel width in a Brief column,
2121
measured via pretext. Caller adds icon/gap/padding chrome and clamps to the min/max column-width range.
2222
- **FullList.svelte** – Reads `listing.sizeDisplay` (via `getSizeDisplayMode()`) and `listing.sizeMismatchWarning` (via
23-
`getSizeMismatchWarning()`) settings. Uses UnoCSS/Lucide `i-lucide:circle-alert` for size mismatch warnings and
24-
`i-lucide:hourglass` for stale index indicators
23+
`getSizeMismatchWarning()`) settings. Uses Lucide icons (via `unplugin-icons`): `~icons/lucide/circle-alert` for size
24+
mismatch warnings and `~icons/lucide/hourglass` for stale index indicators
2525
- **dir-size-display.test.ts** – Tests for `getDirSizeDisplayState` / `buildDirSizeTooltip` (functions in
2626
`full-list-utils.ts`)
2727
- **view-modes.test.ts** – Integration tests for hidden-file filtering and directory listing structure (uses

apps/desktop/src/lib/file-explorer/views/FullList.svelte

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
import IconCircleAlert from '~icons/lucide/circle-alert'
3+
import IconHourglass from '~icons/lucide/hourglass'
24
import type { FileEntry, SortColumn, SortOrder, SyncStatus } from '../types'
35
import { calculateVirtualWindow, getScrollToPosition } from './virtual-scroll'
46
import { startSelectionDragTracking, type DragFileInfo } from '../drag/drag-drop'
@@ -595,8 +597,8 @@
595597
<span class={triad.tierClass}>{triad.value}</span>
596598
{/each}
597599
{#if indexing}
598-
<span class="size-stale" use:tooltip={'Updating index — size may change.'}
599-
><span class="i-lucide:hourglass icon-indicator"></span></span
600+
<span class="size-stale icon-indicator" use:tooltip={'Updating index — size may change.'}
601+
><IconHourglass width="12" height="12" /></span
600602
>
601603
{/if}
602604
{#if showSizeMismatchWarning && hasSizeMismatch(file.recursiveSize, file.recursivePhysicalSize)}
@@ -613,14 +615,14 @@
613615
{@const dirTooltipHtml =
614616
typeof dirTooltip === 'object' ? dirTooltip.html : dirTooltip}
615617
<span
616-
class="size-mismatch"
618+
class="size-mismatch icon-indicator"
617619
use:tooltip={{
618620
html:
619621
'Content and on-disk sizes differ significantly.<br><br>' +
620622
dirTooltipHtml,
621623
}}
622624
>
623-
<span class="i-lucide:circle-alert icon-indicator"></span>
625+
<IconCircleAlert width="12" height="12" />
624626
</span>
625627
{/if}
626628
{:else if indexing}
@@ -764,8 +766,6 @@
764766
.icon-indicator {
765767
/* stylelint-disable-next-line declaration-property-value-disallowed-list -- small icon indicator, not body text */
766768
color: var(--color-accent);
767-
width: 12px;
768-
height: 12px;
769769
}
770770
771771
.size-stale {

apps/desktop/src/routes/+layout.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* Other windows (viewer, debug) get only this minimal layout.
66
*/
77
import { onMount } from 'svelte'
8-
import 'virtual:uno.css'
98
import '../app.css'
109
import { initLogger } from '$lib/logging/logger'
1110

0 commit comments

Comments
 (0)