@aparte/plugin-model-selector 0.7.0 → 0.8.0

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 CHANGED
@@ -28,6 +28,6 @@ import '@aparte/plugin-model-selector'; // registers <aparte-model-selector>
28
28
 
29
29
  Fires **`aparte-model-change`** (`{ providerId, modelId, previousProviderId, previousModelId }`) when the
30
30
  selection changes. It reads providers from the nearest instance config (`attachConfig`), falling back to
31
- the global `AparteConfig`.
31
+ `aparteGlobalConfig`.
32
32
 
33
33
  > ESM-only. Part of the aparté monorepo.
@@ -13,7 +13,8 @@
13
13
  *
14
14
  * @fires aparte-model-change - Fired when the selected model changes
15
15
  */
16
- export declare class AparteModelSelector extends HTMLElement {
16
+ import { type AparteConfig, type AparteConfigAware } from '@aparte/core';
17
+ export declare class AparteModelSelector extends HTMLElement implements AparteConfigAware {
17
18
  private _currentProviderId;
18
19
  private _currentModelId;
19
20
  private _providerModels;
@@ -21,15 +22,36 @@ export declare class AparteModelSelector extends HTMLElement {
21
22
  private _isLoading;
22
23
  private _isRendering;
23
24
  private _expandedGroups;
24
- /** Config governing THIS element — nearest instance boundary, else the global.
25
- Resolved on connect (multi-instance pages get their own config). */
25
+ /**
26
+ * Config governing THIS element — nearest instance boundary, else the global.
27
+ *
28
+ * Cached rather than resolved per call, because this element does not only
29
+ * READ it: it holds a `subscribe()` registration keyed on the object. That
30
+ * cache is only correct if something tells us when the boundary changes, and
31
+ * under all four wrappers it does change after we connect —
32
+ * `AparteChatHost.bind()` runs `attachConfig` from a post-mount hook. Hence
33
+ * {@link AparteConfigAware}; without it this element served the global config's
34
+ * providers to a chat that was given its own.
35
+ */
26
36
  private _cfg;
27
37
  private _configUnsubscribe;
28
38
  private _boundHandleChange;
29
39
  private _handleOptgroupToggle;
30
40
  static get observedAttributes(): string[];
31
41
  connectedCallback(): Promise<void>;
42
+ /** Listen for configuration changes (e.g. from an onboarding flow). */
43
+ private _subscribeToConfig;
32
44
  disconnectedCallback(): void;
45
+ /**
46
+ * The boundary above us appeared or changed: move the subscription and reload,
47
+ * because a different config means different providers and a different
48
+ * selection.
49
+ *
50
+ * See {@link AparteConfigAware}. The element's own per-instance tests passed
51
+ * before this existed only because they mount the boundary FIRST — the one
52
+ * ordering no wrapper produces.
53
+ */
54
+ aparteConfigChanged(next: AparteConfig): void;
33
55
  attributeChangedCallback(): void;
34
56
  /** Current provider ID. */
35
57
  get providerId(): string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"aparte-model-selector.d.ts","sourceRoot":"","sources":["../src/aparte-model-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiCH,qBAAa,mBAAoB,SAAQ,WAAW;IAChD,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAA0B;IAEjD;2EACuE;IACvE,OAAO,CAAC,IAAI,CAAmC;IAE/C,OAAO,CAAC,kBAAkB,CAA6B;IAGvD,OAAO,CAAC,kBAAkB,CAAiC;IAC3D,OAAO,CAAC,qBAAqB,CAA2D;IAExF,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCxC,oBAAoB,IAAI,IAAI;IAM5B,wBAAwB,IAAI,IAAI;IAUhC,2BAA2B;IAC3B,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,wBAAwB;IACxB,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED,0CAA0C;IAC1C,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;YAgBzC,sBAAsB;YAwCtB,iBAAiB;IA2C/B,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,WAAW;IA2HnB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,uBAAuB;CAWlC;AAOD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,uBAAuB,EAAE,mBAAmB,CAAC;KAChD;CACJ"}
1
+ {"version":3,"file":"aparte-model-selector.d.ts","sourceRoot":"","sources":["../src/aparte-model-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAIH,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAOzB,MAAM,cAAc,CAAC;AAoBtB,qBAAa,mBAAoB,SAAQ,WAAY,YAAW,iBAAiB;IAC7E,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAA0B;IAEjD;;;;;;;;;;OAUG;IACH,OAAO,CAAC,IAAI,CAAoC;IAEhD,OAAO,CAAC,kBAAkB,CAA6B;IAGvD,OAAO,CAAC,kBAAkB,CAAiC;IAC3D,OAAO,CAAC,qBAAqB,CAA2D;IAExF,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBxC,uEAAuE;IACvE,OAAO,CAAC,kBAAkB;IAoB1B,oBAAoB,IAAI,IAAI;IAM5B;;;;;;;;OAQG;IACH,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAqB7C,wBAAwB,IAAI,IAAI;IAUhC,2BAA2B;IAC3B,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,wBAAwB;IACxB,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED,0CAA0C;IAC1C,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;YAgBzC,sBAAsB;YAmDtB,iBAAiB;IA2C/B,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,WAAW;IA2HnB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,uBAAuB;CAWlC;AAOD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,uBAAuB,EAAE,mBAAmB,CAAC;KAChD;CACJ"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { AparteConfig, resolveConfig } from "@aparte/core";
1
+ import { aparteGlobalConfig, resolveConfig, escapeAttr } from "@aparte/core";
2
2
  function esc(value) {
3
- return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3
+ return escapeAttr(String(value ?? ""));
4
4
  }
5
5
  class AparteModelSelector extends HTMLElement {
6
6
  _currentProviderId = null;
@@ -10,9 +10,18 @@ class AparteModelSelector extends HTMLElement {
10
10
  _isLoading = false;
11
11
  _isRendering = false;
12
12
  _expandedGroups = /* @__PURE__ */ new Set();
13
- /** Config governing THIS element — nearest instance boundary, else the global.
14
- Resolved on connect (multi-instance pages get their own config). */
15
- _cfg = AparteConfig;
13
+ /**
14
+ * Config governing THIS element — nearest instance boundary, else the global.
15
+ *
16
+ * Cached rather than resolved per call, because this element does not only
17
+ * READ it: it holds a `subscribe()` registration keyed on the object. That
18
+ * cache is only correct if something tells us when the boundary changes, and
19
+ * under all four wrappers it does change after we connect —
20
+ * `AparteChatHost.bind()` runs `attachConfig` from a post-mount hook. Hence
21
+ * {@link AparteConfigAware}; without it this element served the global config's
22
+ * providers to a chat that was given its own.
23
+ */
24
+ _cfg = aparteGlobalConfig;
16
25
  _configUnsubscribe = null;
17
26
  // Bound handlers for cleanup
18
27
  _boundHandleChange = this._handleChange.bind(this);
@@ -36,6 +45,11 @@ class AparteModelSelector extends HTMLElement {
36
45
  }
37
46
  await this._loadAllProviderModels();
38
47
  this._render();
48
+ this._subscribeToConfig();
49
+ }
50
+ /** Listen for configuration changes (e.g. from an onboarding flow). */
51
+ _subscribeToConfig() {
52
+ this._configUnsubscribe?.();
39
53
  this._configUnsubscribe = this._cfg.subscribe(() => {
40
54
  void (async () => {
41
55
  const cfg = this._cfg.getModelConfig();
@@ -53,6 +67,35 @@ class AparteModelSelector extends HTMLElement {
53
67
  this.removeEventListener("aparte-optgroup-toggle", this._handleOptgroupToggle);
54
68
  this._configUnsubscribe?.();
55
69
  }
70
+ /**
71
+ * The boundary above us appeared or changed: move the subscription and reload,
72
+ * because a different config means different providers and a different
73
+ * selection.
74
+ *
75
+ * See {@link AparteConfigAware}. The element's own per-instance tests passed
76
+ * before this existed only because they mount the boundary FIRST — the one
77
+ * ordering no wrapper produces.
78
+ */
79
+ aparteConfigChanged(next) {
80
+ this._configUnsubscribe?.();
81
+ this._configUnsubscribe = null;
82
+ this._cfg = next;
83
+ if (!this.isConnected) return;
84
+ void (async () => {
85
+ this._isLoading = false;
86
+ this._providerModels = [];
87
+ const model = this._cfg.getModelConfig();
88
+ if (model.defaultProvider && model.defaultModel) {
89
+ this._currentProviderId = model.defaultProvider;
90
+ this._currentModelId = model.defaultModel;
91
+ } else {
92
+ this._loadPersistedSelection();
93
+ }
94
+ await this._loadAllProviderModels();
95
+ this._render();
96
+ this._subscribeToConfig();
97
+ })();
98
+ }
56
99
  attributeChangedCallback() {
57
100
  if (this.isConnected && !this._isLoading) {
58
101
  this._render();
@@ -89,20 +132,20 @@ class AparteModelSelector extends HTMLElement {
89
132
  const providers = this._cfg.getAIProviders();
90
133
  const uniqueProviders = Array.from(new Map(providers.map((p) => [p.id, p])).values());
91
134
  const config = this._cfg.getModelConfig();
92
- const tempList = [];
93
- await Promise.all(uniqueProviders.map(async (provider) => {
135
+ const tempList = new Array(uniqueProviders.length);
136
+ await Promise.all(uniqueProviders.map(async (provider, i) => {
94
137
  try {
95
138
  const models = await this._cfg.refreshProviderModels(provider.id);
96
139
  const filters = config.modelFilters?.[provider.id];
97
140
  const filteredModels = filters?.length ? models.filter((m) => filters.includes(m.id)) : models;
98
141
  if (filteredModels.length > 0) {
99
- tempList.push({ provider, models: filteredModels });
142
+ tempList[i] = { provider, models: filteredModels };
100
143
  }
101
144
  } catch (error) {
102
145
  console.warn(`[AparteModelSelector] Failed to load models for ${provider.id}:`, error);
103
146
  }
104
147
  }));
105
- this._providerModels = tempList;
148
+ this._providerModels = tempList.filter((pm) => pm !== void 0);
106
149
  } finally {
107
150
  this._isLoading = false;
108
151
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/aparte-model-selector.ts"],"sourcesContent":["/**\n * Aparté Model Selector\n *\n * Web Component for selecting an AI provider + model. Renders an `aparte-select`\n * primitive with providers grouped into `aparte-optgroup`s (a single provider\n * renders a flat list). Enables the BYOK (Bring Your Own Key) pattern.\n *\n * @element aparte-model-selector\n * @attr {boolean} auto-select - Auto-select the first model (default: false)\n * @attr {boolean} persist - Persist the selection through the config\n * @attr {boolean} searchable - Enable search in the dropdown\n * @attr {string} placeholder - Override the placeholder text\n *\n * @fires aparte-model-change - Fired when the selected model changes\n */\n\nimport {\n AparteConfig,\n resolveConfig,\n type AparteConfigClass,\n AparteSelect,\n type AparteOptgroup,\n type AparteAIProvider,\n type AparteAIModel,\n type AparteModelChangeEventDetail,\n type AparteSelectChangeDetail,\n} from '@aparte/core';\n\ninterface ProviderModels {\n provider: AparteAIProvider;\n models: AparteAIModel[];\n}\n\n/**\n * Escape a value before it is interpolated into an HTML string (text node or\n * double-quoted attribute). Model names/ids come from a remote `/models`\n * endpoint and provider labels from consumer config — both hostile-by-default,\n * and both flow through `innerHTML` when the option list is (re)built.\n */\nfunction esc(value: unknown): string {\n return String(value ?? '')\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\nexport class AparteModelSelector extends HTMLElement {\n private _currentProviderId: string | null = null;\n private _currentModelId: string | null = null;\n private _providerModels: ProviderModels[] = [];\n private _aparteSelect: AparteSelect | null = null;\n private _isLoading = false;\n private _isRendering = false;\n private _expandedGroups: Set<string> = new Set();\n\n /** Config governing THIS element — nearest instance boundary, else the global.\n Resolved on connect (multi-instance pages get their own config). */\n private _cfg: AparteConfigClass = AparteConfig;\n\n private _configUnsubscribe: (() => void) | null = null;\n\n // Bound handlers for cleanup\n private _boundHandleChange = this._handleChange.bind(this);\n private _handleOptgroupToggle = (e: Event): void => { void this._onOptgroupToggle(e); };\n\n static get observedAttributes(): string[] {\n return ['persist', 'auto-select', 'searchable', 'placeholder'];\n }\n\n async connectedCallback(): Promise<void> {\n this._cfg = resolveConfig(this);\n // Reset loading flag so a fresh mount always does a full load\n // (prevents a stale _isLoading=true from a previous mount blocking this one)\n this._isLoading = false;\n this._providerModels = [];\n\n this._setupEventListeners();\n\n // Seed selection from the resolved config first (handles race conditions)\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n } else {\n this._loadPersistedSelection();\n }\n\n await this._loadAllProviderModels();\n this._render();\n\n // Listen for configuration changes (e.g. from an onboarding flow)\n this._configUnsubscribe = this._cfg.subscribe(() => {\n void (async () => {\n const cfg = this._cfg.getModelConfig();\n\n // Guard: only react if the model config actually changed relative to our state\n if (cfg.defaultProvider === this._currentProviderId &&\n cfg.defaultModel === this._currentModelId &&\n this._providerModels.length > 0) {\n return;\n }\n\n this._loadPersistedSelection();\n await this._loadAllProviderModels();\n this._render();\n })();\n });\n }\n\n disconnectedCallback(): void {\n this._aparteSelect?.removeEventListener('aparte-select-change', this._boundHandleChange);\n this.removeEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n this._configUnsubscribe?.();\n }\n\n attributeChangedCallback(): void {\n if (this.isConnected && !this._isLoading) {\n this._render();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Current provider ID. */\n get providerId(): string | null {\n return this._currentProviderId;\n }\n\n /** Current model ID. */\n get modelId(): string | null {\n return this._currentModelId;\n }\n\n /** Programmatically set the selection. */\n setSelection(providerId: string, modelId: string): void {\n this._currentProviderId = providerId;\n this._currentModelId = modelId;\n\n const compositeValue = `${providerId}::${modelId}`;\n if (this._aparteSelect) {\n this._aparteSelect.value = compositeValue;\n }\n\n this._emitChange();\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Data Loading\n // ─────────────────────────────────────────────────────────────────────────\n\n private async _loadAllProviderModels(): Promise<void> {\n if (this._isLoading) return;\n this._isLoading = true;\n\n try {\n const providers = this._cfg.getAIProviders();\n\n // Deduplicate providers by ID just in case\n const uniqueProviders = Array.from(new Map(providers.map(p => [p.id, p])).values());\n\n const config = this._cfg.getModelConfig();\n\n // Store in a temporary list to avoid mid-render state corruption\n const tempList: ProviderModels[] = [];\n\n // Fetch all providers in parallel\n await Promise.all(uniqueProviders.map(async (provider) => {\n try {\n const models = await this._cfg.refreshProviderModels(provider.id);\n\n // Apply model filters if configured\n const filters = config.modelFilters?.[provider.id];\n const filteredModels = filters?.length\n ? models.filter(m => filters.includes(m.id))\n : models;\n\n if (filteredModels.length > 0) {\n tempList.push({ provider, models: filteredModels });\n }\n } catch (error) {\n console.warn(`[AparteModelSelector] Failed to load models for ${provider.id}:`, error);\n }\n }));\n\n this._providerModels = tempList;\n } finally {\n this._isLoading = false;\n }\n }\n\n private async _onOptgroupToggle(e: Event): Promise<void> {\n const detail = (e as CustomEvent).detail;\n\n // Track expansion state\n if (detail.collapsed) {\n this._expandedGroups.delete(detail.label);\n return;\n }\n\n this._expandedGroups.add(detail.label);\n\n // Find which provider this optgroup belongs to\n const pm = this._providerModels.find(p => p.provider.getMetadata().name === detail.label);\n if (!pm || !pm.provider.fetchModels) return;\n\n // Show a loading state on the optgroup while we lazily fetch its models\n const groupEl = e.target as HTMLElement;\n if (groupEl && groupEl.tagName === 'APARTE-OPTGROUP') {\n (groupEl as AparteOptgroup).loading = true;\n }\n\n try {\n // Retrieve latest models via Config (provider-agnostic)\n const freshModels = await this._cfg.refreshProviderModels(pm.provider.id);\n\n if (freshModels && freshModels.length > 0) {\n pm.models = freshModels;\n this._render(true);\n }\n } catch (error) {\n console.error(`[AparteModelSelector] Lazy fetch failed for ${pm.provider.id}:`, error);\n } finally {\n const el = e.target as HTMLElement;\n if (el && el.tagName === 'APARTE-OPTGROUP') {\n (el as AparteOptgroup).loading = false;\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Rendering\n // ─────────────────────────────────────────────────────────────────────────\n\n private _render(keepOpen = false): void {\n if (this._isRendering) return;\n this._isRendering = true;\n\n try {\n this._renderImpl(keepOpen);\n } finally {\n this._isRendering = false;\n }\n }\n\n private _renderImpl(keepOpen: boolean): void {\n // Attribute overrides locale\n const localePlaceholder = this._cfg.getLocale()['modelSelectorPlaceholder'] || 'Select a model...';\n const placeholder = this.getAttribute('placeholder') || localePlaceholder;\n const searchable = this.hasAttribute('searchable');\n const autoSelect = this.hasAttribute('auto-select');\n\n // Capture current open state of aparte-select if it exists\n const wasOpen = keepOpen || this._aparteSelect?.hasAttribute('open');\n\n // Determine current value\n let currentValue = this._currentModelId ? `${this._currentProviderId}::${this._currentModelId}` : '';\n\n if (!currentValue && autoSelect) {\n const first = this._providerModels[0];\n const firstModel = first?.models[0];\n if (first && firstModel) {\n this._currentProviderId = first.provider.id;\n this._currentModelId = firstModel.id;\n currentValue = `${this._currentProviderId}::${this._currentModelId}`;\n // Emit the initial change once the element settles\n setTimeout(() => this._emitChange(), 0);\n }\n }\n\n // Build options HTML — single provider: flat list; multiple: grouped optgroups\n const only = this._providerModels[0];\n const singleProvider = this._providerModels.length === 1;\n const optionsHtml = singleProvider && only\n ? only.models.map(m => {\n const key = `${only.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}</aparte-option>`;\n }).join('')\n : this._providerModels.map(pm => {\n const label = pm.provider.getMetadata().name;\n const isCollapsed = !this._expandedGroups.has(label);\n return `\n <aparte-optgroup\n label=\"${esc(label)}\"\n collapsible\n ${isCollapsed ? 'collapsed' : ''}\n >\n ${pm.models.map(m => {\n const key = `${pm.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}</aparte-option>`;\n }).join('')}\n </aparte-optgroup>\n `;\n }).join('');\n\n if (this._aparteSelect) {\n // Non-destructive update of attributes\n this._aparteSelect.setAttribute('placeholder', placeholder);\n if (currentValue) this._aparteSelect.setAttribute('value', currentValue);\n else this._aparteSelect.removeAttribute('value');\n\n if (searchable) this._aparteSelect.setAttribute('searchable', '');\n else this._aparteSelect.removeAttribute('searchable');\n\n if (wasOpen) this._aparteSelect.setAttribute('open', '');\n else this._aparteSelect.removeAttribute('open');\n\n if (!singleProvider) this._aparteSelect.setAttribute('grouped', '');\n else this._aparteSelect.removeAttribute('grouped');\n\n // Update children cleanly: prefer direct container update if available\n const optionsContainer = this._aparteSelect.querySelector('.aparte-select-options');\n if (optionsContainer) {\n // Direct update (faster, skips the observer)\n optionsContainer.innerHTML = optionsHtml;\n\n // Force label re-evaluation: remove then re-set value so aparte-select\n // re-scans the newly injected options (setAttribute guard blocks same-value updates)\n if (currentValue) {\n this._aparteSelect.removeAttribute('value');\n this._aparteSelect.setAttribute('value', currentValue);\n }\n } else {\n // Fallback: self-healing mode\n const temp = document.createElement('div');\n temp.innerHTML = optionsHtml;\n const newChildren = Array.from(temp.children);\n\n const childrenToRemove = Array.from(this._aparteSelect.children).filter(c =>\n c.tagName === 'APARTE-OPTION' || c.tagName === 'APARTE-OPTGROUP'\n );\n childrenToRemove.forEach(c => c.remove());\n\n newChildren.forEach(c => this._aparteSelect?.appendChild(c));\n }\n\n // Critical: re-apply value after a microtask so AparteSelect has processed\n // the new options and can resolve the label.\n if (currentValue) {\n setTimeout(() => {\n if (this._aparteSelect) {\n this._aparteSelect.value = currentValue;\n }\n }, 0);\n }\n } else {\n // Full initial render\n this.innerHTML = `\n <aparte-select\n class=\"aparte-model-selector-select\"\n placeholder=\"${esc(placeholder)}\"\n ${currentValue ? `value=\"${esc(currentValue)}\"` : ''}\n ${searchable ? 'searchable' : ''}\n ${wasOpen ? 'open' : ''}\n ${!singleProvider ? 'grouped' : ''}\n >\n ${optionsHtml}\n </aparte-select>\n `;\n this._aparteSelect = this.querySelector('aparte-select');\n this._setupEventListeners();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Event Handling\n // ─────────────────────────────────────────────────────────────────────────\n\n private _setupEventListeners(): void {\n this._aparteSelect?.addEventListener('aparte-select-change', this._boundHandleChange);\n this.addEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n }\n\n private _handleChange(e: Event): void {\n const detail = (e as CustomEvent<AparteSelectChangeDetail>).detail;\n const previousProviderId = this._currentProviderId;\n const previousModelId = this._currentModelId;\n\n // Parse composite value \"providerId::modelId\"\n const [providerId, modelId] = detail.value.split('::');\n\n this._currentProviderId = providerId || null;\n this._currentModelId = modelId || null;\n\n this._emitChange(previousProviderId, previousModelId);\n }\n\n private _emitChange(previousProviderId?: string | null, previousModelId?: string | null): void {\n const detail: AparteModelChangeEventDetail = {\n providerId: this._currentProviderId || undefined,\n modelId: this._currentModelId || '',\n previousProviderId: previousProviderId || undefined,\n previousModelId: previousModelId || undefined,\n };\n\n this.dispatchEvent(new CustomEvent<AparteModelChangeEventDetail>('aparte-model-change', {\n bubbles: true,\n composed: true,\n detail,\n }));\n\n // Persist if enabled\n if (this.hasAttribute('persist')) {\n this._persistSelection();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Persistence (opt-in)\n // ─────────────────────────────────────────────────────────────────────────\n\n private _persistSelection(): void {\n if (!this._currentProviderId || !this._currentModelId) return;\n\n // Sync with the config — auto-save is handled by its setModelConfig()\n // if a AparteModelPreferenceProvider has been registered by the host app.\n const currentConfig = this._cfg.getModelConfig();\n if (currentConfig.defaultProvider === this._currentProviderId &&\n currentConfig.defaultModel === this._currentModelId) {\n return;\n }\n\n this._cfg.setModelConfig({\n defaultProvider: this._currentProviderId,\n defaultModel: this._currentModelId,\n });\n }\n\n private _loadPersistedSelection(): void {\n // The resolved config is the single source of truth. The host app is\n // responsible for restoring preferences into it (via restoreModelPreference())\n // before this component mounts.\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n }\n // else: no-op — the config is empty\n }\n}\n\n// Register the custom element\nif (!customElements.get('aparte-model-selector')) {\n customElements.define('aparte-model-selector', AparteModelSelector);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-model-selector': AparteModelSelector;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/aparte-model-selector.ts"],"sourcesContent":["/**\n * Aparté Model Selector\n *\n * Web Component for selecting an AI provider + model. Renders an `aparte-select`\n * primitive with providers grouped into `aparte-optgroup`s (a single provider\n * renders a flat list). Enables the BYOK (Bring Your Own Key) pattern.\n *\n * @element aparte-model-selector\n * @attr {boolean} auto-select - Auto-select the first model (default: false)\n * @attr {boolean} persist - Persist the selection through the config\n * @attr {boolean} searchable - Enable search in the dropdown\n * @attr {string} placeholder - Override the placeholder text\n *\n * @fires aparte-model-change - Fired when the selected model changes\n */\n\nimport {\n aparteGlobalConfig,\n escapeAttr,\n resolveConfig,\n type AparteConfig,\n type AparteConfigAware,\n AparteSelect,\n type AparteOptgroup,\n type AparteAIProvider,\n type AparteAIModel,\n type AparteModelChangeEventDetail,\n type AparteSelectChangeDetail,\n} from '@aparte/core';\n\ninterface ProviderModels {\n provider: AparteAIProvider;\n models: AparteAIModel[];\n}\n\n/**\n * Escape a value before it is interpolated into an HTML string (text node or\n * double-quoted attribute). Model names/ids come from a remote `/models`\n * endpoint and provider labels from consumer config — both hostile-by-default,\n * and both flow through `innerHTML` when the option list is (re)built.\n */\nfunction esc(value: unknown): string {\n // Coerce, then delegate: core owns the escaping. This local copy used to\n // inline it and, like two of its eight siblings across the scope, left the\n // apostrophe through — enough to break out of a single-quoted attribute.\n return escapeAttr(String(value ?? ''));\n}\n\nexport class AparteModelSelector extends HTMLElement implements AparteConfigAware {\n private _currentProviderId: string | null = null;\n private _currentModelId: string | null = null;\n private _providerModels: ProviderModels[] = [];\n private _aparteSelect: AparteSelect | null = null;\n private _isLoading = false;\n private _isRendering = false;\n private _expandedGroups: Set<string> = new Set();\n\n /**\n * Config governing THIS element — nearest instance boundary, else the global.\n *\n * Cached rather than resolved per call, because this element does not only\n * READ it: it holds a `subscribe()` registration keyed on the object. That\n * cache is only correct if something tells us when the boundary changes, and\n * under all four wrappers it does change after we connect —\n * `AparteChatHost.bind()` runs `attachConfig` from a post-mount hook. Hence\n * {@link AparteConfigAware}; without it this element served the global config's\n * providers to a chat that was given its own.\n */\n private _cfg: AparteConfig = aparteGlobalConfig;\n\n private _configUnsubscribe: (() => void) | null = null;\n\n // Bound handlers for cleanup\n private _boundHandleChange = this._handleChange.bind(this);\n private _handleOptgroupToggle = (e: Event): void => { void this._onOptgroupToggle(e); };\n\n static get observedAttributes(): string[] {\n return ['persist', 'auto-select', 'searchable', 'placeholder'];\n }\n\n async connectedCallback(): Promise<void> {\n this._cfg = resolveConfig(this);\n // Reset loading flag so a fresh mount always does a full load\n // (prevents a stale _isLoading=true from a previous mount blocking this one)\n this._isLoading = false;\n this._providerModels = [];\n\n this._setupEventListeners();\n\n // Seed selection from the resolved config first (handles race conditions)\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n } else {\n this._loadPersistedSelection();\n }\n\n await this._loadAllProviderModels();\n this._render();\n\n this._subscribeToConfig();\n }\n\n /** Listen for configuration changes (e.g. from an onboarding flow). */\n private _subscribeToConfig(): void {\n this._configUnsubscribe?.();\n this._configUnsubscribe = this._cfg.subscribe(() => {\n void (async () => {\n const cfg = this._cfg.getModelConfig();\n\n // Guard: only react if the model config actually changed relative to our state\n if (cfg.defaultProvider === this._currentProviderId &&\n cfg.defaultModel === this._currentModelId &&\n this._providerModels.length > 0) {\n return;\n }\n\n this._loadPersistedSelection();\n await this._loadAllProviderModels();\n this._render();\n })();\n });\n }\n\n disconnectedCallback(): void {\n this._aparteSelect?.removeEventListener('aparte-select-change', this._boundHandleChange);\n this.removeEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n this._configUnsubscribe?.();\n }\n\n /**\n * The boundary above us appeared or changed: move the subscription and reload,\n * because a different config means different providers and a different\n * selection.\n *\n * See {@link AparteConfigAware}. The element's own per-instance tests passed\n * before this existed only because they mount the boundary FIRST — the one\n * ordering no wrapper produces.\n */\n aparteConfigChanged(next: AparteConfig): void {\n this._configUnsubscribe?.();\n this._configUnsubscribe = null;\n this._cfg = next;\n if (!this.isConnected) return;\n void (async () => {\n this._isLoading = false;\n this._providerModels = [];\n const model = this._cfg.getModelConfig();\n if (model.defaultProvider && model.defaultModel) {\n this._currentProviderId = model.defaultProvider;\n this._currentModelId = model.defaultModel;\n } else {\n this._loadPersistedSelection();\n }\n await this._loadAllProviderModels();\n this._render();\n this._subscribeToConfig();\n })();\n }\n\n attributeChangedCallback(): void {\n if (this.isConnected && !this._isLoading) {\n this._render();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Current provider ID. */\n get providerId(): string | null {\n return this._currentProviderId;\n }\n\n /** Current model ID. */\n get modelId(): string | null {\n return this._currentModelId;\n }\n\n /** Programmatically set the selection. */\n setSelection(providerId: string, modelId: string): void {\n this._currentProviderId = providerId;\n this._currentModelId = modelId;\n\n const compositeValue = `${providerId}::${modelId}`;\n if (this._aparteSelect) {\n this._aparteSelect.value = compositeValue;\n }\n\n this._emitChange();\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Data Loading\n // ─────────────────────────────────────────────────────────────────────────\n\n private async _loadAllProviderModels(): Promise<void> {\n if (this._isLoading) return;\n this._isLoading = true;\n\n try {\n const providers = this._cfg.getAIProviders();\n\n // Deduplicate providers by ID just in case\n const uniqueProviders = Array.from(new Map(providers.map(p => [p.id, p])).values());\n\n const config = this._cfg.getModelConfig();\n\n // Store in a temporary list to avoid mid-render state corruption.\n //\n // Indexed by REGISTRATION order, not filled by completion order. The\n // fetches run in parallel, so a `push` here ordered the dropdown — and\n // therefore what `auto-select` picks — by whichever /models endpoint\n // answered first. A cloud provider on a CDN beats a local server that\n // has to wake up, so an app registering `[OLLAMA, LMSTUDIO, OPENROUTER]`\n // could silently land on the paid one, and land on a different one on\n // the next reload. `auto-select` documents itself as \"the first model\";\n // first has to mean first, not fastest.\n const tempList: (ProviderModels | undefined)[] = new Array(uniqueProviders.length);\n\n // Fetch all providers in parallel\n await Promise.all(uniqueProviders.map(async (provider, i) => {\n try {\n const models = await this._cfg.refreshProviderModels(provider.id);\n\n // Apply model filters if configured\n const filters = config.modelFilters?.[provider.id];\n const filteredModels = filters?.length\n ? models.filter(m => filters.includes(m.id))\n : models;\n\n if (filteredModels.length > 0) {\n tempList[i] = { provider, models: filteredModels };\n }\n } catch (error) {\n console.warn(`[AparteModelSelector] Failed to load models for ${provider.id}:`, error);\n }\n }));\n\n // The holes are the providers that failed or returned nothing: dropped,\n // which keeps the order of the survivors.\n this._providerModels = tempList.filter((pm): pm is ProviderModels => pm !== undefined);\n } finally {\n this._isLoading = false;\n }\n }\n\n private async _onOptgroupToggle(e: Event): Promise<void> {\n const detail = (e as CustomEvent).detail;\n\n // Track expansion state\n if (detail.collapsed) {\n this._expandedGroups.delete(detail.label);\n return;\n }\n\n this._expandedGroups.add(detail.label);\n\n // Find which provider this optgroup belongs to\n const pm = this._providerModels.find(p => p.provider.getMetadata().name === detail.label);\n if (!pm || !pm.provider.fetchModels) return;\n\n // Show a loading state on the optgroup while we lazily fetch its models\n const groupEl = e.target as HTMLElement;\n if (groupEl && groupEl.tagName === 'APARTE-OPTGROUP') {\n (groupEl as AparteOptgroup).loading = true;\n }\n\n try {\n // Retrieve latest models via Config (provider-agnostic)\n const freshModels = await this._cfg.refreshProviderModels(pm.provider.id);\n\n if (freshModels && freshModels.length > 0) {\n pm.models = freshModels;\n this._render(true);\n }\n } catch (error) {\n console.error(`[AparteModelSelector] Lazy fetch failed for ${pm.provider.id}:`, error);\n } finally {\n const el = e.target as HTMLElement;\n if (el && el.tagName === 'APARTE-OPTGROUP') {\n (el as AparteOptgroup).loading = false;\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Rendering\n // ─────────────────────────────────────────────────────────────────────────\n\n private _render(keepOpen = false): void {\n if (this._isRendering) return;\n this._isRendering = true;\n\n try {\n this._renderImpl(keepOpen);\n } finally {\n this._isRendering = false;\n }\n }\n\n private _renderImpl(keepOpen: boolean): void {\n // Attribute overrides locale\n const localePlaceholder = this._cfg.getLocale()['modelSelectorPlaceholder'] || 'Select a model...';\n const placeholder = this.getAttribute('placeholder') || localePlaceholder;\n const searchable = this.hasAttribute('searchable');\n const autoSelect = this.hasAttribute('auto-select');\n\n // Capture current open state of aparte-select if it exists\n const wasOpen = keepOpen || this._aparteSelect?.hasAttribute('open');\n\n // Determine current value\n let currentValue = this._currentModelId ? `${this._currentProviderId}::${this._currentModelId}` : '';\n\n if (!currentValue && autoSelect) {\n const first = this._providerModels[0];\n const firstModel = first?.models[0];\n if (first && firstModel) {\n this._currentProviderId = first.provider.id;\n this._currentModelId = firstModel.id;\n currentValue = `${this._currentProviderId}::${this._currentModelId}`;\n // Emit the initial change once the element settles\n setTimeout(() => this._emitChange(), 0);\n }\n }\n\n // Build options HTML — single provider: flat list; multiple: grouped optgroups\n const only = this._providerModels[0];\n const singleProvider = this._providerModels.length === 1;\n const optionsHtml = singleProvider && only // safe-text: every value below goes through esc(); the tags are this file's own\n ? only.models.map(m => {\n const key = `${only.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}</aparte-option>`;\n }).join('')\n : this._providerModels.map(pm => {\n const label = pm.provider.getMetadata().name;\n const isCollapsed = !this._expandedGroups.has(label);\n return `\n <aparte-optgroup\n label=\"${esc(label)}\"\n collapsible\n ${isCollapsed ? 'collapsed' : ''}\n >\n ${pm.models.map(m => {\n const key = `${pm.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}</aparte-option>`;\n }).join('')}\n </aparte-optgroup>\n `;\n }).join('');\n\n if (this._aparteSelect) {\n // Non-destructive update of attributes\n this._aparteSelect.setAttribute('placeholder', placeholder);\n if (currentValue) this._aparteSelect.setAttribute('value', currentValue);\n else this._aparteSelect.removeAttribute('value');\n\n if (searchable) this._aparteSelect.setAttribute('searchable', '');\n else this._aparteSelect.removeAttribute('searchable');\n\n if (wasOpen) this._aparteSelect.setAttribute('open', '');\n else this._aparteSelect.removeAttribute('open');\n\n if (!singleProvider) this._aparteSelect.setAttribute('grouped', '');\n else this._aparteSelect.removeAttribute('grouped');\n\n // Update children cleanly: prefer direct container update if available\n const optionsContainer = this._aparteSelect.querySelector('.aparte-select-options');\n if (optionsContainer) {\n // Direct update (faster, skips the observer)\n optionsContainer.innerHTML = optionsHtml;\n\n // Force label re-evaluation: remove then re-set value so aparte-select\n // re-scans the newly injected options (setAttribute guard blocks same-value updates)\n if (currentValue) {\n this._aparteSelect.removeAttribute('value');\n this._aparteSelect.setAttribute('value', currentValue);\n }\n } else {\n // Fallback: self-healing mode\n const temp = document.createElement('div');\n temp.innerHTML = optionsHtml;\n const newChildren = Array.from(temp.children);\n\n const childrenToRemove = Array.from(this._aparteSelect.children).filter(c =>\n c.tagName === 'APARTE-OPTION' || c.tagName === 'APARTE-OPTGROUP'\n );\n childrenToRemove.forEach(c => c.remove());\n\n newChildren.forEach(c => this._aparteSelect?.appendChild(c));\n }\n\n // Critical: re-apply value after a microtask so AparteSelect has processed\n // the new options and can resolve the label.\n if (currentValue) {\n setTimeout(() => {\n if (this._aparteSelect) {\n this._aparteSelect.value = currentValue;\n }\n }, 0);\n }\n } else {\n // Full initial render\n this.innerHTML = `\n <aparte-select\n class=\"aparte-model-selector-select\"\n placeholder=\"${esc(placeholder)}\"\n ${currentValue ? `value=\"${esc(currentValue)}\"` : ''}\n ${searchable ? 'searchable' : ''}\n ${wasOpen ? 'open' : ''}\n ${!singleProvider ? 'grouped' : ''}\n >\n ${optionsHtml}\n </aparte-select>\n `;\n this._aparteSelect = this.querySelector('aparte-select');\n this._setupEventListeners();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Event Handling\n // ─────────────────────────────────────────────────────────────────────────\n\n private _setupEventListeners(): void {\n this._aparteSelect?.addEventListener('aparte-select-change', this._boundHandleChange);\n this.addEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n }\n\n private _handleChange(e: Event): void {\n const detail = (e as CustomEvent<AparteSelectChangeDetail>).detail;\n const previousProviderId = this._currentProviderId;\n const previousModelId = this._currentModelId;\n\n // Parse composite value \"providerId::modelId\"\n const [providerId, modelId] = detail.value.split('::');\n\n this._currentProviderId = providerId || null;\n this._currentModelId = modelId || null;\n\n this._emitChange(previousProviderId, previousModelId);\n }\n\n private _emitChange(previousProviderId?: string | null, previousModelId?: string | null): void {\n const detail: AparteModelChangeEventDetail = {\n providerId: this._currentProviderId || undefined,\n modelId: this._currentModelId || '',\n previousProviderId: previousProviderId || undefined,\n previousModelId: previousModelId || undefined,\n };\n\n this.dispatchEvent(new CustomEvent<AparteModelChangeEventDetail>('aparte-model-change', {\n bubbles: true,\n composed: true,\n detail,\n }));\n\n // Persist if enabled\n if (this.hasAttribute('persist')) {\n this._persistSelection();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Persistence (opt-in)\n // ─────────────────────────────────────────────────────────────────────────\n\n private _persistSelection(): void {\n if (!this._currentProviderId || !this._currentModelId) return;\n\n // Sync with the config — auto-save is handled by its setModelConfig()\n // if a AparteModelPreferenceProvider has been registered by the host app.\n const currentConfig = this._cfg.getModelConfig();\n if (currentConfig.defaultProvider === this._currentProviderId &&\n currentConfig.defaultModel === this._currentModelId) {\n return;\n }\n\n this._cfg.setModelConfig({\n defaultProvider: this._currentProviderId,\n defaultModel: this._currentModelId,\n });\n }\n\n private _loadPersistedSelection(): void {\n // The resolved config is the single source of truth. The host app is\n // responsible for restoring preferences into it (via restoreModelPreference())\n // before this component mounts.\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n }\n // else: no-op — the config is empty\n }\n}\n\n// Register the custom element\nif (!customElements.get('aparte-model-selector')) {\n customElements.define('aparte-model-selector', AparteModelSelector);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-model-selector': AparteModelSelector;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `@aparte/plugin-model-selector` — the DOM-free entry, for Node and SSR.
3
+ *
4
+ * Why this file exists: the browser barrel exports `AparteModelSelector`, and that
5
+ * module does `class AparteModelSelector extends HTMLElement` at MODULE SCOPE. So
6
+ * `import '@aparte/plugin-model-selector'` threw
7
+ *
8
+ * ReferenceError: HTMLElement is not defined
9
+ *
10
+ * in plain Node — and therefore in any Next / Nuxt / SvelteKit / Analog build that
11
+ * evaluates the import on the server. `@aparte/core` builds exactly this kind of
12
+ * entry and has a gate asserting it stays DOM-free; the plugins were outside that
13
+ * gate.
14
+ *
15
+ * This package is a custom element and nothing else, so the DOM-free surface is
16
+ * types only. That is the honest answer rather than a stub class: on the server
17
+ * there is no element to define, and a fake one would let a mistake through to the
18
+ * browser. A consumer importing the element type for a ref keeps working; a
19
+ * consumer importing the class on the server gets a clear error naming THIS package
20
+ * instead of `@aparte/core`.
21
+ */
22
+ export type { AparteAIProvider, AparteAIModel, AparteModelConfig, AparteModelChangeEventDetail, } from '@aparte/core';
23
+ //# sourceMappingURL=index.node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.node.d.ts","sourceRoot":"","sources":["../src/index.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,YAAY,EACR,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,4BAA4B,GAC/B,MAAM,cAAc,CAAC"}
@@ -0,0 +1,2 @@
1
+
2
+ //# sourceMappingURL=index.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.node.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/plugin-model-selector",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Provider + model selector web component (<aparte-model-selector>) for aparté — grouped dropdown, BYOK.",
5
5
  "type": "module",
6
6
  "sideEffects": true,
@@ -10,6 +10,10 @@
10
10
  "exports": {
11
11
  ".": {
12
12
  "@aparte-workspace/source": "./src/index.ts",
13
+ "node": {
14
+ "types": "./dist/index.node.d.ts",
15
+ "default": "./dist/index.node.js"
16
+ },
13
17
  "types": "./dist/index.d.ts",
14
18
  "import": "./dist/index.js",
15
19
  "default": "./dist/index.js"
@@ -24,13 +28,12 @@
24
28
  "node": ">=18"
25
29
  },
26
30
  "peerDependencies": {
27
- "@aparte/core": ">=0.5.0-alpha.0 <1.0.0"
31
+ "@aparte/core": ">=0.7.0 <1.0.0"
28
32
  },
29
33
  "devDependencies": {
30
34
  "typescript": "^5.4.0",
31
35
  "vite": "^6.0.0",
32
- "vite-plugin-dts": "^4.5.4",
33
- "@aparte/core": "0.7.0"
36
+ "@aparte/core": "0.8.0"
34
37
  },
35
38
  "keywords": [
36
39
  "aparte",