@solidrt/components 0.0.63 → 0.0.65
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 -14
- package/README.md +126 -21
- package/docs/badge.md +1 -1
- package/docs/checkbox.md +1 -1
- package/docs/context-menu.md +1 -1
- package/docs/item.md +1 -1
- package/docs/light.md +12 -0
- package/docs/motion.md +13 -0
- package/docs/press.md +19 -0
- 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/slider.md +1 -1
- package/docs/surface.md +19 -0
- package/docs/switch.md +1 -1
- package/docs/theme.md +34 -6
- package/docs/tooltip.md +1 -1
- package/docs/types.md +1 -1
- package/docs/window.md +2 -0
- package/examples/lit.tsx +182 -0
- package/package.json +3 -3
- package/src/badge.tsx +25 -6
- package/src/button.tsx +44 -29
- package/src/card.tsx +76 -20
- package/src/checkbox.tsx +23 -19
- package/src/context-menu.tsx +21 -9
- package/src/editor-field.tsx +12 -7
- package/src/focus-nav.ts +17 -6
- package/src/glow.ts +35 -0
- package/src/index.ts +16 -1
- package/src/item.tsx +35 -13
- package/src/light.ts +274 -0
- package/src/motion.tsx +13 -1
- package/src/nav-shell.tsx +19 -10
- package/src/press.ts +2 -1
- package/src/pressable.tsx +6 -4
- package/src/progress-bar.tsx +25 -7
- package/src/radio.tsx +34 -22
- package/src/scroll-view.tsx +8 -7
- package/src/segmented-control.tsx +104 -23
- package/src/select.tsx +40 -34
- package/src/slider.tsx +74 -22
- package/src/surface.tsx +287 -0
- package/src/switch.tsx +48 -26
- package/src/theme.ts +201 -40
- package/src/tooltip.tsx +18 -14
- package/src/types.ts +66 -2
- package/src/window.tsx +26 -3
package/src/card.tsx
CHANGED
|
@@ -1,67 +1,123 @@
|
|
|
1
|
-
import { Show } from "@solidrt/core"
|
|
2
|
-
import type { LayoutProps } from "@solidrt/core"
|
|
1
|
+
import { Show, createRadialGradient, createSignal, getLayoutBox, withAlpha } from "@solidrt/core"
|
|
2
|
+
import type { LayoutProps, PointerEvent } from "@solidrt/core"
|
|
3
3
|
import { theme } from "./theme"
|
|
4
|
+
import { policy } from "./policy"
|
|
4
5
|
import { typeStyle } from "./typography"
|
|
5
6
|
import { space } from "./spacing"
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
7
|
+
import { Surface } from "./surface"
|
|
8
|
+
import type { ElevationLevel, StyleProps, TransitionProps } from "./types"
|
|
9
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
8
10
|
import { colorFade } from "./motion"
|
|
9
11
|
|
|
10
12
|
export interface CardProps extends TransitionProps {
|
|
11
13
|
children?: any
|
|
12
14
|
// Optional heading rendered above the content.
|
|
13
15
|
title?: string
|
|
16
|
+
// Hover lift: under the pointer the card rises one elevation level (its
|
|
17
|
+
// key shadow grows and moves out on the elevation motion, the card rises
|
|
18
|
+
// LIFT_RISE px) and a faint highlight in the light's color follows the
|
|
19
|
+
// pointer over it. Nothing rotates or scales, so edges and text stay on
|
|
20
|
+
// the pixel grid. Hover interaction policies only; under the stock
|
|
21
|
+
// presets, with no height to rise from, only the highlight shows.
|
|
22
|
+
lift?: boolean
|
|
14
23
|
ref?: (node: { id: number }) => void
|
|
15
24
|
layout?: LayoutProps
|
|
16
25
|
style?: StyleProps
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
//
|
|
28
|
+
// The level a lifting card rises to.
|
|
29
|
+
const LIFTED: Record<ElevationLevel, ElevationLevel> = { flat: "raised", raised: "floating", floating: "overlay", overlay: "overlay" }
|
|
30
|
+
// Whole pixels, so the card comes to rest on the pixel grid.
|
|
31
|
+
const LIFT_RISE = 2
|
|
32
|
+
// Alpha of the light-colored highlight that follows the pointer over a
|
|
33
|
+
// lifted card.
|
|
34
|
+
const GLARE = 0.08
|
|
35
|
+
|
|
36
|
+
// A themed surface container: a padded column box with rounded corners, a
|
|
37
|
+
// surface face drawn by Surface (raised by default, theme.elevation.raised,
|
|
38
|
+
// zero under the stock presets; style.elevation changes the level), reading
|
|
20
39
|
// its colors from the theme so it recolors live. Borderless by default; pass
|
|
21
|
-
// style.borderWidth or style.borderColor to draw an outline. Override any
|
|
22
|
-
// via style, spacing/sizing via layout.
|
|
40
|
+
// style.borderWidth or style.borderColor to draw an outline. Override any
|
|
41
|
+
// paint via style, spacing/sizing via layout.
|
|
23
42
|
export function Card(props: CardProps) {
|
|
24
43
|
// Theme-level per-component overrides merged under the instance style.
|
|
25
44
|
let styled = () => ({ ...theme.components.card, ...props.style })
|
|
26
45
|
let bg = () => styled().backgroundColor ?? theme.color.surface
|
|
27
46
|
let radius = () => styled().borderRadius ?? theme.radius.lg
|
|
47
|
+
let elevation = (): ElevationLevel => styled().elevation ?? "raised"
|
|
28
48
|
let hasBorder = () => styled().borderWidth != null || styled().borderColor != null
|
|
29
49
|
|
|
50
|
+
let node: { id: number } | undefined
|
|
51
|
+
// Pointer position over the card, 0..1 per axis; null when not over it.
|
|
52
|
+
let [pointer, setPointer] = createSignal<[number, number] | null>(null)
|
|
53
|
+
let lifts = () => props.lift === true && policy.interaction !== "touch"
|
|
54
|
+
let track = (e: PointerEvent) => {
|
|
55
|
+
if (!lifts() || !node) return
|
|
56
|
+
let box = getLayoutBox(node)
|
|
57
|
+
if (!box || box.width === 0 || box.height === 0) return
|
|
58
|
+
setPointer([e.localX / box.width, e.localY / box.height])
|
|
59
|
+
}
|
|
60
|
+
let glare = () => {
|
|
61
|
+
let p = pointer()
|
|
62
|
+
if (!p) return "transparent"
|
|
63
|
+
return createRadialGradient(p[0], p[1], 0.9, [
|
|
64
|
+
{ offset: 0, color: withAlpha(theme.light.color, GLARE) },
|
|
65
|
+
{ offset: 1, color: withAlpha(theme.light.color, 0) },
|
|
66
|
+
])
|
|
67
|
+
}
|
|
68
|
+
// The rise: a whole-pixel translate on the elevation motion's timing,
|
|
69
|
+
// only where the face has a height to rise from.
|
|
70
|
+
let rises = () => lifts() && policy.motion === "normal" && theme.elevation[elevation()] > 0
|
|
71
|
+
let riseTransition = () => (rises() ? { y: { duration: theme.motion.slow, bounce: 0.2 } } : undefined)
|
|
72
|
+
|
|
30
73
|
let split = () => splitTransition(props.transition)
|
|
31
74
|
|
|
32
75
|
return (
|
|
33
76
|
<view
|
|
34
77
|
transition={split().root}
|
|
35
78
|
onTransitionEnd={transitionEndFor("root", props.onTransitionEnd)}
|
|
36
|
-
ref={
|
|
79
|
+
ref={(n: { id: number }) => {
|
|
80
|
+
node = n
|
|
81
|
+
props.ref?.(n)
|
|
82
|
+
}}
|
|
37
83
|
repaintBoundary
|
|
38
84
|
flexDirection="column"
|
|
39
85
|
gap={space("lg")}
|
|
40
86
|
padding={space("xl")}
|
|
41
87
|
{...props.layout}
|
|
42
88
|
x={styled().x}
|
|
43
|
-
y={styled().y}
|
|
89
|
+
y={(styled().y ?? 0) - (rises() && pointer() ? LIFT_RISE : 0)}
|
|
44
90
|
scale={styled().scale}
|
|
45
91
|
rotate={styled().rotate}
|
|
46
92
|
opacity={styled().opacity}
|
|
93
|
+
onPointerMove={lifts() ? track : undefined}
|
|
94
|
+
onPointerEnter={lifts() ? track : undefined}
|
|
95
|
+
onPointerLeave={lifts() ? () => setPointer(null) : undefined}
|
|
47
96
|
>
|
|
48
|
-
<
|
|
97
|
+
<Surface
|
|
98
|
+
role="surface"
|
|
99
|
+
elevation={elevation()}
|
|
100
|
+
fill={bg()}
|
|
101
|
+
radius={radius()}
|
|
102
|
+
material={styled().material}
|
|
103
|
+
glow={styled().glow}
|
|
104
|
+
liftTo={lifts() ? LIFTED[elevation()] : undefined}
|
|
105
|
+
lifted={pointer() != null}
|
|
106
|
+
outline={hasBorder() ? { color: styled().borderColor ?? theme.color.border, width: styled().borderWidth ?? theme.borderWidth.sm } : null}
|
|
107
|
+
fillTransition={split().background}
|
|
108
|
+
onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
109
|
+
outlineTransition={split().border}
|
|
110
|
+
onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
111
|
+
/>
|
|
112
|
+
<Show when={lifts()}>
|
|
113
|
+
<d-rect color={glare()} radius={radius()} />
|
|
114
|
+
</Show>
|
|
49
115
|
<Show when={props.title != null}>
|
|
50
116
|
<text transition={colorFade()} color={theme.color.text} {...typeStyle("title")}>
|
|
51
117
|
{props.title}
|
|
52
118
|
</text>
|
|
53
119
|
</Show>
|
|
54
120
|
{props.children}
|
|
55
|
-
<Show when={hasBorder()}>
|
|
56
|
-
<d-rect
|
|
57
|
-
drawStyle="stroke"
|
|
58
|
-
transition={withTransitionDefaults(split().border, colorFade())}
|
|
59
|
-
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
60
|
-
color={styled().borderColor ?? theme.color.border}
|
|
61
|
-
strokeWidth={styled().borderWidth ?? theme.borderWidth.sm}
|
|
62
|
-
radius={radius()}
|
|
63
|
-
/>
|
|
64
|
-
</Show>
|
|
65
121
|
</view>
|
|
66
122
|
)
|
|
67
123
|
}
|
package/src/checkbox.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { theme } from "./theme"
|
|
|
5
5
|
import { policy } from "./policy"
|
|
6
6
|
import { densityScale } from "./density"
|
|
7
7
|
import { Icon } from "./icon"
|
|
8
|
+
import { Surface } from "./surface"
|
|
8
9
|
import type { StyleProps, TransitionProps } from "./types"
|
|
9
10
|
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
10
11
|
import { colorFade, markMotion, pressScale, scaleFeedback } from "./motion"
|
|
@@ -21,11 +22,12 @@ export interface CheckboxProps extends TransitionProps {
|
|
|
21
22
|
|
|
22
23
|
const SIZE = 20
|
|
23
24
|
|
|
24
|
-
// A checkbox.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// A checkbox. Unchecked it is a sunken control face (a bordered well in the
|
|
26
|
+
// surface color); checked it fills with primary as an accent face, with the
|
|
27
|
+
// accent material's glow, and draws a checkmark. The fill fades, the mark
|
|
28
|
+
// pops in/out (markMotion), and a press shrinks the box slightly
|
|
29
|
+
// (pressScale). Controlled via checked/onChange, or uncontrolled via
|
|
30
|
+
// defaultChecked. When disabled, it takes no pointer events at all.
|
|
29
31
|
export function Checkbox(props: CheckboxProps) {
|
|
30
32
|
let [internal, setInternal] = createSignal(props.defaultChecked ?? false)
|
|
31
33
|
let checked = () => (props.checked !== undefined ? props.checked : internal())
|
|
@@ -36,6 +38,9 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
36
38
|
props.onChange?.(next)
|
|
37
39
|
}
|
|
38
40
|
let press = createPress({ onPress: toggle })
|
|
41
|
+
// Widens the glow; hover counts only where a pointer can hover (the
|
|
42
|
+
// tint's rule).
|
|
43
|
+
let active = () => press.pressed() || (press.hovered() && policy.interaction !== "touch")
|
|
39
44
|
|
|
40
45
|
let size = () => Math.round(SIZE * densityScale())
|
|
41
46
|
// The checkmark in box-relative fractions, so it scales with the density.
|
|
@@ -76,11 +81,20 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
76
81
|
focusable={!props.disabled}
|
|
77
82
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
78
83
|
>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
<Surface
|
|
85
|
+
role={checked() ? "accent" : "control"}
|
|
86
|
+
elevation={style().elevation ?? "flat"}
|
|
87
|
+
fill={style().backgroundColor ?? "transparent"}
|
|
83
88
|
radius={style().borderRadius}
|
|
89
|
+
material={style().material}
|
|
90
|
+
glow={checked() && !props.disabled ? style().glow : null}
|
|
91
|
+
sunken={!checked()}
|
|
92
|
+
active={active()}
|
|
93
|
+
outline={(style().borderWidth ?? 0) > 0 ? { color: style().borderColor ?? "transparent", width: style().borderWidth! } : null}
|
|
94
|
+
fillTransition={split().background}
|
|
95
|
+
onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
96
|
+
outlineTransition={split().border}
|
|
97
|
+
onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
84
98
|
/>
|
|
85
99
|
<Show when={checked()}>
|
|
86
100
|
<view
|
|
@@ -113,16 +127,6 @@ export function Checkbox(props: CheckboxProps) {
|
|
|
113
127
|
</Show>
|
|
114
128
|
</view>
|
|
115
129
|
</Show>
|
|
116
|
-
<Show when={(style().borderWidth ?? 0) > 0}>
|
|
117
|
-
<d-rect
|
|
118
|
-
drawStyle="stroke"
|
|
119
|
-
transition={withTransitionDefaults(split().border, colorFade())}
|
|
120
|
-
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
121
|
-
color={style().borderColor ?? "transparent"}
|
|
122
|
-
strokeWidth={style().borderWidth}
|
|
123
|
-
radius={style().borderRadius}
|
|
124
|
-
/>
|
|
125
|
-
</Show>
|
|
126
130
|
</view>
|
|
127
131
|
)
|
|
128
132
|
}
|
package/src/context-menu.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { typeStyle } from "./typography"
|
|
|
8
8
|
import type { TransitionProps } from "./types"
|
|
9
9
|
import { splitTransition, transitionEndFor } from "./types"
|
|
10
10
|
import { colorFade, popupFade, popupFadeOut, PressFeedback } from "./motion"
|
|
11
|
+
import { Surface } from "./surface"
|
|
11
12
|
|
|
12
13
|
export interface ContextMenuItem {
|
|
13
14
|
label: string
|
|
@@ -84,6 +85,7 @@ export function ContextMenu(props: ContextMenuProps) {
|
|
|
84
85
|
// Anchored at the opening pointer position, flipping up when it would run
|
|
85
86
|
// off the bottom. Same reflow-free placement as Tooltip/Select: portal at
|
|
86
87
|
// the window root, measured in onLayout, moved with x/y paint transforms.
|
|
88
|
+
// An overlay face (the sheet sits on a scrim, so it stays flat).
|
|
87
89
|
let Menu = () => {
|
|
88
90
|
let menu: { id: number } | undefined
|
|
89
91
|
let [pos, setPos] = createSignal<{ x: number; y: number } | null>(null)
|
|
@@ -124,17 +126,21 @@ export function ContextMenu(props: ContextMenuProps) {
|
|
|
124
126
|
paddingTop={theme.spacing.sm}
|
|
125
127
|
paddingBottom={theme.spacing.sm}
|
|
126
128
|
>
|
|
127
|
-
<
|
|
129
|
+
<Surface
|
|
130
|
+
role="overlay"
|
|
131
|
+
elevation={theme.components.contextMenu?.elevation ?? "overlay"}
|
|
132
|
+
fill={theme.components.contextMenu?.backgroundColor ?? theme.color.surface}
|
|
133
|
+
radius={theme.components.contextMenu?.borderRadius ?? theme.radius.sm}
|
|
134
|
+
material={theme.components.contextMenu?.material}
|
|
135
|
+
glow={theme.components.contextMenu?.glow}
|
|
136
|
+
outline={{
|
|
137
|
+
color: theme.components.contextMenu?.borderColor ?? theme.color.border,
|
|
138
|
+
width: theme.components.contextMenu?.borderWidth ?? theme.borderWidth.sm,
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
128
141
|
<For each={props.items}>
|
|
129
142
|
{(item: ContextMenuItem) => <ItemRow item={item} padY={space("sm")} />}
|
|
130
143
|
</For>
|
|
131
|
-
<d-rect
|
|
132
|
-
drawStyle="stroke"
|
|
133
|
-
transition={colorFade()}
|
|
134
|
-
color={theme.color.border}
|
|
135
|
-
strokeWidth={theme.borderWidth.sm}
|
|
136
|
-
radius={theme.radius.sm}
|
|
137
|
-
/>
|
|
138
144
|
</view>
|
|
139
145
|
</view>,
|
|
140
146
|
)
|
|
@@ -157,7 +163,13 @@ export function ContextMenu(props: ContextMenuProps) {
|
|
|
157
163
|
paddingTop={theme.spacing.md}
|
|
158
164
|
paddingBottom={theme.spacing.md + env.safeArea.bottom}
|
|
159
165
|
>
|
|
160
|
-
<
|
|
166
|
+
<Surface
|
|
167
|
+
role="overlay"
|
|
168
|
+
fill={theme.components.contextMenu?.backgroundColor ?? theme.color.surface}
|
|
169
|
+
radius={theme.components.contextMenu?.borderRadius ?? theme.radius.sm}
|
|
170
|
+
material={theme.components.contextMenu?.material}
|
|
171
|
+
glow={null}
|
|
172
|
+
/>
|
|
161
173
|
<For each={props.items}>
|
|
162
174
|
{(item: ContextMenuItem) => <ItemRow item={item} padY={Math.round(theme.spacing.md * 1.5)} />}
|
|
163
175
|
</For>
|
package/src/editor-field.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import { registerNavAction } from "./focus-nav"
|
|
|
26
26
|
import type { EditorLayoutProps, StyleProps, TransitionProps } from "./types"
|
|
27
27
|
import { splitTextLayout, splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
28
28
|
import { colorFade } from "./motion"
|
|
29
|
+
import { Surface } from "./surface"
|
|
29
30
|
import { theme } from "./theme"
|
|
30
31
|
import { policy } from "./policy"
|
|
31
32
|
import { space } from "./spacing"
|
|
@@ -497,14 +498,18 @@ export function EditorField(props: EditorFieldProps) {
|
|
|
497
498
|
onKeyDown={handleKeyDown}
|
|
498
499
|
onTextInput={handleTextInput}
|
|
499
500
|
>
|
|
500
|
-
<
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
transition={withTransitionDefaults(split().border, colorFade())}
|
|
504
|
-
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
505
|
-
color={borderColor()}
|
|
506
|
-
strokeWidth={borderWidth()}
|
|
501
|
+
<Surface
|
|
502
|
+
role="control"
|
|
503
|
+
fill={surfaceColor()}
|
|
507
504
|
radius={borderRadius()}
|
|
505
|
+
material={props.style?.material}
|
|
506
|
+
glow={props.style?.glow}
|
|
507
|
+
sunken
|
|
508
|
+
outline={{ color: borderColor(), width: borderWidth() }}
|
|
509
|
+
fillTransition={split().background}
|
|
510
|
+
onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
511
|
+
outlineTransition={split().border}
|
|
512
|
+
onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
508
513
|
/>
|
|
509
514
|
<view
|
|
510
515
|
ref={(n: { id: number }) => (viewport = n)}
|
package/src/focus-nav.ts
CHANGED
|
@@ -129,9 +129,15 @@ function repeating<T>(read: () => T | null, step: (value: T) => void): void {
|
|
|
129
129
|
|
|
130
130
|
// What "select" means per focusable node, registered by createPress's ref.
|
|
131
131
|
// Only the controller path consults it - key activation reaches the focused
|
|
132
|
-
// node by bubbling and never comes through here.
|
|
132
|
+
// node by bubbling and never comes through here.
|
|
133
133
|
let navActions = new Map<number, () => void>()
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Registers what the focus navigation's `select` action does on a focusable
|
|
137
|
+
* node; returns the unregister. createPress does this for every press
|
|
138
|
+
* control, so a custom control needs it only when it activates without
|
|
139
|
+
* createPress.
|
|
140
|
+
*/
|
|
135
141
|
export function registerNavAction(nodeId: number, action: () => void): () => void {
|
|
136
142
|
navActions.set(nodeId, action)
|
|
137
143
|
return () => {
|
|
@@ -334,11 +340,16 @@ export function createFocusNav(options?: FocusNavOptions) {
|
|
|
334
340
|
() => currentScope(),
|
|
335
341
|
(scopeNode) => {
|
|
336
342
|
if (!scopeNode) return
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
// The pull reads focus and the candidates for itself, a snapshot, not
|
|
344
|
+
// a dependency: untracked so the strict-mode read diagnostic stays
|
|
345
|
+
// quiet (as in `repeating`).
|
|
346
|
+
untrack(() => {
|
|
347
|
+
let focused = focusedNode()
|
|
348
|
+
if (focused != null && getNodePath(focused).includes(scopeNode.id)) return
|
|
349
|
+
let placed = reachable()
|
|
350
|
+
if (placed.length > 0) focusFirst(placed)
|
|
351
|
+
else if (focused != null) setFocus(null)
|
|
352
|
+
})
|
|
342
353
|
},
|
|
343
354
|
)
|
|
344
355
|
|
package/src/glow.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseColor, withAlpha } from "@solidrt/core"
|
|
2
|
+
import type { Color, Gradient, ShadowProps } from "@solidrt/core"
|
|
3
|
+
import { theme } from "./theme"
|
|
4
|
+
import type { Glow, MaterialRole } from "./types"
|
|
5
|
+
|
|
6
|
+
// Alpha of a glow in its part's own fill color: reads as emitted light
|
|
7
|
+
// against a dark ground without blurring the part's edge away.
|
|
8
|
+
const GLOW_ALPHA = 0.7
|
|
9
|
+
// Growth of the glow radius while a pressable face is hovered or pressed.
|
|
10
|
+
const GLOW_ACTIVE = 1.35
|
|
11
|
+
|
|
12
|
+
/** The glow a part draws: the instance or theme.components override when set (null clears), else the role's material glow. */
|
|
13
|
+
export function partGlow(override: Glow | null | undefined, role: MaterialRole): Glow | null | undefined {
|
|
14
|
+
return override !== undefined ? override : theme.material[role].glow
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The shadow a part draws as its glow, for a rect's `shadow` prop: zero
|
|
19
|
+
* offset, the glow's radius of blur (grown by GLOW_ACTIVE while `active`),
|
|
20
|
+
* in the glow's color or else the fill at GLOW_ALPHA. A fill with no color
|
|
21
|
+
* to take - transparent, or a gradient - casts nothing rather than a black
|
|
22
|
+
* halo. Draw it on an opaque rect (the part's own fill rect, or one in its
|
|
23
|
+
* color hidden under it), never on a transparent one: a transparent
|
|
24
|
+
* caster's blurred shadow composites in a save layer of its own, which a
|
|
25
|
+
* tiled GPU pays for per part (okf/notes/lit-list-cost.md).
|
|
26
|
+
*/
|
|
27
|
+
export function glowShadow(glow: Glow | null | undefined, fill: Color | Gradient, active = false): ShadowProps | undefined {
|
|
28
|
+
if (!glow) return undefined
|
|
29
|
+
let color = glow.color
|
|
30
|
+
if (color == null) {
|
|
31
|
+
if (typeof fill !== "string" || ((parseColor(fill) >>> 0) & 0xff) === 0) return undefined
|
|
32
|
+
color = withAlpha(fill, GLOW_ALPHA)
|
|
33
|
+
}
|
|
34
|
+
return { x: 0, y: 0, blur: glow.radius * (active ? GLOW_ACTIVE : 1), color }
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,8 +10,9 @@ export { ScrollView, type ScrollViewProps } from "./scroll-view"
|
|
|
10
10
|
export { Dismissible, type DismissibleProps, type DismissDirection } from "./dismissible"
|
|
11
11
|
export { Carousel, type CarouselProps } from "./carousel"
|
|
12
12
|
export { Pressable, type PressableProps, type PressState } from "./pressable"
|
|
13
|
+
export { createPress, type PressOptions } from "./press"
|
|
13
14
|
export { Button, type ButtonProps, type ButtonVariant } from "./button"
|
|
14
|
-
export { createFocusNav, uiActions, uiBindings, type FocusNavOptions, type UiActions, type UiDevices } from "./focus-nav"
|
|
15
|
+
export { createFocusNav, registerNavAction, uiActions, uiBindings, type FocusNavOptions, type UiActions, type UiDevices } from "./focus-nav"
|
|
15
16
|
export { Switch, type SwitchProps } from "./switch"
|
|
16
17
|
export { Checkbox, type CheckboxProps } from "./checkbox"
|
|
17
18
|
export { RadioGroup, Radio, type RadioGroupProps, type RadioProps } from "./radio"
|
|
@@ -33,15 +34,24 @@ export { NavShell, type NavShellProps, type NavItem } from "./nav-shell"
|
|
|
33
34
|
export { SplitView, type SplitViewProps } from "./split-view"
|
|
34
35
|
export { QrCode, type QrCodeProps } from "./qrcode"
|
|
35
36
|
export { Icon, type IconProps } from "./icon"
|
|
37
|
+
export { Surface, facePaint, roleFill, roleMaterial, surfaceSinks, type SurfaceProps, type FacePaint } from "./surface"
|
|
38
|
+
export { lightFrom, lightSource, lightAxis, type LightAxis } from "./light"
|
|
39
|
+
export { PressFeedback } from "./motion"
|
|
36
40
|
export {
|
|
37
41
|
theme,
|
|
38
42
|
setTheme,
|
|
39
43
|
defineTheme,
|
|
40
44
|
darkTheme,
|
|
41
45
|
lightTheme,
|
|
46
|
+
litDarkTheme,
|
|
47
|
+
litLightTheme,
|
|
48
|
+
pill,
|
|
42
49
|
type Theme,
|
|
43
50
|
type ThemeDefinition,
|
|
44
51
|
type ThemeColor,
|
|
52
|
+
type ThemeValue,
|
|
53
|
+
type Light,
|
|
54
|
+
type ShadowTone,
|
|
45
55
|
type ThemedComponent,
|
|
46
56
|
type TextStyle,
|
|
47
57
|
type TextVariant,
|
|
@@ -64,6 +74,11 @@ export { typeStyle, typeWeight, lightOnDark } from "./typography"
|
|
|
64
74
|
export { space } from "./spacing"
|
|
65
75
|
export type {
|
|
66
76
|
StyleProps,
|
|
77
|
+
Glow,
|
|
78
|
+
Glass,
|
|
79
|
+
Material,
|
|
80
|
+
MaterialRole,
|
|
81
|
+
ElevationLevel,
|
|
67
82
|
FontProps,
|
|
68
83
|
TextLayoutProps,
|
|
69
84
|
EditorLayoutProps,
|
package/src/item.tsx
CHANGED
|
@@ -5,9 +5,10 @@ import { theme } from "./theme"
|
|
|
5
5
|
import { policy } from "./policy"
|
|
6
6
|
import { space } from "./spacing"
|
|
7
7
|
import { typeStyle } from "./typography"
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { Surface } from "./surface"
|
|
9
|
+
import type { ElevationLevel, StyleProps, TransitionProps } from "./types"
|
|
10
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
11
|
+
import { colorFade } from "./motion"
|
|
11
12
|
|
|
12
13
|
export interface ItemProps extends TransitionProps {
|
|
13
14
|
// Leading content: an icon, avatar, checkbox, ...
|
|
@@ -38,9 +39,14 @@ export interface ItemProps extends TransitionProps {
|
|
|
38
39
|
// trailing content pushed to the end. The dense-data workhorse - rows compose
|
|
39
40
|
// with <For> inside a plain column (or a ScrollView); this package ships no
|
|
40
41
|
// List wrapper because a column view IS the list. Paddings and the gap are
|
|
41
|
-
// density-scaled, so a <Density> region compacts rows wholesale.
|
|
42
|
-
//
|
|
43
|
-
//
|
|
42
|
+
// density-scaled, so a <Density> region compacts rows wholesale. Its face is
|
|
43
|
+
// a surface drawn by Surface, flat by default whatever the theme: a screen
|
|
44
|
+
// holds dozens of rows, and a blurred shadow per row is what a scrolling list
|
|
45
|
+
// cannot afford on a tablet or a TV (okf/notes/lit-list-cost.md); a raised
|
|
46
|
+
// row is the app's choice through style.elevation. With onPress the row
|
|
47
|
+
// presses like a menu entry: overlay tints for hover/pressed (no scale -
|
|
48
|
+
// rows sit flush in a list; a row with depth sinks), focus ring under the
|
|
49
|
+
// focusRing policy. borderColor/borderWidth box the row.
|
|
44
50
|
export function Item(props: ItemProps) {
|
|
45
51
|
// Theme-level per-component overrides merged under the instance style.
|
|
46
52
|
let styled = () => ({ ...theme.components.item, ...props.style })
|
|
@@ -49,6 +55,10 @@ export function Item(props: ItemProps) {
|
|
|
49
55
|
|
|
50
56
|
let bg = () => styled().backgroundColor ?? (props.selected ? theme.color.surfaceAlt : "transparent")
|
|
51
57
|
let radius = () => styled().borderRadius ?? theme.radius.sm
|
|
58
|
+
let elevation = (): ElevationLevel => styled().elevation ?? "flat"
|
|
59
|
+
let hasBorder = () => styled().borderWidth != null || styled().borderColor != null
|
|
60
|
+
let pressed = () => interactive() && press.pressed()
|
|
61
|
+
let hovered = () => interactive() && press.hovered() && policy.interaction !== "touch"
|
|
52
62
|
|
|
53
63
|
// Resolved once via children(): the typeof probe and the mount site must
|
|
54
64
|
// share one build (see Button).
|
|
@@ -92,11 +102,26 @@ export function Item(props: ItemProps) {
|
|
|
92
102
|
focusable={(props.focusable ?? true) && interactive()}
|
|
93
103
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
94
104
|
>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
<Surface
|
|
106
|
+
role="surface"
|
|
107
|
+
elevation={elevation()}
|
|
108
|
+
fill={bg()}
|
|
99
109
|
radius={radius()}
|
|
110
|
+
material={styled().material}
|
|
111
|
+
glow={styled().glow}
|
|
112
|
+
pressed={pressed()}
|
|
113
|
+
hovered={hovered()}
|
|
114
|
+
outline={
|
|
115
|
+
press.focused() && policy.focusRing
|
|
116
|
+
? { color: theme.color.ring, width: theme.borderWidth.focus }
|
|
117
|
+
: hasBorder()
|
|
118
|
+
? { color: styled().borderColor ?? theme.color.border, width: styled().borderWidth ?? theme.borderWidth.sm }
|
|
119
|
+
: null
|
|
120
|
+
}
|
|
121
|
+
fillTransition={split().background}
|
|
122
|
+
onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
123
|
+
outlineTransition={split().border}
|
|
124
|
+
onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
100
125
|
/>
|
|
101
126
|
{props.startContent}
|
|
102
127
|
<view flexDirection="column" flexGrow={1} flexShrink={1} gap={Math.round(space("sm") / 2)}>
|
|
@@ -114,9 +139,6 @@ export function Item(props: ItemProps) {
|
|
|
114
139
|
</Show>
|
|
115
140
|
</view>
|
|
116
141
|
{props.endContent}
|
|
117
|
-
<Show when={press.focused() && policy.focusRing}>
|
|
118
|
-
<d-rect drawStyle="stroke" color={theme.color.ring} strokeWidth={theme.borderWidth.focus} radius={radius()} />
|
|
119
|
-
</Show>
|
|
120
142
|
</view>
|
|
121
143
|
)
|
|
122
144
|
}
|