@farcaster/snap 2.0.0 → 2.0.2
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/dist/colors.d.ts +4 -4
- package/dist/colors.js +20 -20
- package/dist/constants.d.ts +17 -1
- package/dist/constants.js +19 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/react/accent-context.d.ts +3 -1
- package/dist/react/accent-context.js +7 -4
- package/dist/react/catalog-renderer.js +4 -0
- package/dist/react/components/action-button.d.ts +2 -1
- package/dist/react/components/action-button.js +35 -13
- package/dist/react/components/badge.js +8 -8
- package/dist/react/components/bar-chart.d.ts +5 -0
- package/dist/react/components/bar-chart.js +26 -0
- package/dist/react/components/cell-grid.d.ts +5 -0
- package/dist/react/components/cell-grid.js +87 -0
- package/dist/react/components/icon.js +4 -10
- package/dist/react/components/input.js +12 -6
- package/dist/react/components/item-group.js +3 -1
- package/dist/react/components/item.d.ts +3 -3
- package/dist/react/components/item.js +4 -3
- package/dist/react/components/progress.js +3 -3
- package/dist/react/components/separator.js +3 -1
- package/dist/react/components/slider.js +15 -10
- package/dist/react/components/switch.js +10 -12
- package/dist/react/components/text.js +6 -14
- package/dist/react/components/toggle-group.js +20 -6
- package/dist/react/hooks/use-snap-colors.d.ts +38 -0
- package/dist/react/hooks/use-snap-colors.js +81 -0
- package/dist/react/index.d.ts +13 -1
- package/dist/react/index.js +9 -188
- package/dist/react/snap-view-core.d.ts +11 -0
- package/dist/react/snap-view-core.js +227 -0
- package/dist/react/v1/snap-view.d.ts +16 -0
- package/dist/react/v1/snap-view.js +90 -0
- package/dist/react/v2/snap-view.d.ts +23 -0
- package/dist/react/v2/snap-view.js +91 -0
- package/dist/react-native/catalog-renderer.d.ts +5 -0
- package/dist/react-native/catalog-renderer.js +40 -0
- package/dist/react-native/components/snap-action-button.d.ts +2 -0
- package/dist/react-native/components/snap-action-button.js +69 -0
- package/dist/react-native/components/snap-badge.d.ts +2 -0
- package/dist/react-native/components/snap-badge.js +41 -0
- package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
- package/dist/react-native/components/snap-bar-chart.js +39 -0
- package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
- package/dist/react-native/components/snap-cell-grid.js +94 -0
- package/dist/react-native/components/snap-icon.d.ts +5 -0
- package/dist/react-native/components/snap-icon.js +56 -0
- package/dist/react-native/components/snap-image.d.ts +2 -0
- package/dist/react-native/components/snap-image.js +23 -0
- package/dist/react-native/components/snap-input.d.ts +2 -0
- package/dist/react-native/components/snap-input.js +37 -0
- package/dist/react-native/components/snap-item-group.d.ts +5 -0
- package/dist/react-native/components/snap-item-group.js +23 -0
- package/dist/react-native/components/snap-item.d.ts +5 -0
- package/dist/react-native/components/snap-item.js +42 -0
- package/dist/react-native/components/snap-progress.d.ts +2 -0
- package/dist/react-native/components/snap-progress.js +26 -0
- package/dist/react-native/components/snap-separator.d.ts +2 -0
- package/dist/react-native/components/snap-separator.js +23 -0
- package/dist/react-native/components/snap-slider.d.ts +2 -0
- package/dist/react-native/components/snap-slider.js +43 -0
- package/dist/react-native/components/snap-stack.d.ts +5 -0
- package/dist/react-native/components/snap-stack.js +49 -0
- package/dist/react-native/components/snap-switch.d.ts +2 -0
- package/dist/react-native/components/snap-switch.js +31 -0
- package/dist/react-native/components/snap-text.d.ts +2 -0
- package/dist/react-native/components/snap-text.js +35 -0
- package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
- package/dist/react-native/components/snap-toggle-group.js +99 -0
- package/dist/react-native/confetti-overlay.d.ts +1 -0
- package/dist/react-native/confetti-overlay.js +106 -0
- package/dist/react-native/index.d.ts +28 -0
- package/dist/react-native/index.js +15 -0
- package/dist/react-native/snap-view-core.d.ts +11 -0
- package/dist/react-native/snap-view-core.js +156 -0
- package/dist/react-native/theme.d.ts +27 -0
- package/dist/react-native/theme.js +43 -0
- package/dist/react-native/types.d.ts +42 -0
- package/dist/react-native/types.js +1 -0
- package/dist/react-native/use-snap-palette.d.ts +13 -0
- package/dist/react-native/use-snap-palette.js +48 -0
- package/dist/react-native/v1/snap-view.d.ts +24 -0
- package/dist/react-native/v1/snap-view.js +96 -0
- package/dist/react-native/v2/snap-view.d.ts +33 -0
- package/dist/react-native/v2/snap-view.js +114 -0
- package/dist/schemas.d.ts +100 -13
- package/dist/schemas.js +28 -10
- package/dist/server/parseRequest.d.ts +10 -0
- package/dist/server/parseRequest.js +48 -7
- package/dist/server/verify.d.ts +1 -0
- package/dist/server/verify.js +1 -0
- package/dist/ui/badge.d.ts +7 -2
- package/dist/ui/badge.js +2 -0
- package/dist/ui/bar-chart.d.ts +30 -0
- package/dist/ui/bar-chart.js +30 -0
- package/dist/ui/button.d.ts +4 -6
- package/dist/ui/button.js +1 -1
- package/dist/ui/catalog.d.ts +90 -16
- package/dist/ui/catalog.js +17 -3
- package/dist/ui/cell-grid.d.ts +34 -0
- package/dist/ui/cell-grid.js +39 -0
- package/dist/ui/icon.d.ts +2 -2
- package/dist/ui/image.d.ts +1 -2
- package/dist/ui/image.js +1 -1
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/item.d.ts +1 -3
- package/dist/ui/item.js +1 -1
- package/dist/ui/schema.d.ts +6 -2
- package/dist/ui/schema.js +2 -2
- package/dist/ui/slider.d.ts +1 -0
- package/dist/ui/slider.js +2 -0
- package/dist/ui/text.d.ts +2 -4
- package/dist/ui/text.js +2 -2
- package/dist/validator.d.ts +3 -2
- package/dist/validator.js +203 -2
- package/llms.txt +199 -0
- package/package.json +9 -3
- package/src/colors.ts +20 -20
- package/src/constants.ts +23 -1
- package/src/index.ts +16 -13
- package/src/react/accent-context.tsx +13 -6
- package/src/react/catalog-renderer.tsx +4 -0
- package/src/react/components/action-button.tsx +50 -20
- package/src/react/components/badge.tsx +14 -18
- package/src/react/components/bar-chart.tsx +69 -0
- package/src/react/components/cell-grid.tsx +128 -0
- package/src/react/components/icon.tsx +5 -18
- package/src/react/components/input.tsx +20 -9
- package/src/react/components/item-group.tsx +4 -1
- package/src/react/components/item.tsx +13 -10
- package/src/react/components/progress.tsx +12 -7
- package/src/react/components/separator.tsx +8 -1
- package/src/react/components/slider.tsx +28 -15
- package/src/react/components/switch.tsx +12 -16
- package/src/react/components/text.tsx +14 -23
- package/src/react/components/toggle-group.tsx +26 -9
- package/src/react/hooks/use-snap-colors.ts +128 -0
- package/src/react/index.tsx +49 -265
- package/src/react/snap-view-core.tsx +343 -0
- package/src/react/v1/snap-view.tsx +176 -0
- package/src/react/v2/snap-view.tsx +199 -0
- package/src/react-native/catalog-renderer.tsx +41 -0
- package/src/react-native/components/snap-action-button.tsx +96 -0
- package/src/react-native/components/snap-badge.tsx +60 -0
- package/src/react-native/components/snap-bar-chart.tsx +73 -0
- package/src/react-native/components/snap-cell-grid.tsx +150 -0
- package/src/react-native/components/snap-icon.tsx +102 -0
- package/src/react-native/components/snap-image.tsx +37 -0
- package/src/react-native/components/snap-input.tsx +58 -0
- package/src/react-native/components/snap-item-group.tsx +43 -0
- package/src/react-native/components/snap-item.tsx +66 -0
- package/src/react-native/components/snap-progress.tsx +40 -0
- package/src/react-native/components/snap-separator.tsx +32 -0
- package/src/react-native/components/snap-slider.tsx +85 -0
- package/src/react-native/components/snap-stack.tsx +66 -0
- package/src/react-native/components/snap-switch.tsx +46 -0
- package/src/react-native/components/snap-text.tsx +51 -0
- package/src/react-native/components/snap-toggle-group.tsx +127 -0
- package/src/react-native/confetti-overlay.tsx +134 -0
- package/src/react-native/index.tsx +83 -0
- package/src/react-native/snap-view-core.tsx +212 -0
- package/src/react-native/theme.tsx +85 -0
- package/src/react-native/types.ts +38 -0
- package/src/react-native/use-snap-palette.ts +64 -0
- package/src/react-native/v1/snap-view.tsx +229 -0
- package/src/react-native/v2/snap-view.tsx +283 -0
- package/src/schemas.ts +68 -17
- package/src/server/parseRequest.ts +68 -9
- package/src/server/verify.ts +2 -0
- package/src/ui/README.md +8 -8
- package/src/ui/badge.ts +2 -0
- package/src/ui/bar-chart.ts +38 -0
- package/src/ui/button.ts +1 -1
- package/src/ui/catalog.ts +19 -3
- package/src/ui/cell-grid.ts +49 -0
- package/src/ui/image.ts +1 -1
- package/src/ui/index.ts +6 -0
- package/src/ui/item.ts +1 -1
- package/src/ui/schema.ts +2 -2
- package/src/ui/slider.ts +2 -0
- package/src/ui/text.ts +2 -2
- package/src/validator.ts +251 -2
- package/dist/dataStore.d.ts +0 -12
- package/dist/dataStore.js +0 -35
- package/dist/middleware.d.ts +0 -3
- package/dist/middleware.js +0 -3
- package/dist/react/hooks/use-snap-accent.d.ts +0 -13
- package/dist/react/hooks/use-snap-accent.js +0 -32
- package/src/dataStore.ts +0 -62
- package/src/middleware.ts +0 -7
- package/src/react/hooks/use-snap-accent.ts +0 -45
|
@@ -1,47 +1,77 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ExternalLink } from "lucide-react";
|
|
3
5
|
import { Button } from "@neynar/ui/button";
|
|
4
6
|
import { cn } from "@neynar/ui/utils";
|
|
5
|
-
import {
|
|
7
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
6
8
|
import { ICON_MAP } from "./icon";
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
10
|
+
function isExternalLinkAction(
|
|
11
|
+
on: Record<string, unknown> | undefined,
|
|
12
|
+
): boolean {
|
|
13
|
+
if (!on) return false;
|
|
14
|
+
const press = on.press as
|
|
15
|
+
| { action?: string; params?: Record<string, unknown> }
|
|
16
|
+
| undefined;
|
|
17
|
+
if (!press) return false;
|
|
18
|
+
return press.action === "open_url";
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
export function SnapActionButton({
|
|
16
|
-
element
|
|
22
|
+
element,
|
|
17
23
|
emit,
|
|
18
24
|
}: {
|
|
19
|
-
element: {
|
|
25
|
+
element: {
|
|
26
|
+
props: Record<string, unknown>;
|
|
27
|
+
on?: Record<string, unknown>;
|
|
28
|
+
};
|
|
20
29
|
emit: (name: string) => void;
|
|
21
30
|
}) {
|
|
31
|
+
const { props } = element;
|
|
22
32
|
const label = String(props.label ?? "Action");
|
|
23
|
-
const variant =
|
|
33
|
+
const variant = String(props.variant ?? "secondary");
|
|
34
|
+
const isPrimary = variant === "primary";
|
|
24
35
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
25
|
-
const
|
|
36
|
+
const colors = useSnapColors();
|
|
37
|
+
const [hovered, setHovered] = useState(false);
|
|
26
38
|
|
|
27
39
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
40
|
+
const showExternalIcon = isExternalLinkAction(element.on);
|
|
41
|
+
|
|
42
|
+
const style = {
|
|
43
|
+
cursor: "pointer" as const,
|
|
44
|
+
...(isPrimary
|
|
45
|
+
? {
|
|
46
|
+
backgroundColor: hovered ? colors.accentHover : colors.accent,
|
|
47
|
+
color: colors.accentFg,
|
|
48
|
+
borderColor: "transparent",
|
|
49
|
+
}
|
|
50
|
+
: {
|
|
51
|
+
backgroundColor: hovered
|
|
52
|
+
? `color-mix(in srgb, ${colors.accent} 15%, transparent)`
|
|
53
|
+
: colors.muted,
|
|
54
|
+
color: colors.text,
|
|
55
|
+
borderColor: "transparent",
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
28
58
|
|
|
29
59
|
return (
|
|
30
|
-
<div className="w-full min-w-0 flex-1"
|
|
60
|
+
<div className="w-full min-w-0 flex-1">
|
|
31
61
|
<Button
|
|
32
62
|
type="button"
|
|
33
|
-
variant={
|
|
34
|
-
className={cn(
|
|
35
|
-
|
|
36
|
-
variant === "default" &&
|
|
37
|
-
"hover:!bg-[var(--snap-action-primary-hover)]",
|
|
38
|
-
variant !== "default" &&
|
|
39
|
-
"hover:!bg-[var(--snap-action-outline-hover)]",
|
|
40
|
-
)}
|
|
63
|
+
variant={isPrimary ? "default" : "secondary"}
|
|
64
|
+
className={cn("w-full gap-2")}
|
|
65
|
+
style={style}
|
|
41
66
|
onClick={() => emit("press")}
|
|
67
|
+
onPointerEnter={() => setHovered(true)}
|
|
68
|
+
onPointerLeave={() => setHovered(false)}
|
|
42
69
|
>
|
|
43
70
|
{Icon && <Icon size={16} />}
|
|
44
71
|
{label}
|
|
72
|
+
{showExternalIcon && (
|
|
73
|
+
<ExternalLink size={14} style={{ opacity: 0.6 }} />
|
|
74
|
+
)}
|
|
45
75
|
</Button>
|
|
46
76
|
</div>
|
|
47
77
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Badge } from "@neynar/ui/badge";
|
|
4
|
-
import {
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
5
5
|
import { ICON_MAP } from "./icon";
|
|
6
6
|
|
|
7
7
|
export function SnapBadge({
|
|
@@ -10,28 +10,24 @@ export function SnapBadge({
|
|
|
10
10
|
element: { props: Record<string, unknown> };
|
|
11
11
|
}) {
|
|
12
12
|
const content = String(props.label ?? "");
|
|
13
|
+
const variant = String(props.variant ?? "default") as "default" | "outline";
|
|
13
14
|
const color = props.color ? String(props.color) : undefined;
|
|
14
15
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
15
|
-
const
|
|
16
|
+
const colors = useSnapColors();
|
|
17
|
+
|
|
18
|
+
const badgeColor = colors.colorHex(color);
|
|
16
19
|
|
|
17
|
-
const isAccent = !color || color === "accent";
|
|
18
20
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
19
21
|
|
|
22
|
+
const style =
|
|
23
|
+
variant === "outline"
|
|
24
|
+
? { borderColor: badgeColor, color: badgeColor, backgroundColor: "transparent" }
|
|
25
|
+
: { backgroundColor: `${badgeColor}20`, color: badgeColor, borderColor: "transparent" };
|
|
26
|
+
|
|
20
27
|
return (
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// TODO: fix outline badge border color in @neynar/ui — too bright in dark mode
|
|
26
|
-
style={
|
|
27
|
-
!isAccent
|
|
28
|
-
? { borderColor: `var(--snap-color-${color})`, color: `var(--snap-color-${color})` }
|
|
29
|
-
: undefined
|
|
30
|
-
}
|
|
31
|
-
>
|
|
32
|
-
{Icon && <Icon size={12} />}
|
|
33
|
-
{content}
|
|
34
|
-
</Badge>
|
|
35
|
-
</span>
|
|
28
|
+
<Badge variant={variant} className="gap-1" style={style}>
|
|
29
|
+
{Icon && <Icon size={12} />}
|
|
30
|
+
{content}
|
|
31
|
+
</Badge>
|
|
36
32
|
);
|
|
37
33
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
4
|
+
|
|
5
|
+
export function SnapBarChart({
|
|
6
|
+
element: { props },
|
|
7
|
+
}: {
|
|
8
|
+
element: { props: Record<string, unknown> };
|
|
9
|
+
}) {
|
|
10
|
+
const colors = useSnapColors();
|
|
11
|
+
const bars = Array.isArray(props.bars) ? props.bars : [];
|
|
12
|
+
const chartColor = props.color ? String(props.color) : undefined;
|
|
13
|
+
const maxVal =
|
|
14
|
+
props.max != null
|
|
15
|
+
? Number(props.max)
|
|
16
|
+
: Math.max(
|
|
17
|
+
...bars.map((b: { value?: number }) => Number(b.value ?? 0)),
|
|
18
|
+
1,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function barFill(bar: { color?: string }): string {
|
|
22
|
+
if (bar.color) return colors.colorHex(bar.color);
|
|
23
|
+
return colors.colorHex(chartColor);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex w-full flex-col gap-2">
|
|
28
|
+
{bars.map(
|
|
29
|
+
(
|
|
30
|
+
bar: { label?: string; value?: number; color?: string },
|
|
31
|
+
i: number,
|
|
32
|
+
) => {
|
|
33
|
+
const value = Number(bar.value ?? 0);
|
|
34
|
+
const pct = maxVal > 0 ? Math.min(100, (value / maxVal) * 100) : 0;
|
|
35
|
+
const fill = barFill(bar);
|
|
36
|
+
return (
|
|
37
|
+
<div key={i} className="flex w-full items-center gap-2">
|
|
38
|
+
<span
|
|
39
|
+
className="w-20 shrink-0 truncate text-right text-xs"
|
|
40
|
+
style={{ color: colors.textMuted }}
|
|
41
|
+
>
|
|
42
|
+
{String(bar.label ?? "")}
|
|
43
|
+
</span>
|
|
44
|
+
<div
|
|
45
|
+
className="h-2.5 flex-1 overflow-hidden rounded-full"
|
|
46
|
+
style={{ backgroundColor: colors.muted }}
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
className="h-full rounded-full transition-all"
|
|
50
|
+
style={{
|
|
51
|
+
width: `${pct}%`,
|
|
52
|
+
minWidth: pct > 0 ? 4 : 0,
|
|
53
|
+
backgroundColor: fill,
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<span
|
|
58
|
+
className="w-8 shrink-0 text-xs tabular-nums"
|
|
59
|
+
style={{ color: colors.textMuted }}
|
|
60
|
+
>
|
|
61
|
+
{value}
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { useStateStore } from "@json-render/react";
|
|
5
|
+
import { cn } from "@neynar/ui/utils";
|
|
6
|
+
import { POST_GRID_TAP_KEY } from "@farcaster/snap";
|
|
7
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
8
|
+
|
|
9
|
+
export function SnapCellGrid({
|
|
10
|
+
element: { props },
|
|
11
|
+
}: {
|
|
12
|
+
element: { props: Record<string, unknown> };
|
|
13
|
+
}) {
|
|
14
|
+
const { get, set } = useStateStore();
|
|
15
|
+
const colors = useSnapColors();
|
|
16
|
+
const cols = Number(props.cols ?? 2);
|
|
17
|
+
const rows = Number(props.rows ?? 2);
|
|
18
|
+
const select = String(props.select ?? "off");
|
|
19
|
+
const interactive = select !== "off";
|
|
20
|
+
const isMultiple = select === "multiple";
|
|
21
|
+
const cells = Array.isArray(props.cells) ? props.cells : [];
|
|
22
|
+
const gap = String(props.gap ?? "sm");
|
|
23
|
+
const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
|
|
24
|
+
const gapPx = gapMap[gap] ?? 1;
|
|
25
|
+
const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
|
|
26
|
+
|
|
27
|
+
const name = props.name ? String(props.name) : POST_GRID_TAP_KEY;
|
|
28
|
+
const tapPath = `/inputs/${name}`;
|
|
29
|
+
const tapRaw = get(tapPath);
|
|
30
|
+
|
|
31
|
+
// Parse selection — single mode: "row,col" string; multi mode: "row,col|row,col|..." string
|
|
32
|
+
const selectedSet = new Set<string>();
|
|
33
|
+
if (typeof tapRaw === "string" && tapRaw.length > 0) {
|
|
34
|
+
for (const part of tapRaw.split("|")) {
|
|
35
|
+
if (part.includes(",")) selectedSet.add(part);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isSelected = (r: number, c: number) => selectedSet.has(`${r},${c}`);
|
|
40
|
+
|
|
41
|
+
const handleTap = (r: number, c: number) => {
|
|
42
|
+
const key = `${r},${c}`;
|
|
43
|
+
if (isMultiple) {
|
|
44
|
+
const next = new Set(selectedSet);
|
|
45
|
+
if (next.has(key)) next.delete(key);
|
|
46
|
+
else next.add(key);
|
|
47
|
+
set(tapPath, [...next].join("|"));
|
|
48
|
+
} else {
|
|
49
|
+
set(tapPath, key);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const cellMap = new Map<string, { color?: string; content?: string }>();
|
|
54
|
+
for (const c of cells) {
|
|
55
|
+
cellMap.set(`${Number(c.row)},${Number(c.col)}`, {
|
|
56
|
+
color: c.color as string | undefined,
|
|
57
|
+
content: c.content != null ? String(c.content) : undefined,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cellEls: ReactNode[] = [];
|
|
62
|
+
for (let r = 0; r < rows; r++) {
|
|
63
|
+
for (let c = 0; c < cols; c++) {
|
|
64
|
+
const cell = cellMap.get(`${r},${c}`);
|
|
65
|
+
const selected = interactive && isSelected(r, c);
|
|
66
|
+
const bg = cell?.color ? colors.colorHex(cell.color) : "transparent";
|
|
67
|
+
|
|
68
|
+
cellEls.push(
|
|
69
|
+
<div
|
|
70
|
+
key={`${r}-${c}`}
|
|
71
|
+
role={interactive ? "button" : undefined}
|
|
72
|
+
tabIndex={interactive ? 0 : undefined}
|
|
73
|
+
onClick={interactive ? () => handleTap(r, c) : undefined}
|
|
74
|
+
onKeyDown={
|
|
75
|
+
interactive
|
|
76
|
+
? (e) => {
|
|
77
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
handleTap(r, c);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
: undefined
|
|
83
|
+
}
|
|
84
|
+
className={cn(
|
|
85
|
+
"flex items-center justify-center rounded text-xs font-semibold",
|
|
86
|
+
interactive ? "cursor-pointer select-none" : "cursor-default",
|
|
87
|
+
)}
|
|
88
|
+
style={{
|
|
89
|
+
height: rowHeight,
|
|
90
|
+
background: bg,
|
|
91
|
+
boxShadow: selected
|
|
92
|
+
? `inset 0 0 0 1px ${colors.mode === "dark" ? "#000" : "#fff"}, inset 0 0 0 2px ${colors.mode === "dark" ? "#fff" : "#000"}`
|
|
93
|
+
: undefined,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{cell?.content ?? ""}
|
|
97
|
+
</div>,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const selectionLabel = interactive && selectedSet.size > 0
|
|
103
|
+
? `inputs.${name}: ${[...selectedSet].join(isMultiple ? " | " : "")}`
|
|
104
|
+
: null;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<div
|
|
109
|
+
style={{
|
|
110
|
+
display: "grid",
|
|
111
|
+
width: "100%",
|
|
112
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
113
|
+
gap: gapPx,
|
|
114
|
+
padding: 4,
|
|
115
|
+
borderRadius: 8,
|
|
116
|
+
backgroundColor: colors.muted,
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{cellEls}
|
|
120
|
+
</div>
|
|
121
|
+
{selectionLabel && (
|
|
122
|
+
<div className="mt-1.5 truncate text-xs font-mono" style={{ color: colors.textMuted }}>
|
|
123
|
+
{selectionLabel}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
TrendingDown,
|
|
37
37
|
type LucideIcon,
|
|
38
38
|
} from "lucide-react";
|
|
39
|
-
import {
|
|
39
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
40
40
|
|
|
41
41
|
export const ICON_MAP: Record<string, LucideIcon> = {
|
|
42
42
|
"arrow-right": ArrowRight,
|
|
@@ -87,29 +87,16 @@ export function SnapIcon({
|
|
|
87
87
|
const name = String(props.name ?? "info");
|
|
88
88
|
const size = SIZE_PX[String(props.size ?? "md")] ?? 20;
|
|
89
89
|
const color = props.color ? String(props.color) : undefined;
|
|
90
|
-
const
|
|
90
|
+
const colors = useSnapColors();
|
|
91
91
|
|
|
92
92
|
const Icon = ICON_MAP[name];
|
|
93
93
|
if (!Icon) return null;
|
|
94
94
|
|
|
95
|
-
const
|
|
95
|
+
const iconColor = colors.colorHex(color);
|
|
96
96
|
|
|
97
97
|
return (
|
|
98
|
-
<span
|
|
99
|
-
style={{
|
|
100
|
-
display: "inline-flex",
|
|
101
|
-
alignItems: "center",
|
|
102
|
-
...(isAccent ? accentStyle : {}),
|
|
103
|
-
}}
|
|
104
|
-
>
|
|
105
|
-
<Icon
|
|
106
|
-
size={size}
|
|
107
|
-
style={
|
|
108
|
-
isAccent
|
|
109
|
-
? { color: "var(--snap-accent, currentColor)" }
|
|
110
|
-
: { color: `var(--snap-color-${color}, currentColor)` }
|
|
111
|
-
}
|
|
112
|
-
/>
|
|
98
|
+
<span style={{ display: "inline-flex", alignItems: "center" }}>
|
|
99
|
+
<Icon size={size} style={{ color: iconColor }} />
|
|
113
100
|
</span>
|
|
114
101
|
);
|
|
115
102
|
}
|
|
@@ -4,32 +4,43 @@ import { useId } from "react";
|
|
|
4
4
|
import { useStateStore } from "@json-render/react";
|
|
5
5
|
import { Input } from "@neynar/ui/input";
|
|
6
6
|
import { Label } from "@neynar/ui/label";
|
|
7
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
7
8
|
|
|
8
9
|
export function SnapInput({
|
|
9
10
|
element: { props },
|
|
10
11
|
}: {
|
|
11
12
|
element: { props: Record<string, unknown> };
|
|
12
13
|
}) {
|
|
13
|
-
const id = useId();
|
|
14
14
|
const { get, set } = useStateStore();
|
|
15
|
+
const colors = useSnapColors();
|
|
15
16
|
const name = String(props.name ?? "input");
|
|
16
|
-
const
|
|
17
|
+
const type = String(props.type ?? "text");
|
|
17
18
|
const label = props.label ? String(props.label) : undefined;
|
|
18
19
|
const placeholder = props.placeholder ? String(props.placeholder) : undefined;
|
|
19
|
-
const maxLength =
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
20
|
+
const maxLength = props.maxLength ? Number(props.maxLength) : undefined;
|
|
21
|
+
const path = `/inputs/${name}`;
|
|
22
|
+
const value = (get(path) as string) ?? (props.defaultValue != null ? String(props.defaultValue) : "");
|
|
23
|
+
const id = useId();
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
26
|
<div className="w-full space-y-1.5">
|
|
26
|
-
{label &&
|
|
27
|
+
{label && (
|
|
28
|
+
<Label htmlFor={id} style={{ color: colors.text }}>
|
|
29
|
+
{label}
|
|
30
|
+
</Label>
|
|
31
|
+
)}
|
|
27
32
|
<Input
|
|
28
33
|
id={id}
|
|
29
|
-
|
|
30
|
-
onChange={(e) => set(path, e.target.value)}
|
|
34
|
+
type={type === "number" ? "number" : "text"}
|
|
31
35
|
placeholder={placeholder}
|
|
32
36
|
maxLength={maxLength}
|
|
37
|
+
value={value}
|
|
38
|
+
onChange={(e) => set(path, e.target.value)}
|
|
39
|
+
style={{
|
|
40
|
+
backgroundColor: colors.inputBg,
|
|
41
|
+
borderColor: colors.inputBorder,
|
|
42
|
+
color: colors.text,
|
|
43
|
+
}}
|
|
33
44
|
/>
|
|
34
45
|
</div>
|
|
35
46
|
);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Children, type ReactNode, Fragment } from "react";
|
|
4
4
|
import { cn } from "@neynar/ui/utils";
|
|
5
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
5
6
|
|
|
6
7
|
const GAP_MAP: Record<string, string> = {
|
|
7
8
|
none: "gap-0",
|
|
@@ -21,6 +22,7 @@ export function SnapItemGroup({
|
|
|
21
22
|
const separator = Boolean(props.separator);
|
|
22
23
|
const gap = GAP_MAP[String(props.gap ?? "sm")] ?? "gap-1";
|
|
23
24
|
const items = Children.toArray(children);
|
|
25
|
+
const colors = useSnapColors();
|
|
24
26
|
|
|
25
27
|
return (
|
|
26
28
|
<div
|
|
@@ -29,11 +31,12 @@ export function SnapItemGroup({
|
|
|
29
31
|
border && "rounded-lg border",
|
|
30
32
|
gap,
|
|
31
33
|
)}
|
|
34
|
+
style={border ? { borderColor: colors.border } : undefined}
|
|
32
35
|
>
|
|
33
36
|
{items.map((child, i) => (
|
|
34
37
|
<Fragment key={i}>
|
|
35
38
|
{separator && i > 0 && (
|
|
36
|
-
<div className="h-px
|
|
39
|
+
<div className="h-px" style={{ backgroundColor: colors.border }} />
|
|
37
40
|
)}
|
|
38
41
|
{child}
|
|
39
42
|
</Fragment>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
3
|
import {
|
|
5
4
|
Item,
|
|
6
5
|
ItemContent,
|
|
@@ -8,26 +7,30 @@ import {
|
|
|
8
7
|
ItemDescription,
|
|
9
8
|
ItemActions,
|
|
10
9
|
} from "@neynar/ui/item";
|
|
10
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
11
11
|
|
|
12
12
|
export function SnapItem({
|
|
13
|
-
element: { props },
|
|
13
|
+
element: { props, children: childIds },
|
|
14
14
|
children,
|
|
15
15
|
}: {
|
|
16
|
-
element: { props: Record<string, unknown
|
|
17
|
-
children?: ReactNode;
|
|
16
|
+
element: { props: Record<string, unknown>; children?: string[] };
|
|
17
|
+
children?: React.ReactNode;
|
|
18
18
|
}) {
|
|
19
19
|
const title = String(props.title ?? "");
|
|
20
20
|
const description = props.description ? String(props.description) : undefined;
|
|
21
|
-
const
|
|
22
|
-
(props.variant as "default" | "outline" | "muted") ?? "default";
|
|
21
|
+
const colors = useSnapColors();
|
|
23
22
|
|
|
24
23
|
return (
|
|
25
|
-
<Item
|
|
24
|
+
<Item className="flex-1 py-1.5 px-2.5">
|
|
26
25
|
<ItemContent className="gap-0.5">
|
|
27
|
-
<ItemTitle>{title}</ItemTitle>
|
|
28
|
-
{description &&
|
|
26
|
+
<ItemTitle style={{ color: colors.text }}>{title}</ItemTitle>
|
|
27
|
+
{description && (
|
|
28
|
+
<ItemDescription className="mt-0" style={{ color: colors.textMuted }}>
|
|
29
|
+
{description}
|
|
30
|
+
</ItemDescription>
|
|
31
|
+
)}
|
|
29
32
|
</ItemContent>
|
|
30
|
-
{
|
|
33
|
+
{childIds && childIds.length > 0 && <ItemActions>{children}</ItemActions>}
|
|
31
34
|
</Item>
|
|
32
35
|
);
|
|
33
36
|
}
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
4
4
|
|
|
5
5
|
export function SnapProgress({
|
|
6
6
|
element: { props },
|
|
7
7
|
}: {
|
|
8
8
|
element: { props: Record<string, unknown> };
|
|
9
9
|
}) {
|
|
10
|
-
const
|
|
10
|
+
const colors = useSnapColors();
|
|
11
11
|
const value = Number(props.value ?? 0);
|
|
12
12
|
const max = Math.max(1, Number(props.max ?? 100));
|
|
13
13
|
const percent = Math.min(100, Math.max(0, (value / max) * 100));
|
|
14
14
|
const label = props.label ? String(props.label) : null;
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<div className="flex w-full flex-1 flex-col gap-1"
|
|
17
|
+
<div className="flex w-full flex-1 flex-col gap-1">
|
|
18
18
|
{label && (
|
|
19
|
-
<span className="text-
|
|
19
|
+
<span className="text-xs" style={{ color: colors.textMuted }}>
|
|
20
|
+
{label}
|
|
21
|
+
</span>
|
|
20
22
|
)}
|
|
21
|
-
<div
|
|
23
|
+
<div
|
|
24
|
+
className="h-2.5 w-full overflow-hidden rounded-full"
|
|
25
|
+
style={{ backgroundColor: colors.muted }}
|
|
26
|
+
>
|
|
22
27
|
<div
|
|
23
|
-
className="h-full rounded-full
|
|
24
|
-
style={{ width: `${percent}
|
|
28
|
+
className="h-full rounded-full transition-all"
|
|
29
|
+
style={{ width: `${percent}%`, backgroundColor: colors.accent }}
|
|
25
30
|
/>
|
|
26
31
|
</div>
|
|
27
32
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Separator } from "@neynar/ui/separator";
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
4
5
|
|
|
5
6
|
export function SnapSeparator({
|
|
6
7
|
element: { props },
|
|
@@ -9,6 +10,12 @@ export function SnapSeparator({
|
|
|
9
10
|
}) {
|
|
10
11
|
const orientation =
|
|
11
12
|
(props.orientation as "horizontal" | "vertical") ?? "horizontal";
|
|
13
|
+
const colors = useSnapColors();
|
|
12
14
|
|
|
13
|
-
return
|
|
15
|
+
return (
|
|
16
|
+
<Separator
|
|
17
|
+
orientation={orientation}
|
|
18
|
+
style={{ backgroundColor: colors.border }}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
14
21
|
}
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useStateStore } from "@json-render/react";
|
|
4
4
|
import { Label } from "@neynar/ui/label";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// TODO: switch back to @neynar/ui/slider once Base UI fixes the inline
|
|
8
|
-
// <script> tag that triggers a React console warning on client render.
|
|
5
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
9
6
|
|
|
10
7
|
export function SnapSlider({
|
|
11
8
|
element: { props },
|
|
@@ -13,21 +10,34 @@ export function SnapSlider({
|
|
|
13
10
|
element: { props: Record<string, unknown> };
|
|
14
11
|
}) {
|
|
15
12
|
const { get, set } = useStateStore();
|
|
16
|
-
const
|
|
17
|
-
|
|
13
|
+
const colors = useSnapColors();
|
|
18
14
|
const name = String(props.name ?? "slider");
|
|
19
|
-
const path = `/inputs/${name}`;
|
|
20
|
-
const label = props.label ? String(props.label) : undefined;
|
|
21
15
|
const min = Number(props.min ?? 0);
|
|
22
16
|
const max = Number(props.max ?? 100);
|
|
23
|
-
const step =
|
|
24
|
-
const
|
|
17
|
+
const step = Number(props.step ?? 1);
|
|
18
|
+
const label = props.label ? String(props.label) : undefined;
|
|
19
|
+
const showValue = props.showValue === true;
|
|
20
|
+
const path = `/inputs/${name}`;
|
|
25
21
|
const raw = get(path);
|
|
26
|
-
const value =
|
|
22
|
+
const value =
|
|
23
|
+
raw !== undefined
|
|
24
|
+
? Number(raw)
|
|
25
|
+
: props.defaultValue !== undefined
|
|
26
|
+
? Number(props.defaultValue)
|
|
27
|
+
: (min + max) / 2;
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
|
-
<div className="flex w-full flex-col gap-1.5"
|
|
30
|
-
{label &&
|
|
30
|
+
<div className="flex w-full flex-col gap-1.5">
|
|
31
|
+
{label && (
|
|
32
|
+
<div className="flex items-center justify-between">
|
|
33
|
+
<Label style={{ color: colors.text }}>{label}</Label>
|
|
34
|
+
{showValue && (
|
|
35
|
+
<span style={{ color: colors.textMuted, fontSize: 13, lineHeight: "18px" }}>
|
|
36
|
+
{Math.round(value)}
|
|
37
|
+
</span>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
31
41
|
<input
|
|
32
42
|
type="range"
|
|
33
43
|
min={min}
|
|
@@ -35,8 +45,11 @@ export function SnapSlider({
|
|
|
35
45
|
step={step}
|
|
36
46
|
value={value}
|
|
37
47
|
onChange={(e) => set(path, Number(e.target.value))}
|
|
38
|
-
className="w-full h-2.5 rounded-full appearance-none
|
|
39
|
-
style={{
|
|
48
|
+
className="w-full h-2.5 rounded-full appearance-none cursor-pointer"
|
|
49
|
+
style={{
|
|
50
|
+
backgroundColor: colors.muted,
|
|
51
|
+
accentColor: colors.accent,
|
|
52
|
+
}}
|
|
40
53
|
/>
|
|
41
54
|
</div>
|
|
42
55
|
);
|