@solidrt/components 0.0.19 → 0.0.20
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 +7 -0
- package/README.md +23 -0
- package/package.json +2 -2
- package/src/icon.tsx +35 -0
- package/src/index.ts +1 -0
- package/src/qrcode.tsx +26 -20
- package/src/text-input.tsx +31 -35
package/AGENTS.md
CHANGED
|
@@ -70,6 +70,13 @@ Most components group props into two objects, plus top-level event handlers:
|
|
|
70
70
|
`moduleSize`/`margin`/`radius`/`level` (L/M/Q/H). Paints black-on-white by
|
|
71
71
|
default (NOT the theme) to stay scannable; override `color`/`background` only
|
|
72
72
|
if contrast holds. Deps on `qrcode-generator`.
|
|
73
|
+
- `Icon` - thin themed wrapper over the core `<svg>` document primitive. `src`
|
|
74
|
+
is an SVG string (an imported `.svg` asset, a `lucide-static` string export, or
|
|
75
|
+
an inline literal); `size` sets a square box (default 24); `color` drives
|
|
76
|
+
`currentColor` (default `theme.color.text`). Carries no icon set of its own and
|
|
77
|
+
no icon-name registry: pass the SVG string in, so any currentColor set (Lucide,
|
|
78
|
+
Feather, Heroicons) works and only used icons are bundled. Multi-color
|
|
79
|
+
documents keep their own fills. Reach for `<svg>` directly for a non-square box.
|
|
73
80
|
- `SafeArea` - pads children clear of system UI (notches, status bars); top and
|
|
74
81
|
bottom on by default, pass `false`/a number per edge.
|
|
75
82
|
- `theme` / `setTheme` / `darkTheme` / `lightTheme` - shared REACTIVE appearance
|
package/README.md
CHANGED
|
@@ -536,6 +536,29 @@ import { QrCode } from "@solidrt/components"
|
|
|
536
536
|
| `radius` | `number` | `8` | Corner radius of the background panel. |
|
|
537
537
|
| `layout` | `LayoutProps` | - | Layout of the outer box. |
|
|
538
538
|
|
|
539
|
+
### Icon
|
|
540
|
+
|
|
541
|
+
A thin themed wrapper over the core `<svg>` primitive. `src` is a whole SVG document as a string; the component draws it in a square box and, for monochrome icons that stroke/fill with `currentColor`, recolors it from the theme. It carries no icon set and no name registry, so any `currentColor` SVG works (Lucide, Feather, Heroicons) and only the icons you import are bundled. Multi-color documents keep their own fills. For a non-square box, use `<svg>` directly.
|
|
542
|
+
|
|
543
|
+
Icons are just SVG strings. Import them as assets (`import House from "lucide-static/icons/house.svg"`, resolved to a string), pull them from a string export, or inline a literal:
|
|
544
|
+
|
|
545
|
+
```jsx
|
|
546
|
+
import { Icon } from "@solidrt/components"
|
|
547
|
+
import House from "lucide-static/icons/house.svg"
|
|
548
|
+
|
|
549
|
+
<Icon src={House} />
|
|
550
|
+
<Icon src={House} size={32} color={theme.color.primary} />
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Props**
|
|
554
|
+
|
|
555
|
+
| Prop | Type | Default | Description |
|
|
556
|
+
| -------- | ------------- | ------------------ | -------------------------------------------------------------- |
|
|
557
|
+
| `src` | `string` | - | The SVG document to draw. |
|
|
558
|
+
| `size` | `number` | `24` | Square box side in pixels. |
|
|
559
|
+
| `color` | `string` | `theme.color.text` | Drives `currentColor`; explicit fills/strokes still win. |
|
|
560
|
+
| `layout` | `LayoutProps` | - | Layout of the box. |
|
|
561
|
+
|
|
539
562
|
## License
|
|
540
563
|
|
|
541
564
|
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.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@solidjs/signals": "2.0.0-beta.15",
|
|
20
|
-
"@solidrt/core": "0.0.
|
|
20
|
+
"@solidrt/core": "0.0.20"
|
|
21
21
|
}
|
|
22
22
|
}
|
package/src/icon.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LayoutProps } from "@solidrt/core"
|
|
2
|
+
import { theme } from "./theme"
|
|
3
|
+
|
|
4
|
+
export interface IconProps {
|
|
5
|
+
// An SVG document as a string: an imported `.svg` asset, a `lucide-static`
|
|
6
|
+
// string export, or an inline template literal. Monochrome icons that stroke/
|
|
7
|
+
// fill with `currentColor` (Lucide, Feather, Heroicons, ...) get recolored by
|
|
8
|
+
// `color`; a multi-color document keeps its own fills.
|
|
9
|
+
src: string
|
|
10
|
+
// Rendered box in pixels, square. Defaults to 24 (the common icon grid).
|
|
11
|
+
size?: number
|
|
12
|
+
// Drives `currentColor` in the document. Defaults to the theme text color.
|
|
13
|
+
color?: string
|
|
14
|
+
layout?: LayoutProps
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SIZE = 24
|
|
18
|
+
|
|
19
|
+
// A themed wrapper over the core <svg> document primitive: a square box sized to
|
|
20
|
+
// `size` and colored from the theme by default. This is the only value it adds
|
|
21
|
+
// over `<svg src>` directly, so reach for the primitive when you need a
|
|
22
|
+
// non-square box or want no theme coupling.
|
|
23
|
+
export function Icon(props: IconProps) {
|
|
24
|
+
let size = () => props.size ?? SIZE
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<svg
|
|
28
|
+
width={size()}
|
|
29
|
+
height={size()}
|
|
30
|
+
src={props.src}
|
|
31
|
+
color={props.color ?? theme.color.text}
|
|
32
|
+
{...props.layout}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { ContextMenu, type ContextMenuProps, type ContextMenuItem } from "./cont
|
|
|
24
24
|
export { NavShell, type NavShellProps, type NavItem } from "./nav-shell"
|
|
25
25
|
export { SplitView, type SplitViewProps } from "./split-view"
|
|
26
26
|
export { QrCode, type QrCodeProps } from "./qrcode"
|
|
27
|
+
export { Icon, type IconProps } from "./icon"
|
|
27
28
|
export { theme, setTheme, darkTheme, lightTheme, type Theme } from "./theme"
|
|
28
29
|
export {
|
|
29
30
|
policy,
|
package/src/qrcode.tsx
CHANGED
|
@@ -27,45 +27,51 @@ const MODULE_SIZE = 6
|
|
|
27
27
|
const MARGIN = 16
|
|
28
28
|
const RADIUS = 8
|
|
29
29
|
|
|
30
|
-
// Render a QR for `data` as primitives: merge horizontal runs of
|
|
31
|
-
//
|
|
32
|
-
//
|
|
30
|
+
// Render a QR for `data` as primitives: merge horizontal runs of dark modules
|
|
31
|
+
// per row into a single d-rect, placed at explicit coordinates on a light
|
|
32
|
+
// quiet-zone panel. Everything inside the panel is detached, so a data change
|
|
33
|
+
// repaints without touching layout; the panel view itself has a fixed size
|
|
34
|
+
// (module count * module size + margins) that only changes when the data
|
|
35
|
+
// crosses a QR version boundary.
|
|
33
36
|
export function QrCode(props: QrCodeProps) {
|
|
34
|
-
let
|
|
37
|
+
let grid = createMemo(() => {
|
|
35
38
|
let qr = qrcode(0, props.level ?? "M")
|
|
36
39
|
qr.addData(props.data)
|
|
37
40
|
qr.make()
|
|
38
41
|
let n = qr.getModuleCount()
|
|
39
42
|
|
|
40
|
-
let
|
|
43
|
+
let runs: { x: number; y: number; len: number }[] = []
|
|
41
44
|
for (let y = 0; y < n; y++) {
|
|
42
|
-
let runs: { dark: boolean; len: number }[] = []
|
|
43
45
|
let x = 0
|
|
44
46
|
while (x < n) {
|
|
45
|
-
|
|
47
|
+
if (!qr.isDark(y, x)) {
|
|
48
|
+
x++
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
46
51
|
let len = 1
|
|
47
|
-
while (x + len < n && qr.isDark(y, x + len)
|
|
48
|
-
runs.push({
|
|
52
|
+
while (x + len < n && qr.isDark(y, x + len)) len++
|
|
53
|
+
runs.push({ x, y, len })
|
|
49
54
|
x += len
|
|
50
55
|
}
|
|
51
|
-
out.push(runs)
|
|
52
56
|
}
|
|
53
|
-
return
|
|
57
|
+
return { n, runs }
|
|
54
58
|
})
|
|
55
59
|
|
|
56
60
|
let size = () => props.moduleSize ?? MODULE_SIZE
|
|
61
|
+
let margin = () => props.margin ?? MARGIN
|
|
62
|
+
let side = () => grid().n * size() + 2 * margin()
|
|
57
63
|
|
|
58
64
|
return (
|
|
59
|
-
<view
|
|
65
|
+
<view width={side()} height={side()} {...props.layout}>
|
|
60
66
|
<d-rect color={props.background ?? "#ffffff"} radius={props.radius ?? RADIUS} />
|
|
61
|
-
{
|
|
62
|
-
<
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
{grid().runs.map((run) => (
|
|
68
|
+
<d-rect
|
|
69
|
+
x={margin() + run.x * size()}
|
|
70
|
+
y={margin() + run.y * size()}
|
|
71
|
+
w={run.len * size()}
|
|
72
|
+
h={size()}
|
|
73
|
+
color={props.color ?? "#000000"}
|
|
74
|
+
/>
|
|
69
75
|
))}
|
|
70
76
|
</view>
|
|
71
77
|
)
|
package/src/text-input.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createEffect, createSignal, onCleanup } from "@solidjs/signals"
|
|
2
|
-
import { setFocus } from "@solidrt/core"
|
|
2
|
+
import { measureText, setFocus } from "@solidrt/core"
|
|
3
3
|
import { createCaretScroll, createTextBuffer } from "@solidrt/core/text-input"
|
|
4
4
|
import type { LayoutProps } from "@solidrt/core"
|
|
5
5
|
import type { StyleProps } from "./types"
|
|
@@ -10,6 +10,11 @@ import { policy, densityScale } from "./policy"
|
|
|
10
10
|
// edge column cannot drift apart.
|
|
11
11
|
const CARET_WIDTH = 1
|
|
12
12
|
|
|
13
|
+
// Shaping width handed to the detached value/placeholder text: effectively
|
|
14
|
+
// unbounded, so a single line never wraps. The viewport clips it and scrollX
|
|
15
|
+
// slides it.
|
|
16
|
+
const TEXT_SHAPE_WIDTH = 1e9
|
|
17
|
+
|
|
13
18
|
export interface TextInputProps {
|
|
14
19
|
value?: string
|
|
15
20
|
defaultValue?: string
|
|
@@ -134,20 +139,16 @@ export function TextInput(props: TextInputProps) {
|
|
|
134
139
|
let showPlaceholder = () => !focused() && value().length === 0 && (props.placeholder ?? "").length > 0
|
|
135
140
|
let showCaret = () => focused() && caretOn() && !showPlaceholder()
|
|
136
141
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// createCaretScroll reads its laid-out width after layout, keeps the caret in
|
|
148
|
-
// view, and flushes the offset before paint.
|
|
149
|
-
let beforeCaret = () => value().slice(0, buffer.caret())
|
|
150
|
-
let afterCaret = () => value().slice(buffer.caret())
|
|
142
|
+
// Everything inside the viewport is detached: the value is one d-text shaped
|
|
143
|
+
// at an unbounded width and the caret a d-rect at the measured before-caret
|
|
144
|
+
// width, so typing, caret movement, blink and scroll never touch layout. The
|
|
145
|
+
// viewport carries an explicit height (detached content takes no layout
|
|
146
|
+
// slot) equal to the one-line paragraph height, which keeps the text where
|
|
147
|
+
// the old centered attached row sat. createCaretScroll keeps the caret in
|
|
148
|
+
// view and flushes the offset before paint; scrollX is a paint-time
|
|
149
|
+
// translate that also applies to detached children.
|
|
150
|
+
let rowHeight = () => Math.round(theme.text.body.size * theme.text.body.lineHeight)
|
|
151
|
+
let caretX = () => measureText(value().slice(0, buffer.caret()), { fontSize: theme.text.body.size }).width
|
|
151
152
|
let scrollX = createCaretScroll(
|
|
152
153
|
() => viewport,
|
|
153
154
|
() => ({
|
|
@@ -162,11 +163,11 @@ export function TextInput(props: TextInputProps) {
|
|
|
162
163
|
)
|
|
163
164
|
|
|
164
165
|
let textStyle = (color: string) => ({
|
|
166
|
+
w: TEXT_SHAPE_WIDTH,
|
|
165
167
|
fontSize: theme.text.body.size,
|
|
166
168
|
lineHeight: theme.text.body.lineHeight,
|
|
167
169
|
color,
|
|
168
170
|
maxLines: 1,
|
|
169
|
-
flexShrink: 0,
|
|
170
171
|
})
|
|
171
172
|
|
|
172
173
|
return (
|
|
@@ -199,30 +200,25 @@ export function TextInput(props: TextInputProps) {
|
|
|
199
200
|
<view
|
|
200
201
|
ref={(n: { id: number }) => (viewport = n)}
|
|
201
202
|
flex={1}
|
|
202
|
-
|
|
203
|
-
alignItems="center"
|
|
203
|
+
height={rowHeight()}
|
|
204
204
|
overflow="hidden"
|
|
205
205
|
scrollX={scrollX()}
|
|
206
206
|
>
|
|
207
207
|
{showPlaceholder() ? (
|
|
208
|
-
<text {...textStyle(theme.color.textMuted)}>{props.placeholder ?? ""}</text>
|
|
208
|
+
<d-text {...textStyle(theme.color.textMuted)}>{props.placeholder ?? ""}</d-text>
|
|
209
209
|
) : (
|
|
210
|
-
|
|
211
|
-
<text {...textStyle(textColor())}>{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
) : null}
|
|
223
|
-
</view>
|
|
224
|
-
<text {...textStyle(textColor())}>{afterCaret()}</text>
|
|
225
|
-
</view>
|
|
210
|
+
<>
|
|
211
|
+
<d-text {...textStyle(textColor())}>{value()}</d-text>
|
|
212
|
+
{showCaret() ? (
|
|
213
|
+
<d-rect
|
|
214
|
+
color={textColor()}
|
|
215
|
+
x={caretX()}
|
|
216
|
+
y={(rowHeight() - theme.text.body.size) / 2}
|
|
217
|
+
w={CARET_WIDTH}
|
|
218
|
+
h={theme.text.body.size}
|
|
219
|
+
/>
|
|
220
|
+
) : null}
|
|
221
|
+
</>
|
|
226
222
|
)}
|
|
227
223
|
</view>
|
|
228
224
|
</view>
|