@zkmake/three-textures 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -13,7 +13,7 @@ original was used. Vanilla three and React Three Fiber.
13
13
  bun add -d @zkmake/three-textures # or npm i -D / pnpm add -D
14
14
  ```
15
15
 
16
- Live demo: [three-textures.pages.dev](https://three-textures.pages.dev/), with a vanilla three and
16
+ Live demo: [three-kit.pages.dev/three-textures](https://three-kit.pages.dev/three-textures/), with a vanilla three and
17
17
  a React Three Fiber take on the same scene: an image texture and a clone of it, a data texture, and
18
18
  a KTX2 atlas cut into four cards. Download the atlas, paint on it, drop it back.
19
19
 
@@ -122,9 +122,9 @@ await lab.showOriginal("bunny", true);
122
122
 
123
123
  ## Examples
124
124
 
125
- [`apps/three-textures-site`](https://github.com/zkmake/three-kit/tree/main/apps/three-textures-site)
126
- is what runs at three-textures.pages.dev: [`src/demos/vanilla.ts`](https://github.com/zkmake/three-kit/blob/main/apps/three-textures-site/src/demos/vanilla.ts)
127
- with `mountTexturePanel`, [`src/demos/r3f.tsx`](https://github.com/zkmake/three-kit/blob/main/apps/three-textures-site/src/demos/r3f.tsx)
125
+ [`apps/site/src/demos/three-textures`](https://github.com/zkmake/three-kit/tree/main/apps/site/src/demos/three-textures)
126
+ is what runs at three-kit.pages.dev/three-textures: [`vanilla.ts`](https://github.com/zkmake/three-kit/blob/main/apps/site/src/demos/three-textures/vanilla.ts)
127
+ with `mountTexturePanel`, [`r3f.tsx`](https://github.com/zkmake/three-kit/blob/main/apps/site/src/demos/three-textures/r3f.tsx)
128
128
  with `<TexturePanel />`, each beside the three-meter HUD. `?r3f` opens on Fiber.
129
129
 
130
130
  ## License
@@ -180,6 +180,12 @@ const ICONS = {
180
180
  };
181
181
  const icon = (name, label) => `<button type="button" class="ttx-icon" data-action="${name}" title="${label}" aria-label="${label}"><svg viewBox="0 0 16 16" aria-hidden="true">${ICONS[name]}</svg></button>`;
182
182
  const REFRESH_MS = 2e3;
183
+ /**
184
+ * Just after the panel opens, look again every `SETTLE_STEP_MS` for `SETTLE_MS`, hidden or not:
185
+ * textures that load after mount (R3F scenes, async loaders) show up at once, not a poll later.
186
+ */
187
+ const SETTLE_MS = 3e3;
188
+ const SETTLE_STEP_MS = 250;
183
189
  const THUMBS_PER_TICK = 4;
184
190
  const describe = (entry) => {
185
191
  return [
@@ -392,14 +398,22 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
392
398
  search.addEventListener("input", filter);
393
399
  document.addEventListener("pointerdown", onPointerDown);
394
400
  const unsubscribe = lab.subscribe(render);
401
+ const onVisibility = () => {
402
+ if (refreshTimer && document.visibilityState === "visible") lab.refresh();
403
+ };
404
+ document.addEventListener("visibilitychange", onVisibility);
395
405
  const setActive = (active) => {
396
406
  if (active && !refreshTimer) {
407
+ const opened = performance.now();
408
+ const tick = () => {
409
+ const settling = performance.now() - opened < SETTLE_MS;
410
+ if (settling || document.visibilityState === "visible") lab.refresh();
411
+ refreshTimer = setTimeout(tick, settling ? SETTLE_STEP_MS : REFRESH_MS);
412
+ };
397
413
  lab.refresh();
398
- refreshTimer = setInterval(() => {
399
- if (document.visibilityState === "visible") lab.refresh();
400
- }, REFRESH_MS);
414
+ refreshTimer = setTimeout(tick, SETTLE_STEP_MS);
401
415
  } else if (!active && refreshTimer) {
402
- clearInterval(refreshTimer);
416
+ clearTimeout(refreshTimer);
403
417
  refreshTimer = null;
404
418
  }
405
419
  };
@@ -423,6 +437,7 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
423
437
  if (thumbTimer) clearTimeout(thumbTimer);
424
438
  thumbQueue = [];
425
439
  document.removeEventListener("pointerdown", onPointerDown);
440
+ document.removeEventListener("visibilitychange", onVisibility);
426
441
  preview.remove();
427
442
  element.remove();
428
443
  }
@@ -504,4 +519,4 @@ const mountTexturePanel = (options) => {
504
519
  //#endregion
505
520
  export { injectStyles as i, createTexturePanel as n, TEXTURE_PANEL_STYLES as r, mountTexturePanel as t };
506
521
 
507
- //# sourceMappingURL=mount-D6SH40yP.js.map
522
+ //# sourceMappingURL=mount-CI0vTOLn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount-CI0vTOLn.js","names":[],"sources":["../src/ui/styles.ts","../src/ui/panel.ts","../src/ui/mount.ts"],"sourcesContent":["/** The panel's stylesheet, injected once. Palettes by `data-theme` on `.ttx`. */\nexport const TEXTURE_PANEL_STYLES = /* css */ `\n/* Inside a dev-panel card the tokens come from three-meter's (\\`--perf-*\\`), so the panels match;\n on its own (in a host's tab) the same palette is the fallback. */\n.ttx {\n --ttx-bg: var(--perf-bg, rgba(22, 24, 29, 0.92));\n --ttx-fg: var(--perf-fg, #e6e8eb);\n --ttx-dim: var(--perf-muted, #8b909a);\n --ttx-line: var(--perf-border, rgba(255, 255, 255, 0.12));\n --ttx-field: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-hover: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-accent: var(--perf-accent, #60a5fa);\n --ttx-warn: var(--perf-warn, #f59e0b);\n --ttx-shadow: var(--perf-shadow, 0 8px 24px rgba(0, 0, 0, 0.32));\n --ttx-live: #34d399;\n --ttx-checker: rgba(255, 255, 255, 0.08);\n box-sizing: border-box;\n color: var(--ttx-fg);\n font: 11px/1.35 ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n}\n.ttx[data-theme=\"light\"] {\n --ttx-bg: rgba(250, 250, 252, 0.94);\n --ttx-fg: #1a1c21;\n --ttx-dim: #6b7079;\n --ttx-line: rgba(0, 0, 0, 0.12);\n --ttx-field: rgba(0, 0, 0, 0.05);\n --ttx-hover: rgba(0, 0, 0, 0.05);\n --ttx-accent: #2563eb;\n --ttx-warn: #b45309;\n --ttx-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);\n --ttx-live: #047857;\n --ttx-checker: rgba(0, 0, 0, 0.07);\n}\n.ttx *, .ttx *::before, .ttx *::after { box-sizing: inherit; }\n.ttx button { font: inherit; color: inherit; }\n\n.ttx-panel { display: flex; flex-direction: column; gap: 6px; min-height: 0; padding: 8px; }\n/* In the dev-panel card: a fixed width (the frame's cap raised to fit), the list scrolling\n inside the card's height. */\n.perf-hud.ttx-frame { max-width: min(23rem, calc(100vw - 48px)); }\n.perf-hud__card > .ttx-panel { width: 22rem; max-width: 100%; flex: 1; }\n/* Compact: the brand row alone, with the count. */\n.perf-hud.ttx-compact .ttx-panel { display: none; }\n.perf-hud.ttx-compact .perf-hud__brand { padding-bottom: 6px; }\n.ttx-toolbar { display: flex; gap: 6px; }\n.ttx-search {\n flex: 1;\n min-width: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 4px 6px;\n color: inherit;\n font: inherit;\n}\n.ttx-search::placeholder { color: var(--ttx-dim); }\n.ttx-search:focus { outline: 1px solid var(--ttx-accent); outline-offset: 0; }\n\n.ttx-icon {\n display: inline-grid;\n place-items: center;\n width: 22px;\n height: 22px;\n flex: none;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 4px;\n background: transparent;\n color: var(--ttx-dim);\n cursor: pointer;\n}\n.ttx-icon:hover:not(:disabled) { background: var(--ttx-hover); color: var(--ttx-fg); }\n.ttx-icon:focus-visible { outline: 1px solid var(--ttx-accent); }\n.ttx-icon:disabled { opacity: 0.3; cursor: default; }\n.ttx-icon svg { width: 12px; height: 12px; }\n.ttx-icon.is-on { color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-icon.is-live { color: var(--ttx-live); }\n.ttx-icon.is-paused { color: var(--ttx-warn); }\n.ttx-text-button {\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 3px 8px;\n cursor: pointer;\n white-space: nowrap;\n}\n.ttx-text-button:hover:not(:disabled) { border-color: var(--ttx-accent); }\n.ttx-text-button:disabled { opacity: 0.4; cursor: default; }\n\n.ttx-list { margin: 0; padding: 0; list-style: none; overflow-y: auto; min-height: 0; }\n.ttx-row {\n display: grid;\n grid-template-columns: 40px 1fr auto;\n align-items: center;\n gap: 8px;\n padding: 4px;\n border-radius: 5px;\n border: 1px solid transparent;\n}\n.ttx-row:hover { background: var(--ttx-hover); }\n.ttx-row.is-drop { border-color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-row.is-hidden { display: none; }\n.ttx-row.is-unsupported { opacity: 0.5; }\n.ttx-thumb {\n width: 40px;\n height: 40px;\n padding: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 8px 8px;\n cursor: zoom-in;\n overflow: hidden;\n}\n.ttx-thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }\n.ttx-row.is-swapped .ttx-thumb { border-color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-thumb { border-color: var(--ttx-warn); }\n.ttx-text { min-width: 0; }\n.ttx-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }\n.ttx-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-dim); }\n.ttx-swap-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-swap-name { color: var(--ttx-warn); }\n.ttx-swap-name[hidden] { display: none; }\n.ttx-actions { display: flex; gap: 1px; }\n.ttx-empty, .ttx-status { color: var(--ttx-dim); padding: 6px 4px; }\n.ttx-status.is-error { color: var(--ttx-warn); }\n.ttx-status:empty { display: none; }\n\n.ttx-preview {\n position: fixed;\n z-index: 2147483001;\n top: 50%;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-width: min(46vw, 560px);\n padding: 8px;\n border: 1px solid var(--ttx-line);\n border-radius: 8px;\n background: var(--ttx-bg);\n box-shadow: var(--ttx-shadow);\n transform: translateY(-50%);\n}\n.ttx-preview[hidden] { display: none; }\n.ttx-preview[data-side=\"left\"] { left: 16px; }\n.ttx-preview[data-side=\"right\"] { right: 16px; }\n.ttx-preview img {\n display: block;\n max-width: 100%;\n max-height: 70vh;\n image-rendering: pixelated;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 16px 16px;\n}\n.ttx-preview-label { color: var(--ttx-dim); }\n`;\n\nconst STYLE_ID = \"zkmake-three-textures-styles\";\n\n/** Add the stylesheet to `document.head` once. */\nexport const injectStyles = () => {\n if (typeof document === \"undefined\" || document.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = document.createElement(\"style\");\n\n style.id = STYLE_ID;\n style.textContent = TEXTURE_PANEL_STYLES;\n document.head.append(style);\n};\n","/**\n * The texture panel as an element: search, the list, and each texture's actions. `mountTexturePanel`\n * floats it in a docked card; a host with its own dev panel can put `element` in a tab instead.\n */\nimport { type TextureEntry, TextureLab } from \"../core/lab.ts\";\nimport { injectStyles } from \"./styles.ts\";\n\nexport type PanelEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\nexport type TexturePanel = {\n element: HTMLElement;\n /** Paint the preview, and the panel when it's not in a dev-panel card, `dark` or `light`. Default dark. */\n setTheme(theme: \"dark\" | \"light\"): void;\n /** Which screen edge the panel sits on: the preview opens on the other side. */\n setEdge(edge: PanelEdge): void;\n /** Re-read the scene now (the panel also does every couple of seconds while shown). */\n refresh(): void;\n /** Pause or resume the periodic re-read, e.g. while a host tab is hidden. */\n setActive(active: boolean): void;\n dispose(): void;\n};\n\nconst ICONS = {\n download: '<path fill=\"currentColor\" d=\"M7 1h2v6h2.5L8 10.5 4.5 7H7V1zm-5 12h12v2H2v-2z\"/>',\n replace: '<path fill=\"currentColor\" d=\"M5 2 1 6h3v4h2V6h3L5 2zm6 12 4-4h-3V6h-2v4H7l4 4z\"/>',\n link: '<path fill=\"currentColor\" d=\"M6.5 9.5a3 3 0 0 0 4.2 0l2.6-2.6a3 3 0 0 0-4.2-4.2L8 3.8l1.1 1.1 1.1-1.1a1.5 1.5 0 0 1 2.1 2.1l-2.6 2.6a1.5 1.5 0 0 1-2.1 0L6.5 9.5zm3-3a3 3 0 0 0-4.2 0L2.7 9.1a3 3 0 0 0 4.2 4.2L8 12.2l-1.1-1.1-1.1 1.1a1.5 1.5 0 0 1-2.1-2.1l2.6-2.6a1.5 1.5 0 0 1 2.1 0L9.5 6.5z\"/>',\n compare:\n '<path fill=\"currentColor\" d=\"M8 3C4.5 3 1.7 5.2 0 8c1.7 2.8 4.5 5 8 5s6.3-2.2 8-5c-1.7-2.8-4.5-5-8-5zm0 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z\"/>',\n revert:\n '<path fill=\"currentColor\" d=\"M5 4V2L1 5l4 3V6h5a3 3 0 0 1 0 6H5v2h5a5 5 0 0 0 0-10H5z\"/>',\n refresh:\n '<path fill=\"currentColor\" d=\"M13.6 2.4V6H10l1.4-1.4A4.5 4.5 0 1 0 12.5 9h1.6a6 6 0 1 1-1.6-5.7l1.1-.9z\"/>',\n};\n\nconst icon = (name: keyof typeof ICONS, label: string) =>\n `<button type=\"button\" class=\"ttx-icon\" data-action=\"${name}\" title=\"${label}\" aria-label=\"${label}\"><svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">${ICONS[name]}</svg></button>`;\n\ntype Row = {\n li: HTMLLIElement;\n thumb: HTMLImageElement;\n name: HTMLDivElement;\n meta: HTMLDivElement;\n swap: HTMLDivElement;\n file: HTMLInputElement;\n buttons: Record<\"download\" | \"replace\" | \"link\" | \"compare\" | \"revert\", HTMLButtonElement>;\n /** `revision:state` the thumbnail was drawn at. */\n drawn: string;\n};\n\nconst REFRESH_MS = 2000;\n/**\n * Just after the panel opens, look again every `SETTLE_STEP_MS` for `SETTLE_MS`, hidden or not:\n * textures that load after mount (R3F scenes, async loaders) show up at once, not a poll later.\n */\nconst SETTLE_MS = 3000;\nconst SETTLE_STEP_MS = 250;\nconst THUMBS_PER_TICK = 4;\n\nconst describe = (entry: TextureEntry) => {\n const parts = [\n entry.width && entry.height ? `${entry.width}×${entry.height}` : null,\n entry.format,\n entry.colorSpace === \"srgb\" ? \"sRGB\" : null,\n entry.materials > 0\n ? `${entry.materials} mat${entry.materials === 1 ? \"\" : \"s\"}`\n : \"not in scene\",\n ];\n\n return parts.filter(Boolean).join(\" · \");\n};\n\nexport const createTexturePanel = (lab: TextureLab, { canLink = true } = {}): TexturePanel => {\n injectStyles();\n\n const element = document.createElement(\"div\");\n\n element.className = \"ttx ttx-panel\";\n element.innerHTML = `\n <div class=\"ttx-toolbar\">\n <input type=\"search\" class=\"ttx-search\" placeholder=\"Filter by name…\" autocomplete=\"off\" spellcheck=\"false\" aria-label=\"Filter textures\" />\n ${icon(\"refresh\", \"Look for new textures\")}\n <button type=\"button\" class=\"ttx-text-button\" data-action=\"reset-all\" title=\"Put every texture back and forget saved swaps\" disabled>Reset all</button>\n </div>\n <div class=\"ttx-status\" role=\"status\"></div>\n <ul class=\"ttx-list\"></ul>\n `;\n\n const search = element.querySelector<HTMLInputElement>(\".ttx-search\")!;\n const status = element.querySelector<HTMLDivElement>(\".ttx-status\")!;\n const list = element.querySelector<HTMLUListElement>(\".ttx-list\")!;\n const resetAll = element.querySelector<HTMLButtonElement>('[data-action=\"reset-all\"]')!;\n const rows = new Map<string, Row>();\n const linking = canLink && TextureLab.canLink;\n\n const preview = document.createElement(\"div\");\n\n preview.className = \"ttx ttx-preview\";\n preview.hidden = true;\n preview.dataset.side = \"left\";\n preview.innerHTML = `<img alt=\"\" /><span class=\"ttx-preview-label\"></span>`;\n document.body.append(preview);\n\n const previewImage = preview.querySelector(\"img\")!;\n const previewLabel = preview.querySelector(\"span\")!;\n let previewing: string | null = null;\n let thumbQueue: string[] = [];\n let thumbTimer: ReturnType<typeof setTimeout> | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n const say = (message: string, error = false) => {\n status.textContent = message;\n status.classList.toggle(\"is-error\", error);\n };\n\n const run = async (work: () => Promise<unknown>) => {\n try {\n say(\"\");\n await work();\n } catch (error) {\n say((error as Error).message.replace(/^three-textures: /, \"\"), true);\n }\n };\n\n const showPreview = (id: string) => {\n const url = lab.thumbnail(id, 1024);\n\n if (!url) {\n say(\"No way to draw this one: it has no file and the renderer can't read it back.\", true);\n\n return;\n }\n\n previewing = id;\n previewImage.src = url;\n previewLabel.textContent = id;\n preview.hidden = false;\n };\n\n const hidePreview = () => {\n previewing = null;\n preview.hidden = true;\n };\n\n const drawThumbs = () => {\n thumbTimer = null;\n\n for (const id of thumbQueue.splice(0, THUMBS_PER_TICK)) {\n const row = rows.get(id);\n const url = row ? lab.thumbnail(id, 72) : null;\n\n if (row && url) {\n row.thumb.src = url;\n }\n }\n\n if (thumbQueue.length > 0) {\n thumbTimer = setTimeout(drawThumbs, 16);\n }\n };\n\n const queueThumb = (id: string) => {\n if (!thumbQueue.includes(id)) {\n thumbQueue.push(id);\n }\n\n thumbTimer ??= setTimeout(drawThumbs, 0);\n };\n\n const makeRow = (entry: TextureEntry): Row => {\n const li = document.createElement(\"li\");\n\n li.className = \"ttx-row\";\n li.innerHTML = `\n <button type=\"button\" class=\"ttx-thumb\" data-action=\"preview\" title=\"Preview\"><img alt=\"\" /></button>\n <div class=\"ttx-text\"><div class=\"ttx-name\"></div><div class=\"ttx-meta\"></div><div class=\"ttx-swap-name\"></div></div>\n <div class=\"ttx-actions\">\n ${icon(\"download\", \"Download\")}\n ${icon(\"replace\", \"Replace with an image file (or drop one on the row)\")}\n ${linking ? icon(\"link\", \"Live-link a file: every save shows here\") : \"\"}\n ${icon(\"compare\", \"Show the original\")}\n ${icon(\"revert\", \"Put the original back\")}\n </div>\n <input type=\"file\" accept=\"image/*\" hidden />\n `;\n\n const button = (name: string) =>\n li.querySelector<HTMLButtonElement>(`[data-action=\"${name}\"]`) ??\n document.createElement(\"button\");\n const row: Row = {\n li,\n thumb: li.querySelector(\".ttx-thumb img\")!,\n name: li.querySelector(\".ttx-name\")!,\n meta: li.querySelector(\".ttx-meta\")!,\n swap: li.querySelector(\".ttx-swap-name\")!,\n file: li.querySelector('input[type=\"file\"]')!,\n buttons: {\n download: button(\"download\"),\n replace: button(\"replace\"),\n link: button(\"link\"),\n compare: button(\"compare\"),\n revert: button(\"revert\"),\n },\n drawn: \"\",\n };\n const id = entry.id;\n\n row.name.textContent = id;\n li.addEventListener(\"click\", (event) => {\n const action = (event.target as HTMLElement).closest<HTMLElement>(\"[data-action]\")?.dataset\n .action;\n const current = lab.entry(id);\n\n if (!action || !current) {\n return;\n }\n\n if (action === \"preview\") {\n if (previewing === id) {\n hidePreview();\n } else {\n showPreview(id);\n }\n } else if (action === \"download\") {\n void run(() => lab.download(id));\n } else if (action === \"replace\") {\n row.file.click();\n } else if (action === \"link\") {\n void run(() => (current.link === \"live\" ? lab.unlink(id) : lab.link(id)));\n } else if (action === \"compare\") {\n void run(() => lab.showOriginal(id, current.state === \"swapped\"));\n } else if (action === \"revert\") {\n void run(() => lab.revert(id));\n }\n });\n row.file.addEventListener(\"change\", () => {\n const file = row.file.files?.[0];\n\n row.file.value = \"\";\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n li.addEventListener(\"dragover\", (event) => {\n if (event.dataTransfer?.types.includes(\"Files\") && !lab.entry(id)?.unsupported) {\n event.preventDefault();\n li.classList.add(\"is-drop\");\n }\n });\n li.addEventListener(\"dragleave\", () => li.classList.remove(\"is-drop\"));\n li.addEventListener(\"drop\", (event) => {\n event.preventDefault();\n li.classList.remove(\"is-drop\");\n\n const file = event.dataTransfer?.files[0];\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n\n return row;\n };\n\n const update = (row: Row, entry: TextureEntry) => {\n const swapped = entry.state !== \"original\";\n const { buttons } = row;\n\n row.li.classList.toggle(\"is-swapped\", entry.state === \"swapped\");\n row.li.classList.toggle(\"is-showing-original\", entry.state === \"showing-original\");\n row.li.classList.toggle(\"is-unsupported\", entry.unsupported !== null);\n row.li.title = entry.unsupported ?? \"\";\n row.meta.textContent = describe(entry);\n\n row.swap.hidden = entry.swapName === null;\n row.swap.textContent =\n entry.state === \"showing-original\"\n ? `original · swap: ${entry.swapName}`\n : `→ ${entry.swapName ?? \"\"}`;\n row.swap.title = row.swap.textContent;\n\n buttons.replace.disabled = entry.unsupported !== null;\n buttons.link.disabled = entry.unsupported !== null;\n buttons.link.classList.toggle(\"is-live\", entry.link === \"live\");\n buttons.link.classList.toggle(\"is-paused\", entry.link === \"paused\");\n buttons.link.title =\n entry.link === \"live\"\n ? \"Live: every save of the file shows here. Click to stop.\"\n : entry.link === \"paused\"\n ? \"Live link paused. Click to resume.\"\n : \"Live-link a file: every save shows here\";\n buttons.compare.disabled = !swapped;\n buttons.compare.classList.toggle(\"is-on\", entry.state === \"showing-original\");\n buttons.compare.title =\n entry.state === \"showing-original\" ? \"Show the swap\" : \"Show the original\";\n buttons.revert.disabled = !swapped && entry.link === \"off\";\n\n const key = `${entry.revision}:${entry.state}`;\n\n if (row.drawn !== key) {\n row.drawn = key;\n queueThumb(entry.id);\n\n if (previewing === entry.id) {\n showPreview(entry.id);\n }\n }\n };\n\n const filter = () => {\n const query = search.value.trim().toLowerCase();\n\n for (const [id, row] of rows) {\n row.li.classList.toggle(\"is-hidden\", query.length > 0 && !id.toLowerCase().includes(query));\n }\n };\n\n const render = () => {\n const entries = lab.entries();\n const ids = new Set(entries.map((entry) => entry.id));\n let added = false;\n\n for (const [id, row] of rows) {\n if (!ids.has(id)) {\n row.li.remove();\n rows.delete(id);\n }\n }\n\n for (const entry of entries) {\n let row = rows.get(entry.id);\n\n if (!row) {\n row = makeRow(entry);\n rows.set(entry.id, row);\n added = true;\n }\n\n update(row, entry);\n }\n\n if (added) {\n for (const entry of entries) {\n list.append(rows.get(entry.id)!.li);\n }\n }\n\n if (entries.length === 0) {\n say(\"No textures in the scene yet.\");\n } else if (status.textContent === \"No textures in the scene yet.\") {\n say(\"\");\n }\n\n resetAll.disabled = !entries.some(\n (entry) => entry.state !== \"original\" || entry.link !== \"off\",\n );\n element.dataset.count = String(entries.length);\n element.dispatchEvent(new CustomEvent(\"ttx-count\", { detail: entries.length }));\n filter();\n };\n\n const onPointerDown = (event: PointerEvent) => {\n const target = event.target as Node;\n\n if (previewing && !preview.contains(target) && !element.contains(target)) {\n hidePreview();\n }\n };\n\n element.querySelector('[data-action=\"refresh\"]')!.addEventListener(\"click\", () => lab.refresh());\n resetAll.addEventListener(\"click\", () => void run(() => lab.revertAll()));\n search.addEventListener(\"input\", filter);\n document.addEventListener(\"pointerdown\", onPointerDown);\n\n const unsubscribe = lab.subscribe(render);\n const onVisibility = () => {\n if (refreshTimer && document.visibilityState === \"visible\") {\n lab.refresh();\n }\n };\n\n document.addEventListener(\"visibilitychange\", onVisibility);\n\n const setActive = (active: boolean) => {\n if (active && !refreshTimer) {\n const opened = performance.now();\n const tick = () => {\n const settling = performance.now() - opened < SETTLE_MS;\n\n if (settling || document.visibilityState === \"visible\") {\n lab.refresh();\n }\n\n refreshTimer = setTimeout(tick, settling ? SETTLE_STEP_MS : REFRESH_MS);\n };\n\n lab.refresh();\n refreshTimer = setTimeout(tick, SETTLE_STEP_MS);\n } else if (!active && refreshTimer) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n render();\n setActive(true);\n\n return {\n element,\n setEdge: (edge) => {\n // Open the preview on the side away from the panel.\n preview.dataset.side = edge === \"left\" ? \"right\" : \"left\";\n },\n refresh: () => lab.refresh(),\n setTheme: (theme) => {\n preview.dataset.theme = theme;\n\n // In a dev-panel card the frame's palette (\\`--perf-*\\`) applies; elsewhere the panel's own.\n if (element.closest(\".perf-hud__card\")) {\n delete element.dataset.theme;\n } else {\n element.dataset.theme = theme;\n }\n },\n setActive,\n dispose: () => {\n setActive(false);\n unsubscribe();\n\n if (thumbTimer) {\n clearTimeout(thumbTimer);\n }\n\n thumbQueue = [];\n document.removeEventListener(\"pointerdown\", onPointerDown);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n preview.remove();\n element.remove();\n },\n };\n};\n","/**\n * The panel in the zkmake dev-panel frame (three-meter's `mountDevPanel`), so it docks, wakes,\n * dims and toggles exactly like the three-meter HUD: drag grip, expand/compact and dim-on-leave\n * discs beside a card with the brand label on top. One call for a vanilla three app.\n */\nimport {\n type DefaultPlacement,\n HudTheme,\n mountDevPanel,\n type ScreenEdge,\n type ThemeMode,\n} from \"@zkmake/three-meter/ui\";\n\nimport { TextureLab, type TextureLabOptions } from \"../core/lab.ts\";\nimport { createTexturePanel } from \"./panel.ts\";\n\nexport type MountTexturePanelOptions = TextureLabOptions & {\n /** Where it docks on a first visit. Default the right edge, top. */\n defaultPlacement?: DefaultPlacement;\n /** `dark`, `light` or `system` (default). */\n theme?: ThemeMode;\n /** Start compact: the brand row and the count. The choice is remembered. */\n compact?: boolean;\n /** Where to mount. Default `document.body`. */\n container?: HTMLElement;\n};\n\nexport type TexturePanelHandle = {\n lab: TextureLab;\n element: HTMLElement;\n setCompact(compact: boolean): void;\n /** `dark`, `light` or `system`: the panel follows it. */\n setTheme(mode: ThemeMode): void;\n dispose(): void;\n};\n\nconst readFlag = (key: string) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n};\n\nconst writeFlag = (key: string, value: string) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // Private mode: the choice lasts this page.\n }\n};\n\nexport const mountTexturePanel = (options: MountTexturePanelOptions): TexturePanelHandle => {\n const lab = new TextureLab(options);\n const panel = createTexturePanel(lab);\n const key = options.storageKey === null ? null : (options.storageKey ?? \"three-textures\");\n const theme = new HudTheme(options.theme ?? \"system\");\n const compactKey = key ? `${key}:compact` : null;\n\n const setCompact = (compact: boolean) => {\n frame.element.classList.toggle(\"ttx-compact\", compact);\n panel.setActive(!compact);\n\n if (compactKey) {\n writeFlag(compactKey, compact ? \"1\" : \"0\");\n }\n\n frame.refresh();\n };\n\n const frame = mountDevPanel({\n brand: \"three-textures\",\n content: panel.element,\n defaultPlacement: options.defaultPlacement ?? { edge: \"right\", align: \"start\" },\n label: \"Textures\",\n onEdgeChange: (edge: ScreenEdge) => panel.setEdge(edge),\n onToggle: () => setCompact(!frame.element.classList.contains(\"ttx-compact\")),\n storageKey: key ? `${key}:panel` : null,\n theme,\n toggleLabel: \"Show or hide the texture list\",\n ...(options.container ? { parent: options.container } : {}),\n });\n\n frame.element.classList.add(\"ttx-frame\");\n\n // The preview lives outside the card, in the page: it takes the theme on its own.\n const paintTheme = () => panel.setTheme(theme.resolved);\n const unsubscribeTheme = theme.subscribe(paintTheme);\n const showCount = (count: number) => {\n frame.detail.textContent = `${count} texture${count === 1 ? \"\" : \"s\"}`;\n };\n const onCount = (event: Event) => showCount((event as CustomEvent<number>).detail);\n\n paintTheme();\n panel.element.addEventListener(\"ttx-count\", onCount);\n showCount(Number(panel.element.dataset.count ?? 0));\n\n const stored = compactKey ? readFlag(compactKey) : null;\n\n setCompact(stored === null ? options.compact === true : stored === \"1\");\n\n return {\n lab,\n element: frame.element,\n setCompact,\n setTheme: (mode) => theme.setMode(mode),\n dispose: () => {\n unsubscribeTheme();\n theme.dispose();\n panel.element.removeEventListener(\"ttx-count\", onCount);\n panel.dispose();\n frame.dispose();\n lab.dispose();\n },\n };\n};\n"],"mappings":"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"}
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as mountTexturePanel } from "./mount-D6SH40yP.js";
1
+ import { t as mountTexturePanel } from "./mount-CI0vTOLn.js";
2
2
  import { useThree } from "@react-three/fiber";
3
3
  import { useEffect, useRef } from "react";
4
4
  //#region src/react/index.tsx
package/dist/ui.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as injectStyles, n as createTexturePanel, r as TEXTURE_PANEL_STYLES, t as mountTexturePanel } from "./mount-D6SH40yP.js";
1
+ import { i as injectStyles, n as createTexturePanel, r as TEXTURE_PANEL_STYLES, t as mountTexturePanel } from "./mount-CI0vTOLn.js";
2
2
  export { TEXTURE_PANEL_STYLES, createTexturePanel, injectStyles, mountTexturePanel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkmake/three-textures",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A dev panel for the textures in a three.js scene: see them, download them, paint them, swap them back in live — compressed (KTX2) textures included. Vanilla three and React Three Fiber.",
5
5
  "keywords": [
6
6
  "devtools",
@@ -1 +0,0 @@
1
- {"version":3,"file":"mount-D6SH40yP.js","names":[],"sources":["../src/ui/styles.ts","../src/ui/panel.ts","../src/ui/mount.ts"],"sourcesContent":["/** The panel's stylesheet, injected once. Palettes by `data-theme` on `.ttx`. */\nexport const TEXTURE_PANEL_STYLES = /* css */ `\n/* Inside a dev-panel card the tokens come from three-meter's (\\`--perf-*\\`), so the panels match;\n on its own (in a host's tab) the same palette is the fallback. */\n.ttx {\n --ttx-bg: var(--perf-bg, rgba(22, 24, 29, 0.92));\n --ttx-fg: var(--perf-fg, #e6e8eb);\n --ttx-dim: var(--perf-muted, #8b909a);\n --ttx-line: var(--perf-border, rgba(255, 255, 255, 0.12));\n --ttx-field: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-hover: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-accent: var(--perf-accent, #60a5fa);\n --ttx-warn: var(--perf-warn, #f59e0b);\n --ttx-shadow: var(--perf-shadow, 0 8px 24px rgba(0, 0, 0, 0.32));\n --ttx-live: #34d399;\n --ttx-checker: rgba(255, 255, 255, 0.08);\n box-sizing: border-box;\n color: var(--ttx-fg);\n font: 11px/1.35 ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n}\n.ttx[data-theme=\"light\"] {\n --ttx-bg: rgba(250, 250, 252, 0.94);\n --ttx-fg: #1a1c21;\n --ttx-dim: #6b7079;\n --ttx-line: rgba(0, 0, 0, 0.12);\n --ttx-field: rgba(0, 0, 0, 0.05);\n --ttx-hover: rgba(0, 0, 0, 0.05);\n --ttx-accent: #2563eb;\n --ttx-warn: #b45309;\n --ttx-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);\n --ttx-live: #047857;\n --ttx-checker: rgba(0, 0, 0, 0.07);\n}\n.ttx *, .ttx *::before, .ttx *::after { box-sizing: inherit; }\n.ttx button { font: inherit; color: inherit; }\n\n.ttx-panel { display: flex; flex-direction: column; gap: 6px; min-height: 0; padding: 8px; }\n/* In the dev-panel card: a fixed width (the frame's cap raised to fit), the list scrolling\n inside the card's height. */\n.perf-hud.ttx-frame { max-width: min(23rem, calc(100vw - 48px)); }\n.perf-hud__card > .ttx-panel { width: 22rem; max-width: 100%; flex: 1; }\n/* Compact: the brand row alone, with the count. */\n.perf-hud.ttx-compact .ttx-panel { display: none; }\n.perf-hud.ttx-compact .perf-hud__brand { padding-bottom: 6px; }\n.ttx-toolbar { display: flex; gap: 6px; }\n.ttx-search {\n flex: 1;\n min-width: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 4px 6px;\n color: inherit;\n font: inherit;\n}\n.ttx-search::placeholder { color: var(--ttx-dim); }\n.ttx-search:focus { outline: 1px solid var(--ttx-accent); outline-offset: 0; }\n\n.ttx-icon {\n display: inline-grid;\n place-items: center;\n width: 22px;\n height: 22px;\n flex: none;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 4px;\n background: transparent;\n color: var(--ttx-dim);\n cursor: pointer;\n}\n.ttx-icon:hover:not(:disabled) { background: var(--ttx-hover); color: var(--ttx-fg); }\n.ttx-icon:focus-visible { outline: 1px solid var(--ttx-accent); }\n.ttx-icon:disabled { opacity: 0.3; cursor: default; }\n.ttx-icon svg { width: 12px; height: 12px; }\n.ttx-icon.is-on { color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-icon.is-live { color: var(--ttx-live); }\n.ttx-icon.is-paused { color: var(--ttx-warn); }\n.ttx-text-button {\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 3px 8px;\n cursor: pointer;\n white-space: nowrap;\n}\n.ttx-text-button:hover:not(:disabled) { border-color: var(--ttx-accent); }\n.ttx-text-button:disabled { opacity: 0.4; cursor: default; }\n\n.ttx-list { margin: 0; padding: 0; list-style: none; overflow-y: auto; min-height: 0; }\n.ttx-row {\n display: grid;\n grid-template-columns: 40px 1fr auto;\n align-items: center;\n gap: 8px;\n padding: 4px;\n border-radius: 5px;\n border: 1px solid transparent;\n}\n.ttx-row:hover { background: var(--ttx-hover); }\n.ttx-row.is-drop { border-color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-row.is-hidden { display: none; }\n.ttx-row.is-unsupported { opacity: 0.5; }\n.ttx-thumb {\n width: 40px;\n height: 40px;\n padding: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 8px 8px;\n cursor: zoom-in;\n overflow: hidden;\n}\n.ttx-thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }\n.ttx-row.is-swapped .ttx-thumb { border-color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-thumb { border-color: var(--ttx-warn); }\n.ttx-text { min-width: 0; }\n.ttx-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }\n.ttx-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-dim); }\n.ttx-swap-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-swap-name { color: var(--ttx-warn); }\n.ttx-swap-name[hidden] { display: none; }\n.ttx-actions { display: flex; gap: 1px; }\n.ttx-empty, .ttx-status { color: var(--ttx-dim); padding: 6px 4px; }\n.ttx-status.is-error { color: var(--ttx-warn); }\n.ttx-status:empty { display: none; }\n\n.ttx-preview {\n position: fixed;\n z-index: 2147483001;\n top: 50%;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-width: min(46vw, 560px);\n padding: 8px;\n border: 1px solid var(--ttx-line);\n border-radius: 8px;\n background: var(--ttx-bg);\n box-shadow: var(--ttx-shadow);\n transform: translateY(-50%);\n}\n.ttx-preview[hidden] { display: none; }\n.ttx-preview[data-side=\"left\"] { left: 16px; }\n.ttx-preview[data-side=\"right\"] { right: 16px; }\n.ttx-preview img {\n display: block;\n max-width: 100%;\n max-height: 70vh;\n image-rendering: pixelated;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 16px 16px;\n}\n.ttx-preview-label { color: var(--ttx-dim); }\n`;\n\nconst STYLE_ID = \"zkmake-three-textures-styles\";\n\n/** Add the stylesheet to `document.head` once. */\nexport const injectStyles = () => {\n if (typeof document === \"undefined\" || document.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = document.createElement(\"style\");\n\n style.id = STYLE_ID;\n style.textContent = TEXTURE_PANEL_STYLES;\n document.head.append(style);\n};\n","/**\n * The texture panel as an element: search, the list, and each texture's actions. `mountTexturePanel`\n * floats it in a docked card; a host with its own dev panel can put `element` in a tab instead.\n */\nimport { type TextureEntry, TextureLab } from \"../core/lab.ts\";\nimport { injectStyles } from \"./styles.ts\";\n\nexport type PanelEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\nexport type TexturePanel = {\n element: HTMLElement;\n /** Paint the preview, and the panel when it's not in a dev-panel card, `dark` or `light`. Default dark. */\n setTheme(theme: \"dark\" | \"light\"): void;\n /** Which screen edge the panel sits on: the preview opens on the other side. */\n setEdge(edge: PanelEdge): void;\n /** Re-read the scene now (the panel also does every couple of seconds while shown). */\n refresh(): void;\n /** Pause or resume the periodic re-read, e.g. while a host tab is hidden. */\n setActive(active: boolean): void;\n dispose(): void;\n};\n\nconst ICONS = {\n download: '<path fill=\"currentColor\" d=\"M7 1h2v6h2.5L8 10.5 4.5 7H7V1zm-5 12h12v2H2v-2z\"/>',\n replace: '<path fill=\"currentColor\" d=\"M5 2 1 6h3v4h2V6h3L5 2zm6 12 4-4h-3V6h-2v4H7l4 4z\"/>',\n link: '<path fill=\"currentColor\" d=\"M6.5 9.5a3 3 0 0 0 4.2 0l2.6-2.6a3 3 0 0 0-4.2-4.2L8 3.8l1.1 1.1 1.1-1.1a1.5 1.5 0 0 1 2.1 2.1l-2.6 2.6a1.5 1.5 0 0 1-2.1 0L6.5 9.5zm3-3a3 3 0 0 0-4.2 0L2.7 9.1a3 3 0 0 0 4.2 4.2L8 12.2l-1.1-1.1-1.1 1.1a1.5 1.5 0 0 1-2.1-2.1l2.6-2.6a1.5 1.5 0 0 1 2.1 0L9.5 6.5z\"/>',\n compare:\n '<path fill=\"currentColor\" d=\"M8 3C4.5 3 1.7 5.2 0 8c1.7 2.8 4.5 5 8 5s6.3-2.2 8-5c-1.7-2.8-4.5-5-8-5zm0 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z\"/>',\n revert:\n '<path fill=\"currentColor\" d=\"M5 4V2L1 5l4 3V6h5a3 3 0 0 1 0 6H5v2h5a5 5 0 0 0 0-10H5z\"/>',\n refresh:\n '<path fill=\"currentColor\" d=\"M13.6 2.4V6H10l1.4-1.4A4.5 4.5 0 1 0 12.5 9h1.6a6 6 0 1 1-1.6-5.7l1.1-.9z\"/>',\n};\n\nconst icon = (name: keyof typeof ICONS, label: string) =>\n `<button type=\"button\" class=\"ttx-icon\" data-action=\"${name}\" title=\"${label}\" aria-label=\"${label}\"><svg viewBox=\"0 0 16 16\" aria-hidden=\"true\">${ICONS[name]}</svg></button>`;\n\ntype Row = {\n li: HTMLLIElement;\n thumb: HTMLImageElement;\n name: HTMLDivElement;\n meta: HTMLDivElement;\n swap: HTMLDivElement;\n file: HTMLInputElement;\n buttons: Record<\"download\" | \"replace\" | \"link\" | \"compare\" | \"revert\", HTMLButtonElement>;\n /** `revision:state` the thumbnail was drawn at. */\n drawn: string;\n};\n\nconst REFRESH_MS = 2000;\nconst THUMBS_PER_TICK = 4;\n\nconst describe = (entry: TextureEntry) => {\n const parts = [\n entry.width && entry.height ? `${entry.width}×${entry.height}` : null,\n entry.format,\n entry.colorSpace === \"srgb\" ? \"sRGB\" : null,\n entry.materials > 0\n ? `${entry.materials} mat${entry.materials === 1 ? \"\" : \"s\"}`\n : \"not in scene\",\n ];\n\n return parts.filter(Boolean).join(\" · \");\n};\n\nexport const createTexturePanel = (lab: TextureLab, { canLink = true } = {}): TexturePanel => {\n injectStyles();\n\n const element = document.createElement(\"div\");\n\n element.className = \"ttx ttx-panel\";\n element.innerHTML = `\n <div class=\"ttx-toolbar\">\n <input type=\"search\" class=\"ttx-search\" placeholder=\"Filter by name…\" autocomplete=\"off\" spellcheck=\"false\" aria-label=\"Filter textures\" />\n ${icon(\"refresh\", \"Look for new textures\")}\n <button type=\"button\" class=\"ttx-text-button\" data-action=\"reset-all\" title=\"Put every texture back and forget saved swaps\" disabled>Reset all</button>\n </div>\n <div class=\"ttx-status\" role=\"status\"></div>\n <ul class=\"ttx-list\"></ul>\n `;\n\n const search = element.querySelector<HTMLInputElement>(\".ttx-search\")!;\n const status = element.querySelector<HTMLDivElement>(\".ttx-status\")!;\n const list = element.querySelector<HTMLUListElement>(\".ttx-list\")!;\n const resetAll = element.querySelector<HTMLButtonElement>('[data-action=\"reset-all\"]')!;\n const rows = new Map<string, Row>();\n const linking = canLink && TextureLab.canLink;\n\n const preview = document.createElement(\"div\");\n\n preview.className = \"ttx ttx-preview\";\n preview.hidden = true;\n preview.dataset.side = \"left\";\n preview.innerHTML = `<img alt=\"\" /><span class=\"ttx-preview-label\"></span>`;\n document.body.append(preview);\n\n const previewImage = preview.querySelector(\"img\")!;\n const previewLabel = preview.querySelector(\"span\")!;\n let previewing: string | null = null;\n let thumbQueue: string[] = [];\n let thumbTimer: ReturnType<typeof setTimeout> | null = null;\n let refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n const say = (message: string, error = false) => {\n status.textContent = message;\n status.classList.toggle(\"is-error\", error);\n };\n\n const run = async (work: () => Promise<unknown>) => {\n try {\n say(\"\");\n await work();\n } catch (error) {\n say((error as Error).message.replace(/^three-textures: /, \"\"), true);\n }\n };\n\n const showPreview = (id: string) => {\n const url = lab.thumbnail(id, 1024);\n\n if (!url) {\n say(\"No way to draw this one: it has no file and the renderer can't read it back.\", true);\n\n return;\n }\n\n previewing = id;\n previewImage.src = url;\n previewLabel.textContent = id;\n preview.hidden = false;\n };\n\n const hidePreview = () => {\n previewing = null;\n preview.hidden = true;\n };\n\n const drawThumbs = () => {\n thumbTimer = null;\n\n for (const id of thumbQueue.splice(0, THUMBS_PER_TICK)) {\n const row = rows.get(id);\n const url = row ? lab.thumbnail(id, 72) : null;\n\n if (row && url) {\n row.thumb.src = url;\n }\n }\n\n if (thumbQueue.length > 0) {\n thumbTimer = setTimeout(drawThumbs, 16);\n }\n };\n\n const queueThumb = (id: string) => {\n if (!thumbQueue.includes(id)) {\n thumbQueue.push(id);\n }\n\n thumbTimer ??= setTimeout(drawThumbs, 0);\n };\n\n const makeRow = (entry: TextureEntry): Row => {\n const li = document.createElement(\"li\");\n\n li.className = \"ttx-row\";\n li.innerHTML = `\n <button type=\"button\" class=\"ttx-thumb\" data-action=\"preview\" title=\"Preview\"><img alt=\"\" /></button>\n <div class=\"ttx-text\"><div class=\"ttx-name\"></div><div class=\"ttx-meta\"></div><div class=\"ttx-swap-name\"></div></div>\n <div class=\"ttx-actions\">\n ${icon(\"download\", \"Download\")}\n ${icon(\"replace\", \"Replace with an image file (or drop one on the row)\")}\n ${linking ? icon(\"link\", \"Live-link a file: every save shows here\") : \"\"}\n ${icon(\"compare\", \"Show the original\")}\n ${icon(\"revert\", \"Put the original back\")}\n </div>\n <input type=\"file\" accept=\"image/*\" hidden />\n `;\n\n const button = (name: string) =>\n li.querySelector<HTMLButtonElement>(`[data-action=\"${name}\"]`) ??\n document.createElement(\"button\");\n const row: Row = {\n li,\n thumb: li.querySelector(\".ttx-thumb img\")!,\n name: li.querySelector(\".ttx-name\")!,\n meta: li.querySelector(\".ttx-meta\")!,\n swap: li.querySelector(\".ttx-swap-name\")!,\n file: li.querySelector('input[type=\"file\"]')!,\n buttons: {\n download: button(\"download\"),\n replace: button(\"replace\"),\n link: button(\"link\"),\n compare: button(\"compare\"),\n revert: button(\"revert\"),\n },\n drawn: \"\",\n };\n const id = entry.id;\n\n row.name.textContent = id;\n li.addEventListener(\"click\", (event) => {\n const action = (event.target as HTMLElement).closest<HTMLElement>(\"[data-action]\")?.dataset\n .action;\n const current = lab.entry(id);\n\n if (!action || !current) {\n return;\n }\n\n if (action === \"preview\") {\n if (previewing === id) {\n hidePreview();\n } else {\n showPreview(id);\n }\n } else if (action === \"download\") {\n void run(() => lab.download(id));\n } else if (action === \"replace\") {\n row.file.click();\n } else if (action === \"link\") {\n void run(() => (current.link === \"live\" ? lab.unlink(id) : lab.link(id)));\n } else if (action === \"compare\") {\n void run(() => lab.showOriginal(id, current.state === \"swapped\"));\n } else if (action === \"revert\") {\n void run(() => lab.revert(id));\n }\n });\n row.file.addEventListener(\"change\", () => {\n const file = row.file.files?.[0];\n\n row.file.value = \"\";\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n li.addEventListener(\"dragover\", (event) => {\n if (event.dataTransfer?.types.includes(\"Files\") && !lab.entry(id)?.unsupported) {\n event.preventDefault();\n li.classList.add(\"is-drop\");\n }\n });\n li.addEventListener(\"dragleave\", () => li.classList.remove(\"is-drop\"));\n li.addEventListener(\"drop\", (event) => {\n event.preventDefault();\n li.classList.remove(\"is-drop\");\n\n const file = event.dataTransfer?.files[0];\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n\n return row;\n };\n\n const update = (row: Row, entry: TextureEntry) => {\n const swapped = entry.state !== \"original\";\n const { buttons } = row;\n\n row.li.classList.toggle(\"is-swapped\", entry.state === \"swapped\");\n row.li.classList.toggle(\"is-showing-original\", entry.state === \"showing-original\");\n row.li.classList.toggle(\"is-unsupported\", entry.unsupported !== null);\n row.li.title = entry.unsupported ?? \"\";\n row.meta.textContent = describe(entry);\n\n row.swap.hidden = entry.swapName === null;\n row.swap.textContent =\n entry.state === \"showing-original\"\n ? `original · swap: ${entry.swapName}`\n : `→ ${entry.swapName ?? \"\"}`;\n row.swap.title = row.swap.textContent;\n\n buttons.replace.disabled = entry.unsupported !== null;\n buttons.link.disabled = entry.unsupported !== null;\n buttons.link.classList.toggle(\"is-live\", entry.link === \"live\");\n buttons.link.classList.toggle(\"is-paused\", entry.link === \"paused\");\n buttons.link.title =\n entry.link === \"live\"\n ? \"Live: every save of the file shows here. Click to stop.\"\n : entry.link === \"paused\"\n ? \"Live link paused. Click to resume.\"\n : \"Live-link a file: every save shows here\";\n buttons.compare.disabled = !swapped;\n buttons.compare.classList.toggle(\"is-on\", entry.state === \"showing-original\");\n buttons.compare.title =\n entry.state === \"showing-original\" ? \"Show the swap\" : \"Show the original\";\n buttons.revert.disabled = !swapped && entry.link === \"off\";\n\n const key = `${entry.revision}:${entry.state}`;\n\n if (row.drawn !== key) {\n row.drawn = key;\n queueThumb(entry.id);\n\n if (previewing === entry.id) {\n showPreview(entry.id);\n }\n }\n };\n\n const filter = () => {\n const query = search.value.trim().toLowerCase();\n\n for (const [id, row] of rows) {\n row.li.classList.toggle(\"is-hidden\", query.length > 0 && !id.toLowerCase().includes(query));\n }\n };\n\n const render = () => {\n const entries = lab.entries();\n const ids = new Set(entries.map((entry) => entry.id));\n let added = false;\n\n for (const [id, row] of rows) {\n if (!ids.has(id)) {\n row.li.remove();\n rows.delete(id);\n }\n }\n\n for (const entry of entries) {\n let row = rows.get(entry.id);\n\n if (!row) {\n row = makeRow(entry);\n rows.set(entry.id, row);\n added = true;\n }\n\n update(row, entry);\n }\n\n if (added) {\n for (const entry of entries) {\n list.append(rows.get(entry.id)!.li);\n }\n }\n\n if (entries.length === 0) {\n say(\"No textures in the scene yet.\");\n } else if (status.textContent === \"No textures in the scene yet.\") {\n say(\"\");\n }\n\n resetAll.disabled = !entries.some(\n (entry) => entry.state !== \"original\" || entry.link !== \"off\",\n );\n element.dataset.count = String(entries.length);\n element.dispatchEvent(new CustomEvent(\"ttx-count\", { detail: entries.length }));\n filter();\n };\n\n const onPointerDown = (event: PointerEvent) => {\n const target = event.target as Node;\n\n if (previewing && !preview.contains(target) && !element.contains(target)) {\n hidePreview();\n }\n };\n\n element.querySelector('[data-action=\"refresh\"]')!.addEventListener(\"click\", () => lab.refresh());\n resetAll.addEventListener(\"click\", () => void run(() => lab.revertAll()));\n search.addEventListener(\"input\", filter);\n document.addEventListener(\"pointerdown\", onPointerDown);\n\n const unsubscribe = lab.subscribe(render);\n const setActive = (active: boolean) => {\n if (active && !refreshTimer) {\n lab.refresh();\n refreshTimer = setInterval(() => {\n if (document.visibilityState === \"visible\") {\n lab.refresh();\n }\n }, REFRESH_MS);\n } else if (!active && refreshTimer) {\n clearInterval(refreshTimer);\n refreshTimer = null;\n }\n };\n\n render();\n setActive(true);\n\n return {\n element,\n setEdge: (edge) => {\n // Open the preview on the side away from the panel.\n preview.dataset.side = edge === \"left\" ? \"right\" : \"left\";\n },\n refresh: () => lab.refresh(),\n setTheme: (theme) => {\n preview.dataset.theme = theme;\n\n // In a dev-panel card the frame's palette (\\`--perf-*\\`) applies; elsewhere the panel's own.\n if (element.closest(\".perf-hud__card\")) {\n delete element.dataset.theme;\n } else {\n element.dataset.theme = theme;\n }\n },\n setActive,\n dispose: () => {\n setActive(false);\n unsubscribe();\n\n if (thumbTimer) {\n clearTimeout(thumbTimer);\n }\n\n thumbQueue = [];\n document.removeEventListener(\"pointerdown\", onPointerDown);\n preview.remove();\n element.remove();\n },\n };\n};\n","/**\n * The panel in the zkmake dev-panel frame (three-meter's `mountDevPanel`), so it docks, wakes,\n * dims and toggles exactly like the three-meter HUD: drag grip, expand/compact and dim-on-leave\n * discs beside a card with the brand label on top. One call for a vanilla three app.\n */\nimport {\n type DefaultPlacement,\n HudTheme,\n mountDevPanel,\n type ScreenEdge,\n type ThemeMode,\n} from \"@zkmake/three-meter/ui\";\n\nimport { TextureLab, type TextureLabOptions } from \"../core/lab.ts\";\nimport { createTexturePanel } from \"./panel.ts\";\n\nexport type MountTexturePanelOptions = TextureLabOptions & {\n /** Where it docks on a first visit. Default the right edge, top. */\n defaultPlacement?: DefaultPlacement;\n /** `dark`, `light` or `system` (default). */\n theme?: ThemeMode;\n /** Start compact: the brand row and the count. The choice is remembered. */\n compact?: boolean;\n /** Where to mount. Default `document.body`. */\n container?: HTMLElement;\n};\n\nexport type TexturePanelHandle = {\n lab: TextureLab;\n element: HTMLElement;\n setCompact(compact: boolean): void;\n /** `dark`, `light` or `system`: the panel follows it. */\n setTheme(mode: ThemeMode): void;\n dispose(): void;\n};\n\nconst readFlag = (key: string) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n};\n\nconst writeFlag = (key: string, value: string) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // Private mode: the choice lasts this page.\n }\n};\n\nexport const mountTexturePanel = (options: MountTexturePanelOptions): TexturePanelHandle => {\n const lab = new TextureLab(options);\n const panel = createTexturePanel(lab);\n const key = options.storageKey === null ? null : (options.storageKey ?? \"three-textures\");\n const theme = new HudTheme(options.theme ?? \"system\");\n const compactKey = key ? `${key}:compact` : null;\n\n const setCompact = (compact: boolean) => {\n frame.element.classList.toggle(\"ttx-compact\", compact);\n panel.setActive(!compact);\n\n if (compactKey) {\n writeFlag(compactKey, compact ? \"1\" : \"0\");\n }\n\n frame.refresh();\n };\n\n const frame = mountDevPanel({\n brand: \"three-textures\",\n content: panel.element,\n defaultPlacement: options.defaultPlacement ?? { edge: \"right\", align: \"start\" },\n label: \"Textures\",\n onEdgeChange: (edge: ScreenEdge) => panel.setEdge(edge),\n onToggle: () => setCompact(!frame.element.classList.contains(\"ttx-compact\")),\n storageKey: key ? `${key}:panel` : null,\n theme,\n toggleLabel: \"Show or hide the texture list\",\n ...(options.container ? { parent: options.container } : {}),\n });\n\n frame.element.classList.add(\"ttx-frame\");\n\n // The preview lives outside the card, in the page: it takes the theme on its own.\n const paintTheme = () => panel.setTheme(theme.resolved);\n const unsubscribeTheme = theme.subscribe(paintTheme);\n const showCount = (count: number) => {\n frame.detail.textContent = `${count} texture${count === 1 ? \"\" : \"s\"}`;\n };\n const onCount = (event: Event) => showCount((event as CustomEvent<number>).detail);\n\n paintTheme();\n panel.element.addEventListener(\"ttx-count\", onCount);\n showCount(Number(panel.element.dataset.count ?? 0));\n\n const stored = compactKey ? readFlag(compactKey) : null;\n\n setCompact(stored === null ? options.compact === true : stored === \"1\");\n\n return {\n lab,\n element: frame.element,\n setCompact,\n setTheme: (mode) => theme.setMode(mode),\n dispose: () => {\n unsubscribeTheme();\n theme.dispose();\n panel.element.removeEventListener(\"ttx-count\", onCount);\n panel.dispose();\n frame.dispose();\n lab.dispose();\n },\n };\n};\n"],"mappings":"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"}