@farcaster/snap 1.13.0 → 1.15.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.
- package/dist/react/components/action-button.js +18 -10
- package/dist/react/components/badge.js +8 -6
- package/dist/react/components/bar-chart.js +12 -17
- package/dist/react/components/cell-grid.js +16 -15
- 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 +14 -10
- package/dist/react/components/switch.js +10 -12
- package/dist/react/components/text.js +5 -7
- 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 +82 -0
- package/dist/react/index.js +8 -5
- package/dist/react-native/components/snap-action-button.js +8 -18
- package/dist/react-native/components/snap-cell-grid.js +1 -1
- package/dist/react-native/components/snap-switch.js +1 -1
- package/dist/react-native/components/snap-toggle-group.js +8 -10
- package/dist/react-native/index.js +1 -1
- package/dist/react-native/theme.d.ts +6 -0
- package/dist/react-native/theme.js +12 -6
- package/dist/ui/catalog.d.ts +1 -0
- package/dist/ui/cell-grid.d.ts +1 -0
- package/dist/ui/cell-grid.js +1 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/react/components/action-button.tsx +24 -17
- package/src/react/components/badge.tsx +14 -17
- package/src/react/components/bar-chart.tsx +21 -19
- package/src/react/components/cell-grid.tsx +16 -19
- 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 +18 -15
- package/src/react/components/switch.tsx +12 -16
- package/src/react/components/text.tsx +11 -8
- package/src/react/components/toggle-group.tsx +26 -9
- package/src/react/hooks/use-snap-colors.ts +129 -0
- package/src/react/index.tsx +19 -18
- package/src/react-native/components/snap-action-button.tsx +8 -20
- package/src/react-native/components/snap-cell-grid.tsx +1 -1
- package/src/react-native/components/snap-switch.tsx +1 -1
- package/src/react-native/components/snap-toggle-group.tsx +8 -10
- package/src/react-native/index.tsx +1 -1
- package/src/react-native/theme.tsx +18 -6
- package/src/ui/cell-grid.ts +1 -0
- package/dist/react/hooks/use-snap-accent.d.ts +0 -13
- package/dist/react/hooks/use-snap-accent.js +0 -32
- package/src/react/hooks/use-snap-accent.ts +0 -45
|
@@ -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") ?? "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,24 @@ 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 path = `/inputs/${name}`;
|
|
25
20
|
const raw = get(path);
|
|
26
|
-
const value =
|
|
21
|
+
const value =
|
|
22
|
+
raw !== undefined
|
|
23
|
+
? Number(raw)
|
|
24
|
+
: props.defaultValue !== undefined
|
|
25
|
+
? Number(props.defaultValue)
|
|
26
|
+
: (min + max) / 2;
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
|
-
<div className="flex w-full flex-col gap-1.5"
|
|
30
|
-
{label && <Label>{label}</Label>}
|
|
29
|
+
<div className="flex w-full flex-col gap-1.5">
|
|
30
|
+
{label && <Label style={{ color: colors.text }}>{label}</Label>}
|
|
31
31
|
<input
|
|
32
32
|
type="range"
|
|
33
33
|
min={min}
|
|
@@ -35,8 +35,11 @@ export function SnapSlider({
|
|
|
35
35
|
step={step}
|
|
36
36
|
value={value}
|
|
37
37
|
onChange={(e) => set(path, Number(e.target.value))}
|
|
38
|
-
className="w-full h-2.5 rounded-full appearance-none
|
|
39
|
-
style={{
|
|
38
|
+
className="w-full h-2.5 rounded-full appearance-none cursor-pointer"
|
|
39
|
+
style={{
|
|
40
|
+
backgroundColor: colors.muted,
|
|
41
|
+
accentColor: colors.accent,
|
|
42
|
+
}}
|
|
40
43
|
/>
|
|
41
44
|
</div>
|
|
42
45
|
);
|
|
@@ -2,32 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { useStateStore } from "@json-render/react";
|
|
5
|
-
import { Label } from "@neynar/ui/label";
|
|
6
5
|
import { Switch } from "@neynar/ui/switch";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { useSnapAccentScopeStyle } from "../hooks/use-snap-accent";
|
|
6
|
+
import { Label } from "@neynar/ui/label";
|
|
7
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
10
8
|
|
|
11
9
|
export function SnapSwitch({
|
|
12
10
|
element: { props },
|
|
13
11
|
}: {
|
|
14
12
|
element: { props: Record<string, unknown> };
|
|
15
13
|
}) {
|
|
16
|
-
const id = useId();
|
|
17
14
|
const { get, set } = useStateStore();
|
|
18
|
-
const
|
|
19
|
-
const accentStyle = useSnapAccentScopeStyle();
|
|
15
|
+
const colors = useSnapColors();
|
|
20
16
|
const name = String(props.name ?? "switch");
|
|
21
|
-
const path = `/inputs/${name}`;
|
|
22
17
|
const label = props.label ? String(props.label) : undefined;
|
|
23
|
-
const
|
|
18
|
+
const path = `/inputs/${name}`;
|
|
24
19
|
const raw = get(path);
|
|
25
|
-
const checked =
|
|
20
|
+
const checked =
|
|
21
|
+
raw !== undefined ? Boolean(raw) : Boolean(props.defaultChecked);
|
|
22
|
+
const id = useId();
|
|
26
23
|
|
|
27
24
|
return (
|
|
28
25
|
<div className="flex items-center justify-between gap-3">
|
|
29
26
|
{label && (
|
|
30
|
-
<Label htmlFor={id} className="
|
|
27
|
+
<Label htmlFor={id} className="font-normal" style={{ color: colors.text }}>
|
|
31
28
|
{label}
|
|
32
29
|
</Label>
|
|
33
30
|
)}
|
|
@@ -35,11 +32,10 @@ export function SnapSwitch({
|
|
|
35
32
|
id={id}
|
|
36
33
|
checked={checked}
|
|
37
34
|
onCheckedChange={(v) => set(path, v)}
|
|
38
|
-
style={
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)}
|
|
35
|
+
style={{
|
|
36
|
+
backgroundColor: checked ? colors.accent : colors.muted,
|
|
37
|
+
borderColor: checked ? colors.accent : colors.inputBorder,
|
|
38
|
+
}}
|
|
43
39
|
/>
|
|
44
40
|
</div>
|
|
45
41
|
);
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Text } from "@neynar/ui/typography";
|
|
4
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
4
5
|
|
|
5
6
|
const SIZE_MAP = {
|
|
6
|
-
md: {
|
|
7
|
-
sm: {
|
|
8
|
-
} as const;
|
|
9
|
-
|
|
10
|
-
const WEIGHT_MAP = {
|
|
11
|
-
bold: "bold",
|
|
12
|
-
normal: "normal",
|
|
7
|
+
md: { textSize: "base" as const },
|
|
8
|
+
sm: { textSize: "sm" as const },
|
|
13
9
|
} as const;
|
|
14
10
|
|
|
15
11
|
export function SnapText({
|
|
@@ -22,9 +18,16 @@ export function SnapText({
|
|
|
22
18
|
const weight = props.weight ? String(props.weight) as "bold" | "normal" : undefined;
|
|
23
19
|
const align = (props.align as "left" | "center" | "right") ?? undefined;
|
|
24
20
|
const config = SIZE_MAP[size] ?? SIZE_MAP.md;
|
|
21
|
+
const colors = useSnapColors();
|
|
25
22
|
|
|
26
23
|
return (
|
|
27
|
-
<Text
|
|
24
|
+
<Text
|
|
25
|
+
size={config.textSize}
|
|
26
|
+
weight={weight}
|
|
27
|
+
align={align}
|
|
28
|
+
className="flex-1"
|
|
29
|
+
style={{ color: colors.text }}
|
|
30
|
+
>
|
|
28
31
|
{content}
|
|
29
32
|
</Text>
|
|
30
33
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { useStateStore } from "@json-render/react";
|
|
4
5
|
import { Label } from "@neynar/ui/label";
|
|
5
6
|
import { cn } from "@neynar/ui/utils";
|
|
6
|
-
import {
|
|
7
|
+
import { useSnapColors } from "../hooks/use-snap-colors";
|
|
7
8
|
|
|
8
9
|
export function SnapToggleGroup({
|
|
9
10
|
element: { props },
|
|
@@ -11,7 +12,7 @@ export function SnapToggleGroup({
|
|
|
11
12
|
element: { props: Record<string, unknown> };
|
|
12
13
|
}) {
|
|
13
14
|
const { get, set } = useStateStore();
|
|
14
|
-
const
|
|
15
|
+
const colors = useSnapColors();
|
|
15
16
|
const name = String(props.name ?? "toggle_group");
|
|
16
17
|
const path = `/inputs/${name}`;
|
|
17
18
|
const label = props.label ? String(props.label) : undefined;
|
|
@@ -50,30 +51,46 @@ export function SnapToggleGroup({
|
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
const isVertical = orientation === "vertical";
|
|
54
|
+
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
53
55
|
|
|
54
56
|
return (
|
|
55
|
-
<div className="w-full space-y-1.5"
|
|
56
|
-
{label && <Label>{label}</Label>}
|
|
57
|
+
<div className="w-full space-y-1.5">
|
|
58
|
+
{label && <Label style={{ color: colors.text }}>{label}</Label>}
|
|
57
59
|
<div
|
|
58
60
|
className={cn(
|
|
59
|
-
"flex gap-1 rounded-lg
|
|
61
|
+
"flex gap-1 rounded-lg p-1",
|
|
60
62
|
isVertical ? "flex-col" : "flex-row",
|
|
61
63
|
)}
|
|
64
|
+
style={{ backgroundColor: colors.muted }}
|
|
62
65
|
>
|
|
63
|
-
{options.map((opt) => {
|
|
66
|
+
{options.map((opt, i) => {
|
|
64
67
|
const isSelected = selected.includes(opt);
|
|
68
|
+
const isHovered = hoveredIdx === i && !isSelected;
|
|
65
69
|
return (
|
|
66
70
|
<button
|
|
67
71
|
key={opt}
|
|
68
72
|
type="button"
|
|
69
73
|
onClick={() => toggle(opt)}
|
|
74
|
+
onPointerEnter={() => setHoveredIdx(i)}
|
|
75
|
+
onPointerLeave={() => setHoveredIdx(null)}
|
|
70
76
|
className={cn(
|
|
71
77
|
"rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
|
72
78
|
isVertical ? "w-full" : "flex-1",
|
|
73
|
-
isSelected
|
|
74
|
-
? "bg-primary text-primary-foreground"
|
|
75
|
-
: "text-foreground hover:bg-border/30",
|
|
76
79
|
)}
|
|
80
|
+
style={{
|
|
81
|
+
transition: "background-color 0.15s, color 0.15s",
|
|
82
|
+
...(isSelected
|
|
83
|
+
? {
|
|
84
|
+
backgroundColor: colors.mode === "dark" ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.08)",
|
|
85
|
+
color: colors.text,
|
|
86
|
+
}
|
|
87
|
+
: {
|
|
88
|
+
color: colors.text,
|
|
89
|
+
backgroundColor: isHovered
|
|
90
|
+
? (colors.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)")
|
|
91
|
+
: (colors.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)"),
|
|
92
|
+
}),
|
|
93
|
+
}}
|
|
77
94
|
>
|
|
78
95
|
{opt}
|
|
79
96
|
</button>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { useStateStore } from "@json-render/react";
|
|
5
|
+
import { useColorMode } from "@neynar/ui/color-mode";
|
|
6
|
+
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
|
|
7
|
+
import { useSnapPreviewPageAccent } from "../accent-context";
|
|
8
|
+
import type { PaletteColor } from "@farcaster/snap";
|
|
9
|
+
import { PALETTE_DARK_HEX, PALETTE_LIGHT_HEX } from "@farcaster/snap";
|
|
10
|
+
|
|
11
|
+
/** Readable foreground color (black or white) for a given hex background. */
|
|
12
|
+
export function pickForegroundForBg(hex: string): string {
|
|
13
|
+
const h = hex.replace(/^#/, "");
|
|
14
|
+
if (h.length !== 6) return "#ffffff";
|
|
15
|
+
const r = Number.parseInt(h.slice(0, 2), 16);
|
|
16
|
+
const g = Number.parseInt(h.slice(2, 4), 16);
|
|
17
|
+
const b = Number.parseInt(h.slice(4, 6), 16);
|
|
18
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
19
|
+
return yiq >= 180 ? "#0a0a0a" : "#ffffff";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const NEUTRAL_LIGHT = {
|
|
23
|
+
text: "#111111",
|
|
24
|
+
textMuted: "#6B7280",
|
|
25
|
+
border: "#E5E7EB",
|
|
26
|
+
muted: "rgba(0,0,0,0.06)",
|
|
27
|
+
surface: "#ffffff",
|
|
28
|
+
inputBorder: "#E5E7EB",
|
|
29
|
+
inputBg: "rgba(0,0,0,0.06)",
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
const NEUTRAL_DARK = {
|
|
33
|
+
text: "#FAFAFA",
|
|
34
|
+
textMuted: "#A1A1AA",
|
|
35
|
+
border: "#2D2D44",
|
|
36
|
+
muted: "rgba(255,255,255,0.03)",
|
|
37
|
+
surface: "#23262f",
|
|
38
|
+
inputBorder: "#3F3F46",
|
|
39
|
+
inputBg: "rgba(255,255,255,0.03)",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export type SnapColors = {
|
|
43
|
+
/** Resolved accent hex */
|
|
44
|
+
accent: string;
|
|
45
|
+
/** Readable foreground for accent bg (black or white) */
|
|
46
|
+
accentFg: string;
|
|
47
|
+
/** Primary button hover color */
|
|
48
|
+
accentHover: string;
|
|
49
|
+
/** Secondary/outline button hover color */
|
|
50
|
+
outlineHover: string;
|
|
51
|
+
/** Primary text color */
|
|
52
|
+
text: string;
|
|
53
|
+
/** Muted/secondary text color */
|
|
54
|
+
textMuted: string;
|
|
55
|
+
/** Border color */
|
|
56
|
+
border: string;
|
|
57
|
+
/** Muted background (tracks, containers) */
|
|
58
|
+
muted: string;
|
|
59
|
+
/** Surface/card background */
|
|
60
|
+
surface: string;
|
|
61
|
+
/** Input border */
|
|
62
|
+
inputBorder: string;
|
|
63
|
+
/** Input background */
|
|
64
|
+
inputBg: string;
|
|
65
|
+
/** Current color mode */
|
|
66
|
+
mode: "light" | "dark";
|
|
67
|
+
/** Resolve a palette color name to hex */
|
|
68
|
+
paletteHex: (name: string) => string;
|
|
69
|
+
/** Resolve a palette color name to hex, with accent fallback */
|
|
70
|
+
colorHex: (name: string | undefined) => string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function buildSnapColors(
|
|
74
|
+
accentName: string,
|
|
75
|
+
mode: "light" | "dark",
|
|
76
|
+
): SnapColors {
|
|
77
|
+
const accent = resolveSnapPaletteHex(accentName, mode);
|
|
78
|
+
const accentFg = pickForegroundForBg(accent);
|
|
79
|
+
const neutrals = mode === "dark" ? NEUTRAL_DARK : NEUTRAL_LIGHT;
|
|
80
|
+
const paletteMap = mode === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
|
|
81
|
+
|
|
82
|
+
const accentHover =
|
|
83
|
+
mode === "light"
|
|
84
|
+
? `color-mix(in srgb, ${accent} 82%, #000000)`
|
|
85
|
+
: `color-mix(in srgb, ${accent} 78%, #ffffff)`;
|
|
86
|
+
|
|
87
|
+
const outlineHover = `color-mix(in srgb, ${accent} 14%, ${neutrals.surface})`;
|
|
88
|
+
|
|
89
|
+
const paletteHex = (name: string) => resolveSnapPaletteHex(name, mode);
|
|
90
|
+
|
|
91
|
+
const colorHex = (name: string | undefined) => {
|
|
92
|
+
if (!name || name === "accent") return accent;
|
|
93
|
+
if (Object.hasOwn(paletteMap, name)) return paletteMap[name as PaletteColor];
|
|
94
|
+
return accent;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
accent,
|
|
99
|
+
accentFg,
|
|
100
|
+
accentHover,
|
|
101
|
+
outlineHover,
|
|
102
|
+
...neutrals,
|
|
103
|
+
mode,
|
|
104
|
+
paletteHex,
|
|
105
|
+
colorHex,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns fully resolved color values for snap components.
|
|
111
|
+
* All colors are concrete hex values (or color-mix expressions for hover states)
|
|
112
|
+
* so they can be used as inline styles, independent of host app CSS.
|
|
113
|
+
*/
|
|
114
|
+
export function useSnapColors(): SnapColors {
|
|
115
|
+
const { get } = useStateStore();
|
|
116
|
+
const { mode } = useColorMode();
|
|
117
|
+
const pageAccent = useSnapPreviewPageAccent();
|
|
118
|
+
const fromState = get("/theme/accent");
|
|
119
|
+
const accentRaw =
|
|
120
|
+
(typeof pageAccent === "string" && pageAccent.length > 0
|
|
121
|
+
? pageAccent
|
|
122
|
+
: fromState) ?? undefined;
|
|
123
|
+
const accentName =
|
|
124
|
+
typeof accentRaw === "string" && accentRaw.length > 0
|
|
125
|
+
? accentRaw
|
|
126
|
+
: "purple";
|
|
127
|
+
|
|
128
|
+
return useMemo(() => buildSnapColors(accentName, mode), [accentName, mode]);
|
|
129
|
+
}
|
package/src/react/index.tsx
CHANGED
|
@@ -283,24 +283,25 @@ export function SnapView({
|
|
|
283
283
|
return (
|
|
284
284
|
<div style={{ position: "relative", width: "100%" }}>
|
|
285
285
|
{showConfetti && <ConfettiOverlay />}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
286
|
+
<div
|
|
287
|
+
style={{
|
|
288
|
+
position: "absolute",
|
|
289
|
+
inset: 0,
|
|
290
|
+
display: "flex",
|
|
291
|
+
alignItems: "center",
|
|
292
|
+
justifyContent: "center",
|
|
293
|
+
zIndex: 10,
|
|
294
|
+
fontSize: 14,
|
|
295
|
+
color: appearance === "dark" ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.4)",
|
|
296
|
+
background: appearance === "dark" ? "rgba(0,0,0,0.3)" : "rgba(255,255,255,0.5)",
|
|
297
|
+
backdropFilter: loading ? "blur(8px)" : "blur(0px)",
|
|
298
|
+
opacity: loading ? 1 : 0,
|
|
299
|
+
pointerEvents: loading ? "auto" : "none",
|
|
300
|
+
transition: "opacity 0.3s ease, backdrop-filter 0.3s ease",
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
Loading...
|
|
304
|
+
</div>
|
|
304
305
|
|
|
305
306
|
<div style={previewSurfaceStyle}>
|
|
306
307
|
<SnapPreviewAccentProvider pageAccent={snap.theme?.accent}>
|
|
@@ -6,11 +6,6 @@ import { useSnapPalette } from "../use-snap-palette";
|
|
|
6
6
|
import { useSnapTheme } from "../theme";
|
|
7
7
|
import { ICON_MAP } from "./snap-icon";
|
|
8
8
|
|
|
9
|
-
const VARIANT_MAP: Record<string, "primary" | "secondary"> = {
|
|
10
|
-
primary: "primary",
|
|
11
|
-
secondary: "secondary",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
9
|
export function SnapActionButton({
|
|
15
10
|
element: { props },
|
|
16
11
|
emit,
|
|
@@ -18,28 +13,22 @@ export function SnapActionButton({
|
|
|
18
13
|
const { accentHex } = useSnapPalette();
|
|
19
14
|
const { colors } = useSnapTheme();
|
|
20
15
|
const label = String(props.label ?? "Action");
|
|
21
|
-
const variant =
|
|
16
|
+
const variant = String(props.variant ?? "secondary");
|
|
17
|
+
const isPrimary = variant === "primary";
|
|
22
18
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
23
19
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
case "primary":
|
|
27
|
-
return { backgroundColor: accentHex };
|
|
28
|
-
case "secondary":
|
|
29
|
-
return { backgroundColor: "transparent", borderWidth: 1.5, borderColor: accentHex };
|
|
30
|
-
}
|
|
31
|
-
})();
|
|
32
|
-
|
|
33
|
-
const textColor = variant === "primary" ? "#fff" : accentHex;
|
|
34
|
-
const iconColor = variant === "primary" ? "#fff" : accentHex;
|
|
20
|
+
const textColor = isPrimary ? "#fff" : colors.text;
|
|
21
|
+
const iconColor = isPrimary ? "#fff" : colors.text;
|
|
35
22
|
|
|
36
23
|
return (
|
|
37
24
|
<View style={styles.outer}>
|
|
38
25
|
<Pressable
|
|
39
26
|
style={({ pressed }) => [
|
|
40
27
|
styles.btn,
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
isPrimary ? styles.btnDefault : styles.btnOther,
|
|
29
|
+
isPrimary
|
|
30
|
+
? { backgroundColor: pressed ? accentHex + "DD" : accentHex }
|
|
31
|
+
: { backgroundColor: pressed ? colors.mutedHover : colors.muted },
|
|
43
32
|
pressed && styles.pressed,
|
|
44
33
|
]}
|
|
45
34
|
onPress={() => {
|
|
@@ -48,7 +37,6 @@ export function SnapActionButton({
|
|
|
48
37
|
await emit("press");
|
|
49
38
|
} catch (err: unknown) {
|
|
50
39
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
|
-
// eslint-disable-next-line no-console
|
|
52
40
|
console.error("[snap] action failed", err);
|
|
53
41
|
}
|
|
54
42
|
}
|
|
@@ -118,7 +118,7 @@ export function SnapCellGrid({
|
|
|
118
118
|
: null;
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
|
-
<View style={[styles.wrap, { gap: gapPx }]}>
|
|
121
|
+
<View style={[styles.wrap, { gap: gapPx, backgroundColor: colors.muted, padding: 4, borderRadius: 8 }]}>
|
|
122
122
|
{rowEls}
|
|
123
123
|
{selectionLabel ? (
|
|
124
124
|
<Text style={[styles.selectionText, { color: colors.textSecondary }]}>
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { ComponentRenderProps } from "@json-render/react-native";
|
|
2
2
|
import { useStateStore } from "@json-render/react-native";
|
|
3
3
|
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
4
|
-
import { useSnapPalette } from "../use-snap-palette";
|
|
5
4
|
import { useSnapTheme } from "../theme";
|
|
6
5
|
|
|
7
6
|
export function SnapToggleGroup({
|
|
8
7
|
element: { props },
|
|
9
8
|
}: ComponentRenderProps<Record<string, unknown>>) {
|
|
10
9
|
const { get, set } = useStateStore();
|
|
11
|
-
const { accentHex } = useSnapPalette();
|
|
12
10
|
const { colors } = useSnapTheme();
|
|
13
11
|
const name = String(props.name ?? "toggle_group");
|
|
14
12
|
const path = `/inputs/${name}`;
|
|
@@ -59,7 +57,7 @@ export function SnapToggleGroup({
|
|
|
59
57
|
<View
|
|
60
58
|
style={[
|
|
61
59
|
styles.group,
|
|
62
|
-
{ backgroundColor: colors.
|
|
60
|
+
{ backgroundColor: colors.muted },
|
|
63
61
|
isVertical ? styles.groupVertical : styles.groupHorizontal,
|
|
64
62
|
]}
|
|
65
63
|
>
|
|
@@ -70,8 +68,13 @@ export function SnapToggleGroup({
|
|
|
70
68
|
key={index}
|
|
71
69
|
style={({ pressed }) => [
|
|
72
70
|
styles.option,
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
{
|
|
72
|
+
backgroundColor: isSelected
|
|
73
|
+
? colors.mutedSelected
|
|
74
|
+
: pressed
|
|
75
|
+
? colors.mutedHover
|
|
76
|
+
: colors.mutedSubtle,
|
|
77
|
+
},
|
|
75
78
|
!isVertical && styles.optionHorizontal,
|
|
76
79
|
]}
|
|
77
80
|
onPress={() => handlePress(opt)}
|
|
@@ -80,7 +83,6 @@ export function SnapToggleGroup({
|
|
|
80
83
|
style={[
|
|
81
84
|
styles.optionText,
|
|
82
85
|
{ color: colors.text },
|
|
83
|
-
isSelected && styles.optionTextSelected,
|
|
84
86
|
]}
|
|
85
87
|
>
|
|
86
88
|
{opt}
|
|
@@ -117,12 +119,8 @@ const styles = StyleSheet.create({
|
|
|
117
119
|
optionHorizontal: {
|
|
118
120
|
flex: 1,
|
|
119
121
|
},
|
|
120
|
-
pressed: { opacity: 0.88 },
|
|
121
122
|
optionText: {
|
|
122
123
|
fontSize: 13,
|
|
123
124
|
fontWeight: "500",
|
|
124
125
|
},
|
|
125
|
-
optionTextSelected: {
|
|
126
|
-
color: "#fff",
|
|
127
|
-
},
|
|
128
126
|
});
|