@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,69 @@
|
|
|
1
|
+
import { BoltIcon as BoltBoldWeight } from "@solar-icons/react/bold/bolt"
|
|
2
|
+
import { BoltIcon as BoltBoldDuotone } from "@solar-icons/react/bold-duotone/bolt"
|
|
3
|
+
import { BoltIcon as BoltLinear } from "@solar-icons/react/linear/bolt"
|
|
4
|
+
import { act, render, waitFor } from "@testing-library/react"
|
|
5
|
+
import { beforeEach, describe, expect, it } from "vitest"
|
|
6
|
+
import { createIcon } from "./icon-style"
|
|
7
|
+
|
|
8
|
+
const Bolt = createIcon({
|
|
9
|
+
bold: BoltBoldWeight,
|
|
10
|
+
boldDuotone: BoltBoldDuotone,
|
|
11
|
+
linear: BoltLinear,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function svgClass(container: HTMLElement): string | null {
|
|
15
|
+
return container.querySelector("svg")?.getAttribute("class") ?? null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setIconStyle(style: "duotone" | "grayscale" | "linear" | undefined) {
|
|
19
|
+
act(() => {
|
|
20
|
+
if (style === undefined) {
|
|
21
|
+
delete document.documentElement.dataset.iconStyle
|
|
22
|
+
} else {
|
|
23
|
+
document.documentElement.dataset.iconStyle = style
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
delete document.documentElement.dataset.iconStyle
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("createIcon", () => {
|
|
33
|
+
it("renders the boldDuotone glyph by default with the hd-icon hook", () => {
|
|
34
|
+
const { container } = render(<Bolt className="size-4" />)
|
|
35
|
+
expect(svgClass(container)).toContain("solar-bolt-bold-duotone")
|
|
36
|
+
expect(svgClass(container)).toContain("hd-icon")
|
|
37
|
+
expect(svgClass(container)).toContain("size-4")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("swaps to the linear glyph in linear style", async () => {
|
|
41
|
+
const { container } = render(<Bolt />)
|
|
42
|
+
setIconStyle("linear")
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(svgClass(container)).toContain("solar-bolt-linear")
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("restores the boldDuotone glyph when the style leaves linear", async () => {
|
|
49
|
+
const { container } = render(<Bolt />)
|
|
50
|
+
setIconStyle("linear")
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(svgClass(container)).toContain("solar-bolt-linear")
|
|
53
|
+
})
|
|
54
|
+
setIconStyle("grayscale")
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(svgClass(container)).toContain("solar-bolt-bold-duotone")
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("renders the bold glyph when the mode prop is set", () => {
|
|
61
|
+
const { container } = render(<Bolt mode="bold" />)
|
|
62
|
+
expect(svgClass(container)).toContain("solar-bolt-bold")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("renders each mode explicitly", () => {
|
|
66
|
+
const { container } = render(<Bolt mode="linear" />)
|
|
67
|
+
expect(svgClass(container)).toContain("solar-bolt-linear")
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type ComponentType, createElement, useSyncExternalStore } from "react"
|
|
2
|
+
import type { IconType } from "../components/icon.tsx"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Icon-mode-aware glyph wrapper (Settings → Icons in the host app).
|
|
6
|
+
*
|
|
7
|
+
* The active icon style lives as `data-icon-style` on `document.documentElement`
|
|
8
|
+
* — the host app writes it from its preference store, plugin iframes receive
|
|
9
|
+
* it through the `themeChanged` push — so this module needs no context or
|
|
10
|
+
* injected state: a single shared `MutationObserver` watches the attribute
|
|
11
|
+
* and every wrapped component re-renders through `useSyncExternalStore`
|
|
12
|
+
* when it flips.
|
|
13
|
+
*
|
|
14
|
+
* Only `linear` needs a component swap: the boldDuotone glyphs change to
|
|
15
|
+
* their thin-line linear counterparts. `grayscale` is pure CSS (theme.css
|
|
16
|
+
* recolors the `.hd-icon` hook), so the store only tracks the mode.
|
|
17
|
+
*/
|
|
18
|
+
export type IconStyleValue = "duotone" | "grayscale" | "linear"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The parallel Solar weights an icon registers — named after Solar's own
|
|
22
|
+
* weights (DESIGN.md — Iconography). Every icon carries all of them;
|
|
23
|
+
* adding a mode here forces every registry entry to fill it in.
|
|
24
|
+
*/
|
|
25
|
+
export type IconMode = "bold" | "boldDuotone" | "linear"
|
|
26
|
+
|
|
27
|
+
/** All three mode glyphs of one icon. Mandatory and complete by type. */
|
|
28
|
+
export type IconVariants = Record<IconMode, IconType>
|
|
29
|
+
|
|
30
|
+
function readIconMode(): IconMode {
|
|
31
|
+
return document.documentElement.dataset.iconStyle === "linear"
|
|
32
|
+
? "linear"
|
|
33
|
+
: "boldDuotone"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Never read `document` at module load — pure-logic consumers (node test
|
|
37
|
+
// environments) import this module without a DOM. The first subscribe
|
|
38
|
+
// (component mount) snapshots the current value.
|
|
39
|
+
let currentMode: IconMode = "boldDuotone"
|
|
40
|
+
let observer: MutationObserver | undefined
|
|
41
|
+
const listeners = new Set<() => void>()
|
|
42
|
+
|
|
43
|
+
function ensureObserver(): void {
|
|
44
|
+
if (observer !== undefined || typeof document === "undefined") return
|
|
45
|
+
observer = new MutationObserver(() => {
|
|
46
|
+
const next = readIconMode()
|
|
47
|
+
if (next === currentMode) return
|
|
48
|
+
currentMode = next
|
|
49
|
+
for (const cb of listeners) cb()
|
|
50
|
+
})
|
|
51
|
+
observer.observe(document.documentElement, {
|
|
52
|
+
attributes: true,
|
|
53
|
+
attributeFilter: ["data-icon-style"],
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function subscribeIconStyle(callback: () => void): () => void {
|
|
58
|
+
ensureObserver()
|
|
59
|
+
currentMode = readIconMode()
|
|
60
|
+
listeners.add(callback)
|
|
61
|
+
return () => {
|
|
62
|
+
listeners.delete(callback)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getIconStyleSnapshot(): IconMode {
|
|
67
|
+
return currentMode
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wraps a Solar glyph family so its render follows the host's icon style.
|
|
72
|
+
*
|
|
73
|
+
* `createIcon({ bold, boldDuotone, linear })` — the three parallel weights
|
|
74
|
+
* of one icon. Without a `mode` prop the wrapper renders the glyph for the
|
|
75
|
+
* active icon style; an explicit `mode` overrides it (selected states pass
|
|
76
|
+
* `"bold"` through `Icon`).
|
|
77
|
+
*
|
|
78
|
+
* Every wrapped render merges the `hd-icon` hook class, so the duotone /
|
|
79
|
+
* grayscale CSS in theme.css applies to direct renders too. Returns
|
|
80
|
+
* `undefined`-free components callable through the `Icon` wrapper or bare
|
|
81
|
+
* JSX alike.
|
|
82
|
+
*/
|
|
83
|
+
export function createIcon(
|
|
84
|
+
variants: IconVariants,
|
|
85
|
+
): ComponentType<Record<string, unknown>> {
|
|
86
|
+
function Icon(props: Record<string, unknown>) {
|
|
87
|
+
const current = useSyncExternalStore(
|
|
88
|
+
subscribeIconStyle,
|
|
89
|
+
getIconStyleSnapshot,
|
|
90
|
+
() => "boldDuotone",
|
|
91
|
+
)
|
|
92
|
+
const { mode, ...rest } = props as { readonly mode?: IconMode } & Record<
|
|
93
|
+
string,
|
|
94
|
+
unknown
|
|
95
|
+
>
|
|
96
|
+
const Component = variants[(mode ?? current) as IconMode]
|
|
97
|
+
const className =
|
|
98
|
+
typeof rest.className === "string"
|
|
99
|
+
? `hd-icon ${rest.className}`
|
|
100
|
+
: "hd-icon"
|
|
101
|
+
return createElement(Component, { ...rest, className })
|
|
102
|
+
}
|
|
103
|
+
return Icon
|
|
104
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local utility marks — the three plain glyphs Solar's vocabulary lacks
|
|
3
|
+
* (its ✓/×/+ only exist inside Circle/Square containers, whose marks occupy
|
|
4
|
+
* barely a third of the box and go muddy below 16px). These are drawn
|
|
5
|
+
* in-house for the spots where a bare mark is the whole meaning: checkbox
|
|
6
|
+
* ticks, selected badges, pill clears, add affordances.
|
|
7
|
+
*
|
|
8
|
+
* Check and Cross are strokes in Solar's language (24 viewBox, round caps)
|
|
9
|
+
* at 2 units — diagonals anti-alias gracefully at any tier. Plus is a
|
|
10
|
+
* filled cross instead: its pure orthogonal edges land on integer pixels
|
|
11
|
+
* at the md tier (3-unit arms render as a crisp 2px bar at 16px), where a
|
|
12
|
+
* 1.33px stroke would straddle pixel boundaries and blur. Weight-insensitive:
|
|
13
|
+
* no tone layer, no weight variants. Original geometry — not derived
|
|
14
|
+
* from Solar assets.
|
|
15
|
+
*
|
|
16
|
+
* Marks ride the same `mode` channel as registry icons (declared, ignored):
|
|
17
|
+
* a check is a check in every weight.
|
|
18
|
+
*/
|
|
19
|
+
import type { IconMode } from "./icon-style.ts"
|
|
20
|
+
|
|
21
|
+
type MarkProps = {
|
|
22
|
+
className?: string
|
|
23
|
+
mode?: IconMode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Mark({
|
|
27
|
+
d,
|
|
28
|
+
filled,
|
|
29
|
+
className,
|
|
30
|
+
}: MarkProps & { d: string; filled?: boolean }) {
|
|
31
|
+
return (
|
|
32
|
+
<svg
|
|
33
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
fill="none"
|
|
36
|
+
className={className}
|
|
37
|
+
>
|
|
38
|
+
{filled ? (
|
|
39
|
+
<path d={d} fill="currentColor" />
|
|
40
|
+
) : (
|
|
41
|
+
<path
|
|
42
|
+
d={d}
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="2"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
</svg>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Plain ✓ — checkbox ticks, selected badges. */
|
|
54
|
+
export function Check(props: MarkProps) {
|
|
55
|
+
return <Mark {...props} d="M5 12.8l4.7 4.7L19 7.2" />
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Plain × — pill and chip clears. */
|
|
59
|
+
export function Cross(props: MarkProps) {
|
|
60
|
+
return <Mark {...props} d="M6 6l12 12M18 6L6 18" />
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Plain + — add affordances. Filled cross, 3-unit arms spanning 5–19. */
|
|
64
|
+
export function Plus(props: MarkProps) {
|
|
65
|
+
return (
|
|
66
|
+
<Mark
|
|
67
|
+
{...props}
|
|
68
|
+
filled
|
|
69
|
+
d="M10.5 5h3v5.5H19v3h-5.5V19h-3v-5.5H5v-3h5.5V5Z"
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import * as icons from "./registry"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Source-structure guard for the icon registry. The file is excluded from
|
|
8
|
+
* biome (packages/ui/src/icons), so this test is the only thing that
|
|
9
|
+
* keeps the single export section ordered and every icon's three parallel
|
|
10
|
+
* Solar weights registered — see the header comment in registry.ts for the
|
|
11
|
+
* conventions.
|
|
12
|
+
*/
|
|
13
|
+
const src = readFileSync(new URL("./registry.ts", import.meta.url), "utf8")
|
|
14
|
+
const lines = src.split(/\r?\n/)
|
|
15
|
+
|
|
16
|
+
/** One parsed registry entry: the export name. */
|
|
17
|
+
function parseIcons(): string[] {
|
|
18
|
+
const names: string[] = []
|
|
19
|
+
for (let i = 0; i < lines.length; ) {
|
|
20
|
+
const m = /^export const (\w+) = createIcon\(\{$/.exec(lines[i]!)
|
|
21
|
+
if (m === null) {
|
|
22
|
+
i++
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
const name = m[1]!
|
|
26
|
+
expect(lines[i + 1]).toBe(`\tbold: ${name}BoldWeight,`)
|
|
27
|
+
expect(lines[i + 2]).toBe(`\tboldDuotone: ${name}BoldDuotone,`)
|
|
28
|
+
// MenuDots is the one documented exception: its thin-line dots read
|
|
29
|
+
// as noise at small sizes, so the filled weight stays in every mode.
|
|
30
|
+
expect(lines[i + 3]).toBe(
|
|
31
|
+
name === "MenuDots"
|
|
32
|
+
? "\tlinear: MenuDotsBoldWeight,"
|
|
33
|
+
: `\tlinear: ${name}Linear,`,
|
|
34
|
+
)
|
|
35
|
+
expect(lines[i + 4]).toBe("})")
|
|
36
|
+
names.push(name)
|
|
37
|
+
i += 5
|
|
38
|
+
}
|
|
39
|
+
return names
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const iconNames = parseIcons()
|
|
43
|
+
|
|
44
|
+
const ordinal = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
|
|
45
|
+
|
|
46
|
+
describe("icons registry structure", () => {
|
|
47
|
+
it("keeps the export section alphabetically ordered (ordinal)", () => {
|
|
48
|
+
expect([...iconNames].sort(ordinal)).toEqual(iconNames)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("registers every icon with all three Solar weights", () => {
|
|
52
|
+
for (const name of iconNames) {
|
|
53
|
+
// biome-ignore lint/performance/noDynamicNamespaceImportAccess: the registry is a data module — accessing entries by parsed name is the point of the guard.
|
|
54
|
+
const icon = icons[name as keyof typeof icons] as (
|
|
55
|
+
props: Record<string, unknown>,
|
|
56
|
+
) => unknown
|
|
57
|
+
expect(typeof icon).toBe("function")
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("exports exactly the names declared in the file", () => {
|
|
62
|
+
expect(new Set(iconNames)).toEqual(new Set(Object.keys(icons)))
|
|
63
|
+
expect(iconNames).toHaveLength(Object.keys(icons).length)
|
|
64
|
+
})
|
|
65
|
+
})
|