@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,44 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Check } from "lucide-react";
3
+ import { HoverMenu } from "../components/hover-menu.js";
4
+ import { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from "./topbar-controls.js";
5
+ import { Tooltip } from "../components/tooltip.js";
6
+ function OptionSwitcherMenu({
7
+ icon,
8
+ ariaLabel,
9
+ title,
10
+ heading,
11
+ options,
12
+ value,
13
+ onSelect
14
+ }) {
15
+ return /* @__PURE__ */ jsx(
16
+ HoverMenu,
17
+ {
18
+ ariaLabel,
19
+ trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: title, portal: true, children: /* @__PURE__ */ jsx("button", { type: "button", onClick: toggle, "aria-label": ariaLabel, className: TOPBAR_TRIGGER_CLASS, children: icon }) }),
20
+ children: (close) => /* @__PURE__ */ jsxs("ul", { className: "py-1", children: [
21
+ heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500", children: heading }),
22
+ options.map((opt) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
23
+ "button",
24
+ {
25
+ type: "button",
26
+ onClick: () => {
27
+ onSelect(opt.value);
28
+ close();
29
+ },
30
+ className: TOPBAR_MENU_ITEM_CLASS,
31
+ children: [
32
+ /* @__PURE__ */ jsx("span", { children: opt.label }),
33
+ value === opt.value && /* @__PURE__ */ jsx(Check, { className: "size-4 text-slate-700 dark:text-slate-200" })
34
+ ]
35
+ }
36
+ ) }, opt.value))
37
+ ] })
38
+ }
39
+ );
40
+ }
41
+ export {
42
+ OptionSwitcherMenu
43
+ };
44
+ //# sourceMappingURL=option-switcher-menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/option-switcher-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\nimport { Tooltip } from \"../components/tooltip\";\n\nexport interface OptionSwitcherOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/**\n * A top-bar hover menu that switches between a small set of options, with a\n * check on the active one and an optional heading. Generic glue over\n * {@link HoverMenu} — e.g. a dev role/impersonation switcher, an environment\n * picker. Controlled: the app owns `value` and `onSelect`.\n */\nexport function OptionSwitcherMenu<T extends string>({\n icon,\n ariaLabel,\n title,\n heading,\n options,\n value,\n onSelect,\n}: {\n icon: ReactNode;\n ariaLabel: string;\n title?: string;\n heading?: string;\n options: OptionSwitcherOption<T>[];\n value: T;\n onSelect: (value: T) => void;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={title} portal>\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500\">\n {heading}\n </li>\n )}\n {options.map((opt) => (\n <li key={opt.value}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(opt.value);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span>{opt.label}</span>\n {value === opt.value && <Check className=\"size-4 text-slate-700 dark:text-slate-200\" />}\n </button>\n </li>\n ))}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"AAuCU,cAeI,YAfJ;AAtCV,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,eAAe;AAajB,SAAS,mBAAqC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,OAAO,QAAM,MAC3B,8BAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,QACX;AAAA,mBACC,oBAAC,QAAG,WAAU,kGACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,QACZ,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;AACb,uBAAS,IAAI,KAAK;AAClB,oBAAM;AAAA,YACR;AAAA,YACA,WAAW;AAAA,YAEX;AAAA,kCAAC,UAAM,cAAI,OAAM;AAAA,cAChB,UAAU,IAAI,SAAS,oBAAC,SAAM,WAAU,6CAA4C;AAAA;AAAA;AAAA,QACvF,KAXO,IAAI,KAYb,CACD;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -0,0 +1,19 @@
1
+ import * as react from 'react';
2
+
3
+ interface RoleSwitcherProps<R extends string> {
4
+ roles: readonly R[];
5
+ value: R;
6
+ onChange: (role: R) => void;
7
+ ariaLabel?: string;
8
+ heading?: string;
9
+ title?: string;
10
+ }
11
+ /**
12
+ * A reusable user-role switcher (feedback #337): a thin wrapper over
13
+ * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in
14
+ * the top bar with just the role list + current value + a handler. Typically
15
+ * dev-only / UI-only, but the app decides where and whether to render it.
16
+ */
17
+ declare function RoleSwitcher<R extends string>({ roles, value, onChange, ariaLabel, heading, title, }: RoleSwitcherProps<R>): react.JSX.Element;
18
+
19
+ export { RoleSwitcher, type RoleSwitcherProps };
@@ -0,0 +1,28 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { FlaskConical } from "lucide-react";
3
+ import { OptionSwitcherMenu } from "./option-switcher-menu.js";
4
+ function RoleSwitcher({
5
+ roles,
6
+ value,
7
+ onChange,
8
+ ariaLabel = "Switch role",
9
+ heading,
10
+ title
11
+ }) {
12
+ return /* @__PURE__ */ jsx(
13
+ OptionSwitcherMenu,
14
+ {
15
+ ariaLabel,
16
+ title: title ?? `Role: ${value}`,
17
+ heading,
18
+ icon: /* @__PURE__ */ jsx(FlaskConical, { className: "size-5 text-amber-500" }),
19
+ options: roles.map((r) => ({ value: r, label: r })),
20
+ value,
21
+ onSelect: onChange
22
+ }
23
+ );
24
+ }
25
+ export {
26
+ RoleSwitcher
27
+ };
28
+ //# sourceMappingURL=role-switcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/role-switcher.tsx"],"sourcesContent":["import { FlaskConical } from \"lucide-react\";\nimport { OptionSwitcherMenu } from \"./option-switcher-menu\";\n\nexport interface RoleSwitcherProps<R extends string> {\n roles: readonly R[];\n value: R;\n onChange: (role: R) => void;\n ariaLabel?: string;\n heading?: string;\n title?: string;\n}\n\n/**\n * A reusable user-role switcher (feedback #337): a thin wrapper over\n * {@link OptionSwitcherMenu} with the standard flask icon, so an app drops it in\n * the top bar with just the role list + current value + a handler. Typically\n * dev-only / UI-only, but the app decides where and whether to render it.\n */\nexport function RoleSwitcher<R extends string>({\n roles,\n value,\n onChange,\n ariaLabel = \"Switch role\",\n heading,\n title,\n}: RoleSwitcherProps<R>) {\n return (\n <OptionSwitcherMenu<R>\n ariaLabel={ariaLabel}\n title={title ?? `Role: ${value}`}\n heading={heading}\n icon={<FlaskConical className=\"size-5 text-amber-500\" />}\n options={roles.map((r) => ({ value: r, label: r }))}\n value={value}\n onSelect={onChange}\n />\n );\n}\n"],"mappings":"AA+BY;AA/BZ,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AAiB5B,SAAS,aAA+B;AAAA,EAC7C;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AACF,GAAyB;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,SAAS,SAAS,KAAK;AAAA,MAC9B;AAAA,MACA,MAAM,oBAAC,gBAAa,WAAU,yBAAwB;AAAA,MACtD,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,MAClD;AAAA,MACA,UAAU;AAAA;AAAA,EACZ;AAEJ;","names":[]}
@@ -0,0 +1,15 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * The sticky app header frame: a `brand` slot on the left and an `actions` slot
6
+ * on the right (theme/palette/language controls, account menu, etc.). Domain-free
7
+ * — each app composes its own actions from the shared controls plus its own menus.
8
+ */
9
+ declare function TopBar({ brand, actions, className, }: {
10
+ brand: ReactNode;
11
+ actions?: ReactNode;
12
+ className?: string;
13
+ }): react.JSX.Element;
14
+
15
+ export { TopBar };
@@ -0,0 +1,29 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/cn.js";
3
+ function TopBar({
4
+ brand,
5
+ actions,
6
+ className
7
+ }) {
8
+ return /* @__PURE__ */ jsxs(
9
+ "header",
10
+ {
11
+ className: cn(
12
+ // z-40 keeps the app header above sticky page content — notably the
13
+ // expanded transaction editor (z-30), which on mobile shares the page
14
+ // scroll and otherwise slid over the header while scrolling (feedback
15
+ // #320). Modals/tour (z-50/60) still sit above it.
16
+ "sticky top-0 z-40 flex h-12 items-center justify-between gap-2 border-b border-[var(--border)] bg-[var(--bg-surface)] px-3 md:px-6",
17
+ className
18
+ ),
19
+ children: [
20
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 items-center gap-2", children: brand }),
21
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: actions })
22
+ ]
23
+ }
24
+ );
25
+ }
26
+ export {
27
+ TopBar
28
+ };
29
+ //# sourceMappingURL=top-bar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/top-bar.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * The sticky app header frame: a `brand` slot on the left and an `actions` slot\n * on the right (theme/palette/language controls, account menu, etc.). Domain-free\n * — each app composes its own actions from the shared controls plus its own menus.\n */\nexport function TopBar({\n brand,\n actions,\n className,\n}: {\n brand: ReactNode;\n actions?: ReactNode;\n className?: string;\n}) {\n return (\n <header\n className={cn(\n // z-40 keeps the app header above sticky page content — notably the\n // expanded transaction editor (z-30), which on mobile shares the page\n // scroll and otherwise slid over the header while scrolling (feedback\n // #320). Modals/tour (z-50/60) still sit above it.\n \"sticky top-0 z-40 flex h-12 items-center justify-between gap-2 border-b border-[var(--border)] bg-[var(--bg-surface)] px-3 md:px-6\",\n className,\n )}\n >\n <div className=\"flex min-w-0 items-center gap-2\">{brand}</div>\n <div className=\"flex items-center gap-1\">{actions}</div>\n </header>\n );\n}\n"],"mappings":"AAkBI,SAUE,KAVF;AAjBJ,SAAS,UAAU;AAOZ,SAAS,OAAO;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,SAAI,WAAU,mCAAmC,iBAAM;AAAA,QACxD,oBAAC,SAAI,WAAU,2BAA2B,mBAAQ;AAAA;AAAA;AAAA,EACpD;AAEJ;","names":[]}
@@ -0,0 +1,44 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a
6
+ * `link` navigates via react-router, and a `divider` draws a rule. Non-divider
7
+ * rows close the menu automatically when chosen. `kind` may be omitted for
8
+ * actions, keeping the common case terse.
9
+ */
10
+ type TopBarMenuEntry = {
11
+ kind?: "action";
12
+ key: string;
13
+ icon?: ReactNode;
14
+ label: ReactNode;
15
+ trailing?: ReactNode;
16
+ onSelect: () => void;
17
+ } | {
18
+ kind: "link";
19
+ key: string;
20
+ icon?: ReactNode;
21
+ label: ReactNode;
22
+ trailing?: ReactNode;
23
+ to: string;
24
+ } | {
25
+ kind: "divider";
26
+ key: string;
27
+ };
28
+ /**
29
+ * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
30
+ * with an optional uppercase heading and divider rules. Generic glue over the
31
+ * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —
32
+ * so those call sites describe their rows as data instead of rebuilding the
33
+ * trigger + list markup. For a checkmarked value-switcher use
34
+ * {@link OptionSwitcherMenu} instead.
35
+ */
36
+ declare function TopBarActionMenu({ icon, ariaLabel, heading, entries, panelClassName, }: {
37
+ icon: ReactNode;
38
+ ariaLabel: string;
39
+ heading?: string;
40
+ entries: TopBarMenuEntry[];
41
+ panelClassName?: string;
42
+ }): react.JSX.Element;
43
+
44
+ export { TopBarActionMenu, type TopBarMenuEntry };
@@ -0,0 +1,49 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Link } from "react-router";
3
+ import { cn } from "../lib/cn.js";
4
+ import { HoverMenu } from "../components/hover-menu.js";
5
+ import { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from "./topbar-controls.js";
6
+ function TopBarActionMenu({
7
+ icon,
8
+ ariaLabel,
9
+ heading,
10
+ entries,
11
+ panelClassName
12
+ }) {
13
+ return /* @__PURE__ */ jsx(
14
+ HoverMenu,
15
+ {
16
+ ariaLabel,
17
+ trigger: ({ toggle }) => /* @__PURE__ */ jsx("button", { type: "button", onClick: toggle, "aria-label": ariaLabel, className: TOPBAR_TRIGGER_CLASS, children: icon }),
18
+ children: (close) => /* @__PURE__ */ jsxs("ul", { className: cn("py-1", panelClassName), children: [
19
+ heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500", children: heading }),
20
+ entries.map((entry) => {
21
+ if (entry.kind === "divider") {
22
+ return /* @__PURE__ */ jsx("li", { className: "my-1 border-t border-slate-100 dark:border-slate-800" }, entry.key);
23
+ }
24
+ const content = /* @__PURE__ */ jsxs("span", { className: "flex w-full items-center gap-2", children: [
25
+ entry.icon,
26
+ entry.label,
27
+ entry.trailing && /* @__PURE__ */ jsx("span", { className: "ml-auto pl-2", children: entry.trailing })
28
+ ] });
29
+ return /* @__PURE__ */ jsx("li", { children: entry.kind === "link" ? /* @__PURE__ */ jsx(Link, { to: entry.to, onClick: close, className: TOPBAR_MENU_ITEM_CLASS, children: content }) : /* @__PURE__ */ jsx(
30
+ "button",
31
+ {
32
+ type: "button",
33
+ onClick: () => {
34
+ entry.onSelect();
35
+ close();
36
+ },
37
+ className: TOPBAR_MENU_ITEM_CLASS,
38
+ children: content
39
+ }
40
+ ) }, entry.key);
41
+ })
42
+ ] })
43
+ }
44
+ );
45
+ }
46
+ export {
47
+ TopBarActionMenu
48
+ };
49
+ //# sourceMappingURL=topbar-action-menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/topbar-action-menu.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport { TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS } from \"./topbar-controls\";\n\n/**\n * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a\n * `link` navigates via react-router, and a `divider` draws a rule. Non-divider\n * rows close the menu automatically when chosen. `kind` may be omitted for\n * actions, keeping the common case terse.\n */\nexport type TopBarMenuEntry =\n | { kind?: \"action\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; onSelect: () => void }\n | { kind: \"link\"; key: string; icon?: ReactNode; label: ReactNode; trailing?: ReactNode; to: string }\n | { kind: \"divider\"; key: string };\n\n/**\n * A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,\n * with an optional uppercase heading and divider rules. Generic glue over the\n * shared top-bar chrome — e.g. the feedback launcher or the guided-tours menu —\n * so those call sites describe their rows as data instead of rebuilding the\n * trigger + list markup. For a checkmarked value-switcher use\n * {@link OptionSwitcherMenu} instead.\n */\nexport function TopBarActionMenu({\n icon,\n ariaLabel,\n heading,\n entries,\n panelClassName,\n}: {\n icon: ReactNode;\n ariaLabel: string;\n heading?: string;\n entries: TopBarMenuEntry[];\n panelClassName?: string;\n}) {\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <button type=\"button\" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>\n {icon}\n </button>\n )}\n >\n {(close) => (\n <ul className={cn(\"py-1\", panelClassName)}>\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500\">\n {heading}\n </li>\n )}\n {entries.map((entry) => {\n if (entry.kind === \"divider\") {\n return <li key={entry.key} className=\"my-1 border-t border-slate-100 dark:border-slate-800\" />;\n }\n const content = (\n <span className=\"flex w-full items-center gap-2\">\n {entry.icon}\n {entry.label}\n {entry.trailing && <span className=\"ml-auto pl-2\">{entry.trailing}</span>}\n </span>\n );\n return (\n <li key={entry.key}>\n {entry.kind === \"link\" ? (\n <Link to={entry.to} onClick={close} className={TOPBAR_MENU_ITEM_CLASS}>\n {content}\n </Link>\n ) : (\n <button\n type=\"button\"\n onClick={() => {\n entry.onSelect();\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n {content}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"AA0CQ,cAiBM,YAjBN;AAzCR,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,4BAA4B;AAqBtD,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,YAAO,MAAK,UAAS,SAAS,QAAQ,cAAY,WAAW,WAAW,sBACtE,gBACH;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAW,GAAG,QAAQ,cAAc,GACrC;AAAA,mBACC,oBAAC,QAAG,WAAU,kGACX,mBACH;AAAA,QAED,QAAQ,IAAI,CAAC,UAAU;AACtB,cAAI,MAAM,SAAS,WAAW;AAC5B,mBAAO,oBAAC,QAAmB,WAAU,0DAArB,MAAM,GAAsE;AAAA,UAC9F;AACA,gBAAM,UACJ,qBAAC,UAAK,WAAU,kCACb;AAAA,kBAAM;AAAA,YACN,MAAM;AAAA,YACN,MAAM,YAAY,oBAAC,UAAK,WAAU,gBAAgB,gBAAM,UAAS;AAAA,aACpE;AAEF,iBACE,oBAAC,QACE,gBAAM,SAAS,SACd,oBAAC,QAAK,IAAI,MAAM,IAAI,SAAS,OAAO,WAAW,wBAC5C,mBACH,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,sBAAM,SAAS;AACf,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,cAEV;AAAA;AAAA,UACH,KAfK,MAAM,GAiBf;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -0,0 +1,54 @@
1
+ import * as react from 'react';
2
+ import { PalettePreset } from '../theme/palette-presets.js';
3
+ import { ThemeMode } from '../theme/theme-store.js';
4
+ import '../theme/chart-palette.js';
5
+ import 'zustand/middleware';
6
+ import 'zustand';
7
+
8
+ /** Shared square icon-button styling for top-bar triggers, so app-owned controls
9
+ * (account/feedback menus) line up with the shared ones. */
10
+ declare const TOPBAR_TRIGGER_CLASS = "size-9 rounded-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800";
11
+ /** Shared row styling for the top-bar hover-menu items. */
12
+ declare const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-slate-700 hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800";
13
+ /** Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode. */
14
+ declare function ThemeToggle({ mode, onToggle, ariaLabel, title, className, }: {
15
+ mode: ThemeMode;
16
+ onToggle: () => void;
17
+ ariaLabel?: string;
18
+ title?: string;
19
+ className?: string;
20
+ }): react.JSX.Element;
21
+ /** Appearance-preset switcher: a hover menu of palette presets with per-theme
22
+ * colour swatches. Controlled — the app owns the active id and applies it. */
23
+ declare function PaletteMenu({ palettes, activeId, mode, onSelect, ariaLabel, heading, }: {
24
+ palettes: PalettePreset[];
25
+ activeId: string;
26
+ mode: ThemeMode;
27
+ onSelect: (id: string) => void;
28
+ ariaLabel?: string;
29
+ /** Optional small heading row above the list. */
30
+ heading?: string;
31
+ }): react.JSX.Element;
32
+ interface LanguageOption {
33
+ code: string;
34
+ label: string;
35
+ /** ISO 3166-1 alpha-2 country code (lowercase) for the flag-icons flag. */
36
+ country: string;
37
+ }
38
+ /** Language switcher: a hover menu of options with flags and a check on the
39
+ * active one. Controlled — the app owns the current code and change handler.
40
+ *
41
+ * The trigger is the *active* option's flag, not an icon: lucide's `Languages`
42
+ * glyph (文A) is the same mark Chrome puts in the omnibox for "translate this
43
+ * page", so it read as machine-translation rather than "pick the UI language"
44
+ * (Keksdose feedback #121). The flag also shows which language is currently on,
45
+ * which no icon can, and costs the same width on a crowded mobile bar. A globe
46
+ * covers the case where `current` matches nothing in `options`. */
47
+ declare function LanguageMenu({ options, current, onChange, ariaLabel, }: {
48
+ options: LanguageOption[];
49
+ current: string | undefined;
50
+ onChange: (code: string) => void;
51
+ ariaLabel?: string;
52
+ }): react.JSX.Element;
53
+
54
+ export { LanguageMenu, type LanguageOption, PaletteMenu, TOPBAR_MENU_ITEM_CLASS, TOPBAR_TRIGGER_CLASS, ThemeToggle };
@@ -0,0 +1,152 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Check, Globe, Moon, Palette, Sun } from "lucide-react";
3
+ import { cn } from "../lib/cn.js";
4
+ import { HoverMenu } from "../components/hover-menu.js";
5
+ import { Tooltip } from "../components/tooltip.js";
6
+ const TOPBAR_TRIGGER_CLASS = "size-9 rounded-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800";
7
+ const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-slate-700 hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800";
8
+ function ThemeToggle({
9
+ mode,
10
+ onToggle,
11
+ ariaLabel,
12
+ title,
13
+ className
14
+ }) {
15
+ const Icon = mode === "dark" ? Moon : Sun;
16
+ return /* @__PURE__ */ jsx(Tooltip, { label: title, portal: true, children: /* @__PURE__ */ jsx(
17
+ "button",
18
+ {
19
+ type: "button",
20
+ onClick: onToggle,
21
+ "aria-label": ariaLabel,
22
+ className: cn(TOPBAR_TRIGGER_CLASS, className),
23
+ children: /* @__PURE__ */ jsx(Icon, { className: "size-5" })
24
+ }
25
+ ) });
26
+ }
27
+ function PaletteMenu({
28
+ palettes,
29
+ activeId,
30
+ mode,
31
+ onSelect,
32
+ ariaLabel = "Appearance preset",
33
+ heading
34
+ }) {
35
+ const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;
36
+ return /* @__PURE__ */ jsx(
37
+ HoverMenu,
38
+ {
39
+ ariaLabel,
40
+ trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: activeName, portal: true, children: /* @__PURE__ */ jsx(
41
+ "button",
42
+ {
43
+ type: "button",
44
+ onClick: toggle,
45
+ "aria-label": ariaLabel,
46
+ className: TOPBAR_TRIGGER_CLASS,
47
+ children: /* @__PURE__ */ jsx(Palette, { className: "size-5 text-violet-500" })
48
+ }
49
+ ) }),
50
+ children: (close) => /* @__PURE__ */ jsxs("ul", { className: "w-60 py-1", children: [
51
+ heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500", children: heading }),
52
+ palettes.map((p) => {
53
+ const ts = p[mode];
54
+ const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];
55
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
56
+ "button",
57
+ {
58
+ type: "button",
59
+ onClick: () => {
60
+ onSelect(p.id);
61
+ close();
62
+ },
63
+ className: TOPBAR_MENU_ITEM_CLASS,
64
+ children: [
65
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
66
+ /* @__PURE__ */ jsx("span", { className: "flex shrink-0 gap-0.5", children: swatches.map((c, i) => /* @__PURE__ */ jsx(
67
+ "span",
68
+ {
69
+ className: "size-3.5 rounded-sm ring-1 ring-inset ring-black/10 dark:ring-white/10",
70
+ style: { background: c }
71
+ },
72
+ i
73
+ )) }),
74
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col text-left", children: [
75
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: p.name }),
76
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-slate-400 dark:text-slate-500", children: p.blurb })
77
+ ] })
78
+ ] }),
79
+ activeId === p.id && /* @__PURE__ */ jsx(Check, { className: "size-4 shrink-0 text-slate-700 dark:text-slate-200" })
80
+ ]
81
+ }
82
+ ) }, p.id);
83
+ })
84
+ ] })
85
+ }
86
+ );
87
+ }
88
+ function LanguageMenu({
89
+ options,
90
+ current,
91
+ onChange,
92
+ ariaLabel = "Language"
93
+ }) {
94
+ const active = options.find((o) => o.code === current);
95
+ return /* @__PURE__ */ jsx(
96
+ HoverMenu,
97
+ {
98
+ ariaLabel,
99
+ trigger: ({ toggle }) => /* @__PURE__ */ jsx(Tooltip, { label: active?.label ?? ariaLabel, portal: true, children: /* @__PURE__ */ jsx(
100
+ "button",
101
+ {
102
+ type: "button",
103
+ onClick: toggle,
104
+ "aria-label": ariaLabel,
105
+ className: TOPBAR_TRIGGER_CLASS,
106
+ children: active ? /* @__PURE__ */ jsx(
107
+ "span",
108
+ {
109
+ "aria-hidden": true,
110
+ className: `fi fi-${active.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`
111
+ }
112
+ ) : /* @__PURE__ */ jsx(Globe, { className: "size-5" })
113
+ }
114
+ ) }),
115
+ children: (close) => /* @__PURE__ */ jsx("ul", { className: "py-1", children: options.map((lang) => {
116
+ const isActive = lang.code === current;
117
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
118
+ "button",
119
+ {
120
+ type: "button",
121
+ onClick: () => {
122
+ onChange(lang.code);
123
+ close();
124
+ },
125
+ className: TOPBAR_MENU_ITEM_CLASS,
126
+ children: [
127
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
128
+ /* @__PURE__ */ jsx(
129
+ "span",
130
+ {
131
+ "aria-hidden": true,
132
+ className: `fi fi-${lang.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`
133
+ }
134
+ ),
135
+ lang.label
136
+ ] }),
137
+ isActive && /* @__PURE__ */ jsx(Check, { className: "size-4 text-slate-700 dark:text-slate-200" })
138
+ ]
139
+ }
140
+ ) }, lang.code);
141
+ }) })
142
+ }
143
+ );
144
+ }
145
+ export {
146
+ LanguageMenu,
147
+ PaletteMenu,
148
+ TOPBAR_MENU_ITEM_CLASS,
149
+ TOPBAR_TRIGGER_CLASS,
150
+ ThemeToggle
151
+ };
152
+ //# sourceMappingURL=topbar-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shell/topbar-controls.tsx"],"sourcesContent":["import { Check, Globe, Moon, Palette, Sun } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { HoverMenu } from \"../components/hover-menu\";\nimport type { PalettePreset } from \"../theme/palette-presets\";\nimport type { ThemeMode } from \"../theme/theme-store\";\nimport { Tooltip } from \"../components/tooltip\";\n\n/** Shared square icon-button styling for top-bar triggers, so app-owned controls\n * (account/feedback menus) line up with the shared ones. */\nexport const TOPBAR_TRIGGER_CLASS =\n \"size-9 rounded-md flex items-center justify-center text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800\";\n\n/** Shared row styling for the top-bar hover-menu items. */\nexport const TOPBAR_MENU_ITEM_CLASS =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-slate-700 hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800\";\n\n/** Sun/Moon toggle for the light/dark theme. Controlled: the app owns the mode. */\nexport function ThemeToggle({\n mode,\n onToggle,\n ariaLabel,\n title,\n className,\n}: {\n mode: ThemeMode;\n onToggle: () => void;\n ariaLabel?: string;\n title?: string;\n className?: string;\n}) {\n const Icon = mode === \"dark\" ? Moon : Sun;\n return (\n <Tooltip label={title} portal>\n <button\n type=\"button\"\n onClick={onToggle}\n aria-label={ariaLabel}\n className={cn(TOPBAR_TRIGGER_CLASS, className)}\n >\n <Icon className=\"size-5\" />\n </button>\n </Tooltip>\n );\n}\n\n/** Appearance-preset switcher: a hover menu of palette presets with per-theme\n * colour swatches. Controlled — the app owns the active id and applies it. */\nexport function PaletteMenu({\n palettes,\n activeId,\n mode,\n onSelect,\n ariaLabel = \"Appearance preset\",\n heading,\n}: {\n palettes: PalettePreset[];\n activeId: string;\n mode: ThemeMode;\n onSelect: (id: string) => void;\n ariaLabel?: string;\n /** Optional small heading row above the list. */\n heading?: string;\n}) {\n const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={activeName} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={ariaLabel}\n className={TOPBAR_TRIGGER_CLASS}\n >\n <Palette className=\"size-5 text-violet-500\" />\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"w-60 py-1\">\n {heading && (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500\">\n {heading}\n </li>\n )}\n {palettes.map((p) => {\n const ts = p[mode];\n const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];\n return (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => {\n onSelect(p.id);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n <span className=\"flex shrink-0 gap-0.5\">\n {swatches.map((c, i) => (\n <span\n key={i}\n className=\"size-3.5 rounded-sm ring-1 ring-inset ring-black/10 dark:ring-white/10\"\n style={{ background: c }}\n />\n ))}\n </span>\n <span className=\"flex min-w-0 flex-col text-left\">\n <span className=\"truncate\">{p.name}</span>\n <span className=\"truncate text-[11px] text-slate-400 dark:text-slate-500\">\n {p.blurb}\n </span>\n </span>\n </span>\n {activeId === p.id && (\n <Check className=\"size-4 shrink-0 text-slate-700 dark:text-slate-200\" />\n )}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n\nexport interface LanguageOption {\n code: string;\n label: string;\n /** ISO 3166-1 alpha-2 country code (lowercase) for the flag-icons flag. */\n country: string;\n}\n\n/** Language switcher: a hover menu of options with flags and a check on the\n * active one. Controlled — the app owns the current code and change handler.\n *\n * The trigger is the *active* option's flag, not an icon: lucide's `Languages`\n * glyph (文A) is the same mark Chrome puts in the omnibox for \"translate this\n * page\", so it read as machine-translation rather than \"pick the UI language\"\n * (Keksdose feedback #121). The flag also shows which language is currently on,\n * which no icon can, and costs the same width on a crowded mobile bar. A globe\n * covers the case where `current` matches nothing in `options`. */\nexport function LanguageMenu({\n options,\n current,\n onChange,\n ariaLabel = \"Language\",\n}: {\n options: LanguageOption[];\n current: string | undefined;\n onChange: (code: string) => void;\n ariaLabel?: string;\n}) {\n const active = options.find((o) => o.code === current);\n return (\n <HoverMenu\n ariaLabel={ariaLabel}\n trigger={({ toggle }) => (\n <Tooltip label={active?.label ?? ariaLabel} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={ariaLabel}\n className={TOPBAR_TRIGGER_CLASS}\n >\n {active ? (\n <span\n aria-hidden\n className={`fi fi-${active.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n ) : (\n <Globe className=\"size-5\" />\n )}\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"py-1\">\n {options.map((lang) => {\n const isActive = lang.code === current;\n return (\n <li key={lang.code}>\n <button\n type=\"button\"\n onClick={() => {\n onChange(lang.code);\n close();\n }}\n className={TOPBAR_MENU_ITEM_CLASS}\n >\n <span className=\"flex items-center gap-2\">\n <span\n aria-hidden\n className={`fi fi-${lang.country} inline-block h-[15px] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}\n />\n {lang.label}\n </span>\n {isActive && <Check className=\"size-4 text-slate-700 dark:text-slate-200\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"AAuCQ,cAuEY,YAvEZ;AAvCR,SAAS,OAAO,OAAO,MAAM,SAAS,WAAW;AACjD,SAAS,UAAU;AACnB,SAAS,iBAAiB;AAG1B,SAAS,eAAe;AAIjB,MAAM,uBACX;AAGK,MAAM,yBACX;AAGK,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,OAAO,SAAS,SAAS,OAAO;AACtC,SACE,oBAAC,WAAQ,OAAO,OAAO,QAAM,MAC3B;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,cAAY;AAAA,MACZ,WAAW,GAAG,sBAAsB,SAAS;AAAA,MAE7C,8BAAC,QAAK,WAAU,UAAS;AAAA;AAAA,EAC3B,GACF;AAEJ;AAIO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AACF,GAQG;AACD,QAAM,aAAa,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,GAAG,QAAQ;AACpE,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,YAAY,QAAM,MAChC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,cAAY;AAAA,UACZ,WAAW;AAAA,UAEX,8BAAC,WAAQ,WAAU,0BAAyB;AAAA;AAAA,MAC9C,GACF;AAAA,MAGD,WAAC,UACA,qBAAC,QAAG,WAAU,aACX;AAAA,mBACC,oBAAC,QAAG,WAAU,kGACX,mBACH;AAAA,QAED,SAAS,IAAI,CAAC,MAAM;AACnB,gBAAM,KAAK,EAAE,IAAI;AACjB,gBAAM,WAAW,CAAC,GAAG,QAAQ,GAAG,OAAO,GAAG,aAAa,GAAG,cAAc,GAAG,QAAQ;AACnF,iBACE,oBAAC,QACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,yBAAS,EAAE,EAAE;AACb,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,cAEX;AAAA,qCAAC,UAAK,WAAU,mCACd;AAAA,sCAAC,UAAK,WAAU,yBACb,mBAAS,IAAI,CAAC,GAAG,MAChB;AAAA,oBAAC;AAAA;AAAA,sBAEC,WAAU;AAAA,sBACV,OAAO,EAAE,YAAY,EAAE;AAAA;AAAA,oBAFlB;AAAA,kBAGP,CACD,GACH;AAAA,kBACA,qBAAC,UAAK,WAAU,mCACd;AAAA,wCAAC,UAAK,WAAU,YAAY,YAAE,MAAK;AAAA,oBACnC,oBAAC,UAAK,WAAU,2DACb,YAAE,OACL;AAAA,qBACF;AAAA,mBACF;AAAA,gBACC,aAAa,EAAE,MACd,oBAAC,SAAM,WAAU,sDAAqD;AAAA;AAAA;AAAA,UAE1E,KA7BO,EAAE,EA8BX;AAAA,QAEJ,CAAC;AAAA,SACH;AAAA;AAAA,EAEJ;AAEJ;AAkBO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAKG;AACD,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,OAAO;AACrD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,SAAS,CAAC,EAAE,OAAO,MACjB,oBAAC,WAAQ,OAAO,QAAQ,SAAS,WAAW,QAAM,MAChD;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,cAAY;AAAA,UACZ,WAAW;AAAA,UAEV,mBACC;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,WAAW,SAAS,OAAO,OAAO;AAAA;AAAA,UACpC,IAEA,oBAAC,SAAM,WAAU,UAAS;AAAA;AAAA,MAE9B,GACF;AAAA,MAGD,WAAC,UACA,oBAAC,QAAG,WAAU,QACX,kBAAQ,IAAI,CAAC,SAAS;AACrB,cAAM,WAAW,KAAK,SAAS;AAC/B,eACE,oBAAC,QACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM;AACb,uBAAS,KAAK,IAAI;AAClB,oBAAM;AAAA,YACR;AAAA,YACA,WAAW;AAAA,YAEX;AAAA,mCAAC,UAAK,WAAU,2BACd;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,eAAW;AAAA,oBACX,WAAW,SAAS,KAAK,OAAO;AAAA;AAAA,gBAClC;AAAA,gBACC,KAAK;AAAA,iBACR;AAAA,cACC,YAAY,oBAAC,SAAM,WAAU,6CAA4C;AAAA;AAAA;AAAA,QAC5E,KAjBO,KAAK,IAkBd;AAAA,MAEJ,CAAC,GACH;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -0,0 +1,60 @@
1
+ /** CVD-safe categorical colour for series `index`, as a theme-aware CSS var. */
2
+ declare function paletteFor(index: number): string;
3
+ /** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow
4
+ cues at the call site so colour is never the only signal (CVD). */
5
+ declare const CHART_COLORS: {
6
+ readonly income: "var(--money-income)";
7
+ readonly expense: "var(--money-expense)";
8
+ readonly net: "var(--money-net)";
9
+ readonly assigned: "var(--brand)";
10
+ readonly activity: "var(--money-expense)";
11
+ };
12
+ declare const PALETTE_HEX: {
13
+ readonly light: readonly ["#332288", "#88ccee", "#44aa99", "#117733", "#999933", "#ddcc77", "#cc6677", "#882255", "#aa4499"];
14
+ readonly dark: readonly ["#7e72d6", "#9fd8f2", "#5fc4b0", "#3fa45f", "#bfbf5e", "#e8dda0", "#e08c9a", "#c25a78", "#cc78be"];
15
+ };
16
+ /** A heatmap's interpolation stops for one theme. Open string fields so any
17
+ palette preset (not just the built-in default) can supply its own stops. */
18
+ interface HeatStops {
19
+ neutral: string;
20
+ under: string;
21
+ over: string;
22
+ seqLow: string;
23
+ seqHigh: string;
24
+ empty: string;
25
+ }
26
+ /** Diverging (variance: under←0→over) and sequential (spending intensity)
27
+ heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */
28
+ declare const HEATMAP_HEX: {
29
+ readonly light: {
30
+ readonly neutral: "#f1f5f9";
31
+ readonly under: "#0f766e";
32
+ readonly over: "#b45309";
33
+ readonly seqLow: "#fef3c7";
34
+ readonly seqHigh: "#b45309";
35
+ readonly empty: "rgba(148,163,184,0.14)";
36
+ };
37
+ readonly dark: {
38
+ readonly neutral: "#334155";
39
+ readonly under: "#2dd4bf";
40
+ readonly over: "#fbbf24";
41
+ readonly seqLow: "#3f2d10";
42
+ readonly seqHigh: "#fbbf24";
43
+ readonly empty: "rgba(148,163,184,0.10)";
44
+ };
45
+ };
46
+ /** Linear interpolate two #rrggbb hexes; `t` in [0,1]. Returns #rrggbb. */
47
+ declare function lerpHex(a: string, b: string, t: number): string;
48
+ /** Pick a legible text colour (near-black / near-white) for a solid hex fill,
49
+ flipping by perceived luminance so cell/treemap labels keep contrast.
50
+
51
+ A fill this cannot read gets `currentColor` — inherit, i.e. whatever the
52
+ surrounding text already uses. That is the only answer that is right on both
53
+ themes, because an unreadable fill is usually a translucent one (`HEATMAP_HEX.*
54
+ .empty` is an `rgba()` string, and the variance heatmap does pass it here for any
55
+ category with zero assigned and positive activity — a refund). Picking a constant
56
+ instead is what produced the bug: `NaN > 0.6` is false, so the label came back
57
+ near-white on a near-white cell and the figure was invisible. */
58
+ declare function textOn(hex: string): string;
59
+
60
+ export { CHART_COLORS, HEATMAP_HEX, type HeatStops, PALETTE_HEX, lerpHex, paletteFor, textOn };
@@ -0,0 +1,79 @@
1
+ const PALETTE_SIZE = 9;
2
+ function paletteFor(index) {
3
+ return `var(--chart-${index % PALETTE_SIZE + 1})`;
4
+ }
5
+ const CHART_COLORS = {
6
+ income: "var(--money-income)",
7
+ expense: "var(--money-expense)",
8
+ net: "var(--money-net)",
9
+ assigned: "var(--brand)",
10
+ activity: "var(--money-expense)"
11
+ };
12
+ const PALETTE_HEX = {
13
+ light: ["#332288", "#88ccee", "#44aa99", "#117733", "#999933", "#ddcc77", "#cc6677", "#882255", "#aa4499"],
14
+ dark: ["#7e72d6", "#9fd8f2", "#5fc4b0", "#3fa45f", "#bfbf5e", "#e8dda0", "#e08c9a", "#c25a78", "#cc78be"]
15
+ };
16
+ const HEATMAP_HEX = {
17
+ light: {
18
+ neutral: "#f1f5f9",
19
+ // slate-100
20
+ under: "#0f766e",
21
+ // teal-700
22
+ over: "#b45309",
23
+ // amber-700
24
+ seqLow: "#fef3c7",
25
+ // amber-100
26
+ seqHigh: "#b45309",
27
+ // amber-700
28
+ empty: "rgba(148,163,184,0.14)"
29
+ },
30
+ dark: {
31
+ neutral: "#334155",
32
+ // slate-700
33
+ under: "#2dd4bf",
34
+ // teal-400
35
+ over: "#fbbf24",
36
+ // amber-400
37
+ seqLow: "#3f2d10",
38
+ // dim amber, sits just above slate-950
39
+ seqHigh: "#fbbf24",
40
+ // amber-400
41
+ empty: "rgba(148,163,184,0.10)"
42
+ }
43
+ };
44
+ function lerpHex(a, b, t) {
45
+ const pa = parseHex(a) ?? UNREADABLE;
46
+ const pb = parseHex(b) ?? UNREADABLE;
47
+ const c = (i) => Math.round(pa[i] + (pb[i] - pa[i]) * clamp01(t));
48
+ return `#${[c(0), c(1), c(2)].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
49
+ }
50
+ function textOn(hex) {
51
+ const rgb = parseHex(hex);
52
+ if (!rgb) return "currentColor";
53
+ const [r, g, b] = rgb;
54
+ const luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
55
+ return luma > 0.6 ? "#1a1a1a" : "#f8fafc";
56
+ }
57
+ const UNREADABLE = [128, 128, 128];
58
+ function parseHex(hex) {
59
+ const h = hex.startsWith("#") ? hex.slice(1) : hex;
60
+ const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
61
+ if (!/^[0-9a-fA-F]{6}$/.test(full)) return null;
62
+ return [
63
+ parseInt(full.slice(0, 2), 16),
64
+ parseInt(full.slice(2, 4), 16),
65
+ parseInt(full.slice(4, 6), 16)
66
+ ];
67
+ }
68
+ function clamp01(t) {
69
+ return t < 0 ? 0 : t > 1 ? 1 : t;
70
+ }
71
+ export {
72
+ CHART_COLORS,
73
+ HEATMAP_HEX,
74
+ PALETTE_HEX,
75
+ lerpHex,
76
+ paletteFor,
77
+ textOn
78
+ };
79
+ //# sourceMappingURL=chart-palette.js.map