@graineai/inapp-react-native 0.35.0 → 0.37.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.
@@ -4,6 +4,9 @@ import { Image, Modal, Platform, Pressable, ScrollView, Text, View, useColorSche
4
4
  import { Ctx, useGraineWidget } from "./index.js";
5
5
  import { GraineLoader } from "./loader.js";
6
6
  import { resolveNativeAppearance, resolveWidgetPalette } from "./appearance.js";
7
+ import { useDesign } from "./use-design.js";
8
+ import { alpha, letterSpacing, lineHeight, onAccentFor, shadowStyle, softFor } from "./design-math.js";
9
+ import { DEFAULT_DESIGN_PRESET, designPreset } from "../theme.js";
7
10
  import { PLATFORM_GRAMMAR, manySummary, resolveLoader, resolvePlatform, resolveWidgetLook, widgetSurface } from "../protocol.js";
8
11
  function tint(hex, alpha) {
9
12
  const m = /^#?([0-9a-f]{6})$/i.exec(String(hex).trim());
@@ -12,55 +15,127 @@ function tint(hex, alpha) {
12
15
  const n = parseInt(m[1], 16);
13
16
  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
14
17
  }
18
+ const CLASSIC_SCALE = designPreset(DEFAULT_DESIGN_PRESET).font.scale;
19
+ function classicSkin(theme, accent, radius) {
20
+ return {
21
+ classic: true,
22
+ size: (today) => today,
23
+ weight: (today) => today,
24
+ family: () => undefined,
25
+ line: (today) => today,
26
+ space: (today) => today,
27
+ pad: (h, v) => ({ paddingHorizontal: h, paddingVertical: v }),
28
+ gap: (compact) => (compact ? 6 : 10),
29
+ borderWidth: 1,
30
+ soft: (a) => tint(accent, a),
31
+ chip: tint(theme.border, 0.35),
32
+ choice: { fill: accent, text: theme.onAccent },
33
+ sheet: theme.surface,
34
+ page: theme.surface,
35
+ scrim: "rgba(0,0,0,0.45)",
36
+ rowRadius: Math.max(10, radius - 6),
37
+ imageRadius: Math.max(8, radius - 8),
38
+ slotRadius: 11,
39
+ sheetRadius: Math.max(20, radius),
40
+ depth: {
41
+ shadow: { shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 6 },
42
+ hard: null, ink: "#000",
43
+ },
44
+ anim: (want) => want,
45
+ };
46
+ }
47
+ function designSkin(look, theme, accent, radius, ownRadius, surfaceOverride) {
48
+ const t = look.tokens;
49
+ const f = t.font;
50
+ const density = t.spacing.cardPadding / 14;
51
+ return {
52
+ classic: false,
53
+ size: (today, role, exact) => (exact ? f.scale[role] : Math.round(((today * f.scale[role]) / CLASSIC_SCALE[role]) * 10) / 10),
54
+ weight: (_today, which) => String(f.weights[which]),
55
+ family: (heading) => look.font(heading ? f.heading : f.body),
56
+ line: (_today, size, title) => lineHeight(size, title ? f.lineHeight.title : f.lineHeight.body),
57
+ space: (today) => Math.round(today * density),
58
+ pad: () => ({ paddingHorizontal: t.spacing.cardPadding, paddingVertical: t.spacing.cardPadding }),
59
+ gap: (compact) => (compact ? Math.round(t.spacing.gap * 0.6) : t.spacing.gap),
60
+ borderWidth: t.shape.borderWidth,
61
+ soft: () => softFor(accent, t),
62
+ chip: softFor(accent, t),
63
+ choice: { fill: softFor(accent, t), text: theme.text },
64
+ sheet: surfaceOverride ?? t.colors.surface,
65
+ page: surfaceOverride ?? t.colors.background,
66
+ scrim: alpha(t.colors.scrim, 0.4),
67
+ rowRadius: ownRadius ? Math.max(10, radius - 6) : t.shape.radius.md,
68
+ imageRadius: ownRadius ? Math.max(8, radius - 8) : t.shape.radius.sm,
69
+ slotRadius: t.shape.radius.md,
70
+ sheetRadius: ownRadius ? Math.max(20, radius) : t.shape.radius.xl,
71
+ depth: { shadow: shadowStyle(t.elevation.card), hard: t.elevation.hardOffset, ink: t.colors.text },
72
+ anim: (want) => (look.durationMs > 0 ? want : "none"),
73
+ };
74
+ }
75
+ function Raised({ k, style, children }) {
76
+ if (k.classic || !k.depth.hard) {
77
+ return _jsx(View, { style: k.classic ? style : [style, k.depth.shadow], children: children });
78
+ }
79
+ const { marginBottom, borderRadius, ...rest } = style;
80
+ const h = k.depth.hard;
81
+ return (_jsxs(View, { style: { marginBottom: (marginBottom ?? 0) + h.y, marginRight: h.x }, children: [_jsx(View, { pointerEvents: "none", style: { position: "absolute", top: h.y, left: h.x, right: -h.x, bottom: -h.y, borderRadius, backgroundColor: k.depth.ink } }), _jsx(View, { style: { ...rest, borderRadius, borderColor: k.depth.ink }, children: children })] }));
82
+ }
15
83
  const HERO_HEIGHT = { small: 100, medium: 140, large: 180 };
16
84
  const THUMB = { small: 56, medium: 72, large: 96 };
17
85
  const LIST_THUMB = { small: 36, medium: 44, large: 56 };
18
- function WidgetRow({ row, variant, look, grammar, theme, accent, radius, picked, onPick, }) {
86
+ function WidgetRow({ row, variant, look, grammar, theme, accent, k, picked, onPick, }) {
19
87
  const many = picked !== null;
20
- const r = Math.max(10, radius - 6);
88
+ const r = k.rowRadius;
21
89
  const tight = look.density === "compact";
22
- const pad = (variant === "list" ? (tight ? 6 : 9) : (tight ? 7 : 10)) + grammar.padBoost;
23
- const gap = tight ? 6 : 10;
90
+ const pad = k.space(variant === "list" ? (tight ? 6 : 9) : (tight ? 7 : 10)) + grammar.padBoost;
91
+ const gap = k.gap(tight);
92
+ const edge = (on) => (on ? k.borderWidth + 1 : k.borderWidth);
24
93
  const rowIsButton = many || look.button === "row";
25
94
  const outline = look.button === "outline";
26
95
  const label = row.unavailable ? "Unavailable" : row.selectLabel;
27
96
  const a11y = `${row.selectLabel}: ${row.title}`;
28
97
  const badge = row.subtitle ? (_jsx(View, { style: { flexDirection: "row" }, children: _jsx(Text, { style: {
29
- color: accent, fontSize: 10.5, fontWeight: "800", letterSpacing: 0.4,
30
- textTransform: "uppercase", backgroundColor: tint(accent, 0.15),
98
+ color: accent, fontSize: k.size(10.5, "caption"), fontWeight: k.weight("800", "strong"), letterSpacing: 0.4,
99
+ textTransform: "uppercase", backgroundColor: k.soft(0.15), fontFamily: k.family(),
31
100
  borderRadius: 999, paddingHorizontal: 8, paddingVertical: 3, overflow: "hidden",
32
101
  }, children: row.subtitle }) })) : null;
33
- const titleLine = (big) => (_jsxs(View, { style: { flexDirection: "row", alignItems: "baseline", gap: 8 }, children: [_jsx(Text, { numberOfLines: 2, style: { color: theme.text, fontSize: big ? 16.5 : (variant === "list" ? 13.5 : 14.5), fontWeight: grammar.titleWeight, flex: 1 }, children: row.title }), row.price ? (_jsx(Text, { style: {
34
- color: theme.text, fontSize: big ? 17 : (variant === "list" ? 13.5 : 14.5), fontWeight: "800",
35
- fontVariant: ["tabular-nums"],
102
+ const titleLine = (big) => (_jsxs(View, { style: { flexDirection: "row", alignItems: "baseline", gap: 8 }, children: [_jsx(Text, { numberOfLines: 2, style: {
103
+ color: theme.text, fontWeight: grammar.titleWeight, flex: 1,
104
+ fontSize: big ? k.size(16.5, "title", true) : k.size(variant === "list" ? 13.5 : 14.5, "body"),
105
+ fontFamily: k.family(big),
106
+ }, children: row.title }), row.price ? (_jsx(Text, { style: {
107
+ color: theme.text, fontSize: big ? k.size(17, "title") : k.size(variant === "list" ? 13.5 : 14.5, "body"), fontWeight: k.weight("800", "strong"),
108
+ fontVariant: ["tabular-nums"], fontFamily: k.family(),
36
109
  }, children: row.price })) : null] }));
37
110
  const facts = look.facts && row.facts.length ? (_jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 2 }, children: (variant === "list" ? row.facts.slice(0, 2) : row.facts).map((f, i) => (_jsx(Text, { style: {
38
- color: theme.muted, fontSize: variant === "list" ? 10.5 : 11.5, fontWeight: "600",
39
- backgroundColor: grammar.chipOutlined ? "transparent" : tint(theme.border, 0.35),
40
- borderWidth: grammar.chipOutlined ? 1 : 0, borderColor: theme.border,
111
+ color: theme.muted, fontSize: k.size(variant === "list" ? 10.5 : 11.5, "caption"), fontWeight: k.weight("600", "strong"),
112
+ fontFamily: k.family(),
113
+ backgroundColor: grammar.chipOutlined ? "transparent" : k.chip,
114
+ borderWidth: grammar.chipOutlined ? k.borderWidth : 0, borderColor: theme.border,
41
115
  borderRadius: grammar.chipRadius, paddingHorizontal: variant === "list" ? 6 : 8, paddingVertical: variant === "list" ? 2 : 4, overflow: "hidden",
42
116
  }, children: f }, `${f}-${i}`))) })) : null;
43
117
  const tick = (_jsx(View, { style: {
44
118
  width: 22, height: 22, borderRadius: 999, alignItems: "center", justifyContent: "center",
45
119
  backgroundColor: picked ? accent : "transparent",
46
- borderWidth: picked ? 0 : 1.5, borderColor: theme.border,
120
+ borderWidth: picked ? 0 : k.borderWidth + 0.5, borderColor: theme.border,
47
121
  }, children: picked ? _jsx(Text, { style: { color: theme.onAccent, fontSize: 12, fontWeight: "800" }, children: "\u2713" }) : null }));
48
122
  const button = (wide) => (_jsx(Pressable, { disabled: row.unavailable, onPress: onPick, accessibilityRole: "button", accessibilityLabel: a11y, hitSlop: wide ? undefined : 8, style: {
49
123
  minHeight: 44, justifyContent: "center", alignItems: "center",
50
124
  borderRadius: wide ? grammar.buttonRadius : (grammar.pillButtons ? 999 : grammar.buttonRadius), paddingHorizontal: wide ? 12 : 14,
51
125
  marginTop: wide ? 8 : 0,
52
126
  backgroundColor: row.unavailable || outline ? "transparent" : accent,
53
- borderWidth: row.unavailable || outline ? 1.5 : 0,
127
+ borderWidth: row.unavailable || outline ? k.borderWidth + 0.5 : 0,
54
128
  borderColor: row.unavailable ? theme.border : accent,
55
129
  }, children: _jsx(Text, { style: {
56
130
  color: row.unavailable ? theme.muted : (outline ? accent : theme.onAccent),
57
- fontSize: wide ? 14 : 12.5, fontWeight: "700",
131
+ fontSize: wide ? k.size(14, "body") : k.size(12.5, "caption"), fontWeight: k.weight("700", "strong"),
132
+ fontFamily: k.family(),
58
133
  }, children: label }) }));
59
- const image = (style) => row.image ? (_jsx(Image, { source: { uri: row.image }, accessibilityIgnoresInvertColors: true, resizeMode: "cover", style: { borderRadius: Math.max(8, radius - 8), backgroundColor: theme.border, ...style } })) : null;
134
+ const image = (style) => row.image ? (_jsx(Image, { source: { uri: row.image }, accessibilityIgnoresInvertColors: true, resizeMode: "cover", style: { borderRadius: k.imageRadius, backgroundColor: theme.border, ...style } })) : null;
60
135
  const wrap = (children, style) => rowIsButton ? (_jsx(Pressable, { disabled: row.unavailable, onPress: onPick, accessibilityRole: "button", accessibilityLabel: a11y, style: style, children: children })) : (_jsx(View, { style: style, children: children }));
61
136
  if (variant === "hero") {
62
137
  return wrap(_jsxs(_Fragment, { children: [image({ width: "100%", height: HERO_HEIGHT[look.imageSize] }), _jsxs(View, { style: { gap: 3 }, children: [badge, titleLine(true), facts, many ? null : rowIsButton ? null : button(true)] }), many ? _jsx(View, { style: { position: "absolute", top: pad + 6, right: pad + 6 }, children: tick }) : null] }), {
63
- borderRadius: r, borderWidth: picked ? 2 : 1, borderColor: picked ? accent : theme.border, padding: pad,
138
+ borderRadius: r, borderWidth: edge(picked), borderColor: picked ? accent : theme.border, padding: pad,
64
139
  opacity: row.unavailable ? 0.55 : 1, gap, flexDirection: "column",
65
140
  });
66
141
  }
@@ -69,13 +144,13 @@ function WidgetRow({ row, variant, look, grammar, theme, accent, radius, picked,
69
144
  return wrap(_jsxs(_Fragment, { children: [image({ width: t, height: t }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 2 }, children: [titleLine(false), facts] }), many ? tick : rowIsButton ? (_jsx(Text, { style: { color: theme.muted, fontSize: 22, fontWeight: "600", paddingLeft: 4 }, children: "›" })) : button(false)] }), {
70
145
  flexDirection: "row", alignItems: "center", gap,
71
146
  paddingVertical: pad, paddingHorizontal: 2, minHeight: 44,
72
- borderBottomWidth: 1, borderBottomColor: theme.border, marginLeft: grammar.separatorInset,
147
+ borderBottomWidth: k.borderWidth, borderBottomColor: theme.border, marginLeft: grammar.separatorInset,
73
148
  opacity: row.unavailable ? 0.55 : 1,
74
149
  });
75
150
  }
76
151
  if (variant === "compact") {
77
152
  return wrap(_jsxs(_Fragment, { children: [_jsxs(View, { style: { flex: 1, minWidth: 0, gap: 2 }, children: [badge, titleLine(false), facts] }), many ? tick : rowIsButton ? null : button(false)] }), {
78
- borderRadius: r, borderWidth: picked ? 2 : 1, borderColor: picked ? accent : theme.border,
153
+ borderRadius: r, borderWidth: edge(picked), borderColor: picked ? accent : theme.border,
79
154
  paddingVertical: pad, paddingHorizontal: pad + 2,
80
155
  flexDirection: "row", alignItems: "center", gap,
81
156
  opacity: row.unavailable ? 0.55 : 1, minHeight: 44,
@@ -83,11 +158,11 @@ function WidgetRow({ row, variant, look, grammar, theme, accent, radius, picked,
83
158
  }
84
159
  const t = THUMB[look.imageSize];
85
160
  return wrap(_jsxs(_Fragment, { children: [image({ width: t, height: t }), _jsxs(View, { style: { flex: 1, minWidth: 0, gap: 3 }, children: [badge, titleLine(false), facts] }), many ? tick : rowIsButton ? null : button(false)] }), {
86
- borderRadius: r, borderWidth: picked ? 2 : 1, borderColor: picked ? accent : theme.border, padding: pad,
161
+ borderRadius: r, borderWidth: edge(picked), borderColor: picked ? accent : theme.border, padding: pad,
87
162
  opacity: row.unavailable ? 0.55 : 1, flexDirection: "row", gap, alignItems: "center",
88
163
  });
89
164
  }
90
- function AnsweredLine({ answered, theme, accent, radius, awaiting, onClear }) {
165
+ function AnsweredLine({ answered, theme, accent, radius, k, awaiting, onClear }) {
91
166
  const d = (answered.data || {});
92
167
  const appearance = useContext(Ctx)?.appearance ?? null;
93
168
  const loaderText = resolveLoader(appearance?.loader).text.thinking;
@@ -96,31 +171,66 @@ function AnsweredLine({ answered, theme, accent, radius, awaiting, onClear }) {
96
171
  if (!title && !detail)
97
172
  return null;
98
173
  return (_jsxs(View, { style: {
99
- backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border,
100
- paddingHorizontal: 12, paddingVertical: 10, marginBottom: 8,
174
+ backgroundColor: theme.surface, borderRadius: radius, borderWidth: k.borderWidth, borderColor: theme.border,
175
+ paddingHorizontal: k.space(12), paddingVertical: k.space(10), marginBottom: 8,
101
176
  flexDirection: "row", alignItems: "center", gap: 10,
102
177
  }, children: [_jsx(View, { style: {
103
- width: 26, height: 26, borderRadius: 999, backgroundColor: tint(accent, 0.18),
178
+ width: 26, height: 26, borderRadius: 999, backgroundColor: k.soft(0.18),
104
179
  alignItems: "center", justifyContent: "center",
105
- }, children: awaiting ? _jsx(GraineLoader, { size: 14 }) : (_jsx(Text, { style: { color: accent, fontSize: 14, fontWeight: "800" }, children: "✓" })) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: 13.5, fontWeight: "700" }, children: title || detail }), awaiting ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: 11.5, fontWeight: "600" }, children: loaderText })) : title && detail ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: 11.5, fontWeight: "600" }, children: detail })) : null] }), awaiting ? null : (_jsx(Pressable, { onPress: onClear, accessibilityRole: "button", accessibilityLabel: "Clear what I chose", hitSlop: 10, style: { minHeight: 44, justifyContent: "center", paddingHorizontal: 4 }, children: _jsx(Text, { style: { color: theme.muted, fontSize: 12, fontWeight: "700" }, children: "Clear" }) }))] }));
180
+ }, children: awaiting ? _jsx(GraineLoader, { size: 14 }) : (_jsx(Text, { style: { color: accent, fontSize: 14, fontWeight: "800" }, children: "✓" })) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: k.size(13.5, "body"), fontWeight: k.weight("700", "strong"), fontFamily: k.family() }, children: title || detail }), awaiting ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: k.size(11.5, "caption"), fontWeight: k.weight("600", "body"), fontFamily: k.family() }, children: loaderText })) : title && detail ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: k.size(11.5, "caption"), fontWeight: k.weight("600", "body"), fontFamily: k.family() }, children: detail })) : null] }), awaiting ? null : (_jsx(Pressable, { onPress: onClear, accessibilityRole: "button", accessibilityLabel: "Clear what I chose", hitSlop: 10, style: { minHeight: 44, justifyContent: "center", paddingHorizontal: 4 }, children: _jsx(Text, { style: { color: theme.muted, fontSize: k.size(12, "caption"), fontWeight: k.weight("700", "strong"), fontFamily: k.family() }, children: "Clear" }) }))] }));
106
181
  }
