@ifc-lite/viewer 1.23.0 → 1.25.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 (109) hide show
  1. package/.turbo/turbo-build.log +34 -31
  2. package/CHANGELOG.md +96 -0
  3. package/dist/assets/{basketViewActivator-Dn_bHUl2.js → basketViewActivator-CU8_toGq.js} +7 -7
  4. package/dist/assets/{bcf-B9SFl84i.js → bcf-DXGDhw56.js} +23 -23
  5. package/dist/assets/{deflate-yMpdCIqk.js → deflate-Bb1_H2Yf.js} +1 -1
  6. package/dist/assets/{exporters-D-BvrNIg.js → exporters-DZhLN0ux.js} +1861 -1658
  7. package/dist/assets/geometry-controller.worker-DQOSYqtw.js +7 -0
  8. package/dist/assets/geometry.worker-B62e03Ao.js +1 -0
  9. package/dist/assets/{geotiff-D1tvcDCb.js → geotiff-y0ZxbRJd.js} +10 -10
  10. package/dist/assets/{ids-DZLs0snJ.js → ids-DruUNtfD.js} +4 -4
  11. package/dist/assets/ifc-lite-Ch2T9pP9.js +7 -0
  12. package/dist/assets/{ifc-lite_bg-DyHX37GQ.wasm → ifc-lite_bg-D7O1WHgP.wasm} +0 -0
  13. package/dist/assets/{ifc-lite_bg-BIryVCXQ.wasm → ifc-lite_bg-iH_07wf8.wasm} +0 -0
  14. package/dist/assets/index-Bws3UAkj.css +1 -0
  15. package/dist/assets/{index-CXSBhkcJ.js → index-Dr88ZlSY.js} +64100 -47030
  16. package/dist/assets/{jpeg-DUMcZp24.js → jpeg-B3_loqFe.js} +1 -1
  17. package/dist/assets/lens-PYsLu_MA.js +1 -0
  18. package/dist/assets/{lerc-IN4uWojP.js → lerc-nkwS8ZUe.js} +1 -1
  19. package/dist/assets/{lzw-Cnw0hH-m.js → lzw-D3cW5Wpg.js} +1 -1
  20. package/dist/assets/{native-bridge-BVf2uzoH.js → native-bridge-BcYJooq8.js} +2 -2
  21. package/dist/assets/{packbits-BskJCwk0.js → packbits-DDN4xzB5.js} +1 -1
  22. package/dist/assets/{parser.worker-BdtkkaGf.js → parser.worker-BW1IMUed.js} +3 -3
  23. package/dist/assets/raw-CoIXstQ-.js +1 -0
  24. package/dist/assets/{sandbox-VLI_y7cl.js → sandbox-DETNEyQb.js} +498 -470
  25. package/dist/assets/{server-client-BLcKaWQB.js → server-client-CmzJOeS7.js} +1 -1
  26. package/dist/assets/{wasm-bridge-BAfZh7YT.js → wasm-bridge-CT7mK9W0.js} +1 -1
  27. package/dist/assets/{webimage-Db2xzze3.js → webimage-CBjgg4up.js} +1 -1
  28. package/dist/assets/{workerHelpers--sAYm9yN.js → workerHelpers-IEQDo8r3.js} +1 -1
  29. package/dist/assets/{zstd-BDToOQyD.js → zstd-C8oQ6qdS.js} +1 -1
  30. package/dist/index.html +8 -8
  31. package/package.json +11 -9
  32. package/src/App.tsx +5 -2
  33. package/src/components/extensions/AuditLogPanel.tsx +259 -0
  34. package/src/components/extensions/BundlePreview.tsx +102 -0
  35. package/src/components/extensions/CapabilityReview.tsx +333 -0
  36. package/src/components/extensions/ExtensionDockHost.tsx +192 -0
  37. package/src/components/extensions/ExtensionToolbarSlot.tsx +106 -0
  38. package/src/components/extensions/ExtensionsPanel.tsx +481 -0
  39. package/src/components/extensions/FlavorDialog.tsx +398 -0
  40. package/src/components/extensions/FlavorImportPreview.tsx +79 -0
  41. package/src/components/extensions/FlavorIndicator.tsx +81 -0
  42. package/src/components/extensions/FlavorListView.tsx +318 -0
  43. package/src/components/extensions/FlavorMergeDialog.tsx +326 -0
  44. package/src/components/extensions/HelpHint.tsx +182 -0
  45. package/src/components/extensions/IdeasPanel.tsx +344 -0
  46. package/src/components/extensions/PlanCard.tsx +227 -0
  47. package/src/components/extensions/PrivacyPanel.tsx +312 -0
  48. package/src/components/extensions/PromoteToolDialog.tsx +313 -0
  49. package/src/components/extensions/RepairQueuePanel.tsx +222 -0
  50. package/src/components/extensions/icon-registry.ts +92 -0
  51. package/src/components/extensions/toast-helpers.ts +49 -0
  52. package/src/components/extensions/widget/WidgetErrorBoundary.tsx +62 -0
  53. package/src/components/extensions/widget/WidgetRenderer.tsx +428 -0
  54. package/src/components/viewer/ChatPanel.tsx +251 -3
  55. package/src/components/viewer/CommandPalette.tsx +74 -4
  56. package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
  57. package/src/components/viewer/EntityContextMenu.tsx +70 -0
  58. package/src/components/viewer/ExportDialog.tsx +9 -1
  59. package/src/components/viewer/KeyboardShortcutsDialog.tsx +21 -6
  60. package/src/components/viewer/LensPanel.tsx +50 -0
  61. package/src/components/viewer/MainToolbar.tsx +170 -87
  62. package/src/components/viewer/ScriptPanel.tsx +105 -1
  63. package/src/components/viewer/Section2DPanel.tsx +58 -2
  64. package/src/components/viewer/StatusBar.tsx +18 -0
  65. package/src/components/viewer/ViewerLayout.tsx +53 -4
  66. package/src/components/viewer/Viewport.tsx +72 -0
  67. package/src/hooks/useActionLogger.test.ts +161 -0
  68. package/src/hooks/useActionLogger.ts +141 -0
  69. package/src/hooks/useForkExtension.ts +51 -0
  70. package/src/hooks/useIfcFederation.ts +7 -1
  71. package/src/hooks/useInstalledExtensions.ts +43 -0
  72. package/src/hooks/usePrivacyDisclosure.ts +48 -0
  73. package/src/hooks/useRunExtensionTests.ts +67 -0
  74. package/src/hooks/useSlotContributions.ts +38 -0
  75. package/src/hooks/useSymbolicAnnotations.test.ts +124 -0
  76. package/src/hooks/useSymbolicAnnotations.ts +776 -0
  77. package/src/lib/desktop-product.ts +7 -1
  78. package/src/lib/lens/adapter.ts +14 -0
  79. package/src/lib/llm/prompt-cache.ts +77 -0
  80. package/src/lib/llm/stream-client.ts +20 -2
  81. package/src/lib/llm/stream-direct.ts +11 -1
  82. package/src/lib/llm/system-prompt.ts +42 -0
  83. package/src/lib/safe-mode.ts +30 -0
  84. package/src/sdk/ExtensionHostProvider.tsx +103 -0
  85. package/src/services/extensions/flavor-service.ts +183 -0
  86. package/src/services/extensions/host-commands.ts +112 -0
  87. package/src/services/extensions/host-installer.ts +289 -0
  88. package/src/services/extensions/host.ts +514 -0
  89. package/src/services/extensions/idb-flavor-storage.test.ts +140 -0
  90. package/src/services/extensions/idb-flavor-storage.ts +241 -0
  91. package/src/services/extensions/idb-log-storage.test.ts +110 -0
  92. package/src/services/extensions/idb-log-storage.ts +171 -0
  93. package/src/services/extensions/idb-storage.ts +228 -0
  94. package/src/services/extensions/runtime-errors.ts +26 -0
  95. package/src/services/extensions/sandbox-factory.ts +217 -0
  96. package/src/store/constants.ts +48 -6
  97. package/src/store/index.ts +6 -1
  98. package/src/store/slices/drawing2DSlice.ts +8 -0
  99. package/src/store/slices/extensionsSlice.ts +90 -0
  100. package/src/store/slices/lensSlice.ts +28 -0
  101. package/src/store/slices/visibilitySlice.test.ts +6 -0
  102. package/src/store/slices/visibilitySlice.ts +17 -8
  103. package/src/store/types.ts +2 -0
  104. package/dist/assets/geometry-controller.worker-Cm5pvyR6.js +0 -7
  105. package/dist/assets/geometry.worker-ClNvXIrj.js +0 -1
  106. package/dist/assets/ifc-lite-BDg0iIbj.js +0 -7
  107. package/dist/assets/index-DS_xJQfP.css +0 -1
  108. package/dist/assets/lens-CpjUdqpw.js +0 -1
  109. package/dist/assets/raw-DzTtEZIY.js +0 -1
@@ -0,0 +1,192 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * `ExtensionDockHost` — render dock panels contributed by extensions.
7
+ *
8
+ * Consumes `dock.left | dock.right | dock.bottom` slot contributions
9
+ * and renders each as a tabbed panel. The body of each tab loads the
10
+ * referenced widget JSON from the contributing bundle and renders it
11
+ * via `WidgetRenderer`.
12
+ *
13
+ * Each dock slot is rendered separately so the caller can place them
14
+ * around the viewport layout independently. Empty slots render
15
+ * nothing (no chrome, no overhead).
16
+ *
17
+ * Spec: docs/architecture/ai-customization/03-ui-surface.md §3.
18
+ */
19
+
20
+ import { useEffect, useMemo, useState } from 'react';
21
+ import {
22
+ parseWhen,
23
+ evaluateWhen,
24
+ validateWidget,
25
+ type DockContribution,
26
+ type SlotContribution,
27
+ type WhenContext,
28
+ } from '@ifc-lite/extensions';
29
+ import { useSlotContributions } from '@/hooks/useSlotContributions';
30
+ import { useExtensionHost } from '@/sdk/ExtensionHostProvider';
31
+ import { useViewerStore } from '@/store';
32
+ import { WidgetRenderer, type WidgetRendererContext } from './widget/WidgetRenderer';
33
+ import { WidgetErrorBoundary } from './widget/WidgetErrorBoundary';
34
+ import { ScrollArea } from '@/components/ui/scroll-area';
35
+ import { toast } from '@/components/ui/toast';
36
+ import { describeRunCommandError } from '@/services/extensions/runtime-errors';
37
+ import { cn } from '@/lib/utils';
38
+
39
+ interface ExtensionDockHostProps {
40
+ slot: DockContribution['slot'];
41
+ /** Tailwind class to apply to the container. */
42
+ className?: string;
43
+ }
44
+
45
+ export function ExtensionDockHost({ slot, className }: ExtensionDockHostProps) {
46
+ const contributions = useSlotContributions<DockContribution>(slot);
47
+ // Derive the when-clause context from live viewer state so
48
+ // contributions can key on selection / model presence. Future
49
+ // additions (schema, viewer.open, embed flag) thread through here.
50
+ const modelLoaded = useViewerStore((s) => s.models.size > 0);
51
+ // `selectedEntityIds` is the primary selection set — same source
52
+ // `ExtensionToolbarSlot` reads, so `selection.count` evaluates
53
+ // consistently across every extension surface.
54
+ const selectionCount = useViewerStore((s) => s.selectedEntityIds.size);
55
+ const whenContext = useMemo<WhenContext>(
56
+ () => ({ 'model.loaded': modelLoaded, 'selection.count': selectionCount }),
57
+ [modelLoaded, selectionCount],
58
+ );
59
+ const visible = useFiltered(contributions, whenContext);
60
+ const [activeId, setActiveId] = useState<string | undefined>(visible[0]?.payload.id);
61
+
62
+ useEffect(() => {
63
+ if (!visible.find((v) => v.payload.id === activeId)) {
64
+ setActiveId(visible[0]?.payload.id);
65
+ }
66
+ }, [visible, activeId]);
67
+
68
+ if (visible.length === 0) return null;
69
+
70
+ const active = visible.find((v) => v.payload.id === activeId) ?? visible[0];
71
+
72
+ return (
73
+ <div className={cn('flex flex-col h-full border-t bg-background', className)} role="region" aria-label={`Extension dock (${slot})`}>
74
+ <div className="flex items-center gap-0 border-b overflow-x-auto" role="tablist">
75
+ {visible.map((c) => {
76
+ const isActive = c.payload.id === active.payload.id;
77
+ return (
78
+ <button
79
+ key={`${c.extensionId}:${c.payload.id}`}
80
+ type="button"
81
+ onClick={() => setActiveId(c.payload.id)}
82
+ role="tab"
83
+ aria-selected={isActive}
84
+ aria-controls={`dock-panel-${c.payload.id}`}
85
+ className={cn(
86
+ 'shrink-0 px-3 py-1.5 text-xs font-medium border-b-2 transition-colors',
87
+ isActive
88
+ ? 'border-primary text-primary'
89
+ : 'border-transparent text-muted-foreground hover:text-foreground',
90
+ )}
91
+ title={`${c.payload.title} — ${c.extensionId}`}
92
+ >
93
+ {c.payload.title}
94
+ </button>
95
+ );
96
+ })}
97
+ </div>
98
+ <ScrollArea className="flex-1">
99
+ <DockBody contribution={active} />
100
+ </ScrollArea>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function useFiltered(
106
+ contributions: SlotContribution<DockContribution>[],
107
+ whenContext: WhenContext,
108
+ ): SlotContribution<DockContribution>[] {
109
+ return useMemo(() => {
110
+ return contributions.filter((c) => {
111
+ if (!c.payload.when) return true;
112
+ const parsed = parseWhen(c.payload.when);
113
+ if (!parsed.ok) return false;
114
+ return evaluateWhen(parsed.value, whenContext);
115
+ });
116
+ }, [contributions, whenContext]);
117
+ }
118
+
119
+ function DockBody({ contribution }: { contribution: SlotContribution<DockContribution> }) {
120
+ const host = useExtensionHost();
121
+ const [widget, setWidget] = useState<unknown>();
122
+ const [error, setError] = useState<string | undefined>();
123
+ const ctx: WidgetRendererContext = useMemo(
124
+ () => ({
125
+ state: {},
126
+ invokeCommand: (commandId: string) => {
127
+ host.runCommand(commandId).catch((err) => {
128
+ console.warn('[ExtensionDockHost] command failed:', err);
129
+ toast.error(describeRunCommandError(commandId, err));
130
+ });
131
+ },
132
+ }),
133
+ [host],
134
+ );
135
+
136
+ useEffect(() => {
137
+ let cancelled = false;
138
+ void (async () => {
139
+ try {
140
+ const bundle = host.loader.getBundle(contribution.extensionId);
141
+ if (!bundle) {
142
+ if (!cancelled) setError(`Bundle for ${contribution.extensionId} not loaded.`);
143
+ return;
144
+ }
145
+ const file = bundle.files.get(contribution.payload.widget);
146
+ if (!file) {
147
+ if (!cancelled) setError(`Widget "${contribution.payload.widget}" not found in bundle.`);
148
+ return;
149
+ }
150
+ const text = file.text ?? new TextDecoder().decode(file.bytes);
151
+ const json = JSON.parse(text);
152
+ // Validate the shape before handing it to the renderer so we
153
+ // surface a clean structured error instead of a deep crash.
154
+ const validated = validateWidget(json, contribution.payload.widget);
155
+ if (!validated.ok) {
156
+ const first = validated.errors[0];
157
+ if (!cancelled) setError(`Widget ${first?.path || ''} ${first?.message ?? 'failed validation'}`);
158
+ return;
159
+ }
160
+ if (!cancelled) setWidget(validated.value);
161
+ } catch (err) {
162
+ if (!cancelled) setError(err instanceof Error ? err.message : String(err));
163
+ }
164
+ })();
165
+ return () => {
166
+ cancelled = true;
167
+ };
168
+ }, [host, contribution]);
169
+
170
+ if (error) {
171
+ return (
172
+ <div className="p-3 text-xs text-rose-600 dark:text-rose-400">
173
+ {error}
174
+ </div>
175
+ );
176
+ }
177
+ if (!widget) {
178
+ return <div className="p-3 text-xs text-muted-foreground">Loading widget…</div>;
179
+ }
180
+ return (
181
+ <div className="p-3">
182
+ <WidgetErrorBoundary
183
+ label={`${contribution.extensionId}/${contribution.payload.widget}`}
184
+ >
185
+ <WidgetRenderer
186
+ node={widget as Parameters<typeof WidgetRenderer>[0]['node']}
187
+ ctx={ctx}
188
+ />
189
+ </WidgetErrorBoundary>
190
+ </div>
191
+ );
192
+ }
@@ -0,0 +1,106 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * `ExtensionToolbarSlot` — render extension toolbar contributions.
7
+ *
8
+ * Reads contributions from the slot registry. Each contribution
9
+ * declares a slot (`toolbar.left | toolbar.right | toolbar.center`)
10
+ * and a command id. Clicking dispatches through the host's command
11
+ * runner.
12
+ *
13
+ * Ordering: by `order` ascending; ties broken by command id alpha.
14
+ * Visibility: respects the `when` clause if present (evaluated against
15
+ * a minimal viewer context — model loaded / selection count / etc).
16
+ */
17
+
18
+ import { useMemo } from 'react';
19
+ import type { ResolvedToolbarContribution } from '@ifc-lite/extensions';
20
+ import { evaluateWhen, parseWhen } from '@ifc-lite/extensions';
21
+ import { Button } from '@/components/ui/button';
22
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
23
+ import { toast } from '@/components/ui/toast';
24
+ import { describeRunCommandError } from '@/services/extensions/runtime-errors';
25
+ import { useSlotContributions } from '@/hooks/useSlotContributions';
26
+ import { useOptionalExtensionHost } from '@/sdk/ExtensionHostProvider';
27
+ import { useViewerStore } from '@/store';
28
+ import { resolveExtensionIcon } from './icon-registry';
29
+
30
+ interface ExtensionToolbarSlotProps {
31
+ slot: 'toolbar.left' | 'toolbar.right' | 'toolbar.center';
32
+ }
33
+
34
+ export function ExtensionToolbarSlot({ slot }: ExtensionToolbarSlotProps) {
35
+ const host = useOptionalExtensionHost();
36
+ // Loader enriches the toolbar payload with the linked command's
37
+ // `icon` + `title` — see manifestToContributions in loader.ts.
38
+ const contributions = useSlotContributions<ResolvedToolbarContribution>(slot);
39
+
40
+ const models = useViewerStore((s) => s.models);
41
+ const selectedCount = useViewerStore((s) => s.selectedEntityIds.size);
42
+
43
+ const whenContext = useMemo(() => ({
44
+ 'model.loaded': models.size > 0,
45
+ 'model.schema': undefined,
46
+ 'model.count': models.size,
47
+ 'selection.count': selectedCount,
48
+ 'selection.type': undefined,
49
+ 'viewer.open': true,
50
+ desktop: false,
51
+ embed: false,
52
+ }), [models.size, selectedCount]);
53
+
54
+ const visible = useMemo(() => {
55
+ return contributions
56
+ .filter((c) => {
57
+ const when = c.payload.when;
58
+ if (!when) return true;
59
+ const parsed = parseWhen(when);
60
+ if (!parsed.ok) return false;
61
+ return evaluateWhen(parsed.value, whenContext);
62
+ })
63
+ .sort((a, b) => {
64
+ const oa = a.payload.order ?? 100;
65
+ const ob = b.payload.order ?? 100;
66
+ if (oa !== ob) return oa - ob;
67
+ return a.payload.command.localeCompare(b.payload.command);
68
+ });
69
+ }, [contributions, whenContext]);
70
+
71
+ if (visible.length === 0 || !host) return null;
72
+
73
+ const handleClick = (commandId: string) => {
74
+ void host.dispatcher
75
+ .fire(`onCommand:${commandId}` as `onCommand:${string}`)
76
+ .then(() => host.runCommand(commandId))
77
+ .catch((err) => {
78
+ toast.error(describeRunCommandError(commandId, err));
79
+ });
80
+ };
81
+
82
+ return (
83
+ <div className="flex items-center gap-1">
84
+ {visible.map((c) => {
85
+ const cmd = c.payload.command;
86
+ const title = c.payload.title ?? cmd;
87
+ const Icon = resolveExtensionIcon(c.payload.icon);
88
+ return (
89
+ <Tooltip key={`${c.extensionId}:${cmd}`}>
90
+ <TooltipTrigger asChild>
91
+ <Button
92
+ size="icon"
93
+ variant="ghost"
94
+ onClick={() => handleClick(cmd)}
95
+ aria-label={`Run ${title}`}
96
+ >
97
+ <Icon className="h-4 w-4" />
98
+ </Button>
99
+ </TooltipTrigger>
100
+ <TooltipContent>{title}</TooltipContent>
101
+ </Tooltip>
102
+ );
103
+ })}
104
+ </div>
105
+ );
106
+ }