@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.
- package/README.md +219 -0
- package/dist/components/account-settings.d.ts +71 -0
- package/dist/components/account-settings.js +159 -0
- package/dist/components/account-settings.js.map +1 -0
- package/dist/components/alert-banner.d.ts +20 -0
- package/dist/components/alert-banner.js +39 -0
- package/dist/components/alert-banner.js.map +1 -0
- package/dist/components/amount-input.d.ts +117 -0
- package/dist/components/amount-input.js +243 -0
- package/dist/components/amount-input.js.map +1 -0
- package/dist/components/calculator.d.ts +42 -0
- package/dist/components/calculator.js +161 -0
- package/dist/components/calculator.js.map +1 -0
- package/dist/components/chart.d.ts +58 -0
- package/dist/components/chart.js +158 -0
- package/dist/components/chart.js.map +1 -0
- package/dist/components/combobox-core.d.ts +77 -0
- package/dist/components/combobox-core.js +263 -0
- package/dist/components/combobox-core.js.map +1 -0
- package/dist/components/combobox.d.ts +141 -0
- package/dist/components/combobox.js +550 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/currency-select.d.ts +37 -0
- package/dist/components/currency-select.js +131 -0
- package/dist/components/currency-select.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +28 -0
- package/dist/components/data-table-filter-popover.js +201 -0
- package/dist/components/data-table-filter-popover.js.map +1 -0
- package/dist/components/data-table-filters.d.ts +5 -0
- package/dist/components/data-table-filters.js +118 -0
- package/dist/components/data-table-filters.js.map +1 -0
- package/dist/components/data-table-labels.d.ts +33 -0
- package/dist/components/data-table-labels.js +53 -0
- package/dist/components/data-table-labels.js.map +1 -0
- package/dist/components/data-table-pagination.d.ts +18 -0
- package/dist/components/data-table-pagination.js +92 -0
- package/dist/components/data-table-pagination.js.map +1 -0
- package/dist/components/data-table-sort.d.ts +23 -0
- package/dist/components/data-table-sort.js +55 -0
- package/dist/components/data-table-sort.js.map +1 -0
- package/dist/components/data-table.d.ts +5 -0
- package/dist/components/data-table.js +926 -0
- package/dist/components/data-table.js.map +1 -0
- package/dist/components/date-picker.d.ts +85 -0
- package/dist/components/date-picker.js +253 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/dropdown.d.ts +98 -0
- package/dist/components/dropdown.js +122 -0
- package/dist/components/dropdown.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +49 -0
- package/dist/components/entity-combobox.js +115 -0
- package/dist/components/entity-combobox.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +18 -0
- package/dist/components/file-dropzone.js +101 -0
- package/dist/components/file-dropzone.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +57 -0
- package/dist/components/full-bleed-dialog.js +68 -0
- package/dist/components/full-bleed-dialog.js.map +1 -0
- package/dist/components/grouped-picker.d.ts +37 -0
- package/dist/components/grouped-picker.js +87 -0
- package/dist/components/grouped-picker.js.map +1 -0
- package/dist/components/hover-menu.d.ts +16 -0
- package/dist/components/hover-menu.js +113 -0
- package/dist/components/hover-menu.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +33 -0
- package/dist/components/mini-calendar.js +133 -0
- package/dist/components/mini-calendar.js.map +1 -0
- package/dist/components/modal.d.ts +76 -0
- package/dist/components/modal.js +156 -0
- package/dist/components/modal.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +47 -0
- package/dist/components/multi-entity-combobox.js +116 -0
- package/dist/components/multi-entity-combobox.js.map +1 -0
- package/dist/components/multi-select.d.ts +39 -0
- package/dist/components/multi-select.js +132 -0
- package/dist/components/multi-select.js.map +1 -0
- package/dist/components/number-input.d.ts +74 -0
- package/dist/components/number-input.js +114 -0
- package/dist/components/number-input.js.map +1 -0
- package/dist/components/numpad-sheet.d.ts +45 -0
- package/dist/components/numpad-sheet.js +102 -0
- package/dist/components/numpad-sheet.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +44 -0
- package/dist/components/picker-sheet.js +78 -0
- package/dist/components/picker-sheet.js.map +1 -0
- package/dist/components/popover.d.ts +20 -0
- package/dist/components/popover.js +46 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/search-field.d.ts +52 -0
- package/dist/components/search-field.js +64 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/settings-fields.d.ts +39 -0
- package/dist/components/settings-fields.js +17 -0
- package/dist/components/settings-fields.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +60 -0
- package/dist/components/swipeable-row.js +115 -0
- package/dist/components/swipeable-row.js.map +1 -0
- package/dist/components/toggle-group.d.ts +34 -0
- package/dist/components/toggle-group.js +90 -0
- package/dist/components/toggle-group.js.map +1 -0
- package/dist/components/tooltip.d.ts +86 -0
- package/dist/components/tooltip.js +171 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/ui.d.ts +239 -0
- package/dist/components/ui.js +512 -0
- package/dist/components/ui.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +31 -0
- package/dist/components/use-mobile-reveal.js +33 -0
- package/dist/components/use-mobile-reveal.js.map +1 -0
- package/dist/components/use-table-state.d.ts +54 -0
- package/dist/components/use-table-state.js +153 -0
- package/dist/components/use-table-state.js.map +1 -0
- package/dist/components/user-avatar.d.ts +23 -0
- package/dist/components/user-avatar.js +33 -0
- package/dist/components/user-avatar.js.map +1 -0
- package/dist/components/wizard-stepper.d.ts +14 -0
- package/dist/components/wizard-stepper.js +27 -0
- package/dist/components/wizard-stepper.js.map +1 -0
- package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
- package/dist/feedback/feedback-attachment.d.ts +63 -0
- package/dist/feedback/feedback-attachment.js +160 -0
- package/dist/feedback/feedback-attachment.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +72 -0
- package/dist/feedback/feedback-dialog.js +109 -0
- package/dist/feedback/feedback-dialog.js.map +1 -0
- package/dist/feedback/feedback-inbox.d.ts +226 -0
- package/dist/feedback/feedback-inbox.js +315 -0
- package/dist/feedback/feedback-inbox.js.map +1 -0
- package/dist/hooks/use-anchored-panel.d.ts +80 -0
- package/dist/hooks/use-anchored-panel.js +71 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -0
- package/dist/hooks/use-anchored-rect.d.ts +24 -0
- package/dist/hooks/use-anchored-rect.js +40 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
- package/dist/hooks/use-body-scroll-lock.js +41 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -0
- package/dist/hooks/use-close-transition.d.ts +41 -0
- package/dist/hooks/use-close-transition.js +36 -0
- package/dist/hooks/use-close-transition.js.map +1 -0
- package/dist/hooks/use-dismiss.d.ts +34 -0
- package/dist/hooks/use-dismiss.js +41 -0
- package/dist/hooks/use-dismiss.js.map +1 -0
- package/dist/hooks/use-media-query.d.ts +8 -0
- package/dist/hooks/use-media-query.js +18 -0
- package/dist/hooks/use-media-query.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +10 -0
- package/dist/hooks/use-overlay-history.js +99 -0
- package/dist/hooks/use-overlay-history.js.map +1 -0
- package/dist/hooks/use-row-swipe.d.ts +30 -0
- package/dist/hooks/use-row-swipe.js +128 -0
- package/dist/hooks/use-row-swipe.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/calc.d.ts +89 -0
- package/dist/lib/calc.js +180 -0
- package/dist/lib/calc.js.map +1 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.js +7 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/dates.d.ts +84 -0
- package/dist/lib/dates.js +108 -0
- package/dist/lib/dates.js.map +1 -0
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.js +30 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +23 -0
- package/dist/lib/safe-storage.js +20 -0
- package/dist/lib/safe-storage.js.map +1 -0
- package/dist/search/command-palette.d.ts +71 -0
- package/dist/search/command-palette.js +198 -0
- package/dist/search/command-palette.js.map +1 -0
- package/dist/shell/app-shell.d.ts +64 -0
- package/dist/shell/app-shell.js +237 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/option-switcher-menu.d.ts +24 -0
- package/dist/shell/option-switcher-menu.js +44 -0
- package/dist/shell/option-switcher-menu.js.map +1 -0
- package/dist/shell/role-switcher.d.ts +19 -0
- package/dist/shell/role-switcher.js +28 -0
- package/dist/shell/role-switcher.js.map +1 -0
- package/dist/shell/top-bar.d.ts +15 -0
- package/dist/shell/top-bar.js +29 -0
- package/dist/shell/top-bar.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +44 -0
- package/dist/shell/topbar-action-menu.js +49 -0
- package/dist/shell/topbar-action-menu.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +54 -0
- package/dist/shell/topbar-controls.js +152 -0
- package/dist/shell/topbar-controls.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +60 -0
- package/dist/theme/chart-palette.js +79 -0
- package/dist/theme/chart-palette.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +39 -0
- package/dist/theme/palette-presets.js +440 -0
- package/dist/theme/palette-presets.js.map +1 -0
- package/dist/theme/palette-store.d.ts +45 -0
- package/dist/theme/palette-store.js +52 -0
- package/dist/theme/palette-store.js.map +1 -0
- package/dist/theme/theme-store.d.ts +57 -0
- package/dist/theme/theme-store.js +96 -0
- package/dist/theme/theme-store.js.map +1 -0
- package/dist/tour/tour.d.ts +104 -0
- package/dist/tour/tour.js +351 -0
- package/dist/tour/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +30 -0
- package/dist/wizard/stepper-nav.js +146 -0
- package/dist/wizard/stepper-nav.js.map +1 -0
- package/dist/wizard/types.d.ts +124 -0
- package/dist/wizard/types.js +24 -0
- package/dist/wizard/types.js.map +1 -0
- package/dist/wizard/use-wizard.d.ts +6 -0
- package/dist/wizard/use-wizard.js +222 -0
- package/dist/wizard/use-wizard.js.map +1 -0
- package/dist/wizard/validation.d.ts +23 -0
- package/dist/wizard/validation.js +13 -0
- package/dist/wizard/validation.js.map +1 -0
- package/dist/wizard/wizard-context.d.ts +19 -0
- package/dist/wizard/wizard-context.js +15 -0
- package/dist/wizard/wizard-context.js.map +1 -0
- package/dist/wizard/wizard-step.d.ts +11 -0
- package/dist/wizard/wizard-step.js +12 -0
- package/dist/wizard/wizard-step.js.map +1 -0
- package/dist/wizard/wizard-summary.d.ts +18 -0
- package/dist/wizard/wizard-summary.js +41 -0
- package/dist/wizard/wizard-summary.js.map +1 -0
- package/package.json +87 -0
- 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":"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","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
|