@wippy-fe/webcomponent-core 0.0.32 → 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.
@@ -1,290 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest'
2
- import { ReactiveAdapterImpl } from './reactive-adapter.ts'
3
-
4
- // ─────────────────────────────────────────────────────────────
5
- // Helpers
6
- // ─────────────────────────────────────────────────────────────
7
-
8
- interface TestProps {
9
- count: number
10
- label: string
11
- }
12
-
13
- interface TestEvents {
14
- tick: { n: number }
15
- reset: { reason: string }
16
- }
17
-
18
- function makeAdapter(initial?: {
19
- props?: TestProps
20
- errors?: string[]
21
- content?: string | null
22
- hasContent?: boolean
23
- }) {
24
- const emit = vi.fn()
25
- const adapter = new ReactiveAdapterImpl<TestProps, TestEvents>(
26
- {
27
- props: initial?.props ?? { count: 0, label: 'init' },
28
- errors: initial?.errors ?? [],
29
- content: initial?.content ?? null,
30
- hasContent: initial?.hasContent ?? false,
31
- },
32
- emit,
33
- )
34
- return { adapter, emit }
35
- }
36
-
37
- afterEach(() => {
38
- vi.restoreAllMocks()
39
- })
40
-
41
- // ─────────────────────────────────────────────────────────────
42
- // props subscription
43
- // ─────────────────────────────────────────────────────────────
44
-
45
- describe('reactiveAdapter — props subscription', () => {
46
- it('exposes the initial props via `.value` and `.errors`', () => {
47
- const { adapter } = makeAdapter({
48
- props: { count: 5, label: 'hello' },
49
- errors: ['oops'],
50
- })
51
- expect(adapter.props.value).toEqual({ count: 5, label: 'hello' })
52
- expect(adapter.props.errors).toEqual(['oops'])
53
- })
54
-
55
- it('fires listeners on notifyProps with new values', () => {
56
- const { adapter } = makeAdapter()
57
- const listener = vi.fn()
58
- adapter.props.subscribe(listener)
59
-
60
- adapter.notifyProps({ count: 1, label: 'one' }, ['e'])
61
-
62
- expect(listener).toHaveBeenCalledTimes(1)
63
- expect(listener).toHaveBeenCalledWith({ count: 1, label: 'one' }, ['e'])
64
- expect(adapter.props.value).toEqual({ count: 1, label: 'one' })
65
- expect(adapter.props.errors).toEqual(['e'])
66
- })
67
-
68
- it('returned unsubscribe function stops further callbacks', () => {
69
- const { adapter } = makeAdapter()
70
- const listener = vi.fn()
71
- const unsub = adapter.props.subscribe(listener)
72
-
73
- adapter.notifyProps({ count: 1, label: 'a' }, [])
74
- unsub()
75
- adapter.notifyProps({ count: 2, label: 'b' }, [])
76
-
77
- expect(listener).toHaveBeenCalledTimes(1)
78
- })
79
-
80
- it('abortSignal.abort() cancels the subscription', () => {
81
- const { adapter } = makeAdapter()
82
- const ctrl = new AbortController()
83
- const listener = vi.fn()
84
- adapter.props.subscribe(listener, { signal: ctrl.signal })
85
-
86
- adapter.notifyProps({ count: 1, label: 'a' }, [])
87
- ctrl.abort()
88
- adapter.notifyProps({ count: 2, label: 'b' }, [])
89
-
90
- expect(listener).toHaveBeenCalledTimes(1)
91
- })
92
-
93
- it('does NOT subscribe when the signal is already aborted', () => {
94
- const { adapter } = makeAdapter()
95
- const ctrl = new AbortController()
96
- ctrl.abort()
97
- const listener = vi.fn()
98
- adapter.props.subscribe(listener, { signal: ctrl.signal })
99
-
100
- adapter.notifyProps({ count: 1, label: 'a' }, [])
101
-
102
- expect(listener).not.toHaveBeenCalled()
103
- })
104
-
105
- it('options.immediate fires the listener synchronously with current state', () => {
106
- const { adapter } = makeAdapter({
107
- props: { count: 7, label: 'now' },
108
- errors: ['warn'],
109
- })
110
- const listener = vi.fn()
111
- adapter.props.subscribe(listener, { immediate: true })
112
-
113
- expect(listener).toHaveBeenCalledTimes(1)
114
- expect(listener).toHaveBeenCalledWith({ count: 7, label: 'now' }, ['warn'])
115
- })
116
-
117
- it('multiple subscribers all receive updates; unsubscribing one does not affect others', () => {
118
- const { adapter } = makeAdapter()
119
- const a = vi.fn()
120
- const b = vi.fn()
121
- const c = vi.fn()
122
- adapter.props.subscribe(a)
123
- const unsubB = adapter.props.subscribe(b)
124
- adapter.props.subscribe(c)
125
-
126
- adapter.notifyProps({ count: 1, label: 'x' }, [])
127
- unsubB()
128
- adapter.notifyProps({ count: 2, label: 'y' }, [])
129
-
130
- expect(a).toHaveBeenCalledTimes(2)
131
- expect(b).toHaveBeenCalledTimes(1)
132
- expect(c).toHaveBeenCalledTimes(2)
133
- })
134
- })
135
-
136
- // ─────────────────────────────────────────────────────────────
137
- // events emitter
138
- // ─────────────────────────────────────────────────────────────
139
-
140
- describe('reactiveAdapter — AbortSignal hygiene', () => {
141
- it('manual unsub() removes the abort listener from the signal (no leak)', () => {
142
- const { adapter } = makeAdapter()
143
- const ctrl = new AbortController()
144
- const removeSpy = vi.spyOn(ctrl.signal, 'removeEventListener')
145
- const listener = vi.fn()
146
- const unsub = adapter.props.subscribe(listener, { signal: ctrl.signal })
147
-
148
- unsub()
149
-
150
- expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function))
151
- })
152
-
153
- it('does not double-fire when abort fires after manual unsub', () => {
154
- const { adapter } = makeAdapter()
155
- const ctrl = new AbortController()
156
- const listener = vi.fn()
157
- const unsub = adapter.props.subscribe(listener, { signal: ctrl.signal })
158
-
159
- unsub()
160
- ctrl.abort() // would re-trigger unsub if abort listener weren't removed
161
-
162
- // notifyProps should still find no listeners
163
- adapter.notifyProps({ count: 1, label: 'x' }, [])
164
- expect(listener).not.toHaveBeenCalled()
165
- })
166
- })
167
-
168
- describe('reactiveAdapter — re-entrant notify during immediate fire', () => {
169
- it('captures synchronous re-entrant notifyProps from inside immediate callback', () => {
170
- const { adapter } = makeAdapter()
171
- const seen: Array<{ count: number, label: string }> = []
172
- let triggeredOnce = false
173
- adapter.props.subscribe((props) => {
174
- seen.push(props)
175
- // Re-enter notifyProps from inside the listener (simulating a
176
- // listener that synchronously triggers an attribute change).
177
- if (!triggeredOnce) {
178
- triggeredOnce = true
179
- adapter.notifyProps({ count: 99, label: 'after-immediate' }, [])
180
- }
181
- }, { immediate: true })
182
-
183
- // Expected: 1st call = immediate fire with initial state,
184
- // 2nd call = synchronous notifyProps from inside listener.
185
- // Without the order-swap fix, the 2nd call would be lost.
186
- expect(seen).toEqual([
187
- { count: 0, label: 'init' },
188
- { count: 99, label: 'after-immediate' },
189
- ])
190
- })
191
- })
192
-
193
- describe('reactiveAdapter — events emitter', () => {
194
- it('emit delegates to the provided DOM emit function', () => {
195
- const { adapter, emit } = makeAdapter()
196
- adapter.events.emit('tick', { n: 42 })
197
- expect(emit).toHaveBeenCalledWith('tick', { n: 42 })
198
- })
199
-
200
- it('emit becomes a no-op after dispose', () => {
201
- const { adapter, emit } = makeAdapter()
202
- adapter.dispose()
203
- adapter.events.emit('tick', { n: 1 })
204
- expect(emit).not.toHaveBeenCalled()
205
- })
206
- })
207
-
208
- // ─────────────────────────────────────────────────────────────
209
- // content subscription
210
- // ─────────────────────────────────────────────────────────────
211
-
212
- describe('reactiveAdapter — content', () => {
213
- it('content is null when hasContent is false', () => {
214
- const { adapter } = makeAdapter({ hasContent: false })
215
- expect(adapter.content).toBeNull()
216
- })
217
-
218
- it('content exposes value + subscribe when hasContent is true', () => {
219
- const { adapter } = makeAdapter({
220
- hasContent: true,
221
- content: 'initial-text',
222
- })
223
- expect(adapter.content?.value).toBe('initial-text')
224
- })
225
-
226
- it('fires content listeners on notifyContent', () => {
227
- const { adapter } = makeAdapter({ hasContent: true, content: 'a' })
228
- const listener = vi.fn()
229
- adapter.content!.subscribe(listener)
230
-
231
- adapter.notifyContent('b')
232
-
233
- expect(listener).toHaveBeenCalledWith('b')
234
- expect(adapter.content!.value).toBe('b')
235
- })
236
-
237
- it('immediate fires for content too', () => {
238
- const { adapter } = makeAdapter({ hasContent: true, content: 'preset' })
239
- const listener = vi.fn()
240
- adapter.content!.subscribe(listener, { immediate: true })
241
- expect(listener).toHaveBeenCalledWith('preset')
242
- })
243
- })
244
-
245
- // ─────────────────────────────────────────────────────────────
246
- // dispose
247
- // ─────────────────────────────────────────────────────────────
248
-
249
- describe('reactiveAdapter — dispose', () => {
250
- it('notifyProps becomes a no-op after dispose', () => {
251
- const { adapter } = makeAdapter()
252
- const listener = vi.fn()
253
- adapter.props.subscribe(listener)
254
-
255
- adapter.dispose()
256
- adapter.notifyProps({ count: 99, label: 'after' }, [])
257
-
258
- expect(listener).not.toHaveBeenCalled()
259
- })
260
-
261
- it('notifyContent becomes a no-op after dispose', () => {
262
- const { adapter } = makeAdapter({ hasContent: true })
263
- const listener = vi.fn()
264
- adapter.content!.subscribe(listener)
265
-
266
- adapter.dispose()
267
- adapter.notifyContent('after')
268
-
269
- expect(listener).not.toHaveBeenCalled()
270
- })
271
-
272
- it('subscribe after dispose returns a no-op unsub and never fires', () => {
273
- const { adapter } = makeAdapter()
274
- adapter.dispose()
275
- const listener = vi.fn()
276
- const unsub = adapter.props.subscribe(listener)
277
-
278
- adapter.notifyProps({ count: 1, label: 'a' }, [])
279
- expect(listener).not.toHaveBeenCalled()
280
- expect(() => unsub()).not.toThrow()
281
- })
282
-
283
- it('dispose is idempotent', () => {
284
- const { adapter } = makeAdapter()
285
- expect(() => {
286
- adapter.dispose()
287
- adapter.dispose()
288
- }).not.toThrow()
289
- })
290
- })
@@ -1,220 +0,0 @@
1
- /**
2
- * Opt-in reactive adapter for `WippyElement`.
3
- *
4
- * Framework-agnostic primitive that lets non-Vue consumers subscribe to
5
- * prop / content updates and emit typed events on a web component without
6
- * pulling Vue (or any other reactivity library) into the core bundle.
7
- *
8
- * Access via `this.reactive` on a `WippyElement` subclass — the getter is
9
- * lazy, so subclasses that never touch it pay zero allocation cost. See
10
- * `base-element.ts:get reactive` for the bridge into the lifecycle.
11
- *
12
- * The `notifyProps` / `notifyContent` methods are package-visible: the
13
- * base element calls them from `attributeChangedCallback` and the content
14
- * MutationObserver callback respectively. They are NOT part of the public
15
- * API consumers should reach for.
16
- *
17
- * Cleanup: `dispose()` runs from `disconnectedCallback`. All listener
18
- * sets are cleared; subsequent notifyProps/notifyContent are no-ops; the
19
- * `events.emit` becomes a no-op too. A fresh adapter is allocated on the
20
- * next `this.reactive` access (i.e. after reconnect).
21
- */
22
-
23
- export type PropsListener<Props> = (props: Props, errors: readonly string[]) => void
24
- export type ContentListener = (content: string | null) => void
25
-
26
- export interface SubscribeOptions {
27
- /** Cancel the subscription when the signal aborts. */
28
- signal?: AbortSignal
29
- /** Fire the listener once synchronously with the current value. */
30
- immediate?: boolean
31
- }
32
-
33
- export interface PropsSubscription<Props> {
34
- /** Current parsed props. Always reflects the latest state. */
35
- readonly value: Props
36
- /** Current validation/parse errors. */
37
- readonly errors: readonly string[]
38
- /**
39
- * Register a listener that fires whenever props change. Returns an
40
- * unsubscribe function. Pass `options.signal` for AbortSignal-driven
41
- * cleanup (e.g. tied to an element's lifetime). Pass
42
- * `options.immediate: true` to fire once synchronously with the current
43
- * value — useful when the subscriber is set up mid-flight.
44
- *
45
- * Note: `notifyProps` runs BEFORE the subclass `onPropsChanged()` hook.
46
- * A subscriber added from inside `onPropsChanged()` therefore does NOT
47
- * receive the currently-firing change — only the next one. Use
48
- * `{ immediate: true }` to also see the current value.
49
- */
50
- subscribe: (listener: PropsListener<Props>, options?: SubscribeOptions) => () => void
51
- }
52
-
53
- export 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
-
60
- export interface EventEmitterHandle<Events> {
61
- /**
62
- * Dispatch a typed `CustomEvent` (bubbles + composed, identical
63
- * semantics to the protected `emitEvent` helper). The key is constrained
64
- * to `keyof Events & string`, so consumers narrow `Events` to get
65
- * autocomplete + payload-type checks.
66
- *
67
- * No-op after the adapter is disposed (i.e. after the element is
68
- * detached). The element is the only event target — subscribers on
69
- * the element itself receive the event.
70
- */
71
- emit: <K extends keyof Events & string>(event: K, detail: Events[K]) => void
72
- }
73
-
74
- export interface ReactiveAdapter<Props, Events> {
75
- props: PropsSubscription<Props>
76
- events: EventEmitterHandle<Events>
77
- /**
78
- * Present only when the element's `wippyConfig.contentTemplate` is set;
79
- * `null` otherwise. Avoids fake `.value === null` semantics for elements
80
- * that don't track children content.
81
- */
82
- content: ContentSubscription | null
83
- }
84
-
85
- /**
86
- * Concrete implementation used by `WippyElement`. The `notify*` methods
87
- * are intentionally exposed (not private) so the base-element can call
88
- * them; consumers should never reach for them.
89
- *
90
- * @internal
91
- */
92
- export class ReactiveAdapterImpl<Props, Events> implements ReactiveAdapter<Props, Events> {
93
- private _props: Props
94
- private _errors: readonly string[]
95
- private _content: string | null
96
- private _propsListeners = new Set<PropsListener<Props>>()
97
- private _contentListeners = new Set<ContentListener>()
98
- private _disposed = false
99
- private _emitToDom: (eventName: string, detail?: unknown) => void
100
-
101
- readonly props: PropsSubscription<Props>
102
- readonly events: EventEmitterHandle<Events>
103
- readonly content: ContentSubscription | null
104
-
105
- constructor(
106
- initial: {
107
- props: Props
108
- errors: readonly string[]
109
- content: string | null
110
- hasContent: boolean
111
- },
112
- emitToDom: (eventName: string, detail?: unknown) => void,
113
- ) {
114
- this._props = initial.props
115
- this._errors = initial.errors
116
- this._content = initial.content
117
- this._emitToDom = emitToDom
118
-
119
- // Each getter / closure below runs with the surrounding object literal
120
- // as `this`, not the class instance — so we have to capture the
121
- // instance explicitly here.
122
- // eslint-disable-next-line ts/no-this-alias
123
- const self = this
124
-
125
- this.props = {
126
- get value() {
127
- return self._props
128
- },
129
- get errors() {
130
- return self._errors
131
- },
132
- subscribe(listener, options) {
133
- return self._subscribeProps(listener, options)
134
- },
135
- }
136
-
137
- this.events = {
138
- emit(event, detail) {
139
- if (self._disposed)
140
- return
141
- self._emitToDom(event as string, detail)
142
- },
143
- }
144
-
145
- this.content = initial.hasContent
146
- ? {
147
- get value() {
148
- return self._content
149
- },
150
- subscribe(listener, options) {
151
- return self._subscribeContent(listener, options)
152
- },
153
- }
154
- : null
155
- }
156
-
157
- /** @internal */
158
- notifyProps(props: Props, errors: readonly string[]): void {
159
- if (this._disposed)
160
- return
161
- this._props = props
162
- this._errors = errors
163
- for (const listener of this._propsListeners)
164
- listener(props, errors)
165
- }
166
-
167
- /** @internal */
168
- notifyContent(content: string | null): void {
169
- if (this._disposed)
170
- return
171
- this._content = content
172
- for (const listener of this._contentListeners)
173
- listener(content)
174
- }
175
-
176
- /** @internal */
177
- dispose(): void {
178
- if (this._disposed)
179
- return
180
- this._disposed = true
181
- this._propsListeners.clear()
182
- this._contentListeners.clear()
183
- }
184
-
185
- private _subscribeProps(listener: PropsListener<Props>, options?: SubscribeOptions): () => void {
186
- if (this._disposed || options?.signal?.aborted)
187
- return () => {}
188
- // Add to the set BEFORE firing `immediate` so a re-entrant
189
- // `notifyProps` triggered from inside the immediate callback can
190
- // find this listener and fire it for the synchronous change too.
191
- this._propsListeners.add(listener)
192
- if (options?.immediate)
193
- listener(this._props, this._errors)
194
- const unsub = () => {
195
- this._propsListeners.delete(listener)
196
- // Manual unsub must also detach the abort listener — otherwise it
197
- // accumulates on long-lived signals (e.g. an AbortController kept
198
- // around for the element's whole lifetime that fans out to many
199
- // subscriptions). `{ once: true }` only auto-removes when 'abort'
200
- // actually fires.
201
- options?.signal?.removeEventListener('abort', unsub)
202
- }
203
- options?.signal?.addEventListener('abort', unsub, { once: true })
204
- return unsub
205
- }
206
-
207
- private _subscribeContent(listener: ContentListener, options?: SubscribeOptions): () => void {
208
- if (this._disposed || options?.signal?.aborted)
209
- return () => {}
210
- this._contentListeners.add(listener)
211
- if (options?.immediate)
212
- listener(this._content)
213
- const unsub = () => {
214
- this._contentListeners.delete(listener)
215
- options?.signal?.removeEventListener('abort', unsub)
216
- }
217
- options?.signal?.addEventListener('abort', unsub, { once: true })
218
- return unsub
219
- }
220
- }
package/src/types.ts DELETED
@@ -1,130 +0,0 @@
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 = '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
- * - `themeConfigUrl` — CSS custom properties (color scales, spacing, etc.) matching the
61
- * host theme. **Recommended for all components** — gives your component the same look
62
- * as the host app. At dev time, a local copy (`theme-config.css`) provides fallback
63
- * values; at runtime, the host injects the real theme.
64
- * - `primeVueCssUrl` — PrimeVue component classes (p-button, p-input, etc.) in unstyled
65
- * mode, matching the host's appearance. **Include if using any PrimeVue components**
66
- * (buttons, forms, tables, etc.). Skip only for fully custom UI with no PrimeVue.
67
- * - `markdownCssUrl` — Styles for rendered markdown blocks. **Include only if your
68
- * component renders markdown content.**
69
- * - `iframeCssUrl` — Scrollbar styling and iframe-related styles. **Recommended for all
70
- * components** so scrollbars look identical to the host app.
71
- *
72
- * Pass `[]` to skip host CSS entirely (fully self-styled component).
73
- * Defaults to `['themeConfigUrl', 'primeVueCssUrl', 'markdownCssUrl', 'iframeCssUrl']`.
74
- */
75
- hostCssKeys?: HostCssKey[]
76
- /**
77
- * Component-specific CSS text to inject into the shadow root (synchronous).
78
- *
79
- * Typically from a Vite `?inline` import of your component's stylesheet.
80
- * This is your component's own styling — Tailwind utilities, custom classes, etc.
81
- * Unlike `hostCssKeys` which loads platform CSS at runtime, this is bundled at build time.
82
- */
83
- inlineCss?: string
84
- /**
85
- * Custom prop validation that runs after type parsing.
86
- *
87
- * Receives the already type-coerced props from the schema parser.
88
- * Return an array of error messages for any invalid values, or an empty array if all valid.
89
- * Errors are merged with any type-parsing errors and emitted as `invalid` events.
90
- *
91
- * Use this for domain-specific checks the JSON schema can't express:
92
- * - Value ranges (`max-file-size` must be positive)
93
- * - Array item types (`allowed-types` must be an array of strings)
94
- * - Cross-prop constraints (if X is set, Y must also be set)
95
- *
96
- * Example:
97
- * ```ts
98
- * validateProps: (props) => {
99
- * const errors: string[] = []
100
- * if (typeof props.maxFileSize === 'number' && props.maxFileSize < 0) {
101
- * errors.push('max-file-size must be a positive number')
102
- * }
103
- * return errors
104
- * }
105
- * ```
106
- */
107
- validateProps?: (props: Props) => string[]
108
- /** CSS classes to add to the container div. Defaults to none. */
109
- containerClasses?: string[]
110
- /** Additional attribute names to observe beyond those in the props schema. */
111
- extraObservedAttributes?: string[]
112
- /**
113
- * If set, reads text content from a child `<template data-type="...">` element.
114
- * The value is the type to match, e.g. `'text/vnd.mermaid'`.
115
- * Content is extracted once on mount and updated via MutationObserver.
116
- * Props take priority over children content.
117
- *
118
- * Usage:
119
- * ```html
120
- * <example-mermaid>
121
- * <template data-type="text/vnd.mermaid">graph TD; A --> B</template>
122
- * </example-mermaid>
123
- * ```
124
- *
125
- * Uses `<template>` instead of `<script>` because Vue templates strip script tags.
126
- * The native `<template>` element is inert (not rendered) and works in both
127
- * raw HTML and Vue SFC templates.
128
- */
129
- contentTemplate?: string
130
- }