@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/docs/surface.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Surface
|
|
2
|
+
|
|
3
|
+
The one way a component draws a face: a role's material (`theme.material`) under the theme light (`theme.light`) at an elevation (`theme.elevation`), stacked as layers that fill the view it is rendered in - a contact shadow, the glow, the fill (tinted by the light, with the sheen gradient along it, carrying the key shadow the elevation casts), the hover/pressed tint, the bevel and the outline. Every themed component draws its chrome through it, so a theme's light, materials and elevation restyle the whole library at once; under the stock presets (flat materials, zero elevation) a face is a plain fill, the tint and the outline, the look the package has always had. A custom control uses it the same way: render it as the first child of the laid-out view it dresses, pick a `role` (never a material), give it the base `fill`, the `radius`, the press state for the tint and the sink, and an `outline` for its border or focus ring.
|
|
4
|
+
|
|
5
|
+
```jsx
|
|
6
|
+
import { Surface, createPress, theme } from "@solidrt/components"
|
|
7
|
+
|
|
8
|
+
function Tile(props) {
|
|
9
|
+
let press = createPress(props)
|
|
10
|
+
return (
|
|
11
|
+
<view ref={press.ref} {...press.handlers} padding={16} cursor="pointer" focusable>
|
|
12
|
+
<Surface role="surface" elevation="raised" radius={theme.radius.lg} pressed={press.pressed()} hovered={press.hovered()} />
|
|
13
|
+
<text color={theme.color.text}>{props.label}</text>
|
|
14
|
+
</view>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A pressed face sinks (its key shadow fades toward the contact shadow, its lit edge moves to the far side) on the native shadow transition; a `sunken` face is a well or a groove (shaded like a pressed face, casting nothing); `lifted` with `liftTo` retargets the key shadow to a higher level (the Card hover lift); `chrome={false}` keeps only the tint (a ghost button at rest). `material` merges layers over the role's, `glow` overrides that layer alone. Shadows cast from opaque casters hidden under the fill, since a transparent caster's blurred shadow composites in a save layer of its own, which a tiled GPU pays for per shadow; only a glass material (a frosted backdrop, the overlay role's option) casts from a transparent one. `facePaint` gives the same derivation as plain paint values, for a part a control draws itself (a slider fill, a segmented indicator); `roleFill`, `roleMaterial` and `surfaceSinks` are the pieces it is built from.
|
package/docs/switch.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Switch
|
|
2
2
|
|
|
3
|
-
An on/off toggle: the track fills with `primary` when on and `surfaceAlt` when off (a fade), and the thumb springs across - the `knob` transition entry retimes that travel. Controlled via `value`/`onChange`, or uncontrolled via `defaultValue`. Built on `Pressable`, so `disabled` takes no pointer events. `style` overrides the track colors and radius.
|
|
3
|
+
An on/off toggle: the track fills with `primary` when on and `surfaceAlt` when off (a fade), and the thumb springs across - the `knob` transition entry retimes that travel. Controlled via `value`/`onChange`, or uncontrolled via `defaultValue`. Built on `Pressable`, so `disabled` takes no pointer events. The thumb is `theme.color.thumb` (else `onPrimary`), track and thumb follow `radius.full` through `pill` (square in an all-square theme), and the on track glows with the theme's accent glow. `style` overrides the track colors and radius.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { Switch } from "@solidrt/components"
|
package/docs/theme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Theming
|
|
2
2
|
|
|
3
|
-
Appearance (colors, spacing, border, font roles) comes from one shared, reactive theme backed by a Solid store: reads are tracked, so switching the theme at runtime recolors the live UI without remounting.
|
|
3
|
+
Appearance (colors, spacing, border, font roles) comes from one shared, reactive theme backed by a Solid store: reads are tracked, so switching the theme at runtime recolors the live UI without remounting. Four presets ship, `darkTheme` and `lightTheme` (the flat look, default dark) and `litDarkTheme` and `litLightTheme` (the same palette under a light, see below); `setTheme(preset)` switches, `setTheme(partial)` merges an override one level deep per category. A resolved theme carries every key of every category (unset ones as `undefined`), so switching to one replaces the previous theme outright, per-component overrides and icon slots included; a partial keeps whatever it does not name. Custom themes are authored with `defineTheme`.
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { setTheme, darkTheme, lightTheme } from "@solidrt/components"
|
|
@@ -33,7 +33,7 @@ The type scale derives from `text.base` (the body size, default 14) and `text.ra
|
|
|
33
33
|
|
|
34
34
|
## Tokens
|
|
35
35
|
|
|
36
|
-
The color tokens are `background` (window fill), `surface` (control/card fill), `surfaceAlt` (subtle raised/track fill), `text`, `textMuted`, `border`, `primary`/`onPrimary`, `secondary`/`onSecondary` (lower-emphasis accent), `danger` (validation/destructive), `scrim` (modal dim), `ring` (the focus ring; defaults to `text` so it stays visible on primary fills), and the feedback pair `overlayHover`/`overlayPressed`: translucent tints components draw OVER a control's own fill, so one token pair gives hover/pressed feedback on every fill color, including caller-set ones. Non-color tokens are `spacing`, `radius`, `borderWidth` (`sm` for borders, `focus` for the ring), `size` (app-wide default extents: `navRail` 72, `navSidebar` 220, `splitViewList` 320, `menuMinWidth` 120, `slider` 200; each overridable per instance through its layout or prop), and `text` (the type scale: `caption`/`label`/`body`/`title`/`heading` roles, each `{ size, lineHeight, weight }`, plus `fontFamily` and `monoFamily` for code).
|
|
36
|
+
The color tokens are `background` (window fill), `surface` (control/card fill), `surfaceAlt` (subtle raised/track fill), `text`, `textMuted`, `border`, `primary`/`onPrimary`, `secondary`/`onSecondary` (lower-emphasis accent), `danger` (validation/destructive), `scrim` (modal dim), `ring` (the focus ring; defaults to `text` so it stays visible on primary fills), `thumb` (optional: the cap riding a track, the Switch knob and the Slider thumb; unset they are `onPrimary` and `primary`, so a palette whose `onPrimary` is dark sets a light cap here), and the feedback pair `overlayHover`/`overlayPressed`: translucent tints components draw OVER a control's own fill, so one token pair gives hover/pressed feedback on every fill color, including caller-set ones. Non-color tokens are `spacing`, `radius`, `borderWidth` (`sm` for borders, `focus` for the ring), `size` (app-wide default extents: `navRail` 72, `navSidebar` 220, `splitViewList` 320, `menuMinWidth` 120, `slider` 200; each overridable per instance through its layout or prop), and `text` (the type scale: `caption`/`label`/`body`/`title`/`heading` roles, each `{ size, lineHeight, weight }`, plus `fontFamily` and `monoFamily` for code).
|
|
37
37
|
|
|
38
38
|
## Spacing
|
|
39
39
|
|
|
@@ -41,7 +41,7 @@ Spacing is one base unit: `spacing` in a theme definition is a number (default 4
|
|
|
41
41
|
|
|
42
42
|
## Radius
|
|
43
43
|
|
|
44
|
-
Corner radius is set once: `radius` in a theme definition is a single number, the control radius (default 8), and the scale derives from it: `md` is the base (Button, TextInput, RichTextEditor, Select, SegmentedControl, QrCode), `sm` half of it (Checkbox, Item, NavShell items, Select and ContextMenu popups, Tooltip), `lg` one and a half (Card), and `full` the pill (Badge). Set `radius: 0` for a square theme, `radius: 12` for a soft one; buttons and inputs always match. Shapes derived from a control's own height (Switch, Slider, ProgressBar, Radio)
|
|
44
|
+
Corner radius is set once: `radius` in a theme definition is a single number, the control radius (default 8), and the scale derives from it: `md` is the base (Button, TextInput, RichTextEditor, Select, SegmentedControl, QrCode), `sm` half of it (Checkbox, Item, NavShell items, Select and ContextMenu popups, Tooltip), `lg` one and a half (Card), and `full` the pill (Badge). Set `radius: 0` for a square theme, `radius: 12` for a soft one; buttons and inputs always match. Shapes derived from a control's own height (Switch, Slider, ProgressBar, Radio) take `full` too, through `pill(size)`: half the size, capped by `radius.full`, so `full: 0` squares every pill and circle in the library (`radius: { sm: 0, md: 0, lg: 0, full: 0 }` is the all-square theme) while the default 9999 leaves them round. `pill` is exported for a custom control with a round part. Pass an object (`radius: { sm, md, lg, full }`, any subset) to pin individual steps instead.
|
|
45
45
|
|
|
46
46
|
```jsx
|
|
47
47
|
setTheme({ radius: 4 }) // sm 2, md 4, lg 6
|
|
@@ -49,21 +49,49 @@ setTheme({ radius: 4 }) // sm 2, md 4, lg 6
|
|
|
49
49
|
|
|
50
50
|
## Motion
|
|
51
51
|
|
|
52
|
-
`motion` holds the three durations (ms) every built-in component transition draws from, so one theme edit retimes the whole package: `fast` (default 100) is press/hover feedback, `base` (150) the color and opacity fades - state changes, the theme cross-fade (a `setTheme` fades every themed color rather than snapping), popup enter/exit - and `slow` (250) the travel of a control's moving parts (switch knob, segmented indicator, progress fill). `policy.motion` gates whether these play at all; a per-instance `transition` prop overrides them per property.
|
|
52
|
+
`motion` holds the three durations (ms) every built-in component transition draws from, so one theme edit retimes the whole package: `fast` (default 100) is press/hover feedback, `base` (150) the color and opacity fades - state changes, the theme cross-fade (a `setTheme` fades every themed color rather than snapping), popup enter/exit - and `slow` (250) the travel of a control's moving parts (switch knob, segmented indicator, progress fill) and of a face's shadows as it sinks or lifts. `policy.motion` gates whether these play at all; a per-instance `transition` prop overrides them per property.
|
|
53
53
|
|
|
54
54
|
```jsx
|
|
55
55
|
setTheme({ motion: { base: 250, slow: 400 } }) // a slower, calmer app
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
## Light, materials and elevation
|
|
59
|
+
|
|
60
|
+
Every component draws its faces through `Surface`, and three theme sections decide what a face looks like. `light` is the theme's one light in screen space (`direction` [x right, y down, z into the screen], a `color`, and `ambient`, the fill on the shaded side): shadows fall along it, the sheen and the bevel run from its lit edge to the shaded one, and faces take on its color (a white light leaves every base color as it is). `material` assigns each role its layers: `surface` (Card, Item, the NavShell strips), `control` (a secondary Button, the fields, the Select trigger, the tracks and grooves, the Switch knob and the Slider thumb), `accent` (a primary or danger Button, Switch on, a checked Checkbox, the Radio dot, the Slider and ProgressBar fills, the SegmentedControl indicator, a Badge) and `overlay` (the Tooltip bubble, the Select dropdown, the ContextMenu menu, the sheets). A `Material` is `{ sheen?, bevel?, glass?, glow? }`: `sheen` (0..1) a fill gradient along the light, `bevel` (0..1) a one-pixel stroke from the light color to black, `glass` a frosted backdrop (`{ blur, tint? }`, a deliberate overlay, never a surface material: every glass face captures the pixels under it each frame), `glow` a halo (`{ radius, color? }`; without a color a part glows in its own fill, so a danger button glows red, and a fill with no color to take casts nothing; a pressable face widens it while hovered or pressed). `elevation` maps the four levels (`flat`, `raised`, `floating`, `overlay`) to heights: a face at height h casts a key shadow h px along the light and blurred 2.5 h px, plus a tight contact shadow; a press sinks the face (the key shadow fades toward the contact shadow) on the native shadow transition, and `shadow` (`{ color, strength }`) is what they are cast in.
|
|
61
|
+
|
|
62
|
+
The stock presets are flat: every material `{}` and every height 0, so nothing casts or shines and the components look as they always have. `litDarkTheme` and `litLightTheme` are the same palette under the light: a sheen and a bevel per role, `raised` 2, `floating` 6, `overlay` 14, a light cap for the Switch knob and Slider thumb, and shadows 1.7 times stronger on dark, where they otherwise vanish. Card and Button stand raised by default (a ghost or disabled button flat), the Switch knob, the Slider thumb and the SegmentedControl indicator too; Item, Badge and the Checkbox sit flat whatever the theme, since a screen holds dozens of them and a blurred shadow per row is what a scrolling list cannot afford on a tablet or a TV (measured in the runtime's notes); popups float. Per component, `theme.components.<name>.material` merges layers over the role's, `.elevation` sets the level and `.glow` the halo alone, and an instance's `style` does the same over that.
|
|
63
|
+
|
|
64
|
+
```jsx
|
|
65
|
+
setTheme(litDarkTheme) // the lit look
|
|
66
|
+
setTheme({ light: { ...theme.light, direction: [-0.4, 1, 0.8] } }) // light from the right
|
|
67
|
+
setTheme({ material: { accent: { sheen: 1, bevel: 0.9, glow: { radius: 14 } } } }) // glowing accents
|
|
68
|
+
setTheme({ components: { item: { elevation: "raised" } } }) // raised rows, deliberately
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Window finish
|
|
72
|
+
|
|
73
|
+
`finish` is the theme's window finish: a core window shader declaration (`program`, `params`, and the optional `textures`, `previous`, `vertexCount`) that `Window` declares on the window as it is, so one theme can carry a vignette, scanlines or a color grade. `Window` adds one uniform, `uScale`, the display scale, for effects counted in logical pixels. Strength and layer switches are the theme's own `params`; a theme without a `program` has no finish. An app with a slider updates a param by spreading the current ones, because `setTheme` merges one level deep and a partial replaces the whole `params` object it names: `setTheme({ finish: { params: { ...theme.finish.params, uStrength: v } } })`. The theme's module links the program itself, once, at init (the client brings the GPU up before the bundle runs); the package ships no finish, and the stock presets have none.
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
import { compileShader, linkProgram, destroyShader, glsl } from "@solidrt/core/gpu"
|
|
77
|
+
|
|
78
|
+
let fs = compileShader("fragment", glsl`...`, { header: true }) // reads uSource, uScale and its own params
|
|
79
|
+
let vs = compileShader("vertex", glsl`...`) // the covering triangle
|
|
80
|
+
let program = linkProgram(vs, fs, { label: "crt-finish" })
|
|
81
|
+
destroyShader(vs); destroyShader(fs)
|
|
82
|
+
|
|
83
|
+
export let crtTheme = defineTheme({ color: { /* ... */ }, finish: { program, params: { uStrength: 0.4, uScan: 1 } } })
|
|
84
|
+
```
|
|
85
|
+
|
|
58
86
|
## Per-component overrides
|
|
59
87
|
|
|
60
|
-
`theme.components` restyles a component everywhere without wrapping it: a `StyleProps` object per component name, merged between the component's themed defaults and each instance's `style` prop (instance style still wins).
|
|
88
|
+
`theme.components` restyles a component everywhere without wrapping it: a `StyleProps` object per component name, merged between the component's themed defaults and each instance's `style` prop (instance style still wins). Every themed component draws the `borderColor`/`borderWidth` it is given (Item rows, the SegmentedControl, Badge, the Slider and ProgressBar grooves included), so one theme boxes the whole library; `material`, `elevation` and `glow` restyle its faces (see Light, materials and elevation).
|
|
61
89
|
|
|
62
90
|
```jsx
|
|
63
91
|
setTheme({ components: { button: { borderRadius: 999 } } }) // pill buttons app-wide
|
|
64
92
|
```
|
|
65
93
|
|
|
66
|
-
Keys: `button`, `card`, `badge`, `switch`, `checkbox`, `radio`, `item`, `select`, `segmentedControl`, `textInput`, `richTextEditor`, `tooltip`, `divider`, `progressBar`, `spinner`.
|
|
94
|
+
Keys: `button`, `card`, `badge`, `switch`, `checkbox`, `radio`, `slider`, `item`, `select`, `segmentedControl`, `textInput`, `richTextEditor`, `tooltip`, `divider`, `progressBar`, `spinner`, `contextMenu`, `navShell`.
|
|
67
95
|
|
|
68
96
|
## Icon slots
|
|
69
97
|
|
package/docs/tooltip.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tooltip
|
|
2
2
|
|
|
3
|
-
A hover-only affordance: under the `desktop`/`hybrid` interaction policies, resting a mouse pointer on the wrapped content shows a bubble near it after `delay` (default 500ms). Under the `touch` policy it never shows, so tooltip content must stay non-essential. The bubble is portal-mounted at the window root, clamped to the window edges, takes no pointer events, fades in and out,
|
|
3
|
+
A hover-only affordance: under the `desktop`/`hybrid` interaction policies, resting a mouse pointer on the wrapped content shows a bubble near it after `delay` (default 500ms). Under the `touch` policy it never shows, so tooltip content must stay non-essential. The bubble is portal-mounted at the window root, clamped to the window edges, takes no pointer events, fades in and out, hides on leave and on press, and glows with the theme's overlay glow. A string/number `content` renders as themed body text; anything else as-is. `placement` picks the side (`"top"`, the default, or `"bottom"`).
|
|
4
4
|
|
|
5
5
|
```jsx
|
|
6
6
|
import { Tooltip, Button } from "@solidrt/components"
|
package/docs/types.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Layout and style
|
|
2
2
|
|
|
3
|
-
Most components group their props into two objects, split by one rule: `layout` properties feed the layout engine (flexbox/grid, sizing, padding, margin, position - the core `LayoutProps` set) and changing them triggers a relayout; `style` properties are paint-only and never affect layout: `color`, `backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`, `opacity`, and the transform (`x`, `y`, `scale`, `rotate`, `rotateX`/`rotateY` with `perspective`, `originX`/`originY`, `clipRadius`). Event handlers (`onPointerDown`, `onKeyDown`, ...) are top-level props, never inside `layout` or `style`.
|
|
3
|
+
Most components group their props into two objects, split by one rule: `layout` properties feed the layout engine (flexbox/grid, sizing, padding, margin, position - the core `LayoutProps` set) and changing them triggers a relayout; `style` properties are paint-only and never affect layout: `color`, `backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`, `opacity`, `glow` (a halo on the component's accent part; `null` clears the theme's), `material` (layers merged over the role's material for the component's faces) and `elevation` (the level its raised face stands at), see Theming, and the transform (`x`, `y`, `scale`, `rotate`, `rotateX`/`rotateY` with `perspective`, `originX`/`originY`, `clipRadius`). Event handlers (`onPointerDown`, `onKeyDown`, ...) are top-level props, never inside `layout` or `style`.
|
|
4
4
|
|
|
5
5
|
`StyleProps` is that paint set. `TextLayoutProps` extends `LayoutProps` with the font fields (`fontFamily`, `fontSize`, `lineHeight`, `fontStyle`, `fontWeight`, `textAlign`, `maxLines`) because text shaping affects measurement; note `lineHeight` is a multiplier of `fontSize` (the theme uses 1.3-1.6), not a pixel value. `Option` (`{ value, label }`) is the shared shape of the single-choice controls (`Select`, `SegmentedControl`): shared shapes go through this module so components never import a sibling.
|
|
6
6
|
|
package/docs/window.md
CHANGED
|
@@ -13,3 +13,5 @@ function App() {
|
|
|
13
13
|
)
|
|
14
14
|
}
|
|
15
15
|
```
|
|
16
|
+
|
|
17
|
+
The window takes the theme's finish (see Theming) whenever the theme has one; a theme without a finish declares no shader at all. `shader` is the window's own finish, a core window shader declaration forwarded as-is; set, it replaces the theme's, and `shader={null}` opts out of it. A window shader runs past the point snapshots read, so a finish is checked with `srt render`, not `get_snapshot`.
|
package/examples/lit.tsx
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// The lit presets: the same components under darkTheme, lightTheme,
|
|
2
|
+
// litDarkTheme and litLightTheme, side by side with a light pad. Under the
|
|
3
|
+
// lit presets every face is a role's material under theme.light - a sheen
|
|
4
|
+
// along the light, a bevel on its edges, shadows cast from its elevation,
|
|
5
|
+
// a press that sinks - and dragging the pad moves the light for the whole
|
|
6
|
+
// screen at once (setTheme({ light })). The flat presets are the same
|
|
7
|
+
// screen with every material empty and every height zero.
|
|
8
|
+
import { render, createSignal, getLayoutBox } from "@solidrt/core"
|
|
9
|
+
import type { PointerEvent } from "@solidrt/core"
|
|
10
|
+
import {
|
|
11
|
+
Badge,
|
|
12
|
+
Button,
|
|
13
|
+
Card,
|
|
14
|
+
Checkbox,
|
|
15
|
+
Item,
|
|
16
|
+
ProgressBar,
|
|
17
|
+
Radio,
|
|
18
|
+
RadioGroup,
|
|
19
|
+
SegmentedControl,
|
|
20
|
+
Select,
|
|
21
|
+
Slider,
|
|
22
|
+
Surface,
|
|
23
|
+
Switch,
|
|
24
|
+
Text,
|
|
25
|
+
TextInput,
|
|
26
|
+
View,
|
|
27
|
+
Window,
|
|
28
|
+
darkTheme,
|
|
29
|
+
lightTheme,
|
|
30
|
+
litDarkTheme,
|
|
31
|
+
litLightTheme,
|
|
32
|
+
lightFrom,
|
|
33
|
+
lightSource,
|
|
34
|
+
setTheme,
|
|
35
|
+
theme,
|
|
36
|
+
} from "@solidrt/components"
|
|
37
|
+
|
|
38
|
+
type Preset = "flat" | "lit"
|
|
39
|
+
type Scheme = "dark" | "light"
|
|
40
|
+
|
|
41
|
+
const PRESETS = [
|
|
42
|
+
{ value: "flat" as const, label: "Flat" },
|
|
43
|
+
{ value: "lit" as const, label: "Lit" },
|
|
44
|
+
]
|
|
45
|
+
const SCHEMES = [
|
|
46
|
+
{ value: "dark" as const, label: "Dark" },
|
|
47
|
+
{ value: "light" as const, label: "Light" },
|
|
48
|
+
]
|
|
49
|
+
const RANGES = [
|
|
50
|
+
{ value: "day", label: "Day" },
|
|
51
|
+
{ value: "week", label: "Week" },
|
|
52
|
+
{ value: "month", label: "Month" },
|
|
53
|
+
]
|
|
54
|
+
// The light pad's size, and its marker.
|
|
55
|
+
const PAD = 120
|
|
56
|
+
const MARKER = 14
|
|
57
|
+
|
|
58
|
+
function themeFor(preset: Preset, scheme: Scheme) {
|
|
59
|
+
if (preset === "lit") return scheme === "dark" ? litDarkTheme : litLightTheme
|
|
60
|
+
return scheme === "dark" ? darkTheme : lightTheme
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A pad showing where the light comes from; dragging over it moves the
|
|
64
|
+
// light. The direction belongs to the app, so a preset switch keeps it.
|
|
65
|
+
function LightPad() {
|
|
66
|
+
let node: { id: number } | undefined
|
|
67
|
+
let [dragging, setDragging] = createSignal(false)
|
|
68
|
+
let move = (e: PointerEvent) => {
|
|
69
|
+
if (!node) return
|
|
70
|
+
let box = getLayoutBox(node)
|
|
71
|
+
if (!box || box.width === 0) return
|
|
72
|
+
let px = (e.localX / box.width) * 2 - 1
|
|
73
|
+
let py = (e.localY / box.height) * 2 - 1
|
|
74
|
+
setTheme({ light: { ...theme.light, direction: lightFrom(px, py) } })
|
|
75
|
+
}
|
|
76
|
+
let marker = () => {
|
|
77
|
+
let [sx, sy] = lightSource(theme.light)
|
|
78
|
+
return { x: ((sx + 1) / 2) * PAD - MARKER / 2, y: ((sy + 1) / 2) * PAD - MARKER / 2 }
|
|
79
|
+
}
|
|
80
|
+
return (
|
|
81
|
+
<view
|
|
82
|
+
ref={(n: { id: number }) => (node = n)}
|
|
83
|
+
width={PAD}
|
|
84
|
+
height={PAD}
|
|
85
|
+
cursor="pointer"
|
|
86
|
+
onPointerDown={(e: PointerEvent) => {
|
|
87
|
+
setDragging(true)
|
|
88
|
+
move(e)
|
|
89
|
+
}}
|
|
90
|
+
onPointerMove={(e: PointerEvent) => dragging() && move(e)}
|
|
91
|
+
onPointerUp={() => setDragging(false)}
|
|
92
|
+
>
|
|
93
|
+
<Surface role="control" sunken radius={theme.radius.lg} />
|
|
94
|
+
<d-rect x={marker().x} y={marker().y} w={MARKER} h={MARKER} radius={MARKER / 2} color={theme.light.color} />
|
|
95
|
+
</view>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The look, at module scope: the initial setTheme runs at load, outside any
|
|
100
|
+
// component's owned scope (a theme write belongs to an event or to init,
|
|
101
|
+
// never to a component's setup).
|
|
102
|
+
let [preset, setPreset] = createSignal<Preset>("lit")
|
|
103
|
+
let [scheme, setScheme] = createSignal<Scheme>("dark")
|
|
104
|
+
function apply(p: Preset, s: Scheme) {
|
|
105
|
+
// The light's direction is the app's: carry it across the switch.
|
|
106
|
+
let direction = theme.light.direction
|
|
107
|
+
setTheme(themeFor(p, s))
|
|
108
|
+
setTheme({ light: { ...theme.light, direction } })
|
|
109
|
+
}
|
|
110
|
+
apply(preset(), scheme())
|
|
111
|
+
|
|
112
|
+
function App() {
|
|
113
|
+
let [notify, setNotify] = createSignal(true)
|
|
114
|
+
let [volume, setVolume] = createSignal(62)
|
|
115
|
+
let [range, setRange] = createSignal("week")
|
|
116
|
+
let [query, setQuery] = createSignal("")
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Window title="Lit components" style={{ backgroundColor: theme.color.background }} layout={{ padding: 24, gap: 24, flexDirection: "row", alignItems: "flex-start" }}>
|
|
120
|
+
<View layout={{ flexDirection: "column", gap: 16, width: 200 }}>
|
|
121
|
+
<SegmentedControl
|
|
122
|
+
options={PRESETS}
|
|
123
|
+
value={preset()}
|
|
124
|
+
onChange={(v) => {
|
|
125
|
+
setPreset(v)
|
|
126
|
+
apply(v, scheme())
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
<SegmentedControl
|
|
130
|
+
options={SCHEMES}
|
|
131
|
+
value={scheme()}
|
|
132
|
+
onChange={(v) => {
|
|
133
|
+
setScheme(v)
|
|
134
|
+
apply(preset(), v)
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
<Text variant="caption" style={{ color: theme.color.textMuted }}>
|
|
138
|
+
Drag to move the light
|
|
139
|
+
</Text>
|
|
140
|
+
<LightPad />
|
|
141
|
+
</View>
|
|
142
|
+
<Card title="Controls" lift layout={{ width: 360, gap: 16 }}>
|
|
143
|
+
<View layout={{ flexDirection: "row", gap: 8, flexWrap: "wrap" }}>
|
|
144
|
+
<Button>Run</Button>
|
|
145
|
+
<Button variant="secondary">Save</Button>
|
|
146
|
+
<Button variant="ghost">Cancel</Button>
|
|
147
|
+
<Button variant="danger">Delete</Button>
|
|
148
|
+
<Button disabled>Off</Button>
|
|
149
|
+
</View>
|
|
150
|
+
<View layout={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
|
|
151
|
+
<Text>Notifications</Text>
|
|
152
|
+
<Switch value={notify()} onChange={setNotify} />
|
|
153
|
+
</View>
|
|
154
|
+
<View layout={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
|
|
155
|
+
<Checkbox defaultChecked />
|
|
156
|
+
<Checkbox />
|
|
157
|
+
<RadioGroup defaultValue="a" layout={{ flexDirection: "row", gap: 12 }}>
|
|
158
|
+
<Radio value="a">Alpha</Radio>
|
|
159
|
+
<Radio value="b">Beta</Radio>
|
|
160
|
+
</RadioGroup>
|
|
161
|
+
</View>
|
|
162
|
+
<Slider value={volume()} onChange={setVolume} layout={{ width: "100%" }} />
|
|
163
|
+
<ProgressBar value={volume() / 100} />
|
|
164
|
+
<View layout={{ flexDirection: "row", gap: 8 }}>
|
|
165
|
+
<Badge>New</Badge>
|
|
166
|
+
<Badge variant="neutral">12</Badge>
|
|
167
|
+
<Badge variant="danger">Err</Badge>
|
|
168
|
+
</View>
|
|
169
|
+
<SegmentedControl options={RANGES} value={range()} onChange={setRange} />
|
|
170
|
+
<TextInput value={query()} onInput={setQuery} placeholder="Search" />
|
|
171
|
+
<Select options={RANGES} defaultValue="week" />
|
|
172
|
+
</Card>
|
|
173
|
+
<Card title="Inbox" layout={{ width: 320, gap: 0, padding: 8 }}>
|
|
174
|
+
<Item label="Compile" description="12 s" onPress={() => {}} />
|
|
175
|
+
<Item label="Link" description="1 s" onPress={() => {}} selected />
|
|
176
|
+
<Item label="Package" description="4 s" onPress={() => {}} />
|
|
177
|
+
</Card>
|
|
178
|
+
</Window>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
render(() => <App />)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"funding": "https://github.com/sponsors/wellawaretech",
|
|
6
6
|
"author": "Antoine van Wel",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"qrcode-generator": "^2.0.4"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@solidjs/signals": "2.0.0-rc.
|
|
24
|
-
"@solidrt/core": "0.0.
|
|
23
|
+
"@solidjs/signals": "2.0.0-rc.13",
|
|
24
|
+
"@solidrt/core": "0.0.65"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/badge.tsx
CHANGED
|
@@ -4,8 +4,9 @@ import { theme } from "./theme"
|
|
|
4
4
|
import { space } from "./spacing"
|
|
5
5
|
import { typeStyle, typeWeight, lightOnDark } from "./typography"
|
|
6
6
|
import { policy } from "./policy"
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
7
|
+
import { Surface } from "./surface"
|
|
8
|
+
import type { ElevationLevel, MaterialRole, StyleProps, TransitionProps } from "./types"
|
|
9
|
+
import { splitTransition, transitionEndFor } from "./types"
|
|
9
10
|
import { colorFade } from "./motion"
|
|
10
11
|
|
|
11
12
|
export type BadgeVariant = "primary" | "neutral" | "danger"
|
|
@@ -20,9 +21,12 @@ export interface BadgeProps extends TransitionProps {
|
|
|
20
21
|
style?: StyleProps
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
// A small rounded pill for counts, labels, and status.
|
|
24
|
-
// onPrimary text by default
|
|
25
|
-
//
|
|
24
|
+
// A small rounded pill for counts, labels, and status. An accent face with
|
|
25
|
+
// onPrimary text by default (the neutral variant is a surface tone), drawn
|
|
26
|
+
// by Surface, flat: a badge sits in text and in list rows, so it casts
|
|
27
|
+
// nothing unless style.elevation raises it. Override the fill via
|
|
28
|
+
// style.backgroundColor, the label color via style.color, and box it with
|
|
29
|
+
// borderColor/borderWidth.
|
|
26
30
|
export function Badge(props: BadgeProps) {
|
|
27
31
|
let colors = () => {
|
|
28
32
|
let c = theme.color
|
|
@@ -35,11 +39,14 @@ export function Badge(props: BadgeProps) {
|
|
|
35
39
|
return { bg: c.primary, fg: c.onPrimary }
|
|
36
40
|
}
|
|
37
41
|
}
|
|
42
|
+
let role = (): MaterialRole => ((props.variant ?? "primary") === "neutral" ? "surface" : "accent")
|
|
38
43
|
// Theme-level per-component overrides merged under the instance style.
|
|
39
44
|
let styled = () => ({ ...theme.components.badge, ...props.style })
|
|
40
45
|
let bg = () => styled().backgroundColor ?? colors().bg
|
|
41
46
|
let fg = () => styled().color ?? colors().fg
|
|
42
47
|
let radius = () => styled().borderRadius ?? theme.radius.full
|
|
48
|
+
let elevation = (): ElevationLevel => styled().elevation ?? "flat"
|
|
49
|
+
let hasBorder = () => styled().borderWidth != null || styled().borderColor != null
|
|
43
50
|
// Resolved once via children(): the typeof probe and the mount sites must
|
|
44
51
|
// share one build - reading the raw getter again would orphan native nodes.
|
|
45
52
|
let resolved = children(() => props.children)
|
|
@@ -66,7 +73,19 @@ export function Badge(props: BadgeProps) {
|
|
|
66
73
|
rotate={styled().rotate}
|
|
67
74
|
opacity={styled().opacity}
|
|
68
75
|
>
|
|
69
|
-
<
|
|
76
|
+
<Surface
|
|
77
|
+
role={role()}
|
|
78
|
+
elevation={elevation()}
|
|
79
|
+
fill={bg()}
|
|
80
|
+
radius={radius()}
|
|
81
|
+
material={styled().material}
|
|
82
|
+
glow={styled().glow}
|
|
83
|
+
outline={hasBorder() ? { color: styled().borderColor ?? theme.color.border, width: styled().borderWidth ?? theme.borderWidth.sm } : null}
|
|
84
|
+
fillTransition={split().background}
|
|
85
|
+
onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
86
|
+
outlineTransition={split().border}
|
|
87
|
+
onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
88
|
+
/>
|
|
70
89
|
<Show when={isText()} fallback={resolved()}>
|
|
71
90
|
<text
|
|
72
91
|
transition={colorFade()}
|
package/src/button.tsx
CHANGED
|
@@ -5,10 +5,11 @@ import { policy } from "./policy"
|
|
|
5
5
|
import { space } from "./spacing"
|
|
6
6
|
import { typeStyle, lightOnDark } from "./typography"
|
|
7
7
|
import { Spinner } from "./spinner"
|
|
8
|
+
import { Surface, surfaceSinks } from "./surface"
|
|
8
9
|
import type { LayoutProps } from "@solidrt/core"
|
|
9
|
-
import type { StyleProps, TransitionProps } from "./types"
|
|
10
|
+
import type { ElevationLevel, MaterialRole, StyleProps, TransitionProps } from "./types"
|
|
10
11
|
import { splitTransition, transitionEndFor, withTransitionDefaults } from "./types"
|
|
11
|
-
import { colorFade, scaleFeedback, pressScale
|
|
12
|
+
import { colorFade, scaleFeedback, pressScale } from "./motion"
|
|
12
13
|
|
|
13
14
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger"
|
|
14
15
|
export type ButtonSize = "sm" | "md" | "lg"
|
|
@@ -42,15 +43,21 @@ export interface ButtonProps extends TransitionProps {
|
|
|
42
43
|
// labels wider than the preset never clip.
|
|
43
44
|
const SIZE_WIDTH: Record<ButtonSize, number> = { sm: 88, md: 120, lg: 160 }
|
|
44
45
|
|
|
45
|
-
// A themed press target: a padded, centered
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
46
|
+
// A themed press target: a padded, centered box with a label, its face drawn
|
|
47
|
+
// by Surface. The primary and danger variants are accent faces, secondary
|
|
48
|
+
// and ghost control faces; a button stands raised (theme.elevation.raised,
|
|
49
|
+
// zero under the stock presets) unless ghost or disabled, or style.elevation
|
|
50
|
+
// says otherwise. Press feedback is the overlayPressed tint plus, on a face
|
|
51
|
+
// with depth (a sheen, a bevel or a height), the face sinking - its key
|
|
52
|
+
// shadow fading, its lit edge moving to the far side - and on a flat face a
|
|
53
|
+
// slight sprung scale; hover feedback is the overlayHover tint (non-touch
|
|
54
|
+
// interaction policies only), all reactive reads of the press state so no
|
|
55
|
+
// nodes are recreated. Override the box via style and the padding/sizing
|
|
56
|
+
// via layout; because hover is an overlay, it composes over a caller-set
|
|
57
|
+
// backgroundColor too. When disabled, it takes no pointer events at all and
|
|
58
|
+
// never glows. Focus (spatial nav) draws a ring under the focusRing policy
|
|
59
|
+
// in the theme's ring color; Enter/Space/remote-select activates (handled
|
|
60
|
+
// by createPress).
|
|
54
61
|
export function Button(props: ButtonProps) {
|
|
55
62
|
// Fill and label color per variant, read reactively from the theme. No
|
|
56
63
|
// variant draws a border.
|
|
@@ -67,16 +74,19 @@ export function Button(props: ButtonProps) {
|
|
|
67
74
|
return { fill: c.primary, label: c.onPrimary }
|
|
68
75
|
}
|
|
69
76
|
}
|
|
77
|
+
let variant = () => props.variant ?? "primary"
|
|
78
|
+
let role = (): MaterialRole => (variant() === "primary" || variant() === "danger" ? "accent" : "control")
|
|
70
79
|
// Theme-level per-component overrides merged under the instance style.
|
|
71
80
|
let styled = (): StyleProps => ({ ...theme.components.button, ...props.style })
|
|
72
81
|
let idleFill = () =>
|
|
73
82
|
props.disabled
|
|
74
|
-
?
|
|
83
|
+
? variant() === "ghost"
|
|
75
84
|
? "transparent"
|
|
76
85
|
: theme.color.surface
|
|
77
86
|
: colors().fill
|
|
78
87
|
let bg = () => styled().backgroundColor ?? idleFill()
|
|
79
88
|
let radius = () => styled().borderRadius ?? theme.radius.md
|
|
89
|
+
let elevation = (): ElevationLevel => styled().elevation ?? (props.disabled || variant() === "ghost" ? "flat" : "raised")
|
|
80
90
|
let label = () => (props.disabled ? theme.color.textMuted : colors().label)
|
|
81
91
|
// Resolved once via children(): reading the raw children getter builds a new
|
|
82
92
|
// subtree per read, so the typeof probe and the two mount sites below must
|
|
@@ -85,11 +95,16 @@ export function Button(props: ButtonProps) {
|
|
|
85
95
|
let isText = () => typeof resolved() === "string" || typeof resolved() === "number"
|
|
86
96
|
// The label's polarity against the idle fill: onPrimary on a saturated fill
|
|
87
97
|
// is light-on-dark even in a light theme, so it needs the low-DPI weight
|
|
88
|
-
// compensation there too.
|
|
98
|
+
// compensation there too. The base fill, not the lit gradient: the
|
|
99
|
+
// compensation reads a solid color.
|
|
89
100
|
let labelOnDark = () => lightOnDark(label(), bg())
|
|
90
101
|
|
|
91
102
|
// props (not a literal) so a swapped-in onPress is read at event time.
|
|
92
103
|
let press = createPress(props)
|
|
104
|
+
let pressed = () => !props.disabled && press.pressed()
|
|
105
|
+
let hovered = () => !props.disabled && press.hovered() && policy.interaction !== "touch"
|
|
106
|
+
// A face with depth sinks on press; a flat one shrinks instead.
|
|
107
|
+
let sinks = () => surfaceSinks(role(), elevation(), styled().material)
|
|
93
108
|
let style = () => ({
|
|
94
109
|
...styled(),
|
|
95
110
|
...(press.focused() && policy.focusRing ? { borderWidth: theme.borderWidth.focus, borderColor: theme.color.ring } : {}),
|
|
@@ -98,7 +113,7 @@ export function Button(props: ButtonProps) {
|
|
|
98
113
|
// Always a number: a scale that flips from a number back to undefined
|
|
99
114
|
// hits the transform decoder, which rejects null. Multiply so a
|
|
100
115
|
// caller-set scale is preserved under the press feedback.
|
|
101
|
-
scale: (styled().scale ?? 1) * pressScale(press.pressed()),
|
|
116
|
+
scale: (styled().scale ?? 1) * (sinks() ? 1 : pressScale(press.pressed())),
|
|
102
117
|
})
|
|
103
118
|
|
|
104
119
|
let split = () => splitTransition(props.transition)
|
|
@@ -135,11 +150,21 @@ export function Button(props: ButtonProps) {
|
|
|
135
150
|
focusable={(props.focusable ?? true) && props.disabled !== true}
|
|
136
151
|
pointerEvents={props.disabled ? "none" : undefined}
|
|
137
152
|
>
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
radius={
|
|
153
|
+
<Surface
|
|
154
|
+
role={role()}
|
|
155
|
+
elevation={elevation()}
|
|
156
|
+
fill={bg()}
|
|
157
|
+
radius={radius()}
|
|
158
|
+
material={styled().material}
|
|
159
|
+
glow={props.disabled ? null : styled().glow}
|
|
160
|
+
chrome={variant() !== "ghost" || pressed() || hovered()}
|
|
161
|
+
pressed={pressed()}
|
|
162
|
+
hovered={hovered()}
|
|
163
|
+
outline={(style().borderWidth ?? 0) > 0 ? { color: style().borderColor ?? "transparent", width: style().borderWidth! } : null}
|
|
164
|
+
fillTransition={split().background}
|
|
165
|
+
onFillTransitionEnd={transitionEndFor("background", props.onTransitionEnd)}
|
|
166
|
+
outlineTransition={split().border}
|
|
167
|
+
onOutlineTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
143
168
|
/>
|
|
144
169
|
<Show when={isText()} fallback={resolved()}>
|
|
145
170
|
<text transition={colorFade()} color={press.pending() ? withAlpha(label(), 0) : label()} {...typeStyle("body", labelOnDark())}>
|
|
@@ -151,16 +176,6 @@ export function Button(props: ButtonProps) {
|
|
|
151
176
|
<Spinner size={16} thickness={2} style={{ color: label() }} />
|
|
152
177
|
</view>
|
|
153
178
|
</Show>
|
|
154
|
-
<Show when={(style().borderWidth ?? 0) > 0}>
|
|
155
|
-
<d-rect
|
|
156
|
-
drawStyle="stroke"
|
|
157
|
-
transition={withTransitionDefaults(split().border, colorFade())}
|
|
158
|
-
onTransitionEnd={transitionEndFor("border", props.onTransitionEnd)}
|
|
159
|
-
color={style().borderColor ?? "transparent"}
|
|
160
|
-
strokeWidth={style().borderWidth}
|
|
161
|
-
radius={style().borderRadius}
|
|
162
|
-
/>
|
|
163
|
-
</Show>
|
|
164
179
|
</view>
|
|
165
180
|
)
|
|
166
181
|
}
|