@solidrt/components 0.0.55 → 0.0.57
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 +18 -15
- package/README.md +40 -17
- package/docs/button.md +2 -2
- package/docs/checkbox.md +1 -1
- package/docs/context-menu.md +1 -1
- package/docs/focus-nav.md +16 -3
- package/docs/modal.md +1 -1
- package/docs/policy.md +1 -1
- package/docs/progress-bar.md +1 -1
- package/docs/radio.md +1 -1
- package/docs/segmented-control.md +1 -1
- package/docs/select.md +1 -1
- package/docs/switch.md +1 -1
- package/docs/theme.md +8 -0
- package/docs/tooltip.md +1 -1
- package/docs/types.md +3 -1
- package/package.json +3 -3
- package/src/badge.tsx +4 -2
- package/src/button.tsx +16 -15
- package/src/card.tsx +5 -4
- package/src/checkbox.tsx +45 -21
- package/src/context-menu.tsx +22 -14
- package/src/divider.tsx +3 -2
- package/src/editor-field.tsx +116 -19
- package/src/field.tsx +3 -1
- package/src/focus-nav.ts +152 -53
- package/src/index.ts +1 -1
- package/src/item.tsx +11 -14
- package/src/modal.tsx +7 -1
- package/src/motion.tsx +122 -0
- package/src/nav-shell.tsx +24 -13
- package/src/press.ts +6 -11
- package/src/pressable.tsx +7 -4
- package/src/progress-bar.tsx +19 -6
- package/src/radio.tsx +36 -7
- package/src/segmented-control.tsx +70 -20
- package/src/select.tsx +58 -43
- package/src/slider.tsx +7 -3
- package/src/spinner.tsx +2 -0
- package/src/switch.tsx +30 -11
- package/src/text.tsx +3 -2
- package/src/theme.ts +25 -1
- package/src/tooltip.tsx +10 -4
- package/src/types.ts +74 -6
- package/src/view.tsx +4 -3
- package/src/window.tsx +2 -1
package/src/pressable.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { children } from "@solidrt/core"
|
|
2
2
|
import type { LayoutProps, PointerProps } from "@solidrt/core"
|
|
3
3
|
import type { StyleProps, TransitionProps } from "./types"
|
|
4
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
4
|
+
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
5
5
|
import { createPress, type PressState } from "./press"
|
|
6
|
+
import { colorFade, scaleFeedback } from "./motion"
|
|
6
7
|
|
|
7
8
|
export type { PressState } from "./press"
|
|
8
9
|
|
|
@@ -55,7 +56,9 @@ export function Pressable(props: PressableProps) {
|
|
|
55
56
|
|
|
56
57
|
return (
|
|
57
58
|
<view
|
|
58
|
-
|
|
59
|
+
// The scale default animates a style-function's press scale, so a
|
|
60
|
+
// custom control built on Pressable presses like Button by default.
|
|
61
|
+
transition={withTransitionDefaults(split().root, scaleFeedback())}
|
|
59
62
|
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
60
63
|
ref={(n: { id: number }) => {
|
|
61
64
|
press.ref(n)
|
|
@@ -84,7 +87,7 @@ export function Pressable(props: PressableProps) {
|
|
|
84
87
|
>
|
|
85
88
|
{hasBackground() ? (
|
|
86
89
|
<d-rect
|
|
87
|
-
transition={split().background}
|
|
90
|
+
transition={withTransitionDefaults(split().background, colorFade())}
|
|
88
91
|
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
89
92
|
color={style()?.backgroundColor ?? "transparent"}
|
|
90
93
|
radius={style()?.borderRadius}
|
|
@@ -94,7 +97,7 @@ export function Pressable(props: PressableProps) {
|
|
|
94
97
|
{hasBorder() ? (
|
|
95
98
|
<d-rect
|
|
96
99
|
drawStyle="stroke"
|
|
97
|
-
transition={split().border}
|
|
100
|
+
transition={withTransitionDefaults(split().border, colorFade())}
|
|
98
101
|
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
99
102
|
color={style()?.borderColor ?? "transparent"}
|
|
100
103
|
strokeWidth={style()?.borderWidth}
|
package/src/progress-bar.tsx
CHANGED
|
@@ -2,10 +2,11 @@ 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, TransitionProps } from "./types"
|
|
6
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
5
|
+
import type { StyleProps, TransitionProps, TransitionStyleProp, TransitionViewProp } from "./types"
|
|
6
|
+
import { partTransition, partTransitionEnd, splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
7
|
+
import { colorFade, travelMotion } from "./motion"
|
|
7
8
|
|
|
8
|
-
export interface ProgressBarProps extends TransitionProps {
|
|
9
|
+
export interface ProgressBarProps extends TransitionProps<TransitionViewProp | TransitionStyleProp | "fill"> {
|
|
9
10
|
// Progress from 0 to 1. Omit (or leave undefined) for an indeterminate bar: a
|
|
10
11
|
// segment that slides back and forth.
|
|
11
12
|
value?: number
|
|
@@ -71,13 +72,25 @@ export function ProgressBar(props: ProgressBarProps) {
|
|
|
71
72
|
let effectivePhase = () => (policy.motion === "none" ? 0.5 : phase())
|
|
72
73
|
let offset = () => effectivePhase() * trackWidth() * (1 - SEGMENT)
|
|
73
74
|
|
|
75
|
+
let split = () => splitTransition(props.transition, ["fill"])
|
|
76
|
+
// Determinate only: a value write glides the fill (the `fill` transition
|
|
77
|
+
// entry retimes it; bounce 0, progress must not overshoot). Indeterminate
|
|
78
|
+
// writes w/x per frame, which a transition would lag behind.
|
|
79
|
+
let fillTransition = () => {
|
|
80
|
+
if (props.transition === null) return null
|
|
81
|
+
let travel = indeterminate() ? undefined : partTransition(props.transition, "fill", "w", travelMotion(0))
|
|
82
|
+
let fade = colorFade()
|
|
83
|
+
if (!travel && !fade) return undefined
|
|
84
|
+
return { ...fade, ...(travel as object | undefined) }
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
return (
|
|
75
|
-
<view ref={(n: { id: number }) => (trackNode = n)} position="relative" width="100%" height={h()} transition={
|
|
88
|
+
<view ref={(n: { id: number }) => (trackNode = n)} position="relative" width="100%" height={h()} transition={split().root} onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)} {...props.layout}>
|
|
76
89
|
<Show when={animating()}>
|
|
77
90
|
<Animate />
|
|
78
91
|
</Show>
|
|
79
|
-
<d-rect color={track()} radius={radius()} />
|
|
80
|
-
<d-rect color={fill()} radius={radius()} w={fillWidth()} h={h()} x={indeterminate() ? offset() : 0} />
|
|
92
|
+
<d-rect transition={withTransitionDefaults(split().background, colorFade())} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={track()} radius={radius()} />
|
|
93
|
+
<d-rect transition={fillTransition()} onTransitionEnd={partTransitionEnd("fill", "w", props.onTransitionEnd)} color={fill()} radius={radius()} w={fillWidth()} h={h()} x={indeterminate() ? offset() : 0} />
|
|
81
94
|
</view>
|
|
82
95
|
)
|
|
83
96
|
}
|
package/src/radio.tsx
CHANGED
|
@@ -6,7 +6,8 @@ import { policy } from "./policy"
|
|
|
6
6
|
import { densityScale } from "./density"
|
|
7
7
|
import { typeStyle } from "./typography"
|
|
8
8
|
import type { StyleProps, TransitionProps } from "./types"
|
|
9
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
9
|
+
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
10
|
+
import { colorFade, markMotion, pressScale, scaleFeedback } from "./motion"
|
|
10
11
|
|
|
11
12
|
// Shared selection state for a group. Created and consumed within this module, so
|
|
12
13
|
// RadioGroup/Radio are a self-contained pair, not a cross-component dependency.
|
|
@@ -67,6 +68,8 @@ export interface RadioProps extends TransitionProps {
|
|
|
67
68
|
const RING = 20
|
|
68
69
|
|
|
69
70
|
// A single option in a RadioGroup: a ring with an inner dot when selected.
|
|
71
|
+
// The ring color fades, the dot pops in/out (markMotion), and a press
|
|
72
|
+
// shrinks the ring - not the whole row, so a long label never wobbles.
|
|
70
73
|
export function Radio(props: RadioProps) {
|
|
71
74
|
// useContext throws ContextNotFoundError if a Radio is used outside a
|
|
72
75
|
// RadioGroup (default-less context), so ctx is always present here.
|
|
@@ -90,9 +93,11 @@ export function Radio(props: RadioProps) {
|
|
|
90
93
|
let ringColor = () => (focusRing() ? theme.color.ring : selected() ? theme.color.primary : theme.color.border)
|
|
91
94
|
let ringWidth = () => (focusRing() ? theme.borderWidth.focus : 2)
|
|
92
95
|
|
|
96
|
+
let split = () => splitTransition(props.transition)
|
|
97
|
+
|
|
93
98
|
return (
|
|
94
99
|
<view
|
|
95
|
-
transition={
|
|
100
|
+
transition={split().root}
|
|
96
101
|
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
97
102
|
ref={press.ref}
|
|
98
103
|
repaintBoundary
|
|
@@ -110,22 +115,46 @@ export function Radio(props: RadioProps) {
|
|
|
110
115
|
pointerEvents={disabled() ? "none" : undefined}
|
|
111
116
|
>
|
|
112
117
|
<Show when={styled().backgroundColor != null || styled().borderRadius != null}>
|
|
113
|
-
<d-rect
|
|
118
|
+
<d-rect
|
|
119
|
+
transition={withTransitionDefaults(split().background, colorFade())}
|
|
120
|
+
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
121
|
+
color={styled().backgroundColor ?? "transparent"}
|
|
122
|
+
radius={styled().borderRadius}
|
|
123
|
+
/>
|
|
114
124
|
</Show>
|
|
115
|
-
<view
|
|
116
|
-
|
|
125
|
+
<view
|
|
126
|
+
width={ring()}
|
|
127
|
+
height={ring()}
|
|
128
|
+
position="relative"
|
|
129
|
+
scale={pressScale(press.pressed())}
|
|
130
|
+
transition={scaleFeedback()}
|
|
131
|
+
>
|
|
132
|
+
<d-oval drawStyle="stroke" transition={colorFade()} color={ringColor()} strokeWidth={ringWidth()} />
|
|
117
133
|
<Show when={selected()}>
|
|
118
|
-
<
|
|
134
|
+
<view
|
|
135
|
+
position="absolute"
|
|
136
|
+
top={0}
|
|
137
|
+
bottom={0}
|
|
138
|
+
left={0}
|
|
139
|
+
right={0}
|
|
140
|
+
opacity={1}
|
|
141
|
+
scale={1}
|
|
142
|
+
transition={markMotion()}
|
|
143
|
+
>
|
|
144
|
+
<d-oval x={inset()} y={inset()} w={ring() - inset() * 2} h={ring() - inset() * 2} transition={colorFade()} color={theme.color.primary} />
|
|
145
|
+
</view>
|
|
119
146
|
</Show>
|
|
120
147
|
</view>
|
|
121
148
|
<Show when={isText()} fallback={resolved()}>
|
|
122
|
-
<text color={theme.color.text} {...typeStyle("body")}>
|
|
149
|
+
<text transition={colorFade()} color={theme.color.text} {...typeStyle("body")}>
|
|
123
150
|
{resolved()}
|
|
124
151
|
</text>
|
|
125
152
|
</Show>
|
|
126
153
|
<Show when={(styled().borderWidth ?? 0) > 0}>
|
|
127
154
|
<d-rect
|
|
128
155
|
drawStyle="stroke"
|
|
156
|
+
transition={withTransitionDefaults(split().border, colorFade())}
|
|
157
|
+
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
129
158
|
color={styled().borderColor ?? "transparent"}
|
|
130
159
|
strokeWidth={styled().borderWidth}
|
|
131
160
|
radius={styled().borderRadius}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { createSignal, For, Show } from "@solidrt/core"
|
|
1
|
+
import { createSignal, For, Show, getBoundingBox, onLayout, withAlpha } 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, TransitionProps } from "./types"
|
|
9
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
8
|
+
import type { Option, StyleProps, TransitionProps, TransitionStyleProp, TransitionViewProp } from "./types"
|
|
9
|
+
import { partTransition, partTransitionEnd, splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
10
|
+
import { colorFade, PressFeedback, travelMotion } from "./motion"
|
|
10
11
|
|
|
11
|
-
export interface SegmentedControlProps
|
|
12
|
+
export interface SegmentedControlProps
|
|
13
|
+
extends TransitionProps<TransitionViewProp | TransitionStyleProp | "indicator"> {
|
|
12
14
|
options: Option[]
|
|
13
15
|
// Controlled selected value. If omitted, the control is uncontrolled.
|
|
14
16
|
value?: unknown
|
|
@@ -26,10 +28,12 @@ const DIVIDER = 0
|
|
|
26
28
|
|
|
27
29
|
// A single-choice row of equal-width segments, joined flush (the Material
|
|
28
30
|
// style): only the control's outermost corners are rounded, interior segments
|
|
29
|
-
// are square, and hairline dividers separate them. The active segment
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
31
|
+
// are square, and hairline dividers separate them. The active segment is one
|
|
32
|
+
// indicator rect drawn under the labels that springs between segments - the
|
|
33
|
+
// `indicator` transition entry retimes it. Controlled via value/onChange, or
|
|
34
|
+
// uncontrolled via defaultValue. Hover tints inactive segments (non-touch
|
|
35
|
+
// interaction policies only). Override the inactive fill via style, the
|
|
36
|
+
// spacing/sizing via layout.
|
|
33
37
|
export function SegmentedControl(props: SegmentedControlProps) {
|
|
34
38
|
let [internal, setInternal] = createSignal(props.defaultValue)
|
|
35
39
|
let value = () => (props.value !== undefined ? props.value : internal())
|
|
@@ -60,12 +64,49 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
|
|
60
64
|
let label = (active: boolean) =>
|
|
61
65
|
props.disabled ? theme.color.textMuted : active ? theme.color.onPrimary : theme.color.text
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
// The indicator is positioned from measured segment boxes (control-relative
|
|
68
|
+
// x and width per segment), so a selection change retargets it without a
|
|
69
|
+
// reflow and unequal rounding never misaligns it. Remeasured each layout.
|
|
70
|
+
let root: { id: number } | undefined
|
|
71
|
+
let segs: ({ id: number } | undefined)[] = []
|
|
72
|
+
let [boxes, setBoxes] = createSignal<{ x: number; w: number }[]>([])
|
|
73
|
+
// The indicator's travel spring is armed only after the first placement:
|
|
74
|
+
// the declaration exists from mount, so without this the first measured
|
|
75
|
+
// write would slide the indicator in from x 0. A timer, not the same
|
|
76
|
+
// flush - the declaration must be committed strictly after that write.
|
|
77
|
+
let [placed, setPlaced] = createSignal(false)
|
|
78
|
+
onLayout(() => {
|
|
79
|
+
if (!root) return
|
|
80
|
+
let r = getBoundingBox(root)
|
|
81
|
+
if (!r) return
|
|
82
|
+
let next: { x: number; w: number }[] = []
|
|
83
|
+
for (let i = 0; i < props.options.length; i++) {
|
|
84
|
+
let s = segs[i]
|
|
85
|
+
let b = s && getBoundingBox(s)
|
|
86
|
+
if (!b) return
|
|
87
|
+
next.push({ x: b.x - r.x, w: b.width })
|
|
88
|
+
}
|
|
89
|
+
let cur = boxes()
|
|
90
|
+
if (cur.length !== next.length || cur.some((c, i) => c.x !== next[i]!.x || c.w !== next[i]!.w)) setBoxes(next)
|
|
91
|
+
if (!placed()) setTimeout(() => setPlaced(true), 0)
|
|
92
|
+
})
|
|
93
|
+
let activeIndex = () => props.options.findIndex((o) => o.value === value())
|
|
94
|
+
let indicator = () => boxes()[activeIndex()]
|
|
95
|
+
|
|
96
|
+
let split = () => splitTransition(props.transition, ["indicator"])
|
|
97
|
+
let indicatorTransition = () => {
|
|
98
|
+
if (props.transition === null) return null
|
|
99
|
+
let travel = placed() ? partTransition(props.transition, "indicator", "x", travelMotion()) : undefined
|
|
100
|
+
let fade = colorFade()
|
|
101
|
+
if (!travel && !fade) return undefined
|
|
102
|
+
return { ...fade, ...(travel as object | undefined) }
|
|
103
|
+
}
|
|
64
104
|
|
|
65
105
|
return (
|
|
66
106
|
<view
|
|
67
107
|
transition={split().root}
|
|
68
108
|
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
109
|
+
ref={(n: { id: number }) => (root = n)}
|
|
69
110
|
flexDirection="row"
|
|
70
111
|
gap={DIVIDER}
|
|
71
112
|
{...props.layout}
|
|
@@ -75,20 +116,25 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
|
|
75
116
|
rotate={styled().rotate}
|
|
76
117
|
opacity={styled().opacity}
|
|
77
118
|
>
|
|
78
|
-
<d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={
|
|
119
|
+
<d-rect transition={withTransitionDefaults(split().background, colorFade())} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={idleFill()} radius={radius()} />
|
|
120
|
+
<d-rect
|
|
121
|
+
transition={indicatorTransition()}
|
|
122
|
+
onTransitionEnd={partTransitionEnd("indicator", "x", props.onTransitionEnd)}
|
|
123
|
+
color={indicator() ? activeFill() : withAlpha(activeFill(), 0)}
|
|
124
|
+
x={indicator()?.x ?? 0}
|
|
125
|
+
w={indicator()?.w ?? 0}
|
|
126
|
+
radius={corners(activeIndex())}
|
|
127
|
+
/>
|
|
79
128
|
<For each={props.options}>
|
|
80
129
|
{(opt, i) => {
|
|
81
130
|
let active = () => value() === opt.value
|
|
82
131
|
let press = createPress({ onPress: () => select(opt.value) })
|
|
83
|
-
|
|
84
|
-
// Hover feedback: the theme overlay tint drawn over the segment fill.
|
|
85
|
-
let overlay = () =>
|
|
86
|
-
press.hovered() && !props.disabled && policy.interaction !== "touch"
|
|
87
|
-
? theme.color.overlayHover
|
|
88
|
-
: "transparent"
|
|
89
|
-
return (
|
|
132
|
+
return (
|
|
90
133
|
<view
|
|
91
|
-
ref={
|
|
134
|
+
ref={(n: { id: number }) => {
|
|
135
|
+
segs[i()] = n
|
|
136
|
+
press.ref(n)
|
|
137
|
+
}}
|
|
92
138
|
repaintBoundary
|
|
93
139
|
flexGrow={1}
|
|
94
140
|
flexBasis={0}
|
|
@@ -101,12 +147,16 @@ export function SegmentedControl(props: SegmentedControlProps) {
|
|
|
101
147
|
focusable={!props.disabled}
|
|
102
148
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
103
149
|
>
|
|
104
|
-
<
|
|
105
|
-
|
|
150
|
+
<PressFeedback
|
|
151
|
+
pressed={press.pressed()}
|
|
152
|
+
hovered={press.hovered() && !props.disabled && policy.interaction !== "touch"}
|
|
153
|
+
radius={corners(i())}
|
|
154
|
+
/>
|
|
106
155
|
<Show when={press.focused() && policy.focusRing}>
|
|
107
156
|
<d-rect drawStyle="stroke" color={theme.color.ring} strokeWidth={theme.borderWidth.focus} radius={corners(i())} />
|
|
108
157
|
</Show>
|
|
109
158
|
<text
|
|
159
|
+
transition={colorFade()}
|
|
110
160
|
color={label(active())}
|
|
111
161
|
{...typeStyle("body", active() ? lightOnDark(label(true), activeFill()) : undefined)}
|
|
112
162
|
>
|
package/src/select.tsx
CHANGED
|
@@ -6,8 +6,9 @@ import { policy } from "./policy"
|
|
|
6
6
|
import { space } from "./spacing"
|
|
7
7
|
import { typeStyle } from "./typography"
|
|
8
8
|
import type { Option, StyleProps, TransitionProps } from "./types"
|
|
9
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
9
|
+
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
10
10
|
import { Icon } from "./icon"
|
|
11
|
+
import { colorFade, popupFade, popupFadeOut, PressFeedback, travelMotion } from "./motion"
|
|
11
12
|
|
|
12
13
|
export interface SelectProps extends TransitionProps {
|
|
13
14
|
options: Option[]
|
|
@@ -66,16 +67,11 @@ export function Select(props: SelectProps) {
|
|
|
66
67
|
{...press.handlers}
|
|
67
68
|
focusable
|
|
68
69
|
>
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
? theme.color.overlayPressed
|
|
73
|
-
: (press.hovered() && policy.interaction !== "touch") || (press.focused() && policy.focusRing)
|
|
74
|
-
? theme.color.overlayHover
|
|
75
|
-
: "transparent"
|
|
76
|
-
}
|
|
70
|
+
<PressFeedback
|
|
71
|
+
pressed={press.pressed()}
|
|
72
|
+
hovered={(press.hovered() && policy.interaction !== "touch") || (press.focused() && policy.focusRing)}
|
|
77
73
|
/>
|
|
78
|
-
<text {...bodyText(p.option.value === value() ? theme.color.primary : theme.color.text)}>{p.option.label}</text>
|
|
74
|
+
<text transition={colorFade()} {...bodyText(p.option.value === value() ? theme.color.primary : theme.color.text)}>{p.option.label}</text>
|
|
79
75
|
</view>
|
|
80
76
|
)
|
|
81
77
|
}
|
|
@@ -98,8 +94,19 @@ export function Select(props: SelectProps) {
|
|
|
98
94
|
let cur = pos()
|
|
99
95
|
if (!cur || cur.x !== x || cur.y !== y) setPos({ x, y })
|
|
100
96
|
})
|
|
97
|
+
// The fade is driven by the position write, not a mount-time `from`: the
|
|
98
|
+
// menu parks offscreen until measured, so a `from` would play unseen.
|
|
99
|
+
// The exit fade lives on this root (the node the close unmounts).
|
|
101
100
|
return createPortal(
|
|
102
|
-
<view
|
|
101
|
+
<view
|
|
102
|
+
position="absolute"
|
|
103
|
+
top={0}
|
|
104
|
+
left={0}
|
|
105
|
+
right={0}
|
|
106
|
+
bottom={0}
|
|
107
|
+
opacity={pos() ? 1 : 0}
|
|
108
|
+
transition={popupFadeOut()}
|
|
109
|
+
>
|
|
103
110
|
<view position="absolute" top={0} left={0} right={0} bottom={0} onPointerDown={() => setOpen(false)} />
|
|
104
111
|
<view
|
|
105
112
|
ref={(n: { id: number }) => (menu = n)}
|
|
@@ -113,12 +120,13 @@ export function Select(props: SelectProps) {
|
|
|
113
120
|
paddingTop={theme.spacing.sm}
|
|
114
121
|
paddingBottom={theme.spacing.sm}
|
|
115
122
|
>
|
|
116
|
-
<d-rect color={theme.color.surface} radius={theme.radius.sm} />
|
|
123
|
+
<d-rect transition={colorFade()} color={theme.color.surface} radius={theme.radius.sm} />
|
|
117
124
|
<For each={props.options}>
|
|
118
125
|
{(o: Option) => <OptionRow option={o} padY={space("sm")} />}
|
|
119
126
|
</For>
|
|
120
127
|
<d-rect
|
|
121
128
|
drawStyle="stroke"
|
|
129
|
+
transition={colorFade()}
|
|
122
130
|
color={theme.color.border}
|
|
123
131
|
strokeWidth={theme.borderWidth.sm}
|
|
124
132
|
radius={theme.radius.sm}
|
|
@@ -132,9 +140,9 @@ export function Select(props: SelectProps) {
|
|
|
132
140
|
// trick) so pressing an option never has the scrim on its hit path.
|
|
133
141
|
let Sheet = () =>
|
|
134
142
|
createPortal(
|
|
135
|
-
<view position="absolute" top={0} left={0} right={0} bottom={0}>
|
|
143
|
+
<view position="absolute" top={0} left={0} right={0} bottom={0} opacity={1} transition={popupFade()}>
|
|
136
144
|
<view position="absolute" top={0} left={0} right={0} bottom={0} onPointerDown={() => setOpen(false)}>
|
|
137
|
-
<d-rect color={theme.color.scrim} />
|
|
145
|
+
<d-rect transition={colorFade()} color={theme.color.scrim} />
|
|
138
146
|
</view>
|
|
139
147
|
<view
|
|
140
148
|
position="absolute"
|
|
@@ -145,7 +153,7 @@ export function Select(props: SelectProps) {
|
|
|
145
153
|
paddingTop={theme.spacing.md}
|
|
146
154
|
paddingBottom={theme.spacing.md + env.safeArea.bottom}
|
|
147
155
|
>
|
|
148
|
-
<d-rect color={theme.color.surface} radius={theme.radius.sm} />
|
|
156
|
+
<d-rect transition={colorFade()} color={theme.color.surface} radius={theme.radius.sm} />
|
|
149
157
|
<For each={props.options}>
|
|
150
158
|
{(o: Option) => <OptionRow option={o} padY={Math.round(theme.spacing.md * 1.5)} />}
|
|
151
159
|
</For>
|
|
@@ -167,12 +175,12 @@ export function Select(props: SelectProps) {
|
|
|
167
175
|
...props.style,
|
|
168
176
|
...(press.focused() && policy.focusRing ? { borderWidth: theme.borderWidth.focus, borderColor: theme.color.ring } : {}),
|
|
169
177
|
})
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
// The chevron flips while the picker is open; travel motion, so it snaps
|
|
179
|
+
// under reduced motion (the flip itself stays).
|
|
180
|
+
let chevronSpin = () => {
|
|
181
|
+
let t = travelMotion()
|
|
182
|
+
return t && { rotate: t }
|
|
183
|
+
}
|
|
176
184
|
|
|
177
185
|
let split = () => splitTransition(props.transition)
|
|
178
186
|
|
|
@@ -203,31 +211,38 @@ export function Select(props: SelectProps) {
|
|
|
203
211
|
focusable={!props.disabled}
|
|
204
212
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
205
213
|
>
|
|
206
|
-
<d-rect transition={split().background} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
|
|
207
|
-
<
|
|
214
|
+
<d-rect transition={withTransitionDefaults(split().background, colorFade())} onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)} color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
|
|
215
|
+
<PressFeedback
|
|
216
|
+
pressed={press.pressed() && !props.disabled}
|
|
217
|
+
hovered={press.hovered() && !props.disabled && policy.interaction !== "touch"}
|
|
218
|
+
radius={style().borderRadius}
|
|
219
|
+
/>
|
|
208
220
|
<Show
|
|
209
221
|
when={selected()}
|
|
210
|
-
fallback={<text {...bodyText(theme.color.textMuted)}>{props.placeholder ?? ""}</text>}
|
|
222
|
+
fallback={<text transition={colorFade()} {...bodyText(theme.color.textMuted)}>{props.placeholder ?? ""}</text>}
|
|
211
223
|
>
|
|
212
|
-
<text {...bodyText(props.disabled ? theme.color.textMuted : theme.color.text)}>{selected()!.label}</text>
|
|
213
|
-
</Show>
|
|
214
|
-
<Show
|
|
215
|
-
when={theme.icons.chevronDown}
|
|
216
|
-
fallback={
|
|
217
|
-
<view width={12} height={8}>
|
|
218
|
-
<d-path
|
|
219
|
-
d="M 2 2 L 6 6 L 10 2"
|
|
220
|
-
drawStyle="stroke"
|
|
221
|
-
color={theme.color.textMuted}
|
|
222
|
-
strokeWidth={2}
|
|
223
|
-
strokeCap="round"
|
|
224
|
-
strokeJoin="round"
|
|
225
|
-
/>
|
|
226
|
-
</view>
|
|
227
|
-
}
|
|
228
|
-
>
|
|
229
|
-
<Icon src={theme.icons.chevronDown!} size={12} color={theme.color.textMuted} />
|
|
224
|
+
<text transition={colorFade()} {...bodyText(props.disabled ? theme.color.textMuted : theme.color.text)}>{selected()!.label}</text>
|
|
230
225
|
</Show>
|
|
226
|
+
<view rotate={open() ? Math.PI : 0} transition={chevronSpin()}>
|
|
227
|
+
<Show
|
|
228
|
+
when={theme.icons.chevronDown}
|
|
229
|
+
fallback={
|
|
230
|
+
<view width={12} height={8}>
|
|
231
|
+
<d-path
|
|
232
|
+
d="M 2 2 L 6 6 L 10 2"
|
|
233
|
+
drawStyle="stroke"
|
|
234
|
+
transition={colorFade()}
|
|
235
|
+
color={theme.color.textMuted}
|
|
236
|
+
strokeWidth={2}
|
|
237
|
+
strokeCap="round"
|
|
238
|
+
strokeJoin="round"
|
|
239
|
+
/>
|
|
240
|
+
</view>
|
|
241
|
+
}
|
|
242
|
+
>
|
|
243
|
+
<Icon src={theme.icons.chevronDown!} size={12} color={theme.color.textMuted} />
|
|
244
|
+
</Show>
|
|
245
|
+
</view>
|
|
231
246
|
<Show when={open()}>
|
|
232
247
|
<Show when={policy.interaction === "touch"} fallback={<Dropdown />}>
|
|
233
248
|
<Sheet />
|
|
@@ -236,7 +251,7 @@ export function Select(props: SelectProps) {
|
|
|
236
251
|
<Show when={(style().borderWidth ?? 0) > 0}>
|
|
237
252
|
<d-rect
|
|
238
253
|
drawStyle="stroke"
|
|
239
|
-
transition={split().border}
|
|
254
|
+
transition={withTransitionDefaults(split().border, colorFade())}
|
|
240
255
|
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
241
256
|
color={style().borderColor ?? "transparent"}
|
|
242
257
|
strokeWidth={style().borderWidth}
|
package/src/slider.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { policy } from "./policy"
|
|
|
5
5
|
import { densityScale } from "./density"
|
|
6
6
|
import type { StyleProps, TransitionProps } from "./types"
|
|
7
7
|
import { splitTransition, transitionEndFor } from "./types"
|
|
8
|
+
import { colorFade } from "./motion"
|
|
8
9
|
|
|
9
10
|
export interface SliderProps extends TransitionProps {
|
|
10
11
|
// Controlled value. If omitted, the slider is uncontrolled.
|
|
@@ -137,11 +138,14 @@ export function Slider(props: SliderProps) {
|
|
|
137
138
|
onPointerUp={handleUp}
|
|
138
139
|
onKeyDown={handleKeyDown}
|
|
139
140
|
>
|
|
141
|
+
{/* Colors fade with the theme; the fill's w and the thumb's x never
|
|
142
|
+
get a transition - they track the drag 1:1, and a spring would
|
|
143
|
+
rubber-band it. */}
|
|
140
144
|
<view ref={(n: { id: number }) => (groove = n)} position="relative" flex={1} height={GROOVE}>
|
|
141
|
-
<d-rect color={theme.color.surfaceAlt} radius={GROOVE / 2} />
|
|
142
|
-
<d-rect color={theme.color.primary} w={fillPx()} h={GROOVE} radius={GROOVE / 2} />
|
|
145
|
+
<d-rect transition={colorFade()} color={theme.color.surfaceAlt} radius={GROOVE / 2} />
|
|
146
|
+
<d-rect transition={colorFade()} color={theme.color.primary} w={fillPx()} h={GROOVE} radius={GROOVE / 2} />
|
|
143
147
|
<view position="absolute" left={0} top={(GROOVE - thumb()) / 2} x={fillPx() - thumb() / 2}>
|
|
144
|
-
<d-oval w={thumb()} h={thumb()} color={theme.color.primary} />
|
|
148
|
+
<d-oval transition={colorFade()} w={thumb()} h={thumb()} color={theme.color.primary} />
|
|
145
149
|
<Show when={focused() && policy.focusRing}>
|
|
146
150
|
<d-oval drawStyle="stroke" w={thumb()} h={thumb()} color={theme.color.ring} strokeWidth={theme.borderWidth.focus} />
|
|
147
151
|
</Show>
|
package/src/spinner.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { theme } from "./theme"
|
|
|
4
4
|
import { policy } from "./policy"
|
|
5
5
|
import type { StyleProps, TransitionProps } from "./types"
|
|
6
6
|
import { splitTransition, transitionEndFor } from "./types"
|
|
7
|
+
import { colorFade } from "./motion"
|
|
7
8
|
|
|
8
9
|
export interface SpinnerProps extends TransitionProps {
|
|
9
10
|
// Overall diameter in pixels.
|
|
@@ -70,6 +71,7 @@ export function Spinner(props: SpinnerProps) {
|
|
|
70
71
|
<d-path
|
|
71
72
|
d={path()}
|
|
72
73
|
drawStyle="stroke"
|
|
74
|
+
transition={colorFade()}
|
|
73
75
|
color={color()}
|
|
74
76
|
strokeWidth={thickness()}
|
|
75
77
|
strokeCap="round"
|
package/src/switch.tsx
CHANGED
|
@@ -4,10 +4,11 @@ import { createPress } from "./press"
|
|
|
4
4
|
import { theme } from "./theme"
|
|
5
5
|
import { policy } from "./policy"
|
|
6
6
|
import { densityScale } from "./density"
|
|
7
|
-
import type { StyleProps, TransitionProps } from "./types"
|
|
8
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
7
|
+
import type { StyleProps, TransitionProps, TransitionStyleProp, TransitionViewProp } from "./types"
|
|
8
|
+
import { partTransition, partTransitionEnd, splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
9
|
+
import { colorFade, pressScale, scaleFeedback, travelMotion } from "./motion"
|
|
9
10
|
|
|
10
|
-
export interface SwitchProps extends TransitionProps {
|
|
11
|
+
export interface SwitchProps extends TransitionProps<TransitionViewProp | TransitionStyleProp | "knob"> {
|
|
11
12
|
// Controlled on/off. If omitted, the switch is uncontrolled.
|
|
12
13
|
value?: boolean
|
|
13
14
|
// Initial value for uncontrolled use.
|
|
@@ -24,9 +25,11 @@ const W = 44
|
|
|
24
25
|
const H = 24
|
|
25
26
|
const PAD = 2
|
|
26
27
|
|
|
27
|
-
// A toggle. Track fills with primary when on, surfaceAlt when off
|
|
28
|
-
//
|
|
29
|
-
//
|
|
28
|
+
// A toggle. Track fills with primary when on, surfaceAlt when off (a fade);
|
|
29
|
+
// the thumb springs across - the `knob` transition entry retimes it. A press
|
|
30
|
+
// shrinks the control slightly (pressScale). Controlled via value/onChange,
|
|
31
|
+
// or uncontrolled via defaultValue. When disabled, it takes no pointer
|
|
32
|
+
// events at all.
|
|
30
33
|
export function Switch(props: SwitchProps) {
|
|
31
34
|
let [internal, setInternal] = createSignal(props.defaultValue ?? false)
|
|
32
35
|
let on = () => (props.value !== undefined ? props.value : internal())
|
|
@@ -50,9 +53,11 @@ export function Switch(props: SwitchProps) {
|
|
|
50
53
|
...(press.focused() && policy.focusRing ? { borderWidth: theme.borderWidth.focus, borderColor: theme.color.ring } : {}),
|
|
51
54
|
})
|
|
52
55
|
|
|
56
|
+
let split = () => splitTransition(props.transition, ["knob"])
|
|
57
|
+
|
|
53
58
|
return (
|
|
54
59
|
<view
|
|
55
|
-
transition={
|
|
60
|
+
transition={withTransitionDefaults(split().root, scaleFeedback())}
|
|
56
61
|
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
57
62
|
ref={press.ref}
|
|
58
63
|
repaintBoundary
|
|
@@ -61,20 +66,34 @@ export function Switch(props: SwitchProps) {
|
|
|
61
66
|
{...props.layout}
|
|
62
67
|
x={style().x}
|
|
63
68
|
y={style().y}
|
|
64
|
-
scale={style().scale}
|
|
69
|
+
scale={(style().scale ?? 1) * pressScale(press.pressed())}
|
|
65
70
|
rotate={style().rotate}
|
|
66
71
|
opacity={style().opacity}
|
|
67
72
|
{...press.handlers}
|
|
68
73
|
focusable={!props.disabled}
|
|
69
74
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
70
75
|
>
|
|
71
|
-
<d-rect
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
<d-rect
|
|
77
|
+
transition={withTransitionDefaults(split().background, colorFade())}
|
|
78
|
+
onTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
79
|
+
color={style().backgroundColor ?? "transparent"}
|
|
80
|
+
radius={style().borderRadius}
|
|
81
|
+
/>
|
|
82
|
+
<view
|
|
83
|
+
position="absolute"
|
|
84
|
+
top={PAD}
|
|
85
|
+
left={PAD}
|
|
86
|
+
x={on() ? w() - thumb() - PAD * 2 : 0}
|
|
87
|
+
transition={partTransition(props.transition, "knob", "x", travelMotion())}
|
|
88
|
+
onTransitionEnd={partTransitionEnd("knob", "x", props.onTransitionEnd)}
|
|
89
|
+
>
|
|
90
|
+
<d-oval transition={colorFade()} w={thumb()} h={thumb()} color={theme.color.onPrimary} />
|
|
74
91
|
</view>
|
|
75
92
|
<Show when={(style().borderWidth ?? 0) > 0}>
|
|
76
93
|
<d-rect
|
|
77
94
|
drawStyle="stroke"
|
|
95
|
+
transition={withTransitionDefaults(split().border, colorFade())}
|
|
96
|
+
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
78
97
|
color={style().borderColor ?? "transparent"}
|
|
79
98
|
strokeWidth={style().borderWidth}
|
|
80
99
|
radius={style().borderRadius}
|
package/src/text.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createMemo } from "@solidrt/core"
|
|
2
2
|
import type { PointerProps } from "@solidrt/core"
|
|
3
3
|
import type { StyleProps, TextLayoutProps, TransitionProps, TransitionViewProp } from "./types"
|
|
4
|
-
import { splitTransition, transitionEndFor } from "./types"
|
|
4
|
+
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
5
5
|
import { theme, type TextVariant } from "./theme"
|
|
6
6
|
import { policy } from "./policy"
|
|
7
7
|
import { typeWeight } from "./typography"
|
|
8
|
+
import { colorFade } from "./motion"
|
|
8
9
|
|
|
9
10
|
// Semantic text colors, resolved through the theme. Curated: only tokens that
|
|
10
11
|
// make sense as a text fill; style.color takes raw values for anything else.
|
|
@@ -95,7 +96,7 @@ export function Text(props: TextProps) {
|
|
|
95
96
|
pointerEvents={props.pointerEvents}
|
|
96
97
|
>
|
|
97
98
|
<text
|
|
98
|
-
transition={split().text}
|
|
99
|
+
transition={withTransitionDefaults(split().text, colorFade())}
|
|
99
100
|
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
100
101
|
color={color()}
|
|
101
102
|
fontFamily={props.layout?.fontFamily ?? theme.text.fontFamily}
|