omni-viewer-core 0.2.0 → 0.4.0

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.
@@ -11,5 +11,7 @@ export interface PptxParserDeps {
11
11
  openZip(data: Uint8Array, options?: ParseOptions): Promise<PptxZipReader>;
12
12
  }
13
13
  export { openPptxZip } from './zip-reader.js';
14
- export { parsePptxVscode } from './vscode.js';
14
+ import { parsePptxVscode } from './vscode.js';
15
+ export { parsePptxVscode };
16
+ export declare function parsePptxLegacy(input: Uint8Array, deps: PptxParserDeps, options?: ParseOptions): Promise<ParseOutcome<SlideDeck>>;
15
17
  export declare function parsePptx(input: Uint8Array, deps: PptxParserDeps, options?: ParseOptions): Promise<ParseOutcome<SlideDeck>>;
@@ -2,7 +2,8 @@
2
2
  import { DEFAULT_LIMITS, LimitTracker } from '../types.js';
3
3
  export * from '../slide-model.js';
4
4
  export { openPptxZip } from './zip-reader.js';
5
- export { parsePptxVscode } from './vscode.js';
5
+ import { parsePptxVscode } from './vscode.js';
6
+ export { parsePptxVscode };
6
7
  const EMU_PER_PX = 9525;
7
8
  const DEFAULT_SIZE = { widthPx: 1280, heightPx: 720 };
8
9
  const attr = (tag, name) => new RegExp(`(?:^|\\s)${name}="([^"]*)"`).exec(tag)?.[1];
@@ -54,7 +55,7 @@ async function parseSlide(zip, path, n, size) { const xml = await zip.text(path)
54
55
  else
55
56
  elements.push({ ...common, type: 'shape', presetGeom: geom, fillColor: color(block, 'transparent'), borderColor: color(/<a:ln\b[\s\S]*?<\/a:ln>/.exec(block)?.[0] ?? '') });
56
57
  } return { slideNumber: n, ...size, backgroundColor: color(/<p:bg\b[\s\S]*?<\/p:bg>/.exec(xml)?.[0] ?? '', '#ffffff') ?? '#ffffff', elements }; }
57
- export async function parsePptx(input, deps, options = {}) { const started = Date.now(), diagnostics = []; if (input.byteLength > (options.limits?.maxInputBytes ?? 50 * 1024 * 1024))
58
+ export async function parsePptxLegacy(input, deps, options = {}) { const started = Date.now(), diagnostics = []; if (input.byteLength > (options.limits?.maxInputBytes ?? 50 * 1024 * 1024))
58
59
  return { result: { status: 'failed', failure: { code: 'limit-exceeded', retryable: false, messageKey: 'diag.limit-exceeded.input' }, diagnostics }, execution: { workerUsed: false, hardLimitEnforced: false, elapsedMillis: Date.now() - started } }; let zip; try {
59
60
  zip = await deps.openZip(input, options);
60
61
  const presentation = await zip.text('ppt/presentation.xml');
@@ -86,3 +87,5 @@ catch {
86
87
  finally {
87
88
  await zip?.close?.();
88
89
  } }
90
+ export async function parsePptx(input, deps, options = {}) { const full = await parsePptxVscode(input, options); if (full.result.status !== 'failed' || full.result.failure.code !== 'invalid-format')
91
+ return full; return parsePptxLegacy(input, deps, options); }
@@ -4,7 +4,7 @@
4
4
  .omni-viewer--archive *{box-sizing:border-box}.omni-viewer--archive{padding:28px 20px 40px}.omni-archive__hero,.omni-archive__controls,.omni-archive__stats,.omni-archive__table-wrap,.omni-archive__preview-panel{background:var(--omni-bg-secondary,#252526);border:1px solid var(--omni-border,#3c3c3c);border-radius:18px;box-shadow:0 20px 40px rgba(0,0,0,.12)}
5
5
  .omni-archive__hero{display:flex;justify-content:space-between;gap:16px;padding:24px;align-items:flex-start}.omni-archive__eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--omni-accent,#d97706);font-weight:700}.omni-archive__hero h1{margin:8px 0 6px;font-size:clamp(26px,4vw,40px);line-height:1.1}.omni-archive__subtitle,.omni-archive__preview-meta{color:var(--omni-muted-fg,#9d9d9d);line-height:1.5}.omni-archive__pills{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.omni-archive__pill{padding:10px 14px;border-radius:999px;background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent);border:1px solid color-mix(in srgb,var(--omni-accent,#d97706) 35%,transparent);font-weight:600;white-space:nowrap}
6
6
  .omni-archive__controls,.omni-archive__stats,.omni-archive__workspace{margin-top:16px}.omni-archive__controls{padding:16px}.omni-archive__search-label{display:block;font-weight:600}.omni-archive__search{display:block;width:100%;margin-top:8px;padding:12px 14px;border-radius:12px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-input-bg,#3c3c3c);color:var(--omni-fg,#d4d4d4);font:inherit}.omni-archive__stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;padding:16px}.omni-archive__stat{padding:14px;border-radius:14px;background:color-mix(in srgb,var(--omni-bg-secondary,#252526) 76%,#000);border:1px solid var(--omni-border,#3c3c3c)}.omni-archive__stat-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__stat-value{font-size:24px;font-weight:700}
7
- .omni-archive__workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.95fr);gap:16px;align-items:start}.omni-archive__table-wrap{overflow:auto;min-width:0;max-height:70vh}.omni-archive__table{width:100%;table-layout:fixed;border-collapse:collapse}.omni-archive__table th,.omni-archive__table td{padding:12px 14px;border-bottom:1px solid var(--omni-border,#3c3c3c);text-align:left;vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-archive__table th:first-child{width:42%}.omni-archive__table th{position:sticky;top:0;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);background:var(--omni-bg-secondary,#252526);z-index:1}.omni-archive__path{font-family:var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}.omni-archive__entry{height:43px;cursor:pointer;transition:background-color 120ms ease}.omni-archive__entry:hover{background:color-mix(in srgb,var(--omni-accent,#d97706) 8%,transparent)}.omni-archive__entry:focus-visible{outline:2px solid var(--omni-accent,#d97706);outline-offset:-2px}.omni-archive__entry.is-selected{background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent)}.omni-archive__empty{padding:26px 18px;text-align:center;color:var(--omni-muted-fg,#9d9d9d)}
7
+ .omni-archive__workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.95fr);gap:16px;align-items:start}.omni-archive__table-wrap{overflow:auto;min-width:0;max-height:70vh;overflow-anchor:none}.omni-archive__table{width:100%;table-layout:fixed;border-collapse:collapse}.omni-archive__table th,.omni-archive__table td{padding:12px 14px;border-bottom:1px solid var(--omni-border,#3c3c3c);text-align:left;vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-archive__table th:first-child{width:42%}.omni-archive__table th{position:sticky;top:0;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);background:var(--omni-bg-secondary,#252526);z-index:1}.omni-archive__path{font-family:var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}.omni-archive__entry{height:43px;cursor:pointer;transition:background-color 120ms ease}.omni-archive__entry:hover{background:color-mix(in srgb,var(--omni-accent,#d97706) 8%,transparent)}.omni-archive__entry:focus-visible{outline:2px solid var(--omni-accent,#d97706);outline-offset:-2px}.omni-archive__entry.is-selected{background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent)}.omni-archive__empty{padding:26px 18px;text-align:center;color:var(--omni-muted-fg,#9d9d9d)}
8
8
  .omni-archive__preview-panel{padding:18px;min-height:420px;display:flex;flex-direction:column;gap:14px}.omni-archive__preview-header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.omni-archive__preview-kicker{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__preview-header h2{margin:0;font-size:18px;line-height:1.35;word-break:break-word}.omni-archive__preview-badge{border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;background:var(--omni-badge-bg,#3a3d41);white-space:nowrap}.omni-archive__preview-meta{margin:0}.omni-archive__preview{margin:0;flex:1;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;color:var(--omni-fg,#d4d4d4);font:12px/1.55 var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}
9
9
  .omni-archive__save{align-self:flex-start;padding:8px 12px;border:1px solid var(--omni-border,#3c3c3c);border-radius:6px;background:var(--omni-button-bg,#3a3d41);color:var(--omni-fg,#d4d4d4);font:inherit;cursor:pointer}.omni-archive__save:hover:not(:disabled){background:var(--omni-button-hover-bg,#45494e)}.omni-archive__save:disabled{opacity:.55;cursor:not-allowed}.omni-archive__spacer td{padding:0!important;border:0!important}
10
10
  .omni-archive__preview-media{flex:1;display:flex;align-items:center;justify-content:center;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;overflow:auto}.omni-archive__audio{width:100%}.omni-archive__video{max-width:100%;max-height:100%}.omni-archive__image{max-width:100%;max-height:100%;object-fit:contain}
@@ -1 +1 @@
1
- :host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #777;padding:3px;vertical-align:top}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}
1
+ :host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;border-radius:6px;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{padding:2px;white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table-frame{background:#fff;border:1px solid #bdbdbd;overflow:auto}.omni-ppt__table{border-collapse:collapse;width:100%;font-size:12px;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #d3d3d3;padding:4px 6px;vertical-align:top}.omni-ppt__chart{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:#f7f7f7;border:1px dashed #9a9a9a;color:#333}.omni-ppt__chart--rendered{background:transparent;border:0}.omni-ppt__chart-title{font-size:14px;font-weight:700;margin-bottom:4px}.omni-ppt__chart-subtitle{font-size:12px;opacity:.8}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}
@@ -294,20 +294,57 @@ export async function mountArchiveViewer(inputOrSource, container, ctx, depsOrOp
294
294
  const ROW_HEIGHT = 43;
295
295
  const OVERSCAN = 8;
296
296
  const FALLBACK_VIEWPORT_HEIGHT = 600;
297
- const render = () => {
297
+ let lastStart = -1;
298
+ let lastEnd = -1;
299
+ let lastLength = -1;
300
+ let lastQuery;
301
+ let lastExpanded;
302
+ /** `force` covers state changes (search/select/expand) that must repaint the
303
+ * rows; scroll events pass `false` so an unchanged window is a no-op. */
304
+ const render = (force = true) => {
298
305
  const visible = controller.visibleEntries();
306
+ // Measure before mutating: emptying the tbody first would collapse the
307
+ // scroll height, so the layout flush from reading clientHeight clamps
308
+ // scrollTop to 0 and every scroll snaps back to the top of the list.
309
+ const viewportHeight = tableWrap.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
310
+ const scrollTop = tableWrap.scrollTop;
311
+ const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
312
+ const count = Math.ceil(viewportHeight / ROW_HEIGHT) + OVERSCAN * 2;
313
+ const end = Math.min(visible.length, start + count);
314
+ // Most scroll ticks land inside the overscan margin. Rebuilding the rows
315
+ // anyway would destroy the focused row on every tick, and the resulting
316
+ // focus restore plus scroll anchoring drags the viewport around.
317
+ // The rendered window is fully determined by these; the controller hands
318
+ // out a fresh `expanded` set per toggle, so identity comparison is enough.
319
+ const sameWindow = start === lastStart && end === lastEnd && visible.length === lastLength
320
+ && controller.state.query === lastQuery && controller.state.expanded === lastExpanded;
321
+ // Most scroll ticks land inside the overscan margin. Rebuilding the rows
322
+ // anyway would destroy the focused row on every tick, and the resulting
323
+ // focus restore plus scroll anchoring drags the viewport around.
324
+ if (!force && sameWindow)
325
+ return;
326
+ // Selecting an entry only flips a class. Rebuilding every row for that is
327
+ // what threw the viewport back to the top the moment a row was clicked,
328
+ // so patch the existing rows in place and leave the scroll state alone.
329
+ if (sameWindow) {
330
+ for (const row of tbody.querySelectorAll('.omni-archive__entry'))
331
+ row.classList.toggle('is-selected', Number(row.dataset.entryId) === controller.state.selectedId);
332
+ return;
333
+ }
334
+ lastStart = start;
335
+ lastEnd = end;
336
+ lastLength = visible.length;
337
+ lastQuery = controller.state.query;
338
+ lastExpanded = controller.state.expanded;
299
339
  visibleStat[1] = visible.length;
300
340
  stats.querySelectorAll('.omni-archive__stat-value')[3].textContent = visible.length.toLocaleString();
301
- tbody.replaceChildren();
302
341
  empty.hidden = visible.length !== 0;
303
342
  table.hidden = visible.length === 0;
304
- const viewportHeight = tableWrap.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
305
- const start = Math.max(0, Math.floor(tableWrap.scrollTop / ROW_HEIGHT) - OVERSCAN);
306
- const count = Math.ceil(viewportHeight / ROW_HEIGHT) + OVERSCAN * 2;
307
- const end = Math.min(visible.length, start + count);
308
343
  const spacer = (height) => { const row = element('tr', 'omni-archive__spacer'); const cell = element('td'); cell.colSpan = 5; cell.style.height = `${height}px`; row.append(cell); return row; };
344
+ // Build off-DOM and swap once, so the tbody is never laid out empty.
345
+ const rows = [];
309
346
  if (start > 0)
310
- tbody.append(spacer(start * ROW_HEIGHT));
347
+ rows.push(spacer(start * ROW_HEIGHT));
311
348
  for (const entry of visible.slice(start, end)) {
312
349
  const row = element('tr', `omni-archive__entry${controller.state.selectedId === entry.entryId ? ' is-selected' : ''}`);
313
350
  row.tabIndex = 0;
@@ -320,14 +357,26 @@ export async function mountArchiveViewer(inputOrSource, container, ctx, depsOrOp
320
357
  event.preventDefault();
321
358
  void select(entry);
322
359
  } });
323
- tbody.append(row);
360
+ rows.push(row);
324
361
  }
325
362
  if (end < visible.length)
326
- tbody.append(spacer((visible.length - end) * ROW_HEIGHT));
363
+ rows.push(spacer((visible.length - end) * ROW_HEIGHT));
364
+ // Recycling rows destroys the focused one; hand focus back to its
365
+ // replacement so keyboard navigation survives a scroll.
366
+ const active = (typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot ? root.activeElement : document.activeElement);
367
+ const focusedId = active instanceof HTMLElement && tbody.contains(active) ? active.dataset.entryId : undefined;
368
+ tbody.replaceChildren(...rows);
369
+ // Swapping the rows can still make the browser adjust the offset (focus
370
+ // fixup, anchoring, a transient height change). The list is virtualized,
371
+ // so the pre-swap offset is authoritative — put it back.
372
+ if (tableWrap.scrollTop !== scrollTop)
373
+ tableWrap.scrollTop = scrollTop;
374
+ if (focusedId !== undefined)
375
+ rows.find(row => row.dataset.entryId === focusedId)?.focus({ preventScroll: true });
327
376
  };
328
- tableWrap.addEventListener('scroll', render, { passive: true });
377
+ tableWrap.addEventListener('scroll', () => render(false), { passive: true });
329
378
  search.addEventListener('input', () => { tableWrap.scrollTop = 0; controller.dispatch({ type: 'set-search', query: search.value }); });
330
- const unsubscribe = controller.subscribe(render);
379
+ const unsubscribe = controller.subscribe(() => render(true));
331
380
  render();
332
381
  return { dispose() { ticket++; extraction?.abort(); saveExtraction?.abort(); clearPreviewMedia(); unsubscribe(); wrap.remove(); void handle?.close(); handle = undefined; } };
333
382
  }
@@ -1 +1 @@
1
- export declare const archiveViewerCss = "\n.omni-viewer--archive{all:initial}.omni-viewer--archive :where(header,section,aside,div,h1,h2,p,span,label,input,table,thead,tbody,tr,th,td,pre){all:revert}\n:host,.omni-viewer--archive{display:block;min-height:100%;box-sizing:border-box;color:var(--omni-fg,#d4d4d4);background:radial-gradient(circle at top left,color-mix(in srgb,var(--omni-accent,#d97706) 14%,transparent),transparent 28%),var(--omni-bg,#1e1e1e);font:13px var(--omni-font,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif)}\n.omni-viewer--archive *{box-sizing:border-box}.omni-viewer--archive{padding:28px 20px 40px}.omni-archive__hero,.omni-archive__controls,.omni-archive__stats,.omni-archive__table-wrap,.omni-archive__preview-panel{background:var(--omni-bg-secondary,#252526);border:1px solid var(--omni-border,#3c3c3c);border-radius:18px;box-shadow:0 20px 40px rgba(0,0,0,.12)}\n.omni-archive__hero{display:flex;justify-content:space-between;gap:16px;padding:24px;align-items:flex-start}.omni-archive__eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--omni-accent,#d97706);font-weight:700}.omni-archive__hero h1{margin:8px 0 6px;font-size:clamp(26px,4vw,40px);line-height:1.1}.omni-archive__subtitle,.omni-archive__preview-meta{color:var(--omni-muted-fg,#9d9d9d);line-height:1.5}.omni-archive__pills{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.omni-archive__pill{padding:10px 14px;border-radius:999px;background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent);border:1px solid color-mix(in srgb,var(--omni-accent,#d97706) 35%,transparent);font-weight:600;white-space:nowrap}\n.omni-archive__controls,.omni-archive__stats,.omni-archive__workspace{margin-top:16px}.omni-archive__controls{padding:16px}.omni-archive__search-label{display:block;font-weight:600}.omni-archive__search{display:block;width:100%;margin-top:8px;padding:12px 14px;border-radius:12px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-input-bg,#3c3c3c);color:var(--omni-fg,#d4d4d4);font:inherit}.omni-archive__stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;padding:16px}.omni-archive__stat{padding:14px;border-radius:14px;background:color-mix(in srgb,var(--omni-bg-secondary,#252526) 76%,#000);border:1px solid var(--omni-border,#3c3c3c)}.omni-archive__stat-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__stat-value{font-size:24px;font-weight:700}\n.omni-archive__workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.95fr);gap:16px;align-items:start}.omni-archive__table-wrap{overflow:auto;min-width:0;max-height:70vh}.omni-archive__table{width:100%;table-layout:fixed;border-collapse:collapse}.omni-archive__table th,.omni-archive__table td{padding:12px 14px;border-bottom:1px solid var(--omni-border,#3c3c3c);text-align:left;vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-archive__table th:first-child{width:42%}.omni-archive__table th{position:sticky;top:0;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);background:var(--omni-bg-secondary,#252526);z-index:1}.omni-archive__path{font-family:var(--omni-mono-font,\"SFMono-Regular\",Consolas,monospace)}.omni-archive__entry{height:43px;cursor:pointer;transition:background-color 120ms ease}.omni-archive__entry:hover{background:color-mix(in srgb,var(--omni-accent,#d97706) 8%,transparent)}.omni-archive__entry:focus-visible{outline:2px solid var(--omni-accent,#d97706);outline-offset:-2px}.omni-archive__entry.is-selected{background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent)}.omni-archive__empty{padding:26px 18px;text-align:center;color:var(--omni-muted-fg,#9d9d9d)}\n.omni-archive__preview-panel{padding:18px;min-height:420px;display:flex;flex-direction:column;gap:14px}.omni-archive__preview-header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.omni-archive__preview-kicker{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__preview-header h2{margin:0;font-size:18px;line-height:1.35;word-break:break-word}.omni-archive__preview-badge{border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;background:var(--omni-badge-bg,#3a3d41);white-space:nowrap}.omni-archive__preview-meta{margin:0}.omni-archive__preview{margin:0;flex:1;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;color:var(--omni-fg,#d4d4d4);font:12px/1.55 var(--omni-mono-font,\"SFMono-Regular\",Consolas,monospace)}\n.omni-archive__save{align-self:flex-start;padding:8px 12px;border:1px solid var(--omni-border,#3c3c3c);border-radius:6px;background:var(--omni-button-bg,#3a3d41);color:var(--omni-fg,#d4d4d4);font:inherit;cursor:pointer}.omni-archive__save:hover:not(:disabled){background:var(--omni-button-hover-bg,#45494e)}.omni-archive__save:disabled{opacity:.55;cursor:not-allowed}.omni-archive__spacer td{padding:0!important;border:0!important}\n.omni-archive__preview-media{flex:1;display:flex;align-items:center;justify-content:center;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;overflow:auto}.omni-archive__audio{width:100%}.omni-archive__video{max-width:100%;max-height:100%}.omni-archive__image{max-width:100%;max-height:100%;object-fit:contain}\n@media(max-width:720px){.omni-viewer--archive{padding:16px 10px 24px}.omni-archive__hero{flex-direction:column}.omni-archive__pills{justify-content:flex-start}.omni-archive__table th:nth-child(n+3),.omni-archive__table td:nth-child(n+3){display:none}.omni-archive__workspace{grid-template-columns:1fr}.omni-archive__preview-panel{min-height:280px}}\n";
1
+ export declare const archiveViewerCss = "\n.omni-viewer--archive{all:initial}.omni-viewer--archive :where(header,section,aside,div,h1,h2,p,span,label,input,table,thead,tbody,tr,th,td,pre){all:revert}\n:host,.omni-viewer--archive{display:block;min-height:100%;box-sizing:border-box;color:var(--omni-fg,#d4d4d4);background:radial-gradient(circle at top left,color-mix(in srgb,var(--omni-accent,#d97706) 14%,transparent),transparent 28%),var(--omni-bg,#1e1e1e);font:13px var(--omni-font,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif)}\n.omni-viewer--archive *{box-sizing:border-box}.omni-viewer--archive{padding:28px 20px 40px}.omni-archive__hero,.omni-archive__controls,.omni-archive__stats,.omni-archive__table-wrap,.omni-archive__preview-panel{background:var(--omni-bg-secondary,#252526);border:1px solid var(--omni-border,#3c3c3c);border-radius:18px;box-shadow:0 20px 40px rgba(0,0,0,.12)}\n.omni-archive__hero{display:flex;justify-content:space-between;gap:16px;padding:24px;align-items:flex-start}.omni-archive__eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--omni-accent,#d97706);font-weight:700}.omni-archive__hero h1{margin:8px 0 6px;font-size:clamp(26px,4vw,40px);line-height:1.1}.omni-archive__subtitle,.omni-archive__preview-meta{color:var(--omni-muted-fg,#9d9d9d);line-height:1.5}.omni-archive__pills{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.omni-archive__pill{padding:10px 14px;border-radius:999px;background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent);border:1px solid color-mix(in srgb,var(--omni-accent,#d97706) 35%,transparent);font-weight:600;white-space:nowrap}\n.omni-archive__controls,.omni-archive__stats,.omni-archive__workspace{margin-top:16px}.omni-archive__controls{padding:16px}.omni-archive__search-label{display:block;font-weight:600}.omni-archive__search{display:block;width:100%;margin-top:8px;padding:12px 14px;border-radius:12px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-input-bg,#3c3c3c);color:var(--omni-fg,#d4d4d4);font:inherit}.omni-archive__stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;padding:16px}.omni-archive__stat{padding:14px;border-radius:14px;background:color-mix(in srgb,var(--omni-bg-secondary,#252526) 76%,#000);border:1px solid var(--omni-border,#3c3c3c)}.omni-archive__stat-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__stat-value{font-size:24px;font-weight:700}\n.omni-archive__workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.95fr);gap:16px;align-items:start}.omni-archive__table-wrap{overflow:auto;min-width:0;max-height:70vh;overflow-anchor:none}.omni-archive__table{width:100%;table-layout:fixed;border-collapse:collapse}.omni-archive__table th,.omni-archive__table td{padding:12px 14px;border-bottom:1px solid var(--omni-border,#3c3c3c);text-align:left;vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-archive__table th:first-child{width:42%}.omni-archive__table th{position:sticky;top:0;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);background:var(--omni-bg-secondary,#252526);z-index:1}.omni-archive__path{font-family:var(--omni-mono-font,\"SFMono-Regular\",Consolas,monospace)}.omni-archive__entry{height:43px;cursor:pointer;transition:background-color 120ms ease}.omni-archive__entry:hover{background:color-mix(in srgb,var(--omni-accent,#d97706) 8%,transparent)}.omni-archive__entry:focus-visible{outline:2px solid var(--omni-accent,#d97706);outline-offset:-2px}.omni-archive__entry.is-selected{background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent)}.omni-archive__empty{padding:26px 18px;text-align:center;color:var(--omni-muted-fg,#9d9d9d)}\n.omni-archive__preview-panel{padding:18px;min-height:420px;display:flex;flex-direction:column;gap:14px}.omni-archive__preview-header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.omni-archive__preview-kicker{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__preview-header h2{margin:0;font-size:18px;line-height:1.35;word-break:break-word}.omni-archive__preview-badge{border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;background:var(--omni-badge-bg,#3a3d41);white-space:nowrap}.omni-archive__preview-meta{margin:0}.omni-archive__preview{margin:0;flex:1;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;color:var(--omni-fg,#d4d4d4);font:12px/1.55 var(--omni-mono-font,\"SFMono-Regular\",Consolas,monospace)}\n.omni-archive__save{align-self:flex-start;padding:8px 12px;border:1px solid var(--omni-border,#3c3c3c);border-radius:6px;background:var(--omni-button-bg,#3a3d41);color:var(--omni-fg,#d4d4d4);font:inherit;cursor:pointer}.omni-archive__save:hover:not(:disabled){background:var(--omni-button-hover-bg,#45494e)}.omni-archive__save:disabled{opacity:.55;cursor:not-allowed}.omni-archive__spacer td{padding:0!important;border:0!important}\n.omni-archive__preview-media{flex:1;display:flex;align-items:center;justify-content:center;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;overflow:auto}.omni-archive__audio{width:100%}.omni-archive__video{max-width:100%;max-height:100%}.omni-archive__image{max-width:100%;max-height:100%;object-fit:contain}\n@media(max-width:720px){.omni-viewer--archive{padding:16px 10px 24px}.omni-archive__hero{flex-direction:column}.omni-archive__pills{justify-content:flex-start}.omni-archive__table th:nth-child(n+3),.omni-archive__table td:nth-child(n+3){display:none}.omni-archive__workspace{grid-template-columns:1fr}.omni-archive__preview-panel{min-height:280px}}\n";
@@ -4,7 +4,7 @@ export const archiveViewerCss = `
4
4
  .omni-viewer--archive *{box-sizing:border-box}.omni-viewer--archive{padding:28px 20px 40px}.omni-archive__hero,.omni-archive__controls,.omni-archive__stats,.omni-archive__table-wrap,.omni-archive__preview-panel{background:var(--omni-bg-secondary,#252526);border:1px solid var(--omni-border,#3c3c3c);border-radius:18px;box-shadow:0 20px 40px rgba(0,0,0,.12)}
5
5
  .omni-archive__hero{display:flex;justify-content:space-between;gap:16px;padding:24px;align-items:flex-start}.omni-archive__eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--omni-accent,#d97706);font-weight:700}.omni-archive__hero h1{margin:8px 0 6px;font-size:clamp(26px,4vw,40px);line-height:1.1}.omni-archive__subtitle,.omni-archive__preview-meta{color:var(--omni-muted-fg,#9d9d9d);line-height:1.5}.omni-archive__pills{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}.omni-archive__pill{padding:10px 14px;border-radius:999px;background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent);border:1px solid color-mix(in srgb,var(--omni-accent,#d97706) 35%,transparent);font-weight:600;white-space:nowrap}
6
6
  .omni-archive__controls,.omni-archive__stats,.omni-archive__workspace{margin-top:16px}.omni-archive__controls{padding:16px}.omni-archive__search-label{display:block;font-weight:600}.omni-archive__search{display:block;width:100%;margin-top:8px;padding:12px 14px;border-radius:12px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-input-bg,#3c3c3c);color:var(--omni-fg,#d4d4d4);font:inherit}.omni-archive__stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;padding:16px}.omni-archive__stat{padding:14px;border-radius:14px;background:color-mix(in srgb,var(--omni-bg-secondary,#252526) 76%,#000);border:1px solid var(--omni-border,#3c3c3c)}.omni-archive__stat-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__stat-value{font-size:24px;font-weight:700}
7
- .omni-archive__workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.95fr);gap:16px;align-items:start}.omni-archive__table-wrap{overflow:auto;min-width:0;max-height:70vh}.omni-archive__table{width:100%;table-layout:fixed;border-collapse:collapse}.omni-archive__table th,.omni-archive__table td{padding:12px 14px;border-bottom:1px solid var(--omni-border,#3c3c3c);text-align:left;vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-archive__table th:first-child{width:42%}.omni-archive__table th{position:sticky;top:0;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);background:var(--omni-bg-secondary,#252526);z-index:1}.omni-archive__path{font-family:var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}.omni-archive__entry{height:43px;cursor:pointer;transition:background-color 120ms ease}.omni-archive__entry:hover{background:color-mix(in srgb,var(--omni-accent,#d97706) 8%,transparent)}.omni-archive__entry:focus-visible{outline:2px solid var(--omni-accent,#d97706);outline-offset:-2px}.omni-archive__entry.is-selected{background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent)}.omni-archive__empty{padding:26px 18px;text-align:center;color:var(--omni-muted-fg,#9d9d9d)}
7
+ .omni-archive__workspace{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.95fr);gap:16px;align-items:start}.omni-archive__table-wrap{overflow:auto;min-width:0;max-height:70vh;overflow-anchor:none}.omni-archive__table{width:100%;table-layout:fixed;border-collapse:collapse}.omni-archive__table th,.omni-archive__table td{padding:12px 14px;border-bottom:1px solid var(--omni-border,#3c3c3c);text-align:left;vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-archive__table th:first-child{width:42%}.omni-archive__table th{position:sticky;top:0;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--omni-muted-fg,#9d9d9d);background:var(--omni-bg-secondary,#252526);z-index:1}.omni-archive__path{font-family:var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}.omni-archive__entry{height:43px;cursor:pointer;transition:background-color 120ms ease}.omni-archive__entry:hover{background:color-mix(in srgb,var(--omni-accent,#d97706) 8%,transparent)}.omni-archive__entry:focus-visible{outline:2px solid var(--omni-accent,#d97706);outline-offset:-2px}.omni-archive__entry.is-selected{background:color-mix(in srgb,var(--omni-accent,#d97706) 16%,transparent)}.omni-archive__empty{padding:26px 18px;text-align:center;color:var(--omni-muted-fg,#9d9d9d)}
8
8
  .omni-archive__preview-panel{padding:18px;min-height:420px;display:flex;flex-direction:column;gap:14px}.omni-archive__preview-header{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.omni-archive__preview-kicker{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--omni-muted-fg,#9d9d9d);margin-bottom:6px}.omni-archive__preview-header h2{margin:0;font-size:18px;line-height:1.35;word-break:break-word}.omni-archive__preview-badge{border-radius:999px;padding:8px 12px;font-size:12px;font-weight:700;background:var(--omni-badge-bg,#3a3d41);white-space:nowrap}.omni-archive__preview-meta{margin:0}.omni-archive__preview{margin:0;flex:1;overflow:auto;white-space:pre-wrap;word-break:break-word;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;color:var(--omni-fg,#d4d4d4);font:12px/1.55 var(--omni-mono-font,"SFMono-Regular",Consolas,monospace)}
9
9
  .omni-archive__save{align-self:flex-start;padding:8px 12px;border:1px solid var(--omni-border,#3c3c3c);border-radius:6px;background:var(--omni-button-bg,#3a3d41);color:var(--omni-fg,#d4d4d4);font:inherit;cursor:pointer}.omni-archive__save:hover:not(:disabled){background:var(--omni-button-hover-bg,#45494e)}.omni-archive__save:disabled{opacity:.55;cursor:not-allowed}.omni-archive__spacer td{padding:0!important;border:0!important}
10
10
  .omni-archive__preview-media{flex:1;display:flex;align-items:center;justify-content:center;border-radius:14px;border:1px solid var(--omni-border,#3c3c3c);background:var(--omni-code-bg,#181818);padding:14px;overflow:auto}.omni-archive__audio{width:100%}.omni-archive__video{max-width:100%;max-height:100%}.omni-archive__image{max-width:100%;max-height:100%;object-fit:contain}
@@ -2,7 +2,7 @@ import { type PdfAnnotation, type PdfViewState } from './controller.js';
2
2
  /** Type-only reference — erased at compile time. */
3
3
  export type PdfLibModule = typeof import('pdf-lib');
4
4
  export declare const SIDECAR_LAYER_NAME = "omni-viewer-layer.json";
5
- export declare const SIDECAR_ORIGINAL_NAME = "omni-viewer-original.pdf";
5
+ export declare const SIDECAR_BASE_NAME = "omni-viewer-base.pdf";
6
6
  /** Result of parsing a sidecar's layer JSON. */
7
7
  export interface ParsedLayer {
8
8
  pageOrder: number[];
@@ -14,9 +14,11 @@ export declare function buildEditedPdf(pdfLib: PdfLibModule, source: Uint8Array,
14
14
  /**
15
15
  * Like {@link buildEditedPdf}, but also embeds the hybrid sidecar so the
16
16
  * omni viewer can rehydrate a removable overlay layer on reopen:
17
- * - the pristine (unflattened) source, and
18
- * - the layer JSON `{ version, pageOrder, annotations }`.
19
- * External readers only ever see the flattened pages.
17
+ * - a re-editable base (kept pages only, signatures flattened in), and
18
+ * - the layer JSON `{ version, pageOrder, annotations }` for text/markup,
19
+ * with page references remapped onto the kept-pages base.
20
+ * Deleted pages and signatures are unrecoverable; external readers only ever
21
+ * see the flattened pages.
20
22
  */
21
23
  export declare function buildSavedPdf(pdfLib: PdfLibModule, pristine: Uint8Array, state: PdfViewState): Promise<Uint8Array>;
22
24
  /** Parse and validate a sidecar layer JSON string; null if unusable. */
@@ -5,13 +5,16 @@
5
5
  // imports). Callers inject the loaded module; `viewers/pdf/self-loading`
6
6
  // provides the dynamic-import loader for platforms whose bundler resolves it.
7
7
  import { mergeHighlightRects } from './controller.js';
8
- // Hybrid sidecar: saved PDFs keep flattened (portable) pages while embedding
9
- // the pristine source + a layer JSON, so the omni viewer can restore a
10
- // removable overlay layer on reopen. External readers only see the flat pages.
8
+ // Hybrid sidecar: saved PDFs keep flattened (portable) pages for external
9
+ // readers while embedding a re-editable base + a layer JSON, so the omni
10
+ // viewer can restore a removable overlay layer on reopen. The embedded base
11
+ // holds only the kept pages (deleted pages are gone for good) with signatures
12
+ // permanently flattened in; text/markup stay out so they remain editable.
13
+ // External readers only ever see the flat pages.
11
14
  export const SIDECAR_LAYER_NAME = 'omni-viewer-layer.json';
12
- export const SIDECAR_ORIGINAL_NAME = 'omni-viewer-original.pdf';
15
+ export const SIDECAR_BASE_NAME = 'omni-viewer-base.pdf';
13
16
  /** Bumped when the sidecar shape changes; older/newer versions are ignored. */
14
- const SIDECAR_VERSION = 1;
17
+ const SIDECAR_VERSION = 2;
15
18
  /** Defensive ceilings — the sidecar is untrusted input on reopen. */
16
19
  const MAX_ANNOTATIONS = 2000;
17
20
  const MAX_TEXT_LENGTH = 10_000;
@@ -104,16 +107,39 @@ async function stamp(pdfLib, page, annotation) {
104
107
  height: annotation.height
105
108
  });
106
109
  }
107
- /** Reorder/delete pages and stamp the overlays into a fresh (unsaved) doc. */
108
- async function flattenInto(pdfLib, source, state) {
110
+ /** Copy the kept pages (in display order) from `source` into a fresh doc. */
111
+ async function copyKeptPages(pdfLib, source, pageOrder) {
109
112
  const original = await pdfLib.PDFDocument.load(source);
110
113
  const output = await pdfLib.PDFDocument.create();
111
- const indices = state.pageOrder
114
+ const indices = pageOrder
112
115
  .map((page) => page - 1)
113
116
  .filter((page) => page >= 0 && page < original.getPageCount());
114
117
  const copied = await output.copyPages(original, indices);
115
118
  copied.forEach((page) => output.addPage(page));
119
+ return output;
120
+ }
121
+ /** Reorder/delete pages and stamp the overlays into a fresh (unsaved) doc. */
122
+ async function flattenInto(pdfLib, source, state) {
123
+ const output = await copyKeptPages(pdfLib, source, state.pageOrder);
124
+ for (const annotation of state.annotations) {
125
+ const outputIndex = state.pageOrder.indexOf(annotation.page);
126
+ const page = output.getPages()[outputIndex];
127
+ if (page)
128
+ await stamp(pdfLib, page, annotation);
129
+ }
130
+ return output;
131
+ }
132
+ /**
133
+ * The re-editable base embedded in a saved PDF: the kept pages (unflattened)
134
+ * with ONLY signatures permanently stamped in. Text/markup are deliberately
135
+ * left out so the omni viewer can restore them as a removable layer, while
136
+ * signatures — and any pages the user deleted — cannot be recovered.
137
+ */
138
+ async function buildEditableBase(pdfLib, pristine, state) {
139
+ const output = await copyKeptPages(pdfLib, pristine, state.pageOrder);
116
140
  for (const annotation of state.annotations) {
141
+ if (annotation.kind !== 'signature')
142
+ continue;
117
143
  const outputIndex = state.pageOrder.indexOf(annotation.page);
118
144
  const page = output.getPages()[outputIndex];
119
145
  if (page)
@@ -130,21 +156,29 @@ export async function buildEditedPdf(pdfLib, source, state) {
130
156
  /**
131
157
  * Like {@link buildEditedPdf}, but also embeds the hybrid sidecar so the
132
158
  * omni viewer can rehydrate a removable overlay layer on reopen:
133
- * - the pristine (unflattened) source, and
134
- * - the layer JSON `{ version, pageOrder, annotations }`.
135
- * External readers only ever see the flattened pages.
159
+ * - a re-editable base (kept pages only, signatures flattened in), and
160
+ * - the layer JSON `{ version, pageOrder, annotations }` for text/markup,
161
+ * with page references remapped onto the kept-pages base.
162
+ * Deleted pages and signatures are unrecoverable; external readers only ever
163
+ * see the flattened pages.
136
164
  */
137
165
  export async function buildSavedPdf(pdfLib, pristine, state) {
138
166
  const output = await flattenInto(pdfLib, pristine, state);
167
+ const base = await buildEditableBase(pdfLib, pristine, state);
168
+ // The base is already in display order, so its pages number 1..keptCount.
169
+ // Remap each overlay's page reference (an original page number) onto that.
170
+ const remap = new Map(state.pageOrder.map((page, index) => [page, index + 1]));
139
171
  const layer = JSON.stringify({
140
172
  version: SIDECAR_VERSION,
141
- pageOrder: state.pageOrder,
173
+ pageOrder: base.getPages().map((_, index) => index + 1),
142
174
  annotations: state.annotations
175
+ .filter((annotation) => annotation.kind !== 'signature')
176
+ .map((annotation) => ({ ...annotation, page: remap.get(annotation.page) ?? annotation.page }))
143
177
  });
144
178
  await output.attach(new TextEncoder().encode(layer), SIDECAR_LAYER_NAME, {
145
179
  mimeType: 'application/json'
146
180
  });
147
- await output.attach(pristine, SIDECAR_ORIGINAL_NAME, {
181
+ await output.attach(await base.save(), SIDECAR_BASE_NAME, {
148
182
  mimeType: 'application/pdf'
149
183
  });
150
184
  return output.save();
@@ -11,7 +11,7 @@
11
11
  // annotations use an inline input overlay instead.
12
12
  import { MountAbortedError, VIEWER_ROOT_CLASS } from '../types.js';
13
13
  import { createPdfController, mergeHighlightRects, PDF_ZOOM_LEVELS } from './controller.js';
14
- import { buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_LAYER_NAME, SIDECAR_ORIGINAL_NAME } from './editing.js';
14
+ import { buildSavedPdf, mergePdfBytes, parseLayer, savedPdfName, SIDECAR_LAYER_NAME, SIDECAR_BASE_NAME } from './editing.js';
15
15
  import { pdfViewerCss } from './styles.js';
16
16
  export { createPdfController, PDF_ZOOM_LEVELS } from './controller.js';
17
17
  export { pdfViewerCss } from './styles.js';
@@ -176,7 +176,7 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
176
176
  }
177
177
  }
178
178
  /**
179
- * Read the hybrid sidecar (pristine source + layer JSON) from a document's
179
+ * Read the hybrid sidecar (re-editable base + layer JSON) from a document's
180
180
  * attachments. Returns null unless BOTH parts are present and the layer
181
181
  * parses — a lone layer over already-flattened pages would double up.
182
182
  */
@@ -185,19 +185,19 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
185
185
  const attachments = await d.getAttachments?.();
186
186
  if (!attachments)
187
187
  return null;
188
- let pristine;
188
+ let base;
189
189
  let layerText;
190
190
  for (const entry of Object.values(attachments)) {
191
- if (entry.filename === SIDECAR_ORIGINAL_NAME)
192
- pristine = entry.content;
191
+ if (entry.filename === SIDECAR_BASE_NAME)
192
+ base = entry.content;
193
193
  else if (entry.filename === SIDECAR_LAYER_NAME) {
194
194
  layerText = new TextDecoder().decode(entry.content);
195
195
  }
196
196
  }
197
- if (!pristine || layerText === undefined)
197
+ if (!base || layerText === undefined)
198
198
  return null;
199
199
  const layer = parseLayer(layerText);
200
- return layer ? { pristine, layer } : null;
200
+ return layer ? { base, layer } : null;
201
201
  }
202
202
  catch {
203
203
  return null;
@@ -1374,12 +1374,13 @@ export async function mountPdfViewer(input, container, ctx, deps, options = {})
1374
1374
  }
1375
1375
  throwIfAborted();
1376
1376
  // Hybrid rehydration: if this file was saved by us, its visible pages are
1377
- // flattened but it carries the pristine source + layer JSON. Render/edit
1378
- // from the pristine base and restore the overlays as a removable layer.
1377
+ // flattened but it carries a re-editable base (kept pages, signatures baked
1378
+ // in) + layer JSON. Render/edit from that base and restore the text/markup
1379
+ // overlays as a removable layer.
1379
1380
  const sidecar = await readSidecar(doc);
1380
1381
  if (sidecar) {
1381
1382
  await doc.destroy();
1382
- sourceBytes = sidecar.pristine;
1383
+ sourceBytes = sidecar.base;
1383
1384
  doc = await loadDocument(sourceBytes, password);
1384
1385
  throwIfAborted();
1385
1386
  adoptController(createPdfController(doc.numPages, sidecar.layer));
@@ -9,7 +9,7 @@ export function computeChartBounds(data) { const explicitMin = data.valueAxis?.m
9
9
  const positive = data.categories.map((_, i) => data.series.reduce((sum, s) => sum + Math.max(0, s.values[i] ?? 0), 0)), negative = data.categories.map((_, i) => data.series.reduce((sum, s) => sum + Math.min(0, s.values[i] ?? 0), 0));
10
10
  const min = finite(explicitMin) ? explicitMin : Math.min(0, ...negative), max = finite(explicitMax) ? explicitMax : Math.max(1, ...positive);
11
11
  return min < max ? { min, max } : { min, max: min + 1 };
12
- } const values = data.series.flatMap(s => s.values).filter(finite); let min = finite(explicitMin) ? explicitMin : Math.min(0, ...values), max = finite(explicitMax) ? explicitMax : Math.max(1, ...values); if (min >= max) {
12
+ } const values = data.series.flatMap(s => s.values).filter(finite); let min = finite(explicitMin) ? explicitMin : Math.min(0, ...values), max = finite(explicitMax) ? explicitMax : values.length ? Math.max(...values) : 1; if (min >= max) {
13
13
  const pad = Math.max(1, Math.abs(max || 1) * .1);
14
14
  min -= pad;
15
15
  max += pad;
@@ -26,6 +26,12 @@ function legend(ctx, w, h, data, line) { const style = data.legend ?? {}, font =
26
26
  ctx.moveTo(x, h - 11);
27
27
  ctx.lineTo(x + swatch, h - 11);
28
28
  ctx.stroke();
29
+ ctx.fillStyle = '#fff';
30
+ ctx.beginPath();
31
+ ctx.arc(x + swatch / 2, h - 11, 3.5, 0, Math.PI * 2);
32
+ ctx.fill();
33
+ ctx.strokeStyle = item.color;
34
+ ctx.stroke();
29
35
  }
30
36
  else {
31
37
  ctx.fillStyle = item.color;
@@ -36,7 +42,11 @@ function legend(ctx, w, h, data, line) { const style = data.legend ?? {}, font =
36
42
  ctx.fillText(item.label, x, h - 7);
37
43
  x += item.label.length * font * .55 + gap;
38
44
  } }
39
- function axes(ctx, w, h, data, bounds) { const m = { left: 74, right: 20, top: 20, bottom: 88 }, pw = w - m.left - m.right, ph = h - m.top - m.bottom, axis = data.valueAxis ?? {}, format = axis.numFmt ?? (data.kind === 'stackedColumn' ? '$#,##0.0' : '0.0'), step = finite(axis.majorUnit) && axis.majorUnit > 0 ? axis.majorUnit : niceStep(bounds.max - bounds.min), y = (value) => m.top + (bounds.max - value) / (bounds.max - bounds.min) * ph; ctx.strokeStyle = axis.gridColor ?? 'rgba(0,0,0,0.22)'; ctx.lineWidth = 1; for (let tick = Math.floor(bounds.min / step) * step, count = 0; tick <= bounds.max + step * .5 && count++ < 250; tick += step) {
45
+ function axes(ctx, w, h, data, bounds) { const m = { left: 74, right: 20, top: 20, bottom: 88 }, pw = w - m.left - m.right, ph = h - m.top - m.bottom, axis = data.valueAxis ?? {}, format = axis.numFmt ?? (data.kind === 'stackedColumn' ? '$#,##0.0' : '0.0'), step = finite(axis.majorUnit) && axis.majorUnit > 0 ? axis.majorUnit : niceStep(bounds.max - bounds.min), y = (value) => m.top + (bounds.max - value) / (bounds.max - bounds.min) * ph; if (data.kind === 'stackedColumn') {
46
+ ctx.strokeStyle = axis.lineColor ?? '#4f81bd';
47
+ ctx.lineWidth = 1;
48
+ ctx.strokeRect(m.left, m.top, pw, ph);
49
+ } ctx.strokeStyle = axis.gridColor ?? (data.kind === 'line' ? 'rgba(0,0,0,0.18)' : 'rgba(0,0,0,0.22)'); ctx.lineWidth = 1; for (let tick = Math.floor(bounds.min / step) * step, count = 0; tick <= bounds.max + step * .5 && count++ < 250; tick += step) {
40
50
  const py = y(tick);
41
51
  ctx.beginPath();
42
52
  ctx.moveTo(m.left, py);
@@ -47,7 +57,13 @@ function axes(ctx, w, h, data, bounds) { const m = { left: 74, right: 20, top: 2
47
57
  ctx.textAlign = 'right';
48
58
  ctx.textBaseline = 'middle';
49
59
  ctx.fillText(formatChartValue(tick, format), m.left - 12, py);
50
- } const cross = finite(axis.crossesAt) ? axis.crossesAt : 0; ctx.strokeStyle = axis.lineColor ?? '#111111'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(m.left, y(cross)); ctx.lineTo(m.left + pw, y(cross)); ctx.stroke(); return { ...m, pw, ph, y, format }; }
60
+ } const cross = finite(axis.crossesAt) ? axis.crossesAt : 0; ctx.strokeStyle = data.kind === 'stackedColumn' ? '#111111' : axis.lineColor ?? '#111111'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(m.left, y(cross)); ctx.lineTo(m.left + pw, y(cross)); ctx.stroke(); if (data.kind === 'line') {
61
+ ctx.beginPath();
62
+ ctx.moveTo(m.left, m.top);
63
+ ctx.lineTo(m.left, m.top + ph);
64
+ ctx.lineTo(m.left + pw, m.top + ph);
65
+ ctx.stroke();
66
+ } return { ...m, pw, ph, y, format }; }
51
67
  function label(ctx, series, value, x, y, height, axisFormat) { const style = series.dataLabel ?? {}; if (style.showValue === false || Math.abs(value) < .1 || height <= 12)
52
68
  return; ctx.fillStyle = style.color ?? '#ffffff'; ctx.font = `400 ${Math.max(9, style.fontSizePx ?? 11)}px Arial, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(formatChartValue(value, style.numFmt ?? series.valueFormat ?? axisFormat), x, y); }
53
69
  function stacked(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, bounds), group = a.pw / data.categories.length, gap = finite(data.gapWidth) ? Math.max(.2, Math.min(.9, 1 - data.gapWidth / 300)) : .58, bar = Math.max(8, group * gap), category = data.categoryAxis ?? {}; data.categories.forEach((name, i) => { const cx = a.left + group * i + group / 2; let pos = 0, neg = 0; for (const series of data.series) {
@@ -72,6 +88,9 @@ function line(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, bounds)
72
88
  ctx.arc(px, py, 4.5, 0, Math.PI * 2);
73
89
  ctx.fill();
74
90
  ctx.strokeStyle = series.color || '#4472c4';
91
+ ctx.lineWidth = 2;
92
+ ctx.beginPath();
93
+ ctx.arc(px, py, 4.5, 0, Math.PI * 2);
75
94
  ctx.stroke();
76
95
  const style = series.dataLabel ?? {};
77
96
  if (style.showValue !== false) {
@@ -1,5 +1,5 @@
1
1
  import { parsePptBinaryVscode } from '../../parsers/ppt-binary/index.js';
2
- import { openPptxZip, parsePptx, parsePptxVscode } from '../../parsers/pptx/index.js';
2
+ import { openPptxZip, parsePptxLegacy, parsePptxVscode } from '../../parsers/pptx/index.js';
3
3
  import { mountPdfViewer } from '../pdf/index.js';
4
4
  import { MountAbortedError, VIEWER_ROOT_CLASS } from '../types.js';
5
5
  import { createPptController } from './controller.js';
@@ -41,7 +41,7 @@ export async function mountPptViewer(input, container, ctx, deps = {}, options =
41
41
  return mountPdfFallback(input, container, ctx, deps, options, 'diag.ppt.empty');
42
42
  if (!deck.slides.some(slide => slide.elements.length > 0)) {
43
43
  if (isPptx) {
44
- const legacy = await (deps.parseLegacyPptx?.(input.data, options) ?? parsePptx(input.data, { openZip: openPptxZip }, options));
44
+ const legacy = await (deps.parseLegacyPptx?.(input.data, options) ?? parsePptxLegacy(input.data, { openZip: deps.openZip ?? openPptxZip }, options));
45
45
  if (legacy.result.status !== 'failed' && legacy.result.document.slides.some(slide => slide.elements.length > 0))
46
46
  deck = legacy.result.document;
47
47
  else
@@ -64,7 +64,7 @@ export async function mountPptViewer(input, container, ctx, deps = {}, options =
64
64
  next.setAttribute('aria-label', ctx.i18n.t('ppt.next'));
65
65
  jump.setAttribute('aria-label', ctx.i18n.t('ppt.jump'));
66
66
  deck.slides.forEach(slide => { const option = el('option'); option.value = String(slide.slideNumber); const title = slide.elements.find(element => element.isTitle)?.paragraphs?.map(paragraph => paragraph.text).join(' '); option.textContent = `Slide ${slide.slideNumber}${title ? `: ${title}` : ''}`; jump.append(option); });
67
- toolbar.append(prev, next, jump, count, minus, plus, reset, mode);
67
+ toolbar.append(prev, next, jump, count, minus, reset, plus, mode);
68
68
  const viewport = el('div', 'omni-ppt__viewport'), slides = el('div', 'omni-ppt__slides');
69
69
  viewport.append(slides);
70
70
  frame.append(toolbar, viewport);
@@ -79,8 +79,9 @@ function imageElement(e) { const box = frame(e, 'image'), img = node('img', 'omn
79
79
  img.src = recolored;
80
80
  }
81
81
  catch { /* keep the original raster when recoloring fails */ } }, { once: true }); box.append(img); return box; }
82
- function tableElement(e) { const box = frame(e, 'table'), table = node('table', 'omni-ppt__table'); (e.tableRows ?? []).forEach((row, rowIndex) => { const tr = node('tr'); row.forEach(value => { const cell = node(rowIndex === 0 ? 'th' : 'td'); cell.textContent = value; tr.append(cell); }); table.append(tr); }); box.append(table); return box; }
82
+ function tableElement(e) { const box = frame(e, 'table-frame'), table = node('table', 'omni-ppt__table'); (e.tableRows ?? []).forEach((row, rowIndex) => { const tr = node('tr'); row.forEach(value => { const cell = node(rowIndex === 0 ? 'th' : 'td'); cell.textContent = value; tr.append(cell); }); table.append(tr); }); box.append(table); return box; }
83
83
  function chartElement(e) { const box = frame(e, 'chart'); if (e.chartData) {
84
+ box.classList.add('omni-ppt__chart--rendered');
84
85
  const canvas = node('canvas');
85
86
  canvas.width = Math.max(300, Math.floor(e.width));
86
87
  canvas.height = Math.max(180, Math.floor(e.height));
@@ -90,7 +91,10 @@ function chartElement(e) { const box = frame(e, 'chart'); if (e.chartData) {
90
91
  queueMicrotask(() => drawChart(canvas, e.chartData));
91
92
  }
92
93
  else {
93
- box.textContent = e.chartTitle ?? (e.chartKind === 'smartart' ? 'SmartArt' : 'Chart');
94
+ const title = node('div', 'omni-ppt__chart-title'), subtitle = node('div', 'omni-ppt__chart-subtitle');
95
+ title.textContent = e.chartTitle ?? (e.chartKind === 'smartart' ? 'SmartArt' : 'Chart');
96
+ subtitle.textContent = e.chartKind === 'smartart' ? 'SmartArt placeholder' : 'Chart placeholder';
97
+ box.append(title, subtitle);
94
98
  } return box; }
95
99
  export function presetGeomToSvgPath(preset, w, h) { if (!preset || w <= 0 || h <= 0)
96
100
  return; switch (preset) {
@@ -120,11 +124,25 @@ export function presetGeomToSvgPath(preset, w, h) { if (!preset || w <= 0 || h <
120
124
  case 'rect': return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
121
125
  default: return;
122
126
  } }
123
- function marker(defs, id, type, color) { const value = document.createElementNS(SVG_NS, 'marker'); value.id = id; value.setAttribute('markerWidth', '8'); value.setAttribute('markerHeight', '8'); value.setAttribute('refX', type === 'start' ? '0' : '8'); value.setAttribute('refY', '4'); value.setAttribute('orient', 'auto'); value.setAttribute('markerUnits', 'strokeWidth'); const path = document.createElementNS(SVG_NS, 'path'); path.setAttribute('d', type === 'circle' ? 'M 4 1 A 3 3 0 1 0 4 7 A 3 3 0 1 0 4 1' : 'M 0 0 L 8 4 L 0 8 Z'); path.setAttribute('fill', color); value.append(path); defs.append(value); }
124
- function shapeElement(e) { const box = frame(e, 'shape'), w = Math.max(1, Math.abs(e.width)), h = Math.max(1, Math.abs(e.height)), svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('width', String(w)); svg.setAttribute('height', String(h)); svg.setAttribute('viewBox', `0 0 ${w} ${h}`); const stroke = e.borderColor ?? '#000', strokeWidth = String(Math.max(1, e.borderWidthPx ?? 1)); if (e.presetGeom === 'line' || e.presetGeom === 'straightConnector1') {
127
+ function marker(defs, id, type, color) { const value = document.createElementNS(SVG_NS, 'marker'), circle = type === 'circle'; value.id = id; value.setAttribute('markerWidth', circle ? '6' : '8'); value.setAttribute('markerHeight', '6'); value.setAttribute('refX', circle ? '3' : '8'); value.setAttribute('refY', '3'); value.setAttribute('orient', 'auto'); value.setAttribute('markerUnits', 'strokeWidth'); value.setAttribute('overflow', 'visible'); if (circle) {
128
+ const dot = document.createElementNS(SVG_NS, 'circle');
129
+ dot.setAttribute('cx', '3');
130
+ dot.setAttribute('cy', '3');
131
+ dot.setAttribute('r', '2.5');
132
+ dot.setAttribute('fill', color);
133
+ value.append(dot);
134
+ }
135
+ else {
136
+ const path = document.createElementNS(SVG_NS, 'path');
137
+ path.setAttribute('d', 'M 0 0 L 8 3 L 0 6 Z');
138
+ path.setAttribute('fill', color);
139
+ value.append(path);
140
+ } defs.append(value); }
141
+ function shapeElement(e) { const box = frame(e, 'shape'), w = Math.max(1, Math.abs(e.width)), h = Math.max(1, Math.abs(e.height)), svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('width', String(w)); svg.setAttribute('height', String(h)); svg.setAttribute('viewBox', `0 0 ${w} ${h}`); Object.assign(svg.style, { display: 'block', overflow: 'visible' }); const stroke = e.borderColor ?? '#000', strokeWidth = String(Math.max(1, e.borderWidthPx ?? 1)); if (e.presetGeom === 'line' || e.presetGeom === 'straightConnector1') {
125
142
  box.style.transform = e.rotateDeg ? `rotate(${e.rotateDeg}deg)` : '';
143
+ Object.assign(svg.style, { position: 'absolute', left: '0', top: '0' });
126
144
  const defs = document.createElementNS(SVG_NS, 'defs'), suffix = `${e.zIndex}-${Math.round(e.x)}-${Math.round(e.y)}`;
127
- marker(defs, `arrow-${suffix}`, 'end', stroke);
145
+ marker(defs, `arrow-${suffix}`, 'arrow', stroke);
128
146
  marker(defs, `circle-${suffix}`, 'circle', stroke);
129
147
  svg.append(defs);
130
148
  const line = document.createElementNS(SVG_NS, 'line');
@@ -1 +1 @@
1
- export declare const pptViewerCss = ":host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset=\"cover-title\"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset=\"cover-subtitle\"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #777;padding:3px;vertical-align:top}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}";
1
+ export declare const pptViewerCss = ":host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;border-radius:6px;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{padding:2px;white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset=\"cover-title\"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset=\"cover-subtitle\"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table-frame{background:#fff;border:1px solid #bdbdbd;overflow:auto}.omni-ppt__table{border-collapse:collapse;width:100%;font-size:12px;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #d3d3d3;padding:4px 6px;vertical-align:top}.omni-ppt__chart{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:#f7f7f7;border:1px dashed #9a9a9a;color:#333}.omni-ppt__chart--rendered{background:transparent;border:0}.omni-ppt__chart-title{font-size:14px;font-weight:700;margin-bottom:4px}.omni-ppt__chart-subtitle{font-size:12px;opacity:.8}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}";
@@ -1 +1 @@
1
- export const pptViewerCss = `:host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #777;padding:3px;vertical-align:top}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}`;
1
+ export const pptViewerCss = `:host,.omni-viewer--ppt{color:var(--omni-fg,#ddd);font:13px system-ui}.omni-ppt{height:100%;display:flex;flex-direction:column;background:var(--omni-bg,#181818)}.omni-ppt__toolbar{display:flex;gap:6px;align-items:center;padding:8px;border-bottom:1px solid var(--omni-border,#444)}.omni-ppt__toolbar button,.omni-ppt__toolbar select{background:var(--omni-control-bg,#292929);color:inherit;border:1px solid var(--omni-border,#555);border-radius:4px;padding:4px 8px}.omni-ppt__viewport{overflow:auto;flex:1;padding:20px}.omni-ppt__slides{display:flex;flex-direction:column;align-items:center;gap:20px}.omni-ppt__slide{position:relative;overflow:hidden;border-radius:6px;box-shadow:0 2px 12px #0008;transform-origin:top center;flex:none}.omni-ppt__element{position:absolute;box-sizing:border-box;overflow:hidden}.omni-ppt__text{padding:2px;white-space:pre-wrap;word-break:break-word}.omni-ppt__text p{margin:0 0 4px}.omni-ppt__text[data-text-preset="cover-title"] .omni-ppt__text-line,.omni-ppt__text[data-text-preset="cover-subtitle"] .omni-ppt__text-line{margin-bottom:0}.omni-ppt__image{width:100%;height:100%;object-fit:cover;object-position:center}.omni-ppt__table-frame{background:#fff;border:1px solid #bdbdbd;overflow:auto}.omni-ppt__table{border-collapse:collapse;width:100%;font-size:12px;color:#111}.omni-ppt__table th,.omni-ppt__table td{border:1px solid #d3d3d3;padding:4px 6px;vertical-align:top}.omni-ppt__chart{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;background:#f7f7f7;border:1px dashed #9a9a9a;color:#333}.omni-ppt__chart--rendered{background:transparent;border:0}.omni-ppt__chart-title{font-size:14px;font-weight:700;margin-bottom:4px}.omni-ppt__chart-subtitle{font-size:12px;opacity:.8}.omni-ppt__shape{width:100%;height:100%}.omni-ppt__empty{padding:32px;text-align:center}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-viewer-core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared parsing + rendering core for Omni Viewer (vscode / chrome / obsidian / web)",
5
5
  "type": "module",
6
6
  "license": "MIT",