dsh-codex-subscription 2.2.1 → 2.2.2

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.
package/lib/client.js CHANGED
@@ -877,6 +877,8 @@ window.__ModuleLoader__.load({
877
877
  "runtime/install",
878
878
  "runtime/remove",
879
879
  "runtime/cancel",
880
+ "storage/status",
881
+ "storage/clear-forecast",
880
882
  "reset-credit/inspect",
881
883
  "reset-credit/prepare",
882
884
  "reset-credit/consume",
@@ -1294,6 +1296,53 @@ window.__ModuleLoader__.load({
1294
1296
  });
1295
1297
  }
1296
1298
  //#endregion
1299
+ //#region src/sketch-picture-panel.jsx
1300
+ function SketchPicturePanel({ document, disabled, pictureInput, importImage, runFile, change, t }) {
1301
+ const pictures = document.layers.filter((layer) => layer.image);
1302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
1303
+ className: "codexSketchPictures",
1304
+ children: [
1305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchPictures") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1306
+ type: "button",
1307
+ disabled,
1308
+ onClick: () => pictureInput.current.click(),
1309
+ children: t("sketchPictureAdd")
1310
+ })] }),
1311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1312
+ ref: pictureInput,
1313
+ hidden: true,
1314
+ type: "file",
1315
+ accept: "image/png,image/jpeg,image/webp",
1316
+ onChange: (event) => {
1317
+ const file = event.target.files?.[0];
1318
+ event.target.value = "";
1319
+ if (file) runFile(() => importImage(file));
1320
+ }
1321
+ }),
1322
+ pictures.map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1323
+ "data-active": layer.id === document.active,
1324
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1325
+ type: "button",
1326
+ disabled,
1327
+ "aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
1328
+ onClick: () => change("select", layer.id),
1329
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1330
+ src: layer.image.src,
1331
+ alt: layer.name
1332
+ })
1333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1334
+ type: "button",
1335
+ disabled,
1336
+ "aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
1337
+ onClick: () => change(document.layers.length === 1 ? "clear" : "delete", layer.id),
1338
+ children: "×"
1339
+ })]
1340
+ }, layer.id)),
1341
+ pictures.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
1342
+ ]
1343
+ });
1344
+ }
1345
+ //#endregion
1297
1346
  //#region src/sketch-tool-picker.jsx
1298
1347
  function SketchToolPicker({ t, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen }) {
1299
1348
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1382,1460 +1431,1641 @@ window.__ModuleLoader__.load({
1382
1431
  });
1383
1432
  }
1384
1433
  //#endregion
1385
- //#region src/sketch-objects.js
1386
- const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1387
- const identifyObjects = (doc) => ({
1388
- ...doc,
1389
- layers: doc.layers.map((layer) => ({
1390
- ...layer,
1391
- strokes: layer.strokes.map((s, i) => s.id ? s : {
1392
- ...s,
1393
- id: objectId(s, i)
1394
- })
1395
- }))
1396
- });
1397
- function objectBounds(stroke) {
1398
- const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1399
- return {
1400
- x: Math.min(...xs),
1401
- y: Math.min(...ys),
1402
- width: Math.max(...xs) - Math.min(...xs),
1403
- height: Math.max(...ys) - Math.min(...ys)
1404
- };
1434
+ //#region src/sketch-interactions.js
1435
+ function useSketchDismiss(open, close, host, selectors) {
1436
+ const latest = (0, react.useRef)(close);
1437
+ latest.current = close;
1438
+ (0, react.useEffect)(() => {
1439
+ if (!open) return;
1440
+ const dialog = host.current?.closest("dialog") ?? host.current;
1441
+ if (!dialog) return;
1442
+ const pointer = (event) => {
1443
+ if (selectors.some((selector) => event.target.closest?.(selector))) return;
1444
+ latest.current(false);
1445
+ if (event.target.matches?.("canvas")) {
1446
+ event.preventDefault();
1447
+ event.stopPropagation();
1448
+ event.target.focus({ preventScroll: true });
1449
+ }
1450
+ };
1451
+ const key = (event) => {
1452
+ if (event.key !== "Escape" || event.target.closest?.("dialog") !== dialog) return;
1453
+ event.preventDefault();
1454
+ event.stopPropagation();
1455
+ latest.current(false);
1456
+ dialog.querySelector("canvas")?.focus({ preventScroll: true });
1457
+ };
1458
+ const hidden = () => latest.current(false);
1459
+ document.addEventListener("pointerdown", pointer, true);
1460
+ document.addEventListener("keydown", key, true);
1461
+ dialog.addEventListener("close", hidden);
1462
+ return () => {
1463
+ document.removeEventListener("pointerdown", pointer, true);
1464
+ document.removeEventListener("keydown", key, true);
1465
+ dialog.removeEventListener("close", hidden);
1466
+ };
1467
+ }, [
1468
+ open,
1469
+ host,
1470
+ selectors.join("|")
1471
+ ]);
1405
1472
  }
1406
- function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1407
- if (![
1408
- dx,
1409
- dy,
1410
- scaleX,
1411
- scaleY
1412
- ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1413
- const box = objectBounds(stroke);
1414
- const points = stroke.points.map((p) => ({
1415
- x: box.x + (p.x - box.x) * scaleX + dx,
1416
- y: box.y + (p.y - box.y) * scaleY + dy
1417
- }));
1418
- if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
1473
+ function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
1474
+ const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
1475
+ const update = (event, bounds) => {
1476
+ if (event) last.current = event;
1477
+ const pointer = last.current, node = canvas.current, cursor = ring.current;
1478
+ if (!pointer || !node || !cursor) return;
1479
+ const rect = bounds ?? node.getBoundingClientRect();
1480
+ if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
1481
+ cursor.hidden = true;
1482
+ return;
1483
+ }
1484
+ const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
1485
+ cursor.hidden = false;
1486
+ cursor.style.width = `${diameter}px`;
1487
+ cursor.style.height = `${diameter}px`;
1488
+ cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
1489
+ };
1490
+ (0, react.useEffect)(() => {
1491
+ update();
1492
+ const observer = new ResizeObserver(() => update());
1493
+ if (canvas.current) observer.observe(canvas.current);
1494
+ return () => observer.disconnect();
1495
+ }, [
1496
+ width,
1497
+ brush,
1498
+ zoom,
1499
+ hidden
1500
+ ]);
1419
1501
  return {
1420
- ...stroke,
1421
- points
1502
+ down: (event) => {
1503
+ heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
1504
+ },
1505
+ move: (event, bounds) => update({
1506
+ clientX: event.clientX,
1507
+ clientY: event.clientY,
1508
+ pointerId: event.pointerId,
1509
+ pointerType: event.pointerType,
1510
+ pressure: event.pressure
1511
+ }, bounds),
1512
+ leave: () => {
1513
+ last.current = null;
1514
+ if (ring.current) ring.current.hidden = true;
1515
+ }
1422
1516
  };
1423
1517
  }
1424
- function sketchObjectSummary(doc) {
1425
- return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
1426
- layer: layer.id,
1427
- id: objectId(stroke, i),
1428
- shape: stroke.shape,
1429
- color: stroke.color,
1430
- bounds: objectBounds(stroke),
1431
- ...stroke.text ? { text: stroke.text } : {}
1432
- })));
1433
- }
1434
1518
  //#endregion
1435
- //#region src/sketch-input.js
1436
- function snapLine(start, end) {
1437
- const angle = Math.round(Math.atan2(end.y - start.y, end.x - start.x) / (Math.PI / 4)) * Math.PI / 4;
1438
- const length = Math.hypot(end.x - start.x, end.y - start.y);
1439
- return {
1440
- x: start.x + Math.cos(angle) * length,
1441
- y: start.y + Math.sin(angle) * length
1442
- };
1519
+ //#region src/sketch-shortcuts.js
1520
+ function sketchShortcutAction(keys, key) {
1521
+ key = key.toLowerCase();
1522
+ return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
1523
+ v: "select",
1524
+ t: "text",
1525
+ "+": "zoomIn"
1526
+ }[key];
1443
1527
  }
1444
- function smoothStrokePoints(points, strength = 0) {
1445
- if (!strength || points.length < 3) return points;
1446
- const radius = Math.max(1, Math.round(strength / 25)), amount = Math.min(1, strength / 75);
1447
- return points.map((point, i) => {
1448
- if (i === 0 || i === points.length - 1) return point;
1449
- let x = 0, y = 0, weight = 0;
1450
- for (let j = Math.max(0, i - radius); j <= Math.min(points.length - 1, i + radius); j++) {
1451
- const w = radius + 1 - Math.abs(j - i);
1452
- x += points[j].x * w;
1453
- y += points[j].y * w;
1454
- weight += w;
1528
+ //#endregion
1529
+ //#region src/sketch-view.jsx
1530
+ const DEFAULT_KEYS = {
1531
+ pen: "b",
1532
+ eraser: "e",
1533
+ line: "l",
1534
+ rectangle: "r",
1535
+ circle: "o",
1536
+ pan: " ",
1537
+ zoomIn: "=",
1538
+ zoomOut: "-",
1539
+ fit: "0"
1540
+ };
1541
+ function useSketchView(canvas, open) {
1542
+ const [view, setView] = (0, react.useState)({
1543
+ scale: 1,
1544
+ x: 0,
1545
+ y: 0
1546
+ }), [keys, setKeys] = (0, react.useState)(() => {
1547
+ try {
1548
+ return {
1549
+ ...DEFAULT_KEYS,
1550
+ ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
1551
+ };
1552
+ } catch {
1553
+ return DEFAULT_KEYS;
1455
1554
  }
1456
- return {
1457
- ...point,
1458
- x: point.x + (x / weight - point.x) * amount,
1459
- y: point.y + (y / weight - point.y) * amount
1460
- };
1461
1555
  });
1462
- }
1463
- //#endregion
1464
- //#region src/sketch-gesture.js
1465
- function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
1466
- if (!samples.length) return doc;
1467
- if (gesture.object) {
1468
- const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
1469
- if (!point) return doc;
1470
- const box = objectBounds(gesture.object);
1471
- const stroke = gesture.handle === "end" ? {
1472
- ...gesture.object,
1473
- points: [gesture.object.points[0], point]
1474
- } : gesture.handle === "size" ? transformObject(gesture.object, {
1475
- scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
1476
- scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
1477
- }) : transformObject(gesture.object, {
1478
- dx: Math.max(-box.x, Math.min(1 - box.x - box.width, point.x - gesture.start.x)),
1479
- dy: Math.max(-box.y, Math.min(1 - box.y - box.height, point.y - gesture.start.y))
1480
- });
1481
- doc = {
1482
- ...doc,
1483
- layers: doc.layers.map((l) => l.id === gesture.layer ? {
1484
- ...l,
1485
- strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
1486
- } : l)
1556
+ const [shortcuts, setShortcuts] = (0, react.useState)(() => {
1557
+ try {
1558
+ return localStorage.getItem("codex-sketch-shortcuts") !== "off";
1559
+ } catch {
1560
+ return true;
1561
+ }
1562
+ }), [space, setSpace] = (0, react.useState)(false);
1563
+ const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
1564
+ viewRef.current = view;
1565
+ const zoom = (factor) => setView((v) => ({
1566
+ ...v,
1567
+ scale: Math.max(.25, Math.min(8, v.scale * factor))
1568
+ }));
1569
+ const reset = () => setView({
1570
+ scale: 1,
1571
+ x: 0,
1572
+ y: 0
1573
+ });
1574
+ (0, react.useEffect)(() => {
1575
+ const node = canvas.current;
1576
+ if (!node || !open) return;
1577
+ const wheel = (e) => {
1578
+ if (!shortcuts || !e.altKey) return;
1579
+ e.preventDefault();
1580
+ zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
1487
1581
  };
1488
- return doc;
1489
- }
1490
- for (const sample of samples) {
1491
- let point = sketchPoint(sample.clientX, sample.clientY, rect);
1492
- if (!point) continue;
1493
- const layer = doc.layers.find((layer) => layer.id === gesture.layer);
1494
- if (!layer) return doc;
1495
- if (gesture.eraseStroke) {
1496
- const previous = gesture.last ?? point;
1497
- const steps = Math.min(256, Math.max(1, Math.ceil(Math.hypot(point.x - previous.x, point.y - previous.y) * SKETCH_SIZE / Math.max(2, width / 2))));
1498
- layer.strokes = layer.strokes.filter((stroke) => {
1499
- for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
1500
- x: previous.x + (point.x - previous.x) * i / steps,
1501
- y: previous.y + (point.y - previous.y) * i / steps
1502
- }, width / 2, doc.width, doc.height)) return false;
1582
+ node.addEventListener("wheel", wheel, { passive: false });
1583
+ return () => node.removeEventListener("wheel", wheel);
1584
+ }, [open, shortcuts]);
1585
+ (0, react.useEffect)(() => {
1586
+ const stop = () => {
1587
+ drag.current = null;
1588
+ setSpace(false);
1589
+ };
1590
+ window.addEventListener("blur", stop);
1591
+ return () => window.removeEventListener("blur", stop);
1592
+ }, []);
1593
+ (0, react.useEffect)(() => {
1594
+ if (!open || !shortcuts) {
1595
+ setSpace(false);
1596
+ drag.current = null;
1597
+ }
1598
+ }, [open, shortcuts]);
1599
+ const setKey = (action, key) => {
1600
+ key = key.toLowerCase();
1601
+ if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
1602
+ const next = {
1603
+ ...keys,
1604
+ [action]: key
1605
+ };
1606
+ setKeys(next);
1607
+ try {
1608
+ localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
1609
+ } catch {}
1610
+ };
1611
+ return {
1612
+ view,
1613
+ keys,
1614
+ shortcuts,
1615
+ space,
1616
+ zoom,
1617
+ reset,
1618
+ setKey,
1619
+ toggle: () => setShortcuts((v) => {
1620
+ try {
1621
+ localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
1622
+ } catch {}
1623
+ return !v;
1624
+ }),
1625
+ keyDown: (e) => {
1626
+ if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
1627
+ const action = sketchShortcutAction(keys, e.key);
1628
+ if (action === "pan") {
1629
+ e.preventDefault();
1630
+ setSpace(true);
1503
1631
  return true;
1504
- });
1505
- } else {
1506
- const stroke = layer.strokes.at(-1);
1507
- if (!stroke) return doc;
1632
+ }
1508
1633
  if ([
1509
- "line",
1510
- "arrow",
1511
- "rectangle",
1512
- "circle"
1513
- ].includes(stroke.shape)) {
1514
- if (stroke.shape === "line" && shiftKey) {
1515
- const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
1516
- const snapped = snapLine({
1517
- x: a.x * w,
1518
- y: a.y * h
1519
- }, {
1520
- x: point.x * w,
1521
- y: point.y * h
1522
- });
1523
- point = {
1524
- x: snapped.x / w,
1525
- y: snapped.y / h
1526
- };
1527
- }
1528
- stroke.points = [stroke.points[0], point];
1529
- } else {
1530
- const last = stroke.points.at(-1);
1531
- if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
1532
- if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
1533
- stroke.points.push(point);
1634
+ "zoomIn",
1635
+ "zoomOut",
1636
+ "fit"
1637
+ ].includes(action)) {
1638
+ e.preventDefault();
1639
+ if (action === "fit") reset();
1640
+ else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
1641
+ return true;
1534
1642
  }
1643
+ return false;
1644
+ },
1645
+ keyUp: (e) => {
1646
+ if (e.key.toLowerCase() === keys.pan) setSpace(false);
1647
+ },
1648
+ down: (e) => {
1649
+ if (e.button !== 1 && !space) return false;
1650
+ e.preventDefault();
1651
+ drag.current = {
1652
+ id: e.pointerId,
1653
+ x: e.clientX,
1654
+ y: e.clientY,
1655
+ view: viewRef.current
1656
+ };
1657
+ canvas.current.setPointerCapture(e.pointerId);
1658
+ return true;
1659
+ },
1660
+ move: (e) => {
1661
+ const d = drag.current;
1662
+ if (!d || d.id !== e.pointerId) return false;
1663
+ setView({
1664
+ ...d.view,
1665
+ x: d.view.x + e.clientX - d.x,
1666
+ y: d.view.y + e.clientY - d.y
1667
+ });
1668
+ return true;
1669
+ },
1670
+ end: (e) => {
1671
+ if (drag.current?.id !== e.pointerId) return false;
1672
+ drag.current = null;
1673
+ if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
1674
+ return true;
1535
1675
  }
1536
- gesture.last = point;
1537
- }
1538
- return doc;
1676
+ };
1539
1677
  }
1540
- //#endregion
1541
- //#region src/sketch-drafts.js
1542
- const DATABASE = "dsh-codex-sketches-v1";
1543
- const MAX_STORAGE = 32 * 1024 * 1024;
1544
- const metadata = (kind, row) => ({
1545
- key: `${kind}:${row.id}`,
1546
- kind,
1547
- id: row.id,
1548
- name: row.name,
1549
- updated: row.updated,
1550
- size: JSON.stringify(row).length
1551
- });
1552
- async function sketchDrafts(action, value, recoverySession) {
1553
- const db = await new Promise((resolve, reject) => {
1554
- let blocked = false;
1555
- const request = indexedDB.open(DATABASE, 2);
1556
- request.onblocked = () => {
1557
- blocked = true;
1558
- reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
1559
- };
1560
- request.onupgradeneeded = () => {
1561
- if (blocked) {
1562
- request.transaction.abort();
1563
- return;
1564
- }
1565
- const db = request.result, tx = request.transaction;
1566
- if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
1567
- const meta = db.createObjectStore("metadata", { keyPath: "key" });
1568
- db.createObjectStore("recovery", { keyPath: "id" });
1569
- const cursor = tx.objectStore("drafts").openCursor();
1570
- cursor.onsuccess = () => {
1571
- const row = cursor.result;
1572
- if (row) {
1573
- meta.put(metadata("drafts", row.value));
1574
- row.continue();
1678
+ function SketchViewControls({ navigation, t }) {
1679
+ const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
1680
+ const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
1681
+ const close = () => {
1682
+ setOpen(false);
1683
+ trigger.current?.focus({ preventScroll: true });
1684
+ };
1685
+ useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
1686
+ (0, react.useLayoutEffect)(() => {
1687
+ if (!open) return;
1688
+ const dialog = host.current.closest("dialog");
1689
+ const place = () => {
1690
+ const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
1691
+ const width = Math.min(360, box.width - 24);
1692
+ setPlacement({
1693
+ dialog,
1694
+ style: {
1695
+ width,
1696
+ left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
1697
+ bottom: box.bottom - anchor.top + 8,
1698
+ maxHeight: Math.max(80, anchor.top - box.top - 24)
1575
1699
  }
1576
- };
1700
+ });
1577
1701
  };
1578
- request.onsuccess = () => {
1579
- if (blocked) {
1580
- request.result.close();
1581
- return;
1582
- }
1583
- request.result.onversionchange = () => request.result.close();
1584
- resolve(request.result);
1702
+ place();
1703
+ const observer = new ResizeObserver(place);
1704
+ observer.observe(dialog);
1705
+ observer.observe(host.current);
1706
+ window.addEventListener("resize", place);
1707
+ return () => {
1708
+ observer.disconnect();
1709
+ window.removeEventListener("resize", place);
1585
1710
  };
1586
- request.onerror = () => reject(request.error);
1587
- });
1588
- try {
1589
- return await new Promise((resolve, reject) => {
1590
- const write = [
1591
- "save",
1592
- "delete",
1593
- "checkpoint",
1594
- "clearRecovery"
1595
- ].includes(action);
1596
- const tx = db.transaction([
1597
- "drafts",
1598
- "metadata",
1599
- "recovery"
1600
- ], write ? "readwrite" : "readonly");
1601
- const meta = tx.objectStore("metadata"), kind = [
1602
- "checkpoint",
1603
- "recover",
1604
- "clearRecovery"
1605
- ].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
1606
- let result, failure;
1607
- tx.oncomplete = () => resolve(result);
1608
- tx.onerror = () => reject(tx.error);
1609
- tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
1610
- if (action === "get" || action === "recover") {
1611
- const req = store.get(value);
1612
- req.onsuccess = () => {
1613
- result = req.result;
1614
- };
1615
- return;
1616
- }
1617
- if (action === "delete" || action === "clearRecovery") {
1618
- store.delete(value);
1619
- meta.delete(`${kind}:${value}`);
1620
- return;
1621
- }
1622
- if (![
1623
- "list",
1624
- "save",
1625
- "checkpoint"
1626
- ].includes(action)) {
1627
- tx.abort();
1628
- return;
1629
- }
1630
- const request = meta.getAll();
1631
- request.onsuccess = () => {
1632
- const rows = request.result;
1633
- if (action === "list") {
1634
- result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
1635
- return;
1636
- }
1637
- const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
1638
- const code = others.filter((row) => row.kind === kind).length >= 20 ? "SKETCH_DRAFT_LIMIT" : others.reduce((n, row) => n + row.size, 0) + next.size > MAX_STORAGE ? "SKETCH_STORAGE_LIMIT" : null;
1639
- if (code) {
1640
- failure = Object.assign(Error("Draft storage limit reached"), { code });
1641
- tx.abort();
1642
- return;
1643
- }
1644
- store.put(value);
1645
- meta.put(next);
1646
- if (action === "save" && recoverySession) {
1647
- tx.objectStore("recovery").delete(recoverySession);
1648
- meta.delete(`recovery:${recoverySession}`);
1649
- }
1650
- result = value;
1651
- };
1652
- });
1653
- } finally {
1654
- db.close();
1655
- }
1656
- }
1657
- async function decodeSketchImages(doc, images) {
1658
- for (const layer of doc.layers) {
1659
- const src = layer.image?.src;
1660
- if (!src || images.has(src)) continue;
1661
- if (!src.startsWith("data:image/png;base64,") || src.length > 8 * 1024 * 1024) throw Error("Invalid image");
1662
- const image = new Image();
1663
- image.src = src;
1664
- await image.decode();
1665
- images.set(src, image);
1666
- }
1667
- }
1668
- async function importSketchImage(file) {
1669
- if (![
1670
- "image/png",
1671
- "image/jpeg",
1672
- "image/webp"
1673
- ].includes(file.type) || file.size > 20 * 1024 * 1024) throw Error("Image must be PNG, JPEG or WebP under 20 MB");
1674
- const bitmap = await createImageBitmap(file);
1675
- try {
1676
- if (bitmap.width * bitmap.height > 32 * 1024 * 1024) throw Error("Image too large");
1677
- const scale = Math.min(1, 1024 / Math.max(bitmap.width, bitmap.height));
1678
- const canvas = document.createElement("canvas");
1679
- canvas.width = Math.max(1, Math.round(bitmap.width * scale));
1680
- canvas.height = Math.max(1, Math.round(bitmap.height * scale));
1681
- canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
1682
- return {
1683
- src: canvas.toDataURL("image/png"),
1684
- width: canvas.width,
1685
- height: canvas.height
1686
- };
1687
- } finally {
1688
- bitmap.close();
1689
- }
1711
+ }, [open]);
1712
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1713
+ ref: host,
1714
+ className: "codexSketchViewControls",
1715
+ children: [
1716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1717
+ type: "button",
1718
+ "aria-label": t("sketchZoomOut"),
1719
+ onClick: () => navigation.zoom(1 / 1.2),
1720
+ children: "−"
1721
+ }),
1722
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1723
+ type: "button",
1724
+ title: t("sketchFit"),
1725
+ onClick: navigation.reset,
1726
+ children: [Math.round(navigation.view.scale * 100), "%"]
1727
+ }),
1728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1729
+ type: "button",
1730
+ "aria-label": t("sketchZoomIn"),
1731
+ onClick: () => navigation.zoom(1.2),
1732
+ children: "+"
1733
+ }),
1734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1735
+ ref: trigger,
1736
+ type: "button",
1737
+ "aria-expanded": open,
1738
+ onClick: () => setOpen(!open),
1739
+ children: t("sketchKeys")
1740
+ }),
1741
+ open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1742
+ className: "codexSketchKeyPanel",
1743
+ "aria-label": t("sketchKeys"),
1744
+ style: placement.style,
1745
+ onKeyDown: (e) => {
1746
+ if (e.key === "Escape") {
1747
+ e.preventDefault();
1748
+ e.stopPropagation();
1749
+ close();
1750
+ }
1751
+ },
1752
+ children: [
1753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchKeys") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1754
+ type: "button",
1755
+ "aria-label": t("sketchFileClose"),
1756
+ onClick: close,
1757
+ children: "×"
1758
+ })] }),
1759
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1760
+ className: "codexSketchKeysEnabled",
1761
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1762
+ type: "checkbox",
1763
+ checked: navigation.shortcuts,
1764
+ onChange: navigation.toggle
1765
+ })]
1766
+ }),
1767
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
1768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1769
+ className: "codexSketchKeyGrid",
1770
+ children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1771
+ "aria-label": t(`sketchKey_${action}`),
1772
+ value: key === " " ? "Space" : key,
1773
+ readOnly: true,
1774
+ onKeyDown: (e) => {
1775
+ if (e.key === "Tab" || e.key === "Escape") return;
1776
+ e.preventDefault();
1777
+ e.stopPropagation();
1778
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
1779
+ }
1780
+ })] }, action))
1781
+ }),
1782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
1783
+ ]
1784
+ }), placement.dialog) : null
1785
+ ]
1786
+ });
1690
1787
  }
