@eifi1/ui-kit 0.11.0 → 0.13.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 +59 -34
- 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 +40 -4
- package/dist/components/amount-input.js +50 -12
- 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 +10 -2
- package/dist/components/bulk-action-bar.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/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +26 -18
- 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.d.ts +3 -1
- package/dist/components/combobox-core.js +18 -5
- 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-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +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 +125 -27
- 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 +40 -20
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +281 -0
- package/dist/components/error-boundary.js +302 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- 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/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- 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 -4
- package/dist/components/list.js +35 -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 +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 +42 -20
- 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 +20 -5
- package/dist/components/numpad-sheet.js +3 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- 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 +5 -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/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- 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 +122 -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 +6 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.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 +238 -0
- package/dist/components/text-link.js +144 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +35 -0
- package/dist/components/toggle-group.js +43 -14
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +53 -3
- package/dist/components/toggle-legend.js +54 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +364 -34
- 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-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +21 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +173 -0
- package/dist/feedback/feedback-thread.js +264 -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 +164 -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 +161 -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 +161 -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 +161 -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 +161 -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 +161 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +39 -19
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
- package/dist/lib/format.d.ts +162 -0
- package/dist/lib/format.js +151 -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 +283 -0
- package/dist/rhf/fields.js +631 -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/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +120 -57
- 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 +21 -4
- package/dist/shell/topbar-action-menu.js +18 -2
- 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/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -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 +30 -5
- package/dist/wizard/wizard-step.js +34 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +6 -6
- 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 +109 -10
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +14 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +33 -9
- package/src/components/combobox.tsx +33 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +230 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +25 -7
- package/src/components/error-boundary.tsx +640 -0
- package/src/components/field-sync.tsx +48 -40
- 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/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- 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 +82 -19
- 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 +65 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +27 -7
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +17 -1
- package/src/components/numpad-sheet.tsx +6 -1
- package/src/components/page-header.tsx +42 -5
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +10 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/popover.tsx +3 -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 +295 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +11 -3
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +356 -0
- package/src/components/text.tsx +46 -2
- package/src/components/toggle-group.tsx +93 -20
- package/src/components/toggle-legend.tsx +108 -2
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +707 -49
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +49 -7
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +510 -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 +118 -8
- package/src/i18n/locales/de.ts +161 -0
- package/src/i18n/locales/es.ts +158 -0
- package/src/i18n/locales/fr.ts +161 -0
- package/src/i18n/locales/hu.ts +159 -0
- package/src/i18n/locales/it.ts +159 -0
- package/src/i18n/locales/zh.ts +158 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +348 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1135 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +153 -3
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +32 -4
- package/src/shell.ts +2 -0
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +75 -6
- package/tokens.css +12 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/line-items.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, KeyboardEvent, Key, ReactNode } from \"react\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, IconButton } from \"./ui\";\n\n/** The words {@link LineItems} renders on its own behalf. Row numbers are 1-based. */\nexport interface LineItemsLabels {\n /** The dashed add button. */\n add: string;\n /** The remove button's name for one row. */\n remove: (row: number) => string;\n /** Its name while it waits for the second press (`confirmRemove`). */\n confirmRemove: (row: number) => string;\n /** A row's group name, when the caller gives no `rowLabel`. */\n row: (row: number) => string;\n /** The name a cell's field should carry: the column, and which row. */\n cell: (column: string, row: number) => string;\n /** The totals row's label, and its group name. */\n totals: string;\n}\n\nexport const DEFAULT_LINE_ITEMS_LABELS: LineItemsLabels = {\n add: \"Add row\",\n remove: (row) => `Remove row ${row}`,\n confirmRemove: (row) => `Remove row ${row}? Press again to confirm`,\n row: (row) => `Row ${row}`,\n cell: (column, row) => `${column}, row ${row}`,\n totals: \"Total\",\n};\n\n/** What a column's `render` receives for one row. */\nexport interface LineItemCellContext<T> {\n item: T;\n /** 0-based. */\n index: number;\n /** The accessible name for the cell's field — \"Debit, row 2\". The header is\n * hidden from assistive tech (each row repeats it), so the field has to carry it:\n * `<Input aria-label={label} … />`. */\n label: string;\n}\n\nexport interface LineItemsColumn<T> {\n /** Stable id: the React key, and the key into `totals`. */\n key: string;\n /** The column header (and the small label over the cell once the rows stack). */\n header: ReactNode;\n /** The column's name for {@link LineItemCellContext.label}, when `header` is not a\n * string. */\n label?: string;\n /** The grid track once the rows are wide enough to be a table. Default\n * `minmax(0,1fr)`; `\"7rem\"` for an amount. */\n width?: string;\n /** `end` for figures: the header and the totals cell end-align with the field. */\n align?: \"start\" | \"end\";\n render: (ctx: LineItemCellContext<T>) => ReactNode;\n}\n\nexport interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n items: readonly T[];\n columns: readonly LineItemsColumn<T>[];\n /** The row's React key. Default: the item's `id` (react-hook-form's `useFieldArray`\n * `fields` carry one), else its index. */\n getKey?: (item: T, index: number) => Key;\n /** Append a row. Omitted: no add button. The first field of the new row is focused. */\n onAdd?: () => void;\n /** Remove the row at `index`. Omitted: no remove buttons. */\n onRemove?: (index: number) => void;\n /** Remove is disabled at or below this many rows (a journal entry needs two).\n * Default 0. */\n minItems?: number;\n /** Add is disabled at this many rows. */\n maxItems?: number;\n /**\n * Ask before a row goes:\n * - `true`: the remove button asks for a second press — it fills red and its name\n * becomes `lineItems.confirmRemove`; Escape, or leaving it, calls it off.\n * - a function: called with the row, and the row goes only if it answers `true`\n * (`(item, i) => confirm({ title: … , tone: \"danger\" })` with `useConfirm`).\n */\n confirmRemove?: boolean | ((item: T, index: number) => boolean | Promise<boolean>);\n /** The add button's text. Default `lineItems.add`. */\n addLabel?: ReactNode;\n /** A row's group name (\"Line 2\"). Default `lineItems.row`. */\n rowLabel?: (index: number, item: T) => string;\n /** A totals row under the lines, one entry per column `key`. The first column\n * without an entry carries {@link totalsLabel}. Omitted: no totals row. */\n totals?: Partial<Record<string, ReactNode>>;\n /** Default `lineItems.totals`, \"Total\". */\n totalsLabel?: ReactNode;\n /** Shown in place of the rows when there are none. */\n empty?: ReactNode;\n /** A message about the list as a whole (\"The entry does not balance\"). */\n error?: ReactNode;\n /** Disables add and remove (the fields are the caller's to disable). */\n disabled?: boolean;\n labels?: Partial<LineItemsLabels>;\n}\n\nconst REMOVE_TRACK = \"2.25rem\";\nconst FOCUSABLE =\n \"input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])\";\n\nfunction defaultKey(item: unknown, index: number): Key {\n if (typeof item === \"object\" && item !== null && \"id\" in item) {\n const id = (item as { id: unknown }).id;\n if (typeof id === \"string\" || typeof id === \"number\") return id;\n }\n return index;\n}\n\nfunction hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\ntype PendingFocus = { kind: \"added\" } | { kind: \"removed\"; index: number };\n\n/**\n * An editable list of lines — journal entry lines, lease components, invoice\n * positions, budget rows: column headers, a row of fields per item, a remove button\n * per row, a dashed \"add\" button under them, and an optional totals row.\n *\n * Presentation only: the rows are the caller's `items` and the fields are the\n * caller's, rendered per column. So it works over plain state —\n *\n * ```tsx\n * <LineItems items={lines} columns={cols} onAdd={() => setLines([...lines, blank()])}\n * onRemove={(i) => setLines(lines.filter((_, j) => j !== i))} minItems={2} />\n * ```\n *\n * — and over react-hook-form's `useFieldArray` alike (`items={fields}`, `onAdd={() =>\n * append(blank())}`, `onRemove={remove}`), for which `@eifi1/ui-kit/rhf` has\n * `RhfLineItems`.\n *\n * On a wide pane the rows are a grid under the headers; on a narrow one (a container\n * query, so a dialog stacks where it is cramped) each row stacks its fields, each\n * under a small copy of its header. The headers are `aria-hidden`: every row is a\n * named group and every field is named by `render`'s `label` (\"Debit, row 2\"), which\n * is what a screen reader needs — a header row it would read once, out of context.\n *\n * Keyboard: after Add, focus lands in the new row's first field; after a remove, on\n * the row that took its place (else the add button). Ctrl/⌘+Enter in any row adds a\n * row.\n */\nexport function LineItems<T>({\n items,\n columns,\n getKey = defaultKey,\n onAdd,\n onRemove,\n minItems = 0,\n maxItems,\n confirmRemove,\n addLabel,\n rowLabel,\n totals,\n totalsLabel,\n empty,\n error,\n disabled = false,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: LineItemsProps<T>) {\n const labels = useKitLabels(\"lineItems\", DEFAULT_LINE_ITEMS_LABELS, labelsProp);\n const rowRefs = useRef<Array<HTMLDivElement | null>>([]);\n const addRef = useRef<HTMLButtonElement>(null);\n const pendingFocus = useRef<PendingFocus | null>(null);\n const [confirmingAt, setConfirming] = useState<number | null>(null);\n // A confirm waiting on a row that has since gone is void.\n const confirming = confirmingAt !== null && confirmingAt < items.length ? confirmingAt : null;\n\n const tracks = [...columns.map((c) => c.width ?? \"minmax(0,1fr)\"), ...(onRemove ? [REMOVE_TRACK] : [])];\n const gridStyle = { \"--line-items-cols\": tracks.join(\" \") } as CSSProperties;\n const canAdd = Boolean(onAdd) && !disabled && (maxItems === undefined || items.length < maxItems);\n const canRemove = Boolean(onRemove) && !disabled && items.length > minItems;\n const hasError = hasContent(error);\n\n\n // Focus after the list has re-rendered with the new rows — a layout effect, so the\n // caret is where it belongs before paint.\n useLayoutEffect(() => {\n const pending = pendingFocus.current;\n if (!pending) return;\n pendingFocus.current = null;\n if (pending.kind === \"added\") {\n const row = rowRefs.current[items.length - 1];\n row?.querySelector<HTMLElement>(FOCUSABLE)?.focus();\n return;\n }\n const row = rowRefs.current[Math.min(pending.index, items.length - 1)];\n const target =\n row?.querySelector<HTMLElement>(\"[data-line-items-remove]:not([disabled])\") ??\n row?.querySelector<HTMLElement>(FOCUSABLE) ??\n addRef.current;\n target?.focus();\n }, [items.length]);\n\n const add = useCallback(() => {\n if (!canAdd || !onAdd) return;\n pendingFocus.current = { kind: \"added\" };\n onAdd();\n }, [canAdd, onAdd]);\n\n const removeAt = async (index: number) => {\n if (!canRemove || !onRemove) return;\n if (confirmRemove === true && confirming !== index) {\n setConfirming(index);\n return;\n }\n if (typeof confirmRemove === \"function\") {\n const ok = await confirmRemove(items[index], index);\n if (!ok) return;\n }\n setConfirming(null);\n pendingFocus.current = { kind: \"removed\", index };\n onRemove(index);\n };\n\n const onRowKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key === \"Enter\" && (e.ctrlKey || e.metaKey) && canAdd) {\n e.preventDefault();\n add();\n }\n };\n\n const columnName = (column: LineItemsColumn<T>) =>\n column.label ?? (typeof column.header === \"string\" ? column.header : column.key);\n\n const headerCell = (column: LineItemsColumn<T>) => (\n <span\n key={column.key}\n className={cn(\n \"min-w-0 truncate text-xs font-medium text-[var(--text-muted)]\",\n column.align === \"end\" && \"text-end\",\n )}\n >\n {column.header}\n </span>\n );\n\n const showTotals = totals !== undefined;\n const labelColumn = showTotals ? columns.find((c) => !hasContent(totals[c.key]))?.key : undefined;\n\n return (\n <div\n data-slot=\"line-items\"\n {...rest}\n style={{ ...gridStyle, ...style }}\n className={cn(\"@container space-y-2\", className)}\n >\n <div aria-hidden className=\"hidden gap-2 @lg:grid @lg:grid-cols-[var(--line-items-cols)]\">\n {columns.map(headerCell)}\n {onRemove && <span />}\n </div>\n\n {items.length === 0 && hasContent(empty) && (\n <div className=\"rounded-md border border-dashed border-[var(--border)] px-3 py-4 text-center text-sm text-[var(--text-muted)]\">\n {empty}\n </div>\n )}\n\n {items.map((item, index) => {\n const row = index + 1;\n const isConfirming = confirming === index;\n return (\n // The row's Ctrl/⌘+Enter is a shortcut for the add button, which is itself\n // reachable by Tab; the group only listens for it bubbling from its fields.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n key={getKey(item, index)}\n ref={(el) => {\n rowRefs.current[index] = el;\n }}\n role=\"group\"\n aria-label={rowLabel ? rowLabel(index, item) : labels.row(row)}\n data-line-items-row=\"\"\n onKeyDown={onRowKeyDown}\n className={cn(\n \"grid grid-cols-1 gap-2 @lg:grid-cols-[var(--line-items-cols)] @lg:items-start\",\n // Stacked, each row is a card of its own so the rows stay apart.\n \"@max-lg:rounded-md @max-lg:border @max-lg:border-[var(--border)] @max-lg:p-3\",\n )}\n >\n {columns.map((column) => (\n <div key={column.key} className=\"min-w-0 space-y-1 @lg:space-y-0\">\n <span aria-hidden className=\"block text-xs font-medium text-[var(--text-muted)] @lg:hidden\">\n {column.header}\n </span>\n {column.render({ item, index, label: labels.cell(columnName(column), row) })}\n </div>\n ))}\n {onRemove && (\n <div className=\"flex justify-end @lg:justify-center\">\n <IconButton\n type=\"button\"\n data-line-items-remove=\"\"\n variant={isConfirming ? \"danger\" : \"ghost\"}\n tone={isConfirming ? undefined : \"danger\"}\n label={isConfirming ? labels.confirmRemove(row) : labels.remove(row)}\n disabled={!canRemove}\n onClick={() => void removeAt(index)}\n onBlur={() => {\n if (isConfirming) setConfirming(null);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\" && isConfirming) {\n e.stopPropagation();\n setConfirming(null);\n }\n }}\n >\n <Trash2 />\n </IconButton>\n </div>\n )}\n </div>\n );\n })}\n\n {showTotals && (\n <div\n role=\"group\"\n aria-label={typeof totalsLabel === \"string\" ? totalsLabel : labels.totals}\n data-line-items-totals=\"\"\n className=\"grid grid-cols-1 gap-2 border-t border-[var(--border)] pt-2 text-sm font-semibold tabular-nums text-[var(--text-primary)] @lg:grid-cols-[var(--line-items-cols)]\"\n >\n {columns.map((column) => {\n const value = totals[column.key];\n if (column.key === labelColumn) {\n return (\n <span key={column.key} className=\"min-w-0\">\n {totalsLabel ?? labels.totals}\n </span>\n );\n }\n return (\n <span\n key={column.key}\n className={cn(\n \"min-w-0\",\n column.align === \"end\" && \"@lg:text-end\",\n // Stacked, a column with no total takes no line.\n !hasContent(value) && \"@max-lg:hidden\",\n )}\n >\n {hasContent(value) && (\n <span className=\"flex justify-between gap-2 @lg:block\">\n <span aria-hidden className=\"font-normal text-[var(--text-muted)] @lg:hidden\">\n {column.header}\n </span>\n {value}\n </span>\n )}\n </span>\n );\n })}\n {onRemove && <span className=\"@max-lg:hidden\" />}\n </div>\n )}\n\n {hasError && (\n <p className=\"text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n\n {onAdd && (\n <Button\n ref={addRef}\n type=\"button\"\n variant=\"secondary\"\n className=\"w-full border-dashed\"\n disabled={!canAdd}\n onClick={add}\n >\n <Plus aria-hidden className=\"size-4\" />\n {addLabel ?? labels.add}\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["row"]}
|
|
@@ -50,8 +50,20 @@ interface ListItemLinkProps {
|
|
|
50
50
|
[key: `data-${string}`]: unknown;
|
|
51
51
|
}
|
|
52
52
|
interface ListItemBaseProps {
|
|
53
|
-
/** The row's name: one line, truncated. */
|
|
53
|
+
/** The row's name: one line, truncated — unless {@link titleLines} says otherwise. */
|
|
54
54
|
title: ReactNode;
|
|
55
|
+
/**
|
|
56
|
+
* How many lines the title may take. `1` (default) truncates with an ellipsis, which
|
|
57
|
+
* is right for a name the app chose — a unit number, a user, a file — where the
|
|
58
|
+
* cut-off end is still recognisable and every row keeps one height.
|
|
59
|
+
*
|
|
60
|
+
* `2` clamps at two lines; `"all"` shows every line, breaking inside a long word
|
|
61
|
+
* rather than overflowing. For rows whose only name is free text a person typed
|
|
62
|
+
* (kastlan 44): a defect or a note titled by its description, where the first 30
|
|
63
|
+
* characters on a phone say nothing and there is no other field to tell two rows
|
|
64
|
+
* apart. Pair it with `align="start"` so the icon stays by the first line.
|
|
65
|
+
*/
|
|
66
|
+
titleLines?: 1 | 2 | "all";
|
|
55
67
|
/** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */
|
|
56
68
|
subtitle?: ReactNode;
|
|
57
69
|
/** `2` for a line that is a body rather than a label (keksdose's notification text). */
|
|
@@ -79,6 +91,26 @@ interface ListItemBaseProps {
|
|
|
79
91
|
* on "Copy" from selecting the row.
|
|
80
92
|
*/
|
|
81
93
|
actions?: ReactNode;
|
|
94
|
+
/**
|
|
95
|
+
* Rich content UNDER the title row and outside its target: photos, per-row buttons,
|
|
96
|
+
* form fields — kastlan's handover room inspector (room-inspector.tsx), the handover
|
|
97
|
+
* detail page and the wizard's defects step (wizard/defects-step.tsx), which draw
|
|
98
|
+
* each room or defect as a `Card variant="inset"` because a row could not hold a
|
|
99
|
+
* photo strip with its own buttons.
|
|
100
|
+
*
|
|
101
|
+
* The same rule as {@link actions}, one row down: the title area stays ONE named
|
|
102
|
+
* target (the button or link, with the title and subtitle as its name), and the body
|
|
103
|
+
* is its sibling, never nested in it — a thumbnail's "Remove" inside the row's button
|
|
104
|
+
* would be invalid HTML and unreachable by a screen reader. The body is a
|
|
105
|
+
* `role="group"` named by the row's title (`aria-labelledby`), so tabbing into the
|
|
106
|
+
* third photo's button of the second room is heard as "Kitchen, group" first rather
|
|
107
|
+
* than as a button floating free of any row. Inside the row's box, so a `selected`
|
|
108
|
+
* border and a `divider` hold both; the hover fill stays on the target alone, since
|
|
109
|
+
* the body is not what a click on the row does.
|
|
110
|
+
*/
|
|
111
|
+
children?: ReactNode;
|
|
112
|
+
/** Classes on the {@link children} body. */
|
|
113
|
+
bodyClassName?: string;
|
|
82
114
|
/**
|
|
83
115
|
* The unread mark: a brand dot at the row's end, the title in semibold, and "Unread"
|
|
84
116
|
* read after the title (keksdose notification-inbox.tsx:215). For another status,
|
|
@@ -148,8 +180,9 @@ type ListItemProps = ListItemBaseProps & ({
|
|
|
148
180
|
* cannot do.
|
|
149
181
|
*/
|
|
150
182
|
href: string;
|
|
151
|
-
/** Your router's link for an in-app `href`. Default `<
|
|
152
|
-
*
|
|
183
|
+
/** Your router's link for an in-app `href`. Default: the `<UiKitProvider
|
|
184
|
+
* linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`
|
|
185
|
+
* share. Ignored for an `external` row. */
|
|
153
186
|
renderLink?: (props: ListItemLinkProps) => ReactElement;
|
|
154
187
|
/**
|
|
155
188
|
* The link leaves the app: a plain `<a target="_blank" rel="noopener
|
|
@@ -172,7 +205,7 @@ type ListItemProps = ListItemBaseProps & ({
|
|
|
172
205
|
});
|
|
173
206
|
/**
|
|
174
207
|
* One row of a {@link List}: leading icon or avatar, title and subtitle (both
|
|
175
|
-
* truncating), a trailing slot, and — beside the row, not inside it — its actions.
|
|
208
|
+
* truncating by default), a trailing slot, and — beside the row, not inside it — its actions.
|
|
176
209
|
*
|
|
177
210
|
* All three apps draw this row by hand, a dozen times: kastlan's units
|
|
178
211
|
* (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity
|
package/dist/components/list.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, forwardRef, useContext } from "react";
|
|
3
|
+
import { createContext, forwardRef, useContext, useId } from "react";
|
|
4
4
|
import { ExternalLink } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { Spinner } from "./ui.js";
|
|
7
7
|
import { StatusDot } from "./status-dot.js";
|
|
8
|
-
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
|
+
import { useKitLabels, useKitLink } from "../i18n/kit-labels.js";
|
|
9
|
+
import { pickLinkRenderer } from "./text-link.js";
|
|
9
10
|
const DEFAULT_LIST_LABELS = {
|
|
10
11
|
unread: "Unread",
|
|
11
12
|
opensInNewTab: "opens in a new tab"
|
|
@@ -32,13 +33,14 @@ const List = forwardRef(function List2({ as = "ul", density = "default", separat
|
|
|
32
33
|
) });
|
|
33
34
|
});
|
|
34
35
|
const PAD = {
|
|
35
|
-
compact: { target: "gap-2 px-2 py-1.5", actions: "pe-1.5", gap: "gap-0.5" },
|
|
36
|
-
default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", gap: "gap-1" },
|
|
37
|
-
comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", gap: "gap-1" }
|
|
36
|
+
compact: { target: "gap-2 px-2 py-1.5", actions: "pe-1.5", gap: "gap-0.5", body: "px-2 pb-1.5" },
|
|
37
|
+
default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", gap: "gap-1", body: "px-3 pb-2.5" },
|
|
38
|
+
comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", gap: "gap-1", body: "px-4 pb-3" }
|
|
38
39
|
};
|
|
39
40
|
const TARGET_RING = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
|
|
40
41
|
const ListItem = forwardRef(function ListItem2({
|
|
41
42
|
title,
|
|
43
|
+
titleLines = 1,
|
|
42
44
|
subtitle,
|
|
43
45
|
subtitleLines = 1,
|
|
44
46
|
meta,
|
|
@@ -58,6 +60,8 @@ const ListItem = forwardRef(function ListItem2({
|
|
|
58
60
|
targetProps,
|
|
59
61
|
bordered = false,
|
|
60
62
|
as = "li",
|
|
63
|
+
children,
|
|
64
|
+
bodyClassName,
|
|
61
65
|
href,
|
|
62
66
|
renderLink,
|
|
63
67
|
external = false,
|
|
@@ -67,12 +71,15 @@ const ListItem = forwardRef(function ListItem2({
|
|
|
67
71
|
}, ref) {
|
|
68
72
|
const list = useContext(ListContext);
|
|
69
73
|
const labels = useKitLabels("list", DEFAULT_LIST_LABELS);
|
|
74
|
+
const kitLink = useKitLink();
|
|
70
75
|
const density = densityProp ?? list.density;
|
|
71
76
|
const pad = PAD[density];
|
|
72
77
|
const isLink = href !== void 0 && !disabled;
|
|
73
78
|
const isButton = !isLink && onClick !== void 0 && href === void 0;
|
|
74
79
|
const interactive = isLink || isButton;
|
|
75
80
|
const inert = disabled || loading;
|
|
81
|
+
const hasBody = children !== void 0 && children !== null && children !== false;
|
|
82
|
+
const titleId = useId();
|
|
76
83
|
const mark = status ?? (unread ? /* @__PURE__ */ jsx(StatusDot, { tone: "brand", size: "sm", className: align === "start" ? "mt-1.5" : void 0 }) : null);
|
|
77
84
|
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
78
85
|
Icon && /* @__PURE__ */ jsx(
|
|
@@ -87,8 +94,10 @@ const ListItem = forwardRef(function ListItem2({
|
|
|
87
94
|
/* @__PURE__ */ jsxs(
|
|
88
95
|
"span",
|
|
89
96
|
{
|
|
97
|
+
id: hasBody ? titleId : void 0,
|
|
90
98
|
className: cn(
|
|
91
|
-
"block
|
|
99
|
+
"block text-sm text-[var(--text-primary)]",
|
|
100
|
+
titleLines === 1 ? "truncate" : titleLines === 2 ? "line-clamp-2 break-words" : "break-words",
|
|
92
101
|
unread ? "font-semibold" : "font-medium"
|
|
93
102
|
),
|
|
94
103
|
children: [
|
|
@@ -126,7 +135,10 @@ const ListItem = forwardRef(function ListItem2({
|
|
|
126
135
|
"flex min-w-0 flex-1 text-start",
|
|
127
136
|
align === "start" ? "items-start" : "items-center",
|
|
128
137
|
pad.target,
|
|
129
|
-
|
|
138
|
+
// With a body the target is the top of the box, not all of it, so it takes its
|
|
139
|
+
// own corners and its own hover fill (see `children`).
|
|
140
|
+
hasBody ? "rounded-md" : "rounded-[inherit]",
|
|
141
|
+
hasBody && interactive && !inert && !selected && "transition-colors hover:bg-[var(--bg-hover)]",
|
|
130
142
|
interactive && TARGET_RING,
|
|
131
143
|
interactive && !inert && "cursor-pointer",
|
|
132
144
|
loading && "cursor-progress",
|
|
@@ -148,7 +160,8 @@ const ListItem = forwardRef(function ListItem2({
|
|
|
148
160
|
children: body
|
|
149
161
|
};
|
|
150
162
|
const { children: linkBody, ...anchorProps } = linkProps;
|
|
151
|
-
|
|
163
|
+
const render = external ? void 0 : pickLinkRenderer(renderLink, kitLink, href);
|
|
164
|
+
main = render ? /* @__PURE__ */ jsx(RenderedLink, { render, ...linkProps }) : /* @__PURE__ */ jsx("a", { ...anchorProps, ...external ? { target: "_blank", rel: "noopener noreferrer" } : null, children: linkBody });
|
|
152
165
|
} else if (isButton) {
|
|
153
166
|
main = /* @__PURE__ */ jsx(
|
|
154
167
|
"button",
|
|
@@ -180,6 +193,7 @@ const ListItem = forwardRef(function ListItem2({
|
|
|
180
193
|
}
|
|
181
194
|
);
|
|
182
195
|
}
|
|
196
|
+
const actionsEl = actions != null ? /* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 items-center", pad.gap, pad.actions), children: actions }) : null;
|
|
183
197
|
const Tag = as;
|
|
184
198
|
return /* @__PURE__ */ jsx(
|
|
185
199
|
Tag,
|
|
@@ -188,20 +202,27 @@ const ListItem = forwardRef(function ListItem2({
|
|
|
188
202
|
list.separator === "divider" && "border-b border-[var(--border)] last:border-b-0",
|
|
189
203
|
list.separator === "divider" && as === "li" && "py-0.5"
|
|
190
204
|
),
|
|
191
|
-
children: /* @__PURE__ */
|
|
205
|
+
children: /* @__PURE__ */ jsx(
|
|
192
206
|
"div",
|
|
193
207
|
{
|
|
194
208
|
className: cn(
|
|
195
|
-
"flex min-w-0
|
|
209
|
+
"flex min-w-0 rounded-md border transition-colors",
|
|
210
|
+
hasBody ? "flex-col" : "items-center",
|
|
196
211
|
selected ? "border-[var(--brand)] bg-[var(--bg-surface-2)]" : bordered ? "border-[var(--border)]" : "border-transparent",
|
|
197
|
-
interactive && !inert && !selected && "hover:bg-[var(--bg-hover)]",
|
|
212
|
+
!hasBody && interactive && !inert && !selected && "hover:bg-[var(--bg-hover)]",
|
|
198
213
|
disabled && "opacity-50",
|
|
199
214
|
className
|
|
200
215
|
),
|
|
201
|
-
children: [
|
|
216
|
+
children: hasBody ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
217
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center", children: [
|
|
218
|
+
main,
|
|
219
|
+
actionsEl
|
|
220
|
+
] }),
|
|
221
|
+
/* @__PURE__ */ jsx("div", { role: "group", "aria-labelledby": titleId, className: cn("min-w-0", pad.body, bodyClassName), children })
|
|
222
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
202
223
|
main,
|
|
203
|
-
|
|
204
|
-
]
|
|
224
|
+
actionsEl
|
|
225
|
+
] })
|
|
205
226
|
}
|
|
206
227
|
)
|
|
207
228
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Spinner } from \"./ui\";\nimport { StatusDot } from \"./status-dot\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n}\n\nexport const DEFAULT_LIST_LABELS: ListLabels = {\n unread: \"Unread\",\n opensInNewTab: \"opens in a new tab\",\n};\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n}\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport const List = forwardRef<HTMLUListElement, ListProps>(function List(\n { as = \"ul\", density = \"default\", separator = \"gap\", className, children, ...rest },\n ref,\n) {\n const Tag = as;\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {children}\n </Tag>\n </ListContext.Provider>\n );\n});\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<ListDensity, { target: string; actions: string; gap: string }> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", gap: \"gap-0.5\" },\n default: { target: \"gap-3 px-3 py-2.5\", actions: \"pe-2\", gap: \"gap-1\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", gap: \"gap-1\" },\n};\n\nconst TARGET_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated. */\n title: ReactNode;\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | {\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n | {\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default `<a>`. The API `Chip`,\n * `StatTile` and `MenuItem` share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n }\n | {\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n );\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(\n {\n title,\n subtitle,\n subtitleLines = 1,\n meta,\n icon: Icon,\n leading,\n trailing,\n actions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n ...rest\n },\n ref,\n) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && onClick !== undefined && href === undefined;\n const interactive = isLink || isButton;\n const inert = disabled || loading;\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && (\n <Icon\n className={cn(\"size-4 shrink-0 text-[var(--text-muted)]\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span\n className={cn(\n \"block truncate text-sm text-[var(--text-primary)]\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-[var(--text-muted)]\",\n subtitleLines === 2 ? \"line-clamp-2\" : \"truncate\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && <span className=\"mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]\">{meta}</span>}\n </span>\n {trailing != null && <span className=\"flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]\">{trailing}</span>}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external && href !== undefined && (\n <ExternalLink className=\"size-3.5 shrink-0 text-[var(--text-muted)] rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n \"rounded-[inherit]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading\n ? (e) => e.preventDefault()\n : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n main = renderLink && !external ? (\n <RenderedLink render={renderLink} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading`.\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n onClick={loading ? undefined : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)}\n className={target}\n >\n {body}\n </button>\n );\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-[var(--border)] last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n )}\n >\n <div\n className={cn(\n \"flex min-w-0 items-center rounded-md border transition-colors\",\n selected\n ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {main}\n {actions != null && (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div>\n )}\n </div>\n </Tag>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":["List","ListItem"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Spinner } from \"./ui\";\nimport { StatusDot } from \"./status-dot\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n}\n\nexport const DEFAULT_LIST_LABELS: ListLabels = {\n unread: \"Unread\",\n opensInNewTab: \"opens in a new tab\",\n};\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n}\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport const List = forwardRef<HTMLUListElement, ListProps>(function List(\n { as = \"ul\", density = \"default\", separator = \"gap\", className, children, ...rest },\n ref,\n) {\n const Tag = as;\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {children}\n </Tag>\n </ListContext.Provider>\n );\n});\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<ListDensity, { target: string; actions: string; gap: string; body: string }> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", gap: \"gap-0.5\", body: \"px-2 pb-1.5\" },\n default: { target: \"gap-3 px-3 py-2.5\", actions: \"pe-2\", gap: \"gap-1\", body: \"px-3 pb-2.5\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", gap: \"gap-1\", body: \"px-4 pb-3\" },\n};\n\nconst TARGET_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated — unless {@link titleLines} says otherwise. */\n title: ReactNode;\n /**\n * How many lines the title may take. `1` (default) truncates with an ellipsis, which\n * is right for a name the app chose — a unit number, a user, a file — where the\n * cut-off end is still recognisable and every row keeps one height.\n *\n * `2` clamps at two lines; `\"all\"` shows every line, breaking inside a long word\n * rather than overflowing. For rows whose only name is free text a person typed\n * (kastlan 44): a defect or a note titled by its description, where the first 30\n * characters on a phone say nothing and there is no other field to tell two rows\n * apart. Pair it with `align=\"start\"` so the icon stays by the first line.\n */\n titleLines?: 1 | 2 | \"all\";\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * Rich content UNDER the title row and outside its target: photos, per-row buttons,\n * form fields — kastlan's handover room inspector (room-inspector.tsx), the handover\n * detail page and the wizard's defects step (wizard/defects-step.tsx), which draw\n * each room or defect as a `Card variant=\"inset\"` because a row could not hold a\n * photo strip with its own buttons.\n *\n * The same rule as {@link actions}, one row down: the title area stays ONE named\n * target (the button or link, with the title and subtitle as its name), and the body\n * is its sibling, never nested in it — a thumbnail's \"Remove\" inside the row's button\n * would be invalid HTML and unreachable by a screen reader. The body is a\n * `role=\"group\"` named by the row's title (`aria-labelledby`), so tabbing into the\n * third photo's button of the second room is heard as \"Kitchen, group\" first rather\n * than as a button floating free of any row. Inside the row's box, so a `selected`\n * border and a `divider` hold both; the hover fill stays on the target alone, since\n * the body is not what a click on the row does.\n */\n children?: ReactNode;\n /** Classes on the {@link children} body. */\n bodyClassName?: string;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | {\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n | {\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`\n * share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n }\n | {\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n );\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating by default), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(\n {\n title,\n titleLines = 1,\n subtitle,\n subtitleLines = 1,\n meta,\n icon: Icon,\n leading,\n trailing,\n actions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n ...rest\n },\n ref,\n) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const kitLink = useKitLink();\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && onClick !== undefined && href === undefined;\n const interactive = isLink || isButton;\n const inert = disabled || loading;\n const hasBody = children !== undefined && children !== null && children !== false;\n const titleId = useId();\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && (\n <Icon\n className={cn(\"size-4 shrink-0 text-[var(--text-muted)]\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span\n // Names the body's group; harmless without one.\n id={hasBody ? titleId : undefined}\n className={cn(\n \"block text-sm text-[var(--text-primary)]\",\n titleLines === 1 ? \"truncate\" : titleLines === 2 ? \"line-clamp-2 break-words\" : \"break-words\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-[var(--text-muted)]\",\n subtitleLines === 2 ? \"line-clamp-2\" : \"truncate\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && <span className=\"mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]\">{meta}</span>}\n </span>\n {trailing != null && <span className=\"flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]\">{trailing}</span>}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external && href !== undefined && (\n <ExternalLink className=\"size-3.5 shrink-0 text-[var(--text-muted)] rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // With a body the target is the top of the box, not all of it, so it takes its\n // own corners and its own hover fill (see `children`).\n hasBody ? \"rounded-md\" : \"rounded-[inherit]\",\n hasBody && interactive && !inert && !selected && \"transition-colors hover:bg-[var(--bg-hover)]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading\n ? (e) => e.preventDefault()\n : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n main = render ? (\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading`.\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n onClick={loading ? undefined : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)}\n className={target}\n >\n {body}\n </button>\n );\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-[var(--border)] last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n )}\n >\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n hasBody ? \"flex-col\" : \"items-center\",\n selected\n ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n !hasBody && interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {hasBody ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {main}\n {actionsEl}\n </div>\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n </>\n ) : (\n <>\n {main}\n {actionsEl}\n </>\n )}\n </div>\n </Tag>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":["List","ListItem"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type LoadingStateSize = "sm" | "md" | "lg";
|
|
5
|
+
interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
6
|
+
/** The visible words under the spinner, and what the status region announces.
|
|
7
|
+
* Default: `common.loading` ("Loading…"). */
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
/** `sm` for a panel or a table body, `md` (default) for a section, `lg` for a page. */
|
|
10
|
+
size?: LoadingStateSize;
|
|
11
|
+
/** Spinner beside the text on one line, rather than above it. */
|
|
12
|
+
inline?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A spinner and the words "Loading…", centred where the content will be.
|
|
16
|
+
*
|
|
17
|
+
* kastlan's `LoadingState` (feedback/loading-state.tsx:14), which got the reading
|
|
18
|
+
* right the second time: the WRAPPER is the `role="status"` region and the words are
|
|
19
|
+
* visible text inside it, so the spinner is decorative (`label={null}`) — otherwise
|
|
20
|
+
* the reader hears "Loading… Loading…", one from each.
|
|
21
|
+
*/
|
|
22
|
+
declare function LoadingState({ label, size, inline, className, ...rest }: LoadingStateProps): react.JSX.Element;
|
|
23
|
+
|
|
24
|
+
export { LoadingState, type LoadingStateProps, type LoadingStateSize };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
+
import { Spinner } from "./ui.js";
|
|
6
|
+
const SPINNER = { sm: "h-4 w-4", md: "h-8 w-8", lg: "h-10 w-10" };
|
|
7
|
+
const TEXT = { sm: "text-xs", md: "text-sm", lg: "text-base" };
|
|
8
|
+
const PAD = { sm: "py-4", md: "py-12", lg: "py-20" };
|
|
9
|
+
function LoadingState({ label, size = "md", inline = false, className, ...rest }) {
|
|
10
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
11
|
+
return /* @__PURE__ */ jsxs(
|
|
12
|
+
"div",
|
|
13
|
+
{
|
|
14
|
+
...rest,
|
|
15
|
+
role: "status",
|
|
16
|
+
className: cn(
|
|
17
|
+
"flex items-center justify-center text-center text-[var(--text-muted)]",
|
|
18
|
+
inline ? "gap-2" : "flex-col gap-2",
|
|
19
|
+
PAD[size],
|
|
20
|
+
className
|
|
21
|
+
),
|
|
22
|
+
children: [
|
|
23
|
+
/* @__PURE__ */ jsx(Spinner, { label: null, className: SPINNER[size] }),
|
|
24
|
+
/* @__PURE__ */ jsx("span", { className: TEXT[size], children: label ?? common.loading })
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
LoadingState
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=loading-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/loading-state.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { Spinner } from \"./ui\";\n\nexport type LoadingStateSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** The visible words under the spinner, and what the status region announces.\n * Default: `common.loading` (\"Loading…\"). */\n label?: ReactNode;\n /** `sm` for a panel or a table body, `md` (default) for a section, `lg` for a page. */\n size?: LoadingStateSize;\n /** Spinner beside the text on one line, rather than above it. */\n inline?: boolean;\n}\n\nconst SPINNER: Record<LoadingStateSize, string> = { sm: \"h-4 w-4\", md: \"h-8 w-8\", lg: \"h-10 w-10\" };\nconst TEXT: Record<LoadingStateSize, string> = { sm: \"text-xs\", md: \"text-sm\", lg: \"text-base\" };\nconst PAD: Record<LoadingStateSize, string> = { sm: \"py-4\", md: \"py-12\", lg: \"py-20\" };\n\n/**\n * A spinner and the words \"Loading…\", centred where the content will be.\n *\n * kastlan's `LoadingState` (feedback/loading-state.tsx:14), which got the reading\n * right the second time: the WRAPPER is the `role=\"status\"` region and the words are\n * visible text inside it, so the spinner is decorative (`label={null}`) — otherwise\n * the reader hears \"Loading… Loading…\", one from each.\n */\nexport function LoadingState({ label, size = \"md\", inline = false, className, ...rest }: LoadingStateProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n return (\n <div\n {...rest}\n role=\"status\"\n className={cn(\n \"flex items-center justify-center text-center text-[var(--text-muted)]\",\n inline ? \"gap-2\" : \"flex-col gap-2\",\n PAD[size],\n className,\n )}\n >\n <Spinner label={null} className={SPINNER[size]} />\n <span className={TEXT[size]}>{label ?? common.loading}</span>\n </div>\n );\n}\n"],"mappings":";AAgCI,SAUE,KAVF;AA/BJ,SAAS,UAAU;AACnB,SAAS,uBAAuB,oBAAoB;AACpD,SAAS,eAAe;AAcxB,MAAM,UAA4C,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAClG,MAAM,OAAyC,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAC/F,MAAM,MAAwC,EAAE,IAAI,QAAQ,IAAI,SAAS,IAAI,QAAQ;AAU9E,SAAS,aAAa,EAAE,OAAO,OAAO,MAAM,SAAS,OAAO,WAAW,GAAG,KAAK,GAAsB;AAC1G,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU;AAAA,QACnB,IAAI,IAAI;AAAA,QACR;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,WAAQ,OAAO,MAAM,WAAW,QAAQ,IAAI,GAAG;AAAA,QAChD,oBAAC,UAAK,WAAW,KAAK,IAAI,GAAI,mBAAS,OAAO,SAAQ;AAAA;AAAA;AAAA,EACxD;AAEJ;","names":[]}
|
|
@@ -80,7 +80,8 @@ type MenuItemProps = MenuItemBaseProps & ({
|
|
|
80
80
|
/** Makes the row a link — keksdose's "Manage budgets" and account-menu rows. */
|
|
81
81
|
href: string;
|
|
82
82
|
/** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads
|
|
83
|
-
* a single-page app. Default `<
|
|
83
|
+
* a single-page app. Default: the `<UiKitProvider linkComponent>`, then `<a>`.
|
|
84
|
+
* The same API as `Chip` and `StatTile`. */
|
|
84
85
|
renderLink?: (props: MenuItemLinkProps) => ReactElement;
|
|
85
86
|
/** Runs on the way (close the menu). */
|
|
86
87
|
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
@@ -3,6 +3,8 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { forwardRef } from "react";
|
|
4
4
|
import { Check } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { useKitLink } from "../i18n/kit-labels.js";
|
|
7
|
+
import { pickLinkRenderer } from "./text-link.js";
|
|
6
8
|
const MENU_ITEM_ROW = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
|
|
7
9
|
const MENU_ITEM_FOCUS = "focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
|
|
8
10
|
const MenuItem = forwardRef(function MenuItem2({
|
|
@@ -22,6 +24,7 @@ const MenuItem = forwardRef(function MenuItem2({
|
|
|
22
24
|
current,
|
|
23
25
|
...rest
|
|
24
26
|
}, ref) {
|
|
27
|
+
const kitLink = useKitLink();
|
|
25
28
|
const on = href !== void 0 ? !!current : !!checked;
|
|
26
29
|
const look = cn(
|
|
27
30
|
MENU_ITEM_ROW,
|
|
@@ -56,7 +59,8 @@ const MenuItem = forwardRef(function MenuItem2({
|
|
|
56
59
|
"aria-current": current ? "page" : void 0,
|
|
57
60
|
children: body
|
|
58
61
|
};
|
|
59
|
-
|
|
62
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
63
|
+
if (render) return /* @__PURE__ */ jsx(RenderedLink, { render, ...linkProps });
|
|
60
64
|
const { children: linkBody, ...anchorProps } = linkProps;
|
|
61
65
|
return /* @__PURE__ */ jsx("a", { ...anchorProps, children: linkBody });
|
|
62
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/menu-item.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { Check } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * `TOPBAR_MENU_ITEM_CLASS`'s row, spelled out rather than imported: a component does\n * not reach up into the shell (the same call `Disclosure`'s menu variant made). A test\n * holds the two to the same classes, so a menu mixing `MenuItem`s with the shell's own\n * rows (`TopBarActionMenu`, `OptionSwitcherMenu`) cannot drift into two looks.\n */\nconst MENU_ITEM_ROW =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\n\n/** HoverMenu moves focus onto items with the arrow keys; a pointer user never sees the\n * ring, a keyboard user always does. Inset, because the panel clips its children. */\nconst MENU_ITEM_FOCUS =\n \"focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\nexport type MenuItemTone = \"default\" | \"danger\";\n\n/** What {@link MenuItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what the link calls it —\n * `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface MenuItemLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n role: \"menuitem\";\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"page\";\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\ninterface MenuItemBaseProps {\n /** The row's text. */\n children: ReactNode;\n /** An icon before the label, drawn at the menu's 16px. */\n icon?: LucideIcon;\n /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */\n leading?: ReactNode;\n /**\n * Right after the label text, before the gap that pushes {@link trailing} to the\n * row's end: keksdose's budget switcher tags a budget \"Guest\" or \"Shared\" with a\n * `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end\n * where a count or a check mark sits. Hand-placed inside `children` it was truncated\n * with the name — a long budget name ate the chip first. Here the label truncates\n * and the badge never shrinks, so it stays visible beside what is left of the name.\n * It is part of the row's accessible name (\"Household Shared\"), so keep it text.\n */\n badge?: ReactNode;\n /** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */\n trailing?: ReactNode;\n /** `danger` for the row that destroys or signs out. Colour is not the whole message:\n * the label must still say what the row does. */\n tone?: MenuItemTone;\n disabled?: boolean;\n className?: string;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * A menu row is a button OR a link, and only the button can be CHECKED: a checked link\n * is a link to where you already are, which is `current`, not a selection.\n */\nexport type MenuItemProps = MenuItemBaseProps &\n (\n | {\n href?: never;\n renderLink?: never;\n current?: never;\n /** Runs the row. Close the menu from here — `HoverMenu` hands its `close` to\n * the render prop that draws the rows. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Makes the row a CHOICE and says whether it is chosen. Leave it undefined for a\n * plain command; `false` is a choice that is not chosen, which is different.\n * A check mark is drawn at the end when it is on.\n */\n checked?: boolean;\n /**\n * What kind of choice, when `checked` is given:\n *\n * - `radio` (default): one of a set — keksdose's budget switcher\n * (budget-switcher.tsx:51), the theme and language pickers. `menuitemradio`.\n * - `checkbox`: an independent on/off — \"Show archived\". `menuitemcheckbox`.\n *\n * Not `aria-pressed`: that belongs to a toggle BUTTON, and inside a\n * `role=\"menu\"` a screen reader expects the menu roles and reads \"checked\"\n * only from them. A button with `aria-pressed` in a menu is announced as a\n * button in a menu, which is two widgets in one.\n */\n checkable?: \"radio\" | \"checkbox\";\n }\n | {\n /** Makes the row a link — keksdose's \"Manage budgets\" and account-menu rows. */\n href: string;\n /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads\n * a single-page app. Default `<a>`. The same API as `Chip` and `StatTile`. */\n renderLink?: (props: MenuItemLinkProps) => ReactElement;\n /** Runs on the way (close the menu). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n checked?: never;\n checkable?: never;\n }\n );\n\n/**\n * One row of a `HoverMenu` / `TopBarActionMenu` panel.\n *\n * keksdose's account menu and budget switcher each rebuild this row by hand — the\n * shell's class, an icon, a label, a badge, a check mark on the active budget — and the\n * check mark was the only thing saying which budget was active, so a screen reader\n * heard four identical buttons. This row says it (`menuitemradio` + `aria-checked`).\n *\n * The role is set HERE rather than left to `HoverMenu`, which promotes only role-less\n * buttons and links to `menuitem`: a checked row must be `menuitemradio` or\n * `menuitemcheckbox` from its first render, and `HoverMenu` leaves an element that\n * already has a role alone — so its roving focus (↑/↓, Home/End) picks these rows up\n * with the rest. Render it inside an `<li>` like the shell's own rows; `HoverMenu`\n * turns the list markup into `role=\"none\"`.\n */\nexport const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem(\n {\n children,\n icon: Icon,\n leading,\n badge,\n trailing,\n tone = \"default\",\n disabled = false,\n className,\n href,\n renderLink,\n onClick,\n checked,\n checkable,\n current,\n ...rest\n },\n ref,\n) {\n const on = href !== undefined ? !!current : !!checked;\n const look = cn(\n MENU_ITEM_ROW,\n MENU_ITEM_FOCUS,\n tone === \"danger\" && \"text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:bg-[var(--danger-bg)]\",\n disabled && \"cursor-default opacity-50 hover:bg-transparent\",\n className,\n );\n const body = (\n <>\n <span className=\"flex min-w-0 items-center gap-2\">\n {Icon && <Icon className=\"size-4 shrink-0\" aria-hidden />}\n {leading}\n <span className=\"min-w-0 truncate\">{children}</span>\n {badge != null && badge !== false && (\n <>\n {/* A space between the name and the badge in the accessible name (\"Household\n Shared\", not \"HouseholdShared\"); whitespace between flex items is not\n laid out, so it costs nothing on screen. */}{\" \"}\n <span data-menu-item-badge=\"\" className=\"flex shrink-0 items-center\">\n {badge}\n </span>\n </>\n )}\n </span>\n {(trailing != null || on) && (\n <span className=\"flex shrink-0 items-center gap-2\">\n {trailing}\n {on && <Check className=\"size-4 text-[var(--text-secondary)]\" aria-hidden />}\n </span>\n )}\n </>\n );\n\n if (href !== undefined && !disabled) {\n const linkProps: MenuItemLinkProps = {\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n role: \"menuitem\",\n className: look,\n onClick: onClick as MenuItemLinkProps[\"onClick\"],\n \"aria-current\": current ? \"page\" : undefined,\n children: body,\n };\n if (renderLink) return <RenderedLink render={renderLink} {...linkProps} />;\n const { children: linkBody, ...anchorProps } = linkProps;\n return <a {...anchorProps}>{linkBody}</a>;\n }\n\n const role =\n href === undefined && checked !== undefined\n ? checkable === \"checkbox\"\n ? \"menuitemcheckbox\"\n : \"menuitemradio\"\n : \"menuitem\";\n return (\n <button\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n role={role}\n aria-checked={role === \"menuitem\" ? undefined : !!checked}\n // `aria-disabled` rather than `disabled`: HoverMenu's roving focus skips either,\n // and the ARIA menu pattern keeps an unavailable item perceivable (\"Delete,\n // dimmed\") rather than greyed to the point some browsers stop exposing it.\n aria-disabled={disabled || undefined}\n onClick={\n disabled\n ? (e) => e.preventDefault()\n : (onClick as ((event: MouseEvent<HTMLButtonElement>) => void) | undefined)\n }\n className={look}\n >\n {body}\n </button>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: MenuItemLinkProps & { render: (props: MenuItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAiKiB,SAIP,UAJO,KAIP,YAJO;AAjKjB,SAAS,kBAAkB;AAE3B,SAAS,aAAa;AAEtB,SAAS,UAAU;AAQnB,MAAM,gBACJ;AAIF,MAAM,kBACJ;AAgHK,MAAM,WAAW,WAAuC,SAASA,UACtE;AAAA,EACE;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,KAAK,SAAS,SAAY,CAAC,CAAC,UAAU,CAAC,CAAC;AAC9C,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,SAAS,YAAY;AAAA,IACrB,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,OACJ,iCACE;AAAA,yBAAC,UAAK,WAAU,mCACb;AAAA,cAAQ,oBAAC,QAAK,WAAU,mBAAkB,eAAW,MAAC;AAAA,MACtD;AAAA,MACD,oBAAC,UAAK,WAAU,oBAAoB,UAAS;AAAA,MAC5C,SAAS,QAAQ,UAAU,SAC1B,iCAGmD;AAAA;AAAA,QACjD,oBAAC,UAAK,wBAAqB,IAAG,WAAU,8BACrC,iBACH;AAAA,SACF;AAAA,OAEJ;AAAA,KACE,YAAY,QAAQ,OACpB,qBAAC,UAAK,WAAU,oCACb;AAAA;AAAA,MACA,MAAM,oBAAC,SAAM,WAAU,uCAAsC,eAAW,MAAC;AAAA,OAC5E;AAAA,KAEJ;AAGF,MAAI,SAAS,UAAa,CAAC,UAAU;AACnC,UAAM,YAA+B;AAAA,MACnC,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,MACA,gBAAgB,UAAU,SAAS;AAAA,MACnC,UAAU;AAAA,IACZ;AACA,QAAI,WAAY,QAAO,oBAAC,gBAAa,QAAQ,YAAa,GAAG,WAAW;AACxE,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,WAAO,oBAAC,OAAG,GAAG,aAAc,oBAAS;AAAA,EACvC;AAEA,QAAM,OACJ,SAAS,UAAa,YAAY,SAC9B,cAAc,aACZ,qBACA,kBACF;AACN,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAK;AAAA,MACL;AAAA,MACA,gBAAc,SAAS,aAAa,SAAY,CAAC,CAAC;AAAA,MAIlD,iBAAe,YAAY;AAAA,MAC3B,SACE,WACI,CAAC,MAAM,EAAE,eAAe,IACvB;AAAA,MAEP,WAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":["MenuItem"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/menu-item.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { Check } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n/**\n * `TOPBAR_MENU_ITEM_CLASS`'s row, spelled out rather than imported: a component does\n * not reach up into the shell (the same call `Disclosure`'s menu variant made). A test\n * holds the two to the same classes, so a menu mixing `MenuItem`s with the shell's own\n * rows (`TopBarActionMenu`, `OptionSwitcherMenu`) cannot drift into two looks.\n */\nconst MENU_ITEM_ROW =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\n\n/** HoverMenu moves focus onto items with the arrow keys; a pointer user never sees the\n * ring, a keyboard user always does. Inset, because the panel clips its children. */\nconst MENU_ITEM_FOCUS =\n \"focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\nexport type MenuItemTone = \"default\" | \"danger\";\n\n/** What {@link MenuItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what the link calls it —\n * `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface MenuItemLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n role: \"menuitem\";\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"page\";\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\ninterface MenuItemBaseProps {\n /** The row's text. */\n children: ReactNode;\n /** An icon before the label, drawn at the menu's 16px. */\n icon?: LucideIcon;\n /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */\n leading?: ReactNode;\n /**\n * Right after the label text, before the gap that pushes {@link trailing} to the\n * row's end: keksdose's budget switcher tags a budget \"Guest\" or \"Shared\" with a\n * `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end\n * where a count or a check mark sits. Hand-placed inside `children` it was truncated\n * with the name — a long budget name ate the chip first. Here the label truncates\n * and the badge never shrinks, so it stays visible beside what is left of the name.\n * It is part of the row's accessible name (\"Household Shared\"), so keep it text.\n */\n badge?: ReactNode;\n /** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */\n trailing?: ReactNode;\n /** `danger` for the row that destroys or signs out. Colour is not the whole message:\n * the label must still say what the row does. */\n tone?: MenuItemTone;\n disabled?: boolean;\n className?: string;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * A menu row is a button OR a link, and only the button can be CHECKED: a checked link\n * is a link to where you already are, which is `current`, not a selection.\n */\nexport type MenuItemProps = MenuItemBaseProps &\n (\n | {\n href?: never;\n renderLink?: never;\n current?: never;\n /** Runs the row. Close the menu from here — `HoverMenu` hands its `close` to\n * the render prop that draws the rows. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Makes the row a CHOICE and says whether it is chosen. Leave it undefined for a\n * plain command; `false` is a choice that is not chosen, which is different.\n * A check mark is drawn at the end when it is on.\n */\n checked?: boolean;\n /**\n * What kind of choice, when `checked` is given:\n *\n * - `radio` (default): one of a set — keksdose's budget switcher\n * (budget-switcher.tsx:51), the theme and language pickers. `menuitemradio`.\n * - `checkbox`: an independent on/off — \"Show archived\". `menuitemcheckbox`.\n *\n * Not `aria-pressed`: that belongs to a toggle BUTTON, and inside a\n * `role=\"menu\"` a screen reader expects the menu roles and reads \"checked\"\n * only from them. A button with `aria-pressed` in a menu is announced as a\n * button in a menu, which is two widgets in one.\n */\n checkable?: \"radio\" | \"checkbox\";\n }\n | {\n /** Makes the row a link — keksdose's \"Manage budgets\" and account-menu rows. */\n href: string;\n /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads\n * a single-page app. Default: the `<UiKitProvider linkComponent>`, then `<a>`.\n * The same API as `Chip` and `StatTile`. */\n renderLink?: (props: MenuItemLinkProps) => ReactElement;\n /** Runs on the way (close the menu). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n checked?: never;\n checkable?: never;\n }\n );\n\n/**\n * One row of a `HoverMenu` / `TopBarActionMenu` panel.\n *\n * keksdose's account menu and budget switcher each rebuild this row by hand — the\n * shell's class, an icon, a label, a badge, a check mark on the active budget — and the\n * check mark was the only thing saying which budget was active, so a screen reader\n * heard four identical buttons. This row says it (`menuitemradio` + `aria-checked`).\n *\n * The role is set HERE rather than left to `HoverMenu`, which promotes only role-less\n * buttons and links to `menuitem`: a checked row must be `menuitemradio` or\n * `menuitemcheckbox` from its first render, and `HoverMenu` leaves an element that\n * already has a role alone — so its roving focus (↑/↓, Home/End) picks these rows up\n * with the rest. Render it inside an `<li>` like the shell's own rows; `HoverMenu`\n * turns the list markup into `role=\"none\"`.\n */\nexport const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem(\n {\n children,\n icon: Icon,\n leading,\n badge,\n trailing,\n tone = \"default\",\n disabled = false,\n className,\n href,\n renderLink,\n onClick,\n checked,\n checkable,\n current,\n ...rest\n },\n ref,\n) {\n const kitLink = useKitLink();\n const on = href !== undefined ? !!current : !!checked;\n const look = cn(\n MENU_ITEM_ROW,\n MENU_ITEM_FOCUS,\n tone === \"danger\" && \"text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:bg-[var(--danger-bg)]\",\n disabled && \"cursor-default opacity-50 hover:bg-transparent\",\n className,\n );\n const body = (\n <>\n <span className=\"flex min-w-0 items-center gap-2\">\n {Icon && <Icon className=\"size-4 shrink-0\" aria-hidden />}\n {leading}\n <span className=\"min-w-0 truncate\">{children}</span>\n {badge != null && badge !== false && (\n <>\n {/* A space between the name and the badge in the accessible name (\"Household\n Shared\", not \"HouseholdShared\"); whitespace between flex items is not\n laid out, so it costs nothing on screen. */}{\" \"}\n <span data-menu-item-badge=\"\" className=\"flex shrink-0 items-center\">\n {badge}\n </span>\n </>\n )}\n </span>\n {(trailing != null || on) && (\n <span className=\"flex shrink-0 items-center gap-2\">\n {trailing}\n {on && <Check className=\"size-4 text-[var(--text-secondary)]\" aria-hidden />}\n </span>\n )}\n </>\n );\n\n if (href !== undefined && !disabled) {\n const linkProps: MenuItemLinkProps = {\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n role: \"menuitem\",\n className: look,\n onClick: onClick as MenuItemLinkProps[\"onClick\"],\n \"aria-current\": current ? \"page\" : undefined,\n children: body,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedLink render={render} {...linkProps} />;\n const { children: linkBody, ...anchorProps } = linkProps;\n return <a {...anchorProps}>{linkBody}</a>;\n }\n\n const role =\n href === undefined && checked !== undefined\n ? checkable === \"checkbox\"\n ? \"menuitemcheckbox\"\n : \"menuitemradio\"\n : \"menuitem\";\n return (\n <button\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n role={role}\n aria-checked={role === \"menuitem\" ? undefined : !!checked}\n // `aria-disabled` rather than `disabled`: HoverMenu's roving focus skips either,\n // and the ARIA menu pattern keeps an unavailable item perceivable (\"Delete,\n // dimmed\") rather than greyed to the point some browsers stop exposing it.\n aria-disabled={disabled || undefined}\n onClick={\n disabled\n ? (e) => e.preventDefault()\n : (onClick as ((event: MouseEvent<HTMLButtonElement>) => void) | undefined)\n }\n className={look}\n >\n {body}\n </button>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: MenuItemLinkProps & { render: (props: MenuItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAqKiB,SAIP,UAJO,KAIP,YAJO;AArKjB,SAAS,kBAAkB;AAE3B,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAQjC,MAAM,gBACJ;AAIF,MAAM,kBACJ;AAiHK,MAAM,WAAW,WAAuC,SAASA,UACtE;AAAA,EACE;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,UAAU,WAAW;AAC3B,QAAM,KAAK,SAAS,SAAY,CAAC,CAAC,UAAU,CAAC,CAAC;AAC9C,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,SAAS,YAAY;AAAA,IACrB,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,OACJ,iCACE;AAAA,yBAAC,UAAK,WAAU,mCACb;AAAA,cAAQ,oBAAC,QAAK,WAAU,mBAAkB,eAAW,MAAC;AAAA,MACtD;AAAA,MACD,oBAAC,UAAK,WAAU,oBAAoB,UAAS;AAAA,MAC5C,SAAS,QAAQ,UAAU,SAC1B,iCAGmD;AAAA;AAAA,QACjD,oBAAC,UAAK,wBAAqB,IAAG,WAAU,8BACrC,iBACH;AAAA,SACF;AAAA,OAEJ;AAAA,KACE,YAAY,QAAQ,OACpB,qBAAC,UAAK,WAAU,oCACb;AAAA;AAAA,MACA,MAAM,oBAAC,SAAM,WAAU,uCAAsC,eAAW,MAAC;AAAA,OAC5E;AAAA,KAEJ;AAGF,MAAI,SAAS,UAAa,CAAC,UAAU;AACnC,UAAM,YAA+B;AAAA,MACnC,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,MACA,gBAAgB,UAAU,SAAS;AAAA,MACnC,UAAU;AAAA,IACZ;AACA,UAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,QAAI,OAAQ,QAAO,oBAAC,gBAAa,QAAiB,GAAG,WAAW;AAChE,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,WAAO,oBAAC,OAAG,GAAG,aAAc,oBAAS;AAAA,EACvC;AAEA,QAAM,OACJ,SAAS,UAAa,YAAY,SAC9B,cAAc,aACZ,qBACA,kBACF;AACN,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAK;AAAA,MACL;AAAA,MACA,gBAAc,SAAS,aAAa,SAAY,CAAC,CAAC;AAAA,MAIlD,iBAAe,YAAY;AAAA,MAC3B,SACE,WACI,CAAC,MAAM,EAAE,eAAe,IACvB;AAAA,MAEP,WAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":["MenuItem"]}
|
|
@@ -140,6 +140,18 @@ interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">, "onSel
|
|
|
140
140
|
* it is what names the grid.
|
|
141
141
|
*/
|
|
142
142
|
hideNavigation?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Fill the leading and trailing cells with the neighbouring months' days, muted —
|
|
145
|
+
* the wall-calendar look, where the first row does not start on a hole.
|
|
146
|
+
*
|
|
147
|
+
* They are real days, not decoration: each is named with its whole date, honours
|
|
148
|
+
* `min`/`max`, and a click selects it and moves the grid to its month (focus goes
|
|
149
|
+
* with it). They are NOT stops of the roving tabindex — the keyboard already reaches
|
|
150
|
+
* them by arrowing off the month's edge, which moves the grid the same way — and
|
|
151
|
+
* `renderDay` is not called for them, so a day's events are drawn once, in its own
|
|
152
|
+
* month. Default `false`: the cells stay empty.
|
|
153
|
+
*/
|
|
154
|
+
showOutsideDays?: boolean;
|
|
143
155
|
/** Extra classes for the calendar's root. */
|
|
144
156
|
className?: string;
|
|
145
157
|
}
|
|
@@ -184,6 +196,6 @@ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
|
184
196
|
* month would have holes that the roving tabindex falls into. They stay reachable and
|
|
185
197
|
* readable, and the click handler is what refuses them.
|
|
186
198
|
*/
|
|
187
|
-
declare function MiniCalendar({ from, to, locale: localeProp, weekStartsOn, onSelect, mode, min, max, labels: labelsProp, focusOnOpen, size, renderDay, month, onMonthChange, hideNavigation, className, ...rest }: MiniCalendarProps): react.JSX.Element;
|
|
199
|
+
declare function MiniCalendar({ from, to, locale: localeProp, weekStartsOn, onSelect, mode, min, max, labels: labelsProp, focusOnOpen, size, renderDay, month, onMonthChange, hideNavigation, showOutsideDays, className, ...rest }: MiniCalendarProps): react.JSX.Element;
|
|
188
200
|
|
|
189
201
|
export { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarDayState, type MiniCalendarLabels, type MiniCalendarProps, type WeekDay };
|