@farcaster/snap 2.4.0 → 2.5.1

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.
Files changed (59) hide show
  1. package/dist/button-orientation-utils.d.ts +3 -0
  2. package/dist/button-orientation-utils.js +25 -0
  3. package/dist/constants.d.ts +1 -0
  4. package/dist/constants.js +1 -0
  5. package/dist/react/components/action-button.js +5 -5
  6. package/dist/react/components/cell-grid.js +12 -3
  7. package/dist/react/components/item-group.js +2 -1
  8. package/dist/react/components/item-layout-context.d.ts +2 -0
  9. package/dist/react/components/item-layout-context.js +7 -0
  10. package/dist/react/components/item.js +40 -3
  11. package/dist/react/components/stack.js +46 -37
  12. package/dist/react/components/toggle-group.js +6 -4
  13. package/dist/react-native/components/item-layout-context.d.ts +2 -0
  14. package/dist/react-native/components/item-layout-context.js +6 -0
  15. package/dist/react-native/components/snap-action-button.js +15 -2
  16. package/dist/react-native/components/snap-cell-grid.js +36 -5
  17. package/dist/react-native/components/snap-item-group.js +16 -6
  18. package/dist/react-native/components/snap-item.js +56 -13
  19. package/dist/react-native/components/snap-stack.js +32 -33
  20. package/dist/react-native/components/snap-toggle-group.js +5 -4
  21. package/dist/stack-horizontal-utils.d.ts +7 -5
  22. package/dist/stack-horizontal-utils.js +24 -14
  23. package/dist/ui/catalog.d.ts +70 -1
  24. package/dist/ui/catalog.js +4 -4
  25. package/dist/ui/cell-grid.d.ts +14 -0
  26. package/dist/ui/cell-grid.js +7 -7
  27. package/dist/ui/index.d.ts +2 -2
  28. package/dist/ui/index.js +1 -1
  29. package/dist/ui/item.d.ts +112 -1
  30. package/dist/ui/item.js +28 -2
  31. package/dist/ui/stack.d.ts +1 -0
  32. package/dist/ui/stack.js +3 -1
  33. package/dist/validator.js +19 -1
  34. package/llms.txt +3 -1
  35. package/package.json +1 -1
  36. package/src/button-orientation-utils.ts +36 -0
  37. package/src/constants.ts +1 -0
  38. package/src/react/components/action-button.tsx +5 -4
  39. package/src/react/components/cell-grid.tsx +13 -4
  40. package/src/react/components/item-group.tsx +19 -17
  41. package/src/react/components/item-layout-context.tsx +12 -0
  42. package/src/react/components/item.tsx +97 -4
  43. package/src/react/components/stack.tsx +51 -40
  44. package/src/react/components/toggle-group.tsx +6 -4
  45. package/src/react-native/components/item-layout-context.tsx +10 -0
  46. package/src/react-native/components/snap-action-button.tsx +15 -2
  47. package/src/react-native/components/snap-cell-grid.tsx +43 -6
  48. package/src/react-native/components/snap-item-group.tsx +31 -17
  49. package/src/react-native/components/snap-item.tsx +92 -14
  50. package/src/react-native/components/snap-stack.tsx +37 -36
  51. package/src/react-native/components/snap-toggle-group.tsx +5 -4
  52. package/src/stack-horizontal-utils.ts +32 -13
  53. package/src/ui/README.md +1 -1
  54. package/src/ui/catalog.ts +6 -5
  55. package/src/ui/cell-grid.ts +8 -7
  56. package/src/ui/index.ts +2 -2
  57. package/src/ui/item.ts +35 -5
  58. package/src/ui/stack.ts +3 -1
  59. package/src/validator.ts +29 -1
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Children } from "react";
3
3
  import { StyleSheet, View } from "react-native";
4
- import { countRenderableChildren, defaultHorizontalGapSize, horizontalChildrenAreAllButtons, } from "../../stack-horizontal-utils.js";
4
+ import { childrenShouldUseHorizontalButtonLayout, childrenAreAllButtons, countRenderableChildren, defaultHorizontalGapSize, } from "../../stack-horizontal-utils.js";
5
5
  import { SnapStackDirectionProvider, useSnapStackDirection, } from "../stack-direction-context.js";
6
6
  const VGAP = {
7
7
  none: 0,
8
- sm: 8,
8
+ sm: 4,
9
9
  md: 16,
10
10
  lg: 24,
11
11
  };
@@ -22,62 +22,60 @@ const JUSTIFY = {
22
22
  between: "space-between",
23
23
  around: "space-around",
24
24
  };
