Worker 文件树
这个示例由哪些部分组成
Section titled “这个示例由哪些部分组成”这个页面展示的是完整的 Worker 嵌套文件树场景,而不是模拟预览。docs 页面负责 host DOM 和指针命中测试,remote Vue app 则在专用 Web Worker 中保存树结构、inline 编辑状态和 drag 逻辑。
这个示例包含三部分:
- 把
HostedTree挂到预览区域中的 host runtime; - 渲染文件夹、文件、inline 操作并处理
RemoteSortableEvent的 remote Worker 应用; - 位于树下方的实时 snapshot,用来显示每次编辑和 drop 之后的当前嵌套 remote state。
- 通过 handle 拖拽节点来重排同级节点或把它们移动到其他文件夹。
- 直接 inline 重命名项目、添加文件或文件夹,并从当前树中删除节点。
- 使用
重置树可以恢复初始 workspace 结构。
运行在专用 Worker 中的可排序可编辑文件树
通过 handle 拖拽文件和文件夹,在文件夹之间移动节点,inline 编辑名称,创建新的条目,并在需要时将 workspace 恢复到初始状态。
正在启动专用 Worker runtime...
这个文件树接入的正是库对外公开 runtime API 所使用的同一套 host/worker transport 模型。
import type { Channel } from '@omnicajs/vue-remote/host'
import { createEndpoint, fromWebWorker } from '@remote-ui/rpc'import { createApp, h,} from 'vue'
import { HostedTree, createProvider, createReceiver,} from '@omnicajs/vue-remote/host'
interface SandboxWorkerApi { release (): void; run (channel: Channel, labels: SortableFileTreeSandboxLabels): Promise<void>;}
export interface SortableFileTreeSandboxLabels { addFileLabel: string; addFolderLabel: string; booting: string; cancelLabel: string; collapseLabel: string; deleteLabel: string; dragCanceledAction: string; draggingAction: string; emptyFolderLabel: string; expandLabel: string; failed: string; fileAddedAction: string; fileKindLabel: string; folderAddedAction: string; folderKindLabel: string; initialAction: string; movedAction: string; newFileName: string; newFolderName: string; ready: string; removedAction: string; renameLabel: string; renamedAction: string; resetLabel: string; rootEyebrow: string; saveLabel: string; snapshotLabel: string; snapshotUnavailable?: string; unsupported: string;}
export interface SortableFileTreeSandboxElements { rootElement: HTMLElement;}
export interface SortableFileTreeSandboxHandle { destroy (): Promise<void>;}
export const mountSortableFileTreeSandbox = async ( elements: SortableFileTreeSandboxElements, labels: SortableFileTreeSandboxLabels): Promise<SortableFileTreeSandboxHandle> => { const { rootElement } = elements
if (typeof Worker === 'undefined') { rootElement.textContent = labels.unsupported
return { async destroy () {}, } }
rootElement.textContent = labels.booting
const receiver = createReceiver() const provider = createProvider() const host = createApp({ render: () => h(HostedTree, { provider, receiver }), }) const worker = new Worker(new URL('./remote.worker.ts', import.meta.url), { type: 'module', }) const endpoint = createEndpoint<SandboxWorkerApi>(fromWebWorker(worker)) let destroyed = false
const destroy = async () => { if (destroyed) { return }
destroyed = true
try { await endpoint.call.release() } catch { // Worker teardown should not block page cleanup. } finally { endpoint.terminate() worker.terminate() host.unmount() rootElement.innerHTML = '' } }
try { host.mount(rootElement) await endpoint.call.run(receiver.receive, labels) } catch (error) { await destroy() rootElement.textContent = `${labels.failed}: ${error instanceof Error ? error.message : String(error)}` }
return { destroy, }}import type { MessageEndpoint } from '@remote-ui/rpc'import type { Channel } from '@omnicajs/vue-remote/host'import type { RemoteSortableEvent } from '@omnicajs/vue-remote/remote'
import { createEndpoint, release, retain,} from '@remote-ui/rpc'import { RemoteDragHandle, RemoteSortableContainer, RemoteSortableItem, createRemoteRenderer, createRemoteRoot,} from '@omnicajs/vue-remote/remote'import { defineComponent, h, ref,} from 'vue'
type FolderContainerId = `children:${string}`
interface FileNode { id: string; kind: 'file'; name: string;}
interface FolderNode { children: FileTreeNode[]; expanded: boolean; id: string; kind: 'folder'; name: string;}
type FileTreeNode = FileNode | FolderNode
interface FileTreeSnapshot { lastAction: string; tree: Array<{ children?: FileTreeSnapshot['tree']; id: string; kind: 'file' | 'folder'; name: string; }>;}
interface SandboxWorkerApi { release (): void; run (channel: Channel, labels: SandboxUiLabels): Promise<void>;}
interface SandboxUiLabels { addFileLabel: string; addFolderLabel: string; booting: string; cancelLabel: string; collapseLabel: string; deleteLabel: string; dragCanceledAction: string; draggingAction: string; emptyFolderLabel: string; expandLabel: string; failed: string; fileAddedAction: string; fileKindLabel: string; folderAddedAction: string; folderKindLabel: string; initialAction: string; movedAction: string; newFileName: string; newFolderName: string; ready: string; removedAction: string; renameLabel: string; renamedAction: string; resetLabel: string; rootEyebrow: string; saveLabel: string; snapshotLabel: string; snapshotUnavailable?: string; unsupported: string;}
const endpoint = createEndpoint<SandboxWorkerApi>(self as unknown as MessageEndpoint)
const ROOT_CONTAINER_ID = 'children:root' satisfies FolderContainerId
const initialTree = (): FileTreeNode[] => ([ { children: [ { children: [ { id: 'file-host-ts', kind: 'file', name: 'host.ts', }, { id: 'file-remote-ts', kind: 'file', name: 'remote.ts', }, ], expanded: true, id: 'folder-vue', kind: 'folder', name: 'vue', }, { children: [], expanded: true, id: 'folder-fixtures', kind: 'folder', name: 'fixtures', }, { id: 'file-dnd-ts', kind: 'file', name: 'dnd.ts', }, ], expanded: true, id: 'folder-src', kind: 'folder', name: 'src', }, { children: [ { id: 'file-introduction-mdx', kind: 'file', name: 'introduction.mdx', }, { id: 'file-sortable-mdx', kind: 'file', name: 'sortable-dnd.mdx', }, ], expanded: true, id: 'folder-docs', kind: 'folder', name: 'docs', }, { id: 'file-package-json', kind: 'file', name: 'package.json', },])
const isFolder = (node: FileTreeNode): node is FolderNode => node.kind === 'folder'
const cloneNode = (node: FileTreeNode): FileTreeNode => { if (!isFolder(node)) { return { ...node } }
return { ...node, children: node.children.map(cloneNode), }}
const cloneSnapshot = (nodes: FileTreeNode[], lastAction: string): FileTreeSnapshot => ({ lastAction, tree: nodes.map(node => { if (!isFolder(node)) { return { id: node.id, kind: node.kind, name: node.name, } }
return { children: cloneSnapshot(node.children, lastAction).tree, id: node.id, kind: node.kind, name: node.name, } }),})
const replaceNode = ( nodes: FileTreeNode[], id: string, updater: (node: FileTreeNode) => FileTreeNode): FileTreeNode[] => { return nodes.map(node => { if (node.id === id) { return updater(node) }
if (!isFolder(node)) { return node }
return { ...node, children: replaceNode(node.children, id, updater), } })}
const removeNode = ( nodes: FileTreeNode[], id: string): { nextNodes: FileTreeNode[]; removed: FileTreeNode | null;} => { let removed: FileTreeNode | null = null const nextNodes: FileTreeNode[] = []
nodes.forEach(node => { if (node.id === id) { removed = node return }
if (!isFolder(node)) { nextNodes.push(node) return }
const nested = removeNode(node.children, id)
if (nested.removed != null) { removed = nested.removed nextNodes.push({ ...node, children: nested.nextNodes, }) return }
nextNodes.push(node) })
return { nextNodes, removed, }}
const insertNode = ( nodes: FileTreeNode[], folderId: string, index: number, node: FileTreeNode): FileTreeNode[] => { if (folderId === 'root') { const nextNodes = [...nodes] nextNodes.splice(Math.min(Math.max(index, 0), nextNodes.length), 0, node) return nextNodes }
return nodes.map(entry => { if (!isFolder(entry)) { return entry }
if (entry.id === folderId) { const nextChildren = [...entry.children] nextChildren.splice(Math.min(Math.max(index, 0), nextChildren.length), 0, node)
return { ...entry, children: nextChildren, expanded: true, } }
return { ...entry, children: insertNode(entry.children, folderId, index, node), } })}
const containerFolderId = (containerId: string) => { return containerId === ROOT_CONTAINER_ID ? 'root' : containerId.replace(/^children:/, '')}
const nodeContainsFolder = (node: FileTreeNode, folderId: string): boolean => { if (!isFolder(node)) { return false }
if (node.id === folderId) { return true }
return node.children.some(child => nodeContainsFolder(child, folderId))}
const rows = ref<FileTreeNode[]>(initialTree())const editingId = ref<string | null>(null)const editingName = ref('')const lastAction = ref('Manage folders and files, then drag nodes to reorder or move them.')
let nextNodeIndex = 1let uiLabels: SandboxUiLabels = { addFileLabel: 'Add file', addFolderLabel: 'Add folder', booting: '', cancelLabel: 'Cancel', collapseLabel: 'Collapse', deleteLabel: 'Delete', dragCanceledAction: 'Drag canceled.', draggingAction: 'Dragging node.', emptyFolderLabel: 'Drop items here', expandLabel: 'Expand', failed: '', fileAddedAction: 'File created.', fileKindLabel: 'File', folderAddedAction: 'Folder created.', folderKindLabel: 'Folder', initialAction: 'Manage folders and files, then drag nodes to reorder or move them.', movedAction: 'Node moved.', newFileName: 'new-file.ts', newFolderName: 'new-folder', ready: '', removedAction: 'Node removed.', renameLabel: 'Rename', renamedAction: 'Name updated.', resetLabel: 'Reset tree', rootEyebrow: 'Workspace tree', saveLabel: 'Save', snapshotLabel: 'Snapshot', unsupported: '',}
let releaseRemote = () => {}
const createNodeId = (prefix: 'file' | 'folder') => { nextNodeIndex += 1 return `${prefix}-${nextNodeIndex}`}
const clearEditing = () => { editingId.value = null editingName.value = ''}
const setRows = (nextRows: FileTreeNode[]) => { rows.value = nextRows.map(cloneNode)}
const startRename = (node: FileTreeNode) => { editingId.value = node.id editingName.value = node.name}
const commitRename = (nodeId: string) => { const nextName = editingName.value.trim()
if (nextName.length === 0) { return }
setRows(replaceNode(rows.value, nodeId, node => ({ ...node, name: nextName, }))) clearEditing() lastAction.value = uiLabels.renamedAction}
const toggleFolder = (folderId: string) => { setRows(replaceNode(rows.value, folderId, node => { if (!isFolder(node)) { return node }
return { ...node, expanded: !node.expanded, } }))}
const addNode = (parentFolderId: string, kind: 'file' | 'folder') => { const nextNode: FileTreeNode = kind === 'file' ? { id: createNodeId('file'), kind: 'file', name: uiLabels.newFileName, } : { children: [], expanded: true, id: createNodeId('folder'), kind: 'folder', name: uiLabels.newFolderName, }
setRows(insertNode(rows.value, parentFolderId, Number.MAX_SAFE_INTEGER, nextNode)) startRename(nextNode) lastAction.value = kind === 'file' ? uiLabels.fileAddedAction : uiLabels.folderAddedAction}
const deleteNode = (nodeId: string) => { const removed = removeNode(rows.value, nodeId)
if (removed.removed == null) { return }
setRows(removed.nextNodes)
if (editingId.value === nodeId) { clearEditing() }
lastAction.value = uiLabels.removedAction}
const resetTree = () => { nextNodeIndex = 1 clearEditing() rows.value = initialTree() lastAction.value = uiLabels.initialAction}
const setDragStart = () => { lastAction.value = uiLabels.draggingAction}
const setDragCancel = () => { lastAction.value = uiLabels.dragCanceledAction}
const moveNode = (event: RemoteSortableEvent) => { if (!event.accepted || event.targetContainerId == null || event.targetIndex == null) { return }
const sourceRemoval = removeNode(rows.value, event.itemId) const removedNode = sourceRemoval.removed
if (removedNode == null) { return }
const targetFolderId = containerFolderId(event.targetContainerId)
if ( isFolder(removedNode) && targetFolderId !== 'root' && nodeContainsFolder(removedNode, targetFolderId) ) { lastAction.value = uiLabels.dragCanceledAction return }
setRows(insertNode(sourceRemoval.nextNodes, targetFolderId, event.targetIndex, removedNode)) lastAction.value = uiLabels.movedAction}
const gripIcon = () => h('svg', { 'aria-hidden': 'true', class: 'sandbox-tree__grip', fill: 'none', viewBox: '0 0 8 12',}, [ h('circle', { cx: '1.5', cy: '1.5', fill: 'currentColor', r: '1' }), h('circle', { cx: '6.5', cy: '1.5', fill: 'currentColor', r: '1' }), h('circle', { cx: '1.5', cy: '6', fill: 'currentColor', r: '1' }), h('circle', { cx: '6.5', cy: '6', fill: 'currentColor', r: '1' }), h('circle', { cx: '1.5', cy: '10.5', fill: 'currentColor', r: '1' }), h('circle', { cx: '6.5', cy: '10.5', fill: 'currentColor', r: '1' }),])
const icon = (path: string, viewBox = '0 0 16 16') => h('svg', { 'aria-hidden': 'true', class: 'sandbox-tree__action-icon', fill: 'none', viewBox,}, [ h('path', { d: path, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '1.5', stroke: 'currentColor', }),])
const addFileIcon = () => icon('M8 3.25v9.5M3.25 8h9.5')const addFolderIcon = () => h('svg', { 'aria-hidden': 'true', class: 'sandbox-tree__action-icon', fill: 'none', viewBox: '0 0 16 16',}, [ h('path', { d: 'M2.75 5.25h3l1.1 1.5h6.4v5.5H2.75z', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '1.4', stroke: 'currentColor', }), h('path', { d: 'M8 8.25v3.5M6.25 10h3.5', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '1.4', stroke: 'currentColor', }),])const renameIcon = () => icon('M3 11.75l2.9-.65 5.55-5.55-2.25-2.25-5.55 5.55L3 11.75zM8.4 4.35l2.25 2.25')const deleteIcon = () => icon('M5.5 5.5v5M8 5.5v5M10.5 5.5v5M4.5 3.5h7M6 3.5v-1h4v1M4.75 3.5l.5 8h5.5l.5-8')const saveIcon = () => icon('M3.5 8.25l2.6 2.6 6.4-6.4')const cancelIcon = () => icon('M4.25 4.25l7.5 7.5M11.75 4.25l-7.5 7.5')const folderIcon = (title: string) => h('svg', { 'aria-label': title, class: 'sandbox-tree__node-icon', fill: 'none', role: 'img', title, viewBox: '0 0 16 16',}, [ h('path', { d: 'M2.5 5.25h3.1l1.2 1.55h6.7v4.95H2.5z', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '1.5', stroke: 'currentColor', }),])const fileIcon = (title: string) => h('svg', { 'aria-label': title, class: 'sandbox-tree__node-icon', fill: 'none', role: 'img', title, viewBox: '0 0 16 16',}, [ h('path', { d: 'M5 2.75h4.5l2 2v8.5H5z', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '1.5', stroke: 'currentColor', }), h('path', { d: 'M9.5 2.75v2h2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '1.5', stroke: 'currentColor', }),])
const TreeApp = defineComponent({ name: 'WorkerSortableFileTreeSandbox',
setup () { const renderRowActions = (node: FileTreeNode) => { if (editingId.value === node.id) { return [ h('button', { 'aria-label': uiLabels.saveLabel, class: 'sandbox-tree__action', 'data-testid': `sandbox-save-${node.id}`, onClick: () => commitRename(node.id), title: uiLabels.saveLabel, type: 'button', }, saveIcon()), h('button', { 'aria-label': uiLabels.cancelLabel, class: 'sandbox-tree__action', 'data-testid': `sandbox-cancel-${node.id}`, onClick: clearEditing, title: uiLabels.cancelLabel, type: 'button', }, cancelIcon()), ] }
return [ isFolder(node) ? h('button', { 'aria-label': uiLabels.addFileLabel, class: 'sandbox-tree__action', 'data-testid': `sandbox-add-file-${node.id}`, onClick: () => addNode(node.id, 'file'), title: uiLabels.addFileLabel, type: 'button', }, addFileIcon()) : null, isFolder(node) ? h('button', { 'aria-label': uiLabels.addFolderLabel, class: 'sandbox-tree__action', 'data-testid': `sandbox-add-folder-${node.id}`, onClick: () => addNode(node.id, 'folder'), title: uiLabels.addFolderLabel, type: 'button', }, addFolderIcon()) : null, h('button', { 'aria-label': uiLabels.renameLabel, class: 'sandbox-tree__action', 'data-testid': `sandbox-rename-${node.id}`, onClick: () => startRename(node), title: uiLabels.renameLabel, type: 'button', }, renameIcon()), h('button', { 'aria-label': uiLabels.deleteLabel, class: 'sandbox-tree__action sandbox-tree__action--danger', 'data-testid': `sandbox-delete-${node.id}`, onClick: () => deleteNode(node.id), title: uiLabels.deleteLabel, type: 'button', }, deleteIcon()), ].filter(Boolean) }
const renderNode = ( node: FileTreeNode, containerId: FolderContainerId, depth: number, index: number ): ReturnType<typeof h> => { const nodeClassName = `sandbox-tree__node sandbox-tree__node--${node.kind}` const rowClassName = `sandbox-tree__row sandbox-tree__row--${node.kind}` const childrenContainerId = `children:${node.id}` as FolderContainerId
return h(RemoteSortableItem, { as: 'div', class: nodeClassName, containerId, 'data-testid': `sandbox-node-${node.id}`, index, itemId: node.id, onDragcancel: setDragCancel, onDragstart: setDragStart, style: { '--tree-depth': String(depth), }, type: 'fs-node', }, { default: () => [ h('div', { class: rowClassName, 'data-testid': `sandbox-row-${node.id}`, }, [ h(RemoteDragHandle, { as: 'button', 'aria-label': `Drag ${node.name}`, class: 'sandbox-tree__handle', 'data-testid': `sandbox-handle-${node.id}`, type: 'button', }, { default: gripIcon, }), h('div', { class: 'sandbox-tree__row-main' }, [ isFolder(node) ? h('button', { 'aria-label': node.expanded ? uiLabels.collapseLabel : uiLabels.expandLabel, class: 'sandbox-tree__toggle', 'data-testid': `sandbox-toggle-${node.id}`, onClick: () => toggleFolder(node.id), type: 'button', }, node.expanded ? '−' : '+') : null, isFolder(node) ? folderIcon(uiLabels.folderKindLabel) : fileIcon(uiLabels.fileKindLabel), editingId.value === node.id ? h('input', { class: 'sandbox-tree__input', 'data-testid': `sandbox-input-${node.id}`, onInput: (event: Event) => { const target = event.target
if ( typeof target === 'object' && target != null && 'value' in target && typeof target.value === 'string' ) { editingName.value = target.value } }, onKeydown: (event: KeyboardEvent) => { if (event.key === 'Enter') { event.preventDefault() commitRename(node.id) }
if (event.key === 'Escape') { event.preventDefault() clearEditing() } }, value: editingName.value, }) : h('div', { class: 'sandbox-tree__meta' }, [ h('span', { class: 'sandbox-tree__name' }, node.name), ]), ]), h('div', { class: 'sandbox-tree__row-actions' }, renderRowActions(node)), ]), isFolder(node) && node.expanded ? h(RemoteSortableContainer, { as: 'div', accepts: ['fs-node'], class: node.children.length === 0 ? 'sandbox-tree__list sandbox-tree__children sandbox-tree__children--empty' : 'sandbox-tree__list sandbox-tree__children', containerId: childrenContainerId, 'data-testid': `sandbox-children-${node.id}`, onDrop: moveNode, orientation: 'vertical', }, { default: () => node.children.length > 0 ? node.children.map((child, childIndex) => renderNode(child, childrenContainerId, depth + 1, childIndex)) : [ h('div', { class: 'sandbox-tree__empty', 'data-testid': `sandbox-empty-${node.id}`, }, uiLabels.emptyFolderLabel), ], }) : null, ], }) }
return () => h('div', { class: 'worker-file-tree-sandbox__runtime' }, [ h('div', { class: 'worker-file-tree-sandbox__toolbar' }, [ h('span', { class: 'worker-file-tree-sandbox__status', 'data-sandbox-status': '', }, uiLabels.ready), h('button', { class: 'worker-file-tree-sandbox__reset', 'data-sandbox-reset': '', onClick: resetTree, type: 'button', }, uiLabels.resetLabel), ]), h('div', { class: 'worker-file-tree-sandbox__preview' }, [ h('div', { class: 'sandbox-tree' }, [ h('div', { class: 'sandbox-tree__toolbar' }, [ h('p', { class: 'sandbox-tree__eyebrow' }, uiLabels.rootEyebrow), h('p', { class: 'sandbox-tree__caption' }, lastAction.value), h('div', { class: 'sandbox-tree__actions' }, [ h('button', { 'aria-label': uiLabels.addFileLabel, class: 'sandbox-tree__toolbar-action', 'data-testid': 'sandbox-root-add-file', onClick: () => addNode('root', 'file'), title: uiLabels.addFileLabel, type: 'button', }, addFileIcon()), h('button', { 'aria-label': uiLabels.addFolderLabel, class: 'sandbox-tree__toolbar-action', 'data-testid': 'sandbox-root-add-folder', onClick: () => addNode('root', 'folder'), title: uiLabels.addFolderLabel, type: 'button', }, addFolderIcon()), ]), ]), h(RemoteSortableContainer, { as: 'div', accepts: ['fs-node'], class: 'sandbox-tree__list sandbox-tree__list--root', containerId: ROOT_CONTAINER_ID, 'data-testid': 'sandbox-tree-root', onDrop: moveNode, orientation: 'vertical', }, { default: () => rows.value.map((node, index) => renderNode(node, ROOT_CONTAINER_ID, 0, index)), }), ]), ]), h('div', { class: 'worker-file-tree-sandbox__snapshot', 'data-sandbox-snapshot-wrap': '', }, [ h('div', { class: 'worker-file-tree-sandbox__snapshot-title' }, uiLabels.snapshotLabel), h('pre', { class: 'worker-file-tree-sandbox__snapshot-code', 'data-sandbox-snapshot': '', }, JSON.stringify(cloneSnapshot(rows.value, lastAction.value), null, 2)), ]), ]) },})
endpoint.expose({ async run (channel: Channel, labels: SandboxUiLabels) { uiLabels = labels retain(channel)
const root = createRemoteRoot(channel) await root.mount()
const app = createRemoteRenderer(root).createApp(TreeApp) app.mount(root)
releaseRemote = () => { app.unmount() release(channel) } },
release () { releaseRemote() releaseRemote = () => {} },}).worker-file-tree-sandbox { --worker-sandbox-border: color-mix(in srgb, var(--sl-color-hairline-light) 78%, transparent); --worker-tree-branch: color-mix(in srgb, var(--sl-color-hairline-light) 84%, transparent); --worker-tree-surface-fill: color-mix(in srgb, var(--sl-color-bg) 92%, var(--sl-color-accent-low)); --worker-sandbox-surface: color-mix(in srgb, var(--sl-color-bg) 92%, var(--sl-color-accent-low)); margin-block: 2rem; width: 100%; max-width: 100%; min-width: 0;}
.worker-file-tree-sandbox__layout { display: grid; gap: 1.5rem; width: 100%; max-width: 100%; min-width: 0;}
.worker-file-tree-sandbox__surface { display: grid; gap: 1rem; width: 100%; max-width: 100%; min-width: 0;}
.worker-file-tree-sandbox__heading { display: grid; gap: 0.45rem; margin-bottom: 1rem;}
.worker-file-tree-sandbox__heading h2 { margin: 0; font-size: clamp(1.3rem, 1.15rem + 0.5vw, 1.7rem);}
.worker-file-tree-sandbox__heading p,.worker-file-tree-sandbox__note { margin: 0; color: var(--sl-color-gray-2);}
.worker-file-tree-sandbox__toolbar { display: flex; gap: 0.75rem; align-items: center; justify-content: space-between; margin-bottom: 1rem;}
.worker-file-tree-sandbox__status { color: var(--sl-color-gray-2); font-size: 0.92rem; font-weight: 600;}
.worker-file-tree-sandbox__reset { border: 1px solid var(--worker-sandbox-border); border-radius: 999px; background: color-mix(in srgb, var(--sl-color-white) 7%, transparent); color: var(--sl-color-text); padding: 0.55rem 0.95rem; font: inherit; font-weight: 700; cursor: pointer; transition: transform 140ms ease, border-color 140ms ease;}
.worker-file-tree-sandbox__reset:hover,.worker-file-tree-sandbox__reset:focus-visible { transform: translateY(-1px); border-color: color-mix(in srgb, var(--sl-color-accent-high) 50%, var(--worker-sandbox-border));}
.worker-file-tree-sandbox__runtime-host { min-height: 30rem; width: 100%; max-width: 100%; min-width: 0;}
.worker-file-tree-sandbox__runtime { display: grid; gap: 1rem; width: 100%; max-width: 100%; min-width: 0;}
.worker-file-tree-sandbox__preview { min-height: 30rem; width: 100%; max-width: 100%; min-width: 0;}
.worker-file-tree-sandbox__note { margin-top: 0.85rem; width: min(100%, var(--sl-content-width)); max-width: var(--sl-content-width); justify-self: start;}
.worker-file-tree-sandbox__snapshot { margin-top: 1rem; width: min(100%, var(--sl-content-width)); max-width: var(--sl-content-width); min-width: 0; justify-self: start;}
.worker-file-tree-sandbox__snapshot-title { margin-bottom: 0.45rem; font-size: 0.82rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--sl-color-gray-2);}
.worker-file-tree-sandbox__snapshot-code,.worker-file-tree-sandbox__code .expressive-code { margin: 0;}
.worker-file-tree-sandbox__code { display: grid; gap: 1rem; width: min(100%, var(--sl-content-width)); max-width: var(--sl-content-width); min-width: 0; justify-self: start;}
.worker-file-tree-sandbox__snapshot-code { overflow: auto; padding: 1rem; border-radius: 0.5rem; background: color-mix(in srgb, var(--sl-color-black) 12%, var(--sl-color-bg)); color: var(--sl-color-white); font-size: 0.82rem; line-height: 1.55;}
.worker-file-tree-sandbox__code .tablist-wrapper { margin-bottom: 1rem; max-width: 100%; min-width: 0; overflow-x: auto;}
.worker-file-tree-sandbox__code starlight-tabs,.worker-file-tree-sandbox__code [role='tablist'],.worker-file-tree-sandbox__code [role='tabpanel'] { min-width: 0; max-width: 100%;}
.worker-file-tree-sandbox__code starlight-tabs { display: grid; min-width: 0;}
.worker-file-tree-sandbox__code [role='tabpanel'] { margin-top: 0;}
.worker-file-tree-sandbox__code .expressive-code,.worker-file-tree-sandbox__code .expressive-code pre { max-width: 100%; min-width: 0;}
.worker-file-tree-sandbox .sandbox-tree { display: grid; gap: 1rem; padding: 1rem; margin: 0; border: 1px solid var(--worker-sandbox-border); border-radius: 0.5rem; background: radial-gradient(circle at top right, color-mix(in srgb, #34d399 10%, transparent), transparent 32%), linear-gradient(180deg, color-mix(in srgb, var(--worker-sandbox-surface) 96%, transparent), var(--sl-color-bg)); box-shadow: 0 1rem 2rem -1.5rem color-mix(in srgb, var(--sl-color-black) 25%, transparent);}
.worker-file-tree-sandbox .sandbox-tree__toolbar { display: grid; gap: 0.65rem; margin: 0;}
.worker-file-tree-sandbox .sandbox-tree__eyebrow { margin: 0; color: var(--sl-color-text-accent); font-size: 0.76rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase;}
.worker-file-tree-sandbox .sandbox-tree__caption { margin: 0; color: var(--sl-color-gray-2); font-size: 0.95rem; font-weight: 600;}
.worker-file-tree-sandbox .sandbox-tree__actions { display: flex; flex-wrap: nowrap; align-items: center; gap: 0.55rem; margin: 0; min-width: 0; overflow-x: auto;}
.worker-file-tree-sandbox .sandbox-tree__list { display: grid; gap: 0; margin: 0; min-width: 0;}
.worker-file-tree-sandbox .sandbox-tree__children { position: relative; margin-top: 0; margin-left: 1.15rem; padding-left: 0.85rem; padding-top: 0.15rem;}
.worker-file-tree-sandbox .sandbox-tree__children::before { content: ''; position: absolute; top: 0.15rem; bottom: 0.35rem; left: 0; width: 1px; background: var(--worker-tree-branch);}
.worker-file-tree-sandbox .sandbox-tree__children--empty { display: grid; gap: 0; margin: 0; min-height: 3rem; padding-block: 0.35rem;}
.worker-file-tree-sandbox .sandbox-tree__empty { display: grid; place-items: center start; min-height: 2.65rem; margin: 0; padding: 0.65rem 0.8rem; border: 1px dashed color-mix(in srgb, var(--sl-color-accent-high) 22%, var(--worker-sandbox-border)); border-radius: 0.5rem; color: var(--sl-color-gray-2); font-size: 0.84rem; font-weight: 600;}
.worker-file-tree-sandbox .sandbox-tree__children--empty[data-dnd-drag-over='true'][data-dnd-drop-allowed='true'] .sandbox-tree__empty { border-color: color-mix(in srgb, var(--sl-color-accent-high) 54%, var(--worker-sandbox-border)); background: color-mix(in srgb, var(--sl-color-accent-low) 18%, transparent); color: var(--sl-color-text-accent);}
.worker-file-tree-sandbox .sandbox-tree__children--empty [data-dnd-placeholder='true'] { visibility: hidden;}
.worker-file-tree-sandbox .sandbox-tree__node { display: grid; gap: 0; margin: 0; position: relative;}
.worker-file-tree-sandbox .sandbox-tree__children > .sandbox-tree__node::after { content: ''; position: absolute; top: 1.15rem; left: -0.85rem; width: 0.75rem; height: 0.7rem; border-left: 1px solid var(--worker-tree-branch); border-bottom: 1px solid var(--worker-tree-branch); border-bottom-left-radius: 0.4rem;}
.worker-file-tree-sandbox .sandbox-tree__children > .sandbox-tree__node:last-child::before { content: ''; position: absolute; top: calc(1.15rem + 0.7rem); bottom: -0.35rem; left: -0.85rem; width: 1px; background: var(--worker-tree-surface-fill);}
.worker-file-tree-sandbox .sandbox-tree__row,[data-dnd-overlay='true'] .sandbox-tree__row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.55rem; align-items: center; margin: 0; padding: 0.42rem 0.4rem; border: 0; border-radius: 0.55rem; background: transparent; box-shadow: none; min-width: 0; transition: background-color 140ms ease;}
.worker-file-tree-sandbox .sandbox-tree__row:hover,.worker-file-tree-sandbox .sandbox-tree__row:focus-within { background: color-mix(in srgb, var(--sl-color-accent-high) 4%, transparent);}
.worker-file-tree-sandbox .sandbox-tree__row--folder .sandbox-tree__name,[data-dnd-overlay='true'] .sandbox-tree__row--folder .sandbox-tree__name { color: color-mix(in srgb, #0f766e 82%, var(--sl-color-text));}
.worker-file-tree-sandbox .sandbox-tree__row-main,[data-dnd-overlay='true'] .sandbox-tree__row-main { display: flex; align-items: center; gap: 0.6rem; margin: 0; min-width: 0;}
.worker-file-tree-sandbox .sandbox-tree__toggle,.worker-file-tree-sandbox .sandbox-tree__action,.worker-file-tree-sandbox .sandbox-tree__handle,.worker-file-tree-sandbox .sandbox-tree__toolbar-action,[data-dnd-overlay='true'] .sandbox-tree__handle { margin: 0; border: 0; border-radius: 0.55rem; background: transparent; color: var(--sl-color-text); font: inherit; cursor: pointer; transition: background-color 140ms ease, color 140ms ease, opacity 140ms ease; flex: none;}
.worker-file-tree-sandbox .sandbox-tree__toggle,.worker-file-tree-sandbox .sandbox-tree__handle,[data-dnd-overlay='true'] .sandbox-tree__handle,.worker-file-tree-sandbox .sandbox-tree__action,.worker-file-tree-sandbox .sandbox-tree__toolbar-action { display: inline-grid; place-items: center; width: 1.9rem; height: 1.9rem; padding: 0;}
.worker-file-tree-sandbox .sandbox-tree__toggle:hover,.worker-file-tree-sandbox .sandbox-tree__toggle:focus-visible,.worker-file-tree-sandbox .sandbox-tree__action:hover,.worker-file-tree-sandbox .sandbox-tree__action:focus-visible,.worker-file-tree-sandbox .sandbox-tree__handle:hover,.worker-file-tree-sandbox .sandbox-tree__handle:focus-visible,.worker-file-tree-sandbox .sandbox-tree__toolbar-action:hover,.worker-file-tree-sandbox .sandbox-tree__toolbar-action:focus-visible,[data-dnd-overlay='true'] .sandbox-tree__handle { background: color-mix(in srgb, var(--sl-color-accent-high) 8%, transparent); color: var(--sl-color-text-accent);}
.worker-file-tree-sandbox .sandbox-tree__handle,[data-dnd-overlay='true'] .sandbox-tree__handle { cursor: grab;}
.worker-file-tree-sandbox .sandbox-tree__grip,[data-dnd-overlay='true'] .sandbox-tree__grip { display: block; width: 0.72rem; height: 1.08rem; color: color-mix(in srgb, var(--sl-color-gray-3) 88%, var(--sl-color-text));}
.worker-file-tree-sandbox .sandbox-tree__node-icon,[data-dnd-overlay='true'] .sandbox-tree__node-icon { flex: none; width: 1rem; height: 1rem; margin: 0; color: color-mix(in srgb, var(--sl-color-gray-2) 85%, var(--sl-color-text));}
.worker-file-tree-sandbox .sandbox-tree__meta,[data-dnd-overlay='true'] .sandbox-tree__meta { display: grid; margin: 0; min-width: 0;}
.worker-file-tree-sandbox .sandbox-tree__name,[data-dnd-overlay='true'] .sandbox-tree__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700;}
.worker-file-tree-sandbox .sandbox-tree__input { width: min(100%, 22rem); min-width: 0; margin: 0; padding: 0.36rem 0.55rem; border: 1px solid color-mix(in srgb, var(--sl-color-accent-high) 28%, var(--worker-sandbox-border)); border-radius: 0.7rem; background: color-mix(in srgb, var(--sl-color-white) 8%, transparent); color: var(--sl-color-text); font: inherit;}
.worker-file-tree-sandbox .sandbox-tree__row-actions,[data-dnd-overlay='true'] .sandbox-tree__row-actions { display: flex; align-items: center; justify-self: end; flex-wrap: nowrap; gap: 0.12rem; margin: 0; min-width: 0; white-space: nowrap;}
.worker-file-tree-sandbox .sandbox-tree__action--danger { color: color-mix(in srgb, #ef4444 78%, var(--sl-color-text));}
.worker-file-tree-sandbox .sandbox-tree__action-icon { width: 0.98rem; height: 0.98rem; margin: 0;}
.worker-file-tree-sandbox [data-dnd-dragging='true'] { visibility: hidden; opacity: 0; pointer-events: none;}
.worker-file-tree-sandbox [data-dnd-drop-forbidden='true'] { opacity: 0.55; filter: grayscale(0.22);}
[data-dnd-overlay='true'] { --worker-sandbox-border: color-mix(in srgb, var(--sl-color-hairline-light) 78%, transparent); position: fixed; top: 0; left: 0; z-index: 9999; width: var(--dnd-overlay-width); pointer-events: none; transform: translate3d(-9999px, -9999px, 0); will-change: transform; color: var(--sl-color-text); font: inherit;}
[data-dnd-overlay='true'] .sandbox-tree__row { position: relative; grid-template-columns: auto minmax(0, 1fr); width: fit-content; min-width: 0; max-width: min(36rem, calc(100vw - 2rem)); border: 1px solid color-mix(in srgb, var(--sl-color-accent-high) 40%, var(--worker-sandbox-border)); border-radius: 0.75rem; background: linear-gradient(180deg, color-mix(in srgb, var(--sl-color-white) 72%, transparent), color-mix(in srgb, var(--sl-color-bg) 98%, var(--sl-color-white))), color-mix(in srgb, var(--sl-color-accent-low) 18%, var(--sl-color-bg)); box-shadow: 0 1.1rem 2.4rem -1rem color-mix(in srgb, var(--sl-color-black) 38%, transparent), 0 0 0 1px color-mix(in srgb, var(--sl-color-white) 52%, transparent) inset; opacity: 1;}
[data-dnd-overlay='true'] .sandbox-tree__children { display: none;}
[data-dnd-overlay='true'] .sandbox-tree__row::after { content: var(--worker-dragging-label); position: absolute; top: -0.55rem; right: 0.55rem; display: inline-flex; align-items: center; padding: 0.18rem 0.55rem; border-radius: 999px; background: color-mix(in srgb, var(--sl-color-accent-high) 18%, var(--sl-color-bg)); border: 1px solid color-mix(in srgb, var(--sl-color-accent-high) 36%, var(--worker-sandbox-border)); color: var(--sl-color-text-accent); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase;}
[data-dnd-overlay='true'] .sandbox-tree__toggle,[data-dnd-overlay='true'] .sandbox-tree__action,[data-dnd-overlay='true'] .sandbox-tree__toolbar-action { pointer-events: none;}
[data-dnd-overlay='true'] .sandbox-tree__row-main { min-width: 0;}
[data-dnd-overlay='true'] .sandbox-tree__grip,[data-dnd-overlay='true'] .sandbox-tree__node-icon,[data-dnd-overlay='true'] .sandbox-tree__action-icon,[data-dnd-overlay='true'] .sandbox-tree__name { opacity: 1; visibility: visible;}
[data-dnd-overlay='true'] .sandbox-tree__grip,[data-dnd-overlay='true'] .sandbox-tree__node-icon,[data-dnd-overlay='true'] .sandbox-tree__name { color: var(--sl-color-text);}
[data-dnd-overlay='true'] .sandbox-tree__handle { cursor: grabbing;}
[data-dnd-overlay='true'] .sandbox-tree__row-actions { display: none;}
.worker-file-tree-sandbox [data-dnd-placeholder='true'] { display: grid; gap: 0; margin: 0;}
.worker-file-tree-sandbox [data-dnd-placeholder='true'] > .sandbox-tree__row { border: 1px dashed color-mix(in srgb, var(--sl-color-accent-high) 46%, var(--worker-sandbox-border)); border-radius: 0.75rem; background: linear-gradient(180deg, color-mix(in srgb, var(--sl-color-accent-low) 18%, transparent), color-mix(in srgb, var(--sl-color-accent-high) 4%, transparent)); box-shadow: none;}
.worker-file-tree-sandbox [data-dnd-placeholder='true'] > .sandbox-tree__children { display: none;}
.worker-file-tree-sandbox [data-dnd-placeholder='true'] .sandbox-tree__row-actions { display: none;}
.worker-file-tree-sandbox [data-dnd-placeholder='true'] > * { opacity: 0.14;}
.worker-file-tree-sandbox [data-dnd-placeholder='true'] .sandbox-tree__grip,.worker-file-tree-sandbox [data-dnd-placeholder='true'] .sandbox-tree__node-icon,.worker-file-tree-sandbox [data-dnd-placeholder='true'] .sandbox-tree__name { opacity: 0.28;}
@media (max-width: 52rem) { .worker-file-tree-sandbox .sandbox-tree__row-actions, [data-dnd-overlay='true'] .sandbox-tree__row-actions { justify-self: end; padding-left: 0; }}