@poveste/plugin-svelte 0.4.0 → 0.5.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.
- package/README.md +60 -0
- package/dist/client/RenderStory.js +1 -1
- package/dist/client/RenderStory.svelte +55 -5
- package/dist/client/RenderVariant.js +1 -1
- package/dist/client/RenderVariant.svelte +42 -5
- package/dist/client/Wrap.js +1 -1
- package/dist/client/Wrap.svelte +5 -1
- package/dist/collect/Story.js +1 -1
- package/dist/collect/Story.svelte +8 -1
- package/dist/collect/Variant.js +1 -1
- package/dist/collect/Variant.svelte +3 -0
- package/dist/helpers.d.ts +52 -19
- package/dist/util/svelte.js +23 -8
- package/package.json +6 -6
- package/src/client/RenderStory.svelte +55 -5
- package/src/client/RenderVariant.svelte +42 -5
- package/src/client/Wrap.svelte +5 -1
- package/src/collect/Story.svelte +8 -1
- package/src/collect/Variant.svelte +3 -0
- package/src/helpers.ts +54 -19
- package/src/util/svelte.ts +24 -8
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
|
|
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 {
|
|
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
|
-
|
|
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{
|
|
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 {
|
|
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
|
-
|
|
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}
|
package/dist/client/Wrap.js
CHANGED
|
@@ -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:
|
|
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};
|
package/dist/client/Wrap.svelte
CHANGED
|
@@ -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',
|
package/dist/collect/Story.js
CHANGED
|
@@ -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`),
|
|
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} />
|
package/dist/collect/Variant.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
16
|
-
Variant:
|
|
17
|
-
Button:
|
|
18
|
-
ButtonGroup:
|
|
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:
|
|
55
|
+
Checkbox: Component<{
|
|
24
56
|
value?: boolean;
|
|
25
57
|
title: string;
|
|
26
58
|
}>;
|
|
27
|
-
CheckboxList:
|
|
59
|
+
CheckboxList: Component<{
|
|
28
60
|
value: string[];
|
|
29
61
|
options: (string | HstControlOption)[];
|
|
30
62
|
title?: string;
|
|
31
63
|
}>;
|
|
32
|
-
Text:
|
|
64
|
+
Text: Component<{
|
|
33
65
|
value?: string;
|
|
34
66
|
title: string;
|
|
35
67
|
}>;
|
|
36
|
-
Number:
|
|
68
|
+
Number: Component<{
|
|
37
69
|
value?: number;
|
|
38
70
|
title: string;
|
|
39
71
|
step?: number;
|
|
40
72
|
}>;
|
|
41
|
-
Slider:
|
|
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:
|
|
80
|
+
Textarea: Component<{
|
|
49
81
|
value?: string;
|
|
50
82
|
title: string;
|
|
51
83
|
}>;
|
|
52
|
-
Select:
|
|
84
|
+
Select: Component<{
|
|
53
85
|
value?: string;
|
|
54
86
|
title: string;
|
|
55
87
|
options: Record<string, any> | string[] | HstControlOption[];
|
|
56
88
|
}>;
|
|
57
|
-
Radio:
|
|
89
|
+
Radio: Component<{
|
|
58
90
|
value?: string;
|
|
59
91
|
options: HstControlOption[];
|
|
60
92
|
title?: string;
|
|
61
93
|
}>;
|
|
62
|
-
Json:
|
|
94
|
+
Json: Component<{
|
|
63
95
|
value: unknown;
|
|
64
96
|
title: string;
|
|
65
97
|
}>;
|
|
66
|
-
Shades:
|
|
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:
|
|
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:
|
|
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:
|
|
112
|
+
CopyIcon: Component<{
|
|
81
113
|
content: string;
|
|
82
114
|
}>;
|
|
83
|
-
ColorSelect:
|
|
115
|
+
ColorSelect: Component<{
|
|
84
116
|
value?: string;
|
|
85
117
|
title: string;
|
|
86
118
|
}>;
|
|
87
119
|
}
|
|
120
|
+
export {};
|
package/dist/util/svelte.js
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.5.1",
|
|
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.
|
|
31
|
+
"poveste": "^0.5.1"
|
|
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.
|
|
44
|
-
"@poveste/shared": "^0.
|
|
45
|
-
"@poveste/vendors": "^0.
|
|
43
|
+
"@poveste/controls": "^0.5.1",
|
|
44
|
+
"@poveste/shared": "^0.5.1",
|
|
45
|
+
"@poveste/vendors": "^0.5.1"
|
|
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.
|
|
57
|
+
"poveste": "0.5.1"
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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}
|
package/src/client/Wrap.svelte
CHANGED
|
@@ -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',
|
package/src/collect/Story.svelte
CHANGED
|
@@ -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 {
|
|
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:
|
|
24
|
-
Variant:
|
|
54
|
+
Story: Component<WithState<WithChildren<StoryProps>>>
|
|
55
|
+
Variant: Component<WithState<WithChildren<VariantProps>>>
|
|
25
56
|
// Controls
|
|
26
|
-
|
|
27
|
-
|
|
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:
|
|
67
|
+
Checkbox: Component<{
|
|
33
68
|
value?: boolean
|
|
34
69
|
title: string
|
|
35
70
|
}>
|
|
36
|
-
CheckboxList:
|
|
71
|
+
CheckboxList: Component<{
|
|
37
72
|
value: string[]
|
|
38
73
|
options: (string | HstControlOption)[]
|
|
39
74
|
title?: string
|
|
40
75
|
}>
|
|
41
|
-
Text:
|
|
76
|
+
Text: Component<{
|
|
42
77
|
value?: string
|
|
43
78
|
title: string
|
|
44
79
|
}>
|
|
45
|
-
Number:
|
|
80
|
+
Number: Component<{
|
|
46
81
|
value?: number
|
|
47
82
|
title: string
|
|
48
83
|
step?: number
|
|
49
84
|
}>
|
|
50
|
-
Slider:
|
|
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:
|
|
92
|
+
Textarea: Component<{
|
|
58
93
|
value?: string
|
|
59
94
|
title: string
|
|
60
95
|
}>
|
|
61
|
-
Select:
|
|
96
|
+
Select: Component<{
|
|
62
97
|
value?: string
|
|
63
98
|
title: string
|
|
64
99
|
options: Record<string, any> | string[] | HstControlOption[]
|
|
65
100
|
}>
|
|
66
|
-
Radio:
|
|
101
|
+
Radio: Component<{
|
|
67
102
|
value?: string
|
|
68
103
|
options: HstControlOption[]
|
|
69
104
|
title?: string
|
|
70
105
|
}>
|
|
71
|
-
Json:
|
|
106
|
+
Json: Component<{
|
|
72
107
|
value: unknown
|
|
73
108
|
title: string
|
|
74
109
|
}>
|
|
75
|
-
Shades:
|
|
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:
|
|
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:
|
|
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:
|
|
124
|
+
CopyIcon: Component<{
|
|
90
125
|
content: string
|
|
91
126
|
}>
|
|
92
|
-
ColorSelect:
|
|
127
|
+
ColorSelect: Component<{
|
|
93
128
|
value?: string
|
|
94
129
|
title: string
|
|
95
130
|
}>
|
package/src/util/svelte.ts
CHANGED
|
@@ -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) {
|