25
- /** Equal-width cells for explicit `columns` and all-button horizontal rows. */
25
+ /** Equal-width cells for explicit `equalWidth` / `columns` props. */
26
26
  function wrapEqualColumnCells(children) {
27
27
  const cells = Children.toArray(children).filter((c) => c != null && c !== false);
28
28
  return cells.map((child, i) => (_jsx(View, { style: styles.equalColumnCell, children: child }, i)));
29
29
  }
30
30
  export function SnapStack({ element: { props }, children, }) {
31
31
  const parentDirection = useSnapStackDirection();
32
- const direction = String(props.direction ?? "vertical");
32
+ const buttonContentUsesHorizontal = childrenShouldUseHorizontalButtonLayout(children);
33
+ const direction = buttonContentUsesHorizontal === undefined
34
+ ? String(props.direction ?? "vertical")
35
+ : buttonContentUsesHorizontal
36
+ ? "horizontal"
37
+ : "vertical";
33
38
  const rawGap = props.gap;
34
39
  const isHorizontal = direction === "horizontal";
35
40
  const gapMap = isHorizontal ? HGAP : VGAP;
36
- const buttonRowGrid = isHorizontal && horizontalChildrenAreAllButtons(children);
37
- const buttonRowCount = buttonRowGrid
38
- ? countRenderableChildren(children)
39
- : 0;
41
+ const allChildrenAreButtons = childrenAreAllButtons(children);
40
42
  const columnsRaw = props.columns;
43
+ const equalWidth = props.equalWidth === true;
41
44
  const columns = typeof columnsRaw === "number" &&
42
45
  columnsRaw >= 2 &&
43
46
  columnsRaw <= 6 &&
44
47
  Number.isInteger(columnsRaw)
45
48
  ? columnsRaw
46
49
  : undefined;
47
- // Horizontal default depends on column count: 2→lg, 3→md, 4+→sm. Vertical stays md.
48
- // Count comes from explicit `columns`, then button-row inference, else direct children
49
- // count (any horizontal stack is N columns wide regardless of child types).
50
- const horizontalColumnCount = isHorizontal
51
- ? (columns ??
52
- (buttonRowGrid ? buttonRowCount : undefined) ??
53
- countRenderableChildren(children))
50
+ const equalWidthColumnCount = columns ?? (equalWidth ? countRenderableChildren(children) : undefined);
51
+ const explicitEqualWidth = isHorizontal &&
52
+ equalWidthColumnCount !== undefined &&
53
+ equalWidthColumnCount >= 1 &&
54
+ equalWidthColumnCount <= 6;
55
+ // Button-only stacks always default to sm; mixed horizontal stacks scale by child count.
56
+ // Vertical non-button stacks default to md.
57
+ const horizontalChildCount = isHorizontal
58
+ ? (explicitEqualWidth
59
+ ? equalWidthColumnCount
60
+ : countRenderableChildren(children))
54
61
  : undefined;
55
62
  const gap = typeof rawGap === "number"
56
63
  ? rawGap
57
64
  : typeof rawGap === "string" && rawGap in gapMap
58
65
  ? gapMap[rawGap]
59
- : isHorizontal
60
- ? gapMap[defaultHorizontalGapSize(horizontalColumnCount)]
61
- : VGAP.md;
62
- const explicitColumnGrid = isHorizontal && columns !== undefined && !buttonRowGrid;
66
+ : allChildrenAreButtons
67
+ ? gapMap.sm
68
+ : isHorizontal
69
+ ? gapMap[defaultHorizontalGapSize(horizontalChildCount)]
70
+ : VGAP.md;
63
71
  const justify = props.justify &&
64
- (!isHorizontal || (!buttonRowGrid && !explicitColumnGrid))
72
+ (!isHorizontal || !explicitEqualWidth)
65
73
  ? JUSTIFY[String(props.justify)]
66
74
  : undefined;
67
75
  const isRowChild = parentDirection === "horizontal";
68
- const packedHorizontal = isHorizontal &&
69
- ((buttonRowGrid &&
70
- buttonRowCount >= 1 &&
71
- buttonRowCount <= 6) ||
72
- explicitColumnGrid);
76
+ const packedHorizontal = isHorizontal && explicitEqualWidth;
73
77
  let horizontalBody = children;
74
- if (isHorizontal &&
75
- buttonRowGrid &&
76
- buttonRowCount >= 1 &&
77
- buttonRowCount <= 6) {
78
- horizontalBody = wrapEqualColumnCells(children);
79
- }
80
- else if (isHorizontal && explicitColumnGrid && columns !== undefined) {
78
+ if (isHorizontal && explicitEqualWidth) {
81
79
  horizontalBody = wrapEqualColumnCells(children);
82
80
  }
83
81
  return (_jsx(SnapStackDirectionProvider, { direction: isHorizontal ? "horizontal" : "vertical", children: _jsx(View, { style: [
@@ -117,7 +115,7 @@ const styles = StyleSheet.create({
117
115
  width: "100%",
118
116
  minWidth: 0,
119
117
  },
120
- /** Single row for packed equal-width cells (button grids & explicit columns). */
118
+ /** Single row for packed equal-width cells from explicit equal-width layout. */
121
119
  horizontalPacked: {
122
120
  flexDirection: "row",
123
121
  flexWrap: "nowrap",
@@ -130,5 +128,6 @@ const styles = StyleSheet.create({
130
128
  flexShrink: 1,
131
129
  flexBasis: 0,
132
130
  minWidth: 0,
131
+ alignSelf: "stretch",
133
132
  },
134
133
  });
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useStateStore } from "@json-render/react-native";
3
3
  import { Pressable, StyleSheet, Text, View } from "react-native";
4
+ import { shouldUseHorizontalButtonContent } from "../../button-orientation-utils.js";
4
5
  import { useSnapTheme } from "../theme.js";
5
6
  export function SnapToggleGroup({ element: { props }, }) {
6
7
  const { get, set } = useStateStore();
@@ -9,7 +10,6 @@ export function SnapToggleGroup({ element: { props }, }) {
9
10
  const path = `/inputs/${name}`;
10
11
  const label = props.label ? String(props.label) : undefined;
11
12
  const isMultiple = Boolean(props.multiple);
12
- const orientation = String(props.orientation ?? "horizontal");
13
13
  const options = Array.isArray(props.options)
14
14
  ? props.options
15
15
  : [];
@@ -32,7 +32,7 @@ export function SnapToggleGroup({ element: { props }, }) {
32
32
  }
33
33
  return [];
34
34
  })();
35
- const isVertical = orientation === "vertical";
35
+ const isVertical = !shouldUseHorizontalButtonContent(options);
36
36
  const handlePress = (opt) => {
37
37
  if (isMultiple) {
38
38
  const next = selected.includes(opt)
@@ -64,7 +64,7 @@ export function SnapToggleGroup({ element: { props }, }) {
64
64
  ], onPress: () => handlePress(opt), children: _jsx(Text, { style: [
65
65
  styles.optionText,
66
66
  { color: colors.text },
67
- ], children: opt }) }, index));
67
+ ], children: opt }) }, `${opt}-${index}`));
68
68
  }) })] }));
