compote-ui 0.72.1 → 0.74.1

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.
@@ -7,6 +7,8 @@
7
7
  form,
8
8
  field,
9
9
  helperText,
10
+ errorText,
11
+ hideMessageLine = false,
10
12
  class: className,
11
13
  invalid,
12
14
  required,
@@ -14,9 +16,15 @@
14
16
  ...rest
15
17
  }: FieldRootProps = $props();
16
18
 
17
- const isInvalid = $derived(form && field ? form.invalid(field) : (invalid ?? false));
19
+ // Declared before isInvalid, which reads it.
20
+ const resolvedError = $derived(
21
+ form && field ? (form.errors[field]?.[0] ?? null) : (errorText ?? null)
22
+ );
23
+ // An error that does not also mark the field invalid renders nothing at all:
24
+ // Ark's Field.ErrorText is gated on the field context's `invalid`. So a bare
25
+ // `errorText` implies it, while an explicit `invalid` still wins.
26
+ const isInvalid = $derived(form && field ? form.invalid(field) : (invalid ?? !!resolvedError));
18
27
  const isRequired = $derived(form && field ? form.isRequired(field) : (required ?? false));
19
- const errorText = $derived(form && field ? (form.errors[field]?.[0] ?? null) : null);
20
28
  </script>
21
29
 
22
30
  <Field.Root
@@ -26,9 +34,19 @@
26
34
  class={cn('group flex flex-col gap-1.5', className)}
27
35
  >
28
36
  {@render children?.()}
29
- {#if errorText}
30
- <Field.ErrorText>{errorText}</Field.ErrorText>
37
+ {#if resolvedError}
38
+ <Field.ErrorText>{resolvedError}</Field.ErrorText>
31
39
  {:else if helperText}
32
40
  <Field.HelperText>{helperText}</Field.HelperText>
41
+ {:else if !hideMessageLine}
42
+ <!--
43
+ Holds the message line open so a field does not change height when an
44
+ error appears. `1lh` resolves against this element's own line-height, so
45
+ it tracks the theme's font rather than assuming a pixel height. A plain
46
+ div and not an empty Field.HelperText: Ark gives that one an id which
47
+ aria-describedby points at, which would describe the control as having a
48
+ description that isn't there.
49
+ -->
50
+ <div class="h-lh text-xs" aria-hidden="true"></div>
33
51
  {/if}
34
52
  </Field.Root>
@@ -11,6 +11,21 @@ export interface FieldRootProps extends FieldRootBaseProps {
11
11
  form?: FormAdapter;
12
12
  field?: string;
13
13
  helperText?: string;
14
+ /**
15
+ * Error to render below the control, for validation that does not go through
16
+ * a {@link FormAdapter} (e.g. a SvelteKit remote form's `field.issues()`).
17
+ * Passing it also marks the field invalid unless `invalid` is set explicitly.
18
+ * Ignored when `form` and `field` are given — the adapter wins, as it does
19
+ * for `invalid` and `required`.
20
+ */
21
+ errorText?: string | null;
22
+ /**
23
+ * Whether to drop the spacer that otherwise holds the message line open
24
+ * when there is no error or helper text to show. Defaults to `false`. Set
25
+ * to `true` for a field that never surfaces validation (a toolbar search
26
+ * box, say), so it doesn't reserve height it will never use.
27
+ */
28
+ hideMessageLine?: boolean;
14
29
  }
15
30
  export interface FieldLabelProps extends FieldLabelBaseProps {
16
31
  class?: ClassValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.72.1",
3
+ "version": "0.74.1",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev --open",
@@ -19,6 +19,32 @@ With a form adapter, `Field.Root` derives invalid/required state and renders the
19
19
  </Field.Root>
20
20
  ```
21
21
 
22
+ `errorText` is the per-field route to that same line, for validation that doesn't fit the adapter's
23
+ `Record<string, string[]>` shape — a SvelteKit remote form's `field.issues()`, say. **Passing it
24
+ marks the field invalid**; an explicit `invalid` still wins, and the adapter wins over both.
25
+
26
+ ```svelte
27
+ <Field.Root required errorText={fields.password.issues()?.[0]?.message}>
28
+ <PasswordInput label="Password" name={fields.password.as('password').name} />
29
+ </Field.Root>
30
+ ```
31
+
32
+ One message, not a list: several errors on one value are usually stages of the same judgement, and a
33
+ stack that grows and shrinks per keystroke is what makes a form jump. Independent requirements (a
34
+ password policy) belong in a persistent checklist shown from the start, not in error text. Where you
35
+ genuinely need several, `<Field.ErrorText>` children still work.
36
+
37
+ The message line is **always held open**, so a field never changes height when an error appears and
38
+ nothing below it moves. Errors and `helperText` share that line — an error replaces the helper
39
+ rather than stacking under it. For a field that never surfaces validation (a toolbar search box,
40
+ say), pass `hideMessageLine` to drop the spacer entirely:
41
+
42
+ ```svelte
43
+ <Field.Root hideMessageLine>
44
+ <Field.Input bind:value={search} placeholder="Search..." />
45
+ </Field.Root>
46
+ ```
47
+
22
48
  List controls use `{ value, label }` items:
23
49
 
24
50
  ```svelte