@solidrt/components 0.0.51 → 0.0.52
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/demos/README.md +24 -0
- package/demos/assets/icon.png +0 -0
- package/demos/assets/icon.svg +23 -0
- package/demos/package.json +9 -0
- package/demos/src/gallery.tsx +866 -0
- package/demos/tsconfig.json +15 -0
- 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 +4 -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/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()}
|
package/src/radio.tsx
CHANGED
|
@@ -2,9 +2,11 @@ import { createSignal, createContext, useContext, Show, children } from "@solidr
|
|
|
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 { typeStyle } from "./typography"
|
|
7
|
-
import type { StyleProps } from "./types"
|
|
8
|
+
import type { StyleProps, TransitionProps } from "./types"
|
|
9
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
8
10
|
|
|
9
11
|
// Shared selection state for a group. Created and consumed within this module, so
|
|
10
12
|
// RadioGroup/Radio are a self-contained pair, not a cross-component dependency.
|
|
@@ -51,7 +53,7 @@ export function RadioGroup(props: RadioGroupProps) {
|
|
|
51
53
|
)
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
export interface RadioProps {
|
|
56
|
+
export interface RadioProps extends TransitionProps {
|
|
55
57
|
// This option's value; selecting it makes it the group's value.
|
|
56
58
|
value: unknown
|
|
57
59
|
disabled?: boolean
|
|
@@ -71,7 +73,6 @@ export function Radio(props: RadioProps) {
|
|
|
71
73
|
let ctx = useContext(RadioContext)
|
|
72
74
|
let selected = () => ctx.value() === props.value
|
|
73
75
|
let disabled = () => props.disabled || ctx.disabled()
|
|
74
|
-
let ringColor = () => (selected() ? theme.color.primary : theme.color.border)
|
|
75
76
|
// Resolved once via children(): the typeof probe and the mount sites must
|
|
76
77
|
// share one build - reading the raw getter again would orphan native nodes.
|
|
77
78
|
let resolved = children(() => props.children)
|
|
@@ -84,9 +85,15 @@ export function Radio(props: RadioProps) {
|
|
|
84
85
|
// Theme-level per-component overrides merged under the instance style.
|
|
85
86
|
let styled = () => ({ ...theme.components.radio, ...props.style })
|
|
86
87
|
let press = createPress({ onPress: () => ctx.select(props.value) })
|
|
88
|
+
// The circle doubles as the focus ring: ring color at the focus width.
|
|
89
|
+
let focusRing = () => press.focused() && policy.focusRing
|
|
90
|
+
let ringColor = () => (focusRing() ? theme.color.ring : selected() ? theme.color.primary : theme.color.border)
|
|
91
|
+
let ringWidth = () => (focusRing() ? theme.borderWidth.focus : 2)
|
|
87
92
|
|
|
88
93
|
return (
|
|
89
94
|
<view
|
|
95
|
+
transition={splitTransition(props.transition).root}
|
|
96
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
90
97
|
ref={press.ref}
|
|
91
98
|
repaintBoundary
|
|
92
99
|
flexDirection="row"
|
|
@@ -99,13 +106,14 @@ export function Radio(props: RadioProps) {
|
|
|
99
106
|
rotate={styled().rotate}
|
|
100
107
|
opacity={styled().opacity}
|
|
101
108
|
{...press.handlers}
|
|
109
|
+
focusable={!disabled()}
|
|
102
110
|
pointerEvents={disabled() ? "none" : undefined}
|
|
103
111
|
>
|
|
104
112
|
<Show when={styled().backgroundColor != null || styled().borderRadius != null}>
|
|
105
113
|
<d-rect color={styled().backgroundColor ?? "transparent"} radius={styled().borderRadius} />
|
|
106
114
|
</Show>
|
|
107
115
|
<view width={ring()} height={ring()}>
|
|
108
|
-
<d-oval drawStyle="stroke" color={ringColor()} strokeWidth={
|
|
116
|
+
<d-oval drawStyle="stroke" color={ringColor()} strokeWidth={ringWidth()} />
|
|
109
117
|
<Show when={selected()}>
|
|
110
118
|
<d-oval x={inset()} y={inset()} w={ring() - inset() * 2} h={ring() - inset() * 2} color={theme.color.primary} />
|
|
111
119
|
</Show>
|
package/src/rich-text-editor.tsx
CHANGED
|
@@ -3,11 +3,11 @@ import type { LayoutProps, TextInputHints } from "@solidrt/core"
|
|
|
3
3
|
import type { TextRunRange } from "flux:rendertree"
|
|
4
4
|
import { EditorField } from "./editor-field"
|
|
5
5
|
import { createDocumentBuffer, type Attributes, type Document, type DocumentBuffer } from "./rich-text-document"
|
|
6
|
-
import type { StyleProps } from "./types"
|
|
6
|
+
import type { StyleProps, TransitionProps } from "./types"
|
|
7
7
|
import { theme } from "./theme"
|
|
8
8
|
import { policy } from "./policy"
|
|
9
9
|
|
|
10
|
-
export interface RichTextEditorProps {
|
|
10
|
+
export interface RichTextEditorProps extends TransitionProps {
|
|
11
11
|
value?: Document
|
|
12
12
|
defaultValue?: Document
|
|
13
13
|
onInput?: (value: Document) => void
|
|
@@ -48,7 +48,7 @@ function fontOf(inline: Attributes, block: Attributes, base: number): Font {
|
|
|
48
48
|
else if (heading === 3) font.fontSize = base
|
|
49
49
|
if (heading === 1 || heading === 2 || heading === 3 || inline.bold) font.fontWeight = 700
|
|
50
50
|
if (inline.italic) font.fontStyle = "italic"
|
|
51
|
-
if (inline.code) font.fontFamily =
|
|
51
|
+
if (inline.code) font.fontFamily = theme.text.monoFamily
|
|
52
52
|
return font
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -111,6 +111,8 @@ export function RichTextEditor(props: RichTextEditorProps) {
|
|
|
111
111
|
|
|
112
112
|
return (
|
|
113
113
|
<EditorField
|
|
114
|
+
transition={props.transition}
|
|
115
|
+
onTransitionEnd={props.onTransitionEnd}
|
|
114
116
|
buffer={(step) => {
|
|
115
117
|
editor = createDocumentBuffer({
|
|
116
118
|
value: () => props.value,
|
package/src/scroll-view.tsx
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { createPan, createScroll } from "@solidrt/core"
|
|
2
|
-
import type { LayoutProps, PointerProps, WheelEvent } from "@solidrt/core"
|
|
3
|
-
import type { StyleProps } from "./types"
|
|
1
|
+
import { createPan, createScroll, createSignal, onSettled, untrack } from "@solidrt/core"
|
|
2
|
+
import type { LayoutProps, PointerProps, Scroll, WheelEvent } from "@solidrt/core"
|
|
3
|
+
import type { StyleProps, TransitionProps, TransitionScrollProp, TransitionStyleProp, TransitionViewProp } from "./types"
|
|
4
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
4
5
|
|
|
5
|
-
export interface ScrollViewProps
|
|
6
|
+
export interface ScrollViewProps
|
|
7
|
+
extends PointerProps,
|
|
8
|
+
TransitionProps<TransitionViewProp | TransitionStyleProp | TransitionScrollProp> {
|
|
6
9
|
children?: any
|
|
7
10
|
ref?: (node: { id: number }) => void
|
|
8
11
|
layout?: LayoutProps
|
|
9
12
|
style?: StyleProps
|
|
10
13
|
/** Scroll the horizontal axis instead of the vertical one. */
|
|
11
14
|
horizontal?: boolean
|
|
15
|
+
/** Receives the scroll handle (offset, range, scrollTo) for driving the view
|
|
16
|
+
* from app code; scroll policies such as following a growing log are written
|
|
17
|
+
* against it. Called once the component has settled, outside any reactive
|
|
18
|
+
* scope, so a signal setter can be passed directly. */
|
|
19
|
+
scrollRef?: (scroll: Scroll) => void
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
// A scrollable region. The outer box carries layout/style/transform and the
|
|
@@ -21,31 +29,81 @@ export interface ScrollViewProps extends PointerProps {
|
|
|
21
29
|
// stealing the pointer from a pressable the drag started on (its press
|
|
22
30
|
// feedback retracts), and keeps scrolling when the pointer leaves the box.
|
|
23
31
|
// There is no momentum yet; a fling stops when the finger lifts.
|
|
32
|
+
//
|
|
33
|
+
// Motion: the offset is written as a target and the runtime springs to it,
|
|
34
|
+
// so a wheel tick glides instead of jumping and a burst of ticks retargets
|
|
35
|
+
// one continuous motion. While a finger drags, the spring is withdrawn from
|
|
36
|
+
// the viewport declaration so the content tracks the finger exactly; the
|
|
37
|
+
// first drag write cancels any spring still in flight. A `scrollX`/`scrollY`
|
|
38
|
+
// entry in the `transition` prop replaces the default.
|
|
39
|
+
const SCROLL_SPRING = { duration: 250 }
|
|
40
|
+
|
|
24
41
|
export function ScrollView(props: ScrollViewProps) {
|
|
25
42
|
let viewport: { id: number } | undefined
|
|
26
43
|
let content: { id: number } | undefined
|
|
44
|
+
let [dragging, setDragging] = createSignal(false)
|
|
27
45
|
|
|
28
46
|
let scroll = createScroll(
|
|
29
47
|
() => viewport,
|
|
30
48
|
() => content,
|
|
31
49
|
{ axis: props.horizontal ? "horizontal" : "vertical" },
|
|
32
50
|
)
|
|
51
|
+
// Handed out from onSettled rather than the body: the body is an owned
|
|
52
|
+
// scope, where a signal write (an app passing its setter) is refused.
|
|
53
|
+
onSettled(() => {
|
|
54
|
+
untrack(() => props.scrollRef)?.(scroll)
|
|
55
|
+
})
|
|
33
56
|
|
|
34
57
|
// Content follows the finger: it moves opposite to scroll offsets, which
|
|
35
58
|
// grow toward the bottom/right.
|
|
36
59
|
let pan = createPan({
|
|
37
60
|
axis: props.horizontal ? "horizontal" : "vertical",
|
|
38
|
-
|
|
61
|
+
onPanStart: () => setDragging(true),
|
|
62
|
+
onPanMove: (dx, dy) => scroll.scrollBy({ x: -dx, y: -dy }),
|
|
63
|
+
onPanEnd: () => setDragging(false),
|
|
39
64
|
})
|
|
40
65
|
|
|
41
66
|
let onWheel = (e: WheelEvent) => {
|
|
42
67
|
// A plain mouse wheel only emits deltaY. On a horizontal scroller, route that
|
|
43
68
|
// vertical delta to the x axis so the wheel still scrolls it (trackpads that
|
|
44
69
|
// emit deltaX take precedence).
|
|
45
|
-
if (props.horizontal) scroll.scrollBy(e.deltaX || e.deltaY
|
|
46
|
-
else scroll.scrollBy(e.deltaX, e.deltaY)
|
|
70
|
+
if (props.horizontal) scroll.scrollBy({ x: e.deltaX || e.deltaY })
|
|
71
|
+
else scroll.scrollBy({ x: e.deltaX, y: e.deltaY })
|
|
47
72
|
}
|
|
48
73
|
|
|
74
|
+
// The viewport owns the scroll offset, the outer box everything else: a
|
|
75
|
+
// scrollX/scrollY entry is lifted out of the root declaration so that a
|
|
76
|
+
// shared `all` does not animate opacity twice (outer times viewport).
|
|
77
|
+
let split = () => {
|
|
78
|
+
let t = splitTransition(props.transition)
|
|
79
|
+
if (t.root == null || typeof t.root === "string") return { ...t, viewport: t.root }
|
|
80
|
+
let { scrollX, scrollY, ...rest } = t.root as Record<string, unknown>
|
|
81
|
+
let viewport: Record<string, unknown> = {}
|
|
82
|
+
if (scrollX !== undefined) viewport.scrollX = scrollX
|
|
83
|
+
if (scrollY !== undefined) viewport.scrollY = scrollY
|
|
84
|
+
if (rest.all !== undefined) viewport.all = rest.all
|
|
85
|
+
return {
|
|
86
|
+
...t,
|
|
87
|
+
root: Object.keys(rest).length ? (rest as typeof t.root) : undefined,
|
|
88
|
+
viewport: Object.keys(viewport).length ? (viewport as typeof t.root) : undefined,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// The viewport's declaration: the user's scroll entries over the default
|
|
92
|
+
// spring. During a drag, and while the latest programmatic write asked for
|
|
93
|
+
// no motion (scrollTo behavior "instant"), the scroll entries go, and a
|
|
94
|
+
// user `all` narrows to the one other property the viewport writes
|
|
95
|
+
// (clipRadius) so it cannot put a spring back under the finger or the
|
|
96
|
+
// instant write.
|
|
97
|
+
let viewportTransition = () => {
|
|
98
|
+
let user = split().viewport
|
|
99
|
+
let entries: Record<string, unknown> = typeof user === "string" ? { all: user } : { ...(user ?? {}) }
|
|
100
|
+
if (dragging() || scroll.behavior() === "instant") {
|
|
101
|
+
let { scrollX, scrollY, all, ...rest } = entries
|
|
102
|
+
if (all !== undefined) rest.clipRadius = all
|
|
103
|
+
return Object.keys(rest).length ? rest : null
|
|
104
|
+
}
|
|
105
|
+
return { scrollX: SCROLL_SPRING, scrollY: SCROLL_SPRING, ...entries }
|
|
106
|
+
}
|
|
49
107
|
let direction = () => (props.horizontal ? "row" : "column")
|
|
50
108
|
let hasBackground = () =>
|
|
51
109
|
props.style?.backgroundColor != null || props.style?.borderRadius != null
|
|
@@ -53,6 +111,8 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
53
111
|
|
|
54
112
|
return (
|
|
55
113
|
<view
|
|
114
|
+
transition={split().root}
|
|
115
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
56
116
|
ref={props.ref}
|
|
57
117
|
{...props.layout}
|
|
58
118
|
x={props.style?.x}
|
|
@@ -70,16 +130,23 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
70
130
|
>
|
|
71
131
|
{hasBackground() ? (
|
|
72
132
|
<d-rect
|
|
133
|
+
transition={split().background}
|
|
134
|
+
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
73
135
|
color={props.style?.backgroundColor ?? "transparent"}
|
|
74
136
|
radius={props.style?.borderRadius}
|
|
75
137
|
/>
|
|
76
138
|
) : null}
|
|
139
|
+
{/* transition before scrollX/scrollY: props apply in source order, and
|
|
140
|
+
an instant write needs the withdrawn declaration to land before the
|
|
141
|
+
value in the same flush, or the value starts a spring anyway. */}
|
|
77
142
|
<view
|
|
78
143
|
ref={(n: { id: number }) => (viewport = n)}
|
|
79
144
|
flex={1}
|
|
80
145
|
overflow="hidden"
|
|
81
146
|
clipRadius={props.style?.borderRadius}
|
|
82
147
|
flexDirection={direction()}
|
|
148
|
+
transition={viewportTransition()}
|
|
149
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
83
150
|
scrollX={scroll.offset().x}
|
|
84
151
|
scrollY={scroll.offset().y}
|
|
85
152
|
{...pan.handlers}
|
|
@@ -92,6 +159,8 @@ export function ScrollView(props: ScrollViewProps) {
|
|
|
92
159
|
{hasBorder() ? (
|
|
93
160
|
<d-rect
|
|
94
161
|
drawStyle="stroke"
|
|
162
|
+
transition={split().border}
|
|
163
|
+
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
95
164
|
color={props.style?.borderColor ?? "transparent"}
|
|
96
165
|
strokeWidth={props.style?.borderWidth}
|
|
97
166
|
radius={props.style?.borderRadius}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { createSignal, For } from "@solidrt/core"
|
|
1
|
+
import { createSignal, For, 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
5
|
import { policy } from "./policy"
|
|
6
6
|
import { space } from "./spacing"
|
|
7
7
|
import { typeStyle, lightOnDark } from "./typography"
|
|
8
|
-
import type { Option, StyleProps } from "./types"
|
|
8
|
+
import type { Option, StyleProps, TransitionProps } from "./types"
|
|
9
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
9
10
|
|
|
10
|
-
export interface SegmentedControlProps {
|
|
11
|
+
export interface SegmentedControlProps extends TransitionProps {
|
|
11
12
|
options: Option[]
|
|
12
13
|
// Controlled selected value. If omitted, the control is uncontrolled.
|
|
13
14
|
value?: unknown
|
|
@@ -59,8 +60,12 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
|
|
59
60
|
let label = (active: boolean) =>
|
|
60
61
|
props.disabled ? theme.color.textMuted : active ? theme.color.onPrimary : theme.color.text
|
|
61
62
|
|
|
63
|
+
let split = () => splitTransition(props.transition)
|
|
64
|
+
|
|
62
65
|
return (
|
|
63
66
|
<view
|
|
67
|
+
transition={split().root}
|
|
68
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
64
69
|
flexDirection="row"
|
|
65
70
|
gap={DIVIDER}
|
|
66
71
|
{...props.layout}
|
|
@@ -70,7 +75,7 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
|
|
70
75
|
rotate={styled().rotate}
|
|
71
76
|
opacity={styled().opacity}
|
|
72
77
|
>
|
|
73
|
-
<d-rect color={theme.color.border} radius={radius()} />
|
|
78
|
+
<d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={theme.color.border} radius={radius()} />
|
|
74
79
|
<For each={props.options}>
|
|
75
80
|
{(opt, i) => {
|
|
76
81
|
let active = () => value() === opt.value
|
|
@@ -81,7 +86,7 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
|
|
81
86
|
press.hovered() && !props.disabled && policy.interaction !== "touch"
|
|
82
87
|
? theme.color.overlayHover
|
|
83
88
|
: "transparent"
|
|
84
|
-
|
|
89
|
+
return (
|
|
85
90
|
<view
|
|
86
91
|
ref={press.ref}
|
|
87
92
|
repaintBoundary
|
|
@@ -93,10 +98,14 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
|
|
93
98
|
paddingLeft={space("md")}
|
|
94
99
|
paddingRight={space("md")}
|
|
95
100
|
{...press.handlers}
|
|
101
|
+
focusable={!props.disabled}
|
|
96
102
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
97
103
|
>
|
|
98
104
|
<d-rect color={fill()} radius={corners(i())} />
|
|
99
105
|
<d-rect color={overlay()} radius={corners(i())} />
|
|
106
|
+
<Show when={press.focused() && policy.focusRing}>
|
|
107
|
+
<d-rect drawStyle="stroke" color={theme.color.ring} strokeWidth={theme.borderWidth.focus} radius={corners(i())} />
|
|
108
|
+
</Show>
|
|
100
109
|
<text
|
|
101
110
|
color={label(active())}
|
|
102
111
|
{...typeStyle("body", active() ? lightOnDark(label(true), activeFill()) : undefined)}
|