@solidrt/components 0.0.49 → 0.0.51

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.
Files changed (71) hide show
  1. package/AGENTS.md +66 -85
  2. package/README.md +387 -314
  3. package/docs/badge.md +10 -0
  4. package/docs/button.md +21 -0
  5. package/docs/card.md +11 -0
  6. package/docs/checkbox.md +9 -0
  7. package/docs/context-menu.md +17 -0
  8. package/docs/density.md +13 -0
  9. package/docs/divider.md +10 -0
  10. package/docs/field.md +11 -0
  11. package/docs/focus-nav.md +18 -0
  12. package/docs/icon.md +13 -0
  13. package/docs/image.md +21 -0
  14. package/docs/index.md +13 -0
  15. package/docs/item.md +25 -0
  16. package/docs/modal.md +15 -0
  17. package/docs/nav-shell.md +18 -0
  18. package/docs/policy.md +21 -0
  19. package/docs/portal.md +15 -0
  20. package/docs/pressable.md +17 -0
  21. package/docs/progress-bar.md +10 -0
  22. package/docs/qrcode.md +10 -0
  23. package/docs/radio.md +13 -0
  24. package/docs/rich-text-document.md +5 -0
  25. package/docs/rich-text-editor.md +24 -0
  26. package/docs/safe-area.md +12 -0
  27. package/docs/scroll-view.md +14 -0
  28. package/docs/segmented-control.md +13 -0
  29. package/docs/select.md +15 -0
  30. package/docs/slider.md +9 -0
  31. package/docs/spacing.md +7 -0
  32. package/docs/spinner.md +10 -0
  33. package/docs/split-view.md +14 -0
  34. package/docs/switch.md +13 -0
  35. package/docs/text-input.md +23 -0
  36. package/docs/text.md +15 -0
  37. package/docs/theme.md +56 -0
  38. package/docs/tooltip.md +11 -0
  39. package/docs/types.md +5 -0
  40. package/docs/typography.md +5 -0
  41. package/docs/view.md +14 -0
  42. package/docs/window.md +15 -0
  43. package/package.json +6 -3
  44. package/src/badge.tsx +10 -8
  45. package/src/button.tsx +40 -28
  46. package/src/card.tsx +12 -10
  47. package/src/checkbox.tsx +21 -9
  48. package/src/context-menu.tsx +5 -3
  49. package/src/density.tsx +39 -0
  50. package/src/divider.tsx +3 -1
  51. package/src/editor-field.tsx +361 -0
  52. package/src/field.tsx +45 -0
  53. package/src/index.ts +9 -1
  54. package/src/item.tsx +116 -0
  55. package/src/nav-shell.tsx +1 -1
  56. package/src/policy.ts +0 -8
  57. package/src/press.ts +37 -8
  58. package/src/pressable.tsx +4 -1
  59. package/src/progress-bar.tsx +4 -2
  60. package/src/radio.tsx +14 -12
  61. package/src/rich-text-document.ts +247 -0
  62. package/src/rich-text-editor.tsx +149 -0
  63. package/src/segmented-control.tsx +19 -14
  64. package/src/select.tsx +37 -19
  65. package/src/slider.tsx +1 -1
  66. package/src/spacing.ts +1 -1
  67. package/src/spinner.tsx +6 -4
  68. package/src/switch.tsx +2 -1
  69. package/src/text-input.tsx +50 -262
  70. package/src/theme.ts +163 -76
  71. package/src/tooltip.tsx +7 -4
