@solidrt/components 0.0.62 → 0.0.64
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/AGENTS.md +18 -14
- package/README.md +126 -21
- package/docs/badge.md +1 -1
- package/docs/checkbox.md +1 -1
- package/docs/context-menu.md +1 -1
- package/docs/item.md +1 -1
- package/docs/light.md +12 -0
- package/docs/motion.md +13 -0
- package/docs/press.md +19 -0
- package/docs/progress-bar.md +1 -1
- package/docs/radio.md +1 -1
- package/docs/segmented-control.md +1 -1
- package/docs/select.md +1 -1
- package/docs/slider.md +1 -1
- package/docs/surface.md +19 -0
- package/docs/switch.md +1 -1
- package/docs/theme.md +34 -6
- package/docs/tooltip.md +1 -1
- package/docs/types.md +1 -1
- package/docs/window.md +2 -0
- package/examples/lit.tsx +182 -0
- package/package.json +2 -2
- package/src/badge.tsx +25 -6
- package/src/button.tsx +44 -29
- package/src/card.tsx +76 -20
- package/src/checkbox.tsx +23 -19
- package/src/context-menu.tsx +21 -9
- package/src/editor-field.tsx +12 -7
- package/src/focus-nav.ts +17 -6
- package/src/glow.ts +35 -0
- package/src/index.ts +16 -1
- package/src/item.tsx +35 -13
- package/src/light.ts +274 -0
- package/src/motion.tsx +13 -1
- package/src/nav-shell.tsx +19 -10
- package/src/press.ts +2 -1
- package/src/pressable.tsx +6 -4
- package/src/progress-bar.tsx +25 -7
- package/src/radio.tsx +34 -22
- package/src/segmented-control.tsx +104 -23
- package/src/select.tsx +40 -34
- package/src/slider.tsx +74 -22
- package/src/surface.tsx +287 -0
- package/src/switch.tsx +48 -26
- package/src/theme.ts +201 -40
- package/src/tooltip.tsx +18 -14
- package/src/types.ts +66 -2
- package/src/window.tsx +26 -3
package/src/theme.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createStore } from "@solidjs/signals"
|
|
2
|
-
import type { FontWeight } from "@solidrt/core"
|
|
3
|
-
import type { StyleProps } from "./types"
|
|
2
|
+
import type { FontWeight, WindowShaderProps } from "@solidrt/core"
|
|
3
|
+
import type { ElevationLevel, Material, MaterialRole, StyleProps } from "./types"
|
|
4
4
|
|
|
5
5
|
export type TextStyle = {
|
|
6
6
|
size: number
|
|
@@ -13,23 +13,58 @@ export type TextVariant = "caption" | "label" | "body" | "title" | "heading"
|
|
|
13
13
|
|
|
14
14
|
// The components whose chrome accepts a theme-level paint override (see
|
|
15
15
|
// Theme["components"]). Plain containers (View, Pressable, ...) are not
|
|
16
|
-
// themed, so they take no override either.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
16
|
+
// themed, so they take no override either. A runtime list, so defineTheme
|
|
17
|
+
// can write every key (see there).
|
|
18
|
+
const THEMED_COMPONENTS = [
|
|
19
|
+
"button",
|
|
20
|
+
"card",
|
|
21
|
+
"badge",
|
|
22
|
+
"switch",
|
|
23
|
+
"checkbox",
|
|
24
|
+
"radio",
|
|
25
|
+
"slider",
|
|
26
|
+
"item",
|
|
27
|
+
"select",
|
|
28
|
+
"segmentedControl",
|
|
29
|
+
"textInput",
|
|
30
|
+
"richTextEditor",
|
|
31
|
+
"tooltip",
|
|
32
|
+
"divider",
|
|
33
|
+
"progressBar",
|
|
34
|
+
"spinner",
|
|
35
|
+
"contextMenu",
|
|
36
|
+
"navShell",
|
|
37
|
+
] as const
|
|
38
|
+
export type ThemedComponent = (typeof THEMED_COMPONENTS)[number]
|
|
39
|
+
|
|
40
|
+
// The roles and levels, as runtime lists so defineTheme writes every key.
|
|
41
|
+
const MATERIAL_ROLES = ["surface", "control", "accent", "overlay"] as const satisfies readonly MaterialRole[]
|
|
42
|
+
const ELEVATION_LEVELS = ["flat", "raised", "floating", "overlay"] as const satisfies readonly ElevationLevel[]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The theme's one light, in screen space (x right, y down, z into the
|
|
46
|
+
* screen), the way a 3D scene's directional light is given by the way it
|
|
47
|
+
* travels: `[0.3, 1, 1]` comes from above, slightly left, in front. Every
|
|
48
|
+
* lit face derives from it: shadows fall along its x/y and stretch as it
|
|
49
|
+
* lowers, the sheen and the bevel run from the lit edge to the shaded one,
|
|
50
|
+
* and faces take on its `color`. `ambient` (0..1) is the fill light on the
|
|
51
|
+
* shaded side: the more of it, the fainter the shadows and the flatter the
|
|
52
|
+
* sheen. Fixed per theme, or moved at event rate with `setTheme({ light })`
|
|
53
|
+
* (a drag, a time of day); every face re-derives its paint on each move, so
|
|
54
|
+
* it is not a per-frame animation.
|
|
55
|
+
*/
|
|
56
|
+
export type Light = {
|
|
57
|
+
direction: [x: number, y: number, z: number]
|
|
58
|
+
color: string
|
|
59
|
+
ambient: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What shadows are cast in: their `color` (black, or a deep tint for a
|
|
64
|
+
* colored scheme) and a `strength` factor, since the alpha that reads on a
|
|
65
|
+
* light ground vanishes on a dark one (the lit dark preset uses 1.7).
|
|
66
|
+
*/
|
|
67
|
+
export type ShadowTone = { color: string; strength: number }
|
|
33
68
|
|
|
34
69
|
// A resolved theme: every value is a plain string/number ready to be read by
|
|
35
70
|
// a component. Authoring happens through defineTheme, which is where
|
|
@@ -80,13 +115,20 @@ export type Theme = {
|
|
|
80
115
|
// selected glyphs; translucent so the text stays readable. Defaults to
|
|
81
116
|
// overlayPressed (a neutral scheme-aware tint that needs no color math).
|
|
82
117
|
selection: string
|
|
118
|
+
// The cap riding a track: the Switch knob, the Slider thumb. Unset, the
|
|
119
|
+
// knob is onPrimary and the thumb primary. A palette whose onPrimary is
|
|
120
|
+
// dark (text on a bright accent) sets a light cap here, or its off knob
|
|
121
|
+
// sinks into the track; the lit presets set one, lit like any face.
|
|
122
|
+
thumb?: string
|
|
83
123
|
}
|
|
84
124
|
// Gaps and paddings, multiples of one base unit (sm 1x, md 2x, lg 4x,
|
|
85
125
|
// xl 5x); read through space() where density should apply.
|
|
86
126
|
spacing: { sm: number; md: number; lg: number; xl: number }
|
|
87
127
|
// Corner radii. md is THE control radius (Button, TextInput, Select,
|
|
88
128
|
// SegmentedControl); sm is one step under it (Checkbox, Item, menus,
|
|
89
|
-
// Tooltip), lg one step over (Card), full
|
|
129
|
+
// Tooltip), lg one step over (Card), full the pill: Badge, and through
|
|
130
|
+
// pill() every part shaped by its own height (Switch, Slider, Radio,
|
|
131
|
+
// ProgressBar), which a full of 0 squares.
|
|
90
132
|
radius: { sm: number; md: number; lg: number; full: number }
|
|
91
133
|
borderWidth: { sm: number; focus: number }
|
|
92
134
|
// Motion durations (ms). Every built-in transition the components declare
|
|
@@ -94,7 +136,8 @@ export type Theme = {
|
|
|
94
136
|
// fast is press/hover feedback, base the color/opacity fades (state
|
|
95
137
|
// changes, the theme cross-fade, popup enter/exit), slow the travel of a
|
|
96
138
|
// control's moving parts (switch knob, segmented indicator, progress
|
|
97
|
-
// fill). See motion.tsx;
|
|
139
|
+
// fill) and of a face's shadows as it sinks or lifts. See motion.tsx;
|
|
140
|
+
// policy.motion gates them.
|
|
98
141
|
motion: { fast: number; base: number; slow: number }
|
|
99
142
|
// Default extents of the components that have one: the panes and rails an
|
|
100
143
|
// app lays its screens around, and the smallest sensible popup/track. Each
|
|
@@ -104,26 +147,57 @@ export type Theme = {
|
|
|
104
147
|
// Semantic control glyphs, as SVG document strings (the Icon currency).
|
|
105
148
|
// Components draw their built-in vector paths by default; a theme that sets
|
|
106
149
|
// a slot swaps that glyph everywhere it appears. The package still bundles
|
|
107
|
-
// no icon set.
|
|
150
|
+
// no icon set. Both keys always present (see defineTheme).
|
|
108
151
|
icons: { chevronDown?: string; check?: string }
|
|
109
152
|
// Per-component paint overrides: merged between a component's themed
|
|
110
153
|
// defaults and the instance's style prop, so a theme can restyle every
|
|
111
154
|
// Button (say, pill corners) without wrapping the component. Instance
|
|
112
|
-
// style still wins.
|
|
155
|
+
// style still wins. `material` and `elevation` in one restyle that
|
|
156
|
+
// component's faces. Every key always present (see defineTheme).
|
|
113
157
|
components: { [K in ThemedComponent]?: StyleProps }
|
|
158
|
+
// The light every face is lit by (see Light).
|
|
159
|
+
light: Light
|
|
160
|
+
// What the faces' shadows are cast in (see ShadowTone).
|
|
161
|
+
shadow: ShadowTone
|
|
162
|
+
// The material of each role's faces (see MaterialRole, Material). The
|
|
163
|
+
// stock presets give every role `{}`, the flat look; the lit presets a
|
|
164
|
+
// sheen and a bevel per role. Per component, theme.components.<name>
|
|
165
|
+
// .material merges over the role's, an instance's style.material over
|
|
166
|
+
// that, and style.glow overrides the glow layer alone (null clears it).
|
|
167
|
+
// Every role always present (see defineTheme).
|
|
168
|
+
material: { [R in MaterialRole]: Material }
|
|
169
|
+
// The height of each elevation level, in the light's units: a face at
|
|
170
|
+
// height h casts its key shadow h px along the light's slant and blurs it
|
|
171
|
+
// 2.5 h px, and a tight contact shadow under it. The stock presets are 0
|
|
172
|
+
// throughout (no face casts); the lit presets use flat 0, raised 2,
|
|
173
|
+
// floating 6, overlay 14. Every level always present (see defineTheme).
|
|
174
|
+
elevation: { [L in ElevationLevel]: number }
|
|
175
|
+
// The theme's window finish: a core window shader declaration (a linked
|
|
176
|
+
// program and its params) that Window declares on the window as it is,
|
|
177
|
+
// plus `uScale`, the display scale. Strength and layer switches are the
|
|
178
|
+
// theme's own params; an app adjusts one by spreading the current ones
|
|
179
|
+
// (setTheme merges one level deep, so a partial replaces the whole
|
|
180
|
+
// params object it names). The theme's
|
|
181
|
+
// module links the program itself, once, at init: the client brings the
|
|
182
|
+
// GPU up before the bundle runs. The package ships no finish; the stock
|
|
183
|
+
// presets have none. Every key always present (see defineTheme):
|
|
184
|
+
// `program` unset means no finish.
|
|
185
|
+
finish: Partial<WindowShaderProps>
|
|
114
186
|
}
|
|
115
187
|
|
|
116
188
|
// -- Authoring ---------------------------------------------------------------
|
|
117
189
|
|
|
118
|
-
// A
|
|
190
|
+
// A value in a theme definition: one value, or a [light, dark] pair resolved
|
|
119
191
|
// by defineTheme's scheme argument. Pairs are opt-in per token; a definition
|
|
120
192
|
// without any needs no scheme at all (a game ships one look, not two).
|
|
121
|
-
export type
|
|
193
|
+
export type ThemeValue<T> = T | [light: T, dark: T]
|
|
194
|
+
export type ThemeColor = ThemeValue<string>
|
|
122
195
|
|
|
123
196
|
export type ThemeDefinition = {
|
|
124
|
-
color: { [K in Exclude<keyof Theme["color"], "ring" | "selection">]: ThemeColor } & {
|
|
197
|
+
color: { [K in Exclude<keyof Theme["color"], "ring" | "selection" | "thumb">]: ThemeColor } & {
|
|
125
198
|
ring?: ThemeColor
|
|
126
199
|
selection?: ThemeColor
|
|
200
|
+
thumb?: ThemeColor
|
|
127
201
|
}
|
|
128
202
|
text?: {
|
|
129
203
|
fontFamily?: string
|
|
@@ -148,6 +222,16 @@ export type ThemeDefinition = {
|
|
|
148
222
|
size?: Partial<Theme["size"]>
|
|
149
223
|
icons?: Theme["icons"]
|
|
150
224
|
components?: Theme["components"]
|
|
225
|
+
// The light; a field left out takes LIGHT's (from above, slightly left,
|
|
226
|
+
// in front; white; ambient 0.5).
|
|
227
|
+
light?: { direction?: Light["direction"]; color?: ThemeColor; ambient?: ThemeValue<number> }
|
|
228
|
+
// The shadow tone; a field left out takes SHADOW's (black, strength 1).
|
|
229
|
+
shadow?: { color?: ThemeColor; strength?: ThemeValue<number> }
|
|
230
|
+
// The material per role; a role left out is `{}`, flat.
|
|
231
|
+
material?: { [R in MaterialRole]?: Material }
|
|
232
|
+
// The height per level; a level left out is 0.
|
|
233
|
+
elevation?: { [L in ElevationLevel]?: number }
|
|
234
|
+
finish?: WindowShaderProps
|
|
151
235
|
}
|
|
152
236
|
|
|
153
237
|
const SPACING_BASE = 4
|
|
@@ -169,6 +253,10 @@ const BORDER_WIDTH = { sm: 1, focus: 2 }
|
|
|
169
253
|
// long enough that a part's travel reads as movement rather than a flicker.
|
|
170
254
|
const MOTION = { fast: 100, base: 150, slow: 250 }
|
|
171
255
|
const SIZE = { navRail: 72, navSidebar: 220, splitViewList: 320, menuMinWidth: 120, slider: 200 }
|
|
256
|
+
// The default light: from above, slightly left, in front, white, with half
|
|
257
|
+
// ambient - a neutral key that leaves every base color as it is.
|
|
258
|
+
const LIGHT: Light = { direction: [0.3, 1, 1], color: "#ffffff", ambient: 0.5 }
|
|
259
|
+
const SHADOW: ShadowTone = { color: "#000000", strength: 1 }
|
|
172
260
|
|
|
173
261
|
// Line height and weight per role; body is the base text, label is body at
|
|
174
262
|
// an emphasized weight (form labels, key/value keys, tags), caption the one
|
|
@@ -184,9 +272,16 @@ const ROLE_DEFAULTS: { [K in TextVariant]: { step: number; lineHeight: number; w
|
|
|
184
272
|
heading: { step: 2, lineHeight: 1.3, weight: 700 },
|
|
185
273
|
}
|
|
186
274
|
|
|
275
|
+
// The side of a [light, dark] pair for the scheme; a bare value as it is.
|
|
276
|
+
function pick<T>(name: string, value: ThemeValue<T>, scheme?: "light" | "dark"): T {
|
|
277
|
+
if (!Array.isArray(value)) return value
|
|
278
|
+
if (!scheme) throw new Error(`Theme value "${name}" is a [light, dark] pair; pass a scheme to defineTheme`)
|
|
279
|
+
return value[scheme === "light" ? 0 : 1]
|
|
280
|
+
}
|
|
281
|
+
|
|
187
282
|
/**
|
|
188
283
|
* Resolves a theme definition into a Theme. `scheme` picks the side of every
|
|
189
|
-
* [light, dark]
|
|
284
|
+
* [light, dark] pair; a definition without pairs needs no scheme
|
|
190
285
|
* (modes are a per-theme choice, not a framework requirement). The type
|
|
191
286
|
* scale expands from text.base and text.ratio, with text.roles overriding
|
|
192
287
|
* per role. Throws on a pair without a scheme (throw-in-dev policy).
|
|
@@ -197,13 +292,22 @@ export function defineTheme(def: ThemeDefinition, scheme?: "light" | "dark"): Th
|
|
|
197
292
|
let k = key as keyof Theme["color"]
|
|
198
293
|
let value = def.color[k]
|
|
199
294
|
if (value == null) continue
|
|
200
|
-
|
|
201
|
-
if (!scheme) throw new Error(`Theme color "${key}" is a [light, dark] pair; pass a scheme to defineTheme`)
|
|
202
|
-
color[k] = value[scheme === "light" ? 0 : 1]
|
|
203
|
-
} else color[k] = value
|
|
295
|
+
color[k] = pick(key, value, scheme)
|
|
204
296
|
}
|
|
205
297
|
if (def.color.ring == null) color.ring = color.text
|
|
206
298
|
if (def.color.selection == null) color.selection = color.overlayPressed
|
|
299
|
+
// Optional keys are still written (thumb here; icons, components,
|
|
300
|
+
// material and elevation below): setTheme merges a category with
|
|
301
|
+
// Object.assign, which skips absent keys, so a resolved theme carries
|
|
302
|
+
// every key and a preset that sets none of them still clears what an
|
|
303
|
+
// earlier preset set.
|
|
304
|
+
if (!("thumb" in color)) color.thumb = undefined
|
|
305
|
+
let components = {} as Theme["components"]
|
|
306
|
+
for (let name of THEMED_COMPONENTS) components[name] = def.components?.[name]
|
|
307
|
+
let material = {} as Theme["material"]
|
|
308
|
+
for (let role of MATERIAL_ROLES) material[role] = { ...def.material?.[role] }
|
|
309
|
+
let elevation = {} as Theme["elevation"]
|
|
310
|
+
for (let level of ELEVATION_LEVELS) elevation[level] = def.elevation?.[level] ?? 0
|
|
207
311
|
let base = def.text?.base ?? 14
|
|
208
312
|
let ratio = def.text?.ratio ?? 1.26
|
|
209
313
|
let role = (name: TextVariant): TextStyle => {
|
|
@@ -235,8 +339,26 @@ export function defineTheme(def: ThemeDefinition, scheme?: "light" | "dark"): Th
|
|
|
235
339
|
borderWidth: { ...BORDER_WIDTH, ...def.borderWidth },
|
|
236
340
|
motion: { ...MOTION, ...def.motion },
|
|
237
341
|
size: { ...SIZE, ...def.size },
|
|
238
|
-
icons: def.icons
|
|
239
|
-
components
|
|
342
|
+
icons: { chevronDown: def.icons?.chevronDown, check: def.icons?.check },
|
|
343
|
+
components,
|
|
344
|
+
light: {
|
|
345
|
+
direction: def.light?.direction ?? LIGHT.direction,
|
|
346
|
+
color: def.light?.color != null ? pick("light.color", def.light.color, scheme) : LIGHT.color,
|
|
347
|
+
ambient: def.light?.ambient != null ? pick("light.ambient", def.light.ambient, scheme) : LIGHT.ambient,
|
|
348
|
+
},
|
|
349
|
+
shadow: {
|
|
350
|
+
color: def.shadow?.color != null ? pick("shadow.color", def.shadow.color, scheme) : SHADOW.color,
|
|
351
|
+
strength: def.shadow?.strength != null ? pick("shadow.strength", def.shadow.strength, scheme) : SHADOW.strength,
|
|
352
|
+
},
|
|
353
|
+
material,
|
|
354
|
+
elevation,
|
|
355
|
+
finish: {
|
|
356
|
+
program: def.finish?.program,
|
|
357
|
+
params: def.finish?.params,
|
|
358
|
+
textures: def.finish?.textures,
|
|
359
|
+
previous: def.finish?.previous,
|
|
360
|
+
vertexCount: def.finish?.vertexCount,
|
|
361
|
+
},
|
|
240
362
|
}
|
|
241
363
|
}
|
|
242
364
|
|
|
@@ -256,12 +378,15 @@ const DEFAULT: ThemeDefinition = {
|
|
|
256
378
|
// renders thin on low-DPI and its contrast depends on what sits behind
|
|
257
379
|
// it. Precomputed - core's mixColors delegates to flux:rendertree, and a
|
|
258
380
|
// preset is data that must not need the render engine at import time
|
|
259
|
-
// (the website token build imports this module headless).
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
381
|
+
// (the website token build imports this module headless). Both tones
|
|
382
|
+
// are tuned a step above the 4.5:1 AA floor against surfaceAlt, the
|
|
383
|
+
// least contrasting surface muted text sits on (list rows, tracks,
|
|
384
|
+
// fields) - tuning against `background` alone left them at 4.1:1 and
|
|
385
|
+
// 4.3:1 there. Light lands at 5.2:1 on surfaceAlt (6.0:1 on the
|
|
386
|
+
// background), dark at 5.4:1 (6.8:1), and both keep a clear step of
|
|
387
|
+
// de-emphasis under `text`. If a surface changes, recheck the WCAG
|
|
388
|
+
// ratios against the new surfaceAlt.
|
|
389
|
+
textMuted: ["#606366", "#939aa4"],
|
|
265
390
|
border: ["rgba(0,0,0,0.15)", "rgba(255,255,255,0.14)"],
|
|
266
391
|
// Accent tuned to the puzzle mark's mid blue.
|
|
267
392
|
primary: "#547ebf",
|
|
@@ -284,8 +409,30 @@ const DEFAULT: ThemeDefinition = {
|
|
|
284
409
|
text: { roles: { caption: { size: 12 } } },
|
|
285
410
|
}
|
|
286
411
|
|
|
412
|
+
// The lit presets: the same palette under a light, with a sheen and a bevel
|
|
413
|
+
// per role and the elevation levels raised. Strengths tuned by eye on a
|
|
414
|
+
// low-DPI display, dark and light: the accent faces catch the most light,
|
|
415
|
+
// overlays the least. The cap (Switch knob, Slider thumb) is a light face
|
|
416
|
+
// in both schemes, lit like any other. The shadow strength on dark is what
|
|
417
|
+
// a shadow needs there to read at all.
|
|
418
|
+
const LIT: ThemeDefinition = {
|
|
419
|
+
...DEFAULT,
|
|
420
|
+
color: { ...DEFAULT.color, thumb: "#f4f6fa" },
|
|
421
|
+
light: { ambient: 0.35 },
|
|
422
|
+
shadow: { strength: [1, 1.7] },
|
|
423
|
+
material: {
|
|
424
|
+
surface: { sheen: 0.5, bevel: 0.6 },
|
|
425
|
+
control: { sheen: 0.6, bevel: 0.7 },
|
|
426
|
+
accent: { sheen: 1, bevel: 0.9 },
|
|
427
|
+
overlay: { sheen: 0.4, bevel: 0.6 },
|
|
428
|
+
},
|
|
429
|
+
elevation: { flat: 0, raised: 2, floating: 6, overlay: 14 },
|
|
430
|
+
}
|
|
431
|
+
|
|
287
432
|
export let darkTheme: Theme = defineTheme(DEFAULT, "dark")
|
|
288
433
|
export let lightTheme: Theme = defineTheme(DEFAULT, "light")
|
|
434
|
+
export let litDarkTheme: Theme = defineTheme(LIT, "dark")
|
|
435
|
+
export let litLightTheme: Theme = defineTheme(LIT, "light")
|
|
289
436
|
|
|
290
437
|
let [themeStore, setThemeStore] = createStore<Theme>({ ...darkTheme })
|
|
291
438
|
|
|
@@ -300,7 +447,11 @@ type ThemePartial = { [K in keyof Theme]?: Partial<Theme[K]> }
|
|
|
300
447
|
// Switch themes with a full preset (setTheme(lightTheme)), a resolved
|
|
301
448
|
// definition (setTheme(defineTheme({...}))), or apply a targeted override
|
|
302
449
|
// (setTheme({ color: { primary: "#f00" } })). Merges one level deep per
|
|
303
|
-
// category (for components, that level is the component name
|
|
450
|
+
// category (for components, that level is the component name; for
|
|
451
|
+
// material, the role). A resolved theme carries every key of every
|
|
452
|
+
// category, unset ones as undefined, so it replaces the previous theme
|
|
453
|
+
// outright, per-component overrides and icon slots included; a partial
|
|
454
|
+
// keeps whatever it does not name.
|
|
304
455
|
export function setTheme(partial: ThemePartial) {
|
|
305
456
|
setThemeStore((s) => {
|
|
306
457
|
for (let key in partial) {
|
|
@@ -309,3 +460,13 @@ export function setTheme(partial: ThemePartial) {
|
|
|
309
460
|
}
|
|
310
461
|
})
|
|
311
462
|
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* A pill or circle radius that follows the theme: half of `size`, capped by
|
|
466
|
+
* `radius.full`, so a theme with `full: 0` squares every round part (Switch
|
|
467
|
+
* track and knob, Slider groove and thumb, Radio, ProgressBar) while the
|
|
468
|
+
* default 9999 leaves them round. For a custom control with a round part.
|
|
469
|
+
*/
|
|
470
|
+
export function pill(size: number): number {
|
|
471
|
+
return Math.min(theme.radius.full, size / 2)
|
|
472
|
+
}
|
package/src/tooltip.tsx
CHANGED
|
@@ -5,8 +5,9 @@ import { policy } from "./policy"
|
|
|
5
5
|
import { space } from "./spacing"
|
|
6
6
|
import { typeStyle } from "./typography"
|
|
7
7
|
import type { TransitionProps } from "./types"
|
|
8
|
-
import { splitTransition, transitionEndFor
|
|
8
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
9
9
|
import { colorFade, popupFadeOut } from "./motion"
|
|
10
|
+
import { Surface } from "./surface"
|
|
10
11
|
|
|
11
12
|
export interface TooltipProps extends TransitionProps {
|
|
12
13
|
// The tooltip body. A string/number renders as themed text; anything else
|
|
@@ -32,7 +33,8 @@ let margin = () => theme.spacing.sm
|
|
|
32
33
|
* mouse pointer on the wrapped content shows a bubble near it after a short
|
|
33
34
|
* delay. Under the touch policy it never shows, so tooltip content must stay
|
|
34
35
|
* non-essential. The bubble is portal-mounted at the window root and takes no
|
|
35
|
-
* pointer events; it hides on leave and on press.
|
|
36
|
+
* pointer events; it hides on leave and on press. Its bubble is an
|
|
37
|
+
* overlay face, floating.
|
|
36
38
|
*/
|
|
37
39
|
export function Tooltip(props: TooltipProps) {
|
|
38
40
|
let anchor: { id: number } | undefined
|
|
@@ -94,25 +96,27 @@ export function Tooltip(props: TooltipProps) {
|
|
|
94
96
|
opacity={pos() ? 1 : 0}
|
|
95
97
|
transition={popupFadeOut()}
|
|
96
98
|
>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
<Surface
|
|
100
|
+
role="overlay"
|
|
101
|
+
elevation={theme.components.tooltip?.elevation ?? "floating"}
|
|
102
|
+
fill={theme.components.tooltip?.backgroundColor ?? theme.color.surfaceAlt}
|
|
101
103
|
radius={theme.components.tooltip?.borderRadius ?? theme.radius.sm}
|
|
104
|
+
material={theme.components.tooltip?.material}
|
|
105
|
+
glow={theme.components.tooltip?.glow}
|
|
106
|
+
outline={{
|
|
107
|
+
color: theme.components.tooltip?.borderColor ?? theme.color.border,
|
|
108
|
+
width: theme.components.tooltip?.borderWidth ?? theme.borderWidth.sm,
|
|
109
|
+
}}
|
|
110
|
+
fillTransition={split().background}
|
|
111
|
+
onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
112
|
+
outlineTransition={split().border}
|
|
113
|
+
onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
102
114
|
/>
|
|
103
115
|
<Show when={isText()} fallback={content()}>
|
|
104
116
|
<text transition={colorFade()} color={theme.color.text} {...typeStyle("body")}>
|
|
105
117
|
{content()}
|
|
106
118
|
</text>
|
|
107
119
|
</Show>
|
|
108
|
-
<d-rect
|
|
109
|
-
drawStyle="stroke"
|
|
110
|
-
transition={withTransitionDefaults(split().border, colorFade())}
|
|
111
|
-
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
112
|
-
color={theme.components.tooltip?.borderColor ?? theme.color.border}
|
|
113
|
-
strokeWidth={theme.components.tooltip?.borderWidth ?? theme.borderWidth.sm}
|
|
114
|
-
radius={theme.components.tooltip?.borderRadius ?? theme.radius.sm}
|
|
115
|
-
/>
|
|
116
120
|
</view>,
|
|
117
121
|
)
|
|
118
122
|
}
|
package/src/types.ts
CHANGED
|
@@ -201,6 +201,56 @@ export function transitionEndFor<P extends string>(
|
|
|
201
201
|
// into Taffy and changing them triggers a relayout; style properties are
|
|
202
202
|
// paint-only and can change without affecting layout.
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Emitted light around a filled part: a zero-offset halo, `radius` logical
|
|
206
|
+
* px of blur. Without `color` the part glows in its own fill color (a danger
|
|
207
|
+
* button red, a primary one in the accent); a part whose fill has no color
|
|
208
|
+
* to take (a ghost button, a gradient) then casts nothing, so a theme-wide
|
|
209
|
+
* glow never grows a black halo. A layer of a Material; Theme["material"]
|
|
210
|
+
* says where the roles land.
|
|
211
|
+
*/
|
|
212
|
+
export type Glow = { radius: number; color?: Color }
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The four kinds of face a component's chrome can be. A component picks a
|
|
216
|
+
* role, never a material: the theme assigns each role its Material and the
|
|
217
|
+
* Surface primitive draws it. `surface` is what things sit on (Card, Item,
|
|
218
|
+
* the NavShell strips, the panes), `control` the body of a control (a
|
|
219
|
+
* secondary Button, a field, a Select trigger, the tracks and grooves),
|
|
220
|
+
* `accent` the accent-colored fills (a primary Button, Switch on, the
|
|
221
|
+
* Slider fill, a Badge) and `overlay` what floats over the screen (a
|
|
222
|
+
* Tooltip, a dropdown, a menu).
|
|
223
|
+
*/
|
|
224
|
+
export type MaterialRole = "surface" | "control" | "accent" | "overlay"
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* How far a face stands off the ground, as a named level the theme maps to
|
|
228
|
+
* a height (Theme["elevation"]): its shadows' offset and blur grow with it.
|
|
229
|
+
* `flat` casts nothing; Card and the raised parts of controls sit at
|
|
230
|
+
* `raised` by default, popups at `floating` or `overlay`.
|
|
231
|
+
*/
|
|
232
|
+
export type ElevationLevel = "flat" | "raised" | "floating" | "overlay"
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A frosted backdrop: the face blurs and saturates what is painted beneath
|
|
236
|
+
* it (a core `backdropFilter`) and paints `tint` over that, its own fill at
|
|
237
|
+
* a translucency when unset. A deliberate overlay, never a surface material:
|
|
238
|
+
* every glass face captures the pixels under it each frame it paints, which
|
|
239
|
+
* a tiled GPU pays for per panel (okf/notes/backdrop-filter-cost.md).
|
|
240
|
+
*/
|
|
241
|
+
export type Glass = { blur: number; tint?: Color }
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The stack of optional layers a face is drawn with; `{}` is the flat look,
|
|
245
|
+
* a plain fill. `sheen` (0..1) is a fill gradient along the theme light,
|
|
246
|
+
* lit edge to shaded edge; `bevel` (0..1) a one-pixel stroke from the light
|
|
247
|
+
* color on the lit edge to black on the shaded one; `glass` the frosted
|
|
248
|
+
* backdrop; `glow` the emitted-light halo. Layers compose: glass with a
|
|
249
|
+
* glowing edge is two layers, not a fifth kind. Shadows are not a layer,
|
|
250
|
+
* they follow the elevation.
|
|
251
|
+
*/
|
|
252
|
+
export type Material = { sheen?: number; bevel?: number; glass?: Glass; glow?: Glow }
|
|
253
|
+
|
|
204
254
|
// Paint-only props. None of these change the box Taffy computes. Borders are
|
|
205
255
|
// drawn as a stroke overlay (not part of the box model), and the transform is
|
|
206
256
|
// applied at paint time, so both live here.
|
|
@@ -231,13 +281,27 @@ export interface StyleProps {
|
|
|
231
281
|
// clip, scroll); a single number or [tl, tr, br, bl].
|
|
232
282
|
clipRadius?: number | [number, number, number, number]
|
|
233
283
|
opacity?: number
|
|
284
|
+
// A halo on the component's accent part: the fill of Button, Badge,
|
|
285
|
+
// Checkbox and Switch, the Slider and ProgressBar fills, the Radio dot,
|
|
286
|
+
// the SegmentedControl indicator. Unset, the role's material glow applies
|
|
287
|
+
// (theme.material); null clears it here.
|
|
288
|
+
glow?: Glow | null
|
|
289
|
+
// Layers merged over the role's material for this component's faces (a
|
|
290
|
+
// matte card in a lit theme: `material: { sheen: 0, bevel: 0 }`).
|
|
291
|
+
material?: Partial<Material>
|
|
292
|
+
// The elevation of the component's raised face (a Card, a Button, an
|
|
293
|
+
// Item), replacing its default level: `elevation: "flat"` grounds a card,
|
|
294
|
+
// `"raised"` lifts a list row (priced in okf/notes/lit-list-cost.md).
|
|
295
|
+
elevation?: ElevationLevel
|
|
234
296
|
}
|
|
235
297
|
|
|
236
298
|
// One choice in an options list, shared by the single-choice controls
|
|
237
299
|
// (Select, SegmentedControl). Lives here so those components stay independent
|
|
238
300
|
// of each other: shared shapes go through this module, never a sibling import.
|
|
239
|
-
|
|
240
|
-
|
|
301
|
+
// Generic in the value, so a control's `onChange` hands back the type its
|
|
302
|
+
// options were declared with.
|
|
303
|
+
export interface Option<T = unknown> {
|
|
304
|
+
value: T
|
|
241
305
|
label: string
|
|
242
306
|
}
|
|
243
307
|
|
package/src/window.tsx
CHANGED
|
@@ -1,23 +1,46 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { displayScale } from "@solidrt/core"
|
|
2
|
+
import type { LayoutProps, PointerProps, WindowShaderProps } from "@solidrt/core"
|
|
2
3
|
import type { StyleProps } from "./types"
|
|
3
4
|
import { colorFade } from "./motion"
|
|
5
|
+
import { theme } from "./theme"
|
|
4
6
|
|
|
5
7
|
export interface WindowProps extends PointerProps {
|
|
6
8
|
children?: any
|
|
7
9
|
title?: string
|
|
8
10
|
fullscreen?: boolean
|
|
11
|
+
// The window's own finish, a core window shader declaration forwarded
|
|
12
|
+
// as-is. Unset, the window takes the theme's finish (theme.finish);
|
|
13
|
+
// null opts out of that too.
|
|
14
|
+
shader?: WindowShaderProps | null
|
|
9
15
|
layout?: LayoutProps
|
|
10
16
|
style?: StyleProps
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
// The theme's finish: its declaration with uScale (the display scale, a
|
|
20
|
+
// per-client fact the theme cannot know; for effects counted in logical
|
|
21
|
+
// px) merged into the params, or nothing while the theme has no program.
|
|
22
|
+
function themeFinish(): WindowShaderProps | null {
|
|
23
|
+
let f = theme.finish
|
|
24
|
+
if (f.program == null) return null
|
|
25
|
+
return {
|
|
26
|
+
program: f.program,
|
|
27
|
+
params: { ...f.params, uScale: displayScale() },
|
|
28
|
+
textures: f.textures,
|
|
29
|
+
previous: f.previous,
|
|
30
|
+
vertexCount: f.vertexCount,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
// A window is the root surface: it can't be transformed or bordered, so only
|
|
14
|
-
// the paint-only backgroundColor from style applies here.
|
|
35
|
+
// the paint-only backgroundColor from style applies here. Its finish is the
|
|
36
|
+
// theme's, or the app's own (`shader`).
|
|
15
37
|
export function Window(props: WindowProps) {
|
|
16
38
|
return (
|
|
17
39
|
<window
|
|
18
40
|
{...props.layout}
|
|
19
41
|
title={props.title}
|
|
20
42
|
fullscreen={props.fullscreen}
|
|
43
|
+
shader={props.shader === undefined ? themeFinish() : props.shader}
|
|
21
44
|
onPointerEnter={props.onPointerEnter}
|
|
22
45
|
onPointerLeave={props.onPointerLeave}
|
|
23
46
|
onPointerDown={props.onPointerDown}
|
|
@@ -37,4 +60,4 @@ export function Window(props: WindowProps) {
|
|
|
37
60
|
{props.children}
|
|
38
61
|
</window>
|
|
39
62
|
)
|
|
40
|
-
}
|
|
63
|
+
}
|