@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.
- package/dist/backend.d.ts +2 -2
- package/dist/collapsibleSectionState.js +30 -12
- package/dist/domain.d.ts +1 -0
- package/dist/domain.js +2 -1
- package/dist/frontend.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +10 -4
- package/dist/markdown.d.ts +9 -0
- package/dist/markdown.js +57 -1
- package/dist/mermaid.d.ts +13 -0
- package/dist/mermaid.js +221 -0
- package/dist/mermaidZoom.d.ts +23 -0
- package/dist/mermaidZoom.js +52 -0
- package/dist/openforgePackageMetadataSchema.json +2 -11
- package/dist/publicEntrypoints.d.mts +27 -0
- package/dist/publicEntrypoints.mjs +104 -0
- package/dist/publicUiExports.mjs +12 -22
- package/dist/registryValidation.d.mts +11 -0
- package/dist/registryValidation.mjs +30 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +131 -0
- package/dist/taskBrowserDevToolsShortcuts.d.ts +12 -0
- package/dist/taskBrowserDevToolsShortcuts.js +20 -0
- package/dist/testing/commonApiFake.js +240 -7
- package/dist/testing/contracts.d.ts +15 -3
- package/dist/testing/frontendContributionFake.d.ts +2 -3
- package/dist/testing/frontendContributionFake.js +0 -32
- package/dist/testing/support.d.ts +5 -2
- package/dist/testing/support.js +12 -1
- package/dist/types.d.ts +91 -20
- package/dist/types.js +1 -1
- package/dist/ui/MarkdownContent.svelte +46 -0
- package/dist/ui/MermaidDiagramPreview.svelte +216 -0
- package/dist/ui/Modal.svelte +39 -4
- package/dist/ui/PluginPageHeader.svelte +11 -4
- package/dist/ui/PluginPageShell.svelte +20 -0
- package/dist/ui/ProjectFileTree.svelte +192 -0
- package/dist/vite.js +7 -17
- 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 {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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.
|
|
92
|
-
"marked": "^18.0.
|
|
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.
|
|
99
|
-
"svelte": "5.56.
|
|
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
|
-
"
|
|
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",
|