@farcaster/snap 2.1.2 → 2.3.0

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/dist/colors.d.ts +10 -0
  3. package/dist/colors.js +22 -0
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.js +5 -1
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +3 -3
  8. package/dist/react/components/cell-grid.js +25 -17
  9. package/dist/react/components/stack.js +19 -6
  10. package/dist/react/components/text.js +9 -2
  11. package/dist/react/hooks/use-snap-colors.js +2 -9
  12. package/dist/react-native/components/snap-cell-grid.js +25 -16
  13. package/dist/react-native/components/snap-stack.js +17 -7
  14. package/dist/react-native/use-snap-palette.js +2 -2
  15. package/dist/schemas.d.ts +52 -0
  16. package/dist/schemas.js +10 -4
  17. package/dist/server/index.d.ts +2 -1
  18. package/dist/server/index.js +2 -1
  19. package/dist/server/parseRequest.d.ts +4 -3
  20. package/dist/server/parseRequest.js +91 -67
  21. package/dist/server/verify.d.ts +12 -5
  22. package/dist/server/verify.js +67 -19
  23. package/dist/stack-horizontal-utils.d.ts +6 -0
  24. package/dist/stack-horizontal-utils.js +14 -0
  25. package/dist/ui/catalog.d.ts +3 -2
  26. package/dist/ui/catalog.js +1 -1
  27. package/dist/ui/cell-grid.d.ts +3 -2
  28. package/dist/ui/cell-grid.js +12 -2
  29. package/dist/verify.test.js +3 -3
  30. package/llms.txt +14 -4
  31. package/package.json +1 -1
  32. package/src/colors.ts +27 -0
  33. package/src/constants.ts +6 -1
  34. package/src/index.ts +6 -0
  35. package/src/react/components/cell-grid.tsx +28 -16
  36. package/src/react/components/stack.tsx +21 -5
  37. package/src/react/components/text.tsx +9 -2
  38. package/src/react/hooks/use-snap-colors.ts +3 -8
  39. package/src/react-native/components/snap-cell-grid.tsx +28 -15
  40. package/src/react-native/components/snap-stack.tsx +19 -7
  41. package/src/react-native/use-snap-palette.ts +2 -1
  42. package/src/schemas.ts +14 -4
  43. package/src/server/index.ts +7 -1
  44. package/src/server/parseRequest.ts +117 -71
  45. package/src/server/verify.ts +99 -26
  46. package/src/stack-horizontal-utils.ts +14 -0
  47. package/src/ui/catalog.ts +1 -1
  48. package/src/ui/cell-grid.ts +16 -2
  49. package/src/verify.test.ts +3 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Neynar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/dist/colors.d.ts CHANGED
@@ -24,6 +24,16 @@ export declare const PALETTE_COLOR_ACCENT: "accent";
24
24
  export declare const DEFAULT_THEME_ACCENT: "purple";
25
25
  export declare const PALETTE_COLOR_VALUES: readonly ["gray", "blue", "red", "amber", "green", "teal", "purple", "pink"];
26
26
  export type PaletteColor = (typeof PALETTE_COLOR_VALUES)[number];
27
+ export declare function isSnapHexColorString(s: string): boolean;
28
+ /**
29
+ * Resolve a snap color token for inline styles: `accent`, palette names, or
30
+ * literal `#rrggbb`. Unknown values fall back to `accentHex` (same as legacy
31
+ * `colorHex` behavior for non-hex strings).
32
+ */
33
+ export declare function resolveSnapColorHex(color: string | undefined, opts: {
34
+ accentHex: string;
35
+ appearance: "light" | "dark";
36
+ }): string;
27
37
  /** Light-mode hex for each palette color (emulator / reference client). */
28
38
  export declare const PALETTE_LIGHT_HEX: Record<PaletteColor, string>;
29
39
  /** Dark-mode hex for each palette color (reference). */
package/dist/colors.js CHANGED
@@ -32,6 +32,28 @@ export const PALETTE_COLOR_VALUES = [
32
32
  PALETTE_COLOR.purple,
33
33
  PALETTE_COLOR.pink,
34
34
  ];