107
182
  export function GraineWidget(props) {
108
183
  const { widget, answered, loading, awaitingReply, submit, dismiss } = useGraineWidget();
109
184
  const appearance = useContext(Ctx)?.appearance ?? null;
110
185
  const scheme = useColorScheme();
186
+ const design = useDesign();
111
187
  const [pagerWidth, setPagerWidth] = useState(0);
112
188
  const [page, setPage] = useState(0);
113
189
  const [ticked, setTicked] = useState([]);
114
190
  const look = resolveWidgetLook(widget?.style, widget?.kind);
115
- const grammar = PLATFORM_GRAMMAR[resolvePlatform(look.platform, Platform.OS, Platform.constants?.Manufacturer ?? Platform.constants?.Brand)];
191
+ const phoneGrammar = PLATFORM_GRAMMAR[resolvePlatform(look.platform, Platform.OS, Platform.constants?.Manufacturer ?? Platform.constants?.Brand)];
116
192
  const dash = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "" });
117
- const accent = props.accent ?? look.accent ?? dash.accent;
118
- const radius = props.radius ?? look.radius ?? (look.platform !== "auto" ? grammar.radius : dash.radius);
119
- const theme = {
120
- ...resolveWidgetPalette(appearance, { scheme, accent }),
121
- ...(props.theme ?? {}),
122
- };
123
- const chosen = answered ? (_jsx(AnsweredLine, { answered: answered, theme: theme, accent: accent, radius: radius, awaiting: awaitingReply, onClear: dismiss })) : null;
193
+ let accent;
194
+ let radius;
195
+ let theme;
196
+ let grammar;
197
+ let k;
198
+ if (design.classic) {
199
+ accent = props.accent ?? look.accent ?? dash.accent;
200
+ radius = props.radius ?? look.radius ?? (look.platform !== "auto" ? phoneGrammar.radius : dash.radius);
201
+ theme = {
202
+ ...resolveWidgetPalette(appearance, { scheme, accent }),
203
+ ...(props.theme ?? {}),
204
+ };
205
+ grammar = phoneGrammar;
206
+ k = classicSkin(theme, accent, radius);
207
+ }
208
+ else {
209
+ const t = design.tokens;
210
+ const own = look.platform !== "auto";
211
+ accent = props.accent ?? look.accent ?? t.colors.accent;
212
+ radius = props.radius ?? look.radius ?? (own ? phoneGrammar.radius : t.shape.radius.lg);
213
+ theme = {
214
+ surface: t.colors.surfaceRaised, border: t.colors.border, text: t.colors.text, muted: t.colors.textMuted,
215
+ onAccent: onAccentFor(accent, t),
216
+ ...(props.theme ?? {}),
217
+ };
218
+ grammar = own ? phoneGrammar : {
219
+ ...phoneGrammar,
220
+ radius: t.shape.radius.lg, chipRadius: t.shape.radius.sm, buttonRadius: t.shape.radius.md,
221
+ pillButtons: false, titleWeight: String(t.font.weights.title), padBoost: 0, chipOutlined: false,
222
+ };
223
+ k = designSkin(design, theme, accent, radius, own || props.radius !== undefined || look.radius !== undefined, props.theme?.surface);
224
+ }
225
+ const titleType = (today) => k.classic
226
+ ? { fontSize: today, fontWeight: "800" }
227
+ : {
228
+ fontSize: design.tokens.font.scale.title,
229
+ fontWeight: String(design.tokens.font.weights.title),
230
+ fontFamily: k.family(true),
231
+ letterSpacing: letterSpacing(design.tokens.font.scale.title, design.tokens.font.letterSpacing.title),
232
+ };
233
+ const chosen = answered ? (_jsx(AnsweredLine, { answered: answered, theme: theme, accent: accent, radius: radius, k: k, awaiting: awaitingReply, onClear: dismiss })) : null;
124
234
  if (!widget && loading) {
125
235
  const loader = resolveLoader(appearance?.loader);
126
236
  const kindSurface = widgetSurface(loading.kind);
@@ -128,14 +238,14 @@ export function GraineWidget(props) {
128
238
  const big = pendingSurface === "screen" ? 34 : pendingSurface === "sheet" ? 26 : undefined;
129
239
  const mark = (_jsx(View, { style: { alignItems: "center", gap: 10 }, children: _jsx(GraineLoader, { moment: "component", size: big, stacked: true }) }));
130
240
  const bone = (w, h) => (_jsx(View, { style: { width: w, height: h, borderRadius: 8, backgroundColor: tint(theme.border, 0.6) } }));
131
- const bones = (rows) => (_jsx(View, { style: { gap: 10, opacity: 0.55 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: Array.from({ length: rows }, (_, i) => (_jsxs(View, { style: { borderRadius: Math.max(10, radius - 6), borderWidth: 1, borderColor: theme.border, padding: 10, flexDirection: "row", gap: 10, alignItems: "center" }, children: [bone(72, 72), _jsxs(View, { style: { flex: 1, gap: 8 }, children: [bone("70%", 12), bone("40%", 12), bone("55%", 10)] })] }, i))) }));
241
+ const bones = (rows) => (_jsx(View, { style: { gap: 10, opacity: 0.55 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: Array.from({ length: rows }, (_, i) => (_jsxs(View, { style: { borderRadius: k.rowRadius, borderWidth: k.borderWidth, borderColor: theme.border, padding: 10, flexDirection: "row", gap: 10, alignItems: "center" }, children: [bone(72, 72), _jsxs(View, { style: { flex: 1, gap: 8 }, children: [bone("70%", 12), bone("40%", 12), bone("55%", 10)] })] }, i))) }));
132
242
  if (pendingSurface === "screen") {
133
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: theme.surface, paddingTop: 56, paddingHorizontal: 18 }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: { width: 44, height: 44, borderRadius: 999, alignItems: "center", justifyContent: "center", backgroundColor: tint(theme.border, 0.4) }, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "\u00D7" }) }), _jsx(View, { style: { flex: 1, justifyContent: "center", paddingBottom: loader.skeleton ? 24 : 72 }, children: mark }), loader.skeleton ? bones(3) : null] }) })] }));
243
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: k.anim("fade"), onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: k.page, paddingTop: 56, paddingHorizontal: 18 }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: { width: 44, height: 44, borderRadius: 999, alignItems: "center", justifyContent: "center", backgroundColor: tint(theme.border, 0.4) }, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "\u00D7" }) }), _jsx(View, { style: { flex: 1, justifyContent: "center", paddingBottom: loader.skeleton ? 24 : 72 }, children: mark }), loader.skeleton ? bones(3) : null] }) })] }));
134
244
  }
