@vitavision/ui 0.6.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-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +152 -0
- package/dist/index.d.ts +883 -0
- package/dist/index.js +1348 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
- package/src/components/Tabs.tsx +137 -0
- package/src/components/ThemeToggle.tsx +95 -0
- package/src/components/ui/Badge.tsx +125 -0
- package/src/components/ui/Button.tsx +188 -0
- package/src/components/ui/Density.tsx +66 -0
- package/src/components/ui/Dialog.tsx +187 -0
- package/src/components/ui/Disclosure.tsx +62 -0
- package/src/components/ui/Feedback.tsx +190 -0
- package/src/components/ui/Field.tsx +149 -0
- package/src/components/ui/Input.tsx +100 -0
- package/src/components/ui/Panel.tsx +248 -0
- package/src/components/ui/SegmentedControl.tsx +112 -0
- package/src/components/ui/Select.tsx +168 -0
- package/src/components/ui/Slider.tsx +96 -0
- package/src/components/ui/Table.tsx +162 -0
- package/src/components/ui/Toggle.tsx +163 -0
- package/src/components/ui/ToggleChip.tsx +81 -0
- package/src/components/ui/Tooltip.tsx +99 -0
- package/src/components/ui/cn.ts +41 -0
- package/src/components/ui/index.ts +47 -0
- package/src/index.ts +25 -0
- package/src/styles.css +170 -0
- package/src/theme.ts +113 -0
- package/src/tone.ts +42 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The two binary controls, and the difference between them.
|
|
3
|
+
*
|
|
4
|
+
* A `Switch` changes how the thing behaves from now on -- "allow downloads", "show the
|
|
5
|
+
* mask", "lock the caliper axis" -- and reads as a setting. A `Checkbox` asserts something
|
|
6
|
+
* about a state you are about to commit -- "I have read the warnings", "keep this channel"
|
|
7
|
+
* -- and reads as a statement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as RadixCheckbox from "@radix-ui/react-checkbox";
|
|
11
|
+
import * as RadixSwitch from "@radix-ui/react-switch";
|
|
12
|
+
import { Check, Minus } from "lucide-react";
|
|
13
|
+
import { useId, type MouseEvent, type ReactNode } from "react";
|
|
14
|
+
|
|
15
|
+
import { useFieldDescription } from "./Field";
|
|
16
|
+
import { cn, focusRing } from "./cn";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A setting: changes how the thing behaves from now on, and reads as one ("show the mask").
|
|
20
|
+
* Always labelled; the label and the description sit beside the switch.
|
|
21
|
+
*
|
|
22
|
+
* Controlled. Radix exposes `data-state` (`checked`/`unchecked`) and `data-disabled`.
|
|
23
|
+
*/
|
|
24
|
+
export function Switch({
|
|
25
|
+
checked,
|
|
26
|
+
onCheckedChange,
|
|
27
|
+
label,
|
|
28
|
+
description,
|
|
29
|
+
disabled = false,
|
|
30
|
+
className,
|
|
31
|
+
}: {
|
|
32
|
+
/** Whether it is on. */
|
|
33
|
+
checked: boolean;
|
|
34
|
+
/** Called with the new state. */
|
|
35
|
+
onCheckedChange: (checked: boolean) => void;
|
|
36
|
+
/** What the setting is. */
|
|
37
|
+
label: ReactNode;
|
|
38
|
+
/** What turning it on does. */
|
|
39
|
+
description?: ReactNode;
|
|
40
|
+
/** Blocks the switch. */
|
|
41
|
+
disabled?: boolean | undefined;
|
|
42
|
+
/** Merged with the row's own classes through `cn`. */
|
|
43
|
+
className?: string | undefined;
|
|
44
|
+
}) {
|
|
45
|
+
const descriptionId = useId();
|
|
46
|
+
return (
|
|
47
|
+
<label className={cn("flex min-w-0 items-start gap-2.5", className)}>
|
|
48
|
+
<RadixSwitch.Root
|
|
49
|
+
checked={checked}
|
|
50
|
+
onCheckedChange={onCheckedChange}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
53
|
+
className={cn(
|
|
54
|
+
"mt-0.5 h-4 w-7 shrink-0 rounded-full border border-line-strong bg-raised transition-colors",
|
|
55
|
+
"data-[state=checked]:border-signal data-[state=checked]:bg-signal",
|
|
56
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
57
|
+
focusRing,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
<RadixSwitch.Thumb
|
|
61
|
+
className={cn(
|
|
62
|
+
"block size-3 rounded-full bg-fg-subtle transition-transform will-change-transform",
|
|
63
|
+
"translate-x-0.5 data-[state=checked]:translate-x-3.5 data-[state=checked]:bg-signal-fg",
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
</RadixSwitch.Root>
|
|
67
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
68
|
+
<span className="text-sm leading-tight text-fg">{label}</span>
|
|
69
|
+
{description && (
|
|
70
|
+
<span id={descriptionId} className="text-xs leading-snug text-fg-muted">
|
|
71
|
+
{description}
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</span>
|
|
75
|
+
</label>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A statement about a state you are about to commit ("keep this channel"). With a `label`
|
|
81
|
+
* it renders its own row; without one it is a bare box (give it `aria-label`, e.g. in a
|
|
82
|
+
* table cell).
|
|
83
|
+
*
|
|
84
|
+
* Controlled. Radix exposes `data-state` (`checked`/`unchecked`/`indeterminate`) and
|
|
85
|
+
* `data-disabled`; inside a `Field` it is described by the field's description and error.
|
|
86
|
+
*/
|
|
87
|
+
export function Checkbox({
|
|
88
|
+
checked,
|
|
89
|
+
onCheckedChange,
|
|
90
|
+
onClick,
|
|
91
|
+
label,
|
|
92
|
+
description,
|
|
93
|
+
disabled = false,
|
|
94
|
+
"aria-label": ariaLabel,
|
|
95
|
+
className,
|
|
96
|
+
}: {
|
|
97
|
+
/** `"indeterminate"` for a partial selection over a set. */
|
|
98
|
+
checked: boolean | "indeterminate";
|
|
99
|
+
/** Called with the new state (never `"indeterminate"`: a click resolves it). */
|
|
100
|
+
onCheckedChange: (checked: boolean) => void;
|
|
101
|
+
/**
|
|
102
|
+
* For a checkbox whose meaning depends on the modifier keys — extending a range with
|
|
103
|
+
* shift, toggling one out of a selection with the platform key. `onCheckedChange` gets a
|
|
104
|
+
* boolean and cannot carry them. Keyboard activation synthesises a click with its
|
|
105
|
+
* modifiers intact, so this sees both input paths.
|
|
106
|
+
*/
|
|
107
|
+
onClick?: ((event: MouseEvent<HTMLButtonElement>) => void) | undefined;
|
|
108
|
+
/** What checking it asserts. Omit for a bare box. */
|
|
109
|
+
label?: ReactNode;
|
|
110
|
+
/** More about the statement, under the label. */
|
|
111
|
+
description?: ReactNode;
|
|
112
|
+
/** Blocks the box. */
|
|
113
|
+
disabled?: boolean | undefined;
|
|
114
|
+
/** For a checkbox in a table cell, where the row is the label. */
|
|
115
|
+
"aria-label"?: string | undefined;
|
|
116
|
+
/** Merged with the row's (or, without a label, the box's) own classes through `cn`. */
|
|
117
|
+
className?: string | undefined;
|
|
118
|
+
}) {
|
|
119
|
+
const descriptionId = useId();
|
|
120
|
+
const described = useFieldDescription(description ? descriptionId : undefined);
|
|
121
|
+
const box = (
|
|
122
|
+
<RadixCheckbox.Root
|
|
123
|
+
checked={checked}
|
|
124
|
+
onCheckedChange={(next) => onCheckedChange(next === true)}
|
|
125
|
+
onClick={onClick}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
aria-label={ariaLabel}
|
|
128
|
+
{...described}
|
|
129
|
+
className={cn(
|
|
130
|
+
"flex size-4 shrink-0 items-center justify-center rounded-[0.25rem] border border-line-strong bg-raised transition-colors",
|
|
131
|
+
"data-[state=checked]:border-signal data-[state=checked]:bg-signal",
|
|
132
|
+
"data-[state=indeterminate]:border-signal data-[state=indeterminate]:bg-signal",
|
|
133
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
134
|
+
focusRing,
|
|
135
|
+
label === undefined && className,
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
<RadixCheckbox.Indicator className="text-signal-fg">
|
|
139
|
+
{checked === "indeterminate" ? (
|
|
140
|
+
<Minus className="size-3" strokeWidth={3} />
|
|
141
|
+
) : (
|
|
142
|
+
<Check className="size-3" strokeWidth={3} />
|
|
143
|
+
)}
|
|
144
|
+
</RadixCheckbox.Indicator>
|
|
145
|
+
</RadixCheckbox.Root>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (label === undefined) return box;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<label className={cn("flex min-w-0 items-start gap-2.5", className)}>
|
|
152
|
+
<span className="mt-0.5">{box}</span>
|
|
153
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
154
|
+
<span className="text-sm leading-tight text-fg">{label}</span>
|
|
155
|
+
{description && (
|
|
156
|
+
<span id={descriptionId} className="text-xs leading-snug text-fg-muted">
|
|
157
|
+
{description}
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
</span>
|
|
161
|
+
</label>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A dense on/off control for a thing that is drawn on screen.
|
|
3
|
+
*
|
|
4
|
+
* `Switch` is a settings row — a stacked label, a description, generous padding — and
|
|
5
|
+
* three of them side by side over an image canvas is a panel, not a toolbar. This is the
|
|
6
|
+
* same binary decision in the space a toolbar has: one line, one target, and an optional
|
|
7
|
+
* swatch carrying the colour the layer is actually drawn in — an overlay's caliper boxes,
|
|
8
|
+
* a chart's series — so the legend and the control are the same object rather than two
|
|
9
|
+
* things to reconcile.
|
|
10
|
+
*
|
|
11
|
+
* Hand-built on a native `button` with `role="switch"`: this is not one of the four Radix
|
|
12
|
+
* primitives the rest of the toggle vocabulary is built on, and a chip does not need one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
|
|
17
|
+
import { cn, focusRing } from "./cn";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A dense on/off chip for a layer drawn on screen, with an optional swatch in the layer's
|
|
21
|
+
* colour: a toolbar-sized `Switch` (`role="switch"`, `aria-checked`).
|
|
22
|
+
*
|
|
23
|
+
* Controlled. State is exposed as `data-state` (`checked`/`unchecked`).
|
|
24
|
+
*/
|
|
25
|
+
export function ToggleChip({
|
|
26
|
+
checked,
|
|
27
|
+
onCheckedChange,
|
|
28
|
+
children,
|
|
29
|
+
swatch,
|
|
30
|
+
disabled = false,
|
|
31
|
+
title,
|
|
32
|
+
className,
|
|
33
|
+
}: {
|
|
34
|
+
/** Whether the layer is shown. */
|
|
35
|
+
checked: boolean;
|
|
36
|
+
/** Called with the new state. */
|
|
37
|
+
onCheckedChange: (checked: boolean) => void;
|
|
38
|
+
/** The layer's name. */
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
/** A CSS colour: the colour this layer is drawn in, shown as a dot. */
|
|
41
|
+
swatch?: string;
|
|
42
|
+
/** Blocks the chip. */
|
|
43
|
+
disabled?: boolean | undefined;
|
|
44
|
+
/** Why the control is disabled, or what the layer is. Never the only explanation. */
|
|
45
|
+
title?: string | undefined;
|
|
46
|
+
/** Merged with the chip's own classes through `cn`. */
|
|
47
|
+
className?: string | undefined;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
role="switch"
|
|
53
|
+
aria-checked={checked}
|
|
54
|
+
data-state={checked ? "checked" : "unchecked"}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
title={title}
|
|
57
|
+
onClick={() => onCheckedChange(!checked)}
|
|
58
|
+
className={cn(
|
|
59
|
+
"inline-flex items-center gap-1.5 rounded-control border px-2 py-1 text-xs whitespace-nowrap transition-colors",
|
|
60
|
+
checked
|
|
61
|
+
? "border-line-strong bg-raised text-fg"
|
|
62
|
+
: "border-line bg-transparent text-fg-muted hover:text-fg",
|
|
63
|
+
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-fg-muted",
|
|
64
|
+
focusRing,
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{swatch !== undefined && (
|
|
69
|
+
<span
|
|
70
|
+
aria-hidden
|
|
71
|
+
className={cn(
|
|
72
|
+
"size-2 shrink-0 rounded-full border transition-opacity",
|
|
73
|
+
checked ? "opacity-100" : "opacity-30",
|
|
74
|
+
)}
|
|
75
|
+
style={{ backgroundColor: swatch, borderColor: swatch }}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
{children}
|
|
79
|
+
</button>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A short explanation, on demand.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the native `title` attribute, which appears after an unpredictable delay, in the
|
|
5
|
+
* OS palette, at a size nobody chose, and never at all for a keyboard user.
|
|
6
|
+
*
|
|
7
|
+
* A tooltip is for text that helps but is not needed to operate the control. Anything a
|
|
8
|
+
* reader must have in order to answer the question belongs in the field's description,
|
|
9
|
+
* where it is always visible.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as RadixTooltip from "@radix-ui/react-tooltip";
|
|
13
|
+
import { HelpCircle, type LucideIcon } from "lucide-react";
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
|
|
16
|
+
import { cn, focusRing } from "./cn";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Shares one open delay (200 ms) across the tooltips below it. Mount it once near the root:
|
|
20
|
+
* `Tooltip` and `InfoHint` need it above them.
|
|
21
|
+
*/
|
|
22
|
+
export function TooltipProvider({
|
|
23
|
+
children,
|
|
24
|
+
}: {
|
|
25
|
+
/** The app, or the region that uses tooltips. */
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}) {
|
|
28
|
+
return <RadixTooltip.Provider delayDuration={200}>{children}</RadixTooltip.Provider>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A short explanation on hover or focus of its child, for text that helps but is not needed
|
|
33
|
+
* to operate the control (that belongs in a field's description).
|
|
34
|
+
*
|
|
35
|
+
* The child must be one focusable element that accepts a ref (Radix `asChild`). Radix
|
|
36
|
+
* exposes `data-state` (`closed`/`delayed-open`/`instant-open`) on it.
|
|
37
|
+
*/
|
|
38
|
+
export function Tooltip({
|
|
39
|
+
content,
|
|
40
|
+
children,
|
|
41
|
+
}: {
|
|
42
|
+
/** The explanation. */
|
|
43
|
+
content: ReactNode;
|
|
44
|
+
/** The one element it explains. */
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<RadixTooltip.Root>
|
|
49
|
+
<RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>
|
|
50
|
+
<RadixTooltip.Portal>
|
|
51
|
+
<RadixTooltip.Content
|
|
52
|
+
sideOffset={6}
|
|
53
|
+
collisionPadding={8}
|
|
54
|
+
className={cn(
|
|
55
|
+
"z-50 max-w-xs rounded-control border border-line bg-overlay px-2.5 py-1.5",
|
|
56
|
+
"text-xs leading-snug text-fg shadow-lg shadow-black/25",
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{content}
|
|
60
|
+
<RadixTooltip.Arrow className="fill-overlay" />
|
|
61
|
+
</RadixTooltip.Content>
|
|
62
|
+
</RadixTooltip.Portal>
|
|
63
|
+
</RadixTooltip.Root>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The standard affordance: a quiet mark that yields the explanation on hover or focus.
|
|
69
|
+
*
|
|
70
|
+
* `icon` exists because the mark answers two different questions. The default `?` offers
|
|
71
|
+
* help with a control; `Info` offers *facts about the thing on screen* -- where a dataset
|
|
72
|
+
* came from, which method produced a measurement -- which the tone demotes here rather
|
|
73
|
+
* than spending a permanent line on. Reading them as the same affordance would be a small
|
|
74
|
+
* lie.
|
|
75
|
+
*/
|
|
76
|
+
export function InfoHint({
|
|
77
|
+
children,
|
|
78
|
+
label = "More information",
|
|
79
|
+
icon: Icon = HelpCircle,
|
|
80
|
+
}: {
|
|
81
|
+
/** The explanation shown on hover or focus. */
|
|
82
|
+
children: ReactNode;
|
|
83
|
+
/** The button's accessible name. Defaults to "More information". */
|
|
84
|
+
label?: string | undefined;
|
|
85
|
+
/** The mark: `HelpCircle` (the default) for help, `Info` for facts about the thing. */
|
|
86
|
+
icon?: LucideIcon | undefined;
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<Tooltip content={children}>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
aria-label={label}
|
|
93
|
+
className={cn("rounded-full text-fg-subtle transition-colors hover:text-fg", focusRing)}
|
|
94
|
+
>
|
|
95
|
+
<Icon className="size-3.5" />
|
|
96
|
+
</button>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class composition, and the shared focus treatment.
|
|
3
|
+
*
|
|
4
|
+
* A naive primitive *appends* the caller's `className` to its own base string, so
|
|
5
|
+
* `<Button className="px-6">` produces `px-3 … px-6` and wins or loses on source order
|
|
6
|
+
* rather than on intent. `twMerge` resolves that by conflict group: the caller's `px-6`
|
|
7
|
+
* replaces the base `px-3` and leaves everything else alone.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { clsx, type ClassValue } from "clsx";
|
|
11
|
+
import { twMerge } from "tailwind-merge";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compose class names: `clsx` for the conditionals, then `tailwind-merge` so a later class
|
|
15
|
+
* replaces an earlier one of the same conflict group (the caller's `px-6` beats the base
|
|
16
|
+
* `px-3`). Every primitive merges its caller's `className` through this.
|
|
17
|
+
*
|
|
18
|
+
* @param inputs - Class values: strings, arrays, and `{ class: condition }` records.
|
|
19
|
+
* @returns One class string with conflicts resolved.
|
|
20
|
+
*/
|
|
21
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
22
|
+
return twMerge(clsx(inputs));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One focus treatment for every interactive element in the design system.
|
|
27
|
+
*
|
|
28
|
+
* `outline` rather than `ring` because an outline follows the element's own border-radius
|
|
29
|
+
* and needs no offset *colour* -- a ring offset has to know which surface it sits on, and
|
|
30
|
+
* these primitives are used on `ground`, `surface` and `raised` alike.
|
|
31
|
+
*
|
|
32
|
+
* `focus-visible` rather than `focus`, so a mouse click does not leave a ring behind.
|
|
33
|
+
*/
|
|
34
|
+
export const focusRing =
|
|
35
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The focus treatment for controls whose own box is the hit target, where an offset ring
|
|
39
|
+
* would clip: the same outline as {@link focusRing}, drawn with no offset.
|
|
40
|
+
*/
|
|
41
|
+
export const focusRingInset = "focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-signal";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The primitive set.
|
|
3
|
+
*
|
|
4
|
+
* Import from `components/ui` (or the package root); the individual modules are an
|
|
5
|
+
* implementation detail.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { cn, focusRing, focusRingInset } from "./cn";
|
|
9
|
+
|
|
10
|
+
export { Badge, CountRun, StatusDot, type Tone } from "./Badge";
|
|
11
|
+
export { Button, ButtonLink, buttonClasses, type ButtonSize, type ButtonVariant } from "./Button";
|
|
12
|
+
export { DensityProvider, byDensity, useDensity, type Density } from "./Density";
|
|
13
|
+
export { ConfirmDialog, Dialog, DialogClose } from "./Dialog";
|
|
14
|
+
export { Disclosure } from "./Disclosure";
|
|
15
|
+
export {
|
|
16
|
+
Callout,
|
|
17
|
+
type CalloutTone,
|
|
18
|
+
Empty,
|
|
19
|
+
ErrorBox,
|
|
20
|
+
ProgressBar,
|
|
21
|
+
Skeleton,
|
|
22
|
+
SkeletonRows,
|
|
23
|
+
} from "./Feedback";
|
|
24
|
+
export { Field } from "./Field";
|
|
25
|
+
export {
|
|
26
|
+
Input,
|
|
27
|
+
NumberInput,
|
|
28
|
+
Textarea,
|
|
29
|
+
controlClasses,
|
|
30
|
+
inputClasses,
|
|
31
|
+
useControlHeight,
|
|
32
|
+
} from "./Input";
|
|
33
|
+
export {
|
|
34
|
+
PageHeader,
|
|
35
|
+
type BackLink,
|
|
36
|
+
Panel,
|
|
37
|
+
ReadoutStrip,
|
|
38
|
+
Section,
|
|
39
|
+
type ReadoutItem,
|
|
40
|
+
} from "./Panel";
|
|
41
|
+
export { SegmentedControl } from "./SegmentedControl";
|
|
42
|
+
export { Select, type SelectOption } from "./Select";
|
|
43
|
+
export { Slider } from "./Slider";
|
|
44
|
+
export { Table, type Column } from "./Table";
|
|
45
|
+
export { Checkbox, Switch } from "./Toggle";
|
|
46
|
+
export { ToggleChip } from "./ToggleChip";
|
|
47
|
+
export { InfoHint, Tooltip, TooltipProvider } from "./Tooltip";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@vitavision/ui` — the instrument design system: tokens, theme, and the primitive set.
|
|
3
|
+
*
|
|
4
|
+
* Import `@vitavision/ui/styles.css` once, from the CSS entry that already has
|
|
5
|
+
* `@import "tailwindcss";` — see the README for the wiring.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_THEME_STORAGE_KEY,
|
|
12
|
+
initTheme,
|
|
13
|
+
readThemeChoice,
|
|
14
|
+
resolveTheme,
|
|
15
|
+
setThemeChoice,
|
|
16
|
+
type ThemeChoice,
|
|
17
|
+
} from "./theme";
|
|
18
|
+
|
|
19
|
+
export { toneColor, type MeasureTone } from "./tone";
|
|
20
|
+
|
|
21
|
+
export { ThemeToggle } from "./components/ThemeToggle";
|
|
22
|
+
|
|
23
|
+
export * from "./components/ui";
|
|
24
|
+
|
|
25
|
+
export { Tabs, type TabItem } from "./components/Tabs";
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* This package's own classes: Tailwind resolves `@source` against the stylesheet that declares
|
|
4
|
+
it, and this file is exported from `src/` (which ships), so it scans the package's sources. */
|
|
5
|
+
@source "./";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* The design token layer.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from visual-anomaly-lab's own token layer (its ADR-0021), where it was written
|
|
11
|
+
* to close a real gap: eleven screens with every colour inlined as a raw `slate-*` utility
|
|
12
|
+
* at 250-odd call sites — the same palette restated 250 times rather than defined once.
|
|
13
|
+
* This file is that definition, shared now by every lab app rather than owned by one.
|
|
14
|
+
*
|
|
15
|
+
* The direction is *instrument*: the chrome is grey so the data can be loud. Anomaly maps,
|
|
16
|
+
* colormaps, grayscale sensor images and vector measurement overlays are what these
|
|
17
|
+
* applications exist to show, so the chrome around them carries no saturation at all.
|
|
18
|
+
* Exactly one accent -- `signal` -- and it means "you can act here": focus, selection, the
|
|
19
|
+
* active nav item, the primary button. The verdict colours are reserved for verdicts and
|
|
20
|
+
* never used as decoration; a green border that means nothing is a green border that
|
|
21
|
+
* teaches a reader to ignore green.
|
|
22
|
+
*
|
|
23
|
+
* The greys are true neutral rather than Tailwind's blue-tinted `slate`. A blue-cast grey
|
|
24
|
+
* beside a viridis or inferno map, or beside a caliper overlay drawn in the verdict
|
|
25
|
+
* colours, shifts how the loud thing reads -- a real cost on the one screen that matters
|
|
26
|
+
* most in either app.
|
|
27
|
+
*
|
|
28
|
+
* Mechanism: `@theme inline` registers semantic *names* that point at custom properties, so
|
|
29
|
+
* `bg-surface` compiles to `var(--surface)` rather than to a baked hex. Both themes are then
|
|
30
|
+
* one class swap on <html>, and the whole palette is retunable in this file alone. A
|
|
31
|
+
* consuming app pulls this file in as Tailwind v4 *source* (see the package README), so its
|
|
32
|
+
* own build resolves `@theme inline` against its own Tailwind — nothing here is pre-compiled.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
36
|
+
|
|
37
|
+
@theme inline {
|
|
38
|
+
--color-ground: var(--ground);
|
|
39
|
+
--color-surface: var(--surface);
|
|
40
|
+
--color-raised: var(--raised);
|
|
41
|
+
--color-overlay: var(--overlay);
|
|
42
|
+
--color-line: var(--line);
|
|
43
|
+
--color-line-strong: var(--line-strong);
|
|
44
|
+
--color-canvas: var(--canvas);
|
|
45
|
+
|
|
46
|
+
--color-fg: var(--fg);
|
|
47
|
+
--color-fg-muted: var(--fg-muted);
|
|
48
|
+
--color-fg-subtle: var(--fg-subtle);
|
|
49
|
+
|
|
50
|
+
--color-signal: var(--signal);
|
|
51
|
+
--color-signal-strong: var(--signal-strong);
|
|
52
|
+
--color-signal-fg: var(--signal-fg);
|
|
53
|
+
|
|
54
|
+
--color-normal: var(--normal);
|
|
55
|
+
--color-defect: var(--defect);
|
|
56
|
+
--color-warn: var(--warn);
|
|
57
|
+
|
|
58
|
+
--font-sans: "IBM Plex Sans Variable", ui-sans-serif, system-ui, sans-serif;
|
|
59
|
+
--font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", monospace;
|
|
60
|
+
|
|
61
|
+
--radius-control: 0.375rem;
|
|
62
|
+
--radius-panel: 0.625rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* Light is the default so a document with no class still renders legibly -- a consumer's
|
|
67
|
+
* own no-flash script sets `.dark` before first paint (see `theme.ts` and the README), but
|
|
68
|
+
* a failure there should degrade to a readable page rather than black text on black.
|
|
69
|
+
*
|
|
70
|
+
* Contrast (WCAG AA, 4.5:1 for text): `fg-muted` and `fg-subtle` clear it on `ground`,
|
|
71
|
+
* `surface`, `raised` and `overlay`; `signal-fg` on `signal`; and each verdict colour, and
|
|
72
|
+
* `signal`, as text on its own /8–/15 tint over `ground`. That last pair is what sets the
|
|
73
|
+
* light verdict and accent values — change one only with the ratios re-measured.
|
|
74
|
+
*/
|
|
75
|
+
:root {
|
|
76
|
+
/* Held a little below the panel surface so a card reads as raised off the page without
|
|
77
|
+
needing a shadow to say so. */
|
|
78
|
+
--ground: #eef1f2;
|
|
79
|
+
--surface: #ffffff;
|
|
80
|
+
--raised: #f4f6f7;
|
|
81
|
+
--overlay: #ffffff;
|
|
82
|
+
--line: #e3e6e8;
|
|
83
|
+
--line-strong: #c8cdd1;
|
|
84
|
+
--canvas: #101214;
|
|
85
|
+
|
|
86
|
+
--fg: #14171a;
|
|
87
|
+
--fg-muted: #5b646d;
|
|
88
|
+
--fg-subtle: #646d74;
|
|
89
|
+
|
|
90
|
+
--signal: #0a6b7a;
|
|
91
|
+
--signal-strong: #085763;
|
|
92
|
+
--signal-fg: #ffffff;
|
|
93
|
+
|
|
94
|
+
--normal: #046e4d;
|
|
95
|
+
--defect: #b31d1d;
|
|
96
|
+
--warn: #9d4808;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.dark {
|
|
100
|
+
--ground: #0b0d0f;
|
|
101
|
+
--surface: #14171a;
|
|
102
|
+
--raised: #1b1f23;
|
|
103
|
+
--overlay: #1f242a;
|
|
104
|
+
--line: #23282d;
|
|
105
|
+
--line-strong: #3a4147;
|
|
106
|
+
--canvas: #080a0b;
|
|
107
|
+
|
|
108
|
+
--fg: #e8ebed;
|
|
109
|
+
--fg-muted: #8b949b;
|
|
110
|
+
--fg-subtle: #838d94;
|
|
111
|
+
|
|
112
|
+
--signal: #3bc9db;
|
|
113
|
+
--signal-strong: #6fdde8;
|
|
114
|
+
--signal-fg: #04262c;
|
|
115
|
+
|
|
116
|
+
--normal: #34d399;
|
|
117
|
+
--defect: #f87171;
|
|
118
|
+
--warn: #fbbf24;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@layer base {
|
|
122
|
+
html {
|
|
123
|
+
background-color: var(--ground);
|
|
124
|
+
/* Native scrollbars, range thumbs and select popups follow the app, not the OS. */
|
|
125
|
+
color-scheme: light;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
html.dark {
|
|
129
|
+
color-scheme: dark;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
body {
|
|
133
|
+
font-family: var(--font-sans);
|
|
134
|
+
/* Metric tables, calliper readouts and live-updating counters must not reflow as
|
|
135
|
+
digits change. */
|
|
136
|
+
font-variant-numeric: tabular-nums;
|
|
137
|
+
-webkit-font-smoothing: antialiased;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* A zero that cannot be read as an O, in UIs that print a great many of both. */
|
|
141
|
+
code,
|
|
142
|
+
kbd,
|
|
143
|
+
pre,
|
|
144
|
+
samp,
|
|
145
|
+
.font-mono {
|
|
146
|
+
font-variant-numeric: tabular-nums slashed-zero;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
* The disclosure marker is drawn by `Disclosure` wherever a <details> is styled, so the
|
|
151
|
+
* UA triangle would be a second, differently-aligned caret next to it.
|
|
152
|
+
*/
|
|
153
|
+
summary::-webkit-details-marker {
|
|
154
|
+
display: none;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
summary {
|
|
158
|
+
list-style: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
*,
|
|
163
|
+
*::before,
|
|
164
|
+
*::after {
|
|
165
|
+
animation-duration: 0.01ms !important;
|
|
166
|
+
animation-iteration-count: 1 !important;
|
|
167
|
+
transition-duration: 0.01ms !important;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|