@open-slide/core 1.0.4 → 1.0.5

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 (46) hide show
  1. package/dist/{build-DqfKmw9h.js → build-CoON6kTb.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-CN7J0RDO.js → config-Bxtztw-H.js} +373 -221
  4. package/dist/{config-DweCbRkQ.d.ts → config-D2y1AXaN.d.ts} +3 -0
  5. package/dist/{dev-jWxtWHAG.js → dev-IezNC17X.js} +1 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/locale/index.d.ts +24 -0
  8. package/dist/locale/index.js +1189 -0
  9. package/dist/{preview-CSA05Gfm.js → preview-BwYjtENY.js} +1 -1
  10. package/dist/types-BVvl_xup.d.ts +314 -0
  11. package/dist/vite/index.d.ts +2 -1
  12. package/dist/vite/index.js +1 -1
  13. package/package.json +7 -1
  14. package/src/app/app.tsx +6 -2
  15. package/src/app/components/asset-view.tsx +87 -64
  16. package/src/app/components/click-nav-zones.tsx +4 -2
  17. package/src/app/components/inspector/comment-widget.tsx +9 -7
  18. package/src/app/components/inspector/inspector-panel.tsx +68 -39
  19. package/src/app/components/inspector/inspector-provider.tsx +185 -58
  20. package/src/app/components/inspector/save-bar.tsx +6 -2
  21. package/src/app/components/panel/save-card.tsx +12 -9
  22. package/src/app/components/pdf-progress-toast.tsx +11 -4
  23. package/src/app/components/present/control-bar.tsx +17 -10
  24. package/src/app/components/present/help-overlay.tsx +18 -17
  25. package/src/app/components/present/overview-grid.tsx +6 -4
  26. package/src/app/components/sidebar/folder-item.tsx +16 -7
  27. package/src/app/components/sidebar/icon-picker.tsx +4 -2
  28. package/src/app/components/sidebar/sidebar.tsx +87 -25
  29. package/src/app/components/style-panel/style-panel.tsx +26 -18
  30. package/src/app/components/theme-toggle.tsx +7 -5
  31. package/src/app/components/thumbnail-rail.tsx +4 -2
  32. package/src/app/favicon.ico +0 -0
  33. package/src/app/lib/inspector/use-editor.ts +9 -7
  34. package/src/app/lib/use-locale.ts +20 -0
  35. package/src/app/routes/home.tsx +90 -45
  36. package/src/app/routes/presenter.tsx +45 -25
  37. package/src/app/routes/slide.tsx +37 -24
  38. package/src/app/styles.css +28 -0
  39. package/src/app/virtual.d.ts +4 -0
  40. package/src/locale/en.ts +303 -0
  41. package/src/locale/format.ts +12 -0
  42. package/src/locale/index.ts +6 -0
  43. package/src/locale/ja.ts +307 -0
  44. package/src/locale/types.ts +323 -0
  45. package/src/locale/zh-cn.ts +303 -0
  46. package/src/locale/zh-tw.ts +303 -0
@@ -7,6 +7,9 @@ import { existsSync } from "node:fs";
7
7
  import tailwindcss from "@tailwindcss/vite";
8
8
  import react from "@vitejs/plugin-react";
9
9
  import { parse } from "@babel/parser";
10
+ import * as t$1 from "@babel/types";
11
+ import * as t from "@babel/types";
12
+ import { isJSXElement, isJSXFragment } from "@babel/types";
10
13
  import fg from "fast-glob";
11
14
  import { loadConfigFromFile } from "vite";
12
15
 
@@ -21,28 +24,34 @@ const SKIP_KEYS = new Set([
21
24
  "trailingComments",
22
25
  "innerComments"
23
26
  ]);
