@wippy-fe/webcomponent-core 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @wippy-fe/webcomponent-core
2
+
3
+ Framework-agnostic base class for building Wippy web components. Handles the boilerplate every component needs — shadow DOM, CSS loading, schema-driven prop parsing, ElementInternals state, and lifecycle events — so your subclass only deals with framework-specific mounting.
4
+
5
+ ## What it does
6
+
7
+ - **Shadow DOM setup** with configurable mode (`open`/`closed`) and container `<div>`
8
+ - **Host CSS inheritance** — loads Wippy platform CSS into the shadow root so the component matches the host theme
9
+ - **Inline CSS injection** — injects component-specific styles from `?inline` imports
10
+ - **Schema-driven prop parsing** — reads `wippy.props` from package.json and auto-converts attributes to typed JS values
11
+ - **ElementInternals** — manages `loading` → `ready` / `error` states
12
+ - **Lifecycle hooks** — `onInit`, `onMount`, `onReady`, `onError`, `onUnmount`, `onPropsChanged`
13
+ - **Lifecycle events** — emits `load`, `unload`, `error` as CustomEvents that cross shadow boundaries
14
+ - **Icon registration** — calls `addIcons(addCollection)` from `@wippy-fe/proxy`
15
+
16
+ ## What it does NOT do
17
+
18
+ - Any framework setup (Vue, React, Svelte, etc.) — that's for framework-specific packages like `@wippy-fe/webcomponent-vue`
19
+ - Component registration — use `define(import.meta.url, YourElement)` from this package
20
+ - State management — the base class is stateless; your framework layer manages state
21
+
22
+ ## CSS: How Styling Works in Shadow DOM
23
+
24
+ Shadow DOM blocks style inheritance from the host page. A web component must explicitly bring in any styles it needs. There are two mechanisms:
25
+
26
+ ### Inline CSS (`inlineCss`)
27
+
28
+ Your component's **own** styles — Tailwind utilities, custom classes, layout rules. Bundled at build time via Vite's `?inline` import and injected **synchronously** into the shadow root before mount.
29
+
30
+ ```ts
31
+ import stylesText from './styles.css?inline'
32
+
33
+ static get wippyConfig() {
34
+ return {
35
+ propsSchema: pkg.wippy.props,
36
+ inlineCss: stylesText, // your component's CSS, injected immediately
37
+ }
38
+ }
39
+ ```
40
+
41
+ Every component with its own stylesheet needs this.
42
+
43
+ ### Host CSS Inheritance (`hostCssKeys`)
44
+
45
+ Shared **platform CSS** loaded at runtime from the Wippy host into the shadow root. This is how your component inherits the host app's look-and-feel. Loaded **asynchronously** (non-blocking — the component becomes interactive before CSS finishes loading).
46
+
47
+ | Key | What it provides | When to include |
48
+ |-----|-----------------|-----------------|
49
+ | `fontCssUrl` | Platform font definitions | Almost always — skip only if using fully custom fonts |
50
+ | `themeConfigUrl` | CSS custom properties (color scales, spacing, radii, etc.) matching the host theme | **Recommended for all components.** This is what makes your component look consistent with the host. At dev time, a local `theme-config.css` provides fallback values; at runtime the host injects the real theme. |
51
+ | `primeVueCssUrl` | PrimeVue component classes (`p-button`, `p-input`, etc.) in unstyled mode, styled to match the host | **Include if using any PrimeVue components** (buttons, forms, tables, etc.). Skip only for fully custom UI with zero PrimeVue usage. |
52
+ | `markdownCssUrl` | Styles for rendered markdown blocks | **Include only if rendering markdown.** |
53
+ | `iframeCssUrl` | Scrollbar styling and iframe-related styles | **Recommended for all components** so scrollbars match the host. |
54
+
55
+ **Choose what your component actually uses:**
56
+
57
+ ```ts
58
+ // Standard component using PrimeVue UI (most common)
59
+ hostCssKeys: ['fontCssUrl', 'themeConfigUrl', 'primeVueCssUrl', 'iframeCssUrl']
60
+
61
+ // Also renders markdown content
62
+ hostCssKeys: ['fontCssUrl', 'themeConfigUrl', 'primeVueCssUrl', 'markdownCssUrl', 'iframeCssUrl']
63
+
64
+ // Minimal: just theme variables, no PrimeVue
65
+ hostCssKeys: ['fontCssUrl', 'themeConfigUrl', 'iframeCssUrl']
66
+
67
+ // Fully self-styled, no host inheritance
68
+ hostCssKeys: []
69
+ ```
70
+
71
+ Default (when omitted): all five keys.
72
+
73
+ > **Note on dev-time duplication:** Your component's `styles.css` may import `theme-config.css` for local dev (so Tailwind/PostCSS can resolve theme variables). At runtime, the host provides the real theme via `themeConfigUrl`. This duplication is a known trade-off — the host's runtime values take precedence.
74
+
75
+ ## API Reference
76
+
77
+ ### `WippyElement` (abstract class)
78
+
79
+ Extend this class and implement the hooks you need.
80
+
81
+ #### Static getters to override
82
+
83
+ ```ts
84
+ static get wippyConfig(): WippyElementConfig
85
+ ```
86
+
87
+ Returns the component configuration. **Must be overridden** — the default returns an empty schema.
88
+
89
+ ```ts
90
+ static get observedAttributes(): string[]
91
+ ```
92
+
93
+ Automatically derived from `wippyConfig.propsSchema.properties` + `extraObservedAttributes`. You rarely need to override this.
94
+
95
+ #### Lifecycle hooks
96
+
97
+ All hooks are optional except `onMount` and `onUnmount` (abstract).
98
+
99
+ | Hook | When it runs | Use case |
100
+ |------|-------------|----------|
101
+ | `onInit(shadow)` | After shadow DOM attached, before CSS/container | Add extra DOM elements, configure shadow root |
102
+ | `onMount(shadow, container, props, errors)` | After CSS, container, icons, and props are ready | **Abstract.** Mount your framework here |
103
+ | `onReady()` | After internals state → `ready`, before `load` event | Post-mount logic, telemetry, deferred setup |
104
+ | `onError(error)` | When `connectedCallback` throws | Custom error reporting (default: `console.error`) |
105
+ | `onUnmount()` | During `disconnectedCallback` | **Abstract.** Tear down framework |
106
+ | `onPropsChanged(props, errors)` | When observed attributes change | Push new values into framework reactivity |
107
+
108
+ **Full lifecycle order:**
109
+ 1. Shadow DOM attached (`open` or `closed`)
110
+ 2. `onInit(shadow)`
111
+ 3. Inline CSS injected (sync)
112
+ 4. Host CSS loading started (async, non-blocking)
113
+ 5. Container `<div>` created and appended
114
+ 6. Icons registered
115
+ 7. Props parsed from attributes
116
+ 8. `onMount(shadow, container, props, errors)`
117
+ 9. Internals state → `ready`
118
+ 10. `onReady()`
119
+ 11. `load` event emitted
120
+
121
+ On error: `onError(error)` → state → `error` → `error` event
122
+ On disconnect: `onUnmount()` → `unload` event → states cleared
123
+
124
+ #### Utility
125
+
126
+ ```ts
127
+ protected emitEvent(eventName: string, detail?: unknown): void
128
+ ```
129
+
130
+ Dispatches a `CustomEvent` with `bubbles: true, composed: true`.
131
+
132
+ ### `define(importMetaUrl, ComponentClass)`
133
+
134
+ Re-exported from `@wippy-fe/proxy`. Registers the custom element if the import URL contains a `declare-tag` parameter.
135
+
136
+ ### `parseProps(element, schema)`
137
+
138
+ Parses all attributes on an element according to a `WippyPropsSchema`. Returns `{ props, errors }`.
139
+
140
+ ### `loadHostCss(shadow, keys?)`
141
+
142
+ Loads host CSS URLs into a shadow root. Non-blocking (returns a Promise).
143
+
144
+ ### `injectInlineCss(shadow, text)`
145
+
146
+ Injects a `<style>` element with the given CSS text into the shadow root. Synchronous.
147
+
148
+ ### `attrToCamel(attr)`
149
+
150
+ Converts kebab-case (`allowed-types`) to camelCase (`allowedTypes`).
151
+
152
+ ## Types
153
+
154
+ ```ts
155
+ interface WippyElementConfig {
156
+ propsSchema: WippyPropsSchema
157
+ shadowMode?: 'open' | 'closed' // default: 'open'
158
+ hostCssKeys?: HostCssKey[] // default: font + theme + primeVue + markdown
159
+ inlineCss?: string
160
+ containerClasses?: string[] // default: none
161
+ extraObservedAttributes?: string[]
162
+ }
163
+
164
+ interface WippyPropsSchema {
165
+ type?: string
166
+ properties: Record<string, WippyPropDefinition>
167
+ }
168
+
169
+ interface WippyPropDefinition {
170
+ type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object'
171
+ default?: unknown
172
+ description?: string
173
+ items?: { type: string }
174
+ }
175
+
176
+ type HostCssKey = 'fontCssUrl' | 'themeConfigUrl' | 'primeVueCssUrl' | 'markdownCssUrl' | 'iframeCssUrl'
177
+ ```
178
+
179
+ ## Migration from monolithic pattern
180
+
181
+ **Before** — 170+ lines of boilerplate in every component:
182
+ ```ts
183
+ class MyElement extends HTMLElement {
184
+ // Manual shadow DOM, CSS loading, prop parsing, Vue setup, ...
185
+ }
186
+ ```
187
+
188
+ **After** — extend `WippyElement` (or `WippyVueElement` for Vue components):
189
+ ```ts
190
+ class MyElement extends WippyElement {
191
+ static get wippyConfig() {
192
+ return {
193
+ propsSchema: pkg.wippy.props,
194
+ hostCssKeys: ['fontCssUrl', 'themeConfigUrl', 'primeVueCssUrl', 'iframeCssUrl'],
195
+ containerClasses: ['h-full'],
196
+ inlineCss: stylesText,
197
+ }
198
+ }
199
+ onMount(shadow, container, props, errors) { /* framework setup */ }
200
+ onUnmount() { /* cleanup */ }
201
+ }
202
+ ```
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@wippy-fe/webcomponent-core",
3
+ "version": "0.0.6",
4
+ "description": "Framework-agnostic base class for Wippy web components — shadow DOM, CSS loading, schema-driven prop parsing, and lifecycle management.",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "files": [
9
+ "src/",
10
+ "package.json"
11
+ ],
12
+ "peerDependencies": {
13
+ "@wippy-fe/proxy": "^0.0.6",
14
+ "@iconify/vue": "^5.0.0"
15
+ },
16
+ "license": "UNLICENSED"
17
+ }
@@ -0,0 +1,212 @@
1
+ import { addIcons, define } from '@wippy-fe/proxy'
2
+ import { addCollection } from '@iconify/vue'
3
+ import { loadHostCss, injectInlineCss } from './css-loader.ts'
4
+ import { parseProps } from './prop-parser.ts'
5
+ import type { WippyElementConfig } from './types.ts'
6
+
7
+ /**
8
+ * Abstract base class for Wippy web components.
9
+ *
10
+ * Generic over `Props` — the parsed prop object type. This types `onMount`,
11
+ * `onPropsChanged`, and `validateProps`. Defaults to `Record<string, unknown>`.
12
+ *
13
+ * Lifecycle order:
14
+ * 1. shadow DOM attached
15
+ * 2. `onInit(shadow)` — hook for early shadow DOM customization
16
+ * 3. inline CSS injected
17
+ * 4. host CSS loading started (async, non-blocking)
18
+ * 5. container div created and appended
19
+ * 6. icons registered
20
+ * 7. props parsed from attributes + custom validation
21
+ * 8. `onMount(shadow, container, props, errors)` — framework setup
22
+ * 9. internals state → ready
23
+ * 10. `onReady()` — post-mount hook
24
+ * 11. `load` event emitted
25
+ *
26
+ * On error: `onError(error)` → internals state → error → `error` event
27
+ * On disconnect: `onUnmount()` → `unload` event → internals cleared
28
+ */
29
+ export abstract class WippyElement<Props = Record<string, unknown>> extends HTMLElement {
30
+ private _internals!: ElementInternals
31
+ private _contentObserver: MutationObserver | null = null
32
+
33
+ /**
34
+ * Override to provide the component's configuration.
35
+ * Must be static because `observedAttributes` reads it before construction.
36
+ *
37
+ * Specify the generic to get typed `validateProps`:
38
+ * ```ts
39
+ * static get wippyConfig(): WippyElementConfig<MyProps> { ... }
40
+ * ```
41
+ */
42
+ static get wippyConfig(): WippyElementConfig {
43
+ return { propsSchema: { properties: {} } }
44
+ }
45
+
46
+ /**
47
+ * Derived from the props schema + any `extraObservedAttributes`.
48
+ */
49
+ static get observedAttributes(): string[] {
50
+ const config = this.wippyConfig
51
+ const schemaAttrs = Object.keys(config.propsSchema.properties)
52
+ const extra = config.extraObservedAttributes ?? []
53
+ return [...schemaAttrs, ...extra]
54
+ }
55
+
56
+ constructor() {
57
+ super()
58
+ this._internals = this.attachInternals()
59
+ }
60
+
61
+ /**
62
+ * Emit a CustomEvent that bubbles and crosses shadow DOM boundaries.
63
+ */
64
+ protected emitEvent(eventName: string, detail?: unknown): void {
65
+ this.dispatchEvent(new CustomEvent(eventName, {
66
+ bubbles: true,
67
+ composed: true,
68
+ detail,
69
+ }))
70
+ }
71
+
72
+ // ── Lifecycle ──────────────────────────────────────────────
73
+
74
+ connectedCallback(): void {
75
+ this._internals.states.add('loading')
76
+ try {
77
+ const config = (this.constructor as typeof WippyElement).wippyConfig
78
+
79
+ // 1. Shadow DOM (guard against reconnect — attachShadow throws if called twice)
80
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: config.shadowMode ?? 'open' })
81
+
82
+ // 2. Early hook — customize shadow before CSS/container
83
+ this.onInit(shadow)
84
+
85
+ // 3. Inline CSS
86
+ if (config.inlineCss) {
87
+ injectInlineCss(shadow, config.inlineCss)
88
+ }
89
+
90
+ // 4. Host CSS (async, non-blocking)
91
+ if (config.hostCssKeys === undefined || config.hostCssKeys.length > 0) {
92
+ loadHostCss(shadow, config.hostCssKeys)
93
+ }
94
+
95
+ // 5. Container
96
+ const container = document.createElement('div')
97
+ const classes = config.containerClasses ?? []
98
+ if (classes.length > 0) {
99
+ container.classList.add(...classes)
100
+ }
101
+ shadow.appendChild(container)
102
+
103
+ // 6. Icons
104
+ addIcons(addCollection)
105
+
106
+ // 7. Parse initial props + custom validation
107
+ const { props, errors } = parseProps(this, config.propsSchema)
108
+ if (config.validateProps) {
109
+ errors.push(...config.validateProps(props))
110
+ }
111
+ const typedProps = props as Props
112
+
113
+ // 7b. Extract children content (if contentTemplate configured)
114
+ let initialContent: string | null = null
115
+ if (config.contentTemplate) {
116
+ initialContent = this._extractContent(config.contentTemplate)
117
+ this._contentObserver = new MutationObserver(() => {
118
+ const content = this._extractContent(config.contentTemplate!)
119
+ this.onContentChanged(content)
120
+ })
121
+ this._contentObserver.observe(this, {
122
+ childList: true,
123
+ characterData: true,
124
+ subtree: true,
125
+ })
126
+ }
127
+
128
+ // 8. Framework mount
129
+ this.onMount(shadow, container, typedProps, errors, initialContent)
130
+
131
+ // 9. Ready
132
+ this._internals.states.delete('loading')
133
+ this._internals.states.add('ready')
134
+
135
+ // 10. Post-mount hook
136
+ this.onReady()
137
+
138
+ // 11. Emit load
139
+ this.emitEvent('load')
140
+ } catch (error) {
141
+ this.onError(error)
142
+ this._internals.states.delete('loading')
143
+ this._internals.states.add('error')
144
+ this.emitEvent('error', {
145
+ message: error instanceof Error ? error.message : String(error),
146
+ error,
147
+ })
148
+ }
149
+ }
150
+
151
+ disconnectedCallback(): void {
152
+ if (this._contentObserver) {
153
+ this._contentObserver.disconnect()
154
+ this._contentObserver = null
155
+ }
156
+ this.onUnmount()
157
+ this.emitEvent('unload')
158
+ this._internals.states.clear()
159
+ }
160
+
161
+ attributeChangedCallback(_name: string, oldVal: string | null, newVal: string | null): void {
162
+ if (oldVal === newVal) return
163
+ const config = (this.constructor as typeof WippyElement).wippyConfig
164
+ const { props, errors } = parseProps(this, config.propsSchema)
165
+ if (config.validateProps) {
166
+ errors.push(...config.validateProps(props))
167
+ }
168
+ this.onPropsChanged(props as Props, errors)
169
+ }
170
+
171
+ // ── Hooks ──────────────────────────────────────────────────
172
+
173
+ /** Called right after shadow DOM is attached, before CSS or container. */
174
+ protected onInit(_shadow: ShadowRoot): void {}
175
+
176
+ /** Called once after shadow DOM, CSS, and container are ready. Mount your framework here. */
177
+ protected abstract onMount(
178
+ shadow: ShadowRoot,
179
+ container: HTMLElement,
180
+ initialProps: Props,
181
+ initialErrors: string[],
182
+ initialContent?: string | null,
183
+ ): void
184
+
185
+ /** Called after internals state is set to ready, before the `load` event. */
186
+ protected onReady(): void {}
187
+
188
+ /** Called when connectedCallback throws. Default logs to console. */
189
+ protected onError(error: unknown): void {
190
+ console.error(`${this.constructor.name} initialization failed:`, error)
191
+ }
192
+
193
+ /** Called during disconnectedCallback — clean up framework resources. */
194
+ protected abstract onUnmount(): void
195
+
196
+ /** Called when observed attributes change. Override to update framework state. */
197
+ protected onPropsChanged(_props: Props, _errors: string[]): void {}
198
+
199
+ /**
200
+ * Extract text from a child `<template data-type="...">` element.
201
+ * Uses `.content.textContent` since `<template>` stores content in a DocumentFragment.
202
+ */
203
+ private _extractContent(dataType: string): string | null {
204
+ const tpl = this.querySelector(`template[data-type="${dataType}"]`) as HTMLTemplateElement | null
205
+ return tpl?.content.textContent?.trim() ?? null
206
+ }
207
+
208
+ /** Called when child `<template>` content changes. Override to update framework state. */
209
+ protected onContentChanged(_content: string | null): void {}
210
+ }
211
+
212
+ export { define }
@@ -0,0 +1,39 @@
1
+ import { hostCss, loadCss } from '@wippy-fe/proxy'
2
+ import type { HostCssKey } from './types.ts'
3
+
4
+ const DEFAULT_HOST_CSS_KEYS: HostCssKey[] = [
5
+ 'fontCssUrl',
6
+ 'themeConfigUrl',
7
+ 'primeVueCssUrl',
8
+ 'markdownCssUrl',
9
+ 'iframeCssUrl',
10
+ ]
11
+
12
+ /**
13
+ * Loads host CSS URLs into the shadow root as `<style>` elements.
14
+ * Non-blocking — returns a promise but the component doesn't need to await it.
15
+ */
16
+ export function loadHostCss(shadow: ShadowRoot, keys?: HostCssKey[]): Promise<void> {
17
+ const cssKeys = keys ?? DEFAULT_HOST_CSS_KEYS
18
+ return Promise.all(
19
+ cssKeys.map((key) => loadCss(hostCss[key])),
20
+ ).then((cssChunks) => {
21
+ for (const css of cssChunks) {
22
+ const style = document.createElement('style')
23
+ style.textContent = css
24
+ style.setAttribute('role', '@wippy/host-css')
25
+ shadow.appendChild(style)
26
+ }
27
+ }).catch((err) => {
28
+ console.warn('Failed to load host CSS:', err)
29
+ })
30
+ }
31
+
32
+ /**
33
+ * Injects inline CSS text into the shadow root.
34
+ */
35
+ export function injectInlineCss(shadow: ShadowRoot, text: string): void {
36
+ const style = document.createElement('style')
37
+ style.textContent = text
38
+ shadow.appendChild(style)
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { WippyElement, define } from './base-element.ts'
2
+ export { loadHostCss, injectInlineCss } from './css-loader.ts'
3
+ export { parseProps, attrToCamel } from './prop-parser.ts'
4
+ export type {
5
+ WippyElementConfig,
6
+ WippyPropsSchema,
7
+ WippyPropDefinition,
8
+ HostCssKey,
9
+ } from './types.ts'
10
+ export type { ParseResult } from './prop-parser.ts'
@@ -0,0 +1,87 @@
1
+ import type { WippyPropsSchema, WippyPropDefinition } 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 = parseFloat(raw)
26
+ if (isNaN(n)) return { value: undefined, error: `Invalid ${attr}: expected a number` }
27
+ return { value: n }
28
+ }
29
+
30
+ case 'integer': {
31
+ const n = parseInt(raw, 10)
32
+ if (isNaN(n)) return { value: undefined, error: `Invalid ${attr}: expected an integer` }
33
+ return { value: n }
34
+ }
35
+
36
+ case 'boolean':
37
+ // Presence of the attribute means true (HTML convention), explicit "false" means false
38
+ return { value: raw !== 'false' }
39
+
40
+ case 'array':
41
+ case 'object': {
42
+ try {
43
+ const parsed = JSON.parse(raw)
44
+ if (def.type === 'array' && !Array.isArray(parsed)) {
45
+ return { value: undefined, error: `Invalid ${attr}: expected a JSON array` }
46
+ }
47
+ return { value: parsed }
48
+ } catch {
49
+ return { value: undefined, error: `Invalid ${attr}: must be valid JSON` }
50
+ }
51
+ }
52
+
53
+ default:
54
+ return { value: raw }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Parses all attributes on an element according to its props schema.
60
+ * Returns `{ props, errors }` where errors is an array of human-readable messages.
61
+ */
62
+ export function parseProps(element: HTMLElement, schema: WippyPropsSchema): ParseResult {
63
+ const props: Record<string, unknown> = {}
64
+ const errors: string[] = []
65
+
66
+ for (const [attr, def] of Object.entries(schema.properties)) {
67
+ const raw = element.getAttribute(attr)
68
+ const camel = attrToCamel(attr)
69
+
70
+ if (raw === null) {
71
+ // Attribute not set — use default if available
72
+ if (def.default !== undefined) {
73
+ props[camel] = def.default
74
+ }
75
+ continue
76
+ }
77
+
78
+ const result = parseValue(attr, raw, def)
79
+ if (result.error) {
80
+ errors.push(result.error)
81
+ } else {
82
+ props[camel] = result.value
83
+ }
84
+ }
85
+
86
+ return { props, errors }
87
+ }
package/src/types.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * JSON-Schema-style property descriptor used for attribute→prop parsing.
3
+ */
4
+ export interface WippyPropDefinition {
5
+ type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object'
6
+ default?: unknown
7
+ description?: string
8
+ items?: { type: string }
9
+ }
10
+
11
+ /**
12
+ * JSON-Schema-style props block — matches the `wippy.props` field in package.json.
13
+ *
14
+ * Example:
15
+ * ```json
16
+ * {
17
+ * "type": "object",
18
+ * "properties": {
19
+ * "allowed-types": { "type": "array", "items": { "type": "string" } },
20
+ * "max-file-size": { "type": "number", "default": 104857600 }
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+ export interface WippyPropsSchema {
26
+ type?: string
27
+ properties: Record<string, WippyPropDefinition>
28
+ }
29
+
30
+ /**
31
+ * Keys that map to CSS URLs exposed by `@wippy-fe/proxy`'s `hostCss` object.
32
+ * Pass a subset to `hostCssKeys` in config to load only what you need.
33
+ */
34
+ export type HostCssKey = 'fontCssUrl' | 'themeConfigUrl' | 'primeVueCssUrl' | 'markdownCssUrl' | 'iframeCssUrl'
35
+
36
+ /**
37
+ * Configuration object returned by `static get wippyConfig()`.
38
+ *
39
+ * Generic over `Props` so that `validateProps` receives the typed prop object.
40
+ * Use `WippyElementConfig<MyProps>` as the return type of your static getter
41
+ * to get typed validation:
42
+ *
43
+ * ```ts
44
+ * static get wippyConfig(): WippyElementConfig<MyProps> { ... }
45
+ * ```
46
+ */
47
+ export interface WippyElementConfig<Props = Record<string, unknown>> {
48
+ /** JSON-schema props block (usually from package.json `wippy.props`). */
49
+ propsSchema: WippyPropsSchema
50
+ /** Shadow DOM mode. Defaults to `'open'`. */
51
+ shadowMode?: 'open' | 'closed'
52
+ /**
53
+ * Host CSS to inherit from the Wippy platform into this component's shadow DOM.
54
+ *
55
+ * Shadow DOM blocks style inheritance, so platform styles (theme variables, fonts,
56
+ * UI framework classes, etc.) must be explicitly loaded into each component's shadow root.
57
+ * The host app provides these CSS assets at runtime via `@wippy-fe/proxy`.
58
+ *
59
+ * Available keys:
60
+ * - `fontCssUrl` — Platform font definitions. Include unless using fully custom fonts.
61
+ * - `themeConfigUrl` — CSS custom properties (color scales, spacing, etc.) matching the
62
+ * host theme. **Recommended for all components** — gives your component the same look
63
+ * as the host app. At dev time, a local copy (`theme-config.css`) provides fallback
64
+ * values; at runtime, the host injects the real theme.
65
+ * - `primeVueCssUrl` — PrimeVue component classes (p-button, p-input, etc.) in unstyled
66
+ * mode, matching the host's appearance. **Include if using any PrimeVue components**
67
+ * (buttons, forms, tables, etc.). Skip only for fully custom UI with no PrimeVue.
68
+ * - `markdownCssUrl` — Styles for rendered markdown blocks. **Include only if your
69
+ * component renders markdown content.**
70
+ * - `iframeCssUrl` — Scrollbar styling and iframe-related styles. **Recommended for all
71
+ * components** so scrollbars look identical to the host app.
72
+ *
73
+ * Pass `[]` to skip host CSS entirely (fully self-styled component).
74
+ * Defaults to `['fontCssUrl', 'themeConfigUrl', 'primeVueCssUrl', 'markdownCssUrl', 'iframeCssUrl']`.
75
+ */
76
+ hostCssKeys?: HostCssKey[]
77
+ /**
78
+ * Component-specific CSS text to inject into the shadow root (synchronous).
79
+ *
80
+ * Typically from a Vite `?inline` import of your component's stylesheet.
81
+ * This is your component's own styling — Tailwind utilities, custom classes, etc.
82
+ * Unlike `hostCssKeys` which loads platform CSS at runtime, this is bundled at build time.
83
+ */
84
+ inlineCss?: string
85
+ /**
86
+ * Custom prop validation that runs after type parsing.
87
+ *
88
+ * Receives the already type-coerced props from the schema parser.
89
+ * Return an array of error messages for any invalid values, or an empty array if all valid.
90
+ * Errors are merged with any type-parsing errors and emitted as `invalid` events.
91
+ *
92
+ * Use this for domain-specific checks the JSON schema can't express:
93
+ * - Value ranges (`max-file-size` must be positive)
94
+ * - Array item types (`allowed-types` must be an array of strings)
95
+ * - Cross-prop constraints (if X is set, Y must also be set)
96
+ *
97
+ * Example:
98
+ * ```ts
99
+ * validateProps: (props) => {
100
+ * const errors: string[] = []
101
+ * if (typeof props.maxFileSize === 'number' && props.maxFileSize < 0) {
102
+ * errors.push('max-file-size must be a positive number')
103
+ * }
104
+ * return errors
105
+ * }
106
+ * ```
107
+ */
108
+ validateProps?: (props: Props) => string[]
109
+ /** CSS classes to add to the container div. Defaults to none. */
110
+ containerClasses?: string[]
111
+ /** Additional attribute names to observe beyond those in the props schema. */
112
+ extraObservedAttributes?: string[]
113
+ /**
114
+ * If set, reads text content from a child `<template data-type="...">` element.
115
+ * The value is the type to match, e.g. `'text/vnd.mermaid'`.
116
+ * Content is extracted once on mount and updated via MutationObserver.
117
+ * Props take priority over children content.
118
+ *
119
+ * Usage:
120
+ * ```html
121
+ * <example-mermaid>
122
+ * <template data-type="text/vnd.mermaid">graph TD; A --> B</template>
123
+ * </example-mermaid>
124
+ * ```
125
+ *
126
+ * Uses `<template>` instead of `<script>` because Vue templates strip script tags.
127
+ * The native `<template>` element is inert (not rendered) and works in both
128
+ * raw HTML and Vue SFC templates.
129
+ */
130
+ contentTemplate?: string
131
+ }