@zseven-w/openpencil 0.7.2 → 0.7.4

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.
@@ -2156,7 +2156,7 @@ This context will guide the subtask decomposition and style guide selection.`
2156
2156
  "budget": 500,
2157
2157
  "category": "base"
2158
2158
  },
2159
- "content": 'OVERFLOW PREVENTION (CRITICAL):\n\n- Text in vertical layout: width="fill_container" + textGrowth="fixed-width". In horizontal: width="fit_content".\n- NEVER set fixed pixel width on text inside layout frames (e.g. width:378 in 195px card - overflows!).\n- Fixed-width children must be <= parent content area (parent width - padding).\n- Badges: short labels only (CJK <=8 chars / Latin <=16 chars).'
2159
+ "content": 'OVERFLOW PREVENTION (CRITICAL):\n\n- Text in vertical layout: width="fill_container" + textGrowth="fixed-width". In horizontal: width="fit_content".\n- NEVER set fixed pixel width on text inside layout frames (e.g. width:378 in 195px card - overflows!).\n- Fixed-width children must be <= parent content area (parent width - padding).\n- Badges: short labels only (CJK <=8 chars / Latin <=16 chars).\n\n## HORIZONTAL SCROLL ROWS (cards / chips / categories)\n\nWhen the spec says "horizontal scrolling cards", "swipeable row", "chip row", or similar, generate EXACTLY this structure \u2014 do NOT just emit 6 cards inside a horizontal layout, the children will spill outside the page frame.\n\nStructure:\n\n- A wrapper frame with `width="fill_container"`, `height="fit_content"`, `layout="vertical"`, `clipContent=true`.\n- Inside it, a row frame with `width="fit_content"`, `height="fit_content"`, `layout="horizontal"`, `gap=12`, `padding=[0,20]`.\n- The row frame holds the actual cards.\n\nEvery card in the row MUST:\n\n- Have a FIXED numeric `width` (typically 120-160 for mobile, 200-260 for desktop). Never `fill_container`, never `fit_content` - fixed pixels.\n- Share identical width with its siblings for visual rhythm.\n\nExample - 6 workout cards inside a 375px-wide mobile page:\n\n```json\n{\n "id": "cards-scroll",\n "type": "frame",\n "name": "Workouts Scroll",\n "width": "fill_container",\n "height": "fit_content",\n "layout": "vertical",\n "clipContent": true,\n "children": [\n {\n "id": "cards-row",\n "type": "frame",\n "name": "Workouts Row",\n "width": "fit_content",\n "height": "fit_content",\n "layout": "horizontal",\n "gap": 12,\n "padding": [0, 20],\n "children": [\n {\n "id": "card-hiit",\n "type": "frame",\n "width": 140,\n "height": 160,\n "cornerRadius": 20,\n "layout": "vertical",\n "gap": 8,\n "padding": 16,\n "fill": [{ "type": "solid", "color": "#1a1a1a" }],\n "children": []\n },\n {\n "id": "card-strength",\n "type": "frame",\n "width": 140,\n "height": 160,\n "cornerRadius": 20,\n "layout": "vertical",\n "gap": 8,\n "padding": 16,\n "fill": [{ "type": "solid", "color": "#1a1a1a" }],\n "children": []\n }\n ]\n }\n ]\n}\n```\n\nAnti-patterns (do NOT emit any of these):\n\n- Putting 5+ cards directly inside a `layout="horizontal"` page-root frame (they overflow the phone width).\n- Using `fill_container` on cards in a horizontal row (they squish down to invisibility).\n- Using `width="fit_content"` on cards - text-driven widths are unpredictable and break rhythm.\n- Skipping the `clipContent=true` wrapper and relying on Skia to clip (it doesn\'t \u2014 only `clipContent:true` enables clipping).'
2160
2160
  },
2161
2161
  {
2162
2162
  "meta": {
@@ -2339,14 +2339,19 @@ This context will guide the subtask decomposition and style guide selection.`
2339
2339
  "name": "incremental-add",
2340
2340
  "description": "Rules for adding new elements to existing designs",
