@mrmeg/expo-ui 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,74 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ### Added
9
+
10
+ - **`ThemeSelector`** (`@mrmeg/expo-ui/components`): a compact System / Light /
11
+ Dark switch bound to `useThemeStore`, so a settings screen needs one row for
12
+ appearance instead of three radio rows. Icon-only by default (about 116 pt,
13
+ sized for `ItemActions`); `showLabels` stretches it with text. Translated
14
+ `labels` name each segment for screen readers. Drawn in RN on every platform,
15
+ with a 44 pt vertical hit area and a selection haptic; `value` /
16
+ `onValueChange` make it controlled.
17
+ - **`registerIcons(icons)`** and **`isIconName(value)`**
18
+ (`@mrmeg/expo-ui/components/Icon`): register an app's glyphs once at module
19
+ scope in the root layout, and narrow a runtime string to `IconName`. The
20
+ `IconRegistry` interface they back is what an app's generated module
21
+ augments. Also exported: the `IconGlyph`, `IconTag`, `IconComponent`,
22
+ `IconAccessibilityProps`, and `NamedIconProps` types, and a `testID` prop on
23
+ `Icon` (a named icon defaults to `icon-<name>` in development).
24
+ - **`expo-ui-icons` bin**: compiles `client/icons.json` against the shipped
25
+ Lucide snapshot (`dist/icons/lucide.json`, Lucide 1.55.0, ISC license in
26
+ `dist/icons/LICENSE.lucide`) into `client/icons.generated.ts` (`appIcons` plus
27
+ the `IconRegistry` augmentation). `init` seeds the manifest from the app's
28
+ `name="…"` / `icon="…"` literals, `add` inserts names (an alias becomes its
29
+ canonical name), `--check` fails on a stale module, and `tabs` rasterizes the
30
+ `tabIcons` subset to 1x/2x/3x template PNGs for a native tab bar, with a
31
+ hash manifest so `tabs --check` needs no rasterizer.
32
+ - `design-system.json` `icons` gains `aliases` (Lucide's renamed names →
33
+ canonical) and `lucide` (the snapshot version).
34
+ - Exact `./components/iconTypes` (the glyph types) and
35
+ `./components/iconBuiltins.generated` (`BUILTIN_ICONS`) subpaths. `Icon`
36
+ re-exports everything an app needs; these exist so every `.ts` module the
37
+ `./components/*` pattern exposes also resolves through the repo's source
38
+ condition.
39
+
40
+ ### Changed
41
+
42
+ - **`Icon` renders package-owned glyph data instead of `lucide-react-native`.**
43
+ Glyphs are plain `[tag, attrs]` data drawn by the package: an inline `<svg>`
44
+ on web and in server HTML (stroked `currentColor`, so theme changes recolor
45
+ through CSS), `react-native-svg` on native with Lucide's own geometry and
46
+ stroke handling. The web bundle no longer carries `react-native-svg` or the
47
+ Lucide runtime, and Jest needs no icon mocks.
48
+ - **`IconName` is the 19 builtins plus app-registered names.** The package
49
+ registers only what its components draw (`check`, `x`, the chevrons, the
50
+ status and theme-mode glyphs, `eye`/`eye-off`, `user`, …); every other name
51
+ comes from the app's `client/icons.json`. `<Icon name>` call sites do not
52
+ change once the names are registered.
53
+ - `ToggleIcon` and `ToggleGroupIcon` take `IconProps` (`name` or `component`,
54
+ plus `size`, `style`, `accessibilityLabel`, …) like `Button.Icon`: color from
55
+ the control's text color and decorative by default.
56
+ - `Icon`'s `component` prop is typed `IconComponent` (`size`, `color`, `style`,
57
+ accessibility props); any component taking `size` and `color` still fits.
58
+
59
+ Upgrading from 0.28: run `bunx expo-ui-icons init` once, typecheck, and
60
+ `bunx expo-ui-icons add` each name it reports (names built at runtime are not
61
+ found by the scan); add `registerIcons(appIcons)` at module scope in the root
62
+ layout (`import { appIcons } from "@/client/icons.generated"`); register
63
+ `appIcons` in the Jest setup too; delete the Lucide Jest module mapping or
64
+ `transformIgnorePatterns` entry and any `iconRegistry.generated` mock; and
65
+ remove `lucide-react-native` from `package.json` once nothing imports it.
66
+ Optionally list `tabIcons` and run `bunx expo-ui-icons tabs` to replace
67
+ `@expo/vector-icons` tab glyphs with template PNGs. Add
68
+ `bunx expo-ui-icons --check` to CI.
69
+
70
+ ### Removed
71
+
72
+ - The `lucide-react-native` peer dependency.
73
+ - The `./components/iconRegistry.generated` export (`ICONS`); glyphs are
74
+ registered data now, and `IconName` comes from `@mrmeg/expo-ui/components/Icon`.
75
+
8
76
  ## [0.28.0]