24
- function walkJsx(ast, visit) {
27
+ function walk(ast, visit, accept) {
25
28
  let stopped = false;
26
- const walk = (node) => {
29
+ const recurse = (node) => {
27
30
  if (stopped || !node || typeof node !== "object") return;
28
31
  if (Array.isArray(node)) {
29
- for (const c of node) walk(c);
32
+ for (const c of node) recurse(c);
30
33
  return;
31
34
  }
32
35
  const n = node;
33
36
  if (typeof n.type !== "string") return;
34
- if (n.type === "JSXElement" || n.type === "JSXFragment") {
35
- if (visit(n) === "stop") {
36
- stopped = true;
37
- return;
38
- }
37
+ if (accept(n) && visit(n) === "stop") {
38
+ stopped = true;
39
+ return;
39
40
  }
40
41
  for (const key of Object.keys(n)) {
41
42
  if (SKIP_KEYS.has(key)) continue;
42
- walk(n[key]);
43
+ recurse(n[key]);
43
44
  }
44
45
  };
45
- walk(ast);
46
+ recurse(ast);
47
+ }
48
+ const isJsx = (n) => isJSXElement(n) || isJSXFragment(n);
49
+ const acceptAll = () => true;
50
+ function walkJsx(ast, visit) {
51
+ walk(ast, visit, isJsx);
52
+ }
53
+ function walkAll(ast, visit) {
54
+ walk(ast, visit, acceptAll);
46
55
  }
47
56
 
48
57
  //#endregion
@@ -126,54 +135,44 @@ function lineIndent(source, lineNumber) {
126
135
  function findJsxAncestors(ast, line, column) {
127
136
  const hits = [];
128
137
  walkJsx(ast, (n) => {
129
- if (!n.loc) return;
138
+ if (!n.loc || !t$1.isJSXElement(n) && !t$1.isJSXFragment(n)) return;
130
139
  const s = n.loc.start;
131
140
  const e = n.loc.end;
132
141
  const afterStart = line > s.line || line === s.line && column >= s.column;
133
142
  const beforeEnd = line < e.line || line === e.line && column < e.column;
134
143
  if (afterStart && beforeEnd) hits.push({
135
144
  node: n,
136
- size: n.end - n.start
145
+ size: (n.end ?? 0) - (n.start ?? 0)
137
146
  });
138
147
  });
139
148
  hits.sort((a, b) => a.size - b.size);
140
149
  return hits.map((h) => h.node);
141
150
  }
142
151
  function planInsertion(source, target) {
143
- if (target.type === "JSXFragment") {
152
+ if (t$1.isJSXFragment(target)) {
144
153
  const opening = target.openingFragment;
145
- if (!opening) return null;
146
154
  const startLine = target.loc?.start.line ?? 1;
147
155
  return {
148
- offset: opening.end,
156
+ offset: opening.end ?? 0,
149
157
  indent: `${lineIndent(source, startLine)} `
150
158
  };
151
159
  }
152
- if (target.type === "JSXElement") {
160
+ if (t$1.isJSXElement(target)) {
153
161
  const opening = target.openingElement;
154
- if (!opening || opening.selfClosing) return null;
162
+ if (opening.selfClosing) return null;
155
163
  const startLine = target.loc?.start.line ?? 1;
156
164
  return {
157
- offset: opening.end,
165
+ offset: opening.end ?? 0,
158
166
  indent: `${lineIndent(source, startLine)} `
159
167
  };
160
168
  }
161
169
  return null;
162
170
  }
163
171
  function findInsertion(source, line, column) {
164
- let ast;
165
- try {
166
- ast = parse(source, {
167
- sourceType: "module",
168
- plugins: ["typescript", "jsx"],
169
- errorRecovery: true
170
- });
171
- } catch {
172
- return null;
173
- }
172
+ const ast = parseSource$1(source);
173
+ if (!ast) return null;
174
174
  const col = column ?? 0;
175
175
  const ancestors = findJsxAncestors(ast, line, col);
176
- if (ancestors.length === 0) return null;
177
176
  for (const node of ancestors) {
178
177
  const plan = planInsertion(source, node);
179
178
  if (plan) return plan;
@@ -196,19 +195,16 @@ function parseSource$1(source) {
196
195
  return null;
197
196
  }
198
197
  }
199
- function findInnermostJsxElement(source, line, column) {
200
- const ast = parseSource$1(source);
201
- if (!ast) return null;
198
+ function findInnermostJsxElement(ast, line, column) {
202
199
  const exact = findJsxByStart(ast, line, column);
203
200
  if (exact) return exact;
204
- const ancestors = findJsxAncestors(ast, line, column);
205
- for (const n of ancestors) if (n.type === "JSXElement") return n;
201
+ for (const n of findJsxAncestors(ast, line, column)) if (t$1.isJSXElement(n)) return n;
206
202
  return null;
207
203
  }
208
204
  function findJsxByStart(ast, line, column) {
209
205
  let hit = null;
210
206
  walkJsx(ast, (n) => {
211
- if (n.type !== "JSXElement" || !n.loc) return;
207
+ if (!t$1.isJSXElement(n) || !n.loc) return;
212
208
  const s = n.loc.start;
213
209
  if (s.line === line && s.column === column) {
214
210
  hit = n;
@@ -220,60 +216,56 @@ function findJsxByStart(ast, line, column) {
220
216
  function jsString$1(s) {
221
217
  return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
222
218
  }
223
- function findStyleAttr(opening) {
224
- const attrs = opening.attributes ?? [];
225
- for (const attr of attrs) {
226
- if (attr.type !== "JSXAttribute") continue;
227
- const name = attr.name;
228
- if (name?.type === "JSXIdentifier" && name.name === "style") return attr;
229
- }
219
+ function jsxAttrName(attr) {
220
+ return t$1.isJSXIdentifier(attr.name) ? attr.name.name : null;
221
+ }
222
+ function findJsxAttr(opening, name) {
223
+ for (const attr of opening.attributes) if (t$1.isJSXAttribute(attr) && jsxAttrName(attr) === name) return attr;
230
224
  return null;
231
225
  }
232
226
  function buildStyleSplice(source, element, ops) {
233
227
  const opening = element.openingElement;
234
- if (!opening) return { error: "no opening element" };
235
- const existing = findStyleAttr(opening);
228
+ const existing = findJsxAttr(opening, "style");
236
229
  const style = new Map();
237
230
  if (existing) {
238
231
  const value = existing.value;
239
- if (!value || value.type !== "JSXExpressionContainer") return { error: "style attribute has unsupported form" };
232
+ if (!value || !t$1.isJSXExpressionContainer(value)) return { error: "style attribute has unsupported form" };
240
233
  const expr = value.expression;
241
- if (expr.type !== "ObjectExpression") return { error: "style is not a literal object" };
242
- const properties = expr.properties;
243
- for (const prop of properties) {
244
- if (prop.type !== "ObjectProperty") return { error: "style contains spread or method" };
245
- const p = prop;
246
- if (p.computed) return { error: "style has computed key" };
234
+ if (!t$1.isObjectExpression(expr)) return { error: "style is not a literal object" };
235
+ for (const prop of expr.properties) {
236
+ if (!t$1.isObjectProperty(prop)) return { error: "style contains spread or method" };
237
+ if (prop.computed) return { error: "style has computed key" };
247
238
  let keyName = null;
248
- if (p.key.type === "Identifier" && p.key.name) keyName = p.key.name;
249
- else if (p.key.type === "StringLiteral" && typeof p.key.value === "string") keyName = p.key.value;
239
+ if (t$1.isIdentifier(prop.key)) keyName = prop.key.name;
240
+ else if (t$1.isStringLiteral(prop.key)) keyName = prop.key.value;
250
241
  if (!keyName) return { error: "style has unsupported key" };
251
- style.set(keyName, source.slice(p.value.start, p.value.end));
242
+ const v = prop.value;
243
+ if (typeof v.start !== "number" || typeof v.end !== "number") return { error: "style value missing source range" };
244
+ style.set(keyName, source.slice(v.start, v.end));
252
245
  }
253
246
  }
254
247
  for (const op of ops) if (op.value === null) style.delete(op.key);
255
248
  else style.set(op.key, jsString$1(op.value));
256
249
  if (style.size === 0) {
257
250
  if (!existing) return null;
258
- let from = existing.start;
251
+ let from = existing.start ?? 0;
259
252
  if (from > 0 && source[from - 1] === " ") from -= 1;
260
253
  return {
261
254
  from,
262
- to: existing.end,
255
+ to: existing.end ?? 0,
263
256
  text: ""
264
257
  };
265
258
  }
266
259
  const propsText = Array.from(style.entries()).map(([k, v]) => `${k}: ${v}`).join(", ");
267
260
  const newAttr = `style={{ ${propsText} }}`;
268
261
  if (existing) return {
269
- from: existing.start,
270
- to: existing.end,
262
+ from: existing.start ?? 0,
263
+ to: existing.end ?? 0,
271
264
  text: newAttr
272
265
  };
273
- const name = opening.name;
274
266
  return {
275
- from: name.end,
276
- to: name.end,
267
+ from: opening.name.end ?? 0,
268
+ to: opening.name.end ?? 0,
277
269
  text: ` ${newAttr}`
278
270
  };
279
271
  }
@@ -281,57 +273,280 @@ function formatJsxText(value) {
281
273
  if (/[{}<>]/.test(value) || /^\s|\s$/.test(value) || value === "") return `{${jsString$1(value)}}`;
282
274
  return value;
283
275
  }
284
- function buildTextSplice(element, value) {
285
- const children = element.children ?? [];
286
- if (children.length === 0) return { error: "element has no children to edit" };
287
- const meaningful = children.filter((c) => {
288
- if (c.type === "JSXText") {
289
- const v = c.value;
290
- return v.trim() !== "";
291
- }
276
+ function meaningfulChildren(parent) {
277
+ return parent.children.filter((c) => {
278
+ if (t$1.isJSXText(c)) return c.value.trim() !== "";
292
279
  return true;
293
280
  });
294
- if (meaningful.length !== 1) return { error: "element has complex children" };
281
+ }
282
+ function wrapSplice(parent, text) {
283
+ const first = parent.children[0];
284
+ const last = parent.children[parent.children.length - 1];
285
+ return {
286
+ from: first.start ?? 0,
287
+ to: last.end ?? 0,
288
+ text
289
+ };
290
+ }
291
+ function collectTextCandidates(element, out) {
292
+ const meaningful = meaningfulChildren(element);
293
+ const isSole = meaningful.length === 1;
294
+ for (const child of meaningful) if (t$1.isJSXText(child)) {
295
+ const current = child.value.trim();
296
+ if (!current) continue;
297
+ out.push({
298
+ current,
299
+ splice: (v) => isSole ? wrapSplice(element, formatJsxText(v)) : {
300
+ from: child.start ?? 0,
301
+ to: child.end ?? 0,
302
+ text: formatJsxText(v)
303
+ }
304
+ });
305
+ } else if (t$1.isJSXExpressionContainer(child)) {
306
+ const expr = child.expression;
307
+ if (t$1.isStringLiteral(expr) || t$1.isNumericLiteral(expr)) {
308
+ const current = String(expr.value);
309
+ out.push({
310
+ current,
311
+ splice: (v) => isSole ? wrapSplice(element, `{${jsString$1(v)}}`) : {
312
+ from: child.start ?? 0,
313
+ to: child.end ?? 0,
314
+ text: `{${jsString$1(v)}}`
315
+ }
316
+ });
317
+ }
318
+ } else if (t$1.isJSXElement(child) || t$1.isJSXFragment(child)) collectTextCandidates(child, out);
319
+ }
320
+ function propPassthroughName(element) {
321
+ const meaningful = meaningfulChildren(element);
322
+ if (meaningful.length !== 1) return null;
295
323
  const child = meaningful[0];
296
- if (child.type === "JSXText") {
297
- const first = children[0];
298
- const last = children[children.length - 1];
299
- return {
300
- from: first.start,
301
- to: last.end,
302
- text: formatJsxText(value)
303
- };
324
+ if (!t$1.isJSXExpressionContainer(child)) return null;
325
+ return t$1.isIdentifier(child.expression) ? child.expression.name : null;
326
+ }
327
+ function findEnclosingComponent(ast, target) {
328
+ let best = null;
329
+ let bestSize = Number.POSITIVE_INFINITY;
330
+ const targetStart = target.start ?? 0;
331
+ const targetEnd = target.end ?? 0;
332
+ const consider = (name, fn) => {
333
+ if (!/^[A-Z]/.test(name)) return;
334
+ const fnStart = fn.start ?? 0;
335
+ const fnEnd = fn.end ?? 0;
336
+ if (fnStart > targetStart || fnEnd < targetEnd) return;
337
+ const size = fnEnd - fnStart;
338
+ if (size < bestSize) {
339
+ best = {
340
+ name,
341
+ fn
342
+ };
343
+ bestSize = size;
344
+ }
345
+ };
346
+ const visitDecl = (decl) => {
347
+ if (t$1.isFunctionDeclaration(decl) && decl.id) consider(decl.id.name, decl);
348
+ else if (t$1.isVariableDeclaration(decl)) for (const d of decl.declarations) {
349
+ if (!t$1.isVariableDeclarator(d) || !t$1.isIdentifier(d.id) || !d.init) continue;
350
+ if (t$1.isArrowFunctionExpression(d.init) || t$1.isFunctionExpression(d.init)) consider(d.id.name, d.init);
351
+ }
352
+ };
353
+ for (const decl of ast.program.body) {
354
+ visitDecl(decl);
355
+ if (t$1.isExportNamedDeclaration(decl) || t$1.isExportDefaultDeclaration(decl)) {
356
+ const inner = decl.declaration;
357
+ if (inner && (t$1.isStatement(inner) || t$1.isFunctionDeclaration(inner))) visitDecl(inner);
358
+ }
304
359
  }
305
- if (child.type === "JSXExpressionContainer") {
306
- const expr = child.expression;
307
- if (expr.type === "StringLiteral" || expr.type === "NumericLiteral") return {
308
- from: child.start,
309
- to: child.end,
310
- text: `{${jsString$1(value)}}`
360
+ return best;
361
+ }
362
+ function componentDestructuresProp(fn, propName) {
363
+ if (fn.params.length === 0) return false;
364
+ let first = fn.params[0];
365
+ if (t$1.isAssignmentPattern(first)) first = first.left;
366
+ if (!t$1.isObjectPattern(first)) return false;
367
+ for (const prop of first.properties) {
368
+ if (!t$1.isObjectProperty(prop)) continue;
369
+ if (t$1.isIdentifier(prop.key) && prop.key.name === propName) return true;
370
+ if (t$1.isStringLiteral(prop.key) && prop.key.value === propName) return true;
371
+ }
372
+ return false;
373
+ }
374
+ function collectCallSiteCandidates(ast, componentName) {
375
+ const out = [];
376
+ walkJsx(ast, (n) => {
377
+ if (!t$1.isJSXElement(n)) return;
378
+ const elName = n.openingElement.name;
379
+ if (t$1.isJSXIdentifier(elName) && elName.name === componentName) collectTextCandidates(n, out);
380
+ });
381
+ return out;
382
+ }
383
+ function formatJsxAttrValue(value) {
384
+ if (/^[^"\\<>&{}\n\r]*$/.test(value)) return `"${value}"`;
385
+ return `{${jsString$1(value)}}`;
386
+ }
387
+ function spliceRange(node, text) {
388
+ return {
389
+ from: node.start ?? 0,
390
+ to: node.end ?? 0,
391
+ text
392
+ };
393
+ }
394
+ function collectPropCallSiteCandidates(ast, componentName, propName) {
395
+ const out = [];
396
+ walkJsx(ast, (n) => {
397
+ if (!t$1.isJSXElement(n)) return;
398
+ const elName = n.openingElement.name;
399
+ if (!t$1.isJSXIdentifier(elName) || elName.name !== componentName) return;
400
+ const attr = findJsxAttr(n.openingElement, propName);
401
+ if (!attr?.value) return;
402
+ const v = attr.value;
403
+ if (t$1.isStringLiteral(v)) out.push({
404
+ current: v.value,
405
+ splice: (s) => spliceRange(v, formatJsxAttrValue(s))
406
+ });
407
+ else if (t$1.isJSXExpressionContainer(v)) {
408
+ const expr = v.expression;
409
+ if (t$1.isStringLiteral(expr) || t$1.isNumericLiteral(expr)) out.push({
410
+ current: String(expr.value),
411
+ splice: (s) => spliceRange(v, formatJsxAttrValue(s))
412
+ });
413
+ }
414
+ });
415
+ return out;
416
+ }
417
+ function findEnclosingMapCallback(ast, target) {
418
+ let best = null;
419
+ const targetStart = target.start ?? 0;
420
+ const targetEnd = target.end ?? 0;
421
+ walkAll(ast, (node) => {
422
+ if (!t$1.isCallExpression(node)) return;
423
+ const callee = node.callee;
424
+ if (!t$1.isMemberExpression(callee) || callee.computed) return;
425
+ if (!t$1.isIdentifier(callee.property)) return;
426
+ if (callee.property.name !== "map" && callee.property.name !== "flatMap") return;
427
+ const fn = node.arguments[0];
428
+ if (!fn || !t$1.isArrowFunctionExpression(fn) && !t$1.isFunctionExpression(fn)) return;
429
+ const fnStart = fn.start ?? 0;
430
+ const fnEnd = fn.end ?? 0;
431
+ if (fnStart > targetStart || fnEnd < targetEnd) return;
432
+ if (!t$1.isExpression(callee.object)) return;
433
+ const size = fnEnd - fnStart;
434
+ if (!best || size < best.size) best = {
435
+ fn,
436
+ arrayArg: callee.object,
437
+ size
311
438
  };
312
- return { error: "element has dynamic expression child" };
439
+ });
440
+ if (!best) return null;
441
+ const found = best;
442
+ return {
443
+ fn: found.fn,
444
+ arrayArg: found.arrayArg
445
+ };
446
+ }
447
+ function resolveArrayLiteralElements(ast, expr) {
448
+ const dropHoles = (arr) => arr.elements.filter((e) => e != null);
449
+ if (t$1.isArrayExpression(expr)) return dropHoles(expr);
450
+ if (!t$1.isIdentifier(expr)) return null;
451
+ const name = expr.name;
452
+ const useStart = expr.start ?? 0;
453
+ let init = null;
454
+ walkAll(ast, (node) => {
455
+ if (!t$1.isVariableDeclarator(node)) return;
456
+ if (!t$1.isIdentifier(node.id) || node.id.name !== name) return;
457
+ if (!node.init || !t$1.isArrayExpression(node.init)) return;
458
+ if ((node.init.start ?? 0) > useStart) return;
459
+ init = node.init;
460
+ });
461
+ return init ? dropHoles(init) : null;
462
+ }
463
+ function findObjectProperty(obj, name) {
464
+ if (!t$1.isObjectExpression(obj)) return null;
465
+ for (const prop of obj.properties) {
466
+ if (!t$1.isObjectProperty(prop) || prop.computed) continue;
467
+ if (t$1.isIdentifier(prop.key) && prop.key.name === name) return prop;
468
+ if (t$1.isStringLiteral(prop.key) && prop.key.value === name) return prop;
469
+ }
470
+ return null;
471
+ }
472
+ function decodeMapPassthrough(element, callbackParam) {
473
+ const meaningful = meaningfulChildren(element);
474
+ if (meaningful.length !== 1) return null;
475
+ const child = meaningful[0];
476
+ if (!t$1.isJSXExpressionContainer(child)) return null;
477
+ const expr = child.expression;
478
+ if (t$1.isMemberExpression(expr)) {
479
+ if (expr.computed) return null;
480
+ if (!t$1.isIdentifier(expr.object) || !t$1.isIdentifier(expr.property)) return null;
481
+ if (!callbackParam || !t$1.isIdentifier(callbackParam)) return null;
482
+ if (callbackParam.name !== expr.object.name) return null;
483
+ return expr.property.name;
484
+ }
485
+ if (t$1.isIdentifier(expr)) {
486
+ const fieldName = expr.name;
487
+ if (!callbackParam || !t$1.isObjectPattern(callbackParam)) return null;
488
+ for (const prop of callbackParam.properties) {
489
+ if (!t$1.isObjectProperty(prop) || prop.computed) continue;
490
+ if (!t$1.isIdentifier(prop.key) || prop.key.name !== fieldName) continue;
491
+ return t$1.isIdentifier(prop.value) && prop.value.name === fieldName ? fieldName : null;
492
+ }
493
+ }
494
+ return null;
495
+ }
496
+ function collectArrayMapCandidates(ast, element) {
497
+ const ctx = findEnclosingMapCallback(ast, element);
498
+ if (!ctx) return [];
499
+ const fieldName = decodeMapPassthrough(element, ctx.fn.params[0]);
500
+ if (!fieldName) return [];
501
+ const elements = resolveArrayLiteralElements(ast, ctx.arrayArg);
502
+ if (!elements) return [];
503
+ const out = [];
504
+ for (const obj of elements) {
505
+ const prop = findObjectProperty(obj, fieldName);
506
+ if (!prop) continue;
507
+ const v = prop.value;
508
+ if (t$1.isStringLiteral(v)) out.push({
509
+ current: v.value,
510
+ splice: (s) => spliceRange(v, jsString$1(s))
511
+ });
512
+ else if (t$1.isNumericLiteral(v)) out.push({
513
+ current: String(v.value),
514
+ splice: (s) => spliceRange(v, jsString$1(s))
515
+ });
516
+ }
517
+ return out;
518
+ }
519
+ function buildTextSplice(ast, element, value, prevText) {
520
+ const candidates = [];
521
+ collectTextCandidates(element, candidates);
522
+ if (candidates.length === 0) {
523
+ const passthrough = propPassthroughName(element);
524
+ const enclosing = passthrough ? findEnclosingComponent(ast, element) : null;
525
+ if (passthrough === "children" && enclosing) candidates.push(...collectCallSiteCandidates(ast, enclosing.name));
526
+ else if (passthrough && enclosing && componentDestructuresProp(enclosing.fn, passthrough)) candidates.push(...collectPropCallSiteCandidates(ast, enclosing.name, passthrough));
313
527
  }
314
- return { error: "element has complex children" };
528
+ if (candidates.length === 0) candidates.push(...collectArrayMapCandidates(ast, element));
529
+ if (candidates.length === 0) return { error: "element has no editable text" };
530
+ if (candidates.length === 1) return candidates[0].splice(value);
531
+ if (prevText === void 0) return { error: "element has multiple text candidates; missing prevText" };
532
+ const norm = prevText.trim();
533
+ const matches = candidates.filter((c) => c.current === norm);
534
+ if (matches.length === 0) return { error: "no text candidate matches the current value" };
535
+ if (matches.length > 1) return { error: "multiple text candidates share the same value; cannot disambiguate" };
536
+ return matches[0].splice(value);
315
537
  }
316
538
  function findImports$1(ast) {
317
- const body = ast.program?.body ?? [];
318
539
  const out = [];
319
- for (const node of body) {
320
- if (node.type !== "ImportDeclaration") continue;
321
- const src = node.source?.value;
322
- if (typeof src !== "string") continue;
323
- const specs = node.specifiers ?? [];
540
+ for (const node of ast.program.body) {
541
+ if (!t$1.isImportDeclaration(node)) continue;
324
542
  let def = null;
325
- for (const spec of specs) if (spec.type === "ImportDefaultSpecifier") {
326
- const local = spec.local?.name;
327
- if (typeof local === "string") {
328
- def = local;
329
- break;
330
- }
543
+ for (const spec of node.specifiers) if (t$1.isImportDefaultSpecifier(spec)) {
544
+ def = spec.local.name;
545
+ break;
331
546
  }
332
547
  out.push({
333
548
  node,
334
- source: src,
549
+ source: node.source.value,
335
550
  defaultIdent: def
336
551
  });
337
552
  }
@@ -341,11 +556,7 @@ function collectTopLevelIdentifiers(ast) {
341
556
  const names = new Set();
342
557
  for (const imp of findImports$1(ast)) {
343
558
  if (imp.defaultIdent) names.add(imp.defaultIdent);
344
- const specs = imp.node.specifiers ?? [];
345
- for (const spec of specs) if (spec.type !== "ImportDefaultSpecifier") {
346
- const local = spec.local?.name;
347
- if (typeof local === "string") names.add(local);
348
- }
559
+ for (const spec of imp.node.specifiers) if (!t$1.isImportDefaultSpecifier(spec)) names.add(spec.local.name);
349
560
  }
350
561
  return names;
351
562
  }
@@ -368,56 +579,43 @@ function safeAssetIdentifier(filename, taken) {
368
579
  }
369
580
  return candidate;
370
581
  }
371
- function findJsxAttr(opening, name) {
372
- const attrs = opening.attributes ?? [];
373
- for (const attr of attrs) {
374
- if (attr.type !== "JSXAttribute") continue;
375
- const n = attr.name;
376
- if (n?.type === "JSXIdentifier" && n.name === name) return attr;
377
- }
378
- return null;
379
- }
380
- function planAssetAttr(ast, element, attr, assetPath) {
381
- const opening = element.openingElement;
382
- if (!opening) return { error: "no opening element" };
383
- if (!attr || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) return { error: "invalid attribute name" };
384
- if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
582
+ function planAssetImport(ast, assetPath) {
385
583
  const imports = findImports$1(ast);
386
- let identifier = null;
387
- for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) {
388
- identifier = imp.defaultIdent;
389
- break;
390
- }
391
- let importSplice = null;
392
- if (!identifier) {
393
- const filename = assetPath.slice(assetPath.lastIndexOf("/") + 1);
394
- const taken = collectTopLevelIdentifiers(ast);
395
- identifier = safeAssetIdentifier(filename, taken);
396
- const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
397
- const insertAt = imports.length > 0 ? imports[imports.length - 1].node.end : 0;
398
- const prefix = imports.length > 0 ? "\n" : "";
399
- importSplice = {
584
+ for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) return {
585
+ identifier: imp.defaultIdent,
586
+ importSplice: null
587
+ };
588
+ const filename = assetPath.slice(assetPath.lastIndexOf("/") + 1);
589
+ const identifier = safeAssetIdentifier(filename, collectTopLevelIdentifiers(ast));
590
+ const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
591
+ const last = imports[imports.length - 1];
592
+ const insertAt = last ? last.node.end ?? 0 : 0;
593
+ const prefix = last ? "\n" : "";
594
+ return {
595
+ identifier,
596
+ importSplice: {
400
597
  from: insertAt,
401
598
  to: insertAt,
402
599
  text: prefix + importStmt
403
- };
404
- }
600
+ }
601
+ };
602
+ }
603
+ function planAssetAttr(ast, element, attr, assetPath) {
604
+ if (!attr || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) return { error: "invalid attribute name" };
605
+ if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
606
+ const { identifier, importSplice } = planAssetImport(ast, assetPath);
607
+ const opening = element.openingElement;
405
608
  const newAttr = `${attr}={${identifier}}`;
406
609
  const existing = findJsxAttr(opening, attr);
407
- let attrSplice;
408
- if (existing) attrSplice = {
409
- from: existing.start,
410
- to: existing.end,
610
+ const attrSplice = existing ? {
611
+ from: existing.start ?? 0,
612
+ to: existing.end ?? 0,
411
613
  text: newAttr
614
+ } : {
615
+ from: opening.name.end ?? 0,
616
+ to: opening.name.end ?? 0,
617
+ text: ` ${newAttr}`
412
618
  };
413
- else {
414
- const name = opening.name;
415
- attrSplice = {
416
- from: name.end,
417
- to: name.end,
418
- text: ` ${newAttr}`
419
- };
420
- }
421
619
  return {
422
620
  importSplice,
423
621
  attrSplice
@@ -425,71 +623,36 @@ function planAssetAttr(ast, element, attr, assetPath) {
425
623
  }
426
624
  function readJsxStringAttr(opening, name) {
427
625
  const attr = findJsxAttr(opening, name);
428
- if (!attr) return null;
429
- const value = attr.value ?? null;
430
- if (!value) return null;
431
- if (value.type === "StringLiteral") return value.value;
432
- if (value.type === "JSXExpressionContainer") {
433
- const expr = value.expression;
434
- if (expr.type === "StringLiteral") return expr.value;
435
- }
626
+ const v = attr?.value;
627
+ if (!v) return null;
628
+ if (t$1.isStringLiteral(v)) return v.value;
629
+ if (t$1.isJSXExpressionContainer(v) && t$1.isStringLiteral(v.expression)) return v.expression.value;
436
630
  return null;
437
631
  }
438
632
  function readJsxNumberAttr(opening, name) {
439
633
  const attr = findJsxAttr(opening, name);
440
- if (!attr) return null;
441
- const value = attr.value ?? null;
442
- if (!value || value.type !== "JSXExpressionContainer") return null;
443
- const expr = value.expression;
444
- if (expr.type === "NumericLiteral") {
445
- const n = expr.value;
446
- return Number.isFinite(n) ? n : null;
447
- }
448
- return null;
634
+ const v = attr?.value;
635
+ if (!v || !t$1.isJSXExpressionContainer(v)) return null;
636
+ if (!t$1.isNumericLiteral(v.expression)) return null;
637
+ const n = v.expression.value;
638
+ return Number.isFinite(n) ? n : null;
449
639
  }
450
640
  function planReplacePlaceholder(ast, element, assetPath) {
451
641
  const opening = element.openingElement;
452
- if (!opening) return { error: "no opening element" };
453
- const elName = opening.name;
454
- if (elName?.type !== "JSXIdentifier" || elName.name !== "ImagePlaceholder") return { error: "not a placeholder" };
642
+ if (!t$1.isJSXIdentifier(opening.name) || opening.name.name !== "ImagePlaceholder") return { error: "not a placeholder" };
455
643
  if (!assetPath.startsWith("./assets/")) return { error: "asset path must start with ./assets/" };
456
644
  const hint = readJsxStringAttr(opening, "hint") ?? "";
457
645
  const width = readJsxNumberAttr(opening, "width");
458
646
  const height = readJsxNumberAttr(opening, "height");
459
- const imports = findImports$1(ast);
460
- let identifier = null;
461
- for (const imp of imports) if (imp.source === assetPath && imp.defaultIdent) {
462
- identifier = imp.defaultIdent;
463
- break;
464
- }
465
- let importSplice = null;
466
- if (!identifier) {
467
- const filename = assetPath.slice(assetPath.lastIndexOf("/") + 1);
468
- const taken = collectTopLevelIdentifiers(ast);
469
- identifier = safeAssetIdentifier(filename, taken);
470
- const importStmt = `import ${identifier} from '${assetPath.replace(/'/g, "\\'")}';\n`;
471
- const insertAt = imports.length > 0 ? imports[imports.length - 1].node.end : 0;
472
- const prefix = imports.length > 0 ? "\n" : "";
473
- importSplice = {
474
- from: insertAt,
475
- to: insertAt,
476
- text: prefix + importStmt
477
- };
478
- }
647
+ const { identifier, importSplice } = planAssetImport(ast, assetPath);
479
648
  const styleParts = [];
480
649
  if (width != null) styleParts.push(`width: ${width}`);
481
650
  if (height != null) styleParts.push(`height: ${height}`);
482
651
  styleParts.push(`objectFit: 'cover'`);
483
- const styleAttr = ` style={{ ${styleParts.join(", ")} }}`;
484
- const altAttr = ` alt=${jsString$1(hint)}`;
485
- const replacement = `<img src={${identifier}}${altAttr}${styleAttr} />`;
652
+ const replacement = `<img src={${identifier}} alt=${jsString$1(hint)} style={{ ${styleParts.join(", ")} }} />`;
486
653
  return {
487
654
  importSplice,
488
- elementSplice: {
489
- from: element.start,
490
- to: element.end,
491
- text: replacement
492
- }
655
+ elementSplice: spliceRange(element, replacement)
493
656
  };
494
657
  }
495
658
  function applyEdit(source, line, column, ops) {
@@ -497,7 +660,13 @@ function applyEdit(source, line, column, ops) {
497
660
  ok: true,
498
661
  source
499
662
  };
500
- const element = findInnermostJsxElement(source, line, column);
663
+ const ast = parseSource$1(source);
664
+ if (!ast) return {
665
+ ok: false,
666
+ status: 422,
667
+ error: "could not parse source"
668
+ };
669
+ const element = findInnermostJsxElement(ast, line, column);
501
670
  if (!element) return {
502
671
  ok: false,
503
672
  status: 422,
@@ -519,7 +688,7 @@ function applyEdit(source, line, column, ops) {
519
688
  }
520
689
  for (const op of ops) {
521
690
  if (op.kind !== "set-text") continue;
522
- const result = buildTextSplice(element, op.value);
691
+ const result = buildTextSplice(ast, element, op.value, op.prevText);
523
692
  if ("error" in result) return {
524
693
  ok: false,
525
694
  status: 422,
@@ -530,12 +699,6 @@ function applyEdit(source, line, column, ops) {
530
699
  const assetOps = ops.flatMap((op) => op.kind === "set-attr-asset" ? [op] : []);
531
700
  const placeholderOps = ops.flatMap((op) => op.kind === "replace-placeholder-with-image" ? [op] : []);
532
701
  if (assetOps.length > 0 || placeholderOps.length > 0) {
533
- const ast = parseSource$1(source);
534
- if (!ast) return {
535
- ok: false,
536
- status: 422,
537
- error: "could not parse source"
538
- };
539
702
  const importSplices = [];
540
703
  for (const op of assetOps) {
541
704
  const plan = planAssetAttr(ast, element, op.attr, op.assetPath);
@@ -1703,19 +1866,11 @@ function filesPlugin(opts) {
1703
1866
  //#region src/vite/loc-tags-plugin.ts
1704
1867
  const FORWARDING_COMPONENTS = new Set(["ImagePlaceholder"]);
1705
1868
  function isTaggableJsxName(name) {
1706
- if (!name || typeof name !== "object") return false;
1707
- const n = name;
1708
- if (n.type !== "JSXIdentifier" || typeof n.name !== "string") return false;
1709
- return /^[a-z]/.test(n.name) || FORWARDING_COMPONENTS.has(n.name);
1869
+ if (!t.isJSXIdentifier(name)) return false;
1870
+ return /^[a-z]/.test(name.name) || FORWARDING_COMPONENTS.has(name.name);
1710
1871
  }
1711
1872
  function alreadyTagged(opening) {
1712
- const attrs = opening.attributes ?? [];
1713
- for (const attr of attrs) {
1714
- if (attr.type !== "JSXAttribute") continue;
1715
- const name = attr.name;
1716
- if (name?.type === "JSXIdentifier" && name.name === "data-slide-loc") return true;
1717
- }
1718
- return false;
1873
+ return opening.attributes.some((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-slide-loc");
1719
1874
  }
1720
1875
  function injectLocTags(code) {
1721
1876
  let ast;
@@ -1730,17 +1885,13 @@ function injectLocTags(code) {
1730
1885
  }
1731
1886
  const insertions = [];
1732
1887
  walkJsx(ast, (node) => {
1733
- if (node.type !== "JSXElement") return;
1888
+ if (!t.isJSXElement(node) || !node.loc) return;
1734
1889
  const opening = node.openingElement;
1735
- if (!opening) return;
1736
1890
  const name = opening.name;
1737
- if (!isTaggableJsxName(name)) return;
1738
- if (alreadyTagged(opening)) return;
1739
- const loc = node.loc;
1740
- if (!loc) return;
1891
+ if (!isTaggableJsxName(name) || alreadyTagged(opening)) return;
1741
1892
  insertions.push({
1742
- offset: name.end,
1743
- text: ` data-slide-loc="${loc.start.line}:${loc.start.column}"`
1893
+ offset: name.end ?? 0,
1894
+ text: ` data-slide-loc="${node.loc.start.line}:${node.loc.start.column}"`
1744
1895
  });
1745
1896
  });
1746
1897
  if (insertions.length === 0) return null;
@@ -1953,6 +2104,7 @@ async function createViteConfig(opts) {
1953
2104
  return {
1954
2105
  root: APP_ROOT,
1955
2106
  configFile: false,
2107
+ envDir: userCwd,
1956
2108
  plugins: [
1957
2109
  locTagsPlugin({
1958
2110
  userCwd,