35
+ /** Strict `#rrggbb` literal used by cell_grid (and clients that accept hex). */
36
+ const SNAP_HEX_6 = /^#[0-9a-fA-F]{6}$/;
37
+ export function isSnapHexColorString(s) {
38
+ return SNAP_HEX_6.test(s.trim());
39
+ }
40
+ /**
41
+ * Resolve a snap color token for inline styles: `accent`, palette names, or
42
+ * literal `#rrggbb`. Unknown values fall back to `accentHex` (same as legacy
43
+ * `colorHex` behavior for non-hex strings).
44
+ */
45
+ export function resolveSnapColorHex(color, opts) {
46
+ if (!color || color === PALETTE_COLOR_ACCENT)
47
+ return opts.accentHex;
48
+ const trimmed = color.trim();
49
+ if (isSnapHexColorString(trimmed))
50
+ return trimmed;
51
+ const map = opts.appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
52
+ if (Object.hasOwn(map, trimmed)) {
53
+ return map[trimmed];
54
+ }
55
+ return opts.accentHex;
56
+ }
35
57
  /** Light-mode hex for each palette color (emulator / reference client). */
36
58
  export const PALETTE_LIGHT_HEX = {
37
59
  gray: "#6E6A86",
@@ -3,6 +3,7 @@ export declare const SPEC_VERSION_2: "2.0";
3
3
  export declare const SPEC_VERSION: "2.0";
4
4
  export declare const SUPPORTED_SPEC_VERSIONS: readonly ["1.0", "2.0"];
5
5
  export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
6
+ export declare const SNAP_PAYLOAD_HEADER: "X-Snap-Payload";
6
7
  export declare const MEDIA_TYPE: "application/vnd.farcaster.snap+json";
7
8
  export declare const EFFECT_VALUES: readonly ["confetti"];
8
9
  export declare const POST_GRID_TAP_KEY: "grid_tap";
package/dist/constants.js CHANGED
@@ -1,7 +1,11 @@
1
1
  export const SPEC_VERSION_1 = "1.0";
2
2
  export const SPEC_VERSION_2 = "2.0";
3
3
  export const SPEC_VERSION = SPEC_VERSION_2;
4
- export const SUPPORTED_SPEC_VERSIONS = [SPEC_VERSION_1, SPEC_VERSION_2];
4
+ export const SUPPORTED_SPEC_VERSIONS = [
5
+ SPEC_VERSION_1,
6
+ SPEC_VERSION_2,
7
+ ];
8
+ export const SNAP_PAYLOAD_HEADER = "X-Snap-Payload";
5
9
  export const MEDIA_TYPE = "application/vnd.farcaster.snap+json";
6
10
  export const EFFECT_VALUES = ["confetti"];
7
11
  // ─── Pixel grid ────────────────────────────────────────
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { Spec as SnapSpec, UIElement as SnapUIElement, } from "@json-render/core";
2
- export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, type SpecVersion, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
3
- export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, type PaletteColor, } from "./colors.js";
4
- export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, type SnapAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapElementInput, type SnapSpecInput, type SnapFunction, type SnapPayload, } from "./schemas.js";
2
+ export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, type SpecVersion, SNAP_PAYLOAD_HEADER, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
3
+ export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, isSnapHexColorString, resolveSnapColorHex, type PaletteColor, } from "./colors.js";
4
+ export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, getPayloadSchema, type SnapAction, type SnapGetAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapElementInput, type SnapSpecInput, type SnapFunction, type SnapPayload, type SnapGetPayload, } from "./schemas.js";
5
5
  export { validateSnapResponse, type ValidationResult } from "./validator.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
2
- export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "./colors.js";
3
- export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, } from "./schemas.js";
1
+ export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, SNAP_PAYLOAD_HEADER, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
2
+ export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, isSnapHexColorString, resolveSnapColorHex, } from "./colors.js";
3
+ export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, getPayloadSchema, } from "./schemas.js";
4
4
  export { validateSnapResponse } from "./validator.js";
@@ -22,38 +22,46 @@ export function SnapCellGrid({ element: { props, on }, emit, }) {
22
22
  const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
23
23
  const tapPath = `/inputs/${name}`;
24
24
  const tapRaw = get(tapPath);
25
- // Parse selection single mode: "row,col" string; multi mode: "row,col|row,col|..." string
25
+ const cellMap = new Map();
26
+ for (const c of cells) {
27
+ cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
28
+ color: c.color,
29
+ content: c.content != null ? String(c.content) : undefined,
30
+ value: typeof c.value === "string" ? c.value : undefined,
31
+ });
32
+ }
33
+ // Each cell's wire value — its `value` if set, otherwise "row,col" fallback.
34
+ const cellWireValue = (r, c) => cellMap.get(`${r},${c}`)?.value ?? `${r},${c}`;
35
+ // Multi mode joins values with `|`; single mode is the value itself.
26
36
  const selectedSet = new Set();
