autokap 1.0.7 → 1.0.8

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 (278) hide show
  1. package/assets/cursors/macos.svg +4 -0
  2. package/assets/cursors/windows.svg +15 -0
  3. package/assets/skill/OPCODE-REFERENCE.md +607 -0
  4. package/assets/skill/README.md +39 -0
  5. package/assets/skill/SKILL.md +453 -468
  6. package/assets/skill/STUDIO-SKILL.md +476 -0
  7. package/assets/skill/references/examples.md +104 -0
  8. package/assets/skill/references/interactive-demo.md +225 -0
  9. package/assets/skill/references/mock-data.md +178 -0
  10. package/dist/action-verifier.d.ts +29 -0
  11. package/dist/action-verifier.js +133 -0
  12. package/dist/agent-action-recovery.d.ts +45 -0
  13. package/dist/agent-action-recovery.js +370 -0
  14. package/dist/agent-message-utils.d.ts +21 -0
  15. package/dist/agent-message-utils.js +77 -0
  16. package/dist/agent-url-utils.d.ts +30 -0
  17. package/dist/agent-url-utils.js +138 -0
  18. package/dist/agent.d.ts +92 -8
  19. package/dist/agent.js +2936 -781
  20. package/dist/ak-tree.d.ts +39 -0
  21. package/dist/ak-tree.js +368 -0
  22. package/dist/alt-text.d.ts +26 -0
  23. package/dist/alt-text.js +55 -0
  24. package/dist/auth-capture.d.ts +17 -0
  25. package/dist/auth-capture.js +164 -0
  26. package/dist/benchmark.d.ts +59 -0
  27. package/dist/benchmark.js +135 -0
  28. package/dist/browser-bar.d.ts +14 -6
  29. package/dist/browser-bar.js +145 -8
  30. package/dist/browser-pool.d.ts +7 -0
  31. package/dist/browser-pool.js +15 -5
  32. package/dist/browser-utils.d.ts +31 -0
  33. package/dist/browser-utils.js +97 -0
  34. package/dist/browser.d.ts +51 -1
  35. package/dist/browser.js +1481 -31
  36. package/dist/capture-alt-text.js +2 -1
  37. package/dist/capture-language-preflight.js +14 -0
  38. package/dist/capture-llm-page-identity.js +22 -10
  39. package/dist/capture-page-identity.d.ts +5 -7
  40. package/dist/capture-page-identity.js +211 -78
  41. package/dist/capture-preset-credentials.d.ts +50 -0
  42. package/dist/capture-preset-credentials.js +127 -0
  43. package/dist/capture-request-plan.d.ts +2 -2
  44. package/dist/capture-request-plan.js +64 -16
  45. package/dist/capture-run-optimizer.js +48 -33
  46. package/dist/capture-selector-memory.d.ts +5 -0
  47. package/dist/capture-selector-memory.js +18 -0
  48. package/dist/capture-strategy.d.ts +36 -0
  49. package/dist/capture-strategy.js +95 -0
  50. package/dist/capture-studio-sync.d.ts +1 -0
  51. package/dist/capture-studio-sync.js +9 -3
  52. package/dist/capture-surface-contract.d.ts +36 -0
  53. package/dist/capture-surface-contract.js +299 -0
  54. package/dist/capture-transition-engine.d.ts +28 -0
  55. package/dist/capture-transition-engine.js +292 -0
  56. package/dist/capture-variant-state.d.ts +2 -0
  57. package/dist/capture-variant-state.js +26 -0
  58. package/dist/capture-verification.d.ts +35 -0
  59. package/dist/capture-verification.js +95 -0
  60. package/dist/capture-viewport-lock.d.ts +48 -0
  61. package/dist/capture-viewport-lock.js +74 -0
  62. package/dist/circuit-breaker.d.ts +42 -0
  63. package/dist/circuit-breaker.js +119 -0
  64. package/dist/cli-config.d.ts +8 -1
  65. package/dist/cli-config.js +62 -6
  66. package/dist/cli-contract.d.ts +15 -0
  67. package/dist/cli-contract.js +167 -0
  68. package/dist/cli-runner-local.d.ts +12 -0
  69. package/dist/cli-runner-local.js +102 -0
  70. package/dist/cli-runner.d.ts +34 -0
  71. package/dist/cli-runner.js +433 -0
  72. package/dist/cli-utils.d.ts +0 -1
  73. package/dist/cli-utils.js +2 -5
  74. package/dist/cli.js +1005 -267
  75. package/dist/clip-orchestrator.js +9 -2
  76. package/dist/clip-postprocess.js +25 -16
  77. package/dist/cookie-dismiss.d.ts +2 -0
  78. package/dist/cookie-dismiss.js +48 -13
  79. package/dist/cost-logging.d.ts +8 -0
  80. package/dist/cost-logging.js +160 -46
  81. package/dist/cost-resolution-monitor.d.ts +16 -0
  82. package/dist/cost-resolution-monitor.js +34 -0
  83. package/dist/credential-templates.js +2 -2
  84. package/dist/cursor-overlay-script.d.ts +6 -0
  85. package/dist/cursor-overlay-script.js +169 -0
  86. package/dist/dom-css-purger.d.ts +65 -0
  87. package/dist/dom-css-purger.js +333 -0
  88. package/dist/dom-font-inliner.d.ts +45 -0
  89. package/dist/dom-font-inliner.js +148 -0
  90. package/dist/dom-patch-resolver.d.ts +52 -0
  91. package/dist/dom-patch-resolver.js +242 -0
  92. package/dist/dom-serializer.d.ts +82 -0
  93. package/dist/dom-serializer.js +378 -0
  94. package/dist/element-capture.d.ts +1 -41
  95. package/dist/element-capture.js +202 -446
  96. package/dist/env-validation.d.ts +5 -0
  97. package/dist/env-validation.js +29 -0
  98. package/dist/execution-schema.d.ts +4423 -0
  99. package/dist/execution-schema.js +507 -0
  100. package/dist/execution-types.d.ts +886 -0
  101. package/dist/execution-types.js +65 -0
  102. package/dist/fonts-loader.d.ts +14 -0
  103. package/dist/fonts-loader.js +55 -0
  104. package/dist/hybrid-navigator.js +12 -12
  105. package/dist/index.d.ts +9 -6
  106. package/dist/index.js +10 -4
  107. package/dist/legacy/agent-action-recovery.d.ts +45 -0
  108. package/dist/legacy/agent-action-recovery.js +370 -0
  109. package/dist/legacy/agent-message-utils.d.ts +21 -0
  110. package/dist/legacy/agent-message-utils.js +77 -0
  111. package/dist/legacy/agent-url-utils.d.ts +30 -0
  112. package/dist/legacy/agent-url-utils.js +138 -0
  113. package/dist/legacy/agent.d.ts +226 -0
  114. package/dist/legacy/agent.js +6666 -0
  115. package/dist/legacy/clip-orchestrator.d.ts +148 -0
  116. package/dist/legacy/clip-orchestrator.js +957 -0
  117. package/dist/legacy/credential-templates.d.ts +5 -0
  118. package/dist/legacy/credential-templates.js +60 -0
  119. package/dist/legacy/hybrid-navigator.d.ts +138 -0
  120. package/dist/legacy/hybrid-navigator.js +468 -0
  121. package/dist/legacy/llm-usage.d.ts +17 -0
  122. package/dist/legacy/llm-usage.js +45 -0
  123. package/dist/legacy/prompt-cache.d.ts +10 -0
  124. package/dist/legacy/prompt-cache.js +24 -0
  125. package/dist/legacy/prompts.d.ts +175 -0
  126. package/dist/legacy/prompts.js +1038 -0
  127. package/dist/legacy/tools.d.ts +4 -0
  128. package/dist/legacy/tools.js +216 -0
  129. package/dist/legacy/video-agent.d.ts +143 -0
  130. package/dist/legacy/video-agent.js +4788 -0
  131. package/dist/legacy/video-observation.d.ts +36 -0
  132. package/dist/legacy/video-observation.js +192 -0
  133. package/dist/legacy/video-planner.d.ts +12 -0
  134. package/dist/legacy/video-planner.js +501 -0
  135. package/dist/legacy/video-prompts.d.ts +37 -0
  136. package/dist/legacy/video-prompts.js +569 -0
  137. package/dist/legacy/video-tools.d.ts +3 -0
  138. package/dist/legacy/video-tools.js +59 -0
  139. package/dist/legacy/video-variant-state.d.ts +29 -0
  140. package/dist/legacy/video-variant-state.js +80 -0
  141. package/dist/legacy/vision-model.d.ts +17 -0
  142. package/dist/legacy/vision-model.js +74 -0
  143. package/dist/llm-healer.d.ts +63 -0
  144. package/dist/llm-healer.js +166 -0
  145. package/dist/llm-provider.d.ts +29 -0
  146. package/dist/llm-provider.js +80 -0
  147. package/dist/logger.d.ts +6 -2
  148. package/dist/logger.js +15 -1
  149. package/dist/mockup-html.js +35 -25
  150. package/dist/mockup.d.ts +95 -2
  151. package/dist/mockup.js +427 -166
  152. package/dist/mouse-animation.d.ts +2 -2
  153. package/dist/mouse-animation.js +34 -20
  154. package/dist/opcode-actions.d.ts +42 -0
  155. package/dist/opcode-actions.js +511 -0
  156. package/dist/opcode-runner.d.ts +51 -0
  157. package/dist/opcode-runner.js +770 -0
  158. package/dist/openrouter-client.d.ts +40 -0
  159. package/dist/openrouter-client.js +16 -0
  160. package/dist/overlay-engine.d.ts +24 -0
  161. package/dist/overlay-engine.js +176 -0
  162. package/dist/postcondition.d.ts +16 -0
  163. package/dist/postcondition.js +269 -0
  164. package/dist/program-patcher.d.ts +25 -0
  165. package/dist/program-patcher.js +44 -0
  166. package/dist/prompts.d.ts +13 -5
  167. package/dist/prompts.js +224 -351
  168. package/dist/provider-config.d.ts +12 -0
  169. package/dist/provider-config.js +15 -0
  170. package/dist/recovery-chain.d.ts +37 -0
  171. package/dist/recovery-chain.js +350 -0
  172. package/dist/remote-browser.d.ts +28 -4
  173. package/dist/remote-browser.js +60 -5
  174. package/dist/safari-browser-bar.d.ts +15 -0
  175. package/dist/safari-browser-bar.js +95 -0
  176. package/dist/safari-toolbar-asset.d.ts +15 -0
  177. package/dist/safari-toolbar-asset.js +12 -0
  178. package/dist/security.d.ts +2 -1
  179. package/dist/security.js +49 -10
  180. package/dist/selector-resolver.d.ts +34 -0
  181. package/dist/selector-resolver.js +181 -0
  182. package/dist/semantic-resolver.d.ts +35 -0
  183. package/dist/semantic-resolver.js +161 -0
  184. package/dist/server-capture-runtime.d.ts +5 -3
  185. package/dist/server-capture-runtime.js +42 -95
  186. package/dist/server-credit-usage.d.ts +2 -2
  187. package/dist/server-project-webhooks.d.ts +15 -1
  188. package/dist/server-project-webhooks.js +34 -8
  189. package/dist/server-screenshot-watermark.js +27 -5
  190. package/dist/session-profile.js +164 -1
  191. package/dist/sf-pro-symbols.d.ts +1 -0
  192. package/dist/sf-pro-symbols.js +55 -0
  193. package/dist/skill-packaging.d.ts +28 -0
  194. package/dist/skill-packaging.js +169 -0
  195. package/dist/smart-wait.d.ts +27 -0
  196. package/dist/smart-wait.js +81 -0
  197. package/dist/status-bar-render.d.ts +20 -0
  198. package/dist/status-bar-render.js +410 -0
  199. package/dist/status-bar.d.ts +9 -0
  200. package/dist/status-bar.js +298 -14
  201. package/dist/svg-browser-bar.d.ts +33 -0
  202. package/dist/svg-browser-bar.js +206 -0
  203. package/dist/svg-status-bar.d.ts +36 -0
  204. package/dist/svg-status-bar.js +597 -0
  205. package/dist/svg-text.d.ts +61 -0
  206. package/dist/svg-text.js +118 -0
  207. package/dist/tools.js +89 -451
  208. package/dist/types.d.ts +240 -5
  209. package/dist/types.js +23 -1
  210. package/dist/v2/action-verifier.d.ts +29 -0
  211. package/dist/v2/action-verifier.js +133 -0
  212. package/dist/v2/alt-text.d.ts +26 -0
  213. package/dist/v2/alt-text.js +55 -0
  214. package/dist/v2/benchmark.d.ts +59 -0
  215. package/dist/v2/benchmark.js +135 -0
  216. package/dist/v2/capture-strategy.d.ts +30 -0
  217. package/dist/v2/capture-strategy.js +67 -0
  218. package/dist/v2/capture-verification.d.ts +35 -0
  219. package/dist/v2/capture-verification.js +95 -0
  220. package/dist/v2/circuit-breaker.d.ts +42 -0
  221. package/dist/v2/circuit-breaker.js +119 -0
  222. package/dist/v2/cli-runner-local.d.ts +11 -0
  223. package/dist/v2/cli-runner-local.js +91 -0
  224. package/dist/v2/cli-runner.d.ts +34 -0
  225. package/dist/v2/cli-runner.js +300 -0
  226. package/dist/v2/compiler-prompts.d.ts +27 -0
  227. package/dist/v2/compiler-prompts.js +123 -0
  228. package/dist/v2/compiler.d.ts +37 -0
  229. package/dist/v2/compiler.js +147 -0
  230. package/dist/v2/explorer.d.ts +41 -0
  231. package/dist/v2/explorer.js +56 -0
  232. package/dist/v2/index.d.ts +37 -0
  233. package/dist/v2/index.js +31 -0
  234. package/dist/v2/llm-healer.d.ts +62 -0
  235. package/dist/v2/llm-healer.js +166 -0
  236. package/dist/v2/llm-provider.d.ts +29 -0
  237. package/dist/v2/llm-provider.js +80 -0
  238. package/dist/v2/opcode-runner.d.ts +47 -0
  239. package/dist/v2/opcode-runner.js +634 -0
  240. package/dist/v2/overlay-engine.d.ts +24 -0
  241. package/dist/v2/overlay-engine.js +150 -0
  242. package/dist/v2/postcondition.d.ts +16 -0
  243. package/dist/v2/postcondition.js +249 -0
  244. package/dist/v2/program-patcher.d.ts +25 -0
  245. package/dist/v2/program-patcher.js +44 -0
  246. package/dist/v2/recovery-chain.d.ts +30 -0
  247. package/dist/v2/recovery-chain.js +368 -0
  248. package/dist/v2/schema.d.ts +2580 -0
  249. package/dist/v2/schema.js +295 -0
  250. package/dist/v2/selector-resolver.d.ts +34 -0
  251. package/dist/v2/selector-resolver.js +181 -0
  252. package/dist/v2/semantic-resolver.d.ts +35 -0
  253. package/dist/v2/semantic-resolver.js +161 -0
  254. package/dist/v2/smart-wait.d.ts +27 -0
  255. package/dist/v2/smart-wait.js +81 -0
  256. package/dist/v2/types.d.ts +444 -0
  257. package/dist/v2/types.js +19 -0
  258. package/dist/v2/web-playwright-local.d.ts +69 -0
  259. package/dist/v2/web-playwright-local.js +392 -0
  260. package/dist/version.d.ts +1 -0
  261. package/dist/version.js +5 -0
  262. package/dist/video-agent.js +18 -13
  263. package/dist/video-planner.js +2 -1
  264. package/dist/video-prompts.js +3 -3
  265. package/dist/web-playwright-local.d.ts +126 -0
  266. package/dist/web-playwright-local.js +819 -0
  267. package/dist/ws-auth.js +4 -1
  268. package/dist/ws-broadcast.d.ts +34 -0
  269. package/dist/ws-broadcast.js +85 -0
  270. package/dist/ws-connection-limits.d.ts +12 -0
  271. package/dist/ws-connection-limits.js +44 -0
  272. package/dist/ws-handler-utils.d.ts +32 -0
  273. package/dist/ws-handler-utils.js +139 -0
  274. package/dist/ws-handler.js +294 -164
  275. package/dist/ws-metrics-server.d.ts +9 -0
  276. package/dist/ws-metrics-server.js +31 -0
  277. package/dist/ws-server.js +41 -1
  278. package/package.json +51 -34
