@eifi1/ui-kit 0.11.0 → 0.12.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 +57 -32
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +24 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- 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-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +102 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +23 -2
- package/dist/components/list.js +32 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +15 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +13 -0
- package/dist/components/toggle-group.js +35 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +215 -23
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +156 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +153 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +153 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +153 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +153 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +153 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +37 -17
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +12 -1
- package/dist/shell/topbar-action-menu.js +16 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +224 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +65 -16
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +248 -0
- package/src/components/toggle-group.tsx +58 -10
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/ui.tsx +426 -36
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +102 -8
- package/src/i18n/locales/de.ts +153 -0
- package/src/i18n/locales/es.ts +150 -0
- package/src/i18n/locales/fr.ts +153 -0
- package/src/i18n/locales/hu.ts +151 -0
- package/src/i18n/locales/it.ts +151 -0
- package/src/i18n/locales/zh.ts +150 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +21 -0
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -2,7 +2,8 @@ import { Fragment, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
3
3
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
6
|
+
import { pickLinkRenderer, RenderedKitLink } from "./text-link";
|
|
6
7
|
|
|
7
8
|
export interface BreadcrumbsLabels {
|
|
8
9
|
/** The `nav` landmark's name — what a screen reader lists it as. */
|
|
@@ -35,7 +36,8 @@ export interface BreadcrumbLinkProps {
|
|
|
35
36
|
|
|
36
37
|
export interface BreadcrumbsProps extends Omit<ComponentPropsWithoutRef<"nav">, "children"> {
|
|
37
38
|
items: BreadcrumbItem[];
|
|
38
|
-
/** Your router's link. Default `<
|
|
39
|
+
/** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app
|
|
40
|
+
* `href`), then `<a>`. The API `Chip`, `StatTile` and `ListItem` share. */
|
|
39
41
|
renderLink?: (props: BreadcrumbLinkProps) => ReactElement;
|
|
40
42
|
/**
|
|
41
43
|
* The mark between crumbs. Default a chevron, mirrored in RTL — a chevron pointing
|
|
@@ -78,6 +80,7 @@ export function Breadcrumbs({
|
|
|
78
80
|
...rest
|
|
79
81
|
}: BreadcrumbsProps) {
|
|
80
82
|
const labels = useKitLabels("breadcrumbs", DEFAULT_BREADCRUMBS_LABELS, labelsProp);
|
|
83
|
+
const kitLink = useKitLink();
|
|
81
84
|
const [expanded, setExpanded] = useState(false);
|
|
82
85
|
// The "…" button vanishes when pressed; focus goes to the first crumb it revealed,
|
|
83
86
|
// not back to the top of the document.
|
|
@@ -121,8 +124,11 @@ export function Breadcrumbs({
|
|
|
121
124
|
);
|
|
122
125
|
} else if (item.href !== undefined) {
|
|
123
126
|
const props: BreadcrumbLinkProps = { href: item.href, className: linkClass, children: item.label };
|
|
127
|
+
const kit = pickLinkRenderer<BreadcrumbLinkProps>(undefined, kitLink, item.href);
|
|
124
128
|
content = renderLink ? (
|
|
125
129
|
renderLink(props)
|
|
130
|
+
) : kit ? (
|
|
131
|
+
<RenderedKitLink render={kit} props={props} />
|
|
126
132
|
) : (
|
|
127
133
|
<a href={props.href} className={props.className}>
|
|
128
134
|
{props.children}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
3
|
import { buttonClasses, type ButtonSize, type ButtonVariant } from "./ui";
|
|
4
|
+
import { useKitLink } from "../i18n/kit-labels";
|
|
5
|
+
import { pickLinkRenderer } from "./text-link";
|
|
4
6
|
|
|
5
7
|
// Every class below is spelled out in full rather than assembled from parts: the
|
|
6
8
|
// consumer's Tailwind build finds classes by scanning this package's source as text,
|
|
@@ -147,7 +149,8 @@ export interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<"a">
|
|
|
147
149
|
/** The member for the page you are on: `aria-current="page"` and the "on" look of a
|
|
148
150
|
* pressed member, so a link row reads as a segmented switch between pages. */
|
|
149
151
|
current?: boolean;
|
|
150
|
-
/** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.
|
|
152
|
+
/** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.
|
|
153
|
+
* Default: the `<UiKitProvider linkComponent>`, then `<a>`. */
|
|
151
154
|
renderLink?: (props: ButtonGroupLinkRenderProps) => ReactElement;
|
|
152
155
|
className?: string;
|
|
153
156
|
children: ReactNode;
|
|
@@ -174,6 +177,7 @@ export function ButtonGroupLink({
|
|
|
174
177
|
children,
|
|
175
178
|
...rest
|
|
176
179
|
}: ButtonGroupLinkProps) {
|
|
180
|
+
const kitLink = useKitLink();
|
|
177
181
|
const props: ButtonGroupLinkRenderProps = {
|
|
178
182
|
...rest,
|
|
179
183
|
href,
|
|
@@ -181,7 +185,8 @@ export function ButtonGroupLink({
|
|
|
181
185
|
className: buttonClasses(variant, { size, className: cn(current && LINK_CURRENT, className) }),
|
|
182
186
|
children,
|
|
183
187
|
};
|
|
184
|
-
|
|
188
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
189
|
+
if (render) return <RenderedGroupLink render={render} {...props} />;
|
|
185
190
|
const { children: content, ...anchor } = props;
|
|
186
191
|
return <a {...anchor}>{content}</a>;
|
|
187
192
|
}
|
|
@@ -4,6 +4,8 @@ import { cn } from "../lib/cn";
|
|
|
4
4
|
import { dirOf, horizontalStep } from "../lib/direction";
|
|
5
5
|
import { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from "../lib/dates";
|
|
6
6
|
import { useKitLabels, useKitLocale, useKitWeekStart } from "../i18n/kit-labels";
|
|
7
|
+
import { lerpHex, textOn } from "../theme/chart-palette";
|
|
8
|
+
import { parseHex } from "../theme/color";
|
|
7
9
|
import { Tooltip } from "./tooltip";
|
|
8
10
|
import type { WeekDay } from "./mini-calendar";
|
|
9
11
|
|
|
@@ -103,6 +105,39 @@ export interface CalendarHeatmapProps extends Omit<ComponentPropsWithoutRef<"div
|
|
|
103
105
|
/** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this
|
|
104
106
|
* mixed into `--bg-surface`, so the ramp follows the theme. */
|
|
105
107
|
color?: string;
|
|
108
|
+
/**
|
|
109
|
+
* A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest
|
|
110
|
+
* day), instead of `levels` steps of `color` — keksdose's palette-store ramp,
|
|
111
|
+
* `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`
|
|
112
|
+
* colours are interpolated in JS, so the fill is a concrete hex and the day number's
|
|
113
|
+
* ink (in `"month"`) is MEASURED against it; anything else (a `var()`) is mixed with
|
|
114
|
+
* `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.
|
|
115
|
+
*/
|
|
116
|
+
colorFrom?: string;
|
|
117
|
+
colorTo?: string;
|
|
118
|
+
/**
|
|
119
|
+
* A fill of the caller's own for a day with a value: handed the value and the
|
|
120
|
+
* scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.
|
|
121
|
+
* Also paints the legend, at `max × level / levels`.
|
|
122
|
+
*/
|
|
123
|
+
fill?: (value: number, max: number) => string;
|
|
124
|
+
/** The fill of a day with no value (or a value of 0). Default: a wash of the text
|
|
125
|
+
* colour over the surface, which reads as a cell on both themes. */
|
|
126
|
+
emptyColor?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:
|
|
129
|
+
*
|
|
130
|
+
* - `"to"` (default): on `to`.
|
|
131
|
+
* - `"today"`: on today.
|
|
132
|
+
* - `"latest"`: on the last day that can carry a figure — today, or a later day that
|
|
133
|
+
* actually has a value. A window running into the future (a report's "all" preset
|
|
134
|
+
* ends a year ahead) then spends its cells on the days a person is looking for,
|
|
135
|
+
* not on a year of empty ones.
|
|
136
|
+
* - a `"YYYY-MM-DD"`: on that day.
|
|
137
|
+
*
|
|
138
|
+
* Always kept inside `from`…`to`.
|
|
139
|
+
*/
|
|
140
|
+
anchor?: "to" | "today" | "latest" | (string & {});
|
|
106
141
|
/**
|
|
107
142
|
* The most days to draw. A longer window keeps its LATEST `maxDays` and says how many
|
|
108
143
|
* it left out (`labels.truncated`) rather than silently ending. Default: no cap.
|
|
@@ -131,6 +166,26 @@ const EMPTY_FILL = "color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface
|
|
|
131
166
|
|
|
132
167
|
const weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;
|
|
133
168
|
|
|
169
|
+
/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.
|
|
170
|
+
* `latestData` is the latest ISO date carrying a value, or `""`. */
|
|
171
|
+
export function heatmapWindowEnd(
|
|
172
|
+
from: string,
|
|
173
|
+
to: string,
|
|
174
|
+
anchor: string,
|
|
175
|
+
latestData: string,
|
|
176
|
+
today: string = toLocalIso(new Date()),
|
|
177
|
+
): string {
|
|
178
|
+
let end: string;
|
|
179
|
+
if (anchor === "today") end = today;
|
|
180
|
+
else if (anchor === "latest") end = latestData > today ? latestData : today;
|
|
181
|
+
else if (anchor === "to" || !parseIsoDate(anchor)) end = to;
|
|
182
|
+
else end = anchor;
|
|
183
|
+
// ISO dates sort as strings, so the clamp is plain comparison.
|
|
184
|
+
if (end > to) end = to;
|
|
185
|
+
if (end < from) end = from;
|
|
186
|
+
return end;
|
|
187
|
+
}
|
|
188
|
+
|
|
134
189
|
/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */
|
|
135
190
|
export function heatmapLevel(value: number, max: number, levels: number): number {
|
|
136
191
|
if (!(value > 0) || !(max > 0) || levels < 1) return 0;
|
|
@@ -159,7 +214,9 @@ function levelFill(level: number, levels: number, color: string): string {
|
|
|
159
214
|
* the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go
|
|
160
215
|
* to the window's first/last day. Each cell is named with its whole date AND its value,
|
|
161
216
|
* because the colour is not something a screen reader can say, and carries
|
|
162
|
-
* `data-day="YYYY-MM-DD"` as its identity for tests and for the host.
|
|
217
|
+
* `data-day="YYYY-MM-DD"` as its identity for tests and for the host. Note that in
|
|
218
|
+
* `"weeks"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all
|
|
219
|
+
* Tuesdays…), not by date — query by `data-day`, not by position.
|
|
163
220
|
*
|
|
164
221
|
* The tooltip is portalled (a year of squares always sits in a sideways scroller) and
|
|
165
222
|
* `data-private` unless `sensitive={false}`. Cell size in `"weeks"` is the CSS variable
|
|
@@ -178,6 +235,11 @@ export function CalendarHeatmap({
|
|
|
178
235
|
max: maxProp,
|
|
179
236
|
levels = 4,
|
|
180
237
|
color = "var(--brand)",
|
|
238
|
+
colorFrom,
|
|
239
|
+
colorTo,
|
|
240
|
+
fill: fillProp,
|
|
241
|
+
emptyColor,
|
|
242
|
+
anchor = "to",
|
|
181
243
|
maxDays,
|
|
182
244
|
legend = true,
|
|
183
245
|
locale: localeProp,
|
|
@@ -202,9 +264,16 @@ export function CalendarHeatmap({
|
|
|
202
264
|
|
|
203
265
|
// The window, trimmed from the FRONT: the days a person looks for are the recent
|
|
204
266
|
// ones, and a cap that kept the oldest would end the calendar before today.
|
|
267
|
+
const latestData = useMemo(() => {
|
|
268
|
+
let latest = "";
|
|
269
|
+
for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;
|
|
270
|
+
return latest;
|
|
271
|
+
}, [values]);
|
|
272
|
+
const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);
|
|
273
|
+
|
|
205
274
|
const { days, dropped } = useMemo(() => {
|
|
206
275
|
const start = parseIsoDate(from);
|
|
207
|
-
const end = parseIsoDate(
|
|
276
|
+
const end = parseIsoDate(windowEnd);
|
|
208
277
|
const list: Date[] = [];
|
|
209
278
|
if (!start || !end || end < start) return { days: list, dropped: 0 };
|
|
210
279
|
const cursor = new Date(end);
|
|
@@ -216,7 +285,7 @@ export function CalendarHeatmap({
|
|
|
216
285
|
const first = list[0];
|
|
217
286
|
const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;
|
|
218
287
|
return { days: list, dropped: Math.max(0, trimmed) };
|
|
219
|
-
}, [from,
|
|
288
|
+
}, [from, windowEnd, maxDays]);
|
|
220
289
|
|
|
221
290
|
const firstIso = days.length ? toLocalIso(days[0]) : "";
|
|
222
291
|
const lastIso = days.length ? toLocalIso(days[days.length - 1]) : "";
|
|
@@ -341,13 +410,43 @@ export function CalendarHeatmap({
|
|
|
341
410
|
};
|
|
342
411
|
|
|
343
412
|
const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);
|
|
413
|
+
const empty = emptyColor ?? EMPTY_FILL;
|
|
414
|
+
const continuous = colorFrom !== undefined && colorTo !== undefined;
|
|
415
|
+
const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;
|
|
416
|
+
|
|
417
|
+
/** A day's fill, and the ink its number is set in (`"month"`). */
|
|
418
|
+
const paint = (value: number, level: number): { fill: string; ink: string } => {
|
|
419
|
+
if (level === 0) return { fill: empty, ink: "var(--text-primary)" };
|
|
420
|
+
if (fillProp || continuous) {
|
|
421
|
+
const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;
|
|
422
|
+
const fill = fillProp
|
|
423
|
+
? fillProp(value, max)
|
|
424
|
+
: hexRamp
|
|
425
|
+
? lerpHex(colorFrom!, colorTo!, t)
|
|
426
|
+
: `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;
|
|
427
|
+
// Measured where the fill is a colour; a mix of references cannot be measured,
|
|
428
|
+
// and the page's own text colour is the safe answer there.
|
|
429
|
+
const measured = textOn(fill);
|
|
430
|
+
return { fill, ink: measured === "currentColor" ? "var(--text-primary)" : measured };
|
|
431
|
+
}
|
|
432
|
+
// Light text once the fill is the stronger half of the mix — `--bg-surface` is the
|
|
433
|
+
// colour the scale is mixed INTO, so it is the one that contrasts with its far end
|
|
434
|
+
// on either theme.
|
|
435
|
+
const strong = levelPercent(level, levels) >= 45;
|
|
436
|
+
return {
|
|
437
|
+
fill: levelFill(level, levels, color),
|
|
438
|
+
ink: strong ? "var(--bg-surface)" : "var(--text-primary)",
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
// The legend's swatches: a value that lands on each step (the top one on `max`).
|
|
442
|
+
const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;
|
|
344
443
|
|
|
345
444
|
const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {
|
|
346
445
|
const iso = toLocalIso(d);
|
|
347
446
|
const raw = values.get(iso);
|
|
348
447
|
const value = raw ?? 0;
|
|
349
448
|
const level = heatmapLevel(value, max, levels);
|
|
350
|
-
const fill =
|
|
449
|
+
const { fill, ink } = paint(value, level);
|
|
351
450
|
const formattedDate = formatDay(d);
|
|
352
451
|
const formattedValue = formatValue(value);
|
|
353
452
|
const name = labels.day(formattedDate, formattedValue);
|
|
@@ -355,10 +454,6 @@ export function CalendarHeatmap({
|
|
|
355
454
|
? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })
|
|
356
455
|
: name;
|
|
357
456
|
const isSelected = selected === iso;
|
|
358
|
-
// Light text once the fill is the stronger half of the mix — `--bg-surface` is the
|
|
359
|
-
// colour the scale is mixed INTO, so it is the one that contrasts with its far end
|
|
360
|
-
// on either theme.
|
|
361
|
-
const strong = level > 0 && levelPercent(level, levels) >= 45;
|
|
362
457
|
const shared = {
|
|
363
458
|
role: "gridcell",
|
|
364
459
|
"data-day": iso,
|
|
@@ -370,7 +465,7 @@ export function CalendarHeatmap({
|
|
|
370
465
|
onFocus: () => {
|
|
371
466
|
if (iso !== activeIso) setActiveIso(iso);
|
|
372
467
|
},
|
|
373
|
-
style: { background: fill, color:
|
|
468
|
+
style: { background: fill, color: ink },
|
|
374
469
|
className: cn(
|
|
375
470
|
cellClass,
|
|
376
471
|
isSelected && "ring-2 ring-[var(--text-primary)]",
|
|
@@ -493,7 +588,7 @@ export function CalendarHeatmap({
|
|
|
493
588
|
key={l}
|
|
494
589
|
data-legend-level={l}
|
|
495
590
|
className="size-2.5 rounded-sm"
|
|
496
|
-
style={{ background:
|
|
591
|
+
style={{ background: legendFill(l) }}
|
|
497
592
|
/>
|
|
498
593
|
))}
|
|
499
594
|
<span className="ms-0.5">{labels.more}</span>
|
package/src/components/chip.tsx
CHANGED
|
@@ -5,7 +5,8 @@ import type { LucideIcon } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { horizontalStep } from "../lib/direction";
|
|
7
7
|
import { FIELD_INVALID } from "./ui";
|
|
8
|
-
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
8
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels";
|
|
9
|
+
import { pickLinkRenderer } from "./text-link";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A chip: a compact pill carrying one value.
|
|
@@ -384,7 +385,8 @@ export type ChipProps = ChipBaseProps &
|
|
|
384
385
|
/**
|
|
385
386
|
* Renders the link for `href` — pass your router's `<Link>` here, since a
|
|
386
387
|
* plain `<a>` reloads a single-page app (keksdose's account-menu admin pill).
|
|
387
|
-
* Default: `<a>`. Ignored without
|
|
388
|
+
* Default: the `<UiKitProvider linkComponent>`, then `<a>`. Ignored without
|
|
389
|
+
* `href`.
|
|
388
390
|
*
|
|
389
391
|
* A render function rather than an `as={Link}`, for three reasons. It is the
|
|
390
392
|
* API `StatTile` already has (`renderLink`), so a consumer learns it once.
|
|
@@ -455,6 +457,7 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
|
|
|
455
457
|
ref,
|
|
456
458
|
) {
|
|
457
459
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
|
|
460
|
+
const kitLink = useKitLink();
|
|
458
461
|
const s = SIZE[size];
|
|
459
462
|
const interactive = !!href || !!onClick;
|
|
460
463
|
// One radius for every rounded piece — the pill, the body inside a split pill, the ×
|
|
@@ -570,10 +573,11 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
|
|
|
570
573
|
className: remove ? inner : look,
|
|
571
574
|
children: body,
|
|
572
575
|
};
|
|
573
|
-
const
|
|
576
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
577
|
+
const link = render ? (
|
|
574
578
|
// Through a component rather than called here, so the forwarded ref arrives as
|
|
575
579
|
// an ordinary prop of an element and is never handed to a function mid-render.
|
|
576
|
-
<RenderedLink render={
|
|
580
|
+
<RenderedLink render={render} {...linkProps} />
|
|
577
581
|
) : (
|
|
578
582
|
<a
|
|
579
583
|
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useRef } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ButtonHTMLAttributes,
|
|
4
|
+
ChangeEvent,
|
|
5
|
+
ComponentType,
|
|
6
|
+
InputHTMLAttributes,
|
|
7
|
+
MouseEvent,
|
|
8
|
+
ReactElement,
|
|
9
|
+
ReactNode,
|
|
10
|
+
Ref,
|
|
11
|
+
} from "react";
|
|
3
12
|
import { Check, Minus } from "lucide-react";
|
|
4
13
|
import { cn } from "../lib/cn";
|
|
5
14
|
import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
15
|
+
import { useKitLink } from "../i18n/kit-labels";
|
|
16
|
+
import { pickLinkRenderer } from "./text-link";
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
19
|
* A checkbox or a radio, presented as a bordered card: a title, a line of description,
|
|
@@ -231,6 +242,30 @@ const META_TONE: Record<ActionCardMetaTone, string> = {
|
|
|
231
242
|
success: "text-[var(--success)]",
|
|
232
243
|
};
|
|
233
244
|
|
|
245
|
+
/** The colour of {@link ActionCardProps.icon}. `default` is the secondary text colour
|
|
246
|
+
* the icon has always had; `brand` tints it (keksdose's privacy enrolment). */
|
|
247
|
+
export type ActionCardIconTone = "default" | "muted" | "brand" | "warning" | "danger" | "info" | "success";
|
|
248
|
+
|
|
249
|
+
const ICON_TONE: Record<ActionCardIconTone, string> = {
|
|
250
|
+
default: "text-[var(--text-secondary)]",
|
|
251
|
+
...META_TONE,
|
|
252
|
+
brand: "text-[var(--brand)]",
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** What {@link ActionCardProps.renderLink} (and the provider's `linkComponent`) is
|
|
256
|
+
* handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */
|
|
257
|
+
export interface ActionCardLinkProps {
|
|
258
|
+
href: string;
|
|
259
|
+
/** The card's whole look — keep it. */
|
|
260
|
+
className: string;
|
|
261
|
+
children: ReactNode;
|
|
262
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
263
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
264
|
+
id?: string;
|
|
265
|
+
[key: `aria-${string}`]: string | boolean | number | undefined;
|
|
266
|
+
[key: `data-${string}`]: unknown;
|
|
267
|
+
}
|
|
268
|
+
|
|
234
269
|
export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "title"> {
|
|
235
270
|
/** What the card does — the button's accessible name. */
|
|
236
271
|
title: ReactNode;
|
|
@@ -247,6 +282,21 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
|
|
|
247
282
|
metaTone?: ActionCardMetaTone;
|
|
248
283
|
/** A Lucide icon (or any component taking a `className`), shown at the card's start. */
|
|
249
284
|
icon?: ChoiceCardProps["icon"];
|
|
285
|
+
/** The icon's colour. Default `default` (the secondary text colour). */
|
|
286
|
+
iconTone?: ActionCardIconTone;
|
|
287
|
+
/** Classes on the icon, after {@link iconTone} — a size, a tinted tile behind it. */
|
|
288
|
+
iconClassName?: string;
|
|
289
|
+
/**
|
|
290
|
+
* Makes the card a LINK to another page rather than a button that acts: kastlan's
|
|
291
|
+
* section tiles on the group overview (group-overview-page), each an icon, a name and
|
|
292
|
+
* a line on what is there. A real `<a href>`, so a middle click opens it in a new
|
|
293
|
+
* tab. A `disabled` card renders the inert button instead — a link cannot be
|
|
294
|
+
* disabled. The ref then reaches the `<a>`.
|
|
295
|
+
*/
|
|
296
|
+
href?: string;
|
|
297
|
+
/** Your router's link for an in-app `href`. Default: the `<UiKitProvider
|
|
298
|
+
* linkComponent>`, then a plain `<a>`. Ignored without `href`. */
|
|
299
|
+
renderLink?: (props: ActionCardLinkProps) => ReactElement;
|
|
250
300
|
}
|
|
251
301
|
|
|
252
302
|
/**
|
|
@@ -254,7 +304,8 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
|
|
|
254
304
|
* card's icon, title and description, plus a `meta` line, that runs `onClick` the
|
|
255
305
|
* moment it is pressed. keksdose's privacy enrolment (privacy-enroll-dialog:345,
|
|
256
306
|
* `CustodyOption`) offers two custody modes this way — picking one IS the next step,
|
|
257
|
-
* so there is no checked state to show and no "Continue" to press after it.
|
|
307
|
+
* so there is no checked state to show and no "Continue" to press after it. With
|
|
308
|
+
* `href` it is the same card as a link (kastlan's overview section tiles).
|
|
258
309
|
*
|
|
259
310
|
* A separate component rather than `ChoiceCard as="button"`: a ChoiceCard is an
|
|
260
311
|
* `<input>` (its ref, its `checked`, its form value), and a button shares none of it.
|
|
@@ -262,9 +313,24 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
|
|
|
262
313
|
* "Keep the key yourself, button" and then the explanation, not one long name.
|
|
263
314
|
*/
|
|
264
315
|
export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(function ActionCard(
|
|
265
|
-
{
|
|
316
|
+
{
|
|
317
|
+
title,
|
|
318
|
+
description,
|
|
319
|
+
meta,
|
|
320
|
+
metaTone = "muted",
|
|
321
|
+
icon: Icon,
|
|
322
|
+
iconTone = "default",
|
|
323
|
+
iconClassName,
|
|
324
|
+
href,
|
|
325
|
+
renderLink,
|
|
326
|
+
className,
|
|
327
|
+
id,
|
|
328
|
+
type = "button",
|
|
329
|
+
...rest
|
|
330
|
+
},
|
|
266
331
|
ref,
|
|
267
332
|
) {
|
|
333
|
+
const kitLink = useKitLink();
|
|
268
334
|
const generated = useId();
|
|
269
335
|
const baseId = id ?? generated;
|
|
270
336
|
const titleId = `${baseId}-title`;
|
|
@@ -272,28 +338,23 @@ export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(functio
|
|
|
272
338
|
const metaId = `${baseId}-meta`;
|
|
273
339
|
const showDescription = hasMessage(description);
|
|
274
340
|
const showMeta = hasMessage(meta);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]",
|
|
293
|
-
className,
|
|
294
|
-
)}
|
|
295
|
-
>
|
|
296
|
-
{Icon && <Icon aria-hidden className="mt-0.5 size-5 shrink-0 text-[var(--text-secondary)]" />}
|
|
341
|
+
const look = cn(
|
|
342
|
+
"flex w-full items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 text-start shadow-sm transition-colors",
|
|
343
|
+
"hover:border-[var(--brand)] hover:bg-[var(--bg-hover)]",
|
|
344
|
+
// The outline, as on ChoiceCard: the card is what the eye is on.
|
|
345
|
+
"focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
346
|
+
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]",
|
|
347
|
+
className,
|
|
348
|
+
);
|
|
349
|
+
const labelledBy = rest["aria-labelledby"] ?? titleId;
|
|
350
|
+
const describedBy = mergeDescribedBy(
|
|
351
|
+
rest["aria-describedby"],
|
|
352
|
+
showDescription && descriptionId,
|
|
353
|
+
showMeta && metaId,
|
|
354
|
+
);
|
|
355
|
+
const body = (
|
|
356
|
+
<>
|
|
357
|
+
{Icon && <Icon aria-hidden className={cn("mt-0.5 size-5 shrink-0", ICON_TONE[iconTone], iconClassName)} />}
|
|
297
358
|
<span className="min-w-0 flex-1 space-y-1">
|
|
298
359
|
<span id={titleId} className="block text-sm font-medium leading-5 text-[var(--text-primary)]">
|
|
299
360
|
{title}
|
|
@@ -309,11 +370,61 @@ export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(functio
|
|
|
309
370
|
</span>
|
|
310
371
|
)}
|
|
311
372
|
</span>
|
|
373
|
+
</>
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (href !== undefined && !rest.disabled) {
|
|
377
|
+
// The button-only attributes stay behind; `aria-*`, `data-*` and handlers ride along.
|
|
378
|
+
const {
|
|
379
|
+
disabled: _disabled,
|
|
380
|
+
form: _form,
|
|
381
|
+
formAction: _formAction,
|
|
382
|
+
name: _name,
|
|
383
|
+
value: _value,
|
|
384
|
+
onClick,
|
|
385
|
+
...anchorRest
|
|
386
|
+
} = rest;
|
|
387
|
+
const linkProps: ActionCardLinkProps = {
|
|
388
|
+
...(anchorRest as Partial<ActionCardLinkProps>),
|
|
389
|
+
ref: ref as unknown as Ref<HTMLAnchorElement>,
|
|
390
|
+
id,
|
|
391
|
+
href,
|
|
392
|
+
"aria-labelledby": labelledBy,
|
|
393
|
+
"aria-describedby": describedBy,
|
|
394
|
+
onClick: onClick as unknown as ActionCardLinkProps["onClick"],
|
|
395
|
+
className: look,
|
|
396
|
+
children: body,
|
|
397
|
+
};
|
|
398
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
399
|
+
if (render) return <RenderedActionLink render={render} {...linkProps} />;
|
|
400
|
+
const { children: content, ...anchor } = linkProps;
|
|
401
|
+
return <a {...anchor}>{content}</a>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<button
|
|
406
|
+
ref={ref}
|
|
407
|
+
id={id}
|
|
408
|
+
type={type}
|
|
409
|
+
{...rest}
|
|
410
|
+
aria-labelledby={labelledBy}
|
|
411
|
+
aria-describedby={describedBy}
|
|
412
|
+
className={look}
|
|
413
|
+
>
|
|
414
|
+
{body}
|
|
312
415
|
</button>
|
|
313
416
|
);
|
|
314
417
|
});
|
|
315
418
|
ActionCard.displayName = "ActionCard";
|
|
316
419
|
|
|
420
|
+
/** Calls the link renderer as a component, so a router link's hooks are its own. */
|
|
421
|
+
function RenderedActionLink({
|
|
422
|
+
render,
|
|
423
|
+
...props
|
|
424
|
+
}: ActionCardLinkProps & { render: (props: ActionCardLinkProps) => ReactElement }) {
|
|
425
|
+
return render(props);
|
|
426
|
+
}
|
|
427
|
+
|
|
317
428
|
/* ── ChoiceCardGroup ─────────────────────────────────────────────────────── */
|
|
318
429
|
|
|
319
430
|
export interface ChoiceCardOption<T extends string> {
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "react";
|
|
11
11
|
import { Check, Plus } from "lucide-react";
|
|
12
12
|
import { cn } from "../lib/cn";
|
|
13
|
+
import { CLIPS_ATTRIBUTE } from "../lib/clipping";
|
|
13
14
|
import { DropdownSearchHeader } from "./dropdown";
|
|
14
15
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
|
|
15
16
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
@@ -636,6 +637,10 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
636
637
|
// The rows are provisional while a lookup is in flight — whether or not the
|
|
637
638
|
// previous query's are still showing.
|
|
638
639
|
aria-busy={busy || undefined}
|
|
640
|
+
// It scrolls, and says so where jsdom can see it (see `CLIPS_ATTRIBUTE`): a
|
|
641
|
+
// Tooltip in a row then portals out in a test exactly as it does in the
|
|
642
|
+
// browser, instead of joining the option's accessible name (keksdose E8).
|
|
643
|
+
{...{ [CLIPS_ATTRIBUTE]: "" }}
|
|
639
644
|
className={cn("min-h-0 flex-1 overflow-y-auto py-1", isPhone && "flex-none")}
|
|
640
645
|
>
|
|
641
646
|
{results.map((o, i) => {
|
|
@@ -113,7 +113,7 @@ function SuggestionList({
|
|
|
113
113
|
dir={dir}
|
|
114
114
|
style={{ top, left: rect.left, width: rect.width }}
|
|
115
115
|
>
|
|
116
|
-
<ul id={id} role="listbox" className={LIST_CLASS} style={{ maxHeight }}>
|
|
116
|
+
<ul id={id} role="listbox" data-clips="" className={LIST_CLASS} style={{ maxHeight }}>
|
|
117
117
|
{children}
|
|
118
118
|
</ul>
|
|
119
119
|
</div>,
|
|
@@ -238,9 +238,19 @@ export function Combobox({
|
|
|
238
238
|
onBlur,
|
|
239
239
|
onSubmit,
|
|
240
240
|
"aria-label": ariaLabel,
|
|
241
|
+
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
242
|
+
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
243
|
+
// on the wrapper div the hint and required state described nothing.
|
|
244
|
+
"aria-describedby": ariaDescribedBy,
|
|
245
|
+
"aria-invalid": ariaInvalid,
|
|
246
|
+
"aria-required": ariaRequired,
|
|
241
247
|
...rest
|
|
242
248
|
}: ComboboxProps) {
|
|
243
|
-
const field = useComboboxFieldError(
|
|
249
|
+
const field = useComboboxFieldError(
|
|
250
|
+
error,
|
|
251
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
252
|
+
ariaDescribedBy,
|
|
253
|
+
);
|
|
244
254
|
const generated = useId();
|
|
245
255
|
const fieldId = id ?? generated;
|
|
246
256
|
// Derived from the GENERATED id, never from `id`: a caller's id is theirs to
|
|
@@ -366,9 +376,9 @@ export function Combobox({
|
|
|
366
376
|
: null;
|
|
367
377
|
|
|
368
378
|
return (
|
|
369
|
-
// `rest` dresses the outer box — a `data-tour` anchor, a test id
|
|
370
|
-
//
|
|
371
|
-
// the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
379
|
+
// `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,
|
|
380
|
+
// description, invalid or required state: those belong on the <input> below,
|
|
381
|
+
// which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
372
382
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
373
383
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
374
384
|
{/* A real <label for> since lenkbank's tests met a field `getByLabelText` could
|
|
@@ -407,6 +417,7 @@ export function Combobox({
|
|
|
407
417
|
aria-autocomplete="list"
|
|
408
418
|
aria-invalid={field.isInvalid || undefined}
|
|
409
419
|
aria-describedby={field.describedBy}
|
|
420
|
+
aria-required={ariaRequired}
|
|
410
421
|
autoComplete="off"
|
|
411
422
|
disabled={disabled}
|
|
412
423
|
autoFocus={autoFocus}
|
|
@@ -729,12 +740,22 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
729
740
|
invalid,
|
|
730
741
|
error,
|
|
731
742
|
"aria-label": ariaLabel,
|
|
743
|
+
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
744
|
+
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
745
|
+
// on the wrapper div the hint and required state described nothing.
|
|
746
|
+
"aria-describedby": ariaDescribedBy,
|
|
747
|
+
"aria-invalid": ariaInvalid,
|
|
748
|
+
"aria-required": ariaRequired,
|
|
732
749
|
...rest
|
|
733
750
|
}: InlineEntityComboboxProps<V, C>) {
|
|
734
751
|
// The clear value reads as "nothing selected", whichever one the caller picked —
|
|
735
752
|
// so from here down `value` is the id or `null`, as it always was.
|
|
736
753
|
const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
|
|
737
|
-
const field = useComboboxFieldError(
|
|
754
|
+
const field = useComboboxFieldError(
|
|
755
|
+
error,
|
|
756
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
757
|
+
ariaDescribedBy,
|
|
758
|
+
);
|
|
738
759
|
const generated = useId();
|
|
739
760
|
const fieldId = id ?? generated;
|
|
740
761
|
// See the twin above on why these hang off the generated id.
|
|
@@ -853,9 +874,9 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
853
874
|
};
|
|
854
875
|
|
|
855
876
|
return (
|
|
856
|
-
// `rest` dresses the outer box — a `data-tour` anchor, a test id
|
|
857
|
-
//
|
|
858
|
-
// the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
877
|
+
// `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,
|
|
878
|
+
// description, invalid or required state: those belong on the <input> below,
|
|
879
|
+
// which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
859
880
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
860
881
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
861
882
|
{/* A real <label for> — see {@link Combobox}. */}
|
|
@@ -889,6 +910,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
889
910
|
aria-autocomplete="list"
|
|
890
911
|
aria-invalid={field.isInvalid || undefined}
|
|
891
912
|
aria-describedby={field.describedBy}
|
|
913
|
+
aria-required={ariaRequired}
|
|
892
914
|
autoComplete="off"
|
|
893
915
|
disabled={disabled}
|
|
894
916
|
autoFocus={autoFocus}
|
|
@@ -56,7 +56,8 @@ export interface CopyButtonProps
|
|
|
56
56
|
stopPropagation?: boolean;
|
|
57
57
|
/** Icon variant only: where the result tooltip opens. */
|
|
58
58
|
tooltipSide?: TooltipSide;
|
|
59
|
-
/** Pass through to the tooltip
|
|
59
|
+
/** Pass through to the tooltip. Left unset, the Tooltip decides on its own — it
|
|
60
|
+
* portals inside a clipping container (a DataTable's `data-clips` scroller). */
|
|
60
61
|
tooltipPortal?: boolean;
|
|
61
62
|
/** ms until the button returns to idle. Default 2000. */
|
|
62
63
|
resetAfter?: number;
|
|
@@ -93,7 +94,7 @@ export function CopyButton({
|
|
|
93
94
|
tone,
|
|
94
95
|
stopPropagation,
|
|
95
96
|
tooltipSide = "top",
|
|
96
|
-
tooltipPortal
|
|
97
|
+
tooltipPortal,
|
|
97
98
|
resetAfter = 2000,
|
|
98
99
|
onCopied,
|
|
99
100
|
labels,
|