@eifi1/ui-kit 0.10.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 +66 -37
- 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/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +26 -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/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +107 -3
- package/dist/components/button-group.js +60 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +17 -4
- package/dist/components/calendar-heatmap.d.ts +162 -0
- package/dist/components/calendar-heatmap.js +336 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- 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 +66 -2
- package/dist/components/choice-card.js +108 -1
- 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 +52 -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 +104 -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 +69 -0
- package/dist/components/field.js +75 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +51 -208
- package/dist/components/file-dropzone.d.ts +50 -2
- package/dist/components/floating-panel.d.ts +156 -4
- package/dist/components/floating-panel.js +193 -29
- 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 +37 -3
- package/dist/components/list.js +39 -14
- 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 +13 -2
- package/dist/components/menu-item.js +11 -2
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +82 -5
- package/dist/components/mini-calendar.js +212 -64
- 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 +67 -0
- package/dist/components/nav-pills.js +69 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +26 -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 +27 -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 +17 -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 +73 -3
- package/dist/components/progress-bar.js +110 -8
- 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/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +50 -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/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +77 -11
- package/dist/components/table.js +85 -17
- 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/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +50 -1
- package/dist/components/toggle-group.d.ts +28 -0
- package/dist/components/toggle-group.js +58 -9
- 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/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +52 -729
- package/dist/components/ui.js +296 -27
- 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/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.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 +50 -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 +17 -4
- package/dist/i18n/defaults.js +33 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +47 -310
- 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 +17 -4
- package/dist/i18n/locales/de-CH.d.ts +17 -4
- package/dist/i18n/locales/de-informal.d.ts +17 -4
- package/dist/i18n/locales/de.d.ts +17 -4
- package/dist/i18n/locales/de.js +165 -4
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +17 -4
- package/dist/i18n/locales/es.js +162 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -4
- package/dist/i18n/locales/fr.js +162 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -4
- package/dist/i18n/locales/hu.js +162 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -4
- package/dist/i18n/locales/it.js +162 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -4
- package/dist/i18n/locales/zh.js +162 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- 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 +50 -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/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +63 -1
- package/dist/rhf.js +3 -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 +84 -3
- package/dist/shell/topbar-action-menu.js +115 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +58 -2
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -1
- package/dist/wizard/wizard-context.d.ts +30 -1
- package/dist/wizard/wizard-context.js +23 -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 +51 -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/alert-banner.tsx +36 -7
- 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/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +129 -4
- package/src/components/calendar-heatmap.tsx +599 -0
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +208 -1
- 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 +231 -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 +188 -0
- package/src/components/floating-panel.tsx +380 -16
- 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 +93 -18
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +29 -3
- package/src/components/mini-calendar.tsx +317 -57
- 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 +152 -0
- 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 +241 -13
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/series-chart.tsx +21 -0
- 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/status-dot.tsx +10 -2
- package/src/components/table.tsx +186 -23
- package/src/components/text-link.tsx +248 -0
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +106 -13
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +572 -44
- package/src/components/use-table-state.ts +265 -33
- package/src/components/user-avatar.tsx +57 -3
- 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 +32 -0
- package/src/i18n/kit-labels.tsx +104 -8
- package/src/i18n/locales/de.ts +162 -0
- package/src/i18n/locales/es.ts +159 -0
- package/src/i18n/locales/fr.ts +162 -0
- package/src/i18n/locales/hu.ts +160 -0
- package/src/i18n/locales/it.ts +160 -0
- package/src/i18n/locales/zh.ts +159 -0
- package/src/index.ts +64 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -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/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +8 -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 +260 -49
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +50 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
package/src/components/table.tsx
CHANGED
|
@@ -13,11 +13,37 @@ export type TableDensity = "comfortable" | "compact" | "none";
|
|
|
13
13
|
* figures for room. */
|
|
14
14
|
export type TableLayout = "auto" | "fixed";
|
|
15
15
|
export type TableAlign = "start" | "center" | "end";
|
|
16
|
+
/**
|
|
17
|
+
* Vertical alignment of a cell's content (0.11.0). The defaults stay as they were — a
|
|
18
|
+
* head cell sits on the `bottom` (a wrapped heading ends on the line above the
|
|
19
|
+
* figures), a body cell at the `top` (a wrapped note starts level with its row). A
|
|
20
|
+
* table whose cells are INPUTS wants `middle`: keksdose's VAT summary
|
|
21
|
+
* (invoices/vat-summary.tsx) sets fields beside derived read-out figures in one row,
|
|
22
|
+
* and a figure at the top of its cell sat above the middle of the field next to it —
|
|
23
|
+
* so it writes `<tr className="align-middle">` over raw `<th>`/`<td>`, because the
|
|
24
|
+
* kit's cells each stated `align-top` and a class on the row could not reach them.
|
|
25
|
+
*/
|
|
26
|
+
export type TableVAlign = "top" | "middle" | "bottom";
|
|
27
|
+
/** The head cell's type size: `xs` (the head's default) or `sm` (the body's). */
|
|
28
|
+
export type TableHeaderCellSize = "xs" | "sm";
|
|
29
|
+
/** The header cell's weight. Default `medium`. */
|
|
30
|
+
export type TableHeaderCellWeight = "normal" | "medium" | "semibold";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What a {@link TableRow} is in a statement-style table (0.12.0): an ordinary `row`, a
|
|
34
|
+
* `group` heading ("Current assets", a tinted full-width label row), a `subtotal` under
|
|
35
|
+
* a group, or the `total` of the whole table. kastlan's balance sheet, income statement
|
|
36
|
+
* and journal entry each spelled these as `className="font-medium"`, `"font-bold
|
|
37
|
+
* border-t-2"` and `bg-muted/50` on the cell — three pages, three sets of classes for
|
|
38
|
+
* the same four kinds of line.
|
|
39
|
+
*/
|
|
40
|
+
export type TableRowVariant = "row" | "group" | "subtotal" | "total";
|
|
16
41
|
|
|
17
42
|
interface TableContextValue {
|
|
18
43
|
density: TableDensity;
|
|
19
44
|
zebra: boolean;
|
|
20
45
|
hover: boolean;
|
|
46
|
+
rowDividers: boolean;
|
|
21
47
|
captionId: string;
|
|
22
48
|
registerCaption: () => () => void;
|
|
23
49
|
}
|
|
@@ -26,12 +52,66 @@ const TableContext = createContext<TableContextValue>({
|
|
|
26
52
|
density: "comfortable",
|
|
27
53
|
zebra: false,
|
|
28
54
|
hover: false,
|
|
55
|
+
rowDividers: true,
|
|
29
56
|
captionId: "",
|
|
30
57
|
registerCaption: () => () => {},
|
|
31
58
|
});
|
|
32
59
|
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
type TableSection = "head" | "body" | "foot";
|
|
61
|
+
|
|
62
|
+
/** Which section a row sits in — zebra and hover apply to body rows only. `undefined`
|
|
63
|
+
* outside the kit's section parts: a raw `<thead>` / `<tbody>`, which
|
|
64
|
+
* {@link useSection} then reads off the DOM. */
|
|
65
|
+
const SectionContext = createContext<TableSection | undefined>(undefined);
|
|
66
|
+
|
|
67
|
+
const SECTION_OF_TAG: Record<string, TableSection> = { THEAD: "head", TBODY: "body", TFOOT: "foot" };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The section a row or cell sits in: the kit part's context where there is one, else the
|
|
71
|
+
* nearest `<thead>` / `<tbody>` / `<tfoot>` in the DOM.
|
|
72
|
+
*
|
|
73
|
+
* keksdose's VAT summary writes a raw `<thead>` round kit cells in places, and a
|
|
74
|
+
* `TableHeaderCell` there took itself for a BODY cell — `scope="row"`, body type — with
|
|
75
|
+
* nothing on the page to say why. React cannot see an ancestor's tag, so the fallback
|
|
76
|
+
* reads it after mount; a layout effect, so the corrected cell is what gets painted.
|
|
77
|
+
*/
|
|
78
|
+
function useSection<E extends HTMLElement>(): [TableSection, ((el: E | null) => void) | undefined] {
|
|
79
|
+
const fromContext = useContext(SectionContext);
|
|
80
|
+
const [node, setNode] = useState<E | null>(null);
|
|
81
|
+
const [detected, setDetected] = useState<TableSection | undefined>(undefined);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
if (fromContext !== undefined || !node) return;
|
|
84
|
+
const section = node.closest("thead, tbody, tfoot");
|
|
85
|
+
const found = section ? SECTION_OF_TAG[section.tagName] : undefined;
|
|
86
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- read off the DOM, which only exists after mount
|
|
87
|
+
if (found !== detected) setDetected(found);
|
|
88
|
+
}, [fromContext, node, detected]);
|
|
89
|
+
// No ref at all under a kit section: holding the node would cost every row and cell
|
|
90
|
+
// a second render on mount for an answer the context already gave.
|
|
91
|
+
return [fromContext ?? detected ?? "body", fromContext === undefined ? setNode : undefined];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A {@link TableRow}'s `valign`, which its cells take unless they set their own. It
|
|
95
|
+
* cannot be left to CSS inheritance: every cell states its own default alignment,
|
|
96
|
+
* and a class on the cell beats one on the row. */
|
|
97
|
+
const RowVAlignContext = createContext<TableVAlign | undefined>(undefined);
|
|
98
|
+
|
|
99
|
+
const VALIGN: Record<TableVAlign, string> = {
|
|
100
|
+
top: "align-top",
|
|
101
|
+
middle: "align-middle",
|
|
102
|
+
bottom: "align-bottom",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const HEADER_SIZE: Record<TableHeaderCellSize, string> = {
|
|
106
|
+
xs: "text-xs",
|
|
107
|
+
sm: "text-sm",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const HEADER_WEIGHT: Record<TableHeaderCellWeight, string> = {
|
|
111
|
+
normal: "font-normal",
|
|
112
|
+
medium: "font-medium",
|
|
113
|
+
semibold: "font-semibold",
|
|
114
|
+
};
|
|
35
115
|
|
|
36
116
|
const CELL_PAD: Record<TableDensity, string> = {
|
|
37
117
|
comfortable: "px-3 py-2",
|
|
@@ -65,6 +145,22 @@ export interface TableProps extends ComponentPropsWithoutRef<"table"> {
|
|
|
65
145
|
wrapperClassName?: string;
|
|
66
146
|
/** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */
|
|
67
147
|
layout?: TableLayout;
|
|
148
|
+
/**
|
|
149
|
+
* A rounded border round the whole table (0.12.0) — for a table standing on the page
|
|
150
|
+
* or in a dialog rather than inside a card. kastlan wraps six of them in
|
|
151
|
+
* `<div className="rounded-md border">` by hand (payment allocation, the new-budget
|
|
152
|
+
* lines, the meter readings, the deposit transactions, the unit values editor, the
|
|
153
|
+
* maintenance detail), and a hand-made frame outside the scroll wrapper does not clip
|
|
154
|
+
* the rows' tint to its corners.
|
|
155
|
+
*/
|
|
156
|
+
framed?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* The rule between body rows. Default true. `false` for a small table whose rows are
|
|
159
|
+
* spaced by their own content — keksdose's VAT summary, a block of three quiet
|
|
160
|
+
* columns inside a card — which otherwise had to drop to raw `<tr>`s to lose it. One
|
|
161
|
+
* row can still say otherwise with {@link TableRowProps.bordered}.
|
|
162
|
+
*/
|
|
163
|
+
rowDividers?: boolean;
|
|
68
164
|
}
|
|
69
165
|
|
|
70
166
|
/**
|
|
@@ -90,6 +186,8 @@ export function Table({
|
|
|
90
186
|
hover = false,
|
|
91
187
|
wrapperClassName,
|
|
92
188
|
layout,
|
|
189
|
+
framed = false,
|
|
190
|
+
rowDividers = true,
|
|
93
191
|
className,
|
|
94
192
|
"aria-label": ariaLabel,
|
|
95
193
|
...rest
|
|
@@ -104,13 +202,14 @@ export function Table({
|
|
|
104
202
|
density,
|
|
105
203
|
zebra,
|
|
106
204
|
hover,
|
|
205
|
+
rowDividers,
|
|
107
206
|
captionId,
|
|
108
207
|
registerCaption: () => {
|
|
109
208
|
setCaptions((n) => n + 1);
|
|
110
209
|
return () => setCaptions((n) => n - 1);
|
|
111
210
|
},
|
|
112
211
|
}),
|
|
113
|
-
[density, zebra, hover, captionId],
|
|
212
|
+
[density, zebra, hover, rowDividers, captionId],
|
|
114
213
|
);
|
|
115
214
|
|
|
116
215
|
const regionName = captions > 0 ? { "aria-labelledby": captionId } : ariaLabel ? { "aria-label": ariaLabel } : null;
|
|
@@ -125,9 +224,14 @@ export function Table({
|
|
|
125
224
|
{...(overflowing ? regionName : null)}
|
|
126
225
|
tabIndex={overflowing ? 0 : undefined}
|
|
127
226
|
data-overflowing={overflowing || undefined}
|
|
227
|
+
data-clips=""
|
|
228
|
+
data-framed={framed || undefined}
|
|
128
229
|
className={cn(
|
|
129
230
|
// `relative`: the containing block for an `sr-only` caption.
|
|
130
231
|
"relative w-full overflow-x-auto",
|
|
232
|
+
// On the scroll wrapper itself, so its clipping keeps the rows inside the
|
|
233
|
+
// rounded corners.
|
|
234
|
+
framed && "rounded-md border border-[var(--border)]",
|
|
131
235
|
THIN_SCROLLBAR_CLASS,
|
|
132
236
|
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
133
237
|
wrapperClassName,
|
|
@@ -149,12 +253,16 @@ export function Table({
|
|
|
149
253
|
);
|
|
150
254
|
}
|
|
151
255
|
|
|
152
|
-
export
|
|
256
|
+
export interface TableHeadProps extends ComponentPropsWithoutRef<"thead"> {
|
|
257
|
+
/** The rule under the head. Default true; `false` for a quiet header over a small
|
|
258
|
+
* table (keksdose's VAT summary wrote `className="border-b-0"` to lose it). */
|
|
259
|
+
bordered?: boolean;
|
|
260
|
+
}
|
|
153
261
|
|
|
154
|
-
export function TableHead({ className, ...rest }: TableHeadProps) {
|
|
262
|
+
export function TableHead({ bordered = true, className, ...rest }: TableHeadProps) {
|
|
155
263
|
return (
|
|
156
264
|
<SectionContext.Provider value="head">
|
|
157
|
-
<thead {...rest} className={cn("border-b border-[var(--border)]", className)} />
|
|
265
|
+
<thead {...rest} className={cn(bordered && "border-b border-[var(--border)]", className)} />
|
|
158
266
|
</SectionContext.Provider>
|
|
159
267
|
);
|
|
160
268
|
}
|
|
@@ -252,23 +360,49 @@ export function TableFoot({ className, ...rest }: TableFootProps) {
|
|
|
252
360
|
);
|
|
253
361
|
}
|
|
254
362
|
|
|
255
|
-
|
|
363
|
+
const ROW_VARIANT: Record<TableRowVariant, string | false> = {
|
|
364
|
+
row: false,
|
|
365
|
+
group: "bg-[var(--bg-surface-2)] font-semibold",
|
|
366
|
+
subtotal: "font-medium",
|
|
367
|
+
// A 2px rule over the 1px divider the row above draws on its bottom: collapsed
|
|
368
|
+
// borders meet on one line, the wider wins, and the heavier rule is what says "sum".
|
|
369
|
+
total: "border-t-2 border-t-[var(--border-strong)] font-semibold",
|
|
370
|
+
};
|
|
256
371
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
372
|
+
/** The deprecated HTML `valign` attribute is replaced by a class-backed one. */
|
|
373
|
+
export interface TableRowProps extends ComponentPropsWithoutRef<"tr"> {
|
|
374
|
+
/** Vertical alignment for every cell of the row that does not set its own. See
|
|
375
|
+
* {@link TableVAlign}. Left out, each cell keeps its default. */
|
|
376
|
+
valign?: TableVAlign;
|
|
377
|
+
/** See {@link TableRowVariant}. Default `row`. Zebra and hover tint ordinary rows
|
|
378
|
+
* only: a group heading or a total is not one more record. */
|
|
379
|
+
variant?: TableRowVariant;
|
|
380
|
+
/** The rule under this body row. Default: the table's `rowDividers`. */
|
|
381
|
+
bordered?: boolean;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function TableRow({ valign, variant = "row", bordered, className, ...rest }: TableRowProps) {
|
|
385
|
+
const { zebra, hover, rowDividers } = useContext(TableContext);
|
|
386
|
+
const [section, ref] = useSection<HTMLTableRowElement>();
|
|
260
387
|
const body = section === "body";
|
|
261
|
-
|
|
388
|
+
const record = body && variant === "row";
|
|
389
|
+
const row = (
|
|
262
390
|
<tr
|
|
391
|
+
ref={ref}
|
|
263
392
|
{...rest}
|
|
393
|
+
data-variant={variant === "row" ? undefined : variant}
|
|
264
394
|
className={cn(
|
|
265
|
-
body && "border-b border-[var(--border)]",
|
|
266
|
-
|
|
267
|
-
|
|
395
|
+
body && (bordered ?? rowDividers) && "border-b border-[var(--border)]",
|
|
396
|
+
record && zebra && "even:bg-[var(--bg-surface-2)]",
|
|
397
|
+
record && hover && "transition-colors hover:bg-[var(--bg-hover)]",
|
|
398
|
+
ROW_VARIANT[variant],
|
|
399
|
+
// On the row as well, for a raw `<td>` of the caller's, which inherits it.
|
|
400
|
+
valign && VALIGN[valign],
|
|
268
401
|
className,
|
|
269
402
|
)}
|
|
270
403
|
/>
|
|
271
404
|
);
|
|
405
|
+
return valign ? <RowVAlignContext.Provider value={valign}>{row}</RowVAlignContext.Provider> : row;
|
|
272
406
|
}
|
|
273
407
|
|
|
274
408
|
interface CellAlignProps {
|
|
@@ -276,45 +410,74 @@ interface CellAlignProps {
|
|
|
276
410
|
numeric?: boolean;
|
|
277
411
|
/** Logical alignment. Default `start` (`end` when `numeric`). */
|
|
278
412
|
align?: TableAlign;
|
|
413
|
+
/** Vertical alignment. Default: the row's `valign`, else `bottom` for a head cell and
|
|
414
|
+
* `top` for a body cell. See {@link TableVAlign}. */
|
|
415
|
+
valign?: TableVAlign;
|
|
279
416
|
}
|
|
280
417
|
|
|
281
|
-
/** The deprecated HTML `align`
|
|
282
|
-
export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align">, CellAlignProps {
|
|
418
|
+
/** The deprecated HTML `align` and `valign` attributes are replaced by class-backed ones. */
|
|
419
|
+
export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align" | "valign">, CellAlignProps {
|
|
420
|
+
/**
|
|
421
|
+
* Type size. Left out: `xs` in the head, the table's own size in the body — as
|
|
422
|
+
* before. keksdose's VAT summary heads its columns at the body size and normal weight
|
|
423
|
+
* (vat-summary.tsx:158–168, `font-normal` on each `<th>`), a quiet header over a
|
|
424
|
+
* small table of figures, and could not say so without overriding classes.
|
|
425
|
+
*/
|
|
426
|
+
size?: TableHeaderCellSize;
|
|
427
|
+
/** Default `medium`. `normal` for the quiet header above; see {@link size}. */
|
|
428
|
+
weight?: TableHeaderCellWeight;
|
|
429
|
+
}
|
|
283
430
|
|
|
284
431
|
/**
|
|
285
432
|
* A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header
|
|
286
433
|
* cell in a body row is the row's label ("Net rent" in a key-value table), and a
|
|
287
434
|
* reader needs the scope to read it with each cell beside it.
|
|
288
435
|
*/
|
|
289
|
-
export function TableHeaderCell({
|
|
436
|
+
export function TableHeaderCell({
|
|
437
|
+
numeric = false,
|
|
438
|
+
align,
|
|
439
|
+
valign,
|
|
440
|
+
size,
|
|
441
|
+
weight = "medium",
|
|
442
|
+
scope,
|
|
443
|
+
className,
|
|
444
|
+
...rest
|
|
445
|
+
}: TableHeaderCellProps) {
|
|
290
446
|
const { density } = useContext(TableContext);
|
|
291
|
-
const section =
|
|
447
|
+
const [section, ref] = useSection<HTMLTableCellElement>();
|
|
448
|
+
const rowVAlign = useContext(RowVAlignContext);
|
|
449
|
+
const head = section === "head";
|
|
450
|
+
const textSize = size ?? (head ? "xs" : undefined);
|
|
292
451
|
return (
|
|
293
452
|
<th
|
|
453
|
+
ref={ref}
|
|
294
454
|
{...rest}
|
|
295
455
|
scope={scope ?? (section === "head" ? "col" : "row")}
|
|
296
456
|
className={cn(
|
|
297
457
|
CELL_PAD[density],
|
|
298
458
|
ALIGN[align ?? (numeric ? "end" : "start")],
|
|
299
459
|
numeric && "tabular-nums",
|
|
300
|
-
"
|
|
301
|
-
|
|
460
|
+
VALIGN[valign ?? rowVAlign ?? (head ? "bottom" : "top")],
|
|
461
|
+
HEADER_WEIGHT[weight],
|
|
462
|
+
head ? "text-[var(--text-muted)]" : "text-[var(--text-secondary)]",
|
|
463
|
+
textSize && HEADER_SIZE[textSize],
|
|
302
464
|
className,
|
|
303
465
|
)}
|
|
304
466
|
/>
|
|
305
467
|
);
|
|
306
468
|
}
|
|
307
469
|
|
|
308
|
-
export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align">, CellAlignProps {}
|
|
470
|
+
export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align" | "valign">, CellAlignProps {}
|
|
309
471
|
|
|
310
|
-
export function TableCell({ numeric = false, align, className, ...rest }: TableCellProps) {
|
|
472
|
+
export function TableCell({ numeric = false, align, valign, className, ...rest }: TableCellProps) {
|
|
311
473
|
const { density } = useContext(TableContext);
|
|
474
|
+
const rowVAlign = useContext(RowVAlignContext);
|
|
312
475
|
return (
|
|
313
476
|
<td
|
|
314
477
|
{...rest}
|
|
315
478
|
className={cn(
|
|
316
479
|
CELL_PAD[density],
|
|
317
|
-
"
|
|
480
|
+
VALIGN[valign ?? rowVAlign ?? "top"],
|
|
318
481
|
ALIGN[align ?? (numeric ? "end" : "start")],
|
|
319
482
|
numeric && "tabular-nums whitespace-nowrap",
|
|
320
483
|
className,
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from "react";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
import type { LucideIcon } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
6
|
+
import type { KitLinkComponent } from "../i18n/kit-labels";
|
|
7
|
+
|
|
8
|
+
/* ── Choosing what draws a link ──────────────────────────────────────────── */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An `href` that leaves the app: a scheme (`https:`, `mailto:`, `tel:`) or a
|
|
12
|
+
* protocol-relative `//host`. A router's link is for paths inside the app, so the
|
|
13
|
+
* provider's `linkComponent` is never handed one of these — they stay a plain `<a>`.
|
|
14
|
+
*
|
|
15
|
+
* NOT external, and so handed to the router link: a path (`/x`, `x`, `?q=1`) and the
|
|
16
|
+
* hash-router form `#/x` — that IS a route in a `HashRouter` app, which maps it to
|
|
17
|
+
* `to="/x"`. A plain in-page anchor (`#section`) is not external either, but it is not
|
|
18
|
+
* a route: see {@link isInPageAnchor}.
|
|
19
|
+
*/
|
|
20
|
+
export function isExternalHref(href: string): boolean {
|
|
21
|
+
return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An `href` that only scrolls the current page: `#section` (or a bare `#`). The
|
|
26
|
+
* hash-router form `#/x` is a ROUTE, not an anchor, and is not one of these. A router
|
|
27
|
+
* link must not get an anchor — under a `HashRouter` `#section` would navigate to the
|
|
28
|
+
* path `/section` — so every kit link draws it as a plain `<a>`.
|
|
29
|
+
*/
|
|
30
|
+
export function isInPageAnchor(href: string): boolean {
|
|
31
|
+
return href.startsWith("#") && !href.startsWith("#/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether the provider's `linkComponent` (or a kit component's router fallback) draws
|
|
36
|
+
* `href`: an in-app path, `#/x` included — not an external `href`
|
|
37
|
+
* ({@link isExternalHref}) and not an in-page `#anchor` ({@link isInPageAnchor}).
|
|
38
|
+
*/
|
|
39
|
+
export function isRoutableHref(href: string): boolean {
|
|
40
|
+
return !isExternalHref(href) && !isInPageAnchor(href);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What draws a kit link to `href`: the component's own `renderLink` when it was given
|
|
45
|
+
* one (it always wins — the caller asked for it by name), else the
|
|
46
|
+
* `<UiKitProvider linkComponent>` for a routable `href` ({@link isRoutableHref}), else
|
|
47
|
+
* `undefined` — a plain `<a>`. The one rule every kit link follows: `https:` /
|
|
48
|
+
* `mailto:` / `//host` and `#section` stay plain anchors; `/x`, `x` and a hash-router
|
|
49
|
+
* `#/x` go to the router link. Every kit link props type is a subset of the anchor
|
|
50
|
+
* attributes `KitLinkProps` describes, which is what makes the cast sound.
|
|
51
|
+
* @internal Shared by the kit's link-bearing components; not part of the barrel.
|
|
52
|
+
*/
|
|
53
|
+
export function pickLinkRenderer<P>(
|
|
54
|
+
own: ((props: P) => ReactElement) | undefined,
|
|
55
|
+
kitLink: KitLinkComponent | undefined,
|
|
56
|
+
href: string | undefined,
|
|
57
|
+
): ((props: P) => ReactElement) | undefined {
|
|
58
|
+
if (own) return own;
|
|
59
|
+
if (!kitLink || href === undefined || !isRoutableHref(href)) return undefined;
|
|
60
|
+
return kitLink as unknown as (props: P) => ReactElement;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Draws `props` through `render` as a component of its own, so a router link's hooks
|
|
65
|
+
* belong to it and not to the kit component calling it — for the kit components that
|
|
66
|
+
* call their own `renderLink` inline and reach the provider's link through here.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export function RenderedKitLink<P>({ render, props }: { render: (props: P) => ReactElement; props: P }) {
|
|
70
|
+
return render(props);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ── TextLink ─────────────────────────────────────────────────────────────── */
|
|
74
|
+
|
|
75
|
+
/** `brand` (default) for a link in running text, `muted` for footer and meta links
|
|
76
|
+
* that should not compete with the content, `danger` for a destructive one ("Leave
|
|
77
|
+
* group"), `inherit` for a link that takes its surroundings' colour. */
|
|
78
|
+
export type TextLinkTone = "brand" | "muted" | "danger" | "inherit";
|
|
79
|
+
|
|
80
|
+
/** `hover` (default): underlined under the pointer — the idiom all three apps draw.
|
|
81
|
+
* `always` for a link inside prose, where colour alone must not be the only mark
|
|
82
|
+
* (WCAG 1.4.1). `none` where the context already says "link" (a footer nav). */
|
|
83
|
+
export type TextLinkUnderline = "hover" | "always" | "none";
|
|
84
|
+
|
|
85
|
+
/** The `aria-current` token. `true` on {@link TextLinkProps.current} means `page`. */
|
|
86
|
+
export type TextLinkCurrent = "page" | "step" | "location" | "date" | "time" | "true";
|
|
87
|
+
|
|
88
|
+
/** What {@link TextLinkProps.renderLink} (and the provider's `linkComponent`) is
|
|
89
|
+
* handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */
|
|
90
|
+
export interface TextLinkRenderProps {
|
|
91
|
+
href: string;
|
|
92
|
+
/** The link's look — keep it, or the tone, underline and focus ring go. */
|
|
93
|
+
className: string;
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
96
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
97
|
+
onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;
|
|
98
|
+
"aria-current"?: TextLinkCurrent;
|
|
99
|
+
id?: string;
|
|
100
|
+
title?: string;
|
|
101
|
+
[key: `aria-${string}`]: string | boolean | number | undefined;
|
|
102
|
+
[key: `data-${string}`]: unknown;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface TextLinkProps
|
|
106
|
+
extends Omit<ComponentPropsWithoutRef<"a">, "href" | "children" | "aria-current" | "onClick" | "onKeyDown"> {
|
|
107
|
+
href: string;
|
|
108
|
+
children: ReactNode;
|
|
109
|
+
/** Default `brand`. */
|
|
110
|
+
tone?: TextLinkTone;
|
|
111
|
+
/** Default `hover`. */
|
|
112
|
+
underline?: TextLinkUnderline;
|
|
113
|
+
/**
|
|
114
|
+
* The link leaves the app: a plain `<a target="_blank" rel="noopener noreferrer">`
|
|
115
|
+
* (never the router's), an external-link mark after the text (mirrored in RTL), and
|
|
116
|
+
* "opens in a new tab" (`common.opensInNewTab`) read after it — the mark says it
|
|
117
|
+
* only to the eye. kastlan's BWO / BFS source links (rent-increase-calculator-page).
|
|
118
|
+
*/
|
|
119
|
+
external?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Keep the click (and the Enter that activates the link) from reaching an ancestor —
|
|
122
|
+
* a link in a DataTable row with `onRowClick` goes to its own target instead of
|
|
123
|
+
* running the row's action. kastlan's `CellLink` did this by hand.
|
|
124
|
+
*/
|
|
125
|
+
stopPropagation?: boolean;
|
|
126
|
+
/** Mark the link as the current one of a set — `true` is `aria-current="page"`, the
|
|
127
|
+
* legal footer's link to the page you are on. Drawn in semibold. */
|
|
128
|
+
current?: boolean | TextLinkCurrent;
|
|
129
|
+
/** A Lucide icon before the text, at the text's size — kastlan's "← Back" link. */
|
|
130
|
+
icon?: LucideIcon;
|
|
131
|
+
/** Your router's link for an in-app `href`. Default: the `<UiKitProvider
|
|
132
|
+
* linkComponent>`, then a plain `<a>`. Ignored on an `external` link. */
|
|
133
|
+
renderLink?: (props: TextLinkRenderProps) => ReactElement;
|
|
134
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
135
|
+
onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;
|
|
136
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const TONE: Record<TextLinkTone, string> = {
|
|
140
|
+
brand: "text-[var(--brand)] hover:text-[var(--brand-hover)]",
|
|
141
|
+
muted: "text-[var(--text-muted)] hover:text-[var(--text-primary)]",
|
|
142
|
+
danger: "text-[var(--danger)] hover:text-[var(--danger-hover)]",
|
|
143
|
+
inherit: "",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const UNDERLINE: Record<TextLinkUnderline, string> = {
|
|
147
|
+
hover: "no-underline hover:underline",
|
|
148
|
+
always: "underline",
|
|
149
|
+
none: "no-underline",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The one inline link: a real `<a href>` (middle click, "open in new tab", link
|
|
154
|
+
* semantics) through the app's router for an in-app path, in one of four tones.
|
|
155
|
+
*
|
|
156
|
+
* kastlan draws this link a dozen ways by hand — `EntityLink` and `CellLink` (text-primary,
|
|
157
|
+
* hover underline, one stopping the row click), the legal pages' back link and footer,
|
|
158
|
+
* the feedback page's URL links, the calculator's external sources with a hand-placed
|
|
159
|
+
* ↗ and no word for a screen reader — and the lease wizard's unit step (lease-unit-step)
|
|
160
|
+
* used a raw `<a>` to an in-app route, which reloads the whole app. Here the router
|
|
161
|
+
* link comes from `<UiKitProvider linkComponent>`, set once.
|
|
162
|
+
*
|
|
163
|
+
* Inline, not `inline-flex`: a link inside a sentence must wrap with it. The icons sit
|
|
164
|
+
* on the text's baseline at the text's size, so they scale with whatever type the link
|
|
165
|
+
* is set in. The focus ring is an outline, which follows every line of a wrapped link.
|
|
166
|
+
*/
|
|
167
|
+
export function TextLink({
|
|
168
|
+
href,
|
|
169
|
+
children,
|
|
170
|
+
tone = "brand",
|
|
171
|
+
underline = "hover",
|
|
172
|
+
external = false,
|
|
173
|
+
stopPropagation = false,
|
|
174
|
+
current,
|
|
175
|
+
icon: Icon,
|
|
176
|
+
renderLink,
|
|
177
|
+
className,
|
|
178
|
+
onClick,
|
|
179
|
+
onKeyDown,
|
|
180
|
+
ref,
|
|
181
|
+
...rest
|
|
182
|
+
}: TextLinkProps) {
|
|
183
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
184
|
+
const kitLink = useKitLink();
|
|
185
|
+
const ariaCurrent: TextLinkCurrent | undefined =
|
|
186
|
+
current === true ? "page" : current === false ? undefined : current;
|
|
187
|
+
|
|
188
|
+
const look = cn(
|
|
189
|
+
"rounded-sm underline-offset-2 transition-colors",
|
|
190
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
191
|
+
TONE[tone],
|
|
192
|
+
UNDERLINE[underline],
|
|
193
|
+
ariaCurrent && "font-semibold",
|
|
194
|
+
className,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const body = (
|
|
198
|
+
<>
|
|
199
|
+
{Icon && <Icon aria-hidden className="me-1 inline size-[1em] shrink-0 align-[-0.125em]" />}
|
|
200
|
+
{children}
|
|
201
|
+
{external && (
|
|
202
|
+
<>
|
|
203
|
+
<ExternalLink aria-hidden className="ms-1 inline size-[0.85em] align-[-0.1em] rtl:-scale-x-100" />
|
|
204
|
+
{" "}
|
|
205
|
+
<span className="sr-only">({common.opensInNewTab})</span>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const props: TextLinkRenderProps = {
|
|
212
|
+
...(rest as Partial<TextLinkRenderProps>),
|
|
213
|
+
ref,
|
|
214
|
+
href,
|
|
215
|
+
className: look,
|
|
216
|
+
"aria-current": ariaCurrent,
|
|
217
|
+
onClick: stopPropagation
|
|
218
|
+
? (event) => {
|
|
219
|
+
event.stopPropagation();
|
|
220
|
+
onClick?.(event);
|
|
221
|
+
}
|
|
222
|
+
: onClick,
|
|
223
|
+
onKeyDown: stopPropagation
|
|
224
|
+
? (event) => {
|
|
225
|
+
if (event.key === "Enter") event.stopPropagation();
|
|
226
|
+
onKeyDown?.(event);
|
|
227
|
+
}
|
|
228
|
+
: onKeyDown,
|
|
229
|
+
children: body,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);
|
|
233
|
+
if (render) return <RenderedTextLink render={render} {...props} />;
|
|
234
|
+
const { children: content, ...anchor } = props;
|
|
235
|
+
return (
|
|
236
|
+
<a {...anchor} {...(external ? { target: "_blank", rel: "noopener noreferrer" } : null)}>
|
|
237
|
+
{content}
|
|
238
|
+
</a>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Calls the link renderer as a component, so a router link's hooks are its own. */
|
|
243
|
+
function RenderedTextLink({
|
|
244
|
+
render,
|
|
245
|
+
...props
|
|
246
|
+
}: TextLinkRenderProps & { render: (props: TextLinkRenderProps) => ReactElement }) {
|
|
247
|
+
return render(props);
|
|
248
|
+
}
|
package/src/components/text.tsx
CHANGED
|
@@ -9,8 +9,15 @@ import { cn } from "../lib/cn";
|
|
|
9
9
|
* (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a
|
|
10
10
|
* dense panel (a legend, a menu) or above a block of page content, and one size looked
|
|
11
11
|
* wrong in the other place.
|
|
12
|
+
*
|
|
13
|
+
* `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels
|
|
14
|
+
* its figures ("Assigned", "Activity", "Upcoming", "Available" —
|
|
15
|
+
* budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line
|
|
16
|
+
* (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a
|
|
17
|
+
* figure that size, 12px competed with it. Named `md` rather than slotted in order
|
|
18
|
+
* because `sm` was already the default and renaming it would move every caller.
|
|
12
19
|
*/
|
|
13
|
-
export type SectionLabelSize = "xs" | "sm";
|
|
20
|
+
export type SectionLabelSize = "xs" | "md" | "sm";
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* The small, uppercase, tracked, muted label — as a class string, per size.
|
|
@@ -23,6 +30,7 @@ export type SectionLabelSize = "xs" | "sm";
|
|
|
23
30
|
*/
|
|
24
31
|
export const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {
|
|
25
32
|
xs: "text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
33
|
+
md: "text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
26
34
|
sm: "text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
27
35
|
};
|
|
28
36
|
|