@signal9/era-ui 30.7.0 → 30.8.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.8.0](https://github.com/sig-nine/era-ui/compare/v30.7.0...v30.8.0) (2026-08-16)
2
+
3
+ ### Features
4
+
5
+ * **button:** busy is in-flight without a spinner ([e163c39](https://github.com/sig-nine/era-ui/commit/e163c39b33b4e73e179f59324b6797d64b7563e3))
6
+
1
7
  ## [30.7.0](https://github.com/sig-nine/era-ui/compare/v30.6.0...v30.7.0) (2026-08-16)
2
8
 
3
9
  ### Features
@@ -12,6 +12,7 @@
12
12
  variant,
13
13
  icon,
14
14
  loading,
15
+ busy,
15
16
  loadingLabel,
16
17
  children,
17
18
  class: className,
@@ -32,6 +33,7 @@
32
33
  {variant}
33
34
  {icon}
34
35
  {loading}
36
+ {busy}
35
37
  {loadingLabel}
36
38
  class={className}
37
39
  >
@@ -10,6 +10,7 @@
10
10
  variant,
11
11
  icon,
12
12
  loading,
13
+ busy,
13
14
  loadingLabel,
14
15
  children,
15
16
  class: className,
@@ -30,6 +31,7 @@
30
31
  {variant}
31
32
  {icon}
32
33
  {loading}
34
+ {busy}
33
35
  {loadingLabel}
34
36
  class={className}
35
37
  >
@@ -13,6 +13,7 @@
13
13
  active = false,
14
14
  icon = false,
15
15
  loading = false,
16
+ busy = false,
16
17
  loadingLabel,
17
18
  disabled,
18
19
  lead,
@@ -47,8 +48,35 @@
47
48
  *
48
49
  * The visible label is therefore HIDDEN in flight — the one thing this
49
50
  * trades away, and the reason `loadingLabel` exists.
51
+ *
52
+ * IF YOU DO NOT WANT A SPINNER, use `busy`. Not showing one is a
53
+ * supported posture, not a legacy one — see that prop.
50
54
  */
51
55
  loading?: boolean;
56
+ /**
57
+ * In flight, WITHOUT a spinner: disables the button and sets `aria-busy`,
58
+ * and changes nothing else. The label stays visible and legible.
59
+ *
60
+ * This exists because "show a spinner" is a taste, not a requirement,
61
+ * and era had only one answer. A consumer whose house style is that
62
+ * spinners should not appear had to spell the posture out themselves —
63
+ * `disabled` plus a restated `aria-busy` — and rely on knowing that
64
+ * `restProps` spreads last, which is a real fact about this component
65
+ * that no reader should have to depend on. Two words for two postures
66
+ * is clearer than one word plus a workaround.
67
+ *
68
+ * `loading` implies this: it is `busy` plus the spinner and the hidden
69
+ * label. What you get from `busy` alone is a control that is visibly
70
+ * disabled (the era-interactive opacity) and announced as busy, with
71
+ * its geometry and its ink untouched — which is also why it needs no
72
+ * `loadingLabel`: nothing about the button changed except that you
73
+ * cannot press it.
74
+ *
75
+ * Reported by a consumer whose user asked for no spinners anywhere.
76
+ * They were right that a fix for the width shift did not answer it: a
77
+ * spinner that no longer moves its neighbours is still a spinner.
78
+ */
79
+ busy?: boolean;
52
80
  /** Screen-reader-only label announced while `loading` is true. */
53
81
  loadingLabel?: string;
54
82
  /** Icon rendered BEFORE the label, outside it. Use this rather than putting
@@ -101,7 +129,10 @@
101
129
  children?: Snippet;
102
130
  } = $props();
103
131
 
104
- const effectiveDisabled = $derived(disabled || loading);
132
+ // `loading` is `busy` plus a spinner, so everything that is true of busy is
133
+ // true of loading. Only the spinner and the hidden label read `loading`.
134
+ const inFlight = $derived(loading || busy);
135
+ const effectiveDisabled = $derived(disabled || inFlight);
105
136
  </script>
106
137
 
107
138
  <!--
@@ -122,7 +153,7 @@
122
153
  <Button.Root
123
154
  bind:ref
124
155
  disabled={effectiveDisabled}
125
- aria-busy={loading || undefined}
156
+ aria-busy={inFlight || undefined}
126
157
  data-state={active ? 'on' : undefined}
127
158
  data-size={size}
128
159
  class={cn(
@@ -26,8 +26,35 @@ type $$ComponentProps = Button.RootProps & {
26
26
  *
27
27
  * The visible label is therefore HIDDEN in flight — the one thing this
28
28
  * trades away, and the reason `loadingLabel` exists.
29
+ *
30
+ * IF YOU DO NOT WANT A SPINNER, use `busy`. Not showing one is a
31
+ * supported posture, not a legacy one — see that prop.
29
32
  */
30
33
  loading?: boolean;
34
+ /**
35
+ * In flight, WITHOUT a spinner: disables the button and sets `aria-busy`,
36
+ * and changes nothing else. The label stays visible and legible.
37
+ *
38
+ * This exists because "show a spinner" is a taste, not a requirement,
39
+ * and era had only one answer. A consumer whose house style is that
40
+ * spinners should not appear had to spell the posture out themselves —
41
+ * `disabled` plus a restated `aria-busy` — and rely on knowing that
42
+ * `restProps` spreads last, which is a real fact about this component
43
+ * that no reader should have to depend on. Two words for two postures
44
+ * is clearer than one word plus a workaround.
45
+ *
46
+ * `loading` implies this: it is `busy` plus the spinner and the hidden
47
+ * label. What you get from `busy` alone is a control that is visibly
48
+ * disabled (the era-interactive opacity) and announced as busy, with
49
+ * its geometry and its ink untouched — which is also why it needs no
50
+ * `loadingLabel`: nothing about the button changed except that you
51
+ * cannot press it.
52
+ *
53
+ * Reported by a consumer whose user asked for no spinners anywhere.
54
+ * They were right that a fix for the width shift did not answer it: a
55
+ * spinner that no longer moves its neighbours is still a spinner.
56
+ */
57
+ busy?: boolean;
31
58
  /** Screen-reader-only label announced while `loading` is true. */
32
59
  loadingLabel?: string;
33
60
  /** Icon rendered BEFORE the label, outside it. Use this rather than putting
@@ -8,5 +8,7 @@ export interface ButtonVariants {
8
8
  icon?: boolean;
9
9
  active?: boolean;
10
10
  loading?: boolean;
11
+ /** In flight without a spinner — see button.svelte. */
12
+ busy?: boolean;
11
13
  loadingLabel?: string;
12
14
  }
@@ -10,6 +10,7 @@
10
10
  variant,
11
11
  icon,
12
12
  loading,
13
+ busy,
13
14
  loadingLabel,
14
15
  children,
15
16
  class: className,
@@ -30,6 +31,7 @@
30
31
  {variant}
31
32
  {icon}
32
33
  {loading}
34
+ {busy}
33
35
  {loadingLabel}
34
36
  class={className}
35
37
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "30.7.0",
3
+ "version": "30.8.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",