@wippy-fe/webcomponent-core 0.0.34 → 0.0.36
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/index.d.ts +452 -0
- package/dist/index.js +485 -0
- package/dist/index.js.map +1 -0
- package/package.json +22 -9
- package/src/base-element.host.test.ts +0 -310
- package/src/base-element.ts +0 -399
- package/src/css-loader.ts +0 -52
- package/src/host-bus.ts +0 -44
- package/src/host-scope.test.ts +0 -19
- package/src/host-scope.ts +0 -28
- package/src/index.ts +0 -22
- package/src/prop-parser.ts +0 -91
- package/src/reactive-adapter.test.ts +0 -290
- package/src/reactive-adapter.ts +0 -220
- package/src/types.ts +0 -130
package/src/base-element.ts
DELETED
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
import type { ReactiveAdapter } from './reactive-adapter.ts'
|
|
2
|
-
import type { WippyElementConfig } from './types.ts'
|
|
3
|
-
import { addCollection } from '@iconify/vue'
|
|
4
|
-
import { addIcons, define } from '@wippy-fe/proxy'
|
|
5
|
-
import { injectInlineCss, loadHostCss } from './css-loader.ts'
|
|
6
|
-
import { getWippyHost } from './host-scope.ts'
|
|
7
|
-
import { parseProps } from './prop-parser.ts'
|
|
8
|
-
import { ReactiveAdapterImpl } from './reactive-adapter.ts'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Abstract base class for Wippy web components.
|
|
12
|
-
*
|
|
13
|
-
* Generic over `Props` — the parsed prop object type. This types `onMount`,
|
|
14
|
-
* `onPropsChanged`, and `validateProps`. Defaults to `Record<string, unknown>`.
|
|
15
|
-
*
|
|
16
|
-
* Lifecycle order (first connect):
|
|
17
|
-
* 1. shadow DOM attached
|
|
18
|
-
* 2. `onInit(shadow)` — hook for early shadow DOM customization
|
|
19
|
-
* 3. inline CSS injected
|
|
20
|
-
* 4. host CSS loading started (async, non-blocking)
|
|
21
|
-
* 5. container div created and appended
|
|
22
|
-
* 6. icons registered
|
|
23
|
-
* 7. props parsed from attributes + custom validation
|
|
24
|
-
* 8. `onMount(shadow, container, props, errors)` — framework setup
|
|
25
|
-
* 9. internals state → ready
|
|
26
|
-
* 10. `onReady()` — post-mount hook
|
|
27
|
-
* 11. `load` event emitted
|
|
28
|
-
*
|
|
29
|
-
* On reconnect (move-style DOM op: detach → reattach), the shadow root
|
|
30
|
-
* persists across the cycle, so steps 1, 3, 4, 5 (CSS injection +
|
|
31
|
-
* container creation) are skipped to avoid duplicating `<style>` and
|
|
32
|
-
* container `<div>` siblings inside the persistent shadow. The stored
|
|
33
|
-
* container is reused and `onMount` is re-invoked with `isRemount = true`
|
|
34
|
-
* so framework adapters can decide between fresh-mount and remount paths.
|
|
35
|
-
*
|
|
36
|
-
* On error: `onError(error)` → internals state → error → `error` event
|
|
37
|
-
* On disconnect: `onUnmount()` → `unload` event → internals cleared.
|
|
38
|
-
* Note: `disconnectedCallback` does NOT clear the shadow root or the
|
|
39
|
-
* stored container — they're reused on reconnect.
|
|
40
|
-
*/
|
|
41
|
-
export abstract class WippyElement<
|
|
42
|
-
Props = Record<string, unknown>,
|
|
43
|
-
Events = Record<string, unknown>,
|
|
44
|
-
> extends HTMLElement {
|
|
45
|
-
private _internals!: ElementInternals
|
|
46
|
-
private _contentObserver: MutationObserver | null = null
|
|
47
|
-
/** True after the first successful connectedCallback completes. */
|
|
48
|
-
private _initialized: boolean = false
|
|
49
|
-
/** Container div created on first connect; reused on subsequent connects. */
|
|
50
|
-
private _container: HTMLElement | null = null
|
|
51
|
-
/**
|
|
52
|
-
* Opt-in reactive adapter. Lazy-allocated on first `this.reactive`
|
|
53
|
-
* access, disposed on `disconnectedCallback`, re-allocated on next
|
|
54
|
-
* access after reconnect. Subclasses that never touch `this.reactive`
|
|
55
|
-
* pay zero allocation cost — `notifyProps`/`notifyContent` short-circuit
|
|
56
|
-
* on the null guard.
|
|
57
|
-
*/
|
|
58
|
-
private _reactive: ReactiveAdapterImpl<Props, Events> | null = null
|
|
59
|
-
/**
|
|
60
|
-
* Cached results of the most recent `parseProps` + `validateProps`
|
|
61
|
-
* pass. Set by `connectedCallback` and `attributeChangedCallback`.
|
|
62
|
-
* Read by `get reactive()` to avoid re-parsing on lazy allocation —
|
|
63
|
-
* means `validateProps` only ever runs once per attribute change,
|
|
64
|
-
* even for consumers that touch `this.reactive` inside `onMount`.
|
|
65
|
-
*/
|
|
66
|
-
private _lastProps: Props | null = null
|
|
67
|
-
private _lastErrors: readonly string[] = []
|
|
68
|
-
private _lastContent: string | null = null
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Phantom field to retain the `Events` type parameter so it shows up in
|
|
72
|
-
* type errors / `Reflect.get` shape. Adapter typing reads it indirectly
|
|
73
|
-
* via the generic.
|
|
74
|
-
* @internal
|
|
75
|
-
*/
|
|
76
|
-
declare readonly _events: Events
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Override to provide the component's configuration.
|
|
80
|
-
* Must be static because `observedAttributes` reads it before construction.
|
|
81
|
-
*
|
|
82
|
-
* Specify the generic to get typed `validateProps`:
|
|
83
|
-
* ```ts
|
|
84
|
-
* static get wippyConfig(): WippyElementConfig<MyProps> { ... }
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
static get wippyConfig(): WippyElementConfig {
|
|
88
|
-
return { propsSchema: { properties: {} } }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Derived from the props schema + any `extraObservedAttributes`.
|
|
93
|
-
*/
|
|
94
|
-
static get observedAttributes(): string[] {
|
|
95
|
-
const config = this.wippyConfig
|
|
96
|
-
const schemaAttrs = Object.keys(config.propsSchema.properties)
|
|
97
|
-
const extra = config.extraObservedAttributes ?? []
|
|
98
|
-
return [...schemaAttrs, ...extra]
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
constructor() {
|
|
102
|
-
super()
|
|
103
|
-
this._internals = this.attachInternals()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Panel-scoped `host` wrapper attached by the managed-layout shell's
|
|
108
|
-
* content resolvers (`ComponentResolver` / `WebComponentPackageLoader`).
|
|
109
|
-
*
|
|
110
|
-
* Inside a managed-layout panel, this is a wrapper around the universal
|
|
111
|
-
* `host` API where context-aware calls (`layout.broadcast / send / on`)
|
|
112
|
-
* are routed through the panel-bound bus — so `sourcePanelId` is
|
|
113
|
-
* attributed correctly without postMessage indirection. Layout
|
|
114
|
-
* mutations and other host methods pass through unchanged.
|
|
115
|
-
*
|
|
116
|
-
* `null` outside a managed-layout context (compat shell, chat sidebar,
|
|
117
|
-
* standalone playground). Subclass code that needs a host in those
|
|
118
|
-
* cases can fall back to `import { host } from '@wippy-fe/proxy'`.
|
|
119
|
-
*/
|
|
120
|
-
get host(): ReturnType<typeof getWippyHost> {
|
|
121
|
-
return getWippyHost(this)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Emit a CustomEvent that bubbles and crosses shadow DOM boundaries.
|
|
126
|
-
*/
|
|
127
|
-
protected emitEvent(eventName: string, detail?: unknown): void {
|
|
128
|
-
this.dispatchEvent(new CustomEvent(eventName, {
|
|
129
|
-
bubbles: true,
|
|
130
|
-
composed: true,
|
|
131
|
-
detail,
|
|
132
|
-
}))
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Opt-in reactive adapter — framework-agnostic. Subscribe to prop
|
|
137
|
-
* changes, content changes, or emit typed events from a non-Vue
|
|
138
|
-
* consumer without re-rolling reactivity.
|
|
139
|
-
*
|
|
140
|
-
* ```ts
|
|
141
|
-
* class MyEl extends WippyElement<{ count: number }, { tick: { n: number } }> {
|
|
142
|
-
* protected onMount() {
|
|
143
|
-
* const ctrl = new AbortController()
|
|
144
|
-
* this.reactive.props.subscribe(({ count }) => {
|
|
145
|
-
* this.shadowRoot!.querySelector('.n')!.textContent = String(count)
|
|
146
|
-
* }, { signal: ctrl.signal, immediate: true })
|
|
147
|
-
* }
|
|
148
|
-
* tick(n: number) { this.reactive.events.emit('tick', { n }) }
|
|
149
|
-
* }
|
|
150
|
-
* ```
|
|
151
|
-
*
|
|
152
|
-
* Allocation cost is zero unless this getter is touched. Disposed on
|
|
153
|
-
* `disconnectedCallback`; a fresh adapter is allocated on the next
|
|
154
|
-
* access after reconnect.
|
|
155
|
-
*/
|
|
156
|
-
protected get reactive(): ReactiveAdapter<Props, Events> {
|
|
157
|
-
if (!this._reactive) {
|
|
158
|
-
const config = (this.constructor as typeof WippyElement).wippyConfig
|
|
159
|
-
const hasContent = !!config.contentTemplate
|
|
160
|
-
// Reuse the cached parse result from the most recent
|
|
161
|
-
// `connectedCallback` / `attributeChangedCallback` whenever we
|
|
162
|
-
// have one; otherwise fall back to a fresh parse (this happens
|
|
163
|
-
// when `reactive` is touched before the element is ever
|
|
164
|
-
// connected — rare but legal).
|
|
165
|
-
let props: Props
|
|
166
|
-
let errors: readonly string[]
|
|
167
|
-
if (this._lastProps !== null) {
|
|
168
|
-
props = this._lastProps
|
|
169
|
-
errors = this._lastErrors
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
const parsed = parseProps(this, config.propsSchema)
|
|
173
|
-
if (config.validateProps) {
|
|
174
|
-
parsed.errors.push(...config.validateProps(parsed.props))
|
|
175
|
-
}
|
|
176
|
-
props = parsed.props as Props
|
|
177
|
-
errors = parsed.errors
|
|
178
|
-
this._lastProps = props
|
|
179
|
-
this._lastErrors = errors
|
|
180
|
-
}
|
|
181
|
-
const content = hasContent
|
|
182
|
-
? (this._lastContent ?? this._extractContent(config.contentTemplate!))
|
|
183
|
-
: null
|
|
184
|
-
if (hasContent && this._lastContent === null)
|
|
185
|
-
this._lastContent = content
|
|
186
|
-
this._reactive = new ReactiveAdapterImpl<Props, Events>(
|
|
187
|
-
{ props, errors, content, hasContent },
|
|
188
|
-
this.emitEvent.bind(this),
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
return this._reactive
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Lifecycle ──────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
connectedCallback(): void {
|
|
197
|
-
this._internals.states.add('loading')
|
|
198
|
-
try {
|
|
199
|
-
const config = (this.constructor as typeof WippyElement).wippyConfig
|
|
200
|
-
const isRemount = this._initialized
|
|
201
|
-
|
|
202
|
-
// 1. Shadow DOM (guard against reconnect — attachShadow throws if called twice)
|
|
203
|
-
const shadow = this.shadowRoot ?? this.attachShadow({ mode: config.shadowMode ?? 'open' })
|
|
204
|
-
|
|
205
|
-
let container: HTMLElement
|
|
206
|
-
if (!isRemount) {
|
|
207
|
-
// 2. Early hook — customize shadow before CSS/container
|
|
208
|
-
this.onInit(shadow)
|
|
209
|
-
|
|
210
|
-
// 3. Inline CSS
|
|
211
|
-
if (config.inlineCss) {
|
|
212
|
-
injectInlineCss(shadow, config.inlineCss)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 4. Host CSS (async, non-blocking)
|
|
216
|
-
if (config.hostCssKeys === undefined || config.hostCssKeys.length > 0) {
|
|
217
|
-
loadHostCss(shadow, config.hostCssKeys)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 5. Container
|
|
221
|
-
container = document.createElement('div')
|
|
222
|
-
const classes = config.containerClasses ?? []
|
|
223
|
-
if (classes.length > 0) {
|
|
224
|
-
container.classList.add(...classes)
|
|
225
|
-
}
|
|
226
|
-
shadow.appendChild(container)
|
|
227
|
-
this._container = container
|
|
228
|
-
|
|
229
|
-
// 6. Icons
|
|
230
|
-
addIcons(addCollection)
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
// Reconnect path — reuse the existing shadow children + container.
|
|
234
|
-
// Skip CSS/icon/container setup so we don't accumulate duplicate
|
|
235
|
-
// `<style>` and container `<div>` siblings on every detach/reattach.
|
|
236
|
-
// The framework adapter decides whether to remount on the existing
|
|
237
|
-
// container or recreate its app instance.
|
|
238
|
-
container = this._container!
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// 7. Parse initial props + custom validation
|
|
242
|
-
const { props, errors } = parseProps(this, config.propsSchema)
|
|
243
|
-
if (config.validateProps) {
|
|
244
|
-
errors.push(...config.validateProps(props))
|
|
245
|
-
}
|
|
246
|
-
const typedProps = props as Props
|
|
247
|
-
// Cache so a lazy `get reactive()` call from inside `onMount`
|
|
248
|
-
// doesn't re-parse / re-validate.
|
|
249
|
-
this._lastProps = typedProps
|
|
250
|
-
this._lastErrors = errors
|
|
251
|
-
|
|
252
|
-
// 7b. Extract children content (if contentTemplate configured)
|
|
253
|
-
let initialContent: string | null = null
|
|
254
|
-
if (config.contentTemplate) {
|
|
255
|
-
initialContent = this._extractContent(config.contentTemplate)
|
|
256
|
-
this._lastContent = initialContent
|
|
257
|
-
this._contentObserver = new MutationObserver(() => {
|
|
258
|
-
const content = this._extractContent(config.contentTemplate!)
|
|
259
|
-
this._lastContent = content
|
|
260
|
-
// Fan out to the reactive adapter first so subclass overrides of
|
|
261
|
-
// `onContentChanged` that read `this.reactive.content.value` see
|
|
262
|
-
// the new value. No-op when the adapter hasn't been allocated.
|
|
263
|
-
this._reactive?.notifyContent(content)
|
|
264
|
-
this.onContentChanged(content)
|
|
265
|
-
})
|
|
266
|
-
this._contentObserver.observe(this, {
|
|
267
|
-
childList: true,
|
|
268
|
-
characterData: true,
|
|
269
|
-
subtree: true,
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 8. Framework mount (first time) or remount (reconnect)
|
|
274
|
-
this.onMount(shadow, container, typedProps, errors, initialContent, isRemount)
|
|
275
|
-
|
|
276
|
-
// 9. Ready
|
|
277
|
-
this._internals.states.delete('loading')
|
|
278
|
-
this._internals.states.add('ready')
|
|
279
|
-
|
|
280
|
-
// First-connect bookkeeping after a clean mount.
|
|
281
|
-
if (!isRemount) {
|
|
282
|
-
this._initialized = true
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// 10. Post-mount hook
|
|
286
|
-
this.onReady()
|
|
287
|
-
|
|
288
|
-
// 11. Emit load
|
|
289
|
-
this.emitEvent('load')
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
this.onError(error)
|
|
293
|
-
this._internals.states.delete('loading')
|
|
294
|
-
this._internals.states.add('error')
|
|
295
|
-
this.emitEvent('error', {
|
|
296
|
-
message: error instanceof Error ? error.message : String(error),
|
|
297
|
-
error,
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
disconnectedCallback(): void {
|
|
303
|
-
if (this._contentObserver) {
|
|
304
|
-
this._contentObserver.disconnect()
|
|
305
|
-
this._contentObserver = null
|
|
306
|
-
}
|
|
307
|
-
this.onUnmount()
|
|
308
|
-
this.emitEvent('unload')
|
|
309
|
-
this._internals.states.clear()
|
|
310
|
-
// Drop the reactive adapter so subscribers from this attachment are
|
|
311
|
-
// cleared and a fresh adapter is allocated on the next `this.reactive`
|
|
312
|
-
// access (i.e. after reconnect).
|
|
313
|
-
this._reactive?.dispose()
|
|
314
|
-
this._reactive = null
|
|
315
|
-
// Clear cached parse state — `connectedCallback` re-populates on
|
|
316
|
-
// reconnect; before then, a lazy `get reactive()` access should
|
|
317
|
-
// re-parse from current attributes (rare path).
|
|
318
|
-
this._lastProps = null
|
|
319
|
-
this._lastErrors = []
|
|
320
|
-
this._lastContent = null
|
|
321
|
-
// Drop panel-context wires so a detached element held by a
|
|
322
|
-
// consumer (or re-inserted into a different panel) doesn't carry
|
|
323
|
-
// stale wires that would silently misattribute. The resolver
|
|
324
|
-
// re-installs them on the next mount.
|
|
325
|
-
delete (this as unknown as { __wippyHost?: unknown }).__wippyHost
|
|
326
|
-
delete (this as unknown as { __wippyHostBus?: unknown }).__wippyHostBus
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
attributeChangedCallback(_name: string, oldVal: string | null, newVal: string | null): void {
|
|
330
|
-
if (oldVal === newVal)
|
|
331
|
-
return
|
|
332
|
-
const config = (this.constructor as typeof WippyElement).wippyConfig
|
|
333
|
-
const { props, errors } = parseProps(this, config.propsSchema)
|
|
334
|
-
if (config.validateProps) {
|
|
335
|
-
errors.push(...config.validateProps(props))
|
|
336
|
-
}
|
|
337
|
-
const typedProps = props as Props
|
|
338
|
-
this._lastProps = typedProps
|
|
339
|
-
this._lastErrors = errors
|
|
340
|
-
// Fan out to the reactive adapter BEFORE the legacy hook so subclasses
|
|
341
|
-
// that override `onPropsChanged` AND read `this.reactive.props.value`
|
|
342
|
-
// inside it see consistent state. No-op when the adapter hasn't been
|
|
343
|
-
// lazy-allocated.
|
|
344
|
-
this._reactive?.notifyProps(typedProps, errors)
|
|
345
|
-
this.onPropsChanged(typedProps, errors)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// ── Hooks ──────────────────────────────────────────────────
|
|
349
|
-
|
|
350
|
-
/** Called right after shadow DOM is attached, before CSS or container. */
|
|
351
|
-
protected onInit(_shadow: ShadowRoot): void {}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Called after shadow DOM, CSS, and container are ready. Mount your
|
|
355
|
-
* framework here.
|
|
356
|
-
*
|
|
357
|
-
* Invoked on first connect AND on every subsequent reconnect (move-style
|
|
358
|
-
* DOM op: detach → reattach). On reconnect the same `container` instance
|
|
359
|
-
* is reused. Adapters that hold a framework app handle should either
|
|
360
|
-
* detect `isRemount === true` and re-mount on the existing container, or
|
|
361
|
-
* leave the previous app intact across the detach/reattach cycle.
|
|
362
|
-
*/
|
|
363
|
-
protected abstract onMount(
|
|
364
|
-
shadow: ShadowRoot,
|
|
365
|
-
container: HTMLElement,
|
|
366
|
-
initialProps: Props,
|
|
367
|
-
initialErrors: string[],
|
|
368
|
-
initialContent?: string | null,
|
|
369
|
-
isRemount?: boolean,
|
|
370
|
-
): void
|
|
371
|
-
|
|
372
|
-
/** Called after internals state is set to ready, before the `load` event. */
|
|
373
|
-
protected onReady(): void {}
|
|
374
|
-
|
|
375
|
-
/** Called when connectedCallback throws. Default logs to console. */
|
|
376
|
-
protected onError(error: unknown): void {
|
|
377
|
-
console.error(`${this.constructor.name} initialization failed:`, error)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Called during disconnectedCallback — clean up framework resources. */
|
|
381
|
-
protected abstract onUnmount(): void
|
|
382
|
-
|
|
383
|
-
/** Called when observed attributes change. Override to update framework state. */
|
|
384
|
-
protected onPropsChanged(_props: Props, _errors: string[]): void {}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Extract text from a child `<template data-type="...">` element.
|
|
388
|
-
* Uses `.content.textContent` since `<template>` stores content in a DocumentFragment.
|
|
389
|
-
*/
|
|
390
|
-
private _extractContent(dataType: string): string | null {
|
|
391
|
-
const tpl = this.querySelector(`template[data-type="${dataType}"]`) as HTMLTemplateElement | null
|
|
392
|
-
return tpl?.content.textContent?.trim() ?? null
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** Called when child `<template>` content changes. Override to update framework state. */
|
|
396
|
-
protected onContentChanged(_content: string | null): void {}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
export { define }
|
package/src/css-loader.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { HostCssKey } from './types.ts'
|
|
2
|
-
import { hostCss, loadCss } from '@wippy-fe/proxy'
|
|
3
|
-
|
|
4
|
-
const DEFAULT_HOST_CSS_KEYS: HostCssKey[] = [
|
|
5
|
-
'themeConfigUrl',
|
|
6
|
-
'primeVueCssUrl',
|
|
7
|
-
'markdownCssUrl',
|
|
8
|
-
'iframeCssUrl',
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Loads host CSS URLs into the shadow root as `<style>` elements.
|
|
13
|
-
* Non-blocking — returns a promise but the component doesn't need to await it.
|
|
14
|
-
*/
|
|
15
|
-
export function loadHostCss(shadow: ShadowRoot, keys?: HostCssKey[]): Promise<void> {
|
|
16
|
-
const cssKeys = keys ?? DEFAULT_HOST_CSS_KEYS
|
|
17
|
-
// Filter out keys whose URL is missing (e.g. removed CSS like fontCssUrl).
|
|
18
|
-
// Load each independently so one failure doesn't block the rest.
|
|
19
|
-
const jobs = cssKeys.map(async (key) => {
|
|
20
|
-
const url = hostCss[key]
|
|
21
|
-
if (!url) {
|
|
22
|
-
console.warn(`[wippy-fe/webcomponent-core] hostCss key "${key}" is undefined — skipping. Remove it from hostCssKeys if the CSS was removed.`)
|
|
23
|
-
return null
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
return await loadCss(url)
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
console.warn(`[wippy-fe/webcomponent-core] Failed to load hostCss "${key}" (${url}):`, err)
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
return Promise.all(jobs).then((cssChunks) => {
|
|
34
|
-
for (const css of cssChunks) {
|
|
35
|
-
if (!css)
|
|
36
|
-
continue
|
|
37
|
-
const style = document.createElement('style')
|
|
38
|
-
style.textContent = css
|
|
39
|
-
style.setAttribute('role', '@wippy-fe/host-css')
|
|
40
|
-
shadow.appendChild(style)
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Injects inline CSS text into the shadow root.
|
|
47
|
-
*/
|
|
48
|
-
export function injectInlineCss(shadow: ShadowRoot, text: string): void {
|
|
49
|
-
const style = document.createElement('style')
|
|
50
|
-
style.textContent = text
|
|
51
|
-
shadow.appendChild(style)
|
|
52
|
-
}
|
package/src/host-bus.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layout bus types shared between `@wippy-fe/webcomponent-core` and
|
|
3
|
-
* `@wippy-fe/webcomponent-vue`. Mirror of the host-side
|
|
4
|
-
* `LayoutBusBound` / `BroadcastEnvelope` shapes; structurally
|
|
5
|
-
* compatible so a host-mounted web component can read its
|
|
6
|
-
* `__wippyHostBus` property without importing host-private types.
|
|
7
|
-
*
|
|
8
|
-
* The host (gen-2-chat managed-layout shell) sets:
|
|
9
|
-
* - `el.dataset.wippyPanelId` (string)
|
|
10
|
-
* - `(el as { __wippyHostBus?: LayoutBusBound }).__wippyHostBus`
|
|
11
|
-
* BEFORE the element is appended to the DOM. Web components see them
|
|
12
|
-
* on first `connectedCallback` tick.
|
|
13
|
-
*
|
|
14
|
-
* Outside a managed-layout host, both are absent — read code MUST
|
|
15
|
-
* tolerate `undefined`.
|
|
16
|
-
*
|
|
17
|
-
* The interface bodies live in `@wippy-fe/shared` so the host can
|
|
18
|
-
* import them without dragging in this package's `@wippy-fe/proxy`
|
|
19
|
-
* runtime dependency. Consumers continue to import from
|
|
20
|
-
* `@wippy-fe/webcomponent-core` (this re-export) for backward compat.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { LayoutBusBound } from '@wippy-fe/shared'
|
|
24
|
-
|
|
25
|
-
export type { BroadcastEnvelope, LayoutBusBound } from '@wippy-fe/shared'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Read the host bus attached to an element by `ComponentResolver` in
|
|
29
|
-
* the managed-layout host. Returns `null` outside a managed-layout
|
|
30
|
-
* host (or before the element is connected). Stable across element
|
|
31
|
-
* remounts of the same panel — the resolver re-attaches a fresh bus
|
|
32
|
-
* each time.
|
|
33
|
-
*/
|
|
34
|
-
export function getWippyHostBus(el: HTMLElement): LayoutBusBound | null {
|
|
35
|
-
return (el as { __wippyHostBus?: LayoutBusBound }).__wippyHostBus ?? null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Read the panelId attribute set by `ComponentResolver`. `null`
|
|
40
|
-
* outside a managed-layout host.
|
|
41
|
-
*/
|
|
42
|
-
export function getWippyPanelId(el: HTMLElement): string | null {
|
|
43
|
-
return el.dataset.wippyPanelId ?? null
|
|
44
|
-
}
|
package/src/host-scope.test.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import { getWippyHost } from './host-scope.ts'
|
|
6
|
-
|
|
7
|
-
describe('getWippyHost', () => {
|
|
8
|
-
it('returns the wrapper attached as __wippyHost', () => {
|
|
9
|
-
const el = document.createElement('div')
|
|
10
|
-
const fake = { layout: { broadcast: () => {} } }
|
|
11
|
-
;(el as unknown as { __wippyHost: unknown }).__wippyHost = fake
|
|
12
|
-
expect(getWippyHost(el)).toBe(fake)
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('returns null when no wrapper attached', () => {
|
|
16
|
-
const el = document.createElement('div')
|
|
17
|
-
expect(getWippyHost(el)).toBeNull()
|
|
18
|
-
})
|
|
19
|
-
})
|
package/src/host-scope.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Element-attached host wrapper reader for web components mounted as
|
|
3
|
-
* `kind: 'component'` panels in the gen-2-chat managed-layout shell.
|
|
4
|
-
*
|
|
5
|
-
* The host stamps a panel-scoped wrapper as `__wippyHost` on each
|
|
6
|
-
* element BEFORE it is appended to the DOM (see the host's
|
|
7
|
-
* `panel-context-wires.ts` and `host-scope.ts`). Consumer web
|
|
8
|
-
* components read it back via `getWippyHost(this)` in
|
|
9
|
-
* `connectedCallback` and onwards.
|
|
10
|
-
*
|
|
11
|
-
* The wrapper is **constructed by the host** — this package only
|
|
12
|
-
* exposes the reader. Outside a managed-layout host (raw mounts,
|
|
13
|
-
* compat shells, tests without the connector) the property is absent
|
|
14
|
-
* and `getWippyHost` returns `null`.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Read the panel-scoped host wrapper attached to an element by the
|
|
19
|
-
* managed-layout shell's content resolvers. Returns `null` outside a
|
|
20
|
-
* managed-layout host (or before the element is connected).
|
|
21
|
-
*
|
|
22
|
-
* Subclass authors of `WippyElement` / `WippyVueElement` access this
|
|
23
|
-
* via `this.host` / `useHost()` and rarely need to call this directly.
|
|
24
|
-
* The helper is exposed for raw `HTMLElement` consumers.
|
|
25
|
-
*/
|
|
26
|
-
export function getWippyHost<H = unknown>(el: HTMLElement): H | null {
|
|
27
|
-
return ((el as unknown) as { __wippyHost?: H }).__wippyHost ?? null
|
|
28
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export { define, WippyElement } from './base-element.ts'
|
|
2
|
-
export { injectInlineCss, loadHostCss } from './css-loader.ts'
|
|
3
|
-
export { getWippyHostBus, getWippyPanelId } from './host-bus.ts'
|
|
4
|
-
export type { BroadcastEnvelope, LayoutBusBound } from './host-bus.ts'
|
|
5
|
-
export { getWippyHost } from './host-scope.ts'
|
|
6
|
-
export { attrToCamel, parseProps } from './prop-parser.ts'
|
|
7
|
-
export type { ParseResult } from './prop-parser.ts'
|
|
8
|
-
export type {
|
|
9
|
-
ContentListener,
|
|
10
|
-
ContentSubscription,
|
|
11
|
-
EventEmitterHandle,
|
|
12
|
-
PropsListener,
|
|
13
|
-
PropsSubscription,
|
|
14
|
-
ReactiveAdapter,
|
|
15
|
-
SubscribeOptions,
|
|
16
|
-
} from './reactive-adapter.ts'
|
|
17
|
-
export type {
|
|
18
|
-
HostCssKey,
|
|
19
|
-
WippyElementConfig,
|
|
20
|
-
WippyPropDefinition,
|
|
21
|
-
WippyPropsSchema,
|
|
22
|
-
} from './types.ts'
|
package/src/prop-parser.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { WippyPropDefinition, WippyPropsSchema } from './types.ts'
|
|
2
|
-
|
|
3
|
-
export interface ParseResult {
|
|
4
|
-
props: Record<string, unknown>
|
|
5
|
-
errors: string[]
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Converts a kebab-case attribute name to camelCase.
|
|
10
|
-
* `allowed-types` → `allowedTypes`
|
|
11
|
-
*/
|
|
12
|
-
export function attrToCamel(attr: string): string {
|
|
13
|
-
return attr.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase())
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Parses a single attribute value according to its schema definition.
|
|
18
|
-
*/
|
|
19
|
-
function parseValue(attr: string, raw: string, def: WippyPropDefinition): { value: unknown, error?: string } {
|
|
20
|
-
switch (def.type) {
|
|
21
|
-
case 'string':
|
|
22
|
-
return { value: raw }
|
|
23
|
-
|
|
24
|
-
case 'number': {
|
|
25
|
-
const n = Number.parseFloat(raw)
|
|
26
|
-
if (Number.isNaN(n))
|
|
27
|
-
return { value: undefined, error: `Invalid ${attr}: expected a number` }
|
|
28
|
-
return { value: n }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
case 'integer': {
|
|
32
|
-
const n = Number.parseInt(raw, 10)
|
|
33
|
-
if (Number.isNaN(n))
|
|
34
|
-
return { value: undefined, error: `Invalid ${attr}: expected an integer` }
|
|
35
|
-
return { value: n }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
case 'boolean':
|
|
39
|
-
// Presence of the attribute means true (HTML convention), explicit "false" means false
|
|
40
|
-
return { value: raw !== 'false' }
|
|
41
|
-
|
|
42
|
-
case 'array':
|
|
43
|
-
case 'object': {
|
|
44
|
-
try {
|
|
45
|
-
const parsed = JSON.parse(raw)
|
|
46
|
-
if (def.type === 'array' && !Array.isArray(parsed)) {
|
|
47
|
-
return { value: undefined, error: `Invalid ${attr}: expected a JSON array` }
|
|
48
|
-
}
|
|
49
|
-
return { value: parsed }
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return { value: undefined, error: `Invalid ${attr}: must be valid JSON` }
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
default:
|
|
57
|
-
return { value: raw }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Parses all attributes on an element according to its props schema.
|
|
63
|
-
* Returns `{ props, errors }` where errors is an array of human-readable messages.
|
|
64
|
-
*/
|
|
65
|
-
export function parseProps(element: HTMLElement, schema: WippyPropsSchema): ParseResult {
|
|
66
|
-
const props: Record<string, unknown> = {}
|
|
67
|
-
const errors: string[] = []
|
|
68
|
-
|
|
69
|
-
for (const [attr, def] of Object.entries(schema.properties)) {
|
|
70
|
-
const raw = element.getAttribute(attr)
|
|
71
|
-
const camel = attrToCamel(attr)
|
|
72
|
-
|
|
73
|
-
if (raw === null) {
|
|
74
|
-
// Attribute not set — use default if available
|
|
75
|
-
if (def.default !== undefined) {
|
|
76
|
-
props[camel] = def.default
|
|
77
|
-
}
|
|
78
|
-
continue
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const result = parseValue(attr, raw, def)
|
|
82
|
-
if (result.error) {
|
|
83
|
-
errors.push(result.error)
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
props[camel] = result.value
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { props, errors }
|
|
91
|
-
}
|