@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/src/checkbox.tsx CHANGED
@@ -2,11 +2,13 @@ import { createSignal, Show } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
3
  import { createPress } from "./press"
4
4
  import { theme } from "./theme"
5
+ import { policy } from "./policy"
5
6
  import { densityScale } from "./density"
6
7
  import { Icon } from "./icon"
7
- import type { StyleProps } from "./types"
8
+ import type { StyleProps, TransitionProps } from "./types"
9
+ import { splitTransition, transitionEndFor } from "./types"
8
10
 
9
- export interface CheckboxProps {
11
+ export interface CheckboxProps extends TransitionProps {
10
12
  // Controlled checked state. If omitted, the checkbox is uncontrolled.
11
13
  checked?: boolean
12
14
  defaultChecked?: boolean
@@ -46,10 +48,13 @@ export function Checkbox(props: CheckboxProps) {
46
48
  borderRadius: theme.radius.sm,
47
49
  ...theme.components.checkbox,
48
50
  ...props.style,
51
+ ...(press.focused() && policy.focusRing ? { borderWidth: theme.borderWidth.focus, borderColor: theme.color.ring } : {}),
49
52
  })
50
53
 
51
54
  return (
52
55
  <view
56
+ transition={splitTransition(props.transition).root}
57
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
53
58
  ref={press.ref}
54
59
  repaintBoundary
55
60
  width={size()}
@@ -62,6 +67,7 @@ export function Checkbox(props: CheckboxProps) {
62
67
  rotate={style().rotate}
63
68
  opacity={style().opacity}
64
69
  {...press.handlers}
70
+ focusable={!props.disabled}
65
71
  pointerEvents={props.disabled ? "none" : undefined}
66
72
  >
67
73
  <d-rect color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
@@ -5,6 +5,8 @@ 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 { TransitionProps } from "./types"
9
+ import { splitTransition, transitionEndFor } from "./types"
8
10
 
9
11
  export interface ContextMenuItem {
10
12
  label: string
@@ -12,7 +14,7 @@ export interface ContextMenuItem {
12
14
  disabled?: boolean
13
15
  }
14
16
 
15
- export interface ContextMenuProps {
17
+ export interface ContextMenuProps extends TransitionProps {
16
18
  items: ContextMenuItem[]
17
19
  // The content the menu attaches to.
18
20
  children?: any
@@ -23,8 +25,7 @@ const LONG_PRESS_MS = 500
23
25
  // Finger travel (window px) that cancels a pending long-press.
24
26
  const MOVE_SLOP = 8
25
27
  // Minimum distance kept between the menu and the window edges.
26
- const MARGIN = 4
27
- const MIN_WIDTH = 120
28
+ let margin = () => theme.spacing.sm
28
29
 
29
30
  /**
30
31
  * Secondary actions on the wrapped content. The opening gesture follows the
@@ -113,9 +114,9 @@ export function ContextMenu(props: ContextMenuProps) {
113
114
  let b = menu && getBoundingBox(menu)
114
115
  if (!b) return
115
116
  let p = point()
116
- let x = Math.round(Math.min(Math.max(p.x, MARGIN), env.windowSize.width - b.width - MARGIN))
117
+ let x = Math.round(Math.min(Math.max(p.x, margin()), env.windowSize.width - b.width - margin()))
117
118
  let y = Math.round(
118
- Math.max(p.y + b.height > env.windowSize.height - MARGIN ? p.y - b.height : p.y, MARGIN),
119
+ Math.max(p.y + b.height > env.windowSize.height - margin() ? p.y - b.height : p.y, margin()),
119
120
  )
120
121
  let cur = pos()
121
122
  if (!cur || cur.x !== x || cur.y !== y) setPos({ x, y })
@@ -130,7 +131,7 @@ export function ContextMenu(props: ContextMenuProps) {
130
131
  left={0}
131
132
  x={pos()?.x ?? -10000}
132
133
  y={pos()?.y ?? 0}
133
- minWidth={MIN_WIDTH}
134
+ minWidth={theme.size.menuMinWidth}
134
135
  flexDirection="column"
135
136
  paddingTop={theme.spacing.sm}
136
137
  paddingBottom={theme.spacing.sm}
@@ -177,6 +178,8 @@ export function ContextMenu(props: ContextMenuProps) {
177
178
 
178
179
  return (
179
180
  <view
181
+ transition={splitTransition(props.transition).root}
182
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
180
183
  onPointerDown={handleDown}
181
184
  onPointerMove={handleMove}
182
185
  onPointerUp={cancelHold}
package/src/divider.tsx CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { LayoutProps } from "@solidrt/core"
2
2
  import { theme } from "./theme"
3
- import type { StyleProps } from "./types"
3
+ import type { StyleProps, TransitionProps } from "./types"
4
+ import { splitTransition, transitionEndFor } from "./types"
4
5
 
5
- export interface DividerProps {
6
+ export interface DividerProps extends TransitionProps {
6
7
  // Line direction. Horizontal (default) is a full-width rule; vertical is a
7
8
  // full-height rule for use inside a row.
8
9
  orientation?: "horizontal" | "vertical"
@@ -22,14 +23,18 @@ export function Divider(props: DividerProps) {
22
23
  let styled = () => ({ ...theme.components.divider, ...props.style })
23
24
  let color = () => styled().backgroundColor ?? theme.color.border
24
25
 
26
+ let split = () => splitTransition(props.transition)
27
+
25
28
  return (
26
29
  <view
30
+ transition={split().root}
31
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
27
32
  width={vertical() ? thickness() : "auto"}
28
33
  height={vertical() ? "auto" : thickness()}
29
34
  alignSelf="stretch"
30
35
  {...props.layout}
31
36
  >
32
- <d-rect color={color()} />
37
+ <d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={color()} />
33
38
  </view>
34
39
  )
35
40
  }
@@ -22,7 +22,8 @@ import type { Color, Gradient, KeyEvent, LayoutProps, PointerEvent, TextInputHin
22
22
  import type { Element } from "solid-js"
23
23
  import type { MeasureTextOptions, TextRunRange } from "flux:rendertree"
24
24
  import { registerNavAction } from "./focus-nav"
25
- import type { StyleProps } from "./types"
25
+ import type { StyleProps, TransitionProps } from "./types"
26
+ import { splitTransition, transitionEndFor } from "./types"
26
27
  import { theme } from "./theme"
27
28
  import { policy } from "./policy"
28
29
  import { space } from "./spacing"
@@ -44,7 +45,7 @@ export type LineRender = {
44
45
  color: () => Color | Gradient
45
46
  }
46
47
 
47
- export interface EditorFieldProps {
48
+ export interface EditorFieldProps extends TransitionProps {
48
49
  /**
49
50
  * Creates the buffer the field edits, given the grapheme `step` from the
50
51
  * field's geometry (createTextEditorLayout.step); called once.
@@ -234,14 +235,14 @@ export function EditorField(props: EditorFieldProps) {
234
235
  })
235
236
 
236
237
  // Style overrides fall back to theme defaults. The border doubles as the
237
- // focus ring: primary while focused, when the focus-ring policy asks for a
238
- // visible indicator.
238
+ // focus ring (ring color at the focus width) while focused, when the
239
+ // focus-ring policy asks for a visible indicator.
239
240
  let textColor = () => props.style?.color ?? theme.color.text
240
241
  let surfaceColor = () => props.style?.backgroundColor ?? theme.color.surface
241
- let borderColor = () =>
242
- props.style?.borderColor ?? (focused() && policy.focusRing ? theme.color.primary : theme.color.border)
243
- let borderWidth = () => props.style?.borderWidth ?? theme.borderWidth.sm
244
- let borderRadius = () => props.style?.borderRadius ?? theme.radius.sm
242
+ let ring = () => focused() && policy.focusRing
243
+ let borderColor = () => props.style?.borderColor ?? (ring() ? theme.color.ring : theme.color.border)
244
+ let borderWidth = () => props.style?.borderWidth ?? (ring() ? theme.borderWidth.focus : theme.borderWidth.sm)
245
+ let borderRadius = () => props.style?.borderRadius ?? theme.radius.md
245
246
 
246
247
  let showPlaceholder = () => !focused() && value().length === 0 && (props.placeholder ?? "").length > 0
247
248
  let showCaret = () => focused() && caretOn() && !showPlaceholder()
@@ -290,8 +291,12 @@ export function EditorField(props: EditorFieldProps) {
290
291
  return Math.max(rowHeight(), Math.min(content, max))
291
292
  }
292
293
 
294
+ let split = () => splitTransition(props.transition)
295
+
293
296
  return (
294
297
  <view
298
+ transition={split().root}
299
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
295
300
  ref={(n: { id: number }) => {
296
301
  node = n
297
302
  unregisterNav?.()
@@ -304,8 +309,8 @@ export function EditorField(props: EditorFieldProps) {
304
309
  alignItems="center"
305
310
  paddingLeft={space("md")}
306
311
  paddingRight={space("md")}
307
- paddingTop={space("sm")}
308
- paddingBottom={space("sm")}
312
+ paddingTop={space("md")}
313
+ paddingBottom={space("md")}
309
314
  {...props.layout}
310
315
  x={props.style?.x}
311
316
  y={props.style?.y}
@@ -318,9 +323,11 @@ export function EditorField(props: EditorFieldProps) {
318
323
  onKeyDown={handleKeyDown}
319
324
  onTextInput={handleTextInput}
320
325
  >
321
- <d-rect color={surfaceColor()} radius={borderRadius()} />
326
+ <d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={surfaceColor()} radius={borderRadius()} />
322
327
  <d-rect
323
328
  drawStyle="stroke"
329
+ transition={split().border}
330
+ onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
324
331
  color={borderColor()}
325
332
  strokeWidth={borderWidth()}
326
333
  radius={borderRadius()}
package/src/field.tsx CHANGED
@@ -3,8 +3,10 @@ import type { LayoutProps } from "@solidrt/core"
3
3
  import { theme } from "./theme"
4
4
  import { space } from "./spacing"
5
5
  import { typeStyle } from "./typography"
6
+ import type { TransitionProps } from "./types"
7
+ import { splitTransition, transitionEndFor } from "./types"
6
8
 
7
- export interface FieldProps {
9
+ export interface FieldProps extends TransitionProps {
8
10
  // Label above the control.
9
11
  label?: string
10
12
  // Help text under the control; replaced by `error` while one is set.
@@ -25,7 +27,7 @@ export interface FieldProps {
25
27
  // `description` if that matters.
26
28
  export function Field(props: FieldProps) {
27
29
  return (
28
- <view flexDirection="column" gap={space("sm")} {...props.layout}>
30
+ <view flexDirection="column" gap={space("sm")} transition={splitTransition(props.transition).root} onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)} {...props.layout}>
29
31
  <Show when={props.label != null}>
30
32
  <text color={theme.color.text} {...typeStyle("label")}>
31
33
  {props.label}
package/src/icon.tsx CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createMemo, parseSvg, For } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
3
  import { theme } from "./theme"
4
+ import type { TransitionProps } from "./types"
5
+ import { splitTransition, transitionEndFor } from "./types"
4
6
 
5
- export interface IconProps {
7
+ export interface IconProps extends TransitionProps {
6
8
  // An SVG document as a string: an imported `.svg` asset, a `lucide-static`
7
9
  // string export, or an inline template literal. Monochrome icons that stroke/
8
10
  // fill with `currentColor` (Lucide, Feather, Heroicons, ...) get recolored by
@@ -18,9 +20,9 @@ export interface IconProps {
18
20
  const SIZE = 24
19
21
 
20
22
  // A themed wrapper over parseSvg: the document parsed once per src/color (a
21
- // memo), its draws mapped to <d-path> in a square viewBox-fitted box colored
23
+ // memo), its draws mapped to <d-path> in a square designSize-fitted box colored
22
24
  // from the theme by default. That is the only value it adds, so reach for
23
- // parseSvg plus your own <view viewBox> when you need a non-square box or
25
+ // parseSvg plus your own <view designSize> when you need a non-square box or
24
26
  // want no theme coupling. The plain repaintBoundary keeps the static subtree
25
27
  // from re-recording alongside animating siblings; pointerEvents="all" makes
26
28
  // the box one hit unit (and skips per-path outline tests) - an icon's shapes
@@ -31,11 +33,13 @@ export function Icon(props: IconProps) {
31
33
 
32
34
  return (
33
35
  <view
36
+ transition={splitTransition(props.transition).root}
37
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
34
38
  repaintBoundary
35
39
  pointerEvents="all"
36
40
  width={size()}
37
41
  height={size()}
38
- viewBox={[doc().width, doc().height]}
42
+ designSize={[doc().width, doc().height]}
39
43
  {...props.layout}
40
44
  >
41
45
  <For each={doc().draws}>{(draw) => <d-path {...draw} />}</For>
package/src/image.tsx CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createImage, createEffect, Loading, Errored, pct } from "@solidrt/core"
2
2
  import type { ImageSource, LayoutProps, Pct, PointerProps, TextureProps } from "@solidrt/core"
3
- import type { StyleProps } from "./types"
3
+ import type { StyleProps, TransitionProps } from "./types"
4
+ import { splitTransition, transitionEndFor } from "./types"
4
5
 
5
- export interface ImageProps extends PointerProps {
6
+ export interface ImageProps extends PointerProps, TransitionProps {
6
7
  src: string | Uint8Array
7
8
  /**
8
9
  * How the image maps into the Image's box (CSS object-fit): "fill"
@@ -38,6 +39,7 @@ function FallbackTexture(props: {
38
39
  height?: number | Pct
39
40
  }) {
40
41
  let tex = createImage(() => props.src)
42
+
41
43
  return (
42
44
  <Errored fallback={(_err: unknown) => null}>
43
45
  <texture src={tex()} fit={props.fit} width={props.width} height={props.height} />
@@ -69,9 +71,12 @@ export function Image(props: ImageProps) {
69
71
  let texW = () => (props.fit != null ? pct(100) : typeof props.layout?.width === "number" ? props.layout.width : undefined)
70
72
  let texH = () =>
71
73
  props.fit != null ? pct(100) : typeof props.layout?.height === "number" ? props.layout.height : undefined
74
+ let split = () => splitTransition(props.transition)
72
75
 
73
76
  return (
74
77
  <view
78
+ transition={split().root}
79
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
75
80
  {...props.layout}
76
81
  overflow={props.style?.borderRadius != null ? "hidden" : props.layout?.overflow}
77
82
  clipRadius={props.style?.borderRadius}
@@ -94,7 +99,7 @@ export function Image(props: ImageProps) {
94
99
  pointerEvents={props.pointerEvents}
95
100
  >
96
101
  {props.style?.backgroundColor != null ? (
97
- <d-rect color={props.style?.backgroundColor} radius={props.style?.borderRadius} />
102
+ <d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={props.style?.backgroundColor} radius={props.style?.borderRadius} />
98
103
  ) : null}
99
104
  <Loading fallback={null}>
100
105
  <Errored
@@ -110,6 +115,8 @@ export function Image(props: ImageProps) {
110
115
  {hasBorder() ? (
111
116
  <d-rect
112
117
  drawStyle="stroke"
118
+ transition={split().border}
119
+ onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
113
120
  color={props.style?.borderColor ?? "transparent"}
114
121
  strokeWidth={props.style?.borderWidth}
115
122
  radius={props.style?.borderRadius}
package/src/index.ts CHANGED
@@ -60,4 +60,13 @@ export {
60
60
  export { Density, type DensityProps, densityScale } from "./density"
61
61
  export { typeStyle, typeWeight, lightOnDark } from "./typography"
62
62
  export { space } from "./spacing"
63
- export type { StyleProps, TextLayoutProps, Option } from "./types"
63
+ export type {
64
+ StyleProps,
65
+ TextLayoutProps,
66
+ Option,
67
+ TransitionProps,
68
+ ComponentTransition,
69
+ TransitionViewProp,
70
+ TransitionStyleProp,
71
+ TransitionScrollProp,
72
+ } from "./types"
package/src/item.tsx CHANGED
@@ -5,16 +5,17 @@ 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"
8
+ import type { StyleProps, TransitionProps } from "./types"
9
+ import { splitTransition, transitionEndFor } from "./types"
9
10
 
10
- export interface ItemProps {
11
+ export interface ItemProps extends TransitionProps {
11
12
  // Leading content: an icon, avatar, checkbox, ...
12
13
  startContent?: any
13
14
  // Primary text. A string/number renders as themed body text; anything else
14
15
  // as-is.
15
16
  label: any
16
17
  // Secondary line under the label. A string/number renders as themed muted
17
- // caption text; anything else as-is.
18
+ // body text; anything else as-is.
18
19
  description?: any
19
20
  // Trailing content: a badge, timestamp, chevron, action, ...
20
21
  endContent?: any
@@ -63,8 +64,12 @@ export function Item(props: ItemProps) {
63
64
  let description = children(() => props.description)
64
65
  let descriptionIsText = () => typeof description() === "string" || typeof description() === "number"
65
66
 
67
+ let split = () => splitTransition(props.transition)
68
+
66
69
  return (
67
70
  <view
71
+ transition={split().root}
72
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
68
73
  ref={(n: { id: number }) => {
69
74
  press.ref(n)
70
75
  props.ref?.(n)
@@ -90,10 +95,10 @@ export function Item(props: ItemProps) {
90
95
  focusable={(props.focusable ?? true) && interactive()}
91
96
  pointerEvents={props.disabled ? "none" : undefined}
92
97
  >
93
- <d-rect color={bg()} radius={radius()} />
98
+ <d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={bg()} radius={radius()} />
94
99
  <d-rect color={overlay(press.state())} radius={radius()} />
95
100
  {props.startContent}
96
- <view flexDirection="column" flexGrow={1} flexShrink={1} gap={2}>
101
+ <view flexDirection="column" flexGrow={1} flexShrink={1} gap={Math.round(space("sm") / 2)}>
97
102
  <Show when={labelIsText()} fallback={label()}>
98
103
  <text color={theme.color.text} {...typeStyle("body")} maxLines={1}>
99
104
  {label()}
@@ -101,7 +106,7 @@ export function Item(props: ItemProps) {
101
106
  </Show>
102
107
  <Show when={props.description != null}>
103
108
  <Show when={descriptionIsText()} fallback={description()}>
104
- <text color={theme.color.textMuted} {...typeStyle("caption")} maxLines={1}>
109
+ <text color={theme.color.textMuted} {...typeStyle("body")} maxLines={1}>
105
110
  {description()}
106
111
  </text>
107
112
  </Show>
@@ -109,7 +114,7 @@ export function Item(props: ItemProps) {
109
114
  </view>
110
115
  {props.endContent}
111
116
  <Show when={press.focused() && policy.focusRing}>
112
- <d-rect drawStyle="stroke" color={theme.color.text} strokeWidth={2} radius={radius()} />
117
+ <d-rect drawStyle="stroke" color={theme.color.ring} strokeWidth={theme.borderWidth.focus} radius={radius()} />
113
118
  </Show>
114
119
  </view>
115
120
  )
package/src/nav-shell.tsx CHANGED
@@ -5,6 +5,8 @@ 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 { TransitionProps } from "./types"
9
+ import { splitTransition, transitionEndFor } from "./types"
8
10
 
9
11
  export interface NavItem {
10
12
  value: unknown
@@ -14,7 +16,7 @@ export interface NavItem {
14
16
  icon?: any
15
17
  }
16
18
 
17
- export interface NavShellProps {
19
+ export interface NavShellProps extends TransitionProps {
18
20
  items: NavItem[]
19
21
  // Controlled selected value. If omitted, the shell is uncontrolled.
20
22
  value?: unknown
@@ -25,9 +27,6 @@ export interface NavShellProps {
25
27
  layout?: LayoutProps
26
28
  }
27
29
 
28
- const RAIL_WIDTH = 72
29
- const SIDEBAR_WIDTH = 220
30
-
31
30
  /**
32
31
  * An app shell that arranges primary navigation around the content per the
33
32
  * navigation policy: bottom tabs under it, a narrow rail or a wide sidebar
@@ -77,15 +76,8 @@ export function NavShell(props: NavShellProps) {
77
76
  )
78
77
  }
79
78
 
80
- let Hairline = (p: { vertical?: boolean }) => (
81
- <view width={p.vertical ? 1 : undefined} height={p.vertical ? undefined : 1}>
82
- <d-rect color={theme.color.border} />
83
- </view>
84
- )
85
-
86
79
  let Tabs = () => (
87
80
  <view flexDirection="column" flexShrink={0}>
88
- <Hairline />
89
81
  <view flexDirection="row">
90
82
  <d-rect color={theme.color.surface} />
91
83
  <For each={props.items}>
@@ -97,17 +89,16 @@ export function NavShell(props: NavShellProps) {
97
89
 
98
90
  let Rail = () => (
99
91
  <view flexDirection="row" flexShrink={0}>
100
- <view flexDirection="column" width={RAIL_WIDTH} gap={theme.spacing.sm} paddingTop={theme.spacing.md}>
92
+ <view flexDirection="column" width={theme.size.navRail} gap={theme.spacing.sm} paddingTop={theme.spacing.md}>
101
93
  <d-rect color={theme.color.surface} />
102
94
  <For each={props.items}>{(item: NavItem) => <StackedItem item={item} padY={theme.spacing.md} />}</For>
103
95
  </view>
104
- <Hairline vertical />
105
96
  </view>
106
97
  )
107
98
 
108
99
  let Sidebar = () => (
109
100
  <view flexDirection="row" flexShrink={0}>
110
- <view flexDirection="column" width={SIDEBAR_WIDTH} gap={theme.spacing.sm} paddingTop={theme.spacing.md}>
101
+ <view flexDirection="column" width={theme.size.navSidebar} gap={theme.spacing.sm} paddingTop={theme.spacing.md}>
111
102
  <d-rect color={theme.color.surface} />
112
103
  <For each={props.items}>
113
104
  {(item: NavItem) => {
@@ -140,14 +131,13 @@ export function NavShell(props: NavShellProps) {
140
131
  }}
141
132
  </For>
142
133
  </view>
143
- <Hairline vertical />
144
134
  </view>
145
135
  )
146
136
 
147
137
  // Children order is (content, nav): "column" puts the nav under the content,
148
138
  // "row-reverse" puts it to the left, and the content node never moves.
149
139
  return (
150
- <view flexDirection={policy.navigation === "bottomTabs" ? "column" : "row-reverse"} {...props.layout}>
140
+ <view flexDirection={policy.navigation === "bottomTabs" ? "column" : "row-reverse"} transition={splitTransition(props.transition).root} onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)} {...props.layout}>
151
141
  <view flex={1} flexDirection="column">
152
142
  {props.children}
153
143
  </view>
package/src/policy.ts CHANGED
@@ -29,7 +29,8 @@ export type Policies = {
29
29
  // only (per-run polarity, or the theme's palette polarity as the default),
30
30
  // with one extra step for small font sizes; see typography.ts.
31
31
  textWeightDelta: number
32
- // Application policies: recommendations derived from the window size class.
32
+ // Application policies: recommendations derived from the window size class
33
+ // (layout), and from the resulting pane count (navigation).
33
34
  // The application owns the final decision; accept them by consuming
34
35
  // policy.navigation / policy.layout, or override via setPolicy.
35
36
  navigation: NavigationPolicy
@@ -52,6 +53,7 @@ export function defaultPolicyResolver(caps: Capabilities): Policies {
52
53
  : caps.precisePointer
53
54
  ? "desktop"
54
55
  : "hybrid"
56
+ let layout: LayoutPolicy = caps.windowSizeClass === "expanded" ? "twoPane" : "singlePane"
55
57
  return {
56
58
  interaction,
57
59
  density: interaction === "desktop" ? "compact" : "comfortable",
@@ -62,9 +64,12 @@ export function defaultPolicyResolver(caps: Capabilities): Policies {
62
64
  focusRing: caps.keyboardNav || gamepads().some((p) => p != null),
63
65
  textScale: env.textScale,
64
66
  textWeightDelta: env.displayScale < 1.5 ? 100 : 0,
65
- navigation:
66
- caps.windowSizeClass === "expanded" ? "sidebar" : caps.windowSizeClass === "medium" ? "rail" : "bottomTabs",
67
- layout: caps.windowSizeClass === "expanded" ? "twoPane" : "singlePane",
67
+ // Navigation follows the pane count, not its own breakpoint: a side strip
68
+ // spends width, which a single-pane window is short of, so only a
69
+ // two-pane layout earns one. "rail" is never a default output; it is the
70
+ // narrow side nav an app can pick for a content-dense two-pane layout.
71
+ navigation: layout === "twoPane" ? "sidebar" : "bottomTabs",
72
+ layout,
68
73
  }
69
74
  }
70
75
 
package/src/pressable.tsx CHANGED
@@ -1,11 +1,12 @@
1
1
  import { children } from "@solidrt/core"
2
2
  import type { LayoutProps, PointerProps } from "@solidrt/core"
3
- import type { StyleProps } from "./types"
3
+ import type { StyleProps, TransitionProps } from "./types"
4
+ import { splitTransition, transitionEndFor } from "./types"
4
5
  import { createPress, type PressState } from "./press"
5
6
 
6
7
  export type { PressState } from "./press"
7
8
 
8
- export interface PressableProps extends PointerProps {
9
+ export interface PressableProps extends PointerProps, TransitionProps {
9
10
  // children and style may be functions of the press state, so a caller can
10
11
  // restyle on press/hover without wiring their own signals. The state is live
11
12
  // (getters, not a snapshot): read it inside a prop or child expression, never
@@ -50,8 +51,12 @@ export function Pressable(props: PressableProps) {
50
51
  let hasBackground = () => style()?.backgroundColor != null || style()?.borderRadius != null
51
52
  let hasBorder = () => (style()?.borderWidth ?? 0) > 0
52
53
 
54
+ let split = () => splitTransition(props.transition)
55
+
53
56
  return (
54
57
  <view
58
+ transition={split().root}
59
+ onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
55
60
  ref={(n: { id: number }) => {
56
61
  press.ref(n)
57
62
  props.ref?.(n)
@@ -79,6 +84,8 @@ export function Pressable(props: PressableProps) {
79
84
  >
80
85
  {hasBackground() ? (
81
86
  <d-rect
87
+ transition={split().background}
88
+ onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
82
89
  color={style()?.backgroundColor ?? "transparent"}
83
90
  radius={style()?.borderRadius}
84
91
  />
@@ -87,6 +94,8 @@ export function Pressable(props: PressableProps) {
87
94
  {hasBorder() ? (
88
95
  <d-rect
89
96
  drawStyle="stroke"
97
+ transition={split().border}
98
+ onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
90
99
  color={style()?.borderColor ?? "transparent"}
91
100
  strokeWidth={style()?.borderWidth}
92
101
  radius={style()?.borderRadius}
@@ -2,9 +2,10 @@ import { createSignal, onFrame, onLayout, getBoundingBox, Show } from "@solidrt/
2
2
  import type { LayoutProps } from "@solidrt/core"
3
3
  import { theme } from "./theme"
4
4
  import { policy } from "./policy"
5
- import type { StyleProps } from "./types"
5
+ import type { StyleProps, TransitionProps } from "./types"
6
+ import { splitTransition, transitionEndFor } from "./types"
6
7
 
7
- export interface ProgressBarProps {
8
+ export interface ProgressBarProps extends TransitionProps {
8
9
  // Progress from 0 to 1. Omit (or leave undefined) for an indeterminate bar: a
9
10
  // segment that slides back and forth.
10
11
  value?: number
@@ -71,7 +72,7 @@ export function ProgressBar(props: ProgressBarProps) {
71
72
  let offset = () => effectivePhase() * trackWidth() * (1 - SEGMENT)
72
73
 
73
74
  return (
74
- <view ref={(n: { id: number }) => (trackNode = n)} position="relative" width="100%" height={h()} {...props.layout}>
75
+ <view ref={(n: { id: number }) => (trackNode = n)} position="relative" width="100%" height={h()} transition={splitTransition(props.transition).root} onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)} {...props.layout}>
75
76
  <Show when={animating()}>
76
77
  <Animate />
77
78
  </Show>
package/src/qrcode.tsx CHANGED
@@ -1,8 +1,11 @@
1
1
  import { createMemo } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
3
  import qrcode from "qrcode-generator"
4
+ import { theme } from "./theme"
5
+ import type { TransitionProps } from "./types"
6
+ import { splitTransition, transitionEndFor } from "./types"
4
7
 
5
- export interface QrCodeProps {
8
+ export interface QrCodeProps extends TransitionProps {
6
9
  // The string to encode (URL, pairing ticket, text, ...).
7
10
  data: string
8
11
  // Pixels per QR module (the smallest square). The grid is
@@ -18,14 +21,13 @@ export interface QrCodeProps {
18
21
  // Error-correction level: higher tolerates more damage but packs denser and
19
22
  // caps the data length sooner.
20
23
  level?: "L" | "M" | "Q" | "H"
21
- // Corner radius of the background panel.
24
+ // Corner radius of the background panel; defaults to the theme control radius.
22
25
  radius?: number
23
26
  layout?: LayoutProps
24
27
  }
25
28
 
26
29
  const MODULE_SIZE = 6
27
30
  const MARGIN = 16
28
- const RADIUS = 8
29
31
 
30
32
  // Render a QR for `data` as primitives: merge horizontal runs of dark modules
31
33
  // per row into a single d-rect, placed at explicit coordinates on a light
@@ -62,8 +64,8 @@ export function QrCode(props: QrCodeProps) {
62
64
  let side = () => grid().n * size() + 2 * margin()
63
65
 
64
66
  return (
65
- <view repaintBoundary width={side()} height={side()} {...props.layout}>
66
- <d-rect color={props.background ?? "#ffffff"} radius={props.radius ?? RADIUS} />
67
+ <view repaintBoundary width={side()} height={side()} transition={splitTransition(props.transition).root} onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)} {...props.layout}>
68
+ <d-rect color={props.background ?? "#ffffff"} radius={props.radius ?? theme.radius.md} />
67
69
  {grid().runs.map((run) => (
68
70
  <d-rect
69
71
  x={margin() + run.x * size()}