stimeo-ui 0.12.0 → 0.13.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 +37 -0
- data/dist/cable/index.js +438 -152
- data/dist/controllers/calendar_controller.js +10 -2
- data/dist/controllers/carousel_controller.js +1 -1
- data/dist/controllers/command_palette_controller.js +2 -2
- data/dist/controllers/date_range_picker_controller.js +10 -10
- data/dist/controllers/drawer_controller.js +2 -2
- data/dist/controllers/optimistic_controller.js +141 -20
- data/dist/controllers/toast_controller.js +1 -1
- data/dist/index.js +101 -38
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -51,6 +51,15 @@ function parseISOMonthString(monthStr) {
|
|
|
51
51
|
function toISOMonthString(date) {
|
|
52
52
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
53
53
|
}
|
|
54
|
+
function monthLabelFormatter(locale) {
|
|
55
|
+
const options = { month: "long", year: "numeric" };
|
|
56
|
+
try {
|
|
57
|
+
return new Intl.DateTimeFormat(locale, options);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!(error instanceof RangeError)) throw error;
|
|
60
|
+
return new Intl.DateTimeFormat("en", options);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
54
63
|
|
|
55
64
|
// src/utils/safe_timeout.ts
|
|
56
65
|
var TimerRegistry = class {
|
|
@@ -288,8 +297,7 @@ var CalendarController = class extends Controller {
|
|
|
288
297
|
const month = monthStart.getMonth() + 1;
|
|
289
298
|
if (this.hasLabelTarget) {
|
|
290
299
|
const lang = document.documentElement.lang || "en";
|
|
291
|
-
|
|
292
|
-
this.labelTarget.textContent = formatter.format(monthStart);
|
|
300
|
+
this.labelTarget.textContent = monthLabelFormatter(lang).format(monthStart);
|
|
293
301
|
}
|
|
294
302
|
const days = this.#calculateGridDays(year, month);
|
|
295
303
|
const dayElements = this.dayTargets;
|
|
@@ -252,7 +252,7 @@ var CarouselController = class extends Controller {
|
|
|
252
252
|
#handledEvents = /* @__PURE__ */ new WeakSet();
|
|
253
253
|
/**
|
|
254
254
|
* Whether `connect()` has run. Scheduling is already inert outside that window
|
|
255
|
-
* (
|
|
255
|
+
* (`MicrotaskCoalescer`), so this gates the Value callbacks Stimulus
|
|
256
256
|
* delivers ahead of `connect()`.
|
|
257
257
|
*/
|
|
258
258
|
#connected = false;
|
|
@@ -512,7 +512,7 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
512
512
|
* re-deriving from the declarative `open` Value (which would slam a user-opened
|
|
513
513
|
* palette shut). The `open` Value only seeds the initial state of a genuinely
|
|
514
514
|
* fresh render. We normalize to a clean closed baseline first so {@link open}
|
|
515
|
-
* runs its full setup — the
|
|
515
|
+
* runs its full setup — the `FocusTrap` is a fresh instance after a
|
|
516
516
|
* reconnect and must be re-activated.
|
|
517
517
|
*/
|
|
518
518
|
connect() {
|
|
@@ -617,7 +617,7 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
617
617
|
}
|
|
618
618
|
/**
|
|
619
619
|
* Combobox navigation keys (arrows / Home / End / Enter), bound to the input.
|
|
620
|
-
* `Tab` (focus trap) and `Escape` (close) are owned by the
|
|
620
|
+
* `Tab` (focus trap) and `Escape` (close) are owned by the shared `FocusTrap` at
|
|
621
621
|
* the document level, so they work no matter which element inside the dialog has
|
|
622
622
|
* focus — not only the input.
|
|
623
623
|
*/
|
|
@@ -80,6 +80,15 @@ function parseISOMonthString(monthStr) {
|
|
|
80
80
|
function toISOMonthString(date) {
|
|
81
81
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
82
82
|
}
|
|
83
|
+
function monthLabelFormatter(locale) {
|
|
84
|
+
const options = { month: "long", year: "numeric" };
|
|
85
|
+
try {
|
|
86
|
+
return new Intl.DateTimeFormat(locale, options);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!(error instanceof RangeError)) throw error;
|
|
89
|
+
return new Intl.DateTimeFormat("en", options);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
83
92
|
|
|
84
93
|
// src/utils/microtask_coalescer.ts
|
|
85
94
|
var MicrotaskCoalescer = class {
|
|
@@ -433,7 +442,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
433
442
|
if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
|
|
434
443
|
if (this.hasMonthLabelTarget) {
|
|
435
444
|
const lang = document.documentElement.lang || "en";
|
|
436
|
-
const formatter =
|
|
445
|
+
const formatter = monthLabelFormatter(lang);
|
|
437
446
|
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
438
447
|
}
|
|
439
448
|
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
@@ -588,15 +597,6 @@ function normalizeISO(value) {
|
|
|
588
597
|
function orderRange(start, end) {
|
|
589
598
|
return start && end && end < start ? [end, start] : [start, end];
|
|
590
599
|
}
|
|
591
|
-
function monthFormatter(locale) {
|
|
592
|
-
const options = { month: "long", year: "numeric" };
|
|
593
|
-
try {
|
|
594
|
-
return new Intl.DateTimeFormat(locale, options);
|
|
595
|
-
} catch (error) {
|
|
596
|
-
if (!(error instanceof RangeError)) throw error;
|
|
597
|
-
return new Intl.DateTimeFormat("en", options);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
600
|
function computePreset(name) {
|
|
601
601
|
const today = /* @__PURE__ */ new Date();
|
|
602
602
|
const todayStr = toISODateString(today);
|
|
@@ -586,7 +586,7 @@ var DrawerController = class extends Controller {
|
|
|
586
586
|
* rather than being re-derived from the declarative `open` Value (which would
|
|
587
587
|
* close a user-opened drawer). The `open` Value only seeds a genuinely fresh
|
|
588
588
|
* render. We normalize to a clean closed baseline first so {@link open} runs its
|
|
589
|
-
* full reveal + trap activation — the
|
|
589
|
+
* full reveal + trap activation — the `FocusTrap` is inactive after a
|
|
590
590
|
* disconnect and must be re-activated.
|
|
591
591
|
*/
|
|
592
592
|
connect() {
|
|
@@ -719,7 +719,7 @@ var DrawerController = class extends Controller {
|
|
|
719
719
|
* Runs once the close transition has finished: applies `hidden` to the panel
|
|
720
720
|
* and overlay, then reverts the modal side effects (scroll lock, background
|
|
721
721
|
* `inert`, keydown listener) and restores focus to the opener. Deferring the
|
|
722
|
-
*
|
|
722
|
+
* `FocusTrap` teardown to here — rather than at {@link close} time — keeps
|
|
723
723
|
* the background unreachable and focus trapped for the whole exit animation.
|
|
724
724
|
*/
|
|
725
725
|
#applyHidden(panel) {
|
|
@@ -1,32 +1,115 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
// src/controllers/optimistic_controller.ts
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// src/utils/detach_gate.ts
|
|
6
|
+
var DetachGate = class _DetachGate {
|
|
7
|
+
/** Set while a probe is queued, waiting for a reconnect to cancel it. */
|
|
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
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* True when the disconnect is definitely a real detach — the element left
|
|
21
|
+
* the document, or `data-controller` no longer lists the identifier. False
|
|
22
|
+
* means ambiguous (in-page move or observed-root exit), NOT "alive".
|
|
23
|
+
*/
|
|
24
|
+
static isDetached(host) {
|
|
25
|
+
if (!host.element.isConnected) return true;
|
|
26
|
+
const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
|
|
27
|
+
return !tokens.includes(host.identifier);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Call from `disconnect()`: runs `teardown` synchronously on a definite
|
|
31
|
+
* detach (fast path), otherwise defers it one microtask — a reconnect
|
|
32
|
+
* ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
|
|
33
|
+
* One microtask is the whole probe window: Stimulus reconnects a moved
|
|
34
|
+
* element within the same mutation batch, before the checkpoint drains.
|
|
35
|
+
*/
|
|
36
|
+
disconnected(host, teardown) {
|
|
37
|
+
if (_DetachGate.isDetached(host)) {
|
|
38
|
+
this.#pending = false;
|
|
39
|
+
teardown();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.#pending = true;
|
|
43
|
+
queueMicrotask(() => {
|
|
44
|
+
if (!this.#pending) return;
|
|
45
|
+
this.#pending = false;
|
|
46
|
+
teardown();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Disarms a pending probe. Call from `connect()` (the reconnect that proves
|
|
51
|
+
* an in-page move) and from the head of any teardown path not routed through
|
|
52
|
+
* {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
|
|
53
|
+
* never run the teardown a second time.
|
|
54
|
+
*/
|
|
55
|
+
cancel() {
|
|
56
|
+
this.#pending = false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/controllers/optimistic_controller.ts
|
|
61
|
+
var HIDDEN_MARKER = "data-optimistic-toggled";
|
|
62
|
+
var BUSY_MARKER = "data-optimistic-busy";
|
|
63
|
+
var AUTHORED_ABSENT = "absent";
|
|
64
|
+
var AUTHORED_VALUE = "value:";
|
|
65
|
+
var SYNTHETIC_SUBMISSION = {};
|
|
66
|
+
function submissionOf(event) {
|
|
67
|
+
const detail = event.detail;
|
|
68
|
+
return detail?.formSubmission ?? SYNTHETIC_SUBMISSION;
|
|
69
|
+
}
|
|
5
70
|
var OptimisticController = class extends Controller {
|
|
6
71
|
static targets = ["show", "hide"];
|
|
7
72
|
static events = ["commit", "rollback"];
|
|
73
|
+
/** The submission that owns the applied optimistic state, if any. */
|
|
74
|
+
#pending = null;
|
|
75
|
+
#gate = new DetachGate();
|
|
76
|
+
/** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */
|
|
77
|
+
#connected = false;
|
|
8
78
|
connect() {
|
|
9
|
-
|
|
79
|
+
const restored = !this.#connected && !this.#gate.pending;
|
|
80
|
+
this.#gate.cancel();
|
|
81
|
+
this.#connected = true;
|
|
82
|
+
if (restored && this.element.hasAttribute("data-optimistic")) this.#revert();
|
|
10
83
|
this.element.addEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
11
84
|
this.element.addEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
12
85
|
}
|
|
13
86
|
disconnect() {
|
|
87
|
+
this.#connected = false;
|
|
14
88
|
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
15
89
|
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
90
|
+
this.#gate.disconnected(this, () => {
|
|
91
|
+
this.#pending = null;
|
|
92
|
+
});
|
|
16
93
|
}
|
|
17
|
-
#onSubmitStart = () => {
|
|
94
|
+
#onSubmitStart = (event) => {
|
|
95
|
+
this.#pending = submissionOf(event);
|
|
96
|
+
if (this.element instanceof HTMLFormElement && !this.element.hasAttribute(BUSY_MARKER)) {
|
|
97
|
+
this.element.setAttribute(BUSY_MARKER, AUTHORED_ABSENT);
|
|
98
|
+
}
|
|
99
|
+
this.#write(this.element, "aria-busy", "true", BUSY_MARKER);
|
|
18
100
|
this.element.setAttribute("data-optimistic", "true");
|
|
19
|
-
this.
|
|
20
|
-
for (const target of this.
|
|
21
|
-
for (const target of this.hideTargets) this.#toggleHidden(target, true);
|
|
101
|
+
for (const target of this.showTargets) this.#setHidden(target, false);
|
|
102
|
+
for (const target of this.hideTargets) this.#setHidden(target, true);
|
|
22
103
|
};
|
|
23
104
|
#onSubmitEnd = (event) => {
|
|
105
|
+
if (submissionOf(event) !== this.#pending) return;
|
|
106
|
+
this.#pending = null;
|
|
24
107
|
const success = event.detail?.success === true;
|
|
25
|
-
this.element.removeAttribute("aria-busy");
|
|
26
108
|
if (success) {
|
|
27
109
|
this.element.removeAttribute("data-optimistic");
|
|
110
|
+
this.#restore(this.element, "aria-busy", BUSY_MARKER, "true");
|
|
28
111
|
for (const target of [...this.showTargets, ...this.hideTargets]) {
|
|
29
|
-
target.removeAttribute(
|
|
112
|
+
target.removeAttribute(HIDDEN_MARKER);
|
|
30
113
|
}
|
|
31
114
|
this.dispatch("commit");
|
|
32
115
|
} else {
|
|
@@ -34,21 +117,59 @@ var OptimisticController = class extends Controller {
|
|
|
34
117
|
this.dispatch("rollback");
|
|
35
118
|
}
|
|
36
119
|
};
|
|
37
|
-
/**
|
|
38
|
-
#
|
|
39
|
-
if (target.hidden === hidden) return;
|
|
40
|
-
target
|
|
41
|
-
|
|
120
|
+
/** Hides or reveals a target, recording the authored `hidden` on first write. */
|
|
121
|
+
#setHidden(target, hidden) {
|
|
122
|
+
if (target.hasAttribute("hidden") === hidden) return;
|
|
123
|
+
this.#write(target, "hidden", hidden ? "" : null, HIDDEN_MARKER);
|
|
124
|
+
}
|
|
125
|
+
/** Writes an attribute, recording what it displaced under `marker` once. */
|
|
126
|
+
#write(element, attribute, value, marker) {
|
|
127
|
+
if (!element.hasAttribute(marker)) {
|
|
128
|
+
const authored = element.getAttribute(attribute);
|
|
129
|
+
element.setAttribute(
|
|
130
|
+
marker,
|
|
131
|
+
authored === null ? AUTHORED_ABSENT : `${AUTHORED_VALUE}${authored}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (value === null) element.removeAttribute(attribute);
|
|
135
|
+
else element.setAttribute(attribute, value);
|
|
42
136
|
}
|
|
43
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Puts back the value `marker` recorded and drops the record.
|
|
139
|
+
*
|
|
140
|
+
* `written` is what this controller last put in the attribute. Anything else
|
|
141
|
+
* there now belongs to whoever wrote it — Turbo owns `aria-busy` on a form and
|
|
142
|
+
* clears it before the terminal arrives, and a consumer may have moved a face
|
|
143
|
+
* mid-flight — so the record is dropped without touching the attribute. A
|
|
144
|
+
* marker this build did not write names no authored value, so the attribute
|
|
145
|
+
* goes back to being absent.
|
|
146
|
+
*/
|
|
147
|
+
#restore(element, attribute, marker, written) {
|
|
148
|
+
const recorded = element.getAttribute(marker);
|
|
149
|
+
if (recorded === null) return;
|
|
150
|
+
element.removeAttribute(marker);
|
|
151
|
+
if (element.getAttribute(attribute) !== written) return;
|
|
152
|
+
if (recorded.startsWith(AUTHORED_VALUE)) {
|
|
153
|
+
element.setAttribute(attribute, recorded.slice(AUTHORED_VALUE.length));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
element.removeAttribute(attribute);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Puts back exactly the markup this controller displaced (record-owned only).
|
|
160
|
+
*
|
|
161
|
+
* A face can be registered on both sides, and the hide pass runs last, so what
|
|
162
|
+
* this controller wrote there is `hidden` — matching it against the reveal's
|
|
163
|
+
* absent value instead would read as someone else's write and leave the face
|
|
164
|
+
* hidden. Each target is therefore restored once, against the write it last
|
|
165
|
+
* received.
|
|
166
|
+
*/
|
|
44
167
|
#revert() {
|
|
45
168
|
this.element.removeAttribute("data-optimistic");
|
|
46
|
-
this.element
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
target.removeAttribute(TOGGLED_MARKER);
|
|
51
|
-
}
|
|
169
|
+
this.#restore(this.element, "aria-busy", BUSY_MARKER, "true");
|
|
170
|
+
const hidden = new Set(this.hideTargets);
|
|
171
|
+
for (const target of /* @__PURE__ */ new Set([...this.showTargets, ...this.hideTargets])) {
|
|
172
|
+
this.#restore(target, "hidden", HIDDEN_MARKER, hidden.has(target) ? "" : null);
|
|
52
173
|
}
|
|
53
174
|
}
|
|
54
175
|
};
|
|
@@ -101,7 +101,7 @@ var ToastController = class extends Controller {
|
|
|
101
101
|
static events = ["dismiss", "show"];
|
|
102
102
|
/**
|
|
103
103
|
* Registry for every auto-dismiss and transition-finalize timer the controller
|
|
104
|
-
* schedules.
|
|
104
|
+
* schedules. `SafeTimeout` owns *registration and teardown only*; the
|
|
105
105
|
* pause/resume remaining-time accounting stays in `#activeTimeouts` so the
|
|
106
106
|
* per-widget WCAG 2.2.1 semantics are not flattened into the helper.
|
|
107
107
|
*/
|
data/dist/index.js
CHANGED
|
@@ -1886,6 +1886,15 @@ function parseISOMonthString(monthStr) {
|
|
|
1886
1886
|
function toISOMonthString(date) {
|
|
1887
1887
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
1888
1888
|
}
|
|
1889
|
+
function monthLabelFormatter(locale) {
|
|
1890
|
+
const options = { month: "long", year: "numeric" };
|
|
1891
|
+
try {
|
|
1892
|
+
return new Intl.DateTimeFormat(locale, options);
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
if (!(error instanceof RangeError)) throw error;
|
|
1895
|
+
return new Intl.DateTimeFormat("en", options);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1889
1898
|
|
|
1890
1899
|
// src/controllers/calendar_controller.ts
|
|
1891
1900
|
var OWNED_DISABLED = "data-stimeo--calendar-owns-disabled";
|
|
@@ -2070,8 +2079,7 @@ var CalendarController = class extends Controller {
|
|
|
2070
2079
|
const month = monthStart.getMonth() + 1;
|
|
2071
2080
|
if (this.hasLabelTarget) {
|
|
2072
2081
|
const lang = document.documentElement.lang || "en";
|
|
2073
|
-
|
|
2074
|
-
this.labelTarget.textContent = formatter.format(monthStart);
|
|
2082
|
+
this.labelTarget.textContent = monthLabelFormatter(lang).format(monthStart);
|
|
2075
2083
|
}
|
|
2076
2084
|
const days = this.#calculateGridDays(year, month);
|
|
2077
2085
|
const dayElements = this.dayTargets;
|
|
@@ -2323,7 +2331,7 @@ var CarouselController = class extends Controller {
|
|
|
2323
2331
|
#handledEvents = /* @__PURE__ */ new WeakSet();
|
|
2324
2332
|
/**
|
|
2325
2333
|
* Whether `connect()` has run. Scheduling is already inert outside that window
|
|
2326
|
-
* (
|
|
2334
|
+
* (`MicrotaskCoalescer`), so this gates the Value callbacks Stimulus
|
|
2327
2335
|
* delivers ahead of `connect()`.
|
|
2328
2336
|
*/
|
|
2329
2337
|
#connected = false;
|
|
@@ -4383,7 +4391,7 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
4383
4391
|
* re-deriving from the declarative `open` Value (which would slam a user-opened
|
|
4384
4392
|
* palette shut). The `open` Value only seeds the initial state of a genuinely
|
|
4385
4393
|
* fresh render. We normalize to a clean closed baseline first so {@link open}
|
|
4386
|
-
* runs its full setup — the
|
|
4394
|
+
* runs its full setup — the `FocusTrap` is a fresh instance after a
|
|
4387
4395
|
* reconnect and must be re-activated.
|
|
4388
4396
|
*/
|
|
4389
4397
|
connect() {
|
|
@@ -4488,7 +4496,7 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
|
|
|
4488
4496
|
}
|
|
4489
4497
|
/**
|
|
4490
4498
|
* Combobox navigation keys (arrows / Home / End / Enter), bound to the input.
|
|
4491
|
-
* `Tab` (focus trap) and `Escape` (close) are owned by the
|
|
4499
|
+
* `Tab` (focus trap) and `Escape` (close) are owned by the shared `FocusTrap` at
|
|
4492
4500
|
* the document level, so they work no matter which element inside the dialog has
|
|
4493
4501
|
* focus — not only the input.
|
|
4494
4502
|
*/
|
|
@@ -6566,7 +6574,7 @@ var DateRangePickerController = class extends Controller {
|
|
|
6566
6574
|
if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
|
|
6567
6575
|
if (this.hasMonthLabelTarget) {
|
|
6568
6576
|
const lang = document.documentElement.lang || "en";
|
|
6569
|
-
const formatter =
|
|
6577
|
+
const formatter = monthLabelFormatter(lang);
|
|
6570
6578
|
this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
|
|
6571
6579
|
}
|
|
6572
6580
|
const [rangeStart, rangeEnd] = this.#visualRange();
|
|
@@ -6721,15 +6729,6 @@ function normalizeISO(value) {
|
|
|
6721
6729
|
function orderRange(start, end) {
|
|
6722
6730
|
return start && end && end < start ? [end, start] : [start, end];
|
|
6723
6731
|
}
|
|
6724
|
-
function monthFormatter(locale) {
|
|
6725
|
-
const options = { month: "long", year: "numeric" };
|
|
6726
|
-
try {
|
|
6727
|
-
return new Intl.DateTimeFormat(locale, options);
|
|
6728
|
-
} catch (error) {
|
|
6729
|
-
if (!(error instanceof RangeError)) throw error;
|
|
6730
|
-
return new Intl.DateTimeFormat("en", options);
|
|
6731
|
-
}
|
|
6732
|
-
}
|
|
6733
6732
|
function computePreset(name) {
|
|
6734
6733
|
const today = /* @__PURE__ */ new Date();
|
|
6735
6734
|
const todayStr = toISODateString(today);
|
|
@@ -7411,7 +7410,7 @@ var DrawerController = class extends Controller {
|
|
|
7411
7410
|
* rather than being re-derived from the declarative `open` Value (which would
|
|
7412
7411
|
* close a user-opened drawer). The `open` Value only seeds a genuinely fresh
|
|
7413
7412
|
* render. We normalize to a clean closed baseline first so {@link open} runs its
|
|
7414
|
-
* full reveal + trap activation — the
|
|
7413
|
+
* full reveal + trap activation — the `FocusTrap` is inactive after a
|
|
7415
7414
|
* disconnect and must be re-activated.
|
|
7416
7415
|
*/
|
|
7417
7416
|
connect() {
|
|
@@ -7544,7 +7543,7 @@ var DrawerController = class extends Controller {
|
|
|
7544
7543
|
* Runs once the close transition has finished: applies `hidden` to the panel
|
|
7545
7544
|
* and overlay, then reverts the modal side effects (scroll lock, background
|
|
7546
7545
|
* `inert`, keydown listener) and restores focus to the opener. Deferring the
|
|
7547
|
-
*
|
|
7546
|
+
* `FocusTrap` teardown to here — rather than at {@link close} time — keeps
|
|
7548
7547
|
* the background unreachable and focus trapped for the whole exit animation.
|
|
7549
7548
|
*/
|
|
7550
7549
|
#applyHidden(panel) {
|
|
@@ -14221,32 +14220,58 @@ var NumberInputController = class _NumberInputController extends Controller {
|
|
|
14221
14220
|
this.#ariaValueMax.returnAll();
|
|
14222
14221
|
}
|
|
14223
14222
|
};
|
|
14224
|
-
var
|
|
14223
|
+
var HIDDEN_MARKER = "data-optimistic-toggled";
|
|
14224
|
+
var BUSY_MARKER = "data-optimistic-busy";
|
|
14225
|
+
var AUTHORED_ABSENT = "absent";
|
|
14226
|
+
var AUTHORED_VALUE = "value:";
|
|
14227
|
+
var SYNTHETIC_SUBMISSION = {};
|
|
14228
|
+
function submissionOf(event) {
|
|
14229
|
+
const detail = event.detail;
|
|
14230
|
+
return detail?.formSubmission ?? SYNTHETIC_SUBMISSION;
|
|
14231
|
+
}
|
|
14225
14232
|
var OptimisticController = class extends Controller {
|
|
14226
14233
|
static targets = ["show", "hide"];
|
|
14227
14234
|
static events = ["commit", "rollback"];
|
|
14235
|
+
/** The submission that owns the applied optimistic state, if any. */
|
|
14236
|
+
#pending = null;
|
|
14237
|
+
#gate = new DetachGate();
|
|
14238
|
+
/** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */
|
|
14239
|
+
#connected = false;
|
|
14228
14240
|
connect() {
|
|
14229
|
-
|
|
14241
|
+
const restored = !this.#connected && !this.#gate.pending;
|
|
14242
|
+
this.#gate.cancel();
|
|
14243
|
+
this.#connected = true;
|
|
14244
|
+
if (restored && this.element.hasAttribute("data-optimistic")) this.#revert();
|
|
14230
14245
|
this.element.addEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
14231
14246
|
this.element.addEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
14232
14247
|
}
|
|
14233
14248
|
disconnect() {
|
|
14249
|
+
this.#connected = false;
|
|
14234
14250
|
this.element.removeEventListener("turbo:submit-start", this.#onSubmitStart);
|
|
14235
14251
|
this.element.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
|
|
14252
|
+
this.#gate.disconnected(this, () => {
|
|
14253
|
+
this.#pending = null;
|
|
14254
|
+
});
|
|
14236
14255
|
}
|
|
14237
|
-
#onSubmitStart = () => {
|
|
14256
|
+
#onSubmitStart = (event) => {
|
|
14257
|
+
this.#pending = submissionOf(event);
|
|
14258
|
+
if (this.element instanceof HTMLFormElement && !this.element.hasAttribute(BUSY_MARKER)) {
|
|
14259
|
+
this.element.setAttribute(BUSY_MARKER, AUTHORED_ABSENT);
|
|
14260
|
+
}
|
|
14261
|
+
this.#write(this.element, "aria-busy", "true", BUSY_MARKER);
|
|
14238
14262
|
this.element.setAttribute("data-optimistic", "true");
|
|
14239
|
-
this.
|
|
14240
|
-
for (const target of this.
|
|
14241
|
-
for (const target of this.hideTargets) this.#toggleHidden(target, true);
|
|
14263
|
+
for (const target of this.showTargets) this.#setHidden(target, false);
|
|
14264
|
+
for (const target of this.hideTargets) this.#setHidden(target, true);
|
|
14242
14265
|
};
|
|
14243
14266
|
#onSubmitEnd = (event) => {
|
|
14267
|
+
if (submissionOf(event) !== this.#pending) return;
|
|
14268
|
+
this.#pending = null;
|
|
14244
14269
|
const success = event.detail?.success === true;
|
|
14245
|
-
this.element.removeAttribute("aria-busy");
|
|
14246
14270
|
if (success) {
|
|
14247
14271
|
this.element.removeAttribute("data-optimistic");
|
|
14272
|
+
this.#restore(this.element, "aria-busy", BUSY_MARKER, "true");
|
|
14248
14273
|
for (const target of [...this.showTargets, ...this.hideTargets]) {
|
|
14249
|
-
target.removeAttribute(
|
|
14274
|
+
target.removeAttribute(HIDDEN_MARKER);
|
|
14250
14275
|
}
|
|
14251
14276
|
this.dispatch("commit");
|
|
14252
14277
|
} else {
|
|
@@ -14254,21 +14279,59 @@ var OptimisticController = class extends Controller {
|
|
|
14254
14279
|
this.dispatch("rollback");
|
|
14255
14280
|
}
|
|
14256
14281
|
};
|
|
14257
|
-
/**
|
|
14258
|
-
#
|
|
14259
|
-
if (target.hidden === hidden) return;
|
|
14260
|
-
target
|
|
14261
|
-
|
|
14282
|
+
/** Hides or reveals a target, recording the authored `hidden` on first write. */
|
|
14283
|
+
#setHidden(target, hidden) {
|
|
14284
|
+
if (target.hasAttribute("hidden") === hidden) return;
|
|
14285
|
+
this.#write(target, "hidden", hidden ? "" : null, HIDDEN_MARKER);
|
|
14286
|
+
}
|
|
14287
|
+
/** Writes an attribute, recording what it displaced under `marker` once. */
|
|
14288
|
+
#write(element, attribute, value, marker) {
|
|
14289
|
+
if (!element.hasAttribute(marker)) {
|
|
14290
|
+
const authored = element.getAttribute(attribute);
|
|
14291
|
+
element.setAttribute(
|
|
14292
|
+
marker,
|
|
14293
|
+
authored === null ? AUTHORED_ABSENT : `${AUTHORED_VALUE}${authored}`
|
|
14294
|
+
);
|
|
14295
|
+
}
|
|
14296
|
+
if (value === null) element.removeAttribute(attribute);
|
|
14297
|
+
else element.setAttribute(attribute, value);
|
|
14262
14298
|
}
|
|
14263
|
-
/**
|
|
14299
|
+
/**
|
|
14300
|
+
* Puts back the value `marker` recorded and drops the record.
|
|
14301
|
+
*
|
|
14302
|
+
* `written` is what this controller last put in the attribute. Anything else
|
|
14303
|
+
* there now belongs to whoever wrote it — Turbo owns `aria-busy` on a form and
|
|
14304
|
+
* clears it before the terminal arrives, and a consumer may have moved a face
|
|
14305
|
+
* mid-flight — so the record is dropped without touching the attribute. A
|
|
14306
|
+
* marker this build did not write names no authored value, so the attribute
|
|
14307
|
+
* goes back to being absent.
|
|
14308
|
+
*/
|
|
14309
|
+
#restore(element, attribute, marker, written) {
|
|
14310
|
+
const recorded = element.getAttribute(marker);
|
|
14311
|
+
if (recorded === null) return;
|
|
14312
|
+
element.removeAttribute(marker);
|
|
14313
|
+
if (element.getAttribute(attribute) !== written) return;
|
|
14314
|
+
if (recorded.startsWith(AUTHORED_VALUE)) {
|
|
14315
|
+
element.setAttribute(attribute, recorded.slice(AUTHORED_VALUE.length));
|
|
14316
|
+
return;
|
|
14317
|
+
}
|
|
14318
|
+
element.removeAttribute(attribute);
|
|
14319
|
+
}
|
|
14320
|
+
/**
|
|
14321
|
+
* Puts back exactly the markup this controller displaced (record-owned only).
|
|
14322
|
+
*
|
|
14323
|
+
* A face can be registered on both sides, and the hide pass runs last, so what
|
|
14324
|
+
* this controller wrote there is `hidden` — matching it against the reveal's
|
|
14325
|
+
* absent value instead would read as someone else's write and leave the face
|
|
14326
|
+
* hidden. Each target is therefore restored once, against the write it last
|
|
14327
|
+
* received.
|
|
14328
|
+
*/
|
|
14264
14329
|
#revert() {
|
|
14265
14330
|
this.element.removeAttribute("data-optimistic");
|
|
14266
|
-
this.element
|
|
14267
|
-
|
|
14268
|
-
|
|
14269
|
-
|
|
14270
|
-
target.removeAttribute(TOGGLED_MARKER);
|
|
14271
|
-
}
|
|
14331
|
+
this.#restore(this.element, "aria-busy", BUSY_MARKER, "true");
|
|
14332
|
+
const hidden = new Set(this.hideTargets);
|
|
14333
|
+
for (const target of /* @__PURE__ */ new Set([...this.showTargets, ...this.hideTargets])) {
|
|
14334
|
+
this.#restore(target, "hidden", HIDDEN_MARKER, hidden.has(target) ? "" : null);
|
|
14272
14335
|
}
|
|
14273
14336
|
}
|
|
14274
14337
|
};
|
|
@@ -23635,7 +23698,7 @@ var ToastController = class extends Controller {
|
|
|
23635
23698
|
static events = ["dismiss", "show"];
|
|
23636
23699
|
/**
|
|
23637
23700
|
* Registry for every auto-dismiss and transition-finalize timer the controller
|
|
23638
|
-
* schedules.
|
|
23701
|
+
* schedules. `SafeTimeout` owns *registration and teardown only*; the
|
|
23639
23702
|
* pause/resume remaining-time accounting stays in `#activeTimeouts` so the
|
|
23640
23703
|
* per-widget WCAG 2.2.1 semantics are not flattened into the helper.
|
|
23641
23704
|
*/
|
data/lib/stimeo/ui/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stimeo-ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stimeo Labs
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|