@baseworks/ui 0.0.0-stage → 0.2.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/src/tokens.css ADDED
@@ -0,0 +1,143 @@
1
+ /* @baseworks/ui — design tokens.
2
+
3
+ One palette: **warm paper, ultramarine ink**. Light and dark are the same
4
+ design at two luminances, not two designs — every token below exists in both
5
+ and means the same thing in both.
6
+
7
+ There is no brand switcher: a second palette is a second design to keep
8
+ correct in every state. There is a light/dark toggle, because that one is not
9
+ taste — it is the room the screen is in. The theme follows the operating
10
+ system until the viewer says otherwise, and `data-theme="light|dark"` on
11
+ <html> is how they say it (see `ThemeToggle`).
12
+
13
+ Why these colours:
14
+
15
+ - The neutrals are warm (a paper hue at very low saturation), not grey. A
16
+ console that is mostly tables and text is read for hours; a warm ground is
17
+ easier to sit in front of, and it is what distinguishes this from the
18
+ cool-grey default every admin panel ships with.
19
+
20
+ - The accent is ultramarine, at 231°. That is deliberate and functional: the
21
+ interface is full of status — ok, warn, crit — and those own green, amber
22
+ and red. An accent anywhere near them turns "this is a link" into "this is
23
+ a problem". Ultramarine is 76° from ok, 169° from warn and 132° from crit,
24
+ so an accented thing never reads as a state. The previous palette's
25
+ vermilion sat beside its own crit red, which is exactly the trap.
26
+
27
+ - Every pairing the interface actually makes was measured, not eyeballed:
28
+ body text ≥ 14:1, secondary text ≥ 5.7:1, an accent used as a link ≥ 7.4:1,
29
+ a button label on the accent ≥ 7.4:1, and each status pill against its own
30
+ tinted background ≥ 4.7:1. The faint tier (timestamps, placeholders) is
31
+ ≥ 3:1 and is never the only place something is said.
32
+
33
+ The one serif is the page title. Nothing else is set in it — it marks the
34
+ top of a document and then gets out of the way. */
35
+
36
+ :root{
37
+ /* Ground and surfaces. bg is the page, surface is a card, surface2 is a
38
+ recessed area inside one (a toolbar, a code block, a table header). */
39
+ --bg:#FBFAF8; --surface:#FFFFFF; --surface2:#F2F0EB;
40
+
41
+ /* Text, in three tiers. ink says it, muted explains it, faint timestamps it. */
42
+ --ink:#1A1916; --muted:#67635B; --faint:#98938A;
43
+
44
+ /* line separates, line-strong encloses (an input, a control you can click). */
45
+ --line:#E8E4DC; --line-strong:#D6D0C5;
46
+
47
+ /* The accent: one colour, for the one thing on a screen that acts.
48
+ accent-soft is the same colour at 7%, for a pill or a selected row.
49
+ accent-ink is what is legible on top of it. */
50
+ --accent:#2B44CE; --accent-soft:#2B44CE12; --accent-ink:#FFFFFF;
51
+
52
+ /* State. Never the accent, never each other — see the note above. */
53
+ --ok:#16794F; --ok-soft:#16794F16;
54
+ --warn:#8A6112; --warn-soft:#8A611218;
55
+ --crit:#B3362F; --crit-soft:#B3362F16;
56
+
57
+ --chat-bg:#F5F3EF;
58
+ --shadow:0 8px 28px rgba(26,25,22,.10);
59
+
60
+ --sans:'Instrument Sans',system-ui,-apple-system,'Segoe UI',sans-serif;
61
+ --mono:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,monospace;
62
+ --display:'Instrument Serif',Georgia,'Times New Roman',serif;
63
+ }
64
+
65
+ /* Dark is the same palette rotated down: the paper becomes a warm charcoal and
66
+ the ultramarine comes up to meet it. Not black — a dense table on pure black
67
+ vibrates, and the borders that hold it together disappear. */
68
+ @media (prefers-color-scheme: dark){
69
+ :root:not([data-theme="light"]){
70
+ --bg:#1A1815; --surface:#221F1B; --surface2:#2B2823;
71
+ --ink:#EDE9E1; --muted:#A5A096; --faint:#7B766D;
72
+ --line:#322E28; --line-strong:#453F37;
73
+ --accent:#A9B5FF; --accent-soft:#A9B5FF20; --accent-ink:#151538;
74
+ --ok:#4FC28C; --ok-soft:#4FC28C22;
75
+ --warn:#DFB255; --warn-soft:#DFB25520;
76
+ --crit:#F0736B; --crit-soft:#F0736B1F;
77
+ --chat-bg:#1D1B18;
78
+ --shadow:0 8px 28px rgba(0,0,0,.5);
79
+ }
80
+ }
81
+
82
+ /* The same values again, so an explicit choice wins in both directions: a
83
+ forced dark on a light system, and a forced light on a dark one. */
84
+ :root[data-theme="dark"]{
85
+ --bg:#1A1815; --surface:#221F1B; --surface2:#2B2823;
86
+ --ink:#EDE9E1; --muted:#A5A096; --faint:#7B766D;
87
+ --line:#322E28; --line-strong:#453F37;
88
+ --accent:#A9B5FF; --accent-soft:#A9B5FF20; --accent-ink:#151538;
89
+ --ok:#4FC28C; --ok-soft:#4FC28C22;
90
+ --warn:#DFB255; --warn-soft:#DFB25520;
91
+ --crit:#F0736B; --crit-soft:#F0736B1F;
92
+ --chat-bg:#1D1B18;
93
+ --shadow:0 8px 28px rgba(0,0,0,.5);
94
+ }
95
+
96
+ /* shadcn/ui names for the same colours, so a component copied in from there
97
+ inherits this palette instead of bringing its own. These are raw colour
98
+ values, which is the form shadcn uses with Tailwind v4 (`bg-background`);
99
+ the older `hsl(var(--background))` form expects bare HSL triplets and will
100
+ not read these. */
101
+ :root{
102
+ --background:var(--bg); --foreground:var(--ink);
103
+ --card:var(--surface); --card-foreground:var(--ink);
104
+ --popover:var(--surface); --popover-foreground:var(--ink);
105
+ --primary:var(--accent); --primary-foreground:var(--accent-ink);
106
+ --secondary:var(--surface2); --secondary-foreground:var(--ink);
107
+ --muted-foreground:var(--muted);
108
+ --destructive:var(--crit); --destructive-foreground:var(--accent-ink);
109
+ --border:var(--line); --input:var(--line-strong);
110
+ --ring:var(--accent); --radius:10px;
111
+ }
112
+
113
+ /* Reset and text primitives. */
114
+ *{box-sizing:border-box}
115
+ html,body{margin:0;padding:0}
116
+ body{
117
+ background:var(--bg); color:var(--ink);
118
+ font-family:var(--sans); font-size:14px; line-height:1.5;
119
+ -webkit-font-smoothing:antialiased;
120
+ }
121
+ button{font:inherit;color:inherit;background:none;border:none;cursor:pointer;padding:0}
122
+ a{color:inherit;text-decoration:none}
123
+ button:focus-visible,a:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
124
+ .mono{font-family:var(--mono);font-size:.92em}
125
+ .num{font-variant-numeric:tabular-nums}
126
+
127
+ /* Controls carry the surface, not the browser's white. A bare <select> in a
128
+ table cell is a real case — the wrapper classes in primitives.css size and
129
+ stretch these, but an unwrapped control must already look right, in both
130
+ themes. */
131
+ input,select,textarea{font:inherit;color:var(--ink)}
132
+ select,textarea,
133
+ input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]){
134
+ background:var(--surface);border:1px solid var(--line-strong);border-radius:8px;padding:6px 10px;
135
+ }
136
+ select{padding-right:6px}
137
+ input::placeholder,textarea::placeholder{color:var(--faint)}
138
+ input:disabled,select:disabled,textarea:disabled{opacity:.6;cursor:not-allowed}
139
+
140
+ /* Someone who asked for less motion gets none. */
141
+ @media (prefers-reduced-motion: reduce){
142
+ *{animation:none!important;transition:none!important}
143
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * A small fetch helper: loading and error state, optional periodic refresh.
5
+ * A cache/mutation library is worth adding only when a screen needs more.
6
+ *
7
+ * `enabled` exists for a question that should not be asked yet — a count for a
8
+ * service this realm may not have, a list that needs an organization first.
9
+ */
10
+
11
+ import { useCallback, useEffect, useRef, useState } from 'react'
12
+
13
+ export type Resource<T> = {
14
+ data: T | undefined
15
+ error: string | undefined
16
+ loading: boolean
17
+ reload: () => void
18
+ }
19
+
20
+ export type ResourceOptions = {
21
+ refreshMs?: number
22
+ /** Re-runs when this changes — an id the fetcher closes over, usually. */
23
+ key?: string
24
+ enabled?: boolean
25
+ /** Shown when the failure carries no message of its own. */
26
+ fallbackError?: string
27
+ }
28
+
29
+ export function useResource<T>(fetcher: () => Promise<T>, options: ResourceOptions = {}): Resource<T> {
30
+ const [data, setData] = useState<T>()
31
+ const [error, setError] = useState<string>()
32
+ const [loading, setLoading] = useState(true)
33
+ const [nonce, setNonce] = useState(0)
34
+ // Held in a ref so a caller may write the fetcher inline without the effect
35
+ // re-running on every render.
36
+ const fetcherRef = useRef(fetcher)
37
+ fetcherRef.current = fetcher
38
+ const enabled = options.enabled ?? true
39
+ const fallback = options.fallbackError ?? 'That did not answer'
40
+
41
+ const reload = useCallback(() => setNonce((n) => n + 1), [])
42
+
43
+ useEffect(() => {
44
+ if (!enabled) return
45
+ let cancelled = false
46
+
47
+ const run = async () => {
48
+ try {
49
+ const result = await fetcherRef.current()
50
+ if (cancelled) return
51
+ setData(result)
52
+ setError(undefined)
53
+ } catch (err) {
54
+ if (cancelled) return
55
+ setError(err instanceof Error && err.message ? err.message : fallback)
56
+ } finally {
57
+ if (!cancelled) setLoading(false)
58
+ }
59
+ }
60
+
61
+ run()
62
+ const timer = options.refreshMs ? setInterval(run, options.refreshMs) : undefined
63
+ return () => {
64
+ cancelled = true
65
+ if (timer) clearInterval(timer)
66
+ }
67
+ }, [nonce, options.refreshMs, options.key, enabled, fallback])
68
+
69
+ return { data, error, loading, reload }
70
+ }