@solidrt/components 0.0.37 → 0.0.39

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/README.md CHANGED
@@ -194,10 +194,20 @@ Accepts `layout`, `style`, and all pointer event props, plus:
194
194
  | Prop | Type | Description |
195
195
  | ---------- | ------------------------ | ------------------------------------------------------------------------------------ |
196
196
  | `src` | `string \| Uint8Array` | URL to fetch, or raw image bytes to decode |
197
+ | `fit` | `"fill" \| "cover" \| "contain" \| "none" \| "scale-down"` | How the image maps into the box (CSS object-fit, centered) |
197
198
  | `fallback` | `string \| Uint8Array` | Source shown when `src` fails; if it also fails, the `backgroundColor` placeholder stays |
198
199
  | `onLoad` | `() => void` | Called each time a source finishes loading |
199
200
  | `onError` | `(err: unknown) => void` | Called when `src` fails to load or decode |
200
201
 
202
+ With `fit` the image fills whatever box `layout` gives the component - numbers,
203
+ `pct()`, or flex - and the fit decides how the pixels map into it (`"cover"` is
204
+ the ported-web-hero-image answer). Without `fit`, only *numeric* layout sizes
205
+ reach the image; anything else draws at intrinsic size.
206
+
207
+ ```jsx
208
+ <Image src={hero} fit="cover" layout={{ width: pct(100), height: 240 }} />
209
+ ```
210
+
201
211
  A failing `src` is contained by the component (the fallback or placeholder
202
212
  shows); it does not propagate to an outer `<Errored>` boundary.
203
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/components",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "license": "MIT",
5
5
  "author": "Antoine van Wel",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "qrcode-generator": "^2.0.4"
18
18
  },
19
19
  "peerDependencies": {
20
- "@solidjs/signals": "2.0.0-beta.20",
21
- "@solidrt/core": "0.0.37"
20
+ "@solidjs/signals": "2.0.0-beta.26",
21
+ "@solidrt/core": "0.0.39"
22
22
  }
23
23
  }
package/src/button.tsx CHANGED
@@ -24,6 +24,7 @@ export interface ButtonProps {
24
24
  size?: ButtonSize
25
25
  onPress?: () => void
26
26
  disabled?: boolean
27
+ ref?: (node: { id: number }) => void
27
28
  layout?: LayoutProps
28
29
  style?: StyleProps
29
30
  }
