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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +160 -0
- data/dist/controllers/accordion_controller.js +10 -0
- data/dist/controllers/alert_dialog_controller.js +32 -5
- data/dist/controllers/announcer_controller.js +255 -20
- data/dist/controllers/breadcrumb_controller.js +225 -13
- data/dist/controllers/calendar_controller.js +89 -22
- data/dist/controllers/carousel_controller.js +47 -6
- data/dist/controllers/collapsible_controller.js +2 -2
- data/dist/controllers/color_picker_controller.js +92 -7
- data/dist/controllers/combobox_controller.js +162 -23
- data/dist/controllers/command_palette_controller.js +226 -22
- data/dist/controllers/confirm_controller.js +32 -5
- data/dist/controllers/context_menu_controller.js +32 -10
- data/dist/controllers/countdown_controller.js +112 -12
- data/dist/controllers/data_grid_controller.js +82 -4
- data/dist/controllers/date_range_picker_controller.js +77 -3
- data/dist/controllers/dialog_controller.js +32 -5
- data/dist/controllers/drawer_controller.js +32 -5
- data/dist/controllers/editable_controller.js +1 -0
- data/dist/controllers/empty_state_controller.js +24 -10
- data/dist/controllers/focus_controller.js +32 -5
- data/dist/controllers/form_validation_controller.js +1 -1
- data/dist/controllers/frame_loading_controller.js +177 -15
- data/dist/controllers/intersection_controller.js +36 -11
- data/dist/controllers/lazy_frame_controller.js +31 -10
- data/dist/controllers/listbox_controller.js +257 -53
- data/dist/controllers/local_time_controller.js +102 -8
- data/dist/controllers/menu_controller.js +104 -17
- data/dist/controllers/menubar_controller.js +415 -63
- data/dist/controllers/meter_controller.js +145 -26
- data/dist/controllers/multi_select_controller.js +312 -29
- data/dist/controllers/navigation_menu_controller.js +154 -27
- data/dist/controllers/network_status_controller.js +28 -8
- data/dist/controllers/number_input_controller.js +7 -0
- data/dist/controllers/otp_controller.js +18 -1
- data/dist/controllers/overflow_indicator_controller.js +81 -13
- data/dist/controllers/overflow_menu_controller.js +408 -57
- data/dist/controllers/pagination_controller.js +163 -32
- data/dist/controllers/persist_controller.js +6 -6
- data/dist/controllers/pointer_drag_controller.js +9 -1
- data/dist/controllers/popover_controller.js +2 -2
- data/dist/controllers/progress_controller.js +116 -9
- data/dist/controllers/radio_group_controller.js +22 -3
- data/dist/controllers/range_slider_controller.js +100 -9
- data/dist/controllers/rating_controller.js +69 -2
- data/dist/controllers/read_more_controller.js +63 -19
- data/dist/controllers/relative_time_controller.js +133 -12
- data/dist/controllers/resizable_controller.js +65 -1
- data/dist/controllers/roving_controller.js +17 -2
- data/dist/controllers/scroll_area_controller.js +86 -12
- data/dist/controllers/scroll_restore_controller.js +1 -1
- data/dist/controllers/scroll_visibility_controller.js +33 -3
- data/dist/controllers/scrollspy_controller.js +346 -73
- data/dist/controllers/separator_controller.js +9 -0
- data/dist/controllers/sidebar_controller.js +37 -8
- data/dist/controllers/skeleton_controller.js +73 -20
- data/dist/controllers/slider_controller.js +49 -8
- data/dist/controllers/sortable_controller.js +34 -3
- data/dist/controllers/spinner_controller.js +228 -27
- data/dist/controllers/step_indicator_controller.js +82 -5
- data/dist/controllers/stick_to_bottom_controller.js +61 -10
- data/dist/controllers/sticky_observer_controller.js +32 -11
- data/dist/controllers/switch_controller.js +1 -0
- data/dist/controllers/tabs_controller.js +26 -3
- data/dist/controllers/tags_input_controller.js +22 -2
- data/dist/controllers/theme_controller.js +22 -3
- data/dist/controllers/time_picker_controller.js +20 -1
- data/dist/controllers/toast_controller.js +4 -5
- data/dist/controllers/toggle_group_controller.js +23 -2
- data/dist/controllers/toolbar_controller.js +230 -31
- data/dist/controllers/tree_view_controller.js +467 -51
- data/dist/index.js +4507 -907
- data/lib/stimeo/ui/version.rb +2 -3
- 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
|
-
|
|
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
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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(
|
|
75
|
-
this.element.setAttribute("data-state",
|
|
76
|
-
this.#applyValueText(
|
|
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
|
-
*
|
|
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.
|
|
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(
|
|
89
|
-
const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent)).replaceAll("{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
|
|