capture-flow 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -2
  3. package/bin/capture-flow.mjs +12 -0
  4. package/package.json +55 -4
  5. package/src/build.ts +186 -0
  6. package/src/capture.ts +77 -0
  7. package/src/cli.ts +139 -0
  8. package/src/index.ts +11 -0
  9. package/src/manifest.ts +98 -0
  10. package/src/render.ts +75 -0
  11. package/src/verify.ts +90 -0
  12. package/vendor/wajha/LICENSE +21 -0
  13. package/vendor/wajha/NOTICE +21 -0
  14. package/vendor/wajha/engine/background.ts +55 -0
  15. package/vendor/wajha/engine/bidi.ts +589 -0
  16. package/vendor/wajha/engine/cache.ts +96 -0
  17. package/vendor/wajha/engine/color.ts +125 -0
  18. package/vendor/wajha/engine/devices/android.ts +53 -0
  19. package/vendor/wajha/engine/devices/browser.ts +170 -0
  20. package/vendor/wajha/engine/devices/finish.ts +154 -0
  21. package/vendor/wajha/engine/devices/geometry.ts +326 -0
  22. package/vendor/wajha/engine/devices/index.ts +143 -0
  23. package/vendor/wajha/engine/devices/ipad.ts +21 -0
  24. package/vendor/wajha/engine/devices/iphone.ts +55 -0
  25. package/vendor/wajha/engine/devices/macbook.ts +187 -0
  26. package/vendor/wajha/engine/devices/mock/apps/calendar.ts +145 -0
  27. package/vendor/wajha/engine/devices/mock/apps/chat.ts +213 -0
  28. package/vendor/wajha/engine/devices/mock/apps/finance.ts +164 -0
  29. package/vendor/wajha/engine/devices/mock/apps/fitness.ts +145 -0
  30. package/vendor/wajha/engine/devices/mock/apps/index.ts +14 -0
  31. package/vendor/wajha/engine/devices/mock/apps/meditation.ts +167 -0
  32. package/vendor/wajha/engine/devices/mock/apps/music.ts +152 -0
  33. package/vendor/wajha/engine/devices/mock/apps/shop.ts +116 -0
  34. package/vendor/wajha/engine/devices/mock/apps/social.ts +152 -0
  35. package/vendor/wajha/engine/devices/mock/apps/tasks.ts +131 -0
  36. package/vendor/wajha/engine/devices/mock/apps/travel.ts +121 -0
  37. package/vendor/wajha/engine/devices/mock/art.ts +557 -0
  38. package/vendor/wajha/engine/devices/mock/chrome.ts +337 -0
  39. package/vendor/wajha/engine/devices/mock/icons.ts +614 -0
  40. package/vendor/wajha/engine/devices/mock/kit.ts +213 -0
  41. package/vendor/wajha/engine/devices/mock/rng.ts +40 -0
  42. package/vendor/wajha/engine/devices/mock/theme.ts +133 -0
  43. package/vendor/wajha/engine/devices/mock/widgets.ts +231 -0
  44. package/vendor/wajha/engine/devices/paths.ts +113 -0
  45. package/vendor/wajha/engine/devices/screen.ts +75 -0
  46. package/vendor/wajha/engine/devices/shadow.ts +124 -0
  47. package/vendor/wajha/engine/devices/slab.ts +200 -0
  48. package/vendor/wajha/engine/devices/vision.ts +43 -0
  49. package/vendor/wajha/engine/devices/watch.ts +14 -0
  50. package/vendor/wajha/engine/geometry.ts +152 -0
  51. package/vendor/wajha/engine/images.ts +96 -0
  52. package/vendor/wajha/engine/index.ts +20 -0
  53. package/vendor/wajha/engine/linebreak.ts +226 -0
  54. package/vendor/wajha/engine/markup.ts +84 -0
  55. package/vendor/wajha/engine/mock.ts +158 -0
  56. package/vendor/wajha/engine/paint.ts +474 -0
  57. package/vendor/wajha/engine/patterns.ts +263 -0
  58. package/vendor/wajha/engine/render.ts +364 -0
  59. package/vendor/wajha/engine/shapes.ts +260 -0
  60. package/vendor/wajha/engine/testing/canvasNode.ts +121 -0
  61. package/vendor/wajha/engine/testing/fakeDevices.ts +85 -0
  62. package/vendor/wajha/engine/text.ts +694 -0
  63. package/vendor/wajha/engine/types.ts +190 -0
  64. package/vendor/wajha/export/paths.ts +197 -0
  65. package/vendor/wajha/export/png.ts +223 -0
  66. package/vendor/wajha/export/types.ts +60 -0
  67. package/vendor/wajha/model/adapt.ts +889 -0
  68. package/vendor/wajha/model/catalog.ts +369 -0
  69. package/vendor/wajha/model/clone.ts +58 -0
  70. package/vendor/wajha/model/colorMath.ts +128 -0
  71. package/vendor/wajha/model/defaults.ts +386 -0
  72. package/vendor/wajha/model/geometry.ts +95 -0
  73. package/vendor/wajha/model/ids.ts +13 -0
  74. package/vendor/wajha/model/instantiate.ts +280 -0
  75. package/vendor/wajha/model/mirror.ts +43 -0
  76. package/vendor/wajha/model/resolve.ts +87 -0
  77. package/vendor/wajha/model/rules.ts +249 -0
  78. package/vendor/wajha/model/stub.ts +4 -0
  79. package/vendor/wajha/model/types.ts +456 -0
  80. package/vendor/wajha/model/typography.ts +28 -0
  81. package/vendor/wajha/model/validate.ts +917 -0
  82. package/vendor/wajha/templates/builtin/aurora.ts +163 -0
  83. package/vendor/wajha/templates/builtin/bubblegum.ts +132 -0
  84. package/vendor/wajha/templates/builtin/citrusPop.ts +124 -0
  85. package/vendor/wajha/templates/builtin/clayGarden.ts +114 -0
  86. package/vendor/wajha/templates/builtin/desertDune.ts +97 -0
  87. package/vendor/wajha/templates/builtin/glassPrism.ts +139 -0
  88. package/vendor/wajha/templates/builtin/launchpad.ts +140 -0
  89. package/vendor/wajha/templates/builtin/loudAndClear.ts +109 -0
  90. package/vendor/wajha/templates/builtin/midnightNeon.ts +108 -0
  91. package/vendor/wajha/templates/builtin/monoSwiss.ts +149 -0
  92. package/vendor/wajha/templates/builtin/noirGold.ts +149 -0
  93. package/vendor/wajha/templates/builtin/noor.ts +107 -0
  94. package/vendor/wajha/templates/builtin/obsidian.ts +137 -0
  95. package/vendor/wajha/templates/builtin/panoramaPeaks.ts +145 -0
  96. package/vendor/wajha/templates/builtin/panoramaRibbon.ts +138 -0
  97. package/vendor/wajha/templates/builtin/panoramaWaves.ts +138 -0
  98. package/vendor/wajha/templates/builtin/paperAndInk.ts +98 -0
  99. package/vendor/wajha/templates/builtin/sage.ts +94 -0
  100. package/vendor/wajha/templates/builtin/splitTone.ts +129 -0
  101. package/vendor/wajha/templates/builtin/sunsetBoulevard.ts +127 -0
  102. package/vendor/wajha/templates/index.ts +55 -0
  103. package/vendor/wajha/templates/kit.ts +761 -0
  104. package/vendor/wajha/ui/sortShots.ts +239 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Panorama Waves: one ocean across the whole listing. A deep-blue to
