@solidrt/components 0.0.13 → 0.0.16
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 +8 -0
- package/README.md +76 -0
- package/package.json +3 -3
- package/src/button.tsx +62 -0
- package/src/image.tsx +9 -44
- package/src/index.ts +5 -0
- package/src/modal.tsx +58 -0
- package/src/portal.tsx +21 -0
- package/src/pressable.tsx +92 -0
- package/src/scroll-view.tsx +106 -0
- package/src/theme.ts +6 -0
package/AGENTS.md
CHANGED
|
@@ -35,6 +35,14 @@ Most components group props into two objects, plus top-level event handlers:
|
|
|
35
35
|
- `Image` - fetches/decodes/uploads an image: `src: string | Uint8Array`.
|
|
36
36
|
- `TextInput` - single-line input; `value`/`onInput`/`onSubmit`, controlled or
|
|
37
37
|
uncontrolled, plus `placeholder`, `maxLength`, `autoFocus`, `disabled`.
|
|
38
|
+
- `ScrollView` - scrollable region; vertical by default, `horizontal` to flip.
|
|
39
|
+
Wheel + drag, no momentum yet. Backed by `createScroll` from
|
|
40
|
+
`@solidrt/core/scroll` (headless offset+clamp geometry).
|
|
41
|
+
- `Pressable` - pressable box; `onPress` on a primary press released inside,
|
|
42
|
+
`disabled` opts out. `children`/`style` can be functions of `{ pressed,
|
|
43
|
+
hovered }`. No pointer capture: a drag out cancels via onPointerLeave.
|
|
44
|
+
- `Button` - themed Pressable: accent box + label (string/number child), scales
|
|
45
|
+
on press (via reactive style); colors from `theme.color.primary`/`onPrimary`.
|
|
38
46
|
- `SafeArea` - pads children clear of system UI (notches, status bars); top and
|
|
39
47
|
bottom on by default, pass `false`/a number per edge.
|
|
40
48
|
- `theme` / `setTheme` - shared appearance (colors, spacing, radii, font sizes);
|
package/README.md
CHANGED
|
@@ -217,6 +217,82 @@ Top and bottom insets are applied by default. Pass `false` to opt out of an edge
|
|
|
217
217
|
| `right` | `boolean \| number` | `false` | Apply right inset. A number sets the minimum padding. |
|
|
218
218
|
| `children` | `any` | - | Content to render inside the safe area. |
|
|
219
219
|
|
|
220
|
+
### ScrollView
|
|
221
|
+
|
|
222
|
+
A scrollable region. Scrolls vertically by default; pass `horizontal` to scroll the other axis instead. Both the wheel and dragging scroll the content. There is no momentum/fling yet, and (with no pointer capture) a drag that leaves the box ends the gesture.
|
|
223
|
+
|
|
224
|
+
```jsx
|
|
225
|
+
import { ScrollView, Text } from "@solidrt/components"
|
|
226
|
+
import { For } from "@solidrt/core"
|
|
227
|
+
|
|
228
|
+
<ScrollView layout={{ height: 300 }} style={{ backgroundColor: "#111", borderRadius: 8 }}>
|
|
229
|
+
<For each={items()}>{(item) => <Text style={{ color: "#fff" }}>{item}</Text>}</For>
|
|
230
|
+
</ScrollView>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Props**
|
|
234
|
+
|
|
235
|
+
Accepts all pointer event props, plus:
|
|
236
|
+
|
|
237
|
+
| Prop | Type | Description |
|
|
238
|
+
| ------------ | -------------------------------- | -------------------------------------------- |
|
|
239
|
+
| `horizontal` | `boolean` | Scroll the horizontal axis instead of vertical. |
|
|
240
|
+
| `layout` | `LayoutProps` | Layout of the outer box (e.g. `height`). |
|
|
241
|
+
| `style` | `StyleProps` | Background, border, and transform. |
|
|
242
|
+
| `ref` | `(node: { id: number }) => void` | Reference to the outer box. |
|
|
243
|
+
| `children` | `any` | Scrollable content. |
|
|
244
|
+
|
|
245
|
+
The underlying geometry primitive `createScroll` is available from `@solidrt/core/scroll` for building custom scrollers.
|
|
246
|
+
|
|
247
|
+
### Pressable
|
|
248
|
+
|
|
249
|
+
A pressable box. `onPress` fires on a primary-button press released over the box; a drag out of the box (or a non-primary button) does not fire it. `children` and `style` may each be a function of the `{ pressed, hovered }` state, so the box can restyle on press/hover without extra signals.
|
|
250
|
+
|
|
251
|
+
```jsx
|
|
252
|
+
import { Pressable, Text } from "@solidrt/components"
|
|
253
|
+
|
|
254
|
+
<Pressable
|
|
255
|
+
onPress={() => setCount((c) => c + 1)}
|
|
256
|
+
layout={{ padding: 12 }}
|
|
257
|
+
style={(s) => ({ backgroundColor: s.pressed ? "#333" : "#222", borderRadius: 8 })}
|
|
258
|
+
>
|
|
259
|
+
<Text style={{ color: "#fff" }}>Tap me</Text>
|
|
260
|
+
</Pressable>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Props**
|
|
264
|
+
|
|
265
|
+
Accepts all pointer event props, plus:
|
|
266
|
+
|
|
267
|
+
| Prop | Type | Description |
|
|
268
|
+
| ---------- | --------------------------------------------------- | -------------------------------------------- |
|
|
269
|
+
| `onPress` | `() => void` | Fires on a completed press. |
|
|
270
|
+
| `disabled` | `boolean` | Takes no pointer events when true. |
|
|
271
|
+
| `layout` | `LayoutProps` | Layout properties. |
|
|
272
|
+
| `style` | `StyleProps \| (state) => StyleProps` | Paint properties, or a function of state. |
|
|
273
|
+
| `children` | `any \| (state) => any` | Content, or a function of state. |
|
|
274
|
+
| `ref` | `(node: { id: number }) => void` | Node reference. |
|
|
275
|
+
|
|
276
|
+
### Button
|
|
277
|
+
|
|
278
|
+
Themed convenience over `Pressable`: a padded, centered, accent-colored box with a label that scales slightly on press. A string or number child is rendered as the themed label; any other child renders as-is. Colors come from the theme (`color.primary`, `color.onPrimary`); override per-button via `style`.
|
|
279
|
+
|
|
280
|
+
```jsx
|
|
281
|
+
import { Button } from "@solidrt/components"
|
|
282
|
+
|
|
283
|
+
<Button onPress={save} layout={{ minWidth: 120 }}>Save</Button>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Props**
|
|
287
|
+
|
|
288
|
+
| Prop | Type | Description |
|
|
289
|
+
| ---------- | ------------- | ---------------------------------------------- |
|
|
290
|
+
| `onPress` | `() => void` | Fires on a completed press. |
|
|
291
|
+
| `disabled` | `boolean` | Mutes colors and ignores presses. |
|
|
292
|
+
| `layout` | `LayoutProps` | Overrides padding/sizing. |
|
|
293
|
+
| `style` | `StyleProps` | Overrides background, radius, etc. |
|
|
294
|
+
| `children` | `any` | Label text, or custom content. |
|
|
295
|
+
|
|
220
296
|
## License
|
|
221
297
|
|
|
222
298
|
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.16",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"AGENTS.md"
|
|
14
14
|
],
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"@solidjs/signals": "2.0.0-beta.
|
|
17
|
-
"@solidrt/core": "0.0.
|
|
16
|
+
"@solidjs/signals": "2.0.0-beta.15",
|
|
17
|
+
"@solidrt/core": "0.0.0"
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/button.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Show } from "@solidrt/core"
|
|
2
|
+
import { Pressable, type PressState } from "./pressable"
|
|
3
|
+
import { theme } from "./theme"
|
|
4
|
+
import type { LayoutProps } from "@solidrt/core"
|
|
5
|
+
import type { StyleProps } from "./types"
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps {
|
|
8
|
+
// A string/number is rendered as the themed label; anything else is rendered
|
|
9
|
+
// as-is, so a button can hold custom content (an icon, a row, ...).
|
|
10
|
+
children?: any
|
|
11
|
+
onPress?: () => void
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
layout?: LayoutProps
|
|
14
|
+
style?: StyleProps
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 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.
|
|
21
|
+
export function Button(props: ButtonProps) {
|
|
22
|
+
let bg = () => props.style?.backgroundColor ?? (props.disabled ? theme.color.surface : theme.color.primary)
|
|
23
|
+
let radius = () => props.style?.borderRadius ?? theme.radius.sm
|
|
24
|
+
let label = () => (props.disabled ? theme.color.textMuted : theme.color.onPrimary)
|
|
25
|
+
let isText = () => typeof props.children === "string" || typeof props.children === "number"
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Pressable
|
|
29
|
+
onPress={props.onPress}
|
|
30
|
+
disabled={props.disabled}
|
|
31
|
+
layout={{
|
|
32
|
+
flexDirection: "row",
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
justifyContent: "center",
|
|
35
|
+
paddingTop: theme.spacing.sm,
|
|
36
|
+
paddingBottom: theme.spacing.sm,
|
|
37
|
+
paddingLeft: theme.spacing.md,
|
|
38
|
+
paddingRight: theme.spacing.md,
|
|
39
|
+
...props.layout,
|
|
40
|
+
}}
|
|
41
|
+
style={(s: PressState) => ({
|
|
42
|
+
...props.style,
|
|
43
|
+
backgroundColor: bg(),
|
|
44
|
+
borderRadius: radius(),
|
|
45
|
+
// Always a number: a scale that flips from a number back to undefined
|
|
46
|
+
// hits the transform decoder, which rejects null. Multiply so a
|
|
47
|
+
// caller-set scale is preserved under the press feedback.
|
|
48
|
+
scale: (props.style?.scale ?? 1) * (s.pressed ? 0.97 : 1),
|
|
49
|
+
})}
|
|
50
|
+
>
|
|
51
|
+
<Show when={isText()} fallback={props.children}>
|
|
52
|
+
<text
|
|
53
|
+
color={label()}
|
|
54
|
+
fontSize={theme.text.body.size}
|
|
55
|
+
lineHeight={theme.text.body.lineHeight}
|
|
56
|
+
>
|
|
57
|
+
{props.children}
|
|
58
|
+
</text>
|
|
59
|
+
</Show>
|
|
60
|
+
</Pressable>
|
|
61
|
+
)
|
|
62
|
+
}
|
package/src/image.tsx
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createTexture, destroyTexture } from "@solidrt/core/gpu"
|
|
3
|
-
import { decodeImage } from "@solidrt/core/image"
|
|
1
|
+
import { createImage, Loading } from "@solidrt/core"
|
|
4
2
|
import type { LayoutProps, PointerProps } from "@solidrt/core"
|
|
5
3
|
import type { StyleProps } from "./types"
|
|
6
4
|
|
|
@@ -12,41 +10,11 @@ export interface ImageProps extends PointerProps {
|
|
|
12
10
|
|
|
13
11
|
//TODO onLoad, onError
|
|
14
12
|
export function Image(props: ImageProps) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
// texture is destroyed before installing the new one; on unmount the current
|
|
21
|
-
// texture is destroyed by onCleanup below.
|
|
22
|
-
createEffect(
|
|
23
|
-
() => props.src,
|
|
24
|
-
(source) => {
|
|
25
|
-
let stale = false
|
|
26
|
-
|
|
27
|
-
;(async () => {
|
|
28
|
-
let bytes: Uint8Array
|
|
29
|
-
if (typeof source === "string") {
|
|
30
|
-
let response = await fetch(source)
|
|
31
|
-
bytes = await response.bytes()
|
|
32
|
-
} else {
|
|
33
|
-
bytes = source
|
|
34
|
-
}
|
|
35
|
-
if (stale) return
|
|
36
|
-
let { data, width, height } = decodeImage(bytes)
|
|
37
|
-
let id = createTexture(data, width, height)
|
|
38
|
-
let old = res()
|
|
39
|
-
setRes({ id, width, height })
|
|
40
|
-
if (old !== undefined) destroyTexture(old.id)
|
|
41
|
-
})()
|
|
42
|
-
|
|
43
|
-
return () => {
|
|
44
|
-
stale = true
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
let src = () => res()?.id
|
|
13
|
+
// createImage fetches/decodes/uploads, swaps the texture when src changes, and
|
|
14
|
+
// frees it on cleanup, returning the texture id. Pass an accessor so a reactive
|
|
15
|
+
// src reloads. Reading src() suspends until ready (a SolidJS 2.0 async value),
|
|
16
|
+
// so the <texture> below sits in a <Loading> boundary.
|
|
17
|
+
let src = createImage(() => props.src)
|
|
50
18
|
let hasBorder = () => (props.style?.borderWidth ?? 0) > 0
|
|
51
19
|
|
|
52
20
|
// A texture sizes from its own width/height (not the box around it), so the
|
|
@@ -55,11 +23,6 @@ export function Image(props: ImageProps) {
|
|
|
55
23
|
let texW = () => (typeof props.layout?.width === "number" ? props.layout.width : undefined)
|
|
56
24
|
let texH = () => (typeof props.layout?.height === "number" ? props.layout.height : undefined)
|
|
57
25
|
|
|
58
|
-
onCleanup(() => {
|
|
59
|
-
let r = res()
|
|
60
|
-
if (r !== undefined) destroyTexture(r.id)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
26
|
return (
|
|
64
27
|
<view
|
|
65
28
|
{...props.layout}
|
|
@@ -85,7 +48,9 @@ export function Image(props: ImageProps) {
|
|
|
85
48
|
{props.style?.backgroundColor != null ? (
|
|
86
49
|
<d-rect color={props.style?.backgroundColor} radius={props.style?.borderRadius} />
|
|
87
50
|
) : null}
|
|
88
|
-
<
|
|
51
|
+
<Loading fallback={null}>
|
|
52
|
+
<texture src={src()} width={texW()} height={texH()} />
|
|
53
|
+
</Loading>
|
|
89
54
|
{hasBorder() ? (
|
|
90
55
|
<d-rect
|
|
91
56
|
drawStyle="stroke"
|
package/src/index.ts
CHANGED
|
@@ -4,5 +4,10 @@ export { Text, type TextProps } from "./text"
|
|
|
4
4
|
export { Image, type ImageProps } from "./image"
|
|
5
5
|
export { SafeArea } from "./safe-area"
|
|
6
6
|
export { TextInput, type TextInputProps } from "./text-input"
|
|
7
|
+
export { ScrollView, type ScrollViewProps } from "./scroll-view"
|
|
8
|
+
export { Pressable, type PressableProps, type PressState } from "./pressable"
|
|
9
|
+
export { Button, type ButtonProps } from "./button"
|
|
10
|
+
export { Portal, type PortalProps } from "./portal"
|
|
11
|
+
export { Modal, type ModalProps } from "./modal"
|
|
7
12
|
export { theme, setTheme, type Theme } from "./theme"
|
|
8
13
|
export type { StyleProps, TextLayoutProps } from "./types"
|
package/src/modal.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createPortal } from "@solidrt/core"
|
|
2
|
+
import type { Color, PointerEvent } from "@solidrt/core"
|
|
3
|
+
import { theme } from "./theme"
|
|
4
|
+
|
|
5
|
+
export interface ModalProps {
|
|
6
|
+
// Called when the backdrop (the area around the content) is pressed, unless
|
|
7
|
+
// `dismissable` is false.
|
|
8
|
+
onClose?: () => void
|
|
9
|
+
// The modal content, centered over the backdrop.
|
|
10
|
+
children?: any
|
|
11
|
+
// Scrim color behind the content. Defaults to the theme scrim; pass
|
|
12
|
+
// "transparent" for no dim.
|
|
13
|
+
backdropColor?: Color
|
|
14
|
+
// Whether pressing the backdrop calls onClose. Defaults to true.
|
|
15
|
+
dismissable?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A centered overlay rendered at the window root via core's createPortal, so it
|
|
20
|
+
* escapes the layout and stacking of its surrounding tree. It fills the window
|
|
21
|
+
* with a dimming backdrop and centers `children` on top. Control visibility by
|
|
22
|
+
* mounting/unmounting it, e.g. `<Show when={open()}><Modal .../></Show>`: the
|
|
23
|
+
* portal's onCleanup removes it when the surrounding scope disposes.
|
|
24
|
+
*
|
|
25
|
+
* Pressing the backdrop calls `onClose`; pressing the content does not. This
|
|
26
|
+
* works because pointer events dispatch to the whole hit path with no
|
|
27
|
+
* stopPropagation, so the content is kept a sibling of the backdrop (not a
|
|
28
|
+
* child): a press on the content never has the backdrop on its path.
|
|
29
|
+
*/
|
|
30
|
+
export function Modal(props: ModalProps) {
|
|
31
|
+
let dismiss = (_e: PointerEvent) => {
|
|
32
|
+
if (props.dismissable !== false) props.onClose?.()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return createPortal(
|
|
36
|
+
<view
|
|
37
|
+
position="absolute"
|
|
38
|
+
top={0}
|
|
39
|
+
left={0}
|
|
40
|
+
right={0}
|
|
41
|
+
bottom={0}
|
|
42
|
+
alignItems="center"
|
|
43
|
+
justifyContent="center"
|
|
44
|
+
>
|
|
45
|
+
<view
|
|
46
|
+
position="absolute"
|
|
47
|
+
top={0}
|
|
48
|
+
left={0}
|
|
49
|
+
right={0}
|
|
50
|
+
bottom={0}
|
|
51
|
+
onPointerDown={dismiss}
|
|
52
|
+
>
|
|
53
|
+
<d-rect color={props.backdropColor ?? theme.color.scrim} />
|
|
54
|
+
</view>
|
|
55
|
+
{props.children}
|
|
56
|
+
</view>
|
|
57
|
+
)
|
|
58
|
+
}
|
package/src/portal.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createPortal } from "@solidrt/core"
|
|
2
|
+
|
|
3
|
+
export interface PortalProps {
|
|
4
|
+
children?: any
|
|
5
|
+
// Where to mount the content. A node, typically captured from another
|
|
6
|
+
// element's `ref`. Defaults to the window root.
|
|
7
|
+
mount?: { id: number }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders its child somewhere other than its lexical position: by default at the
|
|
12
|
+
* window root, so overlays (modals, menus, tooltips) escape the clipping and
|
|
13
|
+
* stacking of their surrounding layout. Thin JSX wrapper over core's
|
|
14
|
+
* createPortal; the child should be a single element positioned absolutely
|
|
15
|
+
* (`position="absolute"`), since it is inserted into the window's flex root.
|
|
16
|
+
*/
|
|
17
|
+
export function Portal(props: PortalProps) {
|
|
18
|
+
// A JSX element's runtime value is the built node; createPortal relocates it.
|
|
19
|
+
createPortal(props.children, props.mount as any)
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createSignal } from "@solidjs/signals"
|
|
2
|
+
import type { LayoutProps, PointerEvent, PointerProps } from "@solidrt/core"
|
|
3
|
+
import type { StyleProps } from "./types"
|
|
4
|
+
|
|
5
|
+
export type PressState = { pressed: boolean; hovered: boolean }
|
|
6
|
+
|
|
7
|
+
export interface PressableProps extends PointerProps {
|
|
8
|
+
// children and style may be functions of the press state, so a caller can
|
|
9
|
+
// restyle on press/hover without wiring their own signals.
|
|
10
|
+
children?: any | ((state: PressState) => any)
|
|
11
|
+
ref?: (node: { id: number }) => void
|
|
12
|
+
layout?: LayoutProps
|
|
13
|
+
style?: StyleProps | ((state: PressState) => StyleProps)
|
|
14
|
+
onPress?: () => void
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A pressable box. onPress fires on a primary-button down followed by an up over
|
|
19
|
+
// the same node. Because there is no pointer capture, a drag that leaves the box
|
|
20
|
+
// fires onPointerLeave, which cancels the press (no onPress) and clears hover --
|
|
21
|
+
// this also covers the no-up-outside case. Non-primary buttons (right/middle) do
|
|
22
|
+
// not start a press. When disabled, it takes no pointer events at all.
|
|
23
|
+
export function Pressable(props: PressableProps) {
|
|
24
|
+
let [pressed, setPressed] = createSignal(false)
|
|
25
|
+
let [hovered, setHovered] = createSignal(false)
|
|
26
|
+
|
|
27
|
+
let state = (): PressState => ({ pressed: pressed(), hovered: hovered() })
|
|
28
|
+
let style = () => (typeof props.style === "function" ? props.style(state()) : props.style)
|
|
29
|
+
let kids = () => (typeof props.children === "function" ? props.children(state()) : props.children)
|
|
30
|
+
|
|
31
|
+
let handleDown = (e: PointerEvent) => {
|
|
32
|
+
if (e.button != null && e.button !== 0) return
|
|
33
|
+
setPressed(true)
|
|
34
|
+
props.onPointerDown?.(e)
|
|
35
|
+
}
|
|
36
|
+
let handleUp = (e: PointerEvent) => {
|
|
37
|
+
if (pressed()) props.onPress?.()
|
|
38
|
+
setPressed(false)
|
|
39
|
+
props.onPointerUp?.(e)
|
|
40
|
+
}
|
|
41
|
+
let handleEnter = (e: PointerEvent) => {
|
|
42
|
+
setHovered(true)
|
|
43
|
+
props.onPointerEnter?.(e)
|
|
44
|
+
}
|
|
45
|
+
let handleLeave = (e: PointerEvent) => {
|
|
46
|
+
setHovered(false)
|
|
47
|
+
setPressed(false)
|
|
48
|
+
props.onPointerLeave?.(e)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let hasBackground = () => style()?.backgroundColor != null || style()?.borderRadius != null
|
|
52
|
+
let hasBorder = () => (style()?.borderWidth ?? 0) > 0
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<view
|
|
56
|
+
ref={props.ref}
|
|
57
|
+
{...props.layout}
|
|
58
|
+
x={style()?.x}
|
|
59
|
+
y={style()?.y}
|
|
60
|
+
scale={style()?.scale}
|
|
61
|
+
rotate={style()?.rotate}
|
|
62
|
+
onPointerEnter={handleEnter}
|
|
63
|
+
onPointerLeave={handleLeave}
|
|
64
|
+
onPointerDown={handleDown}
|
|
65
|
+
onPointerUp={handleUp}
|
|
66
|
+
onPointerMove={props.onPointerMove}
|
|
67
|
+
onWheel={props.onWheel}
|
|
68
|
+
onFocus={props.onFocus}
|
|
69
|
+
onBlur={props.onBlur}
|
|
70
|
+
onKeyDown={props.onKeyDown}
|
|
71
|
+
onKeyUp={props.onKeyUp}
|
|
72
|
+
onTextInput={props.onTextInput}
|
|
73
|
+
pointerEvents={props.disabled ? "none" : props.pointerEvents}
|
|
74
|
+
>
|
|
75
|
+
{hasBackground() ? (
|
|
76
|
+
<d-rect
|
|
77
|
+
color={style()?.backgroundColor ?? "transparent"}
|
|
78
|
+
radius={style()?.borderRadius}
|
|
79
|
+
/>
|
|
80
|
+
) : null}
|
|
81
|
+
{kids()}
|
|
82
|
+
{hasBorder() ? (
|
|
83
|
+
<d-rect
|
|
84
|
+
drawStyle="stroke"
|
|
85
|
+
color={style()?.borderColor ?? "transparent"}
|
|
86
|
+
strokeWidth={style()?.borderWidth}
|
|
87
|
+
radius={style()?.borderRadius}
|
|
88
|
+
/>
|
|
89
|
+
) : null}
|
|
90
|
+
</view>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createScroll } from "@solidrt/core/scroll"
|
|
2
|
+
import type { LayoutProps, PointerEvent, PointerProps, WheelEvent } from "@solidrt/core"
|
|
3
|
+
import type { StyleProps } from "./types"
|
|
4
|
+
|
|
5
|
+
export interface ScrollViewProps extends PointerProps {
|
|
6
|
+
children?: any
|
|
7
|
+
ref?: (node: { id: number }) => void
|
|
8
|
+
layout?: LayoutProps
|
|
9
|
+
style?: StyleProps
|
|
10
|
+
/** Scroll the horizontal axis instead of the vertical one. */
|
|
11
|
+
horizontal?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// A scrollable region. The outer box carries layout/style/transform and the
|
|
15
|
+
// optional background and border; inside it a clipping viewport (overflow
|
|
16
|
+
// hidden) holds a content wrapper that takes the children's natural size. The
|
|
17
|
+
// offset from createScroll translates the content via scrollX/scrollY. Wheel and
|
|
18
|
+
// drag map to scroll deltas: positive moves the content up/left, so dragging a
|
|
19
|
+
// finger up reveals content below (natural scrolling). There is no momentum yet;
|
|
20
|
+
// a fling stops when the finger lifts. With no pointer capture, a drag that
|
|
21
|
+
// leaves the box stops scrolling (pointerLeave ends the gesture).
|
|
22
|
+
export function ScrollView(props: ScrollViewProps) {
|
|
23
|
+
let viewport: { id: number } | undefined
|
|
24
|
+
let content: { id: number } | undefined
|
|
25
|
+
|
|
26
|
+
let scroll = createScroll(
|
|
27
|
+
() => viewport,
|
|
28
|
+
() => content,
|
|
29
|
+
{ axis: props.horizontal ? "horizontal" : "vertical" },
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Last pointer position during an active drag, in window coordinates. null
|
|
33
|
+
// when no drag is in progress.
|
|
34
|
+
let last: { x: number; y: number } | null = null
|
|
35
|
+
|
|
36
|
+
let onPointerDown = (e: PointerEvent) => {
|
|
37
|
+
last = { x: e.clientX, y: e.clientY }
|
|
38
|
+
}
|
|
39
|
+
let onPointerMove = (e: PointerEvent) => {
|
|
40
|
+
if (!last) return
|
|
41
|
+
scroll.scrollBy(last.x - e.clientX, last.y - e.clientY)
|
|
42
|
+
last = { x: e.clientX, y: e.clientY }
|
|
43
|
+
}
|
|
44
|
+
let endDrag = () => {
|
|
45
|
+
last = null
|
|
46
|
+
}
|
|
47
|
+
let onWheel = (e: WheelEvent) => {
|
|
48
|
+
scroll.scrollBy(e.deltaX, e.deltaY)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let direction = () => (props.horizontal ? "row" : "column")
|
|
52
|
+
let hasBackground = () =>
|
|
53
|
+
props.style?.backgroundColor != null || props.style?.borderRadius != null
|
|
54
|
+
let hasBorder = () => (props.style?.borderWidth ?? 0) > 0
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<view
|
|
58
|
+
ref={props.ref}
|
|
59
|
+
{...props.layout}
|
|
60
|
+
x={props.style?.x}
|
|
61
|
+
y={props.style?.y}
|
|
62
|
+
scale={props.style?.scale}
|
|
63
|
+
rotate={props.style?.rotate}
|
|
64
|
+
onPointerEnter={props.onPointerEnter}
|
|
65
|
+
onPointerLeave={props.onPointerLeave}
|
|
66
|
+
onPointerDown={props.onPointerDown}
|
|
67
|
+
onPointerUp={props.onPointerUp}
|
|
68
|
+
onPointerMove={props.onPointerMove}
|
|
69
|
+
onWheel={props.onWheel}
|
|
70
|
+
pointerEvents={props.pointerEvents}
|
|
71
|
+
>
|
|
72
|
+
{hasBackground() ? (
|
|
73
|
+
<d-rect
|
|
74
|
+
color={props.style?.backgroundColor ?? "transparent"}
|
|
75
|
+
radius={props.style?.borderRadius}
|
|
76
|
+
/>
|
|
77
|
+
) : null}
|
|
78
|
+
<view
|
|
79
|
+
ref={(n: { id: number }) => (viewport = n)}
|
|
80
|
+
flex={1}
|
|
81
|
+
overflow="hidden"
|
|
82
|
+
clipRadius={props.style?.borderRadius}
|
|
83
|
+
flexDirection={direction()}
|
|
84
|
+
scrollX={scroll.offset().x}
|
|
85
|
+
scrollY={scroll.offset().y}
|
|
86
|
+
onPointerDown={onPointerDown}
|
|
87
|
+
onPointerMove={onPointerMove}
|
|
88
|
+
onPointerUp={endDrag}
|
|
89
|
+
onPointerLeave={endDrag}
|
|
90
|
+
onWheel={onWheel}
|
|
91
|
+
>
|
|
92
|
+
<view ref={(n: { id: number }) => (content = n)} flexShrink={0} flexDirection={direction()}>
|
|
93
|
+
{props.children}
|
|
94
|
+
</view>
|
|
95
|
+
</view>
|
|
96
|
+
{hasBorder() ? (
|
|
97
|
+
<d-rect
|
|
98
|
+
drawStyle="stroke"
|
|
99
|
+
color={props.style?.borderColor ?? "transparent"}
|
|
100
|
+
strokeWidth={props.style?.borderWidth}
|
|
101
|
+
radius={props.style?.borderRadius}
|
|
102
|
+
/>
|
|
103
|
+
) : null}
|
|
104
|
+
</view>
|
|
105
|
+
)
|
|
106
|
+
}
|
package/src/theme.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type Theme = {
|
|
|
10
10
|
textMuted: string
|
|
11
11
|
surface: string
|
|
12
12
|
border: string
|
|
13
|
+
primary: string
|
|
14
|
+
onPrimary: string
|
|
15
|
+
scrim: string
|
|
13
16
|
}
|
|
14
17
|
spacing: { sm: number; md: number }
|
|
15
18
|
radius: { sm: number }
|
|
@@ -25,6 +28,9 @@ export let theme: Theme = {
|
|
|
25
28
|
textMuted: "rgba(0,0,0,0.4)",
|
|
26
29
|
surface: "#ccc",
|
|
27
30
|
border: "rgba(0,0,0,0.2)",
|
|
31
|
+
primary: "#1f6feb",
|
|
32
|
+
onPrimary: "#ffffff",
|
|
33
|
+
scrim: "rgba(0,0,0,0.6)",
|
|
28
34
|
},
|
|
29
35
|
spacing: { sm: 4, md: 8 },
|
|
30
36
|
radius: { sm: 4 },
|