1691
1788
  //#endregion
1692
- //#region src/sketch-presets.js
1693
- const SKETCH_PRESETS = [
1694
- "triangle",
1695
- "diamond",
1696
- "star"
1789
+ //#region src/sketch-controls.jsx
1790
+ const PALETTE = [
1791
+ "#18181b",
1792
+ "#929398",
1793
+ "#ff3936",
1794
+ "#ff9500",
1795
+ "#ffcc00",
1796
+ "#34c759",
1797
+ "#0088ff"
1697
1798
  ];
1698
- function expandSketchPreset(command) {
1699
- if (!SKETCH_PRESETS.includes(command.shape)) return command;
1700
- const points = command.points;
1701
- if (!Array.isArray(points) || points.length !== 2 || points.some((p) => !p || ![p.x, p.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))) throw Error("Shape preset requires two normalized bounding-box corners");
1702
- const [a, b] = points, x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
1703
- const w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);
1704
- if (!w || !h) throw Error("Shape preset requires a non-empty box");
1705
- const vertices = command.shape === "triangle" ? [
1706
- [.5, 0],
1707
- [1, 1],
1708
- [0, 1]
1709
- ] : command.shape === "diamond" ? [
1710
- [.5, 0],
1711
- [1, .5],
1712
- [.5, 1],
1713
- [0, .5]
1714
- ] : Array.from({ length: 10 }, (_, i) => {
1715
- const angle = i * Math.PI / 5 - Math.PI / 2, radius = i % 2 ? .22 : .5;
1716
- return [.5 + Math.cos(angle) * radius, .5 + Math.sin(angle) * radius];
1799
+ function SketchControls({ t, navigation, picturesOpen, setPicturesOpen, disabled, tool, brush, chooseBrush, chooseTool, shapesOpen, setShapesOpen, fillShape, setFillShape, selected, editObject, selection, setTextEdit, stability, setStability, eraser, setEraser, color, pickColor }) {
1800
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1801
+ className: "codexSketchControls",
1802
+ children: [
1803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1804
+ type: "button",
1805
+ className: "codexSketchPicturesToggle",
1806
+ "aria-expanded": picturesOpen,
1807
+ onClick: () => setPicturesOpen((value) => !value),
1808
+ children: t("sketchPictures")
1809
+ }),
1810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchViewControls, {
1811
+ navigation,
1812
+ t
1813
+ }),
1814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
1815
+ t,
1816
+ disabled,
1817
+ tool,
1818
+ brush,
1819
+ chooseBrush,
1820
+ chooseTool,
1821
+ shapesOpen,
1822
+ setShapesOpen
1823
+ }),
1824
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1825
+ className: "codexLayerBrush",
1826
+ children: [
1827
+ ["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1828
+ type: "checkbox",
1829
+ disabled,
1830
+ checked: fillShape,
1831
+ onChange: (event) => setFillShape(event.target.checked)
1832
+ }), t("sketchFill")] }) : null,
1833
+ selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1834
+ className: "codexSketchObjectActions",
1835
+ children: [
1836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1837
+ disabled,
1838
+ onClick: () => editObject({}, "duplicate"),
1839
+ children: t("sketchObjectDuplicate")
1840
+ }),
1841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1842
+ disabled,
1843
+ onClick: () => editObject({}, "delete"),
1844
+ children: t("sketchObjectDelete")
1845
+ }),
1846
+ selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1847
+ disabled,
1848
+ onClick: () => setTextEdit({
1849
+ selection,
1850
+ value: selected.text
1851
+ }),
1852
+ children: t("sketchText")
1853
+ }) : null
1854
+ ]
1855
+ }) : null,
1856
+ tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1857
+ className: "codexSketchStability",
1858
+ children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1859
+ "aria-label": t("sketchStability"),
1860
+ value: stability,
1861
+ disabled,
1862
+ onChange: (event) => setStability(Number(event.target.value)),
1863
+ children: [
1864
+ 0,
1865
+ 25,
1866
+ 50,
1867
+ 75
1868
+ ].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1869
+ value,
1870
+ children: t(`sketchStability${value}`)
1871
+ }, value))
1872
+ })]
1873
+ }) : null,
1874
+ tool === "eraser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1875
+ className: "codexSketchSegment",
1876
+ role: "group",
1877
+ "aria-label": t("sketchEraserMode"),
1878
+ children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1879
+ type: "button",
1880
+ "aria-pressed": eraser === value,
1881
+ disabled,
1882
+ onClick: () => setEraser(value),
1883
+ children: t(`sketchErase_${value}`)
1884
+ }, value))
1885
+ }) : null
1886
+ ]
1887
+ }),
1888
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1889
+ className: "codexSketchPalette",
1890
+ role: "group",
1891
+ "aria-label": t("sketchColor"),
1892
+ children: [PALETTE.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1893
+ type: "button",
1894
+ className: "codexSketchSwatch",
1895
+ style: { "--swatch": value },
1896
+ "aria-label": `${t("sketchColor")} ${value}`,
1897
+ "aria-pressed": (selected?.color ?? color) === value,
1898
+ disabled,
1899
+ onClick: () => pickColor(value)
1900
+ }, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1901
+ className: "codexSketchCustom",
1902
+ title: t("sketchColor"),
1903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { background: color } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1904
+ type: "color",
1905
+ "aria-label": t("sketchColor"),
1906
+ value: color,
1907
+ disabled,
1908
+ onChange: (event) => pickColor(event.target.value)
1909
+ })]
1910
+ })]
1911
+ })
1912
+ ]
1717
1913
  });
1914
+ }
1915
+ //#endregion
1916
+ //#region src/sketch-import-confirm.jsx
1917
+ function useSketchImportConfirm(t) {
1918
+ const [pending, setPending] = (0, react.useState)(false);
1919
+ const resolve = (0, react.useRef)(null);
1920
+ const dialog = (0, react.useRef)(null);
1921
+ const finish = (accepted) => {
1922
+ const done = resolve.current;
1923
+ resolve.current = null;
1924
+ setPending(false);
1925
+ done?.(accepted);
1926
+ };
1927
+ (0, react.useEffect)(() => {
1928
+ if (pending) dialog.current?.showModal();
1929
+ else dialog.current?.close();
1930
+ }, [pending]);
1931
+ (0, react.useEffect)(() => () => {
1932
+ resolve.current?.(false);
1933
+ resolve.current = null;
1934
+ }, []);
1718
1935
  return {
1719
- ...command,
1720
- shape: "polygon",
1721
- points: vertices.map(([u, v]) => ({
1722
- x: x + u * w,
1723
- y: y + v * h
1724
- }))
1936
+ confirmPsd: () => new Promise((done) => {
1937
+ resolve.current?.(false);
1938
+ resolve.current = done;
1939
+ setPending(true);
1940
+ }),
1941
+ prompt: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
1942
+ ref: dialog,
1943
+ className: "codexSketchImportConfirm",
1944
+ "aria-label": t("sketchPsdConfirmTitle"),
1945
+ onKeyDown: (event) => event.stopPropagation(),
1946
+ onCancel: (event) => {
1947
+ event.preventDefault();
1948
+ event.stopPropagation();
1949
+ finish(false);
1950
+ },
1951
+ children: [
1952
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("sketchPsdConfirmTitle") }),
1953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchPsdConfirmBody") }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1955
+ type: "button",
1956
+ autoFocus: true,
1957
+ onClick: () => finish(false),
1958
+ children: t("sketchCancel")
1959
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1960
+ type: "button",
1961
+ onClick: () => finish(true),
1962
+ children: t("sketchImport")
1963
+ })] })
1964
+ ]
1965
+ })
1725
1966
  };
1726
1967
  }
