@solidrt/components 0.0.51 → 0.0.53
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 +34 -7
- package/README.md +47 -10
- package/docs/button.md +1 -1
- package/docs/focus-nav.md +1 -1
- package/docs/icon.md +1 -1
- package/docs/item.md +1 -1
- package/docs/policy.md +1 -1
- package/docs/scroll-view.md +22 -1
- package/docs/split-view.md +1 -1
- package/docs/theme.md +14 -2
- package/docs/types.md +4 -0
- package/package.json +3 -3
- package/src/badge.tsx +21 -14
- package/src/button.tsx +13 -7
- package/src/card.tsx +10 -3
- package/src/checkbox.tsx +8 -2
- package/src/context-menu.tsx +9 -6
- package/src/divider.tsx +8 -3
- package/src/editor-field.tsx +18 -11
- package/src/field.tsx +4 -2
- package/src/icon.tsx +8 -4
- package/src/image.tsx +10 -3
- package/src/index.ts +10 -1
- package/src/item.tsx +12 -7
- package/src/nav-shell.tsx +6 -16
- package/src/policy.ts +9 -4
- package/src/pressable.tsx +11 -2
- package/src/progress-bar.tsx +4 -3
- package/src/qrcode.tsx +7 -5
- package/src/radio.tsx +12 -4
- package/src/rich-text-editor.tsx +5 -3
- package/src/scroll-view.tsx +76 -7
- package/src/segmented-control.tsx +14 -5
- package/src/select.tsx +23 -12
- package/src/slider.tsx +31 -5
- package/src/spinner.tsx +5 -2
- package/src/split-view.tsx +3 -4
- package/src/switch.tsx +8 -2
- package/src/text-input.tsx +4 -2
- package/src/text.tsx +22 -2
- package/src/theme.ts +72 -20
- package/src/tooltip.tsx +16 -5
- package/src/types.ts +119 -1
- package/src/view.tsx +11 -2
package/src/theme.ts
CHANGED
|
@@ -38,6 +38,8 @@ export type Theme = {
|
|
|
38
38
|
text: {
|
|
39
39
|
// Passed through to the core font stack: "sans" | "mono" | a family name.
|
|
40
40
|
fontFamily: string
|
|
41
|
+
// The monospace family for code (RichTextEditor inline code).
|
|
42
|
+
monoFamily: string
|
|
41
43
|
caption: TextStyle
|
|
42
44
|
label: TextStyle
|
|
43
45
|
body: TextStyle
|
|
@@ -69,10 +71,24 @@ export type Theme = {
|
|
|
69
71
|
// caller-set style.backgroundColor. Non-touch interaction policies only.
|
|
70
72
|
overlayHover: string
|
|
71
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
|
|
72
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.
|
|
73
81
|
spacing: { sm: number; md: number; lg: number; xl: number }
|
|
74
|
-
|
|
75
|
-
|
|
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 }
|
|
76
92
|
// Semantic control glyphs, as SVG document strings (the Icon currency).
|
|
77
93
|
// Components draw their built-in vector paths by default; a theme that sets
|
|
78
94
|
// a slot swaps that glyph everywhere it appears. The package still bundles
|
|
@@ -93,9 +109,10 @@ export type Theme = {
|
|
|
93
109
|
export type ThemeColor = string | [light: string, dark: string]
|
|
94
110
|
|
|
95
111
|
export type ThemeDefinition = {
|
|
96
|
-
color: { [K in keyof Theme["color"]]: ThemeColor }
|
|
112
|
+
color: { [K in Exclude<keyof Theme["color"], "ring">]: ThemeColor } & { ring?: ThemeColor }
|
|
97
113
|
text?: {
|
|
98
114
|
fontFamily?: string
|
|
115
|
+
monoFamily?: string
|
|
99
116
|
// The body font size; the other roles derive from it. Default 14.
|
|
100
117
|
base?: number
|
|
101
118
|
// The step between adjacent roles (caption, label, body, title, heading
|
|
@@ -105,23 +122,43 @@ export type ThemeDefinition = {
|
|
|
105
122
|
// and weights.
|
|
106
123
|
roles?: { [K in TextVariant]?: Partial<TextStyle> }
|
|
107
124
|
}
|
|
108
|
-
|
|
109
|
-
|
|
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"]>
|
|
110
131
|
borderWidth?: Partial<Theme["borderWidth"]>
|
|
132
|
+
size?: Partial<Theme["size"]>
|
|
111
133
|
icons?: Theme["icons"]
|
|
112
134
|
components?: Theme["components"]
|
|
113
135
|
}
|
|
114
136
|
|
|
115
|
-
const
|
|
116
|
-
const RADIUS = { sm: 4, md: 8, lg: 12 }
|
|
117
|
-
const BORDER_WIDTH = { sm: 1 }
|
|
137
|
+
const SPACING_BASE = 4
|
|
118
138
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
122
159
|
const ROLE_DEFAULTS: { [K in TextVariant]: { step: number; lineHeight: number; weight: TextStyle["weight"] } } = {
|
|
123
|
-
caption: { step: -
|
|
124
|
-
label: { step:
|
|
160
|
+
caption: { step: -1, lineHeight: 1.3, weight: 400 },
|
|
161
|
+
label: { step: 0, lineHeight: 1.5, weight: 600 },
|
|
125
162
|
body: { step: 0, lineHeight: 1.5, weight: 400 },
|
|
126
163
|
title: { step: 1, lineHeight: 1.4, weight: 700 },
|
|
127
164
|
heading: { step: 2, lineHeight: 1.3, weight: 700 },
|
|
@@ -139,11 +176,13 @@ export function defineTheme(def: ThemeDefinition, scheme?: "light" | "dark"): Th
|
|
|
139
176
|
for (let key in def.color) {
|
|
140
177
|
let k = key as keyof Theme["color"]
|
|
141
178
|
let value = def.color[k]
|
|
179
|
+
if (value == null) continue
|
|
142
180
|
if (Array.isArray(value)) {
|
|
143
181
|
if (!scheme) throw new Error(`Theme color "${key}" is a [light, dark] pair; pass a scheme to defineTheme`)
|
|
144
182
|
color[k] = value[scheme === "light" ? 0 : 1]
|
|
145
183
|
} else color[k] = value
|
|
146
184
|
}
|
|
185
|
+
if (def.color.ring == null) color.ring = color.text
|
|
147
186
|
let base = def.text?.base ?? 14
|
|
148
187
|
let ratio = def.text?.ratio ?? 1.26
|
|
149
188
|
let role = (name: TextVariant): TextStyle => {
|
|
@@ -158,6 +197,7 @@ export function defineTheme(def: ThemeDefinition, scheme?: "light" | "dark"): Th
|
|
|
158
197
|
return {
|
|
159
198
|
text: {
|
|
160
199
|
fontFamily: def.text?.fontFamily ?? "sans",
|
|
200
|
+
monoFamily: def.text?.monoFamily ?? "mono",
|
|
161
201
|
caption: role("caption"),
|
|
162
202
|
label: role("label"),
|
|
163
203
|
body: role("body"),
|
|
@@ -165,9 +205,14 @@ export function defineTheme(def: ThemeDefinition, scheme?: "light" | "dark"): Th
|
|
|
165
205
|
heading: role("heading"),
|
|
166
206
|
},
|
|
167
207
|
color,
|
|
168
|
-
spacing:
|
|
169
|
-
|
|
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 },
|
|
170
214
|
borderWidth: { ...BORDER_WIDTH, ...def.borderWidth },
|
|
215
|
+
size: { ...SIZE, ...def.size },
|
|
171
216
|
icons: def.icons ?? {},
|
|
172
217
|
components: def.components ?? {},
|
|
173
218
|
}
|
|
@@ -180,7 +225,10 @@ const DEFAULT: ThemeDefinition = {
|
|
|
180
225
|
background: ["#ffffff", "#0b0f17"],
|
|
181
226
|
surface: ["#f6f8fa", "#161b22"],
|
|
182
227
|
surfaceAlt: ["#eaeef2", "#21262d"],
|
|
183
|
-
text:
|
|
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"],
|
|
184
232
|
// Muted is an opaque tone between text and background, mixed in oklab
|
|
185
233
|
// (like Material 3's tonal colors, not an alpha overlay): alpha text
|
|
186
234
|
// renders thin on low-DPI and its contrast depends on what sits behind
|
|
@@ -188,7 +236,10 @@ const DEFAULT: ThemeDefinition = {
|
|
|
188
236
|
// preset is data that must not need the render engine at import time
|
|
189
237
|
// (the website token build imports this module headless). If text or
|
|
190
238
|
// background changes, recompute: mixColors(text, background, 0.4).
|
|
191
|
-
|
|
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"],
|
|
192
243
|
border: ["rgba(0,0,0,0.15)", "rgba(255,255,255,0.14)"],
|
|
193
244
|
// Accent tuned to the puzzle mark's mid blue.
|
|
194
245
|
primary: "#547ebf",
|
|
@@ -202,9 +253,10 @@ const DEFAULT: ThemeDefinition = {
|
|
|
202
253
|
overlayHover: ["rgba(0,0,0,0.08)", "rgba(255,255,255,0.08)"],
|
|
203
254
|
overlayPressed: ["rgba(0,0,0,0.14)", "rgba(255,255,255,0.14)"],
|
|
204
255
|
},
|
|
205
|
-
// base 14 and ratio 1.26 derive title 18 and heading 22;
|
|
206
|
-
//
|
|
207
|
-
|
|
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 } } },
|
|
208
260
|
}
|
|
209
261
|
|
|
210
262
|
export let darkTheme: Theme = defineTheme(DEFAULT, "dark")
|
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
|
})
|
|
@@ -86,6 +89,8 @@ export function Tooltip(props: TooltipProps) {
|
|
|
86
89
|
pointerEvents="none"
|
|
87
90
|
>
|
|
88
91
|
<d-rect
|
|
92
|
+
transition={split().background}
|
|
93
|
+
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
89
94
|
color={theme.components.tooltip?.backgroundColor ?? theme.color.surfaceAlt}
|
|
90
95
|
radius={theme.components.tooltip?.borderRadius ?? theme.radius.sm}
|
|
91
96
|
/>
|
|
@@ -96,6 +101,8 @@ export function Tooltip(props: TooltipProps) {
|
|
|
96
101
|
</Show>
|
|
97
102
|
<d-rect
|
|
98
103
|
drawStyle="stroke"
|
|
104
|
+
transition={split().border}
|
|
105
|
+
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
99
106
|
color={theme.components.tooltip?.borderColor ?? theme.color.border}
|
|
100
107
|
strokeWidth={theme.components.tooltip?.borderWidth ?? theme.borderWidth.sm}
|
|
101
108
|
radius={theme.components.tooltip?.borderRadius ?? theme.radius.sm}
|
|
@@ -104,8 +111,12 @@ export function Tooltip(props: TooltipProps) {
|
|
|
104
111
|
)
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
let split = () => splitTransition(props.transition)
|
|
115
|
+
|
|
107
116
|
return (
|
|
108
117
|
<view
|
|
118
|
+
transition={split().root}
|
|
119
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
109
120
|
ref={(n: { id: number }) => (anchor = n)}
|
|
110
121
|
onPointerEnter={enter}
|
|
111
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}
|