3
+ * lagoon gradient with rolling wave lines runs from the first screenshot to
4
+ * the last; swell rings and a low sun sit on the seams so they continue
5
+ * from one slide into the next. Story: a surf and tide forecast app.
6
+ *
7
+ * Like every panorama template it designs each canvas shape: adapting a
8
+ * layout cannot keep pieces on the seams and keep them clear of the copy.
9
+ */
10
+ import type { SlideElement } from '../../model/types';
11
+ import { ALL_CLASSES, F, METAL, background, canvas, circle, copyAt, device, glow, linear, onStrip, pattern, r5, ring, slide, template, theme, typeset, type Cls, type DeviceOptions } from '../kit';
12
+
13
+ const SUN = '#ffd86b';
14
+
15
+ const t = typeset({
16
+ heading: F.sora,
17
+ body: F.manrope,
18
+ ink: '#ffffff',
19
+ soft: '#ffffff',
20
+ accent: SUN,
21
+ head: { weight: 700, size: 0.088, lineHeight: 1.12, letterSpacing: -0.03 },
22
+ sub: { weight: 500, size: 0.04 },
23
+ });
24
+
25
+ const SHADOW = { color: '#020b2480', blur: 0.07, x: 0, y: 0.03 };
26
+ const phone = (o: DeviceOptions) => device({ color: METAL.white, shadow: SHADOW, ...o });
27
+ const pad = (o: DeviceOptions) => device({ cls: 'tablet', frame: 'ipad', color: METAL.white, shadow: SHADOW, ...o });
28
+ const laptop = (o: DeviceOptions) => device({ cls: 'wide', frame: 'macbook', color: METAL.silver, shadow: SHADOW, ...o });
29
+
30
+ /**
31
+ * The sea per canvas shape: decoration scale, and the heights (fractions of
32
+ * the canvas) of the swell rings, the sun and the two rows of waves. Smaller
33
+ * canvases get a smaller sea, so the pieces on the seams stay clear of the copy.
34
+ */
35
+ const SEA: Record<Cls, { k: number; rings: number; sun: number; swell: number; crest: number }> = {
36
+ tall: { k: 1, rings: 0.66, sun: 0.6, swell: 1.05, crest: 1.11 },
37
+ tablet: { k: 0.72, rings: 0.68, sun: 0.66, swell: 1.04, crest: 1.09 },
38
+ wide: { k: 0.5, rings: 0.7, sun: 0.66, swell: 1.03, crest: 1.08 },
39
+ };
40
+
41
+ /** Everything that flows across slides, as seen from slide `i`. */
42
+ function sea(cls: Cls, i: number): SlideElement[] {
43
+ const { wu } = canvas(cls);
44
+ const { k, rings, sun, swell, crest } = SEA[cls];
45
+ const size = (v: number) => r5(v * k);
46
+ /** Half the width of a decoration `v` u wide, in slide widths. */
47
+ const reach = (v: number) => (v * k) / 2 / wu;
48
+ // Rolling swells along the bottom: one row centred on the seams, one between them.
49
+ const swells = [0, 1, 2, 3, 4, 5].flatMap((X) => onStrip(i, X, reach(1.2), (x) => [circle({ name: 'Swell', x, y: swell, size: size(1.2), color: '#1b7fae', opacity: 0.5 })]));
50
+ const crests = [0.5, 1.5, 2.5, 3.5, 4.5].flatMap((X) => onStrip(i, X, reach(1.2), (x) => [circle({ name: 'Crest', x, y: crest, size: size(1.2), color: '#43bfd6', opacity: 0.4 })]));
51
+ const swellRings = (X: number, y: number) =>
52
+ onStrip(i, X, reach(1.48), (x) => [
53
+ ring({ name: 'Swell ring', x, y, size: size(1.44), stroke: 0.006, color: '#ffffff', opacity: 0.34 }),
54
+ ring({ name: 'Swell ring', x, y, size: size(1.12), stroke: 0.004, color: '#ffffff', opacity: 0.24 }),
55
+ ]);
56
+ return [
57
+ ...swellRings(1.0, rings),
58
+ ...swellRings(4.0, r5(rings + 0.02)),
59
+ // The sun going down on the seam between slides 3 | 4.
60
+ ...onStrip(i, 3.0, reach(0.9), (x) => [glow({ name: 'Sun glow', x, y: sun, size: size(0.82), color: '#ffb36b', opacity: 0.55 }), circle({ name: 'Sun', x, y: sun, size: size(0.5), fill: linear(180, '#fff1b0', SUN, '#ff9f5a') })]),
61
+ ...swells,
62
+ ...crests,
63
+ ];
64
+ }
65
+
66
+ function shot(i: number, o: DeviceOptions = {}) {
67
+ return (cls: Cls): SlideElement[] => {
68
+ if (cls === 'wide') {
69
+ // Copy on top and the laptop below, centred: the seams at the slide edges stay free for the sea.
70
+ return [...sea(cls, i), ...copyAt(t, { cls, seam: 0.2, width: 1.0, subWidth: 0.9, head: { size: 0.066 }, sub: { size: 0.03 } }), laptop({ width: 0.96, top: 0.35 })];
71
+ }
72
+ if (cls === 'tablet') {
73
+ return [...sea(cls, i), ...copyAt(t, { cls, seam: 0.165, width: 0.8, subWidth: 0.64, head: { size: 0.068 }, sub: { size: 0.032 } }), pad({ width: 0.66, top: 0.3, rotation: (o.rotation ?? 0) / 2 })];
74
+ }
75
+ return [...sea(cls, i), ...copyAt(t, { seam: 0.15 }), phone({ width: 0.7, top: 0.268, ...o })];
76
+ };
77
+ }
78
+
79
+ const o = { classes: ALL_CLASSES };
80
+
81
+ export const panoramaWaves = template({
82
+ id: 'panorama-waves',
83
+ name: 'Panorama Waves',
84
+ description: 'One ocean across the whole listing: a deep-blue to lagoon gradient with rolling wave lines, swell rings and a low sun that continue from one screenshot into the next.',
85
+ category: 'panorama',
86
+ tags: ['panorama', 'ocean', 'blue', 'waves', 'continuous'],
87
+ apps: ['travel', 'calendar', 'social', 'fitness', 'chat', 'music', 'shop'],
88
+ theme: theme({
89
+ background: background(linear(90, '#061a44', '#0a3a7d', '#0b5a93', '#0a6585'), pattern('waves', '#ffffff', 0.08, 0.07)),
90
+ heading: F.sora,
91
+ body: F.manrope,
92
+ palette: ['#1aa3c7', SUN, '#ff8a5b', '#7ce0d3', '#ffffff', '#061a44'],
93
+ mockDark: false,
94
+ panorama: true,
95
+ }),
96
+ slides: [
97
+ slide(
98
+ {
99
+ en: { headline: 'Catch the *perfect* wave', subtitle: 'Hour-by-hour swell, wind and tide forecasts.' },
100
+ ar: { headline: 'اصطد *الموجة* المثالية', subtitle: 'توقعات الأمواج والرياح والمدّ والجزر ساعة بساعة.' },
101
+ },
102
+ shot(0),
103
+ o,
104
+ ),
105
+ slide(
106
+ {
107
+ en: { headline: 'Know *before* you go', subtitle: 'Live cams and reports from 8,000 surf spots.' },
108
+ ar: { headline: 'اعرف *قبل* أن تنطلق', subtitle: 'كاميرات مباشرة وتقارير من 8000 موقع لركوب الأمواج.' },
109
+ },
110
+ shot(1, { rotation: -4 }),
111
+ o,
112
+ ),
113
+ slide(
114
+ {
115
+ en: { headline: 'Your spots, *your* way', subtitle: 'Favorites, alerts and conditions at a glance.' },
116
+ ar: { headline: 'مواقعك *المفضّلة* دائمًا', subtitle: 'المفضّلة والتنبيهات وأحوال البحر بنظرة واحدة.' },
117
+ },
118
+ shot(2),
119
+ o,
120
+ ),
121
+ slide(
122
+ {
123
+ en: { headline: 'Chase the *sun*', subtitle: 'Sunrise sessions planned around the best light.' },
124
+ ar: { headline: 'طارد *الشمس*', subtitle: 'جلسات الفجر مخطّطة حول أجمل ضوء في اليوم.' },
125
+ },
126
+ shot(3, { rotation: 4 }),
127
+ o,
128
+ ),
129
+ slide(
130
+ {
131
+ en: { headline: 'Ride *together*', subtitle: 'Meet your crew at the next big swell.' },
132
+ ar: { headline: 'اركبوا الموج *معًا*', subtitle: 'التقِ بأصدقائك عند الموجة الكبيرة القادمة.' },
133
+ },
134
+ shot(4),
135
+ o,
136
+ ),
137
+ ],
138
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Paper & Ink: a well-made notebook. Warm cotton paper with grain, Fraunces
3
+ * set like a book title in ink navy with terracotta highlights, fine ruled
4
+ * circles and a small wax-seal dot, white phones. Story: a journaling app.
5
+ */
6
+ import type { SlideElement } from '../../model/types';
7
+ import { F, METAL, background, circle, copyAt, device, pattern, ring, slide, solid, template, theme, typeset } from '../kit';
8
+
9
+ const INK = '#1f2433';
10
+ const TERRACOTTA = '#b5502a';
11
+
12
+ const t = typeset({
13
+ heading: F.fraunces,
14
+ body: F.manrope,
15
+ ink: INK,
16
+ soft: '#4a4e5b',
17
+ accent: TERRACOTTA,
18
+ head: { weight: 600, size: 0.096, lineHeight: 1.1, letterSpacing: -0.02 },
19
+ sub: { weight: 500, size: 0.039, lineHeight: 1.42 },
20
+ });
21
+
22
+ const phone = (o: Parameters<typeof device>[0]) => device({ color: METAL.white, shadow: { color: '#5a46303d', blur: 0.07, x: 0, y: 0.03 }, ...o });
23
+
24
+ /** Fine ruled circles, like a compass drawing on the page. */
25
+ const ruled = (x: number, y: number, s = 1): SlideElement[] => [
26
+ ring({ name: 'Ruled circle', x, y, size: 1.16 * s, stroke: 0.0022, color: INK, opacity: 0.18 }),
27
+ ring({ name: 'Ruled circle', x, y, size: 0.98 * s, stroke: 0.0022, color: INK, opacity: 0.12 }),
28
+ ];
29
+ const seal = (x: number, y: number) => circle({ name: 'Seal', x, y, size: 0.07, color: TERRACOTTA });
30
+
31
+ const top = () => copyAt(t, { seam: 0.152 });
32
+
33
+ const hero = (): SlideElement[] => [...ruled(0.5, 0.62), seal(0.86, 0.33), ...top(), phone({ width: 0.78, top: 0.262 })];
34
+
35
+ const gentle = (): SlideElement[] => [...ruled(0.44, 0.6, 0.9), ...top(), phone({ width: 0.78, top: 0.266, x: 0.54, rotation: -5 }), seal(0.14, 0.86)];
36
+
37
+ const pages = (): SlideElement[] => [
38
+ ...ruled(0.5, 0.66, 1.05),
39
+ ...top(),
40
+ phone({ width: 0.6, top: 0.31, x: 0.7, rotation: 5, slot: 'second' }),
41
+ phone({ width: 0.62, top: 0.262, x: 0.33, rotation: -4 }),
42
+ ];
43
+
44
+ const hanging = (): SlideElement[] => [...ruled(0.5, 0.36), seal(0.88, 0.62), phone({ width: 0.8, bottom: 0.675 }), ...copyAt(t, { seam: 0.82 })];
45
+
46
+ const locked = (): SlideElement[] => [...ruled(0.5, 0.62, 1.1), seal(0.16, 0.34), ...top(), phone({ width: 0.76, top: 0.266 })];
47
+
48
+ export const paperAndInk = template({
49
+ id: 'paper-and-ink',
50
+ name: 'Paper & Ink',
51
+ description: 'A well-made notebook: warm cotton paper with grain, Fraunces set like a book title in ink navy with terracotta highlights, fine ruled circles and a wax-seal dot.',
52
+ category: 'elegant',
53
+ tags: ['serif', 'paper', 'warm', 'journal', 'editorial'],
54
+ apps: ['tasks', 'calendar', 'social', 'chat', 'meditation', 'music'],
55
+ theme: theme({
56
+ background: background(solid('#f4eee3'), pattern('noise', '#5a4630', 0.07, 0.05)),
57
+ heading: F.fraunces,
58
+ body: F.manrope,
59
+ palette: [TERRACOTTA, '#3d6b8c', '#c99a3b', INK, '#f4eee3'],
60
+ }),
61
+ slides: [
62
+ slide(
63
+ {
64
+ en: { headline: 'Write the *story* of your days', subtitle: 'A beautiful journal that grows with you.' },
65
+ ar: { headline: 'اكتب *حكاية* أيامك', subtitle: 'دفتر يوميات أنيق ينمو معك.' },
66
+ },
67
+ hero,
68
+ ),
69
+ slide(
70
+ {
71
+ en: { headline: 'A page a day, *gently*', subtitle: 'Thoughtful prompts for when words come slowly.' },
72
+ ar: { headline: 'صفحة كل يوم *بهدوء*', subtitle: 'أسئلة ملهمة حين تتأخر الكلمات في المجيء.' },
73
+ },
74
+ gentle,
75
+ ),
76
+ slide(
77
+ {
78
+ en: { headline: 'Moments worth *keeping*', subtitle: 'Add photos, places and moods to every entry.' },
79
+ ar: { headline: 'لحظات تستحق *الحفظ*', subtitle: 'أضف الصور والأماكن والمشاعر إلى كل تدوينة.' },
80
+ },
81
+ pages,
82
+ ),
83
+ slide(
84
+ {
85
+ en: { headline: 'Look *back*, grow forward', subtitle: 'On this day: memories from years past.' },
86
+ ar: { headline: 'تأمّل *الماضي* وانطلق', subtitle: 'في مثل هذا اليوم: ذكريات من أعوام مضت.' },
87
+ },
88
+ hanging,
89
+ ),
90
+ slide(
91
+ {
92
+ en: { headline: 'Yours *alone*', subtitle: 'Locked with Face ID and encrypted on your device.' },
93
+ ar: { headline: 'لك *وحدك*', subtitle: 'مقفل ببصمة الوجه ومشفّر على جهازك.' },
94
+ },
95
+ locked,
96
+ ),
97
+ ],
98
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Sage: quiet and unhurried. Soft sage paper, a tonal disc that holds each
3
+ * phone, generous space, Manrope in deep green with a leafy highlight and a
4
+ * natural titanium phone. Story: a habit tracker.
5
+ */
6
+ import type { SlideElement } from '../../model/types';
7
+ import { F, METAL, background, circle, copyAt, device, ring, slide, solid, template, theme, typeset } from '../kit';
8
+
9
+ const DISC = '#d5ddcc';
10
+ const t = typeset({
11
+ heading: F.manrope,
12
+ body: F.manrope,
13
+ ink: '#1e2a1f',
14
+ soft: '#475448',
15
+ accent: '#467446',
16
+ head: { weight: 700, size: 0.088, lineHeight: 1.12, letterSpacing: -0.03 },
17
+ sub: { weight: 500, size: 0.04, lineHeight: 1.38 },
18
+ });
19
+
20
+ const phone = (o: Parameters<typeof device>[0]) => device({ color: METAL.titanium, shadow: { color: '#2d3a2a33', blur: 0.07, x: 0, y: 0.03 }, ...o });
21
+
22
+ const top = () => copyAt(t, { seam: 0.155 });
23
+
24
+ const hero = (): SlideElement[] => [circle({ name: 'Disc', x: 0.5, y: 0.66, size: 1.12, color: DISC }), ...top(), phone({ width: 0.74, top: 0.27 })];
25
+
26
+ const offset = (): SlideElement[] => [circle({ name: 'Disc', x: 0.62, y: 0.64, size: 1.0, color: DISC }), ...top(), phone({ width: 0.72, top: 0.272, x: 0.56 })];
27
+
28
+ const pair = (): SlideElement[] => [
29
+ circle({ name: 'Disc', x: 0.5, y: 0.7, size: 1.2, color: DISC }),
30
+ ...top(),
31
+ phone({ width: 0.56, top: 0.32, x: 0.69, slot: 'second' }),
32
+ phone({ width: 0.6, top: 0.275, x: 0.34 }),
33
+ ];
34
+
35
+ const hanging = (): SlideElement[] => [circle({ name: 'Disc', x: 0.5, y: 0.3, size: 1.12, color: DISC }), phone({ width: 0.76, bottom: 0.655 }), ...copyAt(t, { seam: 0.815 })];
36
+
37
+ const steady = (): SlideElement[] => [
38
+ circle({ name: 'Disc', x: 0.5, y: 0.66, size: 1.0, color: DISC }),
39
+ ring({ x: 0.5, y: 0.66, size: 1.22, stroke: 0.003, color: '#467446', opacity: 0.35 }),
40
+ ...top(),
41
+ phone({ width: 0.72, top: 0.275 }),
42
+ ];
43
+
44
+ export const sage = template({
45
+ id: 'sage',
46
+ name: 'Sage',
47
+ description: 'Quiet and unhurried: soft sage paper, a tonal disc that holds each phone, generous space, Manrope in deep green with a leafy highlight and a natural titanium phone.',
48
+ category: 'minimal',
49
+ tags: ['calm', 'green', 'minimal', 'habits', 'soft'],
50
+ apps: ['tasks', 'fitness', 'meditation', 'calendar', 'chat', 'social'],
51
+ theme: theme({
52
+ background: background(solid('#e4e9de')),
53
+ heading: F.manrope,
54
+ body: F.manrope,
55
+ palette: ['#3d9a50', '#e2a54a', '#7b9cc4', '#1e2a1f', '#e4e9de'],
56
+ }),
57
+ slides: [
58
+ slide(
59
+ {
60
+ en: { headline: 'Small habits, *big* change', subtitle: 'Build routines that actually stick.' },
61
+ ar: { headline: 'عادات صغيرة، *أثر* كبير', subtitle: 'ابنِ روتينًا يدوم فعلًا.' },
62
+ },
63
+ hero,
64
+ ),
65
+ slide(
66
+ {
67
+ en: { headline: 'One day at a *time*', subtitle: 'Gentle reminders, never guilt trips.' },
68
+ ar: { headline: 'يومًا *بيوم*', subtitle: 'تذكيرات لطيفة دون أي شعور بالذنب.' },
69
+ },
70
+ offset,
71
+ ),
72
+ slide(
73
+ {
74
+ en: { headline: 'See your *streaks*', subtitle: 'Progress you can feel, week after week.' },
75
+ ar: { headline: 'تابع *إنجازاتك*', subtitle: 'تقدّم تلمسه أسبوعًا بعد أسبوع.' },
76
+ },
77
+ pair,
78
+ ),
79
+ slide(
80
+ {
81
+ en: { headline: 'Mindful *mornings*', subtitle: 'Start each day with breath, water and intention.' },
82
+ ar: { headline: 'صباحات *واعية*', subtitle: 'ابدأ يومك بالتنفّس والماء ونيّة واضحة.' },
83
+ },
84
+ hanging,
85
+ ),
86
+ slide(
87
+ {
88
+ en: { headline: 'Quietly *motivating*', subtitle: 'No badges, no noise, just steady progress.' },
89
+ ar: { headline: 'تحفيز *هادئ*', subtitle: 'بلا شارات ولا ضجيج، تقدّم ثابت فقط.' },
90
+ },
91
+ steady,
92
+ ),
93
+ ],
94
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Split Tone: two colours, one bold diagonal. Deep navy above, a slab of
3
+ * tangerine, cream or sky below, the phone straddling the cut. DM Sans set
4
+ * heavy and tight. Story: a personal finance app.
5
+ */
6
+ import type { SlideElement } from '../../model/types';
7
+ import { ALL_CLASSES, F, METAL, background, canvas, copyAt, device, r5, rect, slide, solid, template, theme, typeset, type Cls, type DeviceOptions } from '../kit';
8
+
9
+ const NAVY = '#0f1a3d';
10
+ const TANGERINE = '#ff6b2c';
11
+ const CREAM = '#f4efe6';
12
+ const SKY = '#8ec5ff';
13
+
14
+ const t = typeset({
15
+ heading: F.dmSans,
16
+ body: F.dmSans,
17
+ ink: '#ffffff',
18
+ soft: '#c9d2ee',
19
+ accent: '#ff9a5c',
20
+ head: { weight: 800, size: 0.094, lineHeight: 1.08, letterSpacing: -0.04 },
21
+ sub: { weight: 500, size: 0.04 },
22
+ });
23
+
24
+ const SHADOW = { color: '#050b2280', blur: 0.07, x: 0, y: 0.03 };
25
+ const phone = (o: DeviceOptions) => device({ color: METAL.silver, shadow: SHADOW, ...o });
26
+ const pad = (o: DeviceOptions) => device({ cls: 'tablet', frame: 'ipad', color: METAL.silver, shadow: SHADOW, ...o });
27
+ const laptop = (o: DeviceOptions) => device({ cls: 'wide', frame: 'macbook', color: METAL.silver, shadow: SHADOW, ...o });
28
+
29
+ /** Where the cut crosses the middle of each canvas, fraction of its height. */
30
+ const EDGE: Record<Cls, number> = { tall: 0.52, tablet: 0.6, wide: 0.66 };
31
+
32
+ /**
33
+ * The slab of colour beyond the cut: a huge rotated rectangle whose edge is
34
+ * the diagonal, crossing the middle of the canvas at `edge`; `above` puts
35
+ * the slab on the upper side of the cut.
36
+ */
37
+ function slab(cls: Cls, color: string, angle: number, edge = EDGE[cls], above = false) {
38
+ const { wu, hu } = canvas(cls);
39
+ const height = 1.8 * hu;
40
+ const y = above ? edge - height / 2 / hu : edge + height / 2 / hu;
41
+ return rect({ name: 'Color slab', x: 0.5, y: r5(y), width: r5(2.8 * wu), height: r5(height), rotation: angle, radius: 0, color });
42
+ }
43
+
44
+ function copy(cls: Cls, seam?: number): SlideElement[] {
45
+ if (cls === 'wide') return copyAt(t, { cls, seam: seam ?? 0.2, width: 1.2, subWidth: 0.95, head: { size: 0.068 }, sub: { size: 0.03 } });
46
+ if (cls === 'tablet') return copyAt(t, { cls, seam: seam ?? 0.165, width: 0.8, subWidth: 0.64, head: { size: 0.068 }, sub: { size: 0.032 } });
47
+ return copyAt(t, { seam: seam ?? 0.15 });
48
+ }
49
+
50
+ /** One device straddling the cut. */
51
+ function single(color: string, angle: number, o: { x?: number; rotation?: number } = {}) {
52
+ return (cls: Cls): SlideElement[] => {
53
+ if (cls === 'wide') return [slab(cls, color, angle / 2), ...copy(cls), laptop({ width: 0.98, top: 0.35 })];
54
+ if (cls === 'tablet') return [slab(cls, color, angle * 0.7), ...copy(cls), pad({ width: 0.72, top: 0.3, x: 0.5 + ((o.x ?? 0.5) - 0.5) / 2, rotation: (o.rotation ?? 0) / 2 })];
55
+ return [slab(cls, color, angle), ...copy(cls), phone({ width: 0.8, top: 0.255, x: o.x ?? 0.5, rotation: o.rotation ?? 0 })];
56
+ };
57
+ }
58
+
59
+ function duo(cls: Cls): SlideElement[] {
60
+ if (cls === 'wide') return [slab(cls, SKY, -4), ...copy(cls), laptop({ width: 0.86, top: 0.38, x: 0.42, slot: 'second' }), device({ cls, frame: 'none', width: 0.46, x: 0.74, y: 0.7, cornerRadius: 0.03, shadow: SHADOW })];
61
+ if (cls === 'tablet') return [slab(cls, SKY, -6), ...copy(cls), pad({ width: 0.56, top: 0.36, x: 0.68, rotation: 4, slot: 'second' }), pad({ width: 0.58, top: 0.3, x: 0.34, rotation: -3 })];
62
+ return [slab(cls, SKY, -8, 0.56), ...copy(cls), phone({ width: 0.6, top: 0.31, x: 0.7, rotation: 6, slot: 'second' }), phone({ width: 0.62, top: 0.26, x: 0.33, rotation: -5 })];
63
+ }
64
+
65
+ /** The cut runs high and the copy sits below it, on navy again. */
66
+ function flipped(cls: Cls): SlideElement[] {
67
+ if (cls === 'wide') return [slab(cls, TANGERINE, -4, 0.42, true), laptop({ width: 0.9, top: 0.07 }), ...copy(cls, 0.83)];
68
+ if (cls === 'tablet') return [slab(cls, TANGERINE, -6, 0.36, true), pad({ width: 0.74, bottom: 0.68 }), ...copy(cls, 0.83)];
69
+ return [slab(cls, TANGERINE, -9, 0.3, true), phone({ width: 0.8, bottom: 0.68 }), ...copyAt(t, { seam: 0.81 })];
70
+ }
71
+
72
+ const o = { classes: ALL_CLASSES };
73
+
74
+ export const splitTone = template({
75
+ id: 'split-tone',
76
+ name: 'Split Tone',
77
+ description: 'Two colours, one bold diagonal: deep navy above, a slab of tangerine, cream or sky below, and the phone straddling the cut. Heavy, tight DM Sans headlines.',
78
+ category: 'bold',
79
+ tags: ['bold', 'two-tone', 'diagonal', 'finance', 'navy'],
80
+ apps: ['finance', 'shop', 'chat', 'calendar', 'tasks', 'travel', 'social'],
81
+ theme: theme({
82
+ background: background(solid(NAVY)),
83
+ heading: F.dmSans,
84
+ body: F.dmSans,
85
+ palette: [TANGERINE, SKY, '#22c55e', CREAM, '#ffffff', NAVY],
86
+ }),
87
+ slides: [
88
+ slide(
89
+ {
90
+ en: { headline: 'Know where your *money* goes', subtitle: 'Every card and account, sorted automatically.' },
91
+ ar: { headline: 'اعرف أين تذهب *أموالك*', subtitle: 'كل بطاقاتك وحساباتك مصنّفة تلقائيًا.' },
92
+ },
93
+ single(TANGERINE, -10),
94
+ o,
95
+ ),
96
+ slide(
97
+ {
98
+ en: { headline: 'Budgets that *bend*', subtitle: 'Flexible limits that adjust to real life.' },
99
+ ar: { headline: 'ميزانية *مرنة*', subtitle: 'حدود إنفاق تتكيّف مع تفاصيل حياتك اليومية.' },
100
+ },
101
+ single(CREAM, 9, { x: 0.45, rotation: 7 }),
102
+ o,
103
+ ),
104
+ slide(
105
+ {
106
+ en: { headline: 'Split bills in *seconds*', subtitle: 'Settle up with friends without the awkward math.' },
107
+ ar: { headline: 'قسّم الفواتير في *ثوانٍ*', subtitle: 'سدّد حصتك مع الأصدقاء دون حسابات محرجة.' },
108
+ },
109
+ duo,
110
+ o,
111
+ ),
112
+ slide(
113
+ {
114
+ en: { headline: 'Save on *autopilot*', subtitle: 'Round-ups and smart rules grow your savings.' },
115
+ ar: { headline: 'ادّخر *تلقائيًا*', subtitle: 'تقريب المشتريات وقواعد ذكية تنمّي مدّخراتك.' },
116
+ },
117
+ flipped,
118
+ o,
119
+ ),
120
+ slide(
121
+ {
122
+ en: { headline: 'Bank-grade *security*', subtitle: 'Face ID, encryption and alerts for every payment.' },
123
+ ar: { headline: 'أمان *بمستوى البنوك*', subtitle: 'بصمة الوجه والتشفير وتنبيه فوري لكل عملية دفع.' },
124
+ },
125
+ single(TANGERINE, 10),
126
+ o,
127
+ ),
128
+ ],
129
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sunset Boulevard: dusk over the coast road. A violet-to-peach sky with
3
+ * faint waves, a setting sun behind tilted silver devices, Bricolage
4
+ * Grotesque headlines with butter-yellow marker highlights. Story: a
5
+ * road-trip planner.
6
+ */
7
+ import type { SlideElement } from '../../model/types';
8
+ import { ALL_CLASSES, F, METAL, background, circle, copyAt, device, glow, linear, pattern, slide, template, theme, typeset, type Cls, type DeviceOptions } from '../kit';
9
+
10
+ const t = typeset({
11
+ heading: F.bricolage,
12
+ body: F.dmSans,
13
+ ink: '#ffffff',
14
+ soft: '#fbe8ff',
15
+ accent: '#ffd166',
16
+ mode: 'marker',
17
+ head: { weight: 760, size: 0.094, lineHeight: 1.12, letterSpacing: -0.025 },
18
+ sub: { weight: 500, size: 0.041 },
19
+ });
20
+
21
+ const SHADOW = { color: '#3b0a3d73', blur: 0.07, x: 0, y: 0.032 };
22
+ const phone = (o: DeviceOptions) => device({ color: METAL.silver, shadow: SHADOW, ...o });
23
+ const pad = (o: DeviceOptions) => device({ cls: 'tablet', frame: 'ipad', color: METAL.silver, shadow: SHADOW, ...o });
24
+ const laptop = (o: DeviceOptions) => device({ cls: 'wide', frame: 'macbook', color: METAL.silver, shadow: SHADOW, ...o });
25
+
26
+ /** The setting sun: a warm disc with a soft halo. */
27
+ const sun = (x: number, y: number, size: number): SlideElement[] => [
28
+ glow({ name: 'Sun glow', x, y, size: size * 1.08, color: '#ff9a6b', opacity: 0.55, blur: 0.12 }),
29
+ circle({ name: 'Sun', x, y, size, fill: linear(180, '#ffe9a6', '#ffb37a', '#ff7e86') }),
30
+ ];
31
+
32
+ /** Copy at the top, where the sky is darkest (wide canvases stack copy over the laptop). */
33
+ function copy(cls: Cls, seam?: number): SlideElement[] {
34
+ if (cls === 'wide') return copyAt(t, { cls, seam: seam ?? 0.2, width: 1.2, subWidth: 0.95, head: { size: 0.068 }, sub: { size: 0.03 } });
35
+ if (cls === 'tablet') return copyAt(t, { cls, seam: seam ?? 0.165, width: 0.8, subWidth: 0.64, head: { size: 0.068 }, sub: { size: 0.032 } });
36
+ return copyAt(t, { seam: seam ?? 0.152 });
37
+ }
38
+
39
+ /** A tilted device rising from the bottom edge in front of the sun. */
40
+ function tilted(sunAt: [number, number, number], rotation: number, x: number) {
41
+ return (cls: Cls): SlideElement[] => {
42
+ if (cls === 'wide') return [...sun(sunAt[0] * 0.6 + 0.2, 0.84, 0.9), ...copy(cls), laptop({ width: 0.98, top: 0.35, x: 0.5 })];
43
+ if (cls === 'tablet') return [...sun(sunAt[0], 0.74, sunAt[2] * 0.85), ...copy(cls), pad({ width: 0.72, top: 0.3, x: 0.5 + (x - 0.5) / 2, rotation: rotation / 2 })];
44
+ return [...sun(...sunAt), ...copy(cls), phone({ width: 0.78, top: 0.262, x, rotation })];
45
+ };
46
+ }
47
+
48
+ function duo(cls: Cls): SlideElement[] {
49
+ if (cls === 'wide') {
50
+ return [...sun(0.5, 0.86, 0.8), ...copy(cls), laptop({ width: 0.86, top: 0.38, x: 0.42, slot: 'second' }), device({ cls, frame: 'none', width: 0.46, x: 0.74, y: 0.7, cornerRadius: 0.03, shadow: SHADOW })];
51
+ }
52
+ if (cls === 'tablet') return [...sun(0.5, 0.72, 0.6), ...copy(cls), pad({ width: 0.56, top: 0.36, x: 0.68, rotation: 5, slot: 'second' }), pad({ width: 0.58, top: 0.3, x: 0.34, rotation: -4 })];
53
+ return [...sun(0.5, 0.5, 0.7), ...copy(cls), phone({ width: 0.6, top: 0.31, x: 0.71, rotation: 10, slot: 'second' }), phone({ width: 0.62, top: 0.262, x: 0.32, rotation: -8 })];
54
+ }
55
+
56
+ /** The sky turned upside down so copy at the bottom sits on dusk violet. */
57
+ const DUSK_UP = background(linear(0, ['#24104f', 0], ['#6b2a8c', 0.34], ['#d4567c', 0.68], ['#ffb27a', 1]), pattern('waves', '#ffffff', 0.07, 0.06));
58
+
59
+ function hanging(cls: Cls): SlideElement[] {
60
+ if (cls === 'wide') return [...sun(0.5, 0.06, 0.7), laptop({ width: 0.9, top: 0.07 }), ...copyAt(t, { cls, seam: 0.83, width: 1.2, subWidth: 0.95, head: { size: 0.066 }, sub: { size: 0.03 } })];
61
+ if (cls === 'tablet') return [...sun(0.5, 0.08, 0.7), pad({ width: 0.74, bottom: 0.66, rotation: -3 }), ...copyAt(t, { cls, seam: 0.83, width: 0.8, subWidth: 0.64, head: { size: 0.068 }, sub: { size: 0.032 } })];
62
+ return [...sun(0.5, 0.1, 0.9), phone({ width: 0.8, bottom: 0.655, x: 0.53, rotation: -6 }), ...copyAt(t, { seam: 0.8 })];
63
+ }
64
+
65
+ const o = { classes: ALL_CLASSES };
66
+
67
+ export const sunsetBoulevard = template({
68
+ id: 'sunset-boulevard',
69
+ name: 'Sunset Boulevard',
70
+ description: 'Dusk over the coast road: a violet-to-peach sky with faint waves, a setting sun behind tilted silver phones and bold headlines with butter-yellow marker highlights.',
71
+ category: 'gradient',
72
+ tags: ['sunset', 'warm', 'tilted', 'marker', 'travel'],
73
+ apps: ['travel', 'calendar', 'music', 'tasks', 'social', 'chat', 'shop', 'finance'],
74
+ theme: theme({
75
+ background: background(linear(180, ['#24104f', 0], ['#6b2a8c', 0.36], ['#d4567c', 0.7], ['#ffb27a', 1]), pattern('waves', '#ffffff', 0.07, 0.06)),
76
+ heading: F.bricolage,
77
+ body: F.dmSans,
78
+ palette: ['#ff6b8a', '#ffd166', '#8b5cf6', '#ffb27a', '#ffffff', '#24104f'],
79
+ }),
80
+ slides: [
81
+ slide(
82
+ {
83
+ en: { headline: 'Plan the *perfect* road trip', subtitle: 'Routes, stops and stays, organized on one beautiful map.' },
84
+ ar: { headline: 'خطّط لرحلتك *المثالية*', subtitle: 'المسارات والمحطات وأماكن الإقامة على خريطة واحدة أنيقة.' },
85
+ de: { headline: 'Plane den *perfekten* Roadtrip', subtitle: 'Routen, Stopps und Unterkünfte übersichtlich auf einer Karte.' },
86
+ },
87
+ tilted([0.5, 0.55, 0.92], 8, 0.46),
88
+ o,
89
+ ),
90
+ slide(
91
+ {
92
+ en: { headline: 'Travel with *friends*', subtitle: 'Share plans, split costs and vote on the next stop.' },
93
+ ar: { headline: 'سافر مع *أصدقائك*', subtitle: 'شاركوا الخطط وقسّموا التكاليف واختاروا المحطة التالية معًا.' },
94
+ de: { headline: 'Reise mit *Freunden*', subtitle: 'Teilt Pläne und Kosten und stimmt über den nächsten Halt ab.' },
95
+ },
96
+ duo,
97
+ o,
98
+ ),
99
+ slide(
100
+ {
101
+ en: { headline: 'Every stop, *one* tap', subtitle: 'Save cafés, viewpoints and hidden gems along the way.' },
102
+ ar: { headline: 'كل محطة *بلمسة* واحدة', subtitle: 'احفظ المقاهي والإطلالات والأماكن المميزة على طول الطريق.' },
103
+ de: { headline: 'Jeder Stopp mit *einem* Tipp', subtitle: 'Speichere Cafés, Aussichtspunkte und Geheimtipps unterwegs.' },
104
+ },
105
+ hanging,
106
+ { ...o, background: DUSK_UP },
107
+ ),
108
+ slide(
109
+ {
110
+ en: { headline: 'Golden hour, *guaranteed*', subtitle: 'Sunset times and scenic detours for every route.' },
111
+ ar: { headline: 'لحظة الغروب *مضمونة*', subtitle: 'مواعيد الغروب وطرق بديلة خلابة لكل مسار.' },
112
+ de: { headline: 'Goldene Stunde *garantiert*', subtitle: 'Sonnenuntergangszeiten und Panoramaumwege für jede Route.' },
113
+ },
114
+ tilted([0.22, 0.47, 0.62], -8, 0.55),
115
+ o,
116
+ ),
117
+ slide(
118
+ {
119
+ en: { headline: 'Offline, *anywhere*', subtitle: 'Maps and tickets that keep working far beyond the signal.' },
120
+ ar: { headline: 'خرائطك تعمل *دائمًا*', subtitle: 'خرائط وتذاكر متاحة حتى في أبعد الأماكن عن التغطية.' },
121
+ de: { headline: 'Offline, *überall*', subtitle: 'Karten und Tickets, die auch ohne Empfang funktionieren.' },
122
+ },
123
+ tilted([0.5, 0.6, 1.16], 0, 0.5),
124
+ o,
125
+ ),
126
+ ],
127
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Built-in templates shipped with the app. Owned by the templates work package.
3
+ * Each template lives in its own module under ./builtin/ (authored with the
4
+ * builders in ./kit.ts) and is listed here in the order the template picker
5
+ * shows them: alternating light and dark, calm and loud, so any row of the
6
+ * grid looks varied. The first one is the default (`?demo`).
7
+ */
8
+ import type { Template } from '../model/types';
9
+ import { aurora } from './builtin/aurora';
10
+ import { bubblegum } from './builtin/bubblegum';
11
+ import { citrusPop } from './builtin/citrusPop';
12
+ import { clayGarden } from './builtin/clayGarden';
13
+ import { desertDune } from './builtin/desertDune';
14
+ import { glassPrism } from './builtin/glassPrism';
15
+ import { launchpad } from './builtin/launchpad';
16
+ import { loudAndClear } from './builtin/loudAndClear';
17
+ import { midnightNeon } from './builtin/midnightNeon';
18
+ import { monoSwiss } from './builtin/monoSwiss';
19
+ import { noirGold } from './builtin/noirGold';
20
+ import { noor } from './builtin/noor';
21
+ import { obsidian } from './builtin/obsidian';
22
+ import { panoramaPeaks } from './builtin/panoramaPeaks';
23
+ import { panoramaRibbon } from './builtin/panoramaRibbon';
24
+ import { panoramaWaves } from './builtin/panoramaWaves';
25
+ import { paperAndInk } from './builtin/paperAndInk';
26
+ import { sage } from './builtin/sage';
27
+ import { splitTone } from './builtin/splitTone';
28
+ import { sunsetBoulevard } from './builtin/sunsetBoulevard';
29
+
30
+ export const BUILTIN_TEMPLATES: Template[] = [
31
+ aurora,
32
+ sunsetBoulevard,
33
+ monoSwiss,
34
+ midnightNeon,
35
+ clayGarden,
36
+ loudAndClear,
37
+ panoramaWaves,
38
+ paperAndInk,
39
+ glassPrism,
40
+ obsidian,
41
+ citrusPop,
42
+ noor,
43
+ panoramaPeaks,
44
+ bubblegum,
45
+ launchpad,
46
+ noirGold,
47
+ splitTone,
48
+ desertDune,
49
+ panoramaRibbon,
50
+ sage,
51
+ ];
52
+
53
+ export function builtinTemplate(id: string): Template | undefined {
54
+ return BUILTIN_TEMPLATES.find((t) => t.id === id);
55
+ }