@@ -71,53 +71,62 @@ const DEFAULT_LAYOUTS = {
71
71
  // (macOS, Linux VPS, Docker) without requiring system font installation.
72
72
  // local() is tried first so the native SF Pro is used when available.
73
73
  import { SF_PRO_TEXT_REGULAR, SF_PRO_TEXT_SEMIBOLD, SF_PRO_DISPLAY_REGULAR, SF_PRO_DISPLAY_SEMIBOLD, } from './sf-pro-fonts.js';
74
- const SF_PRO_STYLE = `<style>
74
+ import { SF_PRO_SYMBOLS } from './sf-pro-symbols.js';
75
+ export const STATUS_BAR_FONT_CSS = `
75
76
  @font-face {
76
77
  font-family: 'SF Pro Text';
77
- src: local('SF Pro Text'), local('.SFNSText'), local('SFProText-Regular'),
78
+ src: local('SF Pro Text'), local('SF Pro'), local('.SFNSText'), local('SFProText-Regular'),
78
79
  url('${SF_PRO_TEXT_REGULAR}') format('woff2');
79
80
  font-weight: 400;
80
81
  font-style: normal;
81
82
  }
82
83
  @font-face {
83
84
  font-family: 'SF Pro Text';
84
- src: local('SF Pro Text Semibold'), local('.SFNSText-Semibold'), local('SFProText-Semibold'),
85
+ src: local('SF Pro Text Semibold'), local('SF Pro'), local('.SFNSText-Semibold'), local('SFProText-Semibold'),
85
86
  url('${SF_PRO_TEXT_SEMIBOLD}') format('woff2');
86
87
  font-weight: 600;
87
88
  font-style: normal;
88
89
  }
89
90
  @font-face {
90
91
  font-family: 'SF Pro Display';
91
- src: local('SF Pro Display'), local('.SFNSDisplay'), local('SFProDisplay-Regular'),
92
+ src: local('SF Pro Display'), local('SF Pro'), local('.SFNSDisplay'), local('SFProDisplay-Regular'),
92
93
  url('${SF_PRO_DISPLAY_REGULAR}') format('woff2');
93
94
  font-weight: 400;
94
95
  font-style: normal;
95
96
  }
96
97
  @font-face {
97
98
  font-family: 'SF Pro Display';
98
- src: local('SF Pro Display Semibold'), local('.SFNSDisplay-Semibold'), local('SFProDisplay-Semibold'),
99
+ src: local('SF Pro Display Semibold'), local('SF Pro'), local('.SFNSDisplay-Semibold'), local('SFProDisplay-Semibold'),
99
100
  url('${SF_PRO_DISPLAY_SEMIBOLD}') format('woff2');
100
101
  font-weight: 600;
101
102
  font-style: normal;
102
103
  }
103
- </style>`;
104
+ @font-face {
105
+ font-family: 'SF Pro Symbols';
106
+ src: url('${SF_PRO_SYMBOLS}') format('woff2');
107
+ font-weight: 400;
108
+ font-style: normal;
109
+ }
110
+ `;
111
+ const STATUS_BAR_FONT_STYLE_HTML = `<style>${STATUS_BAR_FONT_CSS}</style>`;
104
112
  // ── Main Generator ────────────────────────────────────────────────────
105
113
  export function generateStatusBarHtml(options) {
106
114
  const cfg = { ...DEFAULT_CONFIG, ...options.config };
107
115
  const layout = options.layout ?? DEFAULT_LAYOUTS[options.deviceType];
108
116
  const { width, height } = options;
109
117
  const color = cfg.colorScheme === 'dark' ? '#FFFFFF' : '#000000';
118
+ const fontStyles = options.includeFontStyles === false ? '' : STATUS_BAR_FONT_STYLE_HTML;
110
119
  const fontFamily = "'SF Pro Text','SF Pro Display','-apple-system',BlinkMacSystemFont,system-ui,sans-serif";
111
120
  if (options.deviceType === 'iphone-home-button') {
112
- return SF_PRO_STYLE + buildIPhoneHomeButtonStatusBar(cfg, layout, width, height, color, fontFamily);
121
+ return fontStyles + buildIPhoneHomeButtonStatusBar(cfg, layout, width, height, color, fontFamily);
113
122
  }
114
123
  if (options.deviceType === 'iphone-dynamic-island' || options.deviceType === 'iphone-notch') {
115
- return SF_PRO_STYLE + buildIPhoneStatusBar(cfg, layout, width, height, color, fontFamily);
124
+ return fontStyles + buildIPhoneStatusBar(cfg, layout, width, height, color, fontFamily);
116
125
  }
117
126
  if (options.deviceType === 'mac-notch') {
118
- return SF_PRO_STYLE + buildMacMenuBar(cfg, layout, width, height, color, fontFamily);
127
+ return fontStyles + buildMacMenuBar(cfg, layout, width, height, color, fontFamily);
119
128
  }
120
- return SF_PRO_STYLE + buildIPadStatusBar(cfg, layout, width, height, color, fontFamily);
129
+ return fontStyles + buildIPadStatusBar(cfg, layout, width, height, color, fontFamily);
121
130
  }
122
131
  // ── iPhone Status Bar ──────────────────────────────────────────────────
123
132
  function buildIPhoneStatusBar(cfg, layout, width, height, color, fontFamily) {
@@ -214,8 +223,9 @@ function buildMacMenuBar(cfg, layout, width, height, color, fontFamily) {
214
223
  const wifiSvg = renderWifi(cfg.wifiStrength, color, wifiW);
215
224
  const batterySvg = renderBattery(cfg.batteryLevel, cfg.batteryCharging, color, batteryW, cfg.colorScheme === 'dark');
216
225
  const batteryLabel = `<span style="font-size:${fontSize - 1}px;font-weight:400;color:${color};line-height:1;">${cfg.batteryLevel}%</span>`;
217
- // SF Symbol icon helper uses SF Pro font glyphs from Private Use Area
218
- const sfIcon = (char, size = fontSize) => `<span style="font-family:'SF Pro','SF Pro Display',system-ui;font-size:${size}px;color:${color};line-height:1;">${char}</span>`;
226
+ // Deterministic subset font for macOS menu bar glyphs SF Pro Symbols embeds
227
+ // the exact glyphs needed (Apple logo, search, CC, avatar) as a woff2 data URI.
228
+ const sfIcon = (char, size = fontSize) => `<span style="font-family:'SF Pro Symbols','SF Pro Text','SF Pro Display',system-ui;font-size:${size}px;color:${color};line-height:1;">${char}</span>`;
219
229
  // Apple logo — U+F8FF renders as in SF Pro / Apple system fonts
220
230
  const appleLogo = sfIcon('\uF8FF', fontSize + 3);
221
231
  // SF Symbol characters (macOS Sequoia) — literal glyphs from SF Pro font
@@ -231,7 +241,9 @@ function buildMacMenuBar(cfg, layout, width, height, color, fontFamily) {
231
241
  : '';
232
242
  const timeLabel = `<span style="font-size:${fontSize}px;font-weight:500;color:${color};line-height:1;">${escapeHtml(cfg.time)}</span>`;
233
243
  // macOS right-side order: WiFi → Battery% BatteryIcon → Search → CC → Avatar → Date Time
234
- return `<div style="position:relative;width:${width}px;height:${height}px;font-family:${fontFamily};-webkit-font-smoothing:antialiased;overflow:hidden;">
244
+ // macOS menu bar uses SF Pro Display (not Text) at 13px — matches native rendering
245
+ const macFontFamily = "'SF Pro Display','SF Pro Text','-apple-system',BlinkMacSystemFont,system-ui,sans-serif";
246
+ return `<div style="position:relative;width:${width}px;height:${height}px;font-family:${macFontFamily};-webkit-font-smoothing:antialiased;overflow:hidden;">
235
247
  <div style="position:absolute;left:${layout.leftInset}px;top:0;height:100%;display:flex;align-items:center;gap:${gap + 6}px;">
236
248
  ${appleLogo}
237
249
  ${appName}
@@ -299,7 +311,7 @@ function renderWifi(strength, color, size) {
299
311
  const h = size * (viewH / viewW);
300
312
  return `<svg width="${size}" height="${h}" viewBox="0 0 ${viewW} ${viewH}" fill="none" style="display:block;">${tiers.join('')}</svg>`;
301
313
  }
302
- function renderBattery(level, charging, color, size, isDark = false) {
314
+ export function renderBattery(level, charging, color, size, isDark = false) {
303
315
  if (charging) {
304
316
  // Charging battery icon (from assets/icons/battery_charging.svg)
305
317
  // ViewBox: 35×16 — paths: 1,2=border, 3=nub, 4,5=inner fill, 6=bolt
@@ -333,4 +345,276 @@ function renderBattery(level, charging, color, size, isDark = false) {
333
345
  function escapeHtml(str) {
334
346
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
335
347
  }
348
+ // ── Pure SVG Generator (for sharp compositing — no Playwright) ────────
349
+ // Produces a self-contained SVG string with embedded fonts that can be
350
+ // rasterized by @resvg/resvg-js. The client-side HTML generator above
351
+ // remains unchanged.
352
+ /** Font CSS for embedding inside SVG <defs><style>. Uses only url() — no local() — because resvg doesn't support local font lookup. */
353
+ const SVG_FONT_CSS = `
354
+ @font-face { font-family: 'SF Pro Text'; src: url('${SF_PRO_TEXT_REGULAR}') format('woff2'); font-weight: 400; font-style: normal; }
355
+ @font-face { font-family: 'SF Pro Text'; src: url('${SF_PRO_TEXT_SEMIBOLD}') format('woff2'); font-weight: 600; font-style: normal; }
356
+ @font-face { font-family: 'SF Pro Display'; src: url('${SF_PRO_DISPLAY_REGULAR}') format('woff2'); font-weight: 400; font-style: normal; }
357
+ @font-face { font-family: 'SF Pro Display'; src: url('${SF_PRO_DISPLAY_SEMIBOLD}') format('woff2'); font-weight: 600; font-style: normal; }
358
+ @font-face { font-family: 'SF Pro Symbols'; src: url('${SF_PRO_SYMBOLS}') format('woff2'); font-weight: 400; font-style: normal; }
359
+ `;
360
+ const SVG_FONT_DEFS = `<defs><style>${SVG_FONT_CSS}</style></defs>`;
361
+ /**
362
+ * Generate a self-contained SVG string for the status bar.
363
+ * Used by the sharp compositing pipeline (no Playwright).
364
+ * The returned SVG includes embedded SF Pro fonts via @font-face data URIs.
365
+ */
366
+ export function generateStatusBarSvg(options) {
367
+ const cfg = { ...DEFAULT_CONFIG, ...options.config };
368
+ const layout = options.layout ?? DEFAULT_LAYOUTS[options.deviceType];
369
+ const { width, height } = options;
370
+ const color = cfg.colorScheme === 'dark' ? '#FFFFFF' : '#000000';
371
+ const FF = "'SF Pro Text','SF Pro Display',system-ui,sans-serif";
372
+ let inner;
373
+ if (options.deviceType === 'iphone-home-button') {
374
+ inner = buildIPhoneHomeButtonSvg(cfg, layout, width, height, color, FF);
375
+ }
376
+ else if (options.deviceType === 'iphone-dynamic-island' || options.deviceType === 'iphone-notch') {
377
+ inner = buildIPhoneSvg(cfg, layout, width, height, color, FF);
378
+ }
379
+ else if (options.deviceType === 'mac-notch') {
380
+ inner = buildMacMenuBarSvg(cfg, layout, width, height, color, FF);
381
+ }
382
+ else {
383
+ inner = buildIPadSvg(cfg, layout, width, height, color, FF);
384
+ }
385
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${SVG_FONT_DEFS}${inner}</svg>`;
386
+ }
387
+ // ── SVG icon fragment helpers ──────────────────────────────────────────
388
+ // Return just <g> groups (no wrapping <svg>) for embedding in parent SVG.
389
+ function signalBarsSvgFragment(strength, color, size) {
390
+ const viewW = 20;
391
+ const viewH = 13;
392
+ const w = size;
393
+ const h = size * (viewH / viewW);
394
+ const scale = w / viewW;
395
+ const paths = SIGNAL_BAR_PATHS.map((d, i) => {
396
+ const opacity = i < strength ? 1 : 0.25;
397
+ return `<path d="${d}" fill="${color}" opacity="${opacity}"/>`;
398
+ }).join('');
399
+ return { svg: `<g transform="scale(${scale})">${paths}</g>`, width: w, height: h };
400
+ }
401
+ function wifiSvgFragment(strength, color, size) {
402
+ const viewW = 18;
403
+ const viewH = 13;
404
+ const w = size;
405
+ const h = size * (viewH / viewW);
406
+ const scale = w / viewW;
407
+ const tiers = WIFI_TIER_PATHS.map((d, i) => {
408
+ const tierLevel = 3 - i;
409
+ const opacity = strength >= tierLevel ? 1 : 0.25;
410
+ return `<path d="${d}" fill="${color}" opacity="${opacity}"/>`;
411
+ }).join('');
412
+ return { svg: `<g transform="scale(${scale})">${tiers}</g>`, width: w, height: h };
413
+ }
414
+ function batterySvgFragment(level, charging, color, size, isDark) {
415
+ // Reuse the existing renderBattery which returns a complete <svg>.
416
+ // Extract just the inner content and wrap in a <g> with correct scaling.
417
+ const viewW = 35;
418
+ const viewH = 16;
419
+ const w = size;
420
+ const h = size * (viewH / viewW);
421
+ const scale = w / viewW;
422
+ // Build battery paths inline (same logic as renderBattery)
423
+ let paths;
424
+ if (charging) {
425
+ const chargingGreen = isDark ? '#0DF07D' : '#32CB58';
426
+ const chargingBorder = '#ADADB1';
427
+ const boltColor = color;
428
+ paths = `<path d="M17.332 13.9694L15.8203 15.8561H24.668C25.668 15.8561 26.582 15.778 27.4102 15.6217C28.2383 15.4576 28.9531 15.0748 29.5547 14.4733C30.1484 13.8795 30.5234 13.1725 30.6797 12.3522C30.8438 11.524 30.9258 10.61 30.9258 9.60998V6.2467C30.9258 5.2467 30.8438 4.33655 30.6797 3.51624C30.5234 2.68811 30.1484 1.97717 29.5547 1.38342C28.9531 0.78186 28.2383 0.402954 27.4102 0.246704C26.582 0.0826416 25.668 0.000610352 24.668 0.000610352H19.6992C19.7695 0.18811 19.8086 0.37561 19.8164 0.56311C19.8242 0.742798 19.8047 0.93811 19.7578 1.14905C19.7188 1.35999 19.6523 1.60608 19.5586 1.88733H24.9961C25.5977 1.88733 26.1875 1.94592 26.7656 2.06311C27.3516 2.17249 27.8203 2.40295 28.1719 2.75452C28.5312 3.11389 28.7656 3.58264 28.875 4.16077C28.9844 4.73108 29.0391 5.31702 29.0391 5.91858V9.92639C29.0391 10.5358 28.9844 11.1295 28.875 11.7076C28.7656 12.278 28.5312 12.7428 28.1719 13.1022C27.8203 13.4537 27.3516 13.6881 26.7656 13.8053C26.1875 13.9147 25.5977 13.9694 24.9961 13.9694H17.332Z" fill="${chargingBorder}"/>
429
+ <path d="M13.5937 1.88672L15.1055 9.53674e-07L6.25781 1.72716e-06C5.25781 1.81458e-06 4.34375 0.0781269 3.51562 0.234377C2.6875 0.39844 1.97265 0.781252 1.37109 1.38281C0.777343 1.97656 0.402343 2.6836 0.246093 3.50391C0.0820302 4.33203 -9.27501e-07 5.2461 -8.40078e-07 6.2461L-5.46051e-07 9.60938C-4.58628e-07 10.6094 0.0820309 11.5195 0.246093 12.3398C0.402344 13.168 0.777344 13.8789 1.37109 14.4727C1.97266 15.0742 2.6875 15.4531 3.51562 15.6094C4.34375 15.7734 5.25781 15.8555 6.25781 15.8555L11.2266 15.8555C11.1562 15.668 11.1172 15.4805 11.1094 15.293C11.1016 15.1133 11.1211 14.918 11.168 14.707C11.207 14.4961 11.2734 14.25 11.3672 13.9688L5.92969 13.9688C5.32812 13.9688 4.73828 13.9102 4.16016 13.793C3.57422 13.6836 3.10547 13.4531 2.75391 13.1016C2.39453 12.7422 2.16016 12.2734 2.05078 11.6953C1.94141 11.125 1.88672 10.5391 1.88672 9.9375L1.88672 5.92969C1.88672 5.32031 1.94141 4.72656 2.05078 4.14844C2.16016 3.57813 2.39453 3.11328 2.75391 2.75391C3.10547 2.40235 3.57422 2.16797 4.16016 2.05078C4.73828 1.94141 5.32812 1.88672 5.92969 1.88672L13.5937 1.88672Z" fill="${chargingBorder}"/>
430
+ <path d="M32.5195 4.89905V10.9576C32.832 10.9342 33.1562 10.7975 33.4922 10.5475C33.8281 10.2897 34.1094 9.93811 34.3359 9.4928C34.5703 9.04749 34.6875 8.52405 34.6875 7.92249C34.6875 7.32874 34.5703 6.8092 34.3359 6.36389C34.1094 5.91858 33.8281 5.56702 33.4922 5.3092C33.1562 5.05139 32.832 4.91467 32.5195 4.89905Z" fill="${chargingBorder}"/>
431
+ <path d="M4.32422 12.5162C4.58203 12.5787 4.91016 12.61 5.30859 12.61H12.1758L12.7969 10.9225H11.1328C10.5469 10.9225 10.043 10.7194 9.62109 10.3131C9.20703 9.90686 9 9.40686 9 8.81311C9 8.29749 9.17578 7.81702 9.52734 7.3717L12.8086 3.2467H5.34375C4.92969 3.2467 4.59375 3.28186 4.33594 3.35217C4.07812 3.41467 3.86719 3.52795 3.70312 3.69202C3.54688 3.85608 3.43359 4.06702 3.36328 4.32483C3.29297 4.57483 3.25781 4.90686 3.25781 5.32092V10.5592C3.25781 10.9655 3.29297 11.2936 3.36328 11.5436C3.43359 11.7936 3.54688 12.0006 3.70312 12.1647C3.86719 12.3287 4.07422 12.4459 4.32422 12.5162Z" fill="${chargingGreen}"/>
432
+ <path d="M21.6914 8.48499L18.4102 12.61H25.6172C26.0234 12.61 26.3516 12.5787 26.6016 12.5162C26.8516 12.4459 27.0586 12.3287 27.2227 12.1647C27.5273 11.86 27.6797 11.3248 27.6797 10.5592V5.29749C27.6797 4.89124 27.6445 4.56311 27.5742 4.31311C27.5039 4.06311 27.3867 3.85608 27.2227 3.69202C27.0586 3.52795 26.8516 3.41467 26.6016 3.35217C26.3516 3.28186 26.0234 3.2467 25.6172 3.2467H19.043L18.4219 4.9342H20.0742C20.668 4.9342 21.1719 5.13733 21.5859 5.54358C22.0078 5.94983 22.2188 6.44983 22.2188 7.04358C22.2188 7.5592 22.043 8.03967 21.6914 8.48499Z" fill="${chargingGreen}"/>
433
+ <path d="M10.7812 8.34432C10.6484 8.52401 10.582 8.68026 10.582 8.81307C10.582 8.96151 10.6328 9.0826 10.7344 9.17635C10.8438 9.2701 10.9727 9.31698 11.1211 9.31698H15.0938L12.9727 15.0123C12.8867 15.2623 12.8945 15.4654 12.9961 15.6217C13.1055 15.7779 13.2578 15.8599 13.4531 15.8678C13.6484 15.8678 13.8242 15.7701 13.9805 15.5748L20.4023 7.50057C20.5352 7.3287 20.6016 7.17635 20.6016 7.04354C20.6016 6.8951 20.5469 6.77401 20.4375 6.68026C20.3359 6.58651 20.2109 6.53963 20.0625 6.53963H16.0898L18.2109 0.844323C18.3047 0.594323 18.2969 0.391198 18.1875 0.234948C18.0781 0.0786978 17.9258 0.000572791 17.7305 0.000572791C17.543 -0.00723971 17.3672 0.0865103 17.2031 0.281823L10.7812 8.34432Z" fill="${boltColor}"/>`;
434
+ }
435
+ else {
436
+ const batteryBorder = '#ADADB1';
437
+ paths = `<path d="M12.8057 3.25098H19.042L19.043 3.24707H25.6172C26.0234 3.24707 26.3516 3.28223 26.6016 3.35254C26.8515 3.41504 27.0586 3.52834 27.2227 3.69238C27.3866 3.85642 27.5039 4.06357 27.5742 4.31348C27.6445 4.56343 27.6797 4.89179 27.6797 5.29785V10.5596C27.6796 11.325 27.5273 11.8604 27.2227 12.165C27.0586 12.329 26.8515 12.4463 26.6016 12.5166C26.3516 12.5791 26.0234 12.6104 25.6172 12.6104H5.30859C4.9102 12.6104 4.58202 12.5791 4.32422 12.5166C4.07431 12.4463 3.86716 12.329 3.70312 12.165C3.54695 12.0011 3.4336 11.7938 3.36328 11.5439C3.29299 11.294 3.25783 10.9657 3.25781 10.5596V5.32129C3.25781 4.90741 3.29303 4.57516 3.36328 4.3252C3.43357 4.06748 3.54697 3.85642 3.70312 3.69238C3.86717 3.52834 4.07816 3.41504 4.33594 3.35254C4.59375 3.28223 4.92969 3.24707 5.34375 3.24707H12.8086L12.8057 3.25098Z" fill="${color}"/>
438
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M24.668 0.000976562C25.668 0.000976562 26.582 0.0830078 27.4102 0.24707C28.2383 0.403328 28.9531 0.782242 29.5547 1.38379C30.1483 1.9775 30.5235 2.68856 30.6797 3.5166C30.8437 4.33682 30.9258 5.24721 30.9258 6.24707V9.61035C30.9258 10.6102 30.8437 11.5245 30.6797 12.3525C30.5234 13.1727 30.1484 13.88 29.5547 14.4736C28.9532 15.0751 28.2382 15.458 27.4102 15.6221C26.582 15.7783 25.6679 15.8564 24.668 15.8564L10.5156 15.8574V15.8555H6.25781C5.25781 15.8555 4.34375 15.7734 3.51562 15.6094C2.6875 15.4531 1.97266 15.0742 1.37109 14.4727C0.777344 13.8789 0.402344 13.168 0.246094 12.3398C0.0820312 11.5195 9.34357e-08 10.6094 0 9.60938V6.24609C-6.73586e-08 5.24609 0.0820313 4.33203 0.246094 3.50391C0.402344 2.68359 0.777344 1.97656 1.37109 1.38281C1.97266 0.78125 2.6875 0.398438 3.51562 0.234375C4.34375 0.078125 5.25781 1.64241e-07 6.25781 0L24.668 0.000976562ZM5.92969 1.88672C5.32813 1.88672 4.73828 1.94141 4.16016 2.05078C3.57422 2.16797 3.10547 2.40234 2.75391 2.75391C2.39453 3.11328 2.16016 3.57813 2.05078 4.14844C1.94141 4.72656 1.88672 5.32031 1.88672 5.92969V9.9375C1.88672 10.5391 1.94141 11.125 2.05078 11.6953C2.16016 12.2734 2.39453 12.7422 2.75391 13.1016C3.10547 13.4531 3.57422 13.6836 4.16016 13.793C4.73828 13.9102 5.32813 13.9687 5.92969 13.9688L24.9961 13.9697C25.5976 13.9697 26.1875 13.915 26.7656 13.8057C27.3515 13.6885 27.8203 13.454 28.1719 13.1025C28.5312 12.7432 28.7656 12.2782 28.875 11.708C28.9844 11.13 29.039 10.536 29.0391 9.92676V5.91895C29.0391 5.3175 28.9843 4.73134 28.875 4.16113C28.7656 3.5831 28.5311 3.11423 28.1719 2.75488C27.8203 2.40333 27.3515 2.17286 26.7656 2.06348C26.1875 1.94629 25.5977 1.8877 24.9961 1.8877L13 1.88867V1.88672H5.92969Z" fill="${batteryBorder}"/>
439
+ <path d="M32.5195 4.89941C32.832 4.91504 33.1563 5.05176 33.4922 5.30957C33.828 5.56737 34.1094 5.91902 34.3359 6.36426C34.5702 6.80951 34.6875 7.32923 34.6875 7.92285C34.6875 8.52426 34.5703 9.04794 34.3359 9.49316C34.1094 9.93834 33.8281 10.2901 33.4922 10.5479C33.1563 10.7978 32.832 10.9346 32.5195 10.958V4.89941Z" fill="${batteryBorder}"/>`;
440
+ }
441
+ return { svg: `<g transform="scale(${scale})">${paths}</g>`, width: w, height: h };
442
+ }
443
+ // ── SVG layout builders (no HTML, no CSS flexbox) ─────────────────────
444
+ /** Approximate text width for SF Pro at a given font size.
445
+ * Average character width is ~0.55em for SF Pro (measured).
446
+ * This is a conservative estimate — used only for right-alignment. */
447
+ function estimateTextWidth(text, fontSize, weight = 400) {
448
+ const avgCharWidth = weight >= 600 ? 0.58 : 0.53;
449
+ return text.length * fontSize * avgCharWidth;
450
+ }
451
+ function svgText(text, x, y, fontSize, color, opts = {}) {
452
+ const { weight = 400, letterSpacing, anchor = 'start', fontFamily } = opts;
453
+ const ls = letterSpacing != null ? ` letter-spacing="${letterSpacing}"` : '';
454
+ const ff = fontFamily ? ` font-family="${fontFamily}"` : '';
455
+ return `<text x="${x}" y="${y}" font-size="${fontSize}" font-weight="${weight}" fill="${color}" dominant-baseline="central" text-anchor="${anchor}"${ls}${ff}>${escapeHtml(text)}</text>`;
456
+ }
457
+ // iPhone Dynamic Island / Notch — time left, icons right
458
+ function buildIPhoneSvg(cfg, layout, width, height, color, fontFamily) {
459
+ const cy = height / 2;
460
+ const gap = layout.iconGap;
461
+ const { signal: signalW, wifi: wifiW, battery: batteryW } = layout.iconSize;
462
+ // Left: time
463
+ const timeText = svgText(cfg.time, layout.leftInset, cy, layout.fontSize.time, color, { weight: 600, letterSpacing: -0.4, fontFamily });
464
+ // Right group: [signal] [networkType?] [wifi] [battery] — right-aligned from (width - rightInset)
465
+ const signal = signalBarsSvgFragment(cfg.signalStrength, color, signalW);
466
+ const wifi = wifiSvgFragment(cfg.wifiStrength, color, wifiW);
467
+ const battery = batterySvgFragment(cfg.batteryLevel, cfg.batteryCharging, color, batteryW, cfg.colorScheme === 'dark');
468
+ const networkLabelWidth = cfg.showNetworkType
469
+ ? estimateTextWidth(cfg.networkType, layout.fontSize.networkType, 600)
470
+ : 0;
471
+ // Compute total right group width
472
+ const items = [signal.width];
473
+ if (cfg.showNetworkType)
474
+ items.push(networkLabelWidth);
475
+ items.push(wifi.width, battery.width);
476
+ const totalRightWidth = items.reduce((a, b) => a + b, 0) + (items.length - 1) * gap;
477
+ let x = width - layout.rightInset - totalRightWidth;
478
+ let rightGroup = `<g transform="translate(${x},${cy - signal.height / 2})">${signal.svg}</g>`;
479
+ x += signal.width + gap;
480
+ if (cfg.showNetworkType) {
481
+ rightGroup += svgText(cfg.networkType, x, cy, layout.fontSize.networkType, color, { weight: 600, letterSpacing: 0.2, fontFamily });
482
+ x += networkLabelWidth + gap;
483
+ }
484
+ rightGroup += `<g transform="translate(${x},${cy - wifi.height / 2})">${wifi.svg}</g>`;
485
+ x += wifi.width + gap;
486
+ rightGroup += `<g transform="translate(${x},${cy - battery.height / 2})">${battery.svg}</g>`;
487
+ return `<g font-family="${fontFamily}">${timeText}${rightGroup}</g>`;
488
+ }
489
+ // iPhone Home Button — signal+carrier+wifi left, time center, battery right
490
+ function buildIPhoneHomeButtonSvg(cfg, layout, width, height, color, fontFamily) {
491
+ const cy = height / 2;
492
+ const gap = layout.iconGap;
493
+ const labelSize = layout.fontSize.networkType;
494
+ const { signal: signalW, wifi: wifiW, battery: batteryW } = layout.iconSize;
495
+ const signal = signalBarsSvgFragment(cfg.signalStrength, color, signalW);
496
+ const wifi = wifiSvgFragment(cfg.wifiStrength, color, wifiW);
497
+ const battery = batterySvgFragment(cfg.batteryLevel, cfg.batteryCharging, color, batteryW, cfg.colorScheme === 'dark');
498
+ // Left group: signal + carrier + networkBadge? + wifi
499
+ let x = layout.leftInset;
500
+ let leftGroup = `<g transform="translate(${x},${cy - signal.height / 2})">${signal.svg}</g>`;
501
+ x += signal.width + gap;
502
+ leftGroup += svgText(cfg.carrierName, x, cy, labelSize, color, { fontFamily });
503
+ x += Math.min(80, estimateTextWidth(cfg.carrierName, labelSize)) + gap;
504
+ if (cfg.showNetworkType) {
505
+ leftGroup += svgText(cfg.networkType, x, cy, labelSize - 1, color, { weight: 600, fontFamily });
506
+ x += estimateTextWidth(cfg.networkType, labelSize - 1, 600) + gap;
507
+ }
508
+ leftGroup += `<g transform="translate(${x},${cy - wifi.height / 2})">${wifi.svg}</g>`;
509
+ // Center: time
510
+ const centerText = svgText(cfg.time, width / 2, cy, layout.fontSize.time, color, { weight: 600, letterSpacing: -0.4, anchor: 'middle', fontFamily });
511
+ // Right group: batteryPct? + battery
512
+ const batteryPctWidth = cfg.showBatteryPercentage ? estimateTextWidth(`${cfg.batteryLevel}%`, labelSize) : 0;
513
+ const rightTotalWidth = (cfg.showBatteryPercentage ? batteryPctWidth + gap : 0) + battery.width;
514
+ let rx = width - layout.rightInset - rightTotalWidth;
515
+ let rightGroup = '';
516
+ if (cfg.showBatteryPercentage) {
517
+ rightGroup += svgText(`${cfg.batteryLevel}%`, rx, cy, labelSize, color, { fontFamily });
518
+ rx += batteryPctWidth + gap;
519
+ }
520
+ rightGroup += `<g transform="translate(${rx},${cy - battery.height / 2})">${battery.svg}</g>`;
521
+ return `<g font-family="${fontFamily}">${leftGroup}${centerText}${rightGroup}</g>`;
522
+ }
523
+ // iPad — time+date left, icons right
524
+ function buildIPadSvg(cfg, layout, width, height, color, fontFamily) {
525
+ const cy = height / 2;
526
+ const gap = layout.iconGap;
527
+ const { signal: signalW, wifi: wifiW, battery: batteryW } = layout.iconSize;
528
+ // Left: time + date
529
+ let leftGroup = svgText(cfg.time, layout.leftInset, cy, layout.fontSize.time, color, { letterSpacing: -0.4, fontFamily });
530
+ if (cfg.date) {
531
+ const timeWidth = estimateTextWidth(cfg.time, layout.fontSize.time);
532
+ leftGroup += svgText(cfg.date, layout.leftInset + timeWidth + 16, cy, layout.fontSize.time, color, { letterSpacing: -0.4, fontFamily });
533
+ }
534
+ // Right: signal + wifi + batteryLabel + battery
535
+ const signal = signalBarsSvgFragment(cfg.signalStrength, color, signalW);
536
+ const wifi = wifiSvgFragment(cfg.wifiStrength, color, wifiW);
537
+ const battery = batterySvgFragment(cfg.batteryLevel, cfg.batteryCharging, color, batteryW, cfg.colorScheme === 'dark');
538
+ const batteryLabelW = estimateTextWidth(`${cfg.batteryLevel}%`, layout.fontSize.networkType);
539
+ const rightItems = [signal.width, wifi.width, batteryLabelW, battery.width];
540
+ const totalRight = rightItems.reduce((a, b) => a + b, 0) + (rightItems.length - 1) * gap;
541
+ let x = width - layout.rightInset - totalRight;
542
+ let rightGroup = `<g transform="translate(${x},${cy - signal.height / 2})">${signal.svg}</g>`;
543
+ x += signal.width + gap;
544
+ rightGroup += `<g transform="translate(${x},${cy - wifi.height / 2})">${wifi.svg}</g>`;
545
+ x += wifi.width + gap;
546
+ rightGroup += svgText(`${cfg.batteryLevel}%`, x, cy, layout.fontSize.networkType, color, { fontFamily });
547
+ x += batteryLabelW + gap;
548
+ rightGroup += `<g transform="translate(${x},${cy - battery.height / 2})">${battery.svg}</g>`;
549
+ return `<g font-family="${fontFamily}">${leftGroup}${rightGroup}</g>`;
550
+ }
551
+ // macOS Menu Bar — apple+app+menus left, wifi+battery+icons+date+time right
552
+ function buildMacMenuBarSvg(cfg, layout, width, height, color, _fontFamily) {
553
+ const fontSize = layout.fontSize.time;
554
+ const cy = height / 2;
555
+ const gap = layout.iconGap;
556
+ const { wifi: wifiW, battery: batteryW } = layout.iconSize;
557
+ const macFF = "'SF Pro Display','SF Pro Text',system-ui,sans-serif";
558
+ const symbolsFF = "'SF Pro Symbols','SF Pro Text','SF Pro Display',system-ui,sans-serif";
559
+ // Left group: Apple logo + app name + menu items
560
+ let lx = layout.leftInset;
561
+ const bigGap = gap + 6;
562
+ const menuGap = gap + 10;
563
+ // Apple logo (U+F8FF)
564
+ let leftGroup = svgText('\uF8FF', lx, cy, fontSize + 3, color, { fontFamily: symbolsFF });
565
+ lx += estimateTextWidth('\uF8FF', fontSize + 3) + bigGap;
566
+ // App name (bold)
567
+ leftGroup += svgText(cfg.menuBarApp, lx, cy, fontSize, color, { weight: 700, fontFamily: macFF });
568
+ lx += estimateTextWidth(cfg.menuBarApp, fontSize, 700) + bigGap;
569
+ // Menu items
570
+ for (const item of cfg.menuBarItems) {
571
+ leftGroup += svgText(item, lx, cy, fontSize, color, { fontFamily: macFF });
572
+ lx += estimateTextWidth(item, fontSize) + menuGap;
573
+ }
574
+ // Right group: wifi + battery + search + CC + avatar + date + time
575
+ const wifi = wifiSvgFragment(cfg.wifiStrength, color, wifiW);
576
+ const battery = batterySvgFragment(cfg.batteryLevel, cfg.batteryCharging, color, batteryW, cfg.colorScheme === 'dark');
577
+ const batteryLabelW = cfg.showBatteryPercentage ? estimateTextWidth(`${cfg.batteryLevel}%`, fontSize - 1) : 0;
578
+ const smallGap = gap + 2;
579
+ // Symbol icons
580
+ const searchW = estimateTextWidth('􀊫', fontSize);
581
+ const ccW = estimateTextWidth('􀜊', fontSize);
582
+ const avatarW = estimateTextWidth('􀉭', fontSize);
583
+ const dateW = cfg.date ? estimateTextWidth(cfg.date, fontSize, 500) : 0;
584
+ const timeW = estimateTextWidth(cfg.time, fontSize, 500);
585
+ const rightItems = [
586
+ wifi.width, // wifi
587
+ (cfg.showBatteryPercentage ? batteryLabelW + 3 : 0) + battery.width, // battery group
588
+ searchW, ccW, avatarW, // symbol icons
589
+ ];
590
+ if (cfg.date)
591
+ rightItems.push(dateW);
592
+ rightItems.push(timeW);
593
+ const totalRight = rightItems.reduce((a, b) => a + b, 0) + (rightItems.length - 1) * smallGap;
594
+ let rx = width - layout.rightInset - totalRight;
595
+ // wifi
596
+ let rightGroup = `<g transform="translate(${rx},${cy - wifi.height / 2})">${wifi.svg}</g>`;
597
+ rx += wifi.width + smallGap;
598
+ // battery group: pct + icon
599
+ if (cfg.showBatteryPercentage) {
600
+ rightGroup += svgText(`${cfg.batteryLevel}%`, rx, cy, fontSize - 1, color, { fontFamily: macFF });
601
+ rx += batteryLabelW + 3;
602
+ }
603
+ rightGroup += `<g transform="translate(${rx},${cy - battery.height / 2})">${battery.svg}</g>`;
604
+ rx += battery.width + smallGap;
605
+ // search, CC, avatar
606
+ rightGroup += svgText('􀊫', rx, cy, fontSize, color, { fontFamily: symbolsFF });
607
+ rx += searchW + smallGap;
608
+ rightGroup += svgText('􀜊', rx, cy, fontSize, color, { fontFamily: symbolsFF });
609
+ rx += ccW + smallGap;
610
+ rightGroup += svgText('􀉭', rx, cy, fontSize, color, { fontFamily: symbolsFF });
611
+ rx += avatarW + smallGap;
612
+ // date + time
613
+ if (cfg.date) {
614
+ rightGroup += svgText(cfg.date, rx, cy, fontSize, color, { weight: 500, fontFamily: macFF });
615
+ rx += dateW + smallGap;
616
+ }
617
+ rightGroup += svgText(cfg.time, rx, cy, fontSize, color, { weight: 500, fontFamily: macFF });
618
+ return `<g font-family="${macFF}">${leftGroup}${rightGroup}</g>`;
619
+ }
336
620
  //# sourceMappingURL=status-bar.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Native SVG Chrome browser bar generator (no Playwright).
3
+ *
4
+ * Mirrors `generateBrowserBarHtml` in src/browser-bar.ts pixel for pixel.
5
+ * The original wraps an inline `<svg>` in a CSS-scaled `<div>` with HTML
6
+ * overlays for the page title, URL, and favicon. This module produces a
7
+ * single self-contained SVG at the final output pixel dimensions, with text
8
+ * rendered as native `<path>` via fontkit and an optional embedded favicon
9
+ * as `<image href="data:...">`.
10
+ */
11
+ import type { BrowserBarConfig } from './browser-bar.js';
12
+ export interface BrowserBarSvgOptions {
13
+ config: BrowserBarConfig;
14
+ /** Logical width (CSS pixels). */
15
+ width: number;
16
+ /** Logical height (CSS pixels). */
17
+ height: number;
18
+ /**
19
+ * Output pixel scale. The SVG's `viewBox` is set to the internal Chrome
20
+ * reference coordinate system; the SVG's `width`/`height` attributes are
21
+ * set to logical * pixelScale so librsvg rasterizes at output resolution.
22
+ */
23
+ pixelScale?: number;
24
+ /**
25
+ * Optional pre-fetched favicon, encoded as a `data:image/...;base64,...`
26
+ * URI. The caller is responsible for fetching the URL and converting it
27
+ * (mockup.ts handles this in the Sharp pipeline). When absent, the default
28
+ * grey globe icon is used.
29
+ */
30
+ tabIconDataUri?: string;
31
+ }
32
+ /** Generate a complete `<svg>` document for the Chrome browser bar. */
33
+ export declare function generateBrowserBarSvg(opts: BrowserBarSvgOptions): string;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Native SVG Chrome browser bar generator (no Playwright).
3
+ *
4
+ * Mirrors `generateBrowserBarHtml` in src/browser-bar.ts pixel for pixel.
5
+ * The original wraps an inline `<svg>` in a CSS-scaled `<div>` with HTML
6
+ * overlays for the page title, URL, and favicon. This module produces a
7
+ * single self-contained SVG at the final output pixel dimensions, with text
8
+ * rendered as native `<path>` via fontkit and an optional embedded favicon
9
+ * as `<image href="data:...">`.
10
+ */
11
+ import { layoutText, measureText, } from './svg-text.js';
12
+ // ── Reference geometry (matches src/browser-bar.ts) ────────────────────
13
+ const REF_W = 2160;
14
+ const REF_H = 129;
15
+ const TAB_OVERFLOW_MR = 51;
16
+ const ADDR_BAR_ML = 174;
17
+ const ADDR_BAR_MR = 126;
18
+ const PROFILE_MR = 102;
19
+ const EAR_R_MR = 16.5;
20
+ // Title text box (matches the HTML overlay div)
21
+ const TITLE_LEFT = 187;
22
+ const TITLE_TOP = 15;
23
+ const TITLE_WIDTH = 260;
24
+ const TITLE_HEIGHT = 30;
25
+ const TITLE_FONT_SIZE = 18;
26
+ // URL text box
27
+ const URL_TOP = 69;
28
+ const URL_HEIGHT = 51;
29
+ const URL_FONT_SIZE = 19.5;
30
+ const URL_LEFT_OFFSET = 58; // from ADDR_BAR_ML
31
+ const URL_RIGHT_INSET = 18; // matches the HTML right calculation
32
+ const FAVICON_LEFT = 152;
33
+ const FAVICON_TOP = 18;
34
+ const FAVICON_BOX = 24;
35
+ const FAVICON_INNER = 21;
36
+ const TITLE_FONT = 'text-regular';
37
+ const URL_FONT = 'text-regular';
38
+ /** Generate a complete `<svg>` document for the Chrome browser bar. */
39
+ export function generateBrowserBarSvg(opts) {
40
+ const { width, height, config } = opts;
41
+ const ps = opts.pixelScale ?? 1;
42
+ const isDark = config.colorScheme === 'dark';
43
+ const title = config.pageTitle || 'New Tab';
44
+ const rawUrl = config.url || 'example.com';
45
+ const url = rawUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
46
+ // Derived layout: scale + dynamic right-anchored elements at the reference
47
+ // viewBox (the final SVG width is set on the outer <svg> element so the
48
+ // browser/librsvg renders at the requested pixel size).
49
+ const s = height / REF_H;
50
+ const iw = Math.round(width / s); // internal width at reference scale
51
+ const tabOverflowX = iw - TAB_OVERFLOW_MR;
52
+ const addrBarW = iw - ADDR_BAR_ML - ADDR_BAR_MR;
53
+ const profileX = iw - PROFILE_MR;
54
+ const earRX = iw - EAR_R_MR;
55
+ const dx = iw - REF_W;
56
+ // Color tokens (mirror the HTML)
57
+ const tabStripBg = isDark ? '#1F2020' : '#DEDEDE';
58
+ const tabBg = isDark ? '#3C3C3C' : '#FFFFFF';
59
+ const toolbarBg = isDark ? '#3C3C3C' : '#FFFFFF';
60
+ const addrBarBg = isDark ? '#282828' : '#F1F1F1';
61
+ const addrIconBg = isDark ? '#3C3C3C' : '#FFFFFF';
62
+ const borderColor = isDark ? '#454746' : '#CFCFCF';
63
+ const textColor = isDark ? '#E3E3E3' : '#1D1D1D';
64
+ const textSec = isDark ? '#C7C7C7' : '#5F6368';
65
+ const iconActive = isDark ? '#C7C7C7' : '#5F6368';
66
+ const iconDis = isDark ? '#7B7B7B' : '#BBBFC4';
67
+ // ── Tab strip + traffic lights + tab shape ─────────────────────────
68
+ const tabStrip = `
69
+ <g>
70
+ <rect width="${iw}" height="60" fill="${tabStripBg}"/>
71
+ <circle cx="40.5" cy="30.75" r="9" fill="#EC6A5E"/>
72
+ <circle cx="40.5" cy="30.75" r="8.25" stroke="#1F1F1F" stroke-opacity="0.06" stroke-width="1.5"/>
73
+ <circle cx="70.5" cy="30.75" r="9" fill="#F4BF4F"/>
74
+ <circle cx="70.5" cy="30.75" r="8.25" stroke="#1F1F1F" stroke-opacity="0.06" stroke-width="1.5"/>
75
+ <circle cx="100.5" cy="30.75" r="9" fill="#61C554"/>
76
+ <circle cx="100.5" cy="30.75" r="8.25" stroke="#1F1F1F" stroke-opacity="0.06" stroke-width="1.5"/>
77
+ <path d="M120 60C129.941 60 138 51.9411 138 42V60H120Z" fill="${tabBg}"/>
78
+ <path d="M138 24C138 15.7157 144.716 9 153 9H471C479.284 9 486 15.7157 486 24V60H138V24Z" fill="${tabBg}"/>
79
+ <path d="M457.668 35.5689L456.431 34.3314L460.762 30.0001L456.431 25.6689L457.668 24.4314L461.999 28.7626L466.331 24.4314L467.568 25.6689L463.237 30.0001L467.568 34.3314L466.331 35.5689L461.999 31.2376L457.668 35.5689Z" fill="${textColor}"/>
80
+ <path d="M504 60C494.059 60 486 51.9411 486 42V60H504Z" fill="${tabBg}"/>
81
+ <path d="M513.327 31.1749H506.877V28.8249H513.327V22.3499H515.677V28.8249H522.152V31.1749H515.677V37.6249H513.327V31.1749Z" fill="${iconActive}"/>
82
+ <rect x="${tabOverflowX}" y="9" width="42" height="42" rx="15" fill="${tabBg}"/>
83
+ <path d="M${tabOverflowX + 21} 33.95L${tabOverflowX + 14.4} 27.35L${tabOverflowX + 16.07} 25.675L${tabOverflowX + 21} 30.625L${tabOverflowX + 25.92} 25.7L${tabOverflowX + 27.6} 27.375L${tabOverflowX + 21} 33.95Z" fill="${textColor}"/>
84
+ </g>`;
85
+ const bottomEars = `
86
+ <path d="M16.5 60C7.3873 60 0 67.3873 0 76.5V60H16.5Z" fill="${tabStripBg}"/>
87
+ <path d="M${earRX} 60C${earRX + 9.11} 60 ${iw} 67.3873 ${iw} 76.5V60H${earRX}Z" fill="${tabStripBg}"/>`;
88
+ // ── Toolbar (back, forward, reload, address bar pill, sliders icon) ─
89
+ const toolbar = `
90
+ <g clip-path="url(#bb_clip_toolbar)">
91
+ <path d="M0 76C0 67.1634 7.16344 60 16 60H${iw - 16}C${iw - 7.16} 60 ${iw} 67.1634 ${iw} 76V129H0V76Z" fill="${toolbarBg}"/>
92
+ <path d="M28.3125 95.625L34.5938 101.906L33 103.5L24 94.5L33 85.5L34.5938 87.0938L28.3125 93.375H42V95.625H28.3125Z" fill="${iconDis}"/>
93
+ <path d="M91.6875 95.625L85.4062 101.906L87 103.5L96 94.5L87 85.5L85.4062 87.0938L91.6875 93.375H78V95.625H91.6875Z" fill="${iconDis}"/>
94
+ <path d="M141 103.5C138.5 103.5 136.375 102.625 134.625 100.875C132.875 99.125 132 97 132 94.5C132 92 132.875 89.875 134.625 88.125C136.375 86.375 138.5 85.5 141 85.5C142.354 85.5 143.609 85.7812 144.766 86.3438C145.922 86.9062 146.917 87.6562 147.75 88.5938V85.5H150V93H142.5V90.75H146.594C145.99 89.8333 145.198 89.1042 144.219 88.5625C143.24 88.0208 142.167 87.75 141 87.75C139.125 87.75 137.531 88.4062 136.219 89.7188C134.906 91.0312 134.25 92.625 134.25 94.5C134.25 96.375 134.906 97.9688 136.219 99.2812C137.531 100.594 139.125 101.25 141 101.25C142.75 101.25 144.25 100.672 145.5 99.5156C146.75 98.3594 147.469 96.9375 147.656 95.25H149.969C149.781 97.5833 148.833 99.5417 147.125 101.125C145.417 102.708 143.375 103.5 141 103.5Z" fill="${iconActive}"/>
95
+ <rect x="${ADDR_BAR_ML}" y="69" width="${addrBarW}" height="51" rx="25.5" fill="${addrBarBg}"/>
96
+ <rect x="${ADDR_BAR_ML + 7.5}" y="76.5" width="36" height="36" rx="18" fill="${addrIconBg}"/>
97
+ <g transform="translate(${ADDR_BAR_ML + 16.5},${76.5 + 9}) scale(1.5)">
98
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 6.70898C10.8806 6.70898 11.9998 7.82848 12 9.20898C12 10.5897 10.8807 11.709 9.5 11.709C8.11929 11.709 7 10.5897 7 9.20898C7.00024 7.82848 8.11944 6.70898 9.5 6.70898ZM9.5 8.20898C8.94786 8.20898 8.50024 8.6569 8.5 9.20898C8.5 9.76127 8.94772 10.209 9.5 10.209C10.0523 10.209 10.5 9.76127 10.5 9.20898C10.4998 8.6569 10.0521 8.20898 9.5 8.20898Z" fill="${iconActive}"/>
99
+ <path d="M6 10H0V8.5H6V10Z" fill="${iconActive}"/>
100
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 0C3.88071 0 5 1.11929 5 2.5C5 3.88071 3.88071 5 2.5 5C1.11929 5 0 3.88071 0 2.5C0 1.11929 1.11929 0 2.5 0ZM2.5 1.5C1.94772 1.5 1.5 1.94772 1.5 2.5C1.5 3.05228 1.94772 3.5 2.5 3.5C3.05228 3.5 3.5 3.05228 3.5 2.5C3.5 1.94772 3.05228 1.5 2.5 1.5Z" fill="${iconActive}"/>
101
+ <path d="M12 3.20898H6V1.70898H12V3.20898Z" fill="${iconActive}"/>
102
+ </g>
103
+ <path transform="translate(${dx},0)" d="M1994 100.344L1998 97.9688L2002.03 100.344L2000.97 95.8438L2004.44 92.875L1999.84 92.4688L1998 88.1875L1996.16 92.4688L1991.56 92.875L1995.06 95.8438L1994 100.344ZM1990.59 105L1992.56 96.6875L1986 91.0938L1994.62 90.3438L1998 82.5L2001.38 90.375L2010 91.0938L2003.44 96.6875L2005.41 105L1998 100.594L1990.59 105Z" fill="${iconActive}"/>
104
+ <rect x="${profileX}" y="79.5" width="30" height="30" rx="15" fill="url(#bb_profile_grad)"/>
105
+ <rect x="${profileX + 0.75}" y="80.25" width="28.5" height="28.5" rx="14.25" stroke="#1F1F1F" stroke-opacity="0.06" stroke-width="1.5"/>
106
+ <path transform="translate(${dx},0)" d="M2127 105.404C2126.38 105.404 2125.85 105.183 2125.41 104.743C2124.97 104.302 2124.75 103.773 2124.75 103.154C2124.75 102.535 2124.97 102.005 2125.41 101.565C2125.85 101.124 2126.38 100.904 2127 100.904C2127.62 100.904 2128.15 101.124 2128.59 101.565C2129.03 102.005 2129.25 102.535 2129.25 103.154C2129.25 103.773 2129.03 104.302 2128.59 104.743C2128.15 105.183 2127.62 105.404 2127 105.404ZM2127 96.7499C2126.38 96.7499 2125.85 96.5296 2125.41 96.089C2124.97 95.6484 2124.75 95.1187 2124.75 94.5C2124.75 93.8812 2124.97 93.3516 2125.41 92.9109C2125.85 92.4703 2126.38 92.25 2127 92.25C2127.62 92.25 2128.15 92.4703 2128.59 92.9109C2129.03 93.3516 2129.25 93.8812 2129.25 94.5C2129.25 95.1187 2129.03 95.6484 2128.59 96.089C2128.15 96.5296 2127.62 96.7499 2127 96.7499ZM2127 88.0961C2126.38 88.0961 2125.85 87.8758 2125.41 87.4351C2124.97 86.9945 2124.75 86.4649 2124.75 85.8461C2124.75 85.2274 2124.97 84.6977 2125.41 84.2571C2125.85 83.8165 2126.38 83.5962 2127 83.5962C2127.62 83.5962 2128.15 83.8165 2128.59 84.2571C2129.03 84.6977 2129.25 85.2274 2129.25 85.8461C2129.25 86.4649 2129.03 86.9945 2128.59 87.4351C2128.15 87.8758 2127.62 88.0961 2127 88.0961Z" fill="${iconActive}"/>
107
+ </g>`;
108
+ const toolbarBorder = `<path d="M0 60H${iw}H0M${iw} 130.5H0V127.5H${iw}V130.5ZM${iw} 127.5M0 129V60V129M0 129M${iw} 60V129V60" fill="${borderColor}" mask="url(#bb_mask_toolbar)"/>`;
109
+ // ── Title text (centered vertically in TITLE_HEIGHT box) ────────────
110
+ const titleEllipsized = ellipsizeText(title, TITLE_FONT, TITLE_FONT_SIZE, TITLE_WIDTH);
111
+ const titleMeasure = measureText({ text: titleEllipsized, fontId: TITLE_FONT, fontSize: TITLE_FONT_SIZE });
112
+ const titleBaselineY = TITLE_TOP + (TITLE_HEIGHT + TITLE_FONT_SIZE * 0.7) / 2;
113
+ const titleLayout = layoutText({
114
+ text: titleEllipsized,
115
+ fontId: TITLE_FONT,
116
+ fontSize: TITLE_FONT_SIZE,
117
+ x: TITLE_LEFT,
118
+ baselineY: titleBaselineY,
119
+ });
120
+ void titleMeasure;
121
+ const titleSvg = `<path d="${titleLayout.pathData}" fill="${textColor}"/>`;
122
+ // ── URL text (vertically centered in URL_HEIGHT box) ────────────────
123
+ const urlLeft = ADDR_BAR_ML + URL_LEFT_OFFSET;
124
+ const urlBoxWidth = ADDR_BAR_ML + addrBarW - URL_RIGHT_INSET - urlLeft;
125
+ const urlEllipsized = ellipsizeText(url, URL_FONT, URL_FONT_SIZE, urlBoxWidth);
126
+ const urlBaselineY = URL_TOP + (URL_HEIGHT + URL_FONT_SIZE * 0.7) / 2;
127
+ const urlLayout = layoutText({
128
+ text: urlEllipsized,
129
+ fontId: URL_FONT,
130
+ fontSize: URL_FONT_SIZE,
131
+ x: urlLeft,
132
+ baselineY: urlBaselineY,
133
+ });
134
+ const urlSvg = `<path d="${urlLayout.pathData}" fill="${textSec}"/>`;
135
+ // ── Favicon (embedded image or default globe) ───────────────────────
136
+ let faviconSvg;
137
+ const innerOffset = (FAVICON_BOX - FAVICON_INNER) / 2;
138
+ if (opts.tabIconDataUri) {
139
+ // <image> with data URI — librsvg supports this. The original HTML uses
140
+ // border-radius:4px + object-fit:contain on a 21×21 img. SVG <image>
141
+ // honors preserveAspectRatio (default xMidYMid meet ≈ contain) and we
142
+ // clip to a rounded rect via clipPath.
143
+ faviconSvg = `<g clip-path="url(#bb_clip_favicon)"><image href="${escapeAttr(opts.tabIconDataUri)}" x="${FAVICON_LEFT + innerOffset}" y="${FAVICON_TOP + innerOffset}" width="${FAVICON_INNER}" height="${FAVICON_INNER}" preserveAspectRatio="xMidYMid meet"/></g>`;
144
+ }
145
+ else {
146
+ // Default grey globe (matches the HTML fallback exactly)
147
+ const cx = FAVICON_LEFT + FAVICON_BOX / 2;
148
+ const cy = FAVICON_TOP + FAVICON_BOX / 2;
149
+ faviconSvg = `<g transform="translate(${cx - 10.5},${cy - 10.5}) scale(${21 / 16})"><circle cx="8" cy="8" r="6.5" fill="none" stroke="${iconActive}" stroke-width="1.2"/><ellipse cx="8" cy="8" rx="3" ry="6.5" fill="none" stroke="${iconActive}" stroke-width="1.2"/><line x1="1.5" y1="8" x2="14.5" y2="8" stroke="${iconActive}" stroke-width="1.2"/></g>`;
150
+ }
151
+ // ── <defs> ──────────────────────────────────────────────────────────
152
+ const defs = `
153
+ <defs>
154
+ <linearGradient id="bb_profile_grad" x1="${profileX + 15}" y1="109.5" x2="${profileX + 15}" y2="79.5" gradientUnits="userSpaceOnUse">
155
+ <stop stop-color="#D5D8E4"/>
156
+ <stop offset="0.45" stop-color="#D1CAD6"/>
157
+ <stop offset="1" stop-color="#B7BAD1"/>
158
+ </linearGradient>
159
+ <mask id="bb_mask_toolbar" fill="white">
160
+ <path d="M0 76C0 67.1634 7.16344 60 16 60H${iw - 16}C${iw - 7.16} 60 ${iw} 67.1634 ${iw} 76V129H0V76Z"/>
161
+ </mask>
162
+ <clipPath id="bb_clip_toolbar">
163
+ <path d="M0 76C0 67.1634 7.16344 60 16 60H${iw - 16}C${iw - 7.16} 60 ${iw} 67.1634 ${iw} 76V129H0V76Z"/>
164
+ </clipPath>
165
+ <clipPath id="bb_clip_favicon">
166
+ <rect x="${FAVICON_LEFT + innerOffset}" y="${FAVICON_TOP + innerOffset}" width="${FAVICON_INNER}" height="${FAVICON_INNER}" rx="4" ry="4"/>
167
+ </clipPath>
168
+ </defs>`;
169
+ const outW = Math.round(width * ps);
170
+ const outH = Math.round(height * ps);
171
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${outW}" height="${outH}" viewBox="0 0 ${iw} ${REF_H}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
172
+ ${tabStrip}
173
+ ${bottomEars}
174
+ ${toolbar}
175
+ ${toolbarBorder}
176
+ ${faviconSvg}
177
+ ${titleSvg}
178
+ ${urlSvg}
179
+ ${defs}
180
+ </svg>`;
181
+ }
182
+ // ── Helpers ────────────────────────────────────────────────────────────
183
+ function ellipsizeText(text, fontId, fontSize, maxW) {
184
+ if (maxW <= 0)
185
+ return '';
186
+ const measured = measureText({ text, fontId, fontSize });
187
+ if (measured.width <= maxW)
188
+ return text;
189
+ let lo = 0;
190
+ let hi = text.length;
191
+ while (lo < hi) {
192
+ const mid = (lo + hi + 1) >> 1;
193
+ const candidate = `${text.slice(0, mid)}…`;
194
+ if (measureText({ text: candidate, fontId, fontSize }).width <= maxW) {
195
+ lo = mid;
196
+ }
197
+ else {
198
+ hi = mid - 1;
199
+ }
200
+ }
201
+ return lo === 0 ? '…' : `${text.slice(0, lo)}…`;
202
+ }
203
+ function escapeAttr(s) {
204
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
205
+ }
206
+ //# sourceMappingURL=svg-browser-bar.js.map