9
77
 
10
78
  ### Added
package/LLM_USAGE.md CHANGED
@@ -191,7 +191,7 @@ configureExpoUiI18n((key, options) => i18n.t(key, options));
191
191
  - Web SSR paints the visitor's scheme: wrap the root layout in `<InitialSchemeProvider scheme>` (`@mrmeg/expo-ui/state`) with the `"light"`/`"dark"` value read from a cookie on both server and client (template: `shared/ssrColorScheme.ts`, written by `+html.tsx` and `client/features/app/colorSchemeCookie.ts`); `useTheme()` uses it until `hasLoadedTheme`. Never read `localStorage` for the first render.
192
192
  - A Radix-backed component (`Tabs`, `Accordion`, `Collapsible`, `Select`, `DropdownMenu`, `Popover`) on a server-rendered web route logs a `useId` hydration mismatch (Expo Router hydrates `#root`, the server rendered `+html.tsx`). Wrap that subtree — not the page — in `<Hydrated fallback={…}>` (`@mrmeg/expo-ui/components`) with a fallback that holds its height; never branch on `typeof window`. Content that must be in the server HTML stays outside the gate.
193
193
  - Web console hygiene: animate with `useNativeDriver: shouldUseNativeDriver` (`@mrmeg/expo-ui/lib`), never `true`; set `pointerEvents` in `style`, never as a prop (react-native-web deprecates the prop, and its lazy warning module has overflowed the first SSR render of deep routes). `AnimatedView` folds a `pointerEvents` prop into style for you.
194
- - Haptics are one setting: `<UIProvider haptics="off" | "selection" | "all">` (default `"selection"`: Switch/Checkbox/Toggle/ToggleGroup/SegmentedControl tap on change; `"all"` adds a light tap on press for Button, pressable Card and Item). Do not call `expo-haptics` per control; force one Button with `haptic`. Pressed/disabled looks come from `interaction.pressedOpacity` / `interaction.disabledOpacity` in `@mrmeg/expo-ui/constants`.
194
+ - Haptics are one setting: `<UIProvider haptics="off" | "selection" | "all">` (default `"selection"`: Switch/Checkbox/Toggle/ToggleGroup/SegmentedControl/ThemeSelector tap on change; `"all"` adds a light tap on press for Button, pressable Card and Item). Do not call `expo-haptics` per control; force one Button with `haptic`. Pressed/disabled looks come from `interaction.pressedOpacity` / `interaction.disabledOpacity` in `@mrmeg/expo-ui/constants`.
195
195
  - Bottom toasts must clear the tab bar: the layout that owns a bar calls `useNotificationOffset({ bottom })` (`@mrmeg/expo-ui/hooks`) with the bar's distance from the window edge, inset included — `useBottomTabBarHeight()` for a JS `<Tabs>` layout, platform bar height + `useWindowInsets().bottom` for `NativeTabs` — and `null` while the bar is hidden. Never position a toast yourself.
