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,337 @@
1
+ /**
2
+ * System chrome around mock apps: status bars aligned with the frames'
3
+ * Dynamic Island / punch hole, tab and navigation bars, home indicators,
4
+ * and the sidebar + toolbar shells used on tablets and desktops.
5
+ */
6
+ import { mix, rgba } from '../../color';
7
+ import { ISLAND } from '../iphone';
8
+ import { PUNCH_HOLE } from '../android';
9
+ import type { Area, Mk } from './kit';
10
+ import { bar, body, box, brandFill, card, circle, num, rr, shadowed, title } from './kit';
11
+ import { icon, type IconName } from './icons';
12
+ import { avatar } from './art';
13
+
14
+ /** Status bar heights in points. */
15
+ export function statusHeight(m: Mk): number {
16
+ if (m.form === 'desktop') return 0;
17
+ if (m.form === 'tablet') return m.os === 'ios' ? 26 : 30;
18
+ return m.os === 'ios' ? 54 : 42;
19
+ }
20
+
21
+ function signalBars(m: Mk, x: number, base: number, ink: string, scale = 1): number {
22
+ const w = 3 * scale;
23
+ const gap = 1.6 * scale;
24
+ [4.2, 6.6, 9, 11.4].forEach((h, i) => box(m, x + i * (w + gap), base - h * scale, w, h * scale, 0.9 * scale, ink));
25
+ return 4 * w + 3 * gap;
26
+ }
27
+
28
+ function wifi(m: Mk, cx: number, base: number, ink: string, scale = 1): void {
29
+ const ctx = m.ctx;
30
+ const a0 = -Math.PI * 0.75;
31
+ const a1 = -Math.PI * 0.25;
32
+ for (let i = 0; i < 3; i++) {
33
+ const r = (3.4 + i * 3.6) * scale;
34
+ ctx.beginPath();
35
+ ctx.arc(cx, base, r, a0, a1);
36
+ if (i === 0) {
37
+ ctx.lineTo(cx, base);
38
+ ctx.closePath();
39
+ ctx.fillStyle = ink;
40
+ ctx.fill();
41
+ } else {
42
+ ctx.lineWidth = 2.2 * scale;
43
+ ctx.lineCap = 'round';
44
+ ctx.strokeStyle = ink;
45
+ ctx.stroke();
46
+ }
47
+ }
48
+ ctx.lineCap = 'butt';
49
+ }
50
+
51
+ function battery(m: Mk, x: number, cy: number, ink: string, level: number, scale = 1): number {
52
+ const w = 25 * scale;
53
+ const h = 12 * scale;
54
+ rr(m, x + 0.5, cy - h / 2 + 0.5, w - 1, h - 1, 3.6 * scale);
55
+ m.ctx.lineWidth = 1;
56
+ m.ctx.strokeStyle = rgba(ink, 0.4);
57
+ m.ctx.stroke();
58
+ box(m, x + 2 * scale, cy - h / 2 + 2 * scale, (w - 4 * scale) * level, h - 4 * scale, 2 * scale, ink);
59
+ box(m, x + w + 1 * scale, cy - 2 * scale, 1.6 * scale, 4 * scale, 0.8 * scale, rgba(ink, 0.45));
60
+ return w + 2.6 * scale;
61
+ }
62
+
63
+ function androidGlyphs(m: Mk, right: number, cy: number, ink: string, scale = 1): void {
64
+ const ctx = m.ctx;
65
+ // Battery: vertical pill.
66
+ const bw = 7.5 * scale;
67
+ const bh = 13 * scale;
68
+ let x = right - bw;
69
+ rr(m, x, cy - bh / 2, bw, bh, 2 * scale);
70
+ ctx.fillStyle = rgba(ink, 0.35);
71
+ ctx.fill();
72
+ box(m, x, cy - bh / 2 + bh * 0.22, bw, bh * 0.78, 2 * scale, ink);
73
+ box(m, x + bw * 0.3, cy - bh / 2 - 1.5 * scale, bw * 0.4, 2 * scale, 0.6 * scale, ink);
74
+ // Signal: filled right triangle.
75
+ x -= 18 * scale;
76
+ ctx.beginPath();
77
+ ctx.moveTo(x, cy + 6 * scale);
78
+ ctx.lineTo(x + 12 * scale, cy + 6 * scale);
79
+ ctx.lineTo(x + 12 * scale, cy - 6 * scale);
80
+ ctx.closePath();
81
+ ctx.fillStyle = ink;
82
+ ctx.fill();
83
+ // Wi-Fi: filled fan.
84
+ x -= 10 * scale;
85
+ ctx.beginPath();
86
+ ctx.moveTo(x, cy + 6 * scale);
87
+ ctx.arc(x, cy + 6 * scale, 13 * scale, -Math.PI * 0.75, -Math.PI * 0.25);
88
+ ctx.closePath();
89
+ ctx.fill();
90
+ }
91
+
92
+ /** Status bar in `ink`, phone or tablet, iOS or Android. */
93
+ export function statusBar(m: Mk, W: number, ink: string): void {
94
+ if (m.form === 'desktop') return;
95
+ if (m.form === 'phone' && m.os === 'ios') {
96
+ const cy = (ISLAND.top + ISLAND.height / 2) * W;
97
+ const ear = (W * (1 - ISLAND.width)) / 4;
98
+ num(m, '9:41', ear + 2, cy + 6.2, 17, 600, ink, 'center');
99
+ const groupW = 18 + 6 + 16 + 6 + 27.6;
100
+ let x = W - ear - groupW / 2 - 2;
101
+ x += signalBars(m, x, cy + 5.4, ink) + 6;
102
+ wifi(m, x + 8, cy + 5.4, ink);
103
+ x += 16 + 6;
104
+ battery(m, x, cy, ink, 0.82);
105
+ return;
106
+ }
107
+ if (m.form === 'phone') {
108
+ const cy = PUNCH_HOLE.centerY * W;
109
+ num(m, '9:30', 24, cy + 5, 14.5, 500, ink, 'left');
110
+ androidGlyphs(m, W - 24, cy, ink);
111
+ return;
112
+ }
113
+ // Tablets: compact bar, time and date on the left.
114
+ const cy = m.os === 'ios' ? 13 : 15;
115
+ const tw = num(m, m.os === 'ios' ? '9:41' : '9:30', 22, cy + 4.6, 13.5, 600, ink, 'left');
116
+ bar(m, 22 + tw + 7, cy - 2.6, 62, 5.2, rgba(ink, 0.55));
117
+ if (m.os === 'ios') {
118
+ const bx = W - 22 - 25 * 0.86 - 2;
119
+ battery(m, bx, cy, ink, 0.82, 0.86);
120
+ wifi(m, bx - 13, cy + 4.6, ink, 0.86);
121
+ signalBars(m, bx - 46, cy + 4.6, ink, 0.86);
122
+ } else {
123
+ androidGlyphs(m, W - 22, cy, ink, 0.86);
124
+ }
125
+ }
126
+
127
+ /** iOS home indicator / Android gesture handle. */
128
+ export function homeIndicator(m: Mk, W: number, H: number, ink: string): void {
129
+ if (m.form === 'desktop') return;
130
+ if (m.os === 'ios') {
131
+ const w = m.form === 'tablet' ? 300 : 136;
132
+ bar(m, (W - w) / 2, H - 8 - 5, w, 5, rgba(ink, 0.9));
133
+ } else {
134
+ bar(m, (W - 104) / 2, H - 10 - 4, 104, 4, rgba(ink, 0.65));
135
+ }
136
+ }
137
+
138
+ /** Space the tab bar takes at the bottom of a phone, in points. */
139
+ export function tabBarHeight(m: Mk): number {
140
+ return m.os === 'ios' ? 100 : 98;
141
+ }
142
+
143
+ /**
144
+ * Phone tab bar. iOS draws the floating glass capsule of iOS 26; Android a
145
+ * Material 3 navigation bar with a pill behind the selected icon.
146
+ */
147
+ export function tabBar(m: Mk, W: number, H: number, icons: readonly IconName[], selected: number): void {
148
+ const t = m.t;
149
+ const n = icons.length;
150
+ if (n === 0) return;
151
+ if (m.os === 'ios') {
152
+ const bh = 64;
153
+ const y = H - 34 - bh + 6;
154
+ const x = 20;
155
+ const w = W - 40;
156
+ shadowed(m, 30, 8, t.dark ? 'rgba(0, 0, 0, 0.55)' : 'rgba(20, 28, 48, 0.16)', () =>
157
+ box(m, x, y, w, bh, bh / 2, t.dark ? rgba(mix(t.surface, '#ffffff', 0.08), 0.94) : 'rgba(255, 255, 255, 0.94)'),
158
+ );
159
+ rr(m, x + 0.5, y + 0.5, w - 1, bh - 1, bh / 2 - 0.5);
160
+ m.ctx.lineWidth = 1;
161
+ m.ctx.strokeStyle = t.dark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.9)';
162
+ m.ctx.stroke();
163
+ const slot = (w - 12) / n;
164
+ icons.forEach((name, i) => {
165
+ const cx = x + 6 + slot * (i + 0.5);
166
+ const on = i === selected;
167
+ if (on) box(m, cx - slot / 2 + 2, y + 6, slot - 4, bh - 12, (bh - 12) / 2, t.dark ? 'rgba(255, 255, 255, 0.1)' : rgba(t.accent, 0.1));
168
+ icon(m.ctx, name, cx, y + 25, 24, on ? t.accent : t.text2, false, on ? 2.3 : 2);
169
+ bar(m, cx - 13, y + 43, 26, 4.6, on ? t.accent : t.bar3);
170
+ });
171
+ return;
172
+ }
173
+ const bh = 80;
174
+ const y = H - 18 - bh;
175
+ m.ctx.fillStyle = t.dark ? mix(t.surface, t.accent, 0.04) : mix('#ffffff', t.accent, 0.035);
176
+ m.ctx.fillRect(0, y, W, H - y);
177
+ const slot = W / n;
178
+ icons.forEach((name, i) => {
179
+ const cx = slot * (i + 0.5);
180
+ const on = i === selected;
181
+ if (on) box(m, cx - 32, y + 12, 64, 32, 16, t.soft);
182
+ icon(m.ctx, name, cx, y + 28, 24, on ? t.accent : t.text2, false, 2);
183
+ bar(m, cx - 15, y + 54, 30, 5, on ? t.bar : t.bar3);
184
+ });
185
+ }
186
+
187
+ function logoTile(m: Mk, x: number, y: number, s: number, glyph: IconName): void {
188
+ box(m, x, y, s, s, s * 0.3, brandFill(m, x, y, s, s));
189
+ icon(m.ctx, glyph, x + s / 2, y + s / 2, s * 0.56, '#ffffff', false, 2.4);
190
+ }
191
+
192
+ /** Navigation rows with icons; returns the y below the last row. */
193
+ function navList(m: Mk, x: number, y: number, w: number, icons: readonly IconName[], selected: number, rowH: number): number {
194
+ const t = m.t;
195
+ const widths = [64, 82, 58, 74, 70, 60, 78];
196
+ icons.forEach((name, i) => {
197
+ const on = i === selected;
198
+ const ry = y + i * rowH;
199
+ if (on) box(m, x, ry, w, rowH - 4, 10, t.dark ? rgba(t.accent, 0.18) : rgba(t.accent, 0.1));
200
+ icon(m.ctx, name, x + 20, ry + (rowH - 4) / 2, 18, on ? t.accent : t.text2, false, 2);
201
+ body(m, x + 40, ry + (rowH - 4) / 2 - 7, widths[i % widths.length], 14, on ? t.accent : t.bar2);
202
+ });
203
+ return y + icons.length * rowH;
204
+ }
205
+
206
+ function tagList(m: Mk, x: number, y: number, count: number): number {
207
+ const t = m.t;
208
+ bar(m, x + 8, y, 52, 5, t.bar3);
209
+ const widths = [70, 54, 84, 62];
210
+ for (let i = 0; i < count; i++) {
211
+ const ry = y + 22 + i * 32;
212
+ circle(m, x + 14, ry + 6, 4.5, t.accents[i % 4]);
213
+ body(m, x + 28, ry, widths[i % widths.length], 13);
214
+ }
215
+ return y + 22 + count * 32;
216
+ }
217
+
218
+ export interface Shell {
219
+ /** Navigation icons, first one selected unless `selected` says otherwise. */
220
+ nav: readonly IconName[];
221
+ selected: number;
222
+ /** Glyph in the app's logo tile. */
223
+ glyph: IconName;
224
+ }
225
+
226
+ /**
227
+ * Desktop app shell: sidebar (window controls when `windowControls`),
228
+ * toolbar with search and a primary action. Returns the content area.
229
+ */
230
+ export function desktopShell(m: Mk, W: number, H: number, shell: Shell, windowControls: boolean): Area {
231
+ const t = m.t;
232
+ const ctx = m.ctx;
233
+ const SW = Math.min(250, Math.max(190, W * 0.18));
234
+ ctx.fillStyle = t.sidebar;
235
+ ctx.fillRect(0, 0, SW, H);
236
+ ctx.fillStyle = t.line;
237
+ ctx.fillRect(SW - 1, 0, 1, H);
238
+ let y = 22;
239
+ if (windowControls) {
240
+ ['#ff5f57', '#febc2e', '#28c840'].forEach((c, i) => circle(m, 22 + i * 20, 22, 6, c));
241
+ y = 54;
242
+ }
243
+ logoTile(m, 18, y, 30, shell.glyph);
244
+ title(m, 58, y + 6, 86, 16);
245
+ y += 50;
246
+ box(m, 14, y, SW - 28, 32, 9, t.dark ? t.surface2 : '#ffffff');
247
+ if (!t.dark) {
248
+ rr(m, 14.5, y + 0.5, SW - 29, 31, 8.5);
249
+ ctx.lineWidth = 1;
250
+ ctx.strokeStyle = t.line;
251
+ ctx.stroke();
252
+ }
253
+ icon(ctx, 'search', 32, y + 16, 15, t.text2, false, 2.2);
254
+ bar(m, 46, y + 13.5, 60, 5, t.bar3);
255
+ y = navList(m, 10, y + 48, SW - 20, shell.nav, shell.selected, 40);
256
+ y = tagList(m, 10, y + 22, 3);
257
+ // Profile at the bottom.
258
+ if (H - 56 > y + 12) {
259
+ ctx.fillStyle = t.line;
260
+ ctx.fillRect(14, H - 64, SW - 28, 1);
261
+ avatar(m, 34, H - 34, 32, m.seed + 5);
262
+ title(m, 58, H - 47, 70, 13);
263
+ body(m, 58, H - 31, 96, 11);
264
+ }
265
+
266
+ // Toolbar.
267
+ const cx = SW + 28;
268
+ const right = W - 24;
269
+ title(m, cx, 22, Math.min(170, (W - SW) * 0.2), 24);
270
+ body(m, cx, 52, Math.min(230, (W - SW) * 0.26), 13);
271
+ circle(m, right - 18, 40, 18, t.surface2);
272
+ avatar(m, right - 18, 40, 34, m.seed + 9);
273
+ const bw = 118;
274
+ const bx = right - 44 - bw;
275
+ shadowed(m, 14, 5, t.dark ? 'rgba(0, 0, 0, 0.4)' : rgba(t.accent, 0.3), () => box(m, bx, 22, bw, 36, 11, t.accent));
276
+ icon(ctx, 'plus', bx + 22, 40, 16, t.onAccent, false, 2.6);
277
+ bar(m, bx + 38, 37.5, 60, 5, rgba(t.onAccent, 0.9));
278
+ const sx = bx - 52;
279
+ circle(m, sx + 18, 40, 18, t.dark ? t.surface2 : '#ffffff');
280
+ icon(ctx, 'bell', sx + 18, 40, 17, t.text2, false, 2);
281
+ circle(m, sx + 26, 31, 4, t.bad);
282
+ const fw = Math.min(240, Math.max(0, sx - 14 - (cx + Math.min(230, (W - SW) * 0.26) + 30)));
283
+ if (fw > 90) {
284
+ const fx = sx - 14 - fw;
285
+ box(m, fx, 22, fw, 36, 11, t.dark ? t.surface2 : '#ffffff');
286
+ icon(ctx, 'search', fx + 20, 40, 15, t.text2, false, 2.2);
287
+ bar(m, fx + 36, 37.5, fw * 0.36, 5, t.bar3);
288
+ }
289
+ return { x: cx, y: 84, w: W - SW - 52, h: H - 84 - 24 };
290
+ }
291
+
292
+ /** Tablet shell: floating sidebar panel next to the content. Returns the content area. */
293
+ export function tabletShell(m: Mk, W: number, H: number, top: number, shell: Shell): Area {
294
+ const t = m.t;
295
+ const ctx = m.ctx;
296
+ const SW = Math.min(300, Math.max(210, W * 0.31));
297
+ const x = 10;
298
+ const y = top + 6;
299
+ const h = H - y - 26;
300
+ card(m, x, y, SW, h, 26, t.dark ? t.surface : '#ffffff', 0.8);
301
+ let cy = y + 22;
302
+ logoTile(m, x + 18, cy, 34, shell.glyph);
303
+ title(m, x + 64, cy + 7, 92, 18);
304
+ cy += 54;
305
+ box(m, x + 14, cy, SW - 28, 36, 11, t.surface2);
306
+ icon(ctx, 'search', x + 34, cy + 18, 16, t.text2, false, 2.2);
307
+ bar(m, x + 50, cy + 15.5, 70, 5, t.bar3);
308
+ cy = navList(m, x + 10, cy + 52, SW - 20, shell.nav, shell.selected, 46);
309
+ tagList(m, x + 10, cy + 24, Math.max(0, Math.min(4, Math.floor((y + h - cy - 70) / 32))));
310
+ return { x: x + SW + 18, y: top, w: W - SW - x - 18 - 6, h: H - top };
311
+ }
312
+
313
+ /** Shell for pages in a browser: slim top navigation instead of a sidebar. Returns the content area. */
314
+ export function webShell(m: Mk, W: number, H: number, shell: Shell): Area {
315
+ const t = m.t;
316
+ const ctx = m.ctx;
317
+ ctx.fillStyle = t.dark ? t.sidebar : '#ffffff';
318
+ ctx.fillRect(0, 0, W, 64);
319
+ ctx.fillStyle = t.line;
320
+ ctx.fillRect(0, 63, W, 1);
321
+ logoTile(m, 28, 17, 30, shell.glyph);
322
+ title(m, 68, 22, 84, 16);
323
+ const widths = [52, 64, 48, 58, 44];
324
+ let x = 190;
325
+ shell.nav.slice(0, 5).forEach((_, i) => {
326
+ const on = i === shell.selected;
327
+ if (x + widths[i] > W - 260) return;
328
+ if (on) box(m, x - 12, 18, widths[i] + 24, 28, 14, t.dark ? rgba(t.accent, 0.18) : rgba(t.accent, 0.1));
329
+ body(m, x, 25, widths[i], 14, on ? t.accent : t.bar2);
330
+ x += widths[i] + 40;
331
+ });
332
+ avatar(m, W - 46, 32, 34, m.seed + 9);
333
+ const bw = 118;
334
+ shadowed(m, 14, 5, t.dark ? 'rgba(0, 0, 0, 0.4)' : rgba(t.accent, 0.3), () => box(m, W - 76 - bw, 14, bw, 36, 18, t.accent));
335
+ bar(m, W - 76 - bw + 30, 29.5, bw - 60, 5, rgba(t.onAccent, 0.9));
336
+ return { x: 28, y: 92, w: W - 56, h: H - 92 - 24 };
337
+ }