@eifi1/ui-kit 0.4.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.
Files changed (229) hide show
  1. package/README.md +219 -0
  2. package/dist/components/account-settings.d.ts +71 -0
  3. package/dist/components/account-settings.js +159 -0
  4. package/dist/components/account-settings.js.map +1 -0
  5. package/dist/components/alert-banner.d.ts +20 -0
  6. package/dist/components/alert-banner.js +39 -0
  7. package/dist/components/alert-banner.js.map +1 -0
  8. package/dist/components/amount-input.d.ts +117 -0
  9. package/dist/components/amount-input.js +243 -0
  10. package/dist/components/amount-input.js.map +1 -0
  11. package/dist/components/calculator.d.ts +42 -0
  12. package/dist/components/calculator.js +161 -0
  13. package/dist/components/calculator.js.map +1 -0
  14. package/dist/components/chart.d.ts +58 -0
  15. package/dist/components/chart.js +158 -0
  16. package/dist/components/chart.js.map +1 -0
  17. package/dist/components/combobox-core.d.ts +77 -0
  18. package/dist/components/combobox-core.js +263 -0
  19. package/dist/components/combobox-core.js.map +1 -0
  20. package/dist/components/combobox.d.ts +141 -0
  21. package/dist/components/combobox.js +550 -0
  22. package/dist/components/combobox.js.map +1 -0
  23. package/dist/components/currency-select.d.ts +37 -0
  24. package/dist/components/currency-select.js +131 -0
  25. package/dist/components/currency-select.js.map +1 -0
  26. package/dist/components/data-table-filter-popover.d.ts +28 -0
  27. package/dist/components/data-table-filter-popover.js +201 -0
  28. package/dist/components/data-table-filter-popover.js.map +1 -0
  29. package/dist/components/data-table-filters.d.ts +5 -0
  30. package/dist/components/data-table-filters.js +118 -0
  31. package/dist/components/data-table-filters.js.map +1 -0
  32. package/dist/components/data-table-labels.d.ts +33 -0
  33. package/dist/components/data-table-labels.js +53 -0
  34. package/dist/components/data-table-labels.js.map +1 -0
  35. package/dist/components/data-table-pagination.d.ts +18 -0
  36. package/dist/components/data-table-pagination.js +92 -0
  37. package/dist/components/data-table-pagination.js.map +1 -0
  38. package/dist/components/data-table-sort.d.ts +23 -0
  39. package/dist/components/data-table-sort.js +55 -0
  40. package/dist/components/data-table-sort.js.map +1 -0
  41. package/dist/components/data-table.d.ts +5 -0
  42. package/dist/components/data-table.js +926 -0
  43. package/dist/components/data-table.js.map +1 -0
  44. package/dist/components/date-picker.d.ts +85 -0
  45. package/dist/components/date-picker.js +253 -0
  46. package/dist/components/date-picker.js.map +1 -0
  47. package/dist/components/dropdown.d.ts +98 -0
  48. package/dist/components/dropdown.js +122 -0
  49. package/dist/components/dropdown.js.map +1 -0
  50. package/dist/components/entity-combobox.d.ts +49 -0
  51. package/dist/components/entity-combobox.js +115 -0
  52. package/dist/components/entity-combobox.js.map +1 -0
  53. package/dist/components/file-dropzone.d.ts +18 -0
  54. package/dist/components/file-dropzone.js +101 -0
  55. package/dist/components/file-dropzone.js.map +1 -0
  56. package/dist/components/full-bleed-dialog.d.ts +57 -0
  57. package/dist/components/full-bleed-dialog.js +68 -0
  58. package/dist/components/full-bleed-dialog.js.map +1 -0
  59. package/dist/components/grouped-picker.d.ts +37 -0
  60. package/dist/components/grouped-picker.js +87 -0
  61. package/dist/components/grouped-picker.js.map +1 -0
  62. package/dist/components/hover-menu.d.ts +16 -0
  63. package/dist/components/hover-menu.js +113 -0
  64. package/dist/components/hover-menu.js.map +1 -0
  65. package/dist/components/mini-calendar.d.ts +33 -0
  66. package/dist/components/mini-calendar.js +133 -0
  67. package/dist/components/mini-calendar.js.map +1 -0
  68. package/dist/components/modal.d.ts +76 -0
  69. package/dist/components/modal.js +156 -0
  70. package/dist/components/modal.js.map +1 -0
  71. package/dist/components/multi-entity-combobox.d.ts +47 -0
  72. package/dist/components/multi-entity-combobox.js +116 -0
  73. package/dist/components/multi-entity-combobox.js.map +1 -0
  74. package/dist/components/multi-select.d.ts +39 -0
  75. package/dist/components/multi-select.js +132 -0
  76. package/dist/components/multi-select.js.map +1 -0
  77. package/dist/components/number-input.d.ts +74 -0
  78. package/dist/components/number-input.js +114 -0
  79. package/dist/components/number-input.js.map +1 -0
  80. package/dist/components/numpad-sheet.d.ts +45 -0
  81. package/dist/components/numpad-sheet.js +102 -0
  82. package/dist/components/numpad-sheet.js.map +1 -0
  83. package/dist/components/picker-sheet.d.ts +44 -0
  84. package/dist/components/picker-sheet.js +78 -0
  85. package/dist/components/picker-sheet.js.map +1 -0
  86. package/dist/components/popover.d.ts +20 -0
  87. package/dist/components/popover.js +46 -0
  88. package/dist/components/popover.js.map +1 -0
  89. package/dist/components/search-field.d.ts +52 -0
  90. package/dist/components/search-field.js +64 -0
  91. package/dist/components/search-field.js.map +1 -0
  92. package/dist/components/settings-fields.d.ts +39 -0
  93. package/dist/components/settings-fields.js +17 -0
  94. package/dist/components/settings-fields.js.map +1 -0
  95. package/dist/components/swipeable-row.d.ts +60 -0
  96. package/dist/components/swipeable-row.js +115 -0
  97. package/dist/components/swipeable-row.js.map +1 -0
  98. package/dist/components/toggle-group.d.ts +34 -0
  99. package/dist/components/toggle-group.js +90 -0
  100. package/dist/components/toggle-group.js.map +1 -0
  101. package/dist/components/tooltip.d.ts +86 -0
  102. package/dist/components/tooltip.js +171 -0
  103. package/dist/components/tooltip.js.map +1 -0
  104. package/dist/components/ui.d.ts +239 -0
  105. package/dist/components/ui.js +512 -0
  106. package/dist/components/ui.js.map +1 -0
  107. package/dist/components/use-mobile-reveal.d.ts +31 -0
  108. package/dist/components/use-mobile-reveal.js +33 -0
  109. package/dist/components/use-mobile-reveal.js.map +1 -0
  110. package/dist/components/use-table-state.d.ts +54 -0
  111. package/dist/components/use-table-state.js +153 -0
  112. package/dist/components/use-table-state.js.map +1 -0
  113. package/dist/components/user-avatar.d.ts +23 -0
  114. package/dist/components/user-avatar.js +33 -0
  115. package/dist/components/user-avatar.js.map +1 -0
  116. package/dist/components/wizard-stepper.d.ts +14 -0
  117. package/dist/components/wizard-stepper.js +27 -0
  118. package/dist/components/wizard-stepper.js.map +1 -0
  119. package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
  120. package/dist/feedback/feedback-attachment.d.ts +63 -0
  121. package/dist/feedback/feedback-attachment.js +160 -0
  122. package/dist/feedback/feedback-attachment.js.map +1 -0
  123. package/dist/feedback/feedback-dialog.d.ts +72 -0
  124. package/dist/feedback/feedback-dialog.js +109 -0
  125. package/dist/feedback/feedback-dialog.js.map +1 -0
  126. package/dist/feedback/feedback-inbox.d.ts +226 -0
  127. package/dist/feedback/feedback-inbox.js +315 -0
  128. package/dist/feedback/feedback-inbox.js.map +1 -0
  129. package/dist/hooks/use-anchored-panel.d.ts +80 -0
  130. package/dist/hooks/use-anchored-panel.js +71 -0
  131. package/dist/hooks/use-anchored-panel.js.map +1 -0
  132. package/dist/hooks/use-anchored-rect.d.ts +24 -0
  133. package/dist/hooks/use-anchored-rect.js +40 -0
  134. package/dist/hooks/use-anchored-rect.js.map +1 -0
  135. package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
  136. package/dist/hooks/use-body-scroll-lock.js +41 -0
  137. package/dist/hooks/use-body-scroll-lock.js.map +1 -0
  138. package/dist/hooks/use-close-transition.d.ts +41 -0
  139. package/dist/hooks/use-close-transition.js +36 -0
  140. package/dist/hooks/use-close-transition.js.map +1 -0
  141. package/dist/hooks/use-dismiss.d.ts +34 -0
  142. package/dist/hooks/use-dismiss.js +41 -0
  143. package/dist/hooks/use-dismiss.js.map +1 -0
  144. package/dist/hooks/use-media-query.d.ts +8 -0
  145. package/dist/hooks/use-media-query.js +18 -0
  146. package/dist/hooks/use-media-query.js.map +1 -0
  147. package/dist/hooks/use-overlay-history.d.ts +10 -0
  148. package/dist/hooks/use-overlay-history.js +99 -0
  149. package/dist/hooks/use-overlay-history.js.map +1 -0
  150. package/dist/hooks/use-row-swipe.d.ts +30 -0
  151. package/dist/hooks/use-row-swipe.js +128 -0
  152. package/dist/hooks/use-row-swipe.js.map +1 -0
  153. package/dist/index.d.ts +75 -0
  154. package/dist/index.js +90 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/lib/calc.d.ts +89 -0
  157. package/dist/lib/calc.js +180 -0
  158. package/dist/lib/calc.js.map +1 -0
  159. package/dist/lib/cn.d.ts +6 -0
  160. package/dist/lib/cn.js +7 -0
  161. package/dist/lib/cn.js.map +1 -0
  162. package/dist/lib/dates.d.ts +84 -0
  163. package/dist/lib/dates.js +108 -0
  164. package/dist/lib/dates.js.map +1 -0
  165. package/dist/lib/logger.d.ts +17 -0
  166. package/dist/lib/logger.js +30 -0
  167. package/dist/lib/logger.js.map +1 -0
  168. package/dist/lib/safe-storage.d.ts +23 -0
  169. package/dist/lib/safe-storage.js +20 -0
  170. package/dist/lib/safe-storage.js.map +1 -0
  171. package/dist/search/command-palette.d.ts +71 -0
  172. package/dist/search/command-palette.js +198 -0
  173. package/dist/search/command-palette.js.map +1 -0
  174. package/dist/shell/app-shell.d.ts +64 -0
  175. package/dist/shell/app-shell.js +237 -0
  176. package/dist/shell/app-shell.js.map +1 -0
  177. package/dist/shell/option-switcher-menu.d.ts +24 -0
  178. package/dist/shell/option-switcher-menu.js +44 -0
  179. package/dist/shell/option-switcher-menu.js.map +1 -0
  180. package/dist/shell/role-switcher.d.ts +19 -0
  181. package/dist/shell/role-switcher.js +28 -0
  182. package/dist/shell/role-switcher.js.map +1 -0
  183. package/dist/shell/top-bar.d.ts +15 -0
  184. package/dist/shell/top-bar.js +29 -0
  185. package/dist/shell/top-bar.js.map +1 -0
  186. package/dist/shell/topbar-action-menu.d.ts +44 -0
  187. package/dist/shell/topbar-action-menu.js +49 -0
  188. package/dist/shell/topbar-action-menu.js.map +1 -0
  189. package/dist/shell/topbar-controls.d.ts +54 -0
  190. package/dist/shell/topbar-controls.js +152 -0
  191. package/dist/shell/topbar-controls.js.map +1 -0
  192. package/dist/theme/chart-palette.d.ts +60 -0
  193. package/dist/theme/chart-palette.js +79 -0
  194. package/dist/theme/chart-palette.js.map +1 -0
  195. package/dist/theme/palette-presets.d.ts +39 -0
  196. package/dist/theme/palette-presets.js +440 -0
  197. package/dist/theme/palette-presets.js.map +1 -0
  198. package/dist/theme/palette-store.d.ts +45 -0
  199. package/dist/theme/palette-store.js +52 -0
  200. package/dist/theme/palette-store.js.map +1 -0
  201. package/dist/theme/theme-store.d.ts +57 -0
  202. package/dist/theme/theme-store.js +96 -0
  203. package/dist/theme/theme-store.js.map +1 -0
  204. package/dist/tour/tour.d.ts +104 -0
  205. package/dist/tour/tour.js +351 -0
  206. package/dist/tour/tour.js.map +1 -0
  207. package/dist/wizard/stepper-nav.d.ts +30 -0
  208. package/dist/wizard/stepper-nav.js +146 -0
  209. package/dist/wizard/stepper-nav.js.map +1 -0
  210. package/dist/wizard/types.d.ts +124 -0
  211. package/dist/wizard/types.js +24 -0
  212. package/dist/wizard/types.js.map +1 -0
  213. package/dist/wizard/use-wizard.d.ts +6 -0
  214. package/dist/wizard/use-wizard.js +222 -0
  215. package/dist/wizard/use-wizard.js.map +1 -0
  216. package/dist/wizard/validation.d.ts +23 -0
  217. package/dist/wizard/validation.js +13 -0
  218. package/dist/wizard/validation.js.map +1 -0
  219. package/dist/wizard/wizard-context.d.ts +19 -0
  220. package/dist/wizard/wizard-context.js +15 -0
  221. package/dist/wizard/wizard-context.js.map +1 -0
  222. package/dist/wizard/wizard-step.d.ts +11 -0
  223. package/dist/wizard/wizard-step.js +12 -0
  224. package/dist/wizard/wizard-step.js.map +1 -0
  225. package/dist/wizard/wizard-summary.d.ts +18 -0
  226. package/dist/wizard/wizard-summary.js +41 -0
  227. package/dist/wizard/wizard-summary.js.map +1 -0
  228. package/package.json +87 -0
  229. package/tokens.css +254 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/theme/palette-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport type { StoreApi, UseBoundStore } from \"zustand\";\nimport { logger } from \"../lib/logger\";\nimport type { HeatStops } from \"./chart-palette\";\nimport { applyTokenSet, DEFAULT_PRESET, presetById, type TokenSet } from \"./palette-presets\";\nimport type { ThemeState, ThemeMode } from \"./theme-store\";\n\n/**\n * Pre-hydration counterpart to `applyPersistedTheme`: read the palette id\n * persisted under `storageKey` straight from localStorage and apply that preset's\n * tokens for the given resolved `mode` to <html>, BEFORE React mounts (no flash of\n * the default palette). Falls back to the default preset for missing/malformed\n * state, so an app entrypoint never re-types the persist envelope.\n */\nexport function applyPersistedPalette(storageKey: string, mode: ThemeMode): void {\n let id = DEFAULT_PRESET.id;\n try {\n const blob = JSON.parse(localStorage.getItem(storageKey) ?? \"{}\") as {\n state?: { id?: string };\n };\n if (blob.state?.id) id = blob.state.id;\n } catch {\n /* malformed storage → default preset */\n }\n applyTokenSet(document.documentElement, presetById(id)[mode]);\n}\n\n// The palette layer flips the whole app between candidate appearance presets at\n// runtime (feedback #307), re-skinning every token-driven surface and chart.\n// Persisted like the theme so a reload keeps the selection.\ninterface PaletteState {\n id: string;\n setId: (id: string) => void;\n}\n\n/**\n * Build a palette store bound to a consumer-supplied localStorage key, wired to\n * that app's theme store (for the active light/dark mode). Returns the palette\n * hook plus the effect + selector hooks that read the active preset's tokens.\n */\nexport function createPaletteStore(\n storageKey: string,\n useTheme: UseBoundStore<StoreApi<ThemeState>>,\n) {\n const usePalette = create<PaletteState>()(\n logger(\n persist(\n (set) => ({\n id: DEFAULT_PRESET.id,\n setId: (id) => set({ id }),\n }),\n { name: storageKey, version: 1 },\n ),\n \"palette\",\n ),\n );\n\n /** Apply the active preset's tokens to <html> as inline CSS custom properties,\n * re-applying when the preset or the light/dark theme changes. Tailwind v4\n * strips a bare root token block from the bundle, so this inline write from\n * the TS source of truth is what actually drives the colours. */\n function useApplyPalette(): void {\n const id = usePalette((s) => s.id);\n const mode = useTheme((s) => s.mode);\n useEffect(() => {\n applyTokenSet(document.documentElement, presetById(id)[mode]);\n }, [id, mode]);\n }\n\n /** The active preset's token set for the current theme — the source for the\n * JS-interpolated charts (treemap fills, heatmap stops) that can't read CSS\n * vars. */\n function useActiveTokenSet(): TokenSet {\n const id = usePalette((s) => s.id);\n const mode = useTheme((s) => s.mode);\n return presetById(id)[mode];\n }\n\n /** The 9 categorical chart hues for the active preset/theme (treemap cells). */\n function useChartHex(): string[] {\n return useActiveTokenSet().chart;\n }\n\n /** The active preset/theme heatmap interpolation stops. */\n function useHeatStops(): HeatStops {\n return useActiveTokenSet().heat;\n }\n\n return { usePalette, useApplyPalette, useActiveTokenSet, useChartHex, useHeatStops };\n}\n\nexport type PaletteStore = ReturnType<typeof createPaletteStore>;\n"],"mappings":"AAAA,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AAExB,SAAS,cAAc;AAEvB,SAAS,eAAe,gBAAgB,kBAAiC;AAUlE,SAAS,sBAAsB,YAAoB,MAAuB;AAC/E,MAAI,KAAK,eAAe;AACxB,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,aAAa,QAAQ,UAAU,KAAK,IAAI;AAGhE,QAAI,KAAK,OAAO,GAAI,MAAK,KAAK,MAAM;AAAA,EACtC,QAAQ;AAAA,EAER;AACA,gBAAc,SAAS,iBAAiB,WAAW,EAAE,EAAE,IAAI,CAAC;AAC9D;AAeO,SAAS,mBACd,YACA,UACA;AACA,QAAM,aAAa,OAAqB;AAAA,IACtC;AAAA,MACE;AAAA,QACE,CAAC,SAAS;AAAA,UACR,IAAI,eAAe;AAAA,UACnB,OAAO,CAAC,OAAO,IAAI,EAAE,GAAG,CAAC;AAAA,QAC3B;AAAA,QACA,EAAE,MAAM,YAAY,SAAS,EAAE;AAAA,MACjC;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAMA,WAAS,kBAAwB;AAC/B,UAAM,KAAK,WAAW,CAAC,MAAM,EAAE,EAAE;AACjC,UAAM,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI;AACnC,cAAU,MAAM;AACd,oBAAc,SAAS,iBAAiB,WAAW,EAAE,EAAE,IAAI,CAAC;AAAA,IAC9D,GAAG,CAAC,IAAI,IAAI,CAAC;AAAA,EACf;AAKA,WAAS,oBAA8B;AACrC,UAAM,KAAK,WAAW,CAAC,MAAM,EAAE,EAAE;AACjC,UAAM,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI;AACnC,WAAO,WAAW,EAAE,EAAE,IAAI;AAAA,EAC5B;AAGA,WAAS,cAAwB;AAC/B,WAAO,kBAAkB,EAAE;AAAA,EAC7B;AAGA,WAAS,eAA0B;AACjC,WAAO,kBAAkB,EAAE;AAAA,EAC7B;AAEA,SAAO,EAAE,YAAY,iBAAiB,mBAAmB,aAAa,aAAa;AACrF;","names":[]}
