@vanillaskyai/video 0.8.8 → 0.9.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/CHANGELOG.md +30 -0
- package/PUBLIC-API.md +5 -8
- package/dist/{builtin-server-CUYPIDE7.js → builtin-server-HW6HGXCZ.js} +2 -2
- package/dist/{catalog-types-BIhSpOWK.d.ts → catalog-types-WTbLP6Jh.d.ts} +5 -1
- package/dist/chapter-title-YRLIQY3V.js +7 -0
- package/dist/check-runtime.js +6 -15
- package/dist/{chunk-2USVB3CY.js → chunk-3ZTEFJIR.js} +285 -178
- package/dist/chunk-4YM2M62S.js +13 -0
- package/dist/chunk-77VY4O7A.js +390 -0
- package/dist/{chunk-MMUXVA47.js → chunk-7M56IUUX.js} +9 -8
- package/dist/chunk-7NZOMTAL.js +63 -0
- package/dist/{chunk-2DOLF4QN.js → chunk-AUPC6MDK.js} +12 -14
- package/dist/{chunk-352ZZCET.js → chunk-EGVQODKU.js} +2 -2
- package/dist/chunk-EWWCQFTI.js +18 -0
- package/dist/{chunk-MXZSDGZQ.js → chunk-G7IZBBWF.js} +14 -9
- package/dist/{chunk-KT3M7KDL.js → chunk-HKUI5BTB.js} +3 -3
- package/dist/{chunk-LMG3GHLL.js → chunk-IQMYK5DX.js} +29 -3
- package/dist/chunk-LH4BPALA.js +564 -0
- package/dist/{chunk-OWBT45EN.js → chunk-MQILEEXC.js} +1 -1
- package/dist/chunk-MXEYPK7M.js +552 -0
- package/dist/{chunk-AITKH6QT.js → chunk-OR52UMSF.js} +13 -39
- package/dist/{chunk-5SLENAJW.js → chunk-PAJ44TFW.js} +1 -2
- package/dist/{chunk-L7QGGKJ2.js → chunk-PDQFQIQW.js} +1 -1
- package/dist/{chunk-WJ6FLKNC.js → chunk-R3XAOMKP.js} +14 -0
- package/dist/{chunk-2OKB27TG.js → chunk-RXTN2CW6.js} +2 -2
- package/dist/{chunk-XWWLKRNU.js → chunk-TVIU23OM.js} +38 -49
- package/dist/{chunk-224QNWRA.js → chunk-UDIODEDK.js} +1 -5
- package/dist/{chunk-3VRMKFM7.js → chunk-ZDJNFA52.js} +11 -24
- package/dist/cinema-media-YQHSIFDA.js +11 -0
- package/dist/cli.js +18 -9
- package/dist/comparison-RZMMHZR2.js +27 -0
- package/dist/compose-video-TBFTONUC.js +12 -0
- package/dist/editorial-timeline-OZX7OS6G.js +36 -0
- package/dist/{events-BQ6z264n.d.ts → events-B4YCc4vc.d.ts} +1 -1
- package/dist/focus-cards-TJ72BT7U.js +20 -0
- package/dist/index.d.ts +4 -6
- package/dist/index.js +3 -7
- package/dist/key-figure-GYENXKH3.js +20 -0
- package/dist/{kit-tQnCPMOM.d.ts → kit-DrRpdn0p.d.ts} +2 -2
- package/dist/mobile-message-UZUPIKHD.js +49 -0
- package/dist/{preload-media-LJXWKTWG.js → preload-media-CFTZIMZL.js} +1 -1
- package/dist/quote-EW5HQA6H.js +23 -0
- package/dist/react.d.ts +21 -11
- package/dist/react.js +590 -833
- package/dist/scene-video-backdrop-PCTARAAJ.js +7 -0
- package/dist/server.d.ts +7 -8
- package/dist/server.js +91 -74
- package/dist/{system-prompt-RRXIWDDD.js → system-prompt-SYRT5DWM.js} +1 -1
- package/dist/template-catalog.d.ts +257 -16
- package/dist/template-catalog.js +1 -1
- package/dist/templates.d.ts +5 -5
- package/dist/test.d.ts +3 -3
- package/dist/test.js +18 -18
- package/dist/text-stream-LD364KBI.js +9 -0
- package/dist/{types-DWFO6qca.d.ts → types-CdBmNTGD.d.ts} +1 -1
- package/dist/{types-CEh9eUFu.d.ts → types-DdZw4GRQ.d.ts} +17 -48
- package/dist/validate-4OUD2CLX.js +10 -0
- package/docs/architecture.md +1 -1
- package/docs/concepts.md +1 -1
- package/docs/custom-templates.md +13 -28
- package/docs/customization.md +11 -59
- package/docs/getting-started.md +2 -2
- package/docs/immersive-interface.md +3 -4
- package/docs/maintainers/cinematic-migration.md +34 -0
- package/docs/media-and-audio.md +14 -14
- package/docs/motion-and-effects.md +6 -14
- package/docs/persistence.md +3 -3
- package/docs/production.md +11 -9
- package/docs/provider-integration.md +9 -16
- package/docs/reference/design-system.html +1 -1
- package/docs/reference/protocol.md +14 -4
- package/docs/testing.md +1 -1
- package/package.json +6 -3
- package/registry/items/backgrounds.json +1 -1
- package/registry/items/chapterTitle.json +80 -0
- package/registry/items/cinemaMedia.json +136 -0
- package/registry/items/comparison.json +101 -0
- package/registry/items/editorialTimeline.json +105 -0
- package/registry/items/focusCards.json +90 -0
- package/registry/items/keyFigure.json +97 -0
- package/registry/items/mobileMessage.json +153 -0
- package/registry/items/quote.json +94 -0
- package/registry/items/template-context.json +1 -1
- package/registry/items/theme.json +1 -1
- package/starters/video-chat/README.md +24 -26
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/providers/video.ts +6 -5
- package/starters/video-chat/stock.ts +35 -151
- package/styles/fonts/OFL.txt +93 -0
- package/styles/fonts/manifest.json +66 -0
- package/styles/fonts/roboto-v51-cyrillic-ext.woff2 +0 -0
- package/styles/fonts/roboto-v51-cyrillic.woff2 +0 -0
- package/styles/fonts/roboto-v51-greek-ext.woff2 +0 -0
- package/styles/fonts/roboto-v51-greek.woff2 +0 -0
- package/styles/fonts/roboto-v51-latin-ext.woff2 +0 -0
- package/styles/fonts/roboto-v51-latin.woff2 +0 -0
- package/styles/fonts/roboto-v51-math.woff2 +0 -0
- package/styles/fonts/roboto-v51-symbols.woff2 +0 -0
- package/styles/fonts/roboto-v51-vietnamese.woff2 +0 -0
- package/styles/fonts/roboto.css +91 -0
- package/styles/video-chat.css +30 -22
- package/dist/bg-confetti-HITCGLGD.js +0 -89
- package/dist/bg-emoji-VU4UZYZN.js +0 -150
- package/dist/bg-media-L34PDQXJ.js +0 -106
- package/dist/brand-message-FUJ4STFO.js +0 -256
- package/dist/chart-bar-EGPHJATL.js +0 -310
- package/dist/chart-counter-5FFWQNZO.js +0 -306
- package/dist/chart-progress-ring-JMXQLMLC.js +0 -302
- package/dist/chunk-4UNKZEAT.js +0 -3575
- package/dist/chunk-5CDAM24P.js +0 -31
- package/dist/chunk-AOWSXU2K.js +0 -18
- package/dist/chunk-CJ7F2CE6.js +0 -104
- package/dist/chunk-FRN6WKHA.js +0 -185
- package/dist/chunk-G5JPHBX7.js +0 -336
- package/dist/chunk-L5BTTCZV.js +0 -97
- package/dist/chunk-P22HHFDK.js +0 -43
- package/dist/chunk-PP3MCPN4.js +0 -1998
- package/dist/chunk-QAAO7OAY.js +0 -19
- package/dist/chunk-S7HFLYNN.js +0 -35
- package/dist/chunk-W3VDIBGR.js +0 -217
- package/dist/chunk-XGRA2MUV.js +0 -242
- package/dist/chunk-XTOXFMLM.js +0 -105
- package/dist/chunk-YJJC4N4D.js +0 -761
- package/dist/compose-video-2NFH4DZY.js +0 -13
- package/dist/cta-logo-YR7LYUR5.js +0 -212
- package/dist/cta-media-DQJBKUO2.js +0 -221
- package/dist/incoming-call-54KQ7O5A.js +0 -323
- package/dist/infographic-before-after-ROS52GOW.js +0 -460
- package/dist/infographic-feature-list-E7MGUDAN.js +0 -272
- package/dist/infographic-problem-solution-3S6NGO5N.js +0 -343
- package/dist/infographic-stat-row-SYB7AB7H.js +0 -247
- package/dist/infographic-steps-KCE6JWE2.js +0 -406
- package/dist/prompt-input-MHX4O42G.js +0 -239
- package/dist/reaction-SLEGR3BE.js +0 -122
- package/dist/scene-video-backdrop-XFPX4O3I.js +0 -10
- package/dist/showcase-code-2HLAVKBM.js +0 -357
- package/dist/showcase-phone-Q4HEO4LH.js +0 -278
- package/dist/showcase-terminal-DL45LUFG.js +0 -312
- package/dist/showcase-web-2QZO73FZ.js +0 -396
- package/dist/social-conversation-75SD2637.js +0 -695
- package/dist/social-milestone-GEFUQSBF.js +0 -296
- package/dist/social-notification-FJ5VHVLK.js +0 -247
- package/dist/social-review-stack-KXQCKFRP.js +0 -242
- package/dist/social-testimonial-JZ7CHC7S.js +0 -287
- package/dist/social-tweet-UCDXM25F.js +0 -298
- package/dist/text-stream-54LXABJW.js +0 -10
- package/dist/validate-T7GBU2YF.js +0 -8
- package/registry/items/barChart.json +0 -245
- package/registry/items/beforeAfter.json +0 -180
- package/registry/items/bigNumber.json +0 -232
- package/registry/items/brandMessage.json +0 -164
- package/registry/items/cardList.json +0 -214
- package/registry/items/chatMessenger.json +0 -151
- package/registry/items/chatWhatsapp.json +0 -151
- package/registry/items/codeEditor.json +0 -191
- package/registry/items/confetti.json +0 -107
- package/registry/items/ctaLogo.json +0 -167
- package/registry/items/ctaMedia.json +0 -200
- package/registry/items/emojiBurst.json +0 -101
- package/registry/items/incomingCall.json +0 -177
- package/registry/items/media.json +0 -187
- package/registry/items/milestone.json +0 -212
- package/registry/items/notification.json +0 -189
- package/registry/items/phoneMockup.json +0 -261
- package/registry/items/problemSolution.json +0 -193
- package/registry/items/progressRing.json +0 -206
- package/registry/items/promptInput.json +0 -155
- package/registry/items/reaction.json +0 -176
- package/registry/items/reviewStack.json +0 -231
- package/registry/items/steps.json +0 -215
- package/registry/items/terminal.json +0 -219
- package/registry/items/testimonial.json +0 -171
- package/registry/items/tripleStats.json +0 -235
- package/registry/items/tweet.json +0 -210
- package/registry/items/webMockup.json +0 -280
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
-
"name": "chatMessenger",
|
|
4
|
-
"type": "registry:block",
|
|
5
|
-
"title": "Messenger chat",
|
|
6
|
-
"description": "A staggered Messenger-style conversation with three to five message bubbles.",
|
|
7
|
-
"dependencies": [
|
|
8
|
-
"react"
|
|
9
|
-
],
|
|
10
|
-
"registryDependencies": [
|
|
11
|
-
"@vanillasky/motion",
|
|
12
|
-
"@vanillasky/theme",
|
|
13
|
-
"@vanillasky/typography",
|
|
14
|
-
"@vanillasky/template-context"
|
|
15
|
-
],
|
|
16
|
-
"files": [
|
|
17
|
-
{
|
|
18
|
-
"path": "src/visual-system/scene-templates/social-conversation.tsx",
|
|
19
|
-
"type": "registry:component",
|
|
20
|
-
"target": "vanillasky/scene-templates/social-conversation.tsx",
|
|
21
|
-
"content": "/**\n * chatMessenger + chatWhatsapp — themed conversation scenes.\n *\n * ConversationThread owns bubble chrome, typing, receipts, chips, timing,\n * orientation, and export-safe emoji rendering. This template translates the\n * editable msg1..msg5 fields into the primitive's message collection.\n */\n\nimport * as React from \"react\";\nimport type { SceneTemplateProps } from \"./types\";\nimport { normalizeTextVar } from \"./normalize-var\";\nimport { ConversationThread } from \"../primitives/social/ConversationThread\";\n\nconst DEFAULT_SIDES: Record<number, \"left\" | \"right\"> = {\n 1: \"left\",\n 2: \"right\",\n 3: \"left\",\n 4: \"right\",\n 5: \"right\",\n};\n\nexport function buildConversationMessages(variables: Record<string, unknown>) {\n const messages: Array<{ author: string; text: string; side: \"left\" | \"right\" }> = [];\n for (let index = 1; index <= 5; index += 1) {\n let text = normalizeTextVar(variables[`msg${index}`]).trim();\n if (!text) continue;\n let side = DEFAULT_SIDES[index] ?? (index % 2 === 1 ? \"left\" : \"right\");\n if (text.endsWith(\"|in\")) {\n side = \"left\";\n text = text.slice(0, -3).trim();\n } else if (text.endsWith(\"|out\")) {\n side = \"right\";\n text = text.slice(0, -4).trim();\n }\n messages.push({ author: side === \"left\" ? \"Customer\" : \"You\", text, side });\n }\n return messages;\n}\n\nexport const SocialConversationTemplate: React.FC<SceneTemplateProps> = ({\n variables,\n progress,\n beatIntensity,\n width,\n height,\n sceneDuration,\n safeZone,\n}) => {\n const theme = String(variables.theme || \"messenger\").toLowerCase() === \"whatsapp\"\n ? \"whatsapp\"\n : \"messenger\";\n\n return (\n <div style={{ width, height, position: \"relative\", overflow: \"hidden\", backgroundColor: theme === \"whatsapp\" ? \"#EFE7DC\" : \"#ffffff\" }}>\n {/* [slot: hero] ConversationThread includes the theme-specific canvas. */}\n <ConversationThread\n progress={progress}\n width={width}\n height={height}\n sceneDuration={sceneDuration ?? 8}\n messages={buildConversationMessages(variables)}\n theme={theme}\n dateChip1={String(variables.dateChip1 || \"\")}\n dateChip2={String(variables.dateChip2 || \"\")}\n safeZoneTop={Math.max(safeZone.top, height * 0.08)}\n beatIntensity={beatIntensity}\n />\n </div>\n );\n};\n"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"path": "src/visual-system/primitives/social/ConversationThread.tsx",
|
|
25
|
-
"type": "registry:component",
|
|
26
|
-
"target": "vanillasky/primitives/social/ConversationThread.tsx",
|
|
27
|
-
"content": "/**\n * ConversationThread — multi-bubble chat thread with staggered reveal,\n * typing indicators, and (for WhatsApp) animated read receipts.\n *\n * Lifted from `social-conversation.tsx`. The primitive owns:\n * - the centered chat column (full-width portrait / ~72% landscape)\n * - per-message typing → bubble pop sequence\n * - WhatsApp tick state progression (clock → sent → delivered → read)\n * - optional date chips inserted between specific messages\n * - the WhatsApp compose bar (decorative — drawn 1:1 from the source)\n *\n * It does NOT own a SceneBackground gradient/media or a headline overlay.\n * The primitive fills its own backdrop with the theme background color so\n * the bubbles always have correct contrast; if a caller wants the scene\n * gradient to show through, drop this primitive on top of a SceneBackground.\n *\n * The source template parses messages from `msg1..msg5` variables plus a\n * `|in`/`|out` suffix override. This primitive accepts the already-parsed\n * `messages` array so callers can pre-split however they like.\n *\n * Local helpers (`easeOutBack`, `tickStateAt`, `timingForCount`) are\n * inlined because the source defined them locally; they are not exported.\n *\n * Props:\n * - progress — scene progress 0..1\n * - width / height — frame dimensions\n * - sceneDuration — seconds; drives the 1.5 Hz typing-dot ripple in real time\n * - messages — array of `{ author, text, side }`. `author` is\n * decorative (not rendered in either theme today) but\n * accepted so callers don't lose data when round-tripping.\n * - theme — \"whatsapp\" or \"messenger\" (iMessage). Default \"whatsapp\".\n * - safeZoneTop — top inset to clear the social overlay UI. Default 8% of height.\n * - accent — brand accent (kept for parity; today's chrome is locked\n * to WhatsApp/iMessage native palettes for realism)\n * - beatIntensity — accepted for parity; not currently applied\n */\n\nimport * as React from \"react\";\nimport { interpolate } from \"../../motion\";\nimport { stripPipe } from \"../../typography\";\nimport { renderWithEmoji } from \"../../emoji/emoji-text\";\n\nconst CLAMP = {\n extrapolateLeft: \"clamp\" as const,\n extrapolateRight: \"clamp\" as const,\n};\n\n/* Reference timeline length — every reveal time is a fraction of this. */\nconst REF_DURATION = 16;\n\n/* easeOutBack — same overshoot curve the source uses for pop-ins. */\nfunction easeOutBack(t: number): number {\n const c1 = 1.70158;\n const c3 = c1 + 1;\n return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);\n}\n\n/* WhatsApp palette */\nconst WA = {\n bg: \"#EFE7DC\",\n incomingBubble: \"#FFFFFF\",\n outgoingBubble: \"#D9F4C7\",\n text: \"#0E0E0E\",\n textMuted: \"#6E6E73\",\n accent: \"#0A8F76\",\n chipBg: \"#F1E9D8\",\n chipText: \"#3C342A\",\n navBg: \"#F1E9DA\",\n inputBg: \"#FFFFFF\",\n inputBorder: \"rgba(0,0,0,0.06)\",\n outgoingTime: \"#5C6B4E\",\n};\n\n/* iMessage palette */\nconst IM = {\n bg: \"#ffffff\",\n bubbleOut: \"#007AFF\",\n bubbleIn: \"#E9E9EB\",\n textOut: \"#ffffff\",\n textIn: \"#000000\",\n dotsColor: \"#8E8E93\",\n};\n\ninterface InternalMessage {\n side: \"left\" | \"right\";\n text: string;\n author: string;\n typingStart: number;\n bubbleStart: number;\n time: string;\n}\n\n/* WhatsApp reference timing — mirrors source 5-message cadence. */\nconst WA_TIMING_5: Array<{ typingStart: number; bubbleStart: number; time: string }> = [\n { typingStart: 0.6, bubbleStart: 1.4, time: \"21:54\" },\n { typingStart: 2.2, bubbleStart: 3.0, time: \"21:55\" },\n { typingStart: 3.8, bubbleStart: 4.8, time: \"21:56\" },\n { typingStart: 6.2, bubbleStart: 7.2, time: \"12:35\" },\n { typingStart: 10.0, bubbleStart: 11.0, time: \"11:17\" },\n];\n\nfunction timingForCount(count: number): Array<{ typingStart: number; bubbleStart: number; time: string }> {\n if (count >= 5) return WA_TIMING_5.slice(0, count);\n const slot = 12 / count;\n const out: Array<{ typingStart: number; bubbleStart: number; time: string }> = [];\n for (let i = 0; i < count; i++) {\n const start = 0.6 + i * slot;\n out.push({\n typingStart: start,\n bubbleStart: start + slot * 0.5,\n time: WA_TIMING_5[Math.min(i, 4)].time,\n });\n }\n return out;\n}\n\n/* Read-receipt phases (WhatsApp). */\ntype TickState = \"clock\" | \"sent\" | \"delivered\" | \"read\";\n\nfunction tickStateAt(refTimeSinceSent: number): TickState {\n if (refTimeSinceSent < 0.5) return \"clock\";\n if (refTimeSinceSent < 1.1) return \"sent\";\n if (refTimeSinceSent < 1.8) return \"delivered\";\n return \"read\";\n}\n\nconst ClockIcon: React.FC<{ s: number }> = ({ s }) => (\n <svg width={12 * s} height={12 * s} viewBox=\"0 0 12 12\" style={{ marginLeft: 4 * s }}>\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill=\"none\" stroke=\"#8C8C92\" strokeWidth=\"1.2\" />\n <path d=\"M6 3 v3.5 l2 1\" stroke=\"#8C8C92\" strokeWidth=\"1.2\" fill=\"none\" strokeLinecap=\"round\" />\n </svg>\n);\n\nconst Ticks: React.FC<{ state: TickState; s: number }> = ({ state, s }) => {\n if (state === \"clock\") return <ClockIcon s={s} />;\n const color = state === \"read\" ? \"#53BDEB\" : \"#8C8C92\";\n return (\n <svg width={16 * s} height={11 * s} viewBox=\"0 0 16 11\" style={{ marginLeft: 4 * s }}>\n <path\n d=\"M1 6 l3 3 l6 -7\"\n stroke={color}\n strokeWidth={1.3}\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n opacity={state === \"sent\" ? 0 : 1}\n />\n <path\n d=\"M5 6 l3 3 l7 -8\"\n stroke={color}\n strokeWidth={1.3}\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n\nconst ComposeBar: React.FC<{ s: number }> = ({ s }) => {\n const stroke = \"#1A1A1A\";\n return (\n <div\n style={{\n background: WA.navBg,\n borderTop: \"1px solid rgba(0,0,0,0.05)\",\n padding: `${10 * s}px ${14 * s}px ${12 * s}px`,\n display: \"flex\",\n alignItems: \"center\",\n gap: 14 * s,\n }}\n >\n <svg width={30 * s} height={30 * s} viewBox=\"0 0 30 30\" style={{ flexShrink: 0 }}>\n <path d=\"M15 5 v20 M5 15 h20\" stroke={stroke} strokeWidth=\"1.8\" strokeLinecap=\"round\" />\n </svg>\n <div\n style={{\n flex: 1,\n height: 40 * s,\n background: WA.inputBg,\n borderRadius: 22 * s,\n position: \"relative\",\n border: `0.5px solid ${WA.inputBorder}`,\n }}\n >\n <div\n style={{\n position: \"absolute\",\n right: 12 * s,\n top: \"50%\",\n transform: \"translateY(-50%)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n }}\n >\n <svg width={28 * s} height={28 * s} viewBox=\"0 0 24 24\" fill=\"none\" style={{ display: \"block\" }}>\n <path\n d=\"M3 5 a2 2 0 0 1 2 -2 h10 a2 2 0 0 1 2 2 v6 l-6 6 h-6 a2 2 0 0 1 -2 -2 z M17 11 a4 4 0 0 0 -4 4 v2\"\n stroke=\"#555\"\n strokeWidth=\"1.5\"\n fill=\"none\"\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n </svg>\n </div>\n </div>\n <svg width={28 * s} height={24 * s} viewBox=\"0 0 28 24\" fill=\"none\" style={{ flexShrink: 0 }}>\n <path\n d=\"M3 6 h5 l2 -3 h8 l2 3 h5 a1 1 0 0 1 1 1 v14 a1 1 0 0 1 -1 1 h-22 a1 1 0 0 1 -1 -1 v-14 a1 1 0 0 1 1 -1 z\"\n stroke={stroke}\n strokeWidth=\"1.6\"\n fill=\"none\"\n strokeLinejoin=\"round\"\n />\n <circle cx=\"14\" cy=\"14\" r=\"4.5\" stroke={stroke} strokeWidth=\"1.6\" fill=\"none\" />\n </svg>\n <svg width={20 * s} height={26 * s} viewBox=\"0 0 20 26\" fill=\"none\" style={{ flexShrink: 0 }}>\n <rect x=\"7\" y=\"2\" width=\"6\" height=\"13\" rx=\"3\" stroke={stroke} strokeWidth=\"1.6\" fill=\"none\" />\n <path\n d=\"M3 12 a7 7 0 0 0 14 0 M10 19 v4 M7 23 h6\"\n stroke={stroke}\n strokeWidth=\"1.6\"\n fill=\"none\"\n strokeLinecap=\"round\"\n />\n </svg>\n </div>\n );\n};\n\nconst TypingBubble: React.FC<{\n side: \"left\" | \"right\";\n pop: number;\n realTimeSeconds: number;\n s: number;\n theme: \"whatsapp\" | \"messenger\";\n}> = ({ side, pop, realTimeSeconds, s, theme }) => {\n if (pop <= 0) return null;\n const isOut = side === \"right\";\n const isWA = theme === \"whatsapp\";\n const dotColor = isWA ? (isOut ? \"#6B8A55\" : \"#8C8C92\") : IM.dotsColor;\n const bg = isWA\n ? isOut\n ? WA.outgoingBubble\n : WA.incomingBubble\n : isOut\n ? IM.bubbleOut\n : IM.bubbleIn;\n\n return (\n <div\n style={{\n display: \"flex\",\n justifyContent: isOut ? \"flex-end\" : \"flex-start\",\n padding: `${2 * s}px ${(isWA ? 10 : 14) * s}px`,\n opacity: pop,\n transform: `scale(${0.9 + 0.1 * pop})`,\n transformOrigin: isOut ? \"right bottom\" : \"left bottom\",\n }}\n >\n <div\n style={{\n background: bg,\n borderRadius: (isWA ? 14 : 18) * s,\n ...(isWA\n ? {\n borderTopLeftRadius: isOut ? 14 * s : 2 * s,\n borderTopRightRadius: isOut ? 2 * s : 14 * s,\n }\n : {}),\n padding: `${(isWA ? 10 : 12) * s}px ${(isWA ? 12 : 16) * s}px`,\n display: \"flex\",\n gap: 5 * s,\n alignItems: \"center\",\n ...(isWA\n ? { boxShadow: `0 ${1 * s}px ${1 * s}px rgba(0,0,0,0.06)` }\n : {}),\n }}\n >\n {[0, 1, 2].map((i) => {\n const phase = (realTimeSeconds * 1.6 + i * 0.2) % 1;\n const y = Math.sin(phase * Math.PI * 2) * 2.5 * s;\n const op = 0.45 + 0.55 * Math.max(0, Math.sin(phase * Math.PI));\n const dotSize = isWA ? 6 * s : 7 * s;\n return (\n <div\n key={i}\n style={{\n width: dotSize,\n height: dotSize,\n borderRadius: dotSize / 2,\n background: dotColor,\n transform: `translateY(${-y}px)`,\n opacity: op,\n }}\n />\n );\n })}\n </div>\n </div>\n );\n};\n\nconst DateChip: React.FC<{ label: string; chipProgress: number; s: number }> = ({\n label,\n chipProgress,\n s,\n}) => {\n if (chipProgress <= 0) return null;\n return (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"center\",\n margin: `${12 * s}px 0 ${8 * s}px`,\n opacity: chipProgress,\n transform: `scale(${0.88 + 0.12 * chipProgress})`,\n }}\n >\n <div\n style={{\n background: WA.chipBg,\n color: WA.chipText,\n padding: `${7 * s}px ${16 * s}px`,\n borderRadius: 9 * s,\n fontSize: 17 * s,\n fontWeight: 600,\n boxShadow: `0 ${1 * s}px ${1.5 * s}px rgba(0,0,0,0.05)`,\n }}\n >\n {label}\n </div>\n </div>\n );\n};\n\nconst IncomingBubble: React.FC<{\n children: React.ReactNode;\n time: string;\n pop: number;\n s: number;\n}> = ({ children, time, pop, s }) => (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"flex-start\",\n padding: `${2 * s}px ${10 * s}px`,\n opacity: pop,\n transform: `translateY(${(1 - pop) * 6 * s}px) scale(${0.97 + 0.03 * pop})`,\n transformOrigin: \"left bottom\",\n }}\n >\n <div\n style={{\n maxWidth: \"82%\",\n background: WA.incomingBubble,\n borderRadius: 10 * s,\n borderTopLeftRadius: 2 * s,\n padding: `${12 * s}px ${16 * s}px ${11 * s}px`,\n fontSize: 23 * s,\n lineHeight: `${29 * s}px`,\n color: WA.text,\n boxShadow: `0 ${1 * s}px ${1 * s}px rgba(0,0,0,0.06)`,\n position: \"relative\",\n }}\n >\n <div style={{ paddingRight: 64 * s }}>{children}</div>\n <div\n style={{\n position: \"absolute\",\n right: 14 * s,\n bottom: 8 * s,\n fontSize: 15 * s,\n color: WA.textMuted,\n }}\n >\n {time}\n </div>\n </div>\n </div>\n);\n\nconst OutgoingBubble: React.FC<{\n children: React.ReactNode;\n time: string;\n tickState: TickState;\n pop: number;\n s: number;\n}> = ({ children, time, tickState, pop, s }) => (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"flex-end\",\n padding: `${2 * s}px ${10 * s}px`,\n opacity: pop,\n transform: `translateY(${(1 - pop) * 8 * s}px) scale(${0.96 + 0.04 * pop})`,\n transformOrigin: \"right bottom\",\n }}\n >\n <div\n style={{\n maxWidth: \"82%\",\n background: WA.outgoingBubble,\n borderRadius: 10 * s,\n borderTopRightRadius: 2 * s,\n padding: `${12 * s}px ${16 * s}px ${11 * s}px`,\n fontSize: 23 * s,\n lineHeight: `${29 * s}px`,\n color: WA.text,\n boxShadow: `0 ${1 * s}px ${1 * s}px rgba(0,0,0,0.06)`,\n position: \"relative\",\n }}\n >\n <div style={{ paddingRight: 88 * s }}>{children}</div>\n <div\n style={{\n position: \"absolute\",\n right: 14 * s,\n bottom: 8 * s,\n fontSize: 15 * s,\n color: WA.outgoingTime,\n display: \"flex\",\n alignItems: \"center\",\n }}\n >\n {time}\n <Ticks state={tickState} s={s} />\n </div>\n </div>\n </div>\n);\n\nconst CHIP_TIMING = {\n monday: { start: 5.8, end: 6.2 },\n yesterday: { start: 9.6, end: 10.0 },\n};\n\nexport interface ConversationThreadProps {\n progress: number;\n width: number;\n height: number;\n sceneDuration: number;\n messages: Array<{ author: string; text: string; side: \"left\" | \"right\" }>;\n theme?: \"whatsapp\" | \"messenger\";\n /** Optional date chips inserted before specific message indices.\n * Default behavior matches the source: chip1 before message 4 (index 3),\n * chip2 before message 5 (index 4). Leave blank to suppress. */\n dateChip1?: string;\n dateChip2?: string;\n safeZoneTop?: number;\n accent?: string;\n beatIntensity?: number;\n}\n\nexport const ConversationThread: React.FC<ConversationThreadProps> = ({\n progress,\n width,\n height,\n sceneDuration,\n messages: rawMessages,\n theme = \"whatsapp\",\n dateChip1 = \"\",\n dateChip2 = \"\",\n safeZoneTop,\n}) => {\n const isLandscape = width > height;\n const colWidth = isLandscape ? width * 0.72 : width;\n const colLeft = (width - colWidth) / 2;\n const s = Math.min(width, height) / 534;\n const isWhatsApp = theme === \"whatsapp\";\n\n // Apply stripPipe + attach timing\n const sanitized = rawMessages\n .map((m) => ({ ...m, text: stripPipe(m.text || \"\").trim() }))\n .filter((m) => m.text.length > 0);\n const timing = timingForCount(Math.max(1, sanitized.length));\n const messages: InternalMessage[] = sanitized.map((m, i) => ({\n side: m.side,\n text: m.text,\n author: m.author,\n typingStart: timing[i].typingStart,\n bubbleStart: timing[i].bubbleStart,\n time: timing[i].time,\n }));\n\n const realTimeSeconds = progress * sceneDuration;\n const refT = progress * REF_DURATION;\n const topPad = safeZoneTop != null ? safeZoneTop : height * 0.08;\n\n const chip1Label = stripPipe(dateChip1 || \"\").trim();\n const chip2Label = stripPipe(dateChip2 || \"\").trim();\n\n const popIn = (t: number, start: number, dur = 0.4): number => {\n if (t <= start) return 0;\n if (t >= start + dur) return 1;\n return easeOutBack((t - start) / dur);\n };\n\n const ramp = (t: number, start: number, end: number): number => {\n if (t <= start) return 0;\n if (t >= end) return 1;\n return (t - start) / (end - start);\n };\n\n if (isWhatsApp) {\n return (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n background: WA.bg,\n overflow: \"hidden\",\n fontFamily: '-apple-system, \"SF Pro Text\", system-ui',\n }}\n >\n <div\n style={{\n position: \"absolute\",\n left: colLeft,\n width: colWidth,\n top: 0,\n height,\n display: \"flex\",\n flexDirection: \"column\",\n }}\n >\n <div style={{ flex: 1, position: \"relative\", overflow: \"hidden\" }}>\n <div\n style={{\n position: \"absolute\",\n left: 0,\n right: 0,\n top: 0,\n display: \"flex\",\n flexDirection: \"column\",\n gap: 2 * s,\n padding: `${topPad}px 0 ${10 * s}px`,\n }}\n >\n {messages.map((msg, i) => {\n const typingPop = popIn(refT, msg.typingStart, 0.25);\n const typingFade = interpolate(\n refT,\n [msg.bubbleStart - 0.1, msg.bubbleStart],\n [1, 0],\n CLAMP,\n );\n const showTyping = refT >= msg.typingStart && refT < msg.bubbleStart;\n const bubblePop = popIn(refT, msg.bubbleStart, 0.4);\n const showBubble = refT >= msg.bubbleStart;\n const isOut = msg.side === \"right\";\n\n const refSinceSent = refT - msg.bubbleStart - 0.4;\n const tickState = isOut ? tickStateAt(Math.max(0, refSinceSent)) : \"read\";\n\n const chipBeforeThis: { label: string; progress: number } | null = (() => {\n if (i === 3 && chip1Label) {\n return {\n label: chip1Label,\n progress: ramp(refT, CHIP_TIMING.monday.start, CHIP_TIMING.monday.end),\n };\n }\n if (i === 4 && chip2Label) {\n return {\n label: chip2Label,\n progress: ramp(refT, CHIP_TIMING.yesterday.start, CHIP_TIMING.yesterday.end),\n };\n }\n return null;\n })();\n\n return (\n <React.Fragment key={i}>\n {chipBeforeThis && (\n <DateChip\n label={chipBeforeThis.label}\n chipProgress={chipBeforeThis.progress}\n s={s}\n />\n )}\n\n {showTyping && (\n <div style={{ opacity: typingPop * typingFade }}>\n <TypingBubble\n side={msg.side}\n pop={typingPop}\n realTimeSeconds={realTimeSeconds}\n s={s}\n theme=\"whatsapp\"\n />\n </div>\n )}\n\n {showBubble &&\n (isOut ? (\n <OutgoingBubble pop={bubblePop} time={msg.time} tickState={tickState} s={s}>\n {renderWithEmoji(msg.text, 23 * s)}\n </OutgoingBubble>\n ) : (\n <IncomingBubble pop={bubblePop} time={msg.time} s={s}>\n {renderWithEmoji(msg.text, 23 * s)}\n </IncomingBubble>\n ))}\n </React.Fragment>\n );\n })}\n\n <div style={{ height: 12 * s }} />\n </div>\n </div>\n\n <ComposeBar s={s} />\n </div>\n </div>\n );\n }\n\n // iMessage render\n return (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n background: IM.bg,\n overflow: \"hidden\",\n fontFamily: \"-apple-system, 'SF Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n left: colLeft,\n width: colWidth,\n top: 0,\n height,\n padding: `${topPad}px 0 ${20 * s}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: 4 * s,\n }}\n >\n {messages.map((msg, i) => {\n const typingPop = popIn(refT, msg.typingStart, 0.25);\n const typingFade = interpolate(\n refT,\n [msg.bubbleStart - 0.1, msg.bubbleStart],\n [1, 0],\n CLAMP,\n );\n const showTyping = refT >= msg.typingStart && refT < msg.bubbleStart;\n const bubblePop = popIn(refT, msg.bubbleStart, 0.4);\n const showBubble = refT >= msg.bubbleStart;\n const isOut = msg.side === \"right\";\n\n return (\n <React.Fragment key={i}>\n {showTyping && (\n <div style={{ opacity: typingPop * typingFade }}>\n <TypingBubble\n side={msg.side}\n pop={typingPop}\n realTimeSeconds={realTimeSeconds}\n s={s}\n theme=\"messenger\"\n />\n </div>\n )}\n\n {showBubble && (\n <div\n style={{\n display: \"flex\",\n justifyContent: isOut ? \"flex-end\" : \"flex-start\",\n padding: `${2 * s}px ${14 * s}px`,\n opacity: bubblePop,\n transform: `translateY(${(1 - bubblePop) * 8 * s}px) scale(${0.96 + 0.04 * bubblePop})`,\n transformOrigin: isOut ? \"right bottom\" : \"left bottom\",\n }}\n >\n <div\n style={{\n maxWidth: \"75%\",\n background: isOut ? IM.bubbleOut : IM.bubbleIn,\n color: isOut ? IM.textOut : IM.textIn,\n borderRadius: 22 * s,\n padding: `${12 * s}px ${18 * s}px`,\n fontSize: 23 * s,\n lineHeight: `${29 * s}px`,\n }}\n >\n {renderWithEmoji(msg.text, 23 * s)}\n </div>\n </div>\n )}\n </React.Fragment>\n );\n })}\n </div>\n </div>\n );\n};\n"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"path": "src/visual-system/emoji/emoji-text.tsx",
|
|
31
|
-
"type": "registry:component",
|
|
32
|
-
"target": "vanillasky/emoji/emoji-text.tsx",
|
|
33
|
-
"content": "/**\n * EmojiText / renderWithEmoji — split a string into text runs + emoji runs,\n * rendering each emoji with the viewer's native emoji font while leaving\n * everything else as plain text.\n *\n * Segmentation: grapheme clusters via Intl.Segmenter so ZWJ sequences\n * (👩💻) and skin-tone modifiers (👍🏽) stay single units. Adjacent emoji\n * clusters each render as their own span in sequence.\n */\n\nimport * as React from \"react\";\nimport { Emoji, isNativeEmoji } from \"./index\";\n\n/** Split text into grapheme clusters (ZWJ + skin-tone safe). */\nfunction toGraphemes(text: string): string[] {\n const Seg = (Intl as unknown as { Segmenter?: typeof Intl.Segmenter }).Segmenter;\n if (Seg) {\n const seg = new Seg(undefined, { granularity: \"grapheme\" });\n const out: string[] = [];\n for (const { segment } of seg.segment(text)) out.push(segment);\n return out;\n }\n // Fallback: code-point spread. Won't keep ZWJ/skin-tone as single units, so\n // those land as separate clusters. Modern browsers all have Segmenter.\n return Array.from(text);\n}\n\nexport interface RenderWithEmojiOptions {\n /** vertical-align passed to each <Emoji>. */\n verticalAlign?: React.CSSProperties[\"verticalAlign\"];\n /** Extra style merged onto each emoji span. */\n emojiStyle?: React.CSSProperties;\n}\n\n/**\n * Split `text` into an array of React nodes — plain-text strings interleaved\n * with <Emoji> spans for every emoji cluster. Returns a single-element\n * `[text]` fast-path when there are no emoji (the common case), so\n * non-emoji text pays ~one regex test.\n *\n * @param text the string to render\n * @param fontSizePx surrounding font size in px (emoji box = this size)\n */\nexport function renderWithEmoji(\n text: string,\n fontSizePx: number,\n opts?: RenderWithEmojiOptions,\n): React.ReactNode[] {\n if (!text) return [text];\n // Cheap bail-out: if the whole string has no emoji-range codepoint, return\n // it untouched (no segmentation, no array churn) — the hot path for the vast\n // majority of titles/bodies.\n if (!isNativeEmoji(text)) return [text];\n\n const clusters = toGraphemes(text);\n const nodes: React.ReactNode[] = [];\n let textBuf = \"\";\n let key = 0;\n\n const flushText = () => {\n if (textBuf) {\n nodes.push(textBuf);\n textBuf = \"\";\n }\n };\n\n for (const cluster of clusters) {\n if (isNativeEmoji(cluster)) {\n flushText();\n nodes.push(\n <Emoji\n key={`e${key++}`}\n char={cluster}\n size={fontSizePx}\n verticalAlign={opts?.verticalAlign}\n style={opts?.emojiStyle}\n />,\n );\n } else {\n textBuf += cluster;\n }\n }\n flushText();\n return nodes;\n}\n\n/**\n * Per-code-unit emoji plan for the typewriter archetype, which reveals text\n * one UTF-16 code unit at a time and indexes by `text.length`. We can't just\n * call renderWithEmoji there (it would re-segment and desync the visibleChars\n * counter), so this maps each cluster's START code-unit index to the full emoji\n * and marks the cluster's CONTINUATION indices as covered (render nothing for\n * them — the start index's span already contains the whole cluster).\n *\n * Returns null when the text contains no emoji (the common case),\n * so the typewriter render keeps its plain per-char path.\n */\nexport interface EmojiTypewriterPlan {\n /** code-unit index → full emoji cluster char (cluster start). */\n starts: Map<number, string>;\n /** code-unit indices that are continuations of a cluster (render nothing). */\n covered: Set<number>;\n}\n\nexport function planTypewriterEmoji(text: string): EmojiTypewriterPlan | null {\n if (!text || !isNativeEmoji(text)) return null;\n const starts = new Map<number, string>();\n const covered = new Set<number>();\n let idx = 0;\n let found = false;\n for (const cluster of toGraphemes(text)) {\n const len = cluster.length; // UTF-16 code units\n if (isNativeEmoji(cluster)) {\n found = true;\n // Store the WHOLE cluster char (not just the start unit) so callers can\n // pass it straight to <Emoji> without splitting a grapheme cluster.\n starts.set(idx, cluster);\n for (let k = 1; k < len; k++) covered.add(idx + k);\n }\n idx += len;\n }\n return found ? { starts, covered } : null;\n}\n\nexport interface EmojiTextProps {\n /** The text to render with emoji in the native operating-system font. */\n children: string;\n /** Surrounding font size in px — sizes each emoji to match a glyph. */\n fontSize: number;\n verticalAlign?: React.CSSProperties[\"verticalAlign\"];\n emojiStyle?: React.CSSProperties;\n}\n\n/**\n * Inline wrapper around renderWithEmoji. Renders a React.Fragment of text +\n * emoji spans. Use where a component currently renders a raw `{text}`\n * child and you have the surrounding font size in px.\n */\nexport const EmojiText: React.FC<EmojiTextProps> = ({\n children,\n fontSize,\n verticalAlign,\n emojiStyle,\n}) => {\n const text = typeof children === \"string\" ? children : \"\";\n return <>{renderWithEmoji(text, fontSize, { verticalAlign, emojiStyle })}</>;\n};\n"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"path": "src/visual-system/emoji/index.tsx",
|
|
37
|
-
"type": "registry:component",
|
|
38
|
-
"target": "vanillasky/emoji/index.tsx",
|
|
39
|
-
"content": "import * as React from \"react\";\n\nconst NATIVE_EMOJI_FONT = \"Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif\";\nconst NATIVE_EMOJI = /\\p{Emoji_Presentation}|\\p{Regional_Indicator}|\\u{FE0F}|\\u{20E3}/u;\n\n/** True for emoji-presentation graphemes, not plain symbols such as ★ or ✓. */\nexport function isNativeEmoji(char: string): boolean {\n return NATIVE_EMOJI.test(char);\n}\n\nexport interface EmojiProps {\n /** The emoji character or grapheme cluster, e.g. \"🎉\" or \"👩💻\". */\n char: string;\n /** Font size in px. */\n size: number;\n /** vertical-align for inline flow. Defaults to a glyph-like baseline nudge. */\n verticalAlign?: React.CSSProperties[\"verticalAlign\"];\n /** Extra styles merged onto the native emoji span. */\n style?: React.CSSProperties;\n}\n\n/** Render emoji with the viewer's OS font while preserving text-style symbols. */\nexport const Emoji: React.FC<EmojiProps> = ({ char, size, verticalAlign = \"-0.15em\", style }) => {\n const nativeEmoji = isNativeEmoji(char);\n return (\n <span\n role={nativeEmoji ? \"img\" : undefined}\n aria-label={nativeEmoji ? char : undefined}\n style={{\n ...(nativeEmoji ? { fontFamily: NATIVE_EMOJI_FONT } : {}),\n fontSize: size,\n lineHeight: 1,\n display: \"inline-block\",\n verticalAlign,\n ...style,\n }}\n >\n {char}\n </span>\n );\n};\n"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"path": "src/visual-system/scene-templates/normalize-var.ts",
|
|
43
|
-
"type": "registry:lib",
|
|
44
|
-
"target": "vanillasky/scene-templates/normalize-var.ts",
|
|
45
|
-
"content": "/**\n * normalize-var — Defensive parsing for structured template variables.\n *\n * LLMs may pass variables as colon-separated strings (expected), JSON strings,\n * or raw objects. These helpers normalize any format to the colon-separated\n * string that existing template parsers expect.\n */\n\n/**\n * Normalize a structured variable (e.g. notif1, transaction1) to a colon-separated string.\n *\n * Accepts:\n * - Colon-separated string: \"🔔:App:Hello:1m\" → passed through\n * - JSON string: '{\"emoji\":\"🔔\",\"app\":\"App\"}' → parsed and mapped\n * - Object: {emoji: \"🔔\", app: \"App\"} → mapped to colon-separated\n *\n * @param raw - The variable value (string, object, or unknown)\n * @param keyAliases - Array of alias arrays, one per field position.\n * e.g. [[\"emoji\"], [\"app\", \"title\", \"appName\"], [\"message\", \"body\", \"text\"], [\"time\"]]\n */\nexport function normalizeStructuredVar(\n raw: unknown,\n keyAliases: string[][],\n): string {\n if (typeof raw === \"string\") {\n const trimmed = raw.trim();\n if (trimmed.startsWith(\"{\")) {\n try {\n const obj = JSON.parse(trimmed);\n if (typeof obj === \"object\" && obj !== null) {\n return extractFromObject(obj, keyAliases);\n }\n } catch {\n // Not valid JSON — pass through as regular string\n }\n }\n return raw;\n }\n\n if (typeof raw === \"object\" && raw !== null) {\n return extractFromObject(raw as Record<string, unknown>, keyAliases);\n }\n\n return String(raw || \"\");\n}\n\nfunction extractFromObject(\n obj: Record<string, unknown>,\n keyAliases: string[][],\n): string {\n return keyAliases\n .map((aliases) => {\n for (const key of aliases) {\n if (obj[key] !== undefined && obj[key] !== null && obj[key] !== \"\") {\n return String(obj[key]);\n }\n }\n return \"\";\n })\n .join(\":\");\n}\n\n/**\n * Normalize a simple text variable for chat templates (msg1-msg4).\n *\n * Accepts:\n * - Plain string: \"Hello\" → passed through\n * - String with side: \"Hello|in\" → passed through\n * - JSON string: '{\"text\":\"Hello\",\"side\":\"in\"}' → \"Hello|in\"\n * - Object: {text: \"Hello\", side: \"in\"} → \"Hello|in\"\n */\nexport function normalizeTextVar(raw: unknown): string {\n if (typeof raw === \"string\") {\n const trimmed = raw.trim();\n if (trimmed.startsWith(\"{\")) {\n try {\n const obj = JSON.parse(trimmed);\n if (typeof obj === \"object\" && obj !== null) {\n return extractTextFromObject(obj);\n }\n } catch {\n // Not valid JSON\n }\n }\n return raw;\n }\n\n if (typeof raw === \"object\" && raw !== null) {\n return extractTextFromObject(raw as Record<string, unknown>);\n }\n\n return String(raw || \"\");\n}\n\nfunction extractTextFromObject(obj: Record<string, unknown>): string {\n const text = String(\n obj.text || obj.message || obj.body || obj.content || \"\",\n );\n const side = String(obj.side || obj.direction || \"\").toLowerCase();\n if (\n side === \"in\" ||\n side === \"left\" ||\n side === \"incoming\" ||\n side === \"received\"\n ) {\n return text + \"|in\";\n }\n if (\n side === \"out\" ||\n side === \"right\" ||\n side === \"outgoing\" ||\n side === \"sent\"\n ) {\n return text + \"|out\";\n }\n return text;\n}\n"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"path": "src/visual-system/scene-templates/types.ts",
|
|
49
|
-
"type": "registry:lib",
|
|
50
|
-
"target": "vanillasky/scene-templates/types.ts",
|
|
51
|
-
"content": "/**\n * Scene template types.\n *\n * A template is a reusable React component that defines how a scene looks.\n * It declares the variables an editor or model may provide\n * and receives universal settings as props.\n *\n * Templates are searchable by AI via description, category, jobs, register,\n * and useWhen guidance.\n * The variable schema enables any LLM to fill in template variables via JSON.\n */\n\nimport type { ResolvedTokens } from \"../theme\";\nimport type { SafeZone, TemplateStyle } from \"../template-context\";\n\n/**\n * Props passed to every scene template component.\n *\n * All animation must be driven by `progress` (0→1). No CSS animations,\n * no Framer Motion, no requestAnimationFrame. Use interpolate/spring\n * from animation-utils.ts.\n *\n * Scale factor: use `Math.min(width, height) / 1080` — normalizes to\n * the short edge so visuals are consistent across portrait and landscape.\n */\nexport interface SceneTemplateProps {\n variables: Record<string, unknown>;\n style: TemplateStyle;\n /** 0→1 through the scene's duration */\n progress: number;\n /** Presentation clock. Active templates receive the same complete 0→1 timeline as progress. */\n motionProgress?: number;\n /** 0→1 beat pulse intensity */\n beatIntensity: number;\n /** 1080 (portrait) or 1920 (landscape) */\n width: number;\n /** 1920 (portrait) or 1080 (landscape) */\n height: number;\n /** Video-level default text effect (for templates that opt in via usesGlobalTextEffect) */\n textArchetype?: string;\n /** How text leaves the scene (fade / shrink / pop / blur-scale). Falls back to a sensible default per textArchetype when undefined. */\n /** Video-level default background effect (for templates that opt in via usesGlobalBackgroundEffect) */\n backgroundEffect?: string;\n /** Platform-aware safe zone insets in pixels — use for text placement */\n safeZone: SafeZone;\n /** Scene duration in seconds — use for time-based (not progress-based) animations */\n sceneDuration?: number;\n /**\n * Brand tokens already resolved from `style`. Built-in templates import\n * resolveTokens directly; an ejected `custom_*` scene can't import anything,\n * so without this it has no way to reach the same values and ends up\n * hardcoding white, black and shadows — the body then looks generic next to\n * a frame that IS using the brand.\n */\n tokens?: ResolvedTokens;\n /**\n * True when the preview player is actively advancing progress; false when paused.\n * Templates that play HTML5 <video> elements should pause them when this is false.\n * Undefined (export capture path) is treated as true.\n */\n isPlaying?: boolean;\n}\n"
|
|
52
|
-
}
|
|
53
|
-
],
|
|
54
|
-
"meta": {
|
|
55
|
-
"vanillasky": {
|
|
56
|
-
"layer": "template",
|
|
57
|
-
"tier": "free",
|
|
58
|
-
"register": "mockup-led",
|
|
59
|
-
"jobs": [
|
|
60
|
-
"setup",
|
|
61
|
-
"proof"
|
|
62
|
-
],
|
|
63
|
-
"useWhen": "A grounded back-and-forth naturally explains a use case, objection, support flow, or punchline.",
|
|
64
|
-
"textCanvas": "tight",
|
|
65
|
-
"minDuration": 5,
|
|
66
|
-
"preferredDuration": 8,
|
|
67
|
-
"schema": {
|
|
68
|
-
"type": "object",
|
|
69
|
-
"properties": {
|
|
70
|
-
"msg1": {
|
|
71
|
-
"type": "string",
|
|
72
|
-
"title": "Message 1 (received)",
|
|
73
|
-
"description": "First message — left/received by default. Append |in or |out to override side. Aim for 3-4 messages total; use 5 with date chips only for a real time gap in a 10s+ scene.",
|
|
74
|
-
"default": "Have you tried it yet?"
|
|
75
|
-
},
|
|
76
|
-
"msg2": {
|
|
77
|
-
"type": "string",
|
|
78
|
-
"title": "Message 2 (sent)",
|
|
79
|
-
"description": "Second message — right/sent by default via |out. Replace the suffix with |in only when the grounded exchange requires it.",
|
|
80
|
-
"default": "Yes — it saves me hours|out"
|
|
81
|
-
},
|
|
82
|
-
"msg3": {
|
|
83
|
-
"type": "string",
|
|
84
|
-
"title": "Message 3 (received)",
|
|
85
|
-
"description": "Third message — left/received by default. Leave blank for a two-message exchange.",
|
|
86
|
-
"default": "Okay, send me the link"
|
|
87
|
-
},
|
|
88
|
-
"msg4": {
|
|
89
|
-
"type": "string",
|
|
90
|
-
"title": "Message 4 (sent)",
|
|
91
|
-
"description": "Fourth message — right/sent by default. Optional; most chats end here.",
|
|
92
|
-
"examples": [
|
|
93
|
-
"Sounds great — sharing access now"
|
|
94
|
-
],
|
|
95
|
-
"default": ""
|
|
96
|
-
},
|
|
97
|
-
"dateChip1": {
|
|
98
|
-
"type": "string",
|
|
99
|
-
"title": "Date chip 1",
|
|
100
|
-
"description": "Optional date label before message 4. Use only for a real time gap.",
|
|
101
|
-
"default": ""
|
|
102
|
-
},
|
|
103
|
-
"dateChip2": {
|
|
104
|
-
"type": "string",
|
|
105
|
-
"title": "Date chip 2",
|
|
106
|
-
"description": "Optional date label before message 5. Use only for a real time gap.",
|
|
107
|
-
"default": ""
|
|
108
|
-
},
|
|
109
|
-
"msg5": {
|
|
110
|
-
"type": "string",
|
|
111
|
-
"title": "Message 5 (sent)",
|
|
112
|
-
"description": "Optional fifth message — right/sent by default.",
|
|
113
|
-
"default": ""
|
|
114
|
-
},
|
|
115
|
-
"theme": {
|
|
116
|
-
"type": "string",
|
|
117
|
-
"title": "Conversation theme",
|
|
118
|
-
"enum": [
|
|
119
|
-
"messenger",
|
|
120
|
-
"whatsapp"
|
|
121
|
-
],
|
|
122
|
-
"default": "messenger"
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
"required": [
|
|
126
|
-
"msg1",
|
|
127
|
-
"msg2",
|
|
128
|
-
"msg3"
|
|
129
|
-
],
|
|
130
|
-
"additionalProperties": false
|
|
131
|
-
},
|
|
132
|
-
"label": "Messenger chat",
|
|
133
|
-
"description": "A staggered Messenger-style conversation with three to five message bubbles.",
|
|
134
|
-
"family": "Social & messaging",
|
|
135
|
-
"avoidWhen": "The source is not conversational, fewer than three meaningful messages exist, or pacing must be brief.",
|
|
136
|
-
"usesGlobalTextEffect": false,
|
|
137
|
-
"usesGlobalTransition": false,
|
|
138
|
-
"usesGlobalBackgroundEffect": false,
|
|
139
|
-
"timing": {
|
|
140
|
-
"contentFields": [
|
|
141
|
-
"msg1",
|
|
142
|
-
"msg2",
|
|
143
|
-
"msg3",
|
|
144
|
-
"msg4",
|
|
145
|
-
"msg5"
|
|
146
|
-
],
|
|
147
|
-
"contentUnit": "words"
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
-
"name": "chatWhatsapp",
|
|
4
|
-
"type": "registry:block",
|
|
5
|
-
"title": "WhatsApp chat",
|
|
6
|
-
"description": "A staggered WhatsApp-style conversation with three to five message bubbles.",
|
|
7
|
-
"dependencies": [
|
|
8
|
-
"react"
|
|
9
|
-
],
|
|
10
|
-
"registryDependencies": [
|
|
11
|
-
"@vanillasky/motion",
|
|
12
|
-
"@vanillasky/theme",
|
|
13
|
-
"@vanillasky/typography",
|
|
14
|
-
"@vanillasky/template-context"
|
|
15
|
-
],
|
|
16
|
-
"files": [
|
|
17
|
-
{
|
|
18
|
-
"path": "src/visual-system/scene-templates/social-conversation.tsx",
|
|
19
|
-
"type": "registry:component",
|
|
20
|
-
"target": "vanillasky/scene-templates/social-conversation.tsx",
|
|
21
|
-
"content": "/**\n * chatMessenger + chatWhatsapp — themed conversation scenes.\n *\n * ConversationThread owns bubble chrome, typing, receipts, chips, timing,\n * orientation, and export-safe emoji rendering. This template translates the\n * editable msg1..msg5 fields into the primitive's message collection.\n */\n\nimport * as React from \"react\";\nimport type { SceneTemplateProps } from \"./types\";\nimport { normalizeTextVar } from \"./normalize-var\";\nimport { ConversationThread } from \"../primitives/social/ConversationThread\";\n\nconst DEFAULT_SIDES: Record<number, \"left\" | \"right\"> = {\n 1: \"left\",\n 2: \"right\",\n 3: \"left\",\n 4: \"right\",\n 5: \"right\",\n};\n\nexport function buildConversationMessages(variables: Record<string, unknown>) {\n const messages: Array<{ author: string; text: string; side: \"left\" | \"right\" }> = [];\n for (let index = 1; index <= 5; index += 1) {\n let text = normalizeTextVar(variables[`msg${index}`]).trim();\n if (!text) continue;\n let side = DEFAULT_SIDES[index] ?? (index % 2 === 1 ? \"left\" : \"right\");\n if (text.endsWith(\"|in\")) {\n side = \"left\";\n text = text.slice(0, -3).trim();\n } else if (text.endsWith(\"|out\")) {\n side = \"right\";\n text = text.slice(0, -4).trim();\n }\n messages.push({ author: side === \"left\" ? \"Customer\" : \"You\", text, side });\n }\n return messages;\n}\n\nexport const SocialConversationTemplate: React.FC<SceneTemplateProps> = ({\n variables,\n progress,\n beatIntensity,\n width,\n height,\n sceneDuration,\n safeZone,\n}) => {\n const theme = String(variables.theme || \"messenger\").toLowerCase() === \"whatsapp\"\n ? \"whatsapp\"\n : \"messenger\";\n\n return (\n <div style={{ width, height, position: \"relative\", overflow: \"hidden\", backgroundColor: theme === \"whatsapp\" ? \"#EFE7DC\" : \"#ffffff\" }}>\n {/* [slot: hero] ConversationThread includes the theme-specific canvas. */}\n <ConversationThread\n progress={progress}\n width={width}\n height={height}\n sceneDuration={sceneDuration ?? 8}\n messages={buildConversationMessages(variables)}\n theme={theme}\n dateChip1={String(variables.dateChip1 || \"\")}\n dateChip2={String(variables.dateChip2 || \"\")}\n safeZoneTop={Math.max(safeZone.top, height * 0.08)}\n beatIntensity={beatIntensity}\n />\n </div>\n );\n};\n"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"path": "src/visual-system/primitives/social/ConversationThread.tsx",
|
|
25
|
-
"type": "registry:component",
|
|
26
|
-
"target": "vanillasky/primitives/social/ConversationThread.tsx",
|
|
27
|
-
"content": "/**\n * ConversationThread — multi-bubble chat thread with staggered reveal,\n * typing indicators, and (for WhatsApp) animated read receipts.\n *\n * Lifted from `social-conversation.tsx`. The primitive owns:\n * - the centered chat column (full-width portrait / ~72% landscape)\n * - per-message typing → bubble pop sequence\n * - WhatsApp tick state progression (clock → sent → delivered → read)\n * - optional date chips inserted between specific messages\n * - the WhatsApp compose bar (decorative — drawn 1:1 from the source)\n *\n * It does NOT own a SceneBackground gradient/media or a headline overlay.\n * The primitive fills its own backdrop with the theme background color so\n * the bubbles always have correct contrast; if a caller wants the scene\n * gradient to show through, drop this primitive on top of a SceneBackground.\n *\n * The source template parses messages from `msg1..msg5` variables plus a\n * `|in`/`|out` suffix override. This primitive accepts the already-parsed\n * `messages` array so callers can pre-split however they like.\n *\n * Local helpers (`easeOutBack`, `tickStateAt`, `timingForCount`) are\n * inlined because the source defined them locally; they are not exported.\n *\n * Props:\n * - progress — scene progress 0..1\n * - width / height — frame dimensions\n * - sceneDuration — seconds; drives the 1.5 Hz typing-dot ripple in real time\n * - messages — array of `{ author, text, side }`. `author` is\n * decorative (not rendered in either theme today) but\n * accepted so callers don't lose data when round-tripping.\n * - theme — \"whatsapp\" or \"messenger\" (iMessage). Default \"whatsapp\".\n * - safeZoneTop — top inset to clear the social overlay UI. Default 8% of height.\n * - accent — brand accent (kept for parity; today's chrome is locked\n * to WhatsApp/iMessage native palettes for realism)\n * - beatIntensity — accepted for parity; not currently applied\n */\n\nimport * as React from \"react\";\nimport { interpolate } from \"../../motion\";\nimport { stripPipe } from \"../../typography\";\nimport { renderWithEmoji } from \"../../emoji/emoji-text\";\n\nconst CLAMP = {\n extrapolateLeft: \"clamp\" as const,\n extrapolateRight: \"clamp\" as const,\n};\n\n/* Reference timeline length — every reveal time is a fraction of this. */\nconst REF_DURATION = 16;\n\n/* easeOutBack — same overshoot curve the source uses for pop-ins. */\nfunction easeOutBack(t: number): number {\n const c1 = 1.70158;\n const c3 = c1 + 1;\n return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);\n}\n\n/* WhatsApp palette */\nconst WA = {\n bg: \"#EFE7DC\",\n incomingBubble: \"#FFFFFF\",\n outgoingBubble: \"#D9F4C7\",\n text: \"#0E0E0E\",\n textMuted: \"#6E6E73\",\n accent: \"#0A8F76\",\n chipBg: \"#F1E9D8\",\n chipText: \"#3C342A\",\n navBg: \"#F1E9DA\",\n inputBg: \"#FFFFFF\",\n inputBorder: \"rgba(0,0,0,0.06)\",\n outgoingTime: \"#5C6B4E\",\n};\n\n/* iMessage palette */\nconst IM = {\n bg: \"#ffffff\",\n bubbleOut: \"#007AFF\",\n bubbleIn: \"#E9E9EB\",\n textOut: \"#ffffff\",\n textIn: \"#000000\",\n dotsColor: \"#8E8E93\",\n};\n\ninterface InternalMessage {\n side: \"left\" | \"right\";\n text: string;\n author: string;\n typingStart: number;\n bubbleStart: number;\n time: string;\n}\n\n/* WhatsApp reference timing — mirrors source 5-message cadence. */\nconst WA_TIMING_5: Array<{ typingStart: number; bubbleStart: number; time: string }> = [\n { typingStart: 0.6, bubbleStart: 1.4, time: \"21:54\" },\n { typingStart: 2.2, bubbleStart: 3.0, time: \"21:55\" },\n { typingStart: 3.8, bubbleStart: 4.8, time: \"21:56\" },\n { typingStart: 6.2, bubbleStart: 7.2, time: \"12:35\" },\n { typingStart: 10.0, bubbleStart: 11.0, time: \"11:17\" },\n];\n\nfunction timingForCount(count: number): Array<{ typingStart: number; bubbleStart: number; time: string }> {\n if (count >= 5) return WA_TIMING_5.slice(0, count);\n const slot = 12 / count;\n const out: Array<{ typingStart: number; bubbleStart: number; time: string }> = [];\n for (let i = 0; i < count; i++) {\n const start = 0.6 + i * slot;\n out.push({\n typingStart: start,\n bubbleStart: start + slot * 0.5,\n time: WA_TIMING_5[Math.min(i, 4)].time,\n });\n }\n return out;\n}\n\n/* Read-receipt phases (WhatsApp). */\ntype TickState = \"clock\" | \"sent\" | \"delivered\" | \"read\";\n\nfunction tickStateAt(refTimeSinceSent: number): TickState {\n if (refTimeSinceSent < 0.5) return \"clock\";\n if (refTimeSinceSent < 1.1) return \"sent\";\n if (refTimeSinceSent < 1.8) return \"delivered\";\n return \"read\";\n}\n\nconst ClockIcon: React.FC<{ s: number }> = ({ s }) => (\n <svg width={12 * s} height={12 * s} viewBox=\"0 0 12 12\" style={{ marginLeft: 4 * s }}>\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill=\"none\" stroke=\"#8C8C92\" strokeWidth=\"1.2\" />\n <path d=\"M6 3 v3.5 l2 1\" stroke=\"#8C8C92\" strokeWidth=\"1.2\" fill=\"none\" strokeLinecap=\"round\" />\n </svg>\n);\n\nconst Ticks: React.FC<{ state: TickState; s: number }> = ({ state, s }) => {\n if (state === \"clock\") return <ClockIcon s={s} />;\n const color = state === \"read\" ? \"#53BDEB\" : \"#8C8C92\";\n return (\n <svg width={16 * s} height={11 * s} viewBox=\"0 0 16 11\" style={{ marginLeft: 4 * s }}>\n <path\n d=\"M1 6 l3 3 l6 -7\"\n stroke={color}\n strokeWidth={1.3}\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n opacity={state === \"sent\" ? 0 : 1}\n />\n <path\n d=\"M5 6 l3 3 l7 -8\"\n stroke={color}\n strokeWidth={1.3}\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n\nconst ComposeBar: React.FC<{ s: number }> = ({ s }) => {\n const stroke = \"#1A1A1A\";\n return (\n <div\n style={{\n background: WA.navBg,\n borderTop: \"1px solid rgba(0,0,0,0.05)\",\n padding: `${10 * s}px ${14 * s}px ${12 * s}px`,\n display: \"flex\",\n alignItems: \"center\",\n gap: 14 * s,\n }}\n >\n <svg width={30 * s} height={30 * s} viewBox=\"0 0 30 30\" style={{ flexShrink: 0 }}>\n <path d=\"M15 5 v20 M5 15 h20\" stroke={stroke} strokeWidth=\"1.8\" strokeLinecap=\"round\" />\n </svg>\n <div\n style={{\n flex: 1,\n height: 40 * s,\n background: WA.inputBg,\n borderRadius: 22 * s,\n position: \"relative\",\n border: `0.5px solid ${WA.inputBorder}`,\n }}\n >\n <div\n style={{\n position: \"absolute\",\n right: 12 * s,\n top: \"50%\",\n transform: \"translateY(-50%)\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n }}\n >\n <svg width={28 * s} height={28 * s} viewBox=\"0 0 24 24\" fill=\"none\" style={{ display: \"block\" }}>\n <path\n d=\"M3 5 a2 2 0 0 1 2 -2 h10 a2 2 0 0 1 2 2 v6 l-6 6 h-6 a2 2 0 0 1 -2 -2 z M17 11 a4 4 0 0 0 -4 4 v2\"\n stroke=\"#555\"\n strokeWidth=\"1.5\"\n fill=\"none\"\n strokeLinejoin=\"round\"\n strokeLinecap=\"round\"\n />\n </svg>\n </div>\n </div>\n <svg width={28 * s} height={24 * s} viewBox=\"0 0 28 24\" fill=\"none\" style={{ flexShrink: 0 }}>\n <path\n d=\"M3 6 h5 l2 -3 h8 l2 3 h5 a1 1 0 0 1 1 1 v14 a1 1 0 0 1 -1 1 h-22 a1 1 0 0 1 -1 -1 v-14 a1 1 0 0 1 1 -1 z\"\n stroke={stroke}\n strokeWidth=\"1.6\"\n fill=\"none\"\n strokeLinejoin=\"round\"\n />\n <circle cx=\"14\" cy=\"14\" r=\"4.5\" stroke={stroke} strokeWidth=\"1.6\" fill=\"none\" />\n </svg>\n <svg width={20 * s} height={26 * s} viewBox=\"0 0 20 26\" fill=\"none\" style={{ flexShrink: 0 }}>\n <rect x=\"7\" y=\"2\" width=\"6\" height=\"13\" rx=\"3\" stroke={stroke} strokeWidth=\"1.6\" fill=\"none\" />\n <path\n d=\"M3 12 a7 7 0 0 0 14 0 M10 19 v4 M7 23 h6\"\n stroke={stroke}\n strokeWidth=\"1.6\"\n fill=\"none\"\n strokeLinecap=\"round\"\n />\n </svg>\n </div>\n );\n};\n\nconst TypingBubble: React.FC<{\n side: \"left\" | \"right\";\n pop: number;\n realTimeSeconds: number;\n s: number;\n theme: \"whatsapp\" | \"messenger\";\n}> = ({ side, pop, realTimeSeconds, s, theme }) => {\n if (pop <= 0) return null;\n const isOut = side === \"right\";\n const isWA = theme === \"whatsapp\";\n const dotColor = isWA ? (isOut ? \"#6B8A55\" : \"#8C8C92\") : IM.dotsColor;\n const bg = isWA\n ? isOut\n ? WA.outgoingBubble\n : WA.incomingBubble\n : isOut\n ? IM.bubbleOut\n : IM.bubbleIn;\n\n return (\n <div\n style={{\n display: \"flex\",\n justifyContent: isOut ? \"flex-end\" : \"flex-start\",\n padding: `${2 * s}px ${(isWA ? 10 : 14) * s}px`,\n opacity: pop,\n transform: `scale(${0.9 + 0.1 * pop})`,\n transformOrigin: isOut ? \"right bottom\" : \"left bottom\",\n }}\n >\n <div\n style={{\n background: bg,\n borderRadius: (isWA ? 14 : 18) * s,\n ...(isWA\n ? {\n borderTopLeftRadius: isOut ? 14 * s : 2 * s,\n borderTopRightRadius: isOut ? 2 * s : 14 * s,\n }\n : {}),\n padding: `${(isWA ? 10 : 12) * s}px ${(isWA ? 12 : 16) * s}px`,\n display: \"flex\",\n gap: 5 * s,\n alignItems: \"center\",\n ...(isWA\n ? { boxShadow: `0 ${1 * s}px ${1 * s}px rgba(0,0,0,0.06)` }\n : {}),\n }}\n >\n {[0, 1, 2].map((i) => {\n const phase = (realTimeSeconds * 1.6 + i * 0.2) % 1;\n const y = Math.sin(phase * Math.PI * 2) * 2.5 * s;\n const op = 0.45 + 0.55 * Math.max(0, Math.sin(phase * Math.PI));\n const dotSize = isWA ? 6 * s : 7 * s;\n return (\n <div\n key={i}\n style={{\n width: dotSize,\n height: dotSize,\n borderRadius: dotSize / 2,\n background: dotColor,\n transform: `translateY(${-y}px)`,\n opacity: op,\n }}\n />\n );\n })}\n </div>\n </div>\n );\n};\n\nconst DateChip: React.FC<{ label: string; chipProgress: number; s: number }> = ({\n label,\n chipProgress,\n s,\n}) => {\n if (chipProgress <= 0) return null;\n return (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"center\",\n margin: `${12 * s}px 0 ${8 * s}px`,\n opacity: chipProgress,\n transform: `scale(${0.88 + 0.12 * chipProgress})`,\n }}\n >\n <div\n style={{\n background: WA.chipBg,\n color: WA.chipText,\n padding: `${7 * s}px ${16 * s}px`,\n borderRadius: 9 * s,\n fontSize: 17 * s,\n fontWeight: 600,\n boxShadow: `0 ${1 * s}px ${1.5 * s}px rgba(0,0,0,0.05)`,\n }}\n >\n {label}\n </div>\n </div>\n );\n};\n\nconst IncomingBubble: React.FC<{\n children: React.ReactNode;\n time: string;\n pop: number;\n s: number;\n}> = ({ children, time, pop, s }) => (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"flex-start\",\n padding: `${2 * s}px ${10 * s}px`,\n opacity: pop,\n transform: `translateY(${(1 - pop) * 6 * s}px) scale(${0.97 + 0.03 * pop})`,\n transformOrigin: \"left bottom\",\n }}\n >\n <div\n style={{\n maxWidth: \"82%\",\n background: WA.incomingBubble,\n borderRadius: 10 * s,\n borderTopLeftRadius: 2 * s,\n padding: `${12 * s}px ${16 * s}px ${11 * s}px`,\n fontSize: 23 * s,\n lineHeight: `${29 * s}px`,\n color: WA.text,\n boxShadow: `0 ${1 * s}px ${1 * s}px rgba(0,0,0,0.06)`,\n position: \"relative\",\n }}\n >\n <div style={{ paddingRight: 64 * s }}>{children}</div>\n <div\n style={{\n position: \"absolute\",\n right: 14 * s,\n bottom: 8 * s,\n fontSize: 15 * s,\n color: WA.textMuted,\n }}\n >\n {time}\n </div>\n </div>\n </div>\n);\n\nconst OutgoingBubble: React.FC<{\n children: React.ReactNode;\n time: string;\n tickState: TickState;\n pop: number;\n s: number;\n}> = ({ children, time, tickState, pop, s }) => (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"flex-end\",\n padding: `${2 * s}px ${10 * s}px`,\n opacity: pop,\n transform: `translateY(${(1 - pop) * 8 * s}px) scale(${0.96 + 0.04 * pop})`,\n transformOrigin: \"right bottom\",\n }}\n >\n <div\n style={{\n maxWidth: \"82%\",\n background: WA.outgoingBubble,\n borderRadius: 10 * s,\n borderTopRightRadius: 2 * s,\n padding: `${12 * s}px ${16 * s}px ${11 * s}px`,\n fontSize: 23 * s,\n lineHeight: `${29 * s}px`,\n color: WA.text,\n boxShadow: `0 ${1 * s}px ${1 * s}px rgba(0,0,0,0.06)`,\n position: \"relative\",\n }}\n >\n <div style={{ paddingRight: 88 * s }}>{children}</div>\n <div\n style={{\n position: \"absolute\",\n right: 14 * s,\n bottom: 8 * s,\n fontSize: 15 * s,\n color: WA.outgoingTime,\n display: \"flex\",\n alignItems: \"center\",\n }}\n >\n {time}\n <Ticks state={tickState} s={s} />\n </div>\n </div>\n </div>\n);\n\nconst CHIP_TIMING = {\n monday: { start: 5.8, end: 6.2 },\n yesterday: { start: 9.6, end: 10.0 },\n};\n\nexport interface ConversationThreadProps {\n progress: number;\n width: number;\n height: number;\n sceneDuration: number;\n messages: Array<{ author: string; text: string; side: \"left\" | \"right\" }>;\n theme?: \"whatsapp\" | \"messenger\";\n /** Optional date chips inserted before specific message indices.\n * Default behavior matches the source: chip1 before message 4 (index 3),\n * chip2 before message 5 (index 4). Leave blank to suppress. */\n dateChip1?: string;\n dateChip2?: string;\n safeZoneTop?: number;\n accent?: string;\n beatIntensity?: number;\n}\n\nexport const ConversationThread: React.FC<ConversationThreadProps> = ({\n progress,\n width,\n height,\n sceneDuration,\n messages: rawMessages,\n theme = \"whatsapp\",\n dateChip1 = \"\",\n dateChip2 = \"\",\n safeZoneTop,\n}) => {\n const isLandscape = width > height;\n const colWidth = isLandscape ? width * 0.72 : width;\n const colLeft = (width - colWidth) / 2;\n const s = Math.min(width, height) / 534;\n const isWhatsApp = theme === \"whatsapp\";\n\n // Apply stripPipe + attach timing\n const sanitized = rawMessages\n .map((m) => ({ ...m, text: stripPipe(m.text || \"\").trim() }))\n .filter((m) => m.text.length > 0);\n const timing = timingForCount(Math.max(1, sanitized.length));\n const messages: InternalMessage[] = sanitized.map((m, i) => ({\n side: m.side,\n text: m.text,\n author: m.author,\n typingStart: timing[i].typingStart,\n bubbleStart: timing[i].bubbleStart,\n time: timing[i].time,\n }));\n\n const realTimeSeconds = progress * sceneDuration;\n const refT = progress * REF_DURATION;\n const topPad = safeZoneTop != null ? safeZoneTop : height * 0.08;\n\n const chip1Label = stripPipe(dateChip1 || \"\").trim();\n const chip2Label = stripPipe(dateChip2 || \"\").trim();\n\n const popIn = (t: number, start: number, dur = 0.4): number => {\n if (t <= start) return 0;\n if (t >= start + dur) return 1;\n return easeOutBack((t - start) / dur);\n };\n\n const ramp = (t: number, start: number, end: number): number => {\n if (t <= start) return 0;\n if (t >= end) return 1;\n return (t - start) / (end - start);\n };\n\n if (isWhatsApp) {\n return (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n background: WA.bg,\n overflow: \"hidden\",\n fontFamily: '-apple-system, \"SF Pro Text\", system-ui',\n }}\n >\n <div\n style={{\n position: \"absolute\",\n left: colLeft,\n width: colWidth,\n top: 0,\n height,\n display: \"flex\",\n flexDirection: \"column\",\n }}\n >\n <div style={{ flex: 1, position: \"relative\", overflow: \"hidden\" }}>\n <div\n style={{\n position: \"absolute\",\n left: 0,\n right: 0,\n top: 0,\n display: \"flex\",\n flexDirection: \"column\",\n gap: 2 * s,\n padding: `${topPad}px 0 ${10 * s}px`,\n }}\n >\n {messages.map((msg, i) => {\n const typingPop = popIn(refT, msg.typingStart, 0.25);\n const typingFade = interpolate(\n refT,\n [msg.bubbleStart - 0.1, msg.bubbleStart],\n [1, 0],\n CLAMP,\n );\n const showTyping = refT >= msg.typingStart && refT < msg.bubbleStart;\n const bubblePop = popIn(refT, msg.bubbleStart, 0.4);\n const showBubble = refT >= msg.bubbleStart;\n const isOut = msg.side === \"right\";\n\n const refSinceSent = refT - msg.bubbleStart - 0.4;\n const tickState = isOut ? tickStateAt(Math.max(0, refSinceSent)) : \"read\";\n\n const chipBeforeThis: { label: string; progress: number } | null = (() => {\n if (i === 3 && chip1Label) {\n return {\n label: chip1Label,\n progress: ramp(refT, CHIP_TIMING.monday.start, CHIP_TIMING.monday.end),\n };\n }\n if (i === 4 && chip2Label) {\n return {\n label: chip2Label,\n progress: ramp(refT, CHIP_TIMING.yesterday.start, CHIP_TIMING.yesterday.end),\n };\n }\n return null;\n })();\n\n return (\n <React.Fragment key={i}>\n {chipBeforeThis && (\n <DateChip\n label={chipBeforeThis.label}\n chipProgress={chipBeforeThis.progress}\n s={s}\n />\n )}\n\n {showTyping && (\n <div style={{ opacity: typingPop * typingFade }}>\n <TypingBubble\n side={msg.side}\n pop={typingPop}\n realTimeSeconds={realTimeSeconds}\n s={s}\n theme=\"whatsapp\"\n />\n </div>\n )}\n\n {showBubble &&\n (isOut ? (\n <OutgoingBubble pop={bubblePop} time={msg.time} tickState={tickState} s={s}>\n {renderWithEmoji(msg.text, 23 * s)}\n </OutgoingBubble>\n ) : (\n <IncomingBubble pop={bubblePop} time={msg.time} s={s}>\n {renderWithEmoji(msg.text, 23 * s)}\n </IncomingBubble>\n ))}\n </React.Fragment>\n );\n })}\n\n <div style={{ height: 12 * s }} />\n </div>\n </div>\n\n <ComposeBar s={s} />\n </div>\n </div>\n );\n }\n\n // iMessage render\n return (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n background: IM.bg,\n overflow: \"hidden\",\n fontFamily: \"-apple-system, 'SF Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n left: colLeft,\n width: colWidth,\n top: 0,\n height,\n padding: `${topPad}px 0 ${20 * s}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: 4 * s,\n }}\n >\n {messages.map((msg, i) => {\n const typingPop = popIn(refT, msg.typingStart, 0.25);\n const typingFade = interpolate(\n refT,\n [msg.bubbleStart - 0.1, msg.bubbleStart],\n [1, 0],\n CLAMP,\n );\n const showTyping = refT >= msg.typingStart && refT < msg.bubbleStart;\n const bubblePop = popIn(refT, msg.bubbleStart, 0.4);\n const showBubble = refT >= msg.bubbleStart;\n const isOut = msg.side === \"right\";\n\n return (\n <React.Fragment key={i}>\n {showTyping && (\n <div style={{ opacity: typingPop * typingFade }}>\n <TypingBubble\n side={msg.side}\n pop={typingPop}\n realTimeSeconds={realTimeSeconds}\n s={s}\n theme=\"messenger\"\n />\n </div>\n )}\n\n {showBubble && (\n <div\n style={{\n display: \"flex\",\n justifyContent: isOut ? \"flex-end\" : \"flex-start\",\n padding: `${2 * s}px ${14 * s}px`,\n opacity: bubblePop,\n transform: `translateY(${(1 - bubblePop) * 8 * s}px) scale(${0.96 + 0.04 * bubblePop})`,\n transformOrigin: isOut ? \"right bottom\" : \"left bottom\",\n }}\n >\n <div\n style={{\n maxWidth: \"75%\",\n background: isOut ? IM.bubbleOut : IM.bubbleIn,\n color: isOut ? IM.textOut : IM.textIn,\n borderRadius: 22 * s,\n padding: `${12 * s}px ${18 * s}px`,\n fontSize: 23 * s,\n lineHeight: `${29 * s}px`,\n }}\n >\n {renderWithEmoji(msg.text, 23 * s)}\n </div>\n </div>\n )}\n </React.Fragment>\n );\n })}\n </div>\n </div>\n );\n};\n"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"path": "src/visual-system/emoji/emoji-text.tsx",
|
|
31
|
-
"type": "registry:component",
|
|
32
|
-
"target": "vanillasky/emoji/emoji-text.tsx",
|
|
33
|
-
"content": "/**\n * EmojiText / renderWithEmoji — split a string into text runs + emoji runs,\n * rendering each emoji with the viewer's native emoji font while leaving\n * everything else as plain text.\n *\n * Segmentation: grapheme clusters via Intl.Segmenter so ZWJ sequences\n * (👩💻) and skin-tone modifiers (👍🏽) stay single units. Adjacent emoji\n * clusters each render as their own span in sequence.\n */\n\nimport * as React from \"react\";\nimport { Emoji, isNativeEmoji } from \"./index\";\n\n/** Split text into grapheme clusters (ZWJ + skin-tone safe). */\nfunction toGraphemes(text: string): string[] {\n const Seg = (Intl as unknown as { Segmenter?: typeof Intl.Segmenter }).Segmenter;\n if (Seg) {\n const seg = new Seg(undefined, { granularity: \"grapheme\" });\n const out: string[] = [];\n for (const { segment } of seg.segment(text)) out.push(segment);\n return out;\n }\n // Fallback: code-point spread. Won't keep ZWJ/skin-tone as single units, so\n // those land as separate clusters. Modern browsers all have Segmenter.\n return Array.from(text);\n}\n\nexport interface RenderWithEmojiOptions {\n /** vertical-align passed to each <Emoji>. */\n verticalAlign?: React.CSSProperties[\"verticalAlign\"];\n /** Extra style merged onto each emoji span. */\n emojiStyle?: React.CSSProperties;\n}\n\n/**\n * Split `text` into an array of React nodes — plain-text strings interleaved\n * with <Emoji> spans for every emoji cluster. Returns a single-element\n * `[text]` fast-path when there are no emoji (the common case), so\n * non-emoji text pays ~one regex test.\n *\n * @param text the string to render\n * @param fontSizePx surrounding font size in px (emoji box = this size)\n */\nexport function renderWithEmoji(\n text: string,\n fontSizePx: number,\n opts?: RenderWithEmojiOptions,\n): React.ReactNode[] {\n if (!text) return [text];\n // Cheap bail-out: if the whole string has no emoji-range codepoint, return\n // it untouched (no segmentation, no array churn) — the hot path for the vast\n // majority of titles/bodies.\n if (!isNativeEmoji(text)) return [text];\n\n const clusters = toGraphemes(text);\n const nodes: React.ReactNode[] = [];\n let textBuf = \"\";\n let key = 0;\n\n const flushText = () => {\n if (textBuf) {\n nodes.push(textBuf);\n textBuf = \"\";\n }\n };\n\n for (const cluster of clusters) {\n if (isNativeEmoji(cluster)) {\n flushText();\n nodes.push(\n <Emoji\n key={`e${key++}`}\n char={cluster}\n size={fontSizePx}\n verticalAlign={opts?.verticalAlign}\n style={opts?.emojiStyle}\n />,\n );\n } else {\n textBuf += cluster;\n }\n }\n flushText();\n return nodes;\n}\n\n/**\n * Per-code-unit emoji plan for the typewriter archetype, which reveals text\n * one UTF-16 code unit at a time and indexes by `text.length`. We can't just\n * call renderWithEmoji there (it would re-segment and desync the visibleChars\n * counter), so this maps each cluster's START code-unit index to the full emoji\n * and marks the cluster's CONTINUATION indices as covered (render nothing for\n * them — the start index's span already contains the whole cluster).\n *\n * Returns null when the text contains no emoji (the common case),\n * so the typewriter render keeps its plain per-char path.\n */\nexport interface EmojiTypewriterPlan {\n /** code-unit index → full emoji cluster char (cluster start). */\n starts: Map<number, string>;\n /** code-unit indices that are continuations of a cluster (render nothing). */\n covered: Set<number>;\n}\n\nexport function planTypewriterEmoji(text: string): EmojiTypewriterPlan | null {\n if (!text || !isNativeEmoji(text)) return null;\n const starts = new Map<number, string>();\n const covered = new Set<number>();\n let idx = 0;\n let found = false;\n for (const cluster of toGraphemes(text)) {\n const len = cluster.length; // UTF-16 code units\n if (isNativeEmoji(cluster)) {\n found = true;\n // Store the WHOLE cluster char (not just the start unit) so callers can\n // pass it straight to <Emoji> without splitting a grapheme cluster.\n starts.set(idx, cluster);\n for (let k = 1; k < len; k++) covered.add(idx + k);\n }\n idx += len;\n }\n return found ? { starts, covered } : null;\n}\n\nexport interface EmojiTextProps {\n /** The text to render with emoji in the native operating-system font. */\n children: string;\n /** Surrounding font size in px — sizes each emoji to match a glyph. */\n fontSize: number;\n verticalAlign?: React.CSSProperties[\"verticalAlign\"];\n emojiStyle?: React.CSSProperties;\n}\n\n/**\n * Inline wrapper around renderWithEmoji. Renders a React.Fragment of text +\n * emoji spans. Use where a component currently renders a raw `{text}`\n * child and you have the surrounding font size in px.\n */\nexport const EmojiText: React.FC<EmojiTextProps> = ({\n children,\n fontSize,\n verticalAlign,\n emojiStyle,\n}) => {\n const text = typeof children === \"string\" ? children : \"\";\n return <>{renderWithEmoji(text, fontSize, { verticalAlign, emojiStyle })}</>;\n};\n"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"path": "src/visual-system/emoji/index.tsx",
|
|
37
|
-
"type": "registry:component",
|
|
38
|
-
"target": "vanillasky/emoji/index.tsx",
|
|
39
|
-
"content": "import * as React from \"react\";\n\nconst NATIVE_EMOJI_FONT = \"Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif\";\nconst NATIVE_EMOJI = /\\p{Emoji_Presentation}|\\p{Regional_Indicator}|\\u{FE0F}|\\u{20E3}/u;\n\n/** True for emoji-presentation graphemes, not plain symbols such as ★ or ✓. */\nexport function isNativeEmoji(char: string): boolean {\n return NATIVE_EMOJI.test(char);\n}\n\nexport interface EmojiProps {\n /** The emoji character or grapheme cluster, e.g. \"🎉\" or \"👩💻\". */\n char: string;\n /** Font size in px. */\n size: number;\n /** vertical-align for inline flow. Defaults to a glyph-like baseline nudge. */\n verticalAlign?: React.CSSProperties[\"verticalAlign\"];\n /** Extra styles merged onto the native emoji span. */\n style?: React.CSSProperties;\n}\n\n/** Render emoji with the viewer's OS font while preserving text-style symbols. */\nexport const Emoji: React.FC<EmojiProps> = ({ char, size, verticalAlign = \"-0.15em\", style }) => {\n const nativeEmoji = isNativeEmoji(char);\n return (\n <span\n role={nativeEmoji ? \"img\" : undefined}\n aria-label={nativeEmoji ? char : undefined}\n style={{\n ...(nativeEmoji ? { fontFamily: NATIVE_EMOJI_FONT } : {}),\n fontSize: size,\n lineHeight: 1,\n display: \"inline-block\",\n verticalAlign,\n ...style,\n }}\n >\n {char}\n </span>\n );\n};\n"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"path": "src/visual-system/scene-templates/normalize-var.ts",
|
|
43
|
-
"type": "registry:lib",
|
|
44
|
-
"target": "vanillasky/scene-templates/normalize-var.ts",
|
|
45
|
-
"content": "/**\n * normalize-var — Defensive parsing for structured template variables.\n *\n * LLMs may pass variables as colon-separated strings (expected), JSON strings,\n * or raw objects. These helpers normalize any format to the colon-separated\n * string that existing template parsers expect.\n */\n\n/**\n * Normalize a structured variable (e.g. notif1, transaction1) to a colon-separated string.\n *\n * Accepts:\n * - Colon-separated string: \"🔔:App:Hello:1m\" → passed through\n * - JSON string: '{\"emoji\":\"🔔\",\"app\":\"App\"}' → parsed and mapped\n * - Object: {emoji: \"🔔\", app: \"App\"} → mapped to colon-separated\n *\n * @param raw - The variable value (string, object, or unknown)\n * @param keyAliases - Array of alias arrays, one per field position.\n * e.g. [[\"emoji\"], [\"app\", \"title\", \"appName\"], [\"message\", \"body\", \"text\"], [\"time\"]]\n */\nexport function normalizeStructuredVar(\n raw: unknown,\n keyAliases: string[][],\n): string {\n if (typeof raw === \"string\") {\n const trimmed = raw.trim();\n if (trimmed.startsWith(\"{\")) {\n try {\n const obj = JSON.parse(trimmed);\n if (typeof obj === \"object\" && obj !== null) {\n return extractFromObject(obj, keyAliases);\n }\n } catch {\n // Not valid JSON — pass through as regular string\n }\n }\n return raw;\n }\n\n if (typeof raw === \"object\" && raw !== null) {\n return extractFromObject(raw as Record<string, unknown>, keyAliases);\n }\n\n return String(raw || \"\");\n}\n\nfunction extractFromObject(\n obj: Record<string, unknown>,\n keyAliases: string[][],\n): string {\n return keyAliases\n .map((aliases) => {\n for (const key of aliases) {\n if (obj[key] !== undefined && obj[key] !== null && obj[key] !== \"\") {\n return String(obj[key]);\n }\n }\n return \"\";\n })\n .join(\":\");\n}\n\n/**\n * Normalize a simple text variable for chat templates (msg1-msg4).\n *\n * Accepts:\n * - Plain string: \"Hello\" → passed through\n * - String with side: \"Hello|in\" → passed through\n * - JSON string: '{\"text\":\"Hello\",\"side\":\"in\"}' → \"Hello|in\"\n * - Object: {text: \"Hello\", side: \"in\"} → \"Hello|in\"\n */\nexport function normalizeTextVar(raw: unknown): string {\n if (typeof raw === \"string\") {\n const trimmed = raw.trim();\n if (trimmed.startsWith(\"{\")) {\n try {\n const obj = JSON.parse(trimmed);\n if (typeof obj === \"object\" && obj !== null) {\n return extractTextFromObject(obj);\n }\n } catch {\n // Not valid JSON\n }\n }\n return raw;\n }\n\n if (typeof raw === \"object\" && raw !== null) {\n return extractTextFromObject(raw as Record<string, unknown>);\n }\n\n return String(raw || \"\");\n}\n\nfunction extractTextFromObject(obj: Record<string, unknown>): string {\n const text = String(\n obj.text || obj.message || obj.body || obj.content || \"\",\n );\n const side = String(obj.side || obj.direction || \"\").toLowerCase();\n if (\n side === \"in\" ||\n side === \"left\" ||\n side === \"incoming\" ||\n side === \"received\"\n ) {\n return text + \"|in\";\n }\n if (\n side === \"out\" ||\n side === \"right\" ||\n side === \"outgoing\" ||\n side === \"sent\"\n ) {\n return text + \"|out\";\n }\n return text;\n}\n"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"path": "src/visual-system/scene-templates/types.ts",
|
|
49
|
-
"type": "registry:lib",
|
|
50
|
-
"target": "vanillasky/scene-templates/types.ts",
|
|
51
|
-
"content": "/**\n * Scene template types.\n *\n * A template is a reusable React component that defines how a scene looks.\n * It declares the variables an editor or model may provide\n * and receives universal settings as props.\n *\n * Templates are searchable by AI via description, category, jobs, register,\n * and useWhen guidance.\n * The variable schema enables any LLM to fill in template variables via JSON.\n */\n\nimport type { ResolvedTokens } from \"../theme\";\nimport type { SafeZone, TemplateStyle } from \"../template-context\";\n\n/**\n * Props passed to every scene template component.\n *\n * All animation must be driven by `progress` (0→1). No CSS animations,\n * no Framer Motion, no requestAnimationFrame. Use interpolate/spring\n * from animation-utils.ts.\n *\n * Scale factor: use `Math.min(width, height) / 1080` — normalizes to\n * the short edge so visuals are consistent across portrait and landscape.\n */\nexport interface SceneTemplateProps {\n variables: Record<string, unknown>;\n style: TemplateStyle;\n /** 0→1 through the scene's duration */\n progress: number;\n /** Presentation clock. Active templates receive the same complete 0→1 timeline as progress. */\n motionProgress?: number;\n /** 0→1 beat pulse intensity */\n beatIntensity: number;\n /** 1080 (portrait) or 1920 (landscape) */\n width: number;\n /** 1920 (portrait) or 1080 (landscape) */\n height: number;\n /** Video-level default text effect (for templates that opt in via usesGlobalTextEffect) */\n textArchetype?: string;\n /** How text leaves the scene (fade / shrink / pop / blur-scale). Falls back to a sensible default per textArchetype when undefined. */\n /** Video-level default background effect (for templates that opt in via usesGlobalBackgroundEffect) */\n backgroundEffect?: string;\n /** Platform-aware safe zone insets in pixels — use for text placement */\n safeZone: SafeZone;\n /** Scene duration in seconds — use for time-based (not progress-based) animations */\n sceneDuration?: number;\n /**\n * Brand tokens already resolved from `style`. Built-in templates import\n * resolveTokens directly; an ejected `custom_*` scene can't import anything,\n * so without this it has no way to reach the same values and ends up\n * hardcoding white, black and shadows — the body then looks generic next to\n * a frame that IS using the brand.\n */\n tokens?: ResolvedTokens;\n /**\n * True when the preview player is actively advancing progress; false when paused.\n * Templates that play HTML5 <video> elements should pause them when this is false.\n * Undefined (export capture path) is treated as true.\n */\n isPlaying?: boolean;\n}\n"
|
|
52
|
-
}
|
|
53
|
-
],
|
|
54
|
-
"meta": {
|
|
55
|
-
"vanillasky": {
|
|
56
|
-
"layer": "template",
|
|
57
|
-
"tier": "free",
|
|
58
|
-
"register": "mockup-led",
|
|
59
|
-
"jobs": [
|
|
60
|
-
"setup",
|
|
61
|
-
"proof"
|
|
62
|
-
],
|
|
63
|
-
"useWhen": "A grounded customer-service or personal back-and-forth is naturally associated with WhatsApp.",
|
|
64
|
-
"textCanvas": "tight",
|
|
65
|
-
"minDuration": 5,
|
|
66
|
-
"preferredDuration": 8,
|
|
67
|
-
"schema": {
|
|
68
|
-
"type": "object",
|
|
69
|
-
"properties": {
|
|
70
|
-
"msg1": {
|
|
71
|
-
"type": "string",
|
|
72
|
-
"title": "Message 1 (received)",
|
|
73
|
-
"description": "First message — left/received by default. Append |in or |out to override side. Aim for 3-4 messages total; use 5 with date chips only for a real time gap in a 10s+ scene.",
|
|
74
|
-
"default": "Have you tried it yet?"
|
|
75
|
-
},
|
|
76
|
-
"msg2": {
|
|
77
|
-
"type": "string",
|
|
78
|
-
"title": "Message 2 (sent)",
|
|
79
|
-
"description": "Second message — right/sent by default via |out. Replace the suffix with |in only when the grounded exchange requires it.",
|
|
80
|
-
"default": "Yes — it saves me hours|out"
|
|
81
|
-
},
|
|
82
|
-
"msg3": {
|
|
83
|
-
"type": "string",
|
|
84
|
-
"title": "Message 3 (received)",
|
|
85
|
-
"description": "Third message — left/received by default. Leave blank for a two-message exchange.",
|
|
86
|
-
"default": "Okay, send me the link"
|
|
87
|
-
},
|
|
88
|
-
"msg4": {
|
|
89
|
-
"type": "string",
|
|
90
|
-
"title": "Message 4 (sent)",
|
|
91
|
-
"description": "Fourth message — right/sent by default. Optional; most chats end here.",
|
|
92
|
-
"examples": [
|
|
93
|
-
"Sounds great — sharing access now"
|
|
94
|
-
],
|
|
95
|
-
"default": ""
|
|
96
|
-
},
|
|
97
|
-
"dateChip1": {
|
|
98
|
-
"type": "string",
|
|
99
|
-
"title": "Date chip 1",
|
|
100
|
-
"description": "Optional date label before message 4. Use only for a real time gap.",
|
|
101
|
-
"default": ""
|
|
102
|
-
},
|
|
103
|
-
"dateChip2": {
|
|
104
|
-
"type": "string",
|
|
105
|
-
"title": "Date chip 2",
|
|
106
|
-
"description": "Optional date label before message 5. Use only for a real time gap.",
|
|
107
|
-
"default": ""
|
|
108
|
-
},
|
|
109
|
-
"msg5": {
|
|
110
|
-
"type": "string",
|
|
111
|
-
"title": "Message 5 (sent)",
|
|
112
|
-
"description": "Optional fifth message — right/sent by default.",
|
|
113
|
-
"default": ""
|
|
114
|
-
},
|
|
115
|
-
"theme": {
|
|
116
|
-
"type": "string",
|
|
117
|
-
"title": "Conversation theme",
|
|
118
|
-
"enum": [
|
|
119
|
-
"messenger",
|
|
120
|
-
"whatsapp"
|
|
121
|
-
],
|
|
122
|
-
"default": "whatsapp"
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
"required": [
|
|
126
|
-
"msg1",
|
|
127
|
-
"msg2",
|
|
128
|
-
"msg3"
|
|
129
|
-
],
|
|
130
|
-
"additionalProperties": false
|
|
131
|
-
},
|
|
132
|
-
"label": "WhatsApp chat",
|
|
133
|
-
"description": "A staggered WhatsApp-style conversation with three to five message bubbles.",
|
|
134
|
-
"family": "Social & messaging",
|
|
135
|
-
"avoidWhen": "The source is not conversational, fewer than three meaningful messages exist, or pacing must be brief.",
|
|
136
|
-
"usesGlobalTextEffect": false,
|
|
137
|
-
"usesGlobalTransition": false,
|
|
138
|
-
"usesGlobalBackgroundEffect": false,
|
|
139
|
-
"timing": {
|
|
140
|
-
"contentFields": [
|
|
141
|
-
"msg1",
|
|
142
|
-
"msg2",
|
|
143
|
-
"msg3",
|
|
144
|
-
"msg4",
|
|
145
|
-
"msg5"
|
|
146
|
-
],
|
|
147
|
-
"contentUnit": "words"
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|