135
245
  if (pendingSurface === "sheet") {
136
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: { backgroundColor: theme.surface, borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius), paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 18 }, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), _jsx(View, { style: { paddingVertical: loader.skeleton ? 10 : 34 }, children: mark }), loader.skeleton ? bones(2) : null] })] }) })] }));
246
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: k.anim("fade"), transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: k.scrim, justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: { backgroundColor: k.sheet, borderTopLeftRadius: k.sheetRadius, borderTopRightRadius: k.sheetRadius, paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 18 }, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), _jsx(View, { style: { paddingVertical: loader.skeleton ? 10 : 34 }, children: mark }), loader.skeleton ? bones(2) : null] })] }) })] }));
137
247
  }
138
- return (_jsxs(_Fragment, { children: [chosen, _jsx(View, { style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border, paddingHorizontal: 14, paddingVertical: 14, marginBottom: 8, alignItems: "center" }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
248
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Raised, { k: k, style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: k.borderWidth, borderColor: theme.border, ...k.pad(14, 14), marginBottom: 8, alignItems: "center" }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
139
249
  }
140
250
  if (!widget)
141
251
  return chosen;
@@ -214,24 +324,24 @@ export function GraineWidget(props) {
214
324
  }, manySummary(titles, normalised.selectLabel.toLowerCase()));
215
325
  setTicked([]);
216
326
  };
217
- const body = (_jsx(_Fragment, { children: isCalendar ? (_jsxs(View, { style: { gap: 12 }, children: [data.note ? (_jsx(Text, { style: { color: theme.muted, fontSize: 12.5, lineHeight: 18 }, children: String(data.note) })) : null, data.days.slice(0, 4).map((d, di) => {
327
+ const body = (_jsx(_Fragment, { children: isCalendar ? (_jsxs(View, { style: { gap: 12 }, children: [data.note ? (_jsx(Text, { style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), lineHeight: k.line(18, k.size(12.5, "caption", true)), fontFamily: k.family() }, children: String(data.note) })) : null, data.days.slice(0, 4).map((d, di) => {
218
328
  const dayLabel = String(d?.label ?? d?.id ?? `Day ${di + 1}`);
219
329
  const slots = Array.isArray(d?.slots) ? d.slots.map(String) : [];
220
330
  if (!slots.length)
221
331
  return null;
222
- return (_jsxs(View, { style: { gap: 7 }, children: [_jsx(Text, { style: { color: theme.text, fontSize: 13, fontWeight: "700" }, children: dayLabel }), _jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 8 }, children: slots.slice(0, 8).map((slot, si) => (_jsx(Pressable, { onPress: () => submit({ day: dayLabel, slot }, `${dayLabel} at ${slot}, please.`), accessibilityRole: "button", accessibilityLabel: `${dayLabel} at ${slot}`, style: {
332
+ return (_jsxs(View, { style: { gap: 7 }, children: [_jsx(Text, { style: { color: theme.text, fontSize: k.size(13, "body"), fontWeight: k.weight("700", "strong"), fontFamily: k.family() }, children: dayLabel }), _jsx(View, { style: { flexDirection: "row", flexWrap: "wrap", gap: 8 }, children: slots.slice(0, 8).map((slot, si) => (_jsx(Pressable, { onPress: () => submit({ day: dayLabel, slot }, `${dayLabel} at ${slot}, please.`), accessibilityRole: "button", accessibilityLabel: `${dayLabel} at ${slot}`, style: {
223
333
  minHeight: 44, justifyContent: "center",
224
- borderWidth: 1, borderColor: theme.border, borderRadius: 11,
334
+ borderWidth: k.borderWidth, borderColor: theme.border, borderRadius: k.slotRadius,
225
335
  paddingHorizontal: 14,
226
- }, children: _jsx(Text, { style: { color: theme.text, fontSize: 13.5, fontWeight: "600" }, children: slot }) }, `${slot}-${si}`))) })] }, `${dayLabel}-${di}`));
336
+ }, children: _jsx(Text, { style: { color: theme.text, fontSize: k.size(13.5, "body"), fontWeight: k.weight("600", "strong"), fontFamily: k.family() }, children: slot }) }, `${slot}-${si}`))) })] }, `${dayLabel}-${di}`));
227
337
  })] })) : isProducts ? ((() => {
228
338
  const toggle = (id) => setTicked((cur) => cur.includes(id) ? cur.filter((x) => x !== id)
229
339
  : cur.length >= look.maxSelect ? [...cur.slice(1), id]
230
340
  : [...cur, id]);
231
- const draw = (row, i, variant) => (_jsx(WidgetRow, { row: row, variant: variant, look: look, grammar: grammar, theme: theme, accent: accent, radius: radius, picked: many ? ticked.includes(row.id) : null, onPick: () => many
341
+ const draw = (row, i, variant) => (_jsx(WidgetRow, { row: row, variant: variant, look: look, grammar: grammar, theme: theme, accent: accent, k: k, picked: many ? ticked.includes(row.id) : null, onPick: () => many
232
342
  ? toggle(row.id)
233
343
  : pick({ ...normalised.items[i], id: row.id, title: row.title, price: row.price }) }, `${row.id}-${i}`));
234
- const gap = look.density === "compact" ? 6 : 10;
344
+ const gap = k.gap(look.density === "compact");
235
345
  if (look.preset === "spotlight") {
236
346
  const cardW = pagerWidth ? Math.round(pagerWidth * 0.86) : 300;
237
347
  const step = cardW + gap;
@@ -265,7 +375,7 @@ export function GraineWidget(props) {
265
375
  backgroundColor: chosenRows.length < 2 ? tint(theme.border, 0.5) : accent,
266
376
  }, children: _jsx(Text, { style: {
267
377
  color: chosenRows.length < 2 ? theme.muted : theme.onAccent,
268
- fontSize: 14.5, fontWeight: "700",
378
+ fontSize: k.size(14.5, "body"), fontWeight: k.weight("700", "strong"), fontFamily: k.family(),
269
379
  }, children: chosenRows.length < 2
270
380
  ? `Pick ${2 - chosenRows.length} more to ${normalised.selectLabel.toLowerCase()}`
271
381
  : `${normalised.selectLabel} ${chosenRows.length}` }) })] }));
@@ -273,37 +383,37 @@ export function GraineWidget(props) {
273
383
  const label = String(o?.label ?? o?.title ?? o ?? "");
274
384
  const id = String(o?.id ?? label);
275
385
  return (_jsx(Pressable, { onPress: () => submit(isQuickActions ? { id, label } : { chosen: id, label }, `I'll go with ${label}.`), accessibilityRole: "button", accessibilityLabel: label, style: {
276
- backgroundColor: accent, borderRadius: 999,
386
+ backgroundColor: k.choice.fill, borderRadius: 999,
277
387
  minHeight: 44, justifyContent: "center", paddingHorizontal: 16,
278
- }, children: _jsx(Text, { style: { color: theme.onAccent, fontSize: 13.5, fontWeight: "700" }, children: label }) }, `${id}-${i}`));
279
- }) })) : (_jsx(Text, { style: { color: theme.text, fontSize: 14, lineHeight: 20 }, children: widget.fallbackText || "The assistant has something to show you." })) }));
388
+ }, children: _jsx(Text, { style: { color: k.choice.text, fontSize: k.size(13.5, "body"), fontWeight: k.weight("700", "strong"), fontFamily: k.family() }, children: label }) }, `${id}-${i}`));
389
+ }) })) : (_jsx(Text, { style: { color: theme.text, fontSize: k.size(14, "body", true), lineHeight: k.line(20, k.size(14, "body", true)), fontFamily: k.family() }, children: widget.fallbackText || "The assistant has something to show you." })) }));
280
390
  const title = data?.title ? String(data.title) : "";
