stimeo-ui 0.2.1 → 0.4.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +160 -0
  3. data/dist/controllers/accordion_controller.js +10 -0
  4. data/dist/controllers/alert_dialog_controller.js +32 -5
  5. data/dist/controllers/announcer_controller.js +255 -20
  6. data/dist/controllers/breadcrumb_controller.js +225 -13
  7. data/dist/controllers/calendar_controller.js +89 -22
  8. data/dist/controllers/carousel_controller.js +47 -6
  9. data/dist/controllers/collapsible_controller.js +2 -2
  10. data/dist/controllers/color_picker_controller.js +92 -7
  11. data/dist/controllers/combobox_controller.js +162 -23
  12. data/dist/controllers/command_palette_controller.js +226 -22
  13. data/dist/controllers/confirm_controller.js +32 -5
  14. data/dist/controllers/context_menu_controller.js +32 -10
  15. data/dist/controllers/countdown_controller.js +112 -12
  16. data/dist/controllers/data_grid_controller.js +82 -4
  17. data/dist/controllers/date_range_picker_controller.js +77 -3
  18. data/dist/controllers/dialog_controller.js +32 -5
  19. data/dist/controllers/drawer_controller.js +32 -5
  20. data/dist/controllers/editable_controller.js +1 -0
  21. data/dist/controllers/empty_state_controller.js +24 -10
  22. data/dist/controllers/focus_controller.js +32 -5
  23. data/dist/controllers/form_validation_controller.js +1 -1
  24. data/dist/controllers/frame_loading_controller.js +177 -15
  25. data/dist/controllers/intersection_controller.js +36 -11
  26. data/dist/controllers/lazy_frame_controller.js +31 -10
  27. data/dist/controllers/listbox_controller.js +257 -53
  28. data/dist/controllers/local_time_controller.js +102 -8
  29. data/dist/controllers/menu_controller.js +104 -17
  30. data/dist/controllers/menubar_controller.js +415 -63
  31. data/dist/controllers/meter_controller.js +145 -26
  32. data/dist/controllers/multi_select_controller.js +312 -29
  33. data/dist/controllers/navigation_menu_controller.js +154 -27
  34. data/dist/controllers/network_status_controller.js +28 -8
  35. data/dist/controllers/number_input_controller.js +7 -0
  36. data/dist/controllers/otp_controller.js +18 -1
  37. data/dist/controllers/overflow_indicator_controller.js +81 -13
  38. data/dist/controllers/overflow_menu_controller.js +408 -57
  39. data/dist/controllers/pagination_controller.js +163 -32
  40. data/dist/controllers/persist_controller.js +6 -6
  41. data/dist/controllers/pointer_drag_controller.js +9 -1
  42. data/dist/controllers/popover_controller.js +2 -2
  43. data/dist/controllers/progress_controller.js +116 -9
  44. data/dist/controllers/radio_group_controller.js +22 -3
  45. data/dist/controllers/range_slider_controller.js +100 -9
  46. data/dist/controllers/rating_controller.js +69 -2
  47. data/dist/controllers/read_more_controller.js +63 -19
  48. data/dist/controllers/relative_time_controller.js +133 -12
  49. data/dist/controllers/resizable_controller.js +65 -1
  50. data/dist/controllers/roving_controller.js +17 -2
  51. data/dist/controllers/scroll_area_controller.js +86 -12
  52. data/dist/controllers/scroll_restore_controller.js +1 -1
  53. data/dist/controllers/scroll_visibility_controller.js +33 -3
  54. data/dist/controllers/scrollspy_controller.js +346 -73
  55. data/dist/controllers/separator_controller.js +9 -0
  56. data/dist/controllers/sidebar_controller.js +37 -8
  57. data/dist/controllers/skeleton_controller.js +73 -20
  58. data/dist/controllers/slider_controller.js +49 -8
  59. data/dist/controllers/sortable_controller.js +34 -3
  60. data/dist/controllers/spinner_controller.js +228 -27
  61. data/dist/controllers/step_indicator_controller.js +82 -5
  62. data/dist/controllers/stick_to_bottom_controller.js +61 -10
  63. data/dist/controllers/sticky_observer_controller.js +32 -11
  64. data/dist/controllers/switch_controller.js +1 -0
  65. data/dist/controllers/tabs_controller.js +26 -3
  66. data/dist/controllers/tags_input_controller.js +22 -2
  67. data/dist/controllers/theme_controller.js +22 -3
  68. data/dist/controllers/time_picker_controller.js +20 -1
  69. data/dist/controllers/toast_controller.js +4 -5
  70. data/dist/controllers/toggle_group_controller.js +23 -2
  71. data/dist/controllers/toolbar_controller.js +230 -31
  72. data/dist/controllers/tree_view_controller.js +467 -51
  73. data/dist/index.js +4507 -907
  74. data/lib/stimeo/ui/version.rb +2 -3
  75. metadata +2 -2
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/meter_controller.ts
4
4
 