69
69
  }
70
70
  const styles = StyleSheet.create({
@@ -89,7 +89,8 @@ const styles = StyleSheet.create({
89
89
  justifyContent: "center",
90
90
  },
91
91
  optionHorizontal: {
92
- flex: 1,
92
+ flexGrow: 1,
93
+ flexShrink: 1,
93
94
  },
94
95
  optionText: {
95
96
  fontSize: 13,
@@ -1,10 +1,12 @@
1
1
  import { type ReactNode } from "react";
2
- export declare function horizontalChildrenAreAllButtons(children: ReactNode): boolean;
3
- /** Direct snap catalog children under a stack (used for all-button grid column count). */
2
+ export declare function childrenAreAllButtons(children: ReactNode): boolean;
3
+ export declare function getButtonChildLabels(children: ReactNode): string[] | undefined;
4
+ export declare function childrenShouldUseHorizontalButtonLayout(children: ReactNode): boolean | undefined;
5
+ /** Direct snap catalog children under a stack (used for horizontal gap defaults). */
4
6
  export declare function countRenderableChildren(children: ReactNode): number;
5
7
  /**
6
- * Default horizontal stack gap as a t-shirt size, chosen by column count:
7
- * 2 cols → lg, 3 cols → md, 4+ cols → sm. Unknown count falls back to md.
8
+ * Default horizontal stack gap as a t-shirt size, chosen by direct child count:
9
+ * 2 children → lg, 3 children → md, 4+ children → sm. Unknown count falls back to md.
8
10
  * Tighter gaps for denser layouts; authors can always override via the `gap` prop.
9
11
  */
10
- export declare function defaultHorizontalGapSize(columnCount: number | undefined): "sm" | "md" | "lg";
12
+ export declare function defaultHorizontalGapSize(childCount: number | undefined): "sm" | "md" | "lg";
@@ -1,4 +1,5 @@
1
1
  import { Children, isValidElement } from "react";
2
+ import { shouldUseHorizontalButtonContent } from "./button-orientation-utils.js";
2
3
  /**
3
4
  * True when every rendered child comes from a catalog `button` element.
4
5
  * json-render passes `{ element: { type, props, ... } }` into each catalog component.
@@ -10,34 +11,43 @@ function isRenderableChild(c) {
10
11
  return false;
11
12
  return true;
12
13
  }
13
- export function horizontalChildrenAreAllButtons(children) {
14
+ export function childrenAreAllButtons(children) {
15
+ return getButtonChildLabels(children) !== undefined;
16
+ }
17
+ export function getButtonChildLabels(children) {
14
18
  const items = Children.toArray(children).filter(isRenderableChild);
15
19
  if (items.length === 0)
16
- return false;
20
+ return undefined;
21
+ const labels = [];
17
22
  for (const child of items) {
18
23
  if (!isValidElement(child))
19
- return false;
20
- const typ = child.props.element?.type;
21
- if (typ !== "button")
22
- return false;
24
+ return undefined;
25
+ const element = child.props.element;
26
+ if (element?.type !== "button")
27
+ return undefined;
28
+ labels.push(String(element.props?.label ?? ""));
23
29
  }
24
- return true;
30
+ return labels;
31
+ }
32
+ export function childrenShouldUseHorizontalButtonLayout(children) {
33
+ const labels = getButtonChildLabels(children);
34
+ return labels ? shouldUseHorizontalButtonContent(labels) : undefined;
25
35
  }
26
- /** Direct snap catalog children under a stack (used for all-button grid column count). */
36
+ /** Direct snap catalog children under a stack (used for horizontal gap defaults). */
27
37
  export function countRenderableChildren(children) {
28
38
  return Children.toArray(children).filter(isRenderableChild).length;
29
39
  }
30
40
  /**
31
- * Default horizontal stack gap as a t-shirt size, chosen by column count:
32
- * 2 cols → lg, 3 cols → md, 4+ cols → sm. Unknown count falls back to md.
41
+ * Default horizontal stack gap as a t-shirt size, chosen by direct child count:
42
+ * 2 children → lg, 3 children → md, 4+ children → sm. Unknown count falls back to md.
33
43
  * Tighter gaps for denser layouts; authors can always override via the `gap` prop.
34
44
  */
35
- export function defaultHorizontalGapSize(columnCount) {
36
- if (columnCount === undefined)
45
+ export function defaultHorizontalGapSize(childCount) {
46
+ if (childCount === undefined)
37
47
  return "md";
38
- if (columnCount <= 2)
48
+ if (childCount <= 2)
39
49
  return "lg";
40
- if (columnCount === 3)
50
+ if (childCount === 3)
41
51
  return "md";
42
52
  return "sm";
43
53
  }
@@ -186,7 +186,61 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
186
186
  variant: z.ZodOptional<z.ZodEnum<{
187
187
  default: "default";
188
188
  }>>;
189
- }, z.core.$strip>;
189
+ media: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
190
+ variant: z.ZodLiteral<"icon">;
191
+ name: z.ZodEnum<{
192
+ "arrow-right": "arrow-right";
193
+ "arrow-left": "arrow-left";
194
+ "external-link": "external-link";
195
+ "chevron-right": "chevron-right";
196
+ check: "check";
197
+ x: "x";
198
+ "alert-triangle": "alert-triangle";
199
+ info: "info";
200
+ clock: "clock";
201
+ heart: "heart";
202
+ "message-circle": "message-circle";
203
+ repeat: "repeat";
204
+ share: "share";
205
+ user: "user";
206
+ users: "users";
207
+ star: "star";
208
+ trophy: "trophy";
209
+ zap: "zap";
210
+ flame: "flame";
211
+ gift: "gift";
212
+ image: "image";
213
+ play: "play";
214
+ pause: "pause";
215
+ wallet: "wallet";
216
+ coins: "coins";
217
+ plus: "plus";
218
+ minus: "minus";
219
+ "refresh-cw": "refresh-cw";
220
+ bookmark: "bookmark";
221
+ "thumbs-up": "thumbs-up";
222
+ "thumbs-down": "thumbs-down";
223
+ "trending-up": "trending-up";
224
+ "trending-down": "trending-down";
225
+ }>;
226
+ color: z.ZodOptional<z.ZodEnum<{
227
+ gray: "gray";
228
+ blue: "blue";
229
+ red: "red";
230
+ amber: "amber";
231
+ green: "green";
232
+ teal: "teal";
233
+ purple: "purple";
234
+ pink: "pink";
235
+ accent: "accent";
236
+ }>>;
237
+ }, z.core.$strict>, z.ZodObject<{
238
+ variant: z.ZodLiteral<"image">;
239
+ url: z.ZodString;
240
+ alt: z.ZodOptional<z.ZodString>;
241
+ round: z.ZodOptional<z.ZodBoolean>;
242
+ }, z.core.$strict>], "variant">>;
243
+ }, z.core.$strict>;
190
244
  description: string;
191
245
  };
