@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/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} />
|
package/src/context-menu.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 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
|
-
|
|
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,
|
|
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 -
|
|
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={
|
|
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
|
}
|
package/src/editor-field.tsx
CHANGED
|
@@ -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
|
|
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
|
|
242
|
-
|
|
243
|
-
let borderWidth = () => props.style?.borderWidth ?? theme.borderWidth.sm
|
|
244
|
-
let borderRadius = () => props.style?.borderRadius ?? theme.radius.
|
|
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("
|
|
308
|
-
paddingBottom={space("
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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("
|
|
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.
|
|
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={
|
|
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={
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
layout
|
|
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}
|
package/src/progress-bar.tsx
CHANGED
|
@@ -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 ??
|
|
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()}
|