@signal9/era-ui 4.3.0 → 4.3.2

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.
@@ -391,11 +391,17 @@
391
391
  /* -------------------------------------------------- */
392
392
  /* Signalnine — dark (Tomorrow, measured) */
393
393
  /* */
394
- /* The classic imageboard dark theme. Every gray and */
395
- /* every accent below is Tomorrow Night's own value, */
396
- /* converted to OKLCH — the scale reproduces its */
397
- /* background, current-line, selection, comment and */
398
- /* foreground exactly at steps 1, 2, 3, 8 and 11. */
394
+ /* The classic imageboard dark theme. The accents and */
395
+ /* the upper scale are Tomorrow Night's own values, */
396
+ /* converted to OKLCH — comment and foreground land */
397
+ /* exactly at steps 8 and 11. */
398
+ /* */
399
+ /* The DARK END is deliberately deeper than Tomorrow's. */
400
+ /* Its #1d1f21 page read light next to era's other dark */
401
+ /* themes, so the floor is the default theme's page */
402
+ /* plus a hair and steps 2-7 re-space from there. The */
403
+ /* identity lives in the greys' hue and the accents, not */
404
+ /* in the exact page value. */
399
405
  /* */
400
406
  /* This replaces a scale that lurched warm through the */
401
407
  /* middle so borders read faintly red against cold */
@@ -413,17 +419,21 @@
413
419
  [data-theme='signalnine'].dark {
414
420
  color-scheme: dark;
415
421
 
416
- --color-1: oklch(0.238 0.005 248); /* #1d1f21 background */
417
- --color-2: oklch(0.285 0.008 264); /* #282a2e current line */
418
- --color-3: oklch(0.351 0.012 258); /* #373b41 selection */
419
- --color-4: oklch(0.405 0.012 258);
420
- --color-5: oklch(0.46 0.011 257);
422
+ /* The floor is era's default-dark page (0.162) plus a hair, not Tomorrow's
423
+ * own #1d1f21 that read too light beside the other dark themes. Everything
424
+ * from step 2 up re-spaces evenly from the new floor to the comment anchor,
425
+ * so lowering the page did not leave a cliff between it and the panels. */
426
+ --color-1: oklch(0.168 0.005 248); /* #0d0f11 */
427
+ --color-2: oklch(0.225 0.007 258); /* #1a1c1f */
428
+ --color-3: oklch(0.29 0.01 258); /* #282c30 */
429
+ --color-4: oklch(0.355 0.011 258); /* #383c42 */
430
+ --color-5: oklch(0.435 0.011 257);
421
431
  --color-6: oklch(0.515 0.01 256);
422
- --color-7: oklch(0.57 0.009 255);
423
- --color-8: oklch(0.677 0.006 200); /* #939899 comment */
432
+ --color-7: oklch(0.596 0.009 255);
433
+ --color-8: oklch(0.677 0.006 200); /* #939899 Tomorrow comment */
424
434
  --color-9: oklch(0.73 0.005 190);
425
435
  --color-10: oklch(0.78 0.005 180);
426
- --color-11: oklch(0.83 0.004 157); /* #c5c8c6 foreground */
436
+ --color-11: oklch(0.83 0.004 157); /* #c5c8c6 Tomorrow foreground */
427
437
  --color-12: oklch(0.9 0.004 157);
428
438
 
429
439
  /* Border sits a step LOWER than the usual 5. Tomorrow's own rules are
@@ -432,7 +442,7 @@
432
442
  * page, inside the 1.7–3.2 the other dark themes hold. */
433
443
  --color-border: var(--color-4);
434
444
  --color-surface: var(--color-2);
435
- --color-hover: oklch(0.32 0.011 260);
445
+ --color-hover: oklch(0.255 0.009 258);
436
446
  --color-fg: var(--color-11);
437
447
  --color-bright: var(--color-12);
438
448
  /* Tomorrow's comment grey, which is what it uses for de-emphasised text. */
@@ -443,14 +453,14 @@
443
453
  /* Tomorrow's accents, unmodified. They are pastels, so each takes DARK text
444
454
  * — the page colour — rather than white. */
445
455
  --color-primary: oklch(0.6975 0.0551 244.3); /* #81a2be blue */
446
- --color-primary-fg: oklch(0.238 0.005 248);
456
+ --color-primary-fg: oklch(0.168 0.005 248);
447
457
  --color-primary-hover: color-mix(in oklch, var(--color-primary) 85%, black);
448
458
  --color-destructive: oklch(0.6308 0.1298 21.4); /* #cc6666 red */
449
- --color-destructive-fg: oklch(0.238 0.005 248);
459
+ --color-destructive-fg: oklch(0.168 0.005 248);
450
460
  --color-success: oklch(0.7733 0.1095 113.4); /* #b5bd68 green */
451
- --color-success-fg: oklch(0.238 0.005 248);
461
+ --color-success-fg: oklch(0.168 0.005 248);
452
462
  --color-warning: oklch(0.8462 0.1115 82.9); /* #f0c674 yellow */
453
- --color-warning-fg: oklch(0.238 0.005 248);
463
+ --color-warning-fg: oklch(0.168 0.005 248);
454
464
  }