281
391
  const prompt = data?.prompt ? String(data.prompt) : "";
282
392
  if (surface === "screen") {
283
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "slide", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: theme.surface }, children: [_jsxs(View, { style: {
393
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: k.anim("slide"), onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: k.page }, children: [_jsxs(View, { style: {
284
394
  paddingTop: 52, paddingHorizontal: 18, paddingBottom: 12,
285
395
  flexDirection: "row", alignItems: "center", gap: 12,
286
396
  }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: {
287
397
  width: 44, height: 44, borderRadius: 999, alignItems: "center", justifyContent: "center",
288
398
  backgroundColor: tint(theme.border, 0.4),
289
- }, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "×" }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [title ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: 18, fontWeight: "800" }, children: title })) : null, prompt ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: 12.5, fontWeight: "600", marginTop: 1 }, children: prompt })) : null] })] }), _jsx(ScrollView, { style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 18, paddingBottom: 24 }, keyboardShouldPersistTaps: "handled", children: body }), _jsxs(View, { style: {
399
+ }, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "×" }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [title ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.text, ...titleType(18) }, children: title })) : null, prompt ? (_jsx(Text, { numberOfLines: 1, style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), fontWeight: k.weight("600", "body"), fontFamily: k.family(), marginTop: 1 }, children: prompt })) : null] })] }), _jsx(ScrollView, { style: { flex: 1 }, contentContainerStyle: { paddingHorizontal: 18, paddingBottom: 24 }, keyboardShouldPersistTaps: "handled", children: body }), _jsxs(View, { style: {
290
400
  paddingHorizontal: 18, paddingTop: 12, paddingBottom: 28,
291
- borderTopWidth: 1, borderTopColor: theme.border,
292
- }, children: [_jsx(Text, { style: { color: theme.muted, fontSize: 10.5, fontWeight: "800", letterSpacing: 0.4, textTransform: "uppercase" }, children: "Still listening" }), _jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: 13.5, marginTop: 2 }, children: prompt || "Tap one, or just say it." })] })] }) })] }));
401
+ borderTopWidth: k.borderWidth, borderTopColor: theme.border,
402
+ }, children: [_jsx(Text, { style: { color: theme.muted, fontSize: k.size(10.5, "caption"), fontWeight: k.weight("800", "strong"), fontFamily: k.family(), letterSpacing: 0.4, textTransform: "uppercase" }, children: "Still listening" }), _jsx(Text, { numberOfLines: 1, style: { color: theme.text, fontSize: k.size(13.5, "body", true), fontFamily: k.family(), marginTop: 2 }, children: prompt || "Tap one, or just say it." })] })] }) })] }));
293
403
  }
