@solidrt/components 0.0.63 → 0.0.65

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.
Files changed (49) hide show
  1. package/AGENTS.md +18 -14
  2. package/README.md +126 -21
  3. package/docs/badge.md +1 -1
  4. package/docs/checkbox.md +1 -1
  5. package/docs/context-menu.md +1 -1
  6. package/docs/item.md +1 -1
  7. package/docs/light.md +12 -0
  8. package/docs/motion.md +13 -0
  9. package/docs/press.md +19 -0
  10. package/docs/progress-bar.md +1 -1
  11. package/docs/radio.md +1 -1
  12. package/docs/segmented-control.md +1 -1
  13. package/docs/select.md +1 -1
  14. package/docs/slider.md +1 -1
  15. package/docs/surface.md +19 -0
  16. package/docs/switch.md +1 -1
  17. package/docs/theme.md +34 -6
  18. package/docs/tooltip.md +1 -1
  19. package/docs/types.md +1 -1
  20. package/docs/window.md +2 -0
  21. package/examples/lit.tsx +182 -0
  22. package/package.json +3 -3
  23. package/src/badge.tsx +25 -6
  24. package/src/button.tsx +44 -29
  25. package/src/card.tsx +76 -20
  26. package/src/checkbox.tsx +23 -19
  27. package/src/context-menu.tsx +21 -9
  28. package/src/editor-field.tsx +12 -7
  29. package/src/focus-nav.ts +17 -6
  30. package/src/glow.ts +35 -0
  31. package/src/index.ts +16 -1
  32. package/src/item.tsx +35 -13
  33. package/src/light.ts +274 -0
  34. package/src/motion.tsx +13 -1
  35. package/src/nav-shell.tsx +19 -10
  36. package/src/press.ts +2 -1
  37. package/src/pressable.tsx +6 -4
  38. package/src/progress-bar.tsx +25 -7
  39. package/src/radio.tsx +34 -22
  40. package/src/scroll-view.tsx +8 -7
  41. package/src/segmented-control.tsx +104 -23
  42. package/src/select.tsx +40 -34
  43. package/src/slider.tsx +74 -22
  44. package/src/surface.tsx +287 -0
  45. package/src/switch.tsx +48 -26
  46. package/src/theme.ts +201 -40
  47. package/src/tooltip.tsx +18 -14
  48. package/src/types.ts +66 -2
  49. package/src/window.tsx +26 -3
@@ -1,21 +1,22 @@
1
- import { createSignal, For, Show, getBoundingBox, onLayout, withAlpha } from "@solidrt/core"
1
+ import { createSignal, For, Show, getBoundingBox, onCleanup, onLayout, withAlpha } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
3
  import { createPress } from "./press"
4
4
  import { theme } from "./theme"
5
5
  import { policy } from "./policy"
6
6
  import { space } from "./spacing"
7
7
  import { typeStyle, lightOnDark } from "./typography"