@@ -0,0 +1,5 @@
1
+ # Typography helpers
2
+
3
+ `typeStyle(variant)` resolves a theme type-scale role (`caption`/`label`/`body`/`title`/`heading`) to font props ready to spread onto a `<text>` or `d-text`: `fontSize` carries `policy.textScale`, and `fontWeight` carries the low-DPI weight compensation. Reactive when called inside a tracked scope, like any theme/policy read. `Text` applies it for you; reach for the helpers when building custom text out of core primitives.
4
+
5
+ The compensation exists because the renderer rasterizes glyphs unhinted and composites in nonlinear sRGB, which thins light-on-dark text on low-DPI displays as glyphs shrink. `typeWeight(weight, size, onDark?)` adds `policy.textWeightDelta` (0 on high-DPI displays) plus one extra step below 16px; dark-on-light text passes through untouched. `lightOnDark(text, fill)` computes the polarity for a known pair of colors (Button uses it for its fills); omitted, the theme's own palette polarity is used.
package/docs/view.md ADDED
@@ -0,0 +1,14 @@
1
+ # View
2
+
3
+ A general-purpose box. Spreads `layout` onto the underlying view, applies the transform from `style`, and draws a background and/or border when those style props are set. Takes all pointer event props.
4
+
5
+ ```jsx
6
+ import { View } from "@solidrt/components"
7
+
8
+ <View
9
+ layout={{ padding: 16, flexDirection: "column", gap: 8 }}
10
+ style={{ backgroundColor: "#222", borderRadius: 8 }}
11
+ >
12
+ {/* ... */}
13
+ </View>
14
+ ```
package/docs/window.md ADDED
@@ -0,0 +1,15 @@
1
+ # Window
2
+
3
+ The root surface of an app: renders a core `<window>`, so `render()` accepts it. Applies `layout` and `style.backgroundColor` only (a window cannot be transformed or bordered), plus `title` and `fullscreen`.
4
+
5
+ ```jsx
6
+ import { Window } from "@solidrt/components"
7
+
8
+ function App() {
9
+ return (
10
+ <Window title="My App" style={{ backgroundColor: "#111" }}>
11
+ {/* ... */}
12
+ </Window>
13
+ )
14
+ }
15
+ ```
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@solidrt/components",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
4
4
  "license": "MIT",
5
+ "funding": "https://github.com/sponsors/wellawaretech",
5
6
  "author": "Antoine van Wel",
6
7
  "type": "module",
7
8
  "main": "src/index.ts",
8
9
  "exports": {
9
- ".": "./src/index.ts"
10
+ ".": "./src/index.ts",
11
+ "./theme": "./src/theme.ts"
10
12
  },
11
13
  "files": [
12
14
  "src/",
15
+ "docs/",
13
16
  "examples/",
14
17
  "AGENTS.md"
15
18
  ],
@@ -18,6 +21,6 @@
18
21
  },
19
22
  "peerDependencies": {
20
23
  "@solidjs/signals": "2.0.0-rc.0",
21
- "@solidrt/core": "0.0.49"
24
+ "@solidrt/core": "0.0.51"
22
25
  }
23
26
  }
package/src/badge.tsx CHANGED
@@ -35,9 +35,11 @@ export function Badge(props: BadgeProps) {
35
35
  return { bg: c.primary, fg: c.onPrimary }
36
36
  }
37
37
  }
38
- let bg = () => props.style?.backgroundColor ?? colors().bg
39
- let fg = () => props.style?.color ?? colors().fg
40
- let radius = () => props.style?.borderRadius ?? RADIUS
38
+ // Theme-level per-component overrides merged under the instance style.
39
+ let styled = () => ({ ...theme.components.badge, ...props.style })
40
+ let bg = () => styled().backgroundColor ?? colors().bg
41
+ let fg = () => styled().color ?? colors().fg
42
+ let radius = () => styled().borderRadius ?? RADIUS
41
43
  // Resolved once via children(): the typeof probe and the mount sites must
42
44
  // share one build - reading the raw getter again would orphan native nodes.
43
45
  let resolved = children(() => props.children)