27
37
  if (typeof tapRaw === "string" && tapRaw.length > 0) {
28
- for (const part of tapRaw.split("|")) {
29
- if (part.includes(","))
30
- selectedSet.add(part);
38
+ if (isMultiple) {
39
+ for (const part of tapRaw.split("|")) {
40
+ if (part.length > 0)
41
+ selectedSet.add(part);
42
+ }
43
+ }
44
+ else {
45
+ selectedSet.add(tapRaw);
31
46
  }
32
47
  }
33
- const isSelected = (r, c) => isSelectable && selectedSet.has(`${r},${c}`);
48
+ const isSelected = (r, c) => isSelectable && selectedSet.has(cellWireValue(r, c));
34
49
  const handleTap = (r, c) => {
35
- const key = `${r},${c}`;
50
+ const wire = cellWireValue(r, c);
36
51
  if (isMultiple) {
37
52
  const next = new Set(selectedSet);
38
- if (next.has(key))
39
- next.delete(key);
53
+ if (next.has(wire))
54
+ next.delete(wire);
40
55
  else
41
- next.add(key);
56
+ next.add(wire);
42
57
  set(tapPath, [...next].join("|"));
43
58
  }
44
59
  else {
45
- set(tapPath, key);
60
+ set(tapPath, wire);
46
61
  }
47
62
  if (hasPressAction)
48
63
  emit("press");
49
64
  };
50
- const cellMap = new Map();
51
- for (const c of cells) {
52
- cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
53
- color: c.color,
54
- content: c.content != null ? String(c.content) : undefined,
55
- });
56
- }
57
65
  /** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
58
66
  const emptyCellBg = colors.mode === "dark" ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.05)";
59
67
  const cellEls = [];
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { cn } from "@neynar/ui/utils";
4
- import { countRenderableChildren, horizontalChildrenAreAllButtons, } from "../../stack-horizontal-utils.js";
4
+ import { countRenderableChildren, defaultHorizontalGapSize, horizontalChildrenAreAllButtons, } from "../../stack-horizontal-utils.js";
5
5
  import { SnapStackDirectionProvider, useSnapStackDirection, } from "../stack-direction-context.js";
6
6
  const VGAP = {
7
7
  none: "gap-0",
@@ -13,7 +13,7 @@ const HGAP = {
13
13
  none: "gap-0",
14
14
  sm: "gap-1",
15
15
  md: "gap-2",
16
- lg: "gap-3",
16
+ lg: "gap-4",
17
17
  };
18
18
  const JUSTIFY_FLEX = {
19
19
  start: "justify-start",
@@ -34,11 +34,7 @@ const COLUMN_GRID_CLASS = {
34
34
  export function SnapStack({ element: { props }, children, }) {
35
35
  const parentDirection = useSnapStackDirection();
36
36
  const direction = String(props.direction ?? "vertical");
37
- const gapKey = String(props.gap ?? "md");
38
37
  const isHorizontal = direction === "horizontal";
39
- const gap = isHorizontal
40
- ? (HGAP[gapKey] ?? "gap-2")
41
- : (VGAP[gapKey] ?? "gap-4");
42
38
  const justifyKey = props.justify ? String(props.justify) : undefined;
43
39
  const justifyFlex = justifyKey ? JUSTIFY_FLEX[justifyKey] : undefined;
44
40
  const buttonRowGrid = isHorizontal && horizontalChildrenAreAllButtons(children);
@@ -52,6 +48,23 @@ export function SnapStack({ element: { props }, children, }) {
52
48
  Number.isInteger(columnsRaw)
53
49
  ? columnsRaw
54
50
  : undefined;
51
+ // Horizontal default depends on column count: 2→lg, 3→md, 4+→sm. Vertical stays md.
52
+ // Count comes from explicit `columns`, then button-row inference, else direct children
53
+ // count (any horizontal stack is N columns wide regardless of child types).
54
+ const horizontalColumnCount = isHorizontal
55
+ ? (columns ??
56
+ (buttonRowGrid ? buttonRowCount : undefined) ??
57
+ countRenderableChildren(children))
58
+ : undefined;
59
+ const explicitGap = typeof props.gap === "string" && props.gap in (isHorizontal ? HGAP : VGAP);
60
+ const gapKey = explicitGap
61
+ ? String(props.gap)
62
+ : isHorizontal
63
+ ? defaultHorizontalGapSize(horizontalColumnCount)
64
+ : "md";
65
+ const gap = isHorizontal
66
+ ? (HGAP[gapKey] ?? HGAP.md)
67
+ : (VGAP[gapKey] ?? VGAP.md);
55
68
  const explicitColumnGrid = isHorizontal && columns !== undefined && !buttonRowGrid;
56
69
  const columnGridClass = explicitColumnGrid && columns !== undefined
57
70
  ? COLUMN_GRID_CLASS[columns]
@@ -18,6 +18,13 @@ export function SnapText({ element: { props }, }) {
18
18
  const stackDir = useSnapStackDirection();
19
19
  const inHorizontalStack = stackDir === "horizontal";
20
20
  return (_jsx(Text, { size: config.textSize, weight: weight, align: align, className: cn(
21
- /** Row peers hug content like RN `wrapRow`; avoid `flex-1` stretching peers across the row. */
22
- inHorizontalStack ? "min-w-0 shrink" : "flex-1"), style: { color: colors.text }, children: content }));
21
+ /**
22
+ * Row peers hug content like RN `wrapRow` — `min-w-0 shrink` lets text wrap
23
+ * inside a horizontal stack without forcing peers wide. In a vertical stack
24
+ * the `<p>` already fills its parent's width via `display: block`; avoid
25
+ * `flex-1` here because `flex-grow: 1` on a vertical-flex child fills the
26
+ * column's height, distributing siblings when the row is taller than its
27
+ * content (e.g. text next to a tall image).
28
+ */
29
+ inHorizontalStack ? "min-w-0 shrink" : "min-w-0"), style: { color: colors.text }, children: content }));
23
30
  }