192
246
  item_group: {
@@ -318,6 +372,7 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
318
372
  between: "between";
319
373
  around: "around";
320
374
  }>>;
375
+ equalWidth: z.ZodOptional<z.ZodBoolean>;
321
376
  columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
322
377
  }, z.core.$strip>;
323
378
  description: string;
@@ -390,6 +445,16 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
390
445
  purple: "purple";
391
446
  pink: "pink";
392
447
  }>, z.ZodString]>>>;
448
+ textColor: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodUnion<readonly [z.ZodEnum<{
449
+ gray: "gray";
450
+ blue: "blue";
451
+ red: "red";
452
+ amber: "amber";
453
+ green: "green";
454
+ teal: "teal";
455
+ purple: "purple";
456
+ pink: "pink";
457
+ }>, z.ZodString]>>>;
393
458
  content: z.ZodOptional<z.ZodString>;
394
459
  value: z.ZodOptional<z.ZodString>;
395
460
  }, z.core.$strip>>;
@@ -399,6 +464,10 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
399
464
  none: "none";
400
465
  lg: "lg";
401
466
  }>>;
467
+ cellAspectRatio: z.ZodOptional<z.ZodEnum<{
468
+ auto: "auto";
469
+ square: "square";
470
+ }>>;
402
471
  rowHeight: z.ZodOptional<z.ZodNumber>;