2341
2341
  "phase": [
2342
- "maintenance"
2342
+ "maintenance",
2343
+ "generation"
2343
2344
  ],
2344
2345
  "trigger": {
2345
2346
  "keywords": [
2346
2347
  "add",
2347
2348
  "insert",
2348
2349
  "new section",
2349
- "append"
2350
+ "append",
2351
+ "continue",
2352
+ "\u7EE7\u7EED",
2353
+ "\u518D\u52A0",
2354
+ "\u8FFD\u52A0"
2350
2355
  ]
2351
2356
  },
2352
2357
  "priority": 20,
@@ -2668,7 +2673,7 @@ Your output must remain valid JSON/JSONL only.`
2668
2673
  "budget": 2e3,
2669
2674
  "category": "knowledge"
2670
2675
  },
2671
- "content": 'SEMANTIC ROLES (add "role" to nodes \u2014 system fills unset props based on role):\n\nLayout roles:\n\n- section: layout=vertical, width=fill_container, height=fit_content, gap=24, padding=[60,80] (mobile: [40,16]), alignItems=center\n- row: layout=horizontal, width=fill_container, gap=16, alignItems=center\n- column: layout=vertical, width=fill_container, gap=16\n- centered-content: layout=vertical, width=1080 (mobile: fill_container), gap=24, alignItems=center\n- form-group: layout=vertical, width=fill_container, gap=16\n- divider: width=fill_container, height=1, layout=none (vertical divider: width=1, height=fill_container)\n- spacer: width=fill_container, height=40\n\nNavigation roles:\n\n- navbar: layout=horizontal, width=fill_container, height=72 (mobile: 56), padding=[0,80] (mobile: [0,16]), alignItems=center, justifyContent=space_between\n- nav-links: layout=horizontal, gap=24, alignItems=center\n- nav-link: textGrowth=auto, lineHeight=1.2\n\nInteractive roles:\n\n- button: padding=[12,24], height=44, layout=horizontal, gap=8, alignItems=center, justifyContent=center, cornerRadius=8. In navbar: padding=[8,16], height=36. In form-group: width=fill_container, height=48, cornerRadius=10\n- icon-button: width=44, height=44, layout=horizontal, justifyContent=center, alignItems=center, cornerRadius=8\n- badge: layout=horizontal, padding=[6,12], gap=4, alignItems=center, justifyContent=center, cornerRadius=999\n- tag: layout=horizontal, padding=[4,10], gap=4, alignItems=center, justifyContent=center, cornerRadius=6\n- pill: layout=horizontal, padding=[6,14], gap=6, alignItems=center, justifyContent=center, cornerRadius=999\n- input: height=48, layout=horizontal, padding=[12,16], alignItems=center, cornerRadius=8. In vertical layout: width=fill_container\n- form-input: width=fill_container, height=48, layout=horizontal, padding=[12,16], alignItems=center, cornerRadius=8\n- search-bar: layout=horizontal, height=44, padding=[10,16], gap=8, alignItems=center, cornerRadius=22\n\nDisplay roles:\n\n- card: layout=vertical, gap=12, cornerRadius=12, clipContent=true. In horizontal layout: width=fill_container, height=fill_container\n- stat-card: layout=vertical, gap=8, padding=[24,24], cornerRadius=12. In horizontal layout: width=fill_container, height=fill_container\n- pricing-card: layout=vertical, gap=16, padding=[32,24], cornerRadius=16, clipContent=true. In horizontal layout: width=fill_container, height=fill_container\n- image-card: layout=vertical, gap=0, cornerRadius=12, clipContent=true\n- feature-card: layout=vertical, gap=12, padding=[24,24], cornerRadius=12. In horizontal layout: width=fill_container, height=fill_container\n\nMedia roles:\n\n- phone-mockup: width=280, height=560, cornerRadius=32, layout=none\n- screenshot-frame: cornerRadius=12, clipContent=true\n- avatar: width/height=48, cornerRadius=24, clipContent=true (size adapts to explicit width)\n- icon: width=24, height=24\n\nTypography roles:\n\n- heading: lineHeight=1.2 (CJK: 1.35), letterSpacing=-0.5 (CJK: 0). In vertical layout: textGrowth=fixed-width, width=fill_container\n- subheading: lineHeight=1.3 (CJK: 1.4), textGrowth=fixed-width, width=fill_container\n- body-text: lineHeight=1.5 (CJK: 1.6), textGrowth=fixed-width, width=fill_container\n- caption: lineHeight=1.3 (CJK: 1.4), textGrowth=auto\n- label: lineHeight=1.2, textGrowth=auto, textAlignVertical=middle\n\nContent roles:\n\n- hero: layout=vertical, width=fill_container, height=fit_content, padding=[80,80] (mobile: [40,16]), gap=24, alignItems=center\n- feature-grid: layout=horizontal, width=fill_container, gap=24, alignItems=start\n- testimonial: layout=vertical, gap=16, padding=[24,24], cornerRadius=12\n- cta-section: layout=vertical, width=fill_container, height=fit_content, padding=[60,80] (mobile: [40,16]), gap=20, alignItems=center\n- footer: layout=vertical, width=fill_container, height=fit_content, padding=[48,80] (mobile: [32,16]), gap=24\n- stats-section: layout=horizontal, width=fill_container, height=fit_content, padding=[48,80] (mobile: [32,16]), gap=32, justifyContent=center, alignItems=center\n\nTable roles:\n\n- table: layout=vertical, width=fill_container, gap=0, clipContent=true\n- table-row: layout=horizontal, width=fill_container, alignItems=center, padding=[12,16]\n- table-header: layout=horizontal, width=fill_container, alignItems=center, padding=[12,16]\n- table-cell: width=fill_container\n\nYour explicit props ALWAYS override role defaults. Only unset properties get filled in.'
2676
+ "content": "SEMANTIC ROLES (add \"role\" to nodes \u2014 system fills unset props based on role):\n\nLayout roles:\n\n- section: layout=vertical, width=fill_container, height=fit_content, gap=24, padding=[60,80] (mobile: [40,16]), alignItems=center\n- row: layout=horizontal, width=fill_container, gap=16, alignItems=center\n- column: layout=vertical, width=fill_container, gap=16\n- centered-content: layout=vertical, width=1080 (mobile: fill_container), gap=24, alignItems=center\n- form-group: layout=vertical, width=fill_container, gap=16\n- divider: width=fill_container, height=1, layout=none (vertical divider: width=1, height=fill_container)\n- spacer: width=fill_container, height=40\n\nNavigation roles:\n\n- navbar: layout=horizontal, width=fill_container, height=72 (mobile: 56), padding=[0,80] (mobile: [0,16]), alignItems=center, justifyContent=space_between\n- nav-links: layout=horizontal, gap=24, alignItems=center\n- nav-link: textGrowth=auto, lineHeight=1.2\n\nInteractive roles:\n\n- button: padding=[12,24], height=44, layout=horizontal, gap=8, alignItems=center, justifyContent=center, cornerRadius=8. In navbar: padding=[8,16], height=36. In form-group: width=fill_container, height=48, cornerRadius=10\n- icon-button: width=44, height=44, layout=horizontal, justifyContent=center, alignItems=center, cornerRadius=8\n- badge: layout=horizontal, padding=[6,12], gap=4, alignItems=center, justifyContent=center, cornerRadius=999\n- tag: layout=horizontal, padding=[4,10], gap=4, alignItems=center, justifyContent=center, cornerRadius=6\n- pill: layout=horizontal, padding=[6,14], gap=6, alignItems=center, justifyContent=center, cornerRadius=999\n- input: height=48, layout=horizontal, padding=[12,16], alignItems=center, cornerRadius=8. In vertical layout: width=fill_container\n- form-input: width=fill_container, height=48, layout=horizontal, padding=[12,16], alignItems=center, cornerRadius=8\n- search-bar: layout=horizontal, height=44, padding=[10,16], gap=8, alignItems=center, cornerRadius=22\n\nDisplay roles:\n\n- card: layout=vertical, gap=12, cornerRadius=12, clipContent=true. In horizontal layout: width=fill_container, height=fill_container\n- stat-card: layout=vertical, gap=8, padding=[24,24], cornerRadius=12. In horizontal layout: width=fill_container, height=fill_container\n- pricing-card: layout=vertical, gap=16, padding=[32,24], cornerRadius=16, clipContent=true. In horizontal layout: width=fill_container, height=fill_container\n- image-card: layout=vertical, gap=0, cornerRadius=12, clipContent=true\n- feature-card: layout=vertical, gap=12, padding=[24,24], cornerRadius=12. In horizontal layout: width=fill_container, height=fill_container\n\nMedia roles:\n\n- phone-mockup: width=280, height=560, cornerRadius=32, layout=none\n- screenshot-frame: cornerRadius=12, clipContent=true\n- avatar: width/height=48, cornerRadius=24, clipContent=true (size adapts to explicit width)\n- icon: width=24, height=24\n\nLayout-escape roles:\n\n- overlay: the ONLY way to place a child at absolute x/y inside a parent that uses `layout: vertical|horizontal`. Use for notification dots on an icon, corner ribbons on a card, floating status indicators. The child keeps its explicit `x`/`y` while siblings flow normally. Do NOT use `role: 'overlay'` for inline components \u2014 `badge`, `pill`, `tag` are inline (they flow in layout like any other child, NOT floating). Do NOT use `role: 'overlay'` as a substitute for `layout: 'none'` on the parent.\n\nTypography roles:\n\n- heading: lineHeight=1.2 (CJK: 1.35), letterSpacing=-0.5 (CJK: 0). In vertical layout: textGrowth=fixed-width, width=fill_container\n- subheading: lineHeight=1.3 (CJK: 1.4), textGrowth=fixed-width, width=fill_container\n- body-text: lineHeight=1.5 (CJK: 1.6), textGrowth=fixed-width, width=fill_container\n- caption: lineHeight=1.3 (CJK: 1.4), textGrowth=auto\n- label: lineHeight=1.2, textGrowth=auto, textAlignVertical=middle\n\nContent roles:\n\n- hero: layout=vertical, width=fill_container, height=fit_content, padding=[80,80] (mobile: [40,16]), gap=24, alignItems=center\n- feature-grid: layout=horizontal, width=fill_container, gap=24, alignItems=start\n- testimonial: layout=vertical, gap=16, padding=[24,24], cornerRadius=12\n- cta-section: layout=vertical, width=fill_container, height=fit_content, padding=[60,80] (mobile: [40,16]), gap=20, alignItems=center\n- footer: layout=vertical, width=fill_container, height=fit_content, padding=[48,80] (mobile: [32,16]), gap=24\n- stats-section: layout=horizontal, width=fill_container, height=fit_content, padding=[48,80] (mobile: [32,16]), gap=32, justifyContent=center, alignItems=center\n\nTable roles:\n\n- table: layout=vertical, width=fill_container, gap=0, clipContent=true\n- table-row: layout=horizontal, width=fill_container, alignItems=center, padding=[12,16]\n- table-header: layout=horizontal, width=fill_container, alignItems=center, padding=[12,16]\n- table-cell: width=fill_container\n\nYour explicit props ALWAYS override role defaults. Only unset properties get filled in."
2672
2677
  },
2673
2678
  {
2674
2679
  "meta": {
@@ -4723,7 +4728,7 @@ async function handleSetVariables(params) {
4723
4728
  if (params.replace) {
4724
4729
  doc.variables = params.variables;
4725
4730
  } else {
4726
- doc.variables = { ...doc.variables ?? {}, ...params.variables };
4731
+ doc.variables = { ...doc.variables, ...params.variables };
4727
4732
  }
4728
4733
  await saveDocument(filePath, doc);
4729
4734
  return { variables: doc.variables };
@@ -4734,7 +4739,7 @@ async function handleSetThemes(params) {
4734
4739
  if (params.replace) {
4735
4740
  doc.themes = params.themes;
4736
4741
  } else {
4737
- doc.themes = { ...doc.themes ?? {}, ...params.themes };
4742
+ doc.themes = { ...doc.themes, ...params.themes };
4738
4743
  }
4739
4744
  await saveDocument(filePath, doc);
4740
4745
  return { themes: doc.themes };
@@ -5042,11 +5047,18 @@ var init_font_manager = __esm({
5042
5047
  systemFontFamilies = /* @__PURE__ */ new Set();
5043
5048
  /** In-flight font fetch promises to avoid duplicate requests */
5044
5049
  pendingFetches = /* @__PURE__ */ new Map();
5050
+ /** Cached set of native (OS-installed) font families from Local Font Access API (lowercase) */
5051
+ nativeFontSet = null;
5052
+ /** Full native font entries with blob accessors, keyed by lowercase family name */
5053
+ nativeFontMap = /* @__PURE__ */ new Map();
5054
+ /** Current permission state for native font access (Local Font Access API) */
5055
+ nativeFontPermission = "prompt";
5045
5056
  constructor(ck, options) {
5046
5057
  this.provider = ck.TypefaceFontProvider.Make();
5047
5058
  this.fontBasePath = options?.fontBasePath ?? "/fonts/";
5048
5059
  if (!this.fontBasePath.endsWith("/")) this.fontBasePath += "/";
5049
5060
  this.googleFontsCssUrl = options?.googleFontsCssUrl ?? "https://fonts.googleapis.com/css2";
5061
+ this._checkPermissionState();
5050
5062
  }
5051
5063
  getProvider() {
5052
5064
  return this.provider;
@@ -5118,7 +5130,8 @@ var init_font_manager = __esm({
5118
5130
  }
5119
5131
  }
5120
5132
  /**
5121
- * Ensure a font family is loaded. Tries bundled fonts first, then Google Fonts.
5133
+ * Ensure a font family is loaded. Tries bundled fonts first, then native
5134
+ * fonts (Local Font Access API + canvas heuristic), then Google Fonts CDN.
5122
5135
  */
5123
5136
  async ensureFont(family, weights = [400, 500, 600, 700]) {
5124
5137
  const key = family.toLowerCase();
@@ -5133,6 +5146,7 @@ var init_font_manager = __esm({
5133
5146
  this.pendingFetches.delete(key);
5134
5147
  if (!result) {
5135
5148
  if (isSystemFont(family)) {
5149
+ console.warn(`[FontManager] "${family}" is now a system font fallback after failed load.`);
5136
5150
  this.systemFontFamilies.add(key);
5137
5151
  } else {
5138
5152
  this.failedFamilies.add(key);
@@ -5152,6 +5166,91 @@ var init_font_manager = __esm({
5152
5166
  });
5153
5167
  return loaded;
5154
5168
  }
5169
+ /**
5170
+ * Request native font access from the user via the Local Font Access API.
5171
+ * Must be called from a user gesture context (click handler) for the
5172
+ * browser to show the permission prompt.
5173
+ *
5174
+ * Returns true if access was granted and fonts were enumerated.
5175
+ */
5176
+ async requestNativeFontAccess() {
5177
+ if (typeof window === "undefined") {
5178
+ this.nativeFontPermission = "unavailable";
5179
+ return false;
5180
+ }
5181
+ if (!("queryLocalFonts" in window)) {
5182
+ console.warn("[FontManager] Local Font Access API not available in this browser.");
5183
+ this.nativeFontPermission = "unavailable";
5184
+ return false;
5185
+ }
5186
+ try {
5187
+ const fonts = await window.queryLocalFonts();
5188
+ const families = /* @__PURE__ */ new Set();
5189
+ this.nativeFontMap.clear();
5190
+ for (const f of fonts) {
5191
+ const key = f.family.toLowerCase();
5192
+ families.add(key);
5193
+ const entry = {
5194
+ family: f.family,
5195
+ fullName: f.fullName,
5196
+ postscriptName: f.postscriptName,
5197
+ style: f.style,
5198
+ blob: f.blob.bind(f)
5199
+ };
5200
+ const existing = this.nativeFontMap.get(key) ?? [];
5201
+ existing.push(entry);
5202
+ this.nativeFontMap.set(key, existing);
5203
+ }
5204
+ this.nativeFontSet = families;
5205
+ this.nativeFontPermission = "granted";
5206
+ console.log(`[FontManager] Native font access granted \u2014 ${families.size} families found.`);
5207
+ return true;
5208
+ } catch (e) {
5209
+ const errMsg = e instanceof Error ? e.message : String(e);
5210
+ if (e instanceof DOMException && e.name === "NotAllowedError") {
5211
+ try {
5212
+ const status = await navigator.permissions.query({
5213
+ name: "local-fonts"
5214
+ });
5215
+ if (status.state === "prompt") {
5216
+ console.warn(
5217
+ "[FontManager] Native font access not yet prompted \u2014 will retry on next user gesture."
5218
+ );
5219
+ this.nativeFontPermission = "prompt";
5220
+ this.nativeFontSet = null;
5221
+ return false;
5222
+ }
5223
+ } catch {
5224
+ }
5225
+ console.warn("[FontManager] Native font access denied by user.");
5226
+ this.nativeFontPermission = "denied";
5227
+ } else {
5228
+ console.warn("[FontManager] Native font access failed:", errMsg);
5229
+ this.nativeFontPermission = "denied";
5230
+ }
5231
+ this.nativeFontSet = /* @__PURE__ */ new Set();
5232
+ return false;
5233
+ }
5234
+ }
5235
+ /**
5236
+ * Check the current permission state without triggering a prompt.
5237
+ */
5238
+ async _checkPermissionState() {
5239
+ if (typeof navigator === "undefined" || !navigator.permissions) {
5240
+ return;
5241
+ }
5242
+ try {
5243
+ const result = await navigator.permissions.query({
5244
+ name: "local-fonts"
5245
+ });
5246
+ this.nativeFontPermission = result.state;
5247
+ if (result.state === "granted") {
5248
+ this.requestNativeFontAccess().catch(() => {
5249
+ });
5250
+ }
5251
+ } catch {
5252
+ }
5253
+ }
5155
5254
  async _loadFont(family, weights) {
5156
5255
  const bundled = BUNDLED_FONTS[family.toLowerCase()];
5157
5256
  if (bundled) {
@@ -5159,10 +5258,56 @@ var init_font_manager = __esm({
5159
5258
  const ok = await this._fetchLocalFonts(family, urls, bundled);
5160
5259
  if (ok) return true;
5161
5260
  }
5162
- if (isSystemFont(family)) {
5261
+ if (isKnownNonGoogleFont(family)) {
5163
5262
  return false;
5164
5263
  }
5165
- return this._fetchGoogleFont(family, weights);
5264
+ const nativeLoaded = await this._loadNativeFontData(family);
5265
+ if (nativeLoaded) return true;
5266
+ const nativeFonts = await this.getNativeFontSet();
5267
+ if (nativeFonts.has(family.toLowerCase())) {
5268
+ const blobLoaded = await this._loadNativeFontData(family);
5269
+ if (blobLoaded) return true;
5270
+ this.systemFontFamilies.add(family.toLowerCase());
5271
+ return false;
5272
+ }
5273
+ if (isFontLocallyAvailable(family)) {
5274
+ this.systemFontFamilies.add(family.toLowerCase());
5275
+ return false;
5276
+ }
5277
+ const isFontFromGoogle = await this._fetchGoogleFont(family, weights);
5278
+ if (isFontFromGoogle) return true;
5279
+ return false;
5280
+ }
5281
+ /**
5282
+ * Attempt to load a native font from the Local Font Access API blob data
5283
+ * into CanvasKit's TypefaceFontProvider for vector rendering.
5284
+ */
5285
+ async _loadNativeFontData(family) {
5286
+ const key = family.toLowerCase();
5287
+ const entries = this.nativeFontMap.get(key);
5288
+ if (!entries || entries.length === 0) return false;
5289
+ let registered = 0;
5290
+ for (const entry of entries) {
5291
+ try {
5292
+ const blob = await entry.blob();
5293
+ const buffer = await blob.arrayBuffer();
5294
+ if (buffer.byteLength > 0 && this.registerFont(buffer, family)) {
5295
+ registered++;
5296
+ }
5297
+ } catch (e) {
5298
+ console.warn(
5299
+ `[FontManager] Failed to load native font blob for "${entry.fullName}":`,
5300
+ e instanceof Error ? e.message : String(e)
5301
+ );
5302
+ }
5303
+ }
5304
+ if (registered > 0) {
5305
+ console.log(
5306
+ `[FontManager] Loaded "${family}" from native fonts (${registered}/${entries.length} variants).`
5307
+ );
5308
+ return true;
5309
+ }
5310
+ return false;
5166
5311
  }
5167
5312
  async _fetchLocalFonts(family, urls, relPaths) {
5168
5313
  try {
@@ -5217,8 +5362,8 @@ var init_font_manager = __esm({
5217
5362
  const fontBuffers = await Promise.all(
5218
5363
  urls.map(async (url) => {
5219
5364
  try {
5220
- const resp = await fetchWithTimeout(url, 8e3);
5221
- return resp.ok ? resp.arrayBuffer() : null;
5365
+ const resp = fetchWithTimeout(url, 8e3);
5366
+ return (await resp).ok ? (await resp).arrayBuffer() : null;
5222
5367
  } catch {
5223
5368
  return null;
5224
5369
  }
@@ -5234,12 +5379,27 @@ var init_font_manager = __esm({
5234
5379
  }
5235
5380
  return false;
5236
5381
  }
5382
+ /**
5383
+ * Build a set of all native (OS-installed) font families using the
5384
+ * Local Font Access API (Chrome 103+, Edge 103+).
5385
+ * Falls back to empty set if API is unavailable or permission denied.
5386
+ * Results are cached after the first successful call.
5387
+ */
5388
+ async getNativeFontSet() {
5389
+ if (this.nativeFontSet) return this.nativeFontSet;
5390
+ if (this.nativeFontPermission !== "denied" && this.nativeFontPermission !== "unavailable") {
5391
+ await this.requestNativeFontAccess();
5392
+ }
5393
+ return this.nativeFontSet ?? /* @__PURE__ */ new Set();
5394
+ }
5237
5395
  dispose() {
5238
5396
  this.provider.delete();
5239
5397
  this.loadedFamilies.clear();
5240
5398
  this.failedFamilies.clear();
5241
5399
  this.systemFontFamilies.clear();
5242
5400
  this.pendingFetches.clear();
5401
+ this.nativeFontSet = null;
5402
+ this.nativeFontMap.clear();
5243
5403
  }
5244
5404
  };
5245
5405
  localFontCache = /* @__PURE__ */ new Map();
@@ -6776,8 +6936,8 @@ async function handleLoadThemePreset(params) {
6776
6936
  if (data.type !== "openpencil-theme-preset") {
6777
6937
  throw new Error("Invalid theme preset file");
6778
6938
  }
6779
- doc.themes = { ...doc.themes ?? {}, ...data.themes };
6780
- doc.variables = { ...doc.variables ?? {}, ...data.variables };
6939
+ doc.themes = { ...doc.themes, ...data.themes };
6940
+ doc.variables = { ...doc.variables, ...data.variables };
6781
6941
  await saveDocument(filePath, doc);
6782
6942
  return {
6783
6943
  themes: doc.themes,
@@ -13775,7 +13935,7 @@ init_define_import_meta_env();
13775
13935
  // apps/cli/package.json
13776
13936
  var package_default = {
13777
13937
  name: "@zseven-w/openpencil",
13778
- version: "0.7.2",
13938
+ version: "0.7.4",
13779
13939
  description: "CLI for OpenPencil \u2014 control the design tool from your terminal",
13780
13940
  homepage: "https://github.com/ZSeven-W/openpencil/tree/main/apps/cli",
13781
13941
  bugs: {
@@ -13802,8 +13962,8 @@ var package_default = {
13802
13962
  compile: "cd ../.. && bun run cli:compile"
13803
13963
  },
13804
13964
  dependencies: {
13805
- "@zseven-w/pen-figma": "0.7.2",
13806
- "@zseven-w/pen-mcp": "0.7.2"
13965
+ "@zseven-w/pen-figma": "0.7.4",
13966
+ "@zseven-w/pen-mcp": "0.7.4"
13807
13967
  }
13808
13968
  };
13809
13969