196
196
  - Use `notify` plus a root `UIProvider` for transient global feedback. (`globalUIStore` stays available for reactive subscriptions and tests. In a component, read it with zustand's `useStore(globalUIStore, selector)`, never `globalUIStore()`: the React Compiler only treats `use*` calls as hooks, caches the bare call, and the next render crashes with React error #311.)
197
197
  - Keep app monitoring, auth, API, and domain behavior outside this package.
@@ -261,6 +261,11 @@ tokens and keeps a 44px hit area on native while rendering 40px on web.
261
261
  `spacing.cardPadding` (16) is the inner padding of `Card` and `StatCard` tiles,
262
262
  not a layout inset. See Screen Layout below.
263
263
 
264
+ A settings screen picks the theme with one row: `ThemeSelector` in the row's
265
+ `ItemActions`, with translated `labels`. It reads and writes `useThemeStore`, so
266
+ it needs no state of its own. Describe the resolved scheme under the title only
267
+ while the preference is `system`.
268
+
264
269
  When the saved theme preference is `system`, the package theme store owns the OS
265
270
  color-scheme subscription, including web `prefers-color-scheme`. Do not add
266
271
  app-local Appearance or `matchMedia` listeners. `THEME_STORAGE_KEY` (from
@@ -393,7 +398,7 @@ Check this before creating a new app-local primitive. All components come from
393
398
  | `DropdownMenu` | Menus and command lists | Homemade popover menus |
394
399
  | `EmptyState` | No-data or recoverable error regions | One-off empty placeholders |
395
400
  | `ErrorBoundary` | React render error fallback | Unhandled screen crashes |
396
- | `Icon` | Lucide or custom icons with theme tokens | Raw vector icons with hardcoded colors |
401
+ | `Icon` | Registered Lucide glyphs or custom art with theme tokens | Raw vector icons with hardcoded colors |
397
402
  | `InputOTP` | Verification code entry | Several manually managed text inputs |
398
403
  | `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions` | List / settings rows with density tokens | Hand-rolled row `View`s |
399
404
  | `ItemGroup` | A titled group of full-width `Item` rows with inset hairlines and an optional footer | Bordered or shadowed boxes around rows, a `Card` per row, hand-drawn dividers |
@@ -417,6 +422,7 @@ Check this before creating a new app-local primitive. All components come from
417
422
  | `Switch` | Binary settings | Custom toggle switches |
418
423
  | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | In-page tabbed views | Custom segmented/tab controls |
419
424
  | `TextInput` | Text entry with label, helper/error text, clear, password reveal | Raw `TextInput` plus repeated label/error code |
425
+ | `ThemeSelector` | System / Light / Dark switch wired to the theme store; icon-only fits an `ItemActions` slot, `showLabels` stretches | Three radio rows or a hand-rolled theme picker |
420
426
  | `Toggle`, `ToggleIcon` | Pressed/unpressed control; an `iconOnly` toggle needs `accessibilityLabel` (dev warns once) | Button with local selected styling |
421
427
  | `ToggleGroup`, `ToggleGroupItem`, `ToggleGroupIcon` | Single or multi toggle groups | Custom segmented controls |
422
428
  | `Tooltip` | Short hover/focus help | Persistent helper text or custom hover cards |
@@ -446,7 +452,7 @@ already scope label selectability; ordinary iOS/web text stays selectable.
446
452
  - A plain `View` whose `opacity`/`pointerEvents` follows state must be `collapsable={false}` on Android (Fabric re-parents its children on the flip; a flip racing a navigation pop crashes). Package surfaces are pinned; spread `stateSurfaceProps()` from `@mrmeg/expo-ui/lib` on app-owned ones. `Pressable` already pins itself.
447
453
  - `TextInput` `onBlur` fires only after a real focus on Android (the native field's first-composition blur is dropped in the package); validate on blur directly and do not add a touched-fields guard in app code.
448
454
  - `Drawer.Header` takes `icon`, `title`, and `action` slots for a compact app-brand row; put `Drawer.ToggleCollapse` in `action` for a trailing rail control. `Drawer.Content` owns safe-area top/bottom padding — do not duplicate it in children.
449
- - `Icon` and `Button.Icon` take `name` (a kebab-case name from the package registry, typed `IconName`) or `component` (any Lucide import such as `import House from "lucide-react-native/icons/house"`, or another SVG component taking `size` and `color`). Both are sized, colored with theme tokens, and hidden from assistive tech unless `accessibilityLabel` names a standalone icon (`decorative` only silences a labelled one); `Button.Icon` inherits the button's label color and is decorative by default. Use `component` for icons outside the registry instead of rendering raw Lucide components.
455
+ - `Icon` and `Button.Icon` take `name` (a kebab-case Lucide name typed `IconName`: the package builtins plus the names the app lists in `client/icons.json`, compiled with `bunx expo-ui-icons` and registered once with `registerIcons(appIcons)` at module scope in the root layout) or `component` (an `IconComponent` taking `size` and `color`, for art that is not a Lucide icon). Both are sized, colored with theme tokens, and hidden from assistive tech unless `accessibilityLabel` names a standalone icon (`decorative` only silences a labelled one); `Button.Icon` inherits the button's label color and is decorative by default. A missing name is a type error: run `bunx expo-ui-icons add <name>` (it maps an alias such as `home` to `house`) rather than importing an icon library. Check a name that arrives at runtime with `isIconName`.
450
456
 
451
457
  ## Minimal Examples
452
458
 
@@ -557,7 +563,7 @@ import { Drawer, Icon } from "@mrmeg/expo-ui/components";
557
563
  title="Acme"
558
564
  action={
559
565
  <Drawer.ToggleCollapse>
560
- <Icon name="sidebar" decorative />
566
+ <Icon name="panel-left" decorative />
561
567
  </Drawer.ToggleCollapse>
562
568
  }
563
569
  />
package/README.md CHANGED
@@ -32,9 +32,10 @@ Web 0.21. Install these peers at the versions your Expo SDK recommends: `expo`,
32
32
  `react-native-web`, `react-native-gesture-handler`,
33
33
  `react-native-keyboard-controller`, `react-native-safe-area-context`,
34
34
  `react-native-screens`, `@react-native-async-storage/async-storage`, `zustand`.
35
- `Icon` renders [Lucide](https://lucide.dev/icons) SVGs, so also install its two
36
- peers: `npx expo install react-native-svg` (a native module, pinned by your
37
- SDK) and `bun add lucide-react-native` (`>=1.46 <2`).
35
+ `Icon` draws SVG, so also install `react-native-svg` (a native module, pinned
36
+ by your SDK): `npx expo install react-native-svg`. The icons themselves are
37
+ [Lucide](https://lucide.dev/icons) glyph data the package ships and renders;
38
+ no icon library is a peer (see [Icons](#icons)).
38
39
 
39
40
  `@rn-primitives/*` and `@expo-google-fonts/inter` are package dependencies, not
40
41
  peers. Animations use React Native `Animated`. `BottomSheet`, `Slider`, and
@@ -481,7 +482,7 @@ setHaptics("all");
481
482
  | `haptics` | Fires on |
482
483
  |---|---|
483
484
  | `"off"` | nothing |
484
- | `"selection"` (default) | state changes: Switch, Checkbox, Toggle, ToggleGroup, SegmentedControl |
485
+ | `"selection"` (default) | state changes: Switch, Checkbox, Toggle, ToggleGroup, SegmentedControl, ThemeSelector |
485
486
  | `"all"` | selection plus a light tap on press-in for Button, pressable Card and Item, and `useScalePress` consumers |
486
487
 
487
488
  Two ways to disable a `Button`. `disabled` swallows presses, skips the scale
@@ -567,7 +568,7 @@ monospace.
567
568
  weight is imported from its own `@expo-google-fonts/inter/<weight>` subpath, so
568
569
  a native bundle carries those four files and no other Inter face, and a web
569
570
  bundle carries no Inter file at all. Icons
570
- are SVG (`lucide-react-native`), so there is no icon font to load on any
571
+ are SVG drawn from glyph data, so there is no icon font to load on any
571
572
  platform. On web
572
573
  it injects one Google Fonts Inter stylesheet (all four weights) after hydration
573
574
  unless the app already added it; web weight differentiation then comes from a
@@ -727,7 +728,7 @@ building a new primitive.
727
728
  | `DropdownMenu` | Menus and command lists | Row actions, account menu, sort menu, checkbox/radio groups |
728
729
  | `EmptyState` | No-data or recoverable error regions | Empty inbox, no results, failed list load |
729
730
  | `ErrorBoundary` | React render error fallback | Route-level and feature-level fallbacks |
730
- | `Icon` | Lucide or custom icons on theme tokens | Button accessories, menu icons, status glyphs |
731
+ | `Icon` | Registered Lucide glyphs or custom art on theme tokens | Button accessories, menu icons, status glyphs |
731
732
  | `InputOTP` | Verification code entry | Email/SMS codes, MFA, invite codes |
732
733
  | `Item` | List and settings rows on the density tokens | Settings lists, inbox rows, pickers, detail rows |
733
734
  | `ItemGroup` | Flat grouped list: title, full-width `Item` rows with inset hairlines, footer | Settings sections, profile details, activity feeds |
@@ -751,61 +752,113 @@ building a new primitive.
751
752
  | `Switch` | Binary settings | Notification, privacy, and feature toggles |
752
753
  | `Tabs` | In-page tabbed views | Profile sections, report views, settings categories |
753
754
  | `TextInput` | Text entry with label, helper/error text, clear button, password reveal, numeric filtering, left/right elements | Email/password, search, multiline notes |
755
+ | `ThemeSelector` | Compact System / Light / Dark switch bound to the theme store (icon-only, or `showLabels`) | The appearance row of a settings screen |
754
756
  | `Toggle` | Pressed/unpressed control | Favorite, mute, bold/italic, view mode |
755
757
  | `ToggleGroup` | Single or multi toggle groups | Alignment, formatting toolbar, filter chips |
756
758
  | `Tooltip` | Short hover/focus help | Icon button labels, field hints, disabled-action reasons |
757
759
 
758
760
  ### Icons
759
761
 
760
- `Icon` renders `lucide-react-native` — Lucide is a maintained superset of
761
- Feather in the same 24px, 2px round-stroke style. `name` is typed by
762
- `IconName`, the union of kebab-case Lucide names in the package registry
763
- (`src/components/icon-names.json`, about 150 names). Importing `Icon` includes
764
- that named registry because `name` is looked up at runtime, even when a caller
765
- uses `component`; the root `lucide-react-native` entry (1,800+ icons) is never
766
- imported. `color` takes a theme color name or a
767
- literal. `accessibilityLabel` names an icon that stands alone (`role="img"` +
768
- `aria-label` on web, `accessible` + `accessibilityLabel` on native); an icon
769
- without one is hidden from assistive tech — the glyph beside text that already
770
- says it would otherwise be announced as an unnamed image — and `decorative`
771
- only remains to silence a labelled icon. On web the SVG carries ARIA only
772
- (`aria-hidden` or `role="img"`), never the RN-only accessibility props. Instead of `name`,
773
- `Icon` and `Button.Icon` take `component`: any Lucide import (or another SVG
774
- component that accepts `size` and `color`), sized, colored, themed, and hidden
775
- from assistive tech exactly like a named icon. `Button.Icon` defaults its
776
- color to the button's label color either way.
762
+ `Icon` draws Lucide glyphs from plain data: each icon is a list of
763
+ `[tag, attrs]` SVG nodes on a 24×24 grid, stroked 2 px with round caps and
764
+ joins. The package owns both renderers: an inline `<svg>` on web and in
765
+ server-rendered HTML (stroked `currentColor` with the color as a style, so a
766
+ theme change recolors it through CSS and server and client markup match), and
767
+ `react-native-svg` on iOS and Android, drawn the way Lucide's own native
768
+ component draws it. No icon runtime or font ships, and each registered glyph
769
+ adds about 40 bytes (brotli) to the bundle.
770
+
771
+ `name` is typed by `IconName`: the 19 names the package's own components draw
772
+ (`src/components/icon-names.json`: the chevrons, `check`, `x`, the status and
773
+ theme-mode glyphs) plus every name the app registers. An app lists the Lucide
774
+ names it uses in `client/icons.json` and compiles them with the package's
775
+ `expo-ui-icons` CLI:
776
+
777
+ ```json
778
+ { "icons": ["archive", "bell", "compass"], "tabIcons": ["compass"] }
779
+ ```
780
+
781
+ ```sh
782
+ bunx expo-ui-icons init # seed client/icons.json from name="…" / icon="…" literals in app/, client/, shared/
783
+ bunx expo-ui-icons add calendar # add names; an alias is replaced by its canonical name
784
+ bunx expo-ui-icons # write client/icons.generated.ts (--check: exit 1 when stale)
785
+ bunx expo-ui-icons tabs # rasterize tabIcons to 1x/2x/3x template PNGs (--check never rasterizes)
786
+ ```
787
+
788
+ The generated module exports the glyphs as `appIcons` and augments the
789
+ `IconRegistry` interface, so the new names type-check everywhere. Register them
790
+ once at module scope in the root layout, so they are in place before any route
791
+ renders, on the server as well as the client:
792
+
793
+ ```tsx
794
+ import { registerIcons } from "@mrmeg/expo-ui/components/Icon";
795
+ import { appIcons } from "@/client/icons.generated";
796
+
797
+ registerIcons(appIcons);
798
+ ```
799
+
800
+ An unregistered literal is a type error. A name that arrives at runtime (a
801
+ database field, a server payload) renders nothing and warns once in
802
+ development; check it with `isIconName(value)`, which narrows to `IconName`.
803
+ The CLI rejects a name that is not in the snapshot, and an alias with its
804
+ canonical name (`home → house`); the alias map also ships in
805
+ `design-system.json` as `icons.aliases`. Brand glyphs (`github`, `chrome`,
806
+ `facebook`, `instagram`, `linkedin`, `twitter`) have no Lucide equivalent.
807
+
808
+ `tabs` writes `assets/icons/tabs/<name>.png` (plus `@2x` and `@3x`) and
809
+ `client/tabIcons.generated.ts` (`TAB_ICONS`, `TabIconName`) for a native tab
810
+ bar: `<NativeTabs.Trigger.Icon src={TAB_ICONS.compass} renderingMode="template" />`.
811
+ The PNGs are black on transparent and the platform tints them. Freshness is a
812
+ hash of each glyph's SVG in `assets/icons/tabs/manifest.json`, so `tabs --check`
813
+ and an up-to-date `tabs` need no rasterizer; only a changed tab icon needs
814
+ `rsvg-convert` (`brew install librsvg`, or point `RSVG_CONVERT` at it).
815
+
816
+ `color` takes a theme color name or a literal. `accessibilityLabel` names an
817
+ icon that stands alone (`role="img"` + `aria-label` on web, `accessible` +
818
+ `accessibilityLabel` on native); an icon without one is hidden from assistive
819
+ tech — the glyph beside text that already says it would otherwise be announced
820
+ as an unnamed image — and `decorative` only remains to silence a labelled icon.
821
+ On web the SVG carries ARIA only (`aria-hidden` or `role="img"`), never the
822
+ RN-only accessibility props. A named icon's SVG root takes `testID`, which
823
+ defaults to `icon-<name>` in development. Instead of `name`, `Icon` and
824
+ `Button.Icon` take `component` (an `IconComponent`) for art that is not a
825
+ Lucide icon, such as a brand mark: any component that accepts `size` and
826
+ `color`, sized, colored, themed, and hidden from assistive tech exactly like a
827
+ named icon. `Button.Icon`, `ToggleIcon`, and `ToggleGroupIcon` take the same
828
+ props, default their color to the control's label color, and are decorative.
777
829
 
778
830
  ```tsx
779
831
  import { Button, Icon } from "@mrmeg/expo-ui/components";
780
- import House from "lucide-react-native/icons/house";
781
- import Rocket from "lucide-react-native/icons/rocket";
832
+ import { GitHubMark } from "@/client/components/GitHubMark";
782
833
 
783
834
  <Icon name="circle-check-big" color="success" size={16} />
784
- <Icon component={Rocket} color="accent" />
835
+ <Icon component={GitHubMark} color="accent" />
785
836
  <Button onPress={goHome}>
786
- <Button.Icon component={House} />
837
+ <Button.Icon name="house" />
787
838
  <Button.Text>Home</Button.Text>
788
839
  </Button>
789
840
  ```
790
841
 
791
- Adding an icon: in this repo, add the kebab-case Lucide name to
792
- `packages/ui/src/components/icon-names.json` (keep it sorted), run
793
- `bun run ui:icons`, and commit both files — an unknown name fails with the
794
- alias lines from Lucide's root entry that point at its current name
795
- (`bun run ui:icons:check` guards freshness in CI). From a consuming app, pass
796
- any Lucide component through `component` instead; it needs no registry entry.
797
- Feather-era names that Lucide renamed: `alert-circle` → `circle-alert`,
798
- `check-circle` → `circle-check-big`, `x-circle` → `circle-x`,
799
- `alert-triangle` → `triangle-alert`, `help-circle` → `circle-question-mark`,
800
- `edit` → `square-pen`, `edit-2` → `pen`, `edit-3` → `pencil`,
801
- `sliders` → `sliders-horizontal`, `grid` → `layout-grid`, `home` → `house`,
802
- `unlock` → `lock-open`, `trash-2` → `trash`, `tool` → `wrench`,
803
- `layout` → `panels-top-left`, `bar-chart-2` → `chart-no-axes-column`,
804
- `plus-circle` → `circle-plus`, `stop-circle` → `circle-stop`,
805
- `more-vertical` → `ellipsis-vertical`, `more-horizontal` → `ellipsis`,
806
- `sidebar` → `panel-left`, `smile` → `face-slightly-smiling`. Brand glyphs
807
- (`github`, `chrome`, `facebook`, `instagram`, `linkedin`, `twitter`) have no
808
- Lucide equivalent; use `component` with your own SVG.
842
+ The Lucide snapshot ships as `dist/icons/lucide.json` (Lucide 1.55.0: every
843
+ icon plus the alias map) and the CLI compiles against it, so adding a name needs
844
+ no package release. Lucide renames about one name in six minor releases and
845
+ sometimes redraws a glyph under a stable name, so the snapshot is pinned
846
+ exactly and moves only in a package release: after upgrading, run
847
+ `bunx expo-ui-icons` and `bunx expo-ui-icons tabs`, review the glyph diff, and
848
+ replace any name the CLI now rejects. Lucide is ISC-licensed (its
849
+ Feather-derived icons MIT); the license ships as `dist/icons/LICENSE.lucide`.
850
+
851
+ Tests need no icon mock and no Lucide module mapping: glyphs are plain objects
852
+ drawn with `react-native-svg`. Register `appIcons` in the Jest setup (lazily, in
853
+ a `beforeAll`, so test-file mocks still apply to `Icon`) and query a named icon
854
+ by its `icon-<name>` test id.
855
+
856
+ In this repository, `bun run ui:icons` regenerates `iconBuiltins.generated.ts`,
857
+ `src/icons/lucide.json`, and `src/icons/LICENSE.lucide` from the exactly pinned
858
+ `lucide-static` devDependency and `icon-names.json`. A Lucide bump changes that
859
+ pin, then `bun install`, `bun run ui:icons`, `bun run icons`, and
860
+ `bun run icons:tabs`. The template app's own list is `client/icons.json`
861
+ (`bun run icons`, `bun run icons:tabs`).
809
862
 
810
863
  ### Compound Parts
811
864
 
@@ -841,7 +894,7 @@ are named exports only.
841
894
 
842
895
  | Hook | Returns |
843
896
  |------|---------|
844
- | `useResources()` | `{ loaded, error }` — loads the Inter faces (icons are SVG, nothing to load) |
897
+ | `useResources()` | `{ loaded, error }` — loads the Inter faces (icons are SVG drawn from glyph data, nothing to load) |
845
898
  | `useTheme()` | Active theme, scheme helpers, shadow/contrast/alpha helpers |
846
899
  | `useStyles(factory)` | Memoized theme-aware styles plus the `useTheme()` helpers |
847
900
  | `useDimensions()` | `{ width, height, orientation, isSmallScreen, isMediumScreen, isLargeScreen }` against `SCREEN_SIZES` (768 / 1000 / 1200) |
@@ -1033,7 +1086,7 @@ import { Drawer, Icon } from "@mrmeg/expo-ui/components";
1033
1086
  title="Acme"
1034
1087
  action={
1035
1088
  <Drawer.ToggleCollapse>
1036
- <Icon name="sidebar" decorative />
1089
+ <Icon name="panel-left" decorative />
1037
1090
  </Drawer.ToggleCollapse>
1038
1091
  }
1039
1092
  />
@@ -1218,9 +1271,10 @@ copies of `@radix-ui/react-slot` crashes the web `AlertDialog` with
1218
1271
  `pkg ui build` also writes `dist/design-system.json`
1219
1272
  ([`scripts/build-design-system-manifest.mjs`](../../scripts/build-design-system-manifest.mjs)):
1220
1273
  the spacing, radius and icon scales, the palette, the light and dark themes, the
1221
- font variants, every component's variant and size values, and `icons.names` (the
1222
- `IconName` registry), serialized out of `src` by the same extractor the lint
1223
- rules use. It is exported as
1274
+ font variants, every component's variant and size values, and `icons` (the
1275
+ builtin `IconName` list as `names`, Lucide's renamed-name `aliases`, and the
1276
+ snapshot's `lucide` version), serialized out of `src` by the same extractor the
1277
+ lint rules use. It is exported as
1224
1278
  `@mrmeg/expo-ui/design-system.json`, and `@mrmeg/eslint-plugin-expo-ui` reads it
1225
1279
  in projects that install this package instead of checking out its sources — which
1226
1280
  is what lets the design-system rules quote this release's presets and tokens.