@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/README.md +183 -2
- package/package.json +29 -3
- package/src/assistant.tsx +135 -0
- package/src/button.tsx +31 -0
- package/src/card.tsx +85 -0
- package/src/command-palette.tsx +59 -0
- package/src/commands.ts +27 -0
- package/src/dialog.tsx +96 -0
- package/src/feedback.tsx +37 -0
- package/src/form.tsx +40 -0
- package/src/format.ts +53 -0
- package/src/frame.css +214 -0
- package/src/frame.tsx +293 -0
- package/src/gate.tsx +27 -0
- package/src/index.ts +61 -0
- package/src/menu.tsx +62 -0
- package/src/nav.ts +47 -0
- package/src/primitives.css +239 -0
- package/src/slide-over.tsx +66 -0
- package/src/styles.css +7 -0
- package/src/table.tsx +53 -0
- package/src/theme-toggle.tsx +33 -0
- package/src/theme.ts +55 -0
- package/src/tokens.css +143 -0
- package/src/use-resource.ts +70 -0
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
|
+
}
|