@unabridged/midwest 0.21.1 → 0.22.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 (27) hide show
  1. package/README.md +63 -0
  2. package/app/assets/javascript/midwest/index.ts +8 -2
  3. package/app/assets/javascript/midwest.js +725 -20
  4. package/app/assets/javascript/midwest.js.map +1 -1
  5. package/app/assets/stylesheets/midwest.css +1 -1
  6. package/app/assets/stylesheets/midwest.tailwind.css +6 -1
  7. package/dist/css/midwest.css +1 -1
  8. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -2
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  10. package/dist/javascript/collection/app/components/midwest/code_block_component/code_block_component_controller.js +17 -0
  11. package/dist/javascript/collection/app/components/midwest/code_block_component/code_block_component_controller.js.map +1 -0
  12. package/dist/javascript/collection/app/components/midwest/dialog_component/dialog_component_controller.js.map +1 -1
  13. package/dist/javascript/collection/app/components/midwest/form/date_picker_component/date_picker_component_controller.js +243 -0
  14. package/dist/javascript/collection/app/components/midwest/form/date_picker_component/date_picker_component_controller.js.map +1 -0
  15. package/dist/javascript/collection/app/components/midwest/form/file_component/file_component_controller.js +35 -1
  16. package/dist/javascript/collection/app/components/midwest/form/file_component/file_component_controller.js.map +1 -1
  17. package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js +72 -2
  18. package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js.map +1 -1
  19. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js +12 -0
  20. package/dist/javascript/collection/app/components/midwest/form/live_summary_component/live_summary_component_controller.js.map +1 -1
  21. package/dist/javascript/collection/app/components/midwest/form/time_picker_component/time_picker_component_controller.js +164 -0
  22. package/dist/javascript/collection/app/components/midwest/form/time_picker_component/time_picker_component_controller.js.map +1 -0
  23. package/dist/javascript/collection/app/components/midwest/password_requirements_component/password_requirements_component_controller.js +194 -0
  24. package/dist/javascript/collection/app/components/midwest/password_requirements_component/password_requirements_component_controller.js.map +1 -0
  25. package/dist/javascript/midwest.js +725 -20
  26. package/dist/javascript/midwest.js.map +1 -1
  27. package/package.json +1 -1
@@ -569,10 +569,11 @@ const FORMATTERS = {
569
569
  }
570
570
  };
571
571
  class Input extends Controller {
572
- static targets = ["input", "counter", "capsLockWarning", "matchError"];
572
+ static targets = ["input", "counter", "capsLockWarning", "matchError", "validationError", "validationMessage"];
573
573
  static values = {
574
574
  match: { type: String, default: "" },
575
- autoformat: { type: String, default: "" }
575
+ autoformat: { type: String, default: "" },
576
+ validate: { type: Boolean, default: false }
576
577
  };
577
578
  updateClearButton = null;
578
579
  updateCounter = null;
@@ -583,6 +584,10 @@ class Input extends Controller {
583
584
  matchSourceListener = null;
584
585
  matchSourceElement = null;
585
586
  autoformatListener = null;
587
+ validateBlurListener = null;
588
+ validateInputListener = null;
589
+ validateInvalidListener = null;
590
+ hasTouched = false;
586
591
  connect() {
587
592
  this.applyColor();
588
593
  this.applySearchClear();
@@ -590,6 +595,7 @@ class Input extends Controller {
590
595
  this.applyPassword();
591
596
  this.applyMatch();
592
597
  this.applyAutoformat();
598
+ this.applyValidation();
593
599
  }
594
600
  disconnect() {
595
601
  if (this.updateClearButton) {
@@ -628,6 +634,18 @@ class Input extends Controller {
628
634
  this.inputTarget.removeEventListener("blur", this.autoformatListener);
629
635
  this.autoformatListener = null;
630
636
  }
637
+ if (this.validateBlurListener) {
638
+ this.inputTarget.removeEventListener("blur", this.validateBlurListener);
639
+ this.validateBlurListener = null;
640
+ }
641
+ if (this.validateInputListener) {
642
+ this.inputTarget.removeEventListener("input", this.validateInputListener);
643
+ this.validateInputListener = null;
644
+ }
645
+ if (this.validateInvalidListener) {
646
+ this.inputTarget.removeEventListener("invalid", this.validateInvalidListener);
647
+ this.validateInvalidListener = null;
648
+ }
631
649
  }
632
650
  increment() {
633
651
  const input = this.inputTarget;
@@ -788,10 +806,63 @@ class Input extends Controller {
788
806
  };
789
807
  this.inputTarget.addEventListener("blur", this.autoformatListener);
790
808
  }
809
+ // Shopify-style inline validation: validates on blur using the HTML5
810
+ // Constraint Validation API. Errors are shown inline after the first
811
+ // blur ("touched"), then re-validated on each input keystroke so the
812
+ // error clears as soon as the field becomes valid.
813
+ applyValidation() {
814
+ if (!this.validateValue)
815
+ return;
816
+ const showValidationError = (message) => {
817
+ this.element.classList.add("has-error", "theme-red");
818
+ this.element.classList.remove("is-valid");
819
+ if (this.hasValidationErrorTarget)
820
+ this.validationErrorTarget.hidden = false;
821
+ if (this.hasValidationMessageTarget)
822
+ this.validationMessageTarget.textContent = message;
823
+ };
824
+ const clearValidationError = () => {
825
+ this.element.classList.remove("has-error", "theme-red");
826
+ if (this.hasValidationErrorTarget)
827
+ this.validationErrorTarget.hidden = true;
828
+ if (this.hasValidationMessageTarget)
829
+ this.validationMessageTarget.textContent = "";
830
+ if (this.inputTarget.value) {
831
+ this.element.classList.add("is-valid");
832
+ } else {
833
+ this.element.classList.remove("is-valid");
834
+ }
835
+ };
836
+ const validate = () => {
837
+ if (!this.inputTarget.checkValidity()) {
838
+ showValidationError(this.inputTarget.validationMessage);
839
+ } else {
840
+ clearValidationError();
841
+ }
842
+ };
843
+ this.validateBlurListener = () => {
844
+ this.hasTouched = true;
845
+ validate();
846
+ };
847
+ this.validateInputListener = () => {
848
+ if (!this.hasTouched)
849
+ return;
850
+ validate();
851
+ };
852
+ this.validateInvalidListener = (e) => {
853
+ e.preventDefault();
854
+ this.hasTouched = true;
855
+ showValidationError(this.inputTarget.validationMessage);
856
+ };
857
+ this.inputTarget.addEventListener("blur", this.validateBlurListener);
858
+ this.inputTarget.addEventListener("input", this.validateInputListener);
859
+ this.inputTarget.addEventListener("invalid", this.validateInvalidListener);
860
+ }
791
861
  }
792
862
 
793
863
  class File extends Controller {
794
- static targets = ["input", "output"];
864
+ static targets = ["input", "output", "dropzone"];
865
+ dragCounter = 0;
795
866
  // Arrow function so `this` is stable for add/removeEventListener pairing.
796
867
  handleChange = () => {
797
868
  this.renderPreviews();
@@ -801,8 +872,41 @@ class File extends Controller {
801
872
  }
802
873
  disconnect() {
803
874
  this.inputTarget.removeEventListener("change", this.handleChange);
875
+ this.dragCounter = 0;
876
+ }
877
+ // --- Drag-and-drop ---
878
+ dragenter(e) {
879
+ e.preventDefault();
880
+ this.dragCounter++;
881
+ this.dropzoneTarget.classList.add("is-dragover");
882
+ }
883
+ dragover(e) {
884
+ e.preventDefault();
885
+ }
886
+ dragleave(e) {
887
+ e.preventDefault();
888
+ this.dragCounter--;
889
+ if (this.dragCounter <= 0) {
890
+ this.dragCounter = 0;
891
+ this.dropzoneTarget.classList.remove("is-dragover");
892
+ }
893
+ }
894
+ drop(e) {
895
+ e.preventDefault();
896
+ this.dragCounter = 0;
897
+ this.dropzoneTarget.classList.remove("is-dragover");
898
+ if (!e.dataTransfer?.files.length)
899
+ return;
900
+ this.inputTarget.files = e.dataTransfer.files;
901
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
902
+ }
903
+ browse() {
904
+ this.inputTarget.click();
804
905
  }
906
+ // --- Preview rendering ---
805
907
  renderPreviews() {
908
+ if (!this.hasOutputTarget)
909
+ return;
806
910
  this.outputTarget.innerHTML = "";
807
911
  this.files.forEach((file) => {
808
912
  this.outputTarget.appendChild(this.buildPreview(file));
@@ -3314,6 +3418,18 @@ class FormLiveSummary extends Controller {
3314
3418
  const selected = Array.from(ac.querySelectorAll(".midwest-autocomplete-tokens .value")).map((el) => el.textContent?.trim()).filter(Boolean).join(", ");
3315
3419
  fields.push({ label, value: selected || "\u2014" });
3316
3420
  });
3421
+ form.querySelectorAll(".midwest-date-picker").forEach((picker) => {
3422
+ if (this.element.contains(picker))
3423
+ return;
3424
+ const input = picker.querySelector('[data-midwest-date-picker-target="input"]');
3425
+ if (!input?.name)
3426
+ return;
3427
+ const labelEl = picker.querySelector(".midwest-form-group__top label");
3428
+ const label = labelEl?.textContent?.trim() || this.prettifyName(input.name.replace(/\[.*\]$/, ""));
3429
+ const display = picker.querySelector('[data-midwest-date-picker-target="display"] .value');
3430
+ const value = display?.textContent?.trim() || "";
3431
+ fields.push({ label, value: value || "\u2014" });
3432
+ });
3317
3433
  const seenTreeNames = /* @__PURE__ */ new Set();
3318
3434
  form.querySelectorAll(".midwest-tree-view").forEach((tree) => {
3319
3435
  if (this.element.contains(tree))
@@ -3502,22 +3618,6 @@ class ConfirmationController extends Controller {
3502
3618
  }
3503
3619
  }
3504
3620
 
3505
- class AvatarGroup extends Controller {
3506
- static targets = ["overflow", "hidden"];
3507
- expand() {
3508
- this.element.classList.add("is-expanding");
3509
- this.hiddenTargets.forEach((wrapper) => {
3510
- wrapper.classList.add("is-visible");
3511
- });
3512
- if (this.hasOverflowTarget) {
3513
- this.overflowTarget.classList.add("avatar-overflow-hiding");
3514
- this.overflowTarget.addEventListener("transitionend", () => {
3515
- this.overflowTarget.remove();
3516
- }, { once: true });
3517
- }
3518
- }
3519
- }
3520
-
3521
3621
  class Chart extends Controller {
3522
3622
  static targets = ["tooltip", "svg"];
3523
3623
  static values = { tooltipEnabled: { type: Boolean, default: true } };
@@ -5853,6 +5953,608 @@ class Repeatable extends Controller {
5853
5953
  }
5854
5954
  }
5855
5955
 
5956
+ const VALIDATORS = {
5957
+ min_length: (v, n) => v.length >= n,
5958
+ uppercase: (v, n) => (v.match(/[A-Z]/g) || []).length >= n,
5959
+ lowercase: (v, n) => (v.match(/[a-z]/g) || []).length >= n,
5960
+ number: (v, n) => (v.match(/[0-9]/g) || []).length >= n,
5961
+ special: (v, n) => (v.match(/[^A-Za-z0-9]/g) || []).length >= n
5962
+ };
5963
+ class PasswordRequirements extends Controller {
5964
+ static targets = ["requirement", "list", "complete", "strength", "strengthLabel", "inline", "panel"];
5965
+ static values = {
5966
+ input: { type: String, default: "" },
5967
+ anchored: { type: Boolean, default: false },
5968
+ anchorName: { type: String, default: "" },
5969
+ inline: { type: Boolean, default: false },
5970
+ inlineAnchorName: { type: String, default: "" }
5971
+ };
5972
+ sourceElement = null;
5973
+ inputListener = null;
5974
+ focusListener = null;
5975
+ blurListener = null;
5976
+ connect() {
5977
+ if (!this.inputValue)
5978
+ return;
5979
+ this.sourceElement = document.querySelector(this.inputValue);
5980
+ if (!this.sourceElement)
5981
+ return;
5982
+ this.inputListener = () => this.validate();
5983
+ this.sourceElement.addEventListener("input", this.inputListener);
5984
+ this.applyAnchorNames();
5985
+ if (this.inlineValue) {
5986
+ this.sourceElement.classList.add("has-inline-password-requirements");
5987
+ }
5988
+ if (this.anchoredValue) {
5989
+ this.applyAnchor();
5990
+ }
5991
+ this.validate();
5992
+ }
5993
+ disconnect() {
5994
+ if (this.sourceElement) {
5995
+ if (this.inputListener)
5996
+ this.sourceElement.removeEventListener("input", this.inputListener);
5997
+ if (this.focusListener)
5998
+ this.sourceElement.removeEventListener("focus", this.focusListener);
5999
+ if (this.blurListener)
6000
+ this.sourceElement.removeEventListener("blur", this.blurListener);
6001
+ if (this.anchoredValue || this.inlineValue) {
6002
+ this.sourceElement.style.removeProperty("anchor-name");
6003
+ }
6004
+ if (this.inlineValue) {
6005
+ this.sourceElement.classList.remove("has-inline-password-requirements");
6006
+ }
6007
+ }
6008
+ this.inputListener = null;
6009
+ this.focusListener = null;
6010
+ this.blurListener = null;
6011
+ this.sourceElement = null;
6012
+ }
6013
+ applyAnchorNames() {
6014
+ if (!this.sourceElement)
6015
+ return;
6016
+ const names = [];
6017
+ if (this.anchoredValue && this.anchorNameValue)
6018
+ names.push(this.anchorNameValue);
6019
+ if (this.inlineValue && this.inlineAnchorNameValue)
6020
+ names.push(this.inlineAnchorNameValue);
6021
+ if (names.length > 0) {
6022
+ this.sourceElement.style.setProperty("anchor-name", names.join(", "));
6023
+ }
6024
+ }
6025
+ applyAnchor() {
6026
+ if (!this.sourceElement || !this.anchorNameValue)
6027
+ return;
6028
+ this.focusListener = () => this.showPopover();
6029
+ this.blurListener = () => this.hidePopover();
6030
+ this.sourceElement.addEventListener("focus", this.focusListener);
6031
+ this.sourceElement.addEventListener("blur", this.blurListener);
6032
+ if (document.activeElement === this.sourceElement) {
6033
+ this.showPopover();
6034
+ }
6035
+ }
6036
+ get popoverElement() {
6037
+ return this.hasPanelTarget ? this.panelTarget : this.element;
6038
+ }
6039
+ showPopover() {
6040
+ try {
6041
+ this.popoverElement.showPopover?.();
6042
+ } catch {
6043
+ }
6044
+ }
6045
+ hidePopover() {
6046
+ try {
6047
+ this.popoverElement.hidePopover?.();
6048
+ } catch {
6049
+ }
6050
+ }
6051
+ validate() {
6052
+ const value = this.sourceElement?.value ?? "";
6053
+ let allMet = true;
6054
+ let metCount = 0;
6055
+ const total = this.requirementTargets.length;
6056
+ this.requirementTargets.forEach((li) => {
6057
+ const type = li.dataset.requirementType ?? "";
6058
+ let met = false;
6059
+ if (type === "pattern") {
6060
+ const pattern = li.dataset.requirementPattern;
6061
+ if (pattern) {
6062
+ try {
6063
+ met = new RegExp(pattern).test(value);
6064
+ } catch {
6065
+ met = false;
6066
+ }
6067
+ }
6068
+ } else {
6069
+ const count = parseInt(li.dataset.requirementCount ?? "1", 10);
6070
+ const validator = VALIDATORS[type];
6071
+ met = validator ? validator(value, count) : false;
6072
+ }
6073
+ li.classList.toggle("is-met", met);
6074
+ const label = li.querySelector(".requirement-label")?.textContent ?? "";
6075
+ li.setAttribute("aria-label", `${label}: ${met ? "met" : "not met"}`);
6076
+ if (met)
6077
+ metCount++;
6078
+ if (!met)
6079
+ allMet = false;
6080
+ });
6081
+ const complete = allMet && value.length > 0;
6082
+ this.popoverElement.classList.toggle("is-complete", complete);
6083
+ if (this.hasListTarget)
6084
+ this.listTarget.hidden = complete;
6085
+ if (this.hasCompleteTarget)
6086
+ this.completeTarget.hidden = !complete;
6087
+ if (this.hasStrengthTarget)
6088
+ this.updateStrength(metCount, total, value.length > 0);
6089
+ if (this.inlineValue)
6090
+ this.updateInlineIndicator(metCount, total, value.length > 0);
6091
+ }
6092
+ updateStrength(met, total, hasValue) {
6093
+ const percentage = total > 0 && hasValue ? Math.round(met / total * 100) : 0;
6094
+ const fill = this.strengthTarget.querySelector(".progress-fill");
6095
+ const track = this.strengthTarget.querySelector('[role="progressbar"]');
6096
+ if (fill)
6097
+ fill.style.width = `${percentage}%`;
6098
+ if (track)
6099
+ track.setAttribute("aria-valuenow", String(percentage));
6100
+ let state = "danger";
6101
+ let label = "Weak";
6102
+ if (percentage >= 100) {
6103
+ state = "success";
6104
+ label = "Strong";
6105
+ } else if (percentage >= 60) {
6106
+ state = "alert";
6107
+ label = "Fair";
6108
+ }
6109
+ const meter = this.strengthTarget.querySelector(".midwest-progress-bar");
6110
+ if (meter) {
6111
+ meter.classList.remove("theme-red", "theme-yellow", "theme-green");
6112
+ const themeClass = { danger: "theme-red", alert: "theme-yellow", success: "theme-green" }[state];
6113
+ if (themeClass)
6114
+ meter.classList.add(themeClass);
6115
+ }
6116
+ if (this.hasStrengthLabelTarget) {
6117
+ this.strengthLabelTarget.textContent = label;
6118
+ }
6119
+ }
6120
+ updateInlineIndicator(met, total, hasValue) {
6121
+ if (!this.hasInlineTarget)
6122
+ return;
6123
+ const el = this.inlineTarget;
6124
+ const percentage = total > 0 && hasValue ? Math.round(met / total * 100) : 0;
6125
+ const fill = el.querySelector(".progress-circular-fill");
6126
+ const svg = el.querySelector('[role="progressbar"]');
6127
+ if (fill) {
6128
+ const r = parseFloat(fill.getAttribute("r") ?? "0");
6129
+ const circumference = 2 * Math.PI * r;
6130
+ const offset = circumference * (1 - percentage / 100);
6131
+ fill.setAttribute("stroke-dashoffset", String(offset));
6132
+ }
6133
+ if (svg)
6134
+ svg.setAttribute("aria-valuenow", String(percentage));
6135
+ let themeClass = "theme-red";
6136
+ if (percentage >= 100) {
6137
+ themeClass = "theme-green";
6138
+ } else if (percentage >= 60) {
6139
+ themeClass = "theme-yellow";
6140
+ }
6141
+ el.classList.remove("theme-red", "theme-yellow", "theme-green");
6142
+ el.classList.add(themeClass);
6143
+ }
6144
+ }
6145
+
6146
+ class CodeBlock extends Controller {
6147
+ static targets = ["source"];
6148
+ async copy() {
6149
+ const text = this.sourceTarget.textContent || "";
6150
+ await navigator.clipboard.writeText(text.trim());
6151
+ const button = this.element.querySelector(".code-block-copy");
6152
+ if (!button)
6153
+ return;
6154
+ button.classList.add("is-copied");
6155
+ setTimeout(() => button.classList.remove("is-copied"), 2e3);
6156
+ }
6157
+ }
6158
+
6159
+ class DatePicker extends Controller {
6160
+ static targets = ["input", "display", "trigger", "popover", "grid", "monthLabel"];
6161
+ static values = {
6162
+ month: String,
6163
+ min: String,
6164
+ max: String
6165
+ };
6166
+ currentMonth = /* @__PURE__ */ new Date();
6167
+ connect() {
6168
+ if (this.monthValue) {
6169
+ this.currentMonth = this.parseDate(this.monthValue) || /* @__PURE__ */ new Date();
6170
+ }
6171
+ this.popoverTarget.addEventListener("toggle", this.handleToggle);
6172
+ this.gridTarget.addEventListener("keydown", this.handleGridKeydown);
6173
+ }
6174
+ disconnect() {
6175
+ this.popoverTarget.removeEventListener("toggle", this.handleToggle);
6176
+ this.gridTarget.removeEventListener("keydown", this.handleGridKeydown);
6177
+ }
6178
+ toggle() {
6179
+ this.popoverTarget.togglePopover();
6180
+ }
6181
+ handleToggle = (event) => {
6182
+ const popover = event.target;
6183
+ const open = popover.matches(":popover-open");
6184
+ this.triggerTarget.setAttribute("aria-expanded", String(open));
6185
+ if (open) {
6186
+ requestAnimationFrame(() => this.focusGridDate());
6187
+ } else {
6188
+ this.triggerTarget.focus();
6189
+ }
6190
+ };
6191
+ selectDate(event) {
6192
+ const button = event.currentTarget;
6193
+ const dateStr = button.dataset.date;
6194
+ if (!dateStr)
6195
+ return;
6196
+ this.inputTarget.value = dateStr;
6197
+ const date = this.parseDate(dateStr);
6198
+ if (date) {
6199
+ this.updateDisplayText(this.formatDisplay(date));
6200
+ this.currentMonth = new Date(date.getFullYear(), date.getMonth(), 1);
6201
+ this.monthValue = this.formatISO(this.currentMonth);
6202
+ }
6203
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
6204
+ this.renderCalendar();
6205
+ this.popoverTarget.hidePopover();
6206
+ }
6207
+ selectToday() {
6208
+ const today = /* @__PURE__ */ new Date();
6209
+ const dateStr = this.formatISO(today);
6210
+ if (this.isDisabledDate(today))
6211
+ return;
6212
+ this.inputTarget.value = dateStr;
6213
+ this.updateDisplayText(this.formatDisplay(today));
6214
+ this.currentMonth = new Date(today.getFullYear(), today.getMonth(), 1);
6215
+ this.monthValue = this.formatISO(this.currentMonth);
6216
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
6217
+ this.renderCalendar();
6218
+ this.popoverTarget.hidePopover();
6219
+ }
6220
+ clear() {
6221
+ this.inputTarget.value = "";
6222
+ this.updateDisplayText(this.element.dataset.placeholder || "Select a date");
6223
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
6224
+ this.renderCalendar();
6225
+ this.popoverTarget.hidePopover();
6226
+ }
6227
+ prevMonth() {
6228
+ this.currentMonth = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth() - 1, 1);
6229
+ this.monthValue = this.formatISO(this.currentMonth);
6230
+ this.renderCalendar();
6231
+ }
6232
+ nextMonth() {
6233
+ this.currentMonth = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth() + 1, 1);
6234
+ this.monthValue = this.formatISO(this.currentMonth);
6235
+ this.renderCalendar();
6236
+ }
6237
+ // -- Keyboard navigation (WAI-ARIA grid pattern) --
6238
+ handleGridKeydown = (event) => {
6239
+ const button = event.target;
6240
+ if (!button.dataset?.date)
6241
+ return;
6242
+ const current = this.parseDate(button.dataset.date);
6243
+ if (!current)
6244
+ return;
6245
+ const next = this.nextDateForKey(event.key, current, event.shiftKey);
6246
+ if (!next)
6247
+ return;
6248
+ event.preventDefault();
6249
+ if (this.isDisabledDate(next))
6250
+ return;
6251
+ if (next.getMonth() !== this.currentMonth.getMonth() || next.getFullYear() !== this.currentMonth.getFullYear()) {
6252
+ this.currentMonth = new Date(next.getFullYear(), next.getMonth(), 1);
6253
+ this.monthValue = this.formatISO(this.currentMonth);
6254
+ this.renderCalendar();
6255
+ }
6256
+ this.focusButton(next);
6257
+ };
6258
+ // -- Calendar rendering --
6259
+ renderCalendar() {
6260
+ const year = this.currentMonth.getFullYear();
6261
+ const month = this.currentMonth.getMonth();
6262
+ this.monthLabelTarget.textContent = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(this.currentMonth);
6263
+ const firstDay = new Date(year, month, 1);
6264
+ const startDate = new Date(firstDay);
6265
+ startDate.setDate(startDate.getDate() - startDate.getDay());
6266
+ const lastDay = new Date(year, month + 1, 0);
6267
+ const endDate = new Date(lastDay);
6268
+ endDate.setDate(endDate.getDate() + (6 - endDate.getDay()));
6269
+ const selectedValue = this.inputTarget.value;
6270
+ const today = this.formatISO(/* @__PURE__ */ new Date());
6271
+ let html = "";
6272
+ const cursor = new Date(startDate);
6273
+ while (cursor <= endDate) {
6274
+ html += "<tr>";
6275
+ for (let i = 0; i < 7; i++) {
6276
+ const dateStr = this.formatISO(cursor);
6277
+ const isOutside = cursor.getMonth() !== month;
6278
+ const isSelected = dateStr === selectedValue;
6279
+ const isToday = dateStr === today;
6280
+ const isDisabled = this.isDisabledDate(cursor);
6281
+ const classes = ["date-picker-day"];
6282
+ if (isOutside)
6283
+ classes.push("is-outside");
6284
+ if (isSelected)
6285
+ classes.push("is-selected");
6286
+ if (isToday)
6287
+ classes.push("is-today");
6288
+ if (isDisabled)
6289
+ classes.push("is-disabled");
6290
+ const ariaLabel = new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric" }).format(cursor);
6291
+ html += `<td role="gridcell">`;
6292
+ html += `<button type="button" class="${classes.join(" ")}"${isDisabled ? " disabled" : ""}`;
6293
+ html += ` tabindex="-1"`;
6294
+ html += ` data-date="${dateStr}" data-action="midwest-date-picker#selectDate"`;
6295
+ html += ` aria-label="${ariaLabel}"${isSelected ? ' aria-selected="true"' : ""}>`;
6296
+ html += `${cursor.getDate()}</button></td>`;
6297
+ cursor.setDate(cursor.getDate() + 1);
6298
+ }
6299
+ html += "</tr>";
6300
+ }
6301
+ this.gridTarget.innerHTML = html;
6302
+ this.setRovingTabindex();
6303
+ }
6304
+ // -- Focus helpers --
6305
+ focusGridDate() {
6306
+ this.setRovingTabindex();
6307
+ const target = this.gridTarget.querySelector('.date-picker-day[tabindex="0"]');
6308
+ target?.focus();
6309
+ }
6310
+ focusButton(date) {
6311
+ const dateStr = this.formatISO(date);
6312
+ const button = this.gridTarget.querySelector(`[data-date="${dateStr}"]`);
6313
+ if (!button)
6314
+ return;
6315
+ this.gridTarget.querySelectorAll('.date-picker-day[tabindex="0"]').forEach((b) => {
6316
+ b.tabIndex = -1;
6317
+ });
6318
+ button.tabIndex = 0;
6319
+ button.focus();
6320
+ }
6321
+ setRovingTabindex() {
6322
+ const selected = this.gridTarget.querySelector(".is-selected");
6323
+ const today = this.gridTarget.querySelector(".is-today:not(.is-outside)");
6324
+ const firstDay = this.gridTarget.querySelector(".date-picker-day:not(.is-outside):not(:disabled)");
6325
+ const target = selected || today || firstDay;
6326
+ if (target)
6327
+ target.tabIndex = 0;
6328
+ }
6329
+ nextDateForKey(key, current, shift) {
6330
+ switch (key) {
6331
+ case "ArrowRight":
6332
+ return this.addDays(current, 1);
6333
+ case "ArrowLeft":
6334
+ return this.addDays(current, -1);
6335
+ case "ArrowDown":
6336
+ return this.addDays(current, 7);
6337
+ case "ArrowUp":
6338
+ return this.addDays(current, -7);
6339
+ case "Home":
6340
+ return this.addDays(current, -current.getDay());
6341
+ case "End":
6342
+ return this.addDays(current, 6 - current.getDay());
6343
+ case "PageUp":
6344
+ return this.shiftMonth(current, shift ? -12 : -1);
6345
+ case "PageDown":
6346
+ return this.shiftMonth(current, shift ? 12 : 1);
6347
+ default:
6348
+ return null;
6349
+ }
6350
+ }
6351
+ // -- Date arithmetic --
6352
+ addDays(date, days) {
6353
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
6354
+ }
6355
+ shiftMonth(date, delta) {
6356
+ const target = new Date(date.getFullYear(), date.getMonth() + delta, 1);
6357
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
6358
+ const day = Math.min(date.getDate(), lastDay);
6359
+ return new Date(target.getFullYear(), target.getMonth(), day);
6360
+ }
6361
+ // -- Utilities --
6362
+ isDisabledDate(date) {
6363
+ if (this.hasMinValue) {
6364
+ const min = this.parseDate(this.minValue);
6365
+ if (min && date < min)
6366
+ return true;
6367
+ }
6368
+ if (this.hasMaxValue) {
6369
+ const max = this.parseDate(this.maxValue);
6370
+ if (max && date > max)
6371
+ return true;
6372
+ }
6373
+ return false;
6374
+ }
6375
+ parseDate(str) {
6376
+ const parts = str.split("-").map(Number);
6377
+ if (parts.length < 3)
6378
+ return null;
6379
+ return new Date(parts[0], parts[1] - 1, parts[2]);
6380
+ }
6381
+ formatISO(date) {
6382
+ const y = date.getFullYear();
6383
+ const m = String(date.getMonth() + 1).padStart(2, "0");
6384
+ const d = String(date.getDate()).padStart(2, "0");
6385
+ return `${y}-${m}-${d}`;
6386
+ }
6387
+ updateDisplayText(text) {
6388
+ const valueEl = this.displayTarget.querySelector(".value");
6389
+ if (valueEl) {
6390
+ valueEl.textContent = text;
6391
+ }
6392
+ }
6393
+ formatDisplay(date) {
6394
+ return new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric" }).format(date);
6395
+ }
6396
+ }
6397
+
6398
+ class TimePicker extends Controller {
6399
+ static targets = ["input", "display", "trigger", "popover", "list", "periodGroup"];
6400
+ static values = {
6401
+ step: { type: Number, default: 15 },
6402
+ period: { type: String, default: "AM" },
6403
+ min: String,
6404
+ max: String
6405
+ };
6406
+ selectedHour12 = null;
6407
+ selectedMinutes = null;
6408
+ connect() {
6409
+ this.popoverTarget.addEventListener("toggle", this.handleToggle);
6410
+ this.listTarget.addEventListener("keydown", this.handleKeydown);
6411
+ const currentValue = this.inputTarget.value;
6412
+ if (currentValue) {
6413
+ const [h, m] = currentValue.split(":").map(Number);
6414
+ this.periodValue = h >= 12 ? "PM" : "AM";
6415
+ this.selectedHour12 = h % 12 || 12;
6416
+ this.selectedMinutes = m;
6417
+ }
6418
+ }
6419
+ disconnect() {
6420
+ this.popoverTarget.removeEventListener("toggle", this.handleToggle);
6421
+ this.listTarget.removeEventListener("keydown", this.handleKeydown);
6422
+ }
6423
+ handleToggle = (event) => {
6424
+ const popover = event.target;
6425
+ const open = popover.matches(":popover-open");
6426
+ this.triggerTarget.setAttribute("aria-expanded", String(open));
6427
+ if (open) {
6428
+ requestAnimationFrame(() => this.focusSelectedOption());
6429
+ } else {
6430
+ this.triggerTarget.focus();
6431
+ }
6432
+ };
6433
+ selectTime(event) {
6434
+ const option = event.currentTarget;
6435
+ this.selectOption(option);
6436
+ }
6437
+ setPeriod(event) {
6438
+ const button = event.currentTarget;
6439
+ const period = button.dataset.period;
6440
+ if (!period)
6441
+ return;
6442
+ this.periodValue = period;
6443
+ this.updatePeriodButtons();
6444
+ this.commitValue();
6445
+ }
6446
+ selectOption(option) {
6447
+ const hour = Number(option.dataset.hour);
6448
+ const minutes = Number(option.dataset.minutes);
6449
+ this.selectedHour12 = hour;
6450
+ this.selectedMinutes = minutes;
6451
+ this.updateSelectedOption(option);
6452
+ this.commitValue();
6453
+ this.popoverTarget.hidePopover();
6454
+ }
6455
+ commitValue() {
6456
+ if (this.selectedHour12 === null || this.selectedMinutes === null)
6457
+ return;
6458
+ const hour24 = this.to24h(this.selectedHour12, this.periodValue);
6459
+ const timeStr = `${String(hour24).padStart(2, "0")}:${String(this.selectedMinutes).padStart(2, "0")}`;
6460
+ this.inputTarget.value = timeStr;
6461
+ this.updateDisplayText(this.formatDisplay(this.selectedHour12, this.selectedMinutes, this.periodValue));
6462
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }));
6463
+ }
6464
+ to24h(hour12, period) {
6465
+ if (period === "AM") {
6466
+ return hour12 === 12 ? 0 : hour12;
6467
+ }
6468
+ return hour12 === 12 ? 12 : hour12 + 12;
6469
+ }
6470
+ formatDisplay(hour12, minutes, period) {
6471
+ return `${hour12}:${String(minutes).padStart(2, "0")} ${period}`;
6472
+ }
6473
+ updatePeriodButtons() {
6474
+ this.periodGroupTarget.querySelectorAll(".time-picker-period-btn").forEach((btn) => {
6475
+ const isActive = btn.dataset.period === this.periodValue;
6476
+ btn.classList.toggle("is-active", isActive);
6477
+ btn.setAttribute("aria-checked", String(isActive));
6478
+ });
6479
+ }
6480
+ updateSelectedOption(selected) {
6481
+ this.listTarget.querySelectorAll(".time-picker-option").forEach((opt) => {
6482
+ const isMatch = opt === selected;
6483
+ opt.classList.toggle("is-selected", isMatch);
6484
+ opt.setAttribute("aria-selected", String(isMatch));
6485
+ opt.tabIndex = isMatch ? 0 : -1;
6486
+ });
6487
+ }
6488
+ focusSelectedOption() {
6489
+ const selected = this.listTarget.querySelector(".is-selected");
6490
+ const target = selected || this.listTarget.querySelector(".time-picker-option");
6491
+ if (!target)
6492
+ return;
6493
+ if (!selected) {
6494
+ target.tabIndex = 0;
6495
+ }
6496
+ target.focus();
6497
+ this.scrollToOption(target);
6498
+ }
6499
+ scrollToOption(option) {
6500
+ const container = this.listTarget;
6501
+ const optionTop = option.offsetTop;
6502
+ const optionHeight = option.offsetHeight;
6503
+ const containerHeight = container.clientHeight;
6504
+ const scrollTop = container.scrollTop;
6505
+ if (optionTop < scrollTop) {
6506
+ container.scrollTop = optionTop;
6507
+ } else if (optionTop + optionHeight > scrollTop + containerHeight) {
6508
+ container.scrollTop = optionTop + optionHeight - containerHeight;
6509
+ }
6510
+ }
6511
+ handleKeydown = (event) => {
6512
+ const current = event.target;
6513
+ if (!current.classList.contains("time-picker-option"))
6514
+ return;
6515
+ if (event.key === "Enter" || event.key === " ") {
6516
+ event.preventDefault();
6517
+ this.selectOption(current);
6518
+ return;
6519
+ }
6520
+ if (event.key === "Escape") {
6521
+ event.preventDefault();
6522
+ this.popoverTarget.hidePopover();
6523
+ return;
6524
+ }
6525
+ const next = this.nextOptionForKey(event.key, current);
6526
+ if (next) {
6527
+ event.preventDefault();
6528
+ current.tabIndex = -1;
6529
+ next.tabIndex = 0;
6530
+ next.focus();
6531
+ this.scrollToOption(next);
6532
+ }
6533
+ };
6534
+ nextOptionForKey(key, current) {
6535
+ const options = Array.from(this.listTarget.querySelectorAll(".time-picker-option"));
6536
+ const index = options.indexOf(current);
6537
+ switch (key) {
6538
+ case "ArrowDown":
6539
+ return options[index + 1] || null;
6540
+ case "ArrowUp":
6541
+ return options[index - 1] || null;
6542
+ case "Home":
6543
+ return options[0] || null;
6544
+ case "End":
6545
+ return options[options.length - 1] || null;
6546
+ default:
6547
+ return null;
6548
+ }
6549
+ }
6550
+ updateDisplayText(text) {
6551
+ const valueEl = this.displayTarget.querySelector(".value");
6552
+ if (valueEl) {
6553
+ valueEl.textContent = text;
6554
+ }
6555
+ }
6556
+ }
6557
+
5856
6558
  function registerMidwestControllers(application) {
5857
6559
  application.register("midwest-card", Card);
5858
6560
  application.register("midwest-banner", Banner);
@@ -5869,7 +6571,6 @@ function registerMidwestControllers(application) {
5869
6571
  application.register("midwest-form", Form);
5870
6572
  application.register("midwest-badge", Badge);
5871
6573
  application.register("midwest-confirmation", ConfirmationController);
5872
- application.register("midwest-avatar-group", AvatarGroup);
5873
6574
  application.register("midwest-chart", Chart);
5874
6575
  application.register("midwest-range", Range);
5875
6576
  application.register("midwest-rating", Rating);
@@ -5885,6 +6586,10 @@ function registerMidwestControllers(application) {
5885
6586
  application.register("midwest-tree-view", TreeView);
5886
6587
  application.register("midwest-command-palette", CommandPalette);
5887
6588
  application.register("midwest-repeatable", Repeatable);
6589
+ application.register("midwest-password-requirements", PasswordRequirements);
6590
+ application.register("midwest-code-block", CodeBlock);
6591
+ application.register("midwest-date-picker", DatePicker);
6592
+ application.register("midwest-time-picker", TimePicker);
5888
6593
  }
5889
6594
 
5890
6595
  export { Banner, Card, Chart, CountdownTimer, registerMidwestControllers };