@solidrt/components 0.0.50 → 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 +93 -85
- package/README.md +421 -311
- 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/badge.md +10 -0
- package/docs/button.md +21 -0
- package/docs/card.md +11 -0
- package/docs/checkbox.md +9 -0
- package/docs/context-menu.md +17 -0
- package/docs/density.md +13 -0
- package/docs/divider.md +10 -0
- package/docs/field.md +11 -0
- package/docs/focus-nav.md +18 -0
- package/docs/icon.md +13 -0
- package/docs/image.md +21 -0
- package/docs/index.md +13 -0
- package/docs/item.md +25 -0
- package/docs/modal.md +15 -0
- package/docs/nav-shell.md +18 -0
- package/docs/policy.md +21 -0
- package/docs/portal.md +15 -0
- package/docs/pressable.md +17 -0
- package/docs/progress-bar.md +10 -0
- package/docs/qrcode.md +10 -0
- package/docs/radio.md +13 -0
- package/docs/rich-text-document.md +5 -0
- package/docs/rich-text-editor.md +24 -0
- package/docs/safe-area.md +12 -0
- package/docs/scroll-view.md +35 -0
- package/docs/segmented-control.md +13 -0
- package/docs/select.md +15 -0
- package/docs/slider.md +9 -0
- package/docs/spacing.md +7 -0
- package/docs/spinner.md +10 -0
- package/docs/split-view.md +14 -0
- package/docs/switch.md +13 -0
- package/docs/text-input.md +23 -0
- package/docs/text.md +15 -0
- package/docs/theme.md +68 -0
- package/docs/tooltip.md +11 -0
- package/docs/types.md +9 -0
- package/docs/typography.md +5 -0
- package/docs/view.md +14 -0
- package/docs/window.md +15 -0
- package/package.json +8 -4
- package/src/badge.tsx +30 -21
- package/src/button.tsx +50 -32
- package/src/card.tsx +22 -13
- package/src/checkbox.tsx +29 -11
- package/src/context-menu.tsx +14 -9
- package/src/density.tsx +39 -0
- package/src/divider.tsx +11 -4
- package/src/editor-field.tsx +368 -0
- package/src/field.tsx +47 -0
- package/src/icon.tsx +8 -4
- package/src/image.tsx +10 -3
- package/src/index.ts +19 -2
- package/src/item.tsx +121 -0
- package/src/nav-shell.tsx +7 -17
- package/src/policy.ts +9 -12
- package/src/press.ts +37 -8
- package/src/pressable.tsx +15 -3
- package/src/progress-bar.tsx +8 -5
- package/src/qrcode.tsx +7 -5
- package/src/radio.tsx +26 -16
- package/src/rich-text-document.ts +247 -0
- package/src/rich-text-editor.tsx +151 -0
- package/src/scroll-view.tsx +76 -7
- package/src/segmented-control.tsx +33 -19
- package/src/select.tsx +59 -30
- package/src/slider.tsx +32 -6
- package/src/spacing.ts +1 -1
- package/src/spinner.tsx +11 -6
- package/src/split-view.tsx +3 -4
- package/src/switch.tsx +10 -3
- package/src/text-input.tsx +54 -264
- package/src/text.tsx +22 -2
- package/src/theme.ts +220 -81
- package/src/tooltip.tsx +23 -9
- package/src/types.ts +119 -1
- package/src/view.tsx +11 -2
package/src/item.tsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Show, children } from "@solidrt/core"
|
|
2
|
+
import type { LayoutProps } from "@solidrt/core"
|
|
3
|
+
import { createPress, type PressState } from "./press"
|
|
4
|
+
import { theme } from "./theme"
|
|
5
|
+
import { policy } from "./policy"
|
|
6
|
+
import { space } from "./spacing"
|
|
7
|
+
import { typeStyle } from "./typography"
|
|
8
|
+
import type { StyleProps, TransitionProps } from "./types"
|
|
9
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
10
|
+
|
|
11
|
+
export interface ItemProps extends TransitionProps {
|
|
12
|
+
// Leading content: an icon, avatar, checkbox, ...
|
|
13
|
+
startContent?: any
|
|
14
|
+
// Primary text. A string/number renders as themed body text; anything else
|
|
15
|
+
// as-is.
|
|
16
|
+
label: any
|
|
17
|
+
// Secondary line under the label. A string/number renders as themed muted
|
|
18
|
+
// body text; anything else as-is.
|
|
19
|
+
description?: any
|
|
20
|
+
// Trailing content: a badge, timestamp, chevron, action, ...
|
|
21
|
+
endContent?: any
|
|
22
|
+
// Present = the row is interactive: hover/pressed overlay tints, focusable,
|
|
23
|
+
// Enter/remote activation. A returned promise defers further activations
|
|
24
|
+
// until it settles; non-thenable returns are ignored.
|
|
25
|
+
onPress?: () => unknown
|
|
26
|
+
// Fills the row with surfaceAlt to mark the current selection.
|
|
27
|
+
selected?: boolean
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
// Focus-navigation candidacy; defaults to true for interactive rows.
|
|
30
|
+
focusable?: boolean
|
|
31
|
+
ref?: (node: { id: number }) => void
|
|
32
|
+
layout?: LayoutProps
|
|
33
|
+
style?: StyleProps
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A list row: leading content, a label with an optional description under it,
|
|
37
|
+
// trailing content pushed to the end. The dense-data workhorse - rows compose
|
|
38
|
+
// with <For> inside a plain column (or a ScrollView); this package ships no
|
|
39
|
+
// List wrapper because a column view IS the list. Paddings and the gap are
|
|
40
|
+
// density-scaled, so a <Density> region compacts rows wholesale. With onPress
|
|
41
|
+
// the row presses like a menu entry: overlay tints for hover/pressed (no
|
|
42
|
+
// scale - rows sit flush in a list), focus ring under the focusRing policy.
|
|
43
|
+
export function Item(props: ItemProps) {
|
|
44
|
+
// Theme-level per-component overrides merged under the instance style.
|
|
45
|
+
let styled = () => ({ ...theme.components.item, ...props.style })
|
|
46
|
+
let interactive = () => props.onPress != null && !props.disabled
|
|
47
|
+
let press = createPress(props)
|
|
48
|
+
|
|
49
|
+
let bg = () => styled().backgroundColor ?? (props.selected ? theme.color.surfaceAlt : "transparent")
|
|
50
|
+
let overlay = (s: PressState) =>
|
|
51
|
+
!interactive()
|
|
52
|
+
? "transparent"
|
|
53
|
+
: s.pressed
|
|
54
|
+
? theme.color.overlayPressed
|
|
55
|
+
: s.hovered && policy.interaction !== "touch"
|
|
56
|
+
? theme.color.overlayHover
|
|
57
|
+
: "transparent"
|
|
58
|
+
let radius = () => styled().borderRadius ?? theme.radius.sm
|
|
59
|
+
|
|
60
|
+
// Resolved once via children(): the typeof probe and the mount site must
|
|
61
|
+
// share one build (see Button).
|
|
62
|
+
let label = children(() => props.label)
|
|
63
|
+
let labelIsText = () => typeof label() === "string" || typeof label() === "number"
|
|
64
|
+
let description = children(() => props.description)
|
|
65
|
+
let descriptionIsText = () => typeof description() === "string" || typeof description() === "number"
|
|
66
|
+
|
|
67
|
+
let split = () => splitTransition(props.transition)
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<view
|
|
71
|
+
transition={split().root}
|
|
72
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
73
|
+
ref={(n: { id: number }) => {
|
|
74
|
+
press.ref(n)
|
|
75
|
+
props.ref?.(n)
|
|
76
|
+
}}
|
|
77
|
+
repaintBoundary
|
|
78
|
+
flexDirection="row"
|
|
79
|
+
alignItems="center"
|
|
80
|
+
gap={space("md")}
|
|
81
|
+
paddingTop={space("md")}
|
|
82
|
+
paddingBottom={space("md")}
|
|
83
|
+
paddingLeft={space("lg")}
|
|
84
|
+
paddingRight={space("lg")}
|
|
85
|
+
{...props.layout}
|
|
86
|
+
x={styled().x}
|
|
87
|
+
y={styled().y}
|
|
88
|
+
scale={styled().scale}
|
|
89
|
+
rotate={styled().rotate}
|
|
90
|
+
opacity={props.disabled ? 0.5 : styled().opacity}
|
|
91
|
+
// A passive row attaches no press recognizer: it must not claim
|
|
92
|
+
// pointers from its children (a Switch in a settings row) or from an
|
|
93
|
+
// enclosing pressable. Interactivity is decided at mount.
|
|
94
|
+
{...(props.onPress != null ? press.handlers : {})}
|
|
95
|
+
focusable={(props.focusable ?? true) && interactive()}
|
|
96
|
+
pointerEvents={props.disabled ? "none" : undefined}
|
|
97
|
+
>
|
|
98
|
+
<d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={bg()} radius={radius()} />
|
|
99
|
+
<d-rect color={overlay(press.state())} radius={radius()} />
|
|
100
|
+
{props.startContent}
|
|
101
|
+
<view flexDirection="column" flexGrow={1} flexShrink={1} gap={Math.round(space("sm") / 2)}>
|
|
102
|
+
<Show when={labelIsText()} fallback={label()}>
|
|
103
|
+
<text color={theme.color.text} {...typeStyle("body")} maxLines={1}>
|
|
104
|
+
{label()}
|
|
105
|
+
</text>
|
|
106
|
+
</Show>
|
|
107
|
+
<Show when={props.description != null}>
|
|
108
|
+
<Show when={descriptionIsText()} fallback={description()}>
|
|
109
|
+
<text color={theme.color.textMuted} {...typeStyle("body")} maxLines={1}>
|
|
110
|
+
{description()}
|
|
111
|
+
</text>
|
|
112
|
+
</Show>
|
|
113
|
+
</Show>
|
|
114
|
+
</view>
|
|
115
|
+
{props.endContent}
|
|
116
|
+
<Show when={press.focused() && policy.focusRing}>
|
|
117
|
+
<d-rect drawStyle="stroke" color={theme.color.ring} strokeWidth={theme.borderWidth.focus} radius={radius()} />
|
|
118
|
+
</Show>
|
|
119
|
+
</view>
|
|
120
|
+
)
|
|
121
|
+
}
|
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
|
|
@@ -49,7 +48,7 @@ export function NavShell(props: NavShellProps) {
|
|
|
49
48
|
item.value === value()
|
|
50
49
|
? theme.color.surfaceAlt
|
|
51
50
|
: hovered && policy.interaction !== "touch"
|
|
52
|
-
? theme.color.
|
|
51
|
+
? theme.color.overlayHover
|
|
53
52
|
: "transparent"
|
|
54
53
|
|
|
55
54
|
// Icon over a small label, centered; shared by the tab bar and the rail.
|
|
@@ -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
|
|
|
@@ -118,11 +123,3 @@ export function setPolicy(partial: Partial<Policies>) {
|
|
|
118
123
|
setOverrides((prev) => ({ ...prev, ...partial }))
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
// How density maps to component metrics: a multiplier on control sizes,
|
|
122
|
-
// paddings, and hit targets. Comfortable is the components' designed size.
|
|
123
|
-
const DENSITY_SCALE: Record<DensityPolicy, number> = { comfortable: 1, compact: 0.85, dense: 0.7 }
|
|
124
|
-
|
|
125
|
-
/** Reactive density multiplier for control metrics. */
|
|
126
|
-
export function densityScale(): number {
|
|
127
|
-
return DENSITY_SCALE[policy.density]
|
|
128
|
-
}
|
package/src/press.ts
CHANGED
|
@@ -6,10 +6,16 @@ import { registerNavAction } from "./focus-nav"
|
|
|
6
6
|
// so a consumer that reads one inside a JSX prop or child expression tracks that
|
|
7
7
|
// signal there and nothing else re-runs. Read them in those positions, not
|
|
8
8
|
// eagerly into a local, or the read lands in whatever scope destructured it.
|
|
9
|
-
export type PressState = { pressed: boolean; hovered: boolean; focused: boolean }
|
|
9
|
+
export type PressState = { pressed: boolean; hovered: boolean; focused: boolean; pending: boolean }
|
|
10
10
|
|
|
11
11
|
export interface PressOptions {
|
|
12
|
-
|
|
12
|
+
// A returned promise marks the press `pending` until it settles - further
|
|
13
|
+
// activations (pointer, key, remote) are ignored meanwhile, so an async
|
|
14
|
+
// action (save, submit) cannot double-fire. A rejection still clears
|
|
15
|
+
// pending and surfaces as an unhandled rejection. Typed `unknown` (not
|
|
16
|
+
// `void | Promise<void>`) so plain handlers like `() => setOpen(true)`
|
|
17
|
+
// keep compiling; any non-thenable return is ignored.
|
|
18
|
+
onPress?: () => unknown
|
|
13
19
|
disabled?: boolean
|
|
14
20
|
onPointerDown?: (e: PointerEvent) => void
|
|
15
21
|
onPointerUp?: (e: PointerEvent) => void
|
|
@@ -55,6 +61,28 @@ export function createPress(options: PressOptions) {
|
|
|
55
61
|
let node: { id: number } | null = null
|
|
56
62
|
let unregisterNav: (() => void) | null = null
|
|
57
63
|
|
|
64
|
+
// Async onPress: while a returned promise is unsettled the press is pending
|
|
65
|
+
// and activations are ignored. `inflight` is a plain boolean because signal
|
|
66
|
+
// writes flush on the microtask, so two activations in one dispatch would
|
|
67
|
+
// both read pending() as false; the signal exists for the UI.
|
|
68
|
+
let [pending, setPending] = createSignal(false)
|
|
69
|
+
let inflight = false
|
|
70
|
+
let activate = () => {
|
|
71
|
+
if (options.disabled || inflight) return
|
|
72
|
+
let result = options.onPress?.()
|
|
73
|
+
if (result && typeof (result as Promise<void>).then === "function") {
|
|
74
|
+
inflight = true
|
|
75
|
+
setPending(true)
|
|
76
|
+
// finally, not then(clear, clear): pending clears either way, but a
|
|
77
|
+
// rejection keeps propagating to the unhandled-rejection report
|
|
78
|
+
// instead of being swallowed here.
|
|
79
|
+
;(result as Promise<void>).finally(() => {
|
|
80
|
+
inflight = false
|
|
81
|
+
setPending(false)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
// Focus is derived from core's reactive focus rather than tracked through
|
|
59
87
|
// the onFocus/onBlur handlers - one source of truth. Memoized so a focus
|
|
60
88
|
// move propagates into styling only for the two controls whose value flips.
|
|
@@ -89,14 +117,15 @@ export function createPress(options: PressOptions) {
|
|
|
89
117
|
get focused() {
|
|
90
118
|
return focused()
|
|
91
119
|
},
|
|
120
|
+
get pending() {
|
|
121
|
+
return pending()
|
|
122
|
+
},
|
|
92
123
|
}
|
|
93
124
|
let state = (): PressState => live
|
|
94
125
|
let ref = (n: { id: number }) => {
|
|
95
126
|
node = n
|
|
96
127
|
unregisterNav?.()
|
|
97
|
-
unregisterNav = registerNavAction(n.id,
|
|
98
|
-
if (!options.disabled) options.onPress?.()
|
|
99
|
-
})
|
|
128
|
+
unregisterNav = registerNavAction(n.id, activate)
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
let within = (e: PointerEvent) => {
|
|
@@ -145,7 +174,7 @@ export function createPress(options: PressOptions) {
|
|
|
145
174
|
if (active === e.pointerId) {
|
|
146
175
|
let fire = inside
|
|
147
176
|
cancel()
|
|
148
|
-
if (fire)
|
|
177
|
+
if (fire) activate()
|
|
149
178
|
}
|
|
150
179
|
options.onPointerUp?.(e)
|
|
151
180
|
},
|
|
@@ -161,7 +190,7 @@ export function createPress(options: PressOptions) {
|
|
|
161
190
|
// The remote center key's `key` is "Unidentified"; match its code.
|
|
162
191
|
if ((e.key === "Enter" || e.key === " " || e.code === "Select") && !e.repeat && !options.disabled) {
|
|
163
192
|
e.stopPropagation()
|
|
164
|
-
|
|
193
|
+
activate()
|
|
165
194
|
}
|
|
166
195
|
options.onKeyDown?.(e)
|
|
167
196
|
},
|
|
@@ -173,5 +202,5 @@ export function createPress(options: PressOptions) {
|
|
|
173
202
|
},
|
|
174
203
|
}
|
|
175
204
|
|
|
176
|
-
return { pressed, hovered, focused, state, ref, handlers, cancel }
|
|
205
|
+
return { pressed, hovered, focused, pending, state, ref, handlers, cancel }
|
|
177
206
|
}
|
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
|
|
@@ -14,7 +15,10 @@ export interface PressableProps extends PointerProps {
|
|
|
14
15
|
ref?: (node: { id: number }) => void
|
|
15
16
|
layout?: LayoutProps
|
|
16
17
|
style?: StyleProps | ((state: PressState) => StyleProps)
|
|
17
|
-
|
|
18
|
+
// A returned promise sets `state.pending` until it settles; further
|
|
19
|
+
// presses are ignored meanwhile (no double-fire). Non-thenable returns
|
|
20
|
+
// are ignored.
|
|
21
|
+
onPress?: () => unknown
|
|
18
22
|
disabled?: boolean
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -47,8 +51,12 @@ export function Pressable(props: PressableProps) {
|
|
|
47
51
|
let hasBackground = () => style()?.backgroundColor != null || style()?.borderRadius != null
|
|
48
52
|
let hasBorder = () => (style()?.borderWidth ?? 0) > 0
|
|
49
53
|
|
|
54
|
+
let split = () => splitTransition(props.transition)
|
|
55
|
+
|
|
50
56
|
return (
|
|
51
57
|
<view
|
|
58
|
+
transition={split().root}
|
|
59
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
52
60
|
ref={(n: { id: number }) => {
|
|
53
61
|
press.ref(n)
|
|
54
62
|
props.ref?.(n)
|
|
@@ -76,6 +84,8 @@ export function Pressable(props: PressableProps) {
|
|
|
76
84
|
>
|
|
77
85
|
{hasBackground() ? (
|
|
78
86
|
<d-rect
|
|
87
|
+
transition={split().background}
|
|
88
|
+
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
79
89
|
color={style()?.backgroundColor ?? "transparent"}
|
|
80
90
|
radius={style()?.borderRadius}
|
|
81
91
|
/>
|
|
@@ -84,6 +94,8 @@ export function Pressable(props: PressableProps) {
|
|
|
84
94
|
{hasBorder() ? (
|
|
85
95
|
<d-rect
|
|
86
96
|
drawStyle="stroke"
|
|
97
|
+
transition={split().border}
|
|
98
|
+
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
87
99
|
color={style()?.borderColor ?? "transparent"}
|
|
88
100
|
strokeWidth={style()?.borderWidth}
|
|
89
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
|
|
@@ -27,8 +28,10 @@ let clamp = (x: number, lo: number, hi: number) => (x < lo ? lo : x > hi ? hi :
|
|
|
27
28
|
export function ProgressBar(props: ProgressBarProps) {
|
|
28
29
|
let h = () => (props.layout?.height as number) ?? HEIGHT
|
|
29
30
|
let radius = () => h() / 2
|
|
30
|
-
|
|
31
|
-
let
|
|
31
|
+
// Theme-level per-component overrides merged under the instance style.
|
|
32
|
+
let styled = () => ({ ...theme.components.progressBar, ...props.style })
|
|
33
|
+
let track = () => styled().backgroundColor ?? theme.color.surfaceAlt
|
|
34
|
+
let fill = () => styled().color ?? theme.color.primary
|
|
32
35
|
let indeterminate = () => props.value === undefined
|
|
33
36
|
|
|
34
37
|
// Measured track width in pixels. Both the determinate fill and the
|
|
@@ -69,7 +72,7 @@ export function ProgressBar(props: ProgressBarProps) {
|
|
|
69
72
|
let offset = () => effectivePhase() * trackWidth() * (1 - SEGMENT)
|
|
70
73
|
|
|
71
74
|
return (
|
|
72
|
-
<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}>
|
|
73
76
|
<Show when={animating()}>
|
|
74
77
|
<Animate />
|
|
75
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 {
|
|
5
|
+
import { policy } from "./policy"
|
|
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)
|
|
@@ -81,29 +82,38 @@ export function Radio(props: RadioProps) {
|
|
|
81
82
|
// Inner dot inset as a fraction of the ring, so it scales with the density.
|
|
82
83
|
let inset = () => ring() * 0.3
|
|
83
84
|
|
|
85
|
+
// Theme-level per-component overrides merged under the instance style.
|
|
86
|
+
let styled = () => ({ ...theme.components.radio, ...props.style })
|
|
84
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)
|
|
85
92
|
|
|
86
93
|
return (
|
|
87
94
|
<view
|
|
95
|
+
transition={splitTransition(props.transition).root}
|
|
96
|
+
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
88
97
|
ref={press.ref}
|
|
89
98
|
repaintBoundary
|
|
90
99
|
flexDirection="row"
|
|
91
100
|
alignItems="center"
|
|
92
101
|
gap={theme.spacing.md}
|
|
93
102
|
{...props.layout}
|
|
94
|
-
x={
|
|
95
|
-
y={
|
|
96
|
-
scale={
|
|
97
|
-
rotate={
|
|
98
|
-
opacity={
|
|
103
|
+
x={styled().x}
|
|
104
|
+
y={styled().y}
|
|
105
|
+
scale={styled().scale}
|
|
106
|
+
rotate={styled().rotate}
|
|
107
|
+
opacity={styled().opacity}
|
|
99
108
|
{...press.handlers}
|
|
109
|
+
focusable={!disabled()}
|
|
100
110
|
pointerEvents={disabled() ? "none" : undefined}
|
|
101
111
|
>
|
|
102
|
-
<Show when={
|
|
103
|
-
<d-rect color={
|
|
112
|
+
<Show when={styled().backgroundColor != null || styled().borderRadius != null}>
|
|
113
|
+
<d-rect color={styled().backgroundColor ?? "transparent"} radius={styled().borderRadius} />
|
|
104
114
|
</Show>
|
|
105
115
|
<view width={ring()} height={ring()}>
|
|
106
|
-
<d-oval drawStyle="stroke" color={ringColor()} strokeWidth={
|
|
116
|
+
<d-oval drawStyle="stroke" color={ringColor()} strokeWidth={ringWidth()} />
|
|
107
117
|
<Show when={selected()}>
|
|
108
118
|
<d-oval x={inset()} y={inset()} w={ring() - inset() * 2} h={ring() - inset() * 2} color={theme.color.primary} />
|
|
109
119
|
</Show>
|
|
@@ -113,12 +123,12 @@ export function Radio(props: RadioProps) {
|
|
|
113
123
|
{resolved()}
|
|
114
124
|
</text>
|
|
115
125
|
</Show>
|
|
116
|
-
<Show when={(
|
|
126
|
+
<Show when={(styled().borderWidth ?? 0) > 0}>
|
|
117
127
|
<d-rect
|
|
118
128
|
drawStyle="stroke"
|
|
119
|
-
color={
|
|
120
|
-
strokeWidth={
|
|
121
|
-
radius={
|
|
129
|
+
color={styled().borderColor ?? "transparent"}
|
|
130
|
+
strokeWidth={styled().borderWidth}
|
|
131
|
+
radius={styled().borderRadius}
|
|
122
132
|
/>
|
|
123
133
|
</Show>
|
|
124
134
|
</view>
|