294
404
  if (surface === "sheet") {
295
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "slide", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: {
296
- backgroundColor: theme.surface,
297
- borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius),
405
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: k.anim("slide"), transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: k.scrim, justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: {
406
+ backgroundColor: k.sheet,
407
+ borderTopLeftRadius: k.sheetRadius, borderTopRightRadius: k.sheetRadius,
298
408
  paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 14,
299
- }, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), title ? (_jsx(Text, { style: { color: theme.text, fontSize: 18, fontWeight: "800" }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: 13, lineHeight: 18, marginTop: -8 }, children: prompt })) : null, _jsx(ScrollView, { style: { maxHeight: 420 }, contentContainerStyle: { paddingBottom: 4 }, keyboardShouldPersistTaps: "handled", children: body }), _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "None of these", style: {
409
+ }, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), title ? (_jsx(Text, { style: { color: theme.text, ...titleType(18) }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: k.size(13, "caption", true), lineHeight: k.line(18, k.size(13, "caption", true)), fontFamily: k.family(), marginTop: -8 }, children: prompt })) : null, _jsx(ScrollView, { style: { maxHeight: 420 }, contentContainerStyle: { paddingBottom: 4 }, keyboardShouldPersistTaps: "handled", children: body }), _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "None of these", style: {
300
410
  alignSelf: "flex-start", minHeight: 44, justifyContent: "center",
301
411
  paddingHorizontal: 16, borderRadius: 999, backgroundColor: tint(theme.border, 0.4),
302
- }, children: _jsx(Text, { style: { color: theme.text, fontSize: 13.5, fontWeight: "700" }, children: "None of these" }) })] })] }) })] }));
412
+ }, children: _jsx(Text, { style: { color: theme.text, fontSize: k.size(13.5, "body"), fontWeight: k.weight("700", "strong"), fontFamily: k.family() }, children: "None of these" }) })] })] }) })] }));
303
413
  }
