@hoardodile/ui 0.0.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/LICENSE +18 -0
- package/README.md +70 -0
- package/dist/components/alert.d.ts +13 -0
- package/dist/components/alert.js +97 -0
- package/dist/components/alert.js.map +1 -0
- package/dist/components/app-dialog.d.ts +62 -0
- package/dist/components/app-dialog.js +656 -0
- package/dist/components/app-dialog.js.map +1 -0
- package/dist/components/badge.d.ts +11 -0
- package/dist/components/badge.js +70 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/breadcrumb.d.ts +12 -0
- package/dist/components/breadcrumb.js +617 -0
- package/dist/components/breadcrumb.js.map +1 -0
- package/dist/components/button.d.ts +17 -0
- package/dist/components/button.js +78 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/caption-bar.d.ts +44 -0
- package/dist/components/caption-bar.js +827 -0
- package/dist/components/caption-bar.js.map +1 -0
- package/dist/components/chart-card.d.ts +15 -0
- package/dist/components/chart-card.js +27 -0
- package/dist/components/chart-card.js.map +1 -0
- package/dist/components/checkbox.d.ts +6 -0
- package/dist/components/checkbox.js +76 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/chip-button.d.ts +28 -0
- package/dist/components/chip-button.js +87 -0
- package/dist/components/chip-button.js.map +1 -0
- package/dist/components/close-confirm-dialog.d.ts +19 -0
- package/dist/components/close-confirm-dialog.js +743 -0
- package/dist/components/close-confirm-dialog.js.map +1 -0
- package/dist/components/color-picker.d.ts +32 -0
- package/dist/components/color-picker.js +667 -0
- package/dist/components/color-picker.js.map +1 -0
- package/dist/components/combobox.d.ts +32 -0
- package/dist/components/combobox.js +1211 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/confirm-by-typing-dialog.d.ts +48 -0
- package/dist/components/confirm-by-typing-dialog.js +759 -0
- package/dist/components/confirm-by-typing-dialog.js.map +1 -0
- package/dist/components/confirm-dialog.d.ts +34 -0
- package/dist/components/confirm-dialog.js +710 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/count-badge.d.ts +13 -0
- package/dist/components/count-badge.js +39 -0
- package/dist/components/count-badge.js.map +1 -0
- package/dist/components/dialog.d.ts +40 -0
- package/dist/components/dialog.js +571 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-menu.d.ts +45 -0
- package/dist/components/dropdown-menu.js +1003 -0
- package/dist/components/dropdown-menu.js.map +1 -0
- package/dist/components/dropdown-select.d.ts +29 -0
- package/dist/components/dropdown-select.js +1036 -0
- package/dist/components/dropdown-select.js.map +1 -0
- package/dist/components/empty.d.ts +15 -0
- package/dist/components/empty.js +119 -0
- package/dist/components/empty.js.map +1 -0
- package/dist/components/filter-rail.d.ts +48 -0
- package/dist/components/filter-rail.js +231 -0
- package/dist/components/filter-rail.js.map +1 -0
- package/dist/components/fixed-action-bar.d.ts +24 -0
- package/dist/components/fixed-action-bar.js +48 -0
- package/dist/components/fixed-action-bar.js.map +1 -0
- package/dist/components/font-picker.d.ts +45 -0
- package/dist/components/font-picker.js +759 -0
- package/dist/components/font-picker.js.map +1 -0
- package/dist/components/form.d.ts +17 -0
- package/dist/components/form.js +150 -0
- package/dist/components/form.js.map +1 -0
- package/dist/components/group-label.d.ts +12 -0
- package/dist/components/group-label.js +10 -0
- package/dist/components/group-label.js.map +1 -0
- package/dist/components/icon-tile.d.ts +16 -0
- package/dist/components/icon-tile.js +67 -0
- package/dist/components/icon-tile.js.map +1 -0
- package/dist/components/icon-toggle.d.ts +24 -0
- package/dist/components/icon-toggle.js +81 -0
- package/dist/components/icon-toggle.js.map +1 -0
- package/dist/components/icon.d.ts +2 -0
- package/dist/components/icon.js +49 -0
- package/dist/components/icon.js.map +1 -0
- package/dist/components/image-crop-panel.d.ts +80 -0
- package/dist/components/image-crop-panel.js +1104 -0
- package/dist/components/image-crop-panel.js.map +1 -0
- package/dist/components/image-cropper.d.ts +63 -0
- package/dist/components/image-cropper.js +166 -0
- package/dist/components/image-cropper.js.map +1 -0
- package/dist/components/input-group.d.ts +22 -0
- package/dist/components/input-group.js +270 -0
- package/dist/components/input-group.js.map +1 -0
- package/dist/components/input.d.ts +16 -0
- package/dist/components/input.js +60 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/label.d.ts +5 -0
- package/dist/components/label.js +40 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/list-empty-row.d.ts +16 -0
- package/dist/components/list-empty-row.js +40 -0
- package/dist/components/list-empty-row.js.map +1 -0
- package/dist/components/management-empty.d.ts +14 -0
- package/dist/components/management-empty.js +40 -0
- package/dist/components/management-empty.js.map +1 -0
- package/dist/components/management-skeleton.d.ts +11 -0
- package/dist/components/management-skeleton.js +72 -0
- package/dist/components/management-skeleton.js.map +1 -0
- package/dist/components/meta-chip.d.ts +17 -0
- package/dist/components/meta-chip.js +49 -0
- package/dist/components/meta-chip.js.map +1 -0
- package/dist/components/mobile-drawer.d.ts +29 -0
- package/dist/components/mobile-drawer.js +331 -0
- package/dist/components/mobile-drawer.js.map +1 -0
- package/dist/components/page-header.d.ts +20 -0
- package/dist/components/page-header.js +44 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/page-scaffold.d.ts +34 -0
- package/dist/components/page-scaffold.js +157 -0
- package/dist/components/page-scaffold.js.map +1 -0
- package/dist/components/pagination-bar.d.ts +22 -0
- package/dist/components/pagination-bar.js +703 -0
- package/dist/components/pagination-bar.js.map +1 -0
- package/dist/components/pagination.d.ts +22 -0
- package/dist/components/pagination.js +680 -0
- package/dist/components/pagination.js.map +1 -0
- package/dist/components/panel-toolbar.d.ts +37 -0
- package/dist/components/panel-toolbar.js +781 -0
- package/dist/components/panel-toolbar.js.map +1 -0
- package/dist/components/pill-tabs.d.ts +33 -0
- package/dist/components/pill-tabs.js +66 -0
- package/dist/components/pill-tabs.js.map +1 -0
- package/dist/components/plugin-download-consent.d.ts +52 -0
- package/dist/components/plugin-download-consent.js +780 -0
- package/dist/components/plugin-download-consent.js.map +1 -0
- package/dist/components/popover.d.ts +26 -0
- package/dist/components/popover.js +343 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/progress.d.ts +10 -0
- package/dist/components/progress.js +103 -0
- package/dist/components/progress.js.map +1 -0
- package/dist/components/query-state-view.d.ts +36 -0
- package/dist/components/query-state-view.js +17 -0
- package/dist/components/query-state-view.js.map +1 -0
- package/dist/components/radio-group.d.ts +8 -0
- package/dist/components/radio-group.js +60 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/scroll-area.d.ts +7 -0
- package/dist/components/scroll-area.js +80 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/search-field.d.ts +49 -0
- package/dist/components/search-field.js +586 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/secondary-stat.d.ts +13 -0
- package/dist/components/secondary-stat.js +56 -0
- package/dist/components/secondary-stat.js.map +1 -0
- package/dist/components/section-header.d.ts +23 -0
- package/dist/components/section-header.js +76 -0
- package/dist/components/section-header.js.map +1 -0
- package/dist/components/section-label.d.ts +20 -0
- package/dist/components/section-label.js +46 -0
- package/dist/components/section-label.js.map +1 -0
- package/dist/components/section-tabs.d.ts +47 -0
- package/dist/components/section-tabs.js +145 -0
- package/dist/components/section-tabs.js.map +1 -0
- package/dist/components/separator.d.ts +14 -0
- package/dist/components/separator.js +49 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/sheet.d.ts +22 -0
- package/dist/components/sheet.js +469 -0
- package/dist/components/sheet.js.map +1 -0
- package/dist/components/sidebar.d.ts +69 -0
- package/dist/components/sidebar.js +1645 -0
- package/dist/components/sidebar.js.map +1 -0
- package/dist/components/skeleton.d.ts +8 -0
- package/dist/components/skeleton.js +41 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/slider.d.ts +6 -0
- package/dist/components/slider.js +75 -0
- package/dist/components/slider.js.map +1 -0
- package/dist/components/sortable-chip-list.d.ts +26 -0
- package/dist/components/sortable-chip-list.js +59 -0
- package/dist/components/sortable-chip-list.js.map +1 -0
- package/dist/components/spinner.d.ts +5 -0
- package/dist/components/spinner.js +65 -0
- package/dist/components/spinner.js.map +1 -0
- package/dist/components/surface.d.ts +13 -0
- package/dist/components/surface.js +51 -0
- package/dist/components/surface.js.map +1 -0
- package/dist/components/switch.d.ts +8 -0
- package/dist/components/switch.js +53 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +129 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/textarea.d.ts +15 -0
- package/dist/components/textarea.js +58 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/components/toast.d.ts +19 -0
- package/dist/components/toast.js +805 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle.d.ts +12 -0
- package/dist/components/toggle.js +63 -0
- package/dist/components/toggle.js.map +1 -0
- package/dist/components/tooltip.d.ts +9 -0
- package/dist/components/tooltip.js +82 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/hooks/use-dialog-footer-actions.d.ts +18 -0
- package/dist/hooks/use-dialog-footer-actions.js +11 -0
- package/dist/hooks/use-dialog-footer-actions.js.map +1 -0
- package/dist/hooks/use-form-field.d.ts +33 -0
- package/dist/hooks/use-form-field.js +36 -0
- package/dist/hooks/use-form-field.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +34 -0
- package/dist/hooks/use-mobile.js +29 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-pinch-pan.d.ts +56 -0
- package/dist/hooks/use-pinch-pan.js +223 -0
- package/dist/hooks/use-pinch-pan.js.map +1 -0
- package/dist/hooks/useMobileBackToClose.d.ts +61 -0
- package/dist/hooks/useMobileBackToClose.js +234 -0
- package/dist/hooks/useMobileBackToClose.js.map +1 -0
- package/dist/icon-BOgL5bpL.d.ts +85 -0
- package/dist/icons/actions.d.ts +4 -0
- package/dist/icons/actions.js +520 -0
- package/dist/icons/actions.js.map +1 -0
- package/dist/icons/icon-style.d.ts +2 -0
- package/dist/icons/icon-style.js +51 -0
- package/dist/icons/icon-style.js.map +1 -0
- package/dist/icons/marks.d.ts +15 -0
- package/dist/icons/marks.js +48 -0
- package/dist/icons/marks.js.map +1 -0
- package/dist/icons/registry.d.ts +177 -0
- package/dist/icons/registry.js +1180 -0
- package/dist/icons/registry.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +653 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/merge-config.d.ts +15 -0
- package/dist/lib/merge-config.js +20 -0
- package/dist/lib/merge-config.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.js +26 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +26 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles/theme.css +480 -0
- package/dist/styles/theme.css.map +1 -0
- package/dist/styles/theme.d.ts +2 -0
- package/dist/viewport.d.ts +64 -0
- package/dist/viewport.js +13 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +122 -0
- package/src/components/alert.tsx +76 -0
- package/src/components/app-dialog.tsx +187 -0
- package/src/components/badge.tsx +52 -0
- package/src/components/breadcrumb.tsx +129 -0
- package/src/components/button.tsx +70 -0
- package/src/components/caption-bar.test.tsx +151 -0
- package/src/components/caption-bar.tsx +392 -0
- package/src/components/chart-card.tsx +31 -0
- package/src/components/checkbox.tsx +28 -0
- package/src/components/chip-button.tsx +69 -0
- package/src/components/close-confirm-dialog.tsx +99 -0
- package/src/components/color-picker.tsx +186 -0
- package/src/components/combobox.tsx +335 -0
- package/src/components/confirm-by-typing-dialog.tsx +130 -0
- package/src/components/confirm-dialog.test.tsx +39 -0
- package/src/components/confirm-dialog.tsx +98 -0
- package/src/components/count-badge.tsx +23 -0
- package/src/components/dialog.tsx +315 -0
- package/src/components/dropdown-menu.tsx +311 -0
- package/src/components/dropdown-select.tsx +211 -0
- package/src/components/empty.tsx +104 -0
- package/src/components/filter-rail.tsx +139 -0
- package/src/components/fixed-action-bar.tsx +46 -0
- package/src/components/font-picker.tsx +257 -0
- package/src/components/form.tsx +127 -0
- package/src/components/group-label.tsx +13 -0
- package/src/components/icon-tile.tsx +35 -0
- package/src/components/icon-toggle.tsx +57 -0
- package/src/components/icon.tsx +66 -0
- package/src/components/image-crop-panel.test.tsx +167 -0
- package/src/components/image-crop-panel.tsx +465 -0
- package/src/components/image-cropper.test.tsx +144 -0
- package/src/components/image-cropper.tsx +265 -0
- package/src/components/input-group.tsx +156 -0
- package/src/components/input.tsx +45 -0
- package/src/components/label.tsx +20 -0
- package/src/components/list-empty-row.tsx +27 -0
- package/src/components/management-empty.tsx +25 -0
- package/src/components/management-skeleton.tsx +23 -0
- package/src/components/meta-chip.tsx +40 -0
- package/src/components/mobile-drawer.test.tsx +52 -0
- package/src/components/mobile-drawer.tsx +98 -0
- package/src/components/page-header.tsx +47 -0
- package/src/components/page-scaffold.test.tsx +24 -0
- package/src/components/page-scaffold.tsx +131 -0
- package/src/components/pagination-bar.test.tsx +87 -0
- package/src/components/pagination-bar.tsx +139 -0
- package/src/components/pagination.tsx +139 -0
- package/src/components/panel-toolbar.tsx +99 -0
- package/src/components/pill-tabs.test.tsx +84 -0
- package/src/components/pill-tabs.tsx +77 -0
- package/src/components/plugin-download-consent.test.tsx +90 -0
- package/src/components/plugin-download-consent.tsx +183 -0
- package/src/components/popover.tsx +133 -0
- package/src/components/progress.tsx +88 -0
- package/src/components/query-state-view.tsx +47 -0
- package/src/components/radio-group.tsx +36 -0
- package/src/components/scroll-area.tsx +52 -0
- package/src/components/search-field.test.tsx +86 -0
- package/src/components/search-field.tsx +119 -0
- package/src/components/secondary-stat.tsx +24 -0
- package/src/components/section-header.tsx +54 -0
- package/src/components/section-label.tsx +40 -0
- package/src/components/section-tabs.test.tsx +154 -0
- package/src/components/section-tabs.tsx +190 -0
- package/src/components/separator.tsx +38 -0
- package/src/components/sheet.tsx +170 -0
- package/src/components/sidebar.tsx +725 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/slider.tsx +52 -0
- package/src/components/sortable-chip-list.tsx +56 -0
- package/src/components/spinner.tsx +35 -0
- package/src/components/surface.tsx +37 -0
- package/src/components/switch.tsx +30 -0
- package/src/components/table.tsx +116 -0
- package/src/components/textarea.tsx +41 -0
- package/src/components/toast.tsx +239 -0
- package/src/components/toggle.tsx +45 -0
- package/src/components/tooltip.tsx +64 -0
- package/src/hooks/use-dialog-footer-actions.ts +23 -0
- package/src/hooks/use-form-field.ts +62 -0
- package/src/hooks/use-mobile.ts +54 -0
- package/src/hooks/use-pinch-pan.test.tsx +42 -0
- package/src/hooks/use-pinch-pan.ts +340 -0
- package/src/hooks/useMobileBackToClose.test.tsx +140 -0
- package/src/hooks/useMobileBackToClose.ts +508 -0
- package/src/icons/actions.ts +10 -0
- package/src/icons/icon-style.test.tsx +69 -0
- package/src/icons/icon-style.ts +104 -0
- package/src/icons/marks.tsx +72 -0
- package/src/icons/registry.test.ts +65 -0
- package/src/icons/registry.ts +1163 -0
- package/src/index.ts +22 -0
- package/src/lib/merge-config.test.ts +109 -0
- package/src/lib/merge-config.ts +22 -0
- package/src/lib/pagination.test.ts +55 -0
- package/src/lib/pagination.ts +42 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/scan-contract.test.ts +74 -0
- package/src/styles/theme.css +798 -0
- package/src/test/i18n.tsx +22 -0
- package/src/test/setup.ts +104 -0
- package/src/viewport.ts +69 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
5
|
+
import { useMobileBackToClose } from "@hoardodile/ui/hooks/useMobileBackToClose"
|
|
6
|
+
|
|
7
|
+
function Popover({
|
|
8
|
+
open,
|
|
9
|
+
defaultOpen,
|
|
10
|
+
onOpenChange,
|
|
11
|
+
...props
|
|
12
|
+
}: Omit<PopoverPrimitive.Root.Props, "onOpenChange"> & {
|
|
13
|
+
/**
|
|
14
|
+
* `eventDetails` is absent for closes triggered by the mobile back
|
|
15
|
+
* gesture (a synthetic close with no underlying Base UI event).
|
|
16
|
+
*/
|
|
17
|
+
onOpenChange?: (
|
|
18
|
+
open: boolean,
|
|
19
|
+
eventDetails?: PopoverPrimitive.Root.ChangeEventDetails,
|
|
20
|
+
) => void
|
|
21
|
+
}) {
|
|
22
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
|
|
23
|
+
defaultOpen ?? false,
|
|
24
|
+
)
|
|
25
|
+
const isControlled = open !== undefined
|
|
26
|
+
const currentOpen = isControlled ? open : uncontrolledOpen
|
|
27
|
+
function handleOpenChange(
|
|
28
|
+
next: boolean,
|
|
29
|
+
eventDetails?: PopoverPrimitive.Root.ChangeEventDetails,
|
|
30
|
+
) {
|
|
31
|
+
if (!isControlled) setUncontrolledOpen(next)
|
|
32
|
+
onOpenChange?.(next, eventDetails)
|
|
33
|
+
}
|
|
34
|
+
useMobileBackToClose(currentOpen, handleOpenChange)
|
|
35
|
+
return (
|
|
36
|
+
<PopoverPrimitive.Root
|
|
37
|
+
data-slot="popover"
|
|
38
|
+
open={currentOpen}
|
|
39
|
+
onOpenChange={handleOpenChange}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
|
46
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function PopoverContent({
|
|
50
|
+
className,
|
|
51
|
+
align = "center",
|
|
52
|
+
alignOffset = 0,
|
|
53
|
+
side = "bottom",
|
|
54
|
+
sideOffset = 4,
|
|
55
|
+
container,
|
|
56
|
+
...props
|
|
57
|
+
}: PopoverPrimitive.Popup.Props &
|
|
58
|
+
Pick<
|
|
59
|
+
PopoverPrimitive.Positioner.Props,
|
|
60
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
61
|
+
> & {
|
|
62
|
+
/**
|
|
63
|
+
* Override the portal target. Used by the video player so popovers
|
|
64
|
+
* stay attached to the fullscreen container; otherwise they'd be
|
|
65
|
+
* portalled to `document.body` and rendered behind the fullscreen
|
|
66
|
+
* surface, where every tap registers as outside-click and dismisses
|
|
67
|
+
* the popover immediately.
|
|
68
|
+
*/
|
|
69
|
+
container?: PopoverPrimitive.Portal.Props["container"]
|
|
70
|
+
}) {
|
|
71
|
+
return (
|
|
72
|
+
<PopoverPrimitive.Portal container={container}>
|
|
73
|
+
<PopoverPrimitive.Positioner
|
|
74
|
+
align={align}
|
|
75
|
+
alignOffset={alignOffset}
|
|
76
|
+
side={side}
|
|
77
|
+
sideOffset={sideOffset}
|
|
78
|
+
className="isolate z-50"
|
|
79
|
+
>
|
|
80
|
+
<PopoverPrimitive.Popup
|
|
81
|
+
data-slot="popover-content"
|
|
82
|
+
className={cn(
|
|
83
|
+
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-md bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
</PopoverPrimitive.Positioner>
|
|
89
|
+
</PopoverPrimitive.Portal>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
data-slot="popover-header"
|
|
97
|
+
className={cn("flex flex-col gap-1 text-sm", className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
|
|
104
|
+
return (
|
|
105
|
+
<PopoverPrimitive.Title
|
|
106
|
+
data-slot="popover-title"
|
|
107
|
+
className={cn("font-medium", className)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function PopoverDescription({
|
|
114
|
+
className,
|
|
115
|
+
...props
|
|
116
|
+
}: PopoverPrimitive.Description.Props) {
|
|
117
|
+
return (
|
|
118
|
+
<PopoverPrimitive.Description
|
|
119
|
+
data-slot="popover-description"
|
|
120
|
+
className={cn("text-muted-foreground", className)}
|
|
121
|
+
{...props}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
Popover,
|
|
128
|
+
PopoverContent,
|
|
129
|
+
PopoverDescription,
|
|
130
|
+
PopoverHeader,
|
|
131
|
+
PopoverTitle,
|
|
132
|
+
PopoverTrigger,
|
|
133
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Progress({
|
|
8
|
+
className,
|
|
9
|
+
children,
|
|
10
|
+
value,
|
|
11
|
+
...props
|
|
12
|
+
}: ProgressPrimitive.Root.Props) {
|
|
13
|
+
return (
|
|
14
|
+
// `relative` makes the root the containing block for loose children
|
|
15
|
+
// (e.g. `sr-only` labels for screen readers): without it they anchor
|
|
16
|
+
// to the nearest positioned ancestor instead — typically the app
|
|
17
|
+
// shell, outside the page's scroll clip — and stack a second,
|
|
18
|
+
// viewport-level scrollbar on long pages (Settings → App).
|
|
19
|
+
<ProgressPrimitive.Root
|
|
20
|
+
value={value}
|
|
21
|
+
data-slot="progress"
|
|
22
|
+
className={cn("relative flex flex-wrap gap-3", className)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
<ProgressTrack>
|
|
27
|
+
<ProgressIndicator />
|
|
28
|
+
</ProgressTrack>
|
|
29
|
+
</ProgressPrimitive.Root>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
|
|
34
|
+
return (
|
|
35
|
+
<ProgressPrimitive.Track
|
|
36
|
+
className={cn(
|
|
37
|
+
"relative flex h-1.5 w-full items-center overflow-x-hidden rounded-full bg-muted",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
data-slot="progress-track"
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ProgressIndicator({
|
|
47
|
+
className,
|
|
48
|
+
...props
|
|
49
|
+
}: ProgressPrimitive.Indicator.Props) {
|
|
50
|
+
return (
|
|
51
|
+
<ProgressPrimitive.Indicator
|
|
52
|
+
data-slot="progress-indicator"
|
|
53
|
+
className={cn("h-full bg-primary transition-all", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
|
|
60
|
+
return (
|
|
61
|
+
<ProgressPrimitive.Label
|
|
62
|
+
className={cn("text-sm font-medium", className)}
|
|
63
|
+
data-slot="progress-label"
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
|
|
70
|
+
return (
|
|
71
|
+
<ProgressPrimitive.Value
|
|
72
|
+
className={cn(
|
|
73
|
+
"ml-auto text-sm text-muted-foreground tabular-nums",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
data-slot="progress-value"
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
Progress,
|
|
84
|
+
ProgressTrack,
|
|
85
|
+
ProgressIndicator,
|
|
86
|
+
ProgressLabel,
|
|
87
|
+
ProgressValue,
|
|
88
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slot-based renderer for the loading/empty/error/data quartet that
|
|
5
|
+
* wraps almost every list query in the app. Reduces:
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* {q.isLoading ? <Skeleton/> : null}
|
|
9
|
+
* {!q.isLoading && q.isError ? <Error/> : null}
|
|
10
|
+
* {!q.isLoading && !q.isError && data.length === 0 ? <Empty/> : null}
|
|
11
|
+
* {!q.isLoading && !q.isError && data.length > 0 ? <List data={data}/> : null}
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* to a single declarative element. Pass a `result` describing the
|
|
15
|
+
* query's status, a derived `data`, and the slot renderers.
|
|
16
|
+
*
|
|
17
|
+
* Decoupled from TanStack Query so it can be used with any data source
|
|
18
|
+
* (URL state, suspense, manual fetches) and stays cheap to test.
|
|
19
|
+
*/
|
|
20
|
+
export type QueryStateResult<T> = {
|
|
21
|
+
readonly isLoading: boolean
|
|
22
|
+
readonly isError: boolean
|
|
23
|
+
readonly error?: Error | null
|
|
24
|
+
readonly data: T | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type QueryStateViewProps<T> = {
|
|
28
|
+
readonly result: QueryStateResult<T>
|
|
29
|
+
readonly isEmpty?: (data: T) => boolean
|
|
30
|
+
readonly loading: ReactNode
|
|
31
|
+
readonly empty?: ReactNode
|
|
32
|
+
readonly error?: (error: Error) => ReactNode
|
|
33
|
+
readonly children: (data: T) => ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function QueryStateView<T>(props: QueryStateViewProps<T>): ReactNode {
|
|
37
|
+
const { result, isEmpty, loading, empty, error, children } = props
|
|
38
|
+
if (result.isLoading) return loading
|
|
39
|
+
if (result.isError) {
|
|
40
|
+
const renderError = error
|
|
41
|
+
const err = result.error ?? new Error("Unknown error")
|
|
42
|
+
return renderError !== undefined ? renderError(err) : null
|
|
43
|
+
}
|
|
44
|
+
if (result.data === undefined) return null
|
|
45
|
+
if (isEmpty?.(result.data) === true) return empty ?? null
|
|
46
|
+
return children(result.data)
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
5
|
+
|
|
6
|
+
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
|
|
7
|
+
return (
|
|
8
|
+
<RadioGroupPrimitive
|
|
9
|
+
data-slot="radio-group"
|
|
10
|
+
className={cn("grid w-full gap-3", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
|
|
17
|
+
return (
|
|
18
|
+
<RadioPrimitive.Root
|
|
19
|
+
data-slot="radio-group-item"
|
|
20
|
+
className={cn(
|
|
21
|
+
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<RadioPrimitive.Indicator
|
|
27
|
+
data-slot="radio-group-indicator"
|
|
28
|
+
className="flex size-4 items-center justify-center"
|
|
29
|
+
>
|
|
30
|
+
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
|
|
31
|
+
</RadioPrimitive.Indicator>
|
|
32
|
+
</RadioPrimitive.Root>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { RadioGroup, RadioGroupItem }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
4
|
+
|
|
5
|
+
function ScrollArea({
|
|
6
|
+
className,
|
|
7
|
+
children,
|
|
8
|
+
...props
|
|
9
|
+
}: ScrollAreaPrimitive.Root.Props) {
|
|
10
|
+
return (
|
|
11
|
+
<ScrollAreaPrimitive.Root
|
|
12
|
+
data-slot="scroll-area"
|
|
13
|
+
className={cn("relative", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<ScrollAreaPrimitive.Viewport
|
|
17
|
+
data-slot="scroll-area-viewport"
|
|
18
|
+
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</ScrollAreaPrimitive.Viewport>
|
|
22
|
+
<ScrollBar />
|
|
23
|
+
<ScrollAreaPrimitive.Corner />
|
|
24
|
+
</ScrollAreaPrimitive.Root>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ScrollBar({
|
|
29
|
+
className,
|
|
30
|
+
orientation = "vertical",
|
|
31
|
+
...props
|
|
32
|
+
}: ScrollAreaPrimitive.Scrollbar.Props) {
|
|
33
|
+
return (
|
|
34
|
+
<ScrollAreaPrimitive.Scrollbar
|
|
35
|
+
data-slot="scroll-area-scrollbar"
|
|
36
|
+
data-orientation={orientation}
|
|
37
|
+
orientation={orientation}
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
<ScrollAreaPrimitive.Thumb
|
|
45
|
+
data-slot="scroll-area-thumb"
|
|
46
|
+
className="relative flex-1 rounded-full bg-border"
|
|
47
|
+
/>
|
|
48
|
+
</ScrollAreaPrimitive.Scrollbar>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { ScrollArea, ScrollBar }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react"
|
|
2
|
+
import userEvent from "@testing-library/user-event"
|
|
3
|
+
import { describe, expect, test, vi } from "vitest"
|
|
4
|
+
import { SearchField } from "./search-field"
|
|
5
|
+
|
|
6
|
+
describe("SearchField", () => {
|
|
7
|
+
test("debounces keystrokes before committing", async () => {
|
|
8
|
+
const user = userEvent.setup()
|
|
9
|
+
const onCommit = vi.fn()
|
|
10
|
+
render(
|
|
11
|
+
<SearchField value="" onCommit={onCommit} testId="search" delayMs={50} />,
|
|
12
|
+
)
|
|
13
|
+
await user.type(screen.getByTestId("search"), "hello")
|
|
14
|
+
expect(onCommit).not.toHaveBeenCalledWith("hello")
|
|
15
|
+
await waitFor(() => {
|
|
16
|
+
expect(onCommit).toHaveBeenCalledWith("hello")
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("re-syncs with external resets", () => {
|
|
21
|
+
const { rerender } = render(
|
|
22
|
+
<SearchField value="foo" onCommit={() => {}} testId="search" />,
|
|
23
|
+
)
|
|
24
|
+
const input = screen.getByTestId("search") as HTMLInputElement
|
|
25
|
+
expect(input.value).toBe("foo")
|
|
26
|
+
rerender(<SearchField value="" onCommit={() => {}} testId="search" />)
|
|
27
|
+
expect(input.value).toBe("")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("commits only on Enter in commitOnEnterOnly mode", async () => {
|
|
31
|
+
const user = userEvent.setup()
|
|
32
|
+
const onCommit = vi.fn()
|
|
33
|
+
const onSubmit = vi.fn()
|
|
34
|
+
render(
|
|
35
|
+
<SearchField
|
|
36
|
+
value=""
|
|
37
|
+
onCommit={onCommit}
|
|
38
|
+
onSubmit={onSubmit}
|
|
39
|
+
commitOnEnterOnly
|
|
40
|
+
testId="search"
|
|
41
|
+
delayMs={10}
|
|
42
|
+
/>,
|
|
43
|
+
)
|
|
44
|
+
await user.type(screen.getByTestId("search"), "harbor")
|
|
45
|
+
// Typing never commits on its own in this mode.
|
|
46
|
+
await waitFor(() => {
|
|
47
|
+
expect(onCommit).not.toHaveBeenCalled()
|
|
48
|
+
})
|
|
49
|
+
await user.keyboard("{Enter}")
|
|
50
|
+
expect(onSubmit).toHaveBeenCalledWith("harbor")
|
|
51
|
+
expect(onCommit).not.toHaveBeenCalled()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("commits immediately and submits on Enter in debounced mode", async () => {
|
|
55
|
+
const user = userEvent.setup()
|
|
56
|
+
const onCommit = vi.fn()
|
|
57
|
+
const onSubmit = vi.fn()
|
|
58
|
+
render(
|
|
59
|
+
<SearchField
|
|
60
|
+
value=""
|
|
61
|
+
onCommit={onCommit}
|
|
62
|
+
onSubmit={onSubmit}
|
|
63
|
+
testId="search"
|
|
64
|
+
delayMs={10}
|
|
65
|
+
/>,
|
|
66
|
+
)
|
|
67
|
+
await user.type(screen.getByTestId("search"), "harbor")
|
|
68
|
+
await user.keyboard("{Enter}")
|
|
69
|
+
expect(onCommit).toHaveBeenCalledWith("harbor")
|
|
70
|
+
expect(onSubmit).toHaveBeenCalledWith("harbor")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test("submits the draft through the form", async () => {
|
|
74
|
+
const user = userEvent.setup()
|
|
75
|
+
const onSubmit = vi.fn()
|
|
76
|
+
render(<SearchField value="" onSubmit={onSubmit} testId="search" />)
|
|
77
|
+
await user.type(screen.getByTestId("search"), "harbor")
|
|
78
|
+
await user.keyboard("{Enter}")
|
|
79
|
+
expect(onSubmit).toHaveBeenCalledWith("harbor")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("renders the global-search-input testid by default", () => {
|
|
83
|
+
render(<SearchField value="" />)
|
|
84
|
+
expect(screen.getByTestId("global-search-input")).not.toBeNull()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Icon } from "@hoardodile/ui/components/icon"
|
|
2
|
+
import { Magnifier } from "@hoardodile/ui/icons/registry"
|
|
3
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
4
|
+
import {
|
|
5
|
+
type KeyboardEvent,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type Ref,
|
|
8
|
+
useEffect,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react"
|
|
11
|
+
import { useDebounce } from "react-use"
|
|
12
|
+
|
|
13
|
+
export type SearchFieldProps = {
|
|
14
|
+
/** The committed value; the input re-syncs to it on external changes. */
|
|
15
|
+
readonly value: string
|
|
16
|
+
readonly placeholder?: string
|
|
17
|
+
/**
|
|
18
|
+
* Layout overrides — the hero form passes `h-11 px-4` (the sidebar
|
|
19
|
+
* h-nav and the hero h-11 share one anatomy).
|
|
20
|
+
*/
|
|
21
|
+
readonly className?: string
|
|
22
|
+
/** Optional actions rendered at the trailing edge of the field. */
|
|
23
|
+
readonly actions?: ReactNode
|
|
24
|
+
/** Input length cap (e.g. the app's max search-query length). */
|
|
25
|
+
readonly maxLength?: number
|
|
26
|
+
/**
|
|
27
|
+
* Debounced commit of the draft (unless {@link commitOnEnterOnly}).
|
|
28
|
+
* Filter bars pass their live patch here.
|
|
29
|
+
*/
|
|
30
|
+
readonly onCommit?: (value: string) => void
|
|
31
|
+
/** Enter / form submit — navigation and apply-on-demand surfaces. */
|
|
32
|
+
readonly onSubmit?: (value: string) => void
|
|
33
|
+
/**
|
|
34
|
+
* When true, keystrokes never debounce-commit; the value is delivered
|
|
35
|
+
* only through {@link onSubmit} (Enter). Used by apply-on-demand
|
|
36
|
+
* surfaces where typing stages and Enter applies.
|
|
37
|
+
*/
|
|
38
|
+
readonly commitOnEnterOnly?: boolean
|
|
39
|
+
readonly delayMs?: number
|
|
40
|
+
/** Optional ref to the inner input (sidebar "/" and Ctrl/Cmd+K). */
|
|
41
|
+
readonly inputRef?: Ref<HTMLInputElement>
|
|
42
|
+
readonly testId?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The one search field: muted fill, rounded-lg, leading magnifier,
|
|
47
|
+
* muted placeholder — sidebar (h-nav) and hero (h-11) share this
|
|
48
|
+
* anatomy, as do the index-page filter bars. Commit semantics are the
|
|
49
|
+
* caller's: debounced {@link onCommit} for live filtering, Enter through
|
|
50
|
+
* {@link onSubmit} for navigation and apply-on-demand surfaces.
|
|
51
|
+
*
|
|
52
|
+
* The root is a div, not a form: the field nests inside page forms (the
|
|
53
|
+
* new character/resource editors, which wrap their whole body in one), so
|
|
54
|
+
* a nested form would be invalid HTML. Enter is captured on keydown and
|
|
55
|
+
* preventDefaulted, so it never submits an enclosing form either.
|
|
56
|
+
*/
|
|
57
|
+
export function SearchField(props: SearchFieldProps) {
|
|
58
|
+
const {
|
|
59
|
+
value,
|
|
60
|
+
placeholder,
|
|
61
|
+
className,
|
|
62
|
+
actions,
|
|
63
|
+
maxLength,
|
|
64
|
+
onCommit,
|
|
65
|
+
onSubmit,
|
|
66
|
+
commitOnEnterOnly,
|
|
67
|
+
delayMs,
|
|
68
|
+
inputRef,
|
|
69
|
+
testId,
|
|
70
|
+
} = props
|
|
71
|
+
const [draft, setDraft] = useState(value)
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setDraft(value)
|
|
75
|
+
}, [value])
|
|
76
|
+
|
|
77
|
+
useDebounce(
|
|
78
|
+
() => {
|
|
79
|
+
if (commitOnEnterOnly) return
|
|
80
|
+
if (draft !== value) onCommit?.(draft)
|
|
81
|
+
},
|
|
82
|
+
delayMs ?? 300,
|
|
83
|
+
[draft],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
|
87
|
+
if (event.key !== "Enter") return
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
if (commitOnEnterOnly) {
|
|
90
|
+
onSubmit?.(draft)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (draft !== value) onCommit?.(draft)
|
|
94
|
+
onSubmit?.(draft)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
onKeyDown={handleKeyDown}
|
|
100
|
+
className={cn(
|
|
101
|
+
"flex h-nav items-center gap-2 rounded-lg bg-muted px-3",
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
<Icon icon={Magnifier} className="shrink-0 text-muted-foreground" />
|
|
106
|
+
<input
|
|
107
|
+
ref={inputRef}
|
|
108
|
+
type="text"
|
|
109
|
+
value={draft}
|
|
110
|
+
onChange={(ev) => setDraft(ev.target.value)}
|
|
111
|
+
placeholder={placeholder}
|
|
112
|
+
maxLength={maxLength}
|
|
113
|
+
className="min-w-0 flex-1 bg-transparent text-ui text-foreground outline-none placeholder:text-muted-foreground"
|
|
114
|
+
data-testid={testId ?? "global-search-input"}
|
|
115
|
+
/>
|
|
116
|
+
{actions}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SectionLabel } from "./section-label"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Secondary stat — the quiet supporting readout beside a KPI: uppercase
|
|
5
|
+
* tracking-label label over a bold tabular numeral.
|
|
6
|
+
*/
|
|
7
|
+
export function SecondaryStat({
|
|
8
|
+
label,
|
|
9
|
+
value,
|
|
10
|
+
className,
|
|
11
|
+
}: {
|
|
12
|
+
readonly label: string
|
|
13
|
+
readonly value: string
|
|
14
|
+
readonly className?: string
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<div className={className}>
|
|
18
|
+
<SectionLabel>{label}</SectionLabel>
|
|
19
|
+
<div className="mt-1 text-xl font-bold tabular-nums text-foreground">
|
|
20
|
+
{value}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Icon, type IconType } from "@hoardodile/ui/components/icon"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Section header: icon 16 + 16px semibold title, muted count on the
|
|
6
|
+
* baseline; the right slot holds trailing controls ("View all" links,
|
|
7
|
+
* toggles, marquee chevrons).
|
|
8
|
+
*/
|
|
9
|
+
export function SectionHeader({
|
|
10
|
+
icon,
|
|
11
|
+
title,
|
|
12
|
+
count,
|
|
13
|
+
viewAll,
|
|
14
|
+
onViewAll,
|
|
15
|
+
right,
|
|
16
|
+
className,
|
|
17
|
+
}: {
|
|
18
|
+
readonly icon?: IconType
|
|
19
|
+
readonly title: string
|
|
20
|
+
/** Muted count rendered right after the title, e.g. "(2)". */
|
|
21
|
+
readonly count?: string
|
|
22
|
+
/** Label for the right-aligned "View all" text link. */
|
|
23
|
+
readonly viewAll?: string
|
|
24
|
+
readonly onViewAll?: () => void
|
|
25
|
+
/** Right-aligned trailing slot (the ml-auto position). */
|
|
26
|
+
readonly right?: ReactNode
|
|
27
|
+
readonly className?: string
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`flex items-center gap-2 ${className ?? ""}`}>
|
|
31
|
+
{icon !== undefined ? (
|
|
32
|
+
<Icon icon={icon} className="text-secondary-foreground" />
|
|
33
|
+
) : null}
|
|
34
|
+
<span className="text-base font-semibold text-foreground">{title}</span>
|
|
35
|
+
{count !== undefined ? (
|
|
36
|
+
<span className="text-xs text-muted-foreground">{count}</span>
|
|
37
|
+
) : null}
|
|
38
|
+
{viewAll !== undefined || right !== undefined ? (
|
|
39
|
+
<span className="ml-auto flex items-center gap-4">
|
|
40
|
+
{viewAll !== undefined ? (
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={onViewAll}
|
|
44
|
+
className="cursor-pointer text-xs text-muted-foreground hover:text-secondary-foreground"
|
|
45
|
+
>
|
|
46
|
+
{viewAll}
|
|
47
|
+
</button>
|
|
48
|
+
) : null}
|
|
49
|
+
{right}
|
|
50
|
+
</span>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Section label — the 12px uppercase tracking-label voice (DESIGN.md —
|
|
6
|
+
* Typography), one component instead of a class string repeated across
|
|
7
|
+
* panels, rails, dialogs and settings. Ink tones: `muted` (default, the
|
|
8
|
+
* quiet section voice), `foreground` (structural headings), `danger`
|
|
9
|
+
* (the dialog eyebrow of a destructive ritual). Callers with a
|
|
10
|
+
* right-aligned count arrange it inside `children` (flex via
|
|
11
|
+
* `className`) — the voice is the unit.
|
|
12
|
+
*/
|
|
13
|
+
export function SectionLabel({
|
|
14
|
+
tone = "muted",
|
|
15
|
+
size = "xs",
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
tone?: "muted" | "foreground" | "danger"
|
|
20
|
+
size?: "xs" | "tiny"
|
|
21
|
+
className?: string
|
|
22
|
+
children: ReactNode
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={cn(
|
|
27
|
+
size === "tiny" ? "text-tiny leading-none" : "text-xs",
|
|
28
|
+
"font-semibold tracking-label uppercase",
|
|
29
|
+
tone === "foreground"
|
|
30
|
+
? "text-foreground"
|
|
31
|
+
: tone === "danger"
|
|
32
|
+
? "text-destructive"
|
|
33
|
+
: "text-muted-foreground",
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|