@@ -0,0 +1,57 @@
1
+ import * as zustand_middleware from 'zustand/middleware';
2
+ import * as zustand from 'zustand';
3
+
4
+ type ThemeMode = "light" | "dark";
5
+ /** What the user chose; "system" tracks the OS `prefers-color-scheme`. */
6
+ type ThemePreference = "light" | "dark" | "system";
7
+ interface ThemeState {
8
+ /** The user's choice (persisted). */
9
+ preference: ThemePreference;
10
+ /** The RESOLVED light/dark actually in effect (system → OS). Read this for
11
+ * anything that needs the concrete theme (e.g. token selection, toasts). */
12
+ mode: ThemeMode;
13
+ setPreference: (preference: ThemePreference) => void;
14
+ /** Set an explicit light/dark choice (preference follows). */
15
+ setMode: (mode: ThemeMode) => void;
16
+ /** Flip the effective theme to the opposite explicit choice. */
17
+ toggle: () => void;
18
+ }
19
+ /**
20
+ * Pre-hydration: read the theme PREFERENCE persisted under `storageKey` straight
21
+ * from localStorage, resolve it to a concrete light/dark mode, and toggle the
22
+ * `.dark` class on <html> — BEFORE React and the store mount, so the first paint
23
+ * already has the right theme (no flash). Returns the resolved mode so the caller
24
+ * can chain {@link applyPersistedPalette}. Encapsulates the zustand-persist
25
+ * envelope + the legacy `mode`→`preference` fallback so apps never re-type the
26
+ * storage shape in their entrypoint.
27
+ */
28
+ declare function applyPersistedTheme(storageKey: string): ThemeMode;
29
+ /**
30
+ * Build a theme store bound to a consumer-supplied localStorage key. Tracks a
31
+ * light/dark/system PREFERENCE (feedback #330) and exposes the resolved `mode`.
32
+ * Returns the `useTheme` hook plus a `useApplyTheme` effect that toggles the
33
+ * `.dark` class on <html> and, while on "system", follows OS theme changes live.
34
+ */
35
+ declare function createThemeStore(storageKey: string): {
36
+ useTheme: zustand.UseBoundStore<Omit<zustand.StoreApi<ThemeState>, "setState" | "persist"> & {
37
+ setState(partial: ThemeState | Partial<ThemeState> | ((state: ThemeState) => ThemeState | Partial<ThemeState>), replace?: false | undefined): unknown;
38
+ setState(state: ThemeState | ((state: ThemeState) => ThemeState), replace: true): unknown;
39
+ persist: {
40
+ setOptions: (options: Partial<zustand_middleware.PersistOptions<ThemeState, {
41
+ preference: ThemePreference;
42
+ }, unknown>>) => void;
43
+ clearStorage: () => void;
44
+ rehydrate: () => Promise<void> | void;
45
+ hasHydrated: () => boolean;
46
+ onHydrate: (fn: (state: ThemeState) => void) => () => void;
47
+ onFinishHydration: (fn: (state: ThemeState) => void) => () => void;
48
+ getOptions: () => Partial<zustand_middleware.PersistOptions<ThemeState, {
49
+ preference: ThemePreference;
50
+ }, unknown>>;
51
+ };
52
+ }>;
53
+ useApplyTheme: () => void;
54
+ };
55
+ type ThemeStore = ReturnType<typeof createThemeStore>;
56
+
57
+ export { type ThemeMode, type ThemePreference, type ThemeState, type ThemeStore, applyPersistedTheme, createThemeStore };
@@ -0,0 +1,96 @@
1
+ import { useEffect } from "react";
2
+ import { create } from "zustand";
3
+ import { persist } from "zustand/middleware";
4
+ import { logger } from "../lib/logger.js";
5
+ function systemPreference() {
6
+ try {
7
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
8
+ } catch {
9
+ return "light";
10
+ }
11
+ }
12
+ function resolve(preference) {
13
+ return preference === "system" ? systemPreference() : preference;
14
+ }
15
+ function preferenceFromState(s) {
16
+ if (s?.preference === "light" || s?.preference === "dark" || s?.preference === "system") {
17
+ return s.preference;
18
+ }
19
+ if (s?.mode === "light" || s?.mode === "dark") return s.mode;
20
+ return "system";
21
+ }
22
+ function applyThemeClass(mode) {
23
+ const root = document.documentElement;
24
+ if (mode === "dark") root.classList.add("dark");
25
+ else root.classList.remove("dark");
26
+ }
27
+ function applyPersistedTheme(storageKey) {
28
+ let preference = "system";
29
+ try {
30
+ const blob = JSON.parse(localStorage.getItem(storageKey) ?? "{}");
31
+ preference = preferenceFromState(blob.state);
32
+ } catch {
33
+ }
34
+ const mode = resolve(preference);
35
+ applyThemeClass(mode);
36
+ return mode;
37
+ }
38
+ function createThemeStore(storageKey) {
39
+ const useTheme = create()(
40
+ logger(
41
+ persist(
42
+ (set, get) => ({
43
+ preference: "system",
44
+ mode: systemPreference(),
45
+ setPreference: (preference) => set({ preference, mode: resolve(preference) }),
46
+ setMode: (mode) => set({ preference: mode, mode }),
47
+ toggle: () => {
48
+ const next = get().mode === "dark" ? "light" : "dark";
49
+ set({ preference: next, mode: next });
50
+ }
51
+ }),
52
+ {
53
+ name: storageKey,
54
+ version: 2,
55
+ // Only the preference is persisted; `mode` is derived on load.
56
+ partialize: (s) => ({ preference: s.preference }),
57
+ migrate: (persistedState) => ({
58
+ preference: preferenceFromState(
59
+ persistedState
60
+ )
61
+ }),
62
+ onRehydrateStorage: () => (state) => {
63
+ if (state) state.mode = resolve(state.preference);
64
+ }
65
+ }
66
+ ),
67
+ "theme"
68
+ )
69
+ );
70
+ function useApplyTheme() {
71
+ const mode = useTheme((s) => s.mode);
72
+ const preference = useTheme((s) => s.preference);
73
+ useEffect(() => {
74
+ applyThemeClass(mode);
75
+ }, [mode]);
76
+ useEffect(() => {
77
+ if (preference !== "system") return;
78
+ let mq;
79
+ try {
80
+ mq = window.matchMedia("(prefers-color-scheme: dark)");
81
+ } catch {
82
+ return;
83
+ }
84
+ const onChange = () => useTheme.setState({ mode: mq.matches ? "dark" : "light" });
85
+ onChange();
86
+ mq.addEventListener?.("change", onChange);
87
+ return () => mq.removeEventListener?.("change", onChange);
88
+ }, [preference]);
89
+ }
90
+ return { useTheme, useApplyTheme };
91
+ }
92
+ export {
93
+ applyPersistedTheme,
94
+ createThemeStore
95
+ };
96
+ //# sourceMappingURL=theme-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/theme/theme-store.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport { logger } from \"../lib/logger\";\n\nexport type ThemeMode = \"light\" | \"dark\";\n/** What the user chose; \"system\" tracks the OS `prefers-color-scheme`. */\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\n\nexport interface ThemeState {\n /** The user's choice (persisted). */\n preference: ThemePreference;\n /** The RESOLVED light/dark actually in effect (system → OS). Read this for\n * anything that needs the concrete theme (e.g. token selection, toasts). */\n mode: ThemeMode;\n setPreference: (preference: ThemePreference) => void;\n /** Set an explicit light/dark choice (preference follows). */\n setMode: (mode: ThemeMode) => void;\n /** Flip the effective theme to the opposite explicit choice. */\n toggle: () => void;\n}\n\nfunction systemPreference(): ThemeMode {\n try {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n } catch {\n return \"light\";\n }\n}\n\nfunction resolve(preference: ThemePreference): ThemeMode {\n return preference === \"system\" ? systemPreference() : preference;\n}\n\n/** Normalize a persisted theme-state blob to a valid preference. Older blobs\n * persisted the concrete `mode` (light/dark); carry that over as an explicit\n * preference so the choice survives. Shared by the store's `migrate` and the\n * pre-hydration {@link applyPersistedTheme}. */\nfunction preferenceFromState(\n s: { mode?: string; preference?: string } | null | undefined,\n): ThemePreference {\n if (s?.preference === \"light\" || s?.preference === \"dark\" || s?.preference === \"system\") {\n return s.preference;\n }\n if (s?.mode === \"light\" || s?.mode === \"dark\") return s.mode;\n return \"system\";\n}\n\n/** Toggle the `.dark` class on <html> for a resolved light/dark mode. */\nfunction applyThemeClass(mode: ThemeMode): void {\n const root = document.documentElement;\n if (mode === \"dark\") root.classList.add(\"dark\");\n else root.classList.remove(\"dark\");\n}\n\n/**\n * Pre-hydration: read the theme PREFERENCE persisted under `storageKey` straight\n * from localStorage, resolve it to a concrete light/dark mode, and toggle the\n * `.dark` class on <html> — BEFORE React and the store mount, so the first paint\n * already has the right theme (no flash). Returns the resolved mode so the caller\n * can chain {@link applyPersistedPalette}. Encapsulates the zustand-persist\n * envelope + the legacy `mode`→`preference` fallback so apps never re-type the\n * storage shape in their entrypoint.\n */\nexport function applyPersistedTheme(storageKey: string): ThemeMode {\n let preference: ThemePreference = \"system\";\n try {\n const blob = JSON.parse(localStorage.getItem(storageKey) ?? \"{}\") as {\n state?: { mode?: string; preference?: string };\n };\n preference = preferenceFromState(blob.state);\n } catch {\n /* malformed storage → system */\n }\n const mode = resolve(preference);\n applyThemeClass(mode);\n return mode;\n}\n\n/**\n * Build a theme store bound to a consumer-supplied localStorage key. Tracks a\n * light/dark/system PREFERENCE (feedback #330) and exposes the resolved `mode`.\n * Returns the `useTheme` hook plus a `useApplyTheme` effect that toggles the\n * `.dark` class on <html> and, while on \"system\", follows OS theme changes live.\n */\nexport function createThemeStore(storageKey: string) {\n const useTheme = create<ThemeState>()(\n logger(\n persist(\n (set, get) => ({\n preference: \"system\" as ThemePreference,\n mode: systemPreference(),\n setPreference: (preference) => set({ preference, mode: resolve(preference) }),\n setMode: (mode) => set({ preference: mode, mode }),\n toggle: () => {\n const next: ThemeMode = get().mode === \"dark\" ? \"light\" : \"dark\";\n set({ preference: next, mode: next });\n },\n }),\n {\n name: storageKey,\n version: 2,\n // Only the preference is persisted; `mode` is derived on load.\n partialize: (s) => ({ preference: s.preference }),\n migrate: (persistedState): { preference: ThemePreference } => ({\n preference: preferenceFromState(\n persistedState as { mode?: string; preference?: string } | null,\n ),\n }),\n onRehydrateStorage: () => (state) => {\n // The persisted preference decides the concrete mode at load.\n if (state) state.mode = resolve(state.preference);\n },\n },\n ),\n \"theme\",\n ),\n );\n\n function useApplyTheme(): void {\n const mode = useTheme((s) => s.mode);\n const preference = useTheme((s) => s.preference);\n useEffect(() => {\n applyThemeClass(mode);\n }, [mode]);\n // While tracking the system, follow OS theme changes live.\n useEffect(() => {\n if (preference !== \"system\") return;\n let mq: MediaQueryList;\n try {\n mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n } catch {\n return;\n }\n const onChange = () => useTheme.setState({ mode: mq.matches ? \"dark\" : \"light\" });\n onChange();\n mq.addEventListener?.(\"change\", onChange);\n return () => mq.removeEventListener?.(\"change\", onChange);\n }, [preference]);\n }\n\n return { useTheme, useApplyTheme };\n}\n\nexport type ThemeStore = ReturnType<typeof createThemeStore>;\n"],"mappings":"AAAA,SAAS,iBAAiB;AAC1B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,cAAc;AAmBvB,SAAS,mBAA8B;AACrC,MAAI;AACF,WAAO,OAAO,WAAW,8BAA8B,EAAE,UAAU,SAAS;AAAA,EAC9E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAQ,YAAwC;AACvD,SAAO,eAAe,WAAW,iBAAiB,IAAI;AACxD;AAMA,SAAS,oBACP,GACiB;AACjB,MAAI,GAAG,eAAe,WAAW,GAAG,eAAe,UAAU,GAAG,eAAe,UAAU;AACvF,WAAO,EAAE;AAAA,EACX;AACA,MAAI,GAAG,SAAS,WAAW,GAAG,SAAS,OAAQ,QAAO,EAAE;AACxD,SAAO;AACT;AAGA,SAAS,gBAAgB,MAAuB;AAC9C,QAAM,OAAO,SAAS;AACtB,MAAI,SAAS,OAAQ,MAAK,UAAU,IAAI,MAAM;AAAA,MACzC,MAAK,UAAU,OAAO,MAAM;AACnC;AAWO,SAAS,oBAAoB,YAA+B;AACjE,MAAI,aAA8B;AAClC,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,aAAa,QAAQ,UAAU,KAAK,IAAI;AAGhE,iBAAa,oBAAoB,KAAK,KAAK;AAAA,EAC7C,QAAQ;AAAA,EAER;AACA,QAAM,OAAO,QAAQ,UAAU;AAC/B,kBAAgB,IAAI;AACpB,SAAO;AACT;AAQO,SAAS,iBAAiB,YAAoB;AACnD,QAAM,WAAW,OAAmB;AAAA,IAClC;AAAA,MACE;AAAA,QACE,CAAC,KAAK,SAAS;AAAA,UACb,YAAY;AAAA,UACZ,MAAM,iBAAiB;AAAA,UACvB,eAAe,CAAC,eAAe,IAAI,EAAE,YAAY,MAAM,QAAQ,UAAU,EAAE,CAAC;AAAA,UAC5E,SAAS,CAAC,SAAS,IAAI,EAAE,YAAY,MAAM,KAAK,CAAC;AAAA,UACjD,QAAQ,MAAM;AACZ,kBAAM,OAAkB,IAAI,EAAE,SAAS,SAAS,UAAU;AAC1D,gBAAI,EAAE,YAAY,MAAM,MAAM,KAAK,CAAC;AAAA,UACtC;AAAA,QACF;AAAA,QACA;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA;AAAA,UAET,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW;AAAA,UAC/C,SAAS,CAAC,oBAAqD;AAAA,YAC7D,YAAY;AAAA,cACV;AAAA,YACF;AAAA,UACF;AAAA,UACA,oBAAoB,MAAM,CAAC,UAAU;AAEnC,gBAAI,MAAO,OAAM,OAAO,QAAQ,MAAM,UAAU;AAAA,UAClD;AAAA,QACF;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,WAAS,gBAAsB;AAC7B,UAAM,OAAO,SAAS,CAAC,MAAM,EAAE,IAAI;AACnC,UAAM,aAAa,SAAS,CAAC,MAAM,EAAE,UAAU;AAC/C,cAAU,MAAM;AACd,sBAAgB,IAAI;AAAA,IACtB,GAAG,CAAC,IAAI,CAAC;AAET,cAAU,MAAM;AACd,UAAI,eAAe,SAAU;AAC7B,UAAI;AACJ,UAAI;AACF,aAAK,OAAO,WAAW,8BAA8B;AAAA,MACvD,QAAQ;AACN;AAAA,MACF;AACA,YAAM,WAAW,MAAM,SAAS,SAAS,EAAE,MAAM,GAAG,UAAU,SAAS,QAAQ,CAAC;AAChF,eAAS;AACT,SAAG,mBAAmB,UAAU,QAAQ;AACxC,aAAO,MAAM,GAAG,sBAAsB,UAAU,QAAQ;AAAA,IAC1D,GAAG,CAAC,UAAU,CAAC;AAAA,EACjB;AAEA,SAAO,EAAE,UAAU,cAAc;AACnC;","names":[]}
@@ -0,0 +1,104 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ type TourPlacement = "top" | "right" | "bottom" | "left" | "center";
5
+ interface TourStep {
6
+ /** CSS selector for the element to spotlight. Omit for a centered step
7
+ * (welcome/finish). If the target isn't found/visible, the step centers. */
8
+ target?: string;
9
+ title: string;
10
+ body: ReactNode;
11
+ /** Extra footer content, e.g. a "load sample data" shortcut. */
12
+ action?: ReactNode;
13
+ /** Spotlight padding around the target, px (default 8). */
14
+ padding?: number;
15
+ /** Where the card sits relative to the target. Omit for auto placement
16
+ * (below → above → centered). "center" ignores the target and centers on
17
+ * screen. An explicit side flips to the opposite side if it would overflow,
18
+ * then clamps into the viewport. */
19
+ placement?: TourPlacement;
20
+ /** Advance only when the user clicks the spotlighted target itself (not the
21
+ * card's Next button). Requires `target`: the real control's own handler runs
22
+ * first, then the tour steps forward on a microtask. The overlay stays
23
+ * click-through to the target. */
24
+ awaitClick?: boolean;
25
+ /** Runs before the step is shown — e.g. navigate to a route, open a panel.
26
+ * May be async; the step waits for it, then locates the target. */
27
+ beforeStep?: () => void | Promise<void>;
28
+ }
29
+ interface TourLabels {
30
+ next: string;
31
+ back: string;
32
+ skip: string;
33
+ done: string;
34
+ /** Shown in place of the Next button on an `awaitClick` step. */
35
+ awaitClickHint: string;
36
+ /** Step counter, e.g. (2, 7) => "2 / 7". */
37
+ step: (current: number, total: number) => string;
38
+ }
39
+ interface StartOptions {
40
+ /** Stable id of the tour, surfaced as `activeId` so the app can mirror it to
41
+ * the URL (deep-link / evaluation tracking) and record completion. */
42
+ id?: string;
43
+ /** Start at this step index instead of 0 (clamped) — for URL/deep-link resume. */
44
+ startIndex?: number;
45
+ /** Fires synchronously when the tour starts (e.g. enable a mock interceptor). */
46
+ onStart?: () => void;
47
+ onFinish?: () => void;
48
+ onSkip?: () => void;
49
+ }
50
+ interface TourContextValue {
51
+ active: boolean;
52
+ /** Id of the running tour (from {@link StartOptions.id}), else null. */
53
+ activeId: string | null;
54
+ /** Start a tour with the given steps. */
55
+ start: (steps: TourStep[], opts?: StartOptions) => void;
56
+ /** End the current tour (no callbacks fire). */
57
+ stop: () => void;
58
+ /** Advance one step; past the last step it finishes the tour (fires onFinish). */
59
+ next: () => void;
60
+ /** Go back one step (no-op on the first). */
61
+ prev: () => void;
62
+ /** Jump to a step by index, clamped to range — e.g. for URL/deep-link resume. */
63
+ goToStep: (index: number) => void;
64
+ /** Current step (0-based; 0 while inactive). */
65
+ index: number;
66
+ /** Total steps (0 while inactive). */
67
+ total: number;
68
+ }
69
+ declare function useTour(): TourContextValue;
70
+ /**
71
+ * The tour engine if it is there, `null` if it is not — for a control that is a
72
+ * DECORATION rather than a participant.
73
+ *
74
+ * {@link useTour}'s throw is correct and stays: a component that runs a tour cannot do
75
+ * its job without the provider, and failing loudly is how that gets fixed. But the
76
+ * throw's blast radius is whatever error boundary is above it, and for a launcher in
77
+ * the app shell that is the whole app.
78
+ *
79
+ * Keksdose feedback dev#524 is what that costs: an `@hb/ui` module was edited while a
80
+ * dev server was running, vite re-evaluated it, and the module's freshly created
81
+ * context object was no longer the one the mounted provider was providing. React's
82
+ * `useContext` then answers `null` for a provider that is, structurally, right there —
83
+ * and the top bar's tour menu took the entire shell down with it. **Build-time module
84
+ * graphs cannot produce that**, so it is a development-only failure; a menu that
85
+ * disappears for a second is the proportionate response to it either way.
86
+ *
87
+ * Only for controls that can honestly render nothing. If a component would show a
88
+ * broken half of itself without the tour, it wants {@link useTour}.
89
+ */
90
+ declare function useTourOptional(): TourContextValue | null;
91
+ /**
92
+ * Provides the guided-tour engine: a dimmed spotlight overlay + step card driven
93
+ * by a list of {@link TourStep}s. Domain-free — the app supplies the steps (copy,
94
+ * selectors, per-step navigation, placement) and the translated {@link TourLabels}.
95
+ * Mount once inside the router; start a tour from anywhere via {@link useTour},
96
+ * which also exposes step control (`next`/`prev`/`goToStep`) and the current
97
+ * `index`/`total` for URL mirroring, `awaitClick` and deep-link resume.
98
+ */
99
+ declare function TourProvider({ children, labels, }: {
100
+ children: ReactNode;
101
+ labels?: Partial<TourLabels>;
102
+ }): react.JSX.Element;
103
+
104
+ export { type TourLabels, type TourPlacement, TourProvider, type TourStep, useTour, useTourOptional };
@@ -0,0 +1,351 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ import { createPortal } from "react-dom";
12
+ import { cn } from "../lib/cn.js";
13
+ import { Button } from "../components/ui.js";
14
+ const DEFAULT_LABELS = {
15
+ next: "Next",
16
+ back: "Back",
17
+ skip: "Skip",
18
+ done: "Done",
19
+ awaitClickHint: "Click the highlighted element to continue",
20
+ step: (c, t) => `${c} / ${t}`
21
+ };
22
+ const TourContext = createContext(null);
23
+ function useTour() {
24
+ const ctx = useContext(TourContext);
25
+ if (!ctx) throw new Error("useTour must be used within <TourProvider>");
26
+ return ctx;
27
+ }
28
+ function useTourOptional() {
29
+ return useContext(TourContext);
30
+ }
31
+ function TourProvider({
32
+ children,
33
+ labels
34
+ }) {
35
+ const merged = { ...DEFAULT_LABELS, ...labels };
36
+ const [steps, setSteps] = useState(null);
37
+ const [index, setIndex] = useState(0);
38
+ const [activeId, setActiveId] = useState(null);
39
+ const optsRef = useRef({});
40
+ const stepsRef = useRef(null);
41
+ stepsRef.current = steps;
42
+ const indexRef = useRef(0);
43
+ indexRef.current = index;
44
+ const active = !!steps && steps.length > 0;
45
+ const total = steps?.length ?? 0;
46
+ const start = useCallback((s, opts) => {
47
+ optsRef.current = opts ?? {};
48
+ optsRef.current.onStart?.();
49
+ setIndex(Math.min(Math.max(0, opts?.startIndex ?? 0), Math.max(0, s.length - 1)));
50
+ setActiveId(opts?.id ?? null);
51
+ setSteps(s);
52
+ }, []);
53
+ const stop = useCallback(() => {
54
+ setSteps(null);
55
+ setActiveId(null);
56
+ }, []);
57
+ const finish = useCallback(() => {
58
+ optsRef.current.onFinish?.();
59
+ setSteps(null);
60
+ setActiveId(null);
61
+ }, []);
62
+ const skip = useCallback(() => {
63
+ optsRef.current.onSkip?.();
64
+ setSteps(null);
65
+ setActiveId(null);
66
+ }, []);
67
+ const goToStep = useCallback((i) => {
68
+ const len = stepsRef.current?.length ?? 0;
69
+ if (len === 0) return;
70
+ setIndex(Math.min(Math.max(0, i), len - 1));
71
+ }, []);
72
+ const next = useCallback(() => {
73
+ const len = stepsRef.current?.length ?? 0;
74
+ if (len === 0) return;
75
+ if (indexRef.current >= len - 1) finish();
76
+ else setIndex(indexRef.current + 1);
77
+ }, [finish]);
78
+ const prev = useCallback(() => {
79
+ if (indexRef.current > 0) setIndex(indexRef.current - 1);
80
+ }, []);
81
+ return /* @__PURE__ */ jsxs(TourContext.Provider, { value: { active, activeId, start, stop, next, prev, goToStep, index, total }, children: [
82
+ children,
83
+ active && /* @__PURE__ */ jsx(
84
+ TourOverlay,
85
+ {
86
+ steps,
87
+ index,
88
+ labels: merged,
89
+ onNext: next,
90
+ onPrev: prev,
91
+ onSkip: skip
92
+ }
93
+ )
94
+ ] });
95
+ }
96
+ function isVisible(el) {
97
+ const r = el.getBoundingClientRect();
98
+ if (r.width <= 0 || r.height <= 0) return false;
99
+ const style = getComputedStyle(el);
100
+ return style.visibility !== "hidden" && style.display !== "none";
101
+ }
102
+ function queryVisibleTarget(selector) {
103
+ const els = document.querySelectorAll(selector);
104
+ for (const el of els) {
105
+ if (isVisible(el)) return el;
106
+ }
107
+ return null;
108
+ }
109
+ function sameRect(a, b) {
110
+ if (a === b) return true;
111
+ if (!a || !b) return false;
112
+ return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;
113
+ }
114
+ function TourOverlay({
115
+ steps,
116
+ index,
117
+ labels,
118
+ onNext,
119
+ onPrev,
120
+ onSkip
121
+ }) {
122
+ const step = steps[index];
123
+ const isFirst = index === 0;
124
+ const isLast = index === steps.length - 1;
125
+ const [rect, setRect] = useState(null);
126
+ const [ready, setReady] = useState(false);
127
+ useEffect(() => {
128
+ let cancelled = false;
129
+ let raf = 0;
130
+ setReady(false);
131
+ setRect(null);
132
+ void (async () => {
133
+ await step.beforeStep?.();
134
+ if (cancelled) return;
135
+ if (!step.target) {
136
+ setReady(true);
137
+ return;
138
+ }
139
+ let tries = 0;
140
+ const find = () => {
141
+ if (cancelled) return;
142
+ const el = queryVisibleTarget(step.target);
143
+ if (el) {
144
+ el.scrollIntoView({ block: "center", behavior: "smooth" });
145
+ const r = el.getBoundingClientRect();
146
+ setRect({ top: r.top, left: r.left, width: r.width, height: r.height });
147
+ setReady(true);
148
+ return;
149
+ }
150
+ if (tries++ < 45) raf = requestAnimationFrame(find);
151
+ else setReady(true);
152
+ };
153
+ find();
154
+ })();
155
+ return () => {
156
+ cancelled = true;
157
+ cancelAnimationFrame(raf);
158
+ };
159
+ }, [index, step]);
160
+ useEffect(() => {
161
+ if (!step.target) return;
162
+ const update = () => {
163
+ const el = queryVisibleTarget(step.target);
164
+ if (el) {
165
+ const r = el.getBoundingClientRect();
166
+ const next = { top: r.top, left: r.left, width: r.width, height: r.height };
167
+ setRect((prev) => sameRect(prev, next) ? prev : next);
168
+ }
169
+ };
170
+ window.addEventListener("scroll", update, true);
171
+ window.addEventListener("resize", update);
172
+ const id = window.setInterval(update, 250);
173
+ return () => {
174
+ window.removeEventListener("scroll", update, true);
175
+ window.removeEventListener("resize", update);
176
+ clearInterval(id);
177
+ };
178
+ }, [index, step]);
179
+ useEffect(() => {
180
+ if (!ready || !step.awaitClick || !step.target) return;
181
+ const el = queryVisibleTarget(step.target);
182
+ if (!el) return;
183
+ const onClick = () => window.setTimeout(() => onNext(), 0);
184
+ el.addEventListener("click", onClick, { once: true });
185
+ return () => el.removeEventListener("click", onClick);
186
+ }, [ready, index, step, onNext]);
187
+ useEffect(() => {
188
+ const onKey = (e) => {
189
+ if (e.key === "Escape") {
190
+ onSkip();
191
+ } else if (e.key === "ArrowRight") {
192
+ e.preventDefault();
193
+ onNext();
194
+ } else if (e.key === "Enter") {
195
+ if (step.awaitClick) return;
196
+ e.preventDefault();
197
+ onNext();
198
+ } else if (e.key === "ArrowLeft") {
199
+ if (!isFirst) onPrev();
200
+ }
201
+ };
202
+ document.addEventListener("keydown", onKey);
203
+ return () => document.removeEventListener("keydown", onKey);
204
+ }, [isFirst, step, onNext, onPrev, onSkip]);
205
+ if (!ready) return null;
206
+ const pad = step.padding ?? 8;
207
+ const spot = rect ? { top: rect.top - pad, left: rect.left - pad, width: rect.width + pad * 2, height: rect.height + pad * 2 } : null;
208
+ return createPortal(
209
+ /* @__PURE__ */ jsxs("div", { className: "pointer-events-none fixed inset-0 z-[60]", role: "dialog", "aria-modal": "true", "aria-label": step.title, children: [
210
+ spot ? /* @__PURE__ */ jsx(
211
+ "div",
212
+ {
213
+ className: "pointer-events-none fixed rounded-lg transition-all duration-200 ease-out",
214
+ style: {
215
+ top: spot.top,
216
+ left: spot.left,
217
+ width: spot.width,
218
+ height: spot.height,
219
+ // A two-tone frame, not a single translucent ring.
220
+ //
221
+ // The old treatment was `ring-2 ring-white/80` over a 55% scrim, and it
222
+ // "was barely visible" on the app's blue-tinted surfaces. Two reasons,
223
+ // and a single ring can't fix either: at 55% the surround stays bright
224
+ // enough that the un-dimmed hole barely reads AS a hole, and a 80%-white
225
+ // ring sitting on the edge of a light card is white-on-near-white.
226
+ //
227
+ // So: an opaque white band immediately around the target, a dark keyline
228
+ // outside it, then the scrim. Whatever the target's own colour is, one of
229
+ // the two bands is always in contrast with it and the other is always in
230
+ // contrast with the dimmed surround — there is no background that can
231
+ // swallow both. The blurred layer between them softens the step so the
232
+ // frame reads as lighting rather than as a pasted-on rectangle.
233
+ boxShadow: [
234
+ "0 0 0 3px rgba(255,255,255,0.98)",
235
+ "0 0 0 5px rgba(15,23,42,0.92)",
236
+ "0 0 18px 6px rgba(15,23,42,0.45)",
237
+ "0 0 0 9999px rgba(15,23,42,0.70)"
238
+ ].join(", ")
239
+ }
240
+ }
241
+ ) : /* @__PURE__ */ jsx("div", { className: "pointer-events-auto fixed inset-0 bg-slate-900/70" }),
242
+ /* @__PURE__ */ jsx(
243
+ TourCard,
244
+ {
245
+ step,
246
+ index,
247
+ total: steps.length,
248
+ labels,
249
+ isFirst,
250
+ isLast,
251
+ rect: spot,
252
+ onBack: onPrev,
253
+ onNext,
254
+ onSkip
255
+ }
256
+ )
257
+ ] }),
258
+ document.body
259
+ );
260
+ }
261
+ function placeCard(rect, cw, ch, vw, vh, m, placement) {
262
+ if (!rect || placement === "center") {
263
+ return { top: Math.max(m, (vh - ch) / 2), left: Math.max(m, (vw - cw) / 2) };
264
+ }
265
+ const clampX = (x) => Math.min(Math.max(m, x), Math.max(m, vw - cw - m));
266
+ const clampY = (y) => Math.min(Math.max(m, y), Math.max(m, vh - ch - m));
267
+ const centerX = clampX(rect.left + rect.width / 2 - cw / 2);
268
+ const centerY = clampY(rect.top + rect.height / 2 - ch / 2);
269
+ const belowTop = rect.top + rect.height + m;
270
+ const aboveTop = rect.top - ch - m;
271
+ const rightLeft = rect.left + rect.width + m;
272
+ const leftLeft = rect.left - cw - m;
273
+ const fitsBelow = belowTop + ch <= vh;
274
+ const fitsAbove = aboveTop >= 0;
275
+ const fitsRight = rightLeft + cw <= vw;
276
+ const fitsLeft = leftLeft >= 0;
277
+ switch (placement) {
278
+ case "top":
279
+ return { top: fitsAbove ? aboveTop : fitsBelow ? belowTop : clampY(aboveTop), left: centerX };
280
+ case "bottom":
281
+ return { top: fitsBelow ? belowTop : fitsAbove ? aboveTop : clampY(belowTop), left: centerX };
282
+ case "right":
283
+ return { top: centerY, left: fitsRight ? rightLeft : fitsLeft ? leftLeft : clampX(rightLeft) };
284
+ case "left":
285
+ return { top: centerY, left: fitsLeft ? leftLeft : fitsRight ? rightLeft : clampX(leftLeft) };
286
+ default:
287
+ if (fitsBelow) return { top: belowTop, left: centerX };
288
+ if (fitsAbove) return { top: aboveTop, left: centerX };
289
+ return { top: Math.max(m, (vh - ch) / 2), left: centerX };
290
+ }
291
+ }
292
+ function TourCard({
293
+ step,
294
+ index,
295
+ total,
296
+ labels,
297
+ isFirst,
298
+ isLast,
299
+ rect,
300
+ onBack,
301
+ onNext,
302
+ onSkip
303
+ }) {
304
+ const ref = useRef(null);
305
+ const [style, setStyle] = useState({ opacity: 0 });
306
+ useLayoutEffect(() => {
307
+ const card = ref.current;
308
+ if (!card) return;
309
+ setStyle(
310
+ placeCard(rect, card.offsetWidth, card.offsetHeight, window.innerWidth, window.innerHeight, 12, step.placement)
311
+ );
312
+ }, [rect, index, step.placement]);
313
+ return /* @__PURE__ */ jsxs(
314
+ "div",
315
+ {
316
+ ref,
317
+ style,
318
+ className: "pointer-events-auto fixed z-[61] w-80 max-w-[calc(100vw-1.5rem)] rounded-lg border border-slate-200 bg-white p-4 shadow-xl transition-all duration-200 ease-out dark:border-slate-700 dark:bg-slate-900",
319
+ children: [
320
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-slate-900 dark:text-slate-100", children: step.title }),
321
+ /* @__PURE__ */ jsx("div", { className: "mt-1 text-sm leading-relaxed text-slate-600 dark:text-slate-300", children: step.body }),
322
+ step.action && /* @__PURE__ */ jsx("div", { className: "mt-3", children: step.action }),
323
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
324
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
325
+ /* @__PURE__ */ jsx(
326
+ "button",
327
+ {
328
+ type: "button",
329
+ onClick: onSkip,
330
+ className: "text-xs text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-300",
331
+ children: labels.skip
332
+ }
333
+ ),
334
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
335
+ /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap text-xs tabular-nums text-slate-400 dark:text-slate-500", children: labels.step(index + 1, total) }),
336
+ !isFirst && /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onBack, className: "px-2.5 py-1 text-xs", children: labels.back }),
337
+ !(step.awaitClick && step.target && rect) && /* @__PURE__ */ jsx(Button, { variant: "brand", onClick: onNext, className: cn("px-2.5 py-1 text-xs"), children: isLast ? labels.done : labels.next })
338
+ ] })
339
+ ] }),
340
+ step.awaitClick && step.target && rect && /* @__PURE__ */ jsx("div", { className: "text-center text-xs italic text-slate-400 dark:text-slate-500", children: labels.awaitClickHint })
341
+ ] })
342
+ ]
343
+ }
344
+ );
345
+ }
346
+ export {
347
+ TourProvider,
348
+ useTour,
349
+ useTourOptional
350
+ };
351
+ //# sourceMappingURL=tour.js.map