@solidrt/components 0.0.35 → 0.0.37

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/src/press.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { createSignal, onSettled, getBoundingBoxViewport } from "@solidrt/core"
2
+ import type { PointerEvent } from "@solidrt/core"
3
+ import { claim, release } from "./arena"
4
+
5
+ export type PressState = { pressed: boolean; hovered: boolean }
6
+
7
+ export interface PressOptions {
8
+ onPress?: () => void
9
+ onPointerDown?: (e: PointerEvent) => void
10
+ onPointerUp?: (e: PointerEvent) => void
11
+ onPointerMove?: (e: PointerEvent) => void
12
+ onPointerEnter?: (e: PointerEvent) => void
13
+ onPointerLeave?: (e: PointerEvent) => void
14
+ }
15
+
16
+ // The press state machine shared by the pressable components. onPress fires on
17
+ // a primary-button down followed by an up over the node. The down provisionally
18
+ // claims the pointer in the arena; pointer events dispatch leaf to root, so the
19
+ // innermost recognizer claims first and recognizers further up the same bubble
20
+ // path find the pointer taken and fail silently (no pressed state, no onPress -
21
+ // ancestors keep hover only). The claim is stealable: a pan recognizer crossing
22
+ // its slop takes the pointer and this press is cancelled through the arena.
23
+ //
24
+ // Moves and the up arrive on the frozen down path, so the press survives
25
+ // leaving the node: while outside its window-relative bounds the pressed state
26
+ // clears (visual feedback retracts), wandering back in restores it (press
27
+ // retention), and only an up inside fires onPress. Enter/leave drive hover
28
+ // alone. Non-primary buttons (right/middle) do not start a press. cancel() is
29
+ // the external-cancel hook; it ends the press without firing. Options are read
30
+ // at event time, so passing a component's reactive props object keeps handler
31
+ // changes live. The host view must attach `ref` for retention bounds; without
32
+ // it every position counts as inside (the up always fires).
33
+ // Deliberately framework-agnostic (no theme, no styling): a candidate for
34
+ // promotion into core once the recognizer family grows
35
+ // (okf/plans/component-gestures.md).
36
+ export function createPress(options: PressOptions) {
37
+ let [pressed, setPressed] = createSignal(false)
38
+ let [hovered, setHovered] = createSignal(false)
39
+ let node: { id: number } | null = null
40
+ // The pointer this recognizer is tracking while a press is in flight, and
41
+ // the retention state at the last move (read on up; the signal itself is
42
+ // not readable same-dispatch because writes flush on the microtask).
43
+ let active: number | null = null
44
+ let inside = false
45
+
46
+ let state = (): PressState => ({ pressed: pressed(), hovered: hovered() })
47
+ let ref = (n: { id: number }) => {
48
+ node = n
49
+ }
50
+
51
+ let within = (e: PointerEvent) => {
52
+ let b = node && getBoundingBoxViewport(node)
53
+ if (!b) return true
54
+ return e.clientX >= b.x && e.clientX < b.x + b.width && e.clientY >= b.y && e.clientY < b.y + b.height
55
+ }
56
+
57
+ let disengage = () => {
58
+ if (active != null) {
59
+ release(active, owner)
60
+ active = null
61
+ }
62
+ }
63
+ let cancel = () => {
64
+ disengage()
65
+ setPressed(false)
66
+ }
67
+ let owner = { cancel }
68
+
69
+ // A press abandoned mid-flight (unmount during a drag) must not leave its
70
+ // claim behind, or that pointer id could never press anything again.
71
+ onSettled(() => disengage)
72
+
73
+ let handlers = {
74
+ onPointerDown: (e: PointerEvent) => {
75
+ if (e.button != null && e.button !== 0) return
76
+ if (active == null && claim(e.pointerId, owner)) {
77
+ active = e.pointerId
78
+ inside = true
79
+ setPressed(true)
80
+ }
81
+ options.onPointerDown?.(e)
82
+ },
83
+ onPointerMove: (e: PointerEvent) => {
84
+ if (active === e.pointerId) {
85
+ inside = within(e)
86
+ setPressed(inside)
87
+ }
88
+ options.onPointerMove?.(e)
89
+ },
90
+ onPointerUp: (e: PointerEvent) => {
91
+ if (active === e.pointerId) {
92
+ let fire = inside
93
+ cancel()
94
+ if (fire) options.onPress?.()
95
+ }
96
+ options.onPointerUp?.(e)
97
+ },
98
+ onPointerEnter: (e: PointerEvent) => {
99
+ setHovered(true)
100
+ options.onPointerEnter?.(e)
101
+ },
102
+ onPointerLeave: (e: PointerEvent) => {
103
+ setHovered(false)
104
+ options.onPointerLeave?.(e)
105
+ },
106
+ }
107
+
108
+ return { pressed, hovered, state, ref, handlers, cancel }
109
+ }
package/src/pressable.tsx CHANGED
@@ -1,8 +1,9 @@
1
- import { createSignal, children } from "@solidrt/core"
2
- import type { LayoutProps, PointerEvent, PointerProps } from "@solidrt/core"
1
+ import { children } from "@solidrt/core"
2
+ import type { LayoutProps, PointerProps } from "@solidrt/core"
3
3
  import type { StyleProps } from "./types"
