@solidrt/components 0.0.18 → 0.0.19

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 CHANGED
@@ -43,10 +43,41 @@ Most components group props into two objects, plus top-level event handlers:
43
43
  hovered }`. No pointer capture: a drag out cancels via onPointerLeave.
44
44
  - `Button` - themed Pressable: accent box + label (string/number child), scales
45
45
  on press (via reactive style); colors from `theme.color.primary`/`onPrimary`.
46
+ - `Switch` - on/off toggle; `value`/`onChange`, controlled or uncontrolled
47
+ (`defaultValue`). Track fills `primary`/`surfaceAlt`, thumb slides.
48
+ - `Checkbox` - `checked`/`onChange` (or `defaultChecked`); checkmark drawn via a
49
+ core `<d-path>` when checked.
50
+ - `RadioGroup` + `Radio` - single-selection pair; the group owns
51
+ `value`/`onChange` (or `defaultValue`) and shares it to `Radio` children via a
52
+ module-local context (not a cross-component dependency). `Radio value=...`;
53
+ string/number child renders as a themed label.
54
+ - `Slider` - horizontal; `value`/`onChange` (or `defaultValue`), `min`/`max`/
55
+ `step`. Pointer x mapped to value via `getBoundingBox`; uses core
56
+ `setPointerCapture` so a drag keeps tracking off the track (within the window).
57
+ - `Card` - themed surface container: padded column box, `surface` fill, `border`
58
+ stroke, rounded. Optional `title` heading; override paint via `style`.
59
+ - `Divider` - thin rule in `border` color; `orientation` (default horizontal),
60
+ `thickness`. Stretches across the cross axis (full width in a column).
61
+ - `Badge` - small rounded pill for counts/labels/status; `primary`/`onPrimary`
62
+ by default, override via `style.backgroundColor`/`style.color`.
63
+ - `Spinner` - indeterminate rotating arc, driven by core `onFrame`; `size`,
64
+ `thickness`, `speed` (rev/s). Color from `primary`, override via `style.color`.
65
+ - `ProgressBar` - horizontal; determinate with `value` in [0,1] (fill grows from
66
+ the left), indeterminate when `value` is undefined (segment slides, `onFrame`).
67
+ Track `surfaceAlt` / fill `primary`; override via `style.backgroundColor`/`color`.
68
+ - `QrCode` - QR for `data: string`, built from primitives (same-color row runs
69
+ merged into one box on a light panel); grid memoized on `data`/`level`.
70
+ `moduleSize`/`margin`/`radius`/`level` (L/M/Q/H). Paints black-on-white by
71
+ default (NOT the theme) to stay scannable; override `color`/`background` only
72
+ if contrast holds. Deps on `qrcode-generator`.
46
73
  - `SafeArea` - pads children clear of system UI (notches, status bars); top and
47
74
  bottom on by default, pass `false`/a number per edge.
48
- - `theme` / `setTheme` - shared appearance (colors, spacing, radii, font sizes);
49
- call `setTheme({...})` to override defaults.
75
+ - `theme` / `setTheme` / `darkTheme` / `lightTheme` - shared REACTIVE appearance
76
+ (backed by a Solid store, so reads are tracked and switching recolors the live
77
+ UI). `setTheme(lightTheme)`/`setTheme(darkTheme)` switch presets;
78
+ `setTheme({...})` merges a one-level-deep override. Default is dark. Color
79
+ tokens: `background`, `surface`, `surfaceAlt`, `text`, `textMuted`, `border`,
80
+ `primary`, `onPrimary`, `danger`, `scrim`.
50
81
 
51
82
  ## Minimal app (verified to render)
52
83
 
package/README.md CHANGED
@@ -12,7 +12,21 @@ bun add @solidrt/components
12
12
 
13
13
  ## Theming
14
14
 
15
- Appearance (colors, spacing, border, font size) is controlled via the shared theme. Call `setTheme` from `@solidrt/components` to override defaults.
15
+ Appearance (colors, spacing, border, font size) is controlled via a shared, reactive theme. Reads are tracked, so switching the theme at runtime recolors the live UI without remounting.
16
+
17
+ Two presets ship out of the box, `darkTheme` and `lightTheme`. The default is dark. Switch by passing a full preset, or apply a targeted override with a partial:
18
+
19
+ ```jsx
20
+ import { setTheme, darkTheme, lightTheme } from "@solidrt/components"
21
+
22
+ setTheme(lightTheme) // switch to light
23
+ setTheme(darkTheme) // switch to dark
24
+ setTheme({ color: { primary: "#ff2d55" } }) // override one token
25
+ ```
26
+
27
+ `setTheme` merges one level deep per category, so a partial only touches the keys you pass.
28
+
29
+ The color tokens are `background` (window fill), `surface` (control/card fill), `surfaceAlt` (subtle raised/track fill), `text`, `textMuted`, `border`, `primary`, `onPrimary`, `danger`, and `scrim` (modal dim). Non-color tokens are `spacing`, `radius`, `borderWidth`, and `text` (font sizes), shared across presets.
16
30
 
17
31
  ## Layout and style
18
32
 
@@ -293,6 +307,235 @@ import { Button } from "@solidrt/components"
293
307
  | `style` | `StyleProps` | Overrides background, radius, etc. |
294
308
  | `children` | `any` | Label text, or custom content. |
295
309
 
310
+ ### Switch
311
+
312
+ An on/off toggle. The track fills with `primary` when on and `surfaceAlt` when off; the thumb slides across. Controlled via `value`/`onChange`, or uncontrolled via `defaultValue`. Built on `Pressable`, so `disabled` takes no pointer events.
313
+
314
+ ```jsx
315
+ import { Switch } from "@solidrt/components"
316
+ import { createSignal } from "@solidjs/signals"
317
+
318
+ function NotifyToggle() {
319
+ let [on, setOn] = createSignal(true)
320
+ return <Switch value={on()} onChange={setOn} />
321
+ }
322
+ ```
323
+
324
+ **Props**
325
+
326
+ | Prop | Type | Default | Description |
327
+ | -------------- | ---------------------------- | ------- | ----------------------------------------------- |
328
+ | `value` | `boolean` | - | Controlled state. Omit for uncontrolled. |
329
+ | `defaultValue` | `boolean` | `false` | Initial value for uncontrolled use. |
330
+ | `onChange` | `(value: boolean) => void` | - | Fires with the new value on toggle. |
331
+ | `disabled` | `boolean` | `false` | Takes no pointer events when true. |
332
+ | `layout` | `LayoutProps` | - | Overrides sizing/positioning of the track. |
333
+ | `style` | `StyleProps` | - | Overrides track colors and radius. |
334
+
335
+ ### Checkbox
336
+
337
+ A checkbox. When checked it fills with `primary` and draws a checkmark; otherwise it is an empty bordered box. Controlled via `checked`/`onChange`, or uncontrolled via `defaultChecked`.
338
+
339
+ ```jsx
340
+ import { Checkbox } from "@solidrt/components"
341
+
342
+ <Checkbox checked={agree()} onChange={setAgree} />
343
+ ```
344
+
345
+ **Props**
346
+
347
+ | Prop | Type | Default | Description |
348
+ | ---------------- | --------------------------- | ------- | ------------------------------------------ |
349
+ | `checked` | `boolean` | - | Controlled state. Omit for uncontrolled. |
350
+ | `defaultChecked` | `boolean` | `false` | Initial value for uncontrolled use. |
351
+ | `onChange` | `(checked: boolean) => void`| - | Fires with the new state on toggle. |
352
+ | `disabled` | `boolean` | `false` | Takes no pointer events when true. |
353
+ | `layout` | `LayoutProps` | - | Overrides sizing. |
354
+ | `style` | `StyleProps` | - | Overrides box colors, border, and radius. |
355
+
356
+ ### RadioGroup / Radio
357
+
358
+ A single-selection group. `RadioGroup` owns the selected value and shares it with its `Radio` children; each `Radio` is a ring with an inner dot when selected. A string/number child of `Radio` renders as a themed label beside the ring. Controlled via `value`/`onChange` on the group, or uncontrolled via `defaultValue`.
359
+
360
+ ```jsx
361
+ import { RadioGroup, Radio } from "@solidrt/components"
362
+
363
+ <RadioGroup value={plan()} onChange={setPlan}>
364
+ <Radio value="free">Free</Radio>
365
+ <Radio value="pro">Pro</Radio>
366
+ <Radio value="team">Team</Radio>
367
+ </RadioGroup>
368
+ ```
369
+
370
+ **RadioGroup props**
371
+
372
+ | Prop | Type | Default | Description |
373
+ | -------------- | --------------------------- | ------- | ---------------------------------------- |
374
+ | `value` | `unknown` | - | Controlled selected value. |
375
+ | `defaultValue` | `unknown` | - | Initial selection for uncontrolled use. |
376
+ | `onChange` | `(value: unknown) => void` | - | Fires with the newly selected value. |
377
+ | `disabled` | `boolean` | `false` | Disables every `Radio` in the group. |
378
+ | `layout` | `LayoutProps` | - | Layout of the group container. |
379
+ | `children` | `any` | - | `Radio` elements. |
380
+
381
+ **Radio props**
382
+
383
+ | Prop | Type | Description |
384
+ | ---------- | ------------- | --------------------------------------------------- |
385
+ | `value` | `unknown` | This option's value; selecting it sets the group. |
386
+ | `disabled` | `boolean` | Disables this option (also disabled by the group). |
387
+ | `layout` | `LayoutProps` | Layout of the option row. |
388
+ | `style` | `StyleProps` | Paint properties of the option row. |
389
+ | `children` | `any` | A string/number label, or custom content. |
390
+
391
+ ### Slider
392
+
393
+ A horizontal slider. The groove fills up to the thumb; pressing or dragging the track sets the value from the pointer position. Controlled via `value`/`onChange`, or uncontrolled via `defaultValue`. The drag uses pointer capture, so it keeps tracking when the pointer drifts off the track (anywhere within the window).
394
+
395
+ ```jsx
396
+ import { Slider } from "@solidrt/components"
397
+
398
+ <Slider value={volume()} onChange={setVolume} min={0} max={100} step={1} layout={{ width: 180 }} />
399
+ ```
400
+
401
+ **Props**
402
+
403
+ | Prop | Type | Default | Description |
404
+ | -------------- | -------------------------- | ------- | -------------------------------------------- |
405
+ | `value` | `number` | - | Controlled value. Omit for uncontrolled. |
406
+ | `defaultValue` | `number` | `min` | Initial value for uncontrolled use. |
407
+ | `min` | `number` | `0` | Lower bound. |
408
+ | `max` | `number` | `100` | Upper bound. |
409
+ | `step` | `number` | - | Snap increment. Omit for continuous. |
410
+ | `onChange` | `(value: number) => void` | - | Fires with the new value while dragging. |
411
+ | `disabled` | `boolean` | `false` | Takes no pointer events when true. |
412
+ | `layout` | `LayoutProps` | - | Layout of the track (e.g. `width`). |
413
+ | `style` | `StyleProps` | - | Transform only (`x`/`y`/`rotate`/`scale`). |
414
+
415
+ ### Card
416
+
417
+ A themed surface container: a padded column box with a `surface` fill, a subtle `border` stroke, and rounded corners. All colors come from the theme, so it recolors live on a theme switch. Pass a `title` for a heading, or lay out the content yourself. Override any paint via `style`, spacing/sizing via `layout`.
418
+
419
+ ```jsx
420
+ import { Card } from "@solidrt/components"
421
+
422
+ <Card title="Profile" layout={{ width: 360 }}>
423
+ <Text>Card body content.</Text>
424
+ </Card>
425
+ ```
426
+
427
+ **Props**
428
+
429
+ | Prop | Type | Default | Description |
430
+ | ---------- | ------------- | --------- | ---------------------------------------------------- |
431
+ | `title` | `string` | - | Optional heading rendered above the content. |
432
+ | `children` | `any` | - | Card content. |
433
+ | `layout` | `LayoutProps` | - | Box layout (e.g. `width`, `gap`, `padding`). |
434
+ | `style` | `StyleProps` | - | Paint overrides: `backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`, transform. |
435
+
436
+ ### Divider
437
+
438
+ A thin rule in the theme `border` color. It stretches across its container on the cross axis: full width inside a column, full height inside a row (pass `orientation="vertical"`). Add spacing with `layout` margins, and override the color via `style.backgroundColor`.
439
+
440
+ ```jsx
441
+ import { Divider } from "@solidrt/components"
442
+
443
+ <Divider />
444
+ <Divider orientation="vertical" />
445
+ ```
446
+
447
+ **Props**
448
+
449
+ | Prop | Type | Default | Description |
450
+ | ------------- | ----------------------------- | -------------- | -------------------------------------- |
451
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Rule direction. |
452
+ | `thickness` | `number` | `1` | Line thickness in pixels. |
453
+ | `layout` | `LayoutProps` | - | Layout (e.g. margins for spacing). |
454
+ | `style` | `StyleProps` | - | `backgroundColor` overrides the color. |
455
+
456
+ ### Badge
457
+
458
+ A small rounded pill for counts, labels, and status. Accent `primary` fill with `onPrimary` text by default; a string/number child is rendered as the themed label, anything else as-is. Override the fill via `style.backgroundColor` and the label color via `style.color`.
459
+
460
+ ```jsx
461
+ import { Badge } from "@solidrt/components"
462
+
463
+ <Badge>New</Badge>
464
+ <Badge style={{ backgroundColor: theme.color.danger }}>Error</Badge>
465
+ ```
466
+
467
+ **Props**
468
+
469
+ | Prop | Type | Default | Description |
470
+ | ---------- | ------------- | ------- | -------------------------------------------------- |
471
+ | `children` | `any` | - | String/number renders as the label; else as-is. |
472
+ | `layout` | `LayoutProps` | - | Box layout (e.g. padding overrides). |
473
+ | `style` | `StyleProps` | - | `backgroundColor` (fill), `color` (label), transform. |
474
+
475
+ ### Spinner
476
+
477
+ An indeterminate spinner: a 270-degree arc that rotates continuously. It is driven by core `onFrame`, so it participates in demand-driven rendering and stops when unmounted. Color comes from the theme `primary`; override via `style.color`.
478
+
479
+ ```jsx
480
+ import { Spinner } from "@solidrt/components"
481
+
482
+ <Spinner />
483
+ <Spinner size={32} thickness={4} speed={1.5} />
484
+ ```
485
+
486
+ **Props**
487
+
488
+ | Prop | Type | Default | Description |
489
+ | ----------- | ------------- | ------- | ---------------------------------- |
490
+ | `size` | `number` | `24` | Overall diameter in pixels. |
491
+ | `thickness` | `number` | `3` | Arc stroke width in pixels. |
492
+ | `speed` | `number` | `1` | Revolutions per second. |
493
+ | `layout` | `LayoutProps` | - | Layout of the box. |
494
+ | `style` | `StyleProps` | - | `color` sets the arc; plus transform. |
495
+
496
+ ### ProgressBar
497
+
498
+ A horizontal progress bar. Determinate when given a `value` in `[0, 1]` (the fill grows from the left); indeterminate when `value` is undefined (a short segment slides back and forth, driven by core `onFrame`). Track and fill colors come from the theme; override the track via `style.backgroundColor` and the fill via `style.color`.
499
+
500
+ ```jsx
501
+ import { ProgressBar } from "@solidrt/components"
502
+
503
+ <ProgressBar value={0.4} /> // determinate
504
+ <ProgressBar /> // indeterminate
505
+ ```
506
+
507
+ **Props**
508
+
509
+ | Prop | Type | Default | Description |
510
+ | -------- | ------------- | ------- | --------------------------------------------------- |
511
+ | `value` | `number` | - | Progress in `[0, 1]`. Omit for an indeterminate bar. |
512
+ | `layout` | `LayoutProps` | - | Layout (e.g. `width`, `height`). |
513
+ | `style` | `StyleProps` | - | `backgroundColor` (track), `color` (fill). |
514
+
515
+ ### QrCode
516
+
517
+ Renders a QR code for `data` out of primitives: same-color modules in a row collapse into one box, drawn on a light quiet-zone panel. The grid recomputes only when `data` or `level` changes. It paints black on white by default (not the theme) so it stays scannable through a theme switch; override `color`/`background` only if the contrast still holds.
518
+
519
+ ```jsx
520
+ import { QrCode } from "@solidrt/components"
521
+
522
+ <QrCode data="https://solidjs.com" />
523
+ <QrCode data={ticket()} moduleSize={8} level="L" />
524
+ ```
525
+
526
+ **Props**
527
+
528
+ | Prop | Type | Default | Description |
529
+ | ------------ | -------------------------- | -------------- | -------------------------------------------------------------- |
530
+ | `data` | `string` | - | The string to encode (URL, pairing ticket, text, ...). |
531
+ | `moduleSize` | `number` | `6` | Pixels per module (the smallest square). |
532
+ | `margin` | `number` | `16` | Quiet-zone padding in pixels around the grid; keep non-zero. |
533
+ | `color` | `string` | `"#000000"` | Dark-module color. |
534
+ | `background` | `string` | `"#ffffff"` | Panel/light-module color. |
535
+ | `level` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | Error-correction level; higher tolerates more damage but caps data length sooner. |
536
+ | `radius` | `number` | `8` | Corner radius of the background panel. |
537
+ | `layout` | `LayoutProps` | - | Layout of the outer box. |
538
+
296
539
  ## License
297
540
 
298
541
  MIT. Copyright (c) 2026 Antoine van Wel.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/components",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -12,8 +12,11 @@
12
12
  "src/",
13
13
  "AGENTS.md"
14
14
  ],
15
+ "dependencies": {
16
+ "qrcode-generator": "^2.0.4"
17
+ },
15
18
  "peerDependencies": {
16
19
  "@solidjs/signals": "2.0.0-beta.15",
17
- "@solidrt/core": "0.0.18"
20
+ "@solidrt/core": "0.0.19"
18
21
  }
19
22
  }
package/src/badge.tsx ADDED
@@ -0,0 +1,50 @@
1
+ import { Show } from "@solidrt/core"
2
+ import type { LayoutProps } from "@solidrt/core"
3
+ import { theme } from "./theme"
4
+ import type { StyleProps } from "./types"
5
+
6
+ export interface BadgeProps {
7
+ // A string/number renders as the themed pill label; anything else is rendered
8
+ // as-is (an icon, a dot, ...).
9
+ children?: any
10
+ layout?: LayoutProps
11
+ style?: StyleProps
12
+ }
13
+
14
+ // A rounded radius large enough to fully pill any typical badge height; the
15
+ // renderer clamps it to half the box, so both ends stay round.
16
+ const RADIUS = 999
17
+
18
+ // A small rounded pill for counts, labels, and status. Accent fill with
19
+ // onPrimary text by default; override the fill via style.backgroundColor and the
20
+ // label color via style.color.
21
+ export function Badge(props: BadgeProps) {
22
+ let bg = () => props.style?.backgroundColor ?? theme.color.primary
23
+ let fg = () => props.style?.color ?? theme.color.onPrimary
24
+ let radius = () => props.style?.borderRadius ?? RADIUS
25
+ let isText = () => typeof props.children === "string" || typeof props.children === "number"
26
+
27
+ return (
28
+ <view
29
+ flexDirection="row"
30
+ alignItems="center"
31
+ justifyContent="center"
32
+ paddingLeft={8}
33
+ paddingRight={8}
34
+ paddingTop={2}
35
+ paddingBottom={2}
36
+ {...props.layout}
37
+ x={props.style?.x}
38
+ y={props.style?.y}
39
+ scale={props.style?.scale}
40
+ rotate={props.style?.rotate}
41
+ >
42
+ <d-rect color={bg()} radius={radius()} />
43
+ <Show when={isText()} fallback={props.children}>
44
+ <text color={fg()} fontSize={12} fontWeight={600}>
45
+ {props.children}
46
+ </text>
47
+ </Show>
48
+ </view>
49
+ )
50
+ }
package/src/button.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Show } from "@solidrt/core"
2
2
  import { Pressable, type PressState } from "./pressable"
3
3
  import { theme } from "./theme"
4
+ import { policy, densityScale } from "./policy"
4
5
  import type { LayoutProps } from "@solidrt/core"
5
6
  import type { StyleProps } from "./types"
6
7
 
@@ -15,11 +16,19 @@ export interface ButtonProps {
15
16
  }
16
17
 
17
18
  // Themed convenience over Pressable: a padded, centered, accent-colored box with
18
- // a label. Press feedback is a slight scale, driven through Pressable's reactive
19
- // style so no nodes are recreated on press. Override the box via style and the
20
- // padding/sizing via layout.
19
+ // a label. Press feedback is a slight scale, hover feedback a tint (non-touch
20
+ // interaction policies only), both driven through Pressable's reactive style so
21
+ // no nodes are recreated. Override the box via style and the padding/sizing via
22
+ // layout. A caller-set backgroundColor disables the hover tint: we cannot know
23
+ // its hover variant.
21
24
  export function Button(props: ButtonProps) {
22
- let bg = () => props.style?.backgroundColor ?? (props.disabled ? theme.color.surface : theme.color.primary)
25
+ let bg = (s: PressState) =>
26
+ props.style?.backgroundColor ??
27
+ (props.disabled
28
+ ? theme.color.surface
29
+ : s.hovered && policy.interaction !== "touch"
30
+ ? theme.color.primaryHover
31
+ : theme.color.primary)
23
32
  let radius = () => props.style?.borderRadius ?? theme.radius.sm
24
33
  let label = () => (props.disabled ? theme.color.textMuted : theme.color.onPrimary)
25
34
  let isText = () => typeof props.children === "string" || typeof props.children === "number"
@@ -32,20 +41,20 @@ export function Button(props: ButtonProps) {
32
41
  flexDirection: "row",
33
42
  alignItems: "center",
34
43
  justifyContent: "center",
35
- paddingTop: theme.spacing.sm,
36
- paddingBottom: theme.spacing.sm,
37
- paddingLeft: theme.spacing.md,
38
- paddingRight: theme.spacing.md,
44
+ paddingTop: Math.round(theme.spacing.sm * densityScale()),
45
+ paddingBottom: Math.round(theme.spacing.sm * densityScale()),
46
+ paddingLeft: Math.round(theme.spacing.md * densityScale()),
47
+ paddingRight: Math.round(theme.spacing.md * densityScale()),
39
48
  ...props.layout,
40
49
  }}
41
50
  style={(s: PressState) => ({
42
51
  ...props.style,
43
- backgroundColor: bg(),
52
+ backgroundColor: bg(s),
44
53
  borderRadius: radius(),
45
54
  // Always a number: a scale that flips from a number back to undefined
46
55
  // hits the transform decoder, which rejects null. Multiply so a
47
56
  // caller-set scale is preserved under the press feedback.
48
- scale: (props.style?.scale ?? 1) * (s.pressed ? 0.97 : 1),
57
+ scale: (props.style?.scale ?? 1) * (s.pressed && policy.motion !== "none" ? 0.97 : 1),
49
58
  })}
50
59
  >
51
60
  <Show when={isText()} fallback={props.children}>
package/src/card.tsx ADDED
@@ -0,0 +1,48 @@
1
+ import { Show } from "@solidrt/core"
2
+ import type { LayoutProps } from "@solidrt/core"
3
+ import { theme } from "./theme"
4
+ import type { StyleProps } from "./types"
5
+
6
+ export interface CardProps {
7
+ children?: any
8
+ // Optional heading rendered above the content.
9
+ title?: string
10
+ ref?: (node: { id: number }) => void
11
+ layout?: LayoutProps
12
+ style?: StyleProps
13
+ }
14
+
15
+ const RADIUS = 12
16
+
17
+ // A themed surface container: a padded column box with a subtle border and
18
+ // rounded corners, reading its colors from the theme so it recolors live.
19
+ // Override any paint via style, spacing/sizing via layout.
20
+ export function Card(props: CardProps) {
21
+ let bg = () => props.style?.backgroundColor ?? theme.color.surface
22
+ let border = () => props.style?.borderColor ?? theme.color.border
23
+ let width = () => props.style?.borderWidth ?? theme.borderWidth.sm
24
+ let radius = () => props.style?.borderRadius ?? RADIUS
25
+
26
+ return (
27
+ <view
28
+ ref={props.ref}
29
+ flexDirection="column"
30
+ gap={16}
31
+ padding={20}
32
+ {...props.layout}
33
+ x={props.style?.x}
34
+ y={props.style?.y}
35
+ scale={props.style?.scale}
36
+ rotate={props.style?.rotate}
37
+ >
38
+ <d-rect color={bg()} radius={radius()} />
39
+ <Show when={props.title != null}>
40
+ <text color={theme.color.text} fontSize={18} fontWeight={700}>
41
+ {props.title}
42
+ </text>
43
+ </Show>
44
+ {props.children}
45
+ <d-rect drawStyle="stroke" color={border()} strokeWidth={width()} radius={radius()} />
46
+ </view>
47
+ )
48
+ }
@@ -0,0 +1,65 @@
1
+ import { createSignal, Show } from "@solidrt/core"
2
+ import type { LayoutProps } from "@solidrt/core"
3
+ import { Pressable } from "./pressable"
4
+ import { theme } from "./theme"
5
+ import { densityScale } from "./policy"
6
+ import type { StyleProps } from "./types"
7
+
8
+ export interface CheckboxProps {
9
+ // Controlled checked state. If omitted, the checkbox is uncontrolled.
10
+ checked?: boolean
11
+ defaultChecked?: boolean
12
+ onChange?: (checked: boolean) => void
13
+ disabled?: boolean
14
+ layout?: LayoutProps
15
+ style?: StyleProps
16
+ }
17
+
18
+ const SIZE = 20
19
+
20
+ // A checkbox. When checked, fills with primary and draws a checkmark; otherwise
21
+ // shows an empty bordered box. Controlled via checked/onChange, or uncontrolled
22
+ // via defaultChecked. Built on Pressable.
23
+ export function Checkbox(props: CheckboxProps) {
24
+ let [internal, setInternal] = createSignal(props.defaultChecked ?? false)
25
+ let checked = () => (props.checked !== undefined ? props.checked : internal())
26
+
27
+ let toggle = () => {
28
+ let next = !checked()
29
+ if (props.checked === undefined) setInternal(next)
30
+ props.onChange?.(next)
31
+ }
32
+
33
+ let size = () => Math.round(SIZE * densityScale())
34
+ // The checkmark in box-relative fractions, so it scales with the density.
35
+ let check = () => {
36
+ let s = size()
37
+ return `M ${0.25 * s} ${0.5 * s} L ${0.45 * s} ${0.7 * s} L ${0.75 * s} ${0.3 * s}`
38
+ }
39
+
40
+ return (
41
+ <Pressable
42
+ onPress={toggle}
43
+ disabled={props.disabled}
44
+ layout={{ width: size(), height: size(), ...props.layout }}
45
+ style={{
46
+ backgroundColor: checked() ? theme.color.primary : theme.color.surface,
47
+ borderColor: theme.color.border,
48
+ borderWidth: theme.borderWidth.sm,
49
+ borderRadius: theme.radius.sm,
50
+ ...props.style,
51
+ }}
52
+ >
53
+ <Show when={checked()}>
54
+ <d-path
55
+ d={check()}
56
+ drawStyle="stroke"
57
+ color={theme.color.onPrimary}
58
+ strokeWidth={2}
59
+ strokeCap="round"
60
+ strokeJoin="round"
61
+ />
62
+ </Show>
63
+ </Pressable>
64
+ )
65
+ }