@lenne.tech/nuxt-extensions 1.12.0 → 1.13.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/CLAUDE.md CHANGED
@@ -76,6 +76,10 @@ export default defineNuxtConfig({
76
76
  enabled: true,
77
77
  basePath: '/ai', // Must match the nest-server AI controller
78
78
  },
79
+ formLabelAssociation: {
80
+ enabled: true, // repair <label for> broken by an SSR/client useId() divergence
81
+ maxRepairMs: 1500, // how long to keep repairing deferred subtrees
82
+ },
79
83
  preHydrationInput: {
80
84
  enabled: true, // keep text typed before hydration instead of losing it
81
85
  maxRestoreMs: 1500, // how long to keep repairing deferred subtrees
@@ -175,6 +179,32 @@ export default defineNuxtRouteMiddleware((to) => {
175
179
  });
176
180
  ```
177
181
 
182
+ ## Form Label Repair
183
+
184
+ **The defect:** Nuxt UI's `FormField` derives the label's `for` and the control's `id` from one
185
+ `useId()` call. When `useId()` diverges between server and client, the control's `id` is
186
+ force-patched on hydration (vuejs/core#9083) while the label's `for` — a prop on reka-ui's
187
+ `Label` component — is not. The label keeps the stale value and the association breaks.
188
+
189
+ **What this module adds:** `runtime/plugins/form-label-association.client.ts` re-points
190
+ dangling Nuxt UI field labels at the control in their own field, at `app:mounted` and for a
191
+ bounded window afterwards (`maxRepairMs`, default 1500 ms).
192
+
193
+ **What this means when you debug a consuming project:**
194
+
195
+ - **A `for` attribute that changes after mount is this plugin, not a bug.** It logs a
196
+ `console.warn` in development naming how many labels it repaired.
197
+ - **It refuses to guess.** The repair only fires when the field holds exactly one eligible
198
+ control. Radio and checkbox groups, hidden form-value proxies, `aria-hidden`/`disabled`
199
+ controls and button-only fields are skipped on purpose — binding a group caption to its
200
+ first option would submit a value the user never chose.
201
+ - **Only Nuxt UI's own field labels are touched** (`label[data-slot="label"][for]`). Labels
202
+ your application wrote are never rewritten.
203
+ - **Do not "fix" a skipped group by loosening the rule.** Inert beats wrong here; the correct
204
+ fix for a group is an SSR-stable id at the source.
205
+ - This is a **stopgap** for a framework-level divergence. Opt out with
206
+ `ltExtensions.formLabelAssociation.enabled: false`.
207
+
178
208
  ## Pre-Hydration Input Preservation
179
209
 
180
210
  **The defect:** until Vue hydrates a server-rendered `<input>`, it carries no framework
package/README.md CHANGED
@@ -42,6 +42,7 @@ npm install tus-js-client
42
42
  - **i18n Support** - English and German translations (works without i18n too)
43
43
  - **Auto-imports** - All composables and components are auto-imported
44
44
  - **Pre-hydration input preservation** - text typed before hydration is kept, not erased
45
+ - **Form label repair** - restores `<label for>` associations that hydration breaks
45
46
 
46
47
  ## Environment Variables
47
48
 
@@ -157,6 +158,77 @@ export default defineNuxtConfig({
157
158
  });
158
159
  ```
159
160
 
161
+ ## Form Label Repair
162
+
163
+ Restores `<label for>` associations that hydration breaks. Active by default.
164
+
165
+ ### The problem
166
+
167
+ Nuxt UI's `FormField` derives the label's `for` and the control's `id` from a single
168
+ `useId()` call, so they cannot disagree — unless `useId()` itself returns different values on
169
+ server and client, which happens when the two walk a different number of async boundaries.
170
+
171
+ Measured against `@nuxt/ui` 4.11.x:
172
+
173
+ | | label `for` | control `id` |
174
+ |---|---|---|
175
+ | SSR payload | `v-0-4-2` | `v-0-4-2` |
176
+ | after hydration | `v-0-4-2` | `v-0-1-2` |
177
+
178
+ Vue 3.5.39 did not cause this, it exposed it
179
+ ([vuejs/core#9083](https://github.com/vuejs/core/pull/9083) force-patches an element's
180
+ dynamic props on hydration). The control's `id` is such a prop; the label's `for`, passed
181
+ through reka-ui's `Label` component, is not — so only the label keeps the stale value.
182
+
183
+ ### What it costs when it breaks
184
+
185
+ The control loses its programmatic label. Without a placeholder it has no accessible name at
186
+ all and a screen reader announces "edit text, blank"; **with** a placeholder that becomes the
187
+ name instead, so the visible label is no longer part of it and speech input stops working.
188
+ Clicking the label focuses nothing either — which on a checkbox or radio is the primary hit
189
+ target, not a convenience. Tests using `getByRole('textbox', { name })` stop finding fields.
190
+
191
+ ### What the repair does
192
+
193
+ After mount, and for a bounded window afterwards, every Nuxt UI field label whose target does
194
+ not resolve is re-pointed at the control in its own field.
195
+
196
+ **It refuses to guess.** A repair that fires where it should not is worse than the defect: a
197
+ dangling label is inert and visible, a mis-pointed one is confidently wrong and silent, and
198
+ on a radio or checkbox a click on the caption would change a value the user never chose. So
199
+ the repair only fires when the field contains exactly **one** eligible control. Skipped
200
+ deliberately:
201
+
202
+ | Case | Why |
203
+ |------|-----|
204
+ | Radio / checkbox groups | One field root, many controls — binding every caption to the first option would submit a value nobody picked |
205
+ | Hidden form-value proxies, `aria-hidden`, `disabled` | Not the user's control; pointing at one restores neither the name nor the click |
206
+ | Fields whose only visible control is a `button` | Nothing safe to point at |
207
+ | Labels your application wrote | Only Nuxt UI's own field labels (`data-slot="label"`) are touched |
208
+
209
+ `for` is repaired rather than `aria-labelledby` added, because `for` carries **both** the
210
+ accessible name and click-to-focus.
211
+
212
+ **This is a repair, not a cure.** The real fix is for the id not to diverge; this exists
213
+ because the divergence sits in the framework stack rather than in any one application.
214
+
215
+ ### Opting out
216
+
217
+ ```ts
218
+ export default defineNuxtConfig({
219
+ ltExtensions: {
220
+ formLabelAssociation: {
221
+ enabled: false,
222
+ // or keep it on and only change how long deferred subtrees are waited for:
223
+ // maxRepairMs: 3000,
224
+ },
225
+ },
226
+ });
227
+ ```
228
+
229
+ Two real reasons to: your application assigns `for` itself and depends on those exact values,
230
+ or your tests assert on literal `for` / `id` strings rather than on the accessible name.
231
+
160
232
  ## Pre-Hydration Input Preservation
161
233
 
162
234
  Text typed before a page has hydrated is kept instead of being silently thrown away.
package/dist/module.d.mts CHANGED
@@ -3,7 +3,7 @@ import { LtExtensionsModuleOptions } from '../dist/runtime/types/index.js';
3
3
  export * from '../dist/runtime/types/index.js';
4
4
 
5
5
  declare const name = "@lenne.tech/nuxt-extensions";
6
- declare const version = "1.12.0";
6
+ declare const version = "1.13.0";
7
7
  declare const configKey = "ltExtensions";
8
8
  declare const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
9
9
  declare const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "configKey": "ltExtensions",
3
3
  "name": "@lenne.tech/nuxt-extensions",
4
- "version": "1.12.0",
4
+ "version": "1.13.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, tryResolveModule, addImports, addComponent, addPlugin, addRouteMiddleware } from '@nuxt/kit';
2
2
 
3
3
  const name = "@lenne.tech/nuxt-extensions";
4
- const version = "1.12.0";
4
+ const version = "1.13.0";
5
5
  const configKey = "ltExtensions";
6
6
  const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
7
7
  const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
@@ -36,6 +36,9 @@ const defaultOptions = {
36
36
  enabled: true,
37
37
  defaultLocale: "de"
38
38
  },
39
+ formLabelAssociation: {
40
+ enabled: true
41
+ },
39
42
  i18n: {
40
43
  autoMerge: true
41
44
  },
@@ -66,6 +69,7 @@ const module$1 = defineNuxtModule({
66
69
  systemSetup: { ...defaultOptions.auth.systemSetup, ...options.auth?.systemSetup }
67
70
  },
68
71
  errorTranslation: { ...defaultOptions.errorTranslation, ...options.errorTranslation },
72
+ formLabelAssociation: { ...defaultOptions.formLabelAssociation, ...options.formLabelAssociation },
69
73
  i18n: { ...defaultOptions.i18n, ...options.i18n },
70
74
  preHydrationInput: { ...defaultOptions.preHydrationInput, ...options.preHydrationInput },
71
75
  tus: { ...defaultOptions.tus, ...options.tus }
@@ -124,6 +128,10 @@ const module$1 = defineNuxtModule({
124
128
  enabled: resolvedOptions.errorTranslation?.enabled ?? true,
125
129
  defaultLocale: resolvedOptions.errorTranslation?.defaultLocale || "de"
126
130
  },
131
+ formLabelAssociation: {
132
+ enabled: resolvedOptions.formLabelAssociation?.enabled ?? true,
133
+ maxRepairMs: resolvedOptions.formLabelAssociation?.maxRepairMs ?? 1500
134
+ },
127
135
  preHydrationInput: {
128
136
  enabled: resolvedOptions.preHydrationInput?.enabled ?? true,
129
137
  maxRestoreMs: resolvedOptions.preHydrationInput?.maxRestoreMs ?? 1500
@@ -219,6 +227,9 @@ const module$1 = defineNuxtModule({
219
227
  if (resolvedOptions.preHydrationInput.enabled !== false) {
220
228
  addPlugin(resolve("./runtime/plugins/pre-hydration-input.client"));
221
229
  }
230
+ if (resolvedOptions.formLabelAssociation.enabled !== false) {
231
+ addPlugin(resolve("./runtime/plugins/form-label-association.client"));
232
+ }
222
233
  nuxt.options.build.transpile.push(resolve("./runtime"));
223
234
  nuxt.options.build.transpile.push("tus-js-client");
224
235
  console.log(`[${name}] Module loaded with config:`, {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Repair `<label for>` associations that hydration leaves pointing at nothing.
3
+ *
4
+ * ## The defect
5
+ *
6
+ * Nuxt UI's `FormField` generates ONE id with `useId()` and uses it for both halves of
7
+ * the association: `<Label :for="id">` and, via `provide`/`inject`, the control's `id`.
8
+ * One source, so the two cannot disagree — unless `useId()` itself returns different
9
+ * values on the server and on the client.
10
+ *
11
+ * It does. Measured on a Nuxt 4 app page (SVL, DEV-3093), against `@nuxt/ui` 4.11.x:
12
+ *
13
+ * | | label `for` | control `id` |
14
+ * | -- | -- | -- |
15
+ * | SSR payload | `v-0-4-2` | `v-0-4-2` |
16
+ * | after hydration | `v-0-4-2` | `v-0-1-2` |
17
+ *
18
+ * The middle segment is Vue's per-branch id prefix, which is extended at every async
19
+ * boundary. Server and client walked a different number of them, so the ids diverge.
20
+ *
21
+ * **Vue >= 3.5.39 did not cause this — it exposed it.** "force patch dynamic props when
22
+ * hydrating" (vuejs/core#9083, merged 2026-06-25) makes a hydrated element adopt its
23
+ * freshly computed client value, where previously the server value simply stayed.
24
+ *
25
+ * Note WHY only the label is left behind, because it points at the real fix: the control's
26
+ * `id` is a compiled element binding (`<input :id="id">`), so it is in the element's
27
+ * `dynamicProps` and gets force-patched. The label's `for` is a PROP on reka-ui's `Label`
28
+ * component and arrives at the `<label>` through `$attrs`, which is not part of that set.
29
+ * The asymmetry is the component boundary, not the id generation. Fixing the divergence
30
+ * upstream — or having `FormField` pass an SSR-stable id — removes the need for this file.
31
+ *
32
+ * ## Why it is worth a plugin
33
+ *
34
+ * A control whose label does not resolve loses its programmatic label: the visible text is
35
+ * no longer its accessible name (WCAG 1.3.1). Where the field has no placeholder it has no
36
+ * accessible name at all and a screen reader announces "edit text, blank" (4.1.2). Where it
37
+ * has one, the placeholder becomes the name instead — which is arguably worse, because the
38
+ * name no longer contains the visible label and speech input stops working (2.5.3 Label in
39
+ * Name). Clicking the label also no longer focuses the control, and for a checkbox or radio
40
+ * that is not a convenience but the primary hit target.
41
+ *
42
+ * And any test that addresses fields the way assistive technology does
43
+ * (`getByRole('textbox', { name })`, the recommended Playwright locator) stops finding
44
+ * them, which is how this surfaced: 39 specs at once.
45
+ *
46
+ * ## What this does, and what it deliberately does not
47
+ *
48
+ * After mount — and for a bounded window afterwards — every Nuxt UI field label whose
49
+ * target does not resolve is re-pointed at the control inside its own field.
50
+ *
51
+ * It **refuses to guess**, which is the rule that matters most here. A repair that fires
52
+ * where it should not is worse than the defect: a dangling label is inert and visible, a
53
+ * mis-pointed one is confidently wrong and silent, and on a radio or checkbox a click on
54
+ * the caption would then change a value the user never chose. So the repair only happens
55
+ * when the field contains exactly ONE eligible control. A group of radios or checkboxes
56
+ * shares one field root and is therefore skipped, not guessed at.
57
+ *
58
+ * `for` is repaired rather than `aria-labelledby` added, because `for` carries BOTH the
59
+ * accessible name and click-to-focus. Adding `aria-labelledby` would restore the name
60
+ * and leave the label dead to the mouse.
61
+ *
62
+ * This is a repair, not a cure: the right fix is for the id not to diverge in the first
63
+ * place (see the component-boundary note above). It is applied here because the divergence
64
+ * sits in the framework stack rather than in any one application, and because a nameless
65
+ * form is not something to leave in place while that is resolved upstream.
66
+ *
67
+ * Default on; opt out with `ltExtensions.formLabelAssociation.enabled: false`.
68
+ */
69
+ /**
70
+ * Re-point every dangling field label at the control in its own field.
71
+ *
72
+ * Exported for the unit test: it runs against a DOM fixture rather than a live app, so the
73
+ * pairing rules stay verifiable without a browser. Not part of the package's public API.
74
+ *
75
+ * @param root Where to look. Ids are resolved against the owning document regardless, since
76
+ * `id` is document-scoped — so a scoped root narrows which labels are visited, not which
77
+ * ids count as resolvable.
78
+ * @returns how many labels were repaired.
79
+ */
80
+ export declare function repairDanglingLabels(root: Document | ParentNode): number;
81
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
82
+ export default _default;
@@ -0,0 +1,67 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from "#imports";
2
+ const COMPONENT_ROOT = '[data-slot="root"]';
3
+ const FIELD_LABEL = 'label[data-slot="label"][for]';
4
+ const CONTROL = "input, textarea, select";
5
+ const NOT_A_USER_CONTROL = '[type="hidden"], [hidden], [aria-hidden="true"], [disabled]';
6
+ function findControlForLabel(field) {
7
+ const eligible = [...field.querySelectorAll(CONTROL)].filter((control) => !control.matches(NOT_A_USER_CONTROL));
8
+ return eligible.length === 1 && eligible[0].id !== "" ? eligible[0] : null;
9
+ }
10
+ export function repairDanglingLabels(root) {
11
+ let repaired = 0;
12
+ for (const label of root.querySelectorAll(FIELD_LABEL)) {
13
+ const field = label.closest(COMPONENT_ROOT);
14
+ if (field === null) {
15
+ continue;
16
+ }
17
+ const target = label.ownerDocument.getElementById(label.htmlFor);
18
+ if (target !== null && field.contains(target)) {
19
+ continue;
20
+ }
21
+ const control = findControlForLabel(field);
22
+ if (control === null || control.id === label.htmlFor) {
23
+ continue;
24
+ }
25
+ label.htmlFor = control.id;
26
+ repaired += 1;
27
+ }
28
+ return repaired;
29
+ }
30
+ export default defineNuxtPlugin({
31
+ // After the app's own plugins, so a page that assigns ids itself has already run.
32
+ enforce: "post",
33
+ name: "lt-form-label-association",
34
+ setup(nuxtApp) {
35
+ const options = useRuntimeConfig().public.ltExtensions?.formLabelAssociation;
36
+ if (options?.enabled === false) {
37
+ return;
38
+ }
39
+ const maxRepairMs = options?.maxRepairMs ?? 1500;
40
+ let sweeping = null;
41
+ const sweep = () => {
42
+ const repaired = repairDanglingLabels(document);
43
+ if (repaired > 0 && import.meta.dev) {
44
+ console.warn(
45
+ `[@lenne.tech/nuxt-extensions] repaired ${repaired} form label association(s) broken by an SSR/client useId() divergence. This is a stopgap \u2014 see runtime/plugins/form-label-association.client.ts.`
46
+ );
47
+ }
48
+ };
49
+ const sweepFor = () => {
50
+ sweep();
51
+ if (maxRepairMs <= 0 || sweeping !== null) {
52
+ return;
53
+ }
54
+ sweeping = setInterval(sweep, 250);
55
+ setTimeout(() => {
56
+ if (sweeping !== null) {
57
+ clearInterval(sweeping);
58
+ sweeping = null;
59
+ }
60
+ }, maxRepairMs);
61
+ };
62
+ nuxtApp.hook("app:mounted", sweepFor);
63
+ nuxtApp.hook("page:finish", () => {
64
+ requestAnimationFrame(sweepFor);
65
+ });
66
+ }
67
+ });
@@ -1,5 +1,5 @@
1
1
  export type { LtAuthClientConfig, LtAuthMode, LtAuthResponse, LtAuthState, LtPasskeyAuthResult, LtPasskeyRegisterResult, LtUser, UseLtAuthReturn } from './auth.js';
2
2
  export type { LtFileInfo, LtUploadItem, LtUploadOptions, LtUploadProgress, LtUploadStatus, UseLtFileReturn, UseLtTusUploadReturn } from './upload.js';
3
- export type { LtAiModuleOptions, LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtPreHydrationInputOptions, LtSystemSetupModuleOptions, LtTusModuleOptions, } from './module.js';
3
+ export type { LtAiModuleOptions, LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtFormLabelAssociationOptions, LtI18nModuleOptions, LtPreHydrationInputOptions, LtSystemSetupModuleOptions, LtTusModuleOptions, } from './module.js';
4
4
  export type { LtErrorTranslationResponse, LtParsedError, UseLtErrorTranslationReturn } from './error.js';
5
5
  export type { LtAiAction, LtAiAvailableConnection, LtAiBudgetLimit, LtAiBudgetSummary, LtAiConnection, LtAiConnectionInput, LtAiConnectionPreference, LtAiEffectiveSlot, LtAiInteraction, LtAiMessage, LtAiMode, LtAiPlaceholder, LtAiPrompt, LtAiPromptHint, LtAiPromptHintInput, LtAiPromptInput, LtAiPromptRunInput, LtAiResponse, LtAiSlot, LtAiSlotInput, LtAiStreamEvent, LtAiStreamHandlers, LtAiUsage, LtAiUsageInfo, LtAiUsageScope, UseLtAiAdminReturn, UseLtAiChatOptions, UseLtAiChatReturn, UseLtAiConnectionsReturn, UseLtAiPlaceholdersReturn, UseLtAiPromptsReturn, UseLtAiReturn, UseLtAiUsageReturn, } from './ai.js';
@@ -127,6 +127,8 @@ export interface LtExtensionsModuleOptions {
127
127
  auth?: LtAuthModuleOptions;
128
128
  /** Error translation configuration */
129
129
  errorTranslation?: LtErrorTranslationModuleOptions;
130
+ /** Form label association repair configuration */
131
+ formLabelAssociation?: LtFormLabelAssociationOptions;
130
132
  /** i18n configuration */
131
133
  i18n?: LtI18nModuleOptions;
132
134
  /** Pre-hydration input preservation configuration */
@@ -134,6 +136,57 @@ export interface LtExtensionsModuleOptions {
134
136
  /** TUS upload module configuration */
135
137
  tus?: LtTusModuleOptions;
136
138
  }
139
+ /**
140
+ * Repair of `<label for>` associations broken by an SSR/client `useId()` divergence.
141
+ *
142
+ * Nuxt UI's `FormField` derives the label's `for` and the control's `id` from ONE `useId()`
143
+ * call, so they cannot disagree — unless `useId()` itself returns different values on server
144
+ * and client, which it does whenever the two walk a different number of async boundaries.
145
+ * The label then keeps the server value while the control adopts the client one.
146
+ *
147
+ * The control loses its programmatic label. Without a placeholder it has no accessible name
148
+ * at all and a screen reader announces "edit text, blank"; with one, the placeholder becomes
149
+ * the name instead, so the visible label is no longer part of it and speech input stops
150
+ * working. Either way clicking the label focuses nothing — which on a checkbox or radio is
151
+ * the primary hit target, not a convenience.
152
+ *
153
+ * Vue >= 3.5.39 did not cause this, it exposed it: vuejs/core#9083 force-patches an element's
154
+ * dynamic props during hydration, and the control's `id` is one while the label's `for` —
155
+ * passed through reka-ui's `Label` component and `$attrs` — is not.
156
+ *
157
+ * The repair refuses to guess: it only fires when the field contains exactly one eligible
158
+ * control, so a radio or checkbox group is skipped rather than having every caption bound to
159
+ * its first option.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * export default defineNuxtConfig({
164
+ * ltExtensions: {
165
+ * formLabelAssociation: { enabled: false },
166
+ * },
167
+ * });
168
+ * ```
169
+ */
170
+ export interface LtFormLabelAssociationOptions {
171
+ /**
172
+ * Enable the repair. Defaults to `true`.
173
+ *
174
+ * Turn it off only with a reason. Two real ones: an application that assigns `for` itself
175
+ * and depends on those exact values, or a test suite asserting on literal `for` / `id`
176
+ * strings rather than on the accessible name. The alternative is a form whose fields carry
177
+ * no programmatic label.
178
+ */
179
+ enabled?: boolean;
180
+ /**
181
+ * How long after mount to keep repairing, in milliseconds. Defaults to `1500`; `0` sweeps
182
+ * once and stops.
183
+ *
184
+ * Most fields are repaired in the first pass. The window exists for server-rendered
185
+ * subtrees whose hydration is deferred — `hydrate-on-visible`, an unresolved `<Suspense>`,
186
+ * an island — which carry the same divergence but hydrate after `app:mounted`.
187
+ */
188
+ maxRepairMs?: number;
189
+ }
137
190
  /**
138
191
  * Pre-hydration input preservation.
139
192
  *
@@ -219,6 +272,10 @@ export interface LtExtensionsPublicRuntimeConfig {
219
272
  enabled: boolean;
220
273
  defaultLocale: string;
221
274
  };
275
+ formLabelAssociation: {
276
+ enabled: boolean;
277
+ maxRepairMs: number;
278
+ };
222
279
  preHydrationInput: {
223
280
  enabled: boolean;
224
281
  maxRestoreMs: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nuxt-extensions",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
5
5
  "repository": {
6
6
  "type": "git",