4
+ import { createPress, type PressState } from "./press"
4
5
 
5
- export type PressState = { pressed: boolean; hovered: boolean }
6
+ export type { PressState } from "./press"
6
7
 
7
8
  export interface PressableProps extends PointerProps {
8
9
  // children and style may be functions of the press state, so a caller can
@@ -15,17 +16,12 @@ export interface PressableProps extends PointerProps {
15
16
  disabled?: boolean
16
17
  }
17
18
 
18
- // A pressable box. onPress fires on a primary-button down followed by an up over
19
- // the same node. Because there is no pointer capture, a drag that leaves the box
20
- // fires onPointerLeave, which cancels the press (no onPress) and clears hover --
21
- // this also covers the no-up-outside case. Non-primary buttons (right/middle) do
22
- // not start a press. When disabled, it takes no pointer events at all.
19
+ // A pressable box: the createPress semantics (see press.ts) on a styled view.
20
+ // When disabled, it takes no pointer events at all.
23
21
  export function Pressable(props: PressableProps) {
24
- let [pressed, setPressed] = createSignal(false)
25
- let [hovered, setHovered] = createSignal(false)
22
+ let press = createPress(props)
26
23
 
27
- let state = (): PressState => ({ pressed: pressed(), hovered: hovered() })
28
- let style = () => (typeof props.style === "function" ? props.style(state()) : props.style)
24
+ let style = () => (typeof props.style === "function" ? props.style(press.state()) : props.style)
29
25
  // Element-valued props build a fresh native subtree on every read, and a
30
26
  // subtree that is never inserted is never destroyed - probing the raw getter
31
27
  // with typeof would orphan one full copy per evaluation. children() memoizes
@@ -35,27 +31,7 @@ export function Pressable(props: PressableProps) {
35
31
  let resolved = children(() => props.children)
36
32
  let kids = () => {
37
33
  let c = resolved()
38
- return typeof c === "function" ? c(state()) : c
39
- }
40
-
41
- let handleDown = (e: PointerEvent) => {
42
- if (e.button != null && e.button !== 0) return
43
- setPressed(true)
44
- props.onPointerDown?.(e)
45
- }
46
- let handleUp = (e: PointerEvent) => {
47
- if (pressed()) props.onPress?.()
48
- setPressed(false)
49
- props.onPointerUp?.(e)
50
- }
51
- let handleEnter = (e: PointerEvent) => {
52
- setHovered(true)
53
- props.onPointerEnter?.(e)
54
- }
55
- let handleLeave = (e: PointerEvent) => {
56
- setHovered(false)
57
- setPressed(false)
58
- props.onPointerLeave?.(e)
34
+ return typeof c === "function" ? c(press.state()) : c
59
35
  }
60
36
 
61
37
  let hasBackground = () => style()?.backgroundColor != null || style()?.borderRadius != null
@@ -63,7 +39,10 @@ export function Pressable(props: PressableProps) {
63
39
 
64
40
  return (
65
41
  <view
66
- ref={props.ref}
42
+ ref={(n: { id: number }) => {
43
+ press.ref(n)
44
+ props.ref?.(n)
45
+ }}
67
46
  repaintBoundary
68
47
  {...props.layout}
69
48
  x={style()?.x}
@@ -71,11 +50,11 @@ export function Pressable(props: PressableProps) {
71
50
  scale={style()?.scale}
72
51
  rotate={style()?.rotate}
73
52
  opacity={style()?.opacity}
74
- onPointerEnter={handleEnter}
75
- onPointerLeave={handleLeave}
76
- onPointerDown={handleDown}
77
- onPointerUp={handleUp}
78
- onPointerMove={props.onPointerMove}
53
+ onPointerEnter={press.handlers.onPointerEnter}
54
+ onPointerLeave={press.handlers.onPointerLeave}
55
+ onPointerDown={press.handlers.onPointerDown}
56
+ onPointerUp={press.handlers.onPointerUp}
57
+ onPointerMove={press.handlers.onPointerMove}
79
58
  onWheel={props.onWheel}
80
59
  onFocus={props.onFocus}
81
60
  onBlur={props.onBlur}
@@ -101,4 +80,4 @@ export function Pressable(props: PressableProps) {
101
80
  ) : null}
102
81
  </view>
103
82
  )
104
- }
83
+ }
package/src/radio.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createSignal, createContext, useContext, Show, children } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
- import { Pressable } from "./pressable"
3
+ import { createPress } from "./press"
4
4
  import { theme } from "./theme"
5
5
  import { densityScale } from "./policy"
6
6
  import { typeStyle } from "./typography"
@@ -81,13 +81,27 @@ export function Radio(props: RadioProps) {
81
81
  // Inner dot inset as a fraction of the ring, so it scales with the density.
82
82
  let inset = () => ring() * 0.3
83
83
 
84
+ let press = createPress({ onPress: () => ctx.select(props.value) })
85
+
84
86
  return (
85
- <Pressable
86
- onPress={() => ctx.select(props.value)}
87
- disabled={disabled()}
88
- layout={{ flexDirection: "row", alignItems: "center", gap: theme.spacing.md, ...props.layout }}
89
- style={props.style}
87
+ <view
88
+ ref={press.ref}
89
+ repaintBoundary
90
+ flexDirection="row"
91
+ alignItems="center"
92
+ gap={theme.spacing.md}
93
+ {...props.layout}
94
+ x={props.style?.x}
95
+ y={props.style?.y}
96
+ scale={props.style?.scale}
97
+ rotate={props.style?.rotate}
98
+ opacity={props.style?.opacity}
99
+ {...press.handlers}
100
+ pointerEvents={disabled() ? "none" : undefined}
90
101
  >
102
+ <Show when={props.style?.backgroundColor != null || props.style?.borderRadius != null}>
103
+ <d-rect color={props.style?.backgroundColor ?? "transparent"} radius={props.style?.borderRadius} />
104
+ </Show>
91
105
  <view width={ring()} height={ring()}>
92
106
  <d-oval x={1} y={1} w={ring() - 2} h={ring() - 2} drawStyle="stroke" color={ringColor()} strokeWidth={2} />
93
107
  <Show when={selected()}>
@@ -99,6 +113,14 @@ export function Radio(props: RadioProps) {
99
113
  {resolved()}
100
114
  </text>
101
115
  </Show>
102
- </Pressable>
116
+ <Show when={(props.style?.borderWidth ?? 0) > 0}>
117
+ <d-rect
118
+ drawStyle="stroke"
119
+ color={props.style?.borderColor ?? "transparent"}
120
+ strokeWidth={props.style?.borderWidth}
121
+ radius={props.style?.borderRadius}
122
+ />
123
+ </Show>
124
+ </view>
103
125
  )
104
126
  }
@@ -1,5 +1,6 @@
1
1
  import { createScroll } from "@solidrt/core"
2
- import type { LayoutProps, PointerEvent, PointerProps, WheelEvent } from "@solidrt/core"
2
+ import type { LayoutProps, PointerProps, WheelEvent } from "@solidrt/core"
3
+ import { createPan } from "./pan"
3
4
  import type { StyleProps } from "./types"
4
5
 
5
6
  export interface ScrollViewProps extends PointerProps {
@@ -14,11 +15,13 @@ export interface ScrollViewProps extends PointerProps {
14
15
  // A scrollable region. The outer box carries layout/style/transform and the
15
16
  // optional background and border; inside it a clipping viewport (overflow
16
17
  // hidden) holds a content wrapper that takes the children's natural size. The
17
- // offset from createScroll translates the content via scrollX/scrollY. Wheel and
18
- // drag map to scroll deltas: positive moves the content up/left, so dragging a
19
- // finger up reveals content below (natural scrolling). There is no momentum yet;
20
- // a fling stops when the finger lifts. With no pointer capture, a drag that
21
- // leaves the box stops scrolling (pointerLeave ends the gesture).
18
+ // offset from createScroll translates the content via scrollX/scrollY. Wheel
19
+ // and drag map to scroll deltas: positive moves the content up/left, so
20
+ // dragging a finger up reveals content below (natural scrolling). The drag is
21
+ // a pan recognizer: it activates on movement slop along the scroll axis,
22
+ // stealing the pointer from a pressable the drag started on (its press
23
+ // feedback retracts), and keeps scrolling when the pointer leaves the box.
24
+ // There is no momentum yet; a fling stops when the finger lifts.
22
25
  export function ScrollView(props: ScrollViewProps) {
23
26
  let viewport: { id: number } | undefined
24
27
  let content: { id: number } | undefined
@@ -29,21 +32,13 @@ export function ScrollView(props: ScrollViewProps) {
29
32
  { axis: props.horizontal ? "horizontal" : "vertical" },
30
33
  )
31
34
 
32
- // Last pointer position during an active drag, in window coordinates. null
33
- // when no drag is in progress.
34
- let last: { x: number; y: number } | null = null
35
+ // Content follows the finger: it moves opposite to scroll offsets, which
36
+ // grow toward the bottom/right.
37
+ let pan = createPan({
38
+ axis: props.horizontal ? "horizontal" : "vertical",
39
+ onPanMove: (dx, dy) => scroll.scrollBy(-dx, -dy),
40
+ })
35
41
 
36
- let onPointerDown = (e: PointerEvent) => {
37
- last = { x: e.clientX, y: e.clientY }
38
- }
39
- let onPointerMove = (e: PointerEvent) => {
40
- if (!last) return
41
- scroll.scrollBy(last.x - e.clientX, last.y - e.clientY)
42
- last = { x: e.clientX, y: e.clientY }
43
- }
44
- let endDrag = () => {
45
- last = null
46
- }
47
42
  let onWheel = (e: WheelEvent) => {
48
43
  // A plain mouse wheel only emits deltaY. On a horizontal scroller, route that
49
44
  // vertical delta to the x axis so the wheel still scrolls it (trackpads that
@@ -88,10 +83,7 @@ export function ScrollView(props: ScrollViewProps) {
88
83
  flexDirection={direction()}
89
84
  scrollX={scroll.offset().x}
90
85
  scrollY={scroll.offset().y}
91
- onPointerDown={onPointerDown}
92
- onPointerMove={onPointerMove}
93
- onPointerUp={endDrag}
94
- onPointerLeave={endDrag}
86
+ {...pan.handlers}
95
87
  onWheel={onWheel}
96
88
  >
97
89
  <view ref={(n: { id: number }) => (content = n)} flexShrink={0} flexDirection={direction()}>
@@ -0,0 +1,107 @@
1
+ import { createSignal, For } from "@solidrt/core"
2
+ import type { LayoutProps } from "@solidrt/core"
3
+ import { createPress } from "./press"
4
+ import { theme } from "./theme"
5
+ import { policy } from "./policy"
6
+ import { space } from "./spacing"
7
+ import { typeStyle, lightOnDark } from "./typography"
8
+ import type { Option, StyleProps } from "./types"
9
+
10
+ export interface SegmentedControlProps {
11
+ options: Option[]
12
+ // Controlled selected value. If omitted, the control is uncontrolled.
13
+ value?: unknown
14
+ defaultValue?: unknown
15
+ onChange?: (value: unknown) => void
16
+ disabled?: boolean
17
+ layout?: LayoutProps
18
+ style?: StyleProps
19
+ }
20
+
21
+ // Width of the hairline divider between segments (logical px). The dividers
22
+ // are the backing rect showing through the row gap, so this is a gap, not a
23
+ // stroke.
24
+ const DIVIDER = 0
25
+
26
+ // A single-choice row of equal-width segments, joined flush (the Material
27
+ // style): only the control's outermost corners are rounded, interior segments
28
+ // are square, and hairline dividers separate them. The active segment fills
29
+ // with the primary color. Controlled via value/onChange, or uncontrolled via
30
+ // defaultValue. Hover tints inactive segments (non-touch interaction policies
31
+ // only). Override the inactive fill via style, spacing/sizing via layout.
32
+ export function SegmentedControl(props: SegmentedControlProps) {
33
+ let [internal, setInternal] = createSignal(props.defaultValue)
34
+ let value = () => (props.value !== undefined ? props.value : internal())
35
+ let select = (v: unknown) => {
36
+ if (props.value === undefined) setInternal(() => v)
37
+ props.onChange?.(v)
38
+ }
39
+
40
+ let radius = () =>
41
+ typeof props.style?.borderRadius === "number" ? props.style.borderRadius : theme.radius.md
42
+ // Per-corner radii: round only the corners on the control's outer edge, so
43
+ // the segments read as one joined control. [tl, tr, br, bl].
44
+ let corners = (i: number): number | [number, number, number, number] => {
45
+ let r = radius()
46
+ let last = props.options.length - 1
47
+ if (last === 0) return r
48
+ if (i === 0) return [r, 0, 0, r]
49
+ if (i === last) return [0, r, r, 0]
50
+ return 0
51
+ }
52
+
53
+ let idleFill = () => props.style?.backgroundColor ?? theme.color.surfaceAlt
54
+ let activeFill = () => (props.disabled ? theme.color.surface : theme.color.primary)
55
+ let label = (active: boolean) =>
56
+ props.disabled ? theme.color.textMuted : active ? theme.color.onPrimary : theme.color.text
57
+
58
+ return (
59
+ <view
60
+ flexDirection="row"
61
+ gap={DIVIDER}
62
+ {...props.layout}
63
+ x={props.style?.x}
64
+ y={props.style?.y}
65
+ scale={props.style?.scale}
66
+ rotate={props.style?.rotate}
67
+ opacity={props.style?.opacity}
68
+ >
69
+ <d-rect color={theme.color.border} radius={radius()} />
70
+ <For each={props.options}>
71
+ {(opt, i) => {
72
+ let active = () => value() === opt.value
73
+ let press = createPress({ onPress: () => select(opt.value) })
74
+ let fill = () =>
75
+ active()
76
+ ? activeFill()
77
+ : press.hovered() && !props.disabled && policy.interaction !== "touch"
78
+ ? theme.color.surfaceHover
79
+ : idleFill()
80
+ return (
81
+ <view
82
+ ref={press.ref}
83
+ repaintBoundary
84
+ flexGrow={1}
85
+ flexBasis={0}
86
+ alignItems="center"
87
+ paddingTop={space("md")}
88
+ paddingBottom={space("md")}
89
+ paddingLeft={space("md")}
90
+ paddingRight={space("md")}
91
+ {...press.handlers}
92
+ pointerEvents={props.disabled ? "none" : undefined}
93
+ >
94
+ <d-rect color={fill()} radius={corners(i())} />
95
+ <text
96
+ color={label(active())}
97
+ {...typeStyle("body", active() ? lightOnDark(label(true), activeFill()) : undefined)}
98
+ >
99
+ {opt.label}
100
+ </text>
101
+ </view>
102
+ )
103
+ }}
104
+ </For>
105
+ </view>
106
+ )
107
+ }
package/src/select.tsx CHANGED
@@ -1,19 +1,14 @@
1
1
  import { createSignal, createPortal, onLayout, getBoundingBox, Show, For, env } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
- import { Pressable, type PressState } from "./pressable"
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 } from "./typography"
8
- import type { StyleProps } from "./types"
9
-
10
- export interface SelectOption {
11
- value: unknown
12
- label: string
13
- }
8
+ import type { Option, StyleProps } from "./types"
14
9
 
15
10
  export interface SelectProps {
16
- options: SelectOption[]
11
+ options: Option[]
17
12
  // Controlled selected value. If omitted, the select is uncontrolled.
18
13
  value?: unknown
19
14
  defaultValue?: unknown
@@ -53,25 +48,31 @@ export function Select(props: SelectProps) {
53
48
 
54
49
  // One option row, shared by both presentations; only the vertical padding
55
50
  // differs (the sheet gets taller touch targets).
56
- let OptionRow = (p: { option: SelectOption; padY: number }) => (
57
- <Pressable
58
- onPress={() => choose(p.option.value)}
59
- layout={{
60
- flexDirection: "row",
61
- alignItems: "center",
62
- paddingTop: p.padY,
63
- paddingBottom: p.padY,
64
- paddingLeft: space("md"),
65
- paddingRight: space("md"),
66
- }}
67
- style={(s: PressState) => ({
68
- backgroundColor:
69
- s.pressed || (s.hovered && policy.interaction !== "touch") ? theme.color.surfaceHover : "transparent",
70
- })}
71
- >
72
- <text {...bodyText(p.option.value === value() ? theme.color.primary : theme.color.text)}>{p.option.label}</text>
73
- </Pressable>
74
- )
51
+ let OptionRow = (p: { option: Option; padY: number }) => {
52
+ let press = createPress({ onPress: () => choose(p.option.value) })
53
+ return (
54
+ <view
55
+ ref={press.ref}
56
+ repaintBoundary
57
+ flexDirection="row"
58
+ alignItems="center"
59
+ paddingTop={p.padY}
60
+ paddingBottom={p.padY}
61
+ paddingLeft={space("md")}
62
+ paddingRight={space("md")}
63
+ {...press.handlers}
64
+ >
65
+ <d-rect
66
+ color={
67
+ press.pressed() || (press.hovered() && policy.interaction !== "touch")
68
+ ? theme.color.surfaceHover
69
+ : "transparent"
70
+ }
71
+ />
72
+ <text {...bodyText(p.option.value === value() ? theme.color.primary : theme.color.text)}>{p.option.label}</text>
73
+ </view>
74
+ )
75
+ }
75
76
 
76
77
  // Anchored under the trigger, sized at least as wide as it. Positioned like
77
78
  // Tooltip: portal at the window root, pinned at 0,0 and moved with the x/y
@@ -108,7 +109,7 @@ export function Select(props: SelectProps) {
108
109
  >
109
110
  <d-rect color={theme.color.surface} radius={theme.radius.sm} />
110
111
  <For each={props.options}>
111
- {(o: SelectOption) => <OptionRow option={o} padY={space("sm")} />}
112
+ {(o: Option) => <OptionRow option={o} padY={space("sm")} />}
112
113
  </For>
113
114
  <d-rect
114
115
  drawStyle="stroke"
@@ -140,40 +141,50 @@ export function Select(props: SelectProps) {
140
141
  >
141
142
  <d-rect color={theme.color.surface} radius={theme.radius.sm} />
142
143
  <For each={props.options}>
143
- {(o: SelectOption) => <OptionRow option={o} padY={Math.round(theme.spacing.md * 1.5)} />}
144
+ {(o: Option) => <OptionRow option={o} padY={Math.round(theme.spacing.md * 1.5)} />}
144
145
  </For>
145
146
  </view>
146
147
  </view>,
147
148
  )
148
149
 
150
+ let press = createPress({ onPress: () => setOpen(!open()) })
151
+ let style = () => ({
152
+ borderColor: theme.color.border,
153
+ borderWidth: theme.borderWidth.sm,
154
+ borderRadius: theme.radius.sm,
155
+ ...props.style,
156
+ backgroundColor:
157
+ props.style?.backgroundColor ??
158
+ (press.hovered() && !props.disabled && policy.interaction !== "touch"
159
+ ? theme.color.surfaceHover
160
+ : theme.color.surface),
161
+ })
162
+
149
163
  return (
150
- <Pressable
151
- ref={(n) => (trigger = n)}
152
- onPress={() => setOpen(!open())}
153
- disabled={props.disabled}
154
- layout={{
155
- flexDirection: "row",
156
- alignItems: "center",
157
- justifyContent: "space-between",
158
- gap: theme.spacing.md,
159
- paddingTop: space("sm"),
160
- paddingBottom: space("sm"),
161
- paddingLeft: space("md"),
162
- paddingRight: space("md"),
163
- ...props.layout,
164
+ <view
165
+ ref={(n: { id: number }) => {
166
+ press.ref(n)
167
+ trigger = n
164
168
  }}
165
- style={(s: PressState) => ({
166
- borderColor: theme.color.border,
167
- borderWidth: theme.borderWidth.sm,
168
- borderRadius: theme.radius.sm,
169
- ...props.style,
170
- backgroundColor:
171
- props.style?.backgroundColor ??
172
- (s.hovered && !props.disabled && policy.interaction !== "touch"
173
- ? theme.color.surfaceHover
174
- : theme.color.surface),
175
- })}
169
+ repaintBoundary
170
+ flexDirection="row"
171
+ alignItems="center"
172
+ justifyContent="space-between"
173
+ gap={theme.spacing.md}
174
+ paddingTop={space("sm")}
175
+ paddingBottom={space("sm")}
176
+ paddingLeft={space("md")}
177
+ paddingRight={space("md")}
178
+ {...props.layout}
179
+ x={style().x}
180
+ y={style().y}
181
+ scale={style().scale}
182
+ rotate={style().rotate}
183
+ opacity={style().opacity}
184
+ {...press.handlers}
185
+ pointerEvents={props.disabled ? "none" : undefined}
176
186
  >
187
+ <d-rect color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
177
188
  <Show
178
189
  when={selected()}
179
190
  fallback={<text {...bodyText(theme.color.textMuted)}>{props.placeholder ?? ""}</text>}
@@ -195,6 +206,14 @@ export function Select(props: SelectProps) {
195
206
  <Sheet />
196
207
  </Show>
197
208
  </Show>
198
- </Pressable>
209
+ <Show when={(style().borderWidth ?? 0) > 0}>
210
+ <d-rect
211
+ drawStyle="stroke"
212
+ color={style().borderColor ?? "transparent"}
213
+ strokeWidth={style().borderWidth}
214
+ radius={style().borderRadius}
215
+ />
216
+ </Show>
217
+ </view>
199
218
  )
200
219
  }