@picked/svelte 0.0.62-next.9 → 0.0.63-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.
- package/dist/components/LazyImage.svelte +1 -1
- package/dist/experience/ExperienceHost.svelte +112 -0
- package/dist/experience/ExperienceSlot.svelte +143 -0
- package/dist/experience/set-picked-context.d.ts +29 -0
- package/dist/experience/set-picked-context.js +1 -0
- package/dist/hooks/action.js +1 -1
- package/dist/hooks/predicate.js +1 -1
- package/dist/hooks/step.js +1 -1
- package/dist/hooks/stores.js +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +62 -21
- package/dist/plugins/devtools/components/Tabs.svelte +30 -6
- package/dist/plugins/devtools/screens/config/ConfigScreen.svelte +153 -0
- package/dist/plugins/devtools/screens/content/ContentListScreen.svelte +89 -0
- package/dist/plugins/devtools/screens/fields/FieldTableRow.svelte +276 -0
- package/dist/plugins/devtools/screens/fields/FieldsListScreen.svelte +12 -0
- package/dist/plugins/devtools/screens/fields/FieldsTable.svelte +78 -0
- package/dist/plugins/devtools/screens/identity/IdentityScreen.svelte +66 -0
- package/dist/render/browser.d.ts +24 -1
- package/dist/render/browser.js +1 -1
- package/package.json +24 -6
- package/dist/plugins/devtools/PickedLogo.svelte +0 -58
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
if (isConvincelyDomain(url)) {
|
|
89
89
|
const encodedUrl = encodeURIComponent(url.toString())
|
|
90
90
|
const optimizedSize = findClosestSize(targetWidth)
|
|
91
|
-
return `https://img.
|
|
91
|
+
return `https://img.picked-content.com/_next/image?url=${encodedUrl}&w=${optimizedSize}&q=75`
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// For other domains, return the original URL
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* @file Internal host that instantiates a single experience, exposes the instance via context,
|
|
4
|
+
* and renders the resolved Svelte component.
|
|
5
|
+
*
|
|
6
|
+
* Lazy loaders and promise-wrapped components load asynchronously via `{#await}`: they render
|
|
7
|
+
* nothing on the server and on the hydration first paint, then swap in once resolved on the
|
|
8
|
+
* client, so the server and the client's first paint agree.
|
|
9
|
+
*/
|
|
10
|
+
import { setContext } from 'svelte'
|
|
11
|
+
import type { Component } from 'svelte'
|
|
12
|
+
import type { ExperienceStore, Picked } from '@picked/client'
|
|
13
|
+
import { createExperienceInstance } from '@picked/client/server'
|
|
14
|
+
import { isLazyLoader } from '@picked/client'
|
|
15
|
+
import { EXPERIENCE_CONTEXT_KEY } from '../context'
|
|
16
|
+
import { useUnit } from '../hooks'
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
experience: ExperienceStore
|
|
20
|
+
client: Picked
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { experience, client }: Props = $props()
|
|
24
|
+
|
|
25
|
+
// `client` and `experience` are stable for this host's lifetime — `Experience` keys each host
|
|
26
|
+
// by `experience.id`, so a different experience mounts a new host rather than mutating props.
|
|
27
|
+
// Capturing their initial values is therefore intentional.
|
|
28
|
+
// svelte-ignore state_referenced_locally
|
|
29
|
+
const instance = createExperienceInstance({ client, experience })
|
|
30
|
+
|
|
31
|
+
setContext(EXPERIENCE_CONTEXT_KEY, instance)
|
|
32
|
+
|
|
33
|
+
const resolved = useUnit(instance.componentAndFrameworkStore)
|
|
34
|
+
// svelte-ignore state_referenced_locally
|
|
35
|
+
const ready = useUnit(client.readyStore)
|
|
36
|
+
|
|
37
|
+
// Slot-rendered experiences have no DOM target, so the normal lifecycle never loads their CMS
|
|
38
|
+
// content. Once the client is ready (so `client.contentLoader` is set by `start()`), load it
|
|
39
|
+
// here; `getStepContent` picks it up reactively. `$effect` does not run during SSR, where
|
|
40
|
+
// content arrives pre-registered via the snapshot.
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if ($ready) {
|
|
43
|
+
// Content fetch failures (e.g. a 404 when the content endpoint is unavailable) are already
|
|
44
|
+
// swallowed inside the effect; the explicit catch guarantees no unhandled rejection even if
|
|
45
|
+
// that contract changes, so a missing/erroring content endpoint never breaks rendering.
|
|
46
|
+
experience.prepareExperienceContentFx().catch(() => undefined)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Narrows to a directly-renderable Svelte component, excluding lazy loaders and promises.
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} value - The adapted component's `component` field.
|
|
54
|
+
* @returns {boolean} True when the value is a direct Svelte component.
|
|
55
|
+
* @example
|
|
56
|
+
* isDirectSvelteComponent(SomeComponent)
|
|
57
|
+
*/
|
|
58
|
+
function isDirectSvelteComponent(value: unknown): value is Component {
|
|
59
|
+
return typeof value === 'function' && !isLazyLoader(value) && !(value instanceof Promise)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns `value` when it is a directly-renderable Svelte component.
|
|
64
|
+
*
|
|
65
|
+
* @param {unknown} value - The candidate component (a loaded module's default export).
|
|
66
|
+
* @returns {Component} The Svelte component.
|
|
67
|
+
* @throws {Error} When the value is not a Svelte component.
|
|
68
|
+
* @example
|
|
69
|
+
* toComponent((await loader.load()).default)
|
|
70
|
+
*/
|
|
71
|
+
function toComponent(value: unknown): Component {
|
|
72
|
+
if (isDirectSvelteComponent(value)) {
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error('Lazy experience component did not resolve to a Svelte component.')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns a promise resolving to the component for a lazy loader or promise-wrapped component,
|
|
81
|
+
* or null for a direct component (or when there is no component).
|
|
82
|
+
*
|
|
83
|
+
* @param {unknown} value - The adapted component's `component` field.
|
|
84
|
+
* @returns {Promise<Component> | null} The loader promise, or null.
|
|
85
|
+
* @example
|
|
86
|
+
* toComponentLoader(lazyLoader)
|
|
87
|
+
*/
|
|
88
|
+
function toComponentLoader(value: unknown): Promise<Component> | null {
|
|
89
|
+
if (isLazyLoader(value)) {
|
|
90
|
+
return value.load().then((mod) => toComponent(mod.default))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (value instanceof Promise) {
|
|
94
|
+
return value.then((mod) => toComponent(mod.default))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const adapted = $derived($resolved?.component.component)
|
|
101
|
+
const svelteComponent = $derived(adapted ? adapted.component : undefined)
|
|
102
|
+
const componentLoader = $derived(toComponentLoader(svelteComponent))
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
{#if isDirectSvelteComponent(svelteComponent)}
|
|
106
|
+
{@const Comp = svelteComponent}
|
|
107
|
+
<Comp {client} experience={instance} />
|
|
108
|
+
{:else if componentLoader}
|
|
109
|
+
{#await componentLoader then Comp}
|
|
110
|
+
<Comp {client} experience={instance} />
|
|
111
|
+
{/await}
|
|
112
|
+
{/if}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* @file Slot-based experience renderer. Renders every registered experience that targets the
|
|
4
|
+
* named slot and whose condition is active, ordered by placement. Active is read synchronously
|
|
5
|
+
* from the seeded condition-results store (sync on SSR / hydration first paint); on a cache miss
|
|
6
|
+
* (standalone, no snapshot) the condition is evaluated asynchronously and the store update
|
|
7
|
+
* re-renders reactively.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExperienceStore, ExperienceTarget } from '@picked/client'
|
|
10
|
+
import { evaluateConditionMemo, hashRule, ruleReferencesAction } from '@picked/client/server'
|
|
11
|
+
import { getPicked, useUnit } from '../hooks'
|
|
12
|
+
import ExperienceHost from './ExperienceHost.svelte'
|
|
13
|
+
|
|
14
|
+
type ExperienceConfig = ReturnType<ExperienceStore['configStore']['getState']>
|
|
15
|
+
type ConditionRule = ExperienceConfig['decisionLogic']
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/**
|
|
19
|
+
* Slot name. Maps to the `.picked-slot-<name>` selector that targeted experiences declare.
|
|
20
|
+
* Named `name` rather than `slot` because Svelte's compiler reserves the `slot` attribute for
|
|
21
|
+
* legacy named-slot assignment, so it cannot be used as a component prop.
|
|
22
|
+
*/
|
|
23
|
+
name: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { name }: Props = $props()
|
|
27
|
+
|
|
28
|
+
const client = getPicked()
|
|
29
|
+
const selector = $derived(`.picked-slot-${name}`)
|
|
30
|
+
|
|
31
|
+
const PLACEMENT_ORDER: ExperienceTarget['placement'][] = [
|
|
32
|
+
'replace',
|
|
33
|
+
'before',
|
|
34
|
+
'prepend',
|
|
35
|
+
'append',
|
|
36
|
+
'after',
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
interface Candidate {
|
|
40
|
+
experience: ExperienceStore
|
|
41
|
+
placement: ExperienceTarget['placement']
|
|
42
|
+
condition: ConditionRule
|
|
43
|
+
hash: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const experiences = useUnit(client.experiences.listStore)
|
|
47
|
+
const conditionResults = useUnit(client.conditionResults.store)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the sort rank for a placement; unknown placements sort last.
|
|
51
|
+
*
|
|
52
|
+
* @param {ExperienceTarget['placement']} placement - The target placement.
|
|
53
|
+
* @returns {number} The placement's index in the precedence order.
|
|
54
|
+
* @example
|
|
55
|
+
* placementRank('append')
|
|
56
|
+
*/
|
|
57
|
+
function placementRank(placement: ExperienceTarget['placement']): number {
|
|
58
|
+
const index = PLACEMENT_ORDER.indexOf(placement)
|
|
59
|
+
return index === -1 ? PLACEMENT_ORDER.length : index
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Maps an experience to a slot candidate, or null when it does not target this slot or carries
|
|
64
|
+
* an action-referencing condition.
|
|
65
|
+
*
|
|
66
|
+
* @param {ExperienceStore} experience - The experience to inspect.
|
|
67
|
+
* @returns {Candidate | null} The candidate, or null when the experience is not an eligible slot match.
|
|
68
|
+
* @example
|
|
69
|
+
* toCandidate(experienceStore)
|
|
70
|
+
*/
|
|
71
|
+
function toCandidate(experience: ExperienceStore): Candidate | null {
|
|
72
|
+
const config = experience.configStore.getState()
|
|
73
|
+
const target = config.targetSelectors.find(
|
|
74
|
+
(selectorTarget) => selectorTarget.selector === selector,
|
|
75
|
+
)
|
|
76
|
+
if (!target) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Action-referencing conditions are never cached (their side effect must re-run), so they can
|
|
81
|
+
// never resolve through the synchronous `peek` the slot path relies on. Slots do not support
|
|
82
|
+
// action-gated conditions in v1; excluding them also stops the selection effect from re-firing
|
|
83
|
+
// the action on every render.
|
|
84
|
+
if (ruleReferencesAction(config.decisionLogic)) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
experience,
|
|
90
|
+
placement: target.placement,
|
|
91
|
+
condition: config.decisionLogic,
|
|
92
|
+
hash: hashRule(config.decisionLogic),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidates = $derived(
|
|
97
|
+
$experiences
|
|
98
|
+
.map(toCandidate)
|
|
99
|
+
.filter((candidate): candidate is Candidate => candidate !== null)
|
|
100
|
+
.sort((a, b) => placementRank(a.placement) - placementRank(b.placement)),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Referencing `$conditionResults` keeps this recompute reactive to late condition writes
|
|
104
|
+
// (the standalone cache-miss path), so a `peek` that returned `undefined` re-renders once
|
|
105
|
+
// the async evaluation resolves and writes the store. An active `replace` experience wins:
|
|
106
|
+
// the first one (it sorts first) renders alone, replacing any other slot matches.
|
|
107
|
+
const active = $derived.by(() => {
|
|
108
|
+
void $conditionResults
|
|
109
|
+
const activeCandidates = candidates.filter(
|
|
110
|
+
(candidate) => client.conditionResults.peek(candidate.hash) === true,
|
|
111
|
+
)
|
|
112
|
+
const firstReplace = activeCandidates.find((candidate) => candidate.placement === 'replace')
|
|
113
|
+
return firstReplace ? [firstReplace] : activeCandidates
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds a plain, JSON-serialisable subset of the current URL for the evaluation context.
|
|
118
|
+
* The live `Location` object is intentionally not passed: json-logic predicates read plain
|
|
119
|
+
* values, and this mirrors the `SerializeLocation` shape the server selects against.
|
|
120
|
+
*
|
|
121
|
+
* @returns {Record<string, string>} The plain location subset.
|
|
122
|
+
* @example
|
|
123
|
+
* buildLocationContext()
|
|
124
|
+
*/
|
|
125
|
+
function buildLocationContext(): Record<string, string> {
|
|
126
|
+
const { href, pathname, search, hostname, origin, protocol, port, host, hash } = window.location
|
|
127
|
+
return { href, pathname, search, hostname, origin, protocol, port, host, hash }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$effect(() => {
|
|
131
|
+
for (const candidate of candidates) {
|
|
132
|
+
if (client.conditionResults.peek(candidate.hash) === undefined) {
|
|
133
|
+
void evaluateConditionMemo(client, candidate.condition, {
|
|
134
|
+
context: { location: buildLocationContext() },
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
{#each active as candidate (candidate.experience.id)}
|
|
142
|
+
<ExperienceHost experience={candidate.experience} {client} />
|
|
143
|
+
{/each}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CreateClientOptions, Picked } from '@picked/client';
|
|
2
|
+
import { PickedSnapshot } from '@picked/client/server';
|
|
3
|
+
type SetPickedContextParams = {
|
|
4
|
+
client: Picked;
|
|
5
|
+
snapshot?: PickedSnapshot;
|
|
6
|
+
} | {
|
|
7
|
+
options: CreateClientOptions;
|
|
8
|
+
snapshot?: PickedSnapshot;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Provides a Picked client to descendant components through Svelte context, starting it in the browser.
|
|
12
|
+
*
|
|
13
|
+
* Call this once at the top of a component's `<script>` so `setContext` and `onMount` run during
|
|
14
|
+
* component initialisation. Pass `options` to create the client and synchronously hydrate it from an
|
|
15
|
+
* optional SSR snapshot (a `versionId` mismatch is tolerated; the client reconciles via `start()`),
|
|
16
|
+
* or pass an existing `client` (such as the app's single global client) to render experiences
|
|
17
|
+
* against it — supply `snapshot` alongside `client` to SSR-seed that shared client rather than
|
|
18
|
+
* creating a second one. In the browser the client is started from `onMount`; `start()` is
|
|
19
|
+
* idempotent, so a client that was already started is left untouched.
|
|
20
|
+
*
|
|
21
|
+
* @param {SetPickedContextParams} params - `{ client, snapshot? }` or `{ options, snapshot? }`.
|
|
22
|
+
* @returns {Picked} The client placed into context.
|
|
23
|
+
* @example
|
|
24
|
+
* setPickedContext({ options })
|
|
25
|
+
* @example
|
|
26
|
+
* setPickedContext({ client: globalClient, snapshot })
|
|
27
|
+
*/
|
|
28
|
+
export declare function setPickedContext(params: SetPickedContextParams): Picked;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CLIENT_CONTEXT_KEY as e}from"../context.js";import{onMount as t,setContext as n}from"svelte";import{createClient as r,supportsWindow as i}from"@picked/client";import{registerProjectConfig as a}from"@picked/client/server";function o(e,t){let n=e.session.store.getState().id;a(e,t.projectConfig,{isPreview:!1,sessionId:n}),t.content.forEach(t=>e.registerContent(t)),e.conditionResults.seed(t.conditionResults)}function s(e,t){let n=r(e);return t&&o(n,t),n}function c(r){let a;return`client`in r?(r.snapshot&&o(r.client,r.snapshot),a=r.client):a=s(r.options,r.snapshot),n(e,a),t(()=>{i()&&a.start()}),a}export{c as setPickedContext};
|
package/dist/hooks/action.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getPicked as e}from"./client.js";import{attach as t,createEffect as n,createEvent as r,createStore as i,sample as a}from"effector";
|
|
1
|
+
import{getPicked as e}from"./client.js";import"@picked/client";import{attach as t,createEffect as n,createEvent as r,createStore as i,sample as a}from"effector";function o(t,n){return e().actions.getEvokableAction(t,n)}function s(e,s){let c=o(e,s),l=i(void 0,{skipVoid:!1}),u=r(),d=t({source:c,mapParams:(e,t)=>t,effect:n(async e=>{if(e)return await e()})});return a({clock:u,target:d}),a({clock:d.doneData,target:l}),u(),a({clock:c,target:u}),l}export{o as getAction,s as getActionResult};
|
package/dist/hooks/predicate.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getPicked as e}from"./client.js";import{attach as t,createEffect as n,createEvent as r,createStore as i,sample as a}from"effector";
|
|
1
|
+
import{getPicked as e}from"./client.js";import"@picked/client";import{attach as t,createEffect as n,createEvent as r,createStore as i,sample as a}from"effector";function o(t){return e().predicates.getEvokablePredicate(t)}function s(e){let s=o(e),c=i(void 0,{skipVoid:!1}),l=r(),u=t({source:s,mapParams:(e,t)=>t,effect:n(async e=>{if(e)return await e()})});return a({clock:l,target:u}),a({clock:u.doneData,target:c}),l(),a({clock:s,target:l}),c}export{o as getPredicate,s as getPredicateResult};
|
package/dist/hooks/step.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{STEP_CONTEXT_KEY as e}from"../context.js";import{getPicked as
|
|
1
|
+
import{STEP_CONTEXT_KEY as e}from"../context.js";import{getExperience as t,getPicked as n}from"./client.js";import{getStepRouter as r}from"../router/context.js";import{getContext as i,setContext as a}from"svelte";import{combine as o}from"effector";import{createForm as s}from"../form/create-form.svelte";function c(t){let i=n(),{activeStep:c}=r(),l=c.map(e=>e?.key===t),u=i.steps.listStore.map(e=>e.find(e=>e.key===t)??{id:t,key:t,name:``,fieldKeys:[]}),d=s(t,u.map(e=>e.name),u.map(e=>e.fieldKeys));return a(e,{step:u,layoutContentStore:o(u,i.contents.indexStore,(e,t)=>e.layoutContentId?t[e.layoutContentId]??null:null,{skipVoid:!1}),isActive:l,isActiveStep:l,...d})}function l(){return i(e)}function u(){return(i(e)?.layoutContentStore??d()).map(e=>e?.data??null,{skipVoid:!1})}function d(){let e=t(),r=n();return o(e.treeStore,r.contents.indexStore,(e,t)=>{let n=e.steps.find(e=>e.layoutContentId);return n?.layoutContentId?t[n.layoutContentId]??null:null})}export{l as getStep,u as getStepContent,c as setStepContext};
|
package/dist/hooks/stores.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SCOPE_CONTEXT_KEY as e}from"../context.js";import{
|
|
1
|
+
import{SCOPE_CONTEXT_KEY as e}from"../context.js";import{getContext as t,setContext as n}from"svelte";import{createWatch as r,fork as i,is as a,scopeBind as o}from"effector";var s=t=>n(e,i(t)),c=()=>t(e);function l(e){let t=c();return t?a.effect(e)||a.event(e)&&typeof e==`function`?o(e,{scope:t}):{subscribe:n=>(n(t.getState(e)),r({unit:e,fn:n,scope:t}))}:e}export{c as getContextScope,s as setContextScope,l as useUnit};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Public entry for the `@picked/svelte` package.
|
|
3
|
+
*/
|
|
1
4
|
export { adaptSvelteComponent, isValidAdaptedComponent } from './component';
|
|
2
5
|
export type { ComponentWithProps } from './component';
|
|
3
6
|
export { LazyImage } from './components';
|
|
7
|
+
export { setPickedContext } from './experience/set-picked-context';
|
|
8
|
+
export { default as ExperienceSlot } from './experience/ExperienceSlot.svelte';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{adaptSvelteComponent as e,isValidAdaptedComponent as t}from"./component.js";import{LazyImage as n}from"./components/index.js";export{n as LazyImage,e as adaptSvelteComponent,t as isValidAdaptedComponent};
|
|
1
|
+
import{adaptSvelteComponent as e,isValidAdaptedComponent as t}from"./component.js";import{LazyImage as n}from"./components/index.js";import{setPickedContext as r}from"./experience/set-picked-context.js";import i from"./experience/ExperienceSlot.svelte";export{i as ExperienceSlot,n as LazyImage,e as adaptSvelteComponent,t as isValidAdaptedComponent,r as setPickedContext};
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
import ComponentsListScreen from './screens/components/ComponentsListScreen.svelte'
|
|
16
16
|
import FrameworksListScreen from './screens/frameworks/FrameworksListScreen.svelte'
|
|
17
17
|
import ProjectScreen from './screens/project/ProjectScreen.svelte'
|
|
18
|
+
import ConfigScreen from './screens/config/ConfigScreen.svelte'
|
|
19
|
+
import IdentityScreen from './screens/identity/IdentityScreen.svelte'
|
|
18
20
|
import HistoryEntriesListScreen from './screens/history/HistoryEntriesListScreen.svelte'
|
|
19
21
|
import ResponsesListScreen from './screens/responses/ResponsesListScreen.svelte'
|
|
22
|
+
import FieldsListScreen from './screens/fields/FieldsListScreen.svelte'
|
|
23
|
+
import ContentListScreen from './screens/content/ContentListScreen.svelte'
|
|
20
24
|
import FeaturesListScreen from './screens/features/FeaturesListScreen.svelte'
|
|
21
25
|
import EventsListScreen from './screens/events/EventsListScreen.svelte'
|
|
22
26
|
import PickedIcon from './PickedIcon.svelte'
|
|
@@ -35,21 +39,27 @@
|
|
|
35
39
|
props: {},
|
|
36
40
|
},
|
|
37
41
|
{
|
|
38
|
-
id: '
|
|
39
|
-
label: '
|
|
40
|
-
component:
|
|
42
|
+
id: 'components',
|
|
43
|
+
label: 'Components',
|
|
44
|
+
component: ComponentsListScreen,
|
|
41
45
|
props: {},
|
|
42
46
|
},
|
|
43
47
|
{
|
|
44
|
-
id: '
|
|
45
|
-
label: '
|
|
46
|
-
component:
|
|
48
|
+
id: 'config',
|
|
49
|
+
label: 'Config',
|
|
50
|
+
component: ConfigScreen,
|
|
47
51
|
props: {},
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
|
-
id: '
|
|
51
|
-
label: '
|
|
52
|
-
component:
|
|
54
|
+
id: 'content',
|
|
55
|
+
label: 'Content',
|
|
56
|
+
component: ContentListScreen,
|
|
57
|
+
props: {},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'events',
|
|
61
|
+
label: 'Events',
|
|
62
|
+
component: EventsListScreen,
|
|
53
63
|
props: {},
|
|
54
64
|
},
|
|
55
65
|
{
|
|
@@ -64,12 +74,6 @@
|
|
|
64
74
|
component: ExperiencesListScreen,
|
|
65
75
|
props: {},
|
|
66
76
|
},
|
|
67
|
-
{
|
|
68
|
-
id: 'predicates',
|
|
69
|
-
label: 'Predicates',
|
|
70
|
-
component: PredicatesListScreen,
|
|
71
|
-
props: {},
|
|
72
|
-
},
|
|
73
77
|
{
|
|
74
78
|
id: 'features',
|
|
75
79
|
label: 'Features',
|
|
@@ -77,9 +81,9 @@
|
|
|
77
81
|
props: {},
|
|
78
82
|
},
|
|
79
83
|
{
|
|
80
|
-
id: '
|
|
81
|
-
label: '
|
|
82
|
-
component:
|
|
84
|
+
id: 'fields',
|
|
85
|
+
label: 'Fields',
|
|
86
|
+
component: FieldsListScreen,
|
|
83
87
|
props: {},
|
|
84
88
|
},
|
|
85
89
|
{
|
|
@@ -88,6 +92,30 @@
|
|
|
88
92
|
component: FrameworksListScreen,
|
|
89
93
|
props: {},
|
|
90
94
|
},
|
|
95
|
+
{
|
|
96
|
+
id: 'history',
|
|
97
|
+
label: 'History',
|
|
98
|
+
component: HistoryEntriesListScreen,
|
|
99
|
+
props: {},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'identity',
|
|
103
|
+
label: 'Identity',
|
|
104
|
+
component: IdentityScreen,
|
|
105
|
+
props: {},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'predicates',
|
|
109
|
+
label: 'Predicates',
|
|
110
|
+
component: PredicatesListScreen,
|
|
111
|
+
props: {},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'responses',
|
|
115
|
+
label: 'Responses',
|
|
116
|
+
component: ResponsesListScreen,
|
|
117
|
+
props: {},
|
|
118
|
+
},
|
|
91
119
|
]
|
|
92
120
|
|
|
93
121
|
const isOpen = experience.isOpenStore
|
|
@@ -118,7 +146,7 @@
|
|
|
118
146
|
</div>
|
|
119
147
|
</header>
|
|
120
148
|
<section class="main">
|
|
121
|
-
<Tabs {tabs} initialTab={
|
|
149
|
+
<Tabs {tabs} initialTab={0} />
|
|
122
150
|
</section>
|
|
123
151
|
</div>
|
|
124
152
|
</div>
|
|
@@ -129,6 +157,7 @@
|
|
|
129
157
|
{/if}
|
|
130
158
|
<button
|
|
131
159
|
class="floating-button"
|
|
160
|
+
class:has-preview-tag={isPreview}
|
|
132
161
|
onclick={() => experience.open()}
|
|
133
162
|
aria-label="Open Picked Devtools"
|
|
134
163
|
title="Open Picked Devtools"
|
|
@@ -316,14 +345,22 @@
|
|
|
316
345
|
.launcher {
|
|
317
346
|
position: fixed;
|
|
318
347
|
bottom: 1rem;
|
|
319
|
-
right:
|
|
348
|
+
right: 0;
|
|
320
349
|
z-index: 999999;
|
|
321
350
|
display: flex;
|
|
322
351
|
align-items: stretch;
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
transform: translateX(calc(100% - 8px));
|
|
354
|
+
transition: transform 180ms ease-out;
|
|
355
|
+
|
|
356
|
+
&:hover,
|
|
357
|
+
&:focus-within {
|
|
358
|
+
transform: translateX(0);
|
|
359
|
+
}
|
|
323
360
|
}
|
|
324
361
|
|
|
325
362
|
.floating-button {
|
|
326
|
-
border-radius:
|
|
363
|
+
border-radius: 6px;
|
|
327
364
|
background-color: #ffffff;
|
|
328
365
|
display: grid;
|
|
329
366
|
place-items: center;
|
|
@@ -337,6 +374,10 @@
|
|
|
337
374
|
}
|
|
338
375
|
}
|
|
339
376
|
|
|
377
|
+
.floating-button.has-preview-tag {
|
|
378
|
+
border-radius: 0 6px 6px 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
340
381
|
.preview-tag {
|
|
341
382
|
display: flex;
|
|
342
383
|
align-items: center;
|
|
@@ -1,30 +1,36 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="P extends Record<string, unknown> = Record<string, never>">
|
|
2
2
|
/** @file Accessible tab container that renders dynamic Svelte components per tab. */
|
|
3
3
|
import type { Component } from 'svelte'
|
|
4
4
|
|
|
5
5
|
interface Tab {
|
|
6
6
|
id: string
|
|
7
7
|
label: string
|
|
8
|
-
component: Component<
|
|
9
|
-
props
|
|
8
|
+
component: Component<P>
|
|
9
|
+
props: P
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export let tabs: Tab[]
|
|
13
13
|
export let initialTab = 0
|
|
14
14
|
|
|
15
15
|
let activeTab = initialTab
|
|
16
|
+
// Keyed by tab index — written at render time via Svelte `bind:this`. A
|
|
17
|
+
// record rather than an array sidesteps `sonarjs/no-empty-collection`,
|
|
18
|
+
// whose flow analysis can't see the bind writes.
|
|
19
|
+
const tabButtons: Record<number, HTMLButtonElement | undefined> = {}
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
* Sets the active tab to the given index
|
|
22
|
+
* Sets the active tab to the given index and scrolls the corresponding button into the
|
|
23
|
+
* horizontal viewport so keyboard navigation never lands off-screen.
|
|
19
24
|
*
|
|
20
25
|
* @param {number} index - The zero-based tab index to activate.
|
|
21
26
|
* @example
|
|
22
27
|
* ```typescript
|
|
23
|
-
* activateTab(2) // switches to the third tab
|
|
28
|
+
* activateTab(2) // switches to the third tab and scrolls it into view
|
|
24
29
|
* ```
|
|
25
30
|
*/
|
|
26
31
|
function activateTab(index: number) {
|
|
27
32
|
activeTab = index
|
|
33
|
+
tabButtons[index]?.scrollIntoView({ inline: 'nearest', block: 'nearest', behavior: 'smooth' })
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -66,6 +72,7 @@
|
|
|
66
72
|
<div role="tablist" tabindex="0" on:keydown={handleKeydown} class="tab-list">
|
|
67
73
|
{#each tabs as tab, i (tab.id)}
|
|
68
74
|
<button
|
|
75
|
+
bind:this={tabButtons[i]}
|
|
69
76
|
role="tab"
|
|
70
77
|
id="tab-{tab.id}"
|
|
71
78
|
aria-controls="panel-{tab.id}"
|
|
@@ -90,7 +97,7 @@
|
|
|
90
97
|
hidden={activeTab !== i}
|
|
91
98
|
>
|
|
92
99
|
{#if activeTab === i}
|
|
93
|
-
<svelte:component this={tab.component} {...tab.props
|
|
100
|
+
<svelte:component this={tab.component} {...tab.props} />
|
|
94
101
|
{/if}
|
|
95
102
|
</div>
|
|
96
103
|
{/each}
|
|
@@ -110,6 +117,23 @@
|
|
|
110
117
|
border-bottom: 1px solid var(--color-gray-200);
|
|
111
118
|
flex-shrink: 0;
|
|
112
119
|
overflow-x: auto;
|
|
120
|
+
overflow-y: hidden;
|
|
121
|
+
scrollbar-width: thin;
|
|
122
|
+
scrollbar-color: var(--color-gray-300) transparent;
|
|
123
|
+
scroll-behavior: smooth;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.tab-list::-webkit-scrollbar {
|
|
127
|
+
height: 6px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.tab-list::-webkit-scrollbar-thumb {
|
|
131
|
+
background-color: var(--color-gray-300);
|
|
132
|
+
border-radius: var(--radius-full);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.tab-list::-webkit-scrollbar-track {
|
|
136
|
+
background-color: transparent;
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
.tab-button {
|