@picked/svelte 0.0.63-next.1 → 0.0.64-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,7 @@
23
23
  import ContentListScreen from './screens/content/ContentListScreen.svelte'
24
24
  import FeaturesListScreen from './screens/features/FeaturesListScreen.svelte'
25
25
  import EventsListScreen from './screens/events/EventsListScreen.svelte'
26
+ import CustomToolsScreen from './screens/custom-tools/CustomToolsScreen.svelte'
26
27
  import PickedIcon from './PickedIcon.svelte'
27
28
 
28
29
  const experience = getExperience()
@@ -116,6 +117,12 @@
116
117
  component: ResponsesListScreen,
117
118
  props: {},
118
119
  },
120
+ {
121
+ id: 'custom-tools',
122
+ label: 'Custom Tools',
123
+ component: CustomToolsScreen,
124
+ props: {},
125
+ },
119
126
  ]
120
127
 
121
128
  const isOpen = experience.isOpenStore
@@ -0,0 +1,103 @@
1
+ <script lang="ts">
2
+ /** @file Renders a custom devtools tool by delegating to its framework's registered renderer. */
3
+ import { onDestroy } from 'svelte'
4
+ import type { Framework } from '@picked/client'
5
+ import { getExperience, getPicked } from '../../../hooks'
6
+ import type { CustomTool } from '../customTools'
7
+ import { Banner } from '../ui'
8
+
9
+ export let tool: CustomTool
10
+
11
+ const client = getPicked()
12
+ const experience = getExperience()
13
+ const frameworkStore = client.frameworks.getByName(tool.component.frameworkName)
14
+
15
+ let host: HTMLDivElement | undefined
16
+ let rendered = false
17
+ let renderFailed = false
18
+ let destroyed = false
19
+ let activeFramework: Framework | undefined
20
+ let activeShadow: ShadowRoot | undefined
21
+
22
+ /**
23
+ * Records a tool render failure: surfaces a banner and logs to the console. A consumer tool's
24
+ * render failure is deliberately not routed through the client event bus, so a third-party tool
25
+ * bug never reaches the platform's Sentry exception stream.
26
+ *
27
+ * @param {unknown} error - The error thrown or rejected by the framework renderer.
28
+ * @example
29
+ * handleRenderError(new Error('tool blew up'))
30
+ */
31
+ function handleRenderError(error: unknown) {
32
+ renderFailed = true
33
+ // eslint-disable-next-line no-console -- surface tool failures locally; consumer errors must not reach Sentry
34
+ console.error(`Custom devtools tool "${tool.label}" failed to render`, error)
35
+ }
36
+
37
+ /**
38
+ * Tears down the tool's mounted instance using the framework that rendered it. Idempotent: the
39
+ * renderer's unmount is a no-op once the instance is gone, so calling it twice is safe.
40
+ *
41
+ * @example
42
+ * teardown()
43
+ */
44
+ function teardown() {
45
+ if (activeShadow && activeFramework?.unmount) {
46
+ activeFramework.unmount(activeShadow)
47
+ }
48
+ }
49
+
50
+ $: framework = $frameworkStore
51
+
52
+ $: if (framework && host && !rendered) {
53
+ rendered = true
54
+ const shadow = host.shadowRoot ?? host.attachShadow({ mode: 'open' })
55
+ const fw = framework
56
+ activeFramework = fw
57
+ activeShadow = shadow
58
+ try {
59
+ void Promise.resolve(fw.render(shadow, tool.component, client, experience))
60
+ .then(() => {
61
+ if (destroyed) teardown()
62
+ })
63
+ .catch(handleRenderError)
64
+ } catch (error) {
65
+ handleRenderError(error)
66
+ }
67
+ }
68
+
69
+ onDestroy(() => {
70
+ destroyed = true
71
+ teardown()
72
+ })
73
+ </script>
74
+
75
+ {#if framework}
76
+ <div class="adapted-tool-host" bind:this={host} hidden={renderFailed}></div>
77
+ {#if renderFailed}
78
+ <div class="adapted-tool-fallback">
79
+ <Banner status="error" title="Tool failed to render">
80
+ The "{tool.label}" tool threw while rendering. See the browser console for details.
81
+ </Banner>
82
+ </div>
83
+ {/if}
84
+ {:else}
85
+ <div class="adapted-tool-fallback">
86
+ <Banner status="warning" title="Framework not registered">
87
+ This tool renders a "{tool.component.frameworkName}" component, but no matching framework is
88
+ registered with the Picked client. Add the corresponding framework plugin to use it.
89
+ </Banner>
90
+ </div>
91
+ {/if}
92
+
93
+ <style>
94
+ .adapted-tool-host {
95
+ height: 100%;
96
+ padding: var(--space-4);
97
+ box-sizing: border-box;
98
+ }
99
+
100
+ .adapted-tool-fallback {
101
+ padding: var(--space-4);
102
+ }
103
+ </style>
@@ -0,0 +1,26 @@
1
+ import { AdaptedComponent, Picked } from '@picked/client';
2
+ /**
3
+ * A user-defined developer tool surfaced under the devtools "Custom Tools" tab.
4
+ *
5
+ * The component is framework-adapted (via `adaptReactComponent` or `adaptSvelteComponent`), so
6
+ * tools can be authored in the host app's framework and still receive the live Picked client the
7
+ * way an experience component does.
8
+ */
9
+ export interface CustomTool {
10
+ id: string;
11
+ label: string;
12
+ component: AdaptedComponent;
13
+ }
14
+ /**
15
+ * Reads the custom tools registered with the devtools plugin for a given client.
16
+ *
17
+ * Malformed entries are dropped, and entries that repeat an already-seen `id` are dropped
18
+ * (first registration wins), so a single bad or duplicate registration can't break the tab — the
19
+ * `id` is used as a keyed-each key when the tools are rendered as tabs.
20
+ *
21
+ * @param {Picked} client - The Picked client whose plugin config holds the registered tools.
22
+ * @returns {CustomTool[]} The valid, id-unique registered custom tools, or an empty array when none exist.
23
+ * @example
24
+ * const tools = readCustomTools(getPicked())
25
+ */
26
+ export declare function readCustomTools(client: Picked): CustomTool[];
@@ -0,0 +1 @@
1
+ import{DEVTOOLS_PLUGIN_NAME as e}from"./plugin.js";function t(e){return typeof e==`object`&&!!e&&`frameworkName`in e&&typeof e.frameworkName==`string`&&`component`in e}function n(e){return typeof e==`object`&&!!e&&`id`in e&&typeof e.id==`string`&&`label`in e&&typeof e.label==`string`&&`component`in e&&t(e.component)}function r(t){let r=t.getPluginConfig(e).customTools;if(!Array.isArray(r))return[];let i=new Set;return r.filter(n).filter(e=>i.has(e.id)?!1:(i.add(e.id),!0))}export{r as readCustomTools};
@@ -1,2 +1,4 @@
1
+ /** @file Public entrypoint for the Svelte devtools plugin — the plugin factory and its consumer-facing types. */
1
2
  export { svelteDevtoolsPlugin } from './plugin';
2
3
  export type * from './plugin';
4
+ export type { CustomTool } from './customTools';
@@ -1,4 +1,7 @@
1
1
  import { Plugin } from '@picked/client';
2
+ import { CustomTool } from './customTools';
3
+ /** Plugin name under which the devtools plugin stores its config (including registered custom tools). */
4
+ export declare const DEVTOOLS_PLUGIN_NAME = "svelte-devtools";
2
5
  export interface SvelteDevtoolsPluginOptions {
3
6
  /**
4
7
  * When `true`, the devtools experience launches automatically on page load.
@@ -9,6 +12,15 @@ export interface SvelteDevtoolsPluginOptions {
9
12
  * @default false
10
13
  */
11
14
  enabled?: boolean;
15
+ /**
16
+ * Project-specific custom dev tools surfaced under the "Custom Tools" tab. Each tool's
17
+ * `component` is framework-adapted (via `adaptReactComponent` or `adaptSvelteComponent`), so it
18
+ * can be authored in the host app's framework and still receive the live Picked client the way
19
+ * an experience component does.
20
+ *
21
+ * @default []
22
+ */
23
+ customTools?: CustomTool[];
12
24
  }
13
25
  /**
14
26
  * Registers the devtools experience for on-demand loading via the Chrome DevTools extension.
@@ -1 +1 @@
1
- import{adaptSvelteComponent as e}from"../../component.js";import{__vitePreload as t}from"../../_virtual/_vite/preload-helper.js";import{lazyComponent as n}from"@picked/client";import{createExperience as r,createExperiencePlugin as i}from"@picked/client/plugins/create-experience";var a=a=>{let o=r({id:`devtools`,key:`devtools`,name:`Picked Devtools`,condition:!1,initializeOpen:!1,targets:[{selector:`body`,placement:`append`}],component:e(n(()=>t(()=>import(`./DevToolsExperience.svelte`),[])))});return o.addStep({name:`Tools`}),[{name:`svelte-devtools`,setup:e=>{let t=!!e.searchParams[`preview-token`]?.[0];!a?.enabled&&!t||e.eventBus.watch(t=>{t.type===`ready`&&e.experiences.launch({key:o.key,options:{bypassCondition:!0}})})}},i({experiences:[o]})]};export{a as svelteDevtoolsPlugin};
1
+ import{adaptSvelteComponent as e}from"../../component.js";import{__vitePreload as t}from"../../_virtual/_vite/preload-helper.js";import{lazyComponent as n}from"@picked/client";import{createExperience as r,createExperiencePlugin as i}from"@picked/client/plugins/create-experience";var a=`svelte-devtools`,o=o=>{let s=r({id:`devtools`,key:`devtools`,name:`Picked Devtools`,condition:!1,initializeOpen:!1,targets:[{selector:`body`,placement:`append`}],component:e(n(()=>t(()=>import(`./DevToolsExperience.svelte`),[])))});return s.addStep({name:`Tools`}),[{name:a,setup:e=>{let t=!!e.searchParams[`preview-token`]?.[0];return(o?.enabled||t)&&e.eventBus.watch(t=>{t.type===`ready`&&e.experiences.launch({key:s.key,options:{bypassCondition:!0}})}),{customTools:o?.customTools??[]}}},i({experiences:[s]})]};export{a as DEVTOOLS_PLUGIN_NAME,o as svelteDevtoolsPlugin};
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ /** @file Screen hosting user-registered custom devtools tools as nested tabs. */
3
+ import { getPicked } from '../../../../hooks'
4
+ import AdaptedToolHost from '../../components/AdaptedToolHost.svelte'
5
+ import Tabs from '../../components/Tabs.svelte'
6
+ import { readCustomTools } from '../../customTools'
7
+ import { Button, EmptyState, Stack } from '../../ui'
8
+
9
+ const client = getPicked()
10
+ const docsUrl = `${client.documentationSiteUrl}/chrome-extension#custom-tools`
11
+ const tools = readCustomTools(client)
12
+ const tabs = tools.map((tool) => ({
13
+ id: tool.id,
14
+ label: tool.label,
15
+ component: AdaptedToolHost,
16
+ props: { tool },
17
+ }))
18
+ </script>
19
+
20
+ {#if tabs.length > 0}
21
+ <Tabs {tabs} />
22
+ {:else}
23
+ <Stack direction="vertical" gap="medium" padding="medium" justify="center">
24
+ <EmptyState message="No custom tools registered." />
25
+ <Button variant="ghost" size="small" href={docsUrl} target="_blank">
26
+ Learn how to add custom tools
27
+ </Button>
28
+ </Stack>
29
+ {/if}
@@ -14,9 +14,8 @@ export interface SvelteFrameworkPluginOptions {
14
14
  * This plugin registers the Svelte framework and allows rendering Svelte components
15
15
  * with the client's experience data.
16
16
  *
17
- * @param {SvelteFrameworkPluginOptions} options - Configuration options for the Svelte plugin
17
+ * @param {SvelteFrameworkPluginOptions} options - Configuration options for the Svelte plugin.
18
18
  * @returns {Plugin} The plugin.
19
- *
20
19
  * @example
21
20
  * ```typescript
22
21
  * createClient({
@@ -1 +1 @@
1
- import{adaptSvelteComponent as e}from"../../component.js";import{renderSvelteComponent as t}from"../../render/browser.js";var n=n=>({name:`svelte-framework`,setup:r=>{r.registerFramework({name:`svelte`,render(...e){return t(...e,{stylesheets:n.stylesheets,css:n.css})}});let i=n.components.experience;i&&r.registerComponent(`experience`,e(i.component,i.style))}});export{n as svelteFrameworkPlugin};
1
+ import{adaptSvelteComponent as e}from"../../component.js";import{renderSvelteComponent as t,unRenderSvelteComponent as n}from"../../render/browser.js";var r=r=>({name:`svelte-framework`,setup:i=>{i.registerFramework({name:`svelte`,render(...e){return t(...e,{stylesheets:r.stylesheets,css:r.css})},unmount:e=>n(e)});let a=r.components.experience;a&&i.registerComponent(`experience`,e(a.component,a.style))}});export{r as svelteFrameworkPlugin};
@@ -1,12 +1,12 @@
1
1
  import { AdaptedComponent, ExperienceInstance, Picked } from '@picked/client';
2
2
  /**
3
- * Builds the Svelte context map exposing the client and experience instance to a mounted component.
3
+ * Builds the Svelte context map that exposes the client and experience to rendered components.
4
4
  *
5
- * @param {Picked} client - The Picked client.
6
- * @param {ExperienceInstance} experience - The experience instance.
7
- * @returns {Map<symbol, Picked | ExperienceInstance>} The context map keyed by the client and experience context symbols.
5
+ * @param {Picked} client - The Picked client to expose via context.
6
+ * @param {ExperienceInstance} experience - The experience instance to expose via context.
7
+ * @returns {Map<symbol, Picked | ExperienceInstance>} The context map keyed by the client and experience symbols.
8
8
  * @example
9
- * createContext(client, experience)
9
+ * mount(Comp, { target, context: createContext(client, experience) })
10
10
  */
11
11
  export declare function createContext(client: Picked, experience: ExperienceInstance): Map<symbol, Picked | ExperienceInstance>;
12
12
  /**
@@ -28,3 +28,12 @@ export declare const renderSvelteComponent: (shadowRoot: ShadowRoot, component:
28
28
  stylesheets?: string[];
29
29
  css?: string[];
30
30
  }) => Promise<void>;
31
+ /**
32
+ * Unmounts a Svelte component previously rendered into the given shadow root and clears its tracked instance.
33
+ *
34
+ * @param {ShadowRoot} target - The shadow root the component was rendered into.
35
+ * @returns {void} Nothing.
36
+ * @example
37
+ * unRenderSvelteComponent(host.shadowRoot)
38
+ */
39
+ export declare const unRenderSvelteComponent: (target: ShadowRoot) => void;
@@ -1 +1 @@
1
- import{isValidAdaptedComponent as e}from"../component.js";import{CLIENT_CONTEXT_KEY as t,EXPERIENCE_CONTEXT_KEY as n}from"../context.js";import{mergeArrays as r}from"../utils/array.js";import{mount as i}from"svelte";import{isLazyLoader as a}from"@picked/client";import o from"../utils/ErrorBoundary.svelte";function s(e,r){return new Map([[t,e],[n,r]])}var c=async(t,n,c,l,u)=>{if(!e(n))throw Error(`The component is invalid.`);let d=r(n.style?.sheets,u.stylesheets),f=r(n.style?.css,u.css);d.length&&d.forEach(e=>{let n=document.createElement(`link`);n.setAttribute(`rel`,`stylesheet`),n.setAttribute(`href`,e),n.setAttribute(`type`,`text/css`),n.setAttribute(`picked-injected`,`true`),t.appendChild(n)}),f.length&&f.forEach(e=>{let n=document.createElement(`style`);n.textContent=e,n.setAttribute(`type`,`text/css`),n.setAttribute(`picked-injected`,`true`),t.appendChild(n)});let p=e=>{i(o,{target:t,props:{component:e,componentProps:{client:c,experience:l}},context:s(c,l)})};if(a(n.component)){let{default:e}=await n.component.load();p(e);return}if(n.component instanceof Promise){let{default:e}=await n.component;p(e);return}p(n.component)};export{s as createContext,c as renderSvelteComponent};
1
+ import{isValidAdaptedComponent as e}from"../component.js";import{CLIENT_CONTEXT_KEY as t,EXPERIENCE_CONTEXT_KEY as n}from"../context.js";import{mergeArrays as r}from"../utils/array.js";import{mount as i,unmount as a}from"svelte";import{isLazyLoader as o}from"@picked/client";import s from"../utils/ErrorBoundary.svelte";var c=new WeakMap;function l(e,r){return new Map([[t,e],[n,r]])}var u=async(t,n,a,u,d)=>{if(!e(n))throw Error(`The component is invalid.`);let f=r(n.style?.sheets,d.stylesheets),p=r(n.style?.css,d.css);f.length&&f.forEach(e=>{let n=document.createElement(`link`);n.setAttribute(`rel`,`stylesheet`),n.setAttribute(`href`,e),n.setAttribute(`type`,`text/css`),n.setAttribute(`picked-injected`,`true`),t.appendChild(n)}),p.length&&p.forEach(e=>{let n=document.createElement(`style`);n.textContent=e,n.setAttribute(`type`,`text/css`),n.setAttribute(`picked-injected`,`true`),t.appendChild(n)});let m=e=>{let n=i(s,{target:t,props:{component:e,componentProps:{client:a,experience:u}},context:l(a,u)});c.set(t,n)};if(o(n.component)){let{default:e}=await n.component.load();m(e);return}if(n.component instanceof Promise){let{default:e}=await n.component;m(e);return}m(n.component)},d=e=>{let t=c.get(e);t&&(a(t),c.delete(e))};export{l as createContext,u as renderSvelteComponent,d as unRenderSvelteComponent};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picked/svelte",
3
- "version": "0.0.63-next.1",
3
+ "version": "0.0.64-next.1",
4
4
  "dependencies": {
5
5
  "d3": "^7.9.0"
6
6
  },
@@ -106,7 +106,7 @@
106
106
  },
107
107
  "module": "./dist/index.js",
108
108
  "peerDependencies": {
109
- "@picked/client": "0.0.63-next.1",
109
+ "@picked/client": "0.0.64-next.1",
110
110
  "effector": "^23.3.0",
111
111
  "svelte": "^5.46.4"
112
112
  },