@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
package/src/theme.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Which of the two palettes is on screen.
|
|
3
|
+
*
|
|
4
|
+
* Judging an anomaly map, or a measurement overlay drawn over a low-contrast part, against
|
|
5
|
+
* a bright room and against a dark one are different tasks, and a researcher switches
|
|
6
|
+
* between them within a session -- so dark mode is a real, storable choice here rather than
|
|
7
|
+
* only `prefers-color-scheme`.
|
|
8
|
+
*
|
|
9
|
+
* Three states, not two. "system" is a real choice and has to survive a reload as itself,
|
|
10
|
+
* otherwise the first time the OS flips at sunset the app disagrees with everything else on
|
|
11
|
+
* the machine and there is no way back to following it.
|
|
12
|
+
*
|
|
13
|
+
* The class is applied to <html> rather than <body> so the page background painted before
|
|
14
|
+
* React mounts is already correct. `applyTheme` (below, as `paint`) has to be duplicated as
|
|
15
|
+
* an inline script in the consumer's `index.html` for exactly that reason -- see the
|
|
16
|
+
* package README for the exact snippet, and keep it in agreement with this module if either
|
|
17
|
+
* changes.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A stored theme preference: a palette, or `system` to follow the OS
|
|
22
|
+
* (`prefers-color-scheme`) and keep following it.
|
|
23
|
+
*/
|
|
24
|
+
export type ThemeChoice = "light" | "dark" | "system";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The `localStorage` key a consumer's theme choice is kept under.
|
|
28
|
+
*
|
|
29
|
+
* Every function here takes it as an optional last argument and defaults to this constant,
|
|
30
|
+
* so a single app needs to pass nothing, while two apps sharing a browser profile -- or one
|
|
31
|
+
* app that wants a namespaced key -- can each choose their own without stepping on the
|
|
32
|
+
* other's stored preference.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_THEME_STORAGE_KEY = "vitavision-theme";
|
|
35
|
+
|
|
36
|
+
// Every DOM access here is inside a function, and guarded: this module is imported on the
|
|
37
|
+
// server too.
|
|
38
|
+
const query = () =>
|
|
39
|
+
typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
40
|
+
? window.matchMedia("(prefers-color-scheme: dark)")
|
|
41
|
+
: null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The stored preference.
|
|
45
|
+
*
|
|
46
|
+
* @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.
|
|
47
|
+
* @returns The stored choice, or `system` when nothing valid is stored or storage cannot be
|
|
48
|
+
* read (private browsing, the server).
|
|
49
|
+
*/
|
|
50
|
+
export function readThemeChoice(storageKey: string = DEFAULT_THEME_STORAGE_KEY): ThemeChoice {
|
|
51
|
+
try {
|
|
52
|
+
const stored = window.localStorage.getItem(storageKey);
|
|
53
|
+
if (stored === "light" || stored === "dark" || stored === "system") return stored;
|
|
54
|
+
} catch {
|
|
55
|
+
// Private browsing, or a WebView with storage disabled. Following the OS is a fine
|
|
56
|
+
// answer to "I could not read your preference", and better than refusing to render.
|
|
57
|
+
}
|
|
58
|
+
return "system";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What `choice` actually resolves to right now.
|
|
63
|
+
*
|
|
64
|
+
* @param choice - The preference.
|
|
65
|
+
* @returns `choice` itself for `light`/`dark`; for `system`, the OS palette (`light` where
|
|
66
|
+
* it cannot be queried).
|
|
67
|
+
*/
|
|
68
|
+
export function resolveTheme(choice: ThemeChoice): "light" | "dark" {
|
|
69
|
+
if (choice !== "system") return choice;
|
|
70
|
+
return query()?.matches ? "dark" : "light";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function paint(choice: ThemeChoice): void {
|
|
74
|
+
if (typeof document === "undefined") return;
|
|
75
|
+
document.documentElement.classList.toggle("dark", resolveTheme(choice) === "dark");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Store a preference and paint it (the `dark` class on `<html>`).
|
|
80
|
+
*
|
|
81
|
+
* @param choice - The new preference.
|
|
82
|
+
* @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.
|
|
83
|
+
*/
|
|
84
|
+
export function setThemeChoice(
|
|
85
|
+
choice: ThemeChoice,
|
|
86
|
+
storageKey: string = DEFAULT_THEME_STORAGE_KEY,
|
|
87
|
+
): void {
|
|
88
|
+
try {
|
|
89
|
+
window.localStorage.setItem(storageKey, choice);
|
|
90
|
+
} catch {
|
|
91
|
+
// Not worth failing the click over; the choice still applies for this session.
|
|
92
|
+
}
|
|
93
|
+
paint(choice);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Paint the stored choice and keep following the OS while the choice is "system".
|
|
98
|
+
*
|
|
99
|
+
* @param storageKey - The `localStorage` key. Defaults to `DEFAULT_THEME_STORAGE_KEY`.
|
|
100
|
+
* @returns An unsubscribe, so a test can tear the listener down; a long-lived app never does.
|
|
101
|
+
*/
|
|
102
|
+
export function initTheme(storageKey: string = DEFAULT_THEME_STORAGE_KEY): () => void {
|
|
103
|
+
paint(readThemeChoice(storageKey));
|
|
104
|
+
|
|
105
|
+
const media = query();
|
|
106
|
+
if (media === null) return () => {};
|
|
107
|
+
|
|
108
|
+
const onSystemChange = () => {
|
|
109
|
+
if (readThemeChoice(storageKey) === "system") paint("system");
|
|
110
|
+
};
|
|
111
|
+
media.addEventListener("change", onSystemChange);
|
|
112
|
+
return () => media.removeEventListener("change", onSystemChange);
|
|
113
|
+
}
|
package/src/tone.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The verdict vocabulary, for the components that paint outside of `className`.
|
|
3
|
+
*
|
|
4
|
+
* Every Tailwind-driven primitive under `components/ui` reaches the palette through a
|
|
5
|
+
* class name, which only works where Tailwind's own build sees the class. `MeasureOverlay`
|
|
6
|
+
* and `LineProfile` paint through raw SVG `stroke`/`fill` attributes instead — a caliper
|
|
7
|
+
* box or an edge mark is drawn from data the consumer supplies at runtime, not from a
|
|
8
|
+
* fixed set of class names a bundler can scan for. So these two read the same five tokens
|
|
9
|
+
* `styles.css` defines (`--signal`, `--normal`, `--defect`, `--warn`, `--fg-subtle`)
|
|
10
|
+
* directly as CSS custom properties, which resolves correctly in both themes without
|
|
11
|
+
* needing Tailwind involved in the overlay's own render path at all.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A paint for SVG overlays and charts: the accent (`signal`), a verdict colour, or `muted`.
|
|
16
|
+
*
|
|
17
|
+
* Named `MeasureTone` rather than `Tone` — `components/ui/Badge.tsx` already owns that name
|
|
18
|
+
* for the verdict-badge vocabulary (`neutral`/`normal`/`defect`/`unlabeled`/`warning`/
|
|
19
|
+
* `info`), and both are re-exported from the package root, where two types of the same name
|
|
20
|
+
* would collide.
|
|
21
|
+
*/
|
|
22
|
+
export type MeasureTone = "signal" | "normal" | "defect" | "warn" | "muted";
|
|
23
|
+
|
|
24
|
+
const TONE_VAR: Record<MeasureTone, string> = {
|
|
25
|
+
signal: "var(--signal)",
|
|
26
|
+
normal: "var(--normal)",
|
|
27
|
+
defect: "var(--defect)",
|
|
28
|
+
warn: "var(--warn)",
|
|
29
|
+
muted: "var(--fg-subtle)",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The CSS paint value for a tone — usable directly as a `stroke`, `fill` or `color`. It is a
|
|
34
|
+
* `var(--token)` reference, so it follows the theme without a re-render.
|
|
35
|
+
*
|
|
36
|
+
* @param tone - The tone; `undefined` falls back.
|
|
37
|
+
* @param fallback - The tone used when `tone` is `undefined`. Defaults to `signal`.
|
|
38
|
+
* @returns A `var(--…)` CSS value.
|
|
39
|
+
*/
|
|
40
|
+
export function toneColor(tone: MeasureTone | undefined, fallback: MeasureTone = "signal"): string {
|
|
41
|
+
return TONE_VAR[tone ?? fallback];
|
|
42
|
+
}
|