304
- return (_jsxs(_Fragment, { children: [chosen, _jsxs(View, { style: {
305
- backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border,
306
- paddingHorizontal: 14, paddingVertical: 12, marginBottom: 8,
307
- shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 6,
308
- }, children: [title ? (_jsx(Text, { style: { color: theme.text, fontSize: 14.5, fontWeight: "800", marginBottom: 2 }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: 12.5, lineHeight: 17, marginBottom: 8 }, children: prompt })) : _jsx(View, { style: { height: 6 } }), body, _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Dismiss", hitSlop: 8, style: { alignSelf: "flex-end", marginTop: 10, minHeight: 44, justifyContent: "center" }, children: _jsx(Text, { style: { color: theme.muted, fontSize: 12.5, fontWeight: "600" }, children: "Dismiss" }) })] })] }));
414
+ return (_jsxs(_Fragment, { children: [chosen, _jsxs(Raised, { k: k, style: {
415
+ backgroundColor: theme.surface, borderRadius: radius, borderWidth: k.borderWidth, borderColor: theme.border,
416
+ ...k.pad(14, 12), marginBottom: 8,
417
+ ...(k.classic ? k.depth.shadow : null),
418
+ }, children: [title ? (_jsx(Text, { style: { color: theme.text, ...titleType(14.5), marginBottom: 2 }, children: title })) : null, prompt ? (_jsx(Text, { style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), lineHeight: k.line(17, k.size(12.5, "caption", true)), fontFamily: k.family(), marginBottom: 8 }, children: prompt })) : _jsx(View, { style: { height: 6 } }), body, _jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Dismiss", hitSlop: 8, style: { alignSelf: "flex-end", marginTop: 10, minHeight: 44, justifyContent: "center" }, children: _jsx(Text, { style: { color: theme.muted, fontSize: k.size(12.5, "caption", true), fontWeight: k.weight("600", "body"), fontFamily: k.family() }, children: "Dismiss" }) })] })] }));
309
419
  }
@@ -0,0 +1,206 @@
1
+ export type DesignScheme = "auto" | "light" | "dark";
2
+ export type DesignDensity = "compact" | "comfortable" | "spacious";
3
+ export type DesignShadow = "preset" | "none" | "soft" | "medium" | "strong" | "hard";
4
+ export type DesignMotion = "preset" | "none" | "subtle" | "lively";
5
+ export type DesignNeutral = "preset" | "cool" | "neutral" | "warm";
6
+ export type DesignBubbles = "filled" | "outline" | "minimal";
7
+ export type LauncherShape = "circle" | "squircle" | "rounded-square" | "square" | "pill";
8
+ export type IconStyle = "outline" | "filled" | "duotone" | "sharp";
9
+ export type MotionEasing = "standard" | "emphasized" | "spring" | "linear" | "none";
10
+ export type DesignFamily = "graine" | "native" | "modern" | "enterprise" | "expressive";
11
+ export interface DesignConfig {
12
+ preset: string;
13
+ accent?: string;
14
+ scheme?: DesignScheme;
15
+ roundness?: number;
16
+ font?: string;
17
+ headingFont?: string;
18
+ density?: DesignDensity;
19
+ shadow?: DesignShadow;
20
+ motion?: DesignMotion;
21
+ neutral?: DesignNeutral;
22
+ glass?: boolean;
23
+ bubbles?: DesignBubbles;
24
+ launcherIcon?: string;
25
+ launcherShape?: LauncherShape;
26
+ iconStyle?: IconStyle;
27
+ }
28
+ export interface ThemeColors {
29
+ background: string;
30
+ surface: string;
31
+ surfaceRaised: string;
32
+ text: string;
33
+ textMuted: string;
34
+ border: string;
35
+ accent: string;
36
+ onAccent: string;
37
+ accentSoft: string;
38
+ success: string;
39
+ warning: string;
40
+ danger: string;
41
+ userBubble: string;
42
+ userBubbleText: string;
43
+ agentBubble: string;
44
+ agentBubbleText: string;
45
+ scrim: string;
46
+ }
47
+ export interface ThemeShadow {
48
+ y: number;
49
+ blur: number;
50
+ spread: number;
51
+ opacity: number;
52
+ color: string;
53
+ }
54
+ export interface ThemeTokens {
55
+ v: 1;
56
+ preset: string;
57
+ scheme: "light" | "dark";
58
+ colors: ThemeColors;
59
+ font: {
60
+ body: string;
61
+ heading: string;
62
+ scale: {
63
+ caption: number;
64
+ body: number;
65
+ title: number;
66
+ display: number;
67
+ };
68
+ weights: {
69
+ body: number;
70
+ strong: number;
71
+ title: number;
72
+ };
73
+ letterSpacing: {
74
+ body: number;
75
+ title: number;
76
+ };
77
+ lineHeight: {
78
+ body: number;
79
+ title: number;
80
+ };
81
+ };
82
+ shape: {
83
+ radius: {
84
+ sm: number;
85
+ md: number;
86
+ lg: number;
87
+ xl: number;
88
+ };
89
+ borderWidth: number;
90
+ launcher: LauncherShape;
91
+ };
92
+ spacing: {
93
+ unit: number;
94
+ cardPadding: number;
95
+ gap: number;
96
+ };
97
+ elevation: {
98
+ card: ThemeShadow | null;
99
+ floating: ThemeShadow | null;
100
+ hardOffset: {
101
+ x: number;
102
+ y: number;
103
+ } | null;
104
+ };
105
+ motion: {
106
+ durationMs: number;
107
+ easing: MotionEasing;
108
+ bounce: number;
109
+ };
110
+ icons: {
111
+ style: IconStyle;
112
+ stroke: number;
113
+ launcher: {
114
+ id: string;
115
+ path: string;
116
+ stroke: boolean;
117
+ } | null;
118
+ };
119
+ glass: {
120
+ blur: number;
121
+ surfaceOpacity: number;
122
+ } | null;
123
+ bubbles: DesignBubbles;
124
+ }
125
+ export interface ResolvedDesign {
126
+ config: DesignConfig;
127
+ light: ThemeTokens;
128
+ dark: ThemeTokens;
129
+ }
130
+ export interface DesignPreset {
131
+ id: string;
132
+ name: string;
133
+ tagline: string;
134
+ family: DesignFamily;
135
+ inspiredBy: string[];
136
+ font: ThemeTokens["font"];
137
+ shape: ThemeTokens["shape"];
138
+ spacing: ThemeTokens["spacing"];
139
+ elevation: ThemeTokens["elevation"];
140
+ motion: ThemeTokens["motion"];
141
+ icons: {
142
+ style: IconStyle;
143
+ stroke: number;
144
+ launcher: string;
145
+ };
146
+ glass: ThemeTokens["glass"];
147
+ bubbles: DesignBubbles;
148
+ colors: {
149
+ light: Omit<ThemeColors, "accentSoft">;
150
+ dark: Omit<ThemeColors, "accentSoft">;
151
+ };
152
+ }
153
+ export declare const DESIGN_SCHEMES: ReadonlyArray<DesignScheme>;
154
+ export declare const DESIGN_DENSITIES: ReadonlyArray<DesignDensity>;
155
+ export declare const DESIGN_SHADOWS: ReadonlyArray<DesignShadow>;
156
+ export declare const DESIGN_MOTIONS: ReadonlyArray<DesignMotion>;
157
+ export declare const DESIGN_NEUTRALS: ReadonlyArray<DesignNeutral>;
158
+ export declare const DESIGN_BUBBLES: ReadonlyArray<DesignBubbles>;
159
+ export declare const LAUNCHER_SHAPES: ReadonlyArray<LauncherShape>;
160
+ export declare const ICON_STYLES: ReadonlyArray<IconStyle>;
161
+ export declare function normaliseHex(value: unknown): string | null;
162
+ export declare function mixHex(a: string, b: string, t: number): string;
163
+ export declare function luminanceOf(hex: string): number;
164
+ export declare function contrastRatio(a: string, b: string): number;
165
+ export declare function readableOn(bg: string, candidates?: ReadonlyArray<string>): string;
166
+ export declare function ensureContrast(fg: string, bg: string, min?: number): string;
167
+ export declare const LAUNCHER_ICONS: ReadonlyArray<{
168
+ id: string;
169
+ label: string;
170
+ path: string;
171
+ stroke: boolean;
172
+ }>;
173
+ export declare const DESIGN_FONTS: ReadonlyArray<{
174
+ family: string;
175
+ category: "sans" | "serif" | "mono" | "display";
176
+ scripts: string[];
177
+ }>;
178
+ export declare const DESIGN_PRESETS: ReadonlyArray<DesignPreset>;
179
+ export declare const DEFAULT_DESIGN_PRESET = "graine-classic";
180
+ export declare function designPreset(id: unknown): DesignPreset;
181
+ export declare function normaliseDesignConfig(raw: unknown): DesignConfig;
182
+ export declare function designFromAppearance(appearance: unknown): DesignConfig;
183
+ export declare function legacyAppearanceFor(config: DesignConfig): {
184
+ accent: string;
185
+ radius: number;
186
+ theme: DesignScheme;
187
+ };
188
+ export declare const ON_ACCENT_MIN = 3;
189
+ export declare function resolveTokens(configInput: DesignConfig, scheme: "light" | "dark"): ThemeTokens;
190
+ export declare function resolveDesign(configInput: unknown): ResolvedDesign;
191
+ export declare function pickTokens(design: ResolvedDesign, deviceScheme: "light" | "dark" | null | undefined): ThemeTokens;
192
+ export declare const GLYPH_ASSET_VERSION = 2;
193
+ export declare function glyphParams(raw: {
194
+ id?: unknown;
195
+ color?: unknown;
196
+ size?: unknown;
197
+ stroke?: unknown;
198
+ }): {
199
+ id: string;
200
+ color: string;
201
+ size: number;
202
+ stroke: number;
203
+ };
204
+ export declare function glyphUrl(origin: string, id: string, color: string, sizePx: number, stroke?: number): string;
205
+ export declare function encodeDesignParam(config: DesignConfig): string;
206
+ export declare function decodeDesignParam(param: string | null | undefined): DesignConfig | null;