403
472
  select: z.ZodOptional<z.ZodEnum<{
404
473
  multiple: "multiple";
@@ -42,7 +42,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
42
42
  },
43
43
  toggle_group: {
44
44
  props: toggleGroupProps,
45
- description: "Single or multi-select choice group; `name` becomes POST inputs key. mode: single (default) | multiple. Optional label.",
45
+ description: "Single or multi-select choice group; `name` becomes POST inputs key. The @farcaster/snap React/React Native components choose row/column orientation from option label length, ignoring snap-sent orientation hints.",
46
46
  },
47
47
  input: {
48
48
  props: inputProps,
@@ -50,7 +50,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
50
50
  },
51
51
  item: {
52
52
  props: itemProps,
53
- description: "Content row with title and optional description. Children render in the actions slot (right side) — badge, button, and icon elements are all valid. The item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.",
53
+ description: "Content row matching shadcn Item: optional media renders on the left, title and optional description render in the content area, and children render in the actions slot (right side). The item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.",
54
54
  },
55
55
  item_group: {
56
56
  props: itemGroupProps,
@@ -78,7 +78,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
78
78
  },
79
79
  stack: {
80
80
  props: stackProps,
81
- description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order. Horizontal stacks use a single flex row so peers stay side-by-side and shrink with min-width 0. Nested stacks participate as flexible row peers. All-button horizontal stacks use an equal N-column grid where N is the number of buttons (1–6). Optional `columns` (`2`–`6`) forces an explicit equal grid for mixed children.",
81
+ description: "Layout container — direction: vertical (default) | horizontal. Children are element ids in order. Horizontal stacks use a single flex row so peers stay side-by-side and shrink with min-width 0. Nested stacks participate as flexible row peers. For all-button stacks, the @farcaster/snap React/React Native components choose row/column orientation from button label length, ignoring snap-sent direction hints; horizontal button rows use content-proportional widths while filling the container unless `equalWidth: true` is provided to force equal-width cells. Vertical button rows default to a tighter gap when `gap` is omitted.",
82
82
  },
83
83
  text: {
84
84
  props: textProps,
@@ -90,7 +90,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
90
90
  },
91
91
  cell_grid: {
92
92
  props: cellGridProps,
93
- description: "Cell grid — sparse colored cells on a rows×cols grid. Cell color is a palette name or literal #rrggbb hex (hex ignores page accent). Two interaction modes: leave select 'off' and bind on.press to fire an action per cell press (inputs[name] is the pressed 'row,col' before the action runs); or set select 'single'/'multiple' for press-to-select with a visual ring (no auto-fire — pair with a separate submit button). on.press is ignored when select is on.",
93
+ description: "Cell grid — sparse colored cells on a rows×cols grid. Cell color and textColor are palette names or literal #rrggbb hex values (hex ignores page accent); textColor overrides the default auto-contrast text color. Two interaction modes: leave select 'off' and bind on.press to fire an action per cell press (inputs[name] is the pressed 'row,col' before the action runs); or set select 'single'/'multiple' for press-to-select with a visual ring (no auto-fire — pair with a separate submit button). on.press is ignored when select is on.",
94
94
  },
