omni-viewer-core 0.1.0 → 0.2.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.
Files changed (40) hide show
  1. package/dist/i18n/catalog.de.d.ts +1 -0
  2. package/dist/i18n/catalog.de.js +68 -0
  3. package/dist/i18n/catalog.en.js +3 -0
  4. package/dist/i18n/catalog.fr.d.ts +1 -0
  5. package/dist/i18n/catalog.fr.js +68 -0
  6. package/dist/i18n/catalog.it.d.ts +1 -0
  7. package/dist/i18n/catalog.it.js +68 -0
  8. package/dist/i18n/catalog.ja.d.ts +1 -0
  9. package/dist/i18n/catalog.ja.js +288 -0
  10. package/dist/i18n/catalog.ko.js +372 -26
  11. package/dist/i18n/catalog.th.d.ts +1 -0
  12. package/dist/i18n/catalog.th.js +68 -0
  13. package/dist/i18n/catalog.zh-cn.d.ts +1 -0
  14. package/dist/i18n/catalog.zh-cn.js +265 -0
  15. package/dist/i18n/catalog.zh-tw.d.ts +1 -0
  16. package/dist/i18n/catalog.zh-tw.js +52 -0
  17. package/dist/i18n/index.d.ts +7 -0
  18. package/dist/i18n/index.js +35 -2
  19. package/dist/parsers/archive/index.d.ts +16 -2
  20. package/dist/parsers/archive/index.js +39 -11
  21. package/dist/parsers/archive/model.d.ts +20 -0
  22. package/dist/parsers/audio/index.d.ts +3 -0
  23. package/dist/parsers/audio/index.js +5 -0
  24. package/dist/parsers/video/index.d.ts +3 -0
  25. package/dist/parsers/video/index.js +3 -0
  26. package/dist/styles/archive.css +1 -0
  27. package/dist/styles/audio.css +8 -0
  28. package/dist/styles/ppt.css +1 -1
  29. package/dist/viewers/archive/index.d.ts +17 -1
  30. package/dist/viewers/archive/index.js +124 -21
  31. package/dist/viewers/archive/styles.d.ts +1 -1
  32. package/dist/viewers/archive/styles.js +1 -0
  33. package/dist/viewers/audio/styles.js +8 -0
  34. package/dist/viewers/ppt/chart.js +11 -4
  35. package/dist/viewers/ppt/render.js +30 -3
  36. package/dist/viewers/ppt/styles.d.ts +1 -1
  37. package/dist/viewers/ppt/styles.js +1 -1
  38. package/package.json +2 -2
  39. package/dist/viewers/hwp/self-loading.d.ts +0 -2
  40. package/dist/viewers/hwp/self-loading.js +0 -6
@@ -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}.omni-ppt__text p{margin:0}.omni-ppt__image{width:100%;height:100%;object-fit:contain}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table td{border:1px solid #777;padding:3px}.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;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,5 +1,5 @@
1
1
  import type { FileSaveService, HostContext } from '../../host/index.js';
2
- import { type ArchiveDecoder } from '../../parsers/archive/index.js';
2
+ import { type ArchiveDecoder, type ArchiveEntrySaver, type ArchiveStreamDecoder } from '../../parsers/archive/index.js';
3
3
  import type { ResourceLimits } from '../../parsers/types.js';
4
4
  import { type MountOptions, type ViewerHandle, type ViewerInput } from '../types.js';
5
5
  export * from '../../parsers/archive/index.js';
@@ -17,9 +17,25 @@ export declare const ARCHIVE_VIEWER_META: {
17
17
  export interface ArchiveViewerDeps extends ArchiveDecoder {
18
18
  }
19
19
  export type ArchiveViewerContext = HostContext & {
20
+ /** Buffered save: core extracts the entry and hands over its bytes. */
20
21
  save?: FileSaveService;
22
+ /** Streaming save: the adapter pipes the entry to disk itself, so a
23
+ * multi-GB entry is never materialized in memory. Preferred over `save`
24
+ * when present. */
25
+ saveEntry?: ArchiveEntrySaver;
21
26
  };
22
27
  export interface ArchiveMountOptions extends MountOptions {
23
28
  limits?: ResourceLimits;
29
+ createObjectUrl?(blob: Blob): string;
30
+ revokeObjectUrl?(url: string): void;
31
+ }
32
+ /** Lazy streaming input: the adapter's path-based decoder opens the archive
33
+ * without core ever holding the full file bytes (opt-in, opposite the buffered
34
+ * {@link ViewerInput} path used by vscode/chrome/web). */
35
+ export interface ArchiveStreamingSource extends ArchiveStreamDecoder {
36
+ fileName: string;
37
+ totalSize?: number;
38
+ lastModified?: number;
24
39
  }
25
40
  export declare function mountArchiveViewer(input: ViewerInput, container: HTMLElement, ctx: ArchiveViewerContext, deps: ArchiveViewerDeps, options?: ArchiveMountOptions): Promise<ViewerHandle>;
41
+ export declare function mountArchiveViewer(source: ArchiveStreamingSource, container: HTMLElement, ctx: ArchiveViewerContext, options?: ArchiveMountOptions): Promise<ViewerHandle>;
@@ -1,4 +1,7 @@
1
- import { ARCHIVE_DEFAULT_LIMITS, ArchiveError, parseArchive } from '../../parsers/archive/index.js';
1
+ import { ARCHIVE_DEFAULT_LIMITS, ArchiveError, openArchiveStream, parseArchive } from '../../parsers/archive/index.js';
2
+ import { audioMimeType } from '../../parsers/audio/index.js';
3
+ import { videoMimeType } from '../../parsers/video/index.js';
4
+ import { detectImageMime } from '../image/decode.js';
2
5
  import { MountAbortedError, VIEWER_ROOT_CLASS } from '../types.js';
3
6
  import { createArchiveController } from './controller.js';
4
7
  import { tryDecodeArchiveEntryPreview } from './archive-preview-decoder.js';
@@ -8,6 +11,17 @@ export * from '../../parsers/archive/index.js';
8
11
  export { createArchiveController } from './controller.js';
9
12
  export { archiveViewerCss } from './styles.js';
10
13
  export const ARCHIVE_VIEWER_META = { id: 'archive', displayNameKey: 'archive.title', extensions: ['zip', 'jar', 'apk', 'tar', 'tgz', 'tar.gz', 'tbz', 'tbz2', 'tar.bz2', 'txz', 'tar.xz', 'gz', 'bz2', 'xz', '7z', 'rar', 'dmg'], priority: 5, requiredServices: [], optionalServices: ['save'], inputOwnership: 'consumes' };
14
+ /** Ceiling for extracting an audio entry into memory for inline playback.
15
+ * Larger than the text/hex preview budget, but bounded to avoid huge allocations. */
16
+ const ARCHIVE_AUDIO_PREVIEW_BYTES = 64 * 1024 * 1024;
17
+ /** Ceiling for extracting an image entry into memory for inline preview. */
18
+ const ARCHIVE_IMAGE_PREVIEW_BYTES = 32 * 1024 * 1024;
19
+ /** Ceiling for extracting a video entry into memory for inline playback. The
20
+ * whole entry is buffered into a blob URL, so this is the largest of the caps. */
21
+ const ARCHIVE_VIDEO_PREVIEW_BYTES = 128 * 1024 * 1024;
22
+ const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']);
23
+ const hasImageExtension = (path) => IMAGE_EXTENSIONS.has(path.toLowerCase().split('.').pop() ?? '');
24
+ const blobPart = (data) => (data.buffer instanceof ArrayBuffer ? data : new Uint8Array(data));
11
25
  const element = (tag, className, text) => {
12
26
  const node = document.createElement(tag);
13
27
  if (className)
@@ -39,10 +53,26 @@ const archiveFormat = (name) => {
39
53
  const lower = name.toLocaleLowerCase();
40
54
  return ['tar.gz', 'tar.bz2', 'tar.xz'].find(ext => lower.endsWith(`.${ext}`))?.toUpperCase() ?? (lower.split('.').pop()?.toUpperCase() || 'ARCHIVE');
41
55
  };
42
- export async function mountArchiveViewer(input, container, ctx, deps, options = {}) {
56
+ export async function mountArchiveViewer(inputOrSource, container, ctx, depsOrOptions, maybeOptions) {
57
+ const streaming = !('data' in inputOrSource);
58
+ const options = (streaming ? depsOrOptions : maybeOptions) ?? {};
43
59
  if (options.signal?.aborted)
44
60
  throw new MountAbortedError();
45
- const parsed = await parseArchive(input.data, deps, { fileName: input.fileName, ...(options.signal ? { signal: options.signal } : {}), ...(options.limits ? { limits: options.limits } : {}) });
61
+ let fileName;
62
+ let totalBytes;
63
+ let parsed;
64
+ if (streaming) {
65
+ const source = inputOrSource;
66
+ fileName = source.fileName;
67
+ totalBytes = source.totalSize;
68
+ parsed = await openArchiveStream(source, { fileName, ...(source.totalSize !== undefined ? { totalSize: source.totalSize } : {}), ...(options.signal ? { signal: options.signal } : {}), ...(options.limits ? { limits: options.limits } : {}) });
69
+ }
70
+ else {
71
+ const input = inputOrSource;
72
+ fileName = input.fileName;
73
+ totalBytes = input.data.byteLength;
74
+ parsed = await parseArchive(input.data, depsOrOptions, { fileName, ...(options.signal ? { signal: options.signal } : {}), ...(options.limits ? { limits: options.limits } : {}) });
75
+ }
46
76
  if (parsed.outcome.result.status === 'failed')
47
77
  throw new Error(parsed.outcome.result.failure.messageKey);
48
78
  if (options.signal?.aborted) {
@@ -64,9 +94,9 @@ export async function mountArchiveViewer(input, container, ctx, deps, options =
64
94
  const wrap = element('section', `${VIEWER_ROOT_CLASS} omni-viewer--archive`);
65
95
  const hero = element('header', 'omni-archive__hero');
66
96
  const heading = element('div');
67
- heading.append(element('div', 'omni-archive__eyebrow', t('archive.preview')), element('h1', '', input.fileName), element('p', 'omni-archive__subtitle', t('archive.summary')));
97
+ heading.append(element('div', 'omni-archive__eyebrow', t('archive.preview')), element('h1', '', fileName), element('p', 'omni-archive__subtitle', t('archive.summary')));
68
98
  const pills = element('div', 'omni-archive__pills');
69
- pills.append(element('span', 'omni-archive__pill', archiveFormat(input.fileName)), element('span', 'omni-archive__pill', t('archive.entries', { count: entries.length })), element('span', 'omni-archive__pill', formatSize(input.data.byteLength)));
99
+ pills.append(element('span', 'omni-archive__pill', archiveFormat(fileName)), element('span', 'omni-archive__pill', t('archive.entries', { count: entries.length })), element('span', 'omni-archive__pill', formatSize(totalBytes)));
70
100
  hero.append(heading, pills);
71
101
  const controls = element('section', 'omni-archive__controls');
72
102
  const label = element('label', 'omni-archive__search-label', t('archive.filter'));
@@ -107,11 +137,13 @@ export async function mountArchiveViewer(input, container, ctx, deps, options =
107
137
  const previewMeta = element('p', 'omni-archive__preview-meta', t('archive.selectHint'));
108
138
  const preview = element('pre', 'omni-archive__preview', t('archive.noSelection'));
109
139
  preview.setAttribute('aria-live', 'polite');
140
+ const previewMedia = element('div', 'omni-archive__preview-media');
141
+ previewMedia.style.display = 'none';
110
142
  const saveButton = element('button', 'omni-archive__save', t('archive.saveEntry'));
111
143
  saveButton.type = 'button';
112
- saveButton.hidden = !ctx.save;
144
+ saveButton.hidden = !ctx.save && !ctx.saveEntry;
113
145
  saveButton.disabled = true;
114
- previewPanel.append(previewHeader, previewMeta, preview, saveButton);
146
+ previewPanel.append(previewHeader, previewMeta, preview, previewMedia, saveButton);
115
147
  workspace.append(tableWrap, previewPanel);
116
148
  wrap.append(hero, controls, stats, workspace);
117
149
  root.append(wrap);
@@ -119,7 +151,52 @@ export async function mountArchiveViewer(input, container, ctx, deps, options =
119
151
  let extraction;
120
152
  let saveExtraction;
121
153
  let selectedEntry;
122
- const setPreview = (entry, status, meta, content) => { previewTitle.textContent = entry?.path ?? t('archive.chooseFile'); previewStatus.textContent = status; previewMeta.textContent = meta; preview.textContent = content; };
154
+ let previewUrl;
155
+ const createUrl = options.createObjectUrl ?? URL.createObjectURL.bind(URL);
156
+ const revokeUrl = options.revokeObjectUrl ?? URL.revokeObjectURL.bind(URL);
157
+ // Inline display beats author CSS (.omni-archive__preview-media sets display:flex,
158
+ // which would otherwise override the [hidden] attribute), so the text preview and
159
+ // the media preview stay reliably mutually exclusive.
160
+ const clearPreviewMedia = () => { if (previewUrl) {
161
+ revokeUrl(previewUrl);
162
+ previewUrl = undefined;
163
+ } previewMedia.replaceChildren(); previewMedia.style.display = 'none'; preview.style.display = ''; };
164
+ const setPreview = (entry, status, meta, content) => { clearPreviewMedia(); previewTitle.textContent = entry?.path ?? t('archive.chooseFile'); previewStatus.textContent = status; previewMeta.textContent = meta; preview.textContent = content; };
165
+ const MEDIA_META = { audio: 'archive.audioReady', image: 'archive.imageReady', video: 'archive.videoReady' };
166
+ const showMediaPreview = (entry, data, kind, mimeType) => {
167
+ clearPreviewMedia();
168
+ let objectUrl;
169
+ try {
170
+ objectUrl = createUrl(new Blob([blobPart(data)], { type: mimeType }));
171
+ }
172
+ catch {
173
+ setPreview(entry, t('archive.previewUnavailable'), t('archive.mediaUnsupported'), t('archive.previewUnavailable'));
174
+ return;
175
+ }
176
+ previewUrl = objectUrl;
177
+ let node;
178
+ if (kind === 'image') {
179
+ const img = document.createElement('img');
180
+ img.className = 'omni-archive__image';
181
+ img.alt = entry.path;
182
+ img.src = objectUrl;
183
+ node = img;
184
+ }
185
+ else {
186
+ const media = document.createElement(kind);
187
+ media.controls = true;
188
+ media.preload = 'metadata';
189
+ media.className = `omni-archive__${kind}`;
190
+ media.src = objectUrl;
191
+ node = media;
192
+ }
193
+ previewMedia.replaceChildren(node);
194
+ previewMedia.style.display = 'flex';
195
+ preview.style.display = 'none';
196
+ previewTitle.textContent = entry.path;
197
+ previewStatus.textContent = t('archive.ready');
198
+ previewMeta.textContent = t(MEDIA_META[kind]);
199
+ };
123
200
  const select = async (entry) => {
124
201
  extraction?.abort();
125
202
  extraction = undefined;
@@ -138,7 +215,10 @@ export async function mountArchiveViewer(input, container, ctx, deps, options =
138
215
  extraction = new AbortController();
139
216
  const request = extraction;
140
217
  setPreview(entry, t('archive.loading'), t('archive.loadingHint'), t('archive.loadingContent'));
141
- const maxPreview = ARCHIVE_DEFAULT_LIMITS.maxPreviewBytes;
218
+ const audioMime = audioMimeType(entry.path);
219
+ const videoMime = !audioMime ? videoMimeType(entry.path) : undefined;
220
+ const imageCandidate = !audioMime && !videoMime && hasImageExtension(entry.path);
221
+ const maxPreview = audioMime ? ARCHIVE_AUDIO_PREVIEW_BYTES : videoMime ? ARCHIVE_VIDEO_PREVIEW_BYTES : imageCandidate ? ARCHIVE_IMAGE_PREVIEW_BYTES : ARCHIVE_DEFAULT_LIMITS.maxPreviewBytes;
142
222
  if (entry.uncompressedSize !== undefined && entry.uncompressedSize > maxPreview) {
143
223
  setPreview(entry, t('archive.previewUnavailable'), t('diag.archive.limit-exceeded'), t('archive.previewUnavailable'));
144
224
  return;
@@ -147,6 +227,21 @@ export async function mountArchiveViewer(input, container, ctx, deps, options =
147
227
  const data = await handle.extract(entry.entryId, { signal: request.signal, maxBytes: maxPreview });
148
228
  if (local !== ticket)
149
229
  return;
230
+ if (audioMime) {
231
+ showMediaPreview(entry, data, 'audio', audioMime);
232
+ return;
233
+ }
234
+ if (videoMime) {
235
+ showMediaPreview(entry, data, 'video', videoMime);
236
+ return;
237
+ }
238
+ if (imageCandidate) {
239
+ const imageMime = detectImageMime(data, entry.path);
240
+ if (imageMime) {
241
+ showMediaPreview(entry, data, 'image', imageMime);
242
+ return;
243
+ }
244
+ }
150
245
  const decoded = tryDecodeArchiveEntryPreview(entry.path, data);
151
246
  const text = isText(data);
152
247
  setPreview(entry, t('archive.ready'), t(decoded ? 'archive.androidBinaryXmlReady' : text ? 'archive.textReady' : 'archive.binaryReady'), decoded?.content ?? (text ? new TextDecoder().decode(data) : hexPreview(data)));
@@ -158,24 +253,32 @@ export async function mountArchiveViewer(input, container, ctx, deps, options =
158
253
  };
159
254
  saveButton.addEventListener('click', async () => {
160
255
  const entry = selectedEntry;
161
- const save = ctx.save;
162
- if (!entry || entry.isDirectory || !save || !handle)
256
+ if (!entry || entry.isDirectory || !handle || (!ctx.save && !ctx.saveEntry))
163
257
  return;
164
258
  saveExtraction?.abort();
165
259
  saveExtraction = new AbortController();
166
260
  const request = saveExtraction;
167
261
  saveButton.disabled = true;
168
262
  saveButton.textContent = t('archive.savingEntry');
263
+ const suggestedName = entry.path.split('/').filter(Boolean).pop() ?? 'archive-entry';
169
264
  try {
170
- const configuredLimit = options.limits?.maxDecompressedBytes ?? ARCHIVE_DEFAULT_LIMITS.maxDecompressedBytes;
171
- const maxBytes = Math.max(1, Math.min(entry.uncompressedSize ?? configuredLimit, configuredLimit));
172
- const data = await handle.extract(entry.entryId, { signal: request.signal, maxBytes });
173
- if (request.signal.aborted)
174
- return;
175
- const suggestedName = entry.path.split('/').filter(Boolean).pop() ?? 'archive-entry';
176
- await save.saveFile(suggestedName, data, entry.mimeType ?? 'application/octet-stream');
177
- if (!request.signal.aborted)
178
- previewMeta.textContent = t('common.saved', { name: suggestedName });
265
+ // Streaming save (adapter pipes the entry to disk) is preferred: a
266
+ // multi-GB entry never lands in memory. Buffered save is the fallback.
267
+ if (ctx.saveEntry) {
268
+ const savedName = await ctx.saveEntry.saveEntry(entry, { signal: request.signal });
269
+ if (!request.signal.aborted && savedName)
270
+ previewMeta.textContent = t('common.saved', { name: savedName });
271
+ }
272
+ else {
273
+ const configuredLimit = options.limits?.maxDecompressedBytes ?? ARCHIVE_DEFAULT_LIMITS.maxDecompressedBytes;
274
+ const maxBytes = Math.max(1, Math.min(entry.uncompressedSize ?? configuredLimit, configuredLimit));
275
+ const data = await handle.extract(entry.entryId, { signal: request.signal, maxBytes });
276
+ if (request.signal.aborted)
277
+ return;
278
+ await ctx.save.saveFile(suggestedName, data, entry.mimeType ?? 'application/octet-stream');
279
+ if (!request.signal.aborted)
280
+ previewMeta.textContent = t('common.saved', { name: suggestedName });
281
+ }
179
282
  }
180
283
  catch {
181
284
  if (!request.signal.aborted)
@@ -226,5 +329,5 @@ export async function mountArchiveViewer(input, container, ctx, deps, options =
226
329
  search.addEventListener('input', () => { tableWrap.scrollTop = 0; controller.dispatch({ type: 'set-search', query: search.value }); });
227
330
  const unsubscribe = controller.subscribe(render);
228
331
  render();
229
- return { dispose() { ticket++; extraction?.abort(); saveExtraction?.abort(); unsubscribe(); wrap.remove(); void handle?.close(); handle = undefined; } };
332
+ return { dispose() { ticket++; extraction?.abort(); saveExtraction?.abort(); clearPreviewMedia(); unsubscribe(); wrap.remove(); void handle?.close(); handle = undefined; } };
230
333
  }
@@ -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@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}.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";
@@ -7,5 +7,6 @@ export const archiveViewerCss = `
7
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)}
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
+ .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}
10
11
  @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}}
11
12
  `;
@@ -61,6 +61,14 @@ export const audioViewerCss = mediaViewerCss + `
61
61
  .omni-audio__spectrogram { min-height: 0; }
62
62
  .omni-audio__spectrogram--active { min-height: 200px; }
63
63
 
64
+ /* Keep the WaveSurfer playhead (cursor + played-region overlay) pinned to the
65
+ waveform strip. Some wavesurfer spectrogram builds render the spectrogram
66
+ inside the main wrapper instead of our dedicated container; without this the
67
+ full-height playhead sweeps down across the spectrogram ("progress bar"
68
+ running along the bottom) rather than staying with the waveform at the top. */
69
+ .omni-audio__waveform ::part(cursor),
70
+ .omni-audio__waveform ::part(progress) { height: 128px !important; }
71
+
64
72
  .omni-audio__status { color: var(--omni-muted, #9d9d9d); font-size: 12px; min-height: 16px; }
65
73
 
66
74
  .omni-audio__loading { display: flex; justify-content: center; align-items: center; min-height: 120px; color: var(--omni-muted, #9d9d9d); }
@@ -1,4 +1,10 @@
1
1
  const finite = (value) => typeof value === 'number' && Number.isFinite(value);
2
+ function categoryLabel(ctx, name, x, h) { const parts = String(name).split(' '); if (parts.length > 1) {
3
+ ctx.fillText(parts[0] ?? '', x, h - 30);
4
+ ctx.fillText(parts.slice(1).join(' '), x, h - 14);
5
+ }
6
+ else
7
+ ctx.fillText(name, x, h - 16); }
2
8
  export function computeChartBounds(data) { const explicitMin = data.valueAxis?.min, explicitMax = data.valueAxis?.max; if (data.kind === 'stackedColumn') {
3
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));
4
10
  const min = finite(explicitMin) ? explicitMin : Math.min(0, ...negative), max = finite(explicitMax) ? explicitMax : Math.max(1, ...positive);
@@ -57,8 +63,8 @@ function stacked(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, boun
57
63
  ctx.fillStyle = series.color || '#d10000';
58
64
  ctx.fillRect(cx - bar / 2, top, bar, height);
59
65
  label(ctx, series, value, cx, top + height / 2, height, a.format);
60
- } ctx.fillStyle = category.color ?? '#222222'; ctx.font = `400 ${Math.max(10, category.fontSizePx ?? 11)}px Arial, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText(name, cx, h - 16); }); legend(ctx, w, h, data, false); }
61
- function line(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, bounds), x = (i) => data.categories.length === 1 ? a.left + a.pw / 2 : a.left + a.pw * i / Math.max(1, data.categories.length - 1), category = data.categoryAxis ?? {}; data.categories.forEach((name, i) => { ctx.fillStyle = category.color ?? '#222222'; ctx.font = `400 ${Math.max(10, category.fontSizePx ?? 11)}px Arial, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText(name, x(i), h - 16); }); data.series.forEach((series, seriesIndex) => { const points = series.values.map((value, i) => ({ value, i })).filter(point => finite(point.value)); if (!points.length)
66
+ } ctx.fillStyle = category.color ?? '#222222'; ctx.font = `400 ${Math.max(10, category.fontSizePx ?? 11)}px Arial, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; categoryLabel(ctx, name, cx, h); }); legend(ctx, w, h, data, false); }
67
+ function line(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, bounds), x = (i) => data.categories.length === 1 ? a.left + a.pw / 2 : a.left + a.pw * i / Math.max(1, data.categories.length - 1), category = data.categoryAxis ?? {}; data.categories.forEach((name, i) => { ctx.fillStyle = category.color ?? '#222222'; ctx.font = `400 ${Math.max(10, category.fontSizePx ?? 11)}px Arial, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; categoryLabel(ctx, name, x(i), h); }); data.series.forEach((series, seriesIndex) => { const points = series.values.map((value, i) => ({ value, i })).filter(point => finite(point.value)); if (!points.length)
62
68
  return; ctx.strokeStyle = series.color || '#4472c4'; ctx.lineWidth = 3; ctx.beginPath(); points.forEach((point, i) => i ? ctx.lineTo(x(point.i), a.y(point.value)) : ctx.moveTo(x(point.i), a.y(point.value))); ctx.stroke(); for (const point of points) {
63
69
  const px = x(point.i), py = a.y(point.value);
64
70
  ctx.fillStyle = '#fff';
@@ -69,11 +75,12 @@ function line(ctx, w, h, data, bounds) { const a = axes(ctx, w, h, data, bounds)
69
75
  ctx.stroke();
70
76
  const style = series.dataLabel ?? {};
71
77
  if (style.showValue !== false) {
78
+ const near = py < a.top + 20;
72
79
  ctx.fillStyle = style.color ?? series.color ?? '#222';
73
80
  ctx.font = `600 ${Math.max(9, style.fontSizePx ?? 11)}px Arial, sans-serif`;
74
81
  ctx.textAlign = 'center';
75
- ctx.textBaseline = 'bottom';
76
- ctx.fillText(formatChartValue(point.value, style.numFmt ?? series.valueFormat ?? a.format), px, py - 10 - seriesIndex * 2);
82
+ ctx.textBaseline = near ? 'top' : 'bottom';
83
+ ctx.fillText(formatChartValue(point.value, style.numFmt ?? series.valueFormat ?? a.format), px, near ? py + 18 + seriesIndex * 2 : py - 10 - seriesIndex * 2);
77
84
  }
78
85
  } }); legend(ctx, w, h, data, true); }
79
86
  export function drawChart(canvas, data) { const ctx = canvas.getContext('2d'); if (!ctx || !data.categories.length || !data.series.length)
@@ -28,7 +28,7 @@ function textElement(e, fallbackColor) { const box = frame(e, 'text'); if (e.tex
28
28
  if (p.bullet || (likelyList && p.text.trim())) {
29
29
  const bullet = node('span', 'omni-ppt__bullet');
30
30
  bullet.textContent = '•';
31
- bullet.style.color = p.color ?? fallbackColor;
31
+ Object.assign(bullet.style, { display: 'inline-block', width: '26px', marginRight: '6px', textAlign: 'center', color: p.color ?? fallbackColor });
32
32
  line.append(bullet);
33
33
  }
34
34
  if (p.runs?.length) {
@@ -48,12 +48,37 @@ function textElement(e, fallbackColor) { const box = frame(e, 'text'); if (e.tex
48
48
  else
49
49
  line.append(document.createTextNode(p.text));
50
50
  box.append(line);
51
- } return box; }
51
+ } requestAnimationFrame(() => { if (box.scrollHeight > box.clientHeight + 2) {
52
+ const ratio = box.clientHeight / box.scrollHeight;
53
+ box.querySelectorAll('p.omni-ppt__text-line').forEach(line => { const cur = parseFloat(line.style.fontSize) || 24, next = Math.max(9, Math.floor(cur * ratio)); line.style.fontSize = `${next}px`; line.style.lineHeight = `${Math.max(12, Math.round(next * 1.18))}px`; });
54
+ } }); return box; }
55
+ function normalizeVectorFallbackLogo(img) { const w = img.naturalWidth || img.width, h = img.naturalHeight || img.height; if (!w || !h)
56
+ return null; const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); if (!ctx)
57
+ return null; ctx.drawImage(img, 0, 0, w, h); const image = ctx.getImageData(0, 0, w, h), data = image.data, target = { r: 237, g: 30, b: 68 }; for (let i = 0; i < data.length; i += 4) {
58
+ const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3];
59
+ if (a === 0)
60
+ continue;
61
+ if (r > 130 && r > g * 1.25 && r > b * 1.25) {
62
+ data[i + 3] = 0;
63
+ continue;
64
+ }
65
+ const alphaRatio = Math.max(0, Math.min(1, (r + g + b) / 3 / 255));
66
+ data[i] = target.r;
67
+ data[i + 1] = target.g;
68
+ data[i + 2] = target.b;
69
+ data[i + 3] = Math.round(a * alphaRatio);
70
+ } ctx.putImageData(image, 0, 0); return canvas.toDataURL('image/png'); }
52
71
  function imageElement(e) { const box = frame(e, 'image'), img = node('img', 'omni-ppt__image'); img.src = e.src ?? ''; img.alt = ''; const crop = e.srcRect; if (crop && (crop.l || crop.t || crop.r || crop.b)) {
53
72
  const visibleW = Math.max(1, 100 - crop.l - crop.r), visibleH = Math.max(1, 100 - crop.t - crop.b), scaleW = 100 / visibleW, scaleH = 100 / visibleH;
54
73
  Object.assign(img.style, { position: 'absolute', width: `${100 * scaleW}%`, height: `${100 * scaleH}%`, left: `${-crop.l * scaleW}%`, top: `${-crop.t * scaleH}%`, objectFit: 'fill', maxWidth: 'none', maxHeight: 'none' });
55
74
  box.style.overflow = 'hidden';
56
- } box.append(img); return box; }
75
+ } if (e.vectorFallback)
76
+ img.addEventListener('load', () => { try {
77
+ const recolored = normalizeVectorFallbackLogo(img);
78
+ if (recolored)
79
+ img.src = recolored;
80
+ }
81
+ catch { /* keep the original raster when recoloring fails */ } }, { once: true }); box.append(img); return box; }
57
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; }
58
83
  function chartElement(e) { const box = frame(e, 'chart'); if (e.chartData) {
59
84
  const canvas = node('canvas');
@@ -115,6 +140,8 @@ function shapeElement(e) { const box = frame(e, 'shape'), w = Math.max(1, Math.a
115
140
  line.setAttribute('marker-start', start);
116
141
  if (end)
117
142
  line.setAttribute('marker-end', end);
143
+ if (!e.headEnd && !e.tailEnd && w + h > 20)
144
+ line.setAttribute('marker-end', `url(#arrow-${suffix})`);
118
145
  svg.append(line);
119
146
  box.style.overflow = 'visible';
120
147
  box.append(svg);
@@ -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}.omni-ppt__text p{margin:0}.omni-ppt__image{width:100%;height:100%;object-fit:contain}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table td{border:1px solid #777;padding:3px}.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;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 +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}.omni-ppt__text p{margin:0}.omni-ppt__image{width:100%;height:100%;object-fit:contain}.omni-ppt__table{border-collapse:collapse;width:100%;height:100%;color:#111}.omni-ppt__table td{border:1px solid #777;padding:3px}.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;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}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-viewer-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared parsing + rendering core for Omni Viewer (vscode / chrome / obsidian / web)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -314,7 +314,7 @@
314
314
  "pdf-lib": "^1.17.1",
315
315
  "pdfjs-dist": "^4.10.38",
316
316
  "proj4": "^2.20.9",
317
- "puml-canvas-js": "0.5.0",
317
+ "puml-canvas-js": ">=0.2.0",
318
318
  "wavesurfer.js": "^7.8.0",
319
319
  "xlsx": ">=0.20.2",
320
320
  "yaml": "2.8.3"
@@ -1,2 +0,0 @@
1
- import type { HwpViewerDeps } from './index.js';
2
- export declare function loadHwpViewerDeps(): Promise<HwpViewerDeps>;
@@ -1,6 +0,0 @@
1
- export async function loadHwpViewerDeps() {
2
- const imported = await import('@rhwp/core');
3
- if (typeof imported.default === 'function')
4
- await imported.default();
5
- return { loadRhwp: async () => imported };
6
- }