@solidrt/components 0.0.17 → 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 +33 -2
- package/README.md +244 -1
- package/package.json +5 -2
- package/src/badge.tsx +50 -0
- package/src/button.tsx +19 -10
- package/src/card.tsx +48 -0
- package/src/checkbox.tsx +65 -0
- package/src/context-menu.tsx +189 -0
- package/src/divider.tsx +33 -0
- package/src/index.ts +30 -1
- package/src/nav-shell.tsx +160 -0
- package/src/policy.ts +108 -0
- package/src/progress-bar.tsx +81 -0
- package/src/qrcode.tsx +72 -0
- package/src/radio.tsx +100 -0
- package/src/scroll-view.tsx +5 -1
- package/src/select.tsx +203 -0
- package/src/slider.tsx +116 -0
- package/src/spinner.tsx +74 -0
- package/src/split-view.tsx +54 -0
- package/src/switch.tsx +58 -0
- package/src/text-input.tsx +24 -16
- package/src/theme.ts +72 -17
- package/src/tooltip.tsx +112 -0
- package/src/types.ts +4 -4
package/src/text-input.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { createCaretScroll, createTextBuffer } from "@solidrt/core/text-input"
|
|
|
4
4
|
import type { LayoutProps } from "@solidrt/core"
|
|
5
5
|
import type { StyleProps } from "./types"
|
|
6
6
|
import { theme } from "./theme"
|
|
7
|
+
import { policy, densityScale } from "./policy"
|
|
7
8
|
|
|
8
9
|
// Caret thickness. Shared so the drawn caret and the scroll offset's reserved
|
|
9
10
|
// edge column cannot drift apart.
|
|
@@ -120,10 +121,13 @@ export function TextInput(props: TextInputProps) {
|
|
|
120
121
|
if (blinkId != null) clearInterval(blinkId)
|
|
121
122
|
})
|
|
122
123
|
|
|
123
|
-
// Style overrides fall back to theme defaults.
|
|
124
|
+
// Style overrides fall back to theme defaults. The border doubles as the
|
|
125
|
+
// focus ring: primary while focused, when the focus-ring policy asks for a
|
|
126
|
+
// visible indicator.
|
|
124
127
|
let textColor = () => props.style?.color ?? theme.color.text
|
|
125
128
|
let surfaceColor = () => props.style?.backgroundColor ?? theme.color.surface
|
|
126
|
-
let borderColor = () =>
|
|
129
|
+
let borderColor = () =>
|
|
130
|
+
props.style?.borderColor ?? (focused() && policy.focusRing ? theme.color.primary : theme.color.border)
|
|
127
131
|
let borderWidth = () => props.style?.borderWidth ?? theme.borderWidth.sm
|
|
128
132
|
let borderRadius = () => props.style?.borderRadius ?? theme.radius.sm
|
|
129
133
|
|
|
@@ -134,6 +138,8 @@ export function TextInput(props: TextInputProps) {
|
|
|
134
138
|
// between them. Flow places the anchor at the caret x (the before-text width),
|
|
135
139
|
// and the caret is a detached d-rect inside it: detached nodes take no layout
|
|
136
140
|
// slot, so the anchor stays zero-width and the after-text is not shifted. The
|
|
141
|
+
// anchor stays mounted while the caret blinks; only a detached d-view toggles
|
|
142
|
+
// inside it, so turning the caret on and off never relays the row. The
|
|
137
143
|
// anchor sits at the row's vertical center (alignItems center, zero height),
|
|
138
144
|
// so the caret is offset up by half its height to straddle it. While the
|
|
139
145
|
// placeholder shows, value() is "" so the slices and the scroll offset are 0
|
|
@@ -168,10 +174,10 @@ export function TextInput(props: TextInputProps) {
|
|
|
168
174
|
ref={(n: { id: number }) => (node = n)}
|
|
169
175
|
flexDirection="row"
|
|
170
176
|
alignItems="center"
|
|
171
|
-
paddingLeft={theme.spacing.md}
|
|
172
|
-
paddingRight={theme.spacing.md}
|
|
173
|
-
paddingTop={theme.spacing.sm}
|
|
174
|
-
paddingBottom={theme.spacing.sm}
|
|
177
|
+
paddingLeft={Math.round(theme.spacing.md * densityScale())}
|
|
178
|
+
paddingRight={Math.round(theme.spacing.md * densityScale())}
|
|
179
|
+
paddingTop={Math.round(theme.spacing.sm * densityScale())}
|
|
180
|
+
paddingBottom={Math.round(theme.spacing.sm * densityScale())}
|
|
175
181
|
{...props.layout}
|
|
176
182
|
x={props.style?.x}
|
|
177
183
|
y={props.style?.y}
|
|
@@ -203,16 +209,18 @@ export function TextInput(props: TextInputProps) {
|
|
|
203
209
|
) : (
|
|
204
210
|
<view flexDirection="row" alignItems="center" flexShrink={0}>
|
|
205
211
|
<text {...textStyle(textColor())}>{beforeCaret()}</text>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<d-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
<view>
|
|
213
|
+
{showCaret() ? (
|
|
214
|
+
<d-view>
|
|
215
|
+
<d-rect
|
|
216
|
+
color={textColor()}
|
|
217
|
+
y={-theme.text.body.size / 2}
|
|
218
|
+
w={CARET_WIDTH}
|
|
219
|
+
h={theme.text.body.size}
|
|
220
|
+
/>
|
|
221
|
+
</d-view>
|
|
222
|
+
) : null}
|
|
223
|
+
</view>
|
|
216
224
|
<text {...textStyle(textColor())}>{afterCaret()}</text>
|
|
217
225
|
</view>
|
|
218
226
|
)}
|
package/src/theme.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createStore } from "@solidrt/core"
|
|
2
|
+
|
|
1
3
|
export type TextStyle = {
|
|
2
4
|
size: number
|
|
3
5
|
lineHeight: number
|
|
@@ -6,12 +8,24 @@ export type TextStyle = {
|
|
|
6
8
|
export type Theme = {
|
|
7
9
|
text: { body: TextStyle }
|
|
8
10
|
color: {
|
|
11
|
+
// Window fill.
|
|
12
|
+
background: string
|
|
13
|
+
// Control/card fill.
|
|
14
|
+
surface: string
|
|
15
|
+
// Subtle raised/track fill (switch off-state, slider track, ...).
|
|
16
|
+
surfaceAlt: string
|
|
17
|
+
// Hover tint for surface-colored controls (non-touch interaction policies).
|
|
18
|
+
surfaceHover: string
|
|
9
19
|
text: string
|
|
10
20
|
textMuted: string
|
|
11
|
-
surface: string
|
|
12
21
|
border: string
|
|
13
22
|
primary: string
|
|
23
|
+
// Hover tint for primary-colored controls.
|
|
24
|
+
primaryHover: string
|
|
14
25
|
onPrimary: string
|
|
26
|
+
// Validation / destructive.
|
|
27
|
+
danger: string
|
|
28
|
+
// Overlay dim behind modals.
|
|
15
29
|
scrim: string
|
|
16
30
|
}
|
|
17
31
|
spacing: { sm: number; md: number }
|
|
@@ -19,29 +33,70 @@ export type Theme = {
|
|
|
19
33
|
borderWidth: { sm: number }
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
// Scheme-independent tokens, shared by both presets.
|
|
37
|
+
const TEXT = { body: { size: 14, lineHeight: 1.5 } }
|
|
38
|
+
const SPACING = { sm: 4, md: 8 }
|
|
39
|
+
const RADIUS = { sm: 4 }
|
|
40
|
+
const BORDER_WIDTH = { sm: 1 }
|
|
41
|
+
|
|
42
|
+
export let darkTheme: Theme = {
|
|
43
|
+
text: TEXT,
|
|
26
44
|
color: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
background: "#0b0f17",
|
|
46
|
+
surface: "#161b22",
|
|
47
|
+
surfaceAlt: "#21262d",
|
|
48
|
+
surfaceHover: "#262c34",
|
|
49
|
+
text: "#e6edf3",
|
|
50
|
+
textMuted: "rgba(230,237,243,0.5)",
|
|
51
|
+
border: "rgba(255,255,255,0.14)",
|
|
31
52
|
primary: "#1f6feb",
|
|
53
|
+
primaryHover: "#388bfd",
|
|
32
54
|
onPrimary: "#ffffff",
|
|
55
|
+
danger: "#f85149",
|
|
33
56
|
scrim: "rgba(0,0,0,0.6)",
|
|
34
57
|
},
|
|
35
|
-
spacing:
|
|
36
|
-
radius:
|
|
37
|
-
borderWidth:
|
|
58
|
+
spacing: SPACING,
|
|
59
|
+
radius: RADIUS,
|
|
60
|
+
borderWidth: BORDER_WIDTH,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export let lightTheme: Theme = {
|
|
64
|
+
text: TEXT,
|
|
65
|
+
color: {
|
|
66
|
+
background: "#ffffff",
|
|
67
|
+
surface: "#f6f8fa",
|
|
68
|
+
surfaceAlt: "#eaeef2",
|
|
69
|
+
surfaceHover: "#e0e5eb",
|
|
70
|
+
text: "#1f2328",
|
|
71
|
+
textMuted: "rgba(31,35,40,0.5)",
|
|
72
|
+
border: "rgba(0,0,0,0.15)",
|
|
73
|
+
primary: "#1f6feb",
|
|
74
|
+
primaryHover: "#1a5fd0",
|
|
75
|
+
onPrimary: "#ffffff",
|
|
76
|
+
danger: "#cf222e",
|
|
77
|
+
scrim: "rgba(0,0,0,0.4)",
|
|
78
|
+
},
|
|
79
|
+
spacing: SPACING,
|
|
80
|
+
radius: RADIUS,
|
|
81
|
+
borderWidth: BORDER_WIDTH,
|
|
38
82
|
}
|
|
39
83
|
|
|
84
|
+
// Backed by a Solid store so reads are tracked: calling setTheme at runtime
|
|
85
|
+
// recolors the live UI without remounting. Components read theme.* through
|
|
86
|
+
// thunks/JSX expressions, so they pick this up with no call-site changes.
|
|
87
|
+
let [theme, setThemeStore] = createStore<Theme>({ ...darkTheme })
|
|
88
|
+
export { theme }
|
|
89
|
+
|
|
40
90
|
type ThemePartial = { [K in keyof Theme]?: Partial<Theme[K]> }
|
|
41
91
|
|
|
92
|
+
// Switch themes with a full preset (setTheme(lightTheme)) or apply a targeted
|
|
93
|
+
// override (setTheme({ color: { primary: "#f00" } })). Merges one level deep per
|
|
94
|
+
// category, matching the previous Object.assign behavior.
|
|
42
95
|
export function setTheme(partial: ThemePartial) {
|
|
43
|
-
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
96
|
+
setThemeStore((s) => {
|
|
97
|
+
for (let key in partial) {
|
|
98
|
+
let k = key as keyof Theme
|
|
99
|
+
Object.assign(s[k], (partial as any)[k])
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
package/src/tooltip.tsx
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createSignal, onCleanup, createPortal, onLayout, getBoundingBox, Show, env } from "@solidrt/core"
|
|
2
|
+
import type { LayoutProps, PointerEvent } from "@solidrt/core"
|
|
3
|
+
import { theme } from "./theme"
|
|
4
|
+
import { policy, densityScale } from "./policy"
|
|
5
|
+
|
|
6
|
+
export interface TooltipProps {
|
|
7
|
+
// The tooltip body. A string/number renders as themed text; anything else
|
|
8
|
+
// renders as-is.
|
|
9
|
+
content?: any
|
|
10
|
+
// The anchor content the tooltip attaches to.
|
|
11
|
+
children?: any
|
|
12
|
+
// Hover delay in milliseconds before showing.
|
|
13
|
+
delay?: number
|
|
14
|
+
// Which side of the anchor the bubble appears on.
|
|
15
|
+
placement?: "top" | "bottom"
|
|
16
|
+
layout?: LayoutProps
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DELAY = 500
|
|
20
|
+
const GAP = 6
|
|
21
|
+
// Minimum distance kept between the bubble and the window edges.
|
|
22
|
+
const MARGIN = 4
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A hover-only affordance: under desktop/hybrid interaction policies, resting a
|
|
26
|
+
* mouse pointer on the wrapped content shows a bubble near it after a short
|
|
27
|
+
* delay. Under the touch policy it never shows, so tooltip content must stay
|
|
28
|
+
* non-essential. The bubble is portal-mounted at the window root and takes no
|
|
29
|
+
* pointer events; it hides on leave and on press.
|
|
30
|
+
*/
|
|
31
|
+
export function Tooltip(props: TooltipProps) {
|
|
32
|
+
let anchor: { id: number } | undefined
|
|
33
|
+
let [open, setOpen] = createSignal(false)
|
|
34
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
35
|
+
|
|
36
|
+
let enter = (e: PointerEvent) => {
|
|
37
|
+
// The policy gates the behavior; the pointerType check keeps a finger from
|
|
38
|
+
// arming the tooltip in a hybrid session.
|
|
39
|
+
if (policy.interaction === "touch" || e.pointerType !== "mouse") return
|
|
40
|
+
clearTimeout(timer)
|
|
41
|
+
timer = setTimeout(() => setOpen(true), props.delay ?? DELAY)
|
|
42
|
+
}
|
|
43
|
+
let hide = () => {
|
|
44
|
+
clearTimeout(timer)
|
|
45
|
+
setOpen(false)
|
|
46
|
+
}
|
|
47
|
+
onCleanup(() => clearTimeout(timer))
|
|
48
|
+
|
|
49
|
+
// The bubble sits at the window root's origin and is placed with the x/y
|
|
50
|
+
// paint transforms, so repositioning never reflows. Measured after its first
|
|
51
|
+
// layout (parked offscreen until then), then pinned to the anchor's current
|
|
52
|
+
// box; recomputed each layout so it stays attached when the anchor moves
|
|
53
|
+
// (scrolling, resizes).
|
|
54
|
+
let Bubble = () => {
|
|
55
|
+
let bubble: { id: number } | undefined
|
|
56
|
+
let [pos, setPos] = createSignal<{ x: number; y: number } | null>(null)
|
|
57
|
+
onLayout(() => {
|
|
58
|
+
let a = anchor && getBoundingBox(anchor)
|
|
59
|
+
let b = bubble && getBoundingBox(bubble)
|
|
60
|
+
if (!a || !b) return
|
|
61
|
+
let x = a.x + a.width / 2 - b.width / 2
|
|
62
|
+
x = Math.round(Math.min(Math.max(x, MARGIN), env.windowSize.width - b.width - MARGIN))
|
|
63
|
+
let y = Math.round(props.placement === "bottom" ? a.y + a.height + GAP : a.y - b.height - GAP)
|
|
64
|
+
let cur = pos()
|
|
65
|
+
if (!cur || cur.x !== x || cur.y !== y) setPos({ x, y })
|
|
66
|
+
})
|
|
67
|
+
let isText = () => typeof props.content === "string" || typeof props.content === "number"
|
|
68
|
+
return createPortal(
|
|
69
|
+
<view
|
|
70
|
+
ref={(n: { id: number }) => (bubble = n)}
|
|
71
|
+
position="absolute"
|
|
72
|
+
top={0}
|
|
73
|
+
left={0}
|
|
74
|
+
x={pos()?.x ?? -10000}
|
|
75
|
+
y={pos()?.y ?? 0}
|
|
76
|
+
paddingTop={Math.round(theme.spacing.sm * densityScale())}
|
|
77
|
+
paddingBottom={Math.round(theme.spacing.sm * densityScale())}
|
|
78
|
+
paddingLeft={Math.round(theme.spacing.md * densityScale())}
|
|
79
|
+
paddingRight={Math.round(theme.spacing.md * densityScale())}
|
|
80
|
+
pointerEvents="none"
|
|
81
|
+
>
|
|
82
|
+
<d-rect color={theme.color.surfaceAlt} radius={theme.radius.sm} />
|
|
83
|
+
<Show when={isText()} fallback={props.content}>
|
|
84
|
+
<text color={theme.color.text} fontSize={theme.text.body.size} lineHeight={theme.text.body.lineHeight}>
|
|
85
|
+
{props.content}
|
|
86
|
+
</text>
|
|
87
|
+
</Show>
|
|
88
|
+
<d-rect
|
|
89
|
+
drawStyle="stroke"
|
|
90
|
+
color={theme.color.border}
|
|
91
|
+
strokeWidth={theme.borderWidth.sm}
|
|
92
|
+
radius={theme.radius.sm}
|
|
93
|
+
/>
|
|
94
|
+
</view>,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<view
|
|
100
|
+
ref={(n: { id: number }) => (anchor = n)}
|
|
101
|
+
onPointerEnter={enter}
|
|
102
|
+
onPointerLeave={hide}
|
|
103
|
+
onPointerDown={hide}
|
|
104
|
+
{...props.layout}
|
|
105
|
+
>
|
|
106
|
+
{props.children}
|
|
107
|
+
<Show when={open()}>
|
|
108
|
+
<Bubble />
|
|
109
|
+
</Show>
|
|
110
|
+
</view>
|
|
111
|
+
)
|
|
112
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Color, LayoutProps } from "@solidrt/core"
|
|
1
|
+
import type { Color, Gradient, LayoutProps } from "@solidrt/core"
|
|
2
2
|
|
|
3
3
|
// The split between layout and style follows one rule: layout properties feed
|
|
4
4
|
// into Taffy and changing them triggers a relayout; style properties are
|
|
@@ -8,9 +8,9 @@ import type { Color, LayoutProps } from "@solidrt/core"
|
|
|
8
8
|
// drawn as a stroke overlay (not part of the box model), and the transform is
|
|
9
9
|
// applied at paint time, so both live here.
|
|
10
10
|
export interface StyleProps {
|
|
11
|
-
color?: Color
|
|
12
|
-
backgroundColor?: Color
|
|
13
|
-
borderColor?: Color
|
|
11
|
+
color?: Color | Gradient
|
|
12
|
+
backgroundColor?: Color | Gradient
|
|
13
|
+
borderColor?: Color | Gradient
|
|
14
14
|
borderWidth?: number
|
|
15
15
|
borderRadius?: number | [number, number, number, number]
|
|
16
16
|
x?: number
|