@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,151 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react"
|
|
2
|
+
import userEvent from "@testing-library/user-event"
|
|
3
|
+
import { describe, expect, it, vi } from "vitest"
|
|
4
|
+
import {
|
|
5
|
+
CaptionBar,
|
|
6
|
+
type CaptionHistoryControls,
|
|
7
|
+
type CaptionWindowControls,
|
|
8
|
+
} from "./caption-bar"
|
|
9
|
+
|
|
10
|
+
function fakeControls(
|
|
11
|
+
overrides: Partial<CaptionWindowControls> = {},
|
|
12
|
+
): CaptionWindowControls {
|
|
13
|
+
return {
|
|
14
|
+
minimize: vi.fn(),
|
|
15
|
+
toggleMaximize: vi.fn(),
|
|
16
|
+
close: vi.fn(),
|
|
17
|
+
isMaximized: async () => false,
|
|
18
|
+
onMaximizedChange: () => () => undefined,
|
|
19
|
+
...overrides,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fakeHistory(
|
|
24
|
+
overrides: Partial<CaptionHistoryControls> = {},
|
|
25
|
+
): CaptionHistoryControls {
|
|
26
|
+
return {
|
|
27
|
+
canGoBack: true,
|
|
28
|
+
canGoForward: true,
|
|
29
|
+
back: vi.fn(),
|
|
30
|
+
forward: vi.fn(),
|
|
31
|
+
reload: vi.fn(),
|
|
32
|
+
...overrides,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("CaptionBar", () => {
|
|
37
|
+
it("invokes window controls from the caption buttons", async () => {
|
|
38
|
+
const user = userEvent.setup()
|
|
39
|
+
const controls = fakeControls()
|
|
40
|
+
render(<CaptionBar controls={controls} history={fakeHistory()} />)
|
|
41
|
+
|
|
42
|
+
await user.click(screen.getByTestId("desktop-caption-minimize"))
|
|
43
|
+
await user.click(screen.getByTestId("desktop-caption-maximize"))
|
|
44
|
+
await user.click(screen.getByTestId("desktop-caption-close"))
|
|
45
|
+
|
|
46
|
+
expect(controls.minimize).toHaveBeenCalledTimes(1)
|
|
47
|
+
expect(controls.toggleMaximize).toHaveBeenCalledTimes(1)
|
|
48
|
+
expect(controls.close).toHaveBeenCalledTimes(1)
|
|
49
|
+
expect(screen.queryByText("hoardodile")).toBeNull()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("hides the DevTools button when the control is absent", () => {
|
|
53
|
+
render(<CaptionBar controls={fakeControls()} history={fakeHistory()} />)
|
|
54
|
+
expect(screen.queryByTestId("desktop-caption-devtools")).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("toggles DevTools from the caption button", async () => {
|
|
58
|
+
const user = userEvent.setup()
|
|
59
|
+
const toggleDevtools = vi.fn()
|
|
60
|
+
render(
|
|
61
|
+
<CaptionBar
|
|
62
|
+
controls={fakeControls({ toggleDevtools })}
|
|
63
|
+
history={fakeHistory()}
|
|
64
|
+
/>,
|
|
65
|
+
)
|
|
66
|
+
await user.click(screen.getByTestId("desktop-caption-devtools"))
|
|
67
|
+
expect(toggleDevtools).toHaveBeenCalledTimes(1)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("does not double-toggle on double-click of the drag region (native handles it)", async () => {
|
|
71
|
+
const user = userEvent.setup()
|
|
72
|
+
const controls = fakeControls()
|
|
73
|
+
render(<CaptionBar controls={controls} history={fakeHistory()} />)
|
|
74
|
+
|
|
75
|
+
await user.dblClick(screen.getByTestId("desktop-caption-drag"))
|
|
76
|
+
expect(controls.toggleMaximize).not.toHaveBeenCalled()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("keeps reload a plain browser reload (never routed through the desktop bridge)", async () => {
|
|
80
|
+
const user = userEvent.setup()
|
|
81
|
+
const retryLoad = vi.fn()
|
|
82
|
+
Reflect.set(window, "hoardodileDesktop", { retryLoad })
|
|
83
|
+
try {
|
|
84
|
+
render(<CaptionBar controls={fakeControls()} />)
|
|
85
|
+
await user.click(screen.getByTestId("desktop-caption-reload"))
|
|
86
|
+
expect(retryLoad).not.toHaveBeenCalled()
|
|
87
|
+
} finally {
|
|
88
|
+
Reflect.deleteProperty(window, "hoardodileDesktop")
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("blurs a caption button focused during load so no stray ring persists", async () => {
|
|
93
|
+
const { unmount } = render(
|
|
94
|
+
<CaptionBar controls={fakeControls()} history={fakeHistory()} />,
|
|
95
|
+
)
|
|
96
|
+
const back = screen.getByTestId("desktop-caption-back")
|
|
97
|
+
back.focus()
|
|
98
|
+
expect(document.activeElement).toBe(back)
|
|
99
|
+
window.dispatchEvent(new Event("load"))
|
|
100
|
+
expect(document.activeElement).not.toBe(back)
|
|
101
|
+
unmount()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("invokes history controls from the navigation buttons", async () => {
|
|
105
|
+
const user = userEvent.setup()
|
|
106
|
+
const history = fakeHistory()
|
|
107
|
+
render(<CaptionBar controls={fakeControls()} history={history} />)
|
|
108
|
+
|
|
109
|
+
await user.click(screen.getByTestId("desktop-caption-back"))
|
|
110
|
+
await user.click(screen.getByTestId("desktop-caption-forward"))
|
|
111
|
+
await user.click(screen.getByTestId("desktop-caption-reload"))
|
|
112
|
+
|
|
113
|
+
expect(history.back).toHaveBeenCalledTimes(1)
|
|
114
|
+
expect(history.forward).toHaveBeenCalledTimes(1)
|
|
115
|
+
expect(history.reload).toHaveBeenCalledTimes(1)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("disables back and forward when the stack cannot move", () => {
|
|
119
|
+
render(
|
|
120
|
+
<CaptionBar
|
|
121
|
+
controls={fakeControls()}
|
|
122
|
+
history={fakeHistory({ canGoBack: false, canGoForward: false })}
|
|
123
|
+
/>,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expect(screen.getByTestId("desktop-caption-back")).toBeDisabled()
|
|
127
|
+
expect(screen.getByTestId("desktop-caption-forward")).toBeDisabled()
|
|
128
|
+
expect(screen.getByTestId("desktop-caption-reload")).toBeEnabled()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("renders the leading slot leftmost, before the history buttons", () => {
|
|
132
|
+
render(
|
|
133
|
+
<CaptionBar
|
|
134
|
+
controls={fakeControls()}
|
|
135
|
+
history={fakeHistory()}
|
|
136
|
+
leading={
|
|
137
|
+
<button type="button" data-testid="desktop-caption-leading">
|
|
138
|
+
menu
|
|
139
|
+
</button>
|
|
140
|
+
}
|
|
141
|
+
/>,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const leading = screen.getByTestId("desktop-caption-leading")
|
|
145
|
+
const back = screen.getByTestId("desktop-caption-back")
|
|
146
|
+
expect(screen.getByTestId("desktop-caption-bar")).toContainElement(leading)
|
|
147
|
+
expect(
|
|
148
|
+
leading.compareDocumentPosition(back) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
149
|
+
).toBeTruthy()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Icon } from "@hoardodile/ui/components/icon"
|
|
4
|
+
import { Refresh } from "@hoardodile/ui/icons/registry"
|
|
5
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
6
|
+
import { type ReactNode, useEffect, useState } from "react"
|
|
7
|
+
import { useTranslation } from "react-i18next"
|
|
8
|
+
|
|
9
|
+
export type CaptionWindowControls = {
|
|
10
|
+
minimize: () => void
|
|
11
|
+
toggleMaximize: () => void
|
|
12
|
+
close: () => void
|
|
13
|
+
/**
|
|
14
|
+
* Toggle the DevTools dock. Present only on dev (unpackaged) runs —
|
|
15
|
+
* the desktop bridge carries it only there, so packaged builds never
|
|
16
|
+
* render the button.
|
|
17
|
+
*/
|
|
18
|
+
toggleDevtools?: () => void
|
|
19
|
+
isMaximized: () => Promise<boolean>
|
|
20
|
+
onMaximizedChange: (listener: (maximized: boolean) => void) => () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const captionChromeButtonClassName =
|
|
24
|
+
"flex h-nav w-[46px] items-center justify-center text-secondary-foreground enabled:hover:bg-muted enabled:hover:text-foreground disabled:text-muted-foreground focus:outline-none focus-visible:outline-none focus-visible:ring-0"
|
|
25
|
+
|
|
26
|
+
export type CaptionHistoryControls = {
|
|
27
|
+
readonly canGoBack: boolean
|
|
28
|
+
readonly canGoForward: boolean
|
|
29
|
+
back: () => void
|
|
30
|
+
forward: () => void
|
|
31
|
+
reload: () => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CaptionBarProps = {
|
|
35
|
+
readonly controls: CaptionWindowControls
|
|
36
|
+
/** Test seam; production uses the window history stack. */
|
|
37
|
+
readonly history?: CaptionHistoryControls
|
|
38
|
+
/**
|
|
39
|
+
* Leftmost slot in the strip's no-drag group, before back / forward /
|
|
40
|
+
* reload. Used by the SPA to host the global sidebar toggle below the
|
|
41
|
+
* sidebar breakpoint; the wizard leaves it empty.
|
|
42
|
+
*/
|
|
43
|
+
readonly leading?: ReactNode
|
|
44
|
+
readonly className?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Frameless-window caption strip (DESIGN.md: 38px `h-nav`). An optional
|
|
49
|
+
* `leading` control, then back / forward / reload on the left; drag region
|
|
50
|
+
* in the middle; Windows caption buttons on the right. Shared by the
|
|
51
|
+
* first-run wizard and the SPA so the two pages cannot drift.
|
|
52
|
+
*/
|
|
53
|
+
function CaptionBar({
|
|
54
|
+
controls,
|
|
55
|
+
history,
|
|
56
|
+
leading,
|
|
57
|
+
className,
|
|
58
|
+
}: CaptionBarProps) {
|
|
59
|
+
const { t } = useTranslation("ui", { useSuspense: false })
|
|
60
|
+
const windowHistory = useWindowHistoryControls()
|
|
61
|
+
const nav = history ?? windowHistory
|
|
62
|
+
const [maximized, setMaximized] = useState(false)
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
let cancelled = false
|
|
66
|
+
void controls.isMaximized().then((value) => {
|
|
67
|
+
if (!cancelled) setMaximized(value)
|
|
68
|
+
})
|
|
69
|
+
const unsubscribe = controls.onMaximizedChange(setMaximized)
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true
|
|
72
|
+
unsubscribe()
|
|
73
|
+
}
|
|
74
|
+
}, [controls])
|
|
75
|
+
|
|
76
|
+
// Chromium may restore focus to the last-focused caption button when
|
|
77
|
+
// the window or session comes back, leaving a stray focus ring on the
|
|
78
|
+
// chrome. Clear focus only when it sits on a caption button — never
|
|
79
|
+
// blur an input the user is typing into. Runs at mount (restored focus
|
|
80
|
+
// may already be present) and again on load (restored afterwards).
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
function clearChromeFocus() {
|
|
83
|
+
const el = document.activeElement
|
|
84
|
+
if (
|
|
85
|
+
el instanceof HTMLElement &&
|
|
86
|
+
el.closest('[data-testid="desktop-caption-bar"]') !== null
|
|
87
|
+
) {
|
|
88
|
+
el.blur()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
clearChromeFocus()
|
|
92
|
+
window.addEventListener("load", clearChromeFocus)
|
|
93
|
+
return () => {
|
|
94
|
+
window.removeEventListener("load", clearChromeFocus)
|
|
95
|
+
}
|
|
96
|
+
}, [])
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
data-testid="desktop-caption-bar"
|
|
101
|
+
className={cn(
|
|
102
|
+
"flex h-nav shrink-0 select-none items-stretch bg-background text-foreground",
|
|
103
|
+
className
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<div className="flex shrink-0 [-webkit-app-region:no-drag]">
|
|
107
|
+
{leading}
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
title={t("caption.back")}
|
|
111
|
+
aria-label={t("caption.back")}
|
|
112
|
+
data-testid="desktop-caption-back"
|
|
113
|
+
disabled={!nav.canGoBack}
|
|
114
|
+
className={captionChromeButtonClassName}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
nav.back()
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<ChevronLeftGlyph />
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
title={t("caption.forward")}
|
|
124
|
+
aria-label={t("caption.forward")}
|
|
125
|
+
data-testid="desktop-caption-forward"
|
|
126
|
+
disabled={!nav.canGoForward}
|
|
127
|
+
className={captionChromeButtonClassName}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
nav.forward()
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<ChevronRightGlyph />
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
title={t("caption.reload")}
|
|
137
|
+
aria-label={t("caption.reload")}
|
|
138
|
+
data-testid="desktop-caption-reload"
|
|
139
|
+
className={captionChromeButtonClassName}
|
|
140
|
+
onClick={() => {
|
|
141
|
+
nav.reload()
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<Icon icon={Refresh} />
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
<div
|
|
148
|
+
data-testid="desktop-caption-drag"
|
|
149
|
+
className="min-w-0 flex-1 [-webkit-app-region:drag]"
|
|
150
|
+
/>
|
|
151
|
+
{/* Double-click toggles maximize natively on Windows drag regions;
|
|
152
|
+
a JS handler here would double-toggle (native + JS). */}
|
|
153
|
+
<div className="flex shrink-0 [-webkit-app-region:no-drag]">
|
|
154
|
+
{controls.toggleDevtools !== undefined ? (
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
title={t("caption.devtools")}
|
|
158
|
+
aria-label={t("caption.devtools")}
|
|
159
|
+
data-testid="desktop-caption-devtools"
|
|
160
|
+
className={captionChromeButtonClassName}
|
|
161
|
+
onClick={() => {
|
|
162
|
+
controls.toggleDevtools?.()
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<DevToolsGlyph />
|
|
166
|
+
</button>
|
|
167
|
+
) : null}
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
aria-label={t("caption.minimize")}
|
|
171
|
+
data-testid="desktop-caption-minimize"
|
|
172
|
+
className={captionChromeButtonClassName}
|
|
173
|
+
onClick={() => {
|
|
174
|
+
controls.minimize()
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<MinimizeGlyph />
|
|
178
|
+
</button>
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
aria-label={maximized ? t("caption.restore") : t("caption.maximize")}
|
|
182
|
+
data-testid="desktop-caption-maximize"
|
|
183
|
+
className={captionChromeButtonClassName}
|
|
184
|
+
onClick={() => {
|
|
185
|
+
controls.toggleMaximize()
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{maximized ? <RestoreGlyph /> : <MaximizeGlyph />}
|
|
189
|
+
</button>
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
aria-label={t("caption.close")}
|
|
193
|
+
data-testid="desktop-caption-close"
|
|
194
|
+
className="flex h-nav w-[46px] items-center justify-center text-secondary-foreground hover:bg-[#c42b1c] hover:text-white"
|
|
195
|
+
onClick={() => {
|
|
196
|
+
controls.close()
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<CloseGlyph />
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type WindowNavigationSource = {
|
|
207
|
+
readonly canGoBack: boolean
|
|
208
|
+
readonly canGoForward: boolean
|
|
209
|
+
addEventListener: (type: string, listener: () => void) => void
|
|
210
|
+
removeEventListener: (type: string, listener: () => void) => void
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isWindowNavigationSource(
|
|
214
|
+
value: unknown,
|
|
215
|
+
): value is WindowNavigationSource {
|
|
216
|
+
if (typeof value !== "object" || value === null) return false
|
|
217
|
+
if (!("canGoBack" in value) || typeof value.canGoBack !== "boolean") {
|
|
218
|
+
return false
|
|
219
|
+
}
|
|
220
|
+
if (!("canGoForward" in value) || typeof value.canGoForward !== "boolean") {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
!("addEventListener" in value) ||
|
|
225
|
+
typeof value.addEventListener !== "function"
|
|
226
|
+
) {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
!("removeEventListener" in value) ||
|
|
231
|
+
typeof value.removeEventListener !== "function"
|
|
232
|
+
) {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readWindowNavigation(): WindowNavigationSource | undefined {
|
|
239
|
+
if (typeof window === "undefined") return undefined
|
|
240
|
+
const value = Reflect.get(window, "navigation")
|
|
241
|
+
return isWindowNavigationSource(value) ? value : undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function useWindowHistoryControls(): CaptionHistoryControls {
|
|
245
|
+
const [canGoBack, setCanGoBack] = useState(false)
|
|
246
|
+
const [canGoForward, setCanGoForward] = useState(false)
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
function sync() {
|
|
250
|
+
const source = readWindowNavigation()
|
|
251
|
+
if (source !== undefined) {
|
|
252
|
+
setCanGoBack(source.canGoBack)
|
|
253
|
+
setCanGoForward(source.canGoForward)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
setCanGoBack(window.history.length > 1)
|
|
257
|
+
setCanGoForward(false)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
sync()
|
|
261
|
+
const source = readWindowNavigation()
|
|
262
|
+
if (source !== undefined) {
|
|
263
|
+
source.addEventListener("currententrychange", sync)
|
|
264
|
+
return () => {
|
|
265
|
+
source.removeEventListener("currententrychange", sync)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
window.addEventListener("popstate", sync)
|
|
269
|
+
return () => {
|
|
270
|
+
window.removeEventListener("popstate", sync)
|
|
271
|
+
}
|
|
272
|
+
}, [])
|
|
273
|
+
|
|
274
|
+
// Plain browser reload. The app's own first frame (index.html splash)
|
|
275
|
+
// covers the pre-paint gap, so routing this through the desktop shell
|
|
276
|
+
// would only add unnecessary hops.
|
|
277
|
+
function reload() {
|
|
278
|
+
window.location.reload()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
canGoBack,
|
|
283
|
+
canGoForward,
|
|
284
|
+
back() {
|
|
285
|
+
window.history.back()
|
|
286
|
+
},
|
|
287
|
+
forward() {
|
|
288
|
+
window.history.forward()
|
|
289
|
+
},
|
|
290
|
+
reload,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ChevronLeftGlyph() {
|
|
295
|
+
return (
|
|
296
|
+
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
|
297
|
+
<path
|
|
298
|
+
d="M10 3.5L5 8l5 4.5"
|
|
299
|
+
fill="none"
|
|
300
|
+
stroke="currentColor"
|
|
301
|
+
strokeWidth="1.5"
|
|
302
|
+
/>
|
|
303
|
+
</svg>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function ChevronRightGlyph() {
|
|
308
|
+
return (
|
|
309
|
+
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
|
|
310
|
+
<path
|
|
311
|
+
d="M6 3.5L11 8l-5 4.5"
|
|
312
|
+
fill="none"
|
|
313
|
+
stroke="currentColor"
|
|
314
|
+
strokeWidth="1.5"
|
|
315
|
+
/>
|
|
316
|
+
</svg>
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function MinimizeGlyph() {
|
|
321
|
+
return (
|
|
322
|
+
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
323
|
+
<path d="M0 5h10" fill="none" stroke="currentColor" strokeWidth="1" />
|
|
324
|
+
</svg>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function DevToolsGlyph() {
|
|
329
|
+
return (
|
|
330
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
331
|
+
<path
|
|
332
|
+
d="M4.5 4.5L2 7l2.5 2.5M9.5 4.5L12 7l-2.5 2.5"
|
|
333
|
+
fill="none"
|
|
334
|
+
stroke="currentColor"
|
|
335
|
+
strokeWidth="1"
|
|
336
|
+
/>
|
|
337
|
+
</svg>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function MaximizeGlyph() {
|
|
342
|
+
return (
|
|
343
|
+
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
344
|
+
<rect
|
|
345
|
+
x="0.5"
|
|
346
|
+
y="0.5"
|
|
347
|
+
width="9"
|
|
348
|
+
height="9"
|
|
349
|
+
fill="none"
|
|
350
|
+
stroke="currentColor"
|
|
351
|
+
strokeWidth="1"
|
|
352
|
+
/>
|
|
353
|
+
</svg>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function RestoreGlyph() {
|
|
358
|
+
return (
|
|
359
|
+
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
360
|
+
<rect
|
|
361
|
+
x="2.5"
|
|
362
|
+
y="0.5"
|
|
363
|
+
width="7"
|
|
364
|
+
height="7"
|
|
365
|
+
fill="none"
|
|
366
|
+
stroke="currentColor"
|
|
367
|
+
strokeWidth="1"
|
|
368
|
+
/>
|
|
369
|
+
<path
|
|
370
|
+
d="M0.5 2.5h7v7h-7z"
|
|
371
|
+
fill="none"
|
|
372
|
+
stroke="currentColor"
|
|
373
|
+
strokeWidth="1"
|
|
374
|
+
/>
|
|
375
|
+
</svg>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function CloseGlyph() {
|
|
380
|
+
return (
|
|
381
|
+
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
382
|
+
<path
|
|
383
|
+
d="M1 1l8 8M9 1L1 9"
|
|
384
|
+
fill="none"
|
|
385
|
+
stroke="currentColor"
|
|
386
|
+
strokeWidth="1"
|
|
387
|
+
/>
|
|
388
|
+
</svg>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export { CaptionBar }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chart card — the stats page's standard chart surface: a floating card
|
|
5
|
+
* with the chart's name and a muted trailing note, the chart below.
|
|
6
|
+
*/
|
|
7
|
+
export function ChartCard({
|
|
8
|
+
title,
|
|
9
|
+
subtitle,
|
|
10
|
+
children,
|
|
11
|
+
className,
|
|
12
|
+
}: {
|
|
13
|
+
readonly title: string
|
|
14
|
+
readonly subtitle: string
|
|
15
|
+
readonly children: ReactNode
|
|
16
|
+
readonly className?: string
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={`rounded-xl border border-border bg-card p-5 shadow-card ${className ?? ""}`}
|
|
21
|
+
>
|
|
22
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
23
|
+
<span className="text-ui font-semibold text-foreground">{title}</span>
|
|
24
|
+
<span className="shrink-0 text-tiny text-muted-foreground">
|
|
25
|
+
{subtitle}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="mt-4">{children}</div>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
6
|
+
import { Check } from "@hoardodile/ui/icons/marks"
|
|
7
|
+
|
|
8
|
+
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
|
9
|
+
return (
|
|
10
|
+
<CheckboxPrimitive.Root
|
|
11
|
+
data-slot="checkbox"
|
|
12
|
+
className={cn(
|
|
13
|
+
"peer relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm bg-muted outline-none group-has-disabled/field:opacity-50 focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:bg-destructive/20 aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-checked:bg-primary data-checked:text-primary-foreground",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<CheckboxPrimitive.Indicator
|
|
19
|
+
data-slot="checkbox-indicator"
|
|
20
|
+
className="grid place-content-center text-current [&>svg]:size-3"
|
|
21
|
+
>
|
|
22
|
+
<Check />
|
|
23
|
+
</CheckboxPrimitive.Indicator>
|
|
24
|
+
</CheckboxPrimitive.Root>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Checkbox }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { IconType } from "@hoardodile/ui/components/icon"
|
|
2
|
+
import { Icon } from "@hoardodile/ui/components/icon"
|
|
3
|
+
import { cn } from "@hoardodile/ui/lib/utils"
|
|
4
|
+
import type { ReactNode } from "react"
|
|
5
|
+
|
|
6
|
+
export type ChipButtonProps = {
|
|
7
|
+
readonly icon?: IconType
|
|
8
|
+
/** Latched-toggle state — accent fill, no hover shift. */
|
|
9
|
+
readonly active?: boolean
|
|
10
|
+
/** The dashed outline (the Unused triage filter) instead of the quiet
|
|
11
|
+
text idle state. */
|
|
12
|
+
readonly dashed?: boolean
|
|
13
|
+
readonly disabled?: boolean
|
|
14
|
+
readonly children: ReactNode
|
|
15
|
+
readonly className?: string
|
|
16
|
+
readonly onClick?: () => void
|
|
17
|
+
readonly testId?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Chip button: the toolbar's compact toggle, h-chip height. Two kinds:
|
|
22
|
+
* the quiet toggle (Reorder) wears medium weight in both states so its
|
|
23
|
+
* width never moves; the dashed filter (Unused) is an indicator, not a
|
|
24
|
+
* toggle, so it stays regular weight with a dashed outline. `active`
|
|
25
|
+
* latches the accent fill on both; the dashed outline keeps a transparent
|
|
26
|
+
* border when active so the width never shifts.
|
|
27
|
+
*/
|
|
28
|
+
export function ChipButton(props: ChipButtonProps) {
|
|
29
|
+
const {
|
|
30
|
+
icon,
|
|
31
|
+
active,
|
|
32
|
+
dashed,
|
|
33
|
+
disabled,
|
|
34
|
+
children,
|
|
35
|
+
className,
|
|
36
|
+
onClick,
|
|
37
|
+
testId,
|
|
38
|
+
} = props
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={disabled ? undefined : onClick}
|
|
43
|
+
data-testid={testId}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
className={cn(
|
|
46
|
+
"inline-flex h-chip shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs whitespace-nowrap",
|
|
47
|
+
!dashed && "font-medium",
|
|
48
|
+
disabled ? "cursor-default" : "cursor-pointer",
|
|
49
|
+
active && "bg-accent text-foreground",
|
|
50
|
+
// The dashed outline keeps a 1px transparent border when
|
|
51
|
+
// active so the width never moves with the state.
|
|
52
|
+
active && dashed && "border border-transparent",
|
|
53
|
+
!active && disabled && "text-muted-foreground",
|
|
54
|
+
!active &&
|
|
55
|
+
!disabled &&
|
|
56
|
+
dashed &&
|
|
57
|
+
"border border-dashed border-border-strong bg-card text-muted-foreground hover:text-secondary-foreground",
|
|
58
|
+
!active &&
|
|
59
|
+
!disabled &&
|
|
60
|
+
!dashed &&
|
|
61
|
+
"text-secondary-foreground hover:text-foreground",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{icon !== undefined ? <Icon icon={icon} /> : null}
|
|
66
|
+
{children}
|
|
67
|
+
</button>
|
|
68
|
+
)
|
|
69
|
+
}
|