@unabridged/midwest 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/app/assets/javascript/midwest/index.ts +4 -0
  2. package/app/assets/javascript/midwest.js +407 -33
  3. package/app/assets/javascript/midwest.js.map +1 -1
  4. package/app/assets/stylesheets/midwest.css +1 -1
  5. package/app/assets/stylesheets/midwest.tailwind.css +4 -1
  6. package/dist/css/midwest.css +1 -1
  7. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +4 -0
  8. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  9. package/dist/javascript/collection/app/components/midwest/color_picker_component/color_picker_component_controller.js +135 -0
  10. package/dist/javascript/collection/app/components/midwest/color_picker_component/color_picker_component_controller.js.map +1 -0
  11. package/dist/javascript/collection/app/components/midwest/countdown_timer_component/countdown_timer_component_controller.js +27 -6
  12. package/dist/javascript/collection/app/components/midwest/countdown_timer_component/countdown_timer_component_controller.js.map +1 -1
  13. package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js +109 -21
  14. package/dist/javascript/collection/app/components/midwest/form/autocomplete_component/autocomplete_component_controller.js.map +1 -1
  15. package/dist/javascript/collection/app/components/midwest/form/color_picker_component/color_picker_component_controller.js +129 -0
  16. package/dist/javascript/collection/app/components/midwest/form/color_picker_component/color_picker_component_controller.js.map +1 -0
  17. package/dist/javascript/collection/app/components/midwest/form/input_component/input_component_controller.js +50 -1
  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 +38 -5
  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/notification_component/notification_component_controller.js +60 -0
  22. package/dist/javascript/collection/app/components/midwest/notification_component/notification_component_controller.js.map +1 -0
  23. package/dist/javascript/midwest.d.ts +5 -0
  24. package/dist/javascript/midwest.js +407 -33
  25. package/dist/javascript/midwest.js.map +1 -1
  26. package/package.json +1 -1
@@ -23,6 +23,8 @@ import Range from '../../../components/midwest/form/range_component/range_compon
23
23
  import Rating from '../../../components/midwest/rating_component/rating_component_controller.js';
24
24
  import Autocomplete from '../../../components/midwest/form/autocomplete_component/autocomplete_component_controller.js';
25
25
  import Image from '../../../components/midwest/image_component/image_component_controller.js';
26
+ import ColorPicker from '../../../components/midwest/form/color_picker_component/color_picker_component_controller.js';
27
+ import Notification from '../../../components/midwest/notification_component/notification_component_controller.js';
26
28
 
