-
(isResizing = true)} />
-
-
diff --git a/src/panel/PropertyList.svelte b/src/panel/PropertyList.svelte
deleted file mode 100644
index 0d33519..0000000
--- a/src/panel/PropertyList.svelte
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
{header}
-
-{#if entries.length}
-
- {#each entries as { key, value } (key)}
- change(key, e.detail)} />
- {/each}
-
-{:else}
-
None
-{/if}
diff --git a/src/profiler/Frame.svelte b/src/profiler/Frame.svelte
deleted file mode 100644
index e6dca49..0000000
--- a/src/profiler/Frame.svelte
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-{#if children}
-
- {#each children as child, i}
-
-
-
-
- {/each}
-
-{/if}
diff --git a/src/profiler/Operation.svelte b/src/profiler/Operation.svelte
deleted file mode 100644
index 24b30c7..0000000
--- a/src/profiler/Operation.svelte
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
dispatch('click', frame)}>
-
- {frame.node.tagName}
-
diff --git a/src/profiler/Profiler.svelte b/src/profiler/Profiler.svelte
deleted file mode 100644
index 07af9e2..0000000
--- a/src/profiler/Profiler.svelte
+++ /dev/null
@@ -1,102 +0,0 @@
-
-
-
-
-
- {#if top}
- (top = null)}>
-
-
-
-
- {:else}
-
- {/if}
- {
- $profileFrame = {}
- top = null
- selected = null
- }}>
-
-
-
-
-
-
- {#if children.length}
-
- {:else}
-
Nothing to display. Perform an action or refresh the page.
- {/if}
-
-{#if selected}
-
-
-
- Tag name
- {selected.node.tagName} (#{selected.node.id})
-
-
Start {round(selected.start)}ms
-
Operation {selected.type}
-
Block type {selected.node.type}
-
End {round(selected.end)}ms
-
- Duration
- {round(selected.children.reduce((acc, o) => acc - o.duration, selected.duration))}ms
- of {round(selected.duration)}ms
-
-
-
-{/if}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
new file mode 100644
index 0000000..02b5166
--- /dev/null
+++ b/src/routes/+layout.svelte
@@ -0,0 +1,178 @@
+
+
+
{
+ if (target !== document.body || !$selected) return;
+
+ if (key === 'Enter') {
+ $selected.expanded = !$selected.expanded;
+ $selected.invalidate();
+ } else if (key === 'ArrowRight') {
+ if (!$selected) $selected = $root[0];
+ $selected.expanded = true;
+ $selected.invalidate();
+ } else if (key === 'ArrowLeft') {
+ $selected.expanded = false;
+ $selected.invalidate();
+ } else if (key === 'ArrowUp') {
+ let nodes = ($selected.parent?.children || $root).filter((n) => $visibility[n.type]);
+ let sibling = nodes[nodes.findIndex((o) => o.id === $selected?.id) - 1];
+ while (sibling?.expanded) {
+ nodes = sibling.children.filter((n) => $visibility[n.type]);
+ sibling = nodes[nodes.length - 1];
+ }
+ $selected = sibling ?? $selected.parent ?? $selected;
+ } else if (key === 'ArrowDown') {
+ const children = $selected.children.filter((n) => $visibility[n.type]);
+
+ if (!$selected.expanded || children.length === 0) {
+ let next = $selected;
+ let current = $selected;
+ do {
+ const nodes = current.parent ? current.parent.children : $root;
+ const siblings = nodes.filter((n) => $visibility[n.type]);
+ const index = siblings.findIndex((o) => o.id === current.id);
+ next = siblings[index + 1];
+ current = current.parent;
+ } while (!next && current);
+
+ $selected = next ?? $selected;
+ } else {
+ $selected = children[0];
+ }
+ }
+ }}
+/>
+
+{#if $root.length}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ chrome.devtools.inspectedWindow.eval('inspect($n).scrollIntoView()')}
+ >
+
+
+
+
+
+
+
+ {#each $root as node (node.id)}
+
+ {/each}
+
+
+
+
+
+
+
+ {@const events = $selected?.detail.listeners?.map((l) => {
+ const suffix = l.modifiers?.length ? `|${l.modifiers.join('|')}` : '';
+ const value = { __is: 'function', source: l.handler };
+ return { key: l.event + suffix, value };
+ })}
+
+ {#if $selected?.type === 'component'}
+ Props
+
+
+
+
+ Events
+
+
+
+
+ State
+
+ {:else if $selected?.type === 'block' || $selected?.type === 'iteration'}
+ State
+
+ {:else if $selected?.type === 'element'}
+ Attributes
+
+
+
+
+ Events
+
+ {/if}
+
+{:else}
+
+{/if}
+
+
diff --git a/src/routes/Breadcrumbs.svelte b/src/routes/Breadcrumbs.svelte
new file mode 100644
index 0000000..6087997
--- /dev/null
+++ b/src/routes/Breadcrumbs.svelte
@@ -0,0 +1,63 @@
+
+
+{#if breadcrumbs.length}
+
+ {#each breadcrumbs as node}
+ {#if $visibility[node.type]}
+ selected.set(node)}
+ on:focus={() => hovered.set(node)}
+ on:mouseover={() => hovered.set(node)}
+ >
+ {node.tagName}
+
+ {/if}
+ {/each}
+
+{/if}
+
+
diff --git a/src/routes/ConnectMessage.svelte b/src/routes/ConnectMessage.svelte
new file mode 100644
index 0000000..753cdb8
--- /dev/null
+++ b/src/routes/ConnectMessage.svelte
@@ -0,0 +1,62 @@
+
+
+
+ Svelte DevTools
+
+ No Svelte app detected
+
+ background.send('page/refresh')}>reload
+
+
+
+ Not working? Did you...
+
+ Build with dev mode enabled?
+ Use Svelte version 4.0.0 or above?
+
+
+
+
+
diff --git a/src/routes/PickerButton.svelte b/src/routes/PickerButton.svelte
new file mode 100644
index 0000000..e792d86
--- /dev/null
+++ b/src/routes/PickerButton.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
diff --git a/src/routes/ProfileButton.svelte b/src/routes/ProfileButton.svelte
new file mode 100644
index 0000000..2ef7830
--- /dev/null
+++ b/src/routes/ProfileButton.svelte
@@ -0,0 +1,18 @@
+
+
+ background.send('ext/profiler', enabled)}>
+ {#if enabled}
+
+
+
+ {:else}
+
+
+
+ {/if}
+
diff --git a/src/routes/Profiler.svelte b/src/routes/Profiler.svelte
new file mode 100644
index 0000000..7ab8ef9
--- /dev/null
+++ b/src/routes/Profiler.svelte
@@ -0,0 +1,124 @@
+
+
+
+ {#if top}
+ (top = null)}>
+
+
+
+
+ {:else}
+
+ {/if}
+ {
+ $profileFrame = undefined;
+ top = null;
+ selected = null;
+ }}
+ >
+
+
+
+
+
+
+ {#if children.length}
+
{
+ if (selected === detail) top = detail;
+ else selected = detail;
+ }}
+ />
+ {:else}
+ Nothing to display. Perform an action or refresh the page.
+ {/if}
+
+{#if selected}
+
+
+
+ Tag name
+ {selected.node.tagName}
+ (#{selected.node.id})
+
+
+ Start
+ {round(selected.start)}ms
+
+
+ Operation
+ {selected.type}
+
+
+ Block type
+ {selected.node.type}
+
+
+ End
+ {round(selected.end)}ms
+
+
+ Duration
+
+ {round(selected.children.reduce((acc, o) => acc - o.duration, selected.duration))}ms
+
+ of
+ {round(selected.duration)}ms
+
+
+
+{/if}
+
+
diff --git a/src/routes/ProfilerFrame.svelte b/src/routes/ProfilerFrame.svelte
new file mode 100644
index 0000000..abc0cb9
--- /dev/null
+++ b/src/routes/ProfilerFrame.svelte
@@ -0,0 +1,63 @@
+
+
+{#if children?.length}
+
+ {#each children as frame}
+
+ dispatch('click', frame)}>
+
+ {frame.node.tagName}
+
+
+ dispatch('click', frame)} />
+
+ {/each}
+
+{/if}
+
+
diff --git a/src/routes/SearchBox.svelte b/src/routes/SearchBox.svelte
new file mode 100644
index 0000000..71d6a4d
--- /dev/null
+++ b/src/routes/SearchBox.svelte
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/src/routes/VisibilitySelection.svelte b/src/routes/VisibilitySelection.svelte
new file mode 100644
index 0000000..f68ffff
--- /dev/null
+++ b/src/routes/VisibilitySelection.svelte
@@ -0,0 +1,76 @@
+
+
+
+ (opened = !opened)}>
+
+
+
+
+
+ {#if opened}
+
+ {/if}
+
+
+
diff --git a/src/store.js b/src/store.js
deleted file mode 100644
index f36f840..0000000
--- a/src/store.js
+++ /dev/null
@@ -1,299 +0,0 @@
-import { writable, get, derived } from 'svelte/store'
-
-export const visibility = writable({
- component: true,
- element: true,
- block: true,
- iteration: true,
- slot: true,
- text: true,
- anchor: false,
-})
-export const selectedNode = writable({})
-export const hoveredNodeId = writable(null)
-export const rootNodes = writable([])
-export const searchValue = writable('')
-export const profilerEnabled = writable(false)
-export const profileFrame = writable({})
-
-// #if process.env.TARGET === 'firefox'
-// Zoom workaround
-let fontSize = 11
-window.addEventListener('keyup', e => {
- if (!e.ctrlKey) return
-
- switch (e.key) {
- case '=':
- fontSize = Math.min(fontSize + 1.1, 22)
- break
- case '-':
- fontSize = Math.max(fontSize - 1.1, 5.5)
- break
- case '0':
- fontSize = 11
- break
- }
-
- document.documentElement.style.fontSize = fontSize + 'px'
-})
-// #endif
-
-function interactableNodes(list) {
- const _visibility = get(visibility)
- return list.filter(
- o => _visibility[o.type] && o.type !== 'text' && o.type !== 'anchor'
- )
-}
-
-window.addEventListener('keydown', e => {
- if (e.target !== document.body) return
-
- selectedNode.update(node => {
- if (node.invalidate === undefined) return node
- switch (e.key) {
- case 'Enter':
- node.collapsed = !node.collapsed
- node.invalidate()
- return node
-
- case 'ArrowRight':
- node.collapsed = false
- node.invalidate()
- return node
-
- case 'ArrowDown': {
- const children = interactableNodes(node.children)
-
- if (node.collapsed || children.length === 0) {
- var next = node
- var current = node
- do {
- const siblings = interactableNodes(
- current.parent === undefined
- ? get(rootNodes)
- : current.parent.children
- )
- const index = siblings.findIndex(o => o.id === current.id)
- next = siblings[index + 1]
-
- current = current.parent
- } while (next === undefined && current !== undefined)
-
- return next ?? node
- } else {
- return children[0]
- }
- }
-
- case 'ArrowLeft':
- node.collapsed = true
- node.invalidate()
- return node
-
- case 'ArrowUp': {
- const siblings = interactableNodes(
- node.parent === undefined ? get(rootNodes) : node.parent.children
- )
- const index = siblings.findIndex(o => o.id === node.id)
- return index > 0 ? siblings[index - 1] : node.parent ?? node
- }
-
- default:
- return node
- }
- })
-})
-
-const nodeMap = new Map()
-
-const port = chrome.runtime.connect()
-port.postMessage({
- type: 'init',
- tabId: chrome.devtools.inspectedWindow.tabId,
-})
-
-export function reload() {
- port.postMessage({
- type: 'reload',
- tabId: chrome.devtools.inspectedWindow.tabId,
- })
-}
-
-export function startPicker() {
- port.postMessage({
- type: 'startPicker',
- tabId: chrome.devtools.inspectedWindow.tabId,
- })
-}
-
-export function stopPicker() {
- port.postMessage({
- type: 'stopPicker',
- tabId: chrome.devtools.inspectedWindow.tabId,
- })
-}
-
-selectedNode.subscribe(node => {
- port.postMessage({
- type: 'setSelected',
- tabId: chrome.devtools.inspectedWindow.tabId,
- nodeId: node.id,
- })
-
- let invalid = null
- while (node.parent) {
- node = node.parent
- if (node.collapsed) {
- invalid = node
- node.collapsed = false
- }
- }
-
- if (invalid) invalid.invalidate()
-})
-
-hoveredNodeId.subscribe(nodeId =>
- port.postMessage({
- type: 'setHover',
- tabId: chrome.devtools.inspectedWindow.tabId,
- nodeId,
- })
-)
-
-profilerEnabled.subscribe(o =>
- port.postMessage({
- type: o ? 'startProfiler' : 'stopProfiler',
- tabId: chrome.devtools.inspectedWindow.tabId,
- })
-)
-
-function noop() {}
-
-function insertNode(node, target, anchorId) {
- node.parent = target
-
- let index = -1
- if (anchorId) index = target.children.findIndex(o => o.id == anchorId)
-
- if (index != -1) {
- target.children.splice(index, 0, node)
- } else {
- target.children.push(node)
- }
-
- target.invalidate()
-}
-
-function resolveFrame(frame) {
- frame.children.forEach(resolveFrame)
-
- if (!frame.node) return
-
- frame.node = nodeMap.get(frame.node) || {
- tagName: 'Unknown',
- type: 'Unknown',
- }
-}
-
-function resolveEventBubble(node) {
- if (!node.detail || !node.detail.listeners) return
-
- for (const listener of node.detail.listeners) {
- if (!listener.handler.includes('bubble($$self, event)')) continue
-
- listener.handler = () => {
- let target = node
- while ((target = target.parent)) if (target.type == 'component') break
-
- const listeners = target.detail.listeners
- if (!listeners) return null
-
- const parentListener = listeners.find(o => o.event == listener.event)
- if (!parentListener) return null
-
- const handler = parentListener.handler
- if (!handler) return null
-
- return (
- '// From parent\n' +
- (typeof handler == 'function' ? handler() : handler)
- )
- }
- }
-}
-
-port.onMessage.addListener(msg => {
- switch (msg.type) {
- case 'clear': {
- selectedNode.set({})
- hoveredNodeId.set(null)
- rootNodes.set([])
-
- break
- }
-
- case 'addNode': {
- const node = msg.node
- node.children = []
- node.collapsed = true
- node.invalidate = noop
- resolveEventBubble(node)
-
- const targetNode = nodeMap.get(msg.target)
- nodeMap.set(node.id, node)
-
- if (targetNode) {
- insertNode(node, targetNode, msg.anchor)
- return
- }
-
- if (node._timeout) return
-
- node._timeout = setTimeout(() => {
- delete node._timeout
- const targetNode = nodeMap.get(msg.target)
- if (targetNode) insertNode(node, targetNode, msg.anchor)
- else rootNodes.update(o => (o.push(node), o))
- }, 100)
-
- break
- }
-
- case 'removeNode': {
- const node = nodeMap.get(msg.node.id)
- const index = node.parent.children.findIndex(o => o.id == node.id)
- node.parent.children.splice(index, 1)
- nodeMap.delete(node.id)
-
- node.parent.invalidate()
-
- break
- }
-
- case 'updateNode': {
- const node = nodeMap.get(msg.node.id)
- Object.assign(node, msg.node)
- resolveEventBubble(node)
-
- const selected = get(selectedNode)
- if (selected && selected.id == msg.node.id) selectedNode.update(o => o)
-
- node.invalidate()
-
- break
- }
-
- case 'inspect': {
- let node = nodeMap.get(msg.node.id)
- selectedNode.set(node)
-
- break
- }
-
- case 'updateProfile': {
- resolveFrame(msg.frame)
- profileFrame.set(msg.frame)
- break
- }
- }
-})
diff --git a/src/toolbar/Button.svelte b/src/toolbar/Button.svelte
deleted file mode 100644
index f59f91f..0000000
--- a/src/toolbar/Button.svelte
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/toolbar/PickerButton.svelte b/src/toolbar/PickerButton.svelte
deleted file mode 100644
index dcfd1d6..0000000
--- a/src/toolbar/PickerButton.svelte
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/toolbar/ProfileButton.svelte b/src/toolbar/ProfileButton.svelte
deleted file mode 100644
index 63b4d04..0000000
--- a/src/toolbar/ProfileButton.svelte
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
- ($profilerEnabled = !$profilerEnabled)}>
- {#if $profilerEnabled}
-
-
-
- {:else}
-
-
-
- {/if}
-
diff --git a/src/toolbar/Search.svelte b/src/toolbar/Search.svelte
deleted file mode 100644
index ba4bb0a..0000000
--- a/src/toolbar/Search.svelte
+++ /dev/null
@@ -1,112 +0,0 @@
-
-
-
-
-
diff --git a/src/toolbar/Toolbar.svelte b/src/toolbar/Toolbar.svelte
deleted file mode 100644
index bf73101..0000000
--- a/src/toolbar/Toolbar.svelte
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
diff --git a/src/toolbar/VisibilityButton.svelte b/src/toolbar/VisibilityButton.svelte
deleted file mode 100644
index 99ebf02..0000000
--- a/src/toolbar/VisibilityButton.svelte
+++ /dev/null
@@ -1,139 +0,0 @@
-
-
-
-
- (isOpen = true)}>
-
-
-
- {#if isOpen}
- (isOpen = false)} />
-
-
- ($visibility.component = !$visibility.component)}>
- Components
-
- ($visibility.element = !$visibility.element)}>
- Elements
-
- ($visibility.block = !$visibility.block)}>
- Blocks
-
- ($visibility.slot = !$visibility.slot)}>
- Slots
-
- ($visibility.anchor = !$visibility.anchor)}>
- Anchors
-
- ($visibility.text = !$visibility.text)}>
- Text
-
-
- {/if}
-
diff --git a/dest/.web-extension-id b/static/.web-extension-id
similarity index 100%
rename from dest/.web-extension-id
rename to static/.web-extension-id
diff --git a/static/background.js b/static/background.js
new file mode 100644
index 0000000..ced758a
--- /dev/null
+++ b/static/background.js
@@ -0,0 +1,132 @@
+/** @type {Map
} */
+const ports = new Map();
+
+chrome.runtime.onConnect.addListener((port) => {
+ if (port.sender?.url !== chrome.runtime.getURL('/index.html')) {
+ console.error(`Unexpected connection from ${port.sender?.url || ''}`);
+ return port.disconnect();
+ }
+
+ // messages are from the devtools page and not content script (courier.js)
+ port.onMessage.addListener((message, sender) => {
+ if (message.type === 'ext/init') {
+ ports.set(message.tabId, sender);
+
+ return chrome.tabs.onUpdated.addListener(courier);
+ } else if (message.type === 'page/refresh') {
+ return chrome.tabs.reload(message.tabId, { bypassCache: true });
+ }
+
+ // relay messages from devtools page to `chrome.scripting`
+ return chrome.tabs.sendMessage(message.tabId, message);
+ });
+
+ port.onDisconnect.addListener((disconnected) => {
+ ports.delete(+disconnected.name);
+
+ if (ports.size === 0) {
+ chrome.tabs.onUpdated.removeListener(courier);
+ }
+ });
+});
+
+// relay messages from `chrome.scripting` to devtools page
+chrome.runtime.onMessage.addListener((message, sender) => {
+ if (sender.id !== chrome.runtime.id) return; // unexpected sender
+
+ if (message.type === 'ext/icon:set') {
+ const selected = message.payload ? 'default' : 'disabled';
+ const icons = [16, 24, 48, 96, 128].map((s) => [s, `icons/${selected}-${s}.png`]);
+ return chrome.action.setIcon({ path: Object.fromEntries(icons) });
+ }
+
+ const port = sender.tab?.id && ports.get(sender.tab.id);
+ if (port) return port.postMessage(message);
+});
+
+/** @type {Parameters[0]} */
+function courier(tabId, changed) {
+ if (!ports.has(tabId) || changed.status !== 'loading') return;
+
+ chrome.scripting.executeScript({
+ target: { tabId },
+ injectImmediately: true,
+
+ // no lexical context, `func` is serialized and deserialized.
+ // a limbo world where both `chrome` and `window` are defined
+ // with many unexpected and out of the ordinary behaviors, do
+ // minimal work here and delegate to `courier.js` in the page.
+ func: () => {
+ const source = chrome.runtime.getURL('/courier.js');
+ if (document.querySelector(`script[src="${source}"]`)) return;
+
+ // attach script manually instead of declaring through `files`
+ // because `detail` in the dispatched custom events is `null`
+ const script = document.createElement('script');
+ script.setAttribute('src', source);
+ document.documentElement.appendChild(script);
+
+ // // TODO: reenable profiler
+ // if (message.type === 'ext/profiler' && message.payload) {
+ // // start profiler
+ // }
+
+ chrome.runtime.onMessage.addListener((message, sender) => {
+ if (sender.id !== chrome.runtime.id) return; // unexpected sender
+ window.postMessage(message); // relay to content script (courier.js)
+
+ // switch (message.type) {
+ // case 'startProfiler':
+ // window.sessionStorage.SvelteDevToolsProfilerEnabled = 'true';
+ // break;
+ // case 'stopProfiler':
+ // case 'ext/clear':
+ // delete window.sessionStorage.SvelteDevToolsProfilerEnabled;
+ // break;
+ // }
+ });
+
+ window.addEventListener('message', ({ source, data }) => {
+ // only accept messages from our application or script
+ if (source === window && data?.source === 'svelte-devtools') {
+ chrome.runtime.sendMessage(data);
+ }
+ });
+
+ window.addEventListener('unload', () => {
+ chrome.runtime.sendMessage({ type: 'ext/clear' });
+ });
+ },
+ });
+}
+
+chrome.tabs.onActivated.addListener(({ tabId }) => sensor(tabId));
+chrome.tabs.onUpdated.addListener(
+ (tabId, changed) => changed.status === 'loading' && sensor(tabId),
+);
+
+/** @param {number} tabId */
+async function sensor(tabId) {
+ try {
+ await chrome.scripting.executeScript({
+ target: { tabId },
+
+ func: () => {
+ const source = chrome.runtime.getURL('/sensor.js');
+ document.querySelector(`script[src="${source}"]`)?.remove();
+ const script = document.createElement('script');
+ script.setAttribute('src', source);
+ document.documentElement.appendChild(script);
+
+ document.addEventListener('SvelteDevTools', ({ detail }) => {
+ chrome.runtime.sendMessage(detail);
+ });
+ },
+ });
+ } catch {
+ // for internal URLs like `chrome://` or `edge://` and extension gallery
+ // https://chromium.googlesource.com/chromium/src/+/ee77a52baa1f8a98d15f9749996f90e9d3200f2d/chrome/common/extensions/chrome_extensions_client.cc#131
+ const icons = [16, 24, 48, 96, 128].map((s) => [s, `icons/disabled-${s}.png`]);
+ chrome.action.setIcon({ path: Object.fromEntries(icons) });
+ }
+}
diff --git a/static/icons/default-128.png b/static/icons/default-128.png
new file mode 100644
index 0000000..e6ee090
Binary files /dev/null and b/static/icons/default-128.png differ
diff --git a/static/icons/default-16.png b/static/icons/default-16.png
new file mode 100644
index 0000000..9b7bb76
Binary files /dev/null and b/static/icons/default-16.png differ
diff --git a/static/icons/default-24.png b/static/icons/default-24.png
new file mode 100644
index 0000000..b5eb83c
Binary files /dev/null and b/static/icons/default-24.png differ
diff --git a/static/icons/default-48.png b/static/icons/default-48.png
new file mode 100644
index 0000000..543a150
Binary files /dev/null and b/static/icons/default-48.png differ
diff --git a/static/icons/default-96.png b/static/icons/default-96.png
new file mode 100644
index 0000000..903efd5
Binary files /dev/null and b/static/icons/default-96.png differ
diff --git a/static/icons/disabled-128.png b/static/icons/disabled-128.png
new file mode 100644
index 0000000..8df184a
Binary files /dev/null and b/static/icons/disabled-128.png differ
diff --git a/static/icons/disabled-16.png b/static/icons/disabled-16.png
new file mode 100644
index 0000000..a3b737f
Binary files /dev/null and b/static/icons/disabled-16.png differ
diff --git a/static/icons/disabled-24.png b/static/icons/disabled-24.png
new file mode 100644
index 0000000..d75f5a2
Binary files /dev/null and b/static/icons/disabled-24.png differ
diff --git a/static/icons/disabled-48.png b/static/icons/disabled-48.png
new file mode 100644
index 0000000..602426f
Binary files /dev/null and b/static/icons/disabled-48.png differ
diff --git a/static/icons/disabled-96.png b/static/icons/disabled-96.png
new file mode 100644
index 0000000..906eccb
Binary files /dev/null and b/static/icons/disabled-96.png differ
diff --git a/dest/devtools/svelte-logo-dark.svg b/static/icons/svelte-dark.svg
similarity index 100%
rename from dest/devtools/svelte-logo-dark.svg
rename to static/icons/svelte-dark.svg
diff --git a/dest/devtools/svelte-logo-light.svg b/static/icons/svelte-default.svg
similarity index 100%
rename from dest/devtools/svelte-logo-light.svg
rename to static/icons/svelte-default.svg
diff --git a/static/icons/svelte-disabled.svg b/static/icons/svelte-disabled.svg
new file mode 100644
index 0000000..43dc3fb
--- /dev/null
+++ b/static/icons/svelte-disabled.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/src/svelte-logo.svg b/static/icons/svelte.svg
similarity index 100%
rename from src/svelte-logo.svg
rename to static/icons/svelte.svg
diff --git a/static/manifest.json b/static/manifest.json
new file mode 100644
index 0000000..27c497b
--- /dev/null
+++ b/static/manifest.json
@@ -0,0 +1,33 @@
+{
+ "manifest_version": 3,
+ "name": "Svelte DevTools",
+ "version": "2.0.0",
+ "description": "Browser DevTools extension for debugging Svelte applications.",
+ "icons": {
+ "16": "icons/default-16.png",
+ "24": "icons/default-24.png",
+ "48": "icons/default-48.png",
+ "96": "icons/default-96.png",
+ "128": "icons/default-128.png"
+ },
+
+ "action": {
+ "default_icon": {
+ "16": "icons/disabled-16.png",
+ "24": "icons/disabled-24.png",
+ "48": "icons/disabled-48.png",
+ "96": "icons/disabled-96.png",
+ "128": "icons/disabled-128.png"
+ }
+ },
+ "background": {
+ "service_worker": "background.js"
+ },
+ "devtools_page": "register.html",
+ "host_permissions": ["*://*/*"],
+ "permissions": ["activeTab", "scripting"],
+ "web_accessible_resources": [
+ { "matches": ["*://*/*"], "resources": ["courier.js"] },
+ { "matches": ["*://*/*"], "resources": ["sensor.js"], "world": "MAIN" }
+ ]
+}
diff --git a/static/register.html b/static/register.html
new file mode 100644
index 0000000..6689014
--- /dev/null
+++ b/static/register.html
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/static/register.js b/static/register.js
new file mode 100644
index 0000000..73cfe9f
--- /dev/null
+++ b/static/register.js
@@ -0,0 +1,12 @@
+chrome.devtools.panels.create(
+ 'Svelte',
+ `icons/svelte-${chrome.devtools.panels.themeName}.svg`,
+ 'index.html',
+ // (panel) => {
+ // panel.onShown.addListener((win) =>
+ // chrome.devtools.inspectedWindow.eval('$0', (payload) =>
+ // win.postMessage({ source: 'svelte-devtools', type: 'ext/inspect', payload }),
+ // ),
+ // );
+ // },
+);
diff --git a/static/sensor.js b/static/sensor.js
new file mode 100644
index 0000000..5255d97
--- /dev/null
+++ b/static/sensor.js
@@ -0,0 +1,10 @@
+(() => {
+ // @ts-ignore - injected if the website is using svelte
+ const [major] = [...(window.__svelte?.v ?? [])];
+
+ document.dispatchEvent(
+ new CustomEvent('SvelteDevTools', {
+ detail: { type: 'ext/icon:set', payload: major },
+ }),
+ );
+})();
diff --git a/svelte.config.js b/svelte.config.js
new file mode 100644
index 0000000..d5a9d0f
--- /dev/null
+++ b/svelte.config.js
@@ -0,0 +1,10 @@
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+export default {
+ preprocess: vitePreprocess(),
+
+ onwarn(warning, handler) {
+ if (warning.message.includes('A11y')) return;
+ !warning.message.includes('chrome') && handler(warning);
+ },
+};
diff --git a/test/public/index.html b/test/public/index.html
deleted file mode 100644
index df93b4e..0000000
--- a/test/public/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
- Svelte Devtools test
-
-
-
-
-
-
diff --git a/test/src/App.svelte b/test/src/App.svelte
deleted file mode 100644
index 0f8e72e..0000000
--- a/test/src/App.svelte
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/test/src/BasicTree/BasicTree.svelte b/test/src/BasicTree/BasicTree.svelte
deleted file mode 100644
index 778502c..0000000
--- a/test/src/BasicTree/BasicTree.svelte
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
- Basic tree rendering
- Element attributes, component properties and state
- Text nodes / anchors
-
-
-
-
diff --git a/test/src/BasicTree/Component.svelte b/test/src/BasicTree/Component.svelte
deleted file mode 100644
index 969d4cc..0000000
--- a/test/src/BasicTree/Component.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- A component with string, number, array, and object attributes. The value is
- {value}
-
diff --git a/test/src/Bind/Bind.svelte b/test/src/Bind/Bind.svelte
deleted file mode 100644
index af67498..0000000
--- a/test/src/Bind/Bind.svelte
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- Prepend 'bind' for bound Component binding. Note: element binds are simple
- implicit event handlers
-
-
-
console.log(e)} />
-
diff --git a/test/src/Bind/BindComponent.svelte b/test/src/Bind/BindComponent.svelte
deleted file mode 100644
index 5e44a4b..0000000
--- a/test/src/Bind/BindComponent.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/test/src/Blocks.svelte b/test/src/Blocks.svelte
deleted file mode 100644
index 108696b..0000000
--- a/test/src/Blocks.svelte
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
- Renders {#each} and {#if} blocks with original
- source line
-
-
- {#each valueList as value}
{value} {/each}
-
-
- {#if value > 10}
- Value is over 10
- {:else if value > 5}Value is over 5{:else}Value is under 5{/if}
-
-
-
- {#await promise}
- waiting for the promise to resolve...
- {:then value}
- Promise resolved to
- {value}
- {:catch error}
- Something went wrong
- {error.message}
- {/await}
-
-
- {#await new Promise(() => {})}
- Pending forever
- {:then value}
- Something went wrong
- {value}
- {:catch error}
- Something went wrong
- {error.message}
- {/await}
-
-
-
- {#await Promise.resolve(5)}
- Something went wrong
- {:then value}
- Promise resolved to
- {value}
- {:catch error}
- Something went wrong
- {error.message}
- {/await}
-
-
- {#await Promise.reject('rejected')}
- Something went wrong
- {:then value}
- Something went wrong
- {value}
- {:catch error}
- Should reject
- {error}
- {/await}
-
-
diff --git a/test/src/Detach/Detach.svelte b/test/src/Detach/Detach.svelte
deleted file mode 100644
index 4068a76..0000000
--- a/test/src/Detach/Detach.svelte
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
Component / element nodes are
-
- positioned correctly when mounted after first render
- removed when detached
-
-
- {#if isShown}
-
-
Element renders below component and above button
- {/if}
-
-
(isShown = !isShown)}>Toggle
-
diff --git a/test/src/Detach/DetachComponent.svelte b/test/src/Detach/DetachComponent.svelte
deleted file mode 100644
index 337dc72..0000000
--- a/test/src/Detach/DetachComponent.svelte
+++ /dev/null
@@ -1 +0,0 @@
-Element renders above both element and button
diff --git a/test/src/Events/Events.svelte b/test/src/Events/Events.svelte
deleted file mode 100644
index ee035d6..0000000
--- a/test/src/Events/Events.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- console.log('Captured a key', e)}>
-
Render event listeners on elements and components.
-
-
console.log(e.detail)} />
-
diff --git a/test/src/Events/EventsComponent.svelte b/test/src/Events/EventsComponent.svelte
deleted file mode 100644
index db96e18..0000000
--- a/test/src/Events/EventsComponent.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- Click me!
-
diff --git a/test/src/Slots/SlotComponent.svelte b/test/src/Slots/SlotComponent.svelte
deleted file mode 100644
index 4fa864c..0000000
--- a/test/src/Slots/SlotComponent.svelte
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/test/src/Slots/Slots.svelte b/test/src/Slots/Slots.svelte
deleted file mode 100644
index 43f6d61..0000000
--- a/test/src/Slots/Slots.svelte
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
Render slots.
-
Slot content
-
diff --git a/test/src/index.js b/test/src/index.js
deleted file mode 100644
index c0c67e9..0000000
--- a/test/src/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import App from './App.svelte'
-
-new App({ target: document.body })
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..10ae081
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+
+ "checkJs": true,
+ "strict": true,
+ "composite": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "useDefineForClassFields": true,
+ "forceConsistentCasingInFileNames": true,
+
+ "paths": {
+ "$lib": ["./src/lib"],
+ "$lib/*": ["./src/lib/*"]
+ }
+ },
+ "include": [
+ "vite.config.ts",
+ "src/**/*.d.ts",
+ "src/**/*.ts",
+ "src/**/*.js",
+ "src/**/*.svelte",
+ "static/**/*.js"
+ ],
+ "exclude": ["static/courier.js"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..553a45c
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,21 @@
+import { resolve } from 'node:path';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import { defineConfig } from 'vite';
+
+export default defineConfig(() => {
+ return {
+ plugins: [svelte()],
+
+ build: {
+ outDir: 'build',
+ },
+
+ publicDir: 'static',
+
+ resolve: {
+ alias: {
+ $lib: resolve(__dirname, 'src/lib'),
+ },
+ },
+ };
+});
diff --git a/zen b/zen
deleted file mode 100644
index 3c6d0dc..0000000
--- a/zen
+++ /dev/null
@@ -1,404 +0,0 @@
-position
-top
-right
-bottom
-left
-z-index
-display
-visibility
-float
-clear
-overflow
--ms-overflow-x
--ms-overflow-y
-overflow-x
-overflow-y
--webkit-overflow-scrolling
-clip
--webkit-align-content
--ms-flex-line-pack
-align-content
--webkit-box-align
--moz-box-align
--webkit-align-items
-align-items
--ms-flex-align
--webkit-align-self
--ms-flex-item-align
--ms-grid-row-align
-align-self
--webkit-box-flex
--webkit-flex
--moz-box-flex
--ms-flex
-flex
--webkit-flex-flow
--ms-flex-flow
-flex-flow
--webkit-flex-basis
--ms-flex-preferred-size
-flex-basis
--webkit-box-orient
--webkit-box-direction
--webkit-flex-direction
--moz-box-orient
--moz-box-direction
--ms-flex-direction
-flex-direction
--webkit-flex-grow
--ms-flex-positive
-flex-grow
--webkit-flex-shrink
--ms-flex-negative
-flex-shrink
--webkit-flex-wrap
--ms-flex-wrap
-flex-wrap
--webkit-box-pack
--moz-box-pack
--ms-flex-pack
--webkit-justify-content
-justify-content
--webkit-box-ordinal-group
--webkit-order
--moz-box-ordinal-group
--ms-flex-order
-order
--webkit-box-sizing
--moz-box-sizing
-box-sizing
-margin
-margin-top
-margin-right
-margin-bottom
-margin-left
-padding
-padding-top
-padding-right
-padding-bottom
-padding-left
-min-width
-min-height
-max-width
-max-height
-width
-height
-outline
-outline-width
-outline-style
-outline-color
-outline-offset
-border
-border-spacing
-border-collapse
-border-width
-border-style
-border-color
-border-top
-border-top-width
-border-top-style
-border-top-color
-border-right
-border-right-width
-border-right-style
-border-right-color
-border-bottom
-border-bottom-width
-border-bottom-style
-border-bottom-color
-border-left
-border-left-width
-border-left-style
-border-left-color
--webkit-border-radius
--moz-border-radius
-border-radius
--webkit-border-top-left-radius
--moz-border-radius-topleft
-border-top-left-radius
--webkit-border-top-right-radius
--moz-border-radius-topright
-border-top-right-radius
--webkit-border-bottom-right-radius
--moz-border-radius-bottomright
-border-bottom-right-radius
--webkit-border-bottom-left-radius
--moz-border-radius-bottomleft
-border-bottom-left-radius
--webkit-border-image
--moz-border-image
--o-border-image
-border-image
--webkit-border-image-source
--moz-border-image-source
--o-border-image-source
-border-image-source
--webkit-border-image-slice
--moz-border-image-slice
--o-border-image-slice
-border-image-slice
--webkit-border-image-width
--moz-border-image-width
--o-border-image-width
-border-image-width
--webkit-border-image-outset
--moz-border-image-outset
--o-border-image-outset
-border-image-outset
--webkit-border-image-repeat
--moz-border-image-repeat
--o-border-image-repeat
-border-image-repeat
--webkit-border-top-image
--moz-border-top-image
--o-border-top-image
-border-top-image
--webkit-border-right-image
--moz-border-right-image
--o-border-right-image
-border-right-image
--webkit-border-bottom-image
--moz-border-bottom-image
--o-border-bottom-image
-border-bottom-image
--webkit-border-left-image
--moz-border-left-image
--o-border-left-image
-border-left-image
--webkit-border-corner-image
--moz-border-corner-image
--o-border-corner-image
-border-corner-image
--webkit-border-top-left-image
--moz-border-top-left-image
--o-border-top-left-image
-border-top-left-image
--webkit-border-top-right-image
--moz-border-top-right-image
--o-border-top-right-image
-border-top-right-image
--webkit-border-bottom-right-image
--moz-border-bottom-right-image
--o-border-bottom-right-image
-border-bottom-right-image
--webkit-border-bottom-left-image
--moz-border-bottom-left-image
--o-border-bottom-left-image
-border-bottom-left-image
-background
-filter:progid:DXImageTransform.Microsoft.AlphaImageLoader
-background-color
-background-image
-background-attachment
-background-position
--ms-background-position-x
--ms-background-position-y
-background-position-x
-background-position-y
--webkit-background-clip
--moz-background-clip
-background-clip
-background-origin
--webkit-background-size
--moz-background-size
--o-background-size
-background-size
-background-repeat
-box-decoration-break
--webkit-box-shadow
--moz-box-shadow
-box-shadow
-color
-table-layout
-caption-side
-empty-cells
-list-style
-list-style-position
-list-style-type
-list-style-image
-quotes
-content
-counter-increment
-counter-reset
--ms-writing-mode
-vertical-align
-text-align
--webkit-text-align-last
--moz-text-align-last
--ms-text-align-last
-text-align-last
-text-decoration
-text-emphasis
-text-emphasis-position
-text-emphasis-style
-text-emphasis-color
-text-indent
--ms-text-justify
-text-justify
-text-outline
-text-transform
-text-wrap
--ms-text-overflow
-text-overflow
-text-overflow-ellipsis
-text-overflow-mode
-text-shadow
-white-space
-word-spacing
--ms-word-wrap
-word-wrap
--ms-word-break
-word-break
--moz-tab-size
--o-tab-size
-tab-size
--webkit-hyphens
--moz-hyphens
-hyphens
-letter-spacing
-font
-font-weight
-font-style
-font-variant
-font-size-adjust
-font-stretch
-font-size
-font-family
-src
-line-height
-opacity
--ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha
-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity
--ms-interpolation-mode
--webkit-filter
--ms-filter
-filter
-resize
-cursor
-nav-index
-nav-up
-nav-right
-nav-down
-nav-left
--webkit-transition
--moz-transition
--ms-transition
--o-transition
-transition
--webkit-transition-delay
--moz-transition-delay
--ms-transition-delay
--o-transition-delay
-transition-delay
--webkit-transition-timing-function
--moz-transition-timing-function
--ms-transition-timing-function
--o-transition-timing-function
-transition-timing-function
--webkit-transition-duration
--moz-transition-duration
--ms-transition-duration
--o-transition-duration
-transition-duration
--webkit-transition-property
--moz-transition-property
--ms-transition-property
--o-transition-property
-transition-property
--webkit-transform
--moz-transform
--ms-transform
--o-transform
-transform
--webkit-transform-origin
--moz-transform-origin
--ms-transform-origin
--o-transform-origin
-transform-origin
--webkit-animation
--moz-animation
--ms-animation
--o-animation
-animation
--webkit-animation-name
--moz-animation-name
--ms-animation-name
--o-animation-name
-animation-name
--webkit-animation-duration
--moz-animation-duration
--ms-animation-duration
--o-animation-duration
-animation-duration
--webkit-animation-play-state
--moz-animation-play-state
--ms-animation-play-state
--o-animation-play-state
-animation-play-state
--webkit-animation-timing-function
--moz-animation-timing-function
--ms-animation-timing-function
--o-animation-timing-function
-animation-timing-function
--webkit-animation-delay
--moz-animation-delay
--ms-animation-delay
--o-animation-delay
-animation-delay
--webkit-animation-iteration-count
--moz-animation-iteration-count
--ms-animation-iteration-count
--o-animation-iteration-count
-animation-iteration-count
--webkit-animation-direction
--moz-animation-direction
--ms-animation-direction
--o-animation-direction
-animation-direction
-pointer-events
-unicode-bidi
-direction
--webkit-columns
--moz-columns
-columns
--webkit-column-span
--moz-column-span
-column-span
--webkit-column-width
--moz-column-width
-column-width
--webkit-column-count
--moz-column-count
-column-count
--webkit-column-fill
--moz-column-fill
-column-fill
--webkit-column-gap
--moz-column-gap
-column-gap
--webkit-column-rule
--moz-column-rule
-column-rule
--webkit-column-rule-width
--moz-column-rule-width
-column-rule-width
--webkit-column-rule-style
--moz-column-rule-style
-column-rule-style
--webkit-column-rule-color
--moz-column-rule-color
-column-rule-color
-break-before
-break-inside
-break-after
-page-break-before
-page-break-inside
-page-break-after
-orphans
-widows
--ms-zoom
-zoom
-max-zoom
-min-zoom
-user-zoom
-orientation