@signal9/era-ui 30.4.0 → 30.5.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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [30.5.0](https://github.com/sig-nine/era-ui/compare/v30.4.0...v30.5.0) (2026-08-16)
2
+
3
+ ### Features
4
+
5
+ * **meter:** start and end snippets caption what the range means ([1a6bf54](https://github.com/sig-nine/era-ui/commit/1a6bf544b5b3992649f66c5e9a05f24057f495cd))
6
+
1
7
  ## [30.4.0](https://github.com/sig-nine/era-ui/compare/v30.3.0...v30.4.0) (2026-08-16)
2
8
 
3
9
  ### Features
@@ -35,6 +35,7 @@
35
35
  </script>
36
36
 
37
37
  <script lang="ts">
38
+ import type { Snippet } from 'svelte';
38
39
  import { Meter } from 'bits-ui';
39
40
  import { cn } from '../../utils/index.js';
40
41
 
@@ -44,6 +45,8 @@
44
45
  min = 0,
45
46
  max = 100,
46
47
  tone = 'default',
48
+ start,
49
+ end,
47
50
  class: className,
48
51
  ...restProps
49
52
  }: Meter.RootProps & {
@@ -57,30 +60,73 @@
57
60
  * `aria-label` / a visible label, never rely on the hue alone.
58
61
  */
59
62
  tone?: 'default' | 'accent' | 'destructive' | 'success' | 'warning' | 'info';
63
+ /**
64
+ * Captions under the two ends of the bar — what the range MEANS.
65
+ *
66
+ * A meter states a proportion and leaves the reader to work out what of.
67
+ * "0m used" on the left and "50h before approval" on the right is not
68
+ * decoration, it is the reading; every consumer of a bare meter ends up
69
+ * hand-rolling the same flex row underneath it, which is where this came
70
+ * from. `start` is left, `end` is right, either may stand alone.
71
+ *
72
+ * NOT ANNOUNCED. They are captions, so a screen reader gets the meter's
73
+ * value and its own `aria-label`, not these. If the range needs saying
74
+ * out loud, say it in the label too — the same rule `tone` carries.
75
+ *
76
+ * THE DOM IS UNCHANGED WITHOUT THEM. With neither snippet this renders
77
+ * exactly the element it always did, so `class` still lands on the track
78
+ * and the geometry audits still walk the same box. Passing one wraps the
79
+ * track in a column, and `class` still lands on the track.
80
+ */
81
+ start?: Snippet;
82
+ end?: Snippet;
60
83
  } = $props();
61
84
 
62
85
  let percentage = $derived(((value - min) / (max - min)) * 100);
63
86
  </script>
64
87
 
65
- <Meter.Root
66
- bind:ref
67
- {value}
68
- {min}
69
- {max}
70
- data-tone={tone}
71
- class={cn(
72
- // bg-field, not bg-elevated: the track is recessed (era-track-well), so it
73
- // wears the recessed tier. See Checkbox for the full version of this note.
74
- // era-track-well, not shadow-well: the fill below covers the whole box,
75
- // and an inset shadow paints under its own descendants so the plain
76
- // well survived only on the empty side of the value.
77
- 'era-track-well h-(--era-sp) w-full overflow-hidden rounded-control bg-field',
78
- className
79
- )}
80
- {...restProps}
81
- >
82
- <div
83
- class={meterFillVariants({ tone })}
84
- style="transform: translateX(-{100 - percentage}%)"
85
- ></div>
86
- </Meter.Root>
88
+ {#snippet track()}
89
+ <Meter.Root
90
+ bind:ref
91
+ {value}
92
+ {min}
93
+ {max}
94
+ data-tone={tone}
95
+ class={cn(
96
+ // bg-field, not bg-elevated: the track is recessed (era-track-well), so it
97
+ // wears the recessed tier. See Checkbox for the full version of this note.
98
+ // era-track-well, not shadow-well: the fill below covers the whole box,
99
+ // and an inset shadow paints under its own descendants — so the plain
100
+ // well survived only on the empty side of the value.
101
+ 'era-track-well h-(--era-sp) w-full overflow-hidden rounded-control bg-field',
102
+ className
103
+ )}
104
+ {...restProps}
105
+ >
106
+ <div
107
+ class={meterFillVariants({ tone })}
108
+ style="transform: translateX(-{100 - percentage}%)"
109
+ ></div>
110
+ </Meter.Root>
111
+ {/snippet}
112
+
113
+ <!--
114
+ The wrapper exists ONLY when a caption does. Rendering it always would change
115
+ the element every existing consumer measures and styles — the same opt-in the
116
+ Button and Chip label boxes are built on, for the same reason.
117
+
118
+ `justify-between` rather than a grid: with one caption it pins to its own end,
119
+ which is what a lone "0 used" or a lone "quota" should do. min-w-0 + truncate
120
+ because a caption is prose and the bar's width is not negotiable.
121
+ -->
122
+ {#if start || end}
123
+ <div class="flex flex-col gap-gutter">
124
+ {@render track()}
125
+ <div class="flex items-baseline justify-between gap-gutter text-body text-muted">
126
+ <span class="min-w-0 truncate">{@render start?.()}</span>
127
+ <span class="min-w-0 truncate text-right">{@render end?.()}</span>
128
+ </div>
129
+ </div>
130
+ {:else}
131
+ {@render track()}
132
+ {/if}
@@ -26,6 +26,7 @@ export declare const meterFillVariants: import("tailwind-variants").TVReturnType
26
26
  info: "bg-info";
27
27
  };
28
28
  }, undefined>>;
29
+ import type { Snippet } from 'svelte';
29
30
  import { Meter } from 'bits-ui';
30
31
  type $$ComponentProps = Meter.RootProps & {
31
32
  /**
@@ -38,6 +39,26 @@ type $$ComponentProps = Meter.RootProps & {
38
39
  * `aria-label` / a visible label, never rely on the hue alone.
39
40
  */
40
41
  tone?: 'default' | 'accent' | 'destructive' | 'success' | 'warning' | 'info';
42
+ /**
43
+ * Captions under the two ends of the bar — what the range MEANS.
44
+ *
45
+ * A meter states a proportion and leaves the reader to work out what of.
46
+ * "0m used" on the left and "50h before approval" on the right is not
47
+ * decoration, it is the reading; every consumer of a bare meter ends up
48
+ * hand-rolling the same flex row underneath it, which is where this came
49
+ * from. `start` is left, `end` is right, either may stand alone.
50
+ *
51
+ * NOT ANNOUNCED. They are captions, so a screen reader gets the meter's
52
+ * value and its own `aria-label`, not these. If the range needs saying
53
+ * out loud, say it in the label too — the same rule `tone` carries.
54
+ *
55
+ * THE DOM IS UNCHANGED WITHOUT THEM. With neither snippet this renders
56
+ * exactly the element it always did, so `class` still lands on the track
57
+ * and the geometry audits still walk the same box. Passing one wraps the
58
+ * track in a column, and `class` still lands on the track.
59
+ */
60
+ start?: Snippet;
61
+ end?: Snippet;
41
62
  };
42
63
  declare const Meter: import("svelte").Component<$$ComponentProps, {}, "ref">;
43
64
  type Meter = ReturnType<typeof Meter>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "30.4.0",
3
+ "version": "30.5.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",