@@ -54,11 +56,11 @@ export function Badge(props: BadgeProps) {
54
56
  paddingTop={2}
55
57
  paddingBottom={2}
56
58
  {...props.layout}
57
- x={props.style?.x}
58
- y={props.style?.y}
59
- scale={props.style?.scale}
60
- rotate={props.style?.rotate}
61
- opacity={props.style?.opacity}
59
+ x={styled().x}
60
+ y={styled().y}
61
+ scale={styled().scale}
62
+ rotate={styled().rotate}
63
+ opacity={styled().opacity}
62
64
  >
63
65
  <d-rect color={bg()} radius={radius()} />
64
66
  <Show when={isText()} fallback={resolved()}>
package/src/button.tsx CHANGED
@@ -4,6 +4,7 @@ import { theme } from "./theme"
4
4
  import { policy } from "./policy"
5
5
  import { space } from "./spacing"
6
6
  import { typeStyle, lightOnDark } from "./typography"
7
+ import { Spinner } from "./spinner"
7
8
  import type { LayoutProps } from "@solidrt/core"
8
9
  import type { StyleProps } from "./types"
9
10
 
@@ -22,7 +23,10 @@ export interface ButtonProps {
22
23
  // stretches to the container's width (the default). Padding is the same at
23
24
  // every size.
24
25
  size?: ButtonSize
25
- onPress?: () => void
26
+ // A returned promise makes this an async action: the button shows a
27
+ // centered spinner in place of the label (geometry unchanged) and ignores
28
+ // presses until it settles. Non-thenable returns are ignored.
29
+ onPress?: () => unknown
26
30
  disabled?: boolean
27
31
  // Focus-navigation candidacy (spatial nav, TV remotes); on by default.
28
32
  // Disabled buttons are never candidates.
@@ -37,44 +41,45 @@ export interface ButtonProps {
37
41
  const SIZE_WIDTH: Record<ButtonSize, number> = { sm: 88, md: 120, lg: 160 }
38
42
 
39
43
  // A themed press target: a padded, centered, accent-colored box with a label.
40
- // Press feedback is a slight scale, hover feedback a tint (non-touch
41
- // interaction policies only), both reactive reads of the press state so no
42
- // nodes are recreated. Override the box via style and the padding/sizing via
43
- // layout. A caller-set backgroundColor disables the hover tint: we cannot know
44
- // its hover variant. When disabled, it takes no pointer events at all.
45
- // Focus (spatial nav) draws a ring under the focusRing policy, text-colored
46
- // rather than primary so it stays visible on primary-filled buttons; Enter/
47
- // Space/remote-select activates (handled by createPress).
44
+ // Press feedback is a slight scale, hover feedback the theme's overlayHover
45
+ // tint drawn over the fill (non-touch interaction policies only), both
46
+ // reactive reads of the press state so no nodes are recreated. Override the
47
+ // box via style and the padding/sizing via layout; because hover is an
48
+ // overlay, it composes over a caller-set backgroundColor too. When disabled,
49
+ // it takes no pointer events at all. Focus (spatial nav) draws a ring under
50
+ // the focusRing policy, text-colored rather than primary so it stays visible
51
+ // on primary-filled buttons; Enter/Space/remote-select activates (handled by
52
+ // createPress).
48
53
  export function Button(props: ButtonProps) {
49
- // Fill, hover fill, and label color per variant, read reactively from the
50
- // theme. No variant draws a border.
54
+ // Fill and label color per variant, read reactively from the theme. No
55
+ // variant draws a border.
51
56
  let colors = () => {
52
57
  let c = theme.color
53
58
  switch (props.variant ?? "primary") {
54
59
  case "secondary":
55
- return { fill: c.secondary, hover: c.secondaryHover, label: c.onSecondary }
60
+ return { fill: c.secondary, label: c.onSecondary }
56
61
  case "ghost":
57
- return { fill: "transparent", hover: c.surfaceHover, label: c.text }
62
+ return { fill: "transparent", label: c.text }
58
63
  case "danger":
59
- return { fill: c.danger, hover: c.dangerHover, label: c.onPrimary }
64
+ return { fill: c.danger, label: c.onPrimary }
60
65
  default:
61
- return { fill: c.primary, hover: c.primaryHover, label: c.onPrimary }
66
+ return { fill: c.primary, label: c.onPrimary }
62
67
  }
63
68
  }
69
+ // Theme-level per-component overrides merged under the instance style.
70
+ let styled = (): StyleProps => ({ ...theme.components.button, ...props.style })
64
71
  let idleFill = () =>
65
72
  props.disabled
66
73
  ? props.variant === "ghost"
67
74
  ? "transparent"
68
75
  : theme.color.surface
69
76
  : colors().fill
70
- let bg = (s: PressState) =>
71
- props.style?.backgroundColor ??
72
- (props.disabled
73
- ? idleFill()
74
- : s.hovered && policy.interaction !== "touch"
75
- ? colors().hover
76
- : colors().fill)
77
- let radius = () => props.style?.borderRadius ?? theme.radius.md
77
+ let bg = () => styled().backgroundColor ?? idleFill()
78
+ // The hover feedback: the theme's overlay tint drawn over the fill, so it
79
+ // composes with any backgroundColor (variant, theme override, or caller).
80
+ let overlay = (s: PressState) =>
81
+ s.hovered && !props.disabled && policy.interaction !== "touch" ? theme.color.overlayHover : "transparent"
82
+ let radius = () => styled().borderRadius ?? theme.radius.md
78
83
  let label = () => (props.disabled ? theme.color.textMuted : colors().label)
79
84
  // Resolved once via children(): reading the raw children getter builds a new
80
85
  // subtree per read, so the typeof probe and the two mount sites below must
@@ -84,19 +89,19 @@ export function Button(props: ButtonProps) {
84
89
  // The label's polarity against the idle fill: onPrimary on a saturated fill
85
90
  // is light-on-dark even in a light theme, so it needs the low-DPI weight
86
91
  // compensation there too.
87
- let labelOnDark = () => lightOnDark(label(), props.style?.backgroundColor ?? idleFill())
92
+ let labelOnDark = () => lightOnDark(label(), bg())
88
93
 
89
94
  // props (not a literal) so a swapped-in onPress is read at event time.
90
95
  let press = createPress(props)
91
96
  let style = () => ({
92
- ...props.style,
97
+ ...styled(),
93
98
  ...(press.focused() && policy.focusRing ? { borderWidth: 2, borderColor: theme.color.text } : {}),
94
- backgroundColor: bg(press.state()),
99
+ backgroundColor: bg(),
95
100
  borderRadius: radius(),
96
101
  // Always a number: a scale that flips from a number back to undefined
97
102
  // hits the transform decoder, which rejects null. Multiply so a
98
103
  // caller-set scale is preserved under the press feedback.
99
- scale: (props.style?.scale ?? 1) * (press.pressed() && policy.motion !== "none" ? 0.97 : 1),
104
+ scale: (styled().scale ?? 1) * (press.pressed() && policy.motion !== "none" ? 0.97 : 1),
100
105
  })
101
106
 
102
107
  return (
@@ -109,6 +114,7 @@ export function Button(props: ButtonProps) {
109
114
  flexDirection="row"
110
115
  alignItems="center"
111
116
  justifyContent="center"
117
+ position="relative"
112
118
  paddingTop={space("md")}
113
119
  paddingBottom={space("md")}
114
120
  paddingLeft={space("lg")}
@@ -125,11 +131,17 @@ export function Button(props: ButtonProps) {
125
131
  pointerEvents={props.disabled ? "none" : undefined}
126
132
  >
127
133
  <d-rect color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
134
+ <d-rect color={overlay(press.state())} radius={style().borderRadius} />
128
135
  <Show when={isText()} fallback={resolved()}>
129
- <text color={label()} {...typeStyle("body", labelOnDark())}>
136
+ <text color={press.pending() ? "transparent" : label()} {...typeStyle("body", labelOnDark())}>
130
137
  {resolved()}
131
138
  </text>
132
139
  </Show>
140
+ <Show when={press.pending()}>
141
+ <view position="absolute" top={0} bottom={0} left={0} right={0} alignItems="center" justifyContent="center">
142
+ <Spinner size={16} thickness={2} style={{ color: label() }} />
143
+ </view>
144
+ </Show>
133
145
  <Show when={(style().borderWidth ?? 0) > 0}>
134
146
  <d-rect
135
147
  drawStyle="stroke"
package/src/card.tsx CHANGED
@@ -19,9 +19,11 @@ export interface CardProps {
19
19
  // style.borderWidth or style.borderColor to draw an outline. Override any paint
20
20
  // via style, spacing/sizing via layout.
21
21
  export function Card(props: CardProps) {
22
- let bg = () => props.style?.backgroundColor ?? theme.color.surface
23
- let radius = () => props.style?.borderRadius ?? theme.radius.lg
24
- let hasBorder = () => props.style?.borderWidth != null || props.style?.borderColor != null
22
+ // Theme-level per-component overrides merged under the instance style.
23
+ let styled = () => ({ ...theme.components.card, ...props.style })
24
+ let bg = () => styled().backgroundColor ?? theme.color.surface
25
+ let radius = () => styled().borderRadius ?? theme.radius.lg
26
+ let hasBorder = () => styled().borderWidth != null || styled().borderColor != null
25
27
 
26
28
  return (
27
29
  <view
@@ -31,11 +33,11 @@ export function Card(props: CardProps) {
31
33
  gap={space("lg")}
32
34
  padding={space("xl")}
33
35
  {...props.layout}
34
- x={props.style?.x}
35
- y={props.style?.y}
36
- scale={props.style?.scale}
37
- rotate={props.style?.rotate}
38
- opacity={props.style?.opacity}
36
+ x={styled().x}
37
+ y={styled().y}
38
+ scale={styled().scale}
39
+ rotate={styled().rotate}
40
+ opacity={styled().opacity}
39
41
  >
40
42
  <d-rect color={bg()} radius={radius()} />
41
43
  <Show when={props.title != null}>
@@ -47,8 +49,8 @@ export function Card(props: CardProps) {
47
49
  <Show when={hasBorder()}>
48
50
  <d-rect
49
51
  drawStyle="stroke"
50
- color={props.style?.borderColor ?? theme.color.border}
51
- strokeWidth={props.style?.borderWidth ?? theme.borderWidth.sm}
52
+ color={styled().borderColor ?? theme.color.border}
53
+ strokeWidth={styled().borderWidth ?? theme.borderWidth.sm}
52
54
  radius={radius()}
53
55
  />
54
56
  </Show>
package/src/checkbox.tsx CHANGED
@@ -2,7 +2,8 @@ import { createSignal, Show } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
3
  import { createPress } from "./press"
4
4
  import { theme } from "./theme"
5
- import { densityScale } from "./policy"
5
+ import { densityScale } from "./density"
6
+ import { Icon } from "./icon"
6
7
  import type { StyleProps } from "./types"
7
8
 
8
9
  export interface CheckboxProps {
@@ -43,6 +44,7 @@ export function Checkbox(props: CheckboxProps) {
43
44
  borderColor: theme.color.border,
44
45
  borderWidth: theme.borderWidth.sm,
45
46
  borderRadius: theme.radius.sm,
47
+ ...theme.components.checkbox,
46
48
  ...props.style,
47
49
  })
48
50
 
@@ -52,6 +54,7 @@ export function Checkbox(props: CheckboxProps) {
52
54
  repaintBoundary
53
55
  width={size()}
54
56
  height={size()}
57
+ position="relative"
55
58
  {...props.layout}
56
59
  x={style().x}
57
60
  y={style().y}
@@ -63,14 +66,23 @@ export function Checkbox(props: CheckboxProps) {
63
66
  >
64
67
  <d-rect color={style().backgroundColor ?? "transparent"} radius={style().borderRadius} />
65
68
  <Show when={checked()}>
66
- <d-path
67
- d={check()}
68
- drawStyle="stroke"
69
- color={theme.color.onPrimary}
70
- strokeWidth={2}
71
- strokeCap="round"
72
- strokeJoin="round"
73
- />
69
+ <Show
70
+ when={theme.icons.check}
71
+ fallback={
72
+ <d-path
73
+ d={check()}
74
+ drawStyle="stroke"
75
+ color={theme.color.onPrimary}
76
+ strokeWidth={2}
77
+ strokeCap="round"
78
+ strokeJoin="round"
79
+ />
80
+ }
81
+ >
82
+ <view position="absolute" top={0} bottom={0} left={0} right={0} alignItems="center" justifyContent="center">
83
+ <Icon src={theme.icons.check!} size={Math.round(size() * 0.75)} color={theme.color.onPrimary} />
84
+ </view>
85
+ </Show>
74
86
  </Show>
75
87
  <Show when={(style().borderWidth ?? 0) > 0}>
76
88
  <d-rect
@@ -91,9 +91,11 @@ export function ContextMenu(props: ContextMenuProps) {
91
91
  >
92
92
  <d-rect
93
93
  color={
94
- press.pressed() || (press.hovered() && policy.interaction !== "touch")
95
- ? theme.color.surfaceHover
96
- : "transparent"
94
+ press.pressed()
95
+ ? theme.color.overlayPressed
96
+ : press.hovered() && policy.interaction !== "touch"
97
+ ? theme.color.overlayHover
98
+ : "transparent"
97
99
  }
98
100
  />
99
101
  <text {...bodyText(p.item.disabled ? theme.color.textMuted : theme.color.text)}>{p.item.label}</text>
@@ -0,0 +1,39 @@
1
+ import { createContext, useContext } from "@solidrt/core"
2
+ import { policy, type DensityPolicy } from "./policy"
3
+
4
+ // Subtree density: a region (a toolbar, a table, a sidebar) tightens its
5
+ // controls wholesale instead of per-child props. The context holds an
6
+ // accessor so a reactive `value` stays live for consumers.
7
+ let DensityContext = createContext<() => DensityPolicy | undefined>(() => undefined)
8
+
9
+ export interface DensityProps {
10
+ value: DensityPolicy
11
+ children?: any
12
+ }
13
+
14
+ /**
15
+ * Overrides the density policy for its subtree: every density-scaled metric
16
+ * below - `space()`, control sizes, `Item` row paddings - resolves this value
17
+ * instead of the global `policy.density`. Nests; the nearest wins.
18
+ *
19
+ * <Density value="compact">
20
+ * <Item label="..." /> // compact rows, no per-item props
21
+ * </Density>
22
+ */
23
+ export function Density(props: DensityProps) {
24
+ return <DensityContext value={() => props.value}>{props.children}</DensityContext>
25
+ }
26
+
27
+ // How density maps to component metrics: a multiplier on control sizes,
28
+ // paddings, and hit targets. Comfortable is the components' designed size.
29
+ const DENSITY_SCALE: Record<DensityPolicy, number> = { comfortable: 1, compact: 0.85, dense: 0.7 }
30
+
31
+ /**
32
+ * Reactive density multiplier for control metrics: the nearest <Density>
33
+ * above the calling scope, falling back to the global `policy.density`.
34
+ * Call it during component setup or inside JSX/thunks (an owner is needed to
35
+ * see a <Density> region; without one it reads the global policy).
36
+ */
37
+ export function densityScale(): number {
38
+ return DENSITY_SCALE[useContext(DensityContext)() ?? policy.density]
39
+ }
package/src/divider.tsx CHANGED
@@ -18,7 +18,9 @@ export interface DividerProps {
18
18
  export function Divider(props: DividerProps) {
19
19
  let vertical = () => props.orientation === "vertical"
20
20
  let thickness = () => props.thickness ?? 1
21
- let color = () => props.style?.backgroundColor ?? theme.color.border
21
+ // Theme-level per-component overrides merged under the instance style.
22
+ let styled = () => ({ ...theme.components.divider, ...props.style })
23
+ let color = () => styled().backgroundColor ?? theme.color.border
22
24
 
23
25
  return (
24
26
  <view