1727
- const SKETCH_COMMAND_HELP = {
1728
- coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
1729
- shapes: "line: exactly two endpoints; rectangle/circle (ellipse alias accepted): exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. bezier: start point, then groups of control1/control2/end; use 4 points for one cubic curve, max 64 segments. Prefer bezier for smooth designed curves instead of many pen samples. fill:true closes and fills the curve. fill:true fills rectangle/circle/polygon. Layers and strokes paint in list order, later ones on top. All commands needed for drawing are described here; no source-code search is required.",
1730
- commands: {
1731
- preset: "triangle/diamond/star: {op:\"stroke\",id:\"badge\",shape:\"star\",points:[{x:0.1,y:0.1},{x:0.3,y:0.3}],color:\"#ffcc00\",fill:true}. Two opposite box corners; stored as editable polygon. Inspect objectId for one object without repeating full help.",
1732
- stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|arrow|text|rectangle|circle|polygon|bezier\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
1733
- layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
1734
- curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
1735
- object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
1736
- resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
1737
- },
1738
- limits: {
1739
- strokes: MAX_SKETCH_STROKES,
1740
- pointsPerStroke: MAX_STROKE_POINTS,
1741
- pointsTotal: 2e5,
1742
- commandsPerBatch: 256
1743
- }
1744
- };
1745
- const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1746
- function applySketchCommands(source, commands) {
1747
- if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
1748
- let doc = identifyObjects(source);
1749
- for (let command of commands) {
1750
- if (!command || typeof command !== "object") throw Error("Invalid command");
1751
- if (command.op === "stroke") command = expandSketchPreset(command);
1752
- if (command.op === "resize") {
1753
- doc = resizeSketch(doc, command.ratio);
1754
- continue;
1755
- }
1756
- if (command.op === "layer") {
1757
- if (![
1758
- "add",
1759
- "select",
1760
- "rename",
1761
- "visible",
1762
- "duplicate",
1763
- "up",
1764
- "down",
1765
- "delete",
1766
- "clear"
1767
- ].includes(command.action)) throw Error("Unknown layer action");
1768
- if (command.action === "add") {
1769
- const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
1770
- if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
1771
- const next = changeSketchLayer(doc, "add", after);
1772
- if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
1773
- doc = {
1774
- ...next,
1775
- active: id,
1776
- nextId: Math.max(next.nextId, id + 1),
1777
- layers: next.layers.map((l) => l.id === next.active ? {
1778
- ...l,
1779
- id,
1780
- name: String(command.value ?? "").trim().slice(0, 40)
1781
- } : l)
1782
- };
1783
- continue;
1784
- }
1785
- const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
1786
- if (next === doc) throw Error("Layer action unavailable; inspect the document first");
1787
- doc = next;
1788
- continue;
1789
- }
1790
- if (command.op === "object") {
1791
- const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
1792
- if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
1793
- const strokes = layer.strokes.slice(), original = strokes[index];
1794
- if (command.action === "delete") strokes.splice(index, 1);
1795
- else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
1796
- ...original,
1797
- id: crypto.randomUUID(),
1798
- points: original.points.map((p) => ({ ...p }))
1799
- });
1800
- else if (command.action === "update") {
1801
- const patch = command.patch ?? {};
1802
- if (Object.keys(patch).some((k) => ![
1803
- "color",
1804
- "width",
1805
- "opacity",
1806
- "fill",
1807
- "text",
1808
- "points"
1809
- ].includes(k))) throw Error("Unsupported object property");
1810
- const changed = command.transform ? transformObject({
1811
- ...original,
1812
- ...patch
1813
- }, command.transform) : {
1814
- ...original,
1815
- ...patch
1816
- };
1817
- strokes[index] = {
1818
- ...applySketchCommands({
1819
- ...doc,
1820
- layers: [{
1821
- ...layer,
1822
- strokes: []
1823
- }]
1824
- }, [{
1825
- ...changed,
1826
- op: "stroke",
1827
- layer: layer.id
1828
- }]).layers[0].strokes[0],
1829
- brush: original.brush ?? "pen",
1830
- ...original.pressure !== void 0 ? { pressure: original.pressure } : {},
1831
- ...original.brushVersion === 2 ? { brushVersion: 2 } : {}
1832
- };
1833
- } else throw Error("Unknown object action");
1834
- doc = {
1835
- ...doc,
1836
- layers: doc.layers.map((l) => l === layer ? {
1837
- ...l,
1838
- strokes
1839
- } : l)
1840
- };
1841
- continue;
1842
- }
1843
- if (command.op !== "stroke") throw Error("Unknown command");
1844
- const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
1845
- const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
1846
- let points = command.points;
1847
- if (command.start !== void 0 || command.segments !== void 0) {
1848
- if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
1849
- points = [command.start, ...command.segments.flatMap((s) => [
1850
- s?.control1,
1851
- s?.control2,
1852
- s?.end
1853
- ])];
1968
+ //#endregion
1969
+ //#region src/sketch-objects.js
1970
+ const objectId = (stroke, index) => stroke.id ?? `legacy-${index}`;
1971
+ const identifyObjects = (doc) => ({
1972
+ ...doc,
1973
+ layers: doc.layers.map((layer) => ({
1974
+ ...layer,
1975
+ strokes: layer.strokes.map((s, i) => s.id ? s : {
1976
+ ...s,
1977
+ id: objectId(s, i)
1978
+ })
1979
+ }))
1980
+ });
1981
+ function objectBounds(stroke) {
1982
+ const xs = stroke.points.map((p) => p.x), ys = stroke.points.map((p) => p.y);
1983
+ return {
1984
+ x: Math.min(...xs),
1985
+ y: Math.min(...ys),
1986
+ width: Math.max(...xs) - Math.min(...xs),
1987
+ height: Math.max(...ys) - Math.min(...ys)
1988
+ };
1989
+ }
1990
+ function transformObject(stroke, { dx = 0, dy = 0, scaleX = 1, scaleY = 1 }) {
1991
+ if (![
1992
+ dx,
1993
+ dy,
1994
+ scaleX,
1995
+ scaleY
1996
+ ].every(Number.isFinite) || scaleX <= 0 || scaleY <= 0) throw Error("Invalid object transform");
1997
+ const box = objectBounds(stroke);
1998
+ const points = stroke.points.map((p) => ({
1999
+ x: box.x + (p.x - box.x) * scaleX + dx,
2000
+ y: box.y + (p.y - box.y) * scaleY + dy
2001
+ }));
2002
+ if (points.some((p) => p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1)) throw Error("Object would leave the canvas");
2003
+ return {
2004
+ ...stroke,
2005
+ points
2006
+ };
2007
+ }
2008
+ function sketchObjectSummary(doc) {
2009
+ return doc.layers.flatMap((layer) => layer.strokes.map((stroke, i) => ({
2010
+ layer: layer.id,
2011
+ id: objectId(stroke, i),
2012
+ shape: stroke.shape,
2013
+ color: stroke.color,
2014
+ bounds: objectBounds(stroke),
2015
+ ...stroke.text ? { text: stroke.text } : {}
2016
+ })));
2017
+ }
2018
+ //#endregion
2019
+ //#region src/sketch-input.js
2020
+ function snapLine(start, end) {
2021
+ const angle = Math.round(Math.atan2(end.y - start.y, end.x - start.x) / (Math.PI / 4)) * Math.PI / 4;
2022
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
2023
+ return {
2024
+ x: start.x + Math.cos(angle) * length,
2025
+ y: start.y + Math.sin(angle) * length
2026
+ };
2027
+ }
2028
+ function smoothStrokePoints(points, strength = 0) {
2029
+ if (!strength || points.length < 3) return points;
2030
+ const radius = Math.max(1, Math.round(strength / 25)), amount = Math.min(1, strength / 75);
2031
+ return points.map((point, i) => {
2032
+ if (i === 0 || i === points.length - 1) return point;
2033
+ let x = 0, y = 0, weight = 0;
2034
+ for (let j = Math.max(0, i - radius); j <= Math.min(points.length - 1, i + radius); j++) {
2035
+ const w = radius + 1 - Math.abs(j - i);
2036
+ x += points[j].x * w;
2037
+ y += points[j].y * w;
2038
+ weight += w;
1854
2039
  }
1855
- if (![
1856
- "pen",
1857
- "line",
1858
- "rectangle",
1859
- "circle",
1860
- "polygon",
1861
- "bezier",
1862
- "arrow",
1863
- "text",
1864
- "eraser"
1865
- ].includes(shape) || !/^#[0-9a-f]{6}$/i.test(color ?? "") || !finite(width, 1, 256) || !finite(opacity, 0, 1) || typeof fill !== "boolean" || !Array.isArray(points) || !points.length || points.length > 2e3 || points.some((p) => !p || !finite(p.x, 0, 1) || !finite(p.y, 0, 1))) throw Error("Invalid stroke");
1866
- if ([
1867
- "line",
1868
- "arrow",
1869
- "text",
1870
- "rectangle",
1871
- "circle"
1872
- ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
1873
- if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error(`commands[${commands.indexOf(command)}]: Bezier has ${points.length} points; expected 4, 7, 10, ... 193 (start + control1/control2/end per segment). No commands in this batch were applied.`);
1874
- if (fill && ![
1875
- "rectangle",
1876
- "circle",
1877
- "polygon",
1878
- "bezier"
1879
- ].includes(shape)) throw Error("Fill requires a closed shape");
1880
- const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
1881
- if (!layer?.visible) throw Error("Target layer is missing or hidden");
1882
- if (shape === "text" && (typeof command.text !== "string" || !command.text.trim() || command.text.length > 500 || points[0].x === points[1].x || points[0].y === points[1].y)) throw Error("Text requires 1–500 characters and a non-empty bounding box");
1883
- const id = command.id ?? crypto.randomUUID();
1884
- if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
1885
- const stroke = {
1886
- id,
1887
- ...shape === "text" ? { text: command.text } : {},
1888
- shape,
1889
- color,
1890
- width,
1891
- opacity,
1892
- fill,
1893
- brush: "pen",
1894
- points: points.map((p) => ({
1895
- x: p.x,
1896
- y: p.y
1897
- }))
2040
+ return {
2041
+ ...point,
2042
+ x: point.x + (x / weight - point.x) * amount,
2043
+ y: point.y + (y / weight - point.y) * amount
1898
2044
  };
2045
+ });
2046
+ }
2047
+ //#endregion
2048
+ //#region src/sketch-gesture.js
2049
+ function updateSketchGesture(doc, gesture, samples, rect, width, shiftKey = false) {
2050
+ if (!samples.length) return doc;
2051
+ if (gesture.object) {
2052
+ const point = sketchPoint(samples.at(-1).clientX, samples.at(-1).clientY, rect);
2053
+ if (!point) return doc;
2054
+ const box = objectBounds(gesture.object);
2055
+ const stroke = gesture.handle === "end" ? {
2056
+ ...gesture.object,
2057
+ points: [gesture.object.points[0], point]
2058
+ } : gesture.handle === "size" ? transformObject(gesture.object, {
2059
+ scaleX: Math.max(.001, point.x - box.x) / Math.max(.001, box.width),
2060
+ scaleY: Math.max(.001, point.y - box.y) / Math.max(.001, box.height)
2061
+ }) : transformObject(gesture.object, {
2062
+ dx: Math.max(-box.x, Math.min(1 - box.x - box.width, point.x - gesture.start.x)),
2063
+ dy: Math.max(-box.y, Math.min(1 - box.y - box.height, point.y - gesture.start.y))
2064
+ });
1899
2065
  doc = {
1900
2066
  ...doc,
1901
- layers: doc.layers.map((item) => item === layer ? {
1902
- ...item,
1903
- strokes: [...item.strokes, stroke]
1904
- } : item)
2067
+ layers: doc.layers.map((l) => l.id === gesture.layer ? {
2068
+ ...l,
2069
+ strokes: l.strokes.map((s) => s.id === gesture.object.id ? stroke : s)
2070
+ } : l)
1905
2071
  };
2072
+ return doc;
1906
2073
  }
1907
- if (strokeCount(doc) > 2e3 || doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0) > 2e5) throw Error("Sketch resource budget exceeded");
1908
- return doc;
1909
- }
1910
- function createSketchCommandSession(adapter) {
1911
- const completed = /* @__PURE__ */ new Map();
1912
- let pending = false, cachedCharacters = 0;
1913
- return async (request) => {
1914
- if (!request || typeof request !== "object") throw Error("Invalid sketch request");
1915
- if (!adapter.available()) throw Error("Open the sketch board for this session first");
1916
- const current = adapter.snapshot();
1917
- if (request.action === "inspect") {
1918
- const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
1919
- if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
1920
- return {
1921
- ...current,
1922
- protocolVersion: 2,
1923
- objects: request.objectId ? [] : objects.slice(offset, offset + 50),
1924
- objectCount: objects.length,
1925
- ...!request.objectId && offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
1926
- ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
1927
- recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
1928
- ...!request.objectId && offset === 0 ? { help: SKETCH_COMMAND_HELP } : {}
1929
- };
1930
- }
1931
- if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
1932
- if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
1933
- if (request.action === "preview") return {
1934
- ...current,
1935
- png: await adapter.preview()
1936
- };
1937
- if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
1938
- if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
1939
- const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
1940
- ...request,
1941
- runId: void 0
1942
- });
1943
- const cached = completed.get(key);
1944
- if (cached) {
1945
- if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
1946
- return cached.result;
1947
- }
1948
- if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
1949
- let changedObjects;
1950
- if (request.action === "apply") {
1951
- const before = adapter.document();
1952
- let next;
1953
- try {
1954
- next = applySketchCommands(before, request.commands);
1955
- } catch (cause) {
1956
- const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
1957
- error.code = "SKETCH_INVALID_BATCH";
1958
- throw error;
1959
- }
1960
- adapter.commit(next);
1961
- const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
1962
- changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
1963
- layer: l.id,
1964
- id: s.id
1965
- })));
2074
+ for (const sample of samples) {
2075
+ let point = sketchPoint(sample.clientX, sample.clientY, rect);
2076
+ if (!point) continue;
2077
+ const layer = doc.layers.find((layer) => layer.id === gesture.layer);
2078
+ if (!layer) return doc;
2079
+ if (gesture.eraseStroke) {
2080
+ const previous = gesture.last ?? point;
2081
+ const steps = Math.min(256, Math.max(1, Math.ceil(Math.hypot(point.x - previous.x, point.y - previous.y) * SKETCH_SIZE / Math.max(2, width / 2))));
2082
+ layer.strokes = layer.strokes.filter((stroke) => {
2083
+ for (let i = 1; i <= steps; i++) if (strokeHit(stroke, {
2084
+ x: previous.x + (point.x - previous.x) * i / steps,
2085
+ y: previous.y + (point.y - previous.y) * i / steps
2086
+ }, width / 2, doc.width, doc.height)) return false;
2087
+ return true;
2088
+ });
1966
2089
  } else {
1967
- if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
1968
- pending = true;
1969
- try {
1970
- await adapter.save(request.name);
1971
- } finally {
1972
- pending = false;
1973
- }
1974
- }
1975
- const result = {
1976
- ...adapter.snapshot(),
1977
- ...changedObjects ? {
1978
- changedObjects: changedObjects.slice(0, 100),
1979
- changedObjectCount: changedObjects.length
1980
- } : {}
1981
- };
1982
- completed.set(key, {
1983
- fingerprint,
1984
- result,
1985
- receipt: {
1986
- requestId: request.requestId,
1987
- action: request.action,
1988
- revision: result.revision
2090
+ const stroke = layer.strokes.at(-1);
2091
+ if (!stroke) return doc;
2092
+ if ([
2093
+ "line",
2094
+ "arrow",
2095
+ "rectangle",
2096
+ "circle"
2097
+ ].includes(stroke.shape)) {
2098
+ if (stroke.shape === "line" && shiftKey) {
2099
+ const w = doc.width ?? 1024, h = doc.height ?? 1024, a = stroke.points[0];
2100
+ const snapped = snapLine({
2101
+ x: a.x * w,
2102
+ y: a.y * h
2103
+ }, {
2104
+ x: point.x * w,
2105
+ y: point.y * h
2106
+ });
2107
+ point = {
2108
+ x: snapped.x / w,
2109
+ y: snapped.y / h
2110
+ };
2111
+ }
2112
+ stroke.points = [stroke.points[0], point];
2113
+ } else {
2114
+ const last = stroke.points.at(-1);
2115
+ if (Math.hypot(last.x - point.x, last.y - point.y) < 1e-4) continue;
2116
+ if (stroke.points.length >= 2e3) stroke.points = stroke.points.filter((_, i) => i % 2 === 0);
2117
+ stroke.points.push(point);
1989
2118
  }
1990
- });
1991
- cachedCharacters += fingerprint.length;
1992
- while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
1993
- const oldest = completed.keys().next().value;
1994
- cachedCharacters -= completed.get(oldest).fingerprint.length;
1995
- completed.delete(oldest);
1996
2119
  }
1997
- return result;
1998
- };
2120
+ gesture.last = point;
2121
+ }
2122
+ return doc;
1999
2123
  }
2000
2124
  //#endregion
