@wippy-fe/webcomponent-core 0.0.34 → 0.0.35

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.
@@ -0,0 +1,452 @@
1
+ import { LayoutBusBound } from '@wippy-fe/shared';
2
+ export { BroadcastEnvelope, LayoutBusBound } from '@wippy-fe/shared';
3
+ export { define } from '@wippy-fe/proxy';
4
+
5
+ /**
6
+ * Opt-in reactive adapter for `WippyElement`.
7
+ *
8
+ * Framework-agnostic primitive that lets non-Vue consumers subscribe to
9
+ * prop / content updates and emit typed events on a web component without
10
+ * pulling Vue (or any other reactivity library) into the core bundle.
11
+ *
12
+ * Access via `this.reactive` on a `WippyElement` subclass — the getter is
13
+ * lazy, so subclasses that never touch it pay zero allocation cost. See
14
+ * `base-element.ts:get reactive` for the bridge into the lifecycle.
15
+ *
16
+ * The `notifyProps` / `notifyContent` methods are package-visible: the
17
+ * base element calls them from `attributeChangedCallback` and the content
18
+ * MutationObserver callback respectively. They are NOT part of the public
19
+ * API consumers should reach for.
20
+ *
21
+ * Cleanup: `dispose()` runs from `disconnectedCallback`. All listener
22
+ * sets are cleared; subsequent notifyProps/notifyContent are no-ops; the
23
+ * `events.emit` becomes a no-op too. A fresh adapter is allocated on the
24
+ * next `this.reactive` access (i.e. after reconnect).
25
+ */
26
+ type PropsListener<Props> = (props: Props, errors: readonly string[]) => void;
27
+ type ContentListener = (content: string | null) => void;
28
+ interface SubscribeOptions {
29
+ /** Cancel the subscription when the signal aborts. */
30
+ signal?: AbortSignal;
31
+ /** Fire the listener once synchronously with the current value. */
32
+ immediate?: boolean;
33
+ }
34
+ interface PropsSubscription<Props> {
35
+ /** Current parsed props. Always reflects the latest state. */
36
+ readonly value: Props;
37
+ /** Current validation/parse errors. */
38
+ readonly errors: readonly string[];
39
+ /**
40
+ * Register a listener that fires whenever props change. Returns an
41
+ * unsubscribe function. Pass `options.signal` for AbortSignal-driven
42
+ * cleanup (e.g. tied to an element's lifetime). Pass
43
+ * `options.immediate: true` to fire once synchronously with the current
44
+ * value — useful when the subscriber is set up mid-flight.
45
+ *
46
+ * Note: `notifyProps` runs BEFORE the subclass `onPropsChanged()` hook.
47
+ * A subscriber added from inside `onPropsChanged()` therefore does NOT
48
+ * receive the currently-firing change — only the next one. Use
49
+ * `{ immediate: true }` to also see the current value.
50
+ */
51
+ subscribe: (listener: PropsListener<Props>, options?: SubscribeOptions) => () => void;
52
+ }
53
+ interface ContentSubscription {
54
+ /** Current `<template>` content text, or `null` if not set. */
55
+ readonly value: string | null;
56
+ /** See `PropsSubscription.subscribe` for the same ordering caveat. */
57
+ subscribe: (listener: ContentListener, options?: SubscribeOptions) => () => void;
58
+ }
59
+ interface EventEmitterHandle<Events> {
60
+ /**
61
+ * Dispatch a typed `CustomEvent` (bubbles + composed, identical
62
+ * semantics to the protected `emitEvent` helper). The key is constrained
63
+ * to `keyof Events & string`, so consumers narrow `Events` to get
64
+ * autocomplete + payload-type checks.
65
+ *
66
+ * No-op after the adapter is disposed (i.e. after the element is
67
+ * detached). The element is the only event target — subscribers on
68
+ * the element itself receive the event.
69
+ */
70
+ emit: <K extends keyof Events & string>(event: K, detail: Events[K]) => void;
71
+ }
72
+ interface ReactiveAdapter<Props, Events> {
73
+ props: PropsSubscription<Props>;
74
+ events: EventEmitterHandle<Events>;
75
+ /**
76
+ * Present only when the element's `wippyConfig.contentTemplate` is set;
77
+ * `null` otherwise. Avoids fake `.value === null` semantics for elements
78
+ * that don't track children content.
79
+ */
80
+ content: ContentSubscription | null;
81
+ }
82
+
83
+ /**
84
+ * JSON-Schema-style property descriptor used for attribute→prop parsing.
85
+ */
86
+ interface WippyPropDefinition {
87
+ type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
88
+ default?: unknown;
89
+ description?: string;
90
+ items?: {
91
+ type: string;
92
+ };
93
+ }
94
+ /**
95
+ * JSON-Schema-style props block — matches the `wippy.props` field in package.json.
96
+ *
97
+ * Example:
98
+ * ```json
99
+ * {
100
+ * "type": "object",
101
+ * "properties": {
102
+ * "allowed-types": { "type": "array", "items": { "type": "string" } },
103
+ * "max-file-size": { "type": "number", "default": 104857600 }
104
+ * }
105
+ * }
106
+ * ```
107
+ */
108
+ interface WippyPropsSchema {
109
+ type?: string;
110
+ properties: Record<string, WippyPropDefinition>;
111
+ }
112
+ /**
113
+ * Keys that map to CSS URLs exposed by `@wippy-fe/proxy`'s `hostCss` object.
114
+ * Pass a subset to `hostCssKeys` in config to load only what you need.
115
+ */
116
+ type HostCssKey = 'themeConfigUrl' | 'primeVueCssUrl' | 'markdownCssUrl' | 'iframeCssUrl';
117
+ /**
118
+ * Configuration object returned by `static get wippyConfig()`.
119
+ *
120
+ * Generic over `Props` so that `validateProps` receives the typed prop object.
121
+ * Use `WippyElementConfig<MyProps>` as the return type of your static getter
122
+ * to get typed validation:
123
+ *
124
+ * ```ts
125
+ * static get wippyConfig(): WippyElementConfig<MyProps> { ... }
126
+ * ```
127
+ */
128
+ interface WippyElementConfig<Props = Record<string, unknown>> {
129
+ /** JSON-schema props block (usually from package.json `wippy.props`). */
130
+ propsSchema: WippyPropsSchema;
131
+ /** Shadow DOM mode. Defaults to `'open'`. */
132
+ shadowMode?: 'open' | 'closed';
133
+ /**
134
+ * Host CSS to inherit from the Wippy platform into this component's shadow DOM.
135
+ *
136
+ * Shadow DOM blocks style inheritance, so platform styles (theme variables, fonts,
137
+ * UI framework classes, etc.) must be explicitly loaded into each component's shadow root.
138
+ * The host app provides these CSS assets at runtime via `@wippy-fe/proxy`.
139
+ *
140
+ * Available keys:
141
+ * - `themeConfigUrl` — CSS custom properties (color scales, spacing, etc.) matching the
142
+ * host theme. **Recommended for all components** — gives your component the same look
143
+ * as the host app. At dev time, a local copy (`theme-config.css`) provides fallback
144
+ * values; at runtime, the host injects the real theme.
145
+ * - `primeVueCssUrl` — PrimeVue component classes (p-button, p-input, etc.) in unstyled
146
+ * mode, matching the host's appearance. **Include if using any PrimeVue components**
147
+ * (buttons, forms, tables, etc.). Skip only for fully custom UI with no PrimeVue.
148
+ * - `markdownCssUrl` — Styles for rendered markdown blocks. **Include only if your
149
+ * component renders markdown content.**
150
+ * - `iframeCssUrl` — Scrollbar styling and iframe-related styles. **Recommended for all
151
+ * components** so scrollbars look identical to the host app.
152
+ *
153
+ * Pass `[]` to skip host CSS entirely (fully self-styled component).
154
+ * Defaults to `['themeConfigUrl', 'primeVueCssUrl', 'markdownCssUrl', 'iframeCssUrl']`.
155
+ */
156
+ hostCssKeys?: HostCssKey[];
157
+ /**
158
+ * Component-specific CSS text to inject into the shadow root (synchronous).
159
+ *
160
+ * Typically from a Vite `?inline` import of your component's stylesheet.
161
+ * This is your component's own styling — Tailwind utilities, custom classes, etc.
162
+ * Unlike `hostCssKeys` which loads platform CSS at runtime, this is bundled at build time.
163
+ */
164
+ inlineCss?: string;
165
+ /**
166
+ * Custom prop validation that runs after type parsing.
167
+ *
168
+ * Receives the already type-coerced props from the schema parser.
169
+ * Return an array of error messages for any invalid values, or an empty array if all valid.
170
+ * Errors are merged with any type-parsing errors and emitted as `invalid` events.
171
+ *
172
+ * Use this for domain-specific checks the JSON schema can't express:
173
+ * - Value ranges (`max-file-size` must be positive)
174
+ * - Array item types (`allowed-types` must be an array of strings)
175
+ * - Cross-prop constraints (if X is set, Y must also be set)
176
+ *
177
+ * Example:
178
+ * ```ts
179
+ * validateProps: (props) => {
180
+ * const errors: string[] = []
181
+ * if (typeof props.maxFileSize === 'number' && props.maxFileSize < 0) {
182
+ * errors.push('max-file-size must be a positive number')
183
+ * }
184
+ * return errors
185
+ * }
186
+ * ```
187
+ */
188
+ validateProps?: (props: Props) => string[];
189
+ /** CSS classes to add to the container div. Defaults to none. */
190
+ containerClasses?: string[];
191
+ /** Additional attribute names to observe beyond those in the props schema. */
192
+ extraObservedAttributes?: string[];
193
+ /**
194
+ * If set, reads text content from a child `<template data-type="...">` element.
195
+ * The value is the type to match, e.g. `'text/vnd.mermaid'`.
196
+ * Content is extracted once on mount and updated via MutationObserver.
197
+ * Props take priority over children content.
198
+ *
199
+ * Usage:
200
+ * ```html
201
+ * <example-mermaid>
202
+ * <template data-type="text/vnd.mermaid">graph TD; A --> B</template>
203
+ * </example-mermaid>
204
+ * ```
205
+ *
206
+ * Uses `<template>` instead of `<script>` because Vue templates strip script tags.
207
+ * The native `<template>` element is inert (not rendered) and works in both
208
+ * raw HTML and Vue SFC templates.
209
+ */
210
+ contentTemplate?: string;
211
+ }
212
+
213
+ /**
214
+ * Element-attached host wrapper reader for web components mounted as
215
+ * `kind: 'component'` panels in the gen-2-chat managed-layout shell.
216
+ *
217
+ * The host stamps a panel-scoped wrapper as `__wippyHost` on each
218
+ * element BEFORE it is appended to the DOM (see the host's
219
+ * `panel-context-wires.ts` and `host-scope.ts`). Consumer web
220
+ * components read it back via `getWippyHost(this)` in
221
+ * `connectedCallback` and onwards.
222
+ *
223
+ * The wrapper is **constructed by the host** — this package only
224
+ * exposes the reader. Outside a managed-layout host (raw mounts,
225
+ * compat shells, tests without the connector) the property is absent
226
+ * and `getWippyHost` returns `null`.
227
+ */
228
+ /**
229
+ * Read the panel-scoped host wrapper attached to an element by the
230
+ * managed-layout shell's content resolvers. Returns `null` outside a
231
+ * managed-layout host (or before the element is connected).
232
+ *
233
+ * Subclass authors of `WippyElement` / `WippyVueElement` access this
234
+ * via `this.host` / `useHost()` and rarely need to call this directly.
235
+ * The helper is exposed for raw `HTMLElement` consumers.
236
+ */
237
+ declare function getWippyHost<H = unknown>(el: HTMLElement): H | null;
238
+
239
+ /**
240
+ * Abstract base class for Wippy web components.
241
+ *
242
+ * Generic over `Props` — the parsed prop object type. This types `onMount`,
243
+ * `onPropsChanged`, and `validateProps`. Defaults to `Record<string, unknown>`.
244
+ *
245
+ * Lifecycle order (first connect):
246
+ * 1. shadow DOM attached
247
+ * 2. `onInit(shadow)` — hook for early shadow DOM customization
248
+ * 3. inline CSS injected
249
+ * 4. host CSS loading started (async, non-blocking)
250
+ * 5. container div created and appended
251
+ * 6. icons registered
252
+ * 7. props parsed from attributes + custom validation
253
+ * 8. `onMount(shadow, container, props, errors)` — framework setup
254
+ * 9. internals state → ready
255
+ * 10. `onReady()` — post-mount hook
256
+ * 11. `load` event emitted
257
+ *
258
+ * On reconnect (move-style DOM op: detach → reattach), the shadow root
259
+ * persists across the cycle, so steps 1, 3, 4, 5 (CSS injection +
260
+ * container creation) are skipped to avoid duplicating `<style>` and
261
+ * container `<div>` siblings inside the persistent shadow. The stored
262
+ * container is reused and `onMount` is re-invoked with `isRemount = true`
263
+ * so framework adapters can decide between fresh-mount and remount paths.
264
+ *
265
+ * On error: `onError(error)` → internals state → error → `error` event
266
+ * On disconnect: `onUnmount()` → `unload` event → internals cleared.
267
+ * Note: `disconnectedCallback` does NOT clear the shadow root or the
268
+ * stored container — they're reused on reconnect.
269
+ */
270
+ declare abstract class WippyElement<Props = Record<string, unknown>, Events = Record<string, unknown>> extends HTMLElement {
271
+ private _internals;
272
+ private _contentObserver;
273
+ /** True after the first successful connectedCallback completes. */
274
+ private _initialized;
275
+ /** Container div created on first connect; reused on subsequent connects. */
276
+ private _container;
277
+ /**
278
+ * Opt-in reactive adapter. Lazy-allocated on first `this.reactive`
279
+ * access, disposed on `disconnectedCallback`, re-allocated on next
280
+ * access after reconnect. Subclasses that never touch `this.reactive`
281
+ * pay zero allocation cost — `notifyProps`/`notifyContent` short-circuit
282
+ * on the null guard.
283
+ */
284
+ private _reactive;
285
+ /**
286
+ * Cached results of the most recent `parseProps` + `validateProps`
287
+ * pass. Set by `connectedCallback` and `attributeChangedCallback`.
288
+ * Read by `get reactive()` to avoid re-parsing on lazy allocation —
289
+ * means `validateProps` only ever runs once per attribute change,
290
+ * even for consumers that touch `this.reactive` inside `onMount`.
291
+ */
292
+ private _lastProps;
293
+ private _lastErrors;
294
+ private _lastContent;
295
+ /**
296
+ * Phantom field to retain the `Events` type parameter so it shows up in
297
+ * type errors / `Reflect.get` shape. Adapter typing reads it indirectly
298
+ * via the generic.
299
+ * @internal
300
+ */
301
+ readonly _events: Events;
302
+ /**
303
+ * Override to provide the component's configuration.
304
+ * Must be static because `observedAttributes` reads it before construction.
305
+ *
306
+ * Specify the generic to get typed `validateProps`:
307
+ * ```ts
308
+ * static get wippyConfig(): WippyElementConfig<MyProps> { ... }
309
+ * ```
310
+ */
311
+ static get wippyConfig(): WippyElementConfig;
312
+ /**
313
+ * Derived from the props schema + any `extraObservedAttributes`.
314
+ */
315
+ static get observedAttributes(): string[];
316
+ constructor();
317
+ /**
318
+ * Panel-scoped `host` wrapper attached by the managed-layout shell's
319
+ * content resolvers (`ComponentResolver` / `WebComponentPackageLoader`).
320
+ *
321
+ * Inside a managed-layout panel, this is a wrapper around the universal
322
+ * `host` API where context-aware calls (`layout.broadcast / send / on`)
323
+ * are routed through the panel-bound bus — so `sourcePanelId` is
324
+ * attributed correctly without postMessage indirection. Layout
325
+ * mutations and other host methods pass through unchanged.
326
+ *
327
+ * `null` outside a managed-layout context (compat shell, chat sidebar,
328
+ * standalone playground). Subclass code that needs a host in those
329
+ * cases can fall back to `import { host } from '@wippy-fe/proxy'`.
330
+ */
331
+ get host(): ReturnType<typeof getWippyHost>;
332
+ /**
333
+ * Emit a CustomEvent that bubbles and crosses shadow DOM boundaries.
334
+ */
335
+ protected emitEvent(eventName: string, detail?: unknown): void;
336
+ /**
337
+ * Opt-in reactive adapter — framework-agnostic. Subscribe to prop
338
+ * changes, content changes, or emit typed events from a non-Vue
339
+ * consumer without re-rolling reactivity.
340
+ *
341
+ * ```ts
342
+ * class MyEl extends WippyElement<{ count: number }, { tick: { n: number } }> {
343
+ * protected onMount() {
344
+ * const ctrl = new AbortController()
345
+ * this.reactive.props.subscribe(({ count }) => {
346
+ * this.shadowRoot!.querySelector('.n')!.textContent = String(count)
347
+ * }, { signal: ctrl.signal, immediate: true })
348
+ * }
349
+ * tick(n: number) { this.reactive.events.emit('tick', { n }) }
350
+ * }
351
+ * ```
352
+ *
353
+ * Allocation cost is zero unless this getter is touched. Disposed on
354
+ * `disconnectedCallback`; a fresh adapter is allocated on the next
355
+ * access after reconnect.
356
+ */
357
+ protected get reactive(): ReactiveAdapter<Props, Events>;
358
+ connectedCallback(): void;
359
+ disconnectedCallback(): void;
360
+ attributeChangedCallback(_name: string, oldVal: string | null, newVal: string | null): void;
361
+ /** Called right after shadow DOM is attached, before CSS or container. */
362
+ protected onInit(_shadow: ShadowRoot): void;
363
+ /**
364
+ * Called after shadow DOM, CSS, and container are ready. Mount your
365
+ * framework here.
366
+ *
367
+ * Invoked on first connect AND on every subsequent reconnect (move-style
368
+ * DOM op: detach → reattach). On reconnect the same `container` instance
369
+ * is reused. Adapters that hold a framework app handle should either
370
+ * detect `isRemount === true` and re-mount on the existing container, or
371
+ * leave the previous app intact across the detach/reattach cycle.
372
+ */
373
+ protected abstract onMount(shadow: ShadowRoot, container: HTMLElement, initialProps: Props, initialErrors: string[], initialContent?: string | null, isRemount?: boolean): void;
374
+ /** Called after internals state is set to ready, before the `load` event. */
375
+ protected onReady(): void;
376
+ /** Called when connectedCallback throws. Default logs to console. */
377
+ protected onError(error: unknown): void;
378
+ /** Called during disconnectedCallback — clean up framework resources. */
379
+ protected abstract onUnmount(): void;
380
+ /** Called when observed attributes change. Override to update framework state. */
381
+ protected onPropsChanged(_props: Props, _errors: string[]): void;
382
+ /**
383
+ * Extract text from a child `<template data-type="...">` element.
384
+ * Uses `.content.textContent` since `<template>` stores content in a DocumentFragment.
385
+ */
386
+ private _extractContent;
387
+ /** Called when child `<template>` content changes. Override to update framework state. */
388
+ protected onContentChanged(_content: string | null): void;
389
+ }
390
+
391
+ /**
392
+ * Loads host CSS URLs into the shadow root as `<style>` elements.
393
+ * Non-blocking — returns a promise but the component doesn't need to await it.
394
+ */
395
+ declare function loadHostCss(shadow: ShadowRoot, keys?: HostCssKey[]): Promise<void>;
396
+ /**
397
+ * Injects inline CSS text into the shadow root.
398
+ */
399
+ declare function injectInlineCss(shadow: ShadowRoot, text: string): void;
400
+
401
+ /**
402
+ * Layout bus types shared between `@wippy-fe/webcomponent-core` and
403
+ * `@wippy-fe/webcomponent-vue`. Mirror of the host-side
404
+ * `LayoutBusBound` / `BroadcastEnvelope` shapes; structurally
405
+ * compatible so a host-mounted web component can read its
406
+ * `__wippyHostBus` property without importing host-private types.
407
+ *
408
+ * The host (gen-2-chat managed-layout shell) sets:
409
+ * - `el.dataset.wippyPanelId` (string)
410
+ * - `(el as { __wippyHostBus?: LayoutBusBound }).__wippyHostBus`
411
+ * BEFORE the element is appended to the DOM. Web components see them
412
+ * on first `connectedCallback` tick.
413
+ *
414
+ * Outside a managed-layout host, both are absent — read code MUST
415
+ * tolerate `undefined`.
416
+ *
417
+ * The interface bodies live in `@wippy-fe/shared` so the host can
418
+ * import them without dragging in this package's `@wippy-fe/proxy`
419
+ * runtime dependency. Consumers continue to import from
420
+ * `@wippy-fe/webcomponent-core` (this re-export) for backward compat.
421
+ */
422
+
423
+ /**
424
+ * Read the host bus attached to an element by `ComponentResolver` in
425
+ * the managed-layout host. Returns `null` outside a managed-layout
426
+ * host (or before the element is connected). Stable across element
427
+ * remounts of the same panel — the resolver re-attaches a fresh bus
428
+ * each time.
429
+ */
430
+ declare function getWippyHostBus(el: HTMLElement): LayoutBusBound | null;
431
+ /**
432
+ * Read the panelId attribute set by `ComponentResolver`. `null`
433
+ * outside a managed-layout host.
434
+ */
435
+ declare function getWippyPanelId(el: HTMLElement): string | null;
436
+
437
+ interface ParseResult {
438
+ props: Record<string, unknown>;
439
+ errors: string[];
440
+ }
441
+ /**
442
+ * Converts a kebab-case attribute name to camelCase.
443
+ * `allowed-types` → `allowedTypes`
444
+ */
445
+ declare function attrToCamel(attr: string): string;
446
+ /**
447
+ * Parses all attributes on an element according to its props schema.
448
+ * Returns `{ props, errors }` where errors is an array of human-readable messages.
449
+ */
450
+ declare function parseProps(element: HTMLElement, schema: WippyPropsSchema): ParseResult;
451
+
452
+ export { type ContentListener, type ContentSubscription, type EventEmitterHandle, type HostCssKey, type ParseResult, type PropsListener, type PropsSubscription, type ReactiveAdapter, type SubscribeOptions, WippyElement, type WippyElementConfig, type WippyPropDefinition, type WippyPropsSchema, attrToCamel, getWippyHost, getWippyHostBus, getWippyPanelId, injectInlineCss, loadHostCss, parseProps };