27
29
  function registerMidwestControllers(application) {
28
30
  application.register("midwest-card", Card);
@@ -46,6 +48,8 @@ function registerMidwestControllers(application) {
46
48
  application.register("midwest-rating", Rating);
47
49
  application.register("midwest-autocomplete", Autocomplete);
48
50
  application.register("midwest-image", Image);
51
+ application.register("midwest-color-picker", ColorPicker);
52
+ application.register("midwest-notification", Notification);
49
53
  }
50
54
 
51
55
  export { registerMidwestControllers };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../../../../app/assets/javascript/midwest/index.ts"],"sourcesContent":["import Card from '../../../components/midwest/card_component/card_component_controller'\nimport Banner from '../../../components/midwest/banner_component/banner_component_controller'\nimport Input from '../../../components/midwest/form/input_component/input_component_controller'\nimport File from '../../../components/midwest/form/file_component/file_component_controller'\nimport Dropdown from '../../../components/midwest/dropdown_component/dropdown_component_controller'\nimport Prose from '../../../components/midwest/prose_component/prose_component_controller'\nimport Dialog from '../../../components/midwest/dialog_component/dialog_component_controller'\nimport Tabs from '../../../components/midwest/tabs_component/tabs_component_controller'\nimport Tooltip from '../../../components/midwest/tooltip_component/tooltip_component_controller'\nimport Carousel from '../../../components/midwest/carousel_component/carousel_component_controller'\nimport FormLiveSummary from '../../../components/midwest/form/live_summary_component/live_summary_component_controller'\nimport Form from '../../../components/midwest/form/form_controller'\nimport CountdownTimer from '../../../components/midwest/countdown_timer_component/countdown_timer_component_controller'\nimport Badge from '../../../components/midwest/badge_component/badge_component_controller'\nimport Confirmation from '../../../components/midwest/confirmation_component/confirmation_component_controller'\nimport AvatarGroup from '../../../components/midwest/avatar_group_component/avatar_group_component_controller'\nimport Chart from '../../../components/midwest/chart_component/chart_component_controller'\nimport Range from '../../../components/midwest/form/range_component/range_component_controller'\nimport Rating from '../../../components/midwest/rating_component/rating_component_controller'\nimport Autocomplete from '../../../components/midwest/form/autocomplete_component/autocomplete_component_controller'\nimport Image from '../../../components/midwest/image_component/image_component_controller'\n/* IMPORTS */\n\nexport { Card, Banner, CountdownTimer, Chart }\n\nexport function registerMidwestControllers (application: any) {\n application.register('midwest-card', Card)\n application.register('midwest-banner', Banner)\n application.register('midwest-countdown-timer', CountdownTimer)\n application.register('midwest-input', Input)\n application.register('midwest-file', File)\n application.register('midwest-dropdown', Dropdown)\n application.register('midwest-prose', Prose)\n application.register('midwest-dialog', Dialog)\n application.register('midwest-tabs', Tabs)\n application.register('midwest-tooltip', Tooltip)\n application.register('midwest-carousel', Carousel)\n application.register('midwest-form-live-summary', FormLiveSummary)\n application.register('midwest-form', Form)\n application.register('midwest-badge', Badge)\n application.register('midwest-confirmation', Confirmation)\n application.register('midwest-avatar-group', AvatarGroup)\n application.register('midwest-chart', Chart)\n application.register('midwest-range', Range)\n application.register('midwest-rating', Rating)\n application.register('midwest-autocomplete', Autocomplete)\n application.register('midwest-image', Image)\n /* EXPORTS */\n}\n"],"names":["Confirmation"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyBO,SAAS,2BAA4B,WAAkB,EAAA;AAC5D,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,kBAAkB,MAAM,CAAA,CAAA;AAC7C,EAAY,WAAA,CAAA,QAAA,CAAS,2BAA2B,cAAc,CAAA,CAAA;AAC9D,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,oBAAoB,QAAQ,CAAA,CAAA;AACjD,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,kBAAkB,MAAM,CAAA,CAAA;AAC7C,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,mBAAmB,OAAO,CAAA,CAAA;AAC/C,EAAY,WAAA,CAAA,QAAA,CAAS,oBAAoB,QAAQ,CAAA,CAAA;AACjD,EAAY,WAAA,CAAA,QAAA,CAAS,6BAA6B,eAAe,CAAA,CAAA;AACjE,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwBA,sBAAY,CAAA,CAAA;AACzD,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwB,WAAW,CAAA,CAAA;AACxD,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,kBAAkB,MAAM,CAAA,CAAA;AAC7C,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwB,YAAY,CAAA,CAAA;AACzD,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAE7C;;;;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../../../../app/assets/javascript/midwest/index.ts"],"sourcesContent":["import Card from '../../../components/midwest/card_component/card_component_controller'\nimport Banner from '../../../components/midwest/banner_component/banner_component_controller'\nimport Input from '../../../components/midwest/form/input_component/input_component_controller'\nimport File from '../../../components/midwest/form/file_component/file_component_controller'\nimport Dropdown from '../../../components/midwest/dropdown_component/dropdown_component_controller'\nimport Prose from '../../../components/midwest/prose_component/prose_component_controller'\nimport Dialog from '../../../components/midwest/dialog_component/dialog_component_controller'\nimport Tabs from '../../../components/midwest/tabs_component/tabs_component_controller'\nimport Tooltip from '../../../components/midwest/tooltip_component/tooltip_component_controller'\nimport Carousel from '../../../components/midwest/carousel_component/carousel_component_controller'\nimport FormLiveSummary from '../../../components/midwest/form/live_summary_component/live_summary_component_controller'\nimport Form from '../../../components/midwest/form/form_controller'\nimport CountdownTimer from '../../../components/midwest/countdown_timer_component/countdown_timer_component_controller'\nimport Badge from '../../../components/midwest/badge_component/badge_component_controller'\nimport Confirmation from '../../../components/midwest/confirmation_component/confirmation_component_controller'\nimport AvatarGroup from '../../../components/midwest/avatar_group_component/avatar_group_component_controller'\nimport Chart from '../../../components/midwest/chart_component/chart_component_controller'\nimport Range from '../../../components/midwest/form/range_component/range_component_controller'\nimport Rating from '../../../components/midwest/rating_component/rating_component_controller'\nimport Autocomplete from '../../../components/midwest/form/autocomplete_component/autocomplete_component_controller'\nimport Image from '../../../components/midwest/image_component/image_component_controller'\nimport ColorPicker from '../../../components/midwest/form/color_picker_component/color_picker_component_controller'\nimport Notification from '../../../components/midwest/notification_component/notification_component_controller'\n/* IMPORTS */\n\nexport { Card, Banner, CountdownTimer, Chart }\n\nexport function registerMidwestControllers (application: any) {\n application.register('midwest-card', Card)\n application.register('midwest-banner', Banner)\n application.register('midwest-countdown-timer', CountdownTimer)\n application.register('midwest-input', Input)\n application.register('midwest-file', File)\n application.register('midwest-dropdown', Dropdown)\n application.register('midwest-prose', Prose)\n application.register('midwest-dialog', Dialog)\n application.register('midwest-tabs', Tabs)\n application.register('midwest-tooltip', Tooltip)\n application.register('midwest-carousel', Carousel)\n application.register('midwest-form-live-summary', FormLiveSummary)\n application.register('midwest-form', Form)\n application.register('midwest-badge', Badge)\n application.register('midwest-confirmation', Confirmation)\n application.register('midwest-avatar-group', AvatarGroup)\n application.register('midwest-chart', Chart)\n application.register('midwest-range', Range)\n application.register('midwest-rating', Rating)\n application.register('midwest-autocomplete', Autocomplete)\n application.register('midwest-image', Image)\n application.register('midwest-color-picker', ColorPicker)\n application.register('midwest-notification', Notification)\n /* EXPORTS */\n}\n"],"names":["Confirmation"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BO,SAAS,2BAA4B,WAAkB,EAAA;AAC5D,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,kBAAkB,MAAM,CAAA,CAAA;AAC7C,EAAY,WAAA,CAAA,QAAA,CAAS,2BAA2B,cAAc,CAAA,CAAA;AAC9D,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,oBAAoB,QAAQ,CAAA,CAAA;AACjD,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,kBAAkB,MAAM,CAAA,CAAA;AAC7C,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,mBAAmB,OAAO,CAAA,CAAA;AAC/C,EAAY,WAAA,CAAA,QAAA,CAAS,oBAAoB,QAAQ,CAAA,CAAA;AACjD,EAAY,WAAA,CAAA,QAAA,CAAS,6BAA6B,eAAe,CAAA,CAAA;AACjE,EAAY,WAAA,CAAA,QAAA,CAAS,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwBA,sBAAY,CAAA,CAAA;AACzD,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwB,WAAW,CAAA,CAAA;AACxD,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,kBAAkB,MAAM,CAAA,CAAA;AAC7C,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwB,YAAY,CAAA,CAAA;AACzD,EAAY,WAAA,CAAA,QAAA,CAAS,iBAAiB,KAAK,CAAA,CAAA;AAC3C,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwB,WAAW,CAAA,CAAA;AACxD,EAAY,WAAA,CAAA,QAAA,CAAS,wBAAwB,YAAY,CAAA,CAAA;AAE3D;;;;"}
@@ -0,0 +1,135 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ class ColorPicker extends Controller {
4
+ static targets = ["input", "swatch", "customSwatch", "colorInput"];
5
+ onCustomColorChange = null;
6
+ get scaleTarget() {
7
+ return this.element.dataset.scaleTarget ?? "theme";
8
+ }
9
+ connect() {
10
+ if (this.hasColorInputTarget) {
11
+ this.onCustomColorChange = (e) => this.handleCustomColor(e);
12
+ this.colorInputTarget.addEventListener("input", this.onCustomColorChange);
13
+ }
14
+ if (this.hasInputTarget && this.inputTarget.value.startsWith("#")) {
15
+ this.restoreCustom(this.inputTarget.value);
16
+ }
17
+ }
18
+ disconnect() {
19
+ if (this.hasColorInputTarget && this.onCustomColorChange) {
20
+ this.colorInputTarget.removeEventListener("input", this.onCustomColorChange);
21
+ this.onCustomColorChange = null;
22
+ }
23
+ }
24
+ select(e) {
25
+ const button = e.currentTarget;
26
+ const color = button.dataset.color ?? "";
27
+ this.applySelection(color);
28
+ this.dispatch("update", { detail: { color, scaleTarget: this.scaleTarget } });
29
+ }
30
+ openCustom(e) {
31
+ e.preventDefault();
32
+ this.colorInputTarget.click();
33
+ }
34
+ handleCustomColor(e) {
35
+ const hex = e.target.value;
36
+ if (this.hasCustomSwatchTarget) {
37
+ this.customSwatchTarget.dataset.color = hex;
38
+ this.customSwatchTarget.style.background = hex;
39
+ }
40
+ const scale = this.generateScale(hex);
41
+ this.applyScale(scale);
42
+ this.applySelection(hex);
43
+ this.dispatch("update", { detail: { color: hex, scale, scaleTarget: this.scaleTarget } });
44
+ }
45
+ restoreCustom(hex) {
46
+ if (this.hasColorInputTarget) {
47
+ this.colorInputTarget.value = hex;
48
+ }
49
+ if (this.hasCustomSwatchTarget) {
50
+ this.customSwatchTarget.style.background = hex;
51
+ }
52
+ const scale = this.generateScale(hex);
53
+ this.applyScale(scale);
54
+ }
55
+ applySelection(color) {
56
+ if (this.hasInputTarget) {
57
+ this.inputTarget.value = color;
58
+ }
59
+ this.swatchTargets.forEach((swatch) => {
60
+ const isSelected = swatch.dataset.color === color;
61
+ swatch.classList.toggle("is-selected", isSelected);
62
+ swatch.setAttribute("aria-pressed", String(isSelected));
63
+ });
64
+ }
65
+ // Generates 13 RGB channel strings (space-separated, matching --{color}-{n}-channels format)
66
+ // from a hex color by fixing the hue/saturation and distributing lightness across the scale.
67
+ generateScale(hex) {
68
+ const [h, s] = this.hexToHsl(hex);
69
+ const lightnessSteps = [97, 90, 82, 74, 65, 56, 48, 40, 33, 26, 18, 10, 3];
70
+ return Object.fromEntries(
71
+ lightnessSteps.map((l, i) => {
72
+ const [r, g, b] = this.hslToRgb(h, s, l);
73
+ return [String(i), `${r} ${g} ${b}`];
74
+ })
75
+ );
76
+ }
77
+ applyScale(scale) {
78
+ const el = this.element;
79
+ Object.entries(scale).forEach(([step, channels]) => {
80
+ el.style.setProperty(`--${this.scaleTarget}-${step}-channels`, channels);
81
+ });
82
+ }
83
+ hexToHsl(hex) {
84
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
85
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
86
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
87
+ const max = Math.max(r, g, b);
88
+ const min = Math.min(r, g, b);
89
+ const l = (max + min) / 2;
90
+ if (max === min)
91
+ return [0, 0, l * 100];
92
+ const d = max - min;
93
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
94
+ let h = 0;
95
+ if (max === r)
96
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
97
+ else if (max === g)
98
+ h = ((b - r) / d + 2) / 6;
99
+ else
100
+ h = ((r - g) / d + 4) / 6;
101
+ return [h * 360, s * 100, l * 100];
102
+ }
103
+ hslToRgb(h, s, l) {
104
+ h /= 360;
105
+ s /= 100;
106
+ l /= 100;
107
+ if (s === 0) {
108
+ const v = Math.round(l * 255);
109
+ return [v, v, v];
110
+ }
111
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
112
+ const p = 2 * l - q;
113
+ return [
114
+ Math.round(this.hue2rgb(p, q, h + 1 / 3) * 255),
115
+ Math.round(this.hue2rgb(p, q, h) * 255),
116
+ Math.round(this.hue2rgb(p, q, h - 1 / 3) * 255)
117
+ ];
118
+ }
119
+ hue2rgb(p, q, t) {
120
+ if (t < 0)
121
+ t += 1;
122
+ if (t > 1)
123
+ t -= 1;
124
+ if (t < 1 / 6)
125
+ return p + (q - p) * 6 * t;
126
+ if (t < 1 / 2)
127
+ return q;
128
+ if (t < 2 / 3)
129
+ return p + (q - p) * (2 / 3 - t) * 6;
130
+ return p;
131
+ }
132
+ }
133
+
134
+ export { ColorPicker as default };
135
+ //# sourceMappingURL=color_picker_component_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"color_picker_component_controller.js","sources":["../../../../../../../app/components/midwest/color_picker_component/color_picker_component_controller.ts"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\n\nexport default class ColorPicker extends Controller {\n static targets = ['input', 'swatch', 'customSwatch', 'colorInput']\n\n declare readonly hasInputTarget: boolean\n declare readonly inputTarget: HTMLInputElement\n declare readonly swatchTargets: HTMLElement[]\n declare readonly hasCustomSwatchTarget: boolean\n declare readonly customSwatchTarget: HTMLButtonElement\n declare readonly hasColorInputTarget: boolean\n declare readonly colorInputTarget: HTMLInputElement\n\n private onCustomColorChange: ((e: Event) => void) | null = null\n\n get scaleTarget (): string {\n return (this.element as HTMLElement).dataset.scaleTarget ?? 'theme'\n }\n\n connect () {\n if (this.hasColorInputTarget) {\n this.onCustomColorChange = (e: Event) => this.handleCustomColor(e)\n this.colorInputTarget.addEventListener('input', this.onCustomColorChange)\n }\n\n // Restore state when initial value is a custom hex color\n if (this.hasInputTarget && this.inputTarget.value.startsWith('#')) {\n this.restoreCustom(this.inputTarget.value)\n }\n }\n\n disconnect () {\n if (this.hasColorInputTarget && this.onCustomColorChange) {\n this.colorInputTarget.removeEventListener('input', this.onCustomColorChange)\n this.onCustomColorChange = null\n }\n }\n\n select (e: Event) {\n const button = e.currentTarget as HTMLElement\n const color = button.dataset.color ?? ''\n this.applySelection(color)\n this.dispatch('update', { detail: { color, scaleTarget: this.scaleTarget } })\n }\n\n openCustom (e: Event) {\n e.preventDefault()\n this.colorInputTarget.click()\n }\n\n private handleCustomColor (e: Event) {\n const hex = (e.target as HTMLInputElement).value\n\n if (this.hasCustomSwatchTarget) {\n this.customSwatchTarget.dataset.color = hex\n this.customSwatchTarget.style.background = hex\n }\n\n const scale = this.generateScale(hex)\n this.applyScale(scale)\n this.applySelection(hex)\n this.dispatch('update', { detail: { color: hex, scale, scaleTarget: this.scaleTarget } })\n }\n\n private restoreCustom (hex: string) {\n if (this.hasColorInputTarget) {\n this.colorInputTarget.value = hex\n }\n if (this.hasCustomSwatchTarget) {\n this.customSwatchTarget.style.background = hex\n }\n const scale = this.generateScale(hex)\n this.applyScale(scale)\n }\n\n private applySelection (color: string) {\n if (this.hasInputTarget) {\n this.inputTarget.value = color\n }\n\n this.swatchTargets.forEach(swatch => {\n const isSelected = swatch.dataset.color === color\n swatch.classList.toggle('is-selected', isSelected)\n swatch.setAttribute('aria-pressed', String(isSelected))\n })\n }\n\n // Generates 13 RGB channel strings (space-separated, matching --{color}-{n}-channels format)\n // from a hex color by fixing the hue/saturation and distributing lightness across the scale.\n private generateScale (hex: string): Record<string, string> {\n const [h, s] = this.hexToHsl(hex)\n\n // Lightness curve derived from the built-in named color scales (0 = lightest, 12 = darkest)\n const lightnessSteps = [97, 90, 82, 74, 65, 56, 48, 40, 33, 26, 18, 10, 3]\n\n return Object.fromEntries(\n lightnessSteps.map((l, i) => {\n const [r, g, b] = this.hslToRgb(h, s, l)\n return [String(i), `${r} ${g} ${b}`]\n })\n )\n }\n\n private applyScale (scale: Record<string, string>) {\n const el = this.element as HTMLElement\n Object.entries(scale).forEach(([step, channels]) => {\n el.style.setProperty(`--${this.scaleTarget}-${step}-channels`, channels)\n })\n }\n\n private hexToHsl (hex: string): [number, number, number] {\n const r = parseInt(hex.slice(1, 3), 16) / 255\n const g = parseInt(hex.slice(3, 5), 16) / 255\n const b = parseInt(hex.slice(5, 7), 16) / 255\n\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n const l = (max + min) / 2\n\n if (max === min) return [0, 0, l * 100]\n\n const d = max - min\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)\n\n let h = 0\n if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6\n else if (max === g) h = ((b - r) / d + 2) / 6\n else h = ((r - g) / d + 4) / 6\n\n return [h * 360, s * 100, l * 100]\n }\n\n private hslToRgb (h: number, s: number, l: number): [number, number, number] {\n h /= 360\n s /= 100\n l /= 100\n\n if (s === 0) {\n const v = Math.round(l * 255)\n return [v, v, v]\n }\n\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s\n const p = 2 * l - q\n\n return [\n Math.round(this.hue2rgb(p, q, h + 1 / 3) * 255),\n Math.round(this.hue2rgb(p, q, h) * 255),\n Math.round(this.hue2rgb(p, q, h - 1 / 3) * 255)\n ]\n }\n\n private hue2rgb (p: number, q: number, t: number): number {\n if (t < 0) t += 1\n if (t > 1) t -= 1\n if (t < 1 / 6) return p + (q - p) * 6 * t\n if (t < 1 / 2) return q\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6\n return p\n }\n}\n"],"names":[],"mappings":"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"}
@@ -5,16 +5,37 @@ class CountdownTimer extends Controller {
5
5
  seconds: { type: Number, default: 5 }
6
6
  };
7
7
  timer = null;
8
+ remaining = 0;
9
+ startTime = 0;
8
10
  connect() {
9
- this.timer = setTimeout(() => {
10
- this.dispatch("complete", { bubbles: true });
11
- }, this.secondsValue * 1e3);
11
+ this.remaining = this.secondsValue * 1e3;
12
+ this.resume();
12
13
  }
13
14
  disconnect() {
14
- if (this.timer !== null) {
15
- clearTimeout(this.timer);
15
+ this.pause();
16
+ }
17
+ pause() {
18
+ if (this.timer === null)
19
+ return;
20
+ clearTimeout(this.timer);
21
+ this.timer = null;
22
+ this.remaining = Math.max(0, this.remaining - (Date.now() - this.startTime));
23
+ this.setPlayState("paused");
24
+ }
25
+ resume() {
26
+ if (this.timer !== null)
27
+ return;
28
+ this.startTime = Date.now();
29
+ this.timer = setTimeout(() => {
16
30
  this.timer = null;
17
- }
31
+ this.dispatch("complete", { bubbles: true });
32
+ }, this.remaining);
33
+ this.setPlayState("running");
34
+ }
35
+ setPlayState(state) {
36
+ const progress = this.element.querySelector(".midwest-countdown-timer__progress");
37
+ if (progress)
38
+ progress.style.animationPlayState = state;
18
39
  }
19
40
  }