2001
- //#region src/sketch-formats.js
2002
- const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
2003
- const canvas = (w, h) => {
2004
- const c = document.createElement("canvas");
2005
- c.width = w;
2006
- c.height = h;
2007
- return c;
2008
- };
2009
- function runPsdCodec(action, payload) {
2010
- return new Promise((resolve, reject) => {
2011
- const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
2012
- const finish = (callback, value) => {
2013
- clearTimeout(timer);
2014
- worker.terminate();
2015
- callback(value);
2125
+ //#region src/sketch-drafts.js
2126
+ const DATABASE = "dsh-codex-sketches-v1";
2127
+ const MAX_STORAGE = 32 * 1024 * 1024;
2128
+ const metadata = (kind, row) => ({
2129
+ key: `${kind}:${row.id}`,
2130
+ kind,
2131
+ id: row.id,
2132
+ name: row.name,
2133
+ updated: row.updated,
2134
+ size: JSON.stringify(row).length
2135
+ });
2136
+ async function sketchDrafts(action, value, recoverySession) {
2137
+ const db = await new Promise((resolve, reject) => {
2138
+ let blocked = false;
2139
+ const request = indexedDB.open(DATABASE, 2);
2140
+ request.onblocked = () => {
2141
+ blocked = true;
2142
+ reject(Object.assign(Error("Close other sketch windows and retry"), { code: "SKETCH_STORAGE_BLOCKED" }));
2016
2143
  };
2017
- const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
2018
- worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
2019
- worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
2020
- worker.postMessage({
2021
- action,
2022
- payload
2023
- });
2024
- });
2025
- }
2026
- function encodeSketchDocument(doc) {
2027
- return JSON.stringify({
2028
- format: "dsh-sketch",
2029
- version: 1,
2030
- doc
2031
- });
2032
- }
2033
- function decodeSketchDocument(text) {
2034
- if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
2035
- const file = JSON.parse(text), source = file.doc;
2036
- if (file.format !== "dsh-sketch" || file.version !== 1 || !source || !Array.isArray(source.layers) || !source.layers.length || source.layers.length > 8) throw Error("Invalid sketch file");
2037
- const w = source.width ?? 1024, h = source.height ?? 1024;
2038
- if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
2039
- let doc = {
2040
- ...createSketchLayers(),
2041
- width: w,
2042
- height: h,
2043
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
2044
- };
2045
- for (let i = 0; i < source.layers.length; i++) {
2046
- const layer = source.layers[i];
2047
- if (i) doc = applySketchCommands(doc, [{
2048
- op: "layer",
2049
- action: "add"
2050
- }]);
2051
- if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
2052
- for (let j = 0; j < layer.strokes.length; j += 256) {
2053
- const strokes = layer.strokes.slice(j, j + 256);
2054
- doc = applySketchCommands(doc, strokes.map((s) => ({
2055
- ...s,
2056
- op: "stroke",
2057
- layer: doc.active,
2058
- fill: s.fill ?? false
2059
- })));
2060
- const added = doc.layers.at(-1).strokes;
2061
- for (let k = 0; k < strokes.length; k++) {
2062
- const s = strokes[k];
2063
- if (s.brush !== void 0 && ![
2064
- "pen",
2065
- "pencil",
2066
- "marker"
2067
- ].includes(s.brush)) throw Error("Invalid brush");
2068
- if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
2069
- if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
2070
- Object.assign(added[added.length - strokes.length + k], {
2071
- brush: s.brush ?? "pen",
2072
- pressure: s.pressure ?? 1,
2073
- ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
2074
- });
2144
+ request.onupgradeneeded = () => {
2145
+ if (blocked) {
2146
+ request.transaction.abort();
2147
+ return;
2075
2148
  }
2076
- }
2077
- const target = doc.layers.at(-1);
2078
- target.name = String(layer.name ?? "").slice(0, 40);
2079
- target.visible = layer.visible !== false;
2080
- if (layer.image) {
2081
- const image = layer.image;
2082
- if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
2083
- "x",
2084
- "y",
2085
- "width",
2086
- "height"
2087
- ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
2088
- const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
2089
- if (bytes.length < 24) throw Error("Invalid draft image");
2090
- const header = new DataView(bytes.buffer);
2091
- if (header.getUint32(0) !== 2303741511 || header.getUint32(4) !== 218765834 || header.getUint32(16) < 1 || header.getUint32(20) < 1 || header.getUint32(16) > 4096 || header.getUint32(20) > 4096) throw Error("Invalid draft image size");
2092
- target.image = {
2093
- src: image.src,
2094
- x: image.x,
2095
- y: image.y,
2096
- width: image.width,
2097
- height: image.height
2149
+ const db = request.result, tx = request.transaction;
2150
+ if (!db.objectStoreNames.contains("drafts")) db.createObjectStore("drafts", { keyPath: "id" });
2151
+ const meta = db.createObjectStore("metadata", { keyPath: "key" });
2152
+ db.createObjectStore("recovery", { keyPath: "id" });
2153
+ const cursor = tx.objectStore("drafts").openCursor();
2154
+ cursor.onsuccess = () => {
2155
+ const row = cursor.result;
2156
+ if (row) {
2157
+ meta.put(metadata("drafts", row.value));
2158
+ row.continue();
2159
+ }
2098
2160
  };
2099
- }
2100
- }
2101
- const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
2102
- doc.active = doc.layers[Math.max(0, activeIndex)].id;
2103
- return doc;
2104
- }
2105
- async function exportSketchPsd(doc, images, composite) {
2106
- const width = doc.width ?? 1024, height = doc.height ?? 1024;
2107
- const children = doc.layers.map((layer, i) => {
2108
- const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
2109
- if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
2110
- paintSketch(ctx, layer.strokes, width, true, height);
2111
- return {
2112
- name: layer.name || `Layer ${i + 1}`,
2113
- hidden: !layer.visible,
2114
- opacity: 1,
2115
- blendMode: "normal",
2116
- imageData: ctx.getImageData(0, 0, width, height)
2117
2161
  };
2162
+ request.onsuccess = () => {
2163
+ if (blocked) {
2164
+ request.result.close();
2165
+ return;
2166
+ }
2167
+ request.result.onversionchange = () => request.result.close();
2168
+ resolve(request.result);
2169
+ };
2170
+ request.onerror = () => reject(request.error);
2118
2171
  });
2119
- const context = canvas(width, height).getContext("2d");
2120
- context.fillStyle = "#fff";
2121
- context.fillRect(0, 0, width, height);
2122
- if (children[0] && !children[0].hidden) {
2123
- const bottom = canvas(width, height);
2124
- bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
2125
- context.drawImage(bottom, 0, 0);
2126
- children[0].imageData = context.getImageData(0, 0, width, height);
2127
- } else {
2128
- if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
2129
- children.unshift({
2130
- name: "Paper",
2131
- opacity: 1,
2132
- blendMode: "normal",
2133
- imageData: context.getImageData(0, 0, width, height)
2172
+ try {
2173
+ return await new Promise((resolve, reject) => {
2174
+ const write = [
2175
+ "save",
2176
+ "delete",
2177
+ "checkpoint",
2178
+ "clearRecovery"
2179
+ ].includes(action);
2180
+ const tx = db.transaction([
2181
+ "drafts",
2182
+ "metadata",
2183
+ "recovery"
2184
+ ], write ? "readwrite" : "readonly");
2185
+ const meta = tx.objectStore("metadata"), kind = [
2186
+ "checkpoint",
2187
+ "recover",
2188
+ "clearRecovery"
2189
+ ].includes(action) ? "recovery" : "drafts", store = tx.objectStore(kind);
2190
+ let result, failure;
2191
+ tx.oncomplete = () => resolve(result);
2192
+ tx.onerror = () => reject(tx.error);
2193
+ tx.onabort = () => reject(failure ?? tx.error ?? Error("Draft transaction aborted"));
2194
+ if (action === "get" || action === "recover") {
2195
+ const req = store.get(value);
2196
+ req.onsuccess = () => {
2197
+ result = req.result;
2198
+ };
2199
+ return;
2200
+ }
2201
+ if (action === "delete" || action === "clearRecovery") {
2202
+ store.delete(value);
2203
+ meta.delete(`${kind}:${value}`);
2204
+ return;
2205
+ }
2206
+ if (![
2207
+ "list",
2208
+ "save",
2209
+ "checkpoint"
2210
+ ].includes(action)) {
2211
+ tx.abort();
2212
+ return;
2213
+ }
2214
+ const request = meta.getAll();
2215
+ request.onsuccess = () => {
2216
+ const rows = request.result;
2217
+ if (action === "list") {
2218
+ result = rows.filter((row) => row.kind === "drafts").sort((a, b) => b.updated - a.updated);
2219
+ return;
2220
+ }
2221
+ const next = metadata(kind, value), others = rows.filter((row) => row.key !== next.key && !(action === "save" && recoverySession && row.key === `recovery:${recoverySession}`));
2222
+ const code = others.filter((row) => row.kind === kind).length >= 20 ? "SKETCH_DRAFT_LIMIT" : others.reduce((n, row) => n + row.size, 0) + next.size > MAX_STORAGE ? "SKETCH_STORAGE_LIMIT" : null;
2223
+ if (code) {
2224
+ failure = Object.assign(Error("Draft storage limit reached"), { code });
2225
+ tx.abort();
2226
+ return;
2227
+ }
2228
+ store.put(value);
2229
+ meta.put(next);
2230
+ if (action === "save" && recoverySession) {
2231
+ tx.objectStore("recovery").delete(recoverySession);
2232
+ meta.delete(`recovery:${recoverySession}`);
2233
+ }
2234
+ result = value;
2235
+ };
2134
2236
  });
2237
+ } finally {
2238
+ db.close();
2135
2239
  }
2136
- return runPsdCodec("write", {
2137
- width,
2138
- height,
2139
- children,
2140
- imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2141
- });
2142
2240
  }
2143
- async function importSketchPsd(file) {
2144
- if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2145
- const psd = await runPsdCodec("read", await file.arrayBuffer());
2146
- const scale = Math.min(1, 1024 / Math.max(psd.width, psd.height)), width = Math.max(1, Math.round(psd.width * scale)), height = Math.max(1, Math.round(psd.height * scale));
2147
- const doc = {
2148
- ...createSketchLayers(),
2149
- width,
2150
- height,
2151
- ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2152
- layers: [],
2153
- nextId: psd.layers.length + 1
2154
- };
2155
- for (const [i, layer] of psd.layers.entries()) {
2156
- const src = canvas(layer.imageData.width, layer.imageData.height);
2157
- src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2158
- const out = canvas(width, height), ctx = out.getContext("2d");
2159
- ctx.globalAlpha = layer.opacity;
2160
- ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2161
- doc.layers.push({
2162
- id: i + 1,
2163
- name: layer.name,
2164
- visible: !layer.hidden,
2165
- strokes: [],
2166
- image: {
2167
- src: out.toDataURL("image/png"),
2168
- x: 0,
2169
- y: 0,
2170
- width: 1,
2171
- height: 1
2172
- }
2173
- });
2241
+ async function decodeSketchImages(doc, images) {
2242
+ for (const layer of doc.layers) {
2243
+ const src = layer.image?.src;
2244
+ if (!src || images.has(src)) continue;
2245
+ if (!src.startsWith("data:image/png;base64,") || src.length > 8 * 1024 * 1024) throw Error("Invalid image");
2246
+ const image = new Image();
2247
+ image.src = src;
2248
+ await image.decode();
2249
+ images.set(src, image);
2174
2250
  }
2175
- return doc;
2176
2251
  }
2177
- //#endregion
2178
- //#region src/sketch-document-lifecycle.js
2179
- function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
2180
- const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
2181
- const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2182
- const save = async (name) => {
2183
- const savingDocument = documentId.current, savingRevision = documentRevision.current;
2184
- const row = {
2185
- id: saved.current?.id ?? crypto.randomUUID(),
2186
- name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2187
- updated: Date.now(),
2188
- doc: structuredClone(doc.current)
2252
+ async function importSketchImage(file) {
2253
+ if (![
2254
+ "image/png",
2255
+ "image/jpeg",
2256
+ "image/webp"
2257
+ ].includes(file.type) || file.size > 20 * 1024 * 1024) throw Error("Image must be PNG, JPEG or WebP under 20 MB");
2258
+ const bitmap = await createImageBitmap(file);
2259
+ try {
2260
+ if (bitmap.width * bitmap.height > 32 * 1024 * 1024) throw Error("Image too large");
2261
+ const scale = Math.min(1, 1024 / Math.max(bitmap.width, bitmap.height));
2262
+ const canvas = document.createElement("canvas");
2263
+ canvas.width = Math.max(1, Math.round(bitmap.width * scale));
2264
+ canvas.height = Math.max(1, Math.round(bitmap.height * scale));
2265
+ canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
2266
+ return {
2267
+ src: canvas.toDataURL("image/png"),
2268
+ width: canvas.width,
2269
+ height: canvas.height
2189
2270
  };
2190
- try {
2191
- await store("save", row, sessionId);
2192
- } catch (error) {
2193
- if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
2194
- if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
2195
- throw error;
2196
- }
2197
- if (documentId.current === savingDocument) {
2198
- saved.current = {
2199
- id: row.id,
2200
- name: row.name
2201
- };
2202
- if (documentRevision.current === savingRevision) {
2203
- dirty.current = false;
2204
- setRecovered(false);
2205
- }
2206
- }
2207
- };
2208
- const saveChanges = async () => {
2209
- if (!dirty.current) return;
2210
- if (hasContent() || saved.current) return save();
2211
- const id = documentId.current, revision = documentRevision.current;
2212
- await store("clearRecovery", sessionId);
2213
- if (documentId.current === id && documentRevision.current === revision) {
2214
- dirty.current = false;
2215
- setRecovered(false);
2216
- }
2271
+ } finally {
2272
+ bitmap.close();
2273
+ }
2274
+ }
2275
+ //#endregion
2276
+ //#region src/sketch-presets.js
2277
+ const SKETCH_PRESETS = [
2278
+ "triangle",
2279
+ "diamond",
2280
+ "star"
2281
+ ];
2282
+ function expandSketchPreset(command) {
2283
+ if (!SKETCH_PRESETS.includes(command.shape)) return command;
2284
+ const points = command.points;
2285
+ if (!Array.isArray(points) || points.length !== 2 || points.some((p) => !p || ![p.x, p.y].every((v) => Number.isFinite(v) && v >= 0 && v <= 1))) throw Error("Shape preset requires two normalized bounding-box corners");
2286
+ const [a, b] = points, x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
2287
+ const w = Math.abs(a.x - b.x), h = Math.abs(a.y - b.y);
2288
+ if (!w || !h) throw Error("Shape preset requires a non-empty box");
2289
+ const vertices = command.shape === "triangle" ? [
2290
+ [.5, 0],
2291
+ [1, 1],
2292
+ [0, 1]
2293
+ ] : command.shape === "diamond" ? [
2294
+ [.5, 0],
2295
+ [1, .5],
2296
+ [.5, 1],
2297
+ [0, .5]
2298
+ ] : Array.from({ length: 10 }, (_, i) => {
2299
+ const angle = i * Math.PI / 5 - Math.PI / 2, radius = i % 2 ? .22 : .5;
2300
+ return [.5 + Math.cos(angle) * radius, .5 + Math.sin(angle) * radius];
2301
+ });
2302
+ return {
2303
+ ...command,
2304
+ shape: "polygon",
2305
+ points: vertices.map(([u, v]) => ({
2306
+ x: x + u * w,
2307
+ y: y + v * h
2308
+ }))
2217
2309
  };
2218
- const replace = (next, decoded, identity) => {
2219
- documentId.current = crypto.randomUUID();
2220
- documentRevision.current++;
2221
- doc.current = identifyObjects(structuredClone(next));
2222
- setSelection(null);
2223
- setTextEdit(null);
2224
- images.current = decoded;
2225
- cache.current.clear();
2226
- undo.current = [];
2227
- redo.current = [];
2228
- saved.current = identity;
2229
- dirty.current = false;
2230
- schedule();
2231
- };
2232
- const fresh = async () => {
2233
- await saveChanges();
2234
- replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2235
- };
2236
- const load = async (row) => {
2237
- if (row.id === saved.current?.id) return;
2238
- row = await store("get", row.id);
2239
- if (!row) throw Error("Draft no longer exists");
2240
- await saveChanges();
2241
- const decoded = /* @__PURE__ */ new Map();
2242
- await decodeImages(row.doc, decoded);
2243
- replace(row.doc, decoded, {
2244
- id: row.id,
2245
- name: row.name
2246
- });
2247
- };
2248
- const importImage = async (file) => {
2249
- if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2250
- if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2251
- const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2252
- const decoded = /* @__PURE__ */ new Map();
2253
- await decodeImages(next, decoded);
2254
- await saveChanges();
2255
- replace(next, decoded, null);
2256
- dirty.current = true;
2257
- return;
2310
+ }
2311
+ const SKETCH_COMMAND_HELP = {
2312
+ coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
2313
+ shapes: "line: exactly two endpoints; rectangle/circle (ellipse alias accepted): exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. bezier: start point, then groups of control1/control2/end; use 4 points for one cubic curve, max 64 segments. Prefer bezier for smooth designed curves instead of many pen samples. fill:true closes and fills the curve. fill:true fills rectangle/circle/polygon. Layers and strokes paint in list order, later ones on top. All commands needed for drawing are described here; no source-code search is required.",
2314
+ commands: {
2315
+ preset: "triangle/diamond/star: {op:\"stroke\",id:\"badge\",shape:\"star\",points:[{x:0.1,y:0.1},{x:0.3,y:0.3}],color:\"#ffcc00\",fill:true}. Two opposite box corners; stored as editable polygon. Inspect objectId for one object without repeating full help.",
2316
+ stroke: "{op:\"stroke\",layer:1,shape:\"pen|line|arrow|text|rectangle|circle|polygon|bezier\",color:\"#rrggbb\",width:2,opacity:1,fill:false,points:[{x:0.1,y:0.1},...]}",
2317
+ layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
2318
+ curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
2319
+ object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
2320
+ resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
2321
+ },
2322
+ limits: {
2323
+ strokes: MAX_SKETCH_STROKES,
2324
+ pointsPerStroke: MAX_STROKE_POINTS,
2325
+ pointsTotal: 2e5,
2326
+ commandsPerBatch: 256
2327
+ }
2328
+ };
2329
+ const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
2330
+ function applySketchCommands(source, commands) {
2331
+ if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
2332
+ let doc = identifyObjects(source);
2333
+ for (let command of commands) {
2334
+ if (!command || typeof command !== "object") throw Error("Invalid command");
2335
+ if (command.op === "stroke") command = expandSketchPreset(command);
2336
+ if (command.op === "resize") {
2337
+ doc = resizeSketch(doc, command.ratio);
2338
+ continue;
2258
2339
  }
2259
- if (doc.current.layers.length >= 8) throw Error("Layer limit");
2260
- const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2261
- const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2262
- const layer = {
2263
- id: doc.current.nextId,
2264
- name: file.name?.slice(0, 40) || t("sketchImport"),
2265
- visible: true,
2266
- strokes: [],
2267
- image: {
2268
- src: image.src,
2269
- x: (1 - width) / 2,
2270
- y: (1 - height) / 2,
2271
- width,
2272
- height
2340
+ if (command.op === "layer") {
2341
+ if (![
2342
+ "add",
2343
+ "select",
2344
+ "rename",
2345
+ "visible",
2346
+ "duplicate",
2347
+ "up",
2348
+ "down",
2349
+ "delete",
2350
+ "clear"
2351
+ ].includes(command.action)) throw Error("Unknown layer action");
2352
+ if (command.action === "add") {
2353
+ const id = command.id ?? doc.nextId, after = command.after ?? doc.active;
2354
+ if (!Number.isSafeInteger(id) || id < 1 || id === Number.MAX_SAFE_INTEGER || doc.layers.some((l) => l.id === id)) throw Error("New layer id must be a unique positive integer; omit id to allocate automatically");
2355
+ const next = changeSketchLayer(doc, "add", after);
2356
+ if (next === doc) throw Error("Cannot add layer: check the existing after layer and the 8-layer limit");
2357
+ doc = {
2358
+ ...next,
2359
+ active: id,
2360
+ nextId: Math.max(next.nextId, id + 1),
2361
+ layers: next.layers.map((l) => l.id === next.active ? {
2362
+ ...l,
2363
+ id,
2364
+ name: String(command.value ?? "").trim().slice(0, 40)
2365
+ } : l)
2366
+ };
2367
+ continue;
2273
2368
  }
2274
- };
2275
- await decodeImages({ layers: [layer] }, images.current);
2276
- checkpoint();
2277
- doc.current = {
2278
- ...doc.current,
2279
- nextId: layer.id + 1,
2280
- active: layer.id,
2281
- layers: [...doc.current.layers, layer]
2282
- };
2283
- schedule();
2284
- };
2285
- const restore = async (isCurrent = () => true) => {
2286
- if (dirty.current || hasContent()) return;
2287
- const id = documentId.current, revision = documentRevision.current;
2288
- const recovery = await store("recover", sessionId);
2289
- const archived = state.restoreId.current;
2290
- const row = recovery ?? (archived ? await store("get", archived) : null);
2291
- const decoded = /* @__PURE__ */ new Map();
2292
- if (row) await decodeImages(row.doc, decoded);
2293
- if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
2294
- if (row) {
2295
- replace(row.doc, decoded, recovery ? null : {
2296
- id: row.id,
2297
- name: row.name
2369
+ const next = changeSketchLayer(doc, command.action, command.id ?? doc.active, command.value);
2370
+ if (next === doc) throw Error("Layer action unavailable; inspect the document first");
2371
+ doc = next;
2372
+ continue;
2373
+ }
2374
+ if (command.op === "object") {
2375
+ const layer = doc.layers.find((l) => l.id === (command.layer ?? doc.active)), index = layer?.strokes.findIndex((s) => s.id === command.id);
2376
+ if (!layer?.visible || index < 0 || index === void 0) throw Error("Object missing or hidden; inspect again");
2377
+ const strokes = layer.strokes.slice(), original = strokes[index];
2378
+ if (command.action === "delete") strokes.splice(index, 1);
2379
+ else if (command.action === "duplicate") strokes.splice(index + 1, 0, {
2380
+ ...original,
2381
+ id: crypto.randomUUID(),
2382
+ points: original.points.map((p) => ({ ...p }))
2298
2383
  });
2299
- dirty.current = Boolean(recovery);
2300
- setRecovered(Boolean(recovery));
2384
+ else if (command.action === "update") {
2385
+ const patch = command.patch ?? {};
2386
+ if (Object.keys(patch).some((k) => ![
2387
+ "color",
2388
+ "width",
2389
+ "opacity",
2390
+ "fill",
2391
+ "text",
2392
+ "points"
2393
+ ].includes(k))) throw Error("Unsupported object property");
2394
+ const changed = command.transform ? transformObject({
2395
+ ...original,
2396
+ ...patch
2397
+ }, command.transform) : {
2398
+ ...original,
2399
+ ...patch
2400
+ };
2401
+ strokes[index] = {
2402
+ ...applySketchCommands({
2403
+ ...doc,
2404
+ layers: [{
2405
+ ...layer,
2406
+ strokes: []
2407
+ }]
2408
+ }, [{
2409
+ ...changed,
2410
+ op: "stroke",
2411
+ layer: layer.id
2412
+ }]).layers[0].strokes[0],
2413
+ brush: original.brush ?? "pen",
2414
+ ...original.pressure !== void 0 ? { pressure: original.pressure } : {},
2415
+ ...original.brushVersion === 2 ? { brushVersion: 2 } : {}
2416
+ };
2417
+ } else throw Error("Unknown object action");
2418
+ doc = {
2419
+ ...doc,
2420
+ layers: doc.layers.map((l) => l === layer ? {
2421
+ ...l,
2422
+ strokes
2423
+ } : l)
2424
+ };
2425
+ continue;
2301
2426
  }
2302
- state.restoreId.current = null;
2303
- };
2304
- return {
2305
- hasContent,
2306
- save,
2307
- saveChanges,
2308
- replace,
2309
- fresh,
2310
- load,
2311
- importImage,
2312
- restore
2313
- };
2314
- }
2315
- //#endregion
2316
- //#region src/sketch-operation-gate.js
2317
- function createSketchOperationGate() {
2318
- let running = false;
2319
- return {
2320
- get running() {
2321
- return running;
2322
- },
2323
- async run(operation, { blocked = false, working, report, rethrow = false }) {
2324
- if (blocked || running) return false;
2325
- running = true;
2326
- try {
2327
- working(true);
2328
- report(null);
2329
- await operation();
2330
- return true;
2331
- } catch (error) {
2332
- report(error);
2333
- if (rethrow) throw error;
2334
- return false;
2335
- } finally {
2336
- running = false;
2337
- working(false);
2338
- }
2427
+ if (command.op !== "stroke") throw Error("Unknown command");
2428
+ const shape = command.shape === "ellipse" ? "circle" : command.shape ?? "pen";
2429
+ const { color, width = shape === "text" ? 24 : 2, opacity = 1, fill = false } = command;
2430
+ let points = command.points;
2431
+ if (command.start !== void 0 || command.segments !== void 0) {
2432
+ if (shape !== "bezier" || points !== void 0 || !command.start || !Array.isArray(command.segments) || !command.segments.length || command.segments.length > 64) throw Error("Bezier requires start and 1–64 segments, without points");
2433
+ points = [command.start, ...command.segments.flatMap((s) => [
2434
+ s?.control1,
2435
+ s?.control2,
2436
+ s?.end
2437
+ ])];
2339
2438
  }
2340
- };
2341
- }
2342
- //#endregion
2343
- //#region src/sketch-agent-export.js
2344
- async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
2345
- let result;
2346
- if (!await gate.run(async () => {
2347
- const { blob, extension } = await exportFile(format);
2348
- const data = new Uint8Array(await blob.arrayBuffer());
2349
- let raw = "";
2350
- for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2351
- result = {
2352
- extension,
2353
- mediaType: blob.type,
2354
- base64: btoa(raw)
2439
+ if (![
2440
+ "pen",
2441
+ "line",
2442
+ "rectangle",
2443
+ "circle",
2444
+ "polygon",
2445
+ "bezier",
2446
+ "arrow",
2447
+ "text",
2448
+ "eraser"
2449
+ ].includes(shape) || !/^#[0-9a-f]{6}$/i.test(color ?? "") || !finite(width, 1, 256) || !finite(opacity, 0, 1) || typeof fill !== "boolean" || !Array.isArray(points) || !points.length || points.length > 2e3 || points.some((p) => !p || !finite(p.x, 0, 1) || !finite(p.y, 0, 1))) throw Error("Invalid stroke");
2450
+ if ([
2451
+ "line",
2452
+ "arrow",
2453
+ "text",
2454
+ "rectangle",
2455
+ "circle"
2456
+ ].includes(shape) && points.length !== 2 || shape === "polygon" && points.length < 3) throw Error("Invalid shape points");
2457
+ if (shape === "bezier" && (points.length < 4 || points.length > 193 || (points.length - 1) % 3 !== 0)) throw Error(`commands[${commands.indexOf(command)}]: Bezier has ${points.length} points; expected 4, 7, 10, ... 193 (start + control1/control2/end per segment). No commands in this batch were applied.`);
2458
+ if (fill && ![
2459
+ "rectangle",
2460
+ "circle",
2461
+ "polygon",
2462
+ "bezier"
2463
+ ].includes(shape)) throw Error("Fill requires a closed shape");
2464
+ const layer = doc.layers.find((layer) => layer.id === (command.layer ?? doc.active));
2465
+ if (!layer?.visible) throw Error("Target layer is missing or hidden");
2466
+ if (shape === "text" && (typeof command.text !== "string" || !command.text.trim() || command.text.length > 500 || points[0].x === points[1].x || points[0].y === points[1].y)) throw Error("Text requires 1–500 characters and a non-empty bounding box");
2467
+ const id = command.id ?? crypto.randomUUID();
2468
+ if (typeof id !== "string" || !id.length || id.length > 100 || layer.strokes.some((s) => s.id === id)) throw Error("Invalid or duplicate object id");
2469
+ const stroke = {
2470
+ id,
2471
+ ...shape === "text" ? { text: command.text } : {},
2472
+ shape,
2473
+ color,
2474
+ width,
2475
+ opacity,
2476
+ fill,
2477
+ brush: "pen",
2478
+ points: points.map((p) => ({
2479
+ x: p.x,
2480
+ y: p.y
2481
+ }))
2355
2482
  };
2356
- }, {
2357
- blocked,
2358
- working,
2359
- report,
2360
- rethrow: true
2361
- })) throw Error("Sketch is being edited; retry after it settles");
2362
- return result;
2363
- }
2364
- //#endregion
2365
- //#region src/sketch-run-status.jsx
2366
- function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
2367
- if (state === "idle") return null;
2368
- const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
2369
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2370
- className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
2371
- role: "status",
2372
- "aria-live": "polite",
2373
- children: [
2374
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2375
- name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
2376
- size: 15
2377
- }),
2378
- floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2379
- type: "button",
2380
- onClick: onOpen,
2381
- children: t(`sketchRun_${state}`)
2382
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
2383
- drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2384
- type: "button",
2385
- className: "codexSketchStop",
2386
- onClick: onStop,
2387
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2388
- name: "stop",
2389
- size: 12
2390
- }), t("sketchRunStop")]
2391
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2392
- type: "button",
2393
- title: t("sketchRunResumeHint"),
2394
- onClick: onResume,
2395
- children: t("sketchRunResume")
2396
- }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2397
- type: "button",
2398
- "aria-label": t("sketchDismissStatus"),
2399
- title: t("sketchDismissStatus"),
2400
- onClick: onDismiss,
2401
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2402
- name: "close",
2403
- size: 14
2404
- })
2405
- }) : null] })
2406
- ]
2407
- });
2408
- }
2409
- //#endregion
2410
- //#region src/sketch-tool-widths.js
2411
- const defaults = {
2412
- pen: 12,
2413
- pencil: 6,
2414
- marker: 28,
2415
- eraser: 24,
2416
- text: 32,
2417
- line: 12,
2418
- arrow: 12,
2419
- rectangle: 12,
2420
- circle: 12
2421
- };
2422
- const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
2423
- function switchSketchToolWidth(memory, current, next) {
2424
- if (current.tool !== "select") memory[keyFor(current)] = current.width;
2425
- if (next.tool === "select") return current.width;
2426
- const key = keyFor(next);
2427
- return memory[key] ?? defaults[key] ?? 12;
2428
- }
2429
- function stepSketchWidth(width, direction) {
2430
- return Math.max(1, Math.min(256, width + direction * 2));
2483
+ doc = {
2484
+ ...doc,
2485
+ layers: doc.layers.map((item) => item === layer ? {
2486
+ ...item,
2487
+ strokes: [...item.strokes, stroke]
2488
+ } : item)
2489
+ };
2490
+ }
2491
+ if (strokeCount(doc) > 2e3 || doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0) > 2e5) throw Error("Sketch resource budget exceeded");
2492
+ return doc;
2431
2493
  }
2432
- //#endregion
2433
- //#region src/sketch-shortcuts.js
2434
- function sketchShortcutAction(keys, key) {
2435
- key = key.toLowerCase();
2436
- return Object.entries(keys).find(([, value]) => value === key)?.[0] ?? {
2437
- v: "select",
2438
- t: "text",
2439
- "+": "zoomIn"
2440
- }[key];
2494
+ function createSketchCommandSession(adapter) {
2495
+ const completed = /* @__PURE__ */ new Map();
2496
+ let pending = false, cachedCharacters = 0;
2497
+ return async (request) => {
2498
+ if (!request || typeof request !== "object") throw Error("Invalid sketch request");
2499
+ if (!adapter.available()) throw Error("Open the sketch board for this session first");
2500
+ const current = adapter.snapshot();
2501
+ if (request.action === "inspect") {
2502
+ const offset = request.offset ?? 0, objects = adapter.objects?.() ?? [];
2503
+ if (!Number.isInteger(offset) || offset < 0) throw Error("offset must be a non-negative integer");
2504
+ return {
2505
+ ...current,
2506
+ protocolVersion: 2,
2507
+ objects: request.objectId ? [] : objects.slice(offset, offset + 50),
2508
+ objectCount: objects.length,
2509
+ ...!request.objectId && offset + 50 < objects.length ? { nextOffset: offset + 50 } : {},
2510
+ ...request.objectId ? { object: adapter.object?.(request.objectId, request.layer) } : {},
2511
+ recentRequests: [...completed.values()].slice(-8).map((entry) => entry.receipt),
2512
+ ...!request.objectId && offset === 0 ? { help: SKETCH_COMMAND_HELP } : {}
2513
+ };
2514
+ }
2515
+ if (request.documentId !== current.documentId) throw Error("Document changed; inspect again");
2516
+ if (pending || adapter.busy()) throw Error("Sketch is being edited; retry after it settles");
2517
+ if (request.action === "preview") return {
2518
+ ...current,
2519
+ png: await adapter.preview()
2520
+ };
2521
+ if (!["apply", "save"].includes(request.action)) throw Error("Unknown sketch action");
2522
+ if (typeof request.requestId !== "string" || !request.requestId.length || request.requestId.length > 100) throw Error("A unique requestId is required");
2523
+ const key = `${current.documentId}:${request.requestId}`, fingerprint = JSON.stringify({
2524
+ ...request,
2525
+ runId: void 0
2526
+ });
2527
+ const cached = completed.get(key);
2528
+ if (cached) {
2529
+ if (cached.fingerprint !== fingerprint) throw Error("requestId reused with different content");
2530
+ return cached.result;
2531
+ }
2532
+ if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
2533
+ let changedObjects;
2534
+ if (request.action === "apply") {
2535
+ const before = adapter.document();
2536
+ let next;
2537
+ try {
2538
+ next = applySketchCommands(before, request.commands);
2539
+ } catch (cause) {
2540
+ const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
2541
+ error.code = "SKETCH_INVALID_BATCH";
2542
+ throw error;
2543
+ }
2544
+ adapter.commit(next);
2545
+ const previous = new Map(before.layers.flatMap((l) => l.strokes.map((s) => [`${l.id}:${s.id}`, s])));
2546
+ changedObjects = next.layers.flatMap((l) => l.strokes.filter((s) => previous.get(`${l.id}:${s.id}`) !== s).map((s) => ({
2547
+ layer: l.id,
2548
+ id: s.id
2549
+ })));
2550
+ } else {
2551
+ if (request.name !== void 0 && (typeof request.name !== "string" || request.name.length > 60)) throw Error("Invalid draft name");
2552
+ pending = true;
2553
+ try {
2554
+ await adapter.save(request.name);
2555
+ } finally {
2556
+ pending = false;
2557
+ }
2558
+ }
2559
+ const result = {
2560
+ ...adapter.snapshot(),
2561
+ ...changedObjects ? {
2562
+ changedObjects: changedObjects.slice(0, 100),
2563
+ changedObjectCount: changedObjects.length
2564
+ } : {}
2565
+ };
2566
+ completed.set(key, {
2567
+ fingerprint,
2568
+ result,
2569
+ receipt: {
2570
+ requestId: request.requestId,
2571
+ action: request.action,
2572
+ revision: result.revision
2573
+ }
2574
+ });
2575
+ cachedCharacters += fingerprint.length;
2576
+ while (completed.size > 1 && (completed.size > 128 || cachedCharacters > 4e6)) {
2577
+ const oldest = completed.keys().next().value;
2578
+ cachedCharacters -= completed.get(oldest).fingerprint.length;
2579
+ completed.delete(oldest);
2580
+ }
2581
+ return result;
2582
+ };
2441
2583
  }
2442
2584
  //#endregion
2443
- //#region src/sketch-layer-renderer.js
2444
- const NO_IMAGES = /* @__PURE__ */ new Map();
2445
- const surface = (width, height) => {
2585
+ //#region src/sketch-formats.js
2586
+ const SKETCH_FILE_ACCEPT = ".psd,.dsh-sketch.json,image/png,image/jpeg,image/webp";
2587
+ const canvas = (w, h) => {
2446
2588
  const c = document.createElement("canvas");
2447
- c.width = width;
2448
- c.height = height;
2589
+ c.width = w;
2590
+ c.height = h;
2449
2591
  return c;
2450
2592
  };
2451
- function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
2452
- context.globalCompositeOperation = "source-over";
2453
- context.globalAlpha = 1;
2454
- context.fillStyle = "#fff";
2455
- context.fillRect(0, 0, size, height);
2456
- for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
2457
- for (const layer of doc.layers) {
2458
- if (!layer.visible) continue;
2459
- let entry = cache.get(layer.id);
2460
- if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
2461
- entry = {
2462
- surface: surface(size, height),
2463
- base: surface(size, height)
2464
- };
2465
- cache.set(layer.id, entry);
2466
- }
2467
- const moving = layer.id === activeLayer;
2468
- const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
2469
- const prefix = layer.strokes[count - 1];
2470
- if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
2471
- const ctx = entry.base.getContext("2d");
2472
- const append = entry.count !== void 0 && count >= entry.count && entry.image === layer.image && (entry.strokes === layer.strokes || entry.strokes.slice(0, entry.count).every((stroke, index) => stroke === layer.strokes[index]));
2473
- if (!append) {
2474
- ctx.clearRect(0, 0, size, height);
2475
- const ref = layer.image, image = ref && images.get(ref.src);
2476
- if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
2593
+ function runPsdCodec(action, payload) {
2594
+ return new Promise((resolve, reject) => {
2595
+ const worker = new Worker("/api/codex-subscription/sketch-psd-worker", { type: "module" });
2596
+ const finish = (callback, value) => {
2597
+ clearTimeout(timer);
2598
+ worker.terminate();
2599
+ callback(value);
2600
+ };
2601
+ const timer = setTimeout(() => finish(reject, Error("PSD operation timed out")), 3e4);
2602
+ worker.onerror = () => finish(reject, Error("PSD codec could not be loaded"));
2603
+ worker.onmessage = (event) => event.data.ok ? finish(resolve, event.data.value) : finish(reject, Error(event.data.error));
2604
+ worker.postMessage({
2605
+ action,
2606
+ payload
2607
+ });
2608
+ });
2609
+ }
2610
+ function encodeSketchDocument(doc) {
2611
+ return JSON.stringify({
2612
+ format: "dsh-sketch",
2613
+ version: 1,
2614
+ doc
2615
+ });
2616
+ }
2617
+ function decodeSketchDocument(text) {
2618
+ if (text.length > 32 * 1024 * 1024) throw Error("Draft exceeds 32 MB");
2619
+ const file = JSON.parse(text), source = file.doc;
2620
+ if (file.format !== "dsh-sketch" || file.version !== 1 || !source || !Array.isArray(source.layers) || !source.layers.length || source.layers.length > 8) throw Error("Invalid sketch file");
2621
+ const w = source.width ?? 1024, h = source.height ?? 1024;
2622
+ if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || h < 1 || w > 2048 || h > 2048) throw Error("Invalid canvas size");
2623
+ let doc = {
2624
+ ...createSketchLayers(),
2625
+ width: w,
2626
+ height: h,
2627
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === w && SKETCH_RATIOS[k][1] === h) ?? "custom"
2628
+ };
2629
+ for (let i = 0; i < source.layers.length; i++) {
2630
+ const layer = source.layers[i];
2631
+ if (i) doc = applySketchCommands(doc, [{
2632
+ op: "layer",
2633
+ action: "add"
2634
+ }]);
2635
+ if (!Array.isArray(layer.strokes)) throw Error("Invalid strokes");
2636
+ for (let j = 0; j < layer.strokes.length; j += 256) {
2637
+ const strokes = layer.strokes.slice(j, j + 256);
2638
+ doc = applySketchCommands(doc, strokes.map((s) => ({
2639
+ ...s,
2640
+ op: "stroke",
2641
+ layer: doc.active,
2642
+ fill: s.fill ?? false
2643
+ })));
2644
+ const added = doc.layers.at(-1).strokes;
2645
+ for (let k = 0; k < strokes.length; k++) {
2646
+ const s = strokes[k];
2647
+ if (s.brush !== void 0 && ![
2648
+ "pen",
2649
+ "pencil",
2650
+ "marker"
2651
+ ].includes(s.brush)) throw Error("Invalid brush");
2652
+ if (s.pressure !== void 0 && (!Number.isFinite(s.pressure) || s.pressure < .2 || s.pressure > 1)) throw Error("Invalid pressure");
2653
+ if (s.brushVersion !== void 0 && s.brushVersion !== 2) throw Error("Unsupported brush version");
2654
+ Object.assign(added[added.length - strokes.length + k], {
2655
+ brush: s.brush ?? "pen",
2656
+ pressure: s.pressure ?? 1,
2657
+ ...s.brushVersion === 2 ? { brushVersion: 2 } : {}
2658
+ });
2477
2659
  }
2478
- paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
2479
- entry.count = count;
2480
- entry.prefix = prefix;
2481
- entry.image = layer.image;
2482
- entry.strokes = layer.strokes;
2483
2660
  }
2484
- if (moving) {
2485
- const ctx = entry.surface.getContext("2d");
2486
- ctx.clearRect(0, 0, size, height);
2487
- ctx.drawImage(entry.base, 0, 0);
2488
- paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
2489
- context.drawImage(entry.surface, 0, 0);
2490
- } else context.drawImage(entry.base, 0, 0);
2661
+ const target = doc.layers.at(-1);
2662
+ target.name = String(layer.name ?? "").slice(0, 40);
2663
+ target.visible = layer.visible !== false;
2664
+ if (layer.image) {
2665
+ const image = layer.image;
2666
+ if (typeof image.src !== "string" || !/^data:image\/png;base64,/.test(image.src) || image.src.length > 8 * 1024 * 1024 || [
2667
+ "x",
2668
+ "y",
2669
+ "width",
2670
+ "height"
2671
+ ].some((k) => !Number.isFinite(image[k]) || image[k] < 0 || image[k] > 1)) throw Error("Invalid draft image");
2672
+ const bytes = Uint8Array.from(atob(image.src.slice(image.src.indexOf(",") + 1)), (c) => c.charCodeAt(0));
2673
+ if (bytes.length < 24) throw Error("Invalid draft image");
2674
+ const header = new DataView(bytes.buffer);
2675
+ if (header.getUint32(0) !== 2303741511 || header.getUint32(4) !== 218765834 || header.getUint32(16) < 1 || header.getUint32(20) < 1 || header.getUint32(16) > 4096 || header.getUint32(20) > 4096) throw Error("Invalid draft image size");
2676
+ target.image = {
2677
+ src: image.src,
2678
+ x: image.x,
2679
+ y: image.y,
2680
+ width: image.width,
2681
+ height: image.height
2682
+ };
2683
+ }
2491
2684
  }
2685
+ const activeIndex = source.layers.findIndex((layer) => layer.id === source.active);
2686
+ doc.active = doc.layers[Math.max(0, activeIndex)].id;
2687
+ return doc;
2492
2688
  }
2493
- //#endregion
2494
- //#region src/sketch-interactions.js
2495
- function useSketchDismiss(open, close, host, selectors) {
2496
- const latest = (0, react.useRef)(close);
2497
- latest.current = close;
2498
- (0, react.useEffect)(() => {
2499
- if (!open) return;
2500
- const dialog = host.current?.closest("dialog") ?? host.current;
2501
- if (!dialog) return;
2502
- const pointer = (event) => {
2503
- if (selectors.some((selector) => event.target.closest?.(selector))) return;
2504
- latest.current(false);
2505
- if (event.target.matches?.("canvas")) {
2506
- event.preventDefault();
2507
- event.stopPropagation();
2508
- event.target.focus({ preventScroll: true });
2509
- }
2510
- };
2511
- const key = (event) => {
2512
- if (event.key !== "Escape") return;
2513
- event.preventDefault();
2514
- event.stopPropagation();
2515
- latest.current(false);
2516
- dialog.querySelector("canvas")?.focus({ preventScroll: true });
2517
- };
2518
- const hidden = () => latest.current(false);
2519
- document.addEventListener("pointerdown", pointer, true);
2520
- document.addEventListener("keydown", key, true);
2521
- dialog.addEventListener("close", hidden);
2522
- return () => {
2523
- document.removeEventListener("pointerdown", pointer, true);
2524
- document.removeEventListener("keydown", key, true);
2525
- dialog.removeEventListener("close", hidden);
2689
+ async function exportSketchPsd(doc, images, composite) {
2690
+ const width = doc.width ?? 1024, height = doc.height ?? 1024;
2691
+ const children = doc.layers.map((layer, i) => {
2692
+ const ctx = canvas(width, height).getContext("2d"), ref = layer.image;
2693
+ if (ref) ctx.drawImage(images.get(ref.src), ref.x * width, ref.y * height, ref.width * width, ref.height * height);
2694
+ paintSketch(ctx, layer.strokes, width, true, height);
2695
+ return {
2696
+ name: layer.name || `Layer ${i + 1}`,
2697
+ hidden: !layer.visible,
2698
+ opacity: 1,
2699
+ blendMode: "normal",
2700
+ imageData: ctx.getImageData(0, 0, width, height)
2526
2701
  };
2527
- }, [
2528
- open,
2529
- host,
2530
- selectors.join("|")
2531
- ]);
2532
- }
2533
- function useSketchCursor(canvas, ring, width, brush, zoom, hidden) {
2534
- const last = (0, react.useRef)(null), heldPressure = (0, react.useRef)(1);
2535
- const update = (event, bounds) => {
2536
- if (event) last.current = event;
2537
- const pointer = last.current, node = canvas.current, cursor = ring.current;
2538
- if (!pointer || !node || !cursor) return;
2539
- const rect = bounds ?? node.getBoundingClientRect();
2540
- if (hidden || pointer.pointerType === "touch" || pointer.clientX < rect.left || pointer.clientX > rect.right || pointer.clientY < rect.top || pointer.clientY > rect.bottom) {
2541
- cursor.hidden = true;
2542
- return;
2543
- }
2544
- const diameter = width * (node.hasPointerCapture(pointer.pointerId) && pointer.pointerType === "pen" ? heldPressure.current : 1) * rect.width / node.width;
2545
- cursor.hidden = false;
2546
- cursor.style.width = `${diameter}px`;
2547
- cursor.style.height = `${diameter}px`;
2548
- cursor.style.transform = `translate(${pointer.clientX - diameter / 2}px,${pointer.clientY - diameter / 2}px)`;
2549
- };
2550
- (0, react.useEffect)(() => {
2551
- update();
2552
- const observer = new ResizeObserver(() => update());
2553
- if (canvas.current) observer.observe(canvas.current);
2554
- return () => observer.disconnect();
2555
- }, [
2702
+ });
2703
+ const context = canvas(width, height).getContext("2d");
2704
+ context.fillStyle = "#fff";
2705
+ context.fillRect(0, 0, width, height);
2706
+ if (children[0] && !children[0].hidden) {
2707
+ const bottom = canvas(width, height);
2708
+ bottom.getContext("2d").putImageData(children[0].imageData, 0, 0);
2709
+ context.drawImage(bottom, 0, 0);
2710
+ children[0].imageData = context.getImageData(0, 0, width, height);
2711
+ } else {
2712
+ if (children.length >= 8) throw Error("Show the bottom layer before exporting this eight-layer drawing");
2713
+ children.unshift({
2714
+ name: "Paper",
2715
+ opacity: 1,
2716
+ blendMode: "normal",
2717
+ imageData: context.getImageData(0, 0, width, height)
2718
+ });
2719
+ }
2720
+ return runPsdCodec("write", {
2556
2721
  width,
2557
- brush,
2558
- zoom,
2559
- hidden
2560
- ]);
2561
- return {
2562
- down: (event) => {
2563
- heldPressure.current = event.pointerType === "pen" ? Math.max(.2, event.pressure) : 1;
2564
- },
2565
- move: (event, bounds) => update({
2566
- clientX: event.clientX,
2567
- clientY: event.clientY,
2568
- pointerId: event.pointerId,
2569
- pointerType: event.pointerType,
2570
- pressure: event.pressure
2571
- }, bounds),
2572
- leave: () => {
2573
- last.current = null;
2574
- if (ring.current) ring.current.hidden = true;
2575
- }
2722
+ height,
2723
+ children,
2724
+ imageData: composite.getContext("2d").getImageData(0, 0, width, height)
2725
+ });
2726
+ }
2727
+ async function importSketchPsd(file) {
2728
+ if (file.size > 32 * 1024 * 1024) throw Error("PSD exceeds 32 MB");
2729
+ const psd = await runPsdCodec("read", await file.arrayBuffer());
2730
+ const scale = Math.min(1, 1024 / Math.max(psd.width, psd.height)), width = Math.max(1, Math.round(psd.width * scale)), height = Math.max(1, Math.round(psd.height * scale));
2731
+ const doc = {
2732
+ ...createSketchLayers(),
2733
+ width,
2734
+ height,
2735
+ ratio: Object.keys(SKETCH_RATIOS).find((k) => SKETCH_RATIOS[k][0] === width && SKETCH_RATIOS[k][1] === height) ?? "custom",
2736
+ layers: [],
2737
+ nextId: psd.layers.length + 1
2576
2738
  };
2739
+ for (const [i, layer] of psd.layers.entries()) {
2740
+ const src = canvas(layer.imageData.width, layer.imageData.height);
2741
+ src.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(layer.imageData.data), layer.imageData.width, layer.imageData.height), 0, 0);
2742
+ const out = canvas(width, height), ctx = out.getContext("2d");
2743
+ ctx.globalAlpha = layer.opacity;
2744
+ ctx.drawImage(src, layer.left * scale, layer.top * scale, src.width * scale, src.height * scale);
2745
+ doc.layers.push({
2746
+ id: i + 1,
2747
+ name: layer.name,
2748
+ visible: !layer.hidden,
2749
+ strokes: [],
2750
+ image: {
2751
+ src: out.toDataURL("image/png"),
2752
+ x: 0,
2753
+ y: 0,
2754
+ width: 1,
2755
+ height: 1
2756
+ }
2757
+ });
2758
+ }
2759
+ return doc;
2577
2760
  }
2578
2761
  //#endregion
2579
- //#region src/sketch-view.jsx
2580
- const DEFAULT_KEYS = {
2581
- pen: "b",
2582
- eraser: "e",
2583
- line: "l",
2584
- rectangle: "r",
2585
- circle: "o",
2586
- pan: " ",
2587
- zoomIn: "=",
2588
- zoomOut: "-",
2589
- fit: "0"
2590
- };
2591
- function useSketchView(canvas, open) {
2592
- const [view, setView] = (0, react.useState)({
2593
- scale: 1,
2594
- x: 0,
2595
- y: 0
2596
- }), [keys, setKeys] = (0, react.useState)(() => {
2762
+ //#region src/sketch-document-lifecycle.js
2763
+ function createSketchDocumentLifecycle(state, { sessionId, t, schedule, checkpoint, cache, setSelection, setTextEdit, setRecovered, confirmPsd = async () => false, store = sketchDrafts, decodeImages = decodeSketchImages, readImage = importSketchImage }) {
2764
+ const { doc, undo, redo, images, saved, dirty, documentId, documentRevision } = state;
2765
+ const hasContent = () => doc.current.layers.some((layer) => layer.image || layer.strokes.length);
2766
+ const save = async (name) => {
2767
+ const savingDocument = documentId.current, savingRevision = documentRevision.current;
2768
+ const row = {
2769
+ id: saved.current?.id ?? crypto.randomUUID(),
2770
+ name: name?.trim() || saved.current?.name || `${t("sketchTitle")} ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
2771
+ updated: Date.now(),
2772
+ doc: structuredClone(doc.current)
2773
+ };
2597
2774
  try {
2598
- return {
2599
- ...DEFAULT_KEYS,
2600
- ...JSON.parse(localStorage.getItem("codex-sketch-keys"))
2775
+ await store("save", row, sessionId);
2776
+ } catch (error) {
2777
+ if (error.code === "SKETCH_DRAFT_LIMIT") error.message = t("sketchDraftLimit");
2778
+ if (error.code === "SKETCH_STORAGE_LIMIT") error.message = t("sketchStorageLimit");
2779
+ throw error;
2780
+ }
2781
+ if (documentId.current === savingDocument) {
2782
+ saved.current = {
2783
+ id: row.id,
2784
+ name: row.name
2601
2785
  };
2602
- } catch {
2603
- return DEFAULT_KEYS;
2786
+ if (documentRevision.current === savingRevision) {
2787
+ dirty.current = false;
2788
+ setRecovered(false);
2789
+ }
2604
2790
  }
2605
- });
2606
- const [shortcuts, setShortcuts] = (0, react.useState)(() => {
2607
- try {
2608
- return localStorage.getItem("codex-sketch-shortcuts") !== "off";
2609
- } catch {
2610
- return true;
2791
+ };
2792
+ const saveChanges = async () => {
2793
+ if (!dirty.current) return;
2794
+ if (hasContent() || saved.current) return save();
2795
+ const id = documentId.current, revision = documentRevision.current;
2796
+ await store("clearRecovery", sessionId);
2797
+ if (documentId.current === id && documentRevision.current === revision) {
2798
+ dirty.current = false;
2799
+ setRecovered(false);
2611
2800
  }
2612
- }), [space, setSpace] = (0, react.useState)(false);
2613
- const drag = (0, react.useRef)(null), viewRef = (0, react.useRef)(view);
2614
- viewRef.current = view;
2615
- const zoom = (factor) => setView((v) => ({
2616
- ...v,
2617
- scale: Math.max(.25, Math.min(8, v.scale * factor))
2618
- }));
2619
- const reset = () => setView({
2620
- scale: 1,
2621
- x: 0,
2622
- y: 0
2623
- });
2624
- (0, react.useEffect)(() => {
2625
- const node = canvas.current;
2626
- if (!node || !open) return;
2627
- const wheel = (e) => {
2628
- if (!shortcuts || !e.altKey) return;
2629
- e.preventDefault();
2630
- zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1);
2801
+ };
2802
+ const replace = (next, decoded, identity) => {
2803
+ documentId.current = crypto.randomUUID();
2804
+ documentRevision.current++;
2805
+ doc.current = identifyObjects(structuredClone(next));
2806
+ setSelection(null);
2807
+ setTextEdit(null);
2808
+ images.current = decoded;
2809
+ cache.current.clear();
2810
+ undo.current = [];
2811
+ redo.current = [];
2812
+ saved.current = identity;
2813
+ dirty.current = false;
2814
+ schedule();
2815
+ };
2816
+ const fresh = async () => {
2817
+ await saveChanges();
2818
+ replace(createSketchLayers(), /* @__PURE__ */ new Map(), null);
2819
+ };
2820
+ const load = async (row) => {
2821
+ if (row.id === saved.current?.id) return;
2822
+ row = await store("get", row.id);
2823
+ if (!row) throw Error("Draft no longer exists");
2824
+ await saveChanges();
2825
+ const decoded = /* @__PURE__ */ new Map();
2826
+ await decodeImages(row.doc, decoded);
2827
+ replace(row.doc, decoded, {
2828
+ id: row.id,
2829
+ name: row.name
2830
+ });
2831
+ };
2832
+ const importImage = async (file) => {
2833
+ if (file.name?.toLowerCase().endsWith(".psd") || file.name?.toLowerCase().endsWith(".dsh-sketch.json")) {
2834
+ if (file.size > 32 * 1024 * 1024) throw Error("File exceeds 32 MB");
2835
+ if (file.name.toLowerCase().endsWith(".psd") && !await confirmPsd()) return;
2836
+ const next = file.name.toLowerCase().endsWith(".psd") ? await importSketchPsd(file) : decodeSketchDocument(await file.text());
2837
+ const decoded = /* @__PURE__ */ new Map();
2838
+ await decodeImages(next, decoded);
2839
+ await saveChanges();
2840
+ replace(next, decoded, null);
2841
+ dirty.current = true;
2842
+ return;
2843
+ }
2844
+ if (doc.current.layers.length >= 8) throw Error("Layer limit");
2845
+ const image = await readImage(file), w = doc.current.width ?? 1024, h = doc.current.height ?? 1024;
2846
+ const scale = Math.min(w / image.width, h / image.height), width = image.width * scale / w, height = image.height * scale / h;
2847
+ const layer = {
2848
+ id: doc.current.nextId,
2849
+ name: file.name?.slice(0, 40) || t("sketchImport"),
2850
+ visible: true,
2851
+ strokes: [],
2852
+ image: {
2853
+ src: image.src,
2854
+ x: (1 - width) / 2,
2855
+ y: (1 - height) / 2,
2856
+ width,
2857
+ height
2858
+ }
2631
2859
  };
2632
- node.addEventListener("wheel", wheel, { passive: false });
2633
- return () => node.removeEventListener("wheel", wheel);
2634
- }, [open, shortcuts]);
2635
- (0, react.useEffect)(() => {
2636
- const stop = () => {
2637
- drag.current = null;
2638
- setSpace(false);
2860
+ await decodeImages({ layers: [layer] }, images.current);
2861
+ checkpoint();
2862
+ doc.current = {
2863
+ ...doc.current,
2864
+ nextId: layer.id + 1,
2865
+ active: layer.id,
2866
+ layers: [...doc.current.layers, layer]
2639
2867
  };
2640
- window.addEventListener("blur", stop);
2641
- return () => window.removeEventListener("blur", stop);
2642
- }, []);
2643
- (0, react.useEffect)(() => {
2644
- if (!open || !shortcuts) {
2645
- setSpace(false);
2646
- drag.current = null;
2868
+ schedule();
2869
+ };
2870
+ const restore = async (isCurrent = () => true) => {
2871
+ if (dirty.current || hasContent()) return;
2872
+ const id = documentId.current, revision = documentRevision.current;
2873
+ const recovery = await store("recover", sessionId);
2874
+ const archived = state.restoreId.current;
2875
+ const row = recovery ?? (archived ? await store("get", archived) : null);
2876
+ const decoded = /* @__PURE__ */ new Map();
2877
+ if (row) await decodeImages(row.doc, decoded);
2878
+ if (!isCurrent() || documentId.current !== id || documentRevision.current !== revision || dirty.current) return;
2879
+ if (row) {
2880
+ replace(row.doc, decoded, recovery ? null : {
2881
+ id: row.id,
2882
+ name: row.name
2883
+ });
2884
+ dirty.current = Boolean(recovery);
2885
+ setRecovered(Boolean(recovery));
2647
2886
  }
2648
- }, [open, shortcuts]);
2649
- const setKey = (action, key) => {
2650
- key = key.toLowerCase();
2651
- if (!key || Object.entries(keys).some(([a, k]) => a !== action && k === key) || ["[", "]"].includes(key)) return;
2652
- const next = {
2653
- ...keys,
2654
- [action]: key
2655
- };
2656
- setKeys(next);
2657
- try {
2658
- localStorage.setItem("codex-sketch-keys", JSON.stringify(next));
2659
- } catch {}
2887
+ state.restoreId.current = null;
2888
+ };
2889
+ return {
2890
+ hasContent,
2891
+ save,
2892
+ saveChanges,
2893
+ replace,
2894
+ fresh,
2895
+ load,
2896
+ importImage,
2897
+ restore
2660
2898
  };
2899
+ }
2900
+ //#endregion
2901
+ //#region src/sketch-operation-gate.js
2902
+ function createSketchOperationGate() {
2903
+ let running = false;
2661
2904
  return {
2662
- view,
2663
- keys,
2664
- shortcuts,
2665
- space,
2666
- zoom,
2667
- reset,
2668
- setKey,
2669
- toggle: () => setShortcuts((v) => {
2905
+ get running() {
2906
+ return running;
2907
+ },
2908
+ async run(operation, { blocked = false, working, report, rethrow = false }) {
2909
+ if (blocked || running) return false;
2910
+ running = true;
2670
2911
  try {
2671
- localStorage.setItem("codex-sketch-shortcuts", v ? "off" : "on");
2672
- } catch {}
2673
- return !v;
2674
- }),
2675
- keyDown: (e) => {
2676
- if (!shortcuts || e.ctrlKey || e.metaKey || e.altKey) return false;
2677
- const action = sketchShortcutAction(keys, e.key);
2678
- if (action === "pan") {
2679
- e.preventDefault();
2680
- setSpace(true);
2681
- return true;
2682
- }
2683
- if ([
2684
- "zoomIn",
2685
- "zoomOut",
2686
- "fit"
2687
- ].includes(action)) {
2688
- e.preventDefault();
2689
- if (action === "fit") reset();
2690
- else zoom(action === "zoomOut" ? 1 / 1.2 : 1.2);
2912
+ working(true);
2913
+ report(null);
2914
+ await operation();
2691
2915
  return true;
2916
+ } catch (error) {
2917
+ report(error);
2918
+ if (rethrow) throw error;
2919
+ return false;
2920
+ } finally {
2921
+ running = false;
2922
+ working(false);
2692
2923
  }
2693
- return false;
2694
- },
2695
- keyUp: (e) => {
2696
- if (e.key.toLowerCase() === keys.pan) setSpace(false);
2697
- },
2698
- down: (e) => {
2699
- if (e.button !== 1 && !space) return false;
2700
- e.preventDefault();
2701
- drag.current = {
2702
- id: e.pointerId,
2703
- x: e.clientX,
2704
- y: e.clientY,
2705
- view: viewRef.current
2706
- };
2707
- canvas.current.setPointerCapture(e.pointerId);
2708
- return true;
2709
- },
2710
- move: (e) => {
2711
- const d = drag.current;
2712
- if (!d || d.id !== e.pointerId) return false;
2713
- setView({
2714
- ...d.view,
2715
- x: d.view.x + e.clientX - d.x,
2716
- y: d.view.y + e.clientY - d.y
2717
- });
2718
- return true;
2719
- },
2720
- end: (e) => {
2721
- if (drag.current?.id !== e.pointerId) return false;
2722
- drag.current = null;
2723
- if (canvas.current.hasPointerCapture(e.pointerId)) canvas.current.releasePointerCapture(e.pointerId);
2724
- return true;
2725
2924
  }
2726
2925
  };
2727
2926
  }
2728
- function SketchViewControls({ navigation, t }) {
2729
- const [open, setOpen] = (0, react.useState)(false), [placement, setPlacement] = (0, react.useState)(null);
2730
- const host = (0, react.useRef)(null), trigger = (0, react.useRef)(null);
2731
- const close = () => {
2732
- setOpen(false);
2733
- trigger.current?.focus({ preventScroll: true });
2734
- };
2735
- useSketchDismiss(open, setOpen, host, [".codexSketchViewControls", ".codexSketchKeyPanel"]);
2736
- (0, react.useLayoutEffect)(() => {
2737
- if (!open) return;
2738
- const dialog = host.current.closest("dialog");
2739
- const place = () => {
2740
- const box = dialog.getBoundingClientRect(), anchor = trigger.current.getBoundingClientRect();
2741
- const width = Math.min(360, box.width - 24);
2742
- setPlacement({
2743
- dialog,
2744
- style: {
2745
- width,
2746
- left: Math.max(12, Math.min(anchor.left - box.left, box.width - width - 12)),
2747
- bottom: box.bottom - anchor.top + 8,
2748
- maxHeight: Math.max(80, anchor.top - box.top - 24)
2749
- }
2750
- });
2751
- };
2752
- place();
2753
- const observer = new ResizeObserver(place);
2754
- observer.observe(dialog);
2755
- observer.observe(host.current);
2756
- window.addEventListener("resize", place);
2757
- return () => {
2758
- observer.disconnect();
2759
- window.removeEventListener("resize", place);
2927
+ //#endregion
2928
+ //#region src/sketch-agent-export.js
2929
+ async function exportSketchAgentFile(format, { gate, blocked, working, report, exportFile }) {
2930
+ let result;
2931
+ if (!await gate.run(async () => {
2932
+ const { blob, extension } = await exportFile(format);
2933
+ const data = new Uint8Array(await blob.arrayBuffer());
2934
+ let raw = "";
2935
+ for (let i = 0; i < data.length; i += 8192) raw += String.fromCharCode(...data.subarray(i, i + 8192));
2936
+ result = {
2937
+ extension,
2938
+ mediaType: blob.type,
2939
+ base64: btoa(raw)
2760
2940
  };
2761
- }, [open]);
2941
+ }, {
2942
+ blocked,
2943
+ working,
2944
+ report,
2945
+ rethrow: true
2946
+ })) throw Error("Sketch is being edited; retry after it settles");
2947
+ return result;
2948
+ }
2949
+ //#endregion
2950
+ //#region src/sketch-run-status.jsx
2951
+ function SketchRunStatus({ state, t, floating = false, onOpen, onStop, onResume, onDismiss }) {
2952
+ if (state === "idle") return null;
2953
+ const drawing = state === "drawing", recover = state === "stopped" || state === "failed";
2762
2954
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2763
- ref: host,
2764
- className: "codexSketchViewControls",
2955
+ className: floating ? "codexSketchBackgroundStatus" : "codexSketchAgentStatus",
2956
+ role: "status",
2957
+ "aria-live": "polite",
2765
2958
  children: [
2766
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2767
- type: "button",
2768
- "aria-label": t("sketchZoomOut"),
2769
- onClick: () => navigation.zoom(1 / 1.2),
2770
- children: "−"
2959
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2960
+ name: drawing ? "pen" : state === "finished" ? "check" : "rectangle",
2961
+ size: 15
2771
2962
  }),
2772
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2963
+ floating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2773
2964
  type: "button",
2774
- title: t("sketchFit"),
2775
- onClick: navigation.reset,
2776
- children: [Math.round(navigation.view.scale * 100), "%"]
2777
- }),
2778
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2965
+ onClick: onOpen,
2966
+ children: t(`sketchRun_${state}`)
2967
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(`sketchRun_${state}`) }),
2968
+ drawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2779
2969
  type: "button",
2780
- "aria-label": t("sketchZoomIn"),
2781
- onClick: () => navigation.zoom(1.2),
2782
- children: "+"
2783
- }),
2784
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2785
- ref: trigger,
2970
+ className: "codexSketchStop",
2971
+ onClick: onStop,
2972
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2973
+ name: "stop",
2974
+ size: 12
2975
+ }), t("sketchRunStop")]
2976
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2786
2977
  type: "button",
2787
- "aria-expanded": open,
2788
- onClick: () => setOpen(!open),
2789
- children: t("sketchKeys")
2790
- }),
2791
- open && placement ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2792
- className: "codexSketchKeyPanel",
2793
- "aria-label": t("sketchKeys"),
2794
- style: placement.style,
2795
- onKeyDown: (e) => {
2796
- if (e.key === "Escape") {
2797
- e.preventDefault();
2798
- e.stopPropagation();
2799
- close();
2800
- }
2801
- },
2802
- children: [
2803
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchKeys") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2804
- type: "button",
2805
- "aria-label": t("sketchFileClose"),
2806
- onClick: close,
2807
- children: "×"
2808
- })] }),
2809
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2810
- className: "codexSketchKeysEnabled",
2811
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sketchKeysEnabled") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2812
- type: "checkbox",
2813
- checked: navigation.shortcuts,
2814
- onChange: navigation.toggle
2815
- })]
2816
- }),
2817
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sketchNavigationHint") }),
2818
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2819
- className: "codexSketchKeyGrid",
2820
- children: Object.entries(navigation.keys).map(([action, key]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t(`sketchKey_${action}`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2821
- "aria-label": t(`sketchKey_${action}`),
2822
- value: key === " " ? "Space" : key,
2823
- readOnly: true,
2824
- onKeyDown: (e) => {
2825
- if (e.key === "Tab" || e.key === "Escape") return;
2826
- e.preventDefault();
2827
- e.stopPropagation();
2828
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) navigation.setKey(action, e.key);
2829
- }
2830
- })] }, action))
2831
- }),
2832
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchKeyHint") })
2833
- ]
2834
- }), placement.dialog) : null
2978
+ title: t("sketchRunResumeHint"),
2979
+ onClick: onResume,
2980
+ children: t("sketchRunResume")
2981
+ }) : null, !recover ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2982
+ type: "button",
2983
+ "aria-label": t("sketchDismissStatus"),
2984
+ title: t("sketchDismissStatus"),
2985
+ onClick: onDismiss,
2986
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceIcon, {
2987
+ name: "close",
2988
+ size: 14
2989
+ })
2990
+ }) : null] })
2835
2991
  ]
2836
2992
  });
2837
2993
  }
2838
2994
  //#endregion
2995
+ //#region src/sketch-tool-widths.js
2996
+ const defaults = {
2997
+ pen: 12,
2998
+ pencil: 6,
2999
+ marker: 28,
3000
+ eraser: 24,
3001
+ text: 32,
3002
+ line: 12,
3003
+ arrow: 12,
3004
+ rectangle: 12,
3005
+ circle: 12
3006
+ };
3007
+ const keyFor = ({ tool, brush }) => tool === "pen" ? brush : tool;
3008
+ function switchSketchToolWidth(memory, current, next) {
3009
+ if (current.tool !== "select") memory[keyFor(current)] = current.width;
3010
+ if (next.tool === "select") return current.width;
3011
+ const key = keyFor(next);
3012
+ return memory[key] ?? defaults[key] ?? 12;
3013
+ }
3014
+ function stepSketchWidth(width, direction) {
3015
+ return Math.max(1, Math.min(256, width + direction * 2));
3016
+ }
3017
+ //#endregion
3018
+ //#region src/sketch-layer-renderer.js
3019
+ const NO_IMAGES = /* @__PURE__ */ new Map();
3020
+ const surface = (width, height) => {
3021
+ const c = document.createElement("canvas");
3022
+ c.width = width;
3023
+ c.height = height;
3024
+ return c;
3025
+ };
3026
+ function paintSketchLayers(context, doc, cache, size = doc.width ?? 1024, height = doc.height ?? size, activeLayer, images = NO_IMAGES) {
3027
+ context.globalCompositeOperation = "source-over";
3028
+ context.globalAlpha = 1;
3029
+ context.fillStyle = "#fff";
3030
+ context.fillRect(0, 0, size, height);
3031
+ for (const id of cache.keys()) if (!doc.layers.some((layer) => layer.id === id)) cache.delete(id);
3032
+ for (const layer of doc.layers) {
3033
+ if (!layer.visible) continue;
3034
+ let entry = cache.get(layer.id);
3035
+ if (!entry || entry.surface.width !== size || entry.surface.height !== height) {
3036
+ entry = {
3037
+ surface: surface(size, height),
3038
+ base: surface(size, height)
3039
+ };
3040
+ cache.set(layer.id, entry);
3041
+ }
3042
+ const moving = layer.id === activeLayer;
3043
+ const count = Math.max(0, layer.strokes.length - (moving ? 1 : 0));
3044
+ const prefix = layer.strokes[count - 1];
3045
+ if (entry.count !== count || entry.prefix !== prefix || entry.image !== layer.image || entry.strokes !== layer.strokes) {
3046
+ const ctx = entry.base.getContext("2d");
3047
+ const append = entry.count !== void 0 && count >= entry.count && entry.image === layer.image && (entry.strokes === layer.strokes || entry.strokes.slice(0, entry.count).every((stroke, index) => stroke === layer.strokes[index]));
3048
+ if (!append) {
3049
+ ctx.clearRect(0, 0, size, height);
3050
+ const ref = layer.image, image = ref && images.get(ref.src);
3051
+ if (image) ctx.drawImage(image, ref.x * size, ref.y * height, ref.width * size, ref.height * height);
3052
+ }
3053
+ paintSketch(ctx, layer.strokes, size, true, height, append ? entry.count : 0, count);
3054
+ entry.count = count;
3055
+ entry.prefix = prefix;
3056
+ entry.image = layer.image;
3057
+ entry.strokes = layer.strokes;
3058
+ }
3059
+ if (moving) {
3060
+ const ctx = entry.surface.getContext("2d");
3061
+ ctx.clearRect(0, 0, size, height);
3062
+ ctx.drawImage(entry.base, 0, 0);
3063
+ paintSketch(ctx, layer.strokes, size, true, height, layer.strokes.length - 1);
3064
+ context.drawImage(entry.surface, 0, 0);
3065
+ } else context.drawImage(entry.base, 0, 0);
3066
+ }
3067
+ }
3068
+ //#endregion
2839
3069
  //#region src/sketch-files.jsx
2840
3070
  function SketchFiles({ save, load, fresh, importImage, download, hasContent, disabled, t, runOperation }) {
2841
3071
  const [open, setOpen] = (0, react.useState)(false), [rows, setRows] = (0, react.useState)([]), [name, setName] = (0, react.useState)(""), [remove, setRemove] = (0, react.useState)(null), [working, setWorking] = (0, react.useState)(false);
@@ -3248,17 +3478,9 @@ window.__ModuleLoader__.load({
3248
3478
  }
3249
3479
  //#endregion
3250
3480
  //#region src/sketch-studio.jsx
3251
- const PALETTE = [
3252
- "#18181b",
3253
- "#929398",
3254
- "#ff3936",
3255
- "#ff9500",
3256
- "#ffcc00",
3257
- "#34c759",
3258
- "#0088ff"
3259
- ];
3260
3481
  function SketchStudio({ open, agentEnabled, agentPreview, onOpen, onClose, attachSketch, enabled, t, incoming, sessionId, rpc, sessionState }) {
3261
3482
  const localSession = (0, react.useRef)(null);
3483
+ const psdImport = useSketchImportConfirm(t);
3262
3484
  localSession.current ??= sessionState ?? createSketchSessionState();
3263
3485
  const { doc, undo, redo, images, saved, dirty, documentId, documentRevision, agentAdapter, agentSession, agentRun } = localSession.current;
3264
3486
  const dialog = (0, react.useRef)(null), canvas = (0, react.useRef)(null), cache = (0, react.useRef)(/* @__PURE__ */ new Map());
@@ -3377,6 +3599,7 @@ window.__ModuleLoader__.load({
3377
3599
  cache,
3378
3600
  setSelection,
3379
3601
  setTextEdit,
3602
+ confirmPsd: psdImport.confirmPsd,
3380
3603
  setRecovered
3381
3604
  });
3382
3605
  (0, react.useEffect)(() => localSession.current.retain?.(), []);
@@ -4133,47 +4356,14 @@ window.__ModuleLoader__.load({
4133
4356
  })
4134
4357
  ]
4135
4358
  }) : null,
4136
- picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
4137
- className: "codexSketchPictures",
4138
- children: [
4139
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("sketchPictures") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4140
- type: "button",
4141
- disabled: agentLocked || busy,
4142
- onClick: () => pictureInput.current.click(),
4143
- children: t("sketchPictureAdd")
4144
- })] }),
4145
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4146
- ref: pictureInput,
4147
- hidden: true,
4148
- type: "file",
4149
- accept: "image/png,image/jpeg,image/webp",
4150
- onChange: (e) => {
4151
- const file = e.target.files?.[0];
4152
- e.target.value = "";
4153
- if (file) runFile(() => importImage(file));
4154
- }
4155
- }),
4156
- doc.current.layers.filter((layer) => layer.image).map((layer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4157
- "data-active": layer.id === doc.current.active,
4158
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4159
- type: "button",
4160
- disabled: agentLocked || busy,
4161
- "aria-label": `${t("sketchPictureSelect")} ${layer.name}`,
4162
- onClick: () => change("select", layer.id),
4163
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
4164
- src: layer.image.src,
4165
- alt: layer.name
4166
- })
4167
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4168
- type: "button",
4169
- disabled: agentLocked || busy,
4170
- "aria-label": `${t("sketchDeleteDraft")} ${layer.name}`,
4171
- onClick: () => change(doc.current.layers.length === 1 ? "clear" : "delete", layer.id),
4172
- children: "×"
4173
- })]
4174
- }, layer.id)),
4175
- !doc.current.layers.some((layer) => layer.image) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("sketchPicturesEmpty") }) : null
4176
- ]
4359
+ picturesOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchPicturePanel, {
4360
+ document: doc.current,
4361
+ disabled: agentLocked || busy,
4362
+ pictureInput,
4363
+ importImage,
4364
+ runFile,
4365
+ change,
4366
+ t
4177
4367
  }) : null,
4178
4368
  layersOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchLayerPanel, {
4179
4369
  document: doc.current,
@@ -4183,119 +4373,30 @@ window.__ModuleLoader__.load({
4183
4373
  }) : null
4184
4374
  ]
4185
4375
  }),
4186
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4187
- className: "codexSketchControls",
4188
- children: [
4189
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4190
- type: "button",
4191
- className: "codexSketchPicturesToggle",
4192
- "aria-expanded": picturesOpen,
4193
- onClick: () => setPicturesOpen((v) => !v),
4194
- children: t("sketchPictures")
4195
- }),
4196
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchViewControls, {
4197
- navigation,
4198
- t
4199
- }),
4200
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchToolPicker, {
4201
- t,
4202
- disabled: agentLocked || busy,
4203
- tool,
4204
- brush,
4205
- chooseBrush,
4206
- chooseTool,
4207
- shapesOpen,
4208
- setShapesOpen
4209
- }),
4210
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4211
- className: "codexLayerBrush",
4212
- children: [
4213
- ["rectangle", "circle"].includes(tool) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4214
- type: "checkbox",
4215
- disabled: agentLocked || busy,
4216
- checked: fillShape,
4217
- onChange: (e) => setFillShape(e.target.checked)
4218
- }), t("sketchFill")] }) : null,
4219
- selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4220
- className: "codexSketchObjectActions",
4221
- children: [
4222
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4223
- disabled: agentLocked || busy,
4224
- onClick: () => editObject({}, "duplicate"),
4225
- children: t("sketchObjectDuplicate")
4226
- }),
4227
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4228
- disabled: agentLocked || busy,
4229
- onClick: () => editObject({}, "delete"),
4230
- children: t("sketchObjectDelete")
4231
- }),
4232
- selected.shape === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4233
- disabled: agentLocked || busy,
4234
- onClick: () => setTextEdit({
4235
- selection,
4236
- value: selected.text
4237
- }),
4238
- children: t("sketchText")
4239
- }) : null
4240
- ]
4241
- }) : null,
4242
- tool === "pen" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4243
- className: "codexSketchStability",
4244
- children: [t("sketchStability"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4245
- "aria-label": t("sketchStability"),
4246
- value: stability,
4247
- disabled: agentLocked || busy,
4248
- onChange: (e) => setStability(Number(e.target.value)),
4249
- children: [
4250
- 0,
4251
- 25,
4252
- 50,
4253
- 75
4254
- ].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4255
- value,
4256
- children: t(`sketchStability${value}`)
4257
- }, value))
4258
- })]
4259
- }) : null,
4260
- tool === "eraser" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4261
- className: "codexSketchSegment",
4262
- role: "group",
4263
- "aria-label": t("sketchEraserMode"),
4264
- children: ["pixel", "stroke"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4265
- type: "button",
4266
- "aria-pressed": eraser === value,
4267
- disabled: agentLocked || busy,
4268
- onClick: () => setEraser(value),
4269
- children: t(`sketchErase_${value}`)
4270
- }, value))
4271
- }) : null
4272
- ]
4273
- }),
4274
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4275
- className: "codexSketchPalette",
4276
- role: "group",
4277
- "aria-label": t("sketchColor"),
4278
- children: [PALETTE.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4279
- type: "button",
4280
- className: "codexSketchSwatch",
4281
- style: { "--swatch": value },
4282
- "aria-label": `${t("sketchColor")} ${value}`,
4283
- "aria-pressed": (selected?.color ?? color) === value,
4284
- disabled: agentLocked || busy,
4285
- onClick: () => pickColor(value)
4286
- }, value)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4287
- className: "codexSketchCustom",
4288
- title: t("sketchColor"),
4289
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { background: color } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4290
- type: "color",
4291
- "aria-label": t("sketchColor"),
4292
- value: color,
4293
- disabled: agentLocked || busy,
4294
- onChange: (e) => pickColor(e.target.value)
4295
- })]
4296
- })]
4297
- })
4298
- ]
4376
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SketchControls, {
4377
+ t,
4378
+ navigation,
4379
+ picturesOpen,
4380
+ setPicturesOpen,
4381
+ disabled: agentLocked || busy,
4382
+ tool,
4383
+ brush,
4384
+ chooseBrush,
4385
+ chooseTool,
4386
+ shapesOpen,
4387
+ setShapesOpen,
4388
+ fillShape,
4389
+ setFillShape,
4390
+ selected,
4391
+ editObject,
4392
+ selection,
4393
+ setTextEdit,
4394
+ stability,
4395
+ setStability,
4396
+ eraser,
4397
+ setEraser,
4398
+ color,
4399
+ pickColor
4299
4400
  }),
4300
4401
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4301
4402
  className: "codexSketchHint",
@@ -4305,13 +4406,15 @@ window.__ModuleLoader__.load({
4305
4406
  onClick: () => retryRestore((value) => value + 1),
4306
4407
  children: t("accountRetry")
4307
4408
  }) : null]
4308
- }) : null
4409
+ }) : null,
4410
+ psdImport.prompt
4309
4411
  ]
4310
4412
  })] });
4311
4413
  }
4312
4414
  //#endregion
4313
4415
  //#region src/sketch-styles.js
4314
4416
  const SKETCH_CSS = `
4417
+ .codexSketchImportConfirm{width:min(440px,calc(100vw - 32px));box-sizing:border-box;padding:24px;border:1px solid var(--sketch-line);border-radius:20px;background:var(--sketch-bg);color:var(--sketch-fg);box-shadow:0 12px 60px #0005}.codexSketchImportConfirm::backdrop{background:#0007}.codexSketchImportConfirm h3{margin:0 0 12px;font-size:16px}.codexSketchImportConfirm p{font-size:14px;line-height:1.7;color:var(--sketch-muted)}.codexSketchImportConfirm>div{display:flex;justify-content:flex-end;gap:12px}.codexSketchImportConfirm button{padding:8px 16px;border-radius:12px;border:1px solid var(--sketch-line);background:var(--sketch-line);color:var(--sketch-fg)}
4315
4418
  .codexSketchSizeModes{display:flex;flex-direction:column;gap:3px}.codexSketchSizeModes button{font-size:11px;padding:4px 6px;border-radius:10px;color:var(--sketch-muted)}.codexSketchSizeModes button[aria-pressed=true]{background:var(--sketch-line);color:var(--sketch-fg)}
4316
4419
 
4317
4420
  .codexSketchSizeControl{position:absolute;left:10px;top:50%;transform:translateY(-50%);z-index:2;display:flex;flex-direction:column;align-items:center;gap:10px;padding:12px 6px;border:1px solid var(--sketch-line);border-radius:24px;background:var(--sketch-glass);backdrop-filter:blur(18px);box-shadow:0 3px 14px #0001}
@@ -4524,6 +4627,15 @@ window.__ModuleLoader__.load({
4524
4627
  //#endregion
4525
4628
  //#region src/client-locales.js
4526
4629
  const zh = {
4630
+ storageLoading: "正在读取缓存占用…",
4631
+ storageTitle: "存储与缓存",
4632
+ storageForecast: "额度预测缓存",
4633
+ storageClear: "清除预测历史",
4634
+ storageHint: "仅清理本地预测历史,保留原图、草稿、登录信息和共享依赖。子任务组件可在“模型与运行”中管理。",
4635
+ storageConfirm: "清除所有已登录账号的本地预测历史后,需要重新积累采样。服务端额度不变,后续查询会重新生成缓存。",
4636
+ storageFailed: "未能读取或清除预测缓存,请重试。",
4637
+ sketchPsdConfirmTitle: "导入 PSD 副本",
4638
+ sketchPsdConfirmBody: "支持最多 8 层、8 位 RGB 的简单 PSD(32 MB 内,边长不超过 4096)。导入后各层转为图片,最长边缩至 1024;不支持蒙版、调整层和特殊混合效果。原文件不会修改。需要保留草图对象的可编辑性,请使用 DSH 草图格式。继续导入?",
4527
4639
  runtimeIncompatible: "检测到不兼容的 Codex 子任务组件,请卸载后重启,再安装受支持版本。",
4528
4640
  runtimeManagedElsewhere: "此组件由宿主或其他依赖管理,请在 DSH 插件管理中查看;这里不能直接卸载。",
4529
4641
  runtimeLoading: "正在检查组件…",
@@ -4891,9 +5003,16 @@ window.__ModuleLoader__.load({
4891
5003
  contextTokens: "Token 上限",
4892
5004
  contextFixed: "固定 {value}",
4893
5005
  contextMaximum: "范围 {minimum}–{value}",
4894
- settingsTab_account: "账号与偏好",
5006
+ settingsTab_account: "账号与额度",
4895
5007
  settingsTab_display: "使用偏好",
4896
- settingsTab_advanced: "高级与诊断",
5008
+ settingsTab_advanced: "模型与运行",
5009
+ settingsTab_creative: "图片与草图",
5010
+ settingsTab_maintenance: "维护",
5011
+ creativeSketch: "草图画板",
5012
+ diagnosticsHint: "遇到问题时生成报告,可复制后用于反馈。不含登录凭据和账号标识。",
5013
+ sketchCanvasSetting: "画板入口",
5014
+ sketchAgentSetting: "Agent 绘图",
5015
+ sketchAgentPreviewSetting: "完成后返回预览",
4897
5016
  quotaShowIndicator: "显示额度",
4898
5017
  quotaIndicatorHint: "输入框只显示图标,点击查看详情。关闭时暂停续航采样。",
4899
5018
  quotaForecastOptional: "续航预测 · 实验",
@@ -4980,6 +5099,15 @@ window.__ModuleLoader__.load({
4980
5099
  imageRemoveAnnotation: "删除标注"
4981
5100
  };
4982
5101
  const en = {
5102
+ storageLoading: "Reading cache size…",
5103
+ storageTitle: "Storage and cache",
5104
+ storageForecast: "Quota forecast cache",
5105
+ storageClear: "Clear forecast history",
5106
+ storageHint: "Clears local forecast history only. Original images, drafts, sign-in data and shared dependencies are preserved. Manage subtask components under Models & runtime.",
5107
+ storageConfirm: "This clears local forecast samples for all signed-in accounts. Forecasts will need new samples. Server quotas stay unchanged; subsequent checks recreate the cache.",
5108
+ storageFailed: "Could not read or clear the forecast cache. Try again.",
5109
+ sketchPsdConfirmTitle: "Import a PSD copy",
5110
+ sketchPsdConfirmBody: "Supports simple 8-bit RGB PSDs with up to 8 layers, 32 MB and 4096 pixels per side. Layers become images and the longest side is reduced to 1024 pixels. Masks, adjustments and special blending effects are unsupported. The original file stays unchanged. Use the DSH sketch format to preserve editable sketch objects. Continue?",
4983
5111
  runtimeIncompatible: "An incompatible Codex subtask component was found. Uninstall it, restart DSH, then install the supported version.",
4984
5112
  runtimeManagedElsewhere: "This component is managed by the host or another dependency. Check DSH plugin management; it cannot be removed here.",
4985
5113
  runtimeLoading: "Checking component…",
@@ -5347,9 +5475,16 @@ window.__ModuleLoader__.load({
5347
5475
  contextTokens: "Token limit",
5348
5476
  contextFixed: "Fixed {value}",
5349
5477
  contextMaximum: "{minimum}–{value}",
5350
- settingsTab_account: "Account & preferences",
5478
+ settingsTab_account: "Account",
5351
5479
  settingsTab_display: "Preferences",
5352
- settingsTab_advanced: "Advanced",
5480
+ settingsTab_advanced: "Models & runtime",
5481
+ settingsTab_creative: "Images & sketch",
5482
+ settingsTab_maintenance: "Maintenance",
5483
+ creativeSketch: "Sketch board",
5484
+ diagnosticsHint: "Generate a report to copy into a support request. Sign-in credentials and account identifiers are excluded.",
5485
+ sketchCanvasSetting: "Board entry",
5486
+ sketchAgentSetting: "Agent drawing",
5487
+ sketchAgentPreviewSetting: "Return preview on completion",
5353
5488
  quotaShowIndicator: "Show quota",
5354
5489
  quotaIndicatorHint: "Show an icon in the composer; click for details. Hiding it pauses forecast sampling.",
5355
5490
  quotaForecastOptional: "Runway forecast · Experimental",
@@ -5530,7 +5665,7 @@ window.__ModuleLoader__.load({
5530
5665
  .codexSubscriptionRecover{display:flex;align-items:center;justify-content:space-between;gap:12px}
5531
5666
  .codexSubscriptionRecover .codexSubscriptionError{flex:1}
5532
5667
  .codexSubscriptionRecover button{flex:0 0 auto}
5533
- .codexSubscriptionDiagnostics{padding:8px 12px;gap:8px;background:transparent;color:var(--dsw-alias-label-secondary)}
5668
+ .codexSubscriptionDiagnostics{color:var(--dsw-alias-label-secondary)}
5534
5669
  .codexSubscriptionDiagnostics pre{max-height:240px;margin:0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-secondary)}
5535
5670
  .codexSubscriptionLink{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-decoration:none;white-space:nowrap}
5536
5671
  .codexSubscriptionLink:hover{background:var(--dsw-alias-bg-module-platform)}
@@ -5588,8 +5723,9 @@ window.__ModuleLoader__.load({
5588
5723
  .codexSubscriptionSpendLimit progress::-webkit-progress-bar{background:var(--dsw-alias-border-l3);border-radius:999px}
5589
5724
  .codexSubscriptionSpendLimit progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
5590
5725
  .codexSubscriptionSpendLimit progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#3964fe);border-radius:999px}
5591
- .codexSettingsTabs{display:flex;gap:4px;padding:3px;background:var(--dsw-alias-bg-module-platform);border-radius:10px}
5592
- .codexSettingsTabs button{flex:1;border:0;border-radius:8px;padding:7px 8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer}
5726
+ .codexSettingsTabs{position:sticky;top:0;z-index:2;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:4px;padding:4px;background:var(--dsw-alias-bg-module-platform);border-radius:12px}
5727
+ .codexSettingsTabs button{min-width:0;min-height:36px;border:0;border-radius:9px;padding:7px 6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}
5728
+ .codexSettingsTabs button:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}
5593
5729
  .codexSettingsTabs button[aria-selected=true]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px #0001}
5594
5730
  .codexSubscription>[role=tabpanel]:not([hidden]){display:flex;flex-direction:column;gap:10px}
5595
5731
  .codexComposerQuota:focus-visible{outline:1px solid var(--dsw-alias-border-l3);outline-offset:2px}
@@ -5669,6 +5805,25 @@ window.__ModuleLoader__.load({
5669
5805
  .codexSubscriptionRuntime button{flex-shrink:0;white-space:nowrap}
5670
5806
  .codexSubscriptionRuntime code{display:block;overflow-wrap:anywhere;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;user-select:text}
5671
5807
  @media(max-width:640px){.codexSubscriptionCard{padding:14px}}
5808
+ .codexSubscriptionHelp{font-size:12px;line-height:1.65;color:var(--dsw-alias-label-tertiary)}
5809
+ .codexSubscriptionBadge{flex:none;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-module-platform);font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}
5810
+ .codexImageSettings>.codexSubscriptionSectionHead{flex-direction:row;align-items:center}
5811
+ .codexSubscriptionSettingRows>.codexImagePreference+.codexImagePreference{border-top:1px solid var(--dsw-alias-border-l2)}
5812
+ .codexSubscriptionDiagnostics .codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column;gap:12px}
5813
+ .codexSubscriptionDiagnostics .codexSubscriptionSectionTitle{display:flex;flex-direction:column;gap:5px}
5814
+ .codexSubscriptionStorageMeter{display:flex;flex-direction:column;gap:9px;padding:12px;border-radius:10px;background:var(--dsw-alias-bg-module-platform)}
5815
+ .codexSubscriptionStorageMeter>div{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:12px;line-height:18px}
5816
+ .codexSubscriptionStorageMeter output{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary)}
5817
+ .codexSubscriptionStorageMeter progress{width:100%;height:4px;border:0;border-radius:4px;overflow:hidden;accent-color:var(--dsw-alias-label-secondary)}
5818
+ .codexSubscriptionStorageMeter progress::-webkit-progress-bar{background:var(--dsw-alias-border-l2)}
5819
+ .codexSubscriptionStorageMeter progress::-webkit-progress-value{background:var(--dsw-alias-label-secondary)}
5820
+ .codexSubscriptionConfirm{padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;font-size:12px;line-height:1.65}
5821
+ .codexSubscriptionConfirm p{margin-bottom:12px}
5822
+ .codexSubscriptionRuntime{padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);margin-top:2px}
5823
+ .codexSubscriptionRuntime details{margin-top:8px}.codexSubscriptionRuntime details[open]>summary{margin-bottom:10px}
5824
+ @container (max-width:480px){.codexSubscriptionPreference,.codexSubscriptionContextHead{flex-wrap:wrap}.codexSubscriptionPreferenceHint{max-width:none}.codexSubscriptionPreference>.codexSubscriptionPreferenceCopy{flex:1 1 220px}.codexSubscriptionQuotaModes{max-width:100%;flex-wrap:wrap}.codexSubscriptionQuotaMode{min-height:30px}.codexSubscriptionActions{flex-wrap:wrap}}
5825
+ @container (max-width:340px){.codexSettingsTabs{grid-template-columns:repeat(2,minmax(0,1fr))}}
5826
+ @media(prefers-reduced-motion:reduce){.codexSubscription *{transition:none!important}}
5672
5827
  `;
5673
5828
  //#endregion
5674
5829
  //#region src/image-edit-reference.js
@@ -7290,7 +7445,7 @@ window.__ModuleLoader__.load({
7290
7445
  }
7291
7446
  //#endregion
7292
7447
  //#region src/version.js
7293
- const PACKAGE_VERSION = "2.2.1";
7448
+ const PACKAGE_VERSION = "2.2.2";
7294
7449
  //#endregion
7295
7450
  //#region src/client-recovery.js
7296
7451
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -7925,29 +8080,30 @@ window.__ModuleLoader__.load({
7925
8080
  });
7926
8081
  }
7927
8082
  const modelLabel = (model) => model.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
7928
- function ImagePreferences({ preference, t }) {
8083
+ function ImagePreferences({ preference, t, section = "image" }) {
7929
8084
  const snapshot = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
7930
8085
  const disabled = snapshot.status !== "ready" || !snapshot.writable || snapshot.saving;
7931
8086
  const active = snapshot.imageGeneration || snapshot.imageEditing;
7932
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
8087
+ const sketch = section === "sketch";
8088
+ const title = t(sketch ? "creativeSketch" : "imageSettings");
8089
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
7933
8090
  className: "codexSubscriptionCard codexImageSettings",
7934
- "aria-label": t("imageSettings"),
7935
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
7936
- className: "codexSubscriptionSettingsDisclosure",
7937
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
7938
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageSettings") }),
7939
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7940
- className: "codexSubscriptionPreferenceHint",
7941
- children: active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
7942
- }),
7943
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDownOutline14, {})
7944
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7945
- className: "codexSubscriptionSettingsDisclosureBody",
8091
+ "aria-label": title,
8092
+ children: [
8093
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8094
+ className: "codexSubscriptionSectionHead",
8095
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8096
+ className: "codexSubscriptionBadge",
8097
+ children: sketch ? "Beta" : active ? modelLabel(snapshot.imageModel) : t("imageCapability_off")
8098
+ })]
8099
+ }),
8100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8101
+ className: "codexSubscriptionSettingRows",
7946
8102
  children: [
7947
- Object.keys(IMAGE_SETTING_GROUPS).map((group) => {
8103
+ Object.keys(IMAGE_SETTING_GROUPS).filter((group) => group.startsWith("sketch") === sketch).map((group) => {
7948
8104
  const value = imageGroupValue(snapshot, group);
7949
8105
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7950
- label: t(group),
8106
+ label: t(sketch ? `${group}Setting` : group),
7951
8107
  hint: t(`${group}Hint`),
7952
8108
  value,
7953
8109
  text: t(value === "mixed" ? "imageGroupMixed" : `${group}_${value}`),
@@ -7961,7 +8117,7 @@ window.__ModuleLoader__.load({
7961
8117
  }
7962
8118
  }, group);
7963
8119
  }),
7964
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8120
+ !sketch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7965
8121
  className: "codexImageDefaultsGroup",
7966
8122
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageChoice, {
7967
8123
  label: t("imageModel"),
@@ -7991,14 +8147,29 @@ window.__ModuleLoader__.load({
7991
8147
  preference.set({ imageQuality });
7992
8148
  }
7993
8149
  })]
7994
- }),
7995
- snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8150
+ }) : null,
8151
+ !sketch && snapshot.imageModel.includes("2.5") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7996
8152
  className: "codexSubscriptionPreferenceHint",
7997
8153
  children: t("imageModelHint")
7998
8154
  }) : null
7999
8155
  ]
8000
- })]
8001
- })
8156
+ }),
8157
+ snapshot.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8158
+ className: "codexSubscriptionRecover",
8159
+ role: "alert",
8160
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8161
+ className: "codexSubscriptionError",
8162
+ children: t("preferenceFailed")
8163
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8164
+ type: "button",
8165
+ variant: "outline",
8166
+ onClick: () => {
8167
+ preference.retry();
8168
+ },
8169
+ children: t("preferenceRetry")
8170
+ })]
8171
+ }) : null
8172
+ ]
8002
8173
  });
8003
8174
  }
8004
8175
  //#endregion
@@ -8225,16 +8396,15 @@ window.__ModuleLoader__.load({
8225
8396
  className: "codexSubscriptionPreference",
8226
8397
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8227
8398
  role: "status",
8228
- children: t(!state ? "runtimeLoading" : state.restartRequired ? "runtimeRestart" : busy ? `runtime_${state.phase}` : state.installed ? "subagentRuntimeInstalled" : state.present ? "runtimeIncompatible" : "subagentRuntimeMissing")
8229
- }), state?.available && (state.present || state.installable) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8399
+ children: t(!state ? "runtimeLoading" : state.restartRequired ? "runtimeRestart" : busy ? `runtime_${state.phase}` : state.installed ? "subagentRuntimeInstalled" : !state.componentVersion ? "runtimeHostUnsupported" : state.present ? state.removable ? "runtimeIncompatible" : "runtimeManagedElsewhere" : "subagentRuntimeMissing")
8400
+ }), state?.available && (state.present ? state.removable : state.installable) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8230
8401
  type: "button",
8231
8402
  variant: "outline",
8232
- disabled: locked || state.present && !state.removable,
8403
+ disabled: locked,
8233
8404
  onClick: () => state.present ? setConfirm(true) : void act("install"),
8234
8405
  children: t(state.present ? "runtimeRemove" : "runtimeInstall")
8235
8406
  }) : null]
8236
8407
  }),
8237
- state?.present && state.available && !state.removable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeManagedElsewhere") }) : null,
8238
8408
  state?.active > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeActive") }) : null,
8239
8409
  busy && state.phase === "installing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8240
8410
  type: "button",
@@ -8276,21 +8446,22 @@ window.__ModuleLoader__.load({
8276
8446
  role: "status",
8277
8447
  children: t("runtimeCancelled")
8278
8448
  }) : null,
8279
- !busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8280
- type: "button",
8281
- variant: "outline",
8282
- disabled: sending,
8283
- onClick: () => {
8284
- read();
8285
- preference.load();
8286
- },
8287
- children: t("runtimeRefresh")
8288
- }) : null,
8289
8449
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
8290
8450
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("subagentRuntimeManage") }),
8451
+ state?.present && state.available && !state.removable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeManagedElsewhere") }) : null,
8452
+ !busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8453
+ type: "button",
8454
+ variant: "outline",
8455
+ disabled: sending,
8456
+ onClick: () => {
8457
+ read();
8458
+ preference.load();
8459
+ },
8460
+ children: t("runtimeRefresh")
8461
+ }) : null,
8291
8462
  state && !state.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeUnavailable") }) : null,
8292
8463
  state?.available && !state.installable && !state.present ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeHostUnsupported") }) : null,
8293
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeInstallHint") }),
8464
+ state?.installable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("runtimeInstallHint") }) : null,
8294
8465
  state?.componentVersion ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", { children: ["@deepseek-ai/dsh-subagent-codex@", state.componentVersion] }) : null,
8295
8466
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("subagentRuntimeCacheHint") }),
8296
8467
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
@@ -9278,9 +9449,12 @@ window.__ModuleLoader__.load({
9278
9449
  children: [
9279
9450
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9280
9451
  className: "codexSubscriptionSectionHead",
9281
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9452
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9282
9453
  className: "codexSubscriptionSectionTitle",
9283
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") })
9454
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("diagnostics") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9455
+ className: "codexSubscriptionHelp",
9456
+ children: t("diagnosticsHint")
9457
+ })]
9284
9458
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9285
9459
  className: "codexSubscriptionActions",
9286
9460
  children: [
@@ -9317,6 +9491,106 @@ window.__ModuleLoader__.load({
9317
9491
  });
9318
9492
  }
9319
9493
  //#endregion
9494
+ //#region src/client-storage.jsx
9495
+ function StorageCard({ rpc, t }) {
9496
+ const [usage, setUsage] = (0, react.useState)();
9497
+ const [busy, setBusy] = (0, react.useState)(false);
9498
+ const [confirm, setConfirm] = (0, react.useState)(false);
9499
+ const [error, setError] = (0, react.useState)(false);
9500
+ const mounted = (0, react.useRef)(false);
9501
+ const pending = (0, react.useRef)(false);
9502
+ const read = async (clear) => {
9503
+ if (pending.current) return;
9504
+ pending.current = true;
9505
+ setBusy(true);
9506
+ setError(false);
9507
+ try {
9508
+ const result = unwrap(await rpc.call(CHANNEL, clear ? "storage/clear-forecast" : "storage/status", {}));
9509
+ if (mounted.current) {
9510
+ setUsage(result);
9511
+ setConfirm(false);
9512
+ }
9513
+ } catch {
9514
+ if (mounted.current) setError(true);
9515
+ } finally {
9516
+ pending.current = false;
9517
+ if (mounted.current) setBusy(false);
9518
+ }
9519
+ };
9520
+ (0, react.useEffect)(() => {
9521
+ mounted.current = true;
9522
+ read(false);
9523
+ return () => {
9524
+ mounted.current = false;
9525
+ };
9526
+ }, [rpc]);
9527
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
9528
+ className: "codexSubscriptionCard codexSubscriptionStorage",
9529
+ "aria-label": t("storageTitle"),
9530
+ children: [
9531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("storageTitle") }),
9532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9533
+ className: "codexSubscriptionStorageMeter",
9534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("storageForecast") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
9535
+ role: "status",
9536
+ children: usage ? `${(usage.bytes / 1024).toFixed(1)} / ${(usage.limit / 1024).toFixed(0)} KiB` : t("storageLoading")
9537
+ })] }), usage ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("progress", {
9538
+ "aria-label": t("storageForecast"),
9539
+ value: usage.bytes,
9540
+ max: usage.limit
9541
+ }) : null]
9542
+ }),
9543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9544
+ className: "codexSubscriptionHelp",
9545
+ children: t("storageHint")
9546
+ }),
9547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9548
+ className: "codexSubscriptionActions",
9549
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9550
+ type: "button",
9551
+ variant: "outline",
9552
+ disabled: busy,
9553
+ onClick: () => void read(false),
9554
+ children: t("runtimeRefresh")
9555
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9556
+ type: "button",
9557
+ variant: "outline",
9558
+ disabled: busy || !usage?.bytes,
9559
+ onClick: () => setConfirm(true),
9560
+ children: t("storageClear")
9561
+ })]
9562
+ }),
9563
+ confirm ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9564
+ className: "codexSubscriptionConfirm",
9565
+ role: "group",
9566
+ "aria-label": t("storageClear"),
9567
+ children: [
9568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("storageConfirm") }),
9569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9570
+ type: "button",
9571
+ variant: "outline",
9572
+ disabled: busy,
9573
+ onClick: () => setConfirm(false),
9574
+ children: t("sketchCancel")
9575
+ }),
9576
+ " ",
9577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9578
+ type: "button",
9579
+ variant: "outline",
9580
+ disabled: busy,
9581
+ onClick: () => void read(true),
9582
+ children: t("storageClear")
9583
+ })
9584
+ ]
9585
+ }) : null,
9586
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9587
+ role: "alert",
9588
+ children: t("storageFailed")
9589
+ }) : null
9590
+ ]
9591
+ });
9592
+ }
9593
+ //#endregion
9320
9594
  //#region src/client-usage.jsx
9321
9595
  function ResetTime({ resetsAt, t }) {
9322
9596
  const date = Number.isSafeInteger(resetsAt) ? validDate(resetsAt * 1e3) : void 0;
@@ -9725,7 +9999,12 @@ window.__ModuleLoader__.load({
9725
9999
  function CodexSection({ preference, rpc, accountStatus, t }) {
9726
10000
  const [tab, setTab] = (0, react.useState)("account");
9727
10001
  const id = (0, react.useId)();
9728
- const tabs = ["account", "advanced"];
10002
+ const tabs = [
10003
+ "account",
10004
+ "advanced",
10005
+ "creative",
10006
+ "maintenance"
10007
+ ];
9729
10008
  const accountSnapshot = useAccountStatusSnapshot(accountStatus);
9730
10009
  const account = accountSnapshot.account;
9731
10010
  const [resetKey, setResetKey] = (0, react.useState)(0);
@@ -9800,27 +10079,44 @@ window.__ModuleLoader__.load({
9800
10079
  })
9801
10080
  ]
9802
10081
  }),
9803
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10082
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9804
10083
  role: "tabpanel",
9805
10084
  id: `${id}-advanced`,
9806
10085
  "aria-labelledby": `${id}-advanced-tab`,
9807
10086
  hidden: tab !== "advanced",
9808
- children: [
9809
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreferencesCard, {
9810
- rpc,
9811
- preference,
9812
- t,
9813
- section: "advanced"
9814
- }),
9815
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
9816
- preference,
9817
- t
9818
- }),
9819
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsCard, {
9820
- rpc,
9821
- t
9822
- })
9823
- ]
10087
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreferencesCard, {
10088
+ rpc,
10089
+ preference,
10090
+ t,
10091
+ section: "advanced"
10092
+ })
10093
+ }),
10094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10095
+ role: "tabpanel",
10096
+ id: `${id}-creative`,
10097
+ "aria-labelledby": `${id}-creative-tab`,
10098
+ hidden: tab !== "creative",
10099
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
10100
+ preference,
10101
+ t
10102
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePreferences, {
10103
+ preference,
10104
+ t,
10105
+ section: "sketch"
10106
+ })]
10107
+ }),
10108
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10109
+ role: "tabpanel",
10110
+ id: `${id}-maintenance`,
10111
+ "aria-labelledby": `${id}-maintenance-tab`,
10112
+ hidden: tab !== "maintenance",
10113
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StorageCard, {
10114
+ rpc,
10115
+ t
10116
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsCard, {
10117
+ rpc,
10118
+ t
10119
+ })]
9824
10120
  })
9825
10121
  ]
9826
10122
  });