@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.
@@ -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 = () => props.style?.borderColor ?? theme.color.border
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
- {showCaret() ? (
207
- <view>
208
- <d-rect
209
- color={textColor()}
210
- y={-theme.text.body.size / 2}
211
- w={CARET_WIDTH}
212
- h={theme.text.body.size}
213
- />
214
- </view>
215
- ) : null}
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
- export let theme: Theme = {
23
- text: {
24
- body: { size: 14, lineHeight: 1.5 },
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
- text: "#333",
28
- textMuted: "rgba(0,0,0,0.4)",
29
- surface: "#ccc",
30
- border: "rgba(0,0,0,0.2)",
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: { sm: 4, md: 8 },
36
- radius: { sm: 4 },
37
- borderWidth: { sm: 1 },
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
- for (let key in partial) {
44
- let k = key as keyof Theme
45
- Object.assign(theme[k], partial[k])
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
+ }
@@ -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