5
+ // src/utils/announce.ts
6
+ function announce(message, options = {}) {
7
+ const text = message.trim();
8
+ if (text.length === 0) return;
9
+ window.dispatchEvent(
10
+ new CustomEvent("stimeo--announcer:announce", {
11
+ detail: { message: text, assertive: options.assertive === true }
12
+ })
13
+ );
14
+ }
15
+ function fillTemplate(template, values) {
16
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
17
+ const replacement = values[name];
18
+ return replacement === void 0 ? match : String(replacement);
19
+ });
20
+ }
21
+
5
22
  // src/utils/coerce.ts
6
23
  function toFiniteNumber(raw) {
7
24
  if (raw === null || raw === void 0 || raw === "") return null;
@@ -9,10 +26,61 @@ function toFiniteNumber(raw) {
9
26
  return Number.isFinite(value) ? value : null;
10
27
  }
11
28
 
29
+ // src/utils/microtask_coalescer.ts
30
+ var MicrotaskCoalescer = class {
31
+ #run;
32
+ #queued = false;
33
+ #active = false;
34
+ #generation = 0;
35
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
36
+ constructor(run) {
37
+ this.#run = run;
38
+ }
39
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
40
+ activate() {
41
+ this.#active = true;
42
+ }
43
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
44
+ cancel() {
45
+ this.#active = false;
46
+ this.#queued = false;
47
+ this.#generation += 1;
48
+ }
49
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
50
+ schedule() {
51
+ if (!this.#active || this.#queued) return;
52
+ this.#queued = true;
53
+ const generation = this.#generation;
54
+ queueMicrotask(() => {
55
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
56
+ this.#queued = false;
57
+ this.#run();
58
+ });
59
+ }
60
+ };
61
+
62
+ // src/utils/range.ts
63
+ function rangeFraction(value, min, max) {
64
+ const span = max - min;
65
+ if (!(span > 0)) return 0;
66
+ const clamped = Math.min(max, Math.max(min, value));
67
+ let fraction;
68
+ if (Number.isFinite(span)) {
69
+ fraction = (clamped - min) / span;
70
+ } else {
71
+ const scale = Math.max(Math.abs(min), Math.abs(max));
72
+ fraction = (clamped / scale - min / scale) / (max / scale - min / scale);
73
+ }
74
+ if (!Number.isFinite(fraction)) return 0;
75
+ return Math.min(1, Math.max(0, fraction));
76
+ }
77
+
12
78
  // src/controllers/meter_controller.ts
79
+ var OWNED_VALUE_TEXT = "data-stimeo--meter-owns-valuetext";
13
80
  var MeterController = class extends Controller {
14
81
  static targets = ["bar"];
15
82
  static values = {
83
+ announceText: { type: String, default: "" },
16
84
  value: { type: Number, default: 0 },
17
85
  min: { type: Number, default: 0 },
18
86
  max: { type: Number, default: 100 },
@@ -23,9 +91,24 @@ var MeterController = class extends Controller {
23
91
  };
24
92
  static actions = ["setValue"];
25
93
  static events = ["change"];
94
+ /**
95
+ * Collapses a morph that swaps several render inputs at once into one repaint.
96
+ * A single update usually rewrites the whole set, and each Value would otherwise
97
+ * repaint on its own.
98
+ */
99
+ #repaint = new MicrotaskCoalescer(() => {
100
+ this.#render();
101
+ });
102
+ /** The segment last announced, so only a change is read out. */
103
+ #announcedState = null;
26
104
  connect() {
105
+ this.#repaint.activate();
27
106
  this.#render();
28
107
  }
108
+ /** Closes the window in which a queued repaint may still run. */
109
+ disconnect() {
110
+ this.#repaint.cancel();
111
+ }
29
112
  /**
30
113
  * Updates the measured value from an action param (`amount`) or a
31
114
  * `detail.value` CustomEvent, syncs ARIA and `data-state`, and dispatches
@@ -35,59 +118,95 @@ var MeterController = class extends Controller {
35
118
  const next = toFiniteNumber(event.params?.amount ?? event.detail?.value);
36
119
  if (next === null) return;
37
120
  this.valueValue = this.#clamp(next);
38
- this.#render();
39
- this.dispatch("change", {
40
- detail: { value: this.valueValue, ratio: this.#ratio, state: this.#state }
41
- });
121
+ const reading = this.#render();
122
+ this.dispatch("change", { detail: reading });
123
+ if (reading.state !== this.#announcedState) {
124
+ this.#announcedState = reading.state;
125
+ announce(
126
+ fillTemplate(this.announceTextValue, { state: reading.state, value: reading.value })
127
+ );
128
+ }
129
+ }
130
+ /** Repaints when application code (or a Turbo morph) changes `value` at runtime. */
131
+ valueValueChanged() {
132
+ this.#repaint.schedule();
133
+ }
134
+ /** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
135
+ minValueChanged() {
136
+ this.#repaint.schedule();
137
+ }
138
+ /** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
139
+ maxValueChanged() {
140
+ this.#repaint.schedule();
141
+ }
142
+ /** Repaints when application code (or a Turbo morph) changes `low` at runtime. */
143
+ lowValueChanged() {
144
+ this.#repaint.schedule();
145
+ }
146
+ /** Repaints when application code (or a Turbo morph) changes `high` at runtime. */
147
+ highValueChanged() {
148
+ this.#repaint.schedule();
149
+ }
150
+ /** Repaints when application code (or a Turbo morph) changes `valueText` at runtime. */
151
+ valueTextValueChanged() {
152
+ this.#repaint.schedule();
42
153
  }
43
154
  /** Clamps `raw` into the configured `[min, max]` range. */
44
155
  #clamp(raw) {
45
156
  return Math.min(this.maxValue, Math.max(this.minValue, raw));
46
157
  }
47
- /** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */
48
- get #ratio() {
49
- const span = this.maxValue - this.minValue;
50
- if (span <= 0) return 0;
51
- return (this.#clamp(this.valueValue) - this.minValue) / span;
52
- }
53
158
  /** Whether a threshold attribute is present (absent = no threshold). */
54
159
  #hasThreshold(name) {
55
160
  return this.element.hasAttribute(`data-stimeo--meter-${name}-value`);
56
161
  }
