visio-solid-viewer 0.7.4 → 1.0.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.
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import { mountViewer, propertyKeys } from "ooxml-ui/visio";
2
- import { className, effect, setAttribute, style, template, use } from "solid-js/web";
3
- import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
1
+ import * as ui from "ooxml-ui/visio";
2
+ import { ViewerController, createWorkerParser, mountViewer, propertyKeys } from "ooxml-ui/visio";
3
+ import { className, createComponent, delegateEvents, effect, insert, setAttribute, style, template, use } from "solid-js/web";
4
+ import { For, Index, Match, Switch, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";
5
+ import * as officeTheme from "ooxml-ui/theme";
4
6
  export * from "ooxml-ui/visio";
5
7
  export * from "visio-core";
6
8
  //#region packages/bindings/src/common.ts
@@ -145,7 +147,7 @@ function viewerStateSource(current) {
145
147
  }
146
148
  //#endregion
147
149
  //#region packages/bindings/src/solid.tsx
148
- var _tmpl$ = /*#__PURE__*/ template(`<div>`);
150
+ var _tmpl$$1 = /*#__PURE__*/ template(`<div>`);
149
151
  /** Native Solid host and ownership; all document work remains in the shared binding. */
150
152
  function VisioViewer(props) {
151
153
  let host;
@@ -181,7 +183,7 @@ function VisioViewer(props) {
181
183
  }
182
184
  });
183
185
  return (() => {
184
- var _el$ = _tmpl$();
186
+ var _el$ = _tmpl$$1();
185
187
  var _ref$ = host;
186
188
  typeof _ref$ === "function" ? use(_ref$, _el$) : host = _el$;
187
189
  effect((_p$) => {
@@ -213,4 +215,369 @@ function createVisioViewerState(handle) {
213
215
  return state;
214
216
  }
215
217
  //#endregion
216
- export { VisioViewer, createVisioViewerState };
218
+ //#region packages/bindings/src/building-blocks.ts
219
+ function buildingBlock(key) {
220
+ const value = Reflect.get(ui, key);
221
+ if (typeof value !== "function") throw new Error("Update ooxml-ui to use Visio building blocks.");
222
+ return value;
223
+ }
224
+ /** The mode a host sees: 'edit' only when permission is granted and the file can be saved back. */
225
+ function effectiveMode(state) {
226
+ return buildingBlock("diagramMode")(state);
227
+ }
228
+ /** Framework-neutral ownership only; each adapter supplies its own reactive state. */
229
+ function createBuildingBlockSession(initial = {}) {
230
+ const controller = new ViewerController(createWorkerParser());
231
+ controller.canEdit = initial.canEdit === true;
232
+ let options = initial;
233
+ let source;
234
+ let host = null;
235
+ let disposeCanvas;
236
+ let destroyed = false;
237
+ const stopEvents = controller.onEvent((name, detail) => {
238
+ if (name === "document-change") {
239
+ const change = detail;
240
+ options.onDocumentChange?.(change.document);
241
+ options.onDirtyChange?.(change.dirty);
242
+ } else if (name === "document-error") options.onDocumentError?.(detail);
243
+ });
244
+ const fit = () => {
245
+ if (host) buildingBlock("fitDiagramCanvas")(host, controller);
246
+ };
247
+ return {
248
+ controller,
249
+ mount(node) {
250
+ if (destroyed || node === host) return;
251
+ disposeCanvas?.();
252
+ disposeCanvas = void 0;
253
+ host = node;
254
+ if (node) disposeCanvas = buildingBlock("mountDiagramCanvas")(node, controller);
255
+ },
256
+ /** Flat control props; pass the framework's latest options so callbacks stay current. */
257
+ toolbar(latest = options) {
258
+ return buildingBlock("diagramToolbarProps")(controller, fit, latest.locale, {
259
+ onOpenFile: latest.onOpenFile,
260
+ onOpenPageSetup: latest.onOpenPageSetup,
261
+ onOpenSettings: latest.onOpenSettings,
262
+ onOpenShareDialog: latest.onOpenShareDialog
263
+ });
264
+ },
265
+ /** Acknowledge a host save: clears dirty and emits the dirty change. */
266
+ markClean() {
267
+ controller.markClean();
268
+ },
269
+ update(next) {
270
+ if (destroyed) return;
271
+ options = next;
272
+ controller.canEdit = next.canEdit === true;
273
+ if (source === next.content) return;
274
+ source = next.content;
275
+ controller.cancelLoad();
276
+ if (!source) return;
277
+ const input = source;
278
+ controller.loadSource(async () => {
279
+ return input instanceof Blob ? new Uint8Array(await input.arrayBuffer()) : input;
280
+ }).catch(() => {});
281
+ },
282
+ destroy() {
283
+ if (destroyed) return;
284
+ destroyed = true;
285
+ disposeCanvas?.();
286
+ disposeCanvas = void 0;
287
+ host = null;
288
+ stopEvents();
289
+ controller.destroy();
290
+ }
291
+ };
292
+ }
293
+ /** Resolve the new optional-control theme API only when that control is mounted. */
294
+ function prepareDiagramBlock(host, kind) {
295
+ if (!host) return;
296
+ const prepare = Reflect.get(officeTheme, "prepareOfficeUiBlock");
297
+ if (typeof prepare !== "function") throw new Error("Update ooxml-ui to theme Visio building blocks.");
298
+ prepare(host, kind, "visio");
299
+ }
300
+ //#endregion
301
+ //#region packages/bindings/src/toolbar-controls.ts
302
+ var TOOLBAR_GROUPS = [
303
+ "file",
304
+ "history",
305
+ "zoom",
306
+ "selection",
307
+ "clipboard",
308
+ "tools",
309
+ "search"
310
+ ];
311
+ var fire = (action) => () => {
312
+ Promise.resolve().then(action).catch(() => {});
313
+ };
314
+ var button = (id, label, enabled, action, pressed) => ({
315
+ kind: "button",
316
+ id,
317
+ label,
318
+ disabled: !enabled,
319
+ ...pressed === void 0 ? {} : { pressed },
320
+ run: fire(action)
321
+ });
322
+ /**
323
+ * What the library Toolbar shows, as plain data every framework maps onto its own elements.
324
+ * Every control reflects `toolbarProps`: edit actions are disabled while the diagram is read-only,
325
+ * viewing actions (zoom, fit, search) never are, and a host dialog button appears only when the
326
+ * host supplied its callback.
327
+ */
328
+ function toolbarGroups(props, include = TOOLBAR_GROUPS) {
329
+ const { labels } = props;
330
+ const has = props.pages.length > 0;
331
+ const groups = [];
332
+ const add = (id, controls) => {
333
+ if (include.includes(id) && controls.length) groups.push({
334
+ id,
335
+ controls
336
+ });
337
+ };
338
+ add("file", [
339
+ ...props.onOpenFile ? [button("open", labels.Open, true, props.onOpenFile)] : [],
340
+ ...props.onOpenPageSetup ? [button("page-setup", labels["Page Setup"], has, props.onOpenPageSetup)] : [],
341
+ ...props.onOpenShareDialog ? [button("share", labels.Share, has, props.onOpenShareDialog)] : [],
342
+ ...props.onOpenSettings ? [button("settings", labels.Options, true, props.onOpenSettings)] : []
343
+ ]);
344
+ add("history", [button("undo", labels.Undo, props.canUndo, props.onUndo), button("redo", labels.Redo, props.canRedo, props.onRedo)]);
345
+ add("zoom", [
346
+ button("zoom-out", labels["Zoom Out"], props.canZoomOut, props.onZoomOut),
347
+ {
348
+ kind: "text",
349
+ id: "zoom",
350
+ text: `${Math.round(props.zoom * 100)}%`
351
+ },
352
+ button("zoom-in", labels["Zoom In"], props.canZoomIn, props.onZoomIn),
353
+ button("fit", labels["Fit to Window"], has, props.onFit)
354
+ ]);
355
+ add("selection", [
356
+ button("duplicate", labels.Duplicate, props.canDuplicate, props.onDuplicate),
357
+ button("group", labels.Group, props.canGroup, props.onGroup),
358
+ button("ungroup", labels.Ungroup, props.canUngroup, props.onUngroup),
359
+ button("delete", labels.Delete, props.canDelete, props.onDelete)
360
+ ]);
361
+ add("clipboard", [
362
+ button("cut", labels.Cut, props.canCut, props.onCut),
363
+ button("copy", labels.Copy, props.canCopy, props.onCopy),
364
+ button("paste", labels.Paste, props.canPaste, props.onPaste)
365
+ ]);
366
+ add("tools", props.tools.map((tool) => button(`tool-${tool.id}`, tool.label, tool.id === "pointer" || props.canDraw, () => props.onToolChange(tool.id), tool.active)));
367
+ const { search } = props;
368
+ add("search", [{
369
+ kind: "search",
370
+ id: "search",
371
+ label: labels.Find,
372
+ query: search.query,
373
+ summary: search.query ? search.resultCount ? `${search.activeIndex >= 0 ? search.activeIndex + 1 : "-"} / ${search.resultCount}` : "0" : "",
374
+ previous: button("search-previous", labels["Previous Result"], search.resultCount > 0, props.onPreviousSearchResult),
375
+ next: button("search-next", labels["Next Result"], search.resultCount > 0, props.onNextSearchResult),
376
+ change: (query) => props.onSearchChange(query)
377
+ }]);
378
+ return groups;
379
+ }
380
+ //#endregion
381
+ //#region packages/solid/src/building-blocks.tsx
382
+ var _tmpl$ = /*#__PURE__*/ template(`<div>`);
383
+ var _tmpl$2 = /*#__PURE__*/ template(`<span>`);
384
+ var _tmpl$3 = /*#__PURE__*/ template(`<button type=button>`);
385
+ var _tmpl$4 = /*#__PURE__*/ template(`<span data-control=search><input type=search><span aria-live=polite>`);
386
+ var _tmpl$5 = /*#__PURE__*/ template(`<div role=toolbar>`);
387
+ var _tmpl$6 = /*#__PURE__*/ template(`<select>`);
388
+ var _tmpl$7 = /*#__PURE__*/ template(`<option>`);
389
+ function createViewerBuildingBlocks(options = () => ({})) {
390
+ const session = createBuildingBlockSession(options());
391
+ const [state, setState] = createSignal(session.controller.state);
392
+ const stop = session.controller.subscribe((value) => setState(() => value));
393
+ createEffect(() => session.update(options()));
394
+ onCleanup(() => {
395
+ stop();
396
+ session.destroy();
397
+ });
398
+ const toolbarProps = createMemo(() => {
399
+ state();
400
+ return session.toolbar(options());
401
+ });
402
+ return {
403
+ controller: session.controller,
404
+ state,
405
+ canvasProps: { mount: session.mount },
406
+ toolbarProps,
407
+ inspectorProps: { controller: session.controller },
408
+ shapesProps: {
409
+ controller: session.controller,
410
+ get locale() {
411
+ return options().locale;
412
+ }
413
+ },
414
+ /** Acknowledge a host save: clears dirty and fires `onDirtyChange(false)`. */
415
+ markClean: () => session.markClean(),
416
+ loading: () => state().loading,
417
+ error: () => state().error ?? state().edit.error,
418
+ /** Effective permission: host `canEdit` and a drawing that can be saved back. */
419
+ mode: () => effectiveMode(state())
420
+ };
421
+ }
422
+ function DiagramCanvas(props) {
423
+ let host;
424
+ createEffect(() => {
425
+ const mount = props.mount;
426
+ mount(host);
427
+ onCleanup(() => mount(null));
428
+ });
429
+ return (() => {
430
+ var _el$ = _tmpl$();
431
+ var _ref$ = host;
432
+ typeof _ref$ === "function" ? use(_ref$, _el$) : host = _el$;
433
+ effect(() => className(_el$, props.class));
434
+ return _el$;
435
+ })();
436
+ }
437
+ /** Index-keyed, so the DOM (and focus) survives every state update; only attributes change. */
438
+ function Control(props) {
439
+ const kind = () => props.control.kind;
440
+ const asButton = () => props.control;
441
+ const asSearch = () => props.control;
442
+ return createComponent(Switch, { get children() {
443
+ return [
444
+ createComponent(Match, {
445
+ get when() {
446
+ return kind() === "text";
447
+ },
448
+ get children() {
449
+ var _el$2 = _tmpl$2();
450
+ insert(_el$2, () => props.control.text);
451
+ effect(() => setAttribute(_el$2, "data-control", props.control.id));
452
+ return _el$2;
453
+ }
454
+ }),
455
+ createComponent(Match, {
456
+ get when() {
457
+ return kind() === "button";
458
+ },
459
+ get children() {
460
+ var _el$3 = _tmpl$3();
461
+ _el$3.$$click = () => asButton().run();
462
+ insert(_el$3, () => asButton().label);
463
+ effect((_p$) => {
464
+ var _v$ = props.control.id, _v$2 = asButton().disabled, _v$3 = asButton().pressed;
465
+ _v$ !== _p$.e && setAttribute(_el$3, "data-control", _p$.e = _v$);
466
+ _v$2 !== _p$.t && (_el$3.disabled = _p$.t = _v$2);
467
+ _v$3 !== _p$.a && setAttribute(_el$3, "aria-pressed", _p$.a = _v$3);
468
+ return _p$;
469
+ }, {
470
+ e: void 0,
471
+ t: void 0,
472
+ a: void 0
473
+ });
474
+ return _el$3;
475
+ }
476
+ }),
477
+ createComponent(Match, {
478
+ get when() {
479
+ return kind() === "search";
480
+ },
481
+ get children() {
482
+ var _el$4 = _tmpl$4(), _el$5 = _el$4.firstChild, _el$6 = _el$5.nextSibling;
483
+ _el$5.$$input = (event) => asSearch().change(event.currentTarget.value);
484
+ insert(_el$6, () => asSearch().summary);
485
+ insert(_el$4, createComponent(Control, { get control() {
486
+ return asSearch().previous;
487
+ } }), null);
488
+ insert(_el$4, createComponent(Control, { get control() {
489
+ return asSearch().next;
490
+ } }), null);
491
+ effect((_p$) => {
492
+ var _v$4 = asSearch().label, _v$5 = asSearch().label;
493
+ _v$4 !== _p$.e && setAttribute(_el$5, "aria-label", _p$.e = _v$4);
494
+ _v$5 !== _p$.t && setAttribute(_el$5, "placeholder", _p$.t = _v$5);
495
+ return _p$;
496
+ }, {
497
+ e: void 0,
498
+ t: void 0
499
+ });
500
+ effect(() => _el$5.value = asSearch().query);
501
+ return _el$4;
502
+ }
503
+ })
504
+ ];
505
+ } });
506
+ }
507
+ /** Optional library toolbar; `include` picks groups. Hosts may build their own from toolbarProps. */
508
+ function Toolbar(props) {
509
+ const groups = createMemo(() => toolbarGroups(props, props.include));
510
+ return (() => {
511
+ var _el$7 = _tmpl$5();
512
+ use((host) => prepareDiagramBlock(host, "toolbar"), _el$7);
513
+ insert(_el$7, createComponent(Index, {
514
+ get each() {
515
+ return groups();
516
+ },
517
+ children: (group) => (() => {
518
+ var _el$8 = _tmpl$2();
519
+ insert(_el$8, createComponent(Index, {
520
+ get each() {
521
+ return group().controls;
522
+ },
523
+ children: (control) => createComponent(Control, { get control() {
524
+ return control();
525
+ } })
526
+ }));
527
+ effect(() => setAttribute(_el$8, "data-group", group().id));
528
+ return _el$8;
529
+ })()
530
+ }));
531
+ return _el$7;
532
+ })();
533
+ }
534
+ function PageNavigation(props) {
535
+ return (() => {
536
+ var _el$9 = _tmpl$6();
537
+ _el$9.addEventListener("change", (event) => props.onPageChange(Number(event.currentTarget.value)));
538
+ use((host) => prepareDiagramBlock(host, "tabs"), _el$9);
539
+ insert(_el$9, createComponent(For, {
540
+ get each() {
541
+ return props.pages;
542
+ },
543
+ children: (page, index) => (() => {
544
+ var _el$0 = _tmpl$7();
545
+ insert(_el$0, () => page.name);
546
+ effect(() => _el$0.value = index());
547
+ return _el$0;
548
+ })()
549
+ }));
550
+ effect(() => setAttribute(_el$9, "aria-label", props.labels.Page));
551
+ effect(() => _el$9.value = props.pageIndex);
552
+ return _el$9;
553
+ })();
554
+ }
555
+ function Inspector(props) {
556
+ let host;
557
+ createEffect(() => {
558
+ const stop = buildingBlock("mountDiagramInspector")(host, props.controller);
559
+ onCleanup(stop);
560
+ });
561
+ return (() => {
562
+ var _el$1 = _tmpl$();
563
+ var _ref$2 = host;
564
+ typeof _ref$2 === "function" ? use(_ref$2, _el$1) : host = _el$1;
565
+ return _el$1;
566
+ })();
567
+ }
568
+ function ShapesPane(props) {
569
+ let host;
570
+ createEffect(() => {
571
+ const stop = buildingBlock("mountDiagramShapes")(host, props.controller, { locale: props.locale });
572
+ onCleanup(stop);
573
+ });
574
+ return (() => {
575
+ var _el$10 = _tmpl$();
576
+ var _ref$3 = host;
577
+ typeof _ref$3 === "function" ? use(_ref$3, _el$10) : host = _el$10;
578
+ return _el$10;
579
+ })();
580
+ }
581
+ delegateEvents(["click", "input"]);
582
+ //#endregion
583
+ export { DiagramCanvas, Inspector, PageNavigation, ShapesPane, Toolbar, VisioViewer, createViewerBuildingBlocks, createVisioViewerState };
@@ -1,6 +1,8 @@
1
- import { mountViewer, propertyKeys } from "ooxml-ui/visio";
2
- import { escape, ssr, ssrAttribute, ssrHydrationKey, ssrStyle } from "solid-js/web";
3
- import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
1
+ import * as ui from "ooxml-ui/visio";
2
+ import { ViewerController, createWorkerParser, mountViewer, propertyKeys } from "ooxml-ui/visio";
3
+ import { createComponent, escape, ssr, ssrAttribute, ssrHydrationKey, ssrStyle } from "solid-js/web";
4
+ import { For, Index, Match, Switch, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";
5
+ import "ooxml-ui/theme";
4
6
  export * from "ooxml-ui/visio";
5
7
  export * from "visio-core";
6
8
  //#region packages/bindings/src/common.ts
@@ -145,7 +147,7 @@ function viewerStateSource(current) {
145
147
  }
146
148
  //#endregion
147
149
  //#region packages/bindings/src/solid.tsx
148
- var _tmpl$ = [
150
+ var _tmpl$$1 = [
149
151
  "<div",
150
152
  " style=\"",
151
153
  "\"",
@@ -185,7 +187,7 @@ function VisioViewer(props) {
185
187
  mounted?.destroy();
186
188
  }
187
189
  });
188
- return ssr(_tmpl$, ssrHydrationKey() + ssrAttribute("class", escape(props.class, true), false), ssrStyle(props.style), ssrAttribute("aria-label", escape(props["aria-label"], true), false));
190
+ return ssr(_tmpl$$1, ssrHydrationKey() + ssrAttribute("class", escape(props.class, true), false), ssrStyle(props.style), ssrAttribute("aria-label", escape(props["aria-label"], true), false));
189
191
  }
190
192
  /**
191
193
  * Reactive viewer state as a Solid accessor for a handle accessor (for example a signal set by
@@ -202,4 +204,319 @@ function createVisioViewerState(handle) {
202
204
  return state;
203
205
  }
204
206
  //#endregion
205
- export { VisioViewer, createVisioViewerState };
207
+ //#region packages/bindings/src/building-blocks.ts
208
+ function buildingBlock(key) {
209
+ const value = Reflect.get(ui, key);
210
+ if (typeof value !== "function") throw new Error("Update ooxml-ui to use Visio building blocks.");
211
+ return value;
212
+ }
213
+ /** The mode a host sees: 'edit' only when permission is granted and the file can be saved back. */
214
+ function effectiveMode(state) {
215
+ return buildingBlock("diagramMode")(state);
216
+ }
217
+ /** Framework-neutral ownership only; each adapter supplies its own reactive state. */
218
+ function createBuildingBlockSession(initial = {}) {
219
+ const controller = new ViewerController(createWorkerParser());
220
+ controller.canEdit = initial.canEdit === true;
221
+ let options = initial;
222
+ let source;
223
+ let host = null;
224
+ let disposeCanvas;
225
+ let destroyed = false;
226
+ const stopEvents = controller.onEvent((name, detail) => {
227
+ if (name === "document-change") {
228
+ const change = detail;
229
+ options.onDocumentChange?.(change.document);
230
+ options.onDirtyChange?.(change.dirty);
231
+ } else if (name === "document-error") options.onDocumentError?.(detail);
232
+ });
233
+ const fit = () => {
234
+ if (host) buildingBlock("fitDiagramCanvas")(host, controller);
235
+ };
236
+ return {
237
+ controller,
238
+ mount(node) {
239
+ if (destroyed || node === host) return;
240
+ disposeCanvas?.();
241
+ disposeCanvas = void 0;
242
+ host = node;
243
+ if (node) disposeCanvas = buildingBlock("mountDiagramCanvas")(node, controller);
244
+ },
245
+ /** Flat control props; pass the framework's latest options so callbacks stay current. */
246
+ toolbar(latest = options) {
247
+ return buildingBlock("diagramToolbarProps")(controller, fit, latest.locale, {
248
+ onOpenFile: latest.onOpenFile,
249
+ onOpenPageSetup: latest.onOpenPageSetup,
250
+ onOpenSettings: latest.onOpenSettings,
251
+ onOpenShareDialog: latest.onOpenShareDialog
252
+ });
253
+ },
254
+ /** Acknowledge a host save: clears dirty and emits the dirty change. */
255
+ markClean() {
256
+ controller.markClean();
257
+ },
258
+ update(next) {
259
+ if (destroyed) return;
260
+ options = next;
261
+ controller.canEdit = next.canEdit === true;
262
+ if (source === next.content) return;
263
+ source = next.content;
264
+ controller.cancelLoad();
265
+ if (!source) return;
266
+ const input = source;
267
+ controller.loadSource(async () => {
268
+ return input instanceof Blob ? new Uint8Array(await input.arrayBuffer()) : input;
269
+ }).catch(() => {});
270
+ },
271
+ destroy() {
272
+ if (destroyed) return;
273
+ destroyed = true;
274
+ disposeCanvas?.();
275
+ disposeCanvas = void 0;
276
+ host = null;
277
+ stopEvents();
278
+ controller.destroy();
279
+ }
280
+ };
281
+ }
282
+ //#endregion
283
+ //#region packages/bindings/src/toolbar-controls.ts
284
+ var TOOLBAR_GROUPS = [
285
+ "file",
286
+ "history",
287
+ "zoom",
288
+ "selection",
289
+ "clipboard",
290
+ "tools",
291
+ "search"
292
+ ];
293
+ var fire = (action) => () => {
294
+ Promise.resolve().then(action).catch(() => {});
295
+ };
296
+ var button = (id, label, enabled, action, pressed) => ({
297
+ kind: "button",
298
+ id,
299
+ label,
300
+ disabled: !enabled,
301
+ ...pressed === void 0 ? {} : { pressed },
302
+ run: fire(action)
303
+ });
304
+ /**
305
+ * What the library Toolbar shows, as plain data every framework maps onto its own elements.
306
+ * Every control reflects `toolbarProps`: edit actions are disabled while the diagram is read-only,
307
+ * viewing actions (zoom, fit, search) never are, and a host dialog button appears only when the
308
+ * host supplied its callback.
309
+ */
310
+ function toolbarGroups(props, include = TOOLBAR_GROUPS) {
311
+ const { labels } = props;
312
+ const has = props.pages.length > 0;
313
+ const groups = [];
314
+ const add = (id, controls) => {
315
+ if (include.includes(id) && controls.length) groups.push({
316
+ id,
317
+ controls
318
+ });
319
+ };
320
+ add("file", [
321
+ ...props.onOpenFile ? [button("open", labels.Open, true, props.onOpenFile)] : [],
322
+ ...props.onOpenPageSetup ? [button("page-setup", labels["Page Setup"], has, props.onOpenPageSetup)] : [],
323
+ ...props.onOpenShareDialog ? [button("share", labels.Share, has, props.onOpenShareDialog)] : [],
324
+ ...props.onOpenSettings ? [button("settings", labels.Options, true, props.onOpenSettings)] : []
325
+ ]);
326
+ add("history", [button("undo", labels.Undo, props.canUndo, props.onUndo), button("redo", labels.Redo, props.canRedo, props.onRedo)]);
327
+ add("zoom", [
328
+ button("zoom-out", labels["Zoom Out"], props.canZoomOut, props.onZoomOut),
329
+ {
330
+ kind: "text",
331
+ id: "zoom",
332
+ text: `${Math.round(props.zoom * 100)}%`
333
+ },
334
+ button("zoom-in", labels["Zoom In"], props.canZoomIn, props.onZoomIn),
335
+ button("fit", labels["Fit to Window"], has, props.onFit)
336
+ ]);
337
+ add("selection", [
338
+ button("duplicate", labels.Duplicate, props.canDuplicate, props.onDuplicate),
339
+ button("group", labels.Group, props.canGroup, props.onGroup),
340
+ button("ungroup", labels.Ungroup, props.canUngroup, props.onUngroup),
341
+ button("delete", labels.Delete, props.canDelete, props.onDelete)
342
+ ]);
343
+ add("clipboard", [
344
+ button("cut", labels.Cut, props.canCut, props.onCut),
345
+ button("copy", labels.Copy, props.canCopy, props.onCopy),
346
+ button("paste", labels.Paste, props.canPaste, props.onPaste)
347
+ ]);
348
+ add("tools", props.tools.map((tool) => button(`tool-${tool.id}`, tool.label, tool.id === "pointer" || props.canDraw, () => props.onToolChange(tool.id), tool.active)));
349
+ const { search } = props;
350
+ add("search", [{
351
+ kind: "search",
352
+ id: "search",
353
+ label: labels.Find,
354
+ query: search.query,
355
+ summary: search.query ? search.resultCount ? `${search.activeIndex >= 0 ? search.activeIndex + 1 : "-"} / ${search.resultCount}` : "0" : "",
356
+ previous: button("search-previous", labels["Previous Result"], search.resultCount > 0, props.onPreviousSearchResult),
357
+ next: button("search-next", labels["Next Result"], search.resultCount > 0, props.onNextSearchResult),
358
+ change: (query) => props.onSearchChange(query)
359
+ }]);
360
+ return groups;
361
+ }
362
+ //#endregion
363
+ //#region packages/solid/src/building-blocks.tsx
364
+ var _tmpl$ = ["<div", "></div>"];
365
+ var _tmpl$2 = [
366
+ "<span",
367
+ ">",
368
+ "</span>"
369
+ ];
370
+ var _tmpl$3 = [
371
+ "<button",
372
+ " type=\"button\"",
373
+ "",
374
+ ">",
375
+ "</button>"
376
+ ];
377
+ var _tmpl$4 = [
378
+ "<span",
379
+ " data-control=\"search\"><input type=\"search\"",
380
+ "><span aria-live=\"polite\">",
381
+ "</span><!--$-->",
382
+ "<!--/--><!--$-->",
383
+ "<!--/--></span>"
384
+ ];
385
+ var _tmpl$5 = [
386
+ "<div",
387
+ " role=\"toolbar\">",
388
+ "</div>"
389
+ ];
390
+ var _tmpl$6 = [
391
+ "<select",
392
+ ">",
393
+ "</select>"
394
+ ];
395
+ var _tmpl$7 = [
396
+ "<option",
397
+ ">",
398
+ "</option>"
399
+ ];
400
+ function createViewerBuildingBlocks(options = () => ({})) {
401
+ const session = createBuildingBlockSession(options());
402
+ const [state, setState] = createSignal(session.controller.state);
403
+ const stop = session.controller.subscribe((value) => setState(() => value));
404
+ createEffect(() => session.update(options()));
405
+ onCleanup(() => {
406
+ stop();
407
+ session.destroy();
408
+ });
409
+ const toolbarProps = createMemo(() => {
410
+ state();
411
+ return session.toolbar(options());
412
+ });
413
+ return {
414
+ controller: session.controller,
415
+ state,
416
+ canvasProps: { mount: session.mount },
417
+ toolbarProps,
418
+ inspectorProps: { controller: session.controller },
419
+ shapesProps: {
420
+ controller: session.controller,
421
+ get locale() {
422
+ return options().locale;
423
+ }
424
+ },
425
+ /** Acknowledge a host save: clears dirty and fires `onDirtyChange(false)`. */
426
+ markClean: () => session.markClean(),
427
+ loading: () => state().loading,
428
+ error: () => state().error ?? state().edit.error,
429
+ /** Effective permission: host `canEdit` and a drawing that can be saved back. */
430
+ mode: () => effectiveMode(state())
431
+ };
432
+ }
433
+ function DiagramCanvas(props) {
434
+ let host;
435
+ createEffect(() => {
436
+ const mount = props.mount;
437
+ mount(host);
438
+ onCleanup(() => mount(null));
439
+ });
440
+ return ssr(_tmpl$, ssrHydrationKey() + ssrAttribute("class", escape(props.class, true), false));
441
+ }
442
+ /** Index-keyed, so the DOM (and focus) survives every state update; only attributes change. */
443
+ function Control(props) {
444
+ const kind = () => props.control.kind;
445
+ const asButton = () => props.control;
446
+ const asSearch = () => props.control;
447
+ return createComponent(Switch, { get children() {
448
+ return [
449
+ createComponent(Match, {
450
+ get when() {
451
+ return kind() === "text";
452
+ },
453
+ get children() {
454
+ return ssr(_tmpl$2, ssrHydrationKey() + ssrAttribute("data-control", escape(props.control.id, true), false), escape(props.control.text));
455
+ }
456
+ }),
457
+ createComponent(Match, {
458
+ get when() {
459
+ return kind() === "button";
460
+ },
461
+ get children() {
462
+ return ssr(_tmpl$3, ssrHydrationKey(), ssrAttribute("data-control", escape(props.control.id, true), false), ssrAttribute("disabled", asButton().disabled, true) + ssrAttribute("aria-pressed", escape(asButton().pressed, true), false), escape(asButton().label));
463
+ }
464
+ }),
465
+ createComponent(Match, {
466
+ get when() {
467
+ return kind() === "search";
468
+ },
469
+ get children() {
470
+ return ssr(_tmpl$4, ssrHydrationKey(), ssrAttribute("aria-label", escape(asSearch().label, true), false) + ssrAttribute("placeholder", escape(asSearch().label, true), false) + ssrAttribute("value", escape(asSearch().query, true), false), escape(asSearch().summary), escape(createComponent(Control, { get control() {
471
+ return asSearch().previous;
472
+ } })), escape(createComponent(Control, { get control() {
473
+ return asSearch().next;
474
+ } })));
475
+ }
476
+ })
477
+ ];
478
+ } });
479
+ }
480
+ /** Optional library toolbar; `include` picks groups. Hosts may build their own from toolbarProps. */
481
+ function Toolbar(props) {
482
+ const groups = createMemo(() => toolbarGroups(props, props.include));
483
+ return ssr(_tmpl$5, ssrHydrationKey(), escape(createComponent(Index, {
484
+ get each() {
485
+ return groups();
486
+ },
487
+ children: (group) => ssr(_tmpl$2, ssrHydrationKey() + ssrAttribute("data-group", escape(group().id, true), false), escape(createComponent(Index, {
488
+ get each() {
489
+ return group().controls;
490
+ },
491
+ children: (control) => createComponent(Control, { get control() {
492
+ return control();
493
+ } })
494
+ })))
495
+ })));
496
+ }
497
+ function PageNavigation(props) {
498
+ return ssr(_tmpl$6, ssrHydrationKey() + ssrAttribute("aria-label", escape(props.labels.Page, true), false) + ssrAttribute("value", escape(props.pageIndex, true), false), escape(createComponent(For, {
499
+ get each() {
500
+ return props.pages;
501
+ },
502
+ children: (page, index) => ssr(_tmpl$7, ssrHydrationKey() + ssrAttribute("value", escape(index(), true), false), escape(page.name))
503
+ })));
504
+ }
505
+ function Inspector(props) {
506
+ let host;
507
+ createEffect(() => {
508
+ const stop = buildingBlock("mountDiagramInspector")(host, props.controller);
509
+ onCleanup(stop);
510
+ });
511
+ return ssr(_tmpl$, ssrHydrationKey());
512
+ }
513
+ function ShapesPane(props) {
514
+ let host;
515
+ createEffect(() => {
516
+ const stop = buildingBlock("mountDiagramShapes")(host, props.controller, { locale: props.locale });
517
+ onCleanup(stop);
518
+ });
519
+ return ssr(_tmpl$, ssrHydrationKey());
520
+ }
521
+ //#endregion
522
+ export { DiagramCanvas, Inspector, PageNavigation, ShapesPane, Toolbar, VisioViewer, createViewerBuildingBlocks, createVisioViewerState };
@@ -0,0 +1,95 @@
1
+ import { ViewerController, type VsdxSource } from 'ooxml-ui/visio';
2
+ import * as ui from 'ooxml-ui/visio';
3
+ /** Host-owned dialogs, as in the PowerPoint building blocks; omitted ones report not-enabled. */
4
+ export interface BuildingBlockHostCallbacks {
5
+ onOpenFile?: (() => void) | undefined;
6
+ onOpenPageSetup?: (() => void) | undefined;
7
+ onOpenSettings?: (() => void) | undefined;
8
+ onOpenShareDialog?: (() => void) | undefined;
9
+ }
10
+ export interface BuildingBlockOptions extends BuildingBlockHostCallbacks {
11
+ content?: VsdxSource | undefined;
12
+ /** Host edit permission. Read-only until it is `true`, so nothing is editable before the host says so. */
13
+ canEdit?: boolean | undefined;
14
+ locale?: string | undefined;
15
+ onDocumentChange?: ((document: NonNullable<ViewerController['state']['document']>) => void) | undefined;
16
+ onDirtyChange?: ((dirty: boolean) => void) | undefined;
17
+ onDocumentError?: ((error: Error) => void) | undefined;
18
+ }
19
+ type Api = Pick<typeof ui, 'mountDiagramCanvas' | 'fitDiagramCanvas' | 'mountDiagramInspector' | 'mountDiagramShapes' | 'diagramToolbarProps' | 'diagramMode'>;
20
+ export declare function buildingBlock<K extends keyof Api>(key: K): Api[K];
21
+ /** The mode a host sees: 'edit' only when permission is granted and the file can be saved back. */
22
+ export declare function effectiveMode(state: ViewerController['state']): 'edit' | 'view';
23
+ /** Framework-neutral ownership only; each adapter supplies its own reactive state. */
24
+ export declare function createBuildingBlockSession(initial?: BuildingBlockOptions): {
25
+ controller: ViewerController;
26
+ mount(node: HTMLElement | null): void;
27
+ /** Flat control props; pass the framework's latest options so callbacks stay current. */
28
+ toolbar(latest?: BuildingBlockOptions): {
29
+ labels: Readonly<Record<"Arc" | "Copy" | "Cut" | "Delete" | "Duplicate" | "Ellipse" | "Find" | "Fit to Window" | "Freeform" | "Group" | "Line" | "Next Result" | "Open" | "Options" | "Page" | "Page Setup" | "Paste" | "Pencil" | "Pointer Tool" | "Previous Result" | "Rectangle" | "Redo" | "Shapes" | "Share" | "Undo" | "Ungroup" | "Zoom In" | "Zoom Out", string>>;
30
+ mode: ui.DiagramMode;
31
+ canEdit: boolean;
32
+ pageIndex: number;
33
+ pages: {
34
+ id: string;
35
+ name: string;
36
+ }[];
37
+ zoom: number;
38
+ selection: readonly import("ooxml-core/visio/ui").VisioShapeSelection[];
39
+ canUndo: boolean;
40
+ canRedo: boolean;
41
+ canDuplicate: boolean;
42
+ canGroup: boolean;
43
+ canUngroup: boolean;
44
+ canDelete: boolean;
45
+ canCut: boolean;
46
+ canCopy: boolean;
47
+ canPaste: boolean;
48
+ canDraw: boolean;
49
+ canZoomIn: boolean;
50
+ canZoomOut: boolean;
51
+ tool: ui.DiagramTool;
52
+ tools: ui.DiagramToolSummary[];
53
+ search: {
54
+ query: string;
55
+ resultCount: number;
56
+ activeIndex: number;
57
+ truncated: boolean;
58
+ };
59
+ onPageChange: (index: number) => void;
60
+ onZoomChange: (zoom: number) => void;
61
+ onZoomIn: () => void;
62
+ onZoomOut: () => void;
63
+ onFit: () => void;
64
+ onUndo: () => Promise<void>;
65
+ onRedo: () => Promise<void>;
66
+ onDuplicate: () => Promise<void>;
67
+ onGroup: () => Promise<void>;
68
+ onUngroup: () => Promise<void>;
69
+ onDelete: () => Promise<void>;
70
+ onCut: () => Promise<void>;
71
+ onCopy: () => Promise<void>;
72
+ onPaste: () => Promise<void>;
73
+ onToolChange: (tool: ui.DiagramTool) => void;
74
+ onTextChange: (pageId: string, shapeId: string, text: string) => Promise<void>;
75
+ onSearchChange: (query: string) => void;
76
+ onNextSearchResult: () => void;
77
+ onPreviousSearchResult: () => void;
78
+ canOpenFile: boolean;
79
+ canOpenPageSetup: boolean;
80
+ canOpenSettings: boolean;
81
+ canOpenShareDialog: boolean;
82
+ onOpenFile?: () => void;
83
+ onOpenPageSetup?: () => void;
84
+ onOpenSettings?: () => void;
85
+ onOpenShareDialog?: () => void;
86
+ };
87
+ /** Acknowledge a host save: clears dirty and emits the dirty change. */
88
+ markClean(): void;
89
+ update(next: BuildingBlockOptions): void;
90
+ destroy(): void;
91
+ };
92
+ export type BuildingBlockSession = ReturnType<typeof createBuildingBlockSession>;
93
+ export type { DiagramToolbarProps, DiagramToolbarLabels, DiagramToolbarHostCallbacks, DiagramMode, DiagramTool, ViewerController, } from 'ooxml-ui/visio';
94
+ /** Resolve the new optional-control theme API only when that control is mounted. */
95
+ export declare function prepareDiagramBlock(host: HTMLElement | null, kind: 'toolbar' | 'tabs'): void;
@@ -0,0 +1,41 @@
1
+ import type { DiagramToolbarProps } from 'ooxml-ui/visio';
2
+ /** The groups of the library toolbar; a host picks the ones it wants with `include`. */
3
+ export type ToolbarGroupId = 'file' | 'history' | 'zoom' | 'selection' | 'clipboard' | 'tools' | 'search';
4
+ export declare const TOOLBAR_GROUPS: readonly ToolbarGroupId[];
5
+ export interface ToolbarButton {
6
+ readonly kind: 'button';
7
+ readonly id: string;
8
+ readonly label: string;
9
+ readonly disabled: boolean;
10
+ /** Set for toggles such as the drawing tools. */
11
+ readonly pressed?: boolean;
12
+ run(): void;
13
+ }
14
+ export interface ToolbarText {
15
+ readonly kind: 'text';
16
+ readonly id: string;
17
+ readonly text: string;
18
+ }
19
+ export interface ToolbarSearch {
20
+ readonly kind: 'search';
21
+ readonly id: 'search';
22
+ readonly label: string;
23
+ readonly query: string;
24
+ /** Result summary for assistive technology, e.g. "2 of 5". */
25
+ readonly summary: string;
26
+ readonly previous: ToolbarButton;
27
+ readonly next: ToolbarButton;
28
+ change(query: string): void;
29
+ }
30
+ export type ToolbarControl = ToolbarButton | ToolbarText | ToolbarSearch;
31
+ export interface ToolbarGroup {
32
+ readonly id: ToolbarGroupId;
33
+ readonly controls: readonly ToolbarControl[];
34
+ }
35
+ /**
36
+ * What the library Toolbar shows, as plain data every framework maps onto its own elements.
37
+ * Every control reflects `toolbarProps`: edit actions are disabled while the diagram is read-only,
38
+ * viewing actions (zoom, fit, search) never are, and a host dialog button appears only when the
39
+ * host supplied its callback.
40
+ */
41
+ export declare function toolbarGroups(props: DiagramToolbarProps, include?: readonly ToolbarGroupId[]): ToolbarGroup[];
@@ -0,0 +1,100 @@
1
+ import { type Accessor } from 'solid-js';
2
+ import { type BuildingBlockOptions, type DiagramToolbarProps, type ViewerController } from '../../bindings/src/building-blocks.js';
3
+ import { type ToolbarGroupId } from '../../bindings/src/toolbar-controls.js';
4
+ export declare function createViewerBuildingBlocks(options?: Accessor<BuildingBlockOptions>): {
5
+ controller: ViewerController;
6
+ state: Accessor<import("ooxml-ui/visio").ViewerState>;
7
+ canvasProps: {
8
+ mount: (node: HTMLElement | null) => void;
9
+ };
10
+ toolbarProps: Accessor<{
11
+ labels: Readonly<Record<"Arc" | "Copy" | "Cut" | "Delete" | "Duplicate" | "Ellipse" | "Find" | "Fit to Window" | "Freeform" | "Group" | "Line" | "Next Result" | "Open" | "Options" | "Page" | "Page Setup" | "Paste" | "Pencil" | "Pointer Tool" | "Previous Result" | "Rectangle" | "Redo" | "Shapes" | "Share" | "Undo" | "Ungroup" | "Zoom In" | "Zoom Out", string>>;
12
+ mode: import("ooxml-ui/visio").DiagramMode;
13
+ canEdit: boolean;
14
+ pageIndex: number;
15
+ pages: {
16
+ id: string;
17
+ name: string;
18
+ }[];
19
+ zoom: number;
20
+ selection: readonly import("ooxml-ui/visio").VisioShapeSelection[];
21
+ canUndo: boolean;
22
+ canRedo: boolean;
23
+ canDuplicate: boolean;
24
+ canGroup: boolean;
25
+ canUngroup: boolean;
26
+ canDelete: boolean;
27
+ canCut: boolean;
28
+ canCopy: boolean;
29
+ canPaste: boolean;
30
+ canDraw: boolean;
31
+ canZoomIn: boolean;
32
+ canZoomOut: boolean;
33
+ tool: import("ooxml-ui/visio").DiagramTool;
34
+ tools: import("ooxml-ui/visio").DiagramToolSummary[];
35
+ search: {
36
+ query: string;
37
+ resultCount: number;
38
+ activeIndex: number;
39
+ truncated: boolean;
40
+ };
41
+ onPageChange: (index: number) => void;
42
+ onZoomChange: (zoom: number) => void;
43
+ onZoomIn: () => void;
44
+ onZoomOut: () => void;
45
+ onFit: () => void;
46
+ onUndo: () => Promise<void>;
47
+ onRedo: () => Promise<void>;
48
+ onDuplicate: () => Promise<void>;
49
+ onGroup: () => Promise<void>;
50
+ onUngroup: () => Promise<void>;
51
+ onDelete: () => Promise<void>;
52
+ onCut: () => Promise<void>;
53
+ onCopy: () => Promise<void>;
54
+ onPaste: () => Promise<void>;
55
+ onToolChange: (tool: import("ooxml-ui/visio").DiagramTool) => void;
56
+ onTextChange: (pageId: string, shapeId: string, text: string) => Promise<void>;
57
+ onSearchChange: (query: string) => void;
58
+ onNextSearchResult: () => void;
59
+ onPreviousSearchResult: () => void;
60
+ canOpenFile: boolean;
61
+ canOpenPageSetup: boolean;
62
+ canOpenSettings: boolean;
63
+ canOpenShareDialog: boolean;
64
+ onOpenFile?: () => void;
65
+ onOpenPageSetup?: () => void;
66
+ onOpenSettings?: () => void;
67
+ onOpenShareDialog?: () => void;
68
+ }>;
69
+ inspectorProps: {
70
+ controller: ViewerController;
71
+ };
72
+ shapesProps: {
73
+ controller: ViewerController;
74
+ readonly locale: string | undefined;
75
+ };
76
+ /** Acknowledge a host save: clears dirty and fires `onDirtyChange(false)`. */
77
+ markClean: () => void;
78
+ loading: () => boolean;
79
+ error: () => Error | null;
80
+ /** Effective permission: host `canEdit` and a drawing that can be saved back. */
81
+ mode: () => "edit" | "view";
82
+ };
83
+ export declare function DiagramCanvas(props: {
84
+ mount(node: HTMLElement | null): void;
85
+ class?: string | undefined;
86
+ }): import("solid-js").JSX.Element;
87
+ /** Optional library toolbar; `include` picks groups. Hosts may build their own from toolbarProps. */
88
+ export declare function Toolbar(props: DiagramToolbarProps & {
89
+ include?: readonly ToolbarGroupId[];
90
+ }): import("solid-js").JSX.Element;
91
+ export declare function PageNavigation(props: DiagramToolbarProps): import("solid-js").JSX.Element;
92
+ export declare function Inspector(props: {
93
+ controller: ViewerController;
94
+ }): import("solid-js").JSX.Element;
95
+ export declare function ShapesPane(props: {
96
+ controller: ViewerController;
97
+ locale?: string | undefined;
98
+ }): import("solid-js").JSX.Element;
99
+ export type { BuildingBlockOptions, BuildingBlockHostCallbacks, DiagramToolbarProps, } from '../../bindings/src/building-blocks.js';
100
+ export type { ToolbarGroupId } from '../../bindings/src/toolbar-controls.js';
@@ -1,3 +1,4 @@
1
1
  export * from '../../../src/index.js';
2
2
  export * from 'visio-core';
3
3
  export * from '../../bindings/src/solid.js';
4
+ export * from './building-blocks.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visio-solid-viewer",
3
- "version": "0.7.4",
3
+ "version": "1.0.0",
4
4
  "description": "Local-first Visio viewer for solid",
5
5
  "homepage": "https://christophervr.github.io/ooxml/visio/",
6
6
  "bugs": {
@@ -32,9 +32,9 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "emf-converter": "4.21.0",
35
- "ooxml-core": "^1.19.2",
36
- "ooxml-ui": "^1.18.3",
37
- "visio-core": "^0.4.1"
35
+ "ooxml-core": "^1.22.0",
36
+ "ooxml-ui": "^1.21.0",
37
+ "visio-core": "^1.0.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "solid-js": "^1.9.0"