20
41
 
@@ -1 +1 @@
1
- {"version":3,"file":"countdown_timer_component_controller.js","sources":["../../../../../../../app/components/midwest/countdown_timer_component/countdown_timer_component_controller.ts"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\n\nexport default class CountdownTimer extends Controller {\n static values = {\n seconds: { type: Number, default: 5 }\n }\n\n declare secondsValue: number\n\n private timer: ReturnType<typeof setTimeout> | null = null\n\n connect () {\n this.timer = setTimeout(() => {\n this.dispatch('complete', { bubbles: true })\n }, this.secondsValue * 1000)\n }\n\n disconnect () {\n if (this.timer !== null) {\n clearTimeout(this.timer)\n this.timer = null\n }\n }\n}\n"],"names":[],"mappings":";;AAEA,MAAqB,uBAAuB,UAAW,CAAA;AAAA,EACrD,OAAO,MAAS,GAAA;AAAA,IACd,OAAS,EAAA,EAAE,IAAM,EAAA,MAAA,EAAQ,SAAS,CAAE,EAAA;AAAA,GACtC,CAAA;AAAA,EAIQ,KAA8C,GAAA,IAAA,CAAA;AAAA,EAEtD,OAAW,GAAA;AACT,IAAK,IAAA,CAAA,KAAA,GAAQ,WAAW,MAAM;AAC5B,MAAA,IAAA,CAAK,QAAS,CAAA,UAAA,EAAY,EAAE,OAAA,EAAS,MAAM,CAAA,CAAA;AAAA,KAC7C,EAAG,IAAK,CAAA,YAAA,GAAe,GAAI,CAAA,CAAA;AAAA,GAC7B;AAAA,EAEA,UAAc,GAAA;AACZ,IAAI,IAAA,IAAA,CAAK,UAAU,IAAM,EAAA;AACvB,MAAA,YAAA,CAAa,KAAK,KAAK,CAAA,CAAA;AACvB,MAAA,IAAA,CAAK,KAAQ,GAAA,IAAA,CAAA;AAAA,KACf;AAAA,GACF;AACF;;;;"}
1
+ {"version":3,"file":"countdown_timer_component_controller.js","sources":["../../../../../../../app/components/midwest/countdown_timer_component/countdown_timer_component_controller.ts"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\n\nexport default class CountdownTimer extends Controller {\n static values = {\n seconds: { type: Number, default: 5 }\n }\n\n declare secondsValue: number\n\n private timer: ReturnType<typeof setTimeout> | null = null\n private remaining = 0\n private startTime = 0\n\n connect () {\n this.remaining = this.secondsValue * 1000\n this.resume()\n }\n\n disconnect () {\n this.pause()\n }\n\n pause () {\n if (this.timer === null) return\n clearTimeout(this.timer)\n this.timer = null\n this.remaining = Math.max(0, this.remaining - (Date.now() - this.startTime))\n this.setPlayState('paused')\n }\n\n resume () {\n if (this.timer !== null) return // already running\n this.startTime = Date.now()\n this.timer = setTimeout(() => {\n this.timer = null\n this.dispatch('complete', { bubbles: true })\n }, this.remaining)\n this.setPlayState('running')\n }\n\n private setPlayState (state: 'running' | 'paused') {\n const progress = this.element.querySelector<SVGElement>('.midwest-countdown-timer__progress')\n if (progress) progress.style.animationPlayState = state\n }\n}\n"],"names":[],"mappings":";;AAEA,MAAqB,uBAAuB,UAAW,CAAA;AAAA,EACrD,OAAO,MAAS,GAAA;AAAA,IACd,OAAS,EAAA,EAAE,IAAM,EAAA,MAAA,EAAQ,SAAS,CAAE,EAAA;AAAA,GACtC,CAAA;AAAA,EAIQ,KAA8C,GAAA,IAAA,CAAA;AAAA,EAC9C,SAAY,GAAA,CAAA,CAAA;AAAA,EACZ,SAAY,GAAA,CAAA,CAAA;AAAA,EAEpB,OAAW,GAAA;AACT,IAAK,IAAA,CAAA,SAAA,GAAY,KAAK,YAAe,GAAA,GAAA,CAAA;AACrC,IAAA,IAAA,CAAK,MAAO,EAAA,CAAA;AAAA,GACd;AAAA,EAEA,UAAc,GAAA;AACZ,IAAA,IAAA,CAAK,KAAM,EAAA,CAAA;AAAA,GACb;AAAA,EAEA,KAAS,GAAA;AACP,IAAA,IAAI,KAAK,KAAU,KAAA,IAAA;AAAM,MAAA,OAAA;AACzB,IAAA,YAAA,CAAa,KAAK,KAAK,CAAA,CAAA;AACvB,IAAA,IAAA,CAAK,KAAQ,GAAA,IAAA,CAAA;AACb,IAAK,IAAA,CAAA,SAAA,GAAY,IAAK,CAAA,GAAA,CAAI,CAAG,EAAA,IAAA,CAAK,aAAa,IAAK,CAAA,GAAA,EAAQ,GAAA,IAAA,CAAK,SAAU,CAAA,CAAA,CAAA;AAC3E,IAAA,IAAA,CAAK,aAAa,QAAQ,CAAA,CAAA;AAAA,GAC5B;AAAA,EAEA,MAAU,GAAA;AACR,IAAA,IAAI,KAAK,KAAU,KAAA,IAAA;AAAM,MAAA,OAAA;AACzB,IAAK,IAAA,CAAA,SAAA,GAAY,KAAK,GAAI,EAAA,CAAA;AAC1B,IAAK,IAAA,CAAA,KAAA,GAAQ,WAAW,MAAM;AAC5B,MAAA,IAAA,CAAK,KAAQ,GAAA,IAAA,CAAA;AACb,MAAA,IAAA,CAAK,QAAS,CAAA,UAAA,EAAY,EAAE,OAAA,EAAS,MAAM,CAAA,CAAA;AAAA,KAC7C,EAAG,KAAK,SAAS,CAAA,CAAA;AACjB,IAAA,IAAA,CAAK,aAAa,SAAS,CAAA,CAAA;AAAA,GAC7B;AAAA,EAEQ,aAAc,KAA6B,EAAA;AACjD,IAAA,MAAM,QAAW,GAAA,IAAA,CAAK,OAAQ,CAAA,aAAA,CAA0B,oCAAoC,CAAA,CAAA;AAC5F,IAAI,IAAA,QAAA;AAAU,MAAA,QAAA,CAAS,MAAM,kBAAqB,GAAA,KAAA,CAAA;AAAA,GACpD;AACF;;;;"}
@@ -1,19 +1,30 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  class Autocomplete extends Controller {
4
- static targets = ["input", "dropdown", "list", "tokens", "multipleTokenTemplate", "singleTokenTemplate", "status", "turboList", "loader"];
4
+ static targets = [
5
+ "input",
6
+ "dropdown",
7
+ "list",
8
+ "tokens",
9
+ "multipleTokenTemplate",
10
+ "singleTokenTemplate",
11
+ "status",
12
+ "turboList",
13
+ "loader",
14
+ "freeFormHintList",
15
+ "freeFormHintValue"
16
+ ];
5
17
  static values = {
6
18
  options: { type: Array, default: [] },
7
19
  url: { type: String, default: "" },
8
20
  multiple: { type: Boolean, default: true },
9
- turboFrameUrl: { type: String, default: "" }
21
+ turboFrameUrl: { type: String, default: "" },
22
+ freeForm: { type: Boolean, default: false }
10
23
  };
11
24
  highlightedIndex = -1;
12
25
  fetchAbort = null;
13
26
  searchTimer;
14
27
  // ── Named handlers ─────────────────────────────────────────────────────────
15
- // Arrow-function class properties preserve `this` and produce a stable
16
- // reference so addEventListener/removeEventListener pairs always match.
17
28
  onFrameLoad = () => {
18
29
  this.syncSelectedState();
19
30
  this.loading = false;
@@ -48,9 +59,6 @@ class Autocomplete extends Controller {
48
59
  clearTimeout(this.searchTimer);
49
60
  this.element.removeEventListener("focusout", this.onFocusOut);
50
61
  }
51
- // Target callbacks — Stimulus calls these automatically when targets connect
52
- // or disconnect, including after the initial connect(). This replaces manual
53
- // hasTurboListTarget guards and addEventListener calls inside connect().
54
62
  dropdownTargetConnected(el) {
55
63
  el.addEventListener("toggle", this.onToggle);
56
64
  }
@@ -75,13 +83,27 @@ class Autocomplete extends Controller {
75
83
  this.open();
76
84
  }
77
85
  open() {
86
+ if (!this.hasDropdownTarget)
87
+ return;
78
88
  if (this.dropdownTarget.matches(":popover-open"))
79
89
  return;
90
+ const query = this.inputTarget.value.trim();
91
+ if (this.freeFormValue && !this.hasListTarget && !this.hasTurboListTarget) {
92
+ if (query) {
93
+ this.updateFreeFormHint(query);
94
+ this.showDropdown();
95
+ }
96
+ return;
97
+ }
80
98
  clearTimeout(this.searchTimer);
81
- this.resolve(this.inputTarget.value.trim());
99
+ this.resolve(query);
82
100
  }
83
101
  search() {
84
102
  const query = this.inputTarget.value.trim();
103
+ if (this.freeFormValue)
104
+ this.updateFreeFormHint(query);
105
+ if (!this.hasListTarget && !this.hasTurboListTarget)
106
+ return;
85
107
  if (!this.hasTurboListTarget && !this.urlValue) {
86
108
  this.renderOptions(this.filteredOptions(query));
87
109
  return;
@@ -96,6 +118,8 @@ class Autocomplete extends Controller {
96
118
  switch (event.key) {
97
119
  case "ArrowDown": {
98
120
  event.preventDefault();
121
+ if (!this.hasDropdownTarget)
122
+ break;
99
123
  if (!this.dropdownTarget.matches(":popover-open"))
100
124
  this.open();
101
125
  const items = this.visibleOptions();
@@ -105,7 +129,7 @@ class Autocomplete extends Controller {
105
129
  }
106
130
  case "ArrowUp": {
107
131
  event.preventDefault();
108
- if (!this.dropdownTarget.matches(":popover-open"))
132
+ if (!this.hasDropdownTarget || !this.dropdownTarget.matches(":popover-open"))
109
133
  break;
110
134
  const items = this.visibleOptions();
111
135
  this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
@@ -114,11 +138,22 @@ class Autocomplete extends Controller {
114
138
  }
115
139
  case "Enter": {
116
140
  event.preventDefault();
117
- if (!this.dropdownTarget.matches(":popover-open"))
118
- break;
119
- const items = this.visibleOptions();
120
- if (this.highlightedIndex >= 0 && items[this.highlightedIndex]) {
121
- this.pickItem(items[this.highlightedIndex]);
141
+ if (this.hasDropdownTarget && this.dropdownTarget.matches(":popover-open") && (this.hasListTarget || this.hasTurboListTarget)) {
142
+ const items = this.visibleOptions();
143
+ if (this.highlightedIndex >= 0 && items[this.highlightedIndex]) {
144
+ this.pickItem(items[this.highlightedIndex]);
145
+ break;
146
+ }
147
+ }
148
+ if (this.freeFormValue) {
149
+ this.commitFreeFormTag();
150
+ }
151
+ break;
152
+ }
153
+ case ",": {
154
+ if (this.freeFormValue && this.inputTarget.value.trim()) {
155
+ event.preventDefault();
156
+ this.commitFreeFormTag();
122
157
  }
123
158
  break;
124
159
  }
@@ -135,6 +170,22 @@ class Autocomplete extends Controller {
135
170
  break;
136
171
  }
137
172
  }
173
+ // Splits pasted text on commas and newlines into individual tags (free-form mode only).
174
+ pasteTag(event) {
175
+ if (!this.freeFormValue)
176
+ return;
177
+ const text = event.clipboardData?.getData("text") ?? "";
178
+ const tags = text.split(/[\n,]+/).map((t) => t.trim()).filter(Boolean);
179
+ if (tags.length === 0)
180
+ return;
181
+ event.preventDefault();
182
+ tags.forEach((tag) => this.addFreeFormTag(tag));
183
+ }
184
+ // Mousedown on the "Hit Enter to add 'X'" hint row.
185
+ addFreeFormTagFromHint(event) {
186
+ event.preventDefault();
187
+ this.commitFreeFormTag();
188
+ }
138
189
  removeToken(event) {
139
190
  event.stopPropagation();
140
191
  const btn = event.currentTarget;
@@ -142,9 +193,6 @@ class Autocomplete extends Controller {
142
193
  if (value)
143
194
  this.deselect(value);
144
195
  }
145
- // Public action used by turbo-frame option items via data-action.
146
- // Each server-rendered <li> should have:
147
- // data-action="mousedown->midwest-autocomplete#selectOption"
148
196
  selectOption(event) {
149
197
  event.preventDefault();
150
198
  this.pickItem(event.currentTarget);
@@ -156,7 +204,6 @@ class Autocomplete extends Controller {
156
204
  isSelected(value) {
157
205
  return !!this.tokensTarget.querySelector(`[data-value="${CSS.escape(value)}"]`);
158
206
  }
159
- // Routes to the correct data source. Shared by open() and the debounced search path.
160
207
  resolve(query) {
161
208
  if (this.hasTurboListTarget) {
162
209
  this.navigateFrame(query);
@@ -185,9 +232,6 @@ class Autocomplete extends Controller {
185
232
  this.turboListTarget.setAttribute("src", url.toString());
186
233
  this.showDropdown();
187
234
  }
188
- // After a turbo frame loads, mark already-selected items and update the live region.
189
- // The empty state element is always present in the frame and CSS shows/hides it
190
- // based on whether any non-selected options exist.
191
235
  syncSelectedState() {
192
236
  const options = this.turboListTarget.querySelectorAll(".midwest-autocomplete-option");
193
237
  options.forEach((option) => {
@@ -223,6 +267,7 @@ class Autocomplete extends Controller {
223
267
  select(option) {
224
268
  this.appendToken(option);
225
269
  this.hideDropdown();
270
+ this.hideFreeFormHint();
226
271
  this.inputTarget.value = "";
227
272
  this.inputTarget.focus();
228
273
  this.announce(`${option.label} selected`);
@@ -294,11 +339,19 @@ class Autocomplete extends Controller {
294
339
  this.renderEmpty("No more options");
295
340
  return;
296
341
  }
342
+ this.hideFreeFormHint();
297
343
  this.loading = false;
298
344
  this.announce(`${availableCount} option${availableCount === 1 ? "" : "s"} available`);
299
345
  this.showDropdown();
300
346
  }
301
347
  renderEmpty(message = "No results") {
348
+ if (this.freeFormValue && this.inputTarget.value.trim()) {
349
+ if (this.hasListTarget)
350
+ this.listTarget.replaceChildren();
351
+ this.loading = false;
352
+ this.updateFreeFormHint(this.inputTarget.value.trim());
353
+ return;
354
+ }
302
355
  const li = document.createElement("li");
303
356
  li.className = "midwest-autocomplete-empty";
304
357
  li.setAttribute("aria-hidden", "true");
@@ -308,15 +361,39 @@ class Autocomplete extends Controller {
308
361
  this.announce(message);
309
362
  this.showDropdown();
310
363
  }
364
+ // Shows or hides the "Hit Enter to add 'X'" hint based on the current query.
365
+ updateFreeFormHint(query) {
366
+ if (!this.hasFreeFormHintListTarget)
367
+ return;
368
+ if (query && !this.isSelected(query)) {
369
+ if (this.hasFreeFormHintValueTarget) {
370
+ this.freeFormHintValueTarget.textContent = `"${query}"`;
371
+ }
372
+ this.freeFormHintListTarget.hidden = false;
373
+ this.showDropdown();
374
+ } else {
375
+ this.hideFreeFormHint();
376
+ if (!this.hasListTarget && !this.hasTurboListTarget)
377
+ this.hideDropdown();
378
+ }
379
+ }
380
+ hideFreeFormHint() {
381
+ if (this.hasFreeFormHintListTarget)
382
+ this.freeFormHintListTarget.hidden = true;
383
+ }
311
384
  set loading(on) {
312
385
  if (this.hasLoaderTarget)
313
386
  this.loaderTarget.hidden = !on;
314
387
  }
315
388
  showDropdown() {
389
+ if (!this.hasDropdownTarget)
390
+ return;
316
391
  if (!this.dropdownTarget.matches(":popover-open"))
317
392
  this.dropdownTarget.showPopover();
318
393
  }
319
394
  hideDropdown() {
395
+ if (!this.hasDropdownTarget)
396
+ return;
320
397
  if (this.dropdownTarget.matches(":popover-open"))
321
398
  this.dropdownTarget.hidePopover();
322
399
  }
@@ -340,6 +417,17 @@ class Autocomplete extends Controller {
340
417
  const support = item.querySelector(".midwest-autocomplete-option-support")?.textContent?.trim();
341
418
  this.select({ value, label, support });
342
419
  }
420
+ commitFreeFormTag() {
421
+ const value = this.inputTarget.value.trim();
422
+ this.addFreeFormTag(value);
423
+ }
424
+ addFreeFormTag(value) {
425
+ if (!value)
426
+ return;
427
+ if (this.isSelected(value))
428
+ return;
429
+ this.select({ value, label: value });
430
+ }
343
431
  announce(message) {
344
432
  if (this.hasStatusTarget)
345
433
  this.statusTarget.textContent = message;