95
95
  },
96
96
  actions: {
@@ -16,6 +16,16 @@ export declare const cellGridProps: z.ZodObject<{
16
16
  purple: "purple";
17
17
  pink: "pink";
18
18
  }>, z.ZodString]>>>;
19
+ textColor: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodUnion<readonly [z.ZodEnum<{
20
+ gray: "gray";
21
+ blue: "blue";
22
+ red: "red";
23
+ amber: "amber";
24
+ green: "green";
25
+ teal: "teal";
26
+ purple: "purple";
27
+ pink: "pink";
28
+ }>, z.ZodString]>>>;
19
29
  content: z.ZodOptional<z.ZodString>;
20
30
  value: z.ZodOptional<z.ZodString>;
21
31
  }, z.core.$strip>>;
@@ -25,6 +35,10 @@ export declare const cellGridProps: z.ZodObject<{
25
35
  none: "none";
26
36
  lg: "lg";
27
37
  }>>;
38
+ cellAspectRatio: z.ZodOptional<z.ZodEnum<{
39
+ auto: "auto";
40
+ square: "square";
41
+ }>>;
28
42
  rowHeight: z.ZodOptional<z.ZodNumber>;
29
43
  select: z.ZodOptional<z.ZodEnum<{
30
44
  multiple: "multiple";
@@ -1,19 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import { isSnapHexColorString, PALETTE_COLOR_VALUES } from "../colors.js";
3
- import { GRID_MIN_COLS, GRID_MAX_COLS, GRID_MIN_ROWS, GRID_MAX_ROWS, GRID_GAP_VALUES, } from "../constants.js";
3
+ import { GRID_MIN_COLS, GRID_MAX_COLS, GRID_MIN_ROWS, GRID_MAX_ROWS, GRID_GAP_VALUES, GRID_CELL_ASPECT_RATIO_VALUES, } from "../constants.js";
4
4
  /** Palette name or `#rrggbb`; input is trimmed so palette and hex rules match runtime resolvers. */
5
- const cellGridCellColorSchema = z.preprocess((v) => (typeof v === "string" ? v.trim() : v), z.union([
5
+ const cellGridCellColorSchema = (field) => z.preprocess((v) => (typeof v === "string" ? v.trim() : v), z.union([
6
6
  z.enum(PALETTE_COLOR_VALUES),
7
- z
8
- .string()
9
- .refine(isSnapHexColorString, {
10
- message: "cell_grid cell hex color must be #rrggbb",
7
+ z.string().refine(isSnapHexColorString, {
8
+ message: `cell_grid cell ${field} hex must be #rrggbb`,
11
9
  }),
12
10
  ]));
13
11
  const cellGridCellSchema = z.object({
14
12
  row: z.number().int().nonnegative(),
15
13
  col: z.number().int().nonnegative(),
16
- color: cellGridCellColorSchema.optional(),
14
+ color: cellGridCellColorSchema("color").optional(),
15
+ textColor: cellGridCellColorSchema("textColor").optional(),
17
16
  content: z.string().optional(),
18
17
  value: z.string().min(1).max(30).optional(),
19
18
  });
@@ -24,6 +23,7 @@ export const cellGridProps = z
24
23
  rows: z.number().int().min(GRID_MIN_ROWS).max(GRID_MAX_ROWS),
25
24
  cells: z.array(cellGridCellSchema),
26
25
  gap: z.enum(GRID_GAP_VALUES).optional(),
26
+ cellAspectRatio: z.enum(GRID_CELL_ASPECT_RATIO_VALUES).optional(),
27
27
  rowHeight: z.number().int().min(8).max(64).optional(),
28
28
  select: z.enum(["off", "single", "multiple"]).optional(),
29
29
  })
@@ -10,8 +10,8 @@ export { toggleGroupProps } from "./toggle-group.js";
10
10
  export type { ToggleGroupProps } from "./toggle-group.js";
11
11
  export { inputProps } from "./input.js";
12
12
  export type { InputProps } from "./input.js";
13
- export { itemProps } from "./item.js";
14
- export type { ItemProps } from "./item.js";
13
+ export { itemProps, itemMediaProps } from "./item.js";
14
+ export type { ItemMediaProps, ItemProps } from "./item.js";
15
15
  export { itemGroupProps } from "./item-group.js";
16
16
  export type { ItemGroupProps } from "./item-group.js";
17
17
  export { iconProps, ICON_NAMES } from "./icon.js";
package/dist/ui/index.js CHANGED
@@ -5,7 +5,7 @@ export { buttonProps } from "./button.js";
5
5
  export { switchProps } from "./switch.js";
6
6
  export { toggleGroupProps } from "./toggle-group.js";
7
7
  export { inputProps } from "./input.js";
8
- export { itemProps } from "./item.js";
8
+ export { itemProps, itemMediaProps } from "./item.js";
9
9
  export { itemGroupProps } from "./item-group.js";
10
10
  export { iconProps, ICON_NAMES } from "./icon.js";
11
11
  export { imageProps } from "./image.js";
package/dist/ui/item.d.ts CHANGED
@@ -1,12 +1,123 @@
1
1
  import { z } from "zod";
2
2
  export declare const ITEM_VARIANTS: readonly ["default"];
3
+ export declare const ITEM_MEDIA_VARIANTS: readonly ["icon", "image"];
3
4
  export declare const ITEM_MAX_TITLE_CHARS = 100;
4
5
  export declare const ITEM_MAX_DESCRIPTION_CHARS = 160;
6
+ export declare const ITEM_MAX_MEDIA_ALT_CHARS = 120;
7
+ export declare const itemMediaProps: z.ZodDiscriminatedUnion<[z.ZodObject<{
8
+ variant: z.ZodLiteral<"icon">;
9
+ name: z.ZodEnum<{
10
+ "arrow-right": "arrow-right";
11
+ "arrow-left": "arrow-left";
12
+ "external-link": "external-link";
13
+ "chevron-right": "chevron-right";
14
+ check: "check";
15
+ x: "x";
16
+ "alert-triangle": "alert-triangle";
17
+ info: "info";
18
+ clock: "clock";
19
+ heart: "heart";
20
+ "message-circle": "message-circle";
21
+ repeat: "repeat";
22
+ share: "share";
23
+ user: "user";
24
+ users: "users";
25
+ star: "star";
26
+ trophy: "trophy";
27
+ zap: "zap";
28
+ flame: "flame";
29
+ gift: "gift";
30
+ image: "image";
31
+ play: "play";
32
+ pause: "pause";
33
+ wallet: "wallet";
34
+ coins: "coins";
35
+ plus: "plus";
36
+ minus: "minus";
37
+ "refresh-cw": "refresh-cw";
38
+ bookmark: "bookmark";
39
+ "thumbs-up": "thumbs-up";
40
+ "thumbs-down": "thumbs-down";
41
+ "trending-up": "trending-up";
42
+ "trending-down": "trending-down";
43
+ }>;
44
+ color: z.ZodOptional<z.ZodEnum<{
45
+ gray: "gray";
46
+ blue: "blue";
47
+ red: "red";
48
+ amber: "amber";
49
+ green: "green";
50
+ teal: "teal";
51
+ purple: "purple";
52
+ pink: "pink";
53
+ accent: "accent";
54
+ }>>;
55
+ }, z.core.$strict>, z.ZodObject<{
56
+ variant: z.ZodLiteral<"image">;
57
+ url: z.ZodString;
58
+ alt: z.ZodOptional<z.ZodString>;
59
+ round: z.ZodOptional<z.ZodBoolean>;
60
+ }, z.core.$strict>], "variant">;
5
61
  export declare const itemProps: z.ZodObject<{
6
62
  title: z.ZodString;
7
63
  description: z.ZodOptional<z.ZodString>;
8
64
  variant: z.ZodOptional<z.ZodEnum<{
9
65
  default: "default";
10
66
  }>>;
11
- }, z.core.$strip>;
67
+ media: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
68
+ variant: z.ZodLiteral<"icon">;
69
+ name: z.ZodEnum<{
70
+ "arrow-right": "arrow-right";
71
+ "arrow-left": "arrow-left";
72
+ "external-link": "external-link";
73
+ "chevron-right": "chevron-right";
74
+ check: "check";
75
+ x: "x";
76
+ "alert-triangle": "alert-triangle";
77
+ info: "info";
78
+ clock: "clock";
79
+ heart: "heart";
80
+ "message-circle": "message-circle";
81
+ repeat: "repeat";
82
+ share: "share";
83
+ user: "user";
84
+ users: "users";
85
+ star: "star";
86
+ trophy: "trophy";
87
+ zap: "zap";
88
+ flame: "flame";
89
+ gift: "gift";
90
+ image: "image";
91
+ play: "play";
92
+ pause: "pause";
93
+ wallet: "wallet";
94
+ coins: "coins";
95
+ plus: "plus";
96
+ minus: "minus";
97
+ "refresh-cw": "refresh-cw";
98
+ bookmark: "bookmark";
99
+ "thumbs-up": "thumbs-up";
100
+ "thumbs-down": "thumbs-down";
101
+ "trending-up": "trending-up";
102
+ "trending-down": "trending-down";
103
+ }>;
104
+ color: z.ZodOptional<z.ZodEnum<{
105
+ gray: "gray";
106
+ blue: "blue";
107
+ red: "red";
108
+ amber: "amber";
109
+ green: "green";
110
+ teal: "teal";
111
+ purple: "purple";
112
+ pink: "pink";
113
+ accent: "accent";
114
+ }>>;
115
+ }, z.core.$strict>, z.ZodObject<{
116
+ variant: z.ZodLiteral<"image">;
117
+ url: z.ZodString;
118
+ alt: z.ZodOptional<z.ZodString>;
119
+ round: z.ZodOptional<z.ZodBoolean>;
120
+ }, z.core.$strict>], "variant">>;
121
+ }, z.core.$strict>;
12
122
  export type ItemProps = z.infer<typeof itemProps>;
123
+ export type ItemMediaProps = z.infer<typeof itemMediaProps>;
package/dist/ui/item.js CHANGED
@@ -1,9 +1,35 @@
1
1
  import { z } from "zod";
2
+ import { PROGRESS_COLOR_VALUES } from "../colors.js";
3
+ import { ICON_NAMES } from "./icon.js";
2
4
  export const ITEM_VARIANTS = ["default"];
5
+ export const ITEM_MEDIA_VARIANTS = ["icon", "image"];
3
6
  export const ITEM_MAX_TITLE_CHARS = 100;
4
7
  export const ITEM_MAX_DESCRIPTION_CHARS = 160;
5
- export const itemProps = z.object({
8
+ export const ITEM_MAX_MEDIA_ALT_CHARS = 120;
9
+ const itemIconMediaProps = z
10
+ .object({
11
+ variant: z.literal("icon"),
12
+ name: z.enum(ICON_NAMES),
13
+ color: z.enum(PROGRESS_COLOR_VALUES).optional(),
14
+ })
15
+ .strict();
16
+ const itemImageMediaProps = z
17
+ .object({
18
+ variant: z.literal("image"),
19
+ url: z.string(),
20
+ alt: z.string().max(ITEM_MAX_MEDIA_ALT_CHARS).optional(),
21
+ round: z.boolean().optional(),
22
+ })
23
+ .strict();
24
+ export const itemMediaProps = z.discriminatedUnion("variant", [
25
+ itemIconMediaProps,
26
+ itemImageMediaProps,
27
+ ]);
28
+ export const itemProps = z
29
+ .object({
6
30
  title: z.string().min(1).max(ITEM_MAX_TITLE_CHARS),
7
31
  description: z.string().max(ITEM_MAX_DESCRIPTION_CHARS).optional(),
8
32
  variant: z.enum(ITEM_VARIANTS).optional(),
9
- });
33
+ media: itemMediaProps.optional(),
34
+ })
35
+ .strict();
@@ -20,6 +20,7 @@ export declare const stackProps: z.ZodObject<{
20
20
  between: "between";
21
21
  around: "around";
22
22
  }>>;
23
+ equalWidth: z.ZodOptional<z.ZodBoolean>;
23
24
  columns: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<2>, z.ZodLiteral<3>, z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>]>>;
24
25
  }, z.core.$strip>;
25
26
  export type StackProps = z.infer<typeof stackProps>;
package/dist/ui/stack.js CHANGED
@@ -6,7 +6,9 @@ export const stackProps = z.object({
6
6
  direction: z.enum(STACK_DIRECTIONS).optional(),
7
7
  gap: z.enum(STACK_GAPS).optional(),
8
8
  justify: z.enum(STACK_JUSTIFY).optional(),
9
- /** Horizontal stacks only: fixed column grid (`2`–`6`). Prefer omitting this when children are stacks — they flex as row peers automatically. */
9
+ /** Horizontal stacks only: make direct children equal width. */
10
+ equalWidth: z.boolean().optional(),
11
+ /** Horizontal stacks only: legacy fixed equal-width column count (`2`–`6`). Prefer `equalWidth`. */
10
12
  columns: z.union([
11
13
  z.literal(2),
12
14
  z.literal(3),