@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 +30 -0
- package/README.md +72 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -1
- package/dist/runtime/plugins/form-label-association.client.d.ts +82 -0
- package/dist/runtime/plugins/form-label-association.client.js +67 -0
- package/dist/runtime/types/index.d.ts +1 -1
- package/dist/runtime/types/module.d.ts +57 -0
- package/package.json +1 -1
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.
|
|
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
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.
|
|
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;
|