stimeo-ui 0.3.0 → 0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +124 -0
- data/dist/controllers/alert_dialog_controller.js +32 -5
- data/dist/controllers/announcer_controller.js +255 -20
- data/dist/controllers/aspect_ratio_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +5 -1
- data/dist/controllers/carousel_controller.js +5 -1
- data/dist/controllers/clipboard_controller.js +8 -3
- data/dist/controllers/collapsible_controller.js +4 -1
- data/dist/controllers/color_picker_controller.js +52 -2
- data/dist/controllers/command_palette_controller.js +32 -5
- data/dist/controllers/confirm_controller.js +32 -5
- data/dist/controllers/context_menu_controller.js +2 -2
- data/dist/controllers/countdown_controller.js +117 -13
- data/dist/controllers/date_range_picker_controller.js +55 -1
- data/dist/controllers/dialog_controller.js +32 -5
- data/dist/controllers/direct_upload_controller.js +3 -3
- data/dist/controllers/drawer_controller.js +32 -5
- data/dist/controllers/empty_state_controller.js +128 -23
- data/dist/controllers/flash_controller.js +161 -21
- data/dist/controllers/focus_controller.js +32 -5
- data/dist/controllers/form_validation_controller.js +8 -2
- data/dist/controllers/frame_loading_controller.js +261 -27
- data/dist/controllers/highlight_controller.js +38 -1
- data/dist/controllers/idle_controller.js +13 -2
- data/dist/controllers/local_time_controller.js +102 -6
- data/dist/controllers/masonry_controller.js +1 -1
- data/dist/controllers/meter_controller.js +147 -26
- data/dist/controllers/network_status_controller.js +29 -11
- data/dist/controllers/number_input_controller.js +191 -24
- data/dist/controllers/overflow_menu_controller.js +34 -7
- data/dist/controllers/pagination_controller.js +5 -1
- data/dist/controllers/password_strength_controller.js +1 -1
- data/dist/controllers/pointer_drag_controller.js +10 -0
- data/dist/controllers/portal_controller.js +10 -0
- data/dist/controllers/progress_controller.js +123 -12
- data/dist/controllers/range_slider_controller.js +449 -93
- data/dist/controllers/rating_controller.js +55 -0
- data/dist/controllers/relative_time_controller.js +135 -12
- data/dist/controllers/scroll_area_controller.js +1 -1
- data/dist/controllers/separator_controller.js +13 -17
- data/dist/controllers/sidebar_controller.js +37 -8
- data/dist/controllers/skeleton_controller.js +143 -22
- data/dist/controllers/slider_controller.js +342 -50
- data/dist/controllers/spinner_controller.js +244 -28
- data/dist/controllers/step_indicator_controller.js +85 -6
- data/dist/controllers/stepper_controller.js +2 -0
- data/dist/controllers/stick_to_bottom_controller.js +60 -10
- data/dist/controllers/switch_controller.js +162 -18
- data/dist/controllers/textarea_autosize_controller.js +1 -1
- data/dist/controllers/time_picker_controller.js +6 -3
- data/dist/controllers/tree_view_controller.js +19 -1
- data/dist/index.js +2278 -596
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/overflow_menu_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/before_cache_reset.ts
|
|
6
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
8
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
9
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
10
|
+
static #onBeforeCache = () => {
|
|
11
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
12
|
+
};
|
|
13
|
+
#rewind;
|
|
14
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
15
|
+
constructor(rewind) {
|
|
16
|
+
this.#rewind = rewind;
|
|
17
|
+
}
|
|
18
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
19
|
+
activate() {
|
|
20
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
21
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
22
|
+
if (first) {
|
|
23
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
27
|
+
deactivate() {
|
|
28
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
29
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
30
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
5
34
|
// src/utils/focus_candidate.ts
|
|
6
35
|
function inheritsFieldsetDisabled(control) {
|
|
7
36
|
let fieldset = control.closest("fieldset[disabled]");
|
|
@@ -161,7 +190,7 @@ var INDEX = "data-stimeo--overflow-menu-index";
|
|
|
161
190
|
var BOUNDARY = "data-stimeo--overflow-menu-boundary";
|
|
162
191
|
var SAVED_ROLE = "data-stimeo--overflow-menu-role";
|
|
163
192
|
var SAVED_TABINDEX = "data-stimeo--overflow-menu-tabindex";
|
|
164
|
-
var SAVED_MENU_TARGET = "data-stimeo--overflow-menu-menu
|
|
193
|
+
var SAVED_MENU_TARGET = "data-stimeo--overflow-menu-saved-menu";
|
|
165
194
|
var MENU_TARGET = "data-stimeo--menu-target";
|
|
166
195
|
var BOOKKEEPING = [BANKED, INDEX, SAVED_ROLE, SAVED_TABINDEX, SAVED_MENU_TARGET];
|
|
167
196
|
var OverflowMenuController = class extends Controller {
|
|
@@ -183,17 +212,15 @@ var OverflowMenuController = class extends Controller {
|
|
|
183
212
|
#lastHidden = null;
|
|
184
213
|
/** The `tabindex` this instance lends the root for the focus fallback. */
|
|
185
214
|
#tabindex = new TabindexLoan();
|
|
186
|
-
/** Hands Turbo a pristine snapshot
|
|
187
|
-
#
|
|
188
|
-
this.#restoreAll();
|
|
189
|
-
};
|
|
215
|
+
/** Hands Turbo a pristine snapshot of the bar, with every item back in place. */
|
|
216
|
+
#beforeCache = new BeforeCacheReset(() => this.#restoreAll());
|
|
190
217
|
connect() {
|
|
191
218
|
if (!this.hasItemsTarget || !this.hasMoreTarget) return;
|
|
192
219
|
const trigger = this.#trigger();
|
|
193
220
|
if (trigger !== null && this.#isBareTrigger(trigger)) {
|
|
194
221
|
trigger.textContent = this.moreLabelValue;
|
|
195
222
|
}
|
|
196
|
-
|
|
223
|
+
this.#beforeCache.activate();
|
|
197
224
|
this.#layout.observe(this.element);
|
|
198
225
|
this.#layout.observeViewport();
|
|
199
226
|
this.update();
|
|
@@ -201,7 +228,7 @@ var OverflowMenuController = class extends Controller {
|
|
|
201
228
|
disconnect() {
|
|
202
229
|
this.#layout.disconnect();
|
|
203
230
|
this.#timers.clearAll();
|
|
204
|
-
|
|
231
|
+
this.#beforeCache.deactivate();
|
|
205
232
|
this.#restoreAll();
|
|
206
233
|
this.#lastHidden = null;
|
|
207
234
|
}
|
|
@@ -132,7 +132,11 @@ var PaginationController = class _PaginationController extends Controller {
|
|
|
132
132
|
if (!Object.is(page, this.pageValue)) this.pageValue = page;
|
|
133
133
|
this.#render();
|
|
134
134
|
}
|
|
135
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* Syncs `aria-current` on the page buttons and the prev/next `disabled` state.
|
|
137
|
+
*
|
|
138
|
+
* @stimeoRenderRoot
|
|
139
|
+
*/
|
|
136
140
|
#render() {
|
|
137
141
|
const page = this.#page;
|
|
138
142
|
for (const button of this.pageTargets) {
|
|
@@ -126,7 +126,7 @@ var PasswordStrengthController = class _PasswordStrengthController extends Contr
|
|
|
126
126
|
this.#toggle("data-strength", band, band.length > 0);
|
|
127
127
|
this.#toggle("data-below-min", "true", score > 0 && score < this.minScoreValue);
|
|
128
128
|
const ratio = max > 0 ? score / max : 0;
|
|
129
|
-
this.element.style.setProperty("--stimeo
|
|
129
|
+
this.element.style.setProperty("--stimeo--password-strength", String(ratio));
|
|
130
130
|
}
|
|
131
131
|
#writeLabel(label) {
|
|
132
132
|
if (this.hasLabelTarget) this.labelTarget.textContent = label;
|
|
@@ -12,6 +12,16 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
12
12
|
var DetachGate = class _DetachGate {
|
|
13
13
|
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
14
14
|
#pending = false;
|
|
15
|
+
/**
|
|
16
|
+
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
17
|
+
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
18
|
+
* reconnect half of an in-page move from a first connect: a controller whose
|
|
19
|
+
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
20
|
+
* counter) must skip it for the move, where nothing actually restarted.
|
|
21
|
+
*/
|
|
22
|
+
get pending() {
|
|
23
|
+
return this.#pending;
|
|
24
|
+
}
|
|
15
25
|
/**
|
|
16
26
|
* True when the disconnect is definitely a real detach — the element left
|
|
17
27
|
* the document, or `data-controller` no longer lists the identifier. False
|
|
@@ -6,6 +6,16 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
6
6
|
var DetachGate = class _DetachGate {
|
|
7
7
|
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
8
8
|
#pending = false;
|
|
9
|
+
/**
|
|
10
|
+
* True while a probe is queued — the last disconnect was ambiguous and no
|
|
11
|
+
* reconnect has cancelled it yet. Read it from `connect()` to tell the
|
|
12
|
+
* reconnect half of an in-page move from a first connect: a controller whose
|
|
13
|
+
* initialisation restarts a measurement (a min-duration floor, an elapsed
|
|
14
|
+
* counter) must skip it for the move, where nothing actually restarted.
|
|
15
|
+
*/
|
|
16
|
+
get pending() {
|
|
17
|
+
return this.#pending;
|
|
18
|
+
}
|
|
9
19
|
/**
|
|
10
20
|
* True when the disconnect is definitely a real detach — the element left
|
|
11
21
|
* the document, or `data-controller` no longer lists the identifier. False
|
|
@@ -2,6 +2,23 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/progress_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,7 +26,57 @@ 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/progress_controller.ts
|
|
79
|
+
var OWNED_VALUE_TEXT = "data-stimeo--progress-owns-valuetext";
|
|
13
80
|
var ProgressController = class extends Controller {
|
|
14
81
|
static targets = ["bar"];
|
|
15
82
|
static values = {
|
|
@@ -17,13 +84,27 @@ var ProgressController = class extends Controller {
|
|
|
17
84
|
min: { type: Number, default: 0 },
|
|
18
85
|
max: { type: Number, default: 100 },
|
|
19
86
|
indeterminate: { type: Boolean, default: false },
|
|
20
|
-
valueText: { type: String, default: "" }
|
|
87
|
+
valueText: { type: String, default: "" },
|
|
88
|
+
announceText: { type: String, default: "" }
|
|
21
89
|
};
|
|
22
90
|
static actions = ["setValue"];
|
|
23
91
|
static events = ["change", "complete"];
|
|
92
|
+
/**
|
|
93
|
+
* Collapses a morph that swaps several render inputs at once into one repaint.
|
|
94
|
+
* A single update usually rewrites the whole set, and each Value would otherwise
|
|
95
|
+
* repaint on its own.
|
|
96
|
+
*/
|
|
97
|
+
#repaint = new MicrotaskCoalescer(() => {
|
|
98
|
+
this.#render();
|
|
99
|
+
});
|
|
24
100
|
connect() {
|
|
101
|
+
this.#repaint.activate();
|
|
25
102
|
this.#render();
|
|
26
103
|
}
|
|
104
|
+
/** Closes the window in which a queued repaint may still run. */
|
|
105
|
+
disconnect() {
|
|
106
|
+
this.#repaint.cancel();
|
|
107
|
+
}
|
|
27
108
|
/**
|
|
28
109
|
* Updates the progress value from an action param (`amount`) or a
|
|
29
110
|
* `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and
|
|
@@ -39,11 +120,30 @@ var ProgressController = class extends Controller {
|
|
|
39
120
|
this.dispatch("change", { detail: { value, ratio: this.#ratio } });
|
|
40
121
|
if (value >= this.maxValue) {
|
|
41
122
|
this.dispatch("complete", { detail: { value } });
|
|
123
|
+
announce(
|
|
124
|
+
fillTemplate(this.announceTextValue, { value, percent: Math.round(this.#ratio * 100) })
|
|
125
|
+
);
|
|
42
126
|
}
|
|
43
127
|
}
|
|
44
|
-
/**
|
|
128
|
+
/** Repaints when application code (or a Turbo morph) changes `indeterminate` at runtime. */
|
|
45
129
|
indeterminateValueChanged() {
|
|
46
|
-
this.#
|
|
130
|
+
this.#repaint.schedule();
|
|
131
|
+
}
|
|
132
|
+
/** Repaints when application code (or a Turbo morph) changes `value` at runtime. */
|
|
133
|
+
valueValueChanged() {
|
|
134
|
+
this.#repaint.schedule();
|
|
135
|
+
}
|
|
136
|
+
/** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
|
|
137
|
+
minValueChanged() {
|
|
138
|
+
this.#repaint.schedule();
|
|
139
|
+
}
|
|
140
|
+
/** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
|
|
141
|
+
maxValueChanged() {
|
|
142
|
+
this.#repaint.schedule();
|
|
143
|
+
}
|
|
144
|
+
/** Repaints when application code (or a Turbo morph) changes `valueText` at runtime. */
|
|
145
|
+
valueTextValueChanged() {
|
|
146
|
+
this.#repaint.schedule();
|
|
47
147
|
}
|
|
48
148
|
/** Clamps `raw` into the configured `[min, max]` range. */
|
|
49
149
|
#clamp(raw) {
|
|
@@ -51,40 +151,51 @@ var ProgressController = class extends Controller {
|
|
|
51
151
|
}
|
|
52
152
|
/** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */
|
|
53
153
|
get #ratio() {
|
|
54
|
-
|
|
55
|
-
if (span <= 0) return 0;
|
|
56
|
-
return (this.#clamp(this.valueValue) - this.minValue) / span;
|
|
154
|
+
return rangeFraction(this.valueValue, this.minValue, this.maxValue);
|
|
57
155
|
}
|
|
58
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio.
|
|
158
|
+
*
|
|
159
|
+
* @stimeoRenderRoot
|
|
160
|
+
*/
|
|
59
161
|
#render() {
|
|
60
162
|
this.element.setAttribute("aria-valuemin", String(this.minValue));
|
|
61
163
|
this.element.setAttribute("aria-valuemax", String(this.maxValue));
|
|
62
164
|
if (this.indeterminateValue) {
|
|
63
165
|
this.element.removeAttribute("aria-valuenow");
|
|
64
|
-
this
|
|
65
|
-
this.element.style.removeProperty("--stimeo
|
|
166
|
+
this.#clearOwnValueText();
|
|
167
|
+
this.element.style.removeProperty("--stimeo--progress-ratio");
|
|
66
168
|
this.element.setAttribute("data-state", "indeterminate");
|
|
67
169
|
return;
|
|
68
170
|
}
|
|
69
171
|
const value = this.#clamp(this.valueValue);
|
|
70
172
|
this.element.setAttribute("aria-valuenow", String(value));
|
|
71
|
-
this.element.style.setProperty("--stimeo
|
|
173
|
+
this.element.style.setProperty("--stimeo--progress-ratio", String(this.#ratio));
|
|
72
174
|
this.element.setAttribute("data-state", "determinate");
|
|
73
175
|
this.#applyValueText(value);
|
|
74
176
|
}
|
|
75
177
|
/**
|
|
76
178
|
* Sets `aria-valuetext` from the consumer-provided template, substituting
|
|
77
179
|
* `{value}` and `{percent}`. Left to the consumer so the human-readable text
|
|
78
|
-
* stays i18n-neutral in the library
|
|
180
|
+
* stays i18n-neutral in the library. With no template the attribute belongs to
|
|
181
|
+
* the consumer, so only a text this controller wrote is taken back
|
|
182
|
+
* ({@link OWNED_VALUE_TEXT}).
|
|
79
183
|
*/
|
|
80
184
|
#applyValueText(value) {
|
|
81
185
|
if (this.valueTextValue.length === 0) {
|
|
82
|
-
this
|
|
186
|
+
this.#clearOwnValueText();
|
|
83
187
|
return;
|
|
84
188
|
}
|
|
85
189
|
const percent = Math.round(this.#ratio * 100);
|
|
86
190
|
const text = this.valueTextValue.replaceAll("{value}", String(value)).replaceAll("{percent}", String(percent));
|
|
87
191
|
this.element.setAttribute("aria-valuetext", text);
|
|
192
|
+
this.element.setAttribute(OWNED_VALUE_TEXT, "");
|
|
193
|
+
}
|
|
194
|
+
/** Removes `aria-valuetext` only when this controller is the one that wrote it. */
|
|
195
|
+
#clearOwnValueText() {
|
|
196
|
+
if (!this.element.hasAttribute(OWNED_VALUE_TEXT)) return;
|
|
197
|
+
this.element.removeAttribute("aria-valuetext");
|
|
198
|
+
this.element.removeAttribute(OWNED_VALUE_TEXT);
|
|
88
199
|
}
|
|
89
200
|
};
|
|
90
201
|
|