57
162
  /**
58
- * Classifies the value into a `low`/`medium`/`high` segment. Values at or
59
- * below `low` are `low`; at or above `high` are `high`; otherwise `medium`.
60
- * With neither threshold present, everything is `medium`.
163
+ * Classifies `value` into a `low`/`medium`/`high` segment. Values at or below
164
+ * `low` are `low`; at or above `high` are `high`; otherwise `medium`. With
165
+ * neither threshold present, everything is `medium`.
61
166
  */
62
- get #state() {
63
- const value = this.#clamp(this.valueValue);
167
+ #stateOf(value) {
64
168
  if (this.#hasThreshold("low") && value <= this.lowValue) return "low";
65
169
  if (this.#hasThreshold("high") && value >= this.highValue) return "high";
66
170
  return "medium";
67
171
  }
68
- /** Reflects value/range onto ARIA, the segment onto `data-state`, and the ratio. */
172
+ /**
173
+ * Reflects value/range onto ARIA, the segment onto `data-state`, and the ratio.
174
+ * The reading is derived once and returned, so the `change` detail reports the
175
+ * same numbers the DOM just received.
176
+ */
69
177
  #render() {
70
178
  const value = this.#clamp(this.valueValue);
179
+ const reading = {
180
+ value,
181
+ ratio: rangeFraction(value, this.minValue, this.maxValue),
182
+ state: this.#stateOf(value)
183
+ };
71
184
  this.element.setAttribute("aria-valuemin", String(this.minValue));
72
185
  this.element.setAttribute("aria-valuemax", String(this.maxValue));
73
- this.element.setAttribute("aria-valuenow", String(value));
74
- this.element.style.setProperty("--stimeo-meter-ratio", String(this.#ratio));
75
- this.element.setAttribute("data-state", this.#state);
76
- this.#applyValueText(value);
186
+ this.element.setAttribute("aria-valuenow", String(reading.value));
187
+ this.element.style.setProperty("--stimeo-meter-ratio", String(reading.ratio));
188
+ this.element.setAttribute("data-state", reading.state);
189
+ this.#applyValueText(reading);
190
+ return reading;
77
191
  }
78
192
  /**
79
193
  * Sets `aria-valuetext` from the consumer-provided template, substituting
80
- * `{value}`, `{percent}`, and `{state}`. Kept i18n-neutral in the library;
81
- * cleared when no template is given.
194
+ * `{value}`, `{percent}`, and `{state}`. Kept i18n-neutral in the library.
195
+ * With no template the attribute belongs to the consumer, so only a text this
196
+ * controller wrote is taken back ({@link OWNED_VALUE_TEXT}).
82
197
  */
83
- #applyValueText(value) {
198
+ #applyValueText({ value, ratio, state }) {
84
199
  if (this.valueTextValue.length === 0) {
85
- this.element.removeAttribute("aria-valuetext");
200
+ if (this.element.hasAttribute(OWNED_VALUE_TEXT)) {
201
+ this.element.removeAttribute("aria-valuetext");
202
+ this.element.removeAttribute(OWNED_VALUE_TEXT);
203
+ }
86
204
  return;
87
205
  }
88
- const percent = Math.round(this.#ratio * 100);
89
- const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent)).replaceAll("{state}", this.#state);
206
+ const percent = Math.round(ratio * 100);
207
+ const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent)).replaceAll("{state}", state);
90
208
  this.element.setAttribute("aria-valuetext", text);
209
+ this.element.setAttribute(OWNED_VALUE_TEXT, "");
91
210
  }
92
211
  };
93
212