@unabridged/midwest 0.19.2 → 0.20.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.
Files changed (24) hide show
  1. package/README.md +1 -1
  2. package/app/assets/javascript/midwest/index.ts +6 -0
  3. package/app/assets/javascript/midwest/template_helpers.ts +27 -0
  4. package/app/assets/javascript/midwest.js +656 -0
  5. package/app/assets/javascript/midwest.js.map +1 -1
  6. package/app/assets/stylesheets/midwest.css +1 -1
  7. package/app/assets/stylesheets/midwest.tailwind.css +5 -1
  8. package/dist/css/midwest.css +1 -1
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +6 -0
  10. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  11. package/dist/javascript/collection/app/assets/javascript/midwest/template_helpers.js +10 -0
  12. package/dist/javascript/collection/app/assets/javascript/midwest/template_helpers.js.map +1 -0
  13. package/dist/javascript/collection/app/components/midwest/card_component/card_component_controller.js.map +1 -1
  14. package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js +350 -0
  15. package/dist/javascript/collection/app/components/midwest/command_palette_component/command_palette_component_controller.js.map +1 -0
  16. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +24 -0
  17. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
  18. package/dist/javascript/collection/app/components/midwest/show_if_component/show_if_component_controller.js +80 -0
  19. package/dist/javascript/collection/app/components/midwest/show_if_component/show_if_component_controller.js.map +1 -0
  20. package/dist/javascript/collection/app/components/midwest/tree_view_component/tree_view_component_controller.js +204 -0
  21. package/dist/javascript/collection/app/components/midwest/tree_view_component/tree_view_component_controller.js.map +1 -0
  22. package/dist/javascript/midwest.js +656 -0
  23. package/dist/javascript/midwest.js.map +1 -1
  24. package/package.json +1 -1
@@ -3244,8 +3244,12 @@ class FormLiveSummary extends Controller {
3244
3244
  return;
3245
3245
  if (this.element.contains(element))
3246
3246
  return;
3247
+ if (element.closest("fieldset.midwest-show-if[hidden]"))
3248
+ return;
3247
3249
  if (element instanceof HTMLButtonElement)
3248
3250
  return;
3251
+ if (element.closest(".midwest-tree-view"))
3252
+ return;
3249
3253
  if (element instanceof HTMLInputElement) {
3250
3254
  if (SKIPPED_INPUT_TYPES.has(element.type))
3251
3255
  return;
@@ -3306,6 +3310,26 @@ class FormLiveSummary extends Controller {
3306
3310
  const selected = Array.from(ac.querySelectorAll(".midwest-autocomplete-tokens .value")).map((el) => el.textContent?.trim()).filter(Boolean).join(", ");
3307
3311
  fields.push({ label, value: selected || "\u2014" });
3308
3312
  });
3313
+ const seenTreeNames = /* @__PURE__ */ new Set();
3314
+ form.querySelectorAll(".midwest-tree-view").forEach((tree) => {
3315
+ if (this.element.contains(tree))
3316
+ return;
3317
+ const checked = Array.from(
3318
+ tree.querySelectorAll(".tree-node-checkbox:checked")
3319
+ );
3320
+ const firstName = checked[0]?.name || tree.querySelectorAll(".tree-node-checkbox")[0]?.name;
3321
+ if (!firstName)
3322
+ return;
3323
+ if (seenTreeNames.has(firstName))
3324
+ return;
3325
+ seenTreeNames.add(firstName);
3326
+ const label = this.prettifyName(firstName.replace(/\[\]$/, ""));
3327
+ const values = checked.map((cb) => {
3328
+ const nodeLabel = cb.closest(".midwest-tree-node")?.querySelector(":scope > .tree-node-trigger > .tree-node-label");
3329
+ return nodeLabel?.textContent?.trim() || cb.value;
3330
+ });
3331
+ fields.push({ label, value: values.join(", ") || "\u2014" });
3332
+ });
3309
3333
  return fields;
3310
3334
  }
3311
3335
  // Builds a small inline colour swatch to display alongside a colour value.
@@ -4925,6 +4949,635 @@ class Pagination extends Controller {
4925
4949
  }
4926
4950
  }
4927
4951
 