@@ -93,7 +94,10 @@ export function Button(props: ButtonProps) {
93
94
 
94
95
  return (
95
96
  <view
96
- ref={press.ref}
97
+ ref={(n: { id: number }) => {
98
+ press.ref(n)
99
+ props.ref?.(n)
100
+ }}
97
101
  repaintBoundary
98
102
  flexDirection="row"
99
103
  alignItems="center"
package/src/image.tsx CHANGED
@@ -1,9 +1,18 @@
1
- import { createImage, createEffect, Loading, Errored } from "@solidrt/core"
2
- import type { ImageSource, LayoutProps, PointerProps } from "@solidrt/core"
1
+ import { createImage, createEffect, Loading, Errored, pct } from "@solidrt/core"
2
+ import type { ImageSource, LayoutProps, Pct, PointerProps, TextureProps } from "@solidrt/core"
3
3
  import type { StyleProps } from "./types"
4
4
 
5
5
  export interface ImageProps extends PointerProps {
6
6
  src: string | Uint8Array
7
+ /**
8
+ * How the image maps into the Image's box (CSS object-fit): "fill"
9
+ * stretches, "cover"/"none" crop, "contain"/"scale-down" letterbox,
10
+ * everything centered. Requires the Image to have a box: give `layout` a
11
+ * size in any form (numbers, `pct()`, flex). Without `fit` the image keeps
12
+ * its legacy sizing: numeric layout sizes are honored, anything else draws
13
+ * at intrinsic size.
14
+ */
15
+ fit?: TextureProps["fit"]
7
16
  /** Image source to show when `src` fails to load. If this also fails, only
8
17
  * the `backgroundColor` placeholder remains. */
9
18
  fallback?: ImageSource
@@ -22,11 +31,16 @@ export interface ImageProps extends PointerProps {
22
31
  // keeps a broken fallback from escaping: the placeholder stays instead.
23
32
  // Both fallbacks here take the error argument: an arity >= 1 fallback tells
24
33
  // <Errored> the error is handled, so dev builds do not console.error it.
25
- function FallbackTexture(props: { src: ImageSource; width?: number; height?: number }) {
34
+ function FallbackTexture(props: {
35
+ src: ImageSource
36
+ fit?: TextureProps["fit"]
37
+ width?: number | Pct
38
+ height?: number | Pct
39
+ }) {
26
40
  let tex = createImage(() => props.src)
27
41
  return (
28
42
  <Errored fallback={(_err: unknown) => null}>
29
- <texture src={tex()} width={props.width} height={props.height} />
43
+ <texture src={tex()} fit={props.fit} width={props.width} height={props.height} />
30
44
  </Errored>
31
45
  )
32
46
  }
@@ -47,11 +61,14 @@ export function Image(props: ImageProps) {
47
61
  error: (err: unknown) => props.onError?.(err),
48
62
  })
49
63
 
50
- // A texture sizes from its own width/height (not the box around it), so the
51
- // numeric layout dimensions are forwarded to it. Omitting height lets the
52
- // texture follow the image's intrinsic aspect ratio.
53
- let texW = () => (typeof props.layout?.width === "number" ? props.layout.width : undefined)
54
- let texH = () => (typeof props.layout?.height === "number" ? props.layout.height : undefined)
64
+ // A texture sizes from its own width/height (not the box around it). With
65
+ // `fit` the texture simply fills the Image's box and the fit maps the pixels
66
+ // into it. Without it, legacy sizing: numeric layout dimensions are
67
+ // forwarded, and omitting height lets the texture follow the image's
68
+ // intrinsic aspect ratio.
69
+ let texW = () => (props.fit != null ? pct(100) : typeof props.layout?.width === "number" ? props.layout.width : undefined)
70
+ let texH = () =>
71
+ props.fit != null ? pct(100) : typeof props.layout?.height === "number" ? props.layout.height : undefined
55
72
 
56
73
  return (
57
74
  <view
@@ -82,10 +99,12 @@ export function Image(props: ImageProps) {
82
99
  <Loading fallback={null}>
83
100
  <Errored
84
101
  fallback={(_err: unknown) =>
85
- props.fallback != null ? <FallbackTexture src={props.fallback} width={texW()} height={texH()} /> : null
102
+ props.fallback != null ? (
103
+ <FallbackTexture src={props.fallback} fit={props.fit} width={texW()} height={texH()} />
104
+ ) : null
86
105
  }
87
106
  >
88
- <texture src={src()} width={texW()} height={texH()} />
107
+ <texture src={src()} fit={props.fit} width={texW()} height={texH()} />
89
108
  </Errored>
90
109
  </Loading>
91
110
  {hasBorder() ? (
package/src/press.ts CHANGED
@@ -2,6 +2,10 @@ import { createSignal, onSettled, getBoundingBoxViewport } from "@solidrt/core"
2
2
  import type { PointerEvent } from "@solidrt/core"
3
3
  import { claim, release } from "./arena"
4
4
 
5
+ // A live view of a recognizer's state, not a snapshot: both fields are getters,
6
+ // so a consumer that reads one inside a JSX prop or child expression tracks that
7
+ // signal there and nothing else re-runs. Read them in those positions, not
8
+ // eagerly into a local, or the read lands in whatever scope destructured it.
5
9
  export type PressState = { pressed: boolean; hovered: boolean }
6
10
 
7
11
  export interface PressOptions {
@@ -43,7 +47,23 @@ export function createPress(options: PressOptions) {
43
47
  let active: number | null = null
44
48
  let inside = false
45
49
 
46
- let state = (): PressState => ({ pressed: pressed(), hovered: hovered() })
50
+ // One stable object of getters, handed out as-is. Returning a fresh snapshot
51
+ // instead would read both signals at call time, making them dependencies of
52
+ // the caller's scope - and for render-prop children that scope is the one
53
+ // that builds the subtree, so a hover or press would rebuild it. A rebuild
54
+ // mid-gesture replaces a nested recognizer with a fresh one that never saw
55
+ // the down, so its up fires nothing: invisible with a mouse (hover settles
56
+ // long before the click) and fatal on touch, where the finger's arrival flips
57
+ // the ancestor's hover during the very gesture it is meant to recognize.
58
+ let live: PressState = {
59
+ get pressed() {
60
+ return pressed()
61
+ },
62
+ get hovered() {
63
+ return hovered()
64
+ },
65
+ }
66
+ let state = (): PressState => live
47
67
  let ref = (n: { id: number }) => {
48
68
  node = n
49
69
  }
package/src/pressable.tsx CHANGED
@@ -7,7 +7,9 @@ export type { PressState } from "./press"
7
7
 
8
8
  export interface PressableProps extends PointerProps {
9
9
  // children and style may be functions of the press state, so a caller can
10
- // restyle on press/hover without wiring their own signals.
10
+ // restyle on press/hover without wiring their own signals. The state is live
11
+ // (getters, not a snapshot): read it inside a prop or child expression, never
12
+ // eagerly into a local, or the value is captured once where it was read.
11
13
  children?: any | ((state: PressState) => any)
12
14
  ref?: (node: { id: number }) => void
13
15
  layout?: LayoutProps
@@ -29,8 +31,13 @@ export function Pressable(props: PressableProps) {
29
31
  // ((state) => ...) passes through it intact because flatten only unwraps
30
32
  // zero-arg functions.
31
33
  let resolved = children(() => props.children)
34
+ // The render prop runs once: the state it receives is a live object of getters
35
+ // (see press.ts), so a press or hover updates only the props that read it
36
+ // rather than rebuilding this subtree - which is what keeps a nested
37
+ // recognizer's in-flight gesture alive.
32
38
  let kids = () => {
33
- let c = resolved()
39
+ // children()'s return type erases the render-prop variant, hence the any.
40
+ let c = resolved() as any
34
41
  return typeof c === "function" ? c(press.state()) : c
35
42
  }
36
43
 
@@ -1,6 +1,5 @@
1
1
  import { Show } from "@solidrt/core"
2
2
  import type { LayoutProps } from "@solidrt/core"
3
- import { theme } from "./theme"
4
3
  import { policy } from "./policy"
5
4
 
6
5
  export interface SplitViewProps {
@@ -20,11 +19,18 @@ const LIST_WIDTH = 320
20
19
 
21
20
  /**
22
21
  * A list-detail container driven by the layout policy: two-pane shows the list
23
- * beside the detail with a hairline between, single-pane shows one pane at a
24
- * time per `showDetail`. Keep pane state (selection, scroll) in the app, not
25
- * in the panes: crossing a breakpoint re-arranges and can remount them.
22
+ * beside the detail, single-pane shows one pane at a time per `showDetail`.
23
+ * Keep pane state (selection, scroll) in the app, not in the panes: crossing
24
+ * a breakpoint re-arranges and can remount them.
26
25
  * SplitView draws no chrome; a back affordance in the single-pane detail is
27
26
  * the app's to render (fork on policy.layout, as the shell example does).
27
+ *
28
+ * Panes get no padding, max-width or alignment either - that is content, and
29
+ * SplitView cannot know the intended reading width. Give the single-pane
30
+ * detail the same treatment as the list (centered max-width column), otherwise
31
+ * crossing the breakpoint leaves the detail hugging the window's left edge
32
+ * while every other screen stays centered. Two-pane wants the opposite: the
33
+ * detail sits against the pane edge, since the pane already bounds its width.
28
34
  */
29
35
  export function SplitView(props: SplitViewProps) {
30
36
  return (
@@ -42,9 +48,6 @@ export function SplitView(props: SplitViewProps) {
42
48
  <view width={props.listWidth ?? LIST_WIDTH} flexDirection="column">
43
49
  {props.list}
44
50
  </view>
45
- <view width={1}>
46
- <d-rect color={theme.color.border} />
47
- </view>
48
51
  <view flex={1} flexDirection="column">
49
52
  {props.detail}
50
53
  </view>