@eifi1/ui-kit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +4 -0
  2. package/dist/components/amount-input.d.ts +2 -0
  3. package/dist/components/calculator.d.ts +2 -0
  4. package/dist/components/chip.d.ts +27 -5
  5. package/dist/components/chip.js +6 -2
  6. package/dist/components/chip.js.map +1 -1
  7. package/dist/components/data-table-filter-popover.d.ts +1 -1
  8. package/dist/components/data-table-filters.d.ts +1 -1
  9. package/dist/components/data-table.d.ts +1 -1
  10. package/dist/components/data-table.js +122 -91
  11. package/dist/components/data-table.js.map +1 -1
  12. package/dist/components/date-picker.d.ts +69 -3
  13. package/dist/components/date-picker.js +152 -65
  14. package/dist/components/date-picker.js.map +1 -1
  15. package/dist/components/disclosure.d.ts +11 -2
  16. package/dist/components/disclosure.js +10 -4
  17. package/dist/components/disclosure.js.map +1 -1
  18. package/dist/components/floating-panel.d.ts +29 -2
  19. package/dist/components/floating-panel.js +16 -2
  20. package/dist/components/floating-panel.js.map +1 -1
  21. package/dist/components/full-bleed-dialog.js +3 -1
  22. package/dist/components/full-bleed-dialog.js.map +1 -1
  23. package/dist/components/number-field.d.ts +2 -0
  24. package/dist/components/number-input.d.ts +2 -0
  25. package/dist/components/numpad-sheet.d.ts +2 -0
  26. package/dist/components/progress-bar.d.ts +16 -2
  27. package/dist/components/progress-bar.js +4 -2
  28. package/dist/components/progress-bar.js.map +1 -1
  29. package/dist/components/treemap.js +4 -1
  30. package/dist/components/treemap.js.map +1 -1
  31. package/dist/components/ui.d.ts +44 -14
  32. package/dist/components/ui.js +28 -22
  33. package/dist/components/ui.js.map +1 -1
  34. package/dist/components/use-table-state.d.ts +1 -1
  35. package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
  36. package/dist/data-table.d.ts +1 -1
  37. package/dist/hooks/use-overlay-history.js +84 -4
  38. package/dist/hooks/use-overlay-history.js.map +1 -1
  39. package/dist/i18n/defaults.d.ts +2 -0
  40. package/dist/i18n/defaults.js +2 -0
  41. package/dist/i18n/defaults.js.map +1 -1
  42. package/dist/i18n/kit-labels.d.ts +3 -0
  43. package/dist/i18n/kit-labels.js.map +1 -1
  44. package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
  45. package/dist/i18n/locales/de-CH.d.ts +2 -0
  46. package/dist/i18n/locales/de-informal.d.ts +2 -0
  47. package/dist/i18n/locales/de.d.ts +2 -0
  48. package/dist/i18n/locales/de.js +10 -0
  49. package/dist/i18n/locales/de.js.map +1 -1
  50. package/dist/i18n/locales/es.d.ts +2 -0
  51. package/dist/i18n/locales/es.js +10 -0
  52. package/dist/i18n/locales/es.js.map +1 -1
  53. package/dist/i18n/locales/fr.d.ts +2 -0
  54. package/dist/i18n/locales/fr.js +10 -0
  55. package/dist/i18n/locales/fr.js.map +1 -1
  56. package/dist/i18n/locales/hu.d.ts +2 -0
  57. package/dist/i18n/locales/hu.js +10 -0
  58. package/dist/i18n/locales/hu.js.map +1 -1
  59. package/dist/i18n/locales/it.d.ts +2 -0
  60. package/dist/i18n/locales/it.js +10 -0
  61. package/dist/i18n/locales/it.js.map +1 -1
  62. package/dist/i18n/locales/zh.d.ts +2 -0
  63. package/dist/i18n/locales/zh.js +10 -0
  64. package/dist/i18n/locales/zh.js.map +1 -1
  65. package/dist/index.d.ts +5 -3
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/search/command-palette.d.ts +49 -1
  69. package/dist/search/command-palette.js +132 -29
  70. package/dist/search/command-palette.js.map +1 -1
  71. package/dist/search/global-search.d.ts +141 -0
  72. package/dist/search/global-search.js +277 -0
  73. package/dist/search/global-search.js.map +1 -0
  74. package/dist/search/search-index.d.ts +105 -0
  75. package/dist/search/search-index.js +142 -0
  76. package/dist/search/search-index.js.map +1 -0
  77. package/dist/search.d.ts +2 -0
  78. package/dist/search.js +2 -0
  79. package/dist/search.js.map +1 -1
  80. package/dist/wizard/stepper-nav.d.ts +10 -1
  81. package/dist/wizard/stepper-nav.js +2 -1
  82. package/dist/wizard/stepper-nav.js.map +1 -1
  83. package/package.json +12 -3
  84. package/src/components/chip.tsx +36 -6
  85. package/src/components/data-table.tsx +81 -5
  86. package/src/components/date-picker.tsx +328 -117
  87. package/src/components/disclosure.tsx +24 -8
  88. package/src/components/floating-panel.tsx +46 -2
  89. package/src/components/full-bleed-dialog.tsx +7 -2
  90. package/src/components/progress-bar.tsx +27 -3
  91. package/src/components/treemap.tsx +3 -0
  92. package/src/components/ui.tsx +95 -37
  93. package/src/hooks/use-overlay-history.ts +181 -4
  94. package/src/i18n/defaults.ts +2 -0
  95. package/src/i18n/kit-labels.tsx +2 -0
  96. package/src/i18n/locales/de.ts +10 -0
  97. package/src/i18n/locales/es.ts +10 -0
  98. package/src/i18n/locales/fr.ts +10 -0
  99. package/src/i18n/locales/hu.ts +10 -0
  100. package/src/i18n/locales/it.ts +10 -0
  101. package/src/i18n/locales/zh.ts +10 -0
  102. package/src/index.ts +4 -0
  103. package/src/search/command-palette.tsx +200 -27
  104. package/src/search/global-search.tsx +484 -0
  105. package/src/search/search-index.ts +295 -0
  106. package/src/search.ts +2 -0
  107. package/src/wizard/stepper-nav.tsx +11 -1
@@ -27,7 +27,7 @@ import '../hooks/use-anchored-rect.js';
27
27
  * looks and what wraps it, because that is presentation and usually depends on
28
28
  * state the app holds (a "replace everything" toggle, a write lock).
29
29
  */
30
- declare function StepperNav<TData extends Record<string, unknown>>({ wizard, children, title, description, className, labels, finishVariant, finishDisabled, renderFinish, }: {
30
+ declare function StepperNav<TData extends Record<string, unknown>>({ wizard, children, title, description, className, labels, finishVariant, finishDisabled, doneDisabled, renderFinish, }: {
31
31
  wizard: UseWizardReturn<TData>;
32
32
  children: ReactNode;
33
33
  title?: ReactNode;
@@ -40,6 +40,15 @@ declare function StepperNav<TData extends Record<string, unknown>>({ wizard, chi
40
40
  /** Disable Finish for a reason of the app's own, on top of the kit's gates
41
41
  * (`canFinish`, submitting, validating). */
42
42
  finishDisabled?: boolean;
43
+ /**
44
+ * Disable Done — the button of the last step after a commit — while the step still
45
+ * has work of its own running. keksdose's YNAB import ends on a recurring step whose
46
+ * convert loop writes one rule at a time (ynab-import-panel.tsx): leaving mid-loop
47
+ * abandons the rest. Until now the panel dropped `onDone` for the duration, which
48
+ * takes the button away instead of saying "not yet": disabled, it stays where the
49
+ * eye left it and comes back to life when the loop ends.
50
+ */
51
+ doneDisabled?: boolean;
43
52
  /**
44
53
  * Wrap the Finish button: receives the kit's button element — already labelled,
45
54
  * gated and wired to `wizard.finish` — and returns what to render in its place.
@@ -18,6 +18,7 @@ function StepperNav({
18
18
  labels,
19
19
  finishVariant = "brand",
20
20
  finishDisabled = false,
21
+ doneDisabled = false,
21
22
  renderFinish
22
23
  }) {
23
24
  const l = useKitLabels("wizard", DEFAULT_WIZARD_LABELS, labels);
@@ -50,7 +51,7 @@ function StepperNav({
50
51
  );
51
52
  forwardButton = renderFinish ? renderFinish(finishButton) : finishButton;
52
53
  } else if (wizard.isLastStep) {
53
- forwardButton = wizard.canDone ? /* @__PURE__ */ jsx(Button, { variant: "brand", "data-tour": "wizard-done", onClick: wizard.done, children: stepNextLabel ?? l.done ?? DEFAULT_WIZARD_LABELS.done }) : null;
54
+ forwardButton = wizard.canDone ? /* @__PURE__ */ jsx(Button, { variant: "brand", "data-tour": "wizard-done", onClick: wizard.done, disabled: doneDisabled, children: stepNextLabel ?? l.done ?? DEFAULT_WIZARD_LABELS.done }) : null;
54
55
  } else {
55
56
  forwardButton = /* @__PURE__ */ jsxs(
56
57
  Button,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/wizard/stepper-nav.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button, Spinner } from \"../components/ui\";\nimport type { ButtonVariant } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { alertFrameClass } from \"../components/alert-banner\";\nimport { WizardContextProvider } from \"./wizard-context\";\nimport { DEFAULT_WIZARD_LABELS } from \"./types\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport type { UseWizardReturn, WizardLabels } from \"./types\";\n\n/**\n * The chrome around a wizard: the step indicator, the step content, and the\n * Cancel / Back / Skip / Next / Finish bar — plus the cancel-confirmation\n * dialog, because \"are you sure you want to lose this\" is part of the wizard's\n * contract rather than something each app should re-decide.\n *\n * It is a component and not a set of parts for one reason: which button shows,\n * and whether it is enabled, is a function of `wizard` state that was getting\n * re-derived (and re-derived slightly differently) at every call site. Finish is\n * gated on `canFinish`, Next and Skip on `nextBlocked`, and Skip only appears on\n * an `optional` step that is not the last — that policy lives here now.\n *\n * The `<StepperNav>` is also what publishes {@link WizardContextProvider}, which\n * is how a mounted step registers its own validator. A step's\n * `useRhfWizardStep` / `useWizardStepValidate` therefore only works inside one.\n *\n * Which buttons show is read off `wizard` (`canCancel`, `canGoBack`, `canDone`,\n * `isCommitStep`), so the policy options — `cancellable`, `onExit`, `commits`,\n * `onDone` — are all set on `useWizard`. What is set HERE is how the Finish button\n * looks and what wraps it, because that is presentation and usually depends on\n * state the app holds (a \"replace everything\" toggle, a write lock).\n */\nexport function StepperNav<TData extends Record<string, unknown>>({\n wizard,\n children,\n title,\n description,\n className,\n labels,\n finishVariant = \"brand\",\n finishDisabled = false,\n renderFinish,\n}: {\n wizard: UseWizardReturn<TData>;\n children: ReactNode;\n title?: ReactNode;\n description?: ReactNode;\n className?: string;\n labels?: Partial<WizardLabels>;\n /** The Finish button's variant — `\"danger\"` for a destructive commit (an import\n * that replaces what is there). Default `\"brand\"`. Next keeps `\"brand\"`. */\n finishVariant?: ButtonVariant;\n /** Disable Finish for a reason of the app's own, on top of the kit's gates\n * (`canFinish`, submitting, validating). */\n finishDisabled?: boolean;\n /**\n * Wrap the Finish button: receives the kit's button element — already labelled,\n * gated and wired to `wizard.finish` — and returns what to render in its place.\n *\n * ```tsx\n * renderFinish={(button) => <SaveGuard lock={lock}>{button}</SaveGuard>}\n * ```\n *\n * A wrapper and not a replacement (`finishButton: (props) => …`) or a hook\n * (`onBeforeFinish`): the app's need is to put something AROUND the commit — a\n * write-lock tooltip, which has to sit on a wrapping element because a disabled\n * button receives no pointer events — while the kit keeps owning what the button\n * says and when it is enabled. A replacement would hand every app the gating to\n * re-derive (the problem this component exists to end); `onBeforeFinish` can veto\n * a click but cannot explain a disabled button. The element is a plain `<Button>`,\n * so a wrapper that clones it with `disabled` works as expected.\n */\n renderFinish?: (button: ReactElement) => ReactNode;\n}) {\n const l = useKitLabels(\"wizard\", DEFAULT_WIZARD_LABELS, labels);\n const cancelTitleId = useId();\n\n const contextValue = useMemo(\n () => ({\n registerStepValidate: wizard.registerStepValidate,\n setNextBlocked: wizard.setNextBlocked,\n }),\n [wizard.registerStepValidate, wizard.setNextBlocked],\n );\n\n // Finish is only valid once every step before the last has been completed or\n // skipped (derived once in useWizard). Prevents a restored last-step index\n // (empty data after a reload) from firing an empty submit.\n const canFinish = wizard.canFinish;\n\n // Pending while the step's validators run (an async server check): a spinner and\n // `aria-busy`, and disabled — the hook's own re-entry guard still refuses a second\n // press, this just stops it looking like the first one did nothing.\n const pending = wizard.isValidating;\n const stepNextLabel = wizard.currentStep.nextLabel;\n\n let forwardButton: ReactNode = null;\n if (wizard.isCommitStep) {\n const finishButton = (\n <Button\n variant={finishVariant}\n data-tour=\"wizard-finish\"\n onClick={wizard.finish}\n disabled={wizard.isSubmitting || pending || !canFinish || finishDisabled}\n aria-busy={wizard.isSubmitting || pending || undefined}\n >\n {/* Decorative: the button's text is what a reader hears. */}\n {pending && <Spinner label={null} className=\"size-4\" />}\n {wizard.isSubmitting ? l.submitting : (stepNextLabel ?? l.finish)}\n </Button>\n );\n forwardButton = renderFinish ? renderFinish(finishButton) : finishButton;\n } else if (wizard.isLastStep) {\n // The last step after a commit. Nothing to finish; Done only when the app gave\n // an `onDone` — otherwise the step carries its own way out.\n forwardButton = wizard.canDone ? (\n <Button variant=\"brand\" data-tour=\"wizard-done\" onClick={wizard.done}>\n {stepNextLabel ?? l.done ?? DEFAULT_WIZARD_LABELS.done}\n </Button>\n ) : null;\n } else {\n forwardButton = (\n <Button\n variant=\"brand\"\n data-tour=\"wizard-next\"\n onClick={wizard.goNext}\n disabled={wizard.nextBlocked || pending}\n aria-busy={pending || undefined}\n >\n {pending && <Spinner label={null} className=\"size-4\" />}\n {stepNextLabel ?? l.next}\n </Button>\n );\n }\n\n return (\n <div className={cn(\"flex flex-col gap-6\", className)}>\n {(title || description) && (\n <div>\n {title && (\n <h1 className=\"text-2xl font-bold tracking-tight text-[var(--text-primary)]\">\n {title}\n </h1>\n )}\n {description && (\n <p className=\"mt-1 text-sm text-[var(--text-muted)]\">{description}</p>\n )}\n </div>\n )}\n\n {/* Step indicator */}\n <nav aria-label={l.steps}>\n <ol className=\"flex w-full items-center\">\n {wizard.steps.map((step, index) => {\n const status = wizard.stepStatus(index);\n const isLast = index === wizard.steps.length - 1;\n\n return (\n <li key={step.id} className={cn(\"flex items-center\", !isLast && \"flex-1\")}>\n <button\n type=\"button\"\n onClick={() => wizard.goToStep(index)}\n // `canGoToStep` adds the commit lock: once past a committing step,\n // the steps up to it are done with and cannot be reopened.\n disabled={status === \"upcoming\" || !wizard.canGoToStep(index)}\n aria-current={status === \"active\" ? \"step\" : undefined}\n className={cn(\n \"group flex flex-col items-center gap-1.5\",\n status === \"upcoming\" ? \"cursor-default\" : \"cursor-pointer\",\n )}\n >\n {/* Circle */}\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-xs font-semibold transition-colors\",\n status === \"completed\" &&\n \"border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]\",\n status === \"active\" &&\n \"border-[var(--brand)] bg-[var(--bg-surface)] text-[var(--brand)]\",\n status === \"upcoming\" &&\n \"border-[var(--border)] bg-[var(--bg-surface)] text-[var(--text-placeholder)]\",\n status === \"skipped\" &&\n \"border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-muted)]\",\n )}\n >\n {status === \"completed\" ? <Check className=\"size-4\" /> : index + 1}\n </span>\n {/* Label — hidden on mobile, where the line below carries it */}\n <span\n className={cn(\n \"hidden text-xs font-medium md:block\",\n status === \"active\" || status === \"completed\"\n ? \"text-[var(--text-primary)]\"\n : \"text-[var(--text-muted)]\",\n )}\n >\n {step.label}\n </span>\n </button>\n\n {/* Connector line */}\n {!isLast && (\n <div\n className={cn(\n \"mx-2 h-0.5 flex-1\",\n status === \"completed\" ? \"bg-[var(--brand)]\" : \"bg-[var(--border)]\",\n )}\n />\n )}\n </li>\n );\n })}\n </ol>\n {/* Mobile: the labels above are hidden, so name the current step here */}\n <p className=\"mt-2 text-center text-sm font-medium text-[var(--text-primary)] md:hidden\">\n {l.step(wizard.currentStepIndex + 1, wizard.steps.length)}\n {\" — \"}\n {wizard.currentStep.label}\n </p>\n </nav>\n\n {/* Step content */}\n <WizardContextProvider value={contextValue}>\n <div className=\"min-h-[300px]\">{children}</div>\n </WizardContextProvider>\n\n {/* Submit error */}\n {wizard.error && (\n <div\n role=\"alert\"\n className={cn(\n \"text-sm text-[var(--danger)]\",\n alertFrameClass(\"danger\"),\n )}\n >\n {wizard.error}\n </div>\n )}\n\n {/* Navigation bar */}\n <div className=\"flex items-center justify-between border-t border-[var(--border)] pt-4\">\n {/* The empty span keeps the right-hand group on the right when Cancel is off. */}\n {wizard.canCancel ? (\n <Button variant=\"secondary\" onClick={wizard.cancel}>\n {l.cancel}\n </Button>\n ) : (\n <span />\n )}\n <div className=\"flex items-center gap-2\">\n {wizard.canGoBack && (\n // Disabled, not hidden, while the commit runs: it comes back if the commit\n // fails, and a button that vanishes mid-click is its own kind of surprise.\n <Button variant=\"secondary\" onClick={wizard.goBack} disabled={wizard.isSubmitting}>\n {l.back}\n </Button>\n )}\n {wizard.currentStep.optional && !wizard.isLastStep && !wizard.isCommitStep && (\n <Button\n variant=\"ghost\"\n onClick={wizard.skip}\n disabled={wizard.nextBlocked || wizard.isValidating}\n >\n {l.skip}\n </Button>\n )}\n {forwardButton}\n </div>\n </div>\n\n {/* Cancel confirmation */}\n {wizard.showCancelDialog && (\n <Modal onClose={wizard.dismissCancel} labelledBy={cancelTitleId} className=\"space-y-4\">\n <div className=\"space-y-1.5\">\n <h2 id={cancelTitleId} className=\"text-lg font-semibold text-[var(--text-primary)]\">\n {l.cancelTitle}\n </h2>\n <p className=\"text-sm text-[var(--text-muted)]\">{l.confirmCancel}</p>\n </div>\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n <Button variant=\"secondary\" onClick={wizard.dismissCancel}>\n {l.cancelDismissLabel}\n </Button>\n <Button variant=\"danger\" onClick={wizard.confirmCancel}>\n {l.cancelConfirmLabel}\n </Button>\n </div>\n </Modal>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/wizard/stepper-nav.tsx"],"sourcesContent":["import { useId, useMemo } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button, Spinner } from \"../components/ui\";\nimport type { ButtonVariant } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { alertFrameClass } from \"../components/alert-banner\";\nimport { WizardContextProvider } from \"./wizard-context\";\nimport { DEFAULT_WIZARD_LABELS } from \"./types\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport type { UseWizardReturn, WizardLabels } from \"./types\";\n\n/**\n * The chrome around a wizard: the step indicator, the step content, and the\n * Cancel / Back / Skip / Next / Finish bar — plus the cancel-confirmation\n * dialog, because \"are you sure you want to lose this\" is part of the wizard's\n * contract rather than something each app should re-decide.\n *\n * It is a component and not a set of parts for one reason: which button shows,\n * and whether it is enabled, is a function of `wizard` state that was getting\n * re-derived (and re-derived slightly differently) at every call site. Finish is\n * gated on `canFinish`, Next and Skip on `nextBlocked`, and Skip only appears on\n * an `optional` step that is not the last — that policy lives here now.\n *\n * The `<StepperNav>` is also what publishes {@link WizardContextProvider}, which\n * is how a mounted step registers its own validator. A step's\n * `useRhfWizardStep` / `useWizardStepValidate` therefore only works inside one.\n *\n * Which buttons show is read off `wizard` (`canCancel`, `canGoBack`, `canDone`,\n * `isCommitStep`), so the policy options — `cancellable`, `onExit`, `commits`,\n * `onDone` — are all set on `useWizard`. What is set HERE is how the Finish button\n * looks and what wraps it, because that is presentation and usually depends on\n * state the app holds (a \"replace everything\" toggle, a write lock).\n */\nexport function StepperNav<TData extends Record<string, unknown>>({\n wizard,\n children,\n title,\n description,\n className,\n labels,\n finishVariant = \"brand\",\n finishDisabled = false,\n doneDisabled = false,\n renderFinish,\n}: {\n wizard: UseWizardReturn<TData>;\n children: ReactNode;\n title?: ReactNode;\n description?: ReactNode;\n className?: string;\n labels?: Partial<WizardLabels>;\n /** The Finish button's variant — `\"danger\"` for a destructive commit (an import\n * that replaces what is there). Default `\"brand\"`. Next keeps `\"brand\"`. */\n finishVariant?: ButtonVariant;\n /** Disable Finish for a reason of the app's own, on top of the kit's gates\n * (`canFinish`, submitting, validating). */\n finishDisabled?: boolean;\n /**\n * Disable Done — the button of the last step after a commit — while the step still\n * has work of its own running. keksdose's YNAB import ends on a recurring step whose\n * convert loop writes one rule at a time (ynab-import-panel.tsx): leaving mid-loop\n * abandons the rest. Until now the panel dropped `onDone` for the duration, which\n * takes the button away instead of saying \"not yet\": disabled, it stays where the\n * eye left it and comes back to life when the loop ends.\n */\n doneDisabled?: boolean;\n /**\n * Wrap the Finish button: receives the kit's button element — already labelled,\n * gated and wired to `wizard.finish` — and returns what to render in its place.\n *\n * ```tsx\n * renderFinish={(button) => <SaveGuard lock={lock}>{button}</SaveGuard>}\n * ```\n *\n * A wrapper and not a replacement (`finishButton: (props) => …`) or a hook\n * (`onBeforeFinish`): the app's need is to put something AROUND the commit — a\n * write-lock tooltip, which has to sit on a wrapping element because a disabled\n * button receives no pointer events — while the kit keeps owning what the button\n * says and when it is enabled. A replacement would hand every app the gating to\n * re-derive (the problem this component exists to end); `onBeforeFinish` can veto\n * a click but cannot explain a disabled button. The element is a plain `<Button>`,\n * so a wrapper that clones it with `disabled` works as expected.\n */\n renderFinish?: (button: ReactElement) => ReactNode;\n}) {\n const l = useKitLabels(\"wizard\", DEFAULT_WIZARD_LABELS, labels);\n const cancelTitleId = useId();\n\n const contextValue = useMemo(\n () => ({\n registerStepValidate: wizard.registerStepValidate,\n setNextBlocked: wizard.setNextBlocked,\n }),\n [wizard.registerStepValidate, wizard.setNextBlocked],\n );\n\n // Finish is only valid once every step before the last has been completed or\n // skipped (derived once in useWizard). Prevents a restored last-step index\n // (empty data after a reload) from firing an empty submit.\n const canFinish = wizard.canFinish;\n\n // Pending while the step's validators run (an async server check): a spinner and\n // `aria-busy`, and disabled — the hook's own re-entry guard still refuses a second\n // press, this just stops it looking like the first one did nothing.\n const pending = wizard.isValidating;\n const stepNextLabel = wizard.currentStep.nextLabel;\n\n let forwardButton: ReactNode = null;\n if (wizard.isCommitStep) {\n const finishButton = (\n <Button\n variant={finishVariant}\n data-tour=\"wizard-finish\"\n onClick={wizard.finish}\n disabled={wizard.isSubmitting || pending || !canFinish || finishDisabled}\n aria-busy={wizard.isSubmitting || pending || undefined}\n >\n {/* Decorative: the button's text is what a reader hears. */}\n {pending && <Spinner label={null} className=\"size-4\" />}\n {wizard.isSubmitting ? l.submitting : (stepNextLabel ?? l.finish)}\n </Button>\n );\n forwardButton = renderFinish ? renderFinish(finishButton) : finishButton;\n } else if (wizard.isLastStep) {\n // The last step after a commit. Nothing to finish; Done only when the app gave\n // an `onDone` — otherwise the step carries its own way out.\n forwardButton = wizard.canDone ? (\n <Button variant=\"brand\" data-tour=\"wizard-done\" onClick={wizard.done} disabled={doneDisabled}>\n {stepNextLabel ?? l.done ?? DEFAULT_WIZARD_LABELS.done}\n </Button>\n ) : null;\n } else {\n forwardButton = (\n <Button\n variant=\"brand\"\n data-tour=\"wizard-next\"\n onClick={wizard.goNext}\n disabled={wizard.nextBlocked || pending}\n aria-busy={pending || undefined}\n >\n {pending && <Spinner label={null} className=\"size-4\" />}\n {stepNextLabel ?? l.next}\n </Button>\n );\n }\n\n return (\n <div className={cn(\"flex flex-col gap-6\", className)}>\n {(title || description) && (\n <div>\n {title && (\n <h1 className=\"text-2xl font-bold tracking-tight text-[var(--text-primary)]\">\n {title}\n </h1>\n )}\n {description && (\n <p className=\"mt-1 text-sm text-[var(--text-muted)]\">{description}</p>\n )}\n </div>\n )}\n\n {/* Step indicator */}\n <nav aria-label={l.steps}>\n <ol className=\"flex w-full items-center\">\n {wizard.steps.map((step, index) => {\n const status = wizard.stepStatus(index);\n const isLast = index === wizard.steps.length - 1;\n\n return (\n <li key={step.id} className={cn(\"flex items-center\", !isLast && \"flex-1\")}>\n <button\n type=\"button\"\n onClick={() => wizard.goToStep(index)}\n // `canGoToStep` adds the commit lock: once past a committing step,\n // the steps up to it are done with and cannot be reopened.\n disabled={status === \"upcoming\" || !wizard.canGoToStep(index)}\n aria-current={status === \"active\" ? \"step\" : undefined}\n className={cn(\n \"group flex flex-col items-center gap-1.5\",\n status === \"upcoming\" ? \"cursor-default\" : \"cursor-pointer\",\n )}\n >\n {/* Circle */}\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-xs font-semibold transition-colors\",\n status === \"completed\" &&\n \"border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]\",\n status === \"active\" &&\n \"border-[var(--brand)] bg-[var(--bg-surface)] text-[var(--brand)]\",\n status === \"upcoming\" &&\n \"border-[var(--border)] bg-[var(--bg-surface)] text-[var(--text-placeholder)]\",\n status === \"skipped\" &&\n \"border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-muted)]\",\n )}\n >\n {status === \"completed\" ? <Check className=\"size-4\" /> : index + 1}\n </span>\n {/* Label — hidden on mobile, where the line below carries it */}\n <span\n className={cn(\n \"hidden text-xs font-medium md:block\",\n status === \"active\" || status === \"completed\"\n ? \"text-[var(--text-primary)]\"\n : \"text-[var(--text-muted)]\",\n )}\n >\n {step.label}\n </span>\n </button>\n\n {/* Connector line */}\n {!isLast && (\n <div\n className={cn(\n \"mx-2 h-0.5 flex-1\",\n status === \"completed\" ? \"bg-[var(--brand)]\" : \"bg-[var(--border)]\",\n )}\n />\n )}\n </li>\n );\n })}\n </ol>\n {/* Mobile: the labels above are hidden, so name the current step here */}\n <p className=\"mt-2 text-center text-sm font-medium text-[var(--text-primary)] md:hidden\">\n {l.step(wizard.currentStepIndex + 1, wizard.steps.length)}\n {\" — \"}\n {wizard.currentStep.label}\n </p>\n </nav>\n\n {/* Step content */}\n <WizardContextProvider value={contextValue}>\n <div className=\"min-h-[300px]\">{children}</div>\n </WizardContextProvider>\n\n {/* Submit error */}\n {wizard.error && (\n <div\n role=\"alert\"\n className={cn(\n \"text-sm text-[var(--danger)]\",\n alertFrameClass(\"danger\"),\n )}\n >\n {wizard.error}\n </div>\n )}\n\n {/* Navigation bar */}\n <div className=\"flex items-center justify-between border-t border-[var(--border)] pt-4\">\n {/* The empty span keeps the right-hand group on the right when Cancel is off. */}\n {wizard.canCancel ? (\n <Button variant=\"secondary\" onClick={wizard.cancel}>\n {l.cancel}\n </Button>\n ) : (\n <span />\n )}\n <div className=\"flex items-center gap-2\">\n {wizard.canGoBack && (\n // Disabled, not hidden, while the commit runs: it comes back if the commit\n // fails, and a button that vanishes mid-click is its own kind of surprise.\n <Button variant=\"secondary\" onClick={wizard.goBack} disabled={wizard.isSubmitting}>\n {l.back}\n </Button>\n )}\n {wizard.currentStep.optional && !wizard.isLastStep && !wizard.isCommitStep && (\n <Button\n variant=\"ghost\"\n onClick={wizard.skip}\n disabled={wizard.nextBlocked || wizard.isValidating}\n >\n {l.skip}\n </Button>\n )}\n {forwardButton}\n </div>\n </div>\n\n {/* Cancel confirmation */}\n {wizard.showCancelDialog && (\n <Modal onClose={wizard.dismissCancel} labelledBy={cancelTitleId} className=\"space-y-4\">\n <div className=\"space-y-1.5\">\n <h2 id={cancelTitleId} className=\"text-lg font-semibold text-[var(--text-primary)]\">\n {l.cancelTitle}\n </h2>\n <p className=\"text-sm text-[var(--text-muted)]\">{l.confirmCancel}</p>\n </div>\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n <Button variant=\"secondary\" onClick={wizard.dismissCancel}>\n {l.cancelDismissLabel}\n </Button>\n <Button variant=\"danger\" onClick={wizard.confirmCancel}>\n {l.cancelConfirmLabel}\n </Button>\n </div>\n </Modal>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -71,7 +71,13 @@
71
71
  "typecheck": "tsc --noEmit",
72
72
  "lint": "eslint .",
73
73
  "check:tokens": "node scripts/check-token-discipline.mjs",
74
- "check": "npm run typecheck && npm run lint && npm run check:tokens && npm test && npm run build && node scripts/verify-package.mjs && npm run build:showcase",
74
+ "test:coverage": "vitest run --coverage",
75
+ "check:graph": "node scripts/check-module-graph.mjs",
76
+ "check:package": "node scripts/verify-package.mjs",
77
+ "check:tailwind": "node scripts/check-tailwind-scan.mjs",
78
+ "check:security": "npm audit --audit-level=high",
79
+ "check:commits": "node scripts/check-commit-range.cjs",
80
+ "check": "npm run typecheck && npm run lint && npm run check:tokens && npm run test:coverage && npm run build && npm run check:graph && npm run check:package && npm run build:showcase && npm run check:tailwind && npm run check:security && npm run check:commits",
75
81
  "test": "vitest run",
76
82
  "test:watch": "vitest",
77
83
  "build": "tsup && node scripts/fix-esm-extensions.mjs",
@@ -83,7 +89,6 @@
83
89
  "prepublishOnly": "npm run build",
84
90
  "release": "commit-and-tag-version",
85
91
  "release:dry": "commit-and-tag-version --dry-run",
86
- "commitlint": "node scripts/check-commit-range.cjs",
87
92
  "prepare": "husky || true"
88
93
  },
89
94
  "peerDependencies": {
@@ -159,5 +164,9 @@
159
164
  "types": "./dist/index.d.ts",
160
165
  "engines": {
161
166
  "node": ">=20.18"
167
+ },
168
+ "_overrides": "esbuild is forced to ^0.28.2 for GHSA-g7r4-m6w7-qqqr (dev-server file read on Windows, fixed in 0.28.1). vite already allows ^0.28; tsup 8.5.1 still pins ^0.27, and `npm run check` (build + package verification) proves it works on 0.28. Drop this once tsup allows 0.28 itself.",
169
+ "overrides": {
170
+ "esbuild": "^0.28.2"
162
171
  }
163
172
  }
@@ -148,6 +148,14 @@ export interface ChipLinkProps {
148
148
  /** The chip's forwarded ref. React 19 passes it to a function component as a prop. */
149
149
  ref?: Ref<HTMLAnchorElement>;
150
150
  "aria-current"?: "true";
151
+ /**
152
+ * The chip's `onClick`, when it was given one — spread it onto the link with the
153
+ * rest. Carried here so a caller need not close over it twice: keksdose's admin pill
154
+ * in the account menu closes the menu on the way to /admin (account-menu.tsx), and
155
+ * with `onClick` on the Chip its `renderLink` stays the one-liner
156
+ * `({ href, ...p }) => <Link to={href} {...p} />`.
157
+ */
158
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
151
159
  id?: string;
152
160
  title?: string;
153
161
  [key: `aria-${string}`]: string | boolean | number | undefined;
@@ -265,14 +273,16 @@ interface ChipBaseProps {
265
273
 
266
274
  /**
267
275
  * A chip is a link OR a button, never both — so the types say so. Before 0.7.0 both were
268
- * accepted and `onClick` was silently dropped whenever `href` was set; a link that must
269
- * also run code wants a router `Link`-style `onClick` on the anchor, which is a different
270
- * component's job.
276
+ * accepted and `onClick` was silently dropped whenever `href` was set. A LINK may still
277
+ * run code on its way (close the menu it sits in): that is an `onClick` typed for the
278
+ * anchor, on the link shape, and the pill stays a link — no `aria-pressed`, no button.
279
+ * That shape wants a definite `href` string, so a bare `onClick` is always the
280
+ * button's, with a button's event.
271
281
  */
272
282
  export type ChipProps = ChipBaseProps &
273
283
  (
274
284
  | {
275
- /** Renders the chip as a link. Mutually exclusive with `onClick`. */
285
+ /** Renders the chip as a link. */
276
286
  href?: string;
277
287
  onClick?: never;
278
288
  /**
@@ -291,6 +301,19 @@ export type ChipProps = ChipBaseProps &
291
301
  */
292
302
  renderLink?: (props: ChipLinkProps) => ReactElement;
293
303
  }
304
+ | {
305
+ /** The link shape again, with an `onClick`. `href` is a required string here,
306
+ * which is what lets TypeScript tell this shape from the button's: a bare
307
+ * `onClick` can only be the button's, and keeps its button event. */
308
+ href: string;
309
+ /** Runs on the link's click, before the navigation — keksdose's admin pill
310
+ * closes the account menu with it. Reaches a `renderLink` link as
311
+ * {@link ChipLinkProps.onClick}. Not called on a disabled chip, which renders
312
+ * no link. */
313
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
314
+ /** See the link shape above. */
315
+ renderLink?: (props: ChipLinkProps) => ReactElement;
316
+ }
294
317
  | {
295
318
  href?: never;
296
319
  renderLink?: never;
@@ -419,10 +442,13 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
419
442
 
420
443
  // ── The three shapes.
421
444
  if (href && !disabled) {
445
+ // On this shape `onClick` is the link shape's, typed for the anchor.
446
+ const onLinkClick = onClick as ChipLinkProps["onClick"];
422
447
  const linkProps: ChipLinkProps = {
423
448
  ...rest,
424
449
  ref: ref as Ref<HTMLAnchorElement>,
425
450
  href,
451
+ onClick: onLinkClick,
426
452
  "aria-current": selected ? "true" : undefined,
427
453
  className: remove ? inner : look,
428
454
  children: body,
@@ -436,6 +462,7 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
436
462
  ref={ref as React.Ref<HTMLAnchorElement>}
437
463
  href={href}
438
464
  aria-current={selected ? "true" : undefined}
465
+ onClick={onLinkClick}
439
466
  className={remove ? inner : look}
440
467
  {...rest}
441
468
  >
@@ -445,12 +472,15 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
445
472
  return remove ? pill(link) : link;
446
473
  }
447
474
 
448
- if (onClick) {
475
+ // `!href`: a DISABLED link renders no link, and its link-shaped `onClick` must not
476
+ // turn it into a button instead — it falls through to the inert span, as before.
477
+ if (onClick && !href) {
478
+ const onButtonClick = onClick as (event: MouseEvent<HTMLButtonElement>) => void;
449
479
  const button = (
450
480
  <button
451
481
  ref={ref as React.Ref<HTMLButtonElement>}
452
482
  type="button"
453
- onClick={onClick}
483
+ onClick={onButtonClick}
454
484
  disabled={disabled}
455
485
  // Only when `selected` is PASSED: a chip with `onClick` and no `selected` is an
456
486
  // action button (it does something), not a toggle (it is on or off), and
@@ -1,5 +1,5 @@
1
1
  import { Fragment, useEffect, useMemo, useRef, useState } from "react";
2
- import type { ReactNode } from "react";
2
+ import type { CSSProperties, ReactNode, TdHTMLAttributes, ThHTMLAttributes } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import {
5
5
  ArrowDown,
@@ -76,6 +76,65 @@ export interface DataTableColumn<T> {
76
76
  * Default `"asc"`.
77
77
  */
78
78
  firstSort?: SortDir;
79
+ /**
80
+ * Extra attributes for this column's `<td>` in a given row — a `data-*` hook, a
81
+ * `title`, an `aria-describedby`, a class. keksdose's accounts page
82
+ * (accounts-page.tsx, the name and IBAN columns) is why it exists: its demo mode
83
+ * blurs everything marked `data-private`, and with nowhere to put the marker on the
84
+ * cell it wrapped every value in an inner `<span data-private>` — which blurs the
85
+ * text but not the cell, and has to be remembered in every `cell` renderer.
86
+ *
87
+ * MERGED under the kit's own, which win: `className` is joined before the column's
88
+ * `className` (so the column's alignment is still the alignment), `style` is laid
89
+ * under a resized column's pinned width, and `data-col` and a `role` stay the
90
+ * table's — `data-col` is how a resize finds the column's cells, and a `<td>` that
91
+ * claims another role leaves its row's cell count wrong for a screen reader.
92
+ *
93
+ * On a phone the card field (`<dd>`, or the primary line) gets the `data-*`
94
+ * attributes only: the rest — padding classes, `colSpan`, a width — describe a
95
+ * table cell and would mean something else, or nothing, on a card.
96
+ */
97
+ cellProps?: (row: T) => DataTableCellProps | undefined;
98
+ /** The same for the column's `<th>`, once. `scope`, `aria-sort`, `data-col` and the
99
+ * width a resize pins stay the table's, and the class list goes before
100
+ * `headClassName` for the same reason as in {@link cellProps}. */
101
+ headProps?: DataTableHeadProps;
102
+ }
103
+
104
+ /** A `data-*` attribute, typed so `{ "data-private": "" }` needs no cast. */
105
+ type DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };
106
+
107
+ /** What {@link DataTableColumn.cellProps} may return. */
108
+ export type DataTableCellProps = TdHTMLAttributes<HTMLTableCellElement> & DataAttributes;
109
+
110
+ /** What {@link DataTableColumn.headProps} takes. */
111
+ export type DataTableHeadProps = ThHTMLAttributes<HTMLTableCellElement> & DataAttributes;
112
+
113
+ /**
114
+ * Split a caller's cell attributes into what may reach the element and what the kit
115
+ * owns. `role` and `data-col` are dropped outright (see `cellProps`); `className` and
116
+ * `style` come back separately so the kit can merge rather than replace them.
117
+ */
118
+ function splitCellProps(
119
+ props: DataTableCellProps | DataTableHeadProps | undefined,
120
+ ): { attrs: Record<string, unknown>; className?: string; style?: CSSProperties } {
121
+ if (!props) return { attrs: {} };
122
+ const {
123
+ className,
124
+ style,
125
+ role: _role,
126
+ "data-col": _col,
127
+ ...attrs
128
+ } = props as DataTableCellProps & { "data-col"?: unknown };
129
+ return { attrs, className, style };
130
+ }
131
+
132
+ /** Only the `data-*` attributes of a cell's props — what a phone card field takes. */
133
+ function dataAttrsOf(props: DataTableCellProps | undefined): Record<string, unknown> | undefined {
134
+ if (!props) return undefined;
135
+ const out: Record<string, unknown> = {};
136
+ for (const [k, v] of Object.entries(props)) if (k.startsWith("data-") && k !== "data-col") out[k] = v;
137
+ return out;
79
138
  }
80
139
 
81
140
  /** A column resize in flight. Held in a ref rather than in state: the live width is
@@ -1226,7 +1285,12 @@ export function DataTable<T>({
1226
1285
  mobileCard(row)
1227
1286
  ) : (
1228
1287
  <>
1229
- {mobilePrimaryCol && <div className="font-medium">{mobilePrimaryCol.cell(row)}</div>}
1288
+ {/* Only the `data-*` half of `cellProps` — see its note. */}
1289
+ {mobilePrimaryCol && (
1290
+ <div {...dataAttrsOf(mobilePrimaryCol.cellProps?.(row))} className="font-medium">
1291
+ {mobilePrimaryCol.cell(row)}
1292
+ </div>
1293
+ )}
1230
1294
  {mobileSecondaryColumns.length > 0 && (
1231
1295
  <dl
1232
1296
  className={cn(
@@ -1239,7 +1303,10 @@ export function DataTable<T>({
1239
1303
  <dt className="text-xs uppercase tracking-wide text-[var(--text-muted)] self-center">
1240
1304
  {col.header}
1241
1305
  </dt>
1242
- <dd className="min-w-0 text-[var(--text-secondary)] self-center">
1306
+ <dd
1307
+ {...dataAttrsOf(col.cellProps?.(row))}
1308
+ className="min-w-0 text-[var(--text-secondary)] self-center"
1309
+ >
1243
1310
  {col.cell(row)}
1244
1311
  </dd>
1245
1312
  </Fragment>
@@ -1526,8 +1593,11 @@ export function DataTable<T>({
1526
1593
  const headClass = logicalAlign(col.headClassName);
1527
1594
  const isEndAligned = !!headClass && END_ALIGN.test(headClass);
1528
1595
  const width = widths[col.key];
1596
+ const ownHead = splitCellProps(col.headProps);
1597
+ const pinnedHead = width ? { width, minWidth: width, maxWidth: width } : undefined;
1529
1598
  return (
1530
1599
  <th
1600
+ {...ownHead.attrs}
1531
1601
  key={col.key}
1532
1602
  ref={(el) => {
1533
1603
  if (el) headRefs.set(col.key, el);
@@ -1554,7 +1624,7 @@ export function DataTable<T>({
1554
1624
  : "none"
1555
1625
  : undefined
1556
1626
  }
1557
- style={width ? { width, minWidth: width, maxWidth: width } : undefined}
1627
+ style={ownHead.style || pinnedHead ? { ...ownHead.style, ...pinnedHead } : undefined}
1558
1628
  className={cn(
1559
1629
  "relative font-medium align-middle whitespace-nowrap",
1560
1630
  compact ? "px-2 py-1" : "px-3 py-2",
@@ -1562,6 +1632,7 @@ export function DataTable<T>({
1562
1632
  // Each <th> carries its own bg so the row doesn't render
1563
1633
  // transparent over the data rows underneath.
1564
1634
  "sticky top-0 z-10 bg-[var(--bg-surface-2)] backdrop-blur-sm",
1635
+ logicalAlign(ownHead.className),
1565
1636
  headClass,
1566
1637
  )}
1567
1638
  >
@@ -1795,15 +1866,20 @@ export function DataTable<T>({
1795
1866
  )}
1796
1867
  {visibleColumns.map((col) => {
1797
1868
  const width = widths[col.key];
1869
+ // The caller's first, the table's over it — see `cellProps`.
1870
+ const own = splitCellProps(col.cellProps?.(row));
1871
+ const pinned = width ? { width, minWidth: width, maxWidth: width } : undefined;
1798
1872
  return (
1799
1873
  <td
1874
+ {...own.attrs}
1800
1875
  key={col.key}
1801
1876
  data-col={col.key}
1802
- style={width ? { width, minWidth: width, maxWidth: width } : undefined}
1877
+ style={own.style || pinned ? { ...own.style, ...pinned } : undefined}
1803
1878
  className={cn(
1804
1879
  "align-top",
1805
1880
  compact ? "px-2 py-1" : "px-3 py-2",
1806
1881
  width && "overflow-hidden text-ellipsis",
1882
+ logicalAlign(own.className),
1807
1883
  logicalAlign(col.className),
1808
1884
  )}
1809
1885
  >