@@ -3,7 +3,7 @@ import { useMemo } from "react";
3
3
  import { useStateStore } from "@json-render/react";
4
4
  import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
5
5
  import { useSnapPreviewPageAccent, useSnapAppearance } from "../accent-context.js";
6
- import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX } from "@farcaster/snap";
6
+ import { resolveSnapColorHex } from "@farcaster/snap";
7
7
  /** Readable foreground color (black or white) for a given hex background. */
8
8
  export function pickForegroundForBg(hex) {
9
9
  const h = hex.replace(/^#/, "");
@@ -37,19 +37,12 @@ function buildSnapColors(accentName, mode) {
37
37
  const accent = resolveSnapPaletteHex(accentName, mode);
38
38
  const accentFg = pickForegroundForBg(accent);
39
39
  const neutrals = mode === "dark" ? NEUTRAL_DARK : NEUTRAL_LIGHT;
40
- const paletteMap = mode === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
41
40
  const accentHover = mode === "light"
42
41
  ? `color-mix(in srgb, ${accent} 82%, #000000)`
43
42
  : `color-mix(in srgb, ${accent} 78%, #ffffff)`;
44
43
  const outlineHover = `color-mix(in srgb, ${accent} 14%, ${neutrals.surface})`;
45
44
  const paletteHex = (name) => resolveSnapPaletteHex(name, mode);
46
- const colorHex = (name) => {
47
- if (!name || name === "accent")
48
- return accent;
49
- if (Object.hasOwn(paletteMap, name))
50
- return paletteMap[name];
51
- return accent;
52
- };
45
+ const colorHex = (name) => resolveSnapColorHex(name, { accentHex: accent, appearance: mode });
53
46
  return {
54
47
  accent,
55
48
  accentFg,
@@ -25,37 +25,46 @@ export function SnapCellGrid({ element, emit, }) {
25
25
  const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
26
26
  const tapPath = `/inputs/${name}`;
27
27
  const tapRaw = get(tapPath);
28
+ const cellMap = new Map();
29
+ for (const c of cells) {
30
+ cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
31
+ color: c.color,
32
+ content: c.content != null ? String(c.content) : undefined,
33
+ value: typeof c.value === "string" ? c.value : undefined,
34
+ });
35
+ }
36
+ // Each cell's wire value — its `value` if set, otherwise "row,col" fallback.
37
+ const cellWireValue = (r, c) => cellMap.get(`${r},${c}`)?.value ?? `${r},${c}`;
38
+ // Multi mode joins values with `|`; single mode is the value itself.
28
39
  const selectedSet = new Set();
29
40
  if (typeof tapRaw === "string" && tapRaw.length > 0) {
30
- for (const part of tapRaw.split("|")) {
31
- if (part.includes(","))
32
- selectedSet.add(part);
41
+ if (isMultiple) {
42
+ for (const part of tapRaw.split("|")) {
43
+ if (part.length > 0)
44
+ selectedSet.add(part);
45
+ }
46
+ }
47
+ else {
48
+ selectedSet.add(tapRaw);
33
49
  }
34
50
  }
35
- const isSelected = (r, c) => isSelectable && selectedSet.has(`${r},${c}`);
51
+ const isSelected = (r, c) => isSelectable && selectedSet.has(cellWireValue(r, c));
36
52
  const handleTap = (r, c) => {
37
- const key = `${r},${c}`;
53
+ const wire = cellWireValue(r, c);
38
54
  if (isMultiple) {
39
55
  const next = new Set(selectedSet);
40
- if (next.has(key))
41
- next.delete(key);
56
+ if (next.has(wire))
57
+ next.delete(wire);
42
58
  else
43
- next.add(key);
59
+ next.add(wire);
44
60
  set(tapPath, [...next].join("|"));
45
61
  }
46
62
  else {
47
- set(tapPath, key);
63
+ set(tapPath, wire);
48
64
  }
49
65
  if (hasPressAction)
50
66
  emit("press");
51
67
  };
52
- const cellMap = new Map();
53
- for (const c of cells) {
54
- cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
55
- color: c.color,
56
- content: c.content != null ? String(c.content) : undefined,
57
- });
58
- }
59
68
  const ringOuter = appearance === "dark" ? "#fff" : "#000";
60
69
  const ringInner = appearance === "dark" ? "#000" : "#fff";
61
70
  /** Cells without a palette `color` — subtle fill so empty slots read as tiles. */
@@ -1,7 +1,7 @@
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, horizontalChildrenAreAllButtons, } from "../../stack-horizontal-utils.js";
4
+ import { countRenderableChildren, defaultHorizontalGapSize, horizontalChildrenAreAllButtons, } from "../../stack-horizontal-utils.js";
5
5
  import { SnapStackDirectionProvider, useSnapStackDirection, } from "../stack-direction-context.js";
6
6
  const VGAP = {
7
7
  none: 0,
@@ -13,7 +13,7 @@ const HGAP = {
13
13
  none: 0,
14
14
  sm: 4,
15
15
  md: 8,
16
- lg: 12,
16
+ lg: 16,
17
17
  };
18
18
  const JUSTIFY = {
19
19
  start: "flex-start",
@@ -33,11 +33,6 @@ export function SnapStack({ element: { props }, children, }) {
33
33
  const rawGap = props.gap;
34
34
  const isHorizontal = direction === "horizontal";
35
35
  const gapMap = isHorizontal ? HGAP : VGAP;
36
- const gap = typeof rawGap === "number"
37
- ? rawGap
38
- : typeof rawGap === "string" && rawGap in gapMap
39
- ? gapMap[rawGap]
40
- : isHorizontal ? HGAP.md : VGAP.md;
41
36
  const buttonRowGrid = isHorizontal && horizontalChildrenAreAllButtons(children);
42
37
  const buttonRowCount = buttonRowGrid
43
38
  ? countRenderableChildren(children)
@@ -49,6 +44,21 @@ export function SnapStack({ element: { props }, children, }) {
49
44
  Number.isInteger(columnsRaw)
50
45
  ? columnsRaw
51
46
  : 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))
54
+ : undefined;
55
+ const gap = typeof rawGap === "number"
56
+ ? rawGap
57
+ : typeof rawGap === "string" && rawGap in gapMap
58
+ ? gapMap[rawGap]
59
+ : isHorizontal
60
+ ? gapMap[defaultHorizontalGapSize(horizontalColumnCount)]
61
+ : VGAP.md;
52
62
  const explicitColumnGrid = isHorizontal && columns !== undefined && !buttonRowGrid;
53
63
  const justify = props.justify &&
54
64
  (!isHorizontal || (!buttonRowGrid && !explicitColumnGrid))
@@ -1,4 +1,4 @@
1
- import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
1
+ import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, resolveSnapColorHex, } from "@farcaster/snap";
2
2
  import { useStateStore } from "@json-render/react-native";
3
3
  import { useSnapTheme } from "./theme.js";
4
4
  function resolveHex(name, appearance) {
@@ -23,7 +23,7 @@ export function useSnapPalette() {
23
23
  const { get } = useStateStore();
24
24
  const accentName = themeAccentFromStore(get);
25
25
  const accentHex = resolveHex(accentName, mode);
26
- const hex = (semantic) => semantic === "accent" ? accentHex : resolveHex(semantic, mode);
26
+ const hex = (semantic) => resolveSnapColorHex(semantic, { accentHex, appearance: mode });
27
27
  return { appearance: mode, accentName, accentHex, hex };
28
28
  }
29
29
  /** `#RRGGBB` + alpha → `rgba(...)` for React Native styles. */
package/dist/schemas.d.ts CHANGED
@@ -89,10 +89,46 @@ export declare const payloadSchema: z.ZodObject<{
89
89
  }, z.core.$strip>], "type">;
90
90
  }, z.core.$strip>;
91
91
  export type SnapPayload = z.infer<typeof payloadSchema>;
92
+ /** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
93
+ export declare const getPayloadSchema: z.ZodObject<{
94
+ user: z.ZodObject<{
95
+ fid: z.ZodNumber;
96
+ }, z.core.$strip>;
97
+ timestamp: z.ZodNumber;
98
+ audience: z.ZodString;
99
+ surface: z.ZodDiscriminatedUnion<[z.ZodObject<{
100
+ type: z.ZodLiteral<"cast">;
101
+ cast: z.ZodObject<{
102
+ hash: z.ZodString;
103
+ author: z.ZodObject<{
104
+ fid: z.ZodNumber;
105
+ }, z.core.$strip>;
106
+ }, z.core.$strip>;
107
+ }, z.core.$strip>, z.ZodObject<{
108
+ type: z.ZodLiteral<"standalone">;
109
+ }, z.core.$strip>], "type">;
110
+ }, z.core.$strip>;
111
+ export type SnapGetPayload = z.infer<typeof getPayloadSchema>;
92
112
  export declare const ACTION_TYPE_GET: "get";
93
113
  export declare const ACTION_TYPE_POST: "post";
94
114
  declare const snapGetActionSchema: z.ZodObject<{
95
115
  type: z.ZodLiteral<"get">;
116
+ user: z.ZodOptional<z.ZodObject<{
117
+ fid: z.ZodNumber;
118
+ }, z.core.$strip>>;
119
+ timestamp: z.ZodOptional<z.ZodNumber>;
120
+ audience: z.ZodOptional<z.ZodString>;
121
+ surface: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
122
+ type: z.ZodLiteral<"cast">;
123
+ cast: z.ZodObject<{
124
+ hash: z.ZodString;
125
+ author: z.ZodObject<{
126
+ fid: z.ZodNumber;
127
+ }, z.core.$strip>;
128
+ }, z.core.$strip>;
129
+ }, z.core.$strip>, z.ZodObject<{
130
+ type: z.ZodLiteral<"standalone">;
131
+ }, z.core.$strip>], "type">>;
96
132
  }, z.core.$strip>;
97
133
  export type SnapGetAction = z.infer<typeof snapGetActionSchema>;
98
134
  declare const snapPostActionSchema: z.ZodObject<{
@@ -119,6 +155,22 @@ declare const snapPostActionSchema: z.ZodObject<{
119
155
  export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
120
156
  export declare const snapActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
121
157
  type: z.ZodLiteral<"get">;
158
+ user: z.ZodOptional<z.ZodObject<{
159
+ fid: z.ZodNumber;
160
+ }, z.core.$strip>>;
161
+ timestamp: z.ZodOptional<z.ZodNumber>;
162
+ audience: z.ZodOptional<z.ZodString>;
163
+ surface: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
164
+ type: z.ZodLiteral<"cast">;
165
+ cast: z.ZodObject<{
166
+ hash: z.ZodString;
167
+ author: z.ZodObject<{
168
+ fid: z.ZodNumber;
169
+ }, z.core.$strip>;
170
+ }, z.core.$strip>;
171
+ }, z.core.$strip>, z.ZodObject<{
172
+ type: z.ZodLiteral<"standalone">;
173
+ }, z.core.$strip>], "type">>;
122
174
  }, z.core.$strip>, z.ZodObject<{
123
175
  fid: z.ZodOptional<z.ZodNumber>;
124
176
  inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
package/dist/schemas.js CHANGED
@@ -47,22 +47,28 @@ const surfaceSchema = z.discriminatedUnion("type", [
47
47
  castSurfaceSchema,
48
48
  standaloneSurfaceSchema,
49
49
  ]);
50
+ const fidSchema = z.number().int().nonnegative();
51
+ const userSchema = z.object({ fid: fidSchema });
50
52
  export const payloadSchema = z
51
53
  .object({
52
- fid: z.number().int().nonnegative().optional(), // deprecated in favor of user.fid
54
+ fid: fidSchema.optional(), // deprecated in favor of user.fid
53
55
  inputs: z.record(z.string(), postInputValueSchema).default({}),
54
56
  timestamp: z.number().int(),
55
57
  audience: z.string(),
56
- user: z.object({
57
- fid: z.number().int().nonnegative(),
58
- }),
58
+ user: userSchema,
59
59
  surface: surfaceSchema,
60
60
  })
61
61
  .strip();
62
+ /** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
63
+ export const getPayloadSchema = payloadSchema.omit({ inputs: true, fid: true });
62
64
  export const ACTION_TYPE_GET = "get";
63
65
  export const ACTION_TYPE_POST = "post";
64
66
  const snapGetActionSchema = z.object({
65
67
  type: z.literal(ACTION_TYPE_GET),
68
+ user: userSchema.optional(),
69
+ timestamp: z.number().int().optional(),
70
+ audience: z.string().optional(),
71
+ surface: surfaceSchema.optional(),
66
72
  });
67
73
  const snapPostActionSchema = payloadSchema.extend({
68
74
  type: z.literal(ACTION_TYPE_POST),
@@ -1,3 +1,4 @@
1
- export { verifyJFSRequestBody, decodePayload, encodePayload } from "./verify.js";
1
+ export { verifyJFS as verifyJFSRequestBody, // deprecated alias. drop in v3
2
+ parseJfs, verifyJFS, decodePayload, encodePayload, } from "./verify.js";
2
3
  export { DEFAULT_SNAP_HUB_HTTP_BASE_URL, getActiveEd25519SignerKeysFromHubHttp, } from "./hubs.js";
3
4
  export { parseRequest, type ParseRequestError, type ParseRequestOptions, type ParseRequestResult, } from "./parseRequest.js";
@@ -1,3 +1,4 @@
1
- export { verifyJFSRequestBody, decodePayload, encodePayload } from "./verify.js";
1
+ export { verifyJFS as verifyJFSRequestBody, // deprecated alias. drop in v3
2
+ parseJfs, verifyJFS, decodePayload, encodePayload, } from "./verify.js";
2
3
  export { DEFAULT_SNAP_HUB_HTTP_BASE_URL, getActiveEd25519SignerKeysFromHubHttp, } from "./hubs.js";
3
4
  export { parseRequest, } from "./parseRequest.js";
@@ -1,5 +1,5 @@
1
- import { type SnapAction } from "../schemas.js";
2
1
  import { z } from "zod";
2
+ import { type SnapAction } from "../schemas.js";
3
3
  export type ParseRequestError = {
4
4
  type: "method_not_allowed";
5
5
  message: string;
@@ -47,7 +47,8 @@ export type ParseRequestResult = {
47
47
  };
48
48
  /**
49
49
  * Parse and validate Farcaster snap requests:
50
- * - `GET` is allowed for first-page loads and returns `{ type: "get" }`.
51
- * - `POST`: the body must be JSON in JFS form (`header` / `payload` / `signature`) even if JFS verification is skipped.
50
+ * - `GET`: returns `{ type: "get" }`, or optional viewer fields when `X-Snap-Payload`
51
+ * carries a JFS compact string whose decoded payload validates against {@link getPayloadSchema}.
52
+ * - `POST`: the body must be a JFS envelope — either JSON `{ header, payload, signature }` or the same **compact** string form as GET (`BASE64URL(header).BASE64URL(payload).BASE64URL(signature)`), even if JFS verification is skipped.
52
53
  */
53
54
  export declare function parseRequest(request: Request, options?: ParseRequestOptions): Promise<ParseRequestResult>;