455
465
 
456
466
  /* Signalnine — light (yahoo.co.jp, measured) */
@@ -71,6 +71,15 @@ export const buttonVariants = tv({
71
71
  // the box: sp+4 in xxs (pad 3), sp+2 in xs/icon (pad 2). Scoped to icon:true
72
72
  // so text+icon buttons keep their h-xs glyph. The descendant selector
73
73
  // outranks the consumer's own size-* on the svg, so no call site changes.
74
+ //
75
+ // default/sm were DESCRIBED by the comment above but had no rule, so an
76
+ // icon button at either size fell through to Lucide's own 24px default
77
+ // unless the call site happened to pass a size class. That is how the
78
+ // notes "new note" button rendered a 24px glyph in a 24px box — flush,
79
+ // no padding at all. Stating them makes the documented intent real and
80
+ // removes the dependency on every call site remembering.
81
+ { icon: true, size: 'default', class: '[&_svg]:size-(--era-h-xs)' },
82
+ { icon: true, size: 'sm', class: '[&_svg]:size-(--era-h-xs)' },
74
83
  { icon: true, size: 'xxs', class: '[&_svg]:size-(--era-icon-xxs)' },
75
84
  { icon: true, size: 'xs', class: '[&_svg]:size-(--era-icon-xs)' },
76
85
  { icon: true, size: 'icon', class: '[&_svg]:size-(--era-icon-xs)' },
@@ -1,14 +1,18 @@
1
1
  <script lang="ts">
2
2
  import { Combobox } from 'bits-ui';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from '../input/variants.js';
4
5
 
5
6
  let { ref = $bindable(null), class: className, ...restProps }: Combobox.InputProps = $props();
6
7
  </script>
7
8
 
8
9
  <Combobox.Input
9
10
  bind:ref
11
+ data-input-field
10
12
  class={cn(
11
- 'flex h-(--era-h-md) w-full era-interactive rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) text-body text-fg shadow-(--era-shadow-well) placeholder:text-muted hover:bg-(--era-highlight) focus:bg-(--era-highlight) data-[state=open]:rounded-b-none data-[state=open]:shadow-(--era-shadow-pressed)',
13
+ inputVariants(),
14
+ // The open list is docked to the field, so the seam between them opens up.
15
+ 'data-[state=open]:rounded-b-none data-[state=open]:shadow-(--era-shadow-pressed)',
12
16
  className
13
17
  )}
14
18
  {...restProps}
@@ -2,7 +2,7 @@
2
2
  import { Command } from 'bits-ui';
3
3
  import type { Component } from 'svelte';
4
4
  import type { IconProps } from '@lucide/svelte';
5
- import { cn } from '../../utils/index.js';
5
+ import Input from '../input/input.svelte';
6
6
 
7
7
  let {
8
8
  ref = $bindable(null),
@@ -16,30 +16,20 @@
16
16
  } = $props();
17
17
  </script>
18
18
 
19
- {#if icon}
20
- {@const Icon = icon}
21
- <div
22
- class={cn(
23
- 'flex h-(--era-h-md) w-full era-interactive items-center gap-(--era-gap) rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) text-body era-text-trim text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight)',
24
- className
25
- )}
26
- >
27
- <Icon class="size-(--era-h-xs) shrink-0 text-muted" />
28
- <Command.Input
29
- bind:ref
30
- bind:value
31
- class="h-full w-full bg-transparent text-fg outline-none placeholder:text-muted"
32
- {...restProps}
33
- />
34
- </div>
35
- {:else}
36
- <Command.Input
37
- bind:ref
38
- bind:value
39
- class={cn(
40
- 'flex h-(--era-h-md) w-full era-interactive rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) text-body text-fg shadow-(--era-shadow-well) placeholder:text-muted hover:bg-(--era-highlight) focus:bg-(--era-highlight)',
41
- className
42
- )}
43
- {...restProps}
44
- />
45
- {/if}
19
+ <!--
20
+ The search line of a command palette is a plain era Input, so this renders the
21
+ real component instead of a copy of its classes. It used to restate the whole
22
+ field chrome AND re-implement the icon branch — a second Input in all but
23
+ name, free to drift from the first, which it had.
24
+
25
+ `child` is what makes the delegation possible: bits hands us its behaviour as
26
+ props (the combobox roles, the active-descendant wiring, the ref attachment)
27
+ and we spread them onto our own element. `value` is bound in both places on
28
+ purpose — bits does not bind it for us in the child branch, so the Input's
29
+ binding is what feeds the palette's search state.
30
+ -->
31
+ <Command.Input bind:ref bind:value {...restProps}>
32
+ {#snippet child({ props })}
33
+ <Input bind:value {icon} class={className} {...props} />
34
+ {/snippet}
35
+ </Command.Input>
@@ -1,14 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { DateField } from 'bits-ui';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from '../input/variants.js';
4
5
 
5
6
  let { ref = $bindable(null), class: className, ...restProps }: DateField.InputProps = $props();
6
7
  </script>
7
8
 
8
9
  <DateField.Input
9
10
  bind:ref
11
+ data-input-field
10
12
  class={cn(
11
- 'flex h-(--era-h-md) w-full items-center rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) text-body text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight) disabled:opacity-50 data-[invalid]:text-destructive',
13
+ inputVariants(),
14
+ 'items-center disabled:opacity-50 data-[invalid]:text-destructive',
12
15
  className
13
16
  )}
14
17
  {...restProps}
@@ -1,14 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { DatePicker } from 'bits-ui';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from '../input/variants.js';
4
5
 
5
6
  let { ref = $bindable(null), class: className, ...restProps }: DatePicker.InputProps = $props();
6
7
  </script>
7
8
 
8
9
  <DatePicker.Input
9
10
  bind:ref
11
+ data-input-field
10
12
  class={cn(
11
- 'flex h-(--era-h-md) w-full items-center rounded-(--era-rd-md) bg-(--era-field-bg) ps-(--era-inset-md) pe-(--era-px-md) text-body text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight) disabled:opacity-50 data-[invalid]:text-destructive',
13
+ inputVariants({ pad: 'trailing' }),
14
+ 'items-center disabled:opacity-50 data-[invalid]:text-destructive',
12
15
  className
13
16
  )}
14
17
  {...restProps}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { DateRangeField } from 'bits-ui';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from '../input/variants.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(null),
@@ -11,8 +12,10 @@
11
12
 
12
13
  <DateRangeField.Input
13
14
  bind:ref
15
+ data-input-field
14
16
  class={cn(
15
- 'flex h-(--era-h-md) w-full items-center rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) text-body text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight) disabled:opacity-50 data-[invalid]:text-destructive',
17
+ inputVariants(),
18
+ 'items-center disabled:opacity-50 data-[invalid]:text-destructive',
16
19
  className
17
20
  )}
18
21
  {...restProps}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { DateRangePicker } from 'bits-ui';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from '../input/variants.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(null),
@@ -11,8 +12,10 @@
11
12
 
12
13
  <DateRangePicker.Input
13
14
  bind:ref
15
+ data-input-field
14
16
  class={cn(
15
- 'flex h-(--era-h-md) items-center rounded-(--era-rd-md) bg-(--era-field-bg) ps-(--era-inset-md) pe-(--era-px-md) text-body text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight) disabled:opacity-50 data-[invalid]:text-destructive',
17
+ inputVariants({ pad: 'trailing' }),
18
+ 'items-center disabled:opacity-50 data-[invalid]:text-destructive',
16
19
  className
17
20
  )}
18
21
  {...restProps}
@@ -1,2 +1,3 @@
1
1
  export { default as Input } from './input.svelte';
2
2
  export { default as Textarea } from './textarea.svelte';
3
+ export { inputVariants } from './variants.js';
@@ -1,2 +1,5 @@
1
1
  export { default as Input } from './input.svelte';
2
2
  export { default as Textarea } from './textarea.svelte';
3
+ // The field chrome itself, so a component that builds its own field composes
4
+ // this rather than restating it. See variants.ts.
5
+ export { inputVariants } from './variants.js';
@@ -1,42 +1,7 @@
1
- <script lang="ts" module>
2
- import { cn, tv } from '../../utils/index.js';
3
-
4
- /**
5
- * The field shell — the chrome an input paints, minus the text styling.
6
- *
7
- * Split out as a variant because an `icon` input has to move that chrome onto
8
- * a WRAPPER (the glyph and the <input> sit inside one field), while a plain
9
- * one keeps it on the <input> itself. Both spellings therefore have to
10
- * describe the same box, and a variant is how they stay one description.
11
- */
12
- export const inputVariants = tv({
13
- base: 'flex w-full era-interactive text-body text-fg',
14
- variants: {
15
- /**
16
- * `bare` drops the field chrome entirely — no fill, no well, no side
17
- * padding. For an input nested in a container that is ALREADY the field:
18
- * a Bar acting as a toolbar, a header row. Without it the input draws a
19
- * second box inside the first, and at the same tier it fills its parent
20
- * edge to edge so the two have no gap between them.
21
- */
22
- bare: {
23
- false:
24
- 'h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)',
25
- // h-full, NOT the md tier: a bare input's container IS the field, so it
26
- // takes that container's height. Keeping h-md here made a bare input
27
- // inside an h-md Bar overflow it by the bar's border — a −0.5px gap,
28
- // which is the even-gap law failing by a hair rather than obviously.
29
- // Implies a parent with a definite height, which is the only place
30
- // `bare` makes sense anyway.
31
- true: 'h-full bg-transparent px-0 shadow-none'
32
- }
33
- },
34
- defaultVariants: { bare: false }
35
- });
36
- </script>
37
-
38
1
  <script lang="ts">
39
2
  import type { Component } from 'svelte';
3
+ import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from './variants.js';
40
5
  import type { IconProps } from '@lucide/svelte';
41
6
  import type { HTMLInputAttributes } from 'svelte/elements';
42
7
 
@@ -67,7 +32,15 @@
67
32
  <!-- The chrome moves to the wrapper so the glyph sits INSIDE the field. The
68
33
  glyph is on the icon tier of the field's own height, and the gap between
69
34
  it and the text is the universal inter-element gap. -->
70
- <div class={cn(inputVariants({ bare }), 'items-center gap-(--era-gap)', className)}>
35
+ <!-- data-input-field marks whichever element actually PAINTS the field. With
36
+ an icon that is this wrapper; without one it is the <input> itself. Tests
37
+ and consumers can then target the field directly instead of guessing from
38
+ the DOM shape — guessing breaks on a `bare` input, whose parent is an
39
+ arbitrary container that may carry its own fill. -->
40
+ <div
41
+ data-input-field
42
+ class={cn(inputVariants({ bare }), 'items-center gap-(--era-gap)', className)}
43
+ >
71
44
  <Icon class="size-(--era-h-xs) shrink-0 text-muted" />
72
45
  <input
73
46
  bind:this={ref}
@@ -80,6 +53,7 @@
80
53
  <input
81
54
  bind:this={ref}
82
55
  bind:value
56
+ data-input-field
83
57
  class={cn(inputVariants({ bare }), textClass, className)}
84
58
  {...restProps}
85
59
  />
@@ -1,48 +1,3 @@
1
- /**
2
- * The field shell — the chrome an input paints, minus the text styling.
3
- *
4
- * Split out as a variant because an `icon` input has to move that chrome onto
5
- * a WRAPPER (the glyph and the <input> sit inside one field), while a plain
6
- * one keeps it on the <input> itself. Both spellings therefore have to
7
- * describe the same box, and a variant is how they stay one description.
8
- */
9
- export declare const inputVariants: import("tailwind-variants").TVReturnType<{
10
- /**
11
- * `bare` drops the field chrome entirely — no fill, no well, no side
12
- * padding. For an input nested in a container that is ALREADY the field:
13
- * a Bar acting as a toolbar, a header row. Without it the input draws a
14
- * second box inside the first, and at the same tier it fills its parent
15
- * edge to edge so the two have no gap between them.
16
- */
17
- bare: {
18
- false: "h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
19
- true: "h-full bg-transparent px-0 shadow-none";
20
- };
21
- }, undefined, "flex w-full era-interactive text-body text-fg", {
22
- /**
23
- * `bare` drops the field chrome entirely — no fill, no well, no side
24
- * padding. For an input nested in a container that is ALREADY the field:
25
- * a Bar acting as a toolbar, a header row. Without it the input draws a
26
- * second box inside the first, and at the same tier it fills its parent
27
- * edge to edge so the two have no gap between them.
28
- */
29
- bare: {
30
- false: "h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
31
- true: "h-full bg-transparent px-0 shadow-none";
32
- };
33
- }, undefined, import("tailwind-variants").TVReturnTypeLike<{
34
- /**
35
- * `bare` drops the field chrome entirely — no fill, no well, no side
36
- * padding. For an input nested in a container that is ALREADY the field:
37
- * a Bar acting as a toolbar, a header row. Without it the input draws a
38
- * second box inside the first, and at the same tier it fills its parent
39
- * edge to edge so the two have no gap between them.
40
- */
41
- bare: {
42
- false: "h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
43
- true: "h-full bg-transparent px-0 shadow-none";
44
- };
45
- }, undefined>>;
46
1
  import type { Component } from 'svelte';
47
2
  import type { IconProps } from '@lucide/svelte';
48
3
  import type { HTMLInputAttributes } from 'svelte/elements';
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLTextareaAttributes } from 'svelte/elements';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from './variants.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(null),
@@ -15,8 +16,12 @@
15
16
  <textarea
16
17
  bind:this={ref}
17
18
  bind:value
19
+ data-input-field
18
20
  class={cn(
19
- 'flex min-h-(--era-h-md) w-full era-interactive resize-y rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) py-(--era-inset-md) text-body text-fg shadow-(--era-shadow-well) placeholder:text-muted hover:bg-(--era-highlight) focus:bg-(--era-highlight)',
21
+ inputVariants({ height: 'auto' }),
22
+ // It grows, so the vertical inset has to be stated: the tier height is what
23
+ // centres one line of text in every other field, and there isn't one here.
24
+ 'resize-y py-(--era-inset-md)',
20
25
  className
21
26
  )}
22
27
  {...restProps}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * THE field. Every text field in the library paints this and nothing else.
3
+ *
4
+ * This module exists because it did not: the chrome below was hand-copied into
5
+ * Input, Textarea, CommandInput, ComboboxInput and all six date/time field
6
+ * inputs, and the copies had already drifted apart — some used `focus:`, others
7
+ * `focus-within:`, some omitted `era-interactive`, the date picker used a
8
+ * different leading padding, and the Notes fields had dropped the chrome
9
+ * entirely. Nine restatements of one idea is nine chances to be inconsistent,
10
+ * so there is now one statement and the rest import it.
11
+ *
12
+ * A component that needs something different asks for it as a VARIANT here,
13
+ * where every other field can see it — never by respelling the base.
14
+ *
15
+ * `tests/visual/input-consistency.spec.ts` measures the rendered result across
16
+ * routes and fails if any two fields differ.
17
+ */
18
+ export declare const inputVariants: import("tailwind-variants").TVReturnType<{
19
+ /**
20
+ * `bare` drops the field chrome entirely — no fill, no well, no side
21
+ * padding. For an input nested in a container that is ALREADY the field:
22
+ * a Bar acting as a toolbar, a header row. Without it the input draws a
23
+ * second box inside the first, and at the same tier it fills its parent
24
+ * edge to edge so the two have no gap between them.
25
+ */
26
+ bare: {
27
+ false: "rounded-(--era-rd-md) bg-(--era-field-bg) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
28
+ true: "bg-transparent shadow-none";
29
+ };
30
+ /** Vertical size. `auto` is for a field that grows with its content (Textarea). */
31
+ height: {
32
+ md: "h-(--era-h-md)";
33
+ full: "h-full";
34
+ auto: "min-h-(--era-h-md)";
35
+ };
36
+ /**
37
+ * Side padding. `trailing` is for a field that holds a control at its end
38
+ * (the date picker's calendar trigger): the leading edge keeps the shared
39
+ * text inset so the text still lines up with every other field, and the
40
+ * trailing edge becomes the concentric inset for that sm-tier control, so
41
+ * its gap to the field edge equals its gap above and below.
42
+ */
43
+ pad: {
44
+ even: "px-(--era-field-px)";
45
+ trailing: "ps-(--era-field-px) pe-(--era-sm-inset-md)";
46
+ none: "px-0";
47
+ };
48
+ }, undefined, "flex w-full era-interactive text-body text-fg placeholder:text-muted", {
49
+ /**
50
+ * `bare` drops the field chrome entirely — no fill, no well, no side
51
+ * padding. For an input nested in a container that is ALREADY the field:
52
+ * a Bar acting as a toolbar, a header row. Without it the input draws a
53
+ * second box inside the first, and at the same tier it fills its parent
54
+ * edge to edge so the two have no gap between them.
55
+ */
56
+ bare: {
57
+ false: "rounded-(--era-rd-md) bg-(--era-field-bg) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
58
+ true: "bg-transparent shadow-none";
59
+ };
60
+ /** Vertical size. `auto` is for a field that grows with its content (Textarea). */
61
+ height: {
62
+ md: "h-(--era-h-md)";
63
+ full: "h-full";
64
+ auto: "min-h-(--era-h-md)";
65
+ };
66
+ /**
67
+ * Side padding. `trailing` is for a field that holds a control at its end
68
+ * (the date picker's calendar trigger): the leading edge keeps the shared
69
+ * text inset so the text still lines up with every other field, and the
70
+ * trailing edge becomes the concentric inset for that sm-tier control, so
71
+ * its gap to the field edge equals its gap above and below.
72
+ */
73
+ pad: {
74
+ even: "px-(--era-field-px)";
75
+ trailing: "ps-(--era-field-px) pe-(--era-sm-inset-md)";
76
+ none: "px-0";
77
+ };
78
+ }, undefined, import("tailwind-variants").TVReturnTypeLike<{
79
+ /**
80
+ * `bare` drops the field chrome entirely — no fill, no well, no side
81
+ * padding. For an input nested in a container that is ALREADY the field:
82
+ * a Bar acting as a toolbar, a header row. Without it the input draws a
83
+ * second box inside the first, and at the same tier it fills its parent
84
+ * edge to edge so the two have no gap between them.
85
+ */
86
+ bare: {
87
+ false: "rounded-(--era-rd-md) bg-(--era-field-bg) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
88
+ true: "bg-transparent shadow-none";
89
+ };
90
+ /** Vertical size. `auto` is for a field that grows with its content (Textarea). */
91
+ height: {
92
+ md: "h-(--era-h-md)";
93
+ full: "h-full";
94
+ auto: "min-h-(--era-h-md)";
95
+ };
96
+ /**
97
+ * Side padding. `trailing` is for a field that holds a control at its end
98
+ * (the date picker's calendar trigger): the leading edge keeps the shared
99
+ * text inset so the text still lines up with every other field, and the
100
+ * trailing edge becomes the concentric inset for that sm-tier control, so
101
+ * its gap to the field edge equals its gap above and below.
102
+ */
103
+ pad: {
104
+ even: "px-(--era-field-px)";
105
+ trailing: "ps-(--era-field-px) pe-(--era-sm-inset-md)";
106
+ none: "px-0";
107
+ };
108
+ }, undefined>>;
@@ -0,0 +1,63 @@
1
+ import { tv } from '../../utils/index.js';
2
+ /**
3
+ * THE field. Every text field in the library paints this and nothing else.
4
+ *
5
+ * This module exists because it did not: the chrome below was hand-copied into
6
+ * Input, Textarea, CommandInput, ComboboxInput and all six date/time field
7
+ * inputs, and the copies had already drifted apart — some used `focus:`, others
8
+ * `focus-within:`, some omitted `era-interactive`, the date picker used a
9
+ * different leading padding, and the Notes fields had dropped the chrome
10
+ * entirely. Nine restatements of one idea is nine chances to be inconsistent,
11
+ * so there is now one statement and the rest import it.
12
+ *
13
+ * A component that needs something different asks for it as a VARIANT here,
14
+ * where every other field can see it — never by respelling the base.
15
+ *
16
+ * `tests/visual/input-consistency.spec.ts` measures the rendered result across
17
+ * routes and fails if any two fields differ.
18
+ */
19
+ export const inputVariants = tv({
20
+ base: 'flex w-full era-interactive text-body text-fg placeholder:text-muted',
21
+ variants: {
22
+ /**
23
+ * `bare` drops the field chrome entirely — no fill, no well, no side
24
+ * padding. For an input nested in a container that is ALREADY the field:
25
+ * a Bar acting as a toolbar, a header row. Without it the input draws a
26
+ * second box inside the first, and at the same tier it fills its parent
27
+ * edge to edge so the two have no gap between them.
28
+ */
29
+ bare: {
30
+ false: 'rounded-(--era-rd-md) bg-(--era-field-bg) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)',
31
+ true: 'bg-transparent shadow-none'
32
+ },
33
+ /** Vertical size. `auto` is for a field that grows with its content (Textarea). */
34
+ height: {
35
+ md: 'h-(--era-h-md)',
36
+ // h-full, NOT the md tier: a bare input's container IS the field, so it
37
+ // takes that container's height. Keeping h-md here made a bare input
38
+ // inside an h-md Bar overflow it by the bar's border — a −0.5px gap,
39
+ // which is the even-gap law failing by a hair rather than obviously.
40
+ full: 'h-full',
41
+ auto: 'min-h-(--era-h-md)'
42
+ },
43
+ /**
44
+ * Side padding. `trailing` is for a field that holds a control at its end
45
+ * (the date picker's calendar trigger): the leading edge keeps the shared
46
+ * text inset so the text still lines up with every other field, and the
47
+ * trailing edge becomes the concentric inset for that sm-tier control, so
48
+ * its gap to the field edge equals its gap above and below.
49
+ */
50
+ pad: {
51
+ even: 'px-(--era-field-px)',
52
+ trailing: 'ps-(--era-field-px) pe-(--era-sm-inset-md)',
53
+ none: 'px-0'
54
+ }
55
+ },
56
+ compoundVariants: [
57
+ // Chrome and padding travel together: a field with no chrome has no box to
58
+ // pad. Spelling this as a compound rather than leaving it to the caller is
59
+ // what stops `bare` from becoming a second, differently-padded field.
60
+ { bare: true, class: 'h-full px-0' }
61
+ ],
62
+ defaultVariants: { bare: false, height: 'md', pad: 'even' }
63
+ });
@@ -1,14 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { TimeField } from 'bits-ui';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from '../input/variants.js';
4
5
 
5
6
  let { ref = $bindable(null), class: className, ...restProps }: TimeField.InputProps = $props();
6
7
  </script>
7
8
 
8
9
  <TimeField.Input
9
10
  bind:ref
11
+ data-input-field
10
12
  class={cn(
11
- 'flex h-(--era-h-md) w-full items-center rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) text-body text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight) disabled:opacity-50 data-[invalid]:text-destructive',
13
+ inputVariants(),
14
+ 'items-center disabled:opacity-50 data-[invalid]:text-destructive',
12
15
  className
13
16
  )}
14
17
  {...restProps}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { TimeRangeField } from 'bits-ui';
3
3
  import { cn } from '../../utils/index.js';
4
+ import { inputVariants } from '../input/variants.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(null),
@@ -11,8 +12,10 @@
11
12
 
12
13
  <TimeRangeField.Input
13
14
  bind:ref
15
+ data-input-field
14
16
  class={cn(
15
- 'flex h-(--era-h-md) w-full items-center rounded-(--era-rd-md) bg-(--era-field-bg) px-(--era-field-px) text-body text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight) disabled:opacity-50 data-[invalid]:text-destructive',
17
+ inputVariants(),
18
+ 'items-center disabled:opacity-50 data-[invalid]:text-destructive',
16
19
  className
17
20
  )}
18
21
  {...restProps}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",