@unabridged/midwest 0.22.1 → 0.23.1
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/README.md +1 -1
- package/app/assets/javascript/midwest/index.ts +8 -0
- package/app/assets/javascript/midwest/keyboard_navigation.ts +68 -0
- package/app/assets/javascript/midwest/markdown.ts +79 -0
- package/app/assets/javascript/midwest.js +795 -39
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest/utilities.css +4 -0
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +13 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js +42 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js.map +1 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/markdown.js +52 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/markdown.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js +270 -2
- package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +8 -37
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js +83 -0
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js +42 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +43 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +273 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -0
- package/dist/javascript/midwest.js +795 -39
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
|
@@ -2511,6 +2511,46 @@ const computePosition = (reference, floating, options) => {
|
|
|
2511
2511
|
});
|
|
2512
2512
|
};
|
|
2513
2513
|
|
|
2514
|
+
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Home", "End", "Escape", "Tab"];
|
|
2515
|
+
function handleRovingFocusKeydown(event, items, options = {}) {
|
|
2516
|
+
if (items.length === 0)
|
|
2517
|
+
return false;
|
|
2518
|
+
if (!NAVIGATION_KEYS.includes(event.key))
|
|
2519
|
+
return false;
|
|
2520
|
+
const { onEscape, onTab, loop = true } = options;
|
|
2521
|
+
const current = items.findIndex((item) => item === document.activeElement);
|
|
2522
|
+
switch (event.key) {
|
|
2523
|
+
case "ArrowDown": {
|
|
2524
|
+
event.preventDefault();
|
|
2525
|
+
const next = current < items.length - 1 ? current + 1 : loop ? 0 : items.length - 1;
|
|
2526
|
+
items[next].focus();
|
|
2527
|
+
break;
|
|
2528
|
+
}
|
|
2529
|
+
case "ArrowUp": {
|
|
2530
|
+
event.preventDefault();
|
|
2531
|
+
const prev = current > 0 ? current - 1 : loop ? items.length - 1 : 0;
|
|
2532
|
+
items[prev].focus();
|
|
2533
|
+
break;
|
|
2534
|
+
}
|
|
2535
|
+
case "Home":
|
|
2536
|
+
event.preventDefault();
|
|
2537
|
+
items[0].focus();
|
|
2538
|
+
break;
|
|
2539
|
+
case "End":
|
|
2540
|
+
event.preventDefault();
|
|
2541
|
+
items[items.length - 1].focus();
|
|
2542
|
+
break;
|
|
2543
|
+
case "Escape":
|
|
2544
|
+
event.preventDefault();
|
|
2545
|
+
onEscape?.();
|
|
2546
|
+
break;
|
|
2547
|
+
case "Tab":
|
|
2548
|
+
onTab?.();
|
|
2549
|
+
break;
|
|
2550
|
+
}
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2514
2554
|
class Dropdown extends Controller {
|
|
2515
2555
|
static values = {
|
|
2516
2556
|
computed: { type: Boolean, default: false },
|
|
@@ -2564,45 +2604,15 @@ class Dropdown extends Controller {
|
|
|
2564
2604
|
}
|
|
2565
2605
|
};
|
|
2566
2606
|
handleKeydown = (event) => {
|
|
2567
|
-
const
|
|
2568
|
-
|
|
2569
|
-
return;
|
|
2570
|
-
if (!["ArrowDown", "ArrowUp", "Home", "End", "Escape", "Tab"].includes(event.key))
|
|
2571
|
-
return;
|
|
2572
|
-
event.stopPropagation();
|
|
2573
|
-
const currentIndex = items.findIndex((item) => item === document.activeElement);
|
|
2574
|
-
switch (event.key) {
|
|
2575
|
-
case "ArrowDown": {
|
|
2576
|
-
event.preventDefault();
|
|
2577
|
-
const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
2578
|
-
items[next].focus();
|
|
2579
|
-
break;
|
|
2580
|
-
}
|
|
2581
|
-
case "ArrowUp": {
|
|
2582
|
-
event.preventDefault();
|
|
2583
|
-
const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
2584
|
-
items[prev].focus();
|
|
2585
|
-
break;
|
|
2586
|
-
}
|
|
2587
|
-
case "Home":
|
|
2588
|
-
event.preventDefault();
|
|
2589
|
-
items[0].focus();
|
|
2590
|
-
break;
|
|
2591
|
-
case "End":
|
|
2592
|
-
event.preventDefault();
|
|
2593
|
-
items[items.length - 1].focus();
|
|
2594
|
-
break;
|
|
2595
|
-
case "Escape":
|
|
2596
|
-
event.preventDefault();
|
|
2607
|
+
const handled = handleRovingFocusKeydown(event, this.menuItems, {
|
|
2608
|
+
onEscape: () => {
|
|
2597
2609
|
this.element.hidePopover();
|
|
2598
2610
|
requestAnimationFrame(() => this.buttonEl?.focus());
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
return;
|
|
2605
|
-
}
|
|
2611
|
+
},
|
|
2612
|
+
onTab: () => this.element.hidePopover()
|
|
2613
|
+
});
|
|
2614
|
+
if (handled)
|
|
2615
|
+
event.stopPropagation();
|
|
2606
2616
|
};
|
|
2607
2617
|
get menuItems() {
|
|
2608
2618
|
return Array.from(
|
|
@@ -5364,20 +5374,97 @@ function cloneTemplate(template) {
|
|
|
5364
5374
|
return el;
|
|
5365
5375
|
}
|
|
5366
5376
|
|
|
5377
|
+
const PLACEHOLDER = "\uE000";
|
|
5378
|
+
function renderMarkdown(src) {
|
|
5379
|
+
const blocks = [];
|
|
5380
|
+
const withoutFences = src.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, _lang, code) => {
|
|
5381
|
+
const body = escapeHtml(code.replace(/\n$/, ""));
|
|
5382
|
+
blocks.push(`<pre class="command-palette-code"><code>${body}</code></pre>`);
|
|
5383
|
+
return `${PLACEHOLDER}${blocks.length - 1}${PLACEHOLDER}`;
|
|
5384
|
+
});
|
|
5385
|
+
const lines = escapeHtml(withoutFences).split("\n");
|
|
5386
|
+
const out = [];
|
|
5387
|
+
let listOpen = false;
|
|
5388
|
+
const closeList = () => {
|
|
5389
|
+
if (listOpen) {
|
|
5390
|
+
out.push("</ul>");
|
|
5391
|
+
listOpen = false;
|
|
5392
|
+
}
|
|
5393
|
+
};
|
|
5394
|
+
for (const line of lines) {
|
|
5395
|
+
const placeholder = line.match(new RegExp(`^${PLACEHOLDER}(\\d+)${PLACEHOLDER}$`));
|
|
5396
|
+
if (placeholder) {
|
|
5397
|
+
closeList();
|
|
5398
|
+
out.push(line);
|
|
5399
|
+
continue;
|
|
5400
|
+
}
|
|
5401
|
+
const item = line.match(/^\s*[-*]\s+(.*)$/);
|
|
5402
|
+
if (item) {
|
|
5403
|
+
if (!listOpen) {
|
|
5404
|
+
out.push("<ul>");
|
|
5405
|
+
listOpen = true;
|
|
5406
|
+
}
|
|
5407
|
+
out.push(`<li>${inline(item[1])}</li>`);
|
|
5408
|
+
continue;
|
|
5409
|
+
}
|
|
5410
|
+
closeList();
|
|
5411
|
+
if (line.trim() !== "")
|
|
5412
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
5413
|
+
}
|
|
5414
|
+
closeList();
|
|
5415
|
+
return out.join("\n").replace(new RegExp(`${PLACEHOLDER}(\\d+)${PLACEHOLDER}`, "g"), (_match, index) => blocks[Number(index)]);
|
|
5416
|
+
}
|
|
5417
|
+
function inline(text) {
|
|
5418
|
+
return text.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(
|
|
5419
|
+
/\[([^\]]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+)\)/g,
|
|
5420
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
|
|
5421
|
+
);
|
|
5422
|
+
}
|
|
5423
|
+
function escapeHtml(text) {
|
|
5424
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
5425
|
+
}
|
|
5426
|
+
|
|
5367
5427
|
class CommandPalette extends Controller {
|
|
5368
|
-
static targets = [
|
|
5428
|
+
static targets = [
|
|
5429
|
+
"input",
|
|
5430
|
+
"list",
|
|
5431
|
+
"empty",
|
|
5432
|
+
"body",
|
|
5433
|
+
"turboList",
|
|
5434
|
+
"itemTemplate",
|
|
5435
|
+
"groupTemplate",
|
|
5436
|
+
"modes",
|
|
5437
|
+
"chat",
|
|
5438
|
+
"transcript",
|
|
5439
|
+
"starters",
|
|
5440
|
+
"messageTemplate",
|
|
5441
|
+
"actionTemplate"
|
|
5442
|
+
];
|
|
5369
5443
|
static values = {
|
|
5370
5444
|
hotkey: { type: String, default: "k" },
|
|
5371
5445
|
items: { type: Array, default: [] },
|
|
5372
5446
|
src: { type: String, default: "" },
|
|
5373
|
-
turboFrameUrl: { type: String, default: "" }
|
|
5447
|
+
turboFrameUrl: { type: String, default: "" },
|
|
5448
|
+
chatEnabled: { type: Boolean, default: false },
|
|
5449
|
+
chatMode: { type: String, default: "proxy" },
|
|
5450
|
+
chatTrigger: { type: String, default: "toggle" },
|
|
5451
|
+
chatAgentic: { type: Boolean, default: true },
|
|
5452
|
+
chatPlaceholder: { type: String, default: "Ask AI\u2026" },
|
|
5453
|
+
chatEndpoint: { type: String, default: "" },
|
|
5454
|
+
chatUrl: { type: String, default: "" },
|
|
5455
|
+
chatModel: { type: String, default: "" }
|
|
5374
5456
|
};
|
|
5375
5457
|
highlightedIndex = -1;
|
|
5376
5458
|
fetchAbort = null;
|
|
5377
5459
|
searchTimer;
|
|
5378
5460
|
allItems = [];
|
|
5461
|
+
mode = "command";
|
|
5462
|
+
commandPlaceholder = "";
|
|
5463
|
+
messages = [];
|
|
5464
|
+
streamAbort = null;
|
|
5379
5465
|
connect() {
|
|
5380
5466
|
this.allItems = this.itemsValue;
|
|
5467
|
+
this.commandPlaceholder = this.inputTarget.placeholder;
|
|
5381
5468
|
document.addEventListener("keydown", this.handleGlobalKeydown);
|
|
5382
5469
|
this.element.addEventListener("click", this.handleBackdropClick);
|
|
5383
5470
|
if (!this.turboFrameUrlValue) {
|
|
@@ -5389,6 +5476,8 @@ class CommandPalette extends Controller {
|
|
|
5389
5476
|
this.element.removeEventListener("click", this.handleBackdropClick);
|
|
5390
5477
|
if (this.fetchAbort)
|
|
5391
5478
|
this.fetchAbort.abort();
|
|
5479
|
+
if (this.streamAbort)
|
|
5480
|
+
this.streamAbort.abort();
|
|
5392
5481
|
if (this.searchTimer)
|
|
5393
5482
|
clearTimeout(this.searchTimer);
|
|
5394
5483
|
}
|
|
@@ -5422,6 +5511,8 @@ class CommandPalette extends Controller {
|
|
|
5422
5511
|
this.element.showModal();
|
|
5423
5512
|
this.inputTarget.value = "";
|
|
5424
5513
|
this.highlightedIndex = -1;
|
|
5514
|
+
if (this.chatEnabledValue)
|
|
5515
|
+
this.resetChat();
|
|
5425
5516
|
if (this.turboFrameUrlValue) {
|
|
5426
5517
|
this.navigateFrame("");
|
|
5427
5518
|
} else {
|
|
@@ -5435,6 +5526,8 @@ class CommandPalette extends Controller {
|
|
|
5435
5526
|
this.element.close();
|
|
5436
5527
|
}
|
|
5437
5528
|
filter() {
|
|
5529
|
+
if (this.mode === "chat")
|
|
5530
|
+
return;
|
|
5438
5531
|
const query = this.inputTarget.value.trim().toLowerCase();
|
|
5439
5532
|
if (this.turboFrameUrlValue) {
|
|
5440
5533
|
this.debouncedNavigateFrame(query);
|
|
@@ -5455,6 +5548,21 @@ class CommandPalette extends Controller {
|
|
|
5455
5548
|
this.renderItems(filtered);
|
|
5456
5549
|
}
|
|
5457
5550
|
navigate(event) {
|
|
5551
|
+
if (this.mode === "chat") {
|
|
5552
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
5553
|
+
event.preventDefault();
|
|
5554
|
+
this.send();
|
|
5555
|
+
} else if (event.key === "Escape") {
|
|
5556
|
+
event.preventDefault();
|
|
5557
|
+
this.close();
|
|
5558
|
+
}
|
|
5559
|
+
return;
|
|
5560
|
+
}
|
|
5561
|
+
if (event.key === "Tab" && !event.shiftKey && this.chatEnabledValue && this.chatTriggerValue === "toggle" && this.inputTarget.value.trim() === "") {
|
|
5562
|
+
event.preventDefault();
|
|
5563
|
+
this.setMode("chat");
|
|
5564
|
+
return;
|
|
5565
|
+
}
|
|
5458
5566
|
const items = this.visibleItems;
|
|
5459
5567
|
switch (event.key) {
|
|
5460
5568
|
case "ArrowDown":
|
|
@@ -5489,6 +5597,225 @@ class CommandPalette extends Controller {
|
|
|
5489
5597
|
this.selectItem(items[index]);
|
|
5490
5598
|
}
|
|
5491
5599
|
}
|
|
5600
|
+
// ── Chat mode ───────────────────────────────────────────────────────
|
|
5601
|
+
switchMode(event) {
|
|
5602
|
+
const mode = event.currentTarget.dataset.mode === "chat" ? "chat" : "command";
|
|
5603
|
+
this.setMode(mode);
|
|
5604
|
+
if (mode === "command")
|
|
5605
|
+
this.filter();
|
|
5606
|
+
this.inputTarget.focus();
|
|
5607
|
+
}
|
|
5608
|
+
useStarter(event) {
|
|
5609
|
+
this.inputTarget.value = event.currentTarget.textContent?.trim() || "";
|
|
5610
|
+
this.send();
|
|
5611
|
+
}
|
|
5612
|
+
resetChat() {
|
|
5613
|
+
if (this.streamAbort)
|
|
5614
|
+
this.streamAbort.abort();
|
|
5615
|
+
this.messages = [];
|
|
5616
|
+
if (this.hasTranscriptTarget)
|
|
5617
|
+
this.transcriptTarget.innerHTML = "";
|
|
5618
|
+
if (this.hasStartersTarget)
|
|
5619
|
+
this.startersTarget.hidden = false;
|
|
5620
|
+
this.setBusy(false);
|
|
5621
|
+
this.setMode("command");
|
|
5622
|
+
}
|
|
5623
|
+
setMode(mode) {
|
|
5624
|
+
if (!this.chatEnabledValue)
|
|
5625
|
+
return;
|
|
5626
|
+
this.mode = mode;
|
|
5627
|
+
const chat = mode === "chat";
|
|
5628
|
+
this.element.classList.toggle("chat-active", chat);
|
|
5629
|
+
if (this.hasModesTarget) {
|
|
5630
|
+
this.modesTarget.querySelectorAll(".command-palette-mode").forEach((el) => {
|
|
5631
|
+
el.setAttribute("aria-pressed", String(el.dataset.mode === mode));
|
|
5632
|
+
});
|
|
5633
|
+
}
|
|
5634
|
+
if (this.hasChatTarget)
|
|
5635
|
+
this.chatTarget.hidden = !chat;
|
|
5636
|
+
if (chat) {
|
|
5637
|
+
this.emptyTarget.hidden = true;
|
|
5638
|
+
if (this.hasListTarget)
|
|
5639
|
+
this.listTarget.hidden = true;
|
|
5640
|
+
if (this.hasTurboListTarget)
|
|
5641
|
+
this.turboListTarget.hidden = true;
|
|
5642
|
+
this.inputTarget.placeholder = this.chatPlaceholderValue;
|
|
5643
|
+
} else {
|
|
5644
|
+
this.inputTarget.placeholder = this.commandPlaceholder;
|
|
5645
|
+
if (this.hasTurboListTarget)
|
|
5646
|
+
this.turboListTarget.hidden = false;
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
stop() {
|
|
5650
|
+
if (this.streamAbort)
|
|
5651
|
+
this.streamAbort.abort();
|
|
5652
|
+
}
|
|
5653
|
+
send() {
|
|
5654
|
+
if (this.streamAbort)
|
|
5655
|
+
return;
|
|
5656
|
+
const text = this.inputTarget.value.trim();
|
|
5657
|
+
if (!text)
|
|
5658
|
+
return;
|
|
5659
|
+
this.appendMessage("user", text);
|
|
5660
|
+
this.messages.push({ role: "user", content: text });
|
|
5661
|
+
this.inputTarget.value = "";
|
|
5662
|
+
if (this.hasStartersTarget)
|
|
5663
|
+
this.startersTarget.hidden = true;
|
|
5664
|
+
void this.streamReply();
|
|
5665
|
+
}
|
|
5666
|
+
async streamReply() {
|
|
5667
|
+
const message = this.appendMessage("assistant", "");
|
|
5668
|
+
const bubble = message.querySelector(".command-palette-message-bubble");
|
|
5669
|
+
this.setBusy(true);
|
|
5670
|
+
this.streamAbort = new AbortController();
|
|
5671
|
+
let reply = "";
|
|
5672
|
+
try {
|
|
5673
|
+
const response = await fetch(this.chatRequestUrl(), {
|
|
5674
|
+
method: "POST",
|
|
5675
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
5676
|
+
body: JSON.stringify(this.chatRequestBody()),
|
|
5677
|
+
signal: this.streamAbort.signal
|
|
5678
|
+
});
|
|
5679
|
+
if (!response.ok || !response.body)
|
|
5680
|
+
throw new Error(`Chat request failed (${response.status})`);
|
|
5681
|
+
const reader = response.body.getReader();
|
|
5682
|
+
const decoder = new TextDecoder();
|
|
5683
|
+
let buffer = "";
|
|
5684
|
+
for (; ; ) {
|
|
5685
|
+
const { value, done } = await reader.read();
|
|
5686
|
+
if (done)
|
|
5687
|
+
break;
|
|
5688
|
+
buffer += decoder.decode(value, { stream: true });
|
|
5689
|
+
const events = buffer.split("\n\n");
|
|
5690
|
+
buffer = events.pop() || "";
|
|
5691
|
+
for (const event of events) {
|
|
5692
|
+
const data = this.parseSseData(event);
|
|
5693
|
+
if (!data)
|
|
5694
|
+
continue;
|
|
5695
|
+
const delta = data.choices?.[0]?.delta?.content;
|
|
5696
|
+
if (delta) {
|
|
5697
|
+
reply += delta;
|
|
5698
|
+
bubble.innerHTML = renderMarkdown(reply);
|
|
5699
|
+
this.scrollTranscript();
|
|
5700
|
+
} else if (data.midwest) {
|
|
5701
|
+
this.handleAgentEvent(data.midwest);
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
this.messages.push({ role: "assistant", content: reply });
|
|
5706
|
+
} catch (error) {
|
|
5707
|
+
if (error.name === "AbortError") {
|
|
5708
|
+
if (reply)
|
|
5709
|
+
this.messages.push({ role: "assistant", content: reply });
|
|
5710
|
+
} else {
|
|
5711
|
+
message.classList.add("is-error");
|
|
5712
|
+
bubble.textContent = "Something went wrong. Please try again.";
|
|
5713
|
+
}
|
|
5714
|
+
} finally {
|
|
5715
|
+
this.streamAbort = null;
|
|
5716
|
+
this.setBusy(false);
|
|
5717
|
+
this.scrollTranscript();
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
parseSseData(event) {
|
|
5721
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
|
|
5722
|
+
if (!data || data === "[DONE]")
|
|
5723
|
+
return null;
|
|
5724
|
+
try {
|
|
5725
|
+
return JSON.parse(data);
|
|
5726
|
+
} catch {
|
|
5727
|
+
return null;
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
// Agentic events: the server streams a tool "action row" and/or a client
|
|
5731
|
+
// directive (navigate / run a palette command). Directives reuse the same
|
|
5732
|
+
// execution path as manual selection.
|
|
5733
|
+
handleAgentEvent(event) {
|
|
5734
|
+
if (event.type === "tool") {
|
|
5735
|
+
this.appendActionRow(event);
|
|
5736
|
+
} else if (event.type === "directive" && event.directive) {
|
|
5737
|
+
this.performDirective(event.directive);
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
// Action rows live in the current assistant message's card footer.
|
|
5741
|
+
appendActionRow(event) {
|
|
5742
|
+
if (!this.hasActionTemplateTarget)
|
|
5743
|
+
return;
|
|
5744
|
+
const footer = this.currentActionFooter();
|
|
5745
|
+
if (!footer)
|
|
5746
|
+
return;
|
|
5747
|
+
const action = cloneTemplate(this.actionTemplateTarget);
|
|
5748
|
+
const label = action.querySelector(".command-palette-action-label");
|
|
5749
|
+
if (label)
|
|
5750
|
+
label.textContent = event.label || "Action";
|
|
5751
|
+
footer.hidden = false;
|
|
5752
|
+
footer.appendChild(action);
|
|
5753
|
+
this.scrollTranscript();
|
|
5754
|
+
}
|
|
5755
|
+
currentActionFooter() {
|
|
5756
|
+
const messages = this.transcriptTarget.querySelectorAll(
|
|
5757
|
+
".command-palette-message.role-assistant"
|
|
5758
|
+
);
|
|
5759
|
+
const last = messages[messages.length - 1];
|
|
5760
|
+
return last ? last.querySelector(".command-palette-action-footer") : null;
|
|
5761
|
+
}
|
|
5762
|
+
performDirective(directive) {
|
|
5763
|
+
if (directive.url) {
|
|
5764
|
+
this.visit(directive.url, directive.method);
|
|
5765
|
+
} else if (directive.action) {
|
|
5766
|
+
this.element.dispatchEvent(
|
|
5767
|
+
new CustomEvent("midwest-command-palette:action", {
|
|
5768
|
+
bubbles: true,
|
|
5769
|
+
detail: { action: directive.action }
|
|
5770
|
+
})
|
|
5771
|
+
);
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
chatRequestUrl() {
|
|
5775
|
+
return this.chatModeValue === "proxy" ? this.chatEndpointValue : this.chatUrlValue;
|
|
5776
|
+
}
|
|
5777
|
+
chatRequestBody() {
|
|
5778
|
+
const body = { messages: this.messages, stream: true };
|
|
5779
|
+
if (this.chatModeValue !== "proxy" && this.chatModelValue)
|
|
5780
|
+
body.model = this.chatModelValue;
|
|
5781
|
+
if (this.chatModeValue === "proxy" && this.chatAgenticValue) {
|
|
5782
|
+
body.commands = this.allItems.map((item) => ({
|
|
5783
|
+
label: item.label,
|
|
5784
|
+
description: item.description,
|
|
5785
|
+
url: item.url,
|
|
5786
|
+
action: item.action,
|
|
5787
|
+
method: item.method,
|
|
5788
|
+
icon: item.icon
|
|
5789
|
+
}));
|
|
5790
|
+
}
|
|
5791
|
+
return body;
|
|
5792
|
+
}
|
|
5793
|
+
setBusy(busy) {
|
|
5794
|
+
this.element.classList.toggle("chat-busy", busy);
|
|
5795
|
+
if (this.hasStopTarget)
|
|
5796
|
+
this.stopTarget.hidden = !busy;
|
|
5797
|
+
if (this.hasTranscriptTarget)
|
|
5798
|
+
this.transcriptTarget.setAttribute("aria-busy", String(busy));
|
|
5799
|
+
}
|
|
5800
|
+
scrollTranscript() {
|
|
5801
|
+
if (this.hasTranscriptTarget)
|
|
5802
|
+
this.transcriptTarget.scrollTop = this.transcriptTarget.scrollHeight;
|
|
5803
|
+
}
|
|
5804
|
+
appendMessage(role, text) {
|
|
5805
|
+
const li = cloneTemplate(this.messageTemplateTarget);
|
|
5806
|
+
li.classList.add(`role-${role}`);
|
|
5807
|
+
if (role === "user")
|
|
5808
|
+
li.classList.add("is-end");
|
|
5809
|
+
const roleEl = li.querySelector(".command-palette-message-role");
|
|
5810
|
+
if (roleEl)
|
|
5811
|
+
roleEl.textContent = role === "user" ? "You" : "Assistant";
|
|
5812
|
+
const bubble = li.querySelector(".command-palette-message-bubble");
|
|
5813
|
+
if (text)
|
|
5814
|
+
bubble.textContent = text;
|
|
5815
|
+
this.transcriptTarget.appendChild(li);
|
|
5816
|
+
this.scrollTranscript();
|
|
5817
|
+
return li;
|
|
5818
|
+
}
|
|
5492
5819
|
// ── Remote search ───────────────────────────────────────────────────
|
|
5493
5820
|
fetchRemote(query) {
|
|
5494
5821
|
if (this.fetchAbort)
|
|
@@ -6555,6 +6882,431 @@ class TimePicker extends Controller {
|
|
|
6555
6882
|
}
|
|
6556
6883
|
}
|
|
6557
6884
|
|
|
6885
|
+
class ViewportObserver extends Controller {
|
|
6886
|
+
static values = {
|
|
6887
|
+
threshold: { type: Number, default: 0 },
|
|
6888
|
+
rootMargin: { type: String, default: "0px" },
|
|
6889
|
+
once: { type: Boolean, default: false },
|
|
6890
|
+
visibleClass: { type: String, default: "" }
|
|
6891
|
+
};
|
|
6892
|
+
observer = null;
|
|
6893
|
+
connect() {
|
|
6894
|
+
this.observer = new IntersectionObserver(
|
|
6895
|
+
(entries) => {
|
|
6896
|
+
for (const entry of entries)
|
|
6897
|
+
this.handle(entry);
|
|
6898
|
+
},
|
|
6899
|
+
{ threshold: this.thresholdValue, rootMargin: this.rootMarginValue }
|
|
6900
|
+
);
|
|
6901
|
+
this.observer.observe(this.element);
|
|
6902
|
+
}
|
|
6903
|
+
disconnect() {
|
|
6904
|
+
this.observer?.disconnect();
|
|
6905
|
+
this.observer = null;
|
|
6906
|
+
}
|
|
6907
|
+
handle(entry) {
|
|
6908
|
+
if (entry.isIntersecting) {
|
|
6909
|
+
this.element.setAttribute("data-intersecting", "true");
|
|
6910
|
+
if (this.visibleClassValue)
|
|
6911
|
+
this.element.classList.add(this.visibleClassValue);
|
|
6912
|
+
this.dispatch("enter", { detail: { entry }, bubbles: true });
|
|
6913
|
+
if (this.onceValue)
|
|
6914
|
+
this.disconnect();
|
|
6915
|
+
} else {
|
|
6916
|
+
this.element.setAttribute("data-intersecting", "false");
|
|
6917
|
+
if (this.visibleClassValue)
|
|
6918
|
+
this.element.classList.remove(this.visibleClassValue);
|
|
6919
|
+
this.dispatch("leave", { detail: { entry }, bubbles: true });
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
const FOCUSABLE$1 = [
|
|
6925
|
+
"a[href]",
|
|
6926
|
+
"button:not([disabled])",
|
|
6927
|
+
"textarea:not([disabled])",
|
|
6928
|
+
"input:not([disabled])",
|
|
6929
|
+
"select:not([disabled])",
|
|
6930
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
6931
|
+
].join(", ");
|
|
6932
|
+
class FocusTrap extends Controller {
|
|
6933
|
+
static values = {
|
|
6934
|
+
active: { type: Boolean, default: true },
|
|
6935
|
+
returnFocus: { type: Boolean, default: true },
|
|
6936
|
+
initialFocus: { type: String, default: "" }
|
|
6937
|
+
};
|
|
6938
|
+
engaged = false;
|
|
6939
|
+
previousFocus = null;
|
|
6940
|
+
// Fires on connect for the initial value and on every subsequent change,
|
|
6941
|
+
// so it is the single entry point for engaging/releasing the trap.
|
|
6942
|
+
activeValueChanged(active) {
|
|
6943
|
+
if (active)
|
|
6944
|
+
this.engage();
|
|
6945
|
+
else
|
|
6946
|
+
this.release();
|
|
6947
|
+
}
|
|
6948
|
+
disconnect() {
|
|
6949
|
+
this.release();
|
|
6950
|
+
}
|
|
6951
|
+
engage() {
|
|
6952
|
+
if (this.engaged)
|
|
6953
|
+
return;
|
|
6954
|
+
this.engaged = true;
|
|
6955
|
+
this.previousFocus = document.activeElement;
|
|
6956
|
+
this.element.addEventListener("keydown", this.handleKeydown);
|
|
6957
|
+
requestAnimationFrame(() => {
|
|
6958
|
+
if (this.engaged)
|
|
6959
|
+
this.focusInitial();
|
|
6960
|
+
});
|
|
6961
|
+
}
|
|
6962
|
+
release() {
|
|
6963
|
+
if (!this.engaged)
|
|
6964
|
+
return;
|
|
6965
|
+
this.engaged = false;
|
|
6966
|
+
this.element.removeEventListener("keydown", this.handleKeydown);
|
|
6967
|
+
if (this.returnFocusValue)
|
|
6968
|
+
this.previousFocus?.focus();
|
|
6969
|
+
this.previousFocus = null;
|
|
6970
|
+
}
|
|
6971
|
+
focusInitial() {
|
|
6972
|
+
const target = this.initialFocusValue ? this.element.querySelector(this.initialFocusValue) : this.focusable()[0];
|
|
6973
|
+
target?.focus();
|
|
6974
|
+
}
|
|
6975
|
+
focusable() {
|
|
6976
|
+
return Array.from(
|
|
6977
|
+
this.element.querySelectorAll(FOCUSABLE$1)
|
|
6978
|
+
).filter((el) => !el.closest("[inert]"));
|
|
6979
|
+
}
|
|
6980
|
+
handleKeydown = (event) => {
|
|
6981
|
+
if (event.key !== "Tab")
|
|
6982
|
+
return;
|
|
6983
|
+
const focusable = this.focusable();
|
|
6984
|
+
if (focusable.length === 0) {
|
|
6985
|
+
event.preventDefault();
|
|
6986
|
+
return;
|
|
6987
|
+
}
|
|
6988
|
+
const first = focusable[0];
|
|
6989
|
+
const last = focusable[focusable.length - 1];
|
|
6990
|
+
const active = document.activeElement;
|
|
6991
|
+
if (event.shiftKey) {
|
|
6992
|
+
if (active === first || !this.element.contains(active)) {
|
|
6993
|
+
event.preventDefault();
|
|
6994
|
+
last.focus();
|
|
6995
|
+
}
|
|
6996
|
+
} else if (active === last || !this.element.contains(active)) {
|
|
6997
|
+
event.preventDefault();
|
|
6998
|
+
first.focus();
|
|
6999
|
+
}
|
|
7000
|
+
};
|
|
7001
|
+
}
|
|
7002
|
+
|
|
7003
|
+
const FOCUSABLE = [
|
|
7004
|
+
"a[href]",
|
|
7005
|
+
"button:not([disabled])",
|
|
7006
|
+
"textarea:not([disabled])",
|
|
7007
|
+
"input:not([disabled])",
|
|
7008
|
+
"select:not([disabled])",
|
|
7009
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
7010
|
+
].join(", ");
|
|
7011
|
+
const GAP = 12;
|
|
7012
|
+
class Onboarding extends Controller {
|
|
7013
|
+
static targets = ["step", "overlay", "card", "title", "body", "back", "next", "done", "arrow"];
|
|
7014
|
+
static values = {
|
|
7015
|
+
autoOpen: { type: Boolean, default: false },
|
|
7016
|
+
confirm: { type: String, default: "" }
|
|
7017
|
+
};
|
|
7018
|
+
steps = [];
|
|
7019
|
+
index = 0;
|
|
7020
|
+
active = false;
|
|
7021
|
+
previousFocus = null;
|
|
7022
|
+
highlighted = null;
|
|
7023
|
+
connect() {
|
|
7024
|
+
this.steps = this.stepTargets.map((el) => ({
|
|
7025
|
+
el,
|
|
7026
|
+
name: el.dataset.name ?? "",
|
|
7027
|
+
selector: el.dataset.selector ?? "",
|
|
7028
|
+
position: el.dataset.position ?? "auto",
|
|
7029
|
+
title: el.dataset.title ?? "",
|
|
7030
|
+
nextText: el.dataset.nextText ?? "Next",
|
|
7031
|
+
backText: el.dataset.backText ?? "Back",
|
|
7032
|
+
completeText: el.dataset.completeText ?? "Done",
|
|
7033
|
+
canClickTarget: el.dataset.canClickTarget === "true"
|
|
7034
|
+
}));
|
|
7035
|
+
window.addEventListener("midwest-onboarding:open", this.handleOpen);
|
|
7036
|
+
if (this.autoOpenValue)
|
|
7037
|
+
this.start();
|
|
7038
|
+
}
|
|
7039
|
+
disconnect() {
|
|
7040
|
+
window.removeEventListener("midwest-onboarding:open", this.handleOpen);
|
|
7041
|
+
this.teardown();
|
|
7042
|
+
}
|
|
7043
|
+
start() {
|
|
7044
|
+
if (this.active || this.steps.length === 0)
|
|
7045
|
+
return;
|
|
7046
|
+
this.active = true;
|
|
7047
|
+
this.previousFocus = document.activeElement;
|
|
7048
|
+
this.overlayTarget.hidden = false;
|
|
7049
|
+
this.cardTarget.hidden = false;
|
|
7050
|
+
document.addEventListener("keydown", this.handleKeydown, true);
|
|
7051
|
+
window.addEventListener("resize", this.reposition);
|
|
7052
|
+
window.addEventListener("scroll", this.reposition, true);
|
|
7053
|
+
this.dispatch("start", { bubbles: true });
|
|
7054
|
+
this.render(0);
|
|
7055
|
+
}
|
|
7056
|
+
next() {
|
|
7057
|
+
if (this.index < this.steps.length - 1)
|
|
7058
|
+
this.render(this.index + 1);
|
|
7059
|
+
else
|
|
7060
|
+
this.complete();
|
|
7061
|
+
}
|
|
7062
|
+
back() {
|
|
7063
|
+
if (this.index > 0)
|
|
7064
|
+
this.render(this.index - 1);
|
|
7065
|
+
}
|
|
7066
|
+
// Jump to a step by its `name`.
|
|
7067
|
+
show(name) {
|
|
7068
|
+
const target = this.steps.findIndex((step) => step.name === name);
|
|
7069
|
+
if (target >= 0) {
|
|
7070
|
+
if (!this.active)
|
|
7071
|
+
this.start();
|
|
7072
|
+
this.render(target);
|
|
7073
|
+
}
|
|
7074
|
+
}
|
|
7075
|
+
complete() {
|
|
7076
|
+
if (!this.active)
|
|
7077
|
+
return;
|
|
7078
|
+
this.teardown();
|
|
7079
|
+
this.dispatch("complete", { bubbles: true });
|
|
7080
|
+
}
|
|
7081
|
+
cancel() {
|
|
7082
|
+
if (!this.active)
|
|
7083
|
+
return;
|
|
7084
|
+
if (this.confirmValue && !window.confirm(this.confirmValue))
|
|
7085
|
+
return;
|
|
7086
|
+
this.teardown();
|
|
7087
|
+
this.dispatch("cancel", { bubbles: true });
|
|
7088
|
+
}
|
|
7089
|
+
render(index) {
|
|
7090
|
+
this.index = index;
|
|
7091
|
+
const step = this.steps[index];
|
|
7092
|
+
this.clearHighlight();
|
|
7093
|
+
this.titleTarget.textContent = step.title;
|
|
7094
|
+
this.renderBody(step);
|
|
7095
|
+
this.renderButtons(step, index);
|
|
7096
|
+
const target = step.selector ? document.querySelector(step.selector) : null;
|
|
7097
|
+
if (target) {
|
|
7098
|
+
this.highlight(target, step.canClickTarget);
|
|
7099
|
+
this.position(target, step.position);
|
|
7100
|
+
} else {
|
|
7101
|
+
this.center();
|
|
7102
|
+
}
|
|
7103
|
+
this.dispatch("show", { detail: { index, name: step.name }, bubbles: true });
|
|
7104
|
+
this.focusCard();
|
|
7105
|
+
}
|
|
7106
|
+
renderBody(step) {
|
|
7107
|
+
this.bodyTarget.replaceChildren();
|
|
7108
|
+
const template = step.el.querySelector("template");
|
|
7109
|
+
if (template)
|
|
7110
|
+
this.bodyTarget.appendChild(template.content.cloneNode(true));
|
|
7111
|
+
}
|
|
7112
|
+
renderButtons(step, index) {
|
|
7113
|
+
const isFirst = index === 0;
|
|
7114
|
+
const isLast = index === this.steps.length - 1;
|
|
7115
|
+
this.backTarget.textContent = step.backText;
|
|
7116
|
+
this.backTarget.style.visibility = isFirst ? "hidden" : "";
|
|
7117
|
+
this.nextTarget.textContent = step.nextText;
|
|
7118
|
+
this.nextTarget.style.display = isLast ? "none" : "";
|
|
7119
|
+
this.doneTarget.textContent = step.completeText;
|
|
7120
|
+
this.doneTarget.style.display = isLast ? "" : "none";
|
|
7121
|
+
}
|
|
7122
|
+
highlight(target, canClickTarget) {
|
|
7123
|
+
target.classList.add("midwest-onboarding-highlight");
|
|
7124
|
+
target.classList.toggle("is-locked", !canClickTarget);
|
|
7125
|
+
this.highlighted = target;
|
|
7126
|
+
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
7127
|
+
}
|
|
7128
|
+
clearHighlight() {
|
|
7129
|
+
this.highlighted?.classList.remove("midwest-onboarding-highlight", "is-locked");
|
|
7130
|
+
this.highlighted = null;
|
|
7131
|
+
}
|
|
7132
|
+
// Places the card against the target, choosing a side when `position` is auto.
|
|
7133
|
+
position(target, position) {
|
|
7134
|
+
const rect = target.getBoundingClientRect();
|
|
7135
|
+
const card = this.cardTarget.getBoundingClientRect();
|
|
7136
|
+
const vw = window.innerWidth;
|
|
7137
|
+
const vh = window.innerHeight;
|
|
7138
|
+
let placement = position;
|
|
7139
|
+
if (placement === "auto") {
|
|
7140
|
+
if (rect.bottom + card.height + GAP <= vh)
|
|
7141
|
+
placement = "bottom";
|
|
7142
|
+
else if (rect.top - card.height - GAP >= 0)
|
|
7143
|
+
placement = "top";
|
|
7144
|
+
else if (rect.right + card.width + GAP <= vw)
|
|
7145
|
+
placement = "right";
|
|
7146
|
+
else
|
|
7147
|
+
placement = "left";
|
|
7148
|
+
}
|
|
7149
|
+
let top;
|
|
7150
|
+
let left;
|
|
7151
|
+
switch (placement) {
|
|
7152
|
+
case "top":
|
|
7153
|
+
top = rect.top - card.height - GAP;
|
|
7154
|
+
left = rect.left + rect.width / 2 - card.width / 2;
|
|
7155
|
+
break;
|
|
7156
|
+
case "left":
|
|
7157
|
+
top = rect.top + rect.height / 2 - card.height / 2;
|
|
7158
|
+
left = rect.left - card.width - GAP;
|
|
7159
|
+
break;
|
|
7160
|
+
case "right":
|
|
7161
|
+
top = rect.top + rect.height / 2 - card.height / 2;
|
|
7162
|
+
left = rect.right + GAP;
|
|
7163
|
+
break;
|
|
7164
|
+
default:
|
|
7165
|
+
top = rect.bottom + GAP;
|
|
7166
|
+
left = rect.left + rect.width / 2 - card.width / 2;
|
|
7167
|
+
break;
|
|
7168
|
+
}
|
|
7169
|
+
left = Math.max(8, Math.min(left, vw - card.width - 8));
|
|
7170
|
+
top = Math.max(8, Math.min(top, vh - card.height - 8));
|
|
7171
|
+
this.cardTarget.style.top = `${top}px`;
|
|
7172
|
+
this.cardTarget.style.left = `${left}px`;
|
|
7173
|
+
this.cardTarget.dataset.placement = placement;
|
|
7174
|
+
this.positionArrow(placement, rect, top, left);
|
|
7175
|
+
}
|
|
7176
|
+
// Nudges the arrow so it points at the target's center, not the card's.
|
|
7177
|
+
positionArrow(placement, rect, top, left) {
|
|
7178
|
+
const card = this.cardTarget.getBoundingClientRect();
|
|
7179
|
+
const arrow = this.arrowTarget.style;
|
|
7180
|
+
if (placement === "top" || placement === "bottom") {
|
|
7181
|
+
const targetCenter = rect.left + rect.width / 2;
|
|
7182
|
+
const offset = Math.max(12, Math.min(targetCenter - left, card.width - 12));
|
|
7183
|
+
arrow.left = `${offset}px`;
|
|
7184
|
+
arrow.top = "";
|
|
7185
|
+
} else {
|
|
7186
|
+
const targetCenter = rect.top + rect.height / 2;
|
|
7187
|
+
const offset = Math.max(12, Math.min(targetCenter - top, card.height - 12));
|
|
7188
|
+
arrow.top = `${offset}px`;
|
|
7189
|
+
arrow.left = "";
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
center() {
|
|
7193
|
+
const card = this.cardTarget.getBoundingClientRect();
|
|
7194
|
+
this.cardTarget.style.top = `${Math.max(8, window.innerHeight / 2 - card.height / 2)}px`;
|
|
7195
|
+
this.cardTarget.style.left = `${Math.max(8, window.innerWidth / 2 - card.width / 2)}px`;
|
|
7196
|
+
this.cardTarget.dataset.placement = "center";
|
|
7197
|
+
}
|
|
7198
|
+
focusCard() {
|
|
7199
|
+
requestAnimationFrame(() => {
|
|
7200
|
+
if (this.active)
|
|
7201
|
+
this.cardTarget.focus();
|
|
7202
|
+
});
|
|
7203
|
+
}
|
|
7204
|
+
teardown() {
|
|
7205
|
+
this.active = false;
|
|
7206
|
+
this.clearHighlight();
|
|
7207
|
+
this.overlayTarget.hidden = true;
|
|
7208
|
+
this.cardTarget.hidden = true;
|
|
7209
|
+
document.removeEventListener("keydown", this.handleKeydown, true);
|
|
7210
|
+
window.removeEventListener("resize", this.reposition);
|
|
7211
|
+
window.removeEventListener("scroll", this.reposition, true);
|
|
7212
|
+
this.previousFocus?.focus();
|
|
7213
|
+
this.previousFocus = null;
|
|
7214
|
+
}
|
|
7215
|
+
handleOpen = (event) => {
|
|
7216
|
+
const detail = event.detail ?? {};
|
|
7217
|
+
const id = String(detail.id ?? "").replace("#", "");
|
|
7218
|
+
if (id === this.element.id)
|
|
7219
|
+
this.start();
|
|
7220
|
+
};
|
|
7221
|
+
reposition = () => {
|
|
7222
|
+
if (!this.active)
|
|
7223
|
+
return;
|
|
7224
|
+
const step = this.steps[this.index];
|
|
7225
|
+
const target = step.selector ? document.querySelector(step.selector) : null;
|
|
7226
|
+
if (target)
|
|
7227
|
+
this.position(target, step.position);
|
|
7228
|
+
else
|
|
7229
|
+
this.center();
|
|
7230
|
+
};
|
|
7231
|
+
handleKeydown = (event) => {
|
|
7232
|
+
if (!this.active)
|
|
7233
|
+
return;
|
|
7234
|
+
switch (event.key) {
|
|
7235
|
+
case "Escape":
|
|
7236
|
+
event.preventDefault();
|
|
7237
|
+
this.cancel();
|
|
7238
|
+
break;
|
|
7239
|
+
case "ArrowRight":
|
|
7240
|
+
event.preventDefault();
|
|
7241
|
+
this.next();
|
|
7242
|
+
break;
|
|
7243
|
+
case "ArrowLeft":
|
|
7244
|
+
event.preventDefault();
|
|
7245
|
+
this.back();
|
|
7246
|
+
break;
|
|
7247
|
+
case "Tab":
|
|
7248
|
+
this.trapTab(event);
|
|
7249
|
+
break;
|
|
7250
|
+
}
|
|
7251
|
+
};
|
|
7252
|
+
// Keep Tab focus within the card while the tour is active.
|
|
7253
|
+
trapTab(event) {
|
|
7254
|
+
const focusable = Array.from(this.cardTarget.querySelectorAll(FOCUSABLE)).filter((el) => !el.hidden && el.offsetParent !== null && getComputedStyle(el).visibility !== "hidden");
|
|
7255
|
+
if (focusable.length === 0)
|
|
7256
|
+
return;
|
|
7257
|
+
const first = focusable[0];
|
|
7258
|
+
const last = focusable[focusable.length - 1];
|
|
7259
|
+
const active = document.activeElement;
|
|
7260
|
+
if (event.shiftKey) {
|
|
7261
|
+
if (active === first || active === this.cardTarget || !this.cardTarget.contains(active)) {
|
|
7262
|
+
event.preventDefault();
|
|
7263
|
+
last.focus();
|
|
7264
|
+
}
|
|
7265
|
+
} else if (active === last || !this.cardTarget.contains(active)) {
|
|
7266
|
+
event.preventDefault();
|
|
7267
|
+
first.focus();
|
|
7268
|
+
}
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
|
|
7272
|
+
class ThemeSwitch extends Controller {
|
|
7273
|
+
static values = {
|
|
7274
|
+
storageKey: { type: String, default: "midwest-theme" }
|
|
7275
|
+
};
|
|
7276
|
+
connect() {
|
|
7277
|
+
const isDark = this.resolveTheme() === "dark";
|
|
7278
|
+
this.apply(isDark);
|
|
7279
|
+
this.element.checked = isDark;
|
|
7280
|
+
}
|
|
7281
|
+
toggle() {
|
|
7282
|
+
const isDark = this.element.checked;
|
|
7283
|
+
this.apply(isDark);
|
|
7284
|
+
this.persist(isDark ? "dark" : "light");
|
|
7285
|
+
}
|
|
7286
|
+
resolveTheme() {
|
|
7287
|
+
const stored = this.read();
|
|
7288
|
+
if (stored === "dark" || stored === "light")
|
|
7289
|
+
return stored;
|
|
7290
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
7291
|
+
}
|
|
7292
|
+
apply(isDark) {
|
|
7293
|
+
document.documentElement.classList.toggle("dark", isDark);
|
|
7294
|
+
}
|
|
7295
|
+
read() {
|
|
7296
|
+
try {
|
|
7297
|
+
return window.localStorage.getItem(this.storageKeyValue);
|
|
7298
|
+
} catch {
|
|
7299
|
+
return null;
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
persist(theme) {
|
|
7303
|
+
try {
|
|
7304
|
+
window.localStorage.setItem(this.storageKeyValue, theme);
|
|
7305
|
+
} catch {
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
|
|
6558
7310
|
function registerMidwestControllers(application) {
|
|
6559
7311
|
application.register("midwest-card", Card);
|
|
6560
7312
|
application.register("midwest-banner", Banner);
|
|
@@ -6590,6 +7342,10 @@ function registerMidwestControllers(application) {
|
|
|
6590
7342
|
application.register("midwest-code-block", CodeBlock);
|
|
6591
7343
|
application.register("midwest-date-picker", DatePicker);
|
|
6592
7344
|
application.register("midwest-time-picker", TimePicker);
|
|
7345
|
+
application.register("midwest-intersection-observer", ViewportObserver);
|
|
7346
|
+
application.register("midwest-focus-trap", FocusTrap);
|
|
7347
|
+
application.register("midwest-onboarding", Onboarding);
|
|
7348
|
+
application.register("midwest-theme-switch", ThemeSwitch);
|
|
6593
7349
|
}
|
|
6594
7350
|
|
|
6595
7351
|
export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };
|