@openforge-app/plugin-sdk 0.2.9 → 0.2.11

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 (41) hide show
  1. package/dist/backend.d.ts +2 -2
  2. package/dist/collapsibleSectionState.js +30 -12
  3. package/dist/domain.d.ts +1 -0
  4. package/dist/domain.js +2 -1
  5. package/dist/frontend.d.ts +2 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +1 -1
  8. package/dist/manifest.d.ts +1 -0
  9. package/dist/manifest.js +10 -4
  10. package/dist/markdown.d.ts +9 -0
  11. package/dist/markdown.js +57 -1
  12. package/dist/mermaid.d.ts +13 -0
  13. package/dist/mermaid.js +221 -0
  14. package/dist/mermaidZoom.d.ts +23 -0
  15. package/dist/mermaidZoom.js +52 -0
  16. package/dist/openforgePackageMetadataSchema.json +2 -11
  17. package/dist/publicEntrypoints.d.mts +27 -0
  18. package/dist/publicEntrypoints.mjs +104 -0
  19. package/dist/publicUiExports.mjs +12 -22
  20. package/dist/registryValidation.d.mts +11 -0
  21. package/dist/registryValidation.mjs +30 -0
  22. package/dist/sanitize.d.ts +2 -0
  23. package/dist/sanitize.js +131 -0
  24. package/dist/taskBrowserDevToolsShortcuts.d.ts +12 -0
  25. package/dist/taskBrowserDevToolsShortcuts.js +20 -0
  26. package/dist/testing/commonApiFake.js +240 -7
  27. package/dist/testing/contracts.d.ts +15 -3
  28. package/dist/testing/frontendContributionFake.d.ts +2 -3
  29. package/dist/testing/frontendContributionFake.js +0 -32
  30. package/dist/testing/support.d.ts +5 -2
  31. package/dist/testing/support.js +12 -1
  32. package/dist/types.d.ts +91 -20
  33. package/dist/types.js +1 -1
  34. package/dist/ui/MarkdownContent.svelte +46 -0
  35. package/dist/ui/MermaidDiagramPreview.svelte +216 -0
  36. package/dist/ui/Modal.svelte +39 -4
  37. package/dist/ui/PluginPageHeader.svelte +11 -4
  38. package/dist/ui/PluginPageShell.svelte +20 -0
  39. package/dist/ui/ProjectFileTree.svelte +192 -0
  40. package/dist/vite.js +7 -17
  41. package/package.json +14 -6
@@ -0,0 +1,192 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte'
3
+ import FileTypeIcon from './FileTypeIcon.svelte'
4
+ import {
5
+ buildProjectFileTree,
6
+ flattenVisibleProjectFileTree,
7
+ formatProjectFileTreeSize,
8
+ getProjectFileTreeDepth,
9
+ getProjectFileTreeItemAccessibility,
10
+ getProjectFileTreeKeyboardAction,
11
+ projectFileTreePathToId,
12
+ type ProjectFileTreeNode,
13
+ } from '../projectFileTree'
14
+ import type { FileEntry } from '../domain'
15
+
16
+ interface Props {
17
+ entries: FileEntry[]
18
+ expandedDirs: Set<string>
19
+ selectedPath: string | null
20
+ onToggleDir: (path: string) => void
21
+ onSelectFile: (path: string) => void
22
+ initialScrollTop?: number
23
+ onScrollTopChange?: (scrollTop: number) => void
24
+ focusSelectedRequest?: number | null
25
+ }
26
+
27
+ type TreeNode = ProjectFileTreeNode<FileEntry>
28
+
29
+ const {
30
+ entries,
31
+ expandedDirs,
32
+ selectedPath,
33
+ onToggleDir,
34
+ onSelectFile,
35
+ initialScrollTop = 0,
36
+ onScrollTopChange,
37
+ focusSelectedRequest = null,
38
+ }: Props = $props()
39
+
40
+ let scrollContainer = $state<HTMLDivElement | null>(null)
41
+ let appliedInitialScrollTop = $state<number | null>(null)
42
+ let focusedPath = $state<string | null>(null)
43
+ let lastSelectedPath = $state<string | null>(null)
44
+ let appliedFocusSelectedRequest = $state<number | null>(null)
45
+
46
+ const treeNodes = $derived(buildProjectFileTree(entries))
47
+ const visibleNodes = $derived(flattenVisibleProjectFileTree(treeNodes, expandedDirs))
48
+ const visiblePaths = $derived(visibleNodes.map((node) => node.entry.path))
49
+
50
+ function getTreeItemElement(path: string): HTMLElement | null {
51
+ const index = visiblePaths.indexOf(path)
52
+ if (index === -1) return null
53
+ return scrollContainer?.querySelector<HTMLElement>(`[data-tree-index="${index}"]`) ?? null
54
+ }
55
+
56
+ async function focusPath(path: string) {
57
+ focusedPath = path
58
+ await tick()
59
+ getTreeItemElement(path)?.focus()
60
+ }
61
+
62
+ function activateNode(node: TreeNode) {
63
+ if (node.entry.isDir) {
64
+ void focusPath(node.entry.path)
65
+ onToggleDir(node.entry.path)
66
+ } else {
67
+ onSelectFile(node.entry.path)
68
+ }
69
+ }
70
+
71
+ function handleKeydown(event: KeyboardEvent, node: TreeNode) {
72
+ const action = getProjectFileTreeKeyboardAction(event, node, { expandedDirs, visiblePaths })
73
+ if (!action.handled) return
74
+
75
+ event.preventDefault()
76
+ event.stopPropagation()
77
+
78
+ switch (action.type) {
79
+ case 'activate':
80
+ activateNode(node)
81
+ break
82
+ case 'focus':
83
+ void focusPath(action.path)
84
+ break
85
+ case 'toggle':
86
+ onToggleDir(action.path)
87
+ break
88
+ case 'none':
89
+ break
90
+ }
91
+ }
92
+
93
+ function handleScroll() {
94
+ if (scrollContainer) {
95
+ onScrollTopChange?.(scrollContainer.scrollTop)
96
+ }
97
+ }
98
+
99
+ $effect(() => {
100
+ if (scrollContainer && appliedInitialScrollTop !== initialScrollTop) {
101
+ scrollContainer.scrollTop = initialScrollTop
102
+ appliedInitialScrollTop = initialScrollTop
103
+ }
104
+ })
105
+
106
+ $effect(() => {
107
+ const selectedChanged = selectedPath !== lastSelectedPath
108
+
109
+ if (selectedChanged && selectedPath !== null && visiblePaths.includes(selectedPath)) {
110
+ focusedPath = selectedPath
111
+ } else if (focusedPath === null || !visiblePaths.includes(focusedPath)) {
112
+ focusedPath = selectedPath !== null && visiblePaths.includes(selectedPath) ? selectedPath : visiblePaths[0] ?? null
113
+ }
114
+
115
+ lastSelectedPath = selectedPath
116
+ })
117
+
118
+ $effect(() => {
119
+ if (focusSelectedRequest === null || appliedFocusSelectedRequest === focusSelectedRequest) return
120
+ appliedFocusSelectedRequest = focusSelectedRequest
121
+ if (selectedPath !== null && visiblePaths.includes(selectedPath)) {
122
+ void focusPath(selectedPath)
123
+ }
124
+ })
125
+ </script>
126
+
127
+ <div class="flex h-full flex-col border-r border-base-300 bg-base-100">
128
+ <div
129
+ class="flex-1 overflow-y-auto py-2"
130
+ bind:this={scrollContainer}
131
+ onscroll={handleScroll}
132
+ role="tree"
133
+ aria-label="Project files"
134
+ >
135
+ {#snippet renderNodes(nodes: TreeNode[])}
136
+ {#each nodes as node (node.entry.path)}
137
+ {@const entry = node.entry}
138
+ {@const isExpanded = expandedDirs.has(entry.path)}
139
+ {@const isSelected = selectedPath === entry.path}
140
+ {@const treeIndex = visiblePaths.indexOf(entry.path)}
141
+ {@const labelId = `${projectFileTreePathToId(entry.path)}-label`}
142
+ {@const sizeId = `${projectFileTreePathToId(entry.path)}-size`}
143
+ {@const a11y = getProjectFileTreeItemAccessibility(node, { expandedDirs, selectedPath, labelId, sizeId })}
144
+ <div
145
+ class="outline-none [&:focus-visible>div:first-child]:ring-2 [&:focus-visible>div:first-child]:ring-inset [&:focus-visible>div:first-child]:ring-primary/60"
146
+ role="treeitem"
147
+ tabindex={focusedPath === entry.path ? 0 : -1}
148
+ aria-level={a11y.level}
149
+ aria-setsize={a11y.setSize}
150
+ aria-posinset={a11y.posInSet}
151
+ aria-expanded={a11y.expanded}
152
+ aria-current={a11y.current}
153
+ aria-selected={a11y.selected}
154
+ aria-labelledby={a11y.labelledBy}
155
+ data-testid="tree-entry"
156
+ data-tree-index={treeIndex}
157
+ onclick={(event) => {
158
+ event.stopPropagation()
159
+ activateNode(node)
160
+ }}
161
+ onkeydown={(event) => handleKeydown(event, node)}
162
+ onfocus={() => {
163
+ focusedPath = entry.path
164
+ }}
165
+ >
166
+ <div
167
+ class="w-full flex items-center gap-2 text-xs cursor-pointer transition-colors py-1.5 pr-3 {entry.isDir ? 'text-base-content hover:bg-base-content/5' : isSelected ? 'bg-primary/10 text-primary font-medium border-l-2 border-l-primary hover:bg-primary/15' : 'text-base-content hover:bg-base-content/5'}"
168
+ style="padding-left: {entry.isDir || !isSelected ? 12 + getProjectFileTreeDepth(entry.path) * 16 : 10 + getProjectFileTreeDepth(entry.path) * 16}px"
169
+ >
170
+ {#if entry.isDir}
171
+ <span class="text-[0.6rem] text-base-content/50 shrink-0" data-testid={`dir-indicator-${entry.path}`} aria-hidden="true">{isExpanded ? '▼' : '▶'}</span>
172
+ <FileTypeIcon folder open={isExpanded} class="w-3.5 h-3.5" />
173
+ <span id={labelId} class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-left" data-testid="entry-label">{entry.name}/</span>
174
+ {:else}
175
+ <FileTypeIcon filename={entry.path} class="w-3.5 h-3.5" />
176
+ <span id={labelId} class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-left" data-testid="entry-label">{entry.name}</span>
177
+ <span id={sizeId} class="text-base-content/50 text-[0.7rem] ml-auto">{formatProjectFileTreeSize(entry.size)}</span>
178
+ {/if}
179
+ </div>
180
+
181
+ {#if entry.isDir && isExpanded && node.children.length > 0}
182
+ <div role="group">
183
+ {@render renderNodes(node.children)}
184
+ </div>
185
+ {/if}
186
+ </div>
187
+ {/each}
188
+ {/snippet}
189
+
190
+ {@render renderNodes(treeNodes)}
191
+ </div>
192
+ </div>
package/dist/vite.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * plugin://host-runtime assets are derived from the same host-runtime contract.
5
5
  */
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
- import { OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS } from './publicUiExports.mjs';
7
+ import { OPENFORGE_PLUGIN_SDK_PUBLIC_ENTRYPOINTS } from './publicEntrypoints.mjs';
8
8
  import { OPENFORGE_HOST_RUNTIME_SVELTE_SPECIFIERS as HOST_RUNTIME_SVELTE_SPECIFIERS } from './svelteHostRuntimeContract.mjs';
9
9
  export const OPENFORGE_HOST_RUNTIME_SVELTE_SPECIFIERS = HOST_RUNTIME_SVELTE_SPECIFIERS;
10
10
  export const OPENFORGE_HOST_SHARED_SVELTE_IMPORTS = OPENFORGE_HOST_RUNTIME_SVELTE_SPECIFIERS;
@@ -26,22 +26,12 @@ export function isOpenForgeHostRuntimeExternal(id) {
26
26
  }
27
27
  export const openforgePluginViteExternals = isOpenForgeHostRuntimeExternal;
28
28
  const OPENFORGE_PLUGIN_SDK_SOURCE_ENTRYPOINTS = Object.freeze([
29
- ['@openforge-app/plugin-sdk/frontend', 'packages/plugin-sdk/src/frontend.ts'],
30
- ['@openforge-app/plugin-sdk/backend', 'packages/plugin-sdk/src/backend.ts'],
31
- ['@openforge-app/plugin-sdk/testing', 'packages/plugin-sdk/src/testing.ts'],
32
- ['@openforge-app/plugin-sdk/vite', 'packages/plugin-sdk/src/vite.ts'],
33
- ['@openforge-app/plugin-sdk/package-metadata-schema.json', 'packages/plugin-sdk/src/openforgePackageMetadataSchema.json'],
34
- ['@openforge-app/plugin-sdk/domain', 'packages/plugin-sdk/src/domain.ts'],
35
- ['@openforge-app/plugin-sdk/prStatusPresentation', 'packages/plugin-sdk/src/prStatusPresentation.ts'],
36
- ['@openforge-app/plugin-sdk/markdown', 'packages/plugin-sdk/src/markdown.ts'],
37
- ['@openforge-app/plugin-sdk/numberParsing', 'packages/plugin-sdk/src/numberParsing.ts'],
38
- ['@openforge-app/plugin-sdk/projectFileTree', 'packages/plugin-sdk/src/projectFileTree.ts'],
39
- ['@openforge-app/plugin-sdk/sanitize', 'packages/plugin-sdk/src/sanitize.ts'],
40
- ['@openforge-app/plugin-sdk/pluginIcons', 'packages/plugin-sdk/src/pluginIcons.ts'],
41
- ['@openforge-app/plugin-sdk/fileIcons', 'packages/plugin-sdk/src/fileIcons.ts'],
42
- ['@openforge-app/plugin-sdk/collapsibleSectionState', 'packages/plugin-sdk/src/collapsibleSectionState.ts'],
43
- ...OPENFORGE_PLUGIN_SDK_PUBLIC_UI_EXPORTS.map(({ importSpecifier, workspaceSourcePath }) => [importSpecifier, workspaceSourcePath]),
44
- ['@openforge-app/plugin-sdk', 'packages/plugin-sdk/src/index.ts'],
29
+ ...OPENFORGE_PLUGIN_SDK_PUBLIC_ENTRYPOINTS
30
+ .filter(({ packageSubpath }) => packageSubpath !== '.')
31
+ .map(({ importSpecifier, workspaceSourcePath }) => [importSpecifier, workspaceSourcePath]),
32
+ ...OPENFORGE_PLUGIN_SDK_PUBLIC_ENTRYPOINTS
33
+ .filter(({ packageSubpath }) => packageSubpath === '.')
34
+ .map(({ importSpecifier, workspaceSourcePath }) => [importSpecifier, workspaceSourcePath]),
45
35
  ]);
46
36
  function repoRootUrl(repoRoot) {
47
37
  if (repoRoot instanceof URL) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openforge-app/plugin-sdk",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -68,15 +68,21 @@
68
68
  "types": "./dist/collapsibleSectionState.d.ts",
69
69
  "default": "./dist/collapsibleSectionState.js"
70
70
  },
71
+ "./taskBrowserDevToolsShortcuts": {
72
+ "types": "./dist/taskBrowserDevToolsShortcuts.d.ts",
73
+ "default": "./dist/taskBrowserDevToolsShortcuts.js"
74
+ },
71
75
  "./ui/Button.svelte": "./dist/ui/Button.svelte",
72
76
  "./ui/Checkbox.svelte": "./dist/ui/Checkbox.svelte",
73
77
  "./ui/MarkdownContent.svelte": "./dist/ui/MarkdownContent.svelte",
74
78
  "./ui/ResizablePanel.svelte": "./dist/ui/ResizablePanel.svelte",
75
79
  "./ui/Modal.svelte": "./dist/ui/Modal.svelte",
76
80
  "./ui/PluginPageHeader.svelte": "./dist/ui/PluginPageHeader.svelte",
81
+ "./ui/PluginPageShell.svelte": "./dist/ui/PluginPageShell.svelte",
77
82
  "./ui/PluginViewState.svelte": "./dist/ui/PluginViewState.svelte",
78
83
  "./ui/PluginSidebarLink.svelte": "./dist/ui/PluginSidebarLink.svelte",
79
84
  "./ui/FileTypeIcon.svelte": "./dist/ui/FileTypeIcon.svelte",
85
+ "./ui/ProjectFileTree.svelte": "./dist/ui/ProjectFileTree.svelte",
80
86
  "./ui/CollapsibleSection.svelte": "./dist/ui/CollapsibleSection.svelte"
81
87
  },
82
88
  "files": [
@@ -88,15 +94,16 @@
88
94
  "access": "public"
89
95
  },
90
96
  "dependencies": {
91
- "dompurify": "^3.4.13",
92
- "marked": "^18.0.10"
97
+ "dompurify": "^3.4.14",
98
+ "marked": "^18.0.11",
99
+ "mermaid": "^11.17.2"
93
100
  },
94
101
  "peerDependencies": {
95
102
  "svelte": "^5.0.0"
96
103
  },
97
104
  "devDependencies": {
98
- "@types/node": "26.2.0",
99
- "svelte": "5.56.9",
105
+ "@types/node": "26.3.0",
106
+ "svelte": "5.56.10",
100
107
  "semver": "^7.8.5",
101
108
  "typescript": "^7.0.2",
102
109
  "vitest": "^4.1.11",
@@ -104,7 +111,8 @@
104
111
  },
105
112
  "scripts": {
106
113
  "clean": "node -e \"import('node:fs').then(({ rmSync }) => rmSync('dist', { recursive: true, force: true }))\"",
107
- "build": "pnpm run clean && tsc && pnpm run build:assets",
114
+ "check:entrypoints": "node ./scripts/check-entrypoint-registries.mjs",
115
+ "build": "pnpm run check:entrypoints && pnpm run clean && tsc && pnpm run build:assets",
108
116
  "build:assets": "node ./scripts/copy-package-assets.mjs",
109
117
  "check:contract": "node ./scripts/check-published-contract.mjs",
110
118
  "icons:generate": "node ./scripts/generate-file-type-icons.mjs",