@wippy-fe/webcomponent-vue 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,173 @@
1
+ # @wippy-fe/webcomponent-vue
2
+
3
+ Vue 3 integration layer for Wippy web components. Extends `@wippy-fe/webcomponent-core` with reactive props, Pinia state management, and Vue provider injection.
4
+
5
+ ## What it does
6
+
7
+ - **Vue app lifecycle** — creates and mounts a Vue 3 app inside the shadow DOM container
8
+ - **Reactive props** — attribute changes flow through a Vue `ref()` that components can `inject()`
9
+ - **Pinia** — automatically installed on every Vue app instance
10
+ - **Provider injection** — exposes props, errors, and an event emitter via Vue's `provide`/`inject`
11
+ - **Plugin support** — install additional Vue plugins via `vueConfig.plugins`
12
+ - **Custom providers** — hook into the Vue app before mount via `vueConfig.providers`
13
+
14
+ ## What it does NOT do
15
+
16
+ - DOM setup, CSS loading, or prop parsing — that's handled by `@wippy-fe/webcomponent-core` (see its README for CSS guide)
17
+ - Component registration — use `define(import.meta.url, YourElement)` (re-exported from core)
18
+ - Any React/Svelte/etc. integration — this package is Vue-only
19
+
20
+ ## Quick Start
21
+
22
+ ```ts
23
+ import { WippyVueElement, define } from '@wippy-fe/webcomponent-vue'
24
+ import MyApp from './app/my-app.vue'
25
+ import stylesText from './styles.css?inline'
26
+ import pkg from '../package.json'
27
+
28
+ class MyElement extends WippyVueElement {
29
+ static get wippyConfig() {
30
+ return {
31
+ propsSchema: pkg.wippy.props,
32
+ hostCssKeys: ['fontCssUrl', 'themeConfigUrl', 'primeVueCssUrl', 'iframeCssUrl'],
33
+ containerClasses: ['h-full'],
34
+ inlineCss: stylesText,
35
+ }
36
+ }
37
+
38
+ static get vueConfig() {
39
+ return {
40
+ rootComponent: MyApp,
41
+ }
42
+ }
43
+ }
44
+
45
+ export async function webComponent() {
46
+ return MyElement
47
+ }
48
+
49
+ define(import.meta.url, MyElement)
50
+ ```
51
+
52
+ ## API Reference
53
+
54
+ ### `WippyVueElement` (abstract class)
55
+
56
+ Extends `WippyElement` from `@wippy-fe/webcomponent-core`.
57
+
58
+ #### Static getters to override
59
+
60
+ ```ts
61
+ static get wippyConfig(): WippyElementConfig // from core — see core README for full options
62
+ static get vueConfig(): WippyVueElementConfig
63
+ ```
64
+
65
+ #### `WippyVueElementConfig`
66
+
67
+ ```ts
68
+ interface WippyVueElementConfig {
69
+ /** The root Vue component to mount. */
70
+ rootComponent: Component
71
+
72
+ /** Additional Vue plugins to install (beyond Pinia). */
73
+ plugins?: Array<{ install: (app: App) => void }>
74
+
75
+ /** Extra providers to inject. Called after standard providers are set up. */
76
+ providers?: (app: App, element: WippyVueElement) => void
77
+ }
78
+ ```
79
+
80
+ #### Lifecycle hooks
81
+
82
+ `WippyVueElement` implements `onMount`, `onUnmount`, and `onPropsChanged` from the base class. You can still override the other hooks from `WippyElement`:
83
+
84
+ | Hook | Available? | Notes |
85
+ |------|-----------|-------|
86
+ | `onInit(shadow)` | Override freely | Runs before CSS/container |
87
+ | `onMount(...)` | Implemented by WippyVueElement | Do not override — use `vueConfig` instead |
88
+ | `onReady()` | Override freely | Runs after Vue app is mounted and state is `ready` |
89
+ | `onError(error)` | Override freely | Custom error handling |
90
+ | `onUnmount()` | Implemented by WippyVueElement | Do not override |
91
+ | `onPropsChanged(...)` | Implemented by WippyVueElement | Updates reactive refs automatically |
92
+
93
+ ### Provider Symbols
94
+
95
+ Import these in your Vue components to access injected values:
96
+
97
+ ```ts
98
+ import { EVENT_PROVIDER, PROPS_PROVIDER, PROPS_ERROR_PROVIDER } from '@wippy-fe/webcomponent-vue'
99
+
100
+ // In setup()
101
+ const props = inject(PROPS_PROVIDER)! // Ref<Record<string, unknown>>
102
+ const errors = inject(PROPS_ERROR_PROVIDER)! // Ref<string[]>
103
+ const emit = inject(EVENT_PROVIDER)! // (event: string, detail?) => void
104
+ ```
105
+
106
+ | Symbol | Type | Description |
107
+ |--------|------|-------------|
108
+ | `EVENT_PROVIDER` | `(event: string, detail?) => void` | Emits CustomEvents from the host element |
109
+ | `PROPS_PROVIDER` | `Ref<Record<string, unknown>>` | Reactive parsed props from attributes |
110
+ | `PROPS_ERROR_PROVIDER` | `Ref<string[]>` | Reactive list of prop parsing errors |
111
+
112
+ ### Re-exports from core
113
+
114
+ For convenience, this package re-exports everything from `@wippy-fe/webcomponent-core`:
115
+
116
+ - `WippyElement`, `define`
117
+ - `WippyElementConfig`, `WippyPropsSchema`, `WippyPropDefinition`, `HostCssKey`, `ParseResult`
118
+
119
+ ## Adding Plugins
120
+
121
+ ```ts
122
+ static get vueConfig() {
123
+ return {
124
+ rootComponent: MyApp,
125
+ plugins: [createI18n({ /* ... */ })],
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Custom Providers
131
+
132
+ ```ts
133
+ import { MY_SERVICE } from './services'
134
+
135
+ static get vueConfig() {
136
+ return {
137
+ rootComponent: MyApp,
138
+ providers(app, element) {
139
+ app.provide(MY_SERVICE, new MyService(element))
140
+ },
141
+ }
142
+ }
143
+ ```
144
+
145
+ ## Migration from monolithic pattern
146
+
147
+ **Before** — every component duplicates ~170 lines:
148
+ ```ts
149
+ class MyElement extends HTMLElement {
150
+ private vueApp: App | null = null
151
+ private props: Ref<...> = ref({})
152
+ // ... shadow DOM, CSS loading, prop parsing, Vue setup, events ...
153
+ }
154
+ ```
155
+
156
+ **After** — ~20 lines:
157
+ ```ts
158
+ class MyElement extends WippyVueElement {
159
+ static get wippyConfig() {
160
+ return {
161
+ propsSchema: pkg.wippy.props,
162
+ hostCssKeys: ['fontCssUrl', 'themeConfigUrl', 'primeVueCssUrl', 'iframeCssUrl'],
163
+ containerClasses: ['h-full'],
164
+ inlineCss: stylesText,
165
+ }
166
+ }
167
+ static get vueConfig() {
168
+ return { rootComponent: MyApp }
169
+ }
170
+ }
171
+ ```
172
+
173
+ All the boilerplate (shadow DOM, CSS, prop parsing, Vue lifecycle, Pinia, providers) is handled by the base classes.
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@wippy-fe/webcomponent-vue",
3
+ "version": "0.0.6",
4
+ "description": "Vue 3 integration layer for Wippy web components — extends @wippy-fe/webcomponent-core with reactive props, Pinia, and provider injection.",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "files": [
9
+ "src/",
10
+ "package.json"
11
+ ],
12
+ "dependencies": {
13
+ "@wippy-fe/webcomponent-core": "^0.0.6"
14
+ },
15
+ "peerDependencies": {
16
+ "@wippy-fe/proxy": "^0.0.6",
17
+ "@iconify/vue": "^5.0.0",
18
+ "pinia": "^2.1.0",
19
+ "vue": "^3.5.0"
20
+ },
21
+ "license": "UNLICENSED"
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Vue layer
2
+ export { WippyVueElement } from './vue-element.ts'
3
+ export type { WippyVueElementConfig } from './vue-element.ts'
4
+
5
+ // Providers: base symbols, composables, and createProviders utility
6
+ export {
7
+ EVENT_PROVIDER, PROPS_PROVIDER, PROPS_ERROR_PROVIDER, CONTENT_PROVIDER,
8
+ useProps, useEvents, usePropsErrors, useContent,
9
+ createProviders,
10
+ } from './providers.ts'
11
+ export type { TypedProviders } from './providers.ts'
12
+
13
+ // Re-export core for convenience
14
+ export { WippyElement, define } from '@wippy-fe/webcomponent-core'
15
+ export type {
16
+ WippyElementConfig,
17
+ WippyPropsSchema,
18
+ WippyPropDefinition,
19
+ HostCssKey,
20
+ ParseResult,
21
+ } from '@wippy-fe/webcomponent-core'
@@ -0,0 +1,116 @@
1
+ import type { InjectionKey, Ref } from 'vue'
2
+ import { inject } from 'vue'
3
+
4
+ // ── Base injection keys (untyped) ────────────────────────────
5
+
6
+ /** Injection key for the event emitter function. */
7
+ export const EVENT_PROVIDER = Symbol('wippy:emit') as InjectionKey<
8
+ (event: string, detail?: unknown) => void
9
+ >
10
+
11
+ /** Injection key for the reactive props ref. */
12
+ export const PROPS_PROVIDER = Symbol('wippy:props') as InjectionKey<Ref<Record<string, unknown>>>
13
+
14
+ /** Injection key for the reactive props error ref. */
15
+ export const PROPS_ERROR_PROVIDER = Symbol('wippy:props_error') as InjectionKey<Ref<string[]>>
16
+
17
+ /** Injection key for reactive content from child <template data-type="..."> elements. */
18
+ export const CONTENT_PROVIDER = Symbol('wippy:content') as InjectionKey<Ref<string | null>>
19
+
20
+ // ── Composables (typed) ──────────────────────────────────────
21
+
22
+ /**
23
+ * Inject the reactive props ref, typed to your component's props interface.
24
+ *
25
+ * Must be called inside a Vue component's `setup()` that lives within a `WippyVueElement`.
26
+ *
27
+ * ```ts
28
+ * const props = useProps<ComponentProps>()
29
+ * // props is Ref<ComponentProps>
30
+ * console.log(props.value.maxFileSize)
31
+ * ```
32
+ */
33
+ export function useProps<Props>(): Ref<Props> {
34
+ const props = inject(PROPS_PROVIDER)
35
+ if (!props) throw new Error('useProps() must be called inside a WippyVueElement')
36
+ return props as unknown as Ref<Props>
37
+ }
38
+
39
+ /**
40
+ * Inject the typed event emitter, constrained to your component's event map.
41
+ *
42
+ * Must be called inside a Vue component's `setup()` that lives within a `WippyVueElement`.
43
+ *
44
+ * ```ts
45
+ * const emit = useEvents<Events>()
46
+ * emit('upload-complete', { fileId: '123', status: 'done' }) // fully typed
47
+ * ```
48
+ */
49
+ export function useEvents<Events>(): <K extends keyof Events>(event: K, detail: Events[K]) => void {
50
+ const emit = inject(EVENT_PROVIDER)
51
+ if (!emit) throw new Error('useEvents() must be called inside a WippyVueElement')
52
+ return emit as unknown as <K extends keyof Events>(event: K, detail: Events[K]) => void
53
+ }
54
+
55
+ /**
56
+ * Inject the reactive prop validation errors ref.
57
+ *
58
+ * Must be called inside a Vue component's `setup()` that lives within a `WippyVueElement`.
59
+ *
60
+ * ```ts
61
+ * const errors = usePropsErrors()
62
+ * // errors is Ref<string[]>
63
+ * ```
64
+ */
65
+ export function usePropsErrors(): Ref<string[]> {
66
+ const errors = inject(PROPS_ERROR_PROVIDER)
67
+ if (!errors) throw new Error('usePropsErrors() must be called inside a WippyVueElement')
68
+ return errors
69
+ }
70
+
71
+ /**
72
+ * Inject the reactive content ref (from child `<template data-type="...">` elements).
73
+ *
74
+ * Must be called inside a Vue component's `setup()` within a `WippyVueElement`
75
+ * that has `contentTemplate` configured.
76
+ *
77
+ * ```ts
78
+ * const content = useContent()
79
+ * // content is Ref<string | null>
80
+ * ```
81
+ */
82
+ export function useContent(): Ref<string | null> {
83
+ const content = inject(CONTENT_PROVIDER)
84
+ if (!content) throw new Error('useContent() must be called inside a WippyVueElement with contentTemplate configured')
85
+ return content
86
+ }
87
+
88
+ // ── createProviders (typed injection keys) ───────────────────
89
+
90
+ /**
91
+ * Typed provider keys for a specific component.
92
+ */
93
+ export interface TypedProviders<Props, Events> {
94
+ EVENT_PROVIDER: InjectionKey<<K extends keyof Events>(event: K, detail: Events[K]) => void>
95
+ PROPS_PROVIDER: InjectionKey<Ref<Props>>
96
+ PROPS_ERROR_PROVIDER: InjectionKey<Ref<string[]>>
97
+ }
98
+
99
+ /**
100
+ * Creates typed provider injection keys for a specific component.
101
+ *
102
+ * Use this if you prefer raw `inject(KEY)` over the composable helpers.
103
+ * Returns the same Symbol instances with narrowed types.
104
+ *
105
+ * ```ts
106
+ * export const { EVENT_PROVIDER, PROPS_PROVIDER, PROPS_ERROR_PROVIDER } =
107
+ * createProviders<ComponentProps, Events>()
108
+ * ```
109
+ */
110
+ export function createProviders<Props, Events = Record<string, unknown>>(): TypedProviders<Props, Events> {
111
+ return {
112
+ EVENT_PROVIDER: EVENT_PROVIDER as unknown as TypedProviders<Props, Events>['EVENT_PROVIDER'],
113
+ PROPS_PROVIDER: PROPS_PROVIDER as unknown as TypedProviders<Props, Events>['PROPS_PROVIDER'],
114
+ PROPS_ERROR_PROVIDER,
115
+ }
116
+ }
@@ -0,0 +1,128 @@
1
+ import type { App, Component, Ref } from 'vue'
2
+ import { WippyElement } from '@wippy-fe/webcomponent-core'
3
+ import { createPinia } from 'pinia'
4
+ import { createApp, ref } from 'vue'
5
+ import { EVENT_PROVIDER, PROPS_PROVIDER, PROPS_ERROR_PROVIDER, CONTENT_PROVIDER } from './providers.ts'
6
+
7
+ /**
8
+ * Vue-specific configuration returned by `static get vueConfig()`.
9
+ */
10
+ export interface WippyVueElementConfig {
11
+ /** The root Vue component to mount. */
12
+ rootComponent: Component
13
+ /** Additional Vue plugins to install (beyond Pinia which is always installed). */
14
+ plugins?: Array<{ install: (app: App) => void }>
15
+ /** Extra providers to inject into the Vue app. */
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ providers?: (app: App, element: WippyVueElement<any, any>) => void
18
+ }
19
+
20
+ /**
21
+ * Vue 3 integration for Wippy web components.
22
+ *
23
+ * Generic over `Props` (parsed prop object type) and `Events` (event map).
24
+ * These flow through to typed hooks, reactive refs, and composable helpers.
25
+ *
26
+ * Usage:
27
+ * ```ts
28
+ * class MyElement extends WippyVueElement<MyProps, MyEvents> {
29
+ * static get wippyConfig(): WippyElementConfig<MyProps> {
30
+ * return { propsSchema: pkg.wippy.props, inlineCss: stylesText }
31
+ * }
32
+ * static get vueConfig() {
33
+ * return { rootComponent: MyApp }
34
+ * }
35
+ * }
36
+ * ```
37
+ *
38
+ * In Vue components, use the typed composable helpers:
39
+ * ```ts
40
+ * const props = useProps<MyProps>()
41
+ * const emit = useEvents<MyEvents>()
42
+ * const errors = usePropsErrors()
43
+ * ```
44
+ */
45
+ export abstract class WippyVueElement<
46
+ Props = Record<string, unknown>,
47
+ Events = Record<string, unknown>,
48
+ > extends WippyElement<Props> {
49
+ private _vueApp: App<Element> | null = null
50
+ private _propsRef: Ref<Props> = ref({}) as Ref<Props>
51
+ private _errorsRef: Ref<string[]> = ref([])
52
+ private _contentRef: Ref<string | null> = ref(null)
53
+
54
+ /** @internal Phantom field to retain the Events type parameter. */
55
+ declare readonly _events: Events
56
+
57
+ /**
58
+ * Override to provide Vue-specific configuration.
59
+ */
60
+ static get vueConfig(): WippyVueElementConfig {
61
+ throw new Error('WippyVueElement subclass must override static get vueConfig()')
62
+ }
63
+
64
+ protected onMount(
65
+ _shadow: ShadowRoot,
66
+ container: HTMLElement,
67
+ initialProps: Props,
68
+ initialErrors: string[],
69
+ initialContent?: string | null,
70
+ ): void {
71
+ const vueConfig = (this.constructor as typeof WippyVueElement).vueConfig
72
+
73
+ // 1. Set reactive state
74
+ this._propsRef.value = initialProps
75
+ this._errorsRef.value = initialErrors
76
+ this._contentRef.value = initialContent ?? null
77
+
78
+ // Emit initial errors as invalid events
79
+ for (const error of initialErrors) {
80
+ this.emitEvent('invalid', { message: error })
81
+ }
82
+
83
+ // 2. Create Vue app
84
+ this._vueApp = createApp(vueConfig.rootComponent)
85
+ this._vueApp.use(createPinia())
86
+
87
+ // 3. Install extra plugins
88
+ if (vueConfig.plugins) {
89
+ for (const plugin of vueConfig.plugins) {
90
+ this._vueApp.use(plugin)
91
+ }
92
+ }
93
+
94
+ // 4. Provide standard injection keys
95
+ this._vueApp.provide(PROPS_PROVIDER, this._propsRef as Ref<Record<string, unknown>>)
96
+ this._vueApp.provide(PROPS_ERROR_PROVIDER, this._errorsRef)
97
+ this._vueApp.provide(EVENT_PROVIDER, this.emitEvent.bind(this))
98
+ this._vueApp.provide(CONTENT_PROVIDER, this._contentRef)
99
+
100
+ // 5. Custom providers
101
+ if (vueConfig.providers) {
102
+ vueConfig.providers(this._vueApp, this)
103
+ }
104
+
105
+ // 6. Mount
106
+ this._vueApp.mount(container)
107
+ }
108
+
109
+ protected onUnmount(): void {
110
+ if (this._vueApp) {
111
+ this._vueApp.unmount()
112
+ this._vueApp = null
113
+ }
114
+ }
115
+
116
+ protected onPropsChanged(props: Props, errors: string[]): void {
117
+ this._propsRef.value = props
118
+ this._errorsRef.value = errors
119
+
120
+ for (const error of errors) {
121
+ this.emitEvent('invalid', { message: error })
122
+ }
123
+ }
124
+
125
+ protected onContentChanged(content: string | null): void {
126
+ this._contentRef.value = content
127
+ }
128
+ }