8
- import type { Option, StyleProps, TransitionProps, TransitionStyleProp, TransitionViewProp } from "./types"
9
- import { partTransition, partTransitionEnd, splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
8
+ import { Surface, facePaint } from "./surface"
9
+ import type { ElevationLevel, Option, StyleProps, TransitionProps, TransitionStyleProp, TransitionViewProp } from "./types"
10
+ import { partTransition, partTransitionEnd, splitTransition, transitionEndFor } from "./types"
10
11
  import { colorFade, PressFeedback, travelMotion } from "./motion"
11
12
 
12
- export interface SegmentedControlProps
13
+ export interface SegmentedControlProps<T = unknown>
13
14
  extends TransitionProps<TransitionViewProp | TransitionStyleProp | "indicator"> {
14
- options: Option[]
15
+ options: Option<T>[]
15
16
  // Controlled selected value. If omitted, the control is uncontrolled.
16
- value?: unknown
17
- defaultValue?: unknown
18
- onChange?: (value: unknown) => void
17
+ value?: T
18
+ defaultValue?: T
19
+ onChange?: (value: T) => void
19
20
  disabled?: boolean
20
21
  layout?: LayoutProps
21
22
  style?: StyleProps
@@ -28,16 +29,21 @@ const DIVIDER = 0
28
29
 
29
30
  // A single-choice row of equal-width segments, joined flush (the Material
30
31
  // style): only the control's outermost corners are rounded, interior segments
31
- // are square, and hairline dividers separate them. The active segment is one
32
- // indicator rect drawn under the labels that springs between segments - the
33
- // `indicator` transition entry retimes it. Controlled via value/onChange, or
34
- // uncontrolled via defaultValue. Hover tints inactive segments (non-touch
35
- // interaction policies only). Override the inactive fill via style, the
36
- // spacing/sizing via layout.
37
- export function SegmentedControl(props: SegmentedControlProps) {
38
- let [internal, setInternal] = createSignal(props.defaultValue)
32
+ // are square, and hairline dividers separate them. The track is a sunken
33
+ // control face; the active segment is an accent face drawn under the labels
34
+ // as detached rects (its fill and, standing raised, its shadows) that spring
35
+ // between segments - the `indicator` transition entry retimes it; a press on
36
+ // its segment sinks it. Controlled via value/onChange, or uncontrolled via
37
+ // defaultValue. Hover tints inactive segments (non-touch interaction
38
+ // policies only). Override the inactive fill via style, box the control
39
+ // with borderColor/borderWidth, the indicator's level with style.elevation,
40
+ // the spacing/sizing via layout.
41
+ export function SegmentedControl<T>(props: SegmentedControlProps<T>) {
42
+ // The value overload: as far as TypeScript knows T could be a function,
43
+ // which createSignal would read as a compute; the cast picks the value form.
44
+ let [internal, setInternal] = createSignal<T | undefined>(props.defaultValue as Exclude<T | undefined, Function>)
39
45
  let value = () => (props.value !== undefined ? props.value : internal())
40
- let select = (v: unknown) => {
46
+ let select = (v: T) => {
41
47
  if (props.value === undefined) setInternal(() => v)
42
48
  props.onChange?.(v)
43
49
  }
@@ -60,16 +66,24 @@ export function SegmentedControl(props: SegmentedControlProps) {
60
66
  }
61
67
 
62
68
  let idleFill = () => styled().backgroundColor ?? theme.color.surfaceAlt
69
+ let hasBorder = () => styled().borderWidth != null || styled().borderColor != null
63
70
  let activeFill = () => (props.disabled ? theme.color.surface : theme.color.primary)
71
+ let elevation = (): ElevationLevel => styled().elevation ?? "raised"
64
72
  let label = (active: boolean) =>
65
73
  props.disabled ? theme.color.textMuted : active ? theme.color.onPrimary : theme.color.text
66
74
 
67
75
  // The indicator is positioned from measured segment boxes (control-relative
68
76
  // x and width per segment), so a selection change retargets it without a
69
77
  // reflow and unequal rounding never misaligns it. Remeasured each layout.
78
+ // Segment nodes are keyed by option value, not by the For index: a ref
79
+ // callback runs in the element's owned scope, where reading the index
80
+ // signal warns (STRICT_READ_UNTRACKED) and would go stale on a reorder.
70
81
  let root: { id: number } | undefined
71
- let segs: ({ id: number } | undefined)[] = []
82
+ let segs = new Map<T, { id: number }>()
72
83
  let [boxes, setBoxes] = createSignal<{ x: number; w: number }[]>([])
84
+ // Which segment is pressed, so the indicator sinks under the finger on
85
+ // its own segment only.
86
+ let [pressedValue, setPressedValue] = createSignal<T | undefined>(undefined)
73
87
  // The indicator's travel spring is armed only after the first placement:
74
88
  // the declaration exists from mount, so without this the first measured
75
89
  // write would slide the indicator in from x 0. A timer, not the same
@@ -81,7 +95,7 @@ export function SegmentedControl(props: SegmentedControlProps) {
81
95
  if (!r) return
82
96
  let next: { x: number; w: number }[] = []
83
97
  for (let i = 0; i < props.options.length; i++) {
84
- let s = segs[i]
98
+ let s = segs.get(props.options[i]!.value)
85
99
  let b = s && getBoundingBox(s)
86
100
  if (!b) return
87
101
  next.push({ x: b.x - r.x, w: b.width })
@@ -92,6 +106,15 @@ export function SegmentedControl(props: SegmentedControlProps) {
92
106
  })
93
107
  let activeIndex = () => props.options.findIndex((o) => o.value === value())
94
108
  let indicator = () => boxes()[activeIndex()]
109
+ let indicatorPaint = () =>
110
+ facePaint({
111
+ role: "accent",
112
+ fill: activeFill(),
113
+ elevation: elevation(),
114
+ material: styled().material,
115
+ glow: indicator() && !props.disabled ? styled().glow : null,
116
+ pressed: pressedValue() !== undefined && pressedValue() === value(),
117
+ })
95
118
 
96
119
  let split = () => splitTransition(props.transition, ["indicator"])
97
120
  let indicatorTransition = () => {
@@ -116,23 +139,81 @@ export function SegmentedControl(props: SegmentedControlProps) {
116
139
  rotate={styled().rotate}
117
140
  opacity={styled().opacity}
118
141
  >
119
- <d-rect transition={withTransitionDefaults(split().background, colorFade())} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={idleFill()} radius={radius()} />
142
+ <Surface
143
+ role="control"
144
+ fill={idleFill()}
145
+ radius={radius()}
146
+ material={styled().material}
147
+ glow={null}
148
+ sunken
149
+ outline={hasBorder() ? { color: styled().borderColor ?? theme.color.border, width: styled().borderWidth ?? theme.borderWidth.sm } : null}
150
+ fillTransition={split().background}
151
+ onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
152
+ outlineTransition={split().border}
153
+ onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
154
+ />
155
+ {/* The indicator's shadows ride two hidden rects in its own color under
156
+ its fill (a rect carries one shadow; opaque casters, see Surface),
157
+ all three on the same x/w so they travel as one. */}
158
+ <Show when={indicatorPaint().contactShadow}>
159
+ {(shadow) => (
160
+ <d-rect
161
+ transition={indicatorTransition()}
162
+ color={indicator() ? activeFill() : withAlpha(activeFill(), 0)}
163
+ x={indicator()?.x ?? 0}
164
+ w={indicator()?.w ?? 0}
165
+ radius={corners(activeIndex())}
166
+ shadow={shadow()}
167
+ />
168
+ )}
169
+ </Show>
170
+ <Show when={indicatorPaint().keyShadow}>
171
+ {(shadow) => (
172
+ <d-rect
173
+ transition={indicatorTransition()}
174
+ color={indicator() ? activeFill() : withAlpha(activeFill(), 0)}
175
+ x={indicator()?.x ?? 0}
176
+ w={indicator()?.w ?? 0}
177
+ radius={corners(activeIndex())}
178
+ shadow={shadow()}
179
+ />
180
+ )}
181
+ </Show>
120
182
  <d-rect
121
183
  transition={indicatorTransition()}
122
184
  onTransitionEnd={partTransitionEnd("indicator", "x", props.onTransitionEnd)}
123
- color={indicator() ? activeFill() : withAlpha(activeFill(), 0)}
185
+ color={indicator() ? indicatorPaint().fill : withAlpha(activeFill(), 0)}
124
186
  x={indicator()?.x ?? 0}
125
187
  w={indicator()?.w ?? 0}
126
188
  radius={corners(activeIndex())}
189
+ shadow={indicatorPaint().glow}
127
190
  />
191
+ <Show when={indicatorPaint().bevel}>
192
+ {(bevel) => (
193
+ <d-rect
194
+ transition={indicatorTransition()}
195
+ drawStyle="stroke"
196
+ strokeWidth={1}
197
+ color={indicator() ? bevel() : withAlpha(activeFill(), 0)}
198
+ x={indicator()?.x ?? 0}
199
+ w={indicator()?.w ?? 0}
200
+ radius={corners(activeIndex())}
201
+ />
202
+ )}
203
+ </Show>
128
204
  <For each={props.options}>
129
205
  {(opt, i) => {
130
206
  let active = () => value() === opt.value
131
- let press = createPress({ onPress: () => select(opt.value) })
207
+ let press = createPress({
208
+ onPress: () => select(opt.value),
209
+ onPointerDown: () => setPressedValue(() => opt.value),
210
+ onPointerUp: () => setPressedValue(undefined),
211
+ })
212
+ onCleanup(() => segs.delete(opt.value))
132
213
  return (
133
214
  <view
134
215
  ref={(n: { id: number }) => {
135
- segs[i()] = n
216
+ segs.set(opt.value, n)
136
217
  press.ref(n)
137
218
  }}
138
219
  repaintBoundary
package/src/select.tsx CHANGED
@@ -6,16 +6,17 @@ import { policy } from "./policy"
6
6
  import { space } from "./spacing"
7
7
  import { typeStyle } from "./typography"
8
8
  import type { Option, StyleProps, TransitionProps } from "./types"
9
- import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
9
+ import { splitTransition, transitionEndFor } from "./types"
10
10
  import { Icon } from "./icon"
11
11
  import { colorFade, popupFade, popupFadeOut, PressFeedback, travelMotion } from "./motion"
12
+ import { Surface } from "./surface"
12
13
 
13
- export interface SelectProps extends TransitionProps {
14
- options: Option[]
14
+ export interface SelectProps<T = unknown> extends TransitionProps {
15
+ options: Option<T>[]
15
16
  // Controlled selected value. If omitted, the select is uncontrolled.
16
- value?: unknown
17
- defaultValue?: unknown
18
- onChange?: (value: unknown) => void
17
+ value?: T
18
+ defaultValue?: T
19
+ onChange?: (value: T) => void
19
20
  // Shown in the trigger while nothing is selected.
20
21
  placeholder?: string
21
22
  disabled?: boolean
@@ -35,14 +36,16 @@ let margin = () => theme.spacing.sm
35
36
  * value/onChange contract either way; pressing outside closes without a change.
36
37
  * The option list is not scrollable yet, so keep it short.
37
38
  */
38
- export function Select(props: SelectProps) {
39
+ export function Select<T>(props: SelectProps<T>) {
39
40
  let trigger: { id: number } | undefined
40
41
  let [open, setOpen] = createSignal(false)
41
- let [internal, setInternal] = createSignal(props.defaultValue)
42
+ // The value overload: as far as TypeScript knows T could be a function,
43
+ // which createSignal would read as a compute; the cast picks the value form.
44
+ let [internal, setInternal] = createSignal<T | undefined>(props.defaultValue as Exclude<T | undefined, Function>)
42
45
  let value = () => (props.value !== undefined ? props.value : internal())
43
46
  let selected = () => props.options.find((o) => o.value === value())
44
47
 
45
- let choose = (v: unknown) => {
48
+ let choose = (v: T) => {
46
49
  setOpen(false)
47
50
  if (props.value === undefined) setInternal(() => v)
48
51
  props.onChange?.(v)
@@ -52,7 +55,7 @@ export function Select(props: SelectProps) {
52
55
 
53
56
  // One option row, shared by both presentations; only the vertical padding
54
57
  // differs (the sheet gets taller touch targets).
55
- let OptionRow = (p: { option: Option; padY: number }) => {
58
+ let OptionRow = (p: { option: Option<T>; padY: number }) => {
56
59
  let press = createPress({ onPress: () => choose(p.option.value) })
57
60
  return (
58
61
  <view
@@ -80,6 +83,7 @@ export function Select(props: SelectProps) {
80
83
  // Anchored under the trigger, sized at least as wide as it. Positioned like
81
84
  // Tooltip: portal at the window root, pinned at 0,0 and moved with the x/y
82
85
  // paint transforms after measuring, so tracking the anchor never reflows.
86
+ // Glows with theme.glow.overlay (the sheet sits on a scrim and does not).
83
87
  let Dropdown = () => {
84
88
  let menu: { id: number } | undefined
85
89
  let [pos, setPos] = createSignal<{ x: number; y: number } | null>(null)
@@ -121,17 +125,18 @@ export function Select(props: SelectProps) {
121
125
  paddingTop={theme.spacing.sm}
122
126
  paddingBottom={theme.spacing.sm}
123
127
  >
124
- <d-rect transition={colorFade()} color={theme.color.surface} radius={theme.radius.sm} />
125
- <For each={props.options}>
126
- {(o: Option) => <OptionRow option={o} padY={space("sm")} />}
127
- </For>
128
- <d-rect
129
- drawStyle="stroke"
130
- transition={colorFade()}
131
- color={theme.color.border}
132
- strokeWidth={theme.borderWidth.sm}
128
+ <Surface
129
+ role="overlay"
130
+ elevation={styled().elevation ?? "overlay"}
131
+ fill={theme.color.surface}
133
132
  radius={theme.radius.sm}
133
+ material={styled().material}
134
+ glow={styled().glow}
135
+ outline={{ color: theme.color.border, width: theme.borderWidth.sm }}
134
136
  />
137
+ <For each={props.options}>
138
+ {(o: Option<T>) => <OptionRow option={o} padY={space("sm")} />}
139
+ </For>
135
140
  </view>
136
141
  </view>,
137
142
  )
@@ -154,9 +159,9 @@ export function Select(props: SelectProps) {
154
159
  paddingTop={theme.spacing.md}
155
160
  paddingBottom={theme.spacing.md + env.safeArea.bottom}
156
161
  >
157
- <d-rect transition={colorFade()} color={theme.color.surface} radius={theme.radius.sm} />
162
+ <Surface role="overlay" fill={theme.color.surface} radius={theme.radius.sm} material={styled().material} glow={null} />
158
163
  <For each={props.options}>
159
- {(o: Option) => <OptionRow option={o} padY={Math.round(theme.spacing.md * 1.5)} />}
164
+ {(o: Option<T>) => <OptionRow option={o} padY={Math.round(theme.spacing.md * 1.5)} />}
160
165
  </For>
161
166
  </view>
162
167
  </view>,
@@ -167,6 +172,8 @@ export function Select(props: SelectProps) {
167
172
  setOpen(!open())
168
173
  },
169
174
  })
175
+ // Theme-level per-component overrides merged under the instance style.
176
+ let styled = () => ({ ...theme.components.select, ...props.style })
170
177
  let style = () => ({
171
178
  borderColor: theme.color.border,
172
179
  borderWidth: theme.borderWidth.sm,
@@ -213,11 +220,20 @@ export function Select(props: SelectProps) {
213
220
  focusable={!props.disabled}
214
221
  pointerEvents={props.disabled ? "none" : undefined}
215
222
  >
216
- <d-rect transition={withTransitionDefaults(split().background, colorFade())} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
217
- <PressFeedback
223
+ <Surface
224
+ role="control"
225
+ elevation={style().elevation ?? "flat"}
226
+ fill={style().backgroundColor ?? "transparent"}
227
+ radius={style().borderRadius}
228
+ material={style().material}
229
+ glow={style().glow}
218
230
  pressed={press.pressed() && !props.disabled}
219
231
  hovered={press.hovered() && !props.disabled && policy.interaction !== "touch"}
220
- radius={style().borderRadius}
232
+ outline={(style().borderWidth ?? 0) > 0 ? { color: style().borderColor ?? "transparent", width: style().borderWidth! } : null}
233
+ fillTransition={split().background}
234
+ onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
235
+ outlineTransition={split().border}
236
+ onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
221
237
  />
222
238
  <Show
223
239
  when={selected()}
@@ -250,16 +266,6 @@ export function Select(props: SelectProps) {
250
266
  <Sheet />
251
267
  </Show>
252
268
  </Show>
253
- <Show when={(style().borderWidth ?? 0) > 0}>
254
- <d-rect
255
- drawStyle="stroke"
256
- transition={withTransitionDefaults(split().border, colorFade())}
257
- onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
258
- color={style().borderColor ?? "transparent"}
259
- strokeWidth={style().borderWidth}
260
- radius={style().borderRadius}
261
- />
262
- </Show>
263
269
  </view>
264
270
  )
265
271
  }
package/src/slider.tsx CHANGED
@@ -1,8 +1,9 @@
1
- import { arena, createMemo, createSignal, focusedNode, getBoundingBox, onLayout, onSettled, Show } from "@solidrt/core"
1
+ import { arena, createMemo, createSignal, focusedNode, getBoundingBox, onLayout, onSettled } from "@solidrt/core"
2
2
  import type { KeyEvent, LayoutProps, PointerEvent } from "@solidrt/core"
3
- import { theme } from "./theme"
3
+ import { pill, theme } from "./theme"
4
4
  import { policy } from "./policy"
5
5
  import { densityScale } from "./density"
6
+ import { Surface, facePaint } from "./surface"
6
7
  import type { StyleProps, TransitionProps } from "./types"
7
8
  import { splitTransition, transitionEndFor } from "./types"
8
9
  import { colorFade } from "./motion"
@@ -32,7 +33,13 @@ let clamp = (x: number, lo: number, hi: number) => (x < lo ? lo : x > hi ? hi :
32
33
  // path, so a drag keeps updating when the pointer drifts off the track.
33
34
  // Focused (spatial nav), arrow keys step the value (by `step`, else 1% of the
34
35
  // range) and the thumb draws the focus ring under the focusRing policy.
35
- // Controlled via value/onChange, or uncontrolled via defaultValue.
36
+ // Controlled via value/onChange, or uncontrolled via defaultValue. The
37
+ // groove is a sunken control face (surfaceAlt), the fill an accent face
38
+ // (primary, with the accent material's glow) drawn as a detached rect so a
39
+ // drag never reflows, the thumb a raised control face (theme.color.thumb,
40
+ // else primary; style.elevation sets its level) that sinks while dragged;
41
+ // groove and thumb take pill(), round by default and square under
42
+ // radius.full 0.
36
43
  export function Slider(props: SliderProps) {
37
44
  let min = () => props.min ?? 0
38
45
  let max = () => props.max ?? 100
@@ -41,10 +48,22 @@ export function Slider(props: SliderProps) {
41
48
 
42
49
  let track: { id: number } | undefined
43
50
  let active: number | null = null
51
+ let [dragging, setDragging] = createSignal(false)
44
52
 
45
53
  let height = () => Math.round(HEIGHT * densityScale())
46
54
  let thumb = () => Math.round(THUMB * densityScale())
47
55
 
56
+ // Theme-level per-component overrides merged under the instance style:
57
+ // backgroundColor is the groove, color the fill, borderRadius the groove's
58
+ // (ProgressBar's reading of the same keys), and the border boxes the groove.
59
+ let styled = () => ({ ...theme.components.slider, ...props.style })
60
+ let grooveColor = () => styled().backgroundColor ?? theme.color.surfaceAlt
61
+ let fillColor = () => styled().color ?? theme.color.primary
62
+ let grooveRadius = () => styled().borderRadius ?? pill(GROOVE)
63
+ let hasBorder = () => styled().borderWidth != null || styled().borderColor != null
64
+ let fillPaint = () => facePaint({ role: "accent", fill: fillColor(), material: styled().material, glow: props.disabled ? null : styled().glow })
65
+ let split = () => splitTransition(props.transition)
66
+
48
67
  let pct = () => clamp(((value() - min()) / (max() - min())) * 100, 0, 100)
49
68
 
50
69
  // Measured groove width in pixels. The fill and thumb are driven off this
@@ -63,11 +82,16 @@ export function Slider(props: SliderProps) {
63
82
  props.onChange?.(v)
64
83
  }
65
84
 
66
- let setFromClientX = (clientX: number) => {
85
+ // The handlers sit on the track, and localX is exact in the track's own
86
+ // frame even after a drag drifts off it (frozen down path), so no origin
87
+ // lookup is needed. clientX against getBoundingBox would mix frames: the
88
+ // box's x/y are relative to the nearest positioned ancestor, not the
89
+ // window clientX lives in.
90
+ let setFromLocalX = (localX: number) => {
67
91
  if (!track) return
68
- let box = getBoundingBox(track)
69
- if (!box || box.width === 0) return
70
- let f = clamp((clientX - box.x) / box.width, 0, 1)
92
+ let width = getBoundingBox(track)?.width ?? 0
93
+ if (width === 0) return
94
+ let f = clamp(localX / width, 0, 1)
71
95
  let raw = min() + f * (max() - min())
72
96
  if (props.step) raw = Math.round(raw / props.step) * props.step
73
97
  commit(clamp(raw, min(), max()))
@@ -78,6 +102,7 @@ export function Slider(props: SliderProps) {
78
102
  arena.release(active, owner)
79
103
  active = null
80
104
  }
105
+ setDragging(false)
81
106
  }
82
107
  let owner = { cancel: endDrag }
83
108
 
@@ -90,11 +115,12 @@ export function Slider(props: SliderProps) {
90
115
  // outright so an ancestor scroller's pan cannot take the pointer over.
91
116
  arena.steal(e.pointerId, owner)
92
117
  active = e.pointerId
93
- setFromClientX(e.clientX)
118
+ setDragging(true)
119
+ setFromLocalX(e.localX)
94
120
  }
95
121
  let handleMove = (e: PointerEvent) => {
96
122
  if (active !== e.pointerId) return
97
- setFromClientX(e.clientX)
123
+ setFromLocalX(e.localX)
98
124
  }
99
125
  let handleUp = (e: PointerEvent) => {
100
126
  if (active === e.pointerId) endDrag()
@@ -117,7 +143,7 @@ export function Slider(props: SliderProps) {
117
143
 
118
144
  return (
119
145
  <view
120
- transition={splitTransition(props.transition).root}
146
+ transition={split().root}
121
147
  onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
122
148
  ref={(n: { id: number }) => (track = n)}
123
149
  repaintBoundary
@@ -126,11 +152,11 @@ export function Slider(props: SliderProps) {
126
152
  height={height()}
127
153
  width={theme.size.slider}
128
154
  {...props.layout}
129
- x={props.style?.x}
130
- y={props.style?.y}
131
- scale={props.style?.scale}
132
- rotate={props.style?.rotate}
133
- opacity={props.style?.opacity}
155
+ x={styled().x}
156
+ y={styled().y}
157
+ scale={styled().scale}
158
+ rotate={styled().rotate}
159
+ opacity={styled().opacity}
134
160
  pointerEvents={props.disabled ? "none" : "auto"}
135
161
  focusable={!props.disabled}
136
162
  onPointerDown={handleDown}
@@ -142,13 +168,39 @@ export function Slider(props: SliderProps) {
142
168
  get a transition - they track the drag 1:1, and a spring would
143
169
  rubber-band it. */}
144
170
  <view ref={(n: { id: number }) => (groove = n)} position="relative" flex={1} height={GROOVE}>
145
- <d-rect transition={colorFade()} color={theme.color.surfaceAlt} radius={GROOVE / 2} />
146
- <d-rect transition={colorFade()} color={theme.color.primary} w={fillPx()} h={GROOVE} radius={GROOVE / 2} />
147
- <view position="absolute" left={0} top={(GROOVE - thumb()) / 2} x={fillPx() - thumb() / 2}>
148
- <d-oval transition={colorFade()} w={thumb()} h={thumb()} color={theme.color.primary} />
149
- <Show when={focused() && policy.focusRing}>
150
- <d-oval drawStyle="stroke" w={thumb()} h={thumb()} color={theme.color.ring} strokeWidth={theme.borderWidth.focus} />
151
- </Show>
171
+ <Surface
172
+ role="control"
173
+ fill={grooveColor()}
174
+ radius={grooveRadius()}
175
+ material={styled().material}
176
+ glow={null}
177
+ sunken
178
+ outline={hasBorder() ? { color: styled().borderColor ?? theme.color.border, width: styled().borderWidth ?? theme.borderWidth.sm } : null}
179
+ fillTransition={split().background}
180
+ onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
181
+ outlineTransition={split().border}
182
+ onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
183
+ />
184
+ <d-rect
185
+ transition={colorFade()}
186
+ color={fillPaint().fill}
187
+ w={fillPx()}
188
+ h={GROOVE}
189
+ radius={grooveRadius()}
190
+ shadow={fillPaint().glow}
191
+ />
192
+ <view position="absolute" left={0} top={(GROOVE - thumb()) / 2} width={thumb()} height={thumb()} x={fillPx() - thumb() / 2}>
193
+ <Surface
194
+ role="control"
195
+ elevation={styled().elevation ?? "raised"}
196
+ fill={theme.color.thumb ?? theme.color.primary}
197
+ radius={pill(thumb())}
198
+ material={styled().material}
199
+ glow={null}
200
+ pressed={dragging()}
201
+ tint={false}
202
+ outline={focused() && policy.focusRing ? { color: theme.color.ring, width: theme.borderWidth.focus } : null}
203
+ />
152
204
  </view>
153
205
  </view>
154
206
  </view>