4952
+ class ShowIf extends Controller {
4953
+ static values = {
4954
+ watch: { type: String, default: "" },
4955
+ is: { type: String, default: "" },
4956
+ negate: { type: Boolean, default: false }
4957
+ };
4958
+ sourceElement = null;
4959
+ listenedElements = [];
4960
+ changeListener = null;
4961
+ connect() {
4962
+ if (!this.watchValue)
4963
+ return;
4964
+ this.sourceElement = document.querySelector(this.watchValue);
4965
+ if (!this.sourceElement)
4966
+ return;
4967
+ this.changeListener = () => this.evaluate();
4968
+ if (this.sourceElement instanceof HTMLInputElement && this.sourceElement.type === "radio") {
4969
+ const radios = document.querySelectorAll(
4970
+ `input[type="radio"][name="${this.sourceElement.name}"]`
4971
+ );
4972
+ const listener = this.changeListener;
4973
+ radios.forEach((radio) => {
4974
+ radio.addEventListener("change", listener);
4975
+ this.listenedElements.push(radio);
4976
+ });
4977
+ } else {
4978
+ this.sourceElement.addEventListener("input", this.changeListener);
4979
+ this.sourceElement.addEventListener("change", this.changeListener);
4980
+ this.listenedElements.push(this.sourceElement);
4981
+ }
4982
+ this.evaluate();
4983
+ }
4984
+ disconnect() {
4985
+ if (this.changeListener) {
4986
+ for (const el of this.listenedElements) {
4987
+ el.removeEventListener("input", this.changeListener);
4988
+ el.removeEventListener("change", this.changeListener);
4989
+ }
4990
+ }
4991
+ this.changeListener = null;
4992
+ this.sourceElement = null;
4993
+ this.listenedElements = [];
4994
+ }
4995
+ evaluate() {
4996
+ const el = this.sourceElement;
4997
+ if (!el)
4998
+ return;
4999
+ const currentValue = this.readValue(el);
5000
+ const match = currentValue === this.isValue;
5001
+ const visible = this.negateValue ? !match : match;
5002
+ this.element.toggleAttribute("hidden", !visible);
5003
+ this.element.toggleAttribute("disabled", !visible);
5004
+ }
5005
+ readValue(el) {
5006
+ if (el instanceof HTMLInputElement) {
5007
+ if (el.type === "radio") {
5008
+ const checked = document.querySelector(
5009
+ `input[name="${el.name}"]:checked`
5010
+ );
5011
+ return checked ? checked.value : "";
5012
+ }
5013
+ if (el.type === "checkbox") {
5014
+ return el.checked ? el.value || "true" : "";
5015
+ }
5016
+ return el.value;
5017
+ }
5018
+ if (el instanceof HTMLSelectElement) {
5019
+ return el.value;
5020
+ }
5021
+ if (el instanceof HTMLTextAreaElement) {
5022
+ return el.value;
5023
+ }
5024
+ return el.getAttribute("value") ?? "";
5025
+ }
5026
+ }
5027
+
5028
+ class TreeView extends Controller {
5029
+ // ── Expand / Collapse ───────────────────────────────────────────────
5030
+ toggle(event) {
5031
+ const chevron = event.currentTarget.closest(".tree-node-chevron");
5032
+ if (!chevron)
5033
+ return;
5034
+ const node = chevron.closest(".midwest-tree-node");
5035
+ if (!node)
5036
+ return;
5037
+ const expanded = node.getAttribute("aria-expanded") === "true";
5038
+ node.setAttribute("aria-expanded", String(!expanded));
5039
+ this.focusNode(node);
5040
+ }
5041
+ // ── Checkbox Cascading ──────────────────────────────────────────────
5042
+ checkboxChanged(event) {
5043
+ const checkbox = event.target;
5044
+ const node = checkbox.closest(".midwest-tree-node");
5045
+ if (!node)
5046
+ return;
5047
+ const descendants = node.querySelectorAll(
5048
+ ".tree-node-children .tree-node-checkbox"
5049
+ );
5050
+ for (const desc of descendants) {
5051
+ desc.checked = checkbox.checked;
5052
+ desc.indeterminate = false;
5053
+ }
5054
+ this.updateAncestors(node);
5055
+ }
5056
+ updateAncestors(node) {
5057
+ const parentGroup = node.parentElement;
5058
+ if (!parentGroup || !parentGroup.classList.contains("tree-node-children"))
5059
+ return;
5060
+ const parentNode = parentGroup.closest(".midwest-tree-node");
5061
+ if (!parentNode)
5062
+ return;
5063
+ const parentCheckbox = parentNode.querySelector(
5064
+ ":scope > .tree-node-trigger > .tree-node-checkbox"
5065
+ );
5066
+ if (!parentCheckbox)
5067
+ return;
5068
+ const childCheckboxes = parentGroup.querySelectorAll(
5069
+ ":scope > .midwest-tree-node > .tree-node-trigger > .tree-node-checkbox"
5070
+ );
5071
+ const total = childCheckboxes.length;
5072
+ let checked = 0;
5073
+ let indeterminate = 0;
5074
+ for (const cb of childCheckboxes) {
5075
+ if (cb.indeterminate)
5076
+ indeterminate++;
5077
+ else if (cb.checked)
5078
+ checked++;
5079
+ }
5080
+ if (indeterminate > 0 || checked > 0 && checked < total) {
5081
+ parentCheckbox.checked = false;
5082
+ parentCheckbox.indeterminate = true;
5083
+ } else if (checked === total) {
5084
+ parentCheckbox.checked = true;
5085
+ parentCheckbox.indeterminate = false;
5086
+ } else {
5087
+ parentCheckbox.checked = false;
5088
+ parentCheckbox.indeterminate = false;
5089
+ }
5090
+ this.updateAncestors(parentNode);
5091
+ }
5092
+ // ── Keyboard Navigation (WAI-ARIA TreeView) ────────────────────────
5093
+ connect() {
5094
+ this.element.addEventListener("keydown", this.handleKeydown);
5095
+ const firstNode = this.element.querySelector(".midwest-tree-node");
5096
+ if (firstNode) {
5097
+ firstNode.setAttribute("tabindex", "0");
5098
+ }
5099
+ }
5100
+ disconnect() {
5101
+ this.element.removeEventListener("keydown", this.handleKeydown);
5102
+ }
5103
+ handleKeydown = (event) => {
5104
+ const target = event.target.closest(".midwest-tree-node");
5105
+ if (!target)
5106
+ return;
5107
+ switch (event.key) {
5108
+ case "ArrowDown":
5109
+ event.preventDefault();
5110
+ this.moveFocus(target, "next");
5111
+ break;
5112
+ case "ArrowUp":
5113
+ event.preventDefault();
5114
+ this.moveFocus(target, "previous");
5115
+ break;
5116
+ case "ArrowRight":
5117
+ event.preventDefault();
5118
+ this.handleArrowRight(target);
5119
+ break;
5120
+ case "ArrowLeft":
5121
+ event.preventDefault();
5122
+ this.handleArrowLeft(target);
5123
+ break;
5124
+ case "Home":
5125
+ event.preventDefault();
5126
+ this.focusFirst();
5127
+ break;
5128
+ case "End":
5129
+ event.preventDefault();
5130
+ this.focusLast();
5131
+ break;
5132
+ case "Enter":
5133
+ case " ":
5134
+ event.preventDefault();
5135
+ this.handleActivate(target);
5136
+ break;
5137
+ }
5138
+ };
5139
+ getVisibleNodes() {
5140
+ const nodes = [];
5141
+ const walk = (container) => {
5142
+ const children = container.querySelectorAll(
5143
+ ":scope > .midwest-tree-node"
5144
+ );
5145
+ for (const child of children) {
5146
+ nodes.push(child);
5147
+ if (child.getAttribute("aria-expanded") === "true") {
5148
+ const group = child.querySelector(":scope > .tree-node-children");
5149
+ if (group)
5150
+ walk(group);
5151
+ }
5152
+ }
5153
+ };
5154
+ walk(this.element);
5155
+ return nodes;
5156
+ }
5157
+ moveFocus(current, direction) {
5158
+ const visible = this.getVisibleNodes();
5159
+ const index = visible.indexOf(current);
5160
+ if (index === -1)
5161
+ return;
5162
+ const nextIndex = direction === "next" ? index + 1 : index - 1;
5163
+ if (nextIndex >= 0 && nextIndex < visible.length) {
5164
+ this.focusNode(visible[nextIndex]);
5165
+ }
5166
+ }
5167
+ handleArrowRight(node) {
5168
+ const isExpanded = node.getAttribute("aria-expanded");
5169
+ if (isExpanded === "false") {
5170
+ node.setAttribute("aria-expanded", "true");
5171
+ } else if (isExpanded === "true") {
5172
+ const firstChild = node.querySelector(
5173
+ ":scope > .tree-node-children > .midwest-tree-node"
5174
+ );
5175
+ if (firstChild)
5176
+ this.focusNode(firstChild);
5177
+ }
5178
+ }
5179
+ handleArrowLeft(node) {
5180
+ const isExpanded = node.getAttribute("aria-expanded");
5181
+ if (isExpanded === "true") {
5182
+ node.setAttribute("aria-expanded", "false");
5183
+ } else {
5184
+ const parentGroup = node.parentElement;
5185
+ if (parentGroup && parentGroup.classList.contains("tree-node-children")) {
5186
+ const parentNode = parentGroup.closest(".midwest-tree-node");
5187
+ if (parentNode)
5188
+ this.focusNode(parentNode);
5189
+ }
5190
+ }
5191
+ }
5192
+ handleActivate(node) {
5193
+ const checkbox = node.querySelector(
5194
+ ":scope > .tree-node-trigger > .tree-node-checkbox"
5195
+ );
5196
+ if (checkbox) {
5197
+ checkbox.checked = !checkbox.checked;
5198
+ checkbox.dispatchEvent(new Event("change", { bubbles: true }));
5199
+ return;
5200
+ }
5201
+ const expanded = node.getAttribute("aria-expanded");
5202
+ if (expanded !== null) {
5203
+ node.setAttribute("aria-expanded", String(expanded !== "true"));
5204
+ }
5205
+ }
5206
+ focusNode(node) {
5207
+ const current = this.element.querySelector(
5208
+ '.midwest-tree-node[tabindex="0"]'
5209
+ );
5210
+ if (current)
5211
+ current.setAttribute("tabindex", "-1");
5212
+ node.setAttribute("tabindex", "0");
5213
+ node.focus();
5214
+ }
5215
+ focusFirst() {
5216
+ const first = this.element.querySelector(".midwest-tree-node");
5217
+ if (first)
5218
+ this.focusNode(first);
5219
+ }
5220
+ focusLast() {
5221
+ const visible = this.getVisibleNodes();
5222
+ if (visible.length > 0) {
5223
+ this.focusNode(visible[visible.length - 1]);
5224
+ }
5225
+ }
5226
+ }
5227
+
5228
+ function cloneTemplate(template) {
5229
+ const fragment = template.content.cloneNode(true);
5230
+ const el = fragment.firstElementChild;
5231
+ if (!el)
5232
+ throw new Error("Template has no element child");
5233
+ return el;
5234
+ }
5235
+
5236
+ class CommandPalette extends Controller {
5237
+ static targets = ["input", "list", "empty", "body", "turboList", "itemTemplate", "groupTemplate"];
5238
+ static values = {
5239
+ hotkey: { type: String, default: "k" },
5240
+ items: { type: Array, default: [] },
5241
+ src: { type: String, default: "" },
5242
+ turboFrameUrl: { type: String, default: "" }
5243
+ };
5244
+ highlightedIndex = -1;
5245
+ fetchAbort = null;
5246
+ searchTimer;
5247
+ allItems = [];
5248
+ connect() {
5249
+ this.allItems = this.itemsValue;
5250
+ document.addEventListener("keydown", this.handleGlobalKeydown);
5251
+ this.element.addEventListener("click", this.handleBackdropClick);
5252
+ if (!this.turboFrameUrlValue) {
5253
+ this.renderItems(this.allItems);
5254
+ }
5255
+ }
5256
+ disconnect() {
5257
+ document.removeEventListener("keydown", this.handleGlobalKeydown);
5258
+ this.element.removeEventListener("click", this.handleBackdropClick);
5259
+ if (this.fetchAbort)
5260
+ this.fetchAbort.abort();
5261
+ if (this.searchTimer)
5262
+ clearTimeout(this.searchTimer);
5263
+ }
5264
+ // ── Turbo frame lifecycle ───────────────────────────────────────────
5265
+ turboListTargetConnected(el) {
5266
+ el.addEventListener("turbo:frame-load", this.onFrameLoad);
5267
+ }
5268
+ turboListTargetDisconnected(el) {
5269
+ el.removeEventListener("turbo:frame-load", this.onFrameLoad);
5270
+ }
5271
+ onFrameLoad = () => {
5272
+ if (!this.hasListTarget)
5273
+ return;
5274
+ const items = this.listTarget.querySelectorAll(".command-palette-item");
5275
+ if (items.length > 0) {
5276
+ this.highlightedIndex = 0;
5277
+ this.updateHighlight();
5278
+ this.emptyTarget.hidden = true;
5279
+ if (this.hasListTarget)
5280
+ this.listTarget.hidden = false;
5281
+ } else {
5282
+ this.emptyTarget.hidden = false;
5283
+ if (this.hasListTarget)
5284
+ this.listTarget.hidden = true;
5285
+ }
5286
+ };
5287
+ // ── Public actions ──────────────────────────────────────────────────
5288
+ open() {
5289
+ if (this.element.open)
5290
+ return;
5291
+ this.element.showModal();
5292
+ this.inputTarget.value = "";
5293
+ this.highlightedIndex = -1;
5294
+ if (this.turboFrameUrlValue) {
5295
+ this.navigateFrame("");
5296
+ } else {
5297
+ this.renderItems(this.allItems);
5298
+ }
5299
+ requestAnimationFrame(() => this.inputTarget.focus());
5300
+ }
5301
+ close() {
5302
+ if (!this.element.open)
5303
+ return;
5304
+ this.element.close();
5305
+ }
5306
+ filter() {
5307
+ const query = this.inputTarget.value.trim().toLowerCase();
5308
+ if (this.turboFrameUrlValue) {
5309
+ this.debouncedNavigateFrame(query);
5310
+ return;
5311
+ }
5312
+ if (this.srcValue && query.length > 0) {
5313
+ this.fetchRemote(query);
5314
+ return;
5315
+ }
5316
+ if (query.length === 0) {
5317
+ this.renderItems(this.allItems);
5318
+ return;
5319
+ }
5320
+ const filtered = this.allItems.filter((item) => {
5321
+ const haystack = `${item.label} ${item.description || ""} ${item.group || ""}`.toLowerCase();
5322
+ return haystack.includes(query);
5323
+ });
5324
+ this.renderItems(filtered);
5325
+ }
5326
+ navigate(event) {
5327
+ const items = this.visibleItems;
5328
+ switch (event.key) {
5329
+ case "ArrowDown":
5330
+ event.preventDefault();
5331
+ this.highlightedIndex = Math.min(this.highlightedIndex + 1, items.length - 1);
5332
+ this.updateHighlight();
5333
+ break;
5334
+ case "ArrowUp":
5335
+ event.preventDefault();
5336
+ this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
5337
+ this.updateHighlight();
5338
+ break;
5339
+ case "Enter":
5340
+ event.preventDefault();
5341
+ if (this.highlightedIndex >= 0 && this.highlightedIndex < items.length) {
5342
+ this.selectItem(items[this.highlightedIndex]);
5343
+ }
5344
+ break;
5345
+ case "Escape":
5346
+ event.preventDefault();
5347
+ this.close();
5348
+ break;
5349
+ }
5350
+ }
5351
+ select(event) {
5352
+ const el = event.target.closest(".command-palette-item");
5353
+ if (!el)
5354
+ return;
5355
+ const index = parseInt(el.dataset.index || "-1", 10);
5356
+ const items = this.visibleItems;
5357
+ if (index >= 0 && index < items.length) {
5358
+ this.selectItem(items[index]);
5359
+ }
5360
+ }
5361
+ // ── Remote search ───────────────────────────────────────────────────
5362
+ fetchRemote(query) {
5363
+ if (this.fetchAbort)
5364
+ this.fetchAbort.abort();
5365
+ if (this.searchTimer)
5366
+ clearTimeout(this.searchTimer);
5367
+ this.searchTimer = window.setTimeout(async () => {
5368
+ this.fetchAbort = new AbortController();
5369
+ try {
5370
+ const url = new URL(this.srcValue, window.location.origin);
5371
+ url.searchParams.set("q", query);
5372
+ const response = await fetch(url.toString(), {
5373
+ headers: { Accept: "application/json" },
5374
+ signal: this.fetchAbort.signal
5375
+ });
5376
+ if (response.ok) {
5377
+ const data = await response.json();
5378
+ this.renderItems(data);
5379
+ }
5380
+ } catch (e) {
5381
+ if (e.name !== "AbortError")
5382
+ throw e;
5383
+ }
5384
+ }, 200);
5385
+ }
5386
+ // ── Turbo frame navigation ──────────────────────────────────────────
5387
+ navigateFrame(query) {
5388
+ if (!this.hasTurboListTarget)
5389
+ return;
5390
+ const url = new URL(this.turboFrameUrlValue, window.location.href);
5391
+ if (query)
5392
+ url.searchParams.set("q", query);
5393
+ this.turboListTarget.setAttribute("src", url.toString());
5394
+ }
5395
+ debouncedNavigateFrame(query) {
5396
+ if (this.searchTimer)
5397
+ clearTimeout(this.searchTimer);
5398
+ this.searchTimer = window.setTimeout(() => {
5399
+ this.navigateFrame(query);
5400
+ }, 200);
5401
+ }
5402
+ // ── Rendering ───────────────────────────────────────────────────────
5403
+ renderItems(items) {
5404
+ if (!this.hasListTarget)
5405
+ return;
5406
+ this.listTarget.innerHTML = "";
5407
+ this.highlightedIndex = -1;
5408
+ if (items.length === 0) {
5409
+ this.emptyTarget.hidden = false;
5410
+ this.listTarget.hidden = true;
5411
+ return;
5412
+ }
5413
+ this.emptyTarget.hidden = true;
5414
+ this.listTarget.hidden = false;
5415
+ const groups = /* @__PURE__ */ new Map();
5416
+ for (const item of items) {
5417
+ const group = item.group || "";
5418
+ if (!groups.has(group))
5419
+ groups.set(group, []);
5420
+ groups.get(group).push(item);
5421
+ }
5422
+ let index = 0;
5423
+ for (const [groupName, groupItems] of groups) {
5424
+ if (groupName) {
5425
+ this.listTarget.appendChild(this.buildGroup(groupName));
5426
+ }
5427
+ for (const item of groupItems) {
5428
+ this.listTarget.appendChild(this.buildItem(item, index));
5429
+ index++;
5430
+ }
5431
+ }
5432
+ if (items.length > 0) {
5433
+ this.highlightedIndex = 0;
5434
+ this.updateHighlight();
5435
+ }
5436
+ }
5437
+ buildGroup(name) {
5438
+ const li = cloneTemplate(this.groupTemplateTarget);
5439
+ const h6 = li.querySelector(".midwest-label");
5440
+ if (h6)
5441
+ h6.textContent = name;
5442
+ return li;
5443
+ }
5444
+ buildItem(item, index) {
5445
+ const li = cloneTemplate(this.itemTemplateTarget);
5446
+ li.dataset.index = String(index);
5447
+ if (item.url)
5448
+ li.dataset.url = item.url;
5449
+ if (item.action)
5450
+ li.dataset.paletteAction = item.action;
5451
+ if (item.method)
5452
+ li.dataset.turboMethod = item.method;
5453
+ if (item.confirm)
5454
+ li.dataset.turboConfirm = item.confirm;
5455
+ const iconSpan = li.querySelector(".command-palette-item-icon");
5456
+ if (item.icon) {
5457
+ iconSpan.innerHTML = this.getIconSvg(item.icon);
5458
+ iconSpan.hidden = false;
5459
+ } else {
5460
+ iconSpan.remove();
5461
+ }
5462
+ const label = li.querySelector(".command-palette-item-label");
5463
+ label.textContent = item.label;
5464
+ const desc = li.querySelector(".command-palette-item-description");
5465
+ if (item.description) {
5466
+ desc.textContent = item.description;
5467
+ desc.hidden = false;
5468
+ } else {
5469
+ desc.remove();
5470
+ }
5471
+ const shortcutSpan = li.querySelector(".command-palette-item-shortcut");
5472
+ if (item.shortcut) {
5473
+ item.shortcut.split("+").forEach((key) => {
5474
+ const kbd = document.createElement("kbd");
5475
+ kbd.className = "midwest-kbd";
5476
+ kbd.textContent = key.trim();
5477
+ shortcutSpan.appendChild(kbd);
5478
+ });
5479
+ shortcutSpan.hidden = false;
5480
+ } else {
5481
+ shortcutSpan.remove();
5482
+ }
5483
+ return li;
5484
+ }
5485
+ // ── Selection ───────────────────────────────────────────────────────
5486
+ selectItem(item) {
5487
+ if (item.confirm && !window.confirm(item.confirm))
5488
+ return;
5489
+ this.close();
5490
+ if (item.url) {
5491
+ this.visit(item.url, item.method);
5492
+ return;
5493
+ }
5494
+ if (item.action) {
5495
+ this.element.dispatchEvent(
5496
+ new CustomEvent("midwest-command-palette:action", {
5497
+ bubbles: true,
5498
+ detail: { action: item.action, item }
5499
+ })
5500
+ );
5501
+ }
5502
+ }
5503
+ visit(url, method) {
5504
+ if (method && method.toLowerCase() !== "get") {
5505
+ const link = document.createElement("a");
5506
+ link.href = url;
5507
+ link.dataset.turboMethod = method;
5508
+ link.style.display = "none";
5509
+ document.body.appendChild(link);
5510
+ link.click();
5511
+ link.remove();
5512
+ return;
5513
+ }
5514
+ const turbo = window.Turbo;
5515
+ if (turbo?.visit) {
5516
+ turbo.visit(url);
5517
+ } else {
5518
+ window.location.href = url;
5519
+ }
5520
+ }
5521
+ // ── Highlight ───────────────────────────────────────────────────────
5522
+ get visibleItems() {
5523
+ if (!this.hasListTarget)
5524
+ return [];
5525
+ const items = [];
5526
+ const els = this.listTarget.querySelectorAll(".command-palette-item");
5527
+ for (const el of els) {
5528
+ items.push({
5529
+ label: el.querySelector(".command-palette-item-label")?.textContent || "",
5530
+ url: el.dataset.url,
5531
+ action: el.dataset.paletteAction,
5532
+ method: el.dataset.turboMethod,
5533
+ confirm: el.dataset.turboConfirm
5534
+ });
5535
+ }
5536
+ return items;
5537
+ }
5538
+ updateHighlight() {
5539
+ if (!this.hasListTarget)
5540
+ return;
5541
+ const items = this.listTarget.querySelectorAll(".command-palette-item");
5542
+ items.forEach((el, i) => {
5543
+ el.setAttribute("aria-selected", String(i === this.highlightedIndex));
5544
+ });
5545
+ const highlighted = items[this.highlightedIndex];
5546
+ if (highlighted) {
5547
+ highlighted.scrollIntoView({ block: "nearest" });
5548
+ this.inputTarget.setAttribute("aria-activedescendant", highlighted.id || "");
5549
+ }
5550
+ }
5551
+ // ── Hotkey ──────────────────────────────────────────────────────────
5552
+ handleGlobalKeydown = (event) => {
5553
+ if ((event.metaKey || event.ctrlKey) && event.key === this.hotkeyValue) {
5554
+ event.preventDefault();
5555
+ if (this.element.open) {
5556
+ this.close();
5557
+ } else {
5558
+ this.open();
5559
+ }
5560
+ }
5561
+ };
5562
+ handleBackdropClick = (event) => {
5563
+ if (event.target === this.element)
5564
+ this.close();
5565
+ };
5566
+ // ── Helpers ─────────────────────────────────────────────────────────
5567
+ getIconSvg(name) {
5568
+ const existing = document.querySelector(`.Midwest-Icon__Svg[aria-label="${name}"]`);
5569
+ if (existing)
5570
+ return existing.outerHTML;
5571
+ const allIcons = document.querySelectorAll(".Midwest-Icon__Svg");
5572
+ for (const icon of allIcons) {
5573
+ if (icon.getAttribute("aria-label")?.toLowerCase() === name.toLowerCase()) {
5574
+ return icon.outerHTML;
5575
+ }
5576
+ }
5577
+ return "";
5578
+ }
5579
+ }
5580
+
4928
5581
  function registerMidwestControllers(application) {
4929
5582
  application.register("midwest-card", Card);
4930
5583
  application.register("midwest-banner", Banner);
@@ -4953,6 +5606,9 @@ function registerMidwestControllers(application) {
4953
5606
  application.register("midwest-table", Table);
4954
5607
  application.register("midwest-load-more", LoadMore);
4955
5608
  application.register("midwest-pagination", Pagination);
5609
+ application.register("midwest-show-if", ShowIf);
5610
+ application.register("midwest-tree-view", TreeView);
5611
+ application.register("midwest-command-palette", CommandPalette);
4956
5612
  }
4957
5613
 
4958
5614
  export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };