@eifi1/ui-kit 0.6.1 → 0.7.1

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 (271) hide show
  1. package/README.md +69 -33
  2. package/dist/chart.d.ts +1 -0
  3. package/dist/components/account-settings.js +3 -2
  4. package/dist/components/account-settings.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +5 -0
  6. package/dist/components/amount-input.js +6 -4
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/autocomplete.d.ts +33 -6
  9. package/dist/components/autocomplete.js +62 -16
  10. package/dist/components/autocomplete.js.map +1 -1
  11. package/dist/components/calculator.d.ts +5 -0
  12. package/dist/components/calculator.js +2 -1
  13. package/dist/components/calculator.js.map +1 -1
  14. package/dist/components/chart-zoom.js +2 -1
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chart.d.ts +33 -2
  17. package/dist/components/chart.js +20 -8
  18. package/dist/components/chart.js.map +1 -1
  19. package/dist/components/chip.d.ts +34 -14
  20. package/dist/components/chip.js +63 -28
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +18 -1
  23. package/dist/components/choice-card.js +21 -8
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/combobox-core.d.ts +61 -2
  26. package/dist/components/combobox-core.js +68 -17
  27. package/dist/components/combobox-core.js.map +1 -1
  28. package/dist/components/combobox.d.ts +11 -7
  29. package/dist/components/combobox.js +51 -28
  30. package/dist/components/combobox.js.map +1 -1
  31. package/dist/components/currency-select.js +3 -3
  32. package/dist/components/currency-select.js.map +1 -1
  33. package/dist/components/danger-confirm.d.ts +30 -6
  34. package/dist/components/danger-confirm.js +51 -32
  35. package/dist/components/danger-confirm.js.map +1 -1
  36. package/dist/components/data-table-filter-popover.d.ts +1 -1
  37. package/dist/components/data-table-filters.d.ts +1 -1
  38. package/dist/components/data-table-pagination.d.ts +6 -3
  39. package/dist/components/data-table-pagination.js +6 -5
  40. package/dist/components/data-table-pagination.js.map +1 -1
  41. package/dist/components/data-table.d.ts +1 -1
  42. package/dist/components/data-table.js +332 -248
  43. package/dist/components/data-table.js.map +1 -1
  44. package/dist/components/date-picker.js +17 -7
  45. package/dist/components/date-picker.js.map +1 -1
  46. package/dist/components/dialog-frame.d.ts +28 -3
  47. package/dist/components/dialog-frame.js +47 -11
  48. package/dist/components/dialog-frame.js.map +1 -1
  49. package/dist/components/disclosure.d.ts +22 -2
  50. package/dist/components/disclosure.js +41 -8
  51. package/dist/components/disclosure.js.map +1 -1
  52. package/dist/components/dropdown.d.ts +7 -3
  53. package/dist/components/dropdown.js +6 -2
  54. package/dist/components/dropdown.js.map +1 -1
  55. package/dist/components/entity-combobox.d.ts +17 -8
  56. package/dist/components/entity-combobox.js +7 -5
  57. package/dist/components/entity-combobox.js.map +1 -1
  58. package/dist/components/file-button.d.ts +38 -5
  59. package/dist/components/file-button.js +23 -2
  60. package/dist/components/file-button.js.map +1 -1
  61. package/dist/components/file-dropzone.d.ts +11 -5
  62. package/dist/components/file-dropzone.js +14 -19
  63. package/dist/components/file-dropzone.js.map +1 -1
  64. package/dist/components/grouped-picker.js +2 -2
  65. package/dist/components/grouped-picker.js.map +1 -1
  66. package/dist/components/hover-menu.d.ts +8 -3
  67. package/dist/components/hover-menu.js +88 -4
  68. package/dist/components/hover-menu.js.map +1 -1
  69. package/dist/components/icon-picker.d.ts +10 -1
  70. package/dist/components/icon-picker.js +5 -1
  71. package/dist/components/icon-picker.js.map +1 -1
  72. package/dist/components/measured-grid.d.ts +165 -0
  73. package/dist/components/measured-grid.js +559 -0
  74. package/dist/components/measured-grid.js.map +1 -0
  75. package/dist/components/mini-calendar.js +7 -8
  76. package/dist/components/mini-calendar.js.map +1 -1
  77. package/dist/components/month-picker.js +13 -9
  78. package/dist/components/month-picker.js.map +1 -1
  79. package/dist/components/multi-entity-combobox.js +2 -2
  80. package/dist/components/multi-entity-combobox.js.map +1 -1
  81. package/dist/components/multi-select.d.ts +9 -1
  82. package/dist/components/multi-select.js +22 -8
  83. package/dist/components/multi-select.js.map +1 -1
  84. package/dist/components/number-field.d.ts +5 -0
  85. package/dist/components/number-input.d.ts +5 -0
  86. package/dist/components/number-input.js +3 -3
  87. package/dist/components/number-input.js.map +1 -1
  88. package/dist/components/numpad-sheet.d.ts +5 -0
  89. package/dist/components/numpad-sheet.js +3 -3
  90. package/dist/components/numpad-sheet.js.map +1 -1
  91. package/dist/components/picker-sheet.js +1 -1
  92. package/dist/components/picker-sheet.js.map +1 -1
  93. package/dist/components/popover.js +11 -2
  94. package/dist/components/popover.js.map +1 -1
  95. package/dist/components/series-chart-ticks.d.ts +13 -0
  96. package/dist/components/series-chart-ticks.js +29 -0
  97. package/dist/components/series-chart-ticks.js.map +1 -0
  98. package/dist/components/series-chart.js +33 -27
  99. package/dist/components/series-chart.js.map +1 -1
  100. package/dist/components/settings-fields.d.ts +2 -0
  101. package/dist/components/swatch-picker.d.ts +10 -1
  102. package/dist/components/swatch-picker.js +2 -0
  103. package/dist/components/swatch-picker.js.map +1 -1
  104. package/dist/components/swipeable-row.d.ts +11 -2
  105. package/dist/components/swipeable-row.js +5 -3
  106. package/dist/components/swipeable-row.js.map +1 -1
  107. package/dist/components/tile-radio.js +5 -7
  108. package/dist/components/tile-radio.js.map +1 -1
  109. package/dist/components/time-input.d.ts +2 -0
  110. package/dist/components/toggle-group.d.ts +13 -3
  111. package/dist/components/toggle-group.js +21 -1
  112. package/dist/components/toggle-group.js.map +1 -1
  113. package/dist/components/toggle-legend.js +1 -2
  114. package/dist/components/toggle-legend.js.map +1 -1
  115. package/dist/components/tooltip.d.ts +10 -4
  116. package/dist/components/tooltip.js +23 -6
  117. package/dist/components/tooltip.js.map +1 -1
  118. package/dist/components/treemap.d.ts +6 -1
  119. package/dist/components/treemap.js +16 -6
  120. package/dist/components/treemap.js.map +1 -1
  121. package/dist/components/ui.d.ts +36 -8
  122. package/dist/components/ui.js +27 -19
  123. package/dist/components/ui.js.map +1 -1
  124. package/dist/components/use-anchor-dir.d.ts +15 -0
  125. package/dist/components/use-anchor-dir.js +14 -0
  126. package/dist/components/use-anchor-dir.js.map +1 -0
  127. package/dist/components/use-mobile-reveal.d.ts +8 -1
  128. package/dist/components/use-mobile-reveal.js +3 -2
  129. package/dist/components/use-mobile-reveal.js.map +1 -1
  130. package/dist/components/use-table-state.d.ts +4 -4
  131. package/dist/components/use-table-state.js +12 -5
  132. package/dist/components/use-table-state.js.map +1 -1
  133. package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
  134. package/dist/data-table.d.ts +1 -1
  135. package/dist/data-table.js.map +1 -1
  136. package/dist/feedback/feedback-attachment.d.ts +22 -3
  137. package/dist/feedback/feedback-attachment.js +15 -7
  138. package/dist/feedback/feedback-attachment.js.map +1 -1
  139. package/dist/feedback/feedback-dialog.d.ts +3 -2
  140. package/dist/feedback/feedback-dialog.js.map +1 -1
  141. package/dist/feedback.d.ts +1 -1
  142. package/dist/hooks/use-windowed-rows.d.ts +29 -0
  143. package/dist/hooks/use-windowed-rows.js +31 -0
  144. package/dist/hooks/use-windowed-rows.js.map +1 -0
  145. package/dist/i18n/defaults.d.ts +5 -0
  146. package/dist/i18n/defaults.js +5 -1
  147. package/dist/i18n/defaults.js.map +1 -1
  148. package/dist/i18n/kit-labels.d.ts +7 -0
  149. package/dist/i18n/kit-labels.js.map +1 -1
  150. package/dist/i18n/locales/de-CH.d.ts +45 -0
  151. package/dist/i18n/locales/de-CH.js +21 -0
  152. package/dist/i18n/locales/de-CH.js.map +1 -0
  153. package/dist/i18n/locales/de.d.ts +45 -0
  154. package/dist/i18n/locales/de.js +344 -0
  155. package/dist/i18n/locales/de.js.map +1 -0
  156. package/dist/i18n/locales/es.d.ts +45 -0
  157. package/dist/i18n/locales/es.js +339 -0
  158. package/dist/i18n/locales/es.js.map +1 -0
  159. package/dist/i18n/locales/fr.d.ts +45 -0
  160. package/dist/i18n/locales/fr.js +344 -0
  161. package/dist/i18n/locales/fr.js.map +1 -0
  162. package/dist/i18n/locales/hu.d.ts +45 -0
  163. package/dist/i18n/locales/hu.js +343 -0
  164. package/dist/i18n/locales/hu.js.map +1 -0
  165. package/dist/i18n/locales/it.d.ts +45 -0
  166. package/dist/i18n/locales/it.js +342 -0
  167. package/dist/i18n/locales/it.js.map +1 -0
  168. package/dist/i18n/locales/zh.d.ts +45 -0
  169. package/dist/i18n/locales/zh.js +338 -0
  170. package/dist/i18n/locales/zh.js.map +1 -0
  171. package/dist/index.d.ts +7 -4
  172. package/dist/index.js +4 -1
  173. package/dist/index.js.map +1 -1
  174. package/dist/lib/direction.d.ts +24 -0
  175. package/dist/lib/direction.js +19 -0
  176. package/dist/lib/direction.js.map +1 -0
  177. package/dist/rhf/form.d.ts +2 -0
  178. package/dist/rhf/form.js.map +1 -1
  179. package/dist/rhf.d.ts +2 -0
  180. package/dist/search/command-palette.d.ts +5 -0
  181. package/dist/search/command-palette.js +80 -50
  182. package/dist/search/command-palette.js.map +1 -1
  183. package/dist/shell/app-shell.d.ts +13 -1
  184. package/dist/shell/app-shell.js +134 -85
  185. package/dist/shell/app-shell.js.map +1 -1
  186. package/dist/shell/topbar-action-menu.js +1 -1
  187. package/dist/shell/topbar-action-menu.js.map +1 -1
  188. package/dist/shell/topbar-controls.d.ts +7 -3
  189. package/dist/shell/topbar-controls.js +38 -31
  190. package/dist/shell/topbar-controls.js.map +1 -1
  191. package/dist/theme/chart-palette.d.ts +7 -1
  192. package/dist/theme/chart-palette.js +2 -1
  193. package/dist/theme/chart-palette.js.map +1 -1
  194. package/dist/tour/tour.d.ts +11 -5
  195. package/dist/tour/tour.js +124 -58
  196. package/dist/tour/tour.js.map +1 -1
  197. package/dist/wizard/use-wizard.js +30 -13
  198. package/dist/wizard/use-wizard.js.map +1 -1
  199. package/dist/wizard/wizard-summary.js +1 -1
  200. package/dist/wizard/wizard-summary.js.map +1 -1
  201. package/package.json +12 -2
  202. package/src/components/account-settings.tsx +6 -2
  203. package/src/components/amount-input.tsx +10 -8
  204. package/src/components/autocomplete.tsx +110 -24
  205. package/src/components/calculator.tsx +5 -1
  206. package/src/components/chart-zoom.tsx +8 -1
  207. package/src/components/chart.tsx +68 -7
  208. package/src/components/chip.tsx +131 -49
  209. package/src/components/choice-card.tsx +77 -36
  210. package/src/components/combobox-core.tsx +147 -20
  211. package/src/components/combobox.tsx +100 -44
  212. package/src/components/currency-select.tsx +6 -6
  213. package/src/components/danger-confirm.tsx +90 -36
  214. package/src/components/data-table-pagination.tsx +32 -19
  215. package/src/components/data-table.tsx +198 -36
  216. package/src/components/date-picker.tsx +24 -7
  217. package/src/components/dialog-frame.tsx +97 -22
  218. package/src/components/disclosure.tsx +75 -13
  219. package/src/components/dropdown.tsx +17 -5
  220. package/src/components/entity-combobox.tsx +26 -13
  221. package/src/components/file-button.tsx +74 -5
  222. package/src/components/file-dropzone.tsx +44 -35
  223. package/src/components/grouped-picker.tsx +2 -2
  224. package/src/components/hover-menu.tsx +139 -10
  225. package/src/components/icon-picker.tsx +14 -1
  226. package/src/components/measured-grid.tsx +852 -0
  227. package/src/components/mini-calendar.tsx +11 -8
  228. package/src/components/month-picker.tsx +18 -11
  229. package/src/components/multi-entity-combobox.tsx +2 -2
  230. package/src/components/multi-select.tsx +34 -7
  231. package/src/components/number-input.tsx +4 -4
  232. package/src/components/numpad-sheet.tsx +5 -3
  233. package/src/components/picker-sheet.tsx +1 -1
  234. package/src/components/popover.tsx +18 -3
  235. package/src/components/series-chart-ticks.ts +52 -0
  236. package/src/components/series-chart.tsx +39 -31
  237. package/src/components/swatch-picker.tsx +11 -0
  238. package/src/components/swipeable-row.tsx +21 -6
  239. package/src/components/tile-radio.tsx +5 -7
  240. package/src/components/toggle-group.tsx +39 -3
  241. package/src/components/toggle-legend.tsx +5 -2
  242. package/src/components/tooltip.tsx +40 -12
  243. package/src/components/treemap.tsx +28 -5
  244. package/src/components/ui.tsx +79 -33
  245. package/src/components/use-anchor-dir.ts +20 -0
  246. package/src/components/use-mobile-reveal.ts +10 -2
  247. package/src/components/use-table-state.ts +44 -7
  248. package/src/data-table.ts +1 -0
  249. package/src/feedback/feedback-attachment.tsx +36 -9
  250. package/src/feedback/feedback-dialog.tsx +3 -2
  251. package/src/hooks/use-windowed-rows.ts +69 -0
  252. package/src/i18n/defaults.ts +4 -0
  253. package/src/i18n/kit-labels.tsx +4 -0
  254. package/src/i18n/locales/de-CH.ts +30 -0
  255. package/src/i18n/locales/de.ts +365 -0
  256. package/src/i18n/locales/es.ts +358 -0
  257. package/src/i18n/locales/fr.ts +371 -0
  258. package/src/i18n/locales/hu.ts +359 -0
  259. package/src/i18n/locales/it.ts +360 -0
  260. package/src/i18n/locales/zh.ts +348 -0
  261. package/src/index.ts +10 -1
  262. package/src/lib/direction.ts +37 -0
  263. package/src/rhf/form.tsx +21 -0
  264. package/src/search/command-palette.tsx +77 -19
  265. package/src/shell/app-shell.tsx +201 -130
  266. package/src/shell/topbar-action-menu.tsx +1 -1
  267. package/src/shell/topbar-controls.tsx +19 -8
  268. package/src/theme/chart-palette.ts +9 -2
  269. package/src/tour/tour.tsx +119 -18
  270. package/src/wizard/use-wizard.ts +55 -17
  271. package/src/wizard/wizard-summary.tsx +1 -1
@@ -6,7 +6,10 @@ import { HoverMenu } from "../components/hover-menu.js";
6
6
  import { Tooltip } from "../components/tooltip.js";
7
7
  import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels.js";
8
8
  const TOPBAR_TRIGGER_CLASS = "size-9 rounded-md flex items-center justify-center text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]";
9
- const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-left text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
9
+ const TOPBAR_MENU_ITEM_CLASS = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
10
+ function MenuHeading({ children }) {
11
+ return /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]", children });
12
+ }
10
13
  function ThemeToggle({
11
14
  mode,
12
15
  onToggle,
@@ -52,7 +55,7 @@ function PaletteMenu({
52
55
  }
53
56
  ) }),
54
57
  children: (close) => /* @__PURE__ */ jsxs("ul", { className: "w-60 py-1", children: [
55
- heading && /* @__PURE__ */ jsx("li", { className: "px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]", children: heading }),
58
+ heading && /* @__PURE__ */ jsx(MenuHeading, { children: heading }),
56
59
  palettes.map((p) => {
57
60
  const ts = p[mode];
58
61
  const swatches = [ts.bgPage, ts.brand, ts.moneyIncome, ts.moneyExpense, ts.moneyNet];
@@ -75,7 +78,7 @@ function PaletteMenu({
75
78
  },
76
79
  i
77
80
  )) }),
78
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col text-left", children: [
81
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col text-start", children: [
79
82
  /* @__PURE__ */ jsx("span", { className: "truncate", children: p.name }),
80
83
  /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-[var(--text-placeholder)]", children: p.blurb })
81
84
  ] })
@@ -93,7 +96,8 @@ function LanguageMenu({
93
96
  options,
94
97
  current,
95
98
  onChange,
96
- ariaLabel
99
+ ariaLabel,
100
+ heading
97
101
  }) {
98
102
  const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });
99
103
  const active = options.find((o) => o.code === current);
@@ -117,33 +121,36 @@ function LanguageMenu({
117
121
  ) : /* @__PURE__ */ jsx(Globe, { className: "size-5" })
118
122
  }
119
123
  ) }),
120
- children: (close) => /* @__PURE__ */ jsx("ul", { className: "py-1", children: options.map((lang) => {
121
- const isActive = lang.code === current;
122
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
123
- "button",
124
- {
125
- type: "button",
126
- onClick: () => {
127
- onChange(lang.code);
128
- close();
129
- },
130
- className: TOPBAR_MENU_ITEM_CLASS,
131
- children: [
132
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
133
- /* @__PURE__ */ jsx(
134
- "span",
135
- {
136
- "aria-hidden": true,
137
- 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)]`
138
- }
139
- ),
140
- lang.label
141
- ] }),
142
- isActive && /* @__PURE__ */ jsx(Check, { className: "size-4 text-[var(--text-secondary)]" })
143
- ]
144
- }
145
- ) }, lang.code);
146
- }) })
124
+ children: (close) => /* @__PURE__ */ jsxs("ul", { className: "py-1", children: [
125
+ heading && /* @__PURE__ */ jsx(MenuHeading, { children: heading }),
126
+ options.map((lang) => {
127
+ const isActive = lang.code === current;
128
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
129
+ "button",
130
+ {
131
+ type: "button",
132
+ onClick: () => {
133
+ onChange(lang.code);
134
+ close();
135
+ },
136
+ className: TOPBAR_MENU_ITEM_CLASS,
137
+ children: [
138
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
139
+ /* @__PURE__ */ jsx(
140
+ "span",
141
+ {
142
+ "aria-hidden": true,
143
+ 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)]`
144
+ }
145
+ ),
146
+ lang.label
147
+ ] }),
148
+ isActive && /* @__PURE__ */ jsx(Check, { className: "size-4 text-[var(--text-secondary)]" })
149
+ ]
150
+ }
151
+ ) }, lang.code);
152
+ })
153
+ ] })
147
154
  }
148
155
  );
149
156
  }
@@ -1 +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\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\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-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\";\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-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\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 /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n title?: string;\n className?: string;\n}) {\n // This had no default at all, so a toggle rendered without `ariaLabel` was an\n // icon button with no name — a reader heard \"button\" and nothing else.\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });\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={labels.theme}\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,\n heading,\n}: {\n palettes: PalettePreset[];\n activeId: string;\n mode: ThemeMode;\n onSelect: (id: string) => void;\n /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n /** Optional small heading row above the list. */\n heading?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });\n const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;\n return (\n <HoverMenu\n ariaLabel={labels.palette}\n trigger={({ toggle }) => (\n <Tooltip label={activeName} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={labels.palette}\n className={TOPBAR_TRIGGER_CLASS}\n >\n {/* A DATA colour, not a semantic one: the palette mark is the\n \"colour\" control, so it carries a colour, and which hue is\n arbitrary. `--chart-1` is the violet end of the CVD-safe\n categorical ramp and flips per theme — a fixed `violet-500` did\n not, and stayed a cool violet island on a warm page. */}\n <Palette className=\"size-5 text-[var(--chart-1)]\" />\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-[var(--text-placeholder)]\">\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 {/* Deliberately a neutral at 10% and not a token: this hairline\n sits on an ARBITRARY preset colour, so it has to read against\n whatever that preset paints, which no themed border does. */}\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-[var(--text-placeholder)]\">\n {p.blurb}\n </span>\n </span>\n </span>\n {activeId === p.id && (\n <Check className=\"size-4 shrink-0 text-[var(--text-secondary)]\" />\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,\n}: {\n options: LanguageOption[];\n current: string | undefined;\n onChange: (code: string) => void;\n /** Default: `topBar.language` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });\n const active = options.find((o) => o.code === current);\n return (\n <HoverMenu\n ariaLabel={labels.language}\n trigger={({ toggle }) => (\n <Tooltip label={active?.label ?? labels.language} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={labels.language}\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-[var(--text-secondary)]\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":[]}
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\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\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-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]\";\n\n/** Shared row styling for the top-bar hover-menu items. `text-start`, so a label that\n * wraps aligns to the reading edge in RTL too. */\nexport const TOPBAR_MENU_ITEM_CLASS =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\n\n/** The small uppercase heading row the top-bar menus share (`heading`). */\nfunction MenuHeading({ children }: { children: string }) {\n return (\n <li className=\"px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\">\n {children}\n </li>\n );\n}\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 /** Default: `topBar.theme` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n title?: string;\n className?: string;\n}) {\n // This had no default at all, so a toggle rendered without `ariaLabel` was an\n // icon button with no name — a reader heard \"button\" and nothing else.\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });\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={labels.theme}\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,\n heading,\n}: {\n palettes: PalettePreset[];\n activeId: string;\n mode: ThemeMode;\n onSelect: (id: string) => void;\n /** Default: `topBar.palette` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n /** Optional small heading row above the list. */\n heading?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { palette: ariaLabel });\n const activeName = palettes.find((p) => p.id === activeId)?.name ?? activeId;\n return (\n <HoverMenu\n ariaLabel={labels.palette}\n trigger={({ toggle }) => (\n <Tooltip label={activeName} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={labels.palette}\n className={TOPBAR_TRIGGER_CLASS}\n >\n {/* A DATA colour, not a semantic one: the palette mark is the\n \"colour\" control, so it carries a colour, and which hue is\n arbitrary. `--chart-1` is the violet end of the CVD-safe\n categorical ramp and flips per theme — a fixed `violet-500` did\n not, and stayed a cool violet island on a warm page. */}\n <Palette className=\"size-5 text-[var(--chart-1)]\" />\n </button>\n </Tooltip>\n )}\n >\n {(close) => (\n <ul className=\"w-60 py-1\">\n {heading && <MenuHeading>{heading}</MenuHeading>}\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 {/* Deliberately a neutral at 10% and not a token: this hairline\n sits on an ARBITRARY preset colour, so it has to read against\n whatever that preset paints, which no themed border does. */}\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-start\">\n <span className=\"truncate\">{p.name}</span>\n <span className=\"truncate text-[11px] text-[var(--text-placeholder)]\">\n {p.blurb}\n </span>\n </span>\n </span>\n {activeId === p.id && (\n <Check className=\"size-4 shrink-0 text-[var(--text-secondary)]\" />\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,\n heading,\n}: {\n options: LanguageOption[];\n current: string | undefined;\n onChange: (code: string) => void;\n /** Default: `topBar.language` from the {@link UiKitProvider}, else English. */\n ariaLabel?: string;\n /** Optional small heading row above the list — as on `TopBarActionMenu`,\n * `OptionSwitcherMenu` and `PaletteMenu`. */\n heading?: string;\n}) {\n const labels = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });\n const active = options.find((o) => o.code === current);\n return (\n <HoverMenu\n ariaLabel={labels.language}\n trigger={({ toggle }) => (\n <Tooltip label={active?.label ?? labels.language} portal>\n <button\n type=\"button\"\n onClick={toggle}\n aria-label={labels.language}\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 {heading && <MenuHeading>{heading}</MenuHeading>}\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-[var(--text-secondary)]\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </HoverMenu>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,10 @@
1
- /** CVD-safe categorical colour for series `index`, as a theme-aware CSS var. */
1
+ /** CVD-safe categorical colour for series `index`, as a theme-aware CSS var.
2
+ *
3
+ * Wraps in both directions, the way `strokeDash` does: JS `%` keeps the sign,
4
+ * so `-1 % 9` is `-1` and the plain formula named `--chart-0`, a token that does not
5
+ * exist — a series with no stroke at all. A fractional index is truncated, and one
6
+ * that is not a number (NaN, ±Infinity — an `indexOf` miss divided by something)
7
+ * takes the first colour rather than `var(--chart-NaN)`. */
2
8
  declare function paletteFor(index: number): string;
3
9
  /** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow
4
10
  cues at the call site so colour is never the only signal (CVD). */
@@ -2,7 +2,8 @@
2
2
  import { contrast, parseHex as parseRgbHex } from "./color.js";
3
3
  const PALETTE_SIZE = 9;
4
4
  function paletteFor(index) {
5
- return `var(--chart-${index % PALETTE_SIZE + 1})`;
5
+ const i = Number.isFinite(index) ? Math.trunc(index) : 0;
6
+ return `var(--chart-${(i % PALETTE_SIZE + PALETTE_SIZE) % PALETTE_SIZE + 1})`;
6
7
  }
7
8
  const CHART_COLORS = {
8
9
  income: "var(--money-income)",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/theme/chart-palette.ts"],"sourcesContent":["// Chart colour system. Single source of truth lives in the design tokens\n// (:root / .dark in tokens.css): brand = indigo, money = teal/amber/purple,\n// chart-1..9 = Paul Tol \"Muted\" (CVD-safe across protan/deutan/tritan).\n// Returning CSS vars here makes every Recharts series theme-aware for free — the\n// SVG fill/stroke resolves --chart-N against the current light/dark token set.\n\nimport { contrast, parseHex as parseRgbHex } from \"./color\";\n\nconst PALETTE_SIZE = 9;\n\n/** CVD-safe categorical colour for series `index`, as a theme-aware CSS var. */\nexport function paletteFor(index: number): string {\n return `var(--chart-${(index % PALETTE_SIZE) + 1})`;\n}\n\n/** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow\n cues at the call site so colour is never the only signal (CVD). */\nexport const CHART_COLORS = {\n income: \"var(--money-income)\",\n expense: \"var(--money-expense)\",\n net: \"var(--money-net)\",\n assigned: \"var(--brand)\",\n activity: \"var(--money-expense)\",\n} as const;\n\n// ── Raw hex stops, by theme ────────────────────────────────────────────────\n// The heatmaps interpolate colours in JS (CSS vars can't be lerped), so they\n// need concrete hex per theme — i.e. the same colours DEFAULT_PRESET already\n// holds, written down a second time.\n//\n// They are duplicated rather than imported because these two objects are the whole\n// of the `@eifi1/ui-kit/chart` subpath's colour layer, and importing the preset bank\n// would pull every preset's full token set into that chunk to read two of them. What\n// keeps the copies together is `chart-palette.test.ts` — \"has not drifted from\n// DEFAULT_PRESET\" — which is not optional: they HAD drifted, five of the six light\n// heat stops, and the light `neutral` was still Tailwind's slate-100, a cool grey on\n// the warm cream page this theme has used since #328.\nexport const PALETTE_HEX = {\n light: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n dark: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n} as const;\n\n/** A heatmap's interpolation stops for one theme. Open string fields so any\n palette preset (not just the built-in default) can supply its own stops. */\nexport interface HeatStops {\n neutral: string;\n under: string;\n over: string;\n seqLow: string;\n seqHigh: string;\n empty: string;\n}\n\n/** Diverging (variance: under←0→over) and sequential (spending intensity)\n heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */\nexport const HEATMAP_HEX = {\n light: {\n neutral: \"#f1e7d2\", // the cream surface itself, not a cool grey\n under: \"#0b6650\", // DEFAULT_PRESET.light.moneyIncome\n over: \"#9c6418\", // DEFAULT_PRESET.light.moneyExpense, warmed to the page\n seqLow: \"#f6ecd0\",\n seqHigh: \"#9c6418\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#363a5a\", // one step above --border on the indigo-noir surface\n under: \"#22c3b6\", // DEFAULT_PRESET.dark.moneyIncome\n // Brighter than dark.moneyExpense (#e4b035) on purpose: a gradient endpoint is\n // meant to be the loudest thing in its scale, and it is a fill, not a figure.\n over: \"#fbbf24\",\n seqLow: \"#3a2c14\", // dim amber, sits just above the page\n seqHigh: \"#fbbf24\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n} as const;\n\n/** Linear interpolate two #rrggbb hexes; `t` in [0,1]. Returns #rrggbb. */\nexport function lerpHex(a: string, b: string, t: number): string {\n const pa = parseHex(a) ?? UNREADABLE;\n const pb = parseHex(b) ?? UNREADABLE;\n const c = (i: number) => Math.round(pa[i] + (pb[i] - pa[i]) * clamp01(t));\n return `#${[c(0), c(1), c(2)].map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** The two inks a cell/treemap label can be set in. Near-black and near-white rather\n than #000/#fff: at full black-on-full-white the label stops looking like part of the\n same type system as the rest of the page. */\nconst INK_DARK = \"#1a1a1a\";\nconst INK_LIGHT = \"#f8fafc\";\n\n/** Pick a legible text colour (near-black / near-white) for a solid hex fill, by\n MEASURING both against it and keeping the one that wins.\n\n This used to flip on Rec.601 luma > 0.6, which is a brightness heuristic and not a\n contrast measure — the two disagree on six of the eighteen default chart hues, a\n third of the ramp, and on each of those the label got the worse of the two inks.\n `#44aa99` is the plainest case: luma calls it dark, so it took white, which measures\n 2.69:1 on it; near-black measures 6.18:1. keksdose measured this too and forked the\n function locally for its treemap, leaving its other two call sites on this one.\n\n A fill this cannot read gets `currentColor` — inherit, i.e. whatever the\n surrounding text already uses. That is the only answer that is right on both\n themes, because an unreadable fill is usually a translucent one (`HEATMAP_HEX.*\n .empty` is an `rgba()` string, and the variance heatmap does pass it here for any\n category with zero assigned and positive activity — a refund). Picking a constant\n instead is what produced the earlier bug: the unparsed fill fell through to the\n light ink, near-white on a near-white cell, and the figure was invisible. */\nexport function textOn(hex: string): string {\n const rgb = parseRgbHex(hex);\n if (!rgb) return \"currentColor\";\n // Ties go to the dark ink: at equal measured contrast it is the one that keeps a\n // small label looking like type rather than like a glow.\n return contrast(rgb, INK_DARK) >= contrast(rgb, INK_LIGHT) ? INK_DARK : INK_LIGHT;\n}\n\n/** Mid-grey, for a stop {@link lerpHex} has to interpolate but cannot read. It has\n * to return SOME valid colour, and a grey is the one that misleads least. */\nconst UNREADABLE: [number, number, number] = [0x80, 0x80, 0x80];\n\n/**\n * `#rgb` or `#rrggbb` to components, or **null** when it is neither.\n *\n * A tuple-shaped shim over `color.ts`'s parser, which is the same function and is\n * already the one `contrast` validates with — so {@link lerpHex} and {@link textOn}\n * cannot disagree about what counts as a colour.\n *\n * The **null** is the whole point of it. This used to `parseInt` whatever it was given\n * and hand the NaNs on, and both callers propagated them silently: {@link lerpHex}\n * produced the literal string `\"#nannannan\"`, and {@link textOn} fell through to the\n * LIGHT label colour.\n *\n * That is not hypothetical — a non-hex value ships in this very module.\n * `HEATMAP_HEX.*.empty` is an `rgba(…)` string, and the variance heatmap paints a\n * figure on an `empty` cell whenever a category has zero assigned and positive\n * activity (a refund). On the light theme the cell is near-white and the label came\n * back near-white: the number was there, and unreadable.\n */\nfunction parseHex(hex: string): [number, number, number] | null {\n const rgb = parseRgbHex(hex);\n return rgb ? [rgb.r, rgb.g, rgb.b] : null;\n}\n\nfunction clamp01(t: number): number {\n return t < 0 ? 0 : t > 1 ? 1 : t;\n}\n"],"mappings":";AAMA,SAAS,UAAU,YAAY,mBAAmB;AAElD,MAAM,eAAe;AAGd,SAAS,WAAW,OAAuB;AAChD,SAAO,eAAgB,QAAQ,eAAgB,CAAC;AAClD;AAIO,MAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAcO,MAAM,cAAc;AAAA,EACzB,OAAO,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,EACzG,MAAM,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAC1G;AAeO,MAAM,cAAc;AAAA,EACzB,OAAO;AAAA,IACL,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA,IACP,MAAM;AAAA;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA;AAAA;AAAA,IAGP,MAAM;AAAA,IACN,QAAQ;AAAA;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AACF;AAGO,SAAS,QAAQ,GAAW,GAAW,GAAmB;AAC/D,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,IAAI,CAAC,MAAc,KAAK,MAAM,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC;AACxE,SAAO,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACpF;AAKA,MAAM,WAAW;AACjB,MAAM,YAAY;AAmBX,SAAS,OAAO,KAAqB;AAC1C,QAAM,MAAM,YAAY,GAAG;AAC3B,MAAI,CAAC,IAAK,QAAO;AAGjB,SAAO,SAAS,KAAK,QAAQ,KAAK,SAAS,KAAK,SAAS,IAAI,WAAW;AAC1E;AAIA,MAAM,aAAuC,CAAC,KAAM,KAAM,GAAI;AAoB9D,SAAS,SAAS,KAA8C;AAC9D,QAAM,MAAM,YAAY,GAAG;AAC3B,SAAO,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI;AACvC;AAEA,SAAS,QAAQ,GAAmB;AAClC,SAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AACjC;","names":[]}
1
+ {"version":3,"sources":["../../src/theme/chart-palette.ts"],"sourcesContent":["// Chart colour system. Single source of truth lives in the design tokens\n// (:root / .dark in tokens.css): brand = indigo, money = teal/amber/purple,\n// chart-1..9 = Paul Tol \"Muted\" (CVD-safe across protan/deutan/tritan).\n// Returning CSS vars here makes every Recharts series theme-aware for free — the\n// SVG fill/stroke resolves --chart-N against the current light/dark token set.\n\nimport { contrast, parseHex as parseRgbHex } from \"./color\";\n\nconst PALETTE_SIZE = 9;\n\n/** CVD-safe categorical colour for series `index`, as a theme-aware CSS var.\n *\n * Wraps in both directions, the way `strokeDash` does: JS `%` keeps the sign,\n * so `-1 % 9` is `-1` and the plain formula named `--chart-0`, a token that does not\n * exist — a series with no stroke at all. A fractional index is truncated, and one\n * that is not a number (NaN, ±Infinity — an `indexOf` miss divided by something)\n * takes the first colour rather than `var(--chart-NaN)`. */\nexport function paletteFor(index: number): string {\n const i = Number.isFinite(index) ? Math.trunc(index) : 0;\n return `var(--chart-${(((i % PALETTE_SIZE) + PALETTE_SIZE) % PALETTE_SIZE) + 1})`;\n}\n\n/** Semantic money colours, theme-aware via tokens. Pair with +/- and arrow\n cues at the call site so colour is never the only signal (CVD). */\nexport const CHART_COLORS = {\n income: \"var(--money-income)\",\n expense: \"var(--money-expense)\",\n net: \"var(--money-net)\",\n assigned: \"var(--brand)\",\n activity: \"var(--money-expense)\",\n} as const;\n\n// ── Raw hex stops, by theme ────────────────────────────────────────────────\n// The heatmaps interpolate colours in JS (CSS vars can't be lerped), so they\n// need concrete hex per theme — i.e. the same colours DEFAULT_PRESET already\n// holds, written down a second time.\n//\n// They are duplicated rather than imported because these two objects are the whole\n// of the `@eifi1/ui-kit/chart` subpath's colour layer, and importing the preset bank\n// would pull every preset's full token set into that chunk to read two of them. What\n// keeps the copies together is `chart-palette.test.ts` — \"has not drifted from\n// DEFAULT_PRESET\" — which is not optional: they HAD drifted, five of the six light\n// heat stops, and the light `neutral` was still Tailwind's slate-100, a cool grey on\n// the warm cream page this theme has used since #328.\nexport const PALETTE_HEX = {\n light: [\"#332288\", \"#88ccee\", \"#44aa99\", \"#117733\", \"#999933\", \"#ddcc77\", \"#cc6677\", \"#882255\", \"#aa4499\"],\n dark: [\"#7e72d6\", \"#9fd8f2\", \"#5fc4b0\", \"#3fa45f\", \"#bfbf5e\", \"#e8dda0\", \"#e08c9a\", \"#c25a78\", \"#cc78be\"],\n} as const;\n\n/** A heatmap's interpolation stops for one theme. Open string fields so any\n palette preset (not just the built-in default) can supply its own stops. */\nexport interface HeatStops {\n neutral: string;\n under: string;\n over: string;\n seqLow: string;\n seqHigh: string;\n empty: string;\n}\n\n/** Diverging (variance: under←0→over) and sequential (spending intensity)\n heatmap stops, per theme. Teal = under/good, amber = over/spend = bad. */\nexport const HEATMAP_HEX = {\n light: {\n neutral: \"#f1e7d2\", // the cream surface itself, not a cool grey\n under: \"#0b6650\", // DEFAULT_PRESET.light.moneyIncome\n over: \"#9c6418\", // DEFAULT_PRESET.light.moneyExpense, warmed to the page\n seqLow: \"#f6ecd0\",\n seqHigh: \"#9c6418\",\n empty: \"rgba(148,163,184,0.14)\",\n },\n dark: {\n neutral: \"#363a5a\", // one step above --border on the indigo-noir surface\n under: \"#22c3b6\", // DEFAULT_PRESET.dark.moneyIncome\n // Brighter than dark.moneyExpense (#e4b035) on purpose: a gradient endpoint is\n // meant to be the loudest thing in its scale, and it is a fill, not a figure.\n over: \"#fbbf24\",\n seqLow: \"#3a2c14\", // dim amber, sits just above the page\n seqHigh: \"#fbbf24\",\n empty: \"rgba(148,163,184,0.10)\",\n },\n} as const;\n\n/** Linear interpolate two #rrggbb hexes; `t` in [0,1]. Returns #rrggbb. */\nexport function lerpHex(a: string, b: string, t: number): string {\n const pa = parseHex(a) ?? UNREADABLE;\n const pb = parseHex(b) ?? UNREADABLE;\n const c = (i: number) => Math.round(pa[i] + (pb[i] - pa[i]) * clamp01(t));\n return `#${[c(0), c(1), c(2)].map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** The two inks a cell/treemap label can be set in. Near-black and near-white rather\n than #000/#fff: at full black-on-full-white the label stops looking like part of the\n same type system as the rest of the page. */\nconst INK_DARK = \"#1a1a1a\";\nconst INK_LIGHT = \"#f8fafc\";\n\n/** Pick a legible text colour (near-black / near-white) for a solid hex fill, by\n MEASURING both against it and keeping the one that wins.\n\n This used to flip on Rec.601 luma > 0.6, which is a brightness heuristic and not a\n contrast measure — the two disagree on six of the eighteen default chart hues, a\n third of the ramp, and on each of those the label got the worse of the two inks.\n `#44aa99` is the plainest case: luma calls it dark, so it took white, which measures\n 2.69:1 on it; near-black measures 6.18:1. keksdose measured this too and forked the\n function locally for its treemap, leaving its other two call sites on this one.\n\n A fill this cannot read gets `currentColor` — inherit, i.e. whatever the\n surrounding text already uses. That is the only answer that is right on both\n themes, because an unreadable fill is usually a translucent one (`HEATMAP_HEX.*\n .empty` is an `rgba()` string, and the variance heatmap does pass it here for any\n category with zero assigned and positive activity — a refund). Picking a constant\n instead is what produced the earlier bug: the unparsed fill fell through to the\n light ink, near-white on a near-white cell, and the figure was invisible. */\nexport function textOn(hex: string): string {\n const rgb = parseRgbHex(hex);\n if (!rgb) return \"currentColor\";\n // Ties go to the dark ink: at equal measured contrast it is the one that keeps a\n // small label looking like type rather than like a glow.\n return contrast(rgb, INK_DARK) >= contrast(rgb, INK_LIGHT) ? INK_DARK : INK_LIGHT;\n}\n\n/** Mid-grey, for a stop {@link lerpHex} has to interpolate but cannot read. It has\n * to return SOME valid colour, and a grey is the one that misleads least. */\nconst UNREADABLE: [number, number, number] = [0x80, 0x80, 0x80];\n\n/**\n * `#rgb` or `#rrggbb` to components, or **null** when it is neither.\n *\n * A tuple-shaped shim over `color.ts`'s parser, which is the same function and is\n * already the one `contrast` validates with — so {@link lerpHex} and {@link textOn}\n * cannot disagree about what counts as a colour.\n *\n * The **null** is the whole point of it. This used to `parseInt` whatever it was given\n * and hand the NaNs on, and both callers propagated them silently: {@link lerpHex}\n * produced the literal string `\"#nannannan\"`, and {@link textOn} fell through to the\n * LIGHT label colour.\n *\n * That is not hypothetical — a non-hex value ships in this very module.\n * `HEATMAP_HEX.*.empty` is an `rgba(…)` string, and the variance heatmap paints a\n * figure on an `empty` cell whenever a category has zero assigned and positive\n * activity (a refund). On the light theme the cell is near-white and the label came\n * back near-white: the number was there, and unreadable.\n */\nfunction parseHex(hex: string): [number, number, number] | null {\n const rgb = parseRgbHex(hex);\n return rgb ? [rgb.r, rgb.g, rgb.b] : null;\n}\n\nfunction clamp01(t: number): number {\n return t < 0 ? 0 : t > 1 ? 1 : t;\n}\n"],"mappings":";AAMA,SAAS,UAAU,YAAY,mBAAmB;AAElD,MAAM,eAAe;AASd,SAAS,WAAW,OAAuB;AAChD,QAAM,IAAI,OAAO,SAAS,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI;AACvD,SAAO,gBAAkB,IAAI,eAAgB,gBAAgB,eAAgB,CAAC;AAChF;AAIO,MAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAcO,MAAM,cAAc;AAAA,EACzB,OAAO,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,EACzG,MAAM,CAAC,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAC1G;AAeO,MAAM,cAAc;AAAA,EACzB,OAAO;AAAA,IACL,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA,IACP,MAAM;AAAA;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA;AAAA,IACT,OAAO;AAAA;AAAA;AAAA;AAAA,IAGP,MAAM;AAAA,IACN,QAAQ;AAAA;AAAA,IACR,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AACF;AAGO,SAAS,QAAQ,GAAW,GAAW,GAAmB;AAC/D,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,KAAK,SAAS,CAAC,KAAK;AAC1B,QAAM,IAAI,CAAC,MAAc,KAAK,MAAM,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC;AACxE,SAAO,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACpF;AAKA,MAAM,WAAW;AACjB,MAAM,YAAY;AAmBX,SAAS,OAAO,KAAqB;AAC1C,QAAM,MAAM,YAAY,GAAG;AAC3B,MAAI,CAAC,IAAK,QAAO;AAGjB,SAAO,SAAS,KAAK,QAAQ,KAAK,SAAS,KAAK,SAAS,IAAI,WAAW;AAC1E;AAIA,MAAM,aAAuC,CAAC,KAAM,KAAM,GAAI;AAoB9D,SAAS,SAAS,KAA8C;AAC9D,QAAM,MAAM,YAAY,GAAG;AAC3B,SAAO,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI;AACvC;AAEA,SAAS,QAAQ,GAAmB;AAClC,SAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;AACjC;","names":[]}
@@ -1,7 +1,12 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
- type TourPlacement = "top" | "right" | "bottom" | "left" | "center";
4
+ /**
5
+ * `start`/`end` follow the reading direction (`end` is the right side in LTR and the
6
+ * left side in RTL) and are what a translated app wants. `left`/`right` stay physical
7
+ * for the steps that genuinely mean a screen side.
8
+ */
9
+ type TourPlacement = "top" | "right" | "bottom" | "left" | "start" | "end" | "center";
5
10
  interface TourStep {
6
11
  /** CSS selector for the element to spotlight. Omit for a centered step
7
12
  * (welcome/finish). If the target isn't found/visible, the step centers. */
@@ -14,13 +19,14 @@ interface TourStep {
14
19
  padding?: number;
15
20
  /** Where the card sits relative to the target. Omit for auto placement
16
21
  * (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. */
22
+ * screen. "start"/"end" are the logical sides and flip in RTL; "left"/"right"
23
+ * are physical. An explicit side flips to the opposite side if it would
24
+ * overflow, then clamps into the viewport. */
19
25
  placement?: TourPlacement;
20
26
  /** Advance only when the user clicks the spotlighted target itself (not the
21
27
  * 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. */
28
+ * first, then the tour steps forward on a microtask. The spotlight hole stays
29
+ * click-through to the target; the rest of the page does not. */
24
30
  awaitClick?: boolean;
25
31
  /** Runs before the step is shown — e.g. navigate to a route, open a panel.
26
32
  * May be async; the step waits for it, then locates the target. */
package/dist/tour/tour.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import {
4
4
  createContext,
5
5
  useCallback,
@@ -14,6 +14,7 @@ import { cn } from "../lib/cn.js";
14
14
  import { useKitLabels } from "../i18n/kit-labels.js";
15
15
  import { Button } from "../components/ui.js";
16
16
  import { useFocusTrap } from "../hooks/use-focus-trap.js";
17
+ import { dirOf, horizontalStep } from "../lib/direction.js";
17
18
  const DEFAULT_TOUR_LABELS = {
18
19
  next: "Next",
19
20
  back: "Back",
@@ -127,6 +128,8 @@ function TourOverlay({
127
128
  const isLast = index === steps.length - 1;
128
129
  const [rect, setRect] = useState(null);
129
130
  const [ready, setReady] = useState(false);
131
+ const [dir, setDir] = useState(() => dirOf(null));
132
+ const rootRef = useRef(null);
130
133
  useEffect(() => {
131
134
  let cancelled = false;
132
135
  let raf = 0;
@@ -136,6 +139,7 @@ function TourOverlay({
136
139
  await step.beforeStep?.();
137
140
  if (cancelled) return;
138
141
  if (!step.target) {
142
+ setDir(dirOf(null));
139
143
  setReady(true);
140
144
  return;
141
145
  }
@@ -147,11 +151,15 @@ function TourOverlay({
147
151
  el.scrollIntoView({ block: "center", behavior: "smooth" });
148
152
  const r = el.getBoundingClientRect();
149
153
  setRect({ top: r.top, left: r.left, width: r.width, height: r.height });
154
+ setDir(dirOf(el));
150
155
  setReady(true);
151
156
  return;
152
157
  }
153
158
  if (tries++ < 45) raf = requestAnimationFrame(find);
154
- else setReady(true);
159
+ else {
160
+ setDir(dirOf(null));
161
+ setReady(true);
162
+ }
155
163
  };
156
164
  find();
157
165
  })();
@@ -191,14 +199,20 @@ function TourOverlay({
191
199
  const onKey = (e) => {
192
200
  if (e.key === "Escape") {
193
201
  onSkip();
194
- } else if (e.key === "ArrowRight") {
202
+ return;
203
+ }
204
+ if (e.key === "Enter") {
205
+ if (step.awaitClick || ownsKey(e.target, "Enter")) return;
195
206
  e.preventDefault();
196
207
  onNext();
197
- } else if (e.key === "Enter") {
198
- if (step.awaitClick) return;
208
+ return;
209
+ }
210
+ if (ownsKey(e.target, e.key)) return;
211
+ const delta = horizontalStep(e.key, rootRef.current);
212
+ if (delta === 1) {
199
213
  e.preventDefault();
200
214
  onNext();
201
- } else if (e.key === "ArrowLeft") {
215
+ } else if (delta === -1) {
202
216
  if (!isFirst) onPrev();
203
217
  }
204
218
  };
@@ -209,59 +223,110 @@ function TourOverlay({
209
223
  const pad = step.padding ?? 8;
210
224
  const spot = rect ? { top: rect.top - pad, left: rect.left - pad, width: rect.width + pad * 2, height: rect.height + pad * 2 } : null;
211
225
  return createPortal(
212
- /* @__PURE__ */ jsxs("div", { className: "pointer-events-none fixed inset-0 z-[60]", role: "dialog", "aria-modal": "true", "aria-label": step.title, children: [
213
- spot ? /* @__PURE__ */ jsx(
214
- "div",
215
- {
216
- className: "pointer-events-none fixed rounded-lg transition-all duration-200 ease-out",
217
- style: {
218
- top: spot.top,
219
- left: spot.left,
220
- width: spot.width,
221
- height: spot.height,
222
- // A two-tone frame, not a single translucent ring.
223
- //
224
- // The old treatment was `ring-2 ring-white/80` over a 55% scrim, and it
225
- // "was barely visible" on the app's blue-tinted surfaces. Two reasons,
226
- // and a single ring can't fix either: at 55% the surround stays bright
227
- // enough that the un-dimmed hole barely reads AS a hole, and a 80%-white
228
- // ring sitting on the edge of a light card is white-on-near-white.
229
- //
230
- // So: an opaque white band immediately around the target, a dark keyline
231
- // outside it, then the scrim. Whatever the target's own colour is, one of
232
- // the two bands is always in contrast with it and the other is always in
233
- // contrast with the dimmed surround — there is no background that can
234
- // swallow both. The blurred layer between them softens the step so the
235
- // frame reads as lighting rather than as a pasted-on rectangle.
236
- boxShadow: [
237
- "0 0 0 3px rgba(255,255,255,0.98)",
238
- "0 0 0 5px rgba(15,23,42,0.92)",
239
- "0 0 18px 6px rgba(15,23,42,0.45)",
240
- "0 0 0 9999px rgba(15,23,42,0.70)"
241
- ].join(", ")
242
- }
243
- }
244
- ) : /* @__PURE__ */ jsx("div", { className: "pointer-events-auto fixed inset-0 bg-black/70" }),
245
- /* @__PURE__ */ jsx(
246
- TourCard,
247
- {
248
- step,
249
- index,
250
- total: steps.length,
251
- labels,
252
- isFirst,
253
- isLast,
254
- rect: spot,
255
- onBack: onPrev,
256
- onNext,
257
- onSkip
258
- }
259
- )
260
- ] }),
226
+ /* @__PURE__ */ jsxs(
227
+ "div",
228
+ {
229
+ ref: rootRef,
230
+ className: "pointer-events-none fixed inset-0 z-[60]",
231
+ role: "dialog",
232
+ "aria-modal": "true",
233
+ "aria-label": step.title,
234
+ dir,
235
+ children: [
236
+ spot && /* @__PURE__ */ jsx(SpotlightShields, { spot }),
237
+ spot ? /* @__PURE__ */ jsx(
238
+ "div",
239
+ {
240
+ className: "pointer-events-none fixed rounded-lg transition-all duration-200 ease-out",
241
+ style: {
242
+ top: spot.top,
243
+ left: spot.left,
244
+ width: spot.width,
245
+ height: spot.height,
246
+ // A two-tone frame, not a single translucent ring.
247
+ //
248
+ // The old treatment was `ring-2 ring-white/80` over a 55% scrim, and it
249
+ // "was barely visible" on the app's blue-tinted surfaces. Two reasons,
250
+ // and a single ring can't fix either: at 55% the surround stays bright
251
+ // enough that the un-dimmed hole barely reads AS a hole, and a 80%-white
252
+ // ring sitting on the edge of a light card is white-on-near-white.
253
+ //
254
+ // So: an opaque white band immediately around the target, a dark keyline
255
+ // outside it, then the scrim. Whatever the target's own colour is, one of
256
+ // the two bands is always in contrast with it and the other is always in
257
+ // contrast with the dimmed surround — there is no background that can
258
+ // swallow both. The blurred layer between them softens the step so the
259
+ // frame reads as lighting rather than as a pasted-on rectangle.
260
+ boxShadow: [
261
+ "0 0 0 3px rgba(255,255,255,0.98)",
262
+ "0 0 0 5px rgba(15,23,42,0.92)",
263
+ "0 0 18px 6px rgba(15,23,42,0.45)",
264
+ "0 0 0 9999px rgba(15,23,42,0.70)"
265
+ ].join(", ")
266
+ }
267
+ }
268
+ ) : /* @__PURE__ */ jsx("div", { className: "pointer-events-auto fixed inset-0 bg-black/70" }),
269
+ /* @__PURE__ */ jsx(
270
+ TourCard,
271
+ {
272
+ step,
273
+ index,
274
+ total: steps.length,
275
+ labels,
276
+ isFirst,
277
+ isLast,
278
+ rect: spot,
279
+ dir,
280
+ onBack: onPrev,
281
+ onNext,
282
+ onSkip
283
+ }
284
+ )
285
+ ]
286
+ }
287
+ ),
261
288
  document.body
262
289
  );
263
290
  }
264
- function placeCard(rect, cw, ch, vw, vh, m, placement) {
291
+ function ownsKey(target, key) {
292
+ if (!(target instanceof HTMLElement)) return false;
293
+ if (target.isContentEditable) return true;
294
+ const tag = target.tagName;
295
+ if (tag === "TEXTAREA" || tag === "SELECT") return true;
296
+ if (tag === "INPUT") {
297
+ const type = target.type;
298
+ return key !== "Enter" || type !== "checkbox";
299
+ }
300
+ if (key !== "Enter") return false;
301
+ return tag === "BUTTON" || tag === "A" && target.hasAttribute("href") || target.getAttribute("role") === "button" || target.getAttribute("role") === "link";
302
+ }
303
+ function SpotlightShields({ spot }) {
304
+ const shield = "pointer-events-auto fixed";
305
+ const bottom = spot.top + spot.height;
306
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
307
+ /* @__PURE__ */ jsx("div", { "data-tour-shield": "", className: shield, style: { top: 0, left: 0, right: 0, height: Math.max(0, spot.top) } }),
308
+ /* @__PURE__ */ jsx("div", { "data-tour-shield": "", className: shield, style: { top: bottom, left: 0, right: 0, bottom: 0 } }),
309
+ /* @__PURE__ */ jsx(
310
+ "div",
311
+ {
312
+ "data-tour-shield": "",
313
+ className: shield,
314
+ style: { top: spot.top, left: 0, width: Math.max(0, spot.left), height: spot.height }
315
+ }
316
+ ),
317
+ /* @__PURE__ */ jsx(
318
+ "div",
319
+ {
320
+ "data-tour-shield": "",
321
+ className: shield,
322
+ style: { top: spot.top, left: spot.left + spot.width, right: 0, height: spot.height }
323
+ }
324
+ )
325
+ ] });
326
+ }
327
+ function placeCard(rect, cw, ch, vw, vh, m, placement, dir = "ltr") {
328
+ if (placement === "start") placement = dir === "rtl" ? "right" : "left";
329
+ else if (placement === "end") placement = dir === "rtl" ? "left" : "right";
265
330
  if (!rect || placement === "center") {
266
331
  return { top: Math.max(m, (vh - ch) / 2), left: Math.max(m, (vw - cw) / 2) };
267
332
  }
@@ -300,6 +365,7 @@ function TourCard({
300
365
  isFirst,
301
366
  isLast,
302
367
  rect,
368
+ dir,
303
369
  onBack,
304
370
  onNext,
305
371
  onSkip
@@ -311,9 +377,9 @@ function TourCard({
311
377
  const card = ref.current;
312
378
  if (!card) return;
313
379
  setStyle(
314
- placeCard(rect, card.offsetWidth, card.offsetHeight, window.innerWidth, window.innerHeight, 12, step.placement)
380
+ placeCard(rect, card.offsetWidth, card.offsetHeight, window.innerWidth, window.innerHeight, 12, step.placement, dir)
315
381
  );
316
- }, [rect, index, step.placement]);
382
+ }, [rect, index, step.placement, dir]);
317
383
  return /* @__PURE__ */ jsxs(
318
384
  "div",
319
385
  {