Árbol de archivos en Worker
Cómo está construido este ejemplo
Enlace al encabezadoEsta página muestra un árbol de archivos anidado basado en Worker sin mocks. La página de docs conserva el DOM host y el pointer hit-testing, mientras la app Vue remota mantiene la estructura del árbol, la edición inline y la lógica de drag dentro de un Web Worker dedicado.
El ejemplo tiene tres partes:
- un host runtime que monta
HostedTreedentro del área de preview; - una app remota en Worker que renderiza carpetas, archivos, acciones inline y procesa
RemoteSortableEvent; - un snapshot en vivo bajo el árbol que muestra el estado remoto anidado después de cada edición y drop.
Cómo interactuar
Enlace al encabezado- Arrastra nodos desde el handle para reordenar hermanos o moverlos a otra carpeta.
- Renombra elementos inline, agrega archivos o carpetas y elimina nodos del árbol actual.
- Usa
Reiniciar árbolpara restaurar la estructura inicial del workspace.
Árbol de archivos sortable y editable en un Worker dedicado
Arrastra archivos y carpetas desde su handle, mueve nodos entre carpetas, renombra elementos inline, crea nuevas entradas y reinicia el workspace cuando quieras volver al estado inicial.
Iniciando runtime dedicado en Worker...
Este árbol está conectado mediante el mismo modelo de transporte host/worker que usan los runtime API públicos de la librería.
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; }}