@poveste/plugin-svelte 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Poveste + Svelte 5 and SvelteKit
2
+
3
+ Requires `svelte@^5.46.4` and `@sveltejs/vite-plugin-svelte@^7.0.0`. SvelteKit is supported
4
+ by this same package — there is no separate plugin — through an optional
5
+ `@sveltejs/kit@^2.53.0` peer.
6
+
7
+ The floor is not `^5.0.0` for a reason: Poveste requires Vite 8, only
8
+ `@sveltejs/vite-plugin-svelte` v7 peers Vite 8, and v7 in turn requires `svelte@^5.46.4`.
9
+ Svelte `5.0`–`5.46.3` cannot be assembled into a working project at all.
10
+
11
+ ```bash
12
+ pnpm add -D poveste @poveste/plugin-svelte
13
+ ```
14
+
15
+ Register the plugin in your Poveste config:
16
+
17
+ ```ts
18
+ import { HstSvelte } from '@poveste/plugin-svelte'
19
+ import { defineConfig } from 'poveste'
20
+
21
+ export default defineConfig({
22
+ plugins: [
23
+ HstSvelte(),
24
+ ],
25
+ })
26
+ ```
27
+
28
+ Write stories as `*.story.svelte`. State that your controls drive belongs on `initState`,
29
+ and your content reads it from the snippet:
30
+
31
+ ```svelte
32
+ <script>
33
+ import MyButton from './MyButton.svelte'
34
+
35
+ export let Hst
36
+
37
+ const initState = () => ({ disabled: false })
38
+ </script>
39
+
40
+ <Hst.Story title="MyButton" {initState}>
41
+ {#snippet children({ state })}
42
+ <MyButton disabled={state.disabled} />
43
+ {/snippet}
44
+
45
+ {#snippet controls({ state })}
46
+ <Hst.Checkbox bind:value={state.disabled} title="Disabled" />
47
+ {/snippet}
48
+ </Hst.Story>
49
+ ```
50
+
51
+ > **Coming from histoire?** Story state must move onto `initState`. Svelte 5 removed the API
52
+ > histoire used to read a story's state, so a plain `let` in your story no longer reaches the
53
+ > preview — Poveste owns the state instead. A story with controls but no `initState` logs an
54
+ > error explaining this.
55
+ >
56
+ > [How, and why](https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate)
57
+
58
+ [Svelte guide](https://poveste.dev/guide/svelte/getting-started.html) ·
59
+ [SvelteKit](https://poveste.dev/guide/svelte/getting-started.html#sveltekit) ·
60
+ [Documentation](https://poveste.dev)
@@ -1 +1 @@
1
- import{getContext as e,setContext as t}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as n from"svelte/internal/client";var r=n.from_html(`<!> <!>`,1);function i(i,a){n.push(a,!1);let o=e(`__pvtStory`),s=e(`__pvtVariant`),c=e(`__pvtSlot`);t(`__pvtIndex`,{value:0});let l=n.prop(a,`source`,8,null);n.legacy_pre_effect(()=>n.deep_read_state(l()),()=>{l()!=null&&Object.assign(s,{source:l()})}),n.legacy_pre_effect_reset(),n.init();var u=r(),d=n.first_child(u),f=e=>{var t=n.comment(),r=n.first_child(t);n.slot(r,a,`controls`,{},null),n.append(e,t)};n.if(d,e=>{c===`controls`&&e(f)});var p=n.sibling(d,2),m=e=>{var t=n.comment(),r=n.first_child(t);n.slot(r,a,`default`,{},null),n.append(e,t)};n.if(p,e=>{n.untrack(()=>c==="default"||o.meta?.hasVariantChildComponents)&&e(m)}),n.append(i,u),n.pop()}export{i as default};
1
+ import{watch as e}from"@poveste/vendors/vue";import{getContext as t,onDestroy as n,setContext as r}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as i from"svelte/internal/client";var a=i.from_html(`<!> <!>`,1);function o(o,s){let c=i.sanitize_slots(s);i.push(s,!1);let l=t(`__pvtStory`),u=i.mutable_source(t(`__pvtVariant`)),d=t(`__pvtSlot`);r(`__pvtIndex`,{value:0});let f=i.prop(s,`source`,8,null),p=i.prop(s,`initState`,8,null);p()&&i.get(u)&&!i.get(u).__pvtStateSeeded&&(i.mutate(u,i.get(u).__pvtStateSeeded=!0),i.mutate(u,i.get(u).state={...i.get(u).state,...p()()})),!p()&&d===`controls`&&c.controls&&i.get(u)&&!i.get(u).__pvtStateWarned&&(i.mutate(u,i.get(u).__pvtStateWarned=!0),console.error([`[poveste] Story "${l?.title??l?.id}" has a controls slot but no \`initState\`.`,`Controls will render and appear to work, but their edits cannot reach the story:`,`the story is mounted separately for each slot, so a component-local variable is not shared.`,"Pass `initState` and read state from the snippet —",`https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate`].join(` `)));let m=i.mutable_source(i.get(u)?.state),h=e(()=>i.get(u)?.state,()=>{i.set(m,i.get(u)?.state)},{deep:!0});n(h),i.legacy_pre_effect(()=>(i.deep_read_state(f()),i.get(m),i.get(u)),()=>{let e=typeof f()==`function`?f()(i.get(m)):f();e!=null&&Object.assign(i.get(u),{source:e})}),i.legacy_pre_effect_reset(),i.init();var g=a(),_=i.first_child(g),v=e=>{var t=i.comment(),n=i.first_child(t);i.slot(n,s,`controls`,{get state(){return i.get(m)}},null),i.append(e,t)};i.if(_,e=>{d===`controls`&&e(v)});var y=i.sibling(_,2),b=e=>{var t=i.comment(),n=i.first_child(t);i.slot(n,s,`default`,{get state(){return i.get(m)}},null),i.append(e,t)};i.if(y,e=>{i.untrack(()=>d==="default"||l.meta?.hasVariantChildComponents)&&e(b)}),i.append(o,g),i.pop()}export{o as default};
@@ -1,5 +1,6 @@
1
1
  <script>
2
- import { getContext, setContext } from 'svelte'
2
+ import { watch as _watch } from '@poveste/vendors/vue'
3
+ import { getContext, onDestroy, setContext } from 'svelte'
3
4
 
4
5
  const story = getContext('__pvtStory')
5
6
  const currentVariant = getContext('__pvtVariant')
@@ -9,19 +10,68 @@
9
10
  setContext('__pvtIndex', index)
10
11
 
11
12
  export let source = null
13
+ export let initState = null
12
14
 
15
+ // Poveste owns the state, not the story component.
16
+ //
17
+ // The story is mounted once per slot — the app renders it with
18
+ // `slot-name="controls"`, the preview with the default — so a `let count` in
19
+ // the story exists twice and cannot be shared. Svelte 4 bridged the two
20
+ // instances with `$capture_state` / `$inject_state`; Svelte 5 provides neither,
21
+ // and no shim does either. So state lives on the variant, which both mounts
22
+ // read from the same context object and which the sandbox bridge already
23
+ // serialises across the iframe boundary (#81).
24
+ //
25
+ // Whichever mount arrives first seeds it; the second must not, or it would
26
+ // clobber whatever the user has already changed.
27
+ if (initState && currentVariant && !currentVariant.__pvtStateSeeded) {
28
+ currentVariant.__pvtStateSeeded = true
29
+ currentVariant.state = { ...currentVariant.state, ...initState() }
30
+ }
31
+
32
+ // Only from the mount that renders the controls. The story is mounted once per
33
+ // slot and, for an iframe layout, in a different realm — so the flag below cannot
34
+ // dedupe across them and the warning would appear twice.
35
+ if (!initState && slotName === 'controls' && $$slots.controls && currentVariant && !currentVariant.__pvtStateWarned) {
36
+ currentVariant.__pvtStateWarned = true
37
+ console.error([
38
+ `[poveste] Story "${story?.title ?? story?.id}" has a controls slot but no \`initState\`.`,
39
+ 'Controls will render and appear to work, but their edits cannot reach the story:',
40
+ 'the story is mounted separately for each slot, so a component-local variable is not shared.',
41
+ 'Pass `initState` and read state from the snippet —',
42
+ 'https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate',
43
+ ].join(' '))
44
+ }
45
+
46
+ // `variant.state` is a Vue-reactive object, so Svelte has no way to know when a
47
+ // control writes into it. Mirroring the reference back into a local on every
48
+ // Vue tick is what makes the template re-render: Svelte's invalidation treats
49
+ // any object assignment as a change, so reassigning the same proxy is enough.
50
+ let state = currentVariant?.state
51
+
52
+ const stopWatchingState = _watch(() => currentVariant?.state, () => {
53
+ state = currentVariant?.state
54
+ }, { deep: true })
55
+
56
+ onDestroy(stopWatchingState)
57
+
58
+ // `source` may be a function of the state. Story props are evaluated in the
59
+ // component's script, where the slot's `state` is not in scope, so a string
60
+ // literal cannot reflect what the controls are doing — which is most of the
61
+ // point of the source panel (#81).
13
62
  $: {
14
- if (source != null) {
63
+ const resolvedSource = typeof source === 'function' ? source(state) : source
64
+ if (resolvedSource != null) {
15
65
  Object.assign(currentVariant, {
16
- source,
66
+ source: resolvedSource,
17
67
  })
18
68
  }
19
69
  }
20
70
  </script>
21
71
 
22
72
  {#if slotName === 'controls'}
23
- <slot name="controls" />
73
+ <slot name="controls" {state} />
24
74
  {/if}
25
75
  {#if slotName === 'default' || story.meta?.hasVariantChildComponents}
26
- <slot />
76
+ <slot {state} />
27
77
  {/if}
@@ -1 +1 @@
1
- import{getContext as e}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as t from"svelte/internal/client";var n=t.from_html(`<!> <!>`,1);function r(r,i){t.push(i,!1);let a=e(`__pvtStory`),o=e(`__pvtVariant`),s=e(`__pvtSlot`),c=e(`__pvtIndex`),l=a.variants[c.value];c.value++;let u=o.id===l.id,d=t.prop(i,`source`,8,null);t.legacy_pre_effect(()=>t.deep_read_state(d()),()=>{d()!=null&&Object.assign(o,{source:d()})}),t.legacy_pre_effect_reset(),t.init();var f=t.comment(),p=t.first_child(f),m=e=>{var r=n(),a=t.first_child(r),o=e=>{var n=t.comment(),r=t.first_child(n);t.slot(r,i,`default`,{},null),t.append(e,n)};t.if(a,e=>{s==="default"&&e(o)});var c=t.sibling(a,2),l=e=>{var n=t.comment(),r=t.first_child(n);t.slot(r,i,`controls`,{},null),t.append(e,n)};t.if(c,e=>{s===`controls`&&e(l)}),t.append(e,r)};t.if(p,e=>{u&&e(m)}),t.append(r,f),t.pop()}export{r as default};
1
+ import{watch as e}from"@poveste/vendors/vue";import{getContext as t,onDestroy as n}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as r from"svelte/internal/client";var i=r.from_html(`<!> <!>`,1);function a(a,o){let s=r.sanitize_slots(o);r.push(o,!1);let c=t(`__pvtStory`),l=r.mutable_source(t(`__pvtVariant`)),u=t(`__pvtSlot`),d=t(`__pvtIndex`),f=c.variants[d.value];d.value++;let p=r.get(l).id===f.id,m=r.prop(o,`source`,8,null),h=r.prop(o,`initState`,8,null);h()&&p&&!r.get(l).__pvtStateSeeded&&(r.mutate(l,r.get(l).__pvtStateSeeded=!0),r.mutate(l,r.get(l).state={...r.get(l).state,...h()()})),!h()&&p&&u===`controls`&&s.controls&&!r.get(l).__pvtStateWarned&&(r.mutate(l,r.get(l).__pvtStateWarned=!0),console.error([`[poveste] Variant "${f?.title??f?.id}" has a controls slot but no \`initState\`.`,`Controls will render and appear to work, but their edits cannot reach the story:`,`the story is mounted separately for each slot, so a component-local variable is not shared.`,"Pass `initState` and read state from the snippet —",`https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate`].join(` `)));let g=r.mutable_source(r.get(l)?.state),_=e(()=>r.get(l)?.state,()=>{r.set(g,r.get(l)?.state)},{deep:!0});n(_),r.legacy_pre_effect(()=>(r.deep_read_state(m()),r.get(g),r.get(l)),()=>{let e=typeof m()==`function`?m()(r.get(g)):m();e!=null&&Object.assign(r.get(l),{source:e})}),r.legacy_pre_effect_reset(),r.init();var v=r.comment(),y=r.first_child(v),b=e=>{var t=i(),n=r.first_child(t),a=e=>{var t=r.comment(),n=r.first_child(t);r.slot(n,o,`default`,{get state(){return r.get(g)}},null),r.append(e,t)};r.if(n,e=>{u==="default"&&e(a)});var s=r.sibling(n,2),c=e=>{var t=r.comment(),n=r.first_child(t);r.slot(n,o,`controls`,{get state(){return r.get(g)}},null),r.append(e,t)};r.if(s,e=>{u===`controls`&&e(c)}),r.append(e,t)};r.if(y,e=>{p&&e(b)}),r.append(a,v),r.pop()}export{a as default};
@@ -1,5 +1,6 @@
1
1
  <script>
2
- import { getContext } from 'svelte'
2
+ import { watch as _watch } from '@poveste/vendors/vue'
3
+ import { getContext, onDestroy } from 'svelte'
3
4
 
4
5
  const story = getContext('__pvtStory')
5
6
  const currentVariant = getContext('__pvtVariant')
@@ -15,11 +16,47 @@
15
16
  const shouldRender = currentVariant.id === variant.id
16
17
 
17
18
  export let source = null
19
+ export let initState = null
18
20
 
21
+ // Same contract as `RenderStory`: poveste owns the state because the story is
22
+ // mounted once per slot, so a component-local variable cannot be shared (#81).
23
+ // `variant.state` is also the only bucket the sandbox bridge carries across the
24
+ // iframe boundary, which is why state is per variant rather than per story.
25
+ if (initState && shouldRender && !currentVariant.__pvtStateSeeded) {
26
+ currentVariant.__pvtStateSeeded = true
27
+ currentVariant.state = { ...currentVariant.state, ...initState() }
28
+ }
29
+
30
+ // Controls mount only — see the note in RenderStory: the two mounts do not share
31
+ // a realm for iframe layouts, so this is what keeps it to one warning.
32
+ if (!initState && shouldRender && slotName === 'controls' && $$slots.controls && !currentVariant.__pvtStateWarned) {
33
+ currentVariant.__pvtStateWarned = true
34
+ console.error([
35
+ `[poveste] Variant "${variant?.title ?? variant?.id}" has a controls slot but no \`initState\`.`,
36
+ 'Controls will render and appear to work, but their edits cannot reach the story:',
37
+ 'the story is mounted separately for each slot, so a component-local variable is not shared.',
38
+ 'Pass `initState` and read state from the snippet —',
39
+ 'https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate',
40
+ ].join(' '))
41
+ }
42
+
43
+ let state = currentVariant?.state
44
+
45
+ const stopWatchingState = _watch(() => currentVariant?.state, () => {
46
+ state = currentVariant?.state
47
+ }, { deep: true })
48
+
49
+ onDestroy(stopWatchingState)
50
+
51
+ // `source` may be a function of the state. Story props are evaluated in the
52
+ // component's script, where the slot's `state` is not in scope, so a string
53
+ // literal cannot reflect what the controls are doing — which is most of the
54
+ // point of the source panel (#81).
19
55
  $: {
20
- if (source != null) {
56
+ const resolvedSource = typeof source === 'function' ? source(state) : source
57
+ if (resolvedSource != null) {
21
58
  Object.assign(currentVariant, {
22
- source,
59
+ source: resolvedSource,
23
60
  })
24
61
  }
25
62
  }
@@ -27,9 +64,9 @@
27
64
 
28
65
  {#if shouldRender}
29
66
  {#if slotName === 'default'}
30
- <slot />
67
+ <slot {state} />
31
68
  {/if}
32
69
  {#if slotName === 'controls'}
33
- <slot name="controls" />
70
+ <slot name="controls" {state} />
34
71
  {/if}
35
72
  {/if}
@@ -1 +1 @@
1
- import{createApp as e,h as t,reactive as n}from"@poveste/vendors/vue";import{createEventDispatcher as r,onMount as i}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as a from"svelte/internal/client";var o=a.from_html(`<div> </div>`);function s(s,c){let l=a.legacy_rest_props(c,[`children`,`$$slots`,`$$events`,`$$legacy`]),u=a.legacy_rest_props(l,[`controlComponent`,`value`]);a.push(c,!1);let d=a.prop(c,`controlComponent`,8),f=a.prop(c,`value`,12),p=r(),m=a.mutable_source(),h,g=n({});function _(e,t){Object.assign(g,{value:e,attrs:t})}i(()=>{_(f(),u),h=e({render(){let e={};if(d().emits)for(let t in d().emits){let n=Array.isArray(d().emits)?d().emits[t]:t,r=n===`input`?`update:modelValue`:n;e[`on${r.charAt(0).toUpperCase()}${r.substring(1)}`]=(...e)=>{n===`update:modelValue`?f(e[0]):p(n,...e)}}return t(d(),{modelValue:f(),...g.attrs,...e,key:`component`})}}),h.mount(a.get(m))}),a.legacy_pre_effect(()=>(a.deep_read_state(f()),a.deep_read_state(u)),()=>{_(f(),u)}),a.legacy_pre_effect_reset(),a.init();var v=o(),y=a.child(v,!0);a.reset(v),a.bind_this(v,e=>a.set(m,e),()=>a.get(m)),a.template_effect(()=>a.set_text(y,(a.deep_read_state(d()),a.untrack(()=>d().name)))),a.append(s,v),a.pop()}export{s as default};
1
+ import{createApp as e,h as t,reactive as n}from"@poveste/vendors/vue";import{createEventDispatcher as r,onMount as i}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as a from"svelte/internal/client";var o=a.from_html(`<div> </div>`);function s(s,c){let l=a.legacy_rest_props(c,[`children`,`$$slots`,`$$events`,`$$legacy`]),u=a.legacy_rest_props(l,[`controlComponent`,`value`]);a.push(c,!1);let d=a.prop(c,`controlComponent`,8),f=a.prop(c,`value`,12),p=r(),m=a.mutable_source(),h,g=n({});function _(e,t){Object.assign(g,{value:e,attrs:t})}i(()=>{_(f(),u),h=e({render(){let e={};if(d().emits)for(let t in d().emits){let n=Array.isArray(d().emits)?d().emits[t]:t,r=n===`input`?`update:modelValue`:n;e[`on${r.charAt(0).toUpperCase()}${r.substring(1)}`]=(...e)=>{n===`update:modelValue`?f(e[0]):p(n,...e)}}return t(d(),{modelValue:g.value,...g.attrs,...e,key:`component`})}}),h.mount(a.get(m))}),a.legacy_pre_effect(()=>(a.deep_read_state(f()),a.deep_read_state(u)),()=>{_(f(),u)}),a.legacy_pre_effect_reset(),a.init();var v=o(),y=a.child(v,!0);a.reset(v),a.bind_this(v,e=>a.set(m,e),()=>a.get(m)),a.template_effect(()=>a.set_text(y,(a.deep_read_state(d()),a.untrack(()=>d().name)))),a.append(s,v),a.pop()}export{s as default};
@@ -45,8 +45,12 @@
45
45
  }
46
46
  }
47
47
 
48
+ // `state` is the Vue-reactive mirror kept in sync by the `$:` below.
49
+ // Reading the plain `value` here would make the render depend on a
50
+ // variable Vue cannot track — it only saw fresh values because the
51
+ // `state` mutation was what triggered the re-render in the first place.
48
52
  return _h(controlComponent, {
49
- modelValue: value,
53
+ modelValue: state.value,
50
54
  ...state.attrs,
51
55
  ...finalListeners,
52
56
  key: 'component',
@@ -1 +1 @@
1
- import{getContext as e,setContext as t}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as n from"svelte/internal/client";function r(r,i){n.push(i,!1);let a=n.prop(i,`title`,8,null),o=n.prop(i,`id`,8,null),s=n.prop(i,`group`,8,null),c=n.prop(i,`layout`,8,null),l=n.prop(i,`icon`,8,null),u=n.prop(i,`iconColor`,8,null),d=n.prop(i,`docsOnly`,8,!1),f=e(`__pvtAddStory`),p=e(`__pvtStoryFile`),m={id:o()??p.id,title:a()??p.fileName,group:s(),layout:c(),icon:l(),iconColor:u(),docsOnly:d(),variants:[]};f(m),t(`__pvtStory`,m),t(`__pvtAddVariant`,e=>{m.variants.push(e)}),n.init();var h=n.comment(),g=n.first_child(h);n.slot(g,i,`default`,{},null),n.append(r,h),n.pop()}export{r as default};
1
+ import{getContext as e,setContext as t}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as n from"svelte/internal/client";function r(r,i){n.push(i,!1);let a=n.prop(i,`title`,8,null),o=n.prop(i,`id`,8,null),s=n.prop(i,`group`,8,null),c=n.prop(i,`layout`,8,null),l=n.prop(i,`icon`,8,null),u=n.prop(i,`iconColor`,8,null),d=n.prop(i,`docsOnly`,8,!1),f=n.prop(i,`initState`,8,null),p=e(`__pvtAddStory`),m=e(`__pvtStoryFile`),h={id:o()??m.id,title:a()??m.fileName,group:s(),layout:c(),icon:l(),iconColor:u(),docsOnly:d(),variants:[]};p(h);let g=f()?f()():{};t(`__pvtStory`,h),t(`__pvtAddVariant`,e=>{h.variants.push(e)}),n.init();var _=n.comment(),v=n.first_child(_);n.slot(v,i,`default`,{get state(){return g}},null),n.append(r,_),n.pop()}export{r as default};
@@ -8,6 +8,7 @@
8
8
  export let icon = null
9
9
  export let iconColor = null
10
10
  export let docsOnly = false
11
+ export let initState = null
11
12
 
12
13
  const addStory = getContext('__pvtAddStory')
13
14
  const file = getContext('__pvtStoryFile')
@@ -25,10 +26,16 @@
25
26
 
26
27
  addStory(story)
27
28
 
29
+ // Collection renders the story's markup purely to discover its variants, but
30
+ // that markup now reads `state`. Without a value here every expression
31
+ // referencing it throws and collection fails with a bare
32
+ // `Cannot read properties of undefined` (#81).
33
+ const state = initState ? initState() : {}
34
+
28
35
  setContext('__pvtStory', story)
29
36
  setContext('__pvtAddVariant', (variant) => {
30
37
  story.variants.push(variant)
31
38
  })
32
39
  </script>
33
40
 
34
- <slot />
41
+ <slot {state} />
@@ -1 +1 @@
1
- import{getContext as e}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as t from"svelte/internal/client";function n(n,r){t.push(r,!1);let i=t.prop(r,`title`,8,`untitled`),a=t.prop(r,`id`,8,null),o=t.prop(r,`icon`,8,null),s=t.prop(r,`iconColor`,8,null),c=e(`__pvtStory`),l=e(`__pvtAddVariant`);function u(){return`${c.id}-${c.variants.length}`}l({id:a()??u(),title:i(),icon:o(),iconColor:s()}),t.init(),t.pop()}export{n as default};
1
+ import{getContext as e}from"svelte";import"svelte/internal/disclose-version";import"svelte/internal/flags/legacy";import*as t from"svelte/internal/client";function n(n,r){t.push(r,!1);let i=t.prop(r,`title`,8,`untitled`),a=t.prop(r,`id`,8,null),o=t.prop(r,`icon`,8,null),s=t.prop(r,`iconColor`,8,null);t.prop(r,`initState`,8,null);let c=e(`__pvtStory`),l=e(`__pvtAddVariant`);function u(){return`${c.id}-${c.variants.length}`}l({id:a()??u(),title:i(),icon:o(),iconColor:s()}),t.init(),t.pop()}export{n as default};
@@ -5,6 +5,9 @@
5
5
  export let id = null
6
6
  export let icon = null
7
7
  export let iconColor = null
8
+ // Declared but unused: collection does not render variant children, and an
9
+ // undeclared prop would warn on every story that passes one.
10
+ export let initState = null
8
11
 
9
12
  const story = getContext('__pvtStory')
10
13
  const addVariant = getContext('__pvtAddVariant')
package/dist/helpers.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { HstControlOption } from '@poveste/controls';
2
2
  import type { Story, StoryProps, Variant, VariantProps } from '@poveste/shared';
3
- import type { SvelteComponentTyped } from 'svelte';
3
+ import type { Component, Snippet } from 'svelte';
4
4
  export interface SvelteStorySetupApi {
5
5
  app: any;
6
6
  story?: Story;
@@ -11,77 +11,110 @@ export declare function defineSetupSvelte(handler: SvelteStorySetupHandler): Sve
11
11
  export declare const defineSetupSvelte3: typeof defineSetupSvelte;
12
12
  export declare const defineSetupSvelte4: typeof defineSetupSvelte;
13
13
  export declare const defineSetupSvelte5: typeof defineSetupSvelte;
14
+ export type StoryState = Record<string, any>;
15
+ /**
16
+ * Props plus Svelte 5 children.
17
+ *
18
+ * `StoryProps` and `VariantProps` live in `@poveste/shared` and are shared with
19
+ * the Vue plugin, so a Svelte-only `Snippet` cannot be added there — it is
20
+ * intersected in here instead.
21
+ *
22
+ * This is what the move off `SvelteComponentTyped` needed. The class form carried
23
+ * Svelte 4 slot typing, so nested content type-checked implicitly; `Component<P>`
24
+ * has no slots, and Svelte 5 passes children as a prop (#99).
25
+ */
26
+ type WithChildren<P> = P & {
27
+ children?: Snippet<[{
28
+ state: StoryState;
29
+ }]>;
30
+ controls?: Snippet<[{
31
+ state: StoryState;
32
+ }]>;
33
+ };
34
+ /**
35
+ * Story state is owned by poveste, not by the story component.
36
+ *
37
+ * The story is mounted once per slot, so a component-local variable exists twice
38
+ * and cannot be shared. `initState` seeds `variant.state`, which both mounts read
39
+ * and which the sandbox bridge carries across the iframe boundary (#81).
40
+ */
41
+ type WithState<P> = Omit<P, 'source'> & {
42
+ initState?: () => StoryState;
43
+ /** A literal, or a function of the variant state so the source panel can track the controls. */
44
+ source?: string | ((state: StoryState) => string);
45
+ };
14
46
  export interface Hst {
15
- Story: typeof SvelteComponentTyped<StoryProps>;
16
- Variant: typeof SvelteComponentTyped<VariantProps>;
17
- Button: typeof SvelteComponentTyped;
18
- ButtonGroup: typeof SvelteComponentTyped<{
47
+ Story: Component<WithState<WithChildren<StoryProps>>>;
48
+ Variant: Component<WithState<WithChildren<VariantProps>>>;
49
+ Button: Component<WithChildren<Record<string, any>>>;
50
+ ButtonGroup: Component<{
19
51
  value?: string;
20
52
  options: (string | HstControlOption)[];
21
53
  title?: string;
22
54
  }>;
23
- Checkbox: typeof SvelteComponentTyped<{
55
+ Checkbox: Component<{
24
56
  value?: boolean;
25
57
  title: string;
26
58
  }>;
27
- CheckboxList: typeof SvelteComponentTyped<{
59
+ CheckboxList: Component<{
28
60
  value: string[];
29
61
  options: (string | HstControlOption)[];
30
62
  title?: string;
31
63
  }>;
32
- Text: typeof SvelteComponentTyped<{
64
+ Text: Component<{
33
65
  value?: string;
34
66
  title: string;
35
67
  }>;
36
- Number: typeof SvelteComponentTyped<{
68
+ Number: Component<{
37
69
  value?: number;
38
70
  title: string;
39
71
  step?: number;
40
72
  }>;
41
- Slider: typeof SvelteComponentTyped<{
73
+ Slider: Component<{
42
74
  value?: number;
43
75
  title: string;
44
76
  min: number;
45
77
  max: number;
46
78
  step?: number;
47
79
  }>;
48
- Textarea: typeof SvelteComponentTyped<{
80
+ Textarea: Component<{
49
81
  value?: string;
50
82
  title: string;
51
83
  }>;
52
- Select: typeof SvelteComponentTyped<{
84
+ Select: Component<{
53
85
  value?: string;
54
86
  title: string;
55
87
  options: Record<string, any> | string[] | HstControlOption[];
56
88
  }>;
57
- Radio: typeof SvelteComponentTyped<{
89
+ Radio: Component<{
58
90
  value?: string;
59
91
  options: HstControlOption[];
60
92
  title?: string;
61
93
  }>;
62
- Json: typeof SvelteComponentTyped<{
94
+ Json: Component<{
63
95
  value: unknown;
64
96
  title: string;
65
97
  }>;
66
- Shades: typeof SvelteComponentTyped<{
98
+ Shades: Component<{
67
99
  shades: Record<string, any>;
68
100
  getName?: (key: string, color: string) => string;
69
101
  search?: string;
70
102
  }>;
71
- TokenList: typeof SvelteComponentTyped<{
103
+ TokenList: Component<{
72
104
  tokens: Record<string, string | number | any[] | Record<string, any>>;
73
105
  getName?: (key: string, value: string | number | any[] | Record<string, any>) => string;
74
106
  }>;
75
- TokenGrid: typeof SvelteComponentTyped<{
107
+ TokenGrid: Component<{
76
108
  tokens: Record<string, string | number | any[] | Record<string, any>>;
77
109
  getName?: (key: string, value: string | number | any[] | Record<string, any>) => string;
78
110
  colSize?: number;
79
111
  }>;
80
- CopyIcon: typeof SvelteComponentTyped<{
112
+ CopyIcon: Component<{
81
113
  content: string;
82
114
  }>;
83
- ColorSelect: typeof SvelteComponentTyped<{
115
+ ColorSelect: Component<{
84
116
  value?: string;
85
117
  title: string;
86
118
  }>;
87
119
  }
120
+ export {};
@@ -79,21 +79,36 @@ export async function callSetupFunctions(generatedSetup, setup, setupApi, varian
79
79
  await variantSetupApp(setupApi);
80
80
  }
81
81
  }
82
+ /**
83
+ * Adds `controlComponent` to a props object **without** spreading it.
84
+ *
85
+ * Svelte 5 passes a bound prop as an accessor pair — `bind:value` becomes a
86
+ * `get value()` / `set value(v)` on the props object. Spreading invokes the
87
+ * getter and writes a plain data property, so the setter is dropped and the
88
+ * child's write-back silently goes nowhere: reads keep working, writes stop.
89
+ * That was the whole of #81.
90
+ *
91
+ * Copying the descriptors keeps the setter intact.
92
+ */
93
+ function withControlComponent(props, controlComponent) {
94
+ const merged = Object.defineProperties({}, Object.getOwnPropertyDescriptors(props ?? {}));
95
+ Object.defineProperty(merged, 'controlComponent', {
96
+ value: controlComponent,
97
+ enumerable: true,
98
+ writable: true,
99
+ configurable: true,
100
+ });
101
+ return merged;
102
+ }
82
103
  export function createWrappedComponent(Wrap, controlComponent) {
83
104
  function ProxyWrap(anchorOrOptions, props) {
84
105
  if (new.target) {
85
106
  return new Wrap({
86
107
  ...anchorOrOptions,
87
- props: {
88
- ...anchorOrOptions?.props,
89
- controlComponent,
90
- },
108
+ props: withControlComponent(anchorOrOptions?.props, controlComponent),
91
109
  });
92
110
  }
93
- return Wrap(anchorOrOptions, {
94
- ...props,
95
- controlComponent,
96
- });
111
+ return Wrap(anchorOrOptions, withControlComponent(props, controlComponent));
97
112
  }
98
113
  if (Wrap?.element) {
99
114
  ProxyWrap.element = Wrap.element;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/plugin-svelte",
3
3
  "type": "module",
4
- "version": "0.3.3",
4
+ "version": "0.5.0",
5
5
  "description": "Poveste plugin for Svelte 5 and SvelteKit support",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -28,7 +28,7 @@
28
28
  "@sveltejs/kit": "^2.53.0",
29
29
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
30
30
  "svelte": "^5.46.4",
31
- "poveste": "^0.3.3"
31
+ "poveste": "^0.5.0"
32
32
  },
33
33
  "peerDependenciesMeta": {
34
34
  "@sveltejs/kit": {
@@ -40,9 +40,9 @@
40
40
  "globby": "^14.0.2",
41
41
  "launch-editor": "^2.9.1",
42
42
  "pathe": "^1.1.2",
43
- "@poveste/controls": "^0.3.3",
44
- "@poveste/shared": "^0.3.3",
45
- "@poveste/vendors": "^0.3.3"
43
+ "@poveste/shared": "^0.5.0",
44
+ "@poveste/controls": "^0.5.0",
45
+ "@poveste/vendors": "^0.5.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
@@ -54,7 +54,7 @@
54
54
  "svelte-preprocess": "^6.0.3",
55
55
  "typescript": "5.6.3",
56
56
  "vite": "^8.0.0",
57
- "poveste": "0.3.3"
57
+ "poveste": "0.5.0"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "rimraf dist && vite build && tsc -d -P tsconfig.build.json && pnpm run build:types",
@@ -1,5 +1,6 @@
1
1
  <script>
2
- import { getContext, setContext } from 'svelte'
2
+ import { watch as _watch } from '@poveste/vendors/vue'
3
+ import { getContext, onDestroy, setContext } from 'svelte'
3
4
 
4
5
  const story = getContext('__pvtStory')
5
6
  const currentVariant = getContext('__pvtVariant')
@@ -9,19 +10,68 @@
9
10
  setContext('__pvtIndex', index)
10
11
 
11
12
  export let source = null
13
+ export let initState = null
12
14
 
15
+ // Poveste owns the state, not the story component.
16
+ //
17
+ // The story is mounted once per slot — the app renders it with
18
+ // `slot-name="controls"`, the preview with the default — so a `let count` in
19
+ // the story exists twice and cannot be shared. Svelte 4 bridged the two
20
+ // instances with `$capture_state` / `$inject_state`; Svelte 5 provides neither,
21
+ // and no shim does either. So state lives on the variant, which both mounts
22
+ // read from the same context object and which the sandbox bridge already
23
+ // serialises across the iframe boundary (#81).
24
+ //
25
+ // Whichever mount arrives first seeds it; the second must not, or it would
26
+ // clobber whatever the user has already changed.
27
+ if (initState && currentVariant && !currentVariant.__pvtStateSeeded) {
28
+ currentVariant.__pvtStateSeeded = true
29
+ currentVariant.state = { ...currentVariant.state, ...initState() }
30
+ }
31
+
32
+ // Only from the mount that renders the controls. The story is mounted once per
33
+ // slot and, for an iframe layout, in a different realm — so the flag below cannot
34
+ // dedupe across them and the warning would appear twice.
35
+ if (!initState && slotName === 'controls' && $$slots.controls && currentVariant && !currentVariant.__pvtStateWarned) {
36
+ currentVariant.__pvtStateWarned = true
37
+ console.error([
38
+ `[poveste] Story "${story?.title ?? story?.id}" has a controls slot but no \`initState\`.`,
39
+ 'Controls will render and appear to work, but their edits cannot reach the story:',
40
+ 'the story is mounted separately for each slot, so a component-local variable is not shared.',
41
+ 'Pass `initState` and read state from the snippet —',
42
+ 'https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate',
43
+ ].join(' '))
44
+ }
45
+
46
+ // `variant.state` is a Vue-reactive object, so Svelte has no way to know when a
47
+ // control writes into it. Mirroring the reference back into a local on every
48
+ // Vue tick is what makes the template re-render: Svelte's invalidation treats
49
+ // any object assignment as a change, so reassigning the same proxy is enough.
50
+ let state = currentVariant?.state
51
+
52
+ const stopWatchingState = _watch(() => currentVariant?.state, () => {
53
+ state = currentVariant?.state
54
+ }, { deep: true })
55
+
56
+ onDestroy(stopWatchingState)
57
+
58
+ // `source` may be a function of the state. Story props are evaluated in the
59
+ // component's script, where the slot's `state` is not in scope, so a string
60
+ // literal cannot reflect what the controls are doing — which is most of the
61
+ // point of the source panel (#81).
13
62
  $: {
14
- if (source != null) {
63
+ const resolvedSource = typeof source === 'function' ? source(state) : source
64
+ if (resolvedSource != null) {
15
65
  Object.assign(currentVariant, {
16
- source,
66
+ source: resolvedSource,
17
67
  })
18
68
  }
19
69
  }
20
70
  </script>
21
71
 
22
72
  {#if slotName === 'controls'}
23
- <slot name="controls" />
73
+ <slot name="controls" {state} />
24
74
  {/if}
25
75
  {#if slotName === 'default' || story.meta?.hasVariantChildComponents}
26
- <slot />
76
+ <slot {state} />
27
77
  {/if}
@@ -1,5 +1,6 @@
1
1
  <script>
2
- import { getContext } from 'svelte'
2
+ import { watch as _watch } from '@poveste/vendors/vue'
3
+ import { getContext, onDestroy } from 'svelte'
3
4
 
4
5
  const story = getContext('__pvtStory')
5
6
  const currentVariant = getContext('__pvtVariant')
@@ -15,11 +16,47 @@
15
16
  const shouldRender = currentVariant.id === variant.id
16
17
 
17
18
  export let source = null
19
+ export let initState = null
18
20
 
21
+ // Same contract as `RenderStory`: poveste owns the state because the story is
22
+ // mounted once per slot, so a component-local variable cannot be shared (#81).
23
+ // `variant.state` is also the only bucket the sandbox bridge carries across the
24
+ // iframe boundary, which is why state is per variant rather than per story.
25
+ if (initState && shouldRender && !currentVariant.__pvtStateSeeded) {
26
+ currentVariant.__pvtStateSeeded = true
27
+ currentVariant.state = { ...currentVariant.state, ...initState() }
28
+ }
29
+
30
+ // Controls mount only — see the note in RenderStory: the two mounts do not share
31
+ // a realm for iframe layouts, so this is what keeps it to one warning.
32
+ if (!initState && shouldRender && slotName === 'controls' && $$slots.controls && !currentVariant.__pvtStateWarned) {
33
+ currentVariant.__pvtStateWarned = true
34
+ console.error([
35
+ `[poveste] Variant "${variant?.title ?? variant?.id}" has a controls slot but no \`initState\`.`,
36
+ 'Controls will render and appear to work, but their edits cannot reach the story:',
37
+ 'the story is mounted separately for each slot, so a component-local variable is not shared.',
38
+ 'Pass `initState` and read state from the snippet —',
39
+ 'https://poveste.dev/guide/migration-from-histoire.html#svelte-story-state-moves-to-initstate',
40
+ ].join(' '))
41
+ }
42
+
43
+ let state = currentVariant?.state
44
+
45
+ const stopWatchingState = _watch(() => currentVariant?.state, () => {
46
+ state = currentVariant?.state
47
+ }, { deep: true })
48
+
49
+ onDestroy(stopWatchingState)
50
+
51
+ // `source` may be a function of the state. Story props are evaluated in the
52
+ // component's script, where the slot's `state` is not in scope, so a string
53
+ // literal cannot reflect what the controls are doing — which is most of the
54
+ // point of the source panel (#81).
19
55
  $: {
20
- if (source != null) {
56
+ const resolvedSource = typeof source === 'function' ? source(state) : source
57
+ if (resolvedSource != null) {
21
58
  Object.assign(currentVariant, {
22
- source,
59
+ source: resolvedSource,
23
60
  })
24
61
  }
25
62
  }
@@ -27,9 +64,9 @@
27
64
 
28
65
  {#if shouldRender}
29
66
  {#if slotName === 'default'}
30
- <slot />
67
+ <slot {state} />
31
68
  {/if}
32
69
  {#if slotName === 'controls'}
33
- <slot name="controls" />
70
+ <slot name="controls" {state} />
34
71
  {/if}
35
72
  {/if}
@@ -45,8 +45,12 @@
45
45
  }
46
46
  }
47
47
 
48
+ // `state` is the Vue-reactive mirror kept in sync by the `$:` below.
49
+ // Reading the plain `value` here would make the render depend on a
50
+ // variable Vue cannot track — it only saw fresh values because the
51
+ // `state` mutation was what triggered the re-render in the first place.
48
52
  return _h(controlComponent, {
49
- modelValue: value,
53
+ modelValue: state.value,
50
54
  ...state.attrs,
51
55
  ...finalListeners,
52
56
  key: 'component',
@@ -8,6 +8,7 @@
8
8
  export let icon = null
9
9
  export let iconColor = null
10
10
  export let docsOnly = false
11
+ export let initState = null
11
12
 
12
13
  const addStory = getContext('__pvtAddStory')
13
14
  const file = getContext('__pvtStoryFile')
@@ -25,10 +26,16 @@
25
26
 
26
27
  addStory(story)
27
28
 
29
+ // Collection renders the story's markup purely to discover its variants, but
30
+ // that markup now reads `state`. Without a value here every expression
31
+ // referencing it throws and collection fails with a bare
32
+ // `Cannot read properties of undefined` (#81).
33
+ const state = initState ? initState() : {}
34
+
28
35
  setContext('__pvtStory', story)
29
36
  setContext('__pvtAddVariant', (variant) => {
30
37
  story.variants.push(variant)
31
38
  })
32
39
  </script>
33
40
 
34
- <slot />
41
+ <slot {state} />
@@ -5,6 +5,9 @@
5
5
  export let id = null
6
6
  export let icon = null
7
7
  export let iconColor = null
8
+ // Declared but unused: collection does not render variant children, and an
9
+ // undeclared prop would warn on every story that passes one.
10
+ export let initState = null
8
11
 
9
12
  const story = getContext('__pvtStory')
10
13
  const addVariant = getContext('__pvtAddVariant')
package/src/helpers.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { HstControlOption } from '@poveste/controls'
2
2
  import type { Story, StoryProps, Variant, VariantProps } from '@poveste/shared'
3
- import type { SvelteComponentTyped } from 'svelte'
3
+ import type { Component, Snippet } from 'svelte'
4
4
 
5
5
  export interface SvelteStorySetupApi {
6
6
  app: any
@@ -18,78 +18,113 @@ export const defineSetupSvelte3 = defineSetupSvelte
18
18
  export const defineSetupSvelte4 = defineSetupSvelte
19
19
  export const defineSetupSvelte5 = defineSetupSvelte
20
20
 
21
+ export type StoryState = Record<string, any>
22
+
23
+ /**
24
+ * Props plus Svelte 5 children.
25
+ *
26
+ * `StoryProps` and `VariantProps` live in `@poveste/shared` and are shared with
27
+ * the Vue plugin, so a Svelte-only `Snippet` cannot be added there — it is
28
+ * intersected in here instead.
29
+ *
30
+ * This is what the move off `SvelteComponentTyped` needed. The class form carried
31
+ * Svelte 4 slot typing, so nested content type-checked implicitly; `Component<P>`
32
+ * has no slots, and Svelte 5 passes children as a prop (#99).
33
+ */
34
+ type WithChildren<P> = P & {
35
+ children?: Snippet<[{ state: StoryState }]>
36
+ controls?: Snippet<[{ state: StoryState }]>
37
+ }
38
+
39
+ /**
40
+ * Story state is owned by poveste, not by the story component.
41
+ *
42
+ * The story is mounted once per slot, so a component-local variable exists twice
43
+ * and cannot be shared. `initState` seeds `variant.state`, which both mounts read
44
+ * and which the sandbox bridge carries across the iframe boundary (#81).
45
+ */
46
+ type WithState<P> = Omit<P, 'source'> & {
47
+ initState?: () => StoryState
48
+ /** A literal, or a function of the variant state so the source panel can track the controls. */
49
+ source?: string | ((state: StoryState) => string)
50
+ }
51
+
21
52
  export interface Hst {
22
53
  // Main built-ins
23
- Story: typeof SvelteComponentTyped<StoryProps>
24
- Variant: typeof SvelteComponentTyped<VariantProps>
54
+ Story: Component<WithState<WithChildren<StoryProps>>>
55
+ Variant: Component<WithState<WithChildren<VariantProps>>>
25
56
  // Controls
26
- Button: typeof SvelteComponentTyped
27
- ButtonGroup: typeof SvelteComponentTyped<{
57
+ // Deliberately permissive. This was previously a bare `SvelteComponentTyped`,
58
+ // i.e. props `any`; it wraps a Vue control whose default slot renders the label,
59
+ // and the prop set `Wrap` forwards is not pinned down. Narrowing it is its own
60
+ // change rather than a silent break.
61
+ Button: Component<WithChildren<Record<string, any>>>
62
+ ButtonGroup: Component<{
28
63
  value?: string
29
64
  options: (string | HstControlOption)[]
30
65
  title?: string
31
66
  }>
32
- Checkbox: typeof SvelteComponentTyped<{
67
+ Checkbox: Component<{
33
68
  value?: boolean
34
69
  title: string
35
70
  }>
36
- CheckboxList: typeof SvelteComponentTyped<{
71
+ CheckboxList: Component<{
37
72
  value: string[]
38
73
  options: (string | HstControlOption)[]
39
74
  title?: string
40
75
  }>
41
- Text: typeof SvelteComponentTyped<{
76
+ Text: Component<{
42
77
  value?: string
43
78
  title: string
44
79
  }>
45
- Number: typeof SvelteComponentTyped<{
80
+ Number: Component<{
46
81
  value?: number
47
82
  title: string
48
83
  step?: number
49
84
  }>
50
- Slider: typeof SvelteComponentTyped<{
85
+ Slider: Component<{
51
86
  value?: number
52
87
  title: string
53
88
  min: number
54
89
  max: number
55
90
  step?: number
56
91
  }>
57
- Textarea: typeof SvelteComponentTyped<{
92
+ Textarea: Component<{
58
93
  value?: string
59
94
  title: string
60
95
  }>
61
- Select: typeof SvelteComponentTyped<{
96
+ Select: Component<{
62
97
  value?: string
63
98
  title: string
64
99
  options: Record<string, any> | string[] | HstControlOption[]
65
100
  }>
66
- Radio: typeof SvelteComponentTyped<{
101
+ Radio: Component<{
67
102
  value?: string
68
103
  options: HstControlOption[]
69
104
  title?: string
70
105
  }>
71
- Json: typeof SvelteComponentTyped<{
106
+ Json: Component<{
72
107
  value: unknown
73
108
  title: string
74
109
  }>
75
- Shades: typeof SvelteComponentTyped<{
110
+ Shades: Component<{
76
111
  shades: Record<string, any>
77
112
  getName?: (key: string, color: string) => string
78
113
  search?: string
79
114
  }>
80
- TokenList: typeof SvelteComponentTyped<{
115
+ TokenList: Component<{
81
116
  tokens: Record<string, string | number | any[] | Record<string, any>>
82
117
  getName?: (key: string, value: string | number | any[] | Record<string, any>) => string
83
118
  }>
84
- TokenGrid: typeof SvelteComponentTyped<{
119
+ TokenGrid: Component<{
85
120
  tokens: Record<string, string | number | any[] | Record<string, any>>
86
121
  getName?: (key: string, value: string | number | any[] | Record<string, any>) => string
87
122
  colSize?: number
88
123
  }>
89
- CopyIcon: typeof SvelteComponentTyped<{
124
+ CopyIcon: Component<{
90
125
  content: string
91
126
  }>
92
- ColorSelect: typeof SvelteComponentTyped<{
127
+ ColorSelect: Component<{
93
128
  value?: string
94
129
  title: string
95
130
  }>
@@ -111,22 +111,38 @@ export async function callSetupFunctions(
111
111
  }
112
112
  }
113
113
 
114
+ /**
115
+ * Adds `controlComponent` to a props object **without** spreading it.
116
+ *
117
+ * Svelte 5 passes a bound prop as an accessor pair — `bind:value` becomes a
118
+ * `get value()` / `set value(v)` on the props object. Spreading invokes the
119
+ * getter and writes a plain data property, so the setter is dropped and the
120
+ * child's write-back silently goes nowhere: reads keep working, writes stop.
121
+ * That was the whole of #81.
122
+ *
123
+ * Copying the descriptors keeps the setter intact.
124
+ */
125
+ function withControlComponent(props: any, controlComponent: any) {
126
+ const merged = Object.defineProperties({}, Object.getOwnPropertyDescriptors(props ?? {}))
127
+ Object.defineProperty(merged, 'controlComponent', {
128
+ value: controlComponent,
129
+ enumerable: true,
130
+ writable: true,
131
+ configurable: true,
132
+ })
133
+ return merged
134
+ }
135
+
114
136
  export function createWrappedComponent(Wrap: any, controlComponent: any) {
115
137
  function ProxyWrap(anchorOrOptions: any, props?: any) {
116
138
  if (new.target) {
117
139
  return new Wrap({
118
140
  ...anchorOrOptions,
119
- props: {
120
- ...anchorOrOptions?.props,
121
- controlComponent,
122
- },
141
+ props: withControlComponent(anchorOrOptions?.props, controlComponent),
123
142
  })
124
143
  }
125
144
 
126
- return Wrap(anchorOrOptions, {
127
- ...props,
128
- controlComponent,
129
- })
145
+ return Wrap(anchorOrOptions, withControlComponent(props, controlComponent))
130
146
  }
131
147
 
132
148
  if (Wrap?.element) {