@solidrt/components 0.0.50 → 0.0.52
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 +93 -85
- package/README.md +421 -311
- package/demos/README.md +24 -0
- package/demos/assets/icon.png +0 -0
- package/demos/assets/icon.svg +23 -0
- package/demos/package.json +9 -0
- package/demos/src/gallery.tsx +866 -0
- package/demos/tsconfig.json +15 -0
- package/docs/badge.md +10 -0
- package/docs/button.md +21 -0
- package/docs/card.md +11 -0
- package/docs/checkbox.md +9 -0
- package/docs/context-menu.md +17 -0
- package/docs/density.md +13 -0
- package/docs/divider.md +10 -0
- package/docs/field.md +11 -0
- package/docs/focus-nav.md +18 -0
- package/docs/icon.md +13 -0
- package/docs/image.md +21 -0
- package/docs/index.md +13 -0
- package/docs/item.md +25 -0
- package/docs/modal.md +15 -0
- package/docs/nav-shell.md +18 -0
- package/docs/policy.md +21 -0
- package/docs/portal.md +15 -0
- package/docs/pressable.md +17 -0
- package/docs/progress-bar.md +10 -0
- package/docs/qrcode.md +10 -0
- package/docs/radio.md +13 -0
- package/docs/rich-text-document.md +5 -0
- package/docs/rich-text-editor.md +24 -0
- package/docs/safe-area.md +12 -0
- package/docs/scroll-view.md +35 -0
- package/docs/segmented-control.md +13 -0
- package/docs/select.md +15 -0
- package/docs/slider.md +9 -0
- package/docs/spacing.md +7 -0
- package/docs/spinner.md +10 -0
- package/docs/split-view.md +14 -0
- package/docs/switch.md +13 -0
- package/docs/text-input.md +23 -0
- package/docs/text.md +15 -0
- package/docs/theme.md +68 -0
- package/docs/tooltip.md +11 -0
- package/docs/types.md +9 -0
- package/docs/typography.md +5 -0
- package/docs/view.md +14 -0
- package/docs/window.md +15 -0
- package/package.json +8 -4
- package/src/badge.tsx +30 -21
- package/src/button.tsx +50 -32
- package/src/card.tsx +22 -13
- package/src/checkbox.tsx +29 -11
- package/src/context-menu.tsx +14 -9
- package/src/density.tsx +39 -0
- package/src/divider.tsx +11 -4
- package/src/editor-field.tsx +368 -0
- package/src/field.tsx +47 -0
- package/src/icon.tsx +8 -4
- package/src/image.tsx +10 -3
- package/src/index.ts +19 -2
- package/src/item.tsx +121 -0
- package/src/nav-shell.tsx +7 -17
- package/src/policy.ts +9 -12
- package/src/press.ts +37 -8
- package/src/pressable.tsx +15 -3
- package/src/progress-bar.tsx +8 -5
- package/src/qrcode.tsx +7 -5
- package/src/radio.tsx +26 -16
- package/src/rich-text-document.ts +247 -0
- package/src/rich-text-editor.tsx +151 -0
- package/src/scroll-view.tsx +76 -7
- package/src/segmented-control.tsx +33 -19
- package/src/select.tsx +59 -30
- package/src/slider.tsx +32 -6
- package/src/spacing.ts +1 -1
- package/src/spinner.tsx +11 -6
- package/src/split-view.tsx +3 -4
- package/src/switch.tsx +10 -3
- package/src/text-input.tsx +54 -264
- package/src/text.tsx +22 -2
- package/src/theme.ts +220 -81
- package/src/tooltip.tsx +23 -9
- package/src/types.ts +119 -1
- package/src/view.tsx +11 -2
package/src/theme.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createStore
|
|
1
|
+
import { createStore } from "@solidjs/signals"
|
|
2
|
+
import type { StyleProps } from "./types"
|
|
2
3
|
|
|
3
4
|
export type TextStyle = {
|
|
4
5
|
size: number
|
|
@@ -9,10 +10,36 @@ export type TextStyle = {
|
|
|
9
10
|
// The type scale's role names. <Text variant> and theme.text are keyed by these.
|
|
10
11
|
export type TextVariant = "caption" | "label" | "body" | "title" | "heading"
|
|
11
12
|
|
|
13
|
+
// The components whose chrome accepts a theme-level paint override (see
|
|
14
|
+
// Theme["components"]). Plain containers (View, Pressable, ...) are not
|
|
15
|
+
// themed, so they take no override either.
|
|
16
|
+
export type ThemedComponent =
|
|
17
|
+
| "button"
|
|
18
|
+
| "card"
|
|
19
|
+
| "badge"
|
|
20
|
+
| "switch"
|
|
21
|
+
| "checkbox"
|
|
22
|
+
| "radio"
|
|
23
|
+
| "item"
|
|
24
|
+
| "select"
|
|
25
|
+
| "segmentedControl"
|
|
26
|
+
| "textInput"
|
|
27
|
+
| "richTextEditor"
|
|
28
|
+
| "tooltip"
|
|
29
|
+
| "divider"
|
|
30
|
+
| "progressBar"
|
|
31
|
+
| "spinner"
|
|
32
|
+
|
|
33
|
+
// A resolved theme: every value is a plain string/number ready to be read by
|
|
34
|
+
// a component. Authoring happens through defineTheme, which is where
|
|
35
|
+
// [light, dark] pairs and the type-scale expansion live; a Theme itself has
|
|
36
|
+
// no notion of modes.
|
|
12
37
|
export type Theme = {
|
|
13
38
|
text: {
|
|
14
39
|
// Passed through to the core font stack: "sans" | "mono" | a family name.
|
|
15
40
|
fontFamily: string
|
|
41
|
+
// The monospace family for code (RichTextEditor inline code).
|
|
42
|
+
monoFamily: string
|
|
16
43
|
caption: TextStyle
|
|
17
44
|
label: TextStyle
|
|
18
45
|
body: TextStyle
|
|
@@ -26,117 +53,229 @@ export type Theme = {
|
|
|
26
53
|
surface: string
|
|
27
54
|
// Subtle raised/track fill (switch off-state, slider track, ...).
|
|
28
55
|
surfaceAlt: string
|
|
29
|
-
// Hover tint for surface-colored controls (non-touch interaction policies).
|
|
30
|
-
surfaceHover: string
|
|
31
56
|
text: string
|
|
32
57
|
textMuted: string
|
|
33
58
|
border: string
|
|
34
59
|
primary: string
|
|
35
|
-
// Hover tint for primary-colored controls.
|
|
36
|
-
primaryHover: string
|
|
37
60
|
onPrimary: string
|
|
38
61
|
// Lower-emphasis accent: the puzzle mark's darker blue.
|
|
39
62
|
secondary: string
|
|
40
|
-
// Hover tint for secondary-colored controls.
|
|
41
|
-
secondaryHover: string
|
|
42
63
|
onSecondary: string
|
|
43
64
|
// Validation / destructive.
|
|
44
65
|
danger: string
|
|
45
|
-
// Hover tint for danger-colored controls.
|
|
46
|
-
dangerHover: string
|
|
47
66
|
// Overlay dim behind modals.
|
|
48
67
|
scrim: string
|
|
68
|
+
// Hover/pressed feedback tints: translucent colors DRAWN OVER a control's
|
|
69
|
+
// own fill (one token pair for every control, instead of a hover variant
|
|
70
|
+
// per fill color), so feedback works over any background, including a
|
|
71
|
+
// caller-set style.backgroundColor. Non-touch interaction policies only.
|
|
72
|
+
overlayHover: string
|
|
73
|
+
overlayPressed: string
|
|
74
|
+
// The focus ring (spatial nav under the focusRing policy), drawn at
|
|
75
|
+
// borderWidth.focus by every focusable control. Defaults to text so it
|
|
76
|
+
// stays visible on primary-filled controls.
|
|
77
|
+
ring: string
|
|
49
78
|
}
|
|
79
|
+
// Gaps and paddings, multiples of one base unit (sm 1x, md 2x, lg 4x,
|
|
80
|
+
// xl 5x); read through space() where density should apply.
|
|
50
81
|
spacing: { sm: number; md: number; lg: number; xl: number }
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
// Corner radii. md is THE control radius (Button, TextInput, Select,
|
|
83
|
+
// SegmentedControl); sm is one step under it (Checkbox, Item, menus,
|
|
84
|
+
// Tooltip), lg one step over (Card), full is the pill.
|
|
85
|
+
radius: { sm: number; md: number; lg: number; full: number }
|
|
86
|
+
borderWidth: { sm: number; focus: number }
|
|
87
|
+
// Default extents of the components that have one: the panes and rails an
|
|
88
|
+
// app lays its screens around, and the smallest sensible popup/track. Each
|
|
89
|
+
// is a per-instance layout override away (listWidth, layout.width, ...);
|
|
90
|
+
// the theme sets the app-wide default.
|
|
91
|
+
size: { navRail: number; navSidebar: number; splitViewList: number; menuMinWidth: number; slider: number }
|
|
92
|
+
// Semantic control glyphs, as SVG document strings (the Icon currency).
|
|
93
|
+
// Components draw their built-in vector paths by default; a theme that sets
|
|
94
|
+
// a slot swaps that glyph everywhere it appears. The package still bundles
|
|
95
|
+
// no icon set.
|
|
96
|
+
icons: { chevronDown?: string; check?: string }
|
|
97
|
+
// Per-component paint overrides: merged between a component's themed
|
|
98
|
+
// defaults and the instance's style prop, so a theme can restyle every
|
|
99
|
+
// Button (say, pill corners) without wrapping the component. Instance
|
|
100
|
+
// style still wins.
|
|
101
|
+
components: { [K in ThemedComponent]?: StyleProps }
|
|
53
102
|
}
|
|
54
103
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
// -- Authoring ---------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
// A color in a theme definition: one value, or a [light, dark] pair resolved
|
|
107
|
+
// by defineTheme's scheme argument. Pairs are opt-in per token; a definition
|
|
108
|
+
// without any needs no scheme at all (a game ships one look, not two).
|
|
109
|
+
export type ThemeColor = string | [light: string, dark: string]
|
|
110
|
+
|
|
111
|
+
export type ThemeDefinition = {
|
|
112
|
+
color: { [K in Exclude<keyof Theme["color"], "ring">]: ThemeColor } & { ring?: ThemeColor }
|
|
113
|
+
text?: {
|
|
114
|
+
fontFamily?: string
|
|
115
|
+
monoFamily?: string
|
|
116
|
+
// The body font size; the other roles derive from it. Default 14.
|
|
117
|
+
base?: number
|
|
118
|
+
// The step between adjacent roles (caption, label, body, title, heading
|
|
119
|
+
// sit at base * ratio^(-2..2), rounded to whole px). Default 1.26.
|
|
120
|
+
ratio?: number
|
|
121
|
+
// Per-role overrides of the derived size and the default line heights
|
|
122
|
+
// and weights.
|
|
123
|
+
roles?: { [K in TextVariant]?: Partial<TextStyle> }
|
|
124
|
+
}
|
|
125
|
+
// One base unit (the steps derive from it, see deriveSpacing) or explicit
|
|
126
|
+
// steps. Default 4.
|
|
127
|
+
spacing?: number | Partial<Theme["spacing"]>
|
|
128
|
+
// One base (the control radius; the steps derive from it, see
|
|
129
|
+
// deriveRadius) or explicit steps. Default 8.
|
|
130
|
+
radius?: number | Partial<Theme["radius"]>
|
|
131
|
+
borderWidth?: Partial<Theme["borderWidth"]>
|
|
132
|
+
size?: Partial<Theme["size"]>
|
|
133
|
+
icons?: Theme["icons"]
|
|
134
|
+
components?: Theme["components"]
|
|
65
135
|
}
|
|
66
|
-
const SPACING = { sm: 4, md: 8, lg: 16, xl: 20 }
|
|
67
|
-
const RADIUS = { sm: 4, md: 8, lg: 12 }
|
|
68
|
-
const BORDER_WIDTH = { sm: 1 }
|
|
69
136
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
137
|
+
const SPACING_BASE = 4
|
|
138
|
+
|
|
139
|
+
// The spacing scale from its base unit.
|
|
140
|
+
function deriveSpacing(base: number): Theme["spacing"] {
|
|
141
|
+
return { sm: base, md: base * 2, lg: base * 4, xl: base * 5 }
|
|
142
|
+
}
|
|
143
|
+
const RADIUS_BASE = 8
|
|
144
|
+
const RADIUS_FULL = 9999
|
|
145
|
+
|
|
146
|
+
// The radius scale from its base: sm half, lg one and a half, full the pill.
|
|
147
|
+
function deriveRadius(base: number): Theme["radius"] {
|
|
148
|
+
return { sm: Math.round(base / 2), md: base, lg: Math.round(base * 1.5), full: RADIUS_FULL }
|
|
149
|
+
}
|
|
150
|
+
const BORDER_WIDTH = { sm: 1, focus: 2 }
|
|
151
|
+
const SIZE = { navRail: 72, navSidebar: 220, splitViewList: 320, menuMinWidth: 120, slider: 200 }
|
|
152
|
+
|
|
153
|
+
// Line height and weight per role; body is the base text, label is body at
|
|
154
|
+
// an emphasized weight (form labels, key/value keys, tags), caption the one
|
|
155
|
+
// small role (badges, tab labels, timestamps), title and heading sit above
|
|
156
|
+
// body (card and page headings). De-emphasis is a color (textMuted), not a
|
|
157
|
+
// size: caption is small text to be glanced at, so keep it in the full text
|
|
158
|
+
// color rather than stacking small and muted.
|
|
159
|
+
const ROLE_DEFAULTS: { [K in TextVariant]: { step: number; lineHeight: number; weight: TextStyle["weight"] } } = {
|
|
160
|
+
caption: { step: -1, lineHeight: 1.3, weight: 400 },
|
|
161
|
+
label: { step: 0, lineHeight: 1.5, weight: 600 },
|
|
162
|
+
body: { step: 0, lineHeight: 1.5, weight: 400 },
|
|
163
|
+
title: { step: 1, lineHeight: 1.4, weight: 700 },
|
|
164
|
+
heading: { step: 2, lineHeight: 1.3, weight: 700 },
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolves a theme definition into a Theme. `scheme` picks the side of every
|
|
169
|
+
* [light, dark] color pair; a definition without pairs needs no scheme
|
|
170
|
+
* (modes are a per-theme choice, not a framework requirement). The type
|
|
171
|
+
* scale expands from text.base and text.ratio, with text.roles overriding
|
|
172
|
+
* per role. Throws on a pair without a scheme (throw-in-dev policy).
|
|
173
|
+
*/
|
|
174
|
+
export function defineTheme(def: ThemeDefinition, scheme?: "light" | "dark"): Theme {
|
|
175
|
+
let color = {} as Theme["color"]
|
|
176
|
+
for (let key in def.color) {
|
|
177
|
+
let k = key as keyof Theme["color"]
|
|
178
|
+
let value = def.color[k]
|
|
179
|
+
if (value == null) continue
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
if (!scheme) throw new Error(`Theme color "${key}" is a [light, dark] pair; pass a scheme to defineTheme`)
|
|
182
|
+
color[k] = value[scheme === "light" ? 0 : 1]
|
|
183
|
+
} else color[k] = value
|
|
184
|
+
}
|
|
185
|
+
if (def.color.ring == null) color.ring = color.text
|
|
186
|
+
let base = def.text?.base ?? 14
|
|
187
|
+
let ratio = def.text?.ratio ?? 1.26
|
|
188
|
+
let role = (name: TextVariant): TextStyle => {
|
|
189
|
+
let d = ROLE_DEFAULTS[name]
|
|
190
|
+
return {
|
|
191
|
+
size: Math.round(base * ratio ** d.step),
|
|
192
|
+
lineHeight: d.lineHeight,
|
|
193
|
+
weight: d.weight,
|
|
194
|
+
...def.text?.roles?.[name],
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
text: {
|
|
199
|
+
fontFamily: def.text?.fontFamily ?? "sans",
|
|
200
|
+
monoFamily: def.text?.monoFamily ?? "mono",
|
|
201
|
+
caption: role("caption"),
|
|
202
|
+
label: role("label"),
|
|
203
|
+
body: role("body"),
|
|
204
|
+
title: role("title"),
|
|
205
|
+
heading: role("heading"),
|
|
206
|
+
},
|
|
207
|
+
color,
|
|
208
|
+
spacing:
|
|
209
|
+
typeof def.spacing === "number"
|
|
210
|
+
? deriveSpacing(def.spacing)
|
|
211
|
+
: { ...deriveSpacing(SPACING_BASE), ...def.spacing },
|
|
212
|
+
radius:
|
|
213
|
+
typeof def.radius === "number" ? deriveRadius(def.radius) : { ...deriveRadius(RADIUS_BASE), ...def.radius },
|
|
214
|
+
borderWidth: { ...BORDER_WIDTH, ...def.borderWidth },
|
|
215
|
+
size: { ...SIZE, ...def.size },
|
|
216
|
+
icons: def.icons ?? {},
|
|
217
|
+
components: def.components ?? {},
|
|
218
|
+
}
|
|
99
219
|
}
|
|
100
220
|
|
|
101
|
-
|
|
102
|
-
|
|
221
|
+
// -- The built-in presets: one definition, resolved twice ---------------------
|
|
222
|
+
|
|
223
|
+
const DEFAULT: ThemeDefinition = {
|
|
103
224
|
color: {
|
|
104
|
-
background: "#ffffff",
|
|
105
|
-
surface: "#f6f8fa",
|
|
106
|
-
surfaceAlt: "#eaeef2",
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
225
|
+
background: ["#ffffff", "#0b0f17"],
|
|
226
|
+
surface: ["#f6f8fa", "#161b22"],
|
|
227
|
+
surfaceAlt: ["#eaeef2", "#21262d"],
|
|
228
|
+
// Dark body text sits well under white (about 10:1 on the background,
|
|
229
|
+
// tuned by eye on a low-DPI display): full brightness glares on a dark
|
|
230
|
+
// ground, and the low-DPI weight compensation thickens it further.
|
|
231
|
+
text: ["#1f2328", "#b1bac4"],
|
|
232
|
+
// Muted is an opaque tone between text and background, mixed in oklab
|
|
233
|
+
// (like Material 3's tonal colors, not an alpha overlay): alpha text
|
|
234
|
+
// renders thin on low-DPI and its contrast depends on what sits behind
|
|
235
|
+
// it. Precomputed - core's mixColors delegates to flux:rendertree, and a
|
|
236
|
+
// preset is data that must not need the render engine at import time
|
|
237
|
+
// (the website token build imports this module headless). If text or
|
|
238
|
+
// background changes, recompute: mixColors(text, background, 0.4).
|
|
239
|
+
// The dark tone sits a step above the 4.5:1 AA floor (about 5.4:1)
|
|
240
|
+
// instead of at the mix: the body text already sits low, and the
|
|
241
|
+
// strict mix falls under it.
|
|
242
|
+
textMuted: ["#707376", "#828993"],
|
|
243
|
+
border: ["rgba(0,0,0,0.15)", "rgba(255,255,255,0.14)"],
|
|
244
|
+
// Accent tuned to the puzzle mark's mid blue.
|
|
113
245
|
primary: "#547ebf",
|
|
114
|
-
primaryHover: "#3f5494",
|
|
115
246
|
onPrimary: "#ffffff",
|
|
116
|
-
// The darker shade of the same puzzle segment
|
|
247
|
+
// The darker shade of the same puzzle segment.
|
|
117
248
|
secondary: "#2b5696",
|
|
118
|
-
secondaryHover: "#1f4176",
|
|
119
249
|
onSecondary: "#ffffff",
|
|
120
|
-
danger: "#cf222e",
|
|
121
|
-
|
|
122
|
-
|
|
250
|
+
danger: ["#cf222e", "#f85149"],
|
|
251
|
+
scrim: ["rgba(0,0,0,0.4)", "rgba(0,0,0,0.6)"],
|
|
252
|
+
// Feedback darkens on a light scheme and lightens on a dark one.
|
|
253
|
+
overlayHover: ["rgba(0,0,0,0.08)", "rgba(255,255,255,0.08)"],
|
|
254
|
+
overlayPressed: ["rgba(0,0,0,0.14)", "rgba(255,255,255,0.14)"],
|
|
123
255
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
256
|
+
// base 14 and ratio 1.26 derive title 18 and heading 22; caption sits
|
|
257
|
+
// one step under body but the ratio lands on 11, too small to read on a
|
|
258
|
+
// low-DPI display or at TV distance, so it is pinned.
|
|
259
|
+
text: { roles: { caption: { size: 12 } } },
|
|
127
260
|
}
|
|
128
261
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let [
|
|
133
|
-
|
|
262
|
+
export let darkTheme: Theme = defineTheme(DEFAULT, "dark")
|
|
263
|
+
export let lightTheme: Theme = defineTheme(DEFAULT, "light")
|
|
264
|
+
|
|
265
|
+
let [themeStore, setThemeStore] = createStore<Theme>({ ...darkTheme })
|
|
266
|
+
|
|
267
|
+
// The shared theme, backed by a Solid store so reads are tracked: calling
|
|
268
|
+
// setTheme at runtime recolors the live UI without remounting. Components
|
|
269
|
+
// read theme.* through thunks/JSX expressions, so they pick this up with no
|
|
270
|
+
// call-site changes.
|
|
271
|
+
export let theme: Theme = themeStore
|
|
134
272
|
|
|
135
273
|
type ThemePartial = { [K in keyof Theme]?: Partial<Theme[K]> }
|
|
136
274
|
|
|
137
|
-
// Switch themes with a full preset (setTheme(lightTheme))
|
|
138
|
-
//
|
|
139
|
-
//
|
|
275
|
+
// Switch themes with a full preset (setTheme(lightTheme)), a resolved
|
|
276
|
+
// definition (setTheme(defineTheme({...}))), or apply a targeted override
|
|
277
|
+
// (setTheme({ color: { primary: "#f00" } })). Merges one level deep per
|
|
278
|
+
// category (for components, that level is the component name).
|
|
140
279
|
export function setTheme(partial: ThemePartial) {
|
|
141
280
|
setThemeStore((s) => {
|
|
142
281
|
for (let key in partial) {
|
package/src/tooltip.tsx
CHANGED
|
@@ -4,8 +4,10 @@ import { theme } from "./theme"
|
|
|
4
4
|
import { policy } from "./policy"
|
|
5
5
|
import { space } from "./spacing"
|
|
6
6
|
import { typeStyle } from "./typography"
|
|
7
|
+
import type { TransitionProps } from "./types"
|
|
8
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
7
9
|
|
|
8
|
-
export interface TooltipProps {
|
|
10
|
+
export interface TooltipProps extends TransitionProps {
|
|
9
11
|
// The tooltip body. A string/number renders as themed text; anything else
|
|
10
12
|
// renders as-is.
|
|
11
13
|
content?: any
|
|
@@ -19,9 +21,10 @@ export interface TooltipProps {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const DELAY = 500
|
|
22
|
-
|
|
24
|
+
// Distance between the anchor and the bubble.
|
|
25
|
+
let gap = () => theme.spacing.sm
|
|
23
26
|
// Minimum distance kept between the bubble and the window edges.
|
|
24
|
-
|
|
27
|
+
let margin = () => theme.spacing.sm
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* A hover-only affordance: under desktop/hybrid interaction policies, resting a
|
|
@@ -61,8 +64,8 @@ export function Tooltip(props: TooltipProps) {
|
|
|
61
64
|
let b = bubble && getBoundingBox(bubble)
|
|
62
65
|
if (!a || !b) return
|
|
63
66
|
let x = a.x + a.width / 2 - b.width / 2
|
|
64
|
-
x = Math.round(Math.min(Math.max(x,
|
|
65
|
-
let y = Math.round(props.placement === "bottom" ? a.y + a.height +
|
|
67
|
+
x = Math.round(Math.min(Math.max(x, margin()), env.windowSize.width - b.width - margin()))
|
|
68
|
+
let y = Math.round(props.placement === "bottom" ? a.y + a.height + gap() : a.y - b.height - gap())
|
|
66
69
|
let cur = pos()
|
|
67
70
|
if (!cur || cur.x !== x || cur.y !== y) setPos({ x, y })
|
|
68
71
|
})
|
|
@@ -85,7 +88,12 @@ export function Tooltip(props: TooltipProps) {
|
|
|
85
88
|
paddingRight={space("md")}
|
|
86
89
|
pointerEvents="none"
|
|
87
90
|
>
|
|
88
|
-
<d-rect
|
|
91
|
+
<d-rect
|
|
92
|
+
transition={split().background}
|
|
93
|
+
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
94
|
+
color={theme.components.tooltip?.backgroundColor ?? theme.color.surfaceAlt}
|
|
95
|
+
radius={theme.components.tooltip?.borderRadius ?? theme.radius.sm}
|
|
96
|
+
/>
|
|
89
97
|
<Show when={isText()} fallback={content()}>
|
|
90
98
|
<text color={theme.color.text} {...typeStyle("body")}>
|
|
91
99
|
{content()}
|
|
@@ -93,16 +101,22 @@ export function Tooltip(props: TooltipProps) {
|
|
|
93
101
|
</Show>
|
|
94
102
|
<d-rect
|
|
95
103
|
drawStyle="stroke"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
transition={split().border}
|
|
105
|
+
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
106
|
+
color={theme.components.tooltip?.borderColor ?? theme.color.border}
|
|
107
|
+
strokeWidth={theme.components.tooltip?.borderWidth ?? theme.borderWidth.sm}
|
|
108
|
+
radius={theme.components.tooltip?.borderRadius ?? theme.radius.sm}
|
|
99
109
|
/>
|
|
100
110
|
</view>,
|
|
101
111
|
)
|
|
102
112
|
}
|
|
103
113
|
|
|
114
|
+
let split = () => splitTransition(props.transition)
|
|
115
|
+
|
|
104
116
|
return (
|
|
105
117
|
<view
|
|
118
|
+
transition={split().root}
|
|
119
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
106
120
|
ref={(n: { id: number }) => (anchor = n)}
|
|
107
121
|
onPointerEnter={enter}
|
|
108
122
|
onPointerLeave={hide}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,122 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Color,
|
|
3
|
+
Gradient,
|
|
4
|
+
LayoutProps,
|
|
5
|
+
Transition,
|
|
6
|
+
TransitionEndEvent,
|
|
7
|
+
TransitionPropName,
|
|
8
|
+
TransitionProps as CoreTransitionProps,
|
|
9
|
+
} from "@solidrt/core"
|
|
10
|
+
|
|
11
|
+
// Native transitions through a component. A component is a root view plus
|
|
12
|
+
// the nodes it draws for `style` (a background d-rect, a stroke d-rect for
|
|
13
|
+
// the border), so a declaration names the STYLE properties - backgroundColor,
|
|
14
|
+
// borderColor, borderWidth, borderRadius - next to the view-level ones, and
|
|
15
|
+
// splitTransition hands each entry to the node that owns it. Core's paint
|
|
16
|
+
// names (color, radius, strokeWidth) are deliberately not accepted: on a
|
|
17
|
+
// component they would land on the root view, which has no paint, and be
|
|
18
|
+
// silently ignored.
|
|
19
|
+
//
|
|
20
|
+
// Controls whose paint is their own (Switch knob, Slider thumb, Checkbox
|
|
21
|
+
// mark, ...) take the view-level entries only for now; see
|
|
22
|
+
// okf/backlog/component-transitions-internal-paint.md.
|
|
23
|
+
export type TransitionViewProp =
|
|
24
|
+
| "opacity"
|
|
25
|
+
| "x"
|
|
26
|
+
| "y"
|
|
27
|
+
| "scale"
|
|
28
|
+
| "scaleX"
|
|
29
|
+
| "scaleY"
|
|
30
|
+
| "rotate"
|
|
31
|
+
| "rotateX"
|
|
32
|
+
| "rotateY"
|
|
33
|
+
| "originX"
|
|
34
|
+
| "originY"
|
|
35
|
+
| "perspective"
|
|
36
|
+
| "clipRadius"
|
|
37
|
+
export type TransitionStyleProp = "backgroundColor" | "borderColor" | "borderWidth" | "borderRadius"
|
|
38
|
+
export type TransitionScrollProp = "scrollX" | "scrollY"
|
|
39
|
+
|
|
40
|
+
type TransitionEntries<P extends string> = {
|
|
41
|
+
/** One spec for every property the component animates (no `from`/`exit` here, as in core). */
|
|
42
|
+
all?: Transition
|
|
43
|
+
/** Group stagger (ms) for descendant enter/exit animations; see core. */
|
|
44
|
+
stagger?: number
|
|
45
|
+
} & { [K in P]?: Transition }
|
|
46
|
+
|
|
47
|
+
/** A component's transition declaration: view-level and style names, `all`, or a shorthand string. */
|
|
48
|
+
export type ComponentTransition<P extends string = TransitionViewProp | TransitionStyleProp> =
|
|
49
|
+
| TransitionEntries<P>
|
|
50
|
+
| string
|
|
51
|
+
| null
|
|
52
|
+
|
|
53
|
+
export interface TransitionProps<P extends string = TransitionViewProp | TransitionStyleProp> {
|
|
54
|
+
/**
|
|
55
|
+
* Animate later writes of the listed properties instead of snapping, like
|
|
56
|
+
* the core `transition` prop but in the component's own vocabulary:
|
|
57
|
+
* `transition={{ opacity: "200ms ease-out", backgroundColor: { duration: 300 } }}`.
|
|
58
|
+
* `all` or a bare string covers every property the component animates.
|
|
59
|
+
*/
|
|
60
|
+
transition?: ComponentTransition<P>
|
|
61
|
+
/** A transition settled; `property` is in the component's vocabulary (`backgroundColor`, not `color`). */
|
|
62
|
+
onTransitionEnd?: (event: { property: P }) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The core transition declarations for a component's root view and its background/border rects. */
|
|
66
|
+
export type SplitTransition = {
|
|
67
|
+
root: CoreTransitionProps["transition"]
|
|
68
|
+
background: CoreTransitionProps["transition"]
|
|
69
|
+
border: CoreTransitionProps["transition"]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const STYLE_TO_BACKGROUND: Record<string, TransitionPropName> = { backgroundColor: "color", borderRadius: "radius" }
|
|
73
|
+
const STYLE_TO_BORDER: Record<string, TransitionPropName> = {
|
|
74
|
+
borderColor: "color",
|
|
75
|
+
borderWidth: "strokeWidth",
|
|
76
|
+
borderRadius: "radius",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Map a component declaration onto the three nodes a styled component
|
|
81
|
+
* draws. `all`, a shorthand string and `stagger` apply to every node; named
|
|
82
|
+
* entries go to the node owning the property. An entry for a node that is
|
|
83
|
+
* not mounted (no background set) simply never animates.
|
|
84
|
+
*/
|
|
85
|
+
export function splitTransition<P extends string>(t: ComponentTransition<P> | undefined): SplitTransition {
|
|
86
|
+
if (t == null || typeof t === "string") return { root: t, background: t, border: t }
|
|
87
|
+
let root: Record<string, unknown> = {}
|
|
88
|
+
let background: Record<string, unknown> = {}
|
|
89
|
+
let border: Record<string, unknown> = {}
|
|
90
|
+
for (let [key, value] of Object.entries(t)) {
|
|
91
|
+
if (key === "all" || key === "stagger") {
|
|
92
|
+
root[key] = value
|
|
93
|
+
background[key] = value
|
|
94
|
+
border[key] = value
|
|
95
|
+
} else if (key in STYLE_TO_BACKGROUND || key in STYLE_TO_BORDER) {
|
|
96
|
+
if (key in STYLE_TO_BACKGROUND) background[STYLE_TO_BACKGROUND[key]!] = value
|
|
97
|
+
if (key in STYLE_TO_BORDER) border[STYLE_TO_BORDER[key]!] = value
|
|
98
|
+
} else {
|
|
99
|
+
root[key] = value
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let pick = (o: Record<string, unknown>) => (Object.keys(o).length ? (o as CoreTransitionProps["transition"]) : undefined)
|
|
103
|
+
return { root: pick(root), background: pick(background), border: pick(border) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Report a settled core property in the component vocabulary for the node it settled on. */
|
|
107
|
+
export function transitionEndFor<P extends string>(
|
|
108
|
+
node: "root" | "background" | "border",
|
|
109
|
+
handler: ((event: { property: P }) => void) | undefined,
|
|
110
|
+
): ((event: TransitionEndEvent) => void) | undefined {
|
|
111
|
+
if (!handler) return undefined
|
|
112
|
+
return (e) => {
|
|
113
|
+
let name: string = e.property
|
|
114
|
+
if (node === "background") name = e.property === "color" ? "backgroundColor" : "borderRadius"
|
|
115
|
+
if (node === "border")
|
|
116
|
+
name = e.property === "color" ? "borderColor" : e.property === "strokeWidth" ? "borderWidth" : "borderRadius"
|
|
117
|
+
handler({ property: name as P })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
2
120
|
|
|
3
121
|
// The split between layout and style follows one rule: layout properties feed
|
|
4
122
|
// into Taffy and changing them triggers a relayout; style properties are
|
package/src/view.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { LayoutProps, PointerProps } from "@solidrt/core"
|
|
2
|
-
import type { StyleProps } from "./types"
|
|
2
|
+
import type { StyleProps, TransitionProps } from "./types"
|
|
3
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
3
4
|
|
|
4
|
-
export interface ViewProps extends PointerProps {
|
|
5
|
+
export interface ViewProps extends PointerProps, TransitionProps {
|
|
5
6
|
children?: any
|
|
6
7
|
ref?: (node: { id: number }) => void
|
|
7
8
|
layout?: LayoutProps
|
|
@@ -13,8 +14,12 @@ export function View(props: ViewProps) {
|
|
|
13
14
|
props.style?.backgroundColor != null || props.style?.borderRadius != null
|
|
14
15
|
let hasBorder = () => (props.style?.borderWidth ?? 0) > 0
|
|
15
16
|
|
|
17
|
+
let split = () => splitTransition(props.transition)
|
|
18
|
+
|
|
16
19
|
return (
|
|
17
20
|
<view
|
|
21
|
+
transition={split().root}
|
|
22
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
18
23
|
ref={props.ref}
|
|
19
24
|
{...props.layout}
|
|
20
25
|
x={props.style?.x}
|
|
@@ -45,6 +50,8 @@ export function View(props: ViewProps) {
|
|
|
45
50
|
>
|
|
46
51
|
{hasBackground() ? (
|
|
47
52
|
<d-rect
|
|
53
|
+
transition={split().background}
|
|
54
|
+
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
48
55
|
color={props.style?.backgroundColor ?? "transparent"}
|
|
49
56
|
radius={props.style?.borderRadius}
|
|
50
57
|
/>
|
|
@@ -53,6 +60,8 @@ export function View(props: ViewProps) {
|
|
|
53
60
|
{hasBorder() ? (
|
|
54
61
|
<d-rect
|
|
55
62
|
drawStyle="stroke"
|
|
63
|
+
transition={split().border}
|
|
64
|
+
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
56
65
|
color={props.style?.borderColor ?? "transparent"}
|
|
57
66
|
strokeWidth={props.style?.borderWidth}
|
|
58
67
|
radius={props.style?.borderRadius}
|