@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,190 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ImgHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { ImageOff } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
7
|
+
import { SKELETON_CLASS } from "./skeleton";
|
|
8
|
+
import { useAuthedSrc } from "../hooks/use-authed-src";
|
|
9
|
+
import type { AuthedFetcher, UseAuthedSrcResult } from "../hooks/use-authed-src";
|
|
10
|
+
|
|
11
|
+
/** The two states an image can be in besides "there". */
|
|
12
|
+
export interface AuthedImageLabels {
|
|
13
|
+
/** Read out while the bytes are on their way (the skeleton itself is silent). */
|
|
14
|
+
loading: string;
|
|
15
|
+
/** Shown, small, in place of an image that could not be fetched or decoded. */
|
|
16
|
+
loadError: string;
|
|
17
|
+
/** The failed tile's accessible name, given the image's alt. */
|
|
18
|
+
failedImage: (alt: string) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_AUTHED_IMAGE_LABELS: AuthedImageLabels = {
|
|
22
|
+
loading: "Loading image…",
|
|
23
|
+
loadError: "Image couldn’t be loaded",
|
|
24
|
+
failedImage: (alt) => `${alt}: image couldn’t be loaded`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface AuthedImageProps
|
|
28
|
+
extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children"> {
|
|
29
|
+
/** The image's URL — an API path when a {@link fetcher} is given. */
|
|
30
|
+
src: string | null | undefined;
|
|
31
|
+
/** Required: an image without a text alternative is a gap a reader falls into.
|
|
32
|
+
* `""` for a purely decorative one. */
|
|
33
|
+
alt: string;
|
|
34
|
+
/** The app's authenticated GET (see {@link useAuthedSrc}). Left out, `src` is loaded
|
|
35
|
+
* by the browser as usual and this is an `<img>` with loading and error states. */
|
|
36
|
+
fetcher?: AuthedFetcher | null;
|
|
37
|
+
/** Classes for the box around the image (the placeholders fill it). */
|
|
38
|
+
wrapperClassName?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Wrap the image in a link to itself, opening in a new tab — kastlan's
|
|
41
|
+
* `AuthedImage` did, because an object URL is the only address of a protected image
|
|
42
|
+
* a browser tab can open. The link's href is the `blob:` URL, never the API path.
|
|
43
|
+
*/
|
|
44
|
+
link?: boolean;
|
|
45
|
+
/** Replaces the default skeleton while loading. */
|
|
46
|
+
loadingFallback?: ReactNode;
|
|
47
|
+
/** Replaces the default "couldn't load" tile. */
|
|
48
|
+
errorFallback?: ReactNode;
|
|
49
|
+
labels?: Partial<AuthedImageLabels>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An image with a loading state, an error state and — given a `fetcher` — the app's
|
|
54
|
+
* authentication. Replaces kastlan's `shared/components/authed-image.tsx`, which
|
|
55
|
+
* rendered NOTHING until the blob arrived and nothing at all if it never did: a card
|
|
56
|
+
* with a photo slot either had the photo or was silently shorter.
|
|
57
|
+
*
|
|
58
|
+
* Two failures are one state here: the fetch failing (401, 404, offline) and the
|
|
59
|
+
* browser failing to decode what arrived (`onError` on the `<img>`, e.g. a PDF served
|
|
60
|
+
* where an image was expected).
|
|
61
|
+
*/
|
|
62
|
+
export function AuthedImage({
|
|
63
|
+
src,
|
|
64
|
+
alt,
|
|
65
|
+
fetcher,
|
|
66
|
+
wrapperClassName,
|
|
67
|
+
className,
|
|
68
|
+
link,
|
|
69
|
+
loadingFallback,
|
|
70
|
+
errorFallback,
|
|
71
|
+
labels: labelsProp,
|
|
72
|
+
onLoad,
|
|
73
|
+
onError,
|
|
74
|
+
...rest
|
|
75
|
+
}: AuthedImageProps) {
|
|
76
|
+
const labels = useKitLabels("authedImage", DEFAULT_AUTHED_IMAGE_LABELS, labelsProp);
|
|
77
|
+
const fetched = useAuthedSrc(src, { fetcher });
|
|
78
|
+
return (
|
|
79
|
+
<ImageStates
|
|
80
|
+
fetched={fetched}
|
|
81
|
+
alt={alt}
|
|
82
|
+
labels={labels}
|
|
83
|
+
className={className}
|
|
84
|
+
wrapperClassName={wrapperClassName}
|
|
85
|
+
link={link}
|
|
86
|
+
loadingFallback={loadingFallback}
|
|
87
|
+
errorFallback={errorFallback}
|
|
88
|
+
imgProps={rest}
|
|
89
|
+
onLoad={onLoad}
|
|
90
|
+
onError={onError}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The rendering half of {@link AuthedImage}, for a caller that already holds a
|
|
97
|
+
* {@link useAuthedSrc} result (the lightbox needs the object URL for its download link
|
|
98
|
+
* as well). Internal to the package.
|
|
99
|
+
*/
|
|
100
|
+
export function ImageStates({
|
|
101
|
+
fetched,
|
|
102
|
+
alt,
|
|
103
|
+
labels,
|
|
104
|
+
className,
|
|
105
|
+
wrapperClassName,
|
|
106
|
+
link,
|
|
107
|
+
loadingFallback,
|
|
108
|
+
errorFallback,
|
|
109
|
+
imgProps,
|
|
110
|
+
onLoad,
|
|
111
|
+
onError,
|
|
112
|
+
}: {
|
|
113
|
+
fetched: UseAuthedSrcResult;
|
|
114
|
+
alt: string;
|
|
115
|
+
labels: AuthedImageLabels;
|
|
116
|
+
className?: string;
|
|
117
|
+
wrapperClassName?: string;
|
|
118
|
+
link?: boolean;
|
|
119
|
+
loadingFallback?: ReactNode;
|
|
120
|
+
errorFallback?: ReactNode;
|
|
121
|
+
imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children" | "onLoad" | "onError">;
|
|
122
|
+
onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
|
|
123
|
+
onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
|
|
124
|
+
}) {
|
|
125
|
+
// Decode state, per src: keyed so a new picture starts over rather than inheriting
|
|
126
|
+
// the previous one's "loaded" or "broken".
|
|
127
|
+
const [decode, setDecode] = useState<{ src: string | null; state: "loaded" | "error" } | null>(null);
|
|
128
|
+
const decoded = decode && decode.src === fetched.src ? decode.state : "pending";
|
|
129
|
+
const failed = fetched.status === "error" || decoded === "error";
|
|
130
|
+
const waiting = !failed && (fetched.status === "loading" || (fetched.src !== null && decoded === "pending"));
|
|
131
|
+
|
|
132
|
+
const img = fetched.src !== null && !failed && (
|
|
133
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- load/error are resource events, not interactions
|
|
134
|
+
<img
|
|
135
|
+
{...imgProps}
|
|
136
|
+
src={fetched.src}
|
|
137
|
+
alt={alt}
|
|
138
|
+
onLoad={(e) => {
|
|
139
|
+
setDecode({ src: fetched.src, state: "loaded" });
|
|
140
|
+
onLoad?.(e);
|
|
141
|
+
}}
|
|
142
|
+
onError={(e) => {
|
|
143
|
+
setDecode({ src: fetched.src, state: "error" });
|
|
144
|
+
onError?.(e);
|
|
145
|
+
}}
|
|
146
|
+
className={cn(
|
|
147
|
+
// Kept in the tree while it decodes (the browser needs it there to load at
|
|
148
|
+
// all), but invisible, so the skeleton is what shows.
|
|
149
|
+
decoded === "pending" && "opacity-0",
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<span
|
|
157
|
+
data-image-state={failed ? "error" : waiting ? "loading" : fetched.status === "idle" ? "idle" : "ready"}
|
|
158
|
+
className={cn("relative block overflow-hidden", wrapperClassName)}
|
|
159
|
+
aria-busy={waiting || undefined}
|
|
160
|
+
>
|
|
161
|
+
{link && img && fetched.src ? (
|
|
162
|
+
<a href={fetched.src} target="_blank" rel="noreferrer noopener" className="block h-full w-full">
|
|
163
|
+
{img}
|
|
164
|
+
</a>
|
|
165
|
+
) : (
|
|
166
|
+
img
|
|
167
|
+
)}
|
|
168
|
+
{waiting &&
|
|
169
|
+
(loadingFallback ?? (
|
|
170
|
+
<span className={cn(SKELETON_CLASS, "absolute inset-0 block rounded-none")}>
|
|
171
|
+
<span className="sr-only">{labels.loading}</span>
|
|
172
|
+
</span>
|
|
173
|
+
))}
|
|
174
|
+
{failed &&
|
|
175
|
+
(errorFallback ?? (
|
|
176
|
+
<span
|
|
177
|
+
// `role="img"` with the alt as its name: the picture that failed still
|
|
178
|
+
// occupies its place in the reading order, and "couldn't be loaded" is
|
|
179
|
+
// the one fact a reader needs about it.
|
|
180
|
+
role="img"
|
|
181
|
+
aria-label={alt ? labels.failedImage(alt) : labels.loadError}
|
|
182
|
+
className="flex h-full min-h-16 w-full flex-col items-center justify-center gap-1 bg-[var(--bg-hover)] p-2 text-center text-xs text-[var(--text-muted)]"
|
|
183
|
+
>
|
|
184
|
+
<ImageOff aria-hidden className="size-5 shrink-0" />
|
|
185
|
+
<span aria-hidden className="line-clamp-2">{labels.loadError}</span>
|
|
186
|
+
</span>
|
|
187
|
+
))}
|
|
188
|
+
</span>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -2,7 +2,8 @@ import { Fragment, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
3
3
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
6
|
+
import { pickLinkRenderer, RenderedKitLink } from "./text-link";
|
|
6
7
|
|
|
7
8
|
export interface BreadcrumbsLabels {
|
|
8
9
|
/** The `nav` landmark's name — what a screen reader lists it as. */
|
|
@@ -35,7 +36,8 @@ export interface BreadcrumbLinkProps {
|
|
|
35
36
|
|
|
36
37
|
export interface BreadcrumbsProps extends Omit<ComponentPropsWithoutRef<"nav">, "children"> {
|
|
37
38
|
items: BreadcrumbItem[];
|
|
38
|
-
/** Your router's link. Default `<
|
|
39
|
+
/** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app
|
|
40
|
+
* `href`), then `<a>`. The API `Chip`, `StatTile` and `ListItem` share. */
|
|
39
41
|
renderLink?: (props: BreadcrumbLinkProps) => ReactElement;
|
|
40
42
|
/**
|
|
41
43
|
* The mark between crumbs. Default a chevron, mirrored in RTL — a chevron pointing
|
|
@@ -78,6 +80,7 @@ export function Breadcrumbs({
|
|
|
78
80
|
...rest
|
|
79
81
|
}: BreadcrumbsProps) {
|
|
80
82
|
const labels = useKitLabels("breadcrumbs", DEFAULT_BREADCRUMBS_LABELS, labelsProp);
|
|
83
|
+
const kitLink = useKitLink();
|
|
81
84
|
const [expanded, setExpanded] = useState(false);
|
|
82
85
|
// The "…" button vanishes when pressed; focus goes to the first crumb it revealed,
|
|
83
86
|
// not back to the top of the document.
|
|
@@ -121,8 +124,11 @@ export function Breadcrumbs({
|
|
|
121
124
|
);
|
|
122
125
|
} else if (item.href !== undefined) {
|
|
123
126
|
const props: BreadcrumbLinkProps = { href: item.href, className: linkClass, children: item.label };
|
|
127
|
+
const kit = pickLinkRenderer<BreadcrumbLinkProps>(undefined, kitLink, item.href);
|
|
124
128
|
content = renderLink ? (
|
|
125
129
|
renderLink(props)
|
|
130
|
+
) : kit ? (
|
|
131
|
+
<RenderedKitLink render={kit} props={props} />
|
|
126
132
|
) : (
|
|
127
133
|
<a href={props.href} className={props.className}>
|
|
128
134
|
{props.children}
|
|
@@ -99,8 +99,16 @@ export interface BulkActionBarProps extends Omit<ComponentPropsWithoutRef<"div">
|
|
|
99
99
|
open?: boolean;
|
|
100
100
|
/** Clear the selection / leave selection mode. */
|
|
101
101
|
onClear: () => void;
|
|
102
|
-
/**
|
|
103
|
-
*
|
|
102
|
+
/**
|
|
103
|
+
* The actions: buttons (`IconButton size="lg"` on a phone, `Button` from md up),
|
|
104
|
+
* in the caller's order. They are the toolbar's arrow-key stops, after the clear.
|
|
105
|
+
*
|
|
106
|
+
* LINKS rove too (keksdose F8): a `<Button href>` — "View report", which navigates
|
|
107
|
+
* rather than acts — is an `<a href>`, and it is one more arrow-key stop in its place
|
|
108
|
+
* among the buttons, not a second Tab stop beside the toolbar's one. A disabled
|
|
109
|
+
* `<Button href disabled>` has no `href` and is skipped, as a disabled button is. Any
|
|
110
|
+
* other focusable of your own joins with `data-toolbar-item`.
|
|
111
|
+
*/
|
|
104
112
|
children?: ReactNode;
|
|
105
113
|
/** Default `floating`. See {@link BulkActionBarVariant}; an object picks one per
|
|
106
114
|
* breakpoint, see {@link ResponsiveBulkActionBarVariant}. */
|
|
@@ -135,7 +143,12 @@ const FLOATING_STYLE: CSSProperties = {
|
|
|
135
143
|
|
|
136
144
|
/** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by
|
|
137
145
|
* `tabindex`, because the roving below sets every one but the current to `-1` — a
|
|
138
|
-
* custom control joins with `data-toolbar-item`.
|
|
146
|
+
* custom control joins with `data-toolbar-item`.
|
|
147
|
+
*
|
|
148
|
+
* `a[href]` is here on purpose (keksdose F8, a "View report" `Button href` in the bar):
|
|
149
|
+
* a link in a toolbar is a toolbar item like any other, and left out it would keep its
|
|
150
|
+
* own Tab stop, so Tab would stop twice in a bar that promises one. `[href]` rather than `a`: the kit's disabled link is an `<a>`
|
|
151
|
+
* WITHOUT one, and is skipped as a disabled button is. */
|
|
139
152
|
const ITEMS =
|
|
140
153
|
"button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]";
|
|
141
154
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
3
|
import { buttonClasses, type ButtonSize, type ButtonVariant } from "./ui";
|
|
4
|
+
import { useKitLink } from "../i18n/kit-labels";
|
|
5
|
+
import { pickLinkRenderer } from "./text-link";
|
|
4
6
|
|
|
5
7
|
// Every class below is spelled out in full rather than assembled from parts: the
|
|
6
8
|
// consumer's Tailwind build finds classes by scanning this package's source as text,
|
|
@@ -147,7 +149,8 @@ export interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<"a">
|
|
|
147
149
|
/** The member for the page you are on: `aria-current="page"` and the "on" look of a
|
|
148
150
|
* pressed member, so a link row reads as a segmented switch between pages. */
|
|
149
151
|
current?: boolean;
|
|
150
|
-
/** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.
|
|
152
|
+
/** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.
|
|
153
|
+
* Default: the `<UiKitProvider linkComponent>`, then `<a>`. */
|
|
151
154
|
renderLink?: (props: ButtonGroupLinkRenderProps) => ReactElement;
|
|
152
155
|
className?: string;
|
|
153
156
|
children: ReactNode;
|
|
@@ -174,6 +177,7 @@ export function ButtonGroupLink({
|
|
|
174
177
|
children,
|
|
175
178
|
...rest
|
|
176
179
|
}: ButtonGroupLinkProps) {
|
|
180
|
+
const kitLink = useKitLink();
|
|
177
181
|
const props: ButtonGroupLinkRenderProps = {
|
|
178
182
|
...rest,
|
|
179
183
|
href,
|
|
@@ -181,7 +185,8 @@ export function ButtonGroupLink({
|
|
|
181
185
|
className: buttonClasses(variant, { size, className: cn(current && LINK_CURRENT, className) }),
|
|
182
186
|
children,
|
|
183
187
|
};
|
|
184
|
-
|
|
188
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
189
|
+
if (render) return <RenderedGroupLink render={render} {...props} />;
|
|
185
190
|
const { children: content, ...anchor } = props;
|
|
186
191
|
return <a {...anchor}>{content}</a>;
|
|
187
192
|
}
|
|
@@ -4,6 +4,8 @@ import { cn } from "../lib/cn";
|
|
|
4
4
|
import { dirOf, horizontalStep } from "../lib/direction";
|
|
5
5
|
import { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from "../lib/dates";
|
|
6
6
|
import { useKitLabels, useKitLocale, useKitWeekStart } from "../i18n/kit-labels";
|
|
7
|
+
import { lerpHex, textOn } from "../theme/chart-palette";
|
|
8
|
+
import { parseHex } from "../theme/color";
|
|
7
9
|
import { Tooltip } from "./tooltip";
|
|
8
10
|
import type { WeekDay } from "./mini-calendar";
|
|
9
11
|
|
|
@@ -103,6 +105,39 @@ export interface CalendarHeatmapProps extends Omit<ComponentPropsWithoutRef<"div
|
|
|
103
105
|
/** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this
|
|
104
106
|
* mixed into `--bg-surface`, so the ramp follows the theme. */
|
|
105
107
|
color?: string;
|
|
108
|
+
/**
|
|
109
|
+
* A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest
|
|
110
|
+
* day), instead of `levels` steps of `color` — keksdose's palette-store ramp,
|
|
111
|
+
* `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`
|
|
112
|
+
* colours are interpolated in JS, so the fill is a concrete hex and the day number's
|
|
113
|
+
* ink (in `"month"`) is MEASURED against it; anything else (a `var()`) is mixed with
|
|
114
|
+
* `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.
|
|
115
|
+
*/
|
|
116
|
+
colorFrom?: string;
|
|
117
|
+
colorTo?: string;
|
|
118
|
+
/**
|
|
119
|
+
* A fill of the caller's own for a day with a value: handed the value and the
|
|
120
|
+
* scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.
|
|
121
|
+
* Also paints the legend, at `max × level / levels`.
|
|
122
|
+
*/
|
|
123
|
+
fill?: (value: number, max: number) => string;
|
|
124
|
+
/** The fill of a day with no value (or a value of 0). Default: a wash of the text
|
|
125
|
+
* colour over the surface, which reads as a cell on both themes. */
|
|
126
|
+
emptyColor?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:
|
|
129
|
+
*
|
|
130
|
+
* - `"to"` (default): on `to`.
|
|
131
|
+
* - `"today"`: on today.
|
|
132
|
+
* - `"latest"`: on the last day that can carry a figure — today, or a later day that
|
|
133
|
+
* actually has a value. A window running into the future (a report's "all" preset
|
|
134
|
+
* ends a year ahead) then spends its cells on the days a person is looking for,
|
|
135
|
+
* not on a year of empty ones.
|
|
136
|
+
* - a `"YYYY-MM-DD"`: on that day.
|
|
137
|
+
*
|
|
138
|
+
* Always kept inside `from`…`to`.
|
|
139
|
+
*/
|
|
140
|
+
anchor?: "to" | "today" | "latest" | (string & {});
|
|
106
141
|
/**
|
|
107
142
|
* The most days to draw. A longer window keeps its LATEST `maxDays` and says how many
|
|
108
143
|
* it left out (`labels.truncated`) rather than silently ending. Default: no cap.
|
|
@@ -131,6 +166,26 @@ const EMPTY_FILL = "color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface
|
|
|
131
166
|
|
|
132
167
|
const weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;
|
|
133
168
|
|
|
169
|
+
/** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.
|
|
170
|
+
* `latestData` is the latest ISO date carrying a value, or `""`. */
|
|
171
|
+
export function heatmapWindowEnd(
|
|
172
|
+
from: string,
|
|
173
|
+
to: string,
|
|
174
|
+
anchor: string,
|
|
175
|
+
latestData: string,
|
|
176
|
+
today: string = toLocalIso(new Date()),
|
|
177
|
+
): string {
|
|
178
|
+
let end: string;
|
|
179
|
+
if (anchor === "today") end = today;
|
|
180
|
+
else if (anchor === "latest") end = latestData > today ? latestData : today;
|
|
181
|
+
else if (anchor === "to" || !parseIsoDate(anchor)) end = to;
|
|
182
|
+
else end = anchor;
|
|
183
|
+
// ISO dates sort as strings, so the clamp is plain comparison.
|
|
184
|
+
if (end > to) end = to;
|
|
185
|
+
if (end < from) end = from;
|
|
186
|
+
return end;
|
|
187
|
+
}
|
|
188
|
+
|
|
134
189
|
/** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */
|
|
135
190
|
export function heatmapLevel(value: number, max: number, levels: number): number {
|
|
136
191
|
if (!(value > 0) || !(max > 0) || levels < 1) return 0;
|
|
@@ -159,7 +214,9 @@ function levelFill(level: number, levels: number, color: string): string {
|
|
|
159
214
|
* the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go
|
|
160
215
|
* to the window's first/last day. Each cell is named with its whole date AND its value,
|
|
161
216
|
* because the colour is not something a screen reader can say, and carries
|
|
162
|
-
* `data-day="YYYY-MM-DD"` as its identity for tests and for the host.
|
|
217
|
+
* `data-day="YYYY-MM-DD"` as its identity for tests and for the host. Note that in
|
|
218
|
+
* `"weeks"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all
|
|
219
|
+
* Tuesdays…), not by date — query by `data-day`, not by position.
|
|
163
220
|
*
|
|
164
221
|
* The tooltip is portalled (a year of squares always sits in a sideways scroller) and
|
|
165
222
|
* `data-private` unless `sensitive={false}`. Cell size in `"weeks"` is the CSS variable
|
|
@@ -178,6 +235,11 @@ export function CalendarHeatmap({
|
|
|
178
235
|
max: maxProp,
|
|
179
236
|
levels = 4,
|
|
180
237
|
color = "var(--brand)",
|
|
238
|
+
colorFrom,
|
|
239
|
+
colorTo,
|
|
240
|
+
fill: fillProp,
|
|
241
|
+
emptyColor,
|
|
242
|
+
anchor = "to",
|
|
181
243
|
maxDays,
|
|
182
244
|
legend = true,
|
|
183
245
|
locale: localeProp,
|
|
@@ -202,9 +264,16 @@ export function CalendarHeatmap({
|
|
|
202
264
|
|
|
203
265
|
// The window, trimmed from the FRONT: the days a person looks for are the recent
|
|
204
266
|
// ones, and a cap that kept the oldest would end the calendar before today.
|
|
267
|
+
const latestData = useMemo(() => {
|
|
268
|
+
let latest = "";
|
|
269
|
+
for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;
|
|
270
|
+
return latest;
|
|
271
|
+
}, [values]);
|
|
272
|
+
const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);
|
|
273
|
+
|
|
205
274
|
const { days, dropped } = useMemo(() => {
|
|
206
275
|
const start = parseIsoDate(from);
|
|
207
|
-
const end = parseIsoDate(
|
|
276
|
+
const end = parseIsoDate(windowEnd);
|
|
208
277
|
const list: Date[] = [];
|
|
209
278
|
if (!start || !end || end < start) return { days: list, dropped: 0 };
|
|
210
279
|
const cursor = new Date(end);
|
|
@@ -216,7 +285,7 @@ export function CalendarHeatmap({
|
|
|
216
285
|
const first = list[0];
|
|
217
286
|
const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;
|
|
218
287
|
return { days: list, dropped: Math.max(0, trimmed) };
|
|
219
|
-
}, [from,
|
|
288
|
+
}, [from, windowEnd, maxDays]);
|
|
220
289
|
|
|
221
290
|
const firstIso = days.length ? toLocalIso(days[0]) : "";
|
|
222
291
|
const lastIso = days.length ? toLocalIso(days[days.length - 1]) : "";
|
|
@@ -341,13 +410,43 @@ export function CalendarHeatmap({
|
|
|
341
410
|
};
|
|
342
411
|
|
|
343
412
|
const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);
|
|
413
|
+
const empty = emptyColor ?? EMPTY_FILL;
|
|
414
|
+
const continuous = colorFrom !== undefined && colorTo !== undefined;
|
|
415
|
+
const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;
|
|
416
|
+
|
|
417
|
+
/** A day's fill, and the ink its number is set in (`"month"`). */
|
|
418
|
+
const paint = (value: number, level: number): { fill: string; ink: string } => {
|
|
419
|
+
if (level === 0) return { fill: empty, ink: "var(--text-primary)" };
|
|
420
|
+
if (fillProp || continuous) {
|
|
421
|
+
const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;
|
|
422
|
+
const fill = fillProp
|
|
423
|
+
? fillProp(value, max)
|
|
424
|
+
: hexRamp
|
|
425
|
+
? lerpHex(colorFrom!, colorTo!, t)
|
|
426
|
+
: `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;
|
|
427
|
+
// Measured where the fill is a colour; a mix of references cannot be measured,
|
|
428
|
+
// and the page's own text colour is the safe answer there.
|
|
429
|
+
const measured = textOn(fill);
|
|
430
|
+
return { fill, ink: measured === "currentColor" ? "var(--text-primary)" : measured };
|
|
431
|
+
}
|
|
432
|
+
// Light text once the fill is the stronger half of the mix — `--bg-surface` is the
|
|
433
|
+
// colour the scale is mixed INTO, so it is the one that contrasts with its far end
|
|
434
|
+
// on either theme.
|
|
435
|
+
const strong = levelPercent(level, levels) >= 45;
|
|
436
|
+
return {
|
|
437
|
+
fill: levelFill(level, levels, color),
|
|
438
|
+
ink: strong ? "var(--bg-surface)" : "var(--text-primary)",
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
// The legend's swatches: a value that lands on each step (the top one on `max`).
|
|
442
|
+
const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;
|
|
344
443
|
|
|
345
444
|
const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {
|
|
346
445
|
const iso = toLocalIso(d);
|
|
347
446
|
const raw = values.get(iso);
|
|
348
447
|
const value = raw ?? 0;
|
|
349
448
|
const level = heatmapLevel(value, max, levels);
|
|
350
|
-
const fill =
|
|
449
|
+
const { fill, ink } = paint(value, level);
|
|
351
450
|
const formattedDate = formatDay(d);
|
|
352
451
|
const formattedValue = formatValue(value);
|
|
353
452
|
const name = labels.day(formattedDate, formattedValue);
|
|
@@ -355,10 +454,6 @@ export function CalendarHeatmap({
|
|
|
355
454
|
? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })
|
|
356
455
|
: name;
|
|
357
456
|
const isSelected = selected === iso;
|
|
358
|
-
// Light text once the fill is the stronger half of the mix — `--bg-surface` is the
|
|
359
|
-
// colour the scale is mixed INTO, so it is the one that contrasts with its far end
|
|
360
|
-
// on either theme.
|
|
361
|
-
const strong = level > 0 && levelPercent(level, levels) >= 45;
|
|
362
457
|
const shared = {
|
|
363
458
|
role: "gridcell",
|
|
364
459
|
"data-day": iso,
|
|
@@ -370,7 +465,7 @@ export function CalendarHeatmap({
|
|
|
370
465
|
onFocus: () => {
|
|
371
466
|
if (iso !== activeIso) setActiveIso(iso);
|
|
372
467
|
},
|
|
373
|
-
style: { background: fill, color:
|
|
468
|
+
style: { background: fill, color: ink },
|
|
374
469
|
className: cn(
|
|
375
470
|
cellClass,
|
|
376
471
|
isSelected && "ring-2 ring-[var(--text-primary)]",
|
|
@@ -493,7 +588,7 @@ export function CalendarHeatmap({
|
|
|
493
588
|
key={l}
|
|
494
589
|
data-legend-level={l}
|
|
495
590
|
className="size-2.5 rounded-sm"
|
|
496
|
-
style={{ background:
|
|
591
|
+
style={{ background: legendFill(l) }}
|
|
497
592
|
/>
|
|
498
593
|
))}
|
|
499
594
|
<span className="ms-0.5">{labels.more}</span>
|
package/src/components/chart.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The value on show is the *structure*: config-driven colors injected as CSS
|
|
9
9
|
// vars, muted axis/grid styling, and a polished tooltip/legend — the things
|
|
10
10
|
// that make shadcn charts read as "designed" rather than "default Recharts".
|
|
11
|
-
import { createContext, useContext, useId, useRef } from "react";
|
|
11
|
+
import { createContext, useContext, useId, useLayoutEffect, useRef, useState } from "react";
|
|
12
12
|
import type { ComponentProps, CSSProperties, ReactNode, RefObject } from "react";
|
|
13
13
|
import {
|
|
14
14
|
Legend as RechartsLegend,
|
|
@@ -263,7 +263,7 @@ export function ChartTooltipContent({
|
|
|
263
263
|
// `ChartTooltip` keeps null values (so they can print "—"), which also keeps the
|
|
264
264
|
// series a legend toggle switched off with `hide`; those go here instead.
|
|
265
265
|
const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);
|
|
266
|
-
|
|
266
|
+
const visible = Boolean(active && items?.length);
|
|
267
267
|
// Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart
|
|
268
268
|
// width; subtract the container's scrollLeft to get its on-screen x, then flip
|
|
269
269
|
// left if the tooltip's own width would run past the visible right edge.
|
|
@@ -272,16 +272,27 @@ export function ChartTooltipContent({
|
|
|
272
272
|
// `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —
|
|
273
273
|
// 0 at the start, negative going left — so there the content's left edge sits
|
|
274
274
|
// `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
//
|
|
276
|
+
// Measured in a layout effect (the DOM is not readable during render), so it commits
|
|
277
|
+
// before paint. `payload` and `label` are deps because they change the tooltip's own
|
|
278
|
+
// width, and recharts hands a fresh `payload` on every tooltip render, so a scroll
|
|
279
|
+
// followed by any re-render is re-measured just as it was when this ran in render.
|
|
280
|
+
const [flip, setFlip] = useState(false);
|
|
281
|
+
const x = coordinate?.x;
|
|
282
|
+
useLayoutEffect(() => {
|
|
283
|
+
const boundary = boundaryRef?.current;
|
|
284
|
+
if (!visible || !boundary || x == null) {
|
|
285
|
+
setFlip(false);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
278
288
|
const offset = isRtl(boundary)
|
|
279
289
|
? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft
|
|
280
290
|
: boundary.scrollLeft;
|
|
281
|
-
const visibleX =
|
|
291
|
+
const visibleX = x - offset;
|
|
282
292
|
const tipWidth = tipRef.current?.offsetWidth ?? 160;
|
|
283
|
-
|
|
284
|
-
}
|
|
293
|
+
setFlip(visibleX + tipWidth + 12 > boundary.clientWidth);
|
|
294
|
+
}, [visible, x, boundaryRef, payload, label]);
|
|
295
|
+
if (!visible || !items) return null;
|
|
285
296
|
return (
|
|
286
297
|
<div
|
|
287
298
|
ref={tipRef}
|
package/src/components/chip.tsx
CHANGED
|
@@ -5,7 +5,8 @@ import type { LucideIcon } from "lucide-react";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { horizontalStep } from "../lib/direction";
|
|
7
7
|
import { FIELD_INVALID } from "./ui";
|
|
8
|
-
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
8
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels";
|
|
9
|
+
import { pickLinkRenderer } from "./text-link";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A chip: a compact pill carrying one value.
|
|
@@ -384,7 +385,8 @@ export type ChipProps = ChipBaseProps &
|
|
|
384
385
|
/**
|
|
385
386
|
* Renders the link for `href` — pass your router's `<Link>` here, since a
|
|
386
387
|
* plain `<a>` reloads a single-page app (keksdose's account-menu admin pill).
|
|
387
|
-
* Default: `<a>`. Ignored without
|
|
388
|
+
* Default: the `<UiKitProvider linkComponent>`, then `<a>`. Ignored without
|
|
389
|
+
* `href`.
|
|
388
390
|
*
|
|
389
391
|
* A render function rather than an `as={Link}`, for three reasons. It is the
|
|
390
392
|
* API `StatTile` already has (`renderLink`), so a consumer learns it once.
|
|
@@ -455,6 +457,7 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
|
|
|
455
457
|
ref,
|
|
456
458
|
) {
|
|
457
459
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
|
|
460
|
+
const kitLink = useKitLink();
|
|
458
461
|
const s = SIZE[size];
|
|
459
462
|
const interactive = !!href || !!onClick;
|
|
460
463
|
// One radius for every rounded piece — the pill, the body inside a split pill, the ×
|
|
@@ -570,10 +573,11 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
|
|
|
570
573
|
className: remove ? inner : look,
|
|
571
574
|
children: body,
|
|
572
575
|
};
|
|
573
|
-
const
|
|
576
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
577
|
+
const link = render ? (
|
|
574
578
|
// Through a component rather than called here, so the forwarded ref arrives as
|
|
575
579
|
// an ordinary prop of an element and is never handed to a function mid-render.
|
|
576
|
-
<RenderedLink render={
|
|
580
|
+
<RenderedLink render={render} {...linkProps} />
|
|
577
581
|
) : (
|
|
578
582
|
<a
|
|
579
583
|
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
@@ -617,6 +621,9 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
|
|
|
617
621
|
}
|
|
618
622
|
|
|
619
623
|
return (
|
|
624
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
|
|
625
|
+
the roving-focus hooks, live only when the container passes `role` and `tabIndex`
|
|
626
|
+
(ChipInput does); the role is a prop, so the rule cannot see it. */
|
|
620
627
|
<span
|
|
621
628
|
ref={ref as React.Ref<HTMLSpanElement>}
|
|
622
629
|
className={look}
|
|
@@ -889,6 +896,9 @@ export function ChipInput({
|
|
|
889
896
|
{label}
|
|
890
897
|
</label>
|
|
891
898
|
)}
|
|
899
|
+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- a
|
|
900
|
+
pointer convenience that widens the input's hit area to the whole field; the
|
|
901
|
+
keyboard path is the <input> itself, which is in the tab order. */}
|
|
892
902
|
<div
|
|
893
903
|
// `relative` so the live region below can be `sr-only` without escaping to the
|
|
894
904
|
// initial containing block and inflating the document height.
|