@eifi1/ui-kit 0.8.1 → 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.
- package/README.md +4 -0
- package/dist/components/amount-input.d.ts +2 -0
- package/dist/components/calculator.d.ts +2 -0
- package/dist/components/chip.d.ts +27 -5
- package/dist/components/chip.js +6 -2
- package/dist/components/chip.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +122 -91
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +69 -3
- package/dist/components/date-picker.js +152 -65
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/disclosure.d.ts +11 -2
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/floating-panel.d.ts +29 -2
- package/dist/components/floating-panel.js +16 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/number-field.d.ts +2 -0
- package/dist/components/number-input.d.ts +2 -0
- package/dist/components/numpad-sheet.d.ts +2 -0
- package/dist/components/progress-bar.d.ts +16 -2
- package/dist/components/progress-bar.js +4 -2
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/ui.d.ts +37 -14
- package/dist/components/ui.js +28 -22
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-overlay-history.js +84 -4
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +2 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
- package/dist/i18n/locales/de-CH.d.ts +2 -0
- package/dist/i18n/locales/de-informal.d.ts +2 -0
- package/dist/i18n/locales/de.d.ts +2 -0
- package/dist/i18n/locales/de.js +10 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -0
- package/dist/i18n/locales/es.js +10 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -0
- package/dist/i18n/locales/fr.js +10 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -0
- package/dist/i18n/locales/hu.js +10 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -0
- package/dist/i18n/locales/it.js +10 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +10 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/search/command-palette.d.ts +49 -1
- package/dist/search/command-palette.js +132 -29
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +141 -0
- package/dist/search/global-search.js +277 -0
- package/dist/search/global-search.js.map +1 -0
- package/dist/search/search-index.d.ts +105 -0
- package/dist/search/search-index.js +142 -0
- package/dist/search/search-index.js.map +1 -0
- package/dist/search.d.ts +2 -0
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -1
- package/dist/wizard/stepper-nav.js +2 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/package.json +12 -3
- package/src/components/chip.tsx +36 -6
- package/src/components/data-table.tsx +81 -5
- package/src/components/date-picker.tsx +328 -117
- package/src/components/disclosure.tsx +24 -8
- package/src/components/floating-panel.tsx +46 -2
- package/src/components/progress-bar.tsx +27 -3
- package/src/components/ui.tsx +88 -37
- package/src/hooks/use-overlay-history.ts +181 -4
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -0
- package/src/i18n/locales/de.ts +10 -0
- package/src/i18n/locales/es.ts +10 -0
- package/src/i18n/locales/fr.ts +10 -0
- package/src/i18n/locales/hu.ts +10 -0
- package/src/i18n/locales/it.ts +10 -0
- package/src/i18n/locales/zh.ts +10 -0
- package/src/index.ts +4 -0
- package/src/search/command-palette.tsx +200 -27
- package/src/search/global-search.tsx +484 -0
- package/src/search/search-index.ts +295 -0
- package/src/search.ts +2 -0
- 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.
|
|
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
|
-
"
|
|
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
|
}
|
package/src/components/chip.tsx
CHANGED
|
@@ -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
|
|
269
|
-
*
|
|
270
|
-
*
|
|
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.
|
|
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
|
-
|
|
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={
|
|
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
|
-
{
|
|
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
|
|
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={
|
|
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={
|
|
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
|
>
|