shadcn_phlexcomponents 0.1.11 → 0.1.16

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/app/javascript/controllers/accordion_controller.js +107 -0
  3. data/app/javascript/controllers/alert_dialog_controller.js +7 -0
  4. data/app/javascript/controllers/avatar_controller.js +14 -0
  5. data/app/javascript/controllers/checkbox_controller.js +29 -0
  6. data/app/javascript/controllers/collapsible_controller.js +39 -0
  7. data/app/javascript/controllers/combobox_controller.js +278 -0
  8. data/app/javascript/controllers/command_controller.js +207 -0
  9. data/app/javascript/controllers/date_picker_controller.js +258 -0
  10. data/app/javascript/controllers/date_range_picker_controller.js +200 -0
  11. data/app/javascript/controllers/dialog_controller.js +83 -0
  12. data/app/javascript/controllers/dropdown_menu_controller.js +238 -0
  13. data/app/javascript/controllers/dropdown_menu_sub_controller.js +118 -0
  14. data/app/javascript/controllers/form_field_controller.js +20 -0
  15. data/app/javascript/controllers/hover_card_controller.js +73 -0
  16. data/app/javascript/controllers/loading_button_controller.js +14 -0
  17. data/app/javascript/controllers/popover_controller.js +90 -0
  18. data/app/javascript/controllers/progress_controller.js +14 -0
  19. data/app/javascript/controllers/radio_group_controller.js +80 -0
  20. data/app/javascript/controllers/select_controller.js +265 -0
  21. data/app/javascript/controllers/sidebar_controller.js +29 -0
  22. data/app/javascript/controllers/sidebar_trigger_controller.js +15 -0
  23. data/app/javascript/controllers/slider_controller.js +82 -0
  24. data/app/javascript/controllers/switch_controller.js +26 -0
  25. data/app/javascript/controllers/tabs_controller.js +66 -0
  26. data/app/javascript/controllers/theme_switcher_controller.js +32 -0
  27. data/app/javascript/controllers/toast_container_controller.js +48 -0
  28. data/app/javascript/controllers/toast_controller.js +22 -0
  29. data/app/javascript/controllers/toggle_controller.js +20 -0
  30. data/app/javascript/controllers/toggle_group_controller.js +20 -0
  31. data/app/javascript/controllers/tooltip_controller.js +79 -0
  32. data/app/javascript/shadcn_phlexcomponents.js +60 -0
  33. data/app/javascript/utils/command.js +448 -0
  34. data/app/javascript/utils/floating_ui.js +160 -0
  35. data/app/javascript/utils/index.js +288 -0
  36. data/app/stylesheets/date_picker.css +118 -0
  37. data/app/typescript/controllers/accordion_controller.ts +136 -0
  38. data/app/typescript/controllers/alert_dialog_controller.ts +12 -0
  39. data/app/{javascript → typescript}/controllers/avatar_controller.ts +7 -2
  40. data/app/{javascript → typescript}/controllers/checkbox_controller.ts +11 -4
  41. data/app/{javascript → typescript}/controllers/collapsible_controller.ts +12 -5
  42. data/app/typescript/controllers/combobox_controller.ts +376 -0
  43. data/app/typescript/controllers/command_controller.ts +301 -0
  44. data/app/{javascript → typescript}/controllers/date_picker_controller.ts +185 -125
  45. data/app/{javascript → typescript}/controllers/date_range_picker_controller.ts +89 -79
  46. data/app/{javascript → typescript}/controllers/dialog_controller.ts +59 -57
  47. data/app/typescript/controllers/dropdown_menu_controller.ts +309 -0
  48. data/app/{javascript → typescript}/controllers/dropdown_menu_sub_controller.ts +31 -29
  49. data/app/{javascript → typescript}/controllers/form_field_controller.ts +6 -1
  50. data/app/{javascript → typescript}/controllers/hover_card_controller.ts +36 -26
  51. data/app/{javascript → typescript}/controllers/loading_button_controller.ts +6 -1
  52. data/app/{javascript → typescript}/controllers/popover_controller.ts +42 -65
  53. data/app/{javascript → typescript}/controllers/progress_controller.ts +9 -3
  54. data/app/{javascript → typescript}/controllers/radio_group_controller.ts +16 -9
  55. data/app/typescript/controllers/select_controller.ts +341 -0
  56. data/app/{javascript → typescript}/controllers/slider_controller.ts +23 -16
  57. data/app/{javascript → typescript}/controllers/switch_controller.ts +11 -4
  58. data/app/{javascript → typescript}/controllers/tabs_controller.ts +26 -18
  59. data/app/{javascript → typescript}/controllers/theme_switcher_controller.ts +6 -1
  60. data/app/{javascript → typescript}/controllers/toast_container_controller.ts +6 -1
  61. data/app/{javascript → typescript}/controllers/toast_controller.ts +7 -1
  62. data/app/typescript/controllers/toggle_controller.ts +28 -0
  63. data/app/typescript/controllers/toggle_group_controller.ts +28 -0
  64. data/app/{javascript → typescript}/controllers/tooltip_controller.ts +43 -31
  65. data/app/typescript/shadcn_phlexcomponents.ts +61 -0
  66. data/app/typescript/utils/command.ts +544 -0
  67. data/app/typescript/utils/floating_ui.ts +196 -0
  68. data/app/typescript/utils/index.ts +424 -0
  69. data/lib/install/install_shadcn_phlexcomponents.rb +10 -3
  70. data/lib/shadcn_phlexcomponents/alias.rb +3 -0
  71. data/lib/shadcn_phlexcomponents/components/accordion.rb +2 -1
  72. data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +18 -15
  73. data/lib/shadcn_phlexcomponents/components/base.rb +14 -0
  74. data/lib/shadcn_phlexcomponents/components/collapsible.rb +1 -2
  75. data/lib/shadcn_phlexcomponents/components/combobox.rb +87 -57
  76. data/lib/shadcn_phlexcomponents/components/command.rb +77 -47
  77. data/lib/shadcn_phlexcomponents/components/date_picker.rb +25 -81
  78. data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +21 -4
  79. data/lib/shadcn_phlexcomponents/components/dialog.rb +14 -12
  80. data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +5 -4
  81. data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +2 -1
  82. data/lib/shadcn_phlexcomponents/components/form/form_combobox.rb +64 -0
  83. data/lib/shadcn_phlexcomponents/components/form.rb +14 -0
  84. data/lib/shadcn_phlexcomponents/components/hover_card.rb +3 -2
  85. data/lib/shadcn_phlexcomponents/components/popover.rb +3 -3
  86. data/lib/shadcn_phlexcomponents/components/select.rb +10 -25
  87. data/lib/shadcn_phlexcomponents/components/sheet.rb +15 -11
  88. data/lib/shadcn_phlexcomponents/components/table.rb +1 -1
  89. data/lib/shadcn_phlexcomponents/components/tabs.rb +1 -1
  90. data/lib/shadcn_phlexcomponents/components/toast_container.rb +1 -1
  91. data/lib/shadcn_phlexcomponents/components/toggle.rb +54 -0
  92. data/lib/shadcn_phlexcomponents/components/tooltip.rb +3 -2
  93. data/lib/shadcn_phlexcomponents/engine.rb +1 -5
  94. data/lib/shadcn_phlexcomponents/version.rb +1 -1
  95. metadata +71 -32
  96. data/app/javascript/controllers/accordion_controller.ts +0 -133
  97. data/app/javascript/controllers/combobox_controller.ts +0 -145
  98. data/app/javascript/controllers/command_controller.ts +0 -129
  99. data/app/javascript/controllers/command_root_controller.ts +0 -355
  100. data/app/javascript/controllers/dropdown_menu_controller.ts +0 -133
  101. data/app/javascript/controllers/dropdown_menu_root_controller.ts +0 -234
  102. data/app/javascript/controllers/select_controller.ts +0 -200
  103. data/app/javascript/shadcn_phlexcomponents.ts +0 -57
  104. data/app/javascript/utils.ts +0 -437
  105. /data/app/{javascript → typescript}/controllers/sidebar_controller.ts +0 -0
  106. /data/app/{javascript → typescript}/controllers/sidebar_trigger_controller.ts +0 -0
@@ -0,0 +1,160 @@
1
+ import {
2
+ computePosition,
3
+ flip,
4
+ shift,
5
+ offset,
6
+ autoUpdate,
7
+ size,
8
+ arrow,
9
+ } from "@floating-ui/dom";
10
+ const OPPOSITE_SIDE = {
11
+ top: "bottom",
12
+ right: "left",
13
+ bottom: "top",
14
+ left: "right",
15
+ };
16
+ const ARROW_TRANSFORM_ORIGIN = {
17
+ top: "",
18
+ right: "0 0",
19
+ bottom: "center 0",
20
+ left: "100% 0",
21
+ };
22
+ const ARROW_TRANSFORM = {
23
+ top: "translateY(100%)",
24
+ right: "translateY(50%) rotate(90deg) translateX(-50%)",
25
+ bottom: `rotate(180deg)`,
26
+ left: "translateY(50%) rotate(-90deg) translateX(50%)",
27
+ };
28
+ const initFloatingUi = ({
29
+ referenceElement,
30
+ floatingElement,
31
+ side = "bottom",
32
+ align = "center",
33
+ sideOffset = 0,
34
+ alignOffset = 0,
35
+ arrowElement,
36
+ }) => {
37
+ let placement = `${side}-${align}`;
38
+ placement = placement.replace(/-center/g, "");
39
+ let arrowHeight = 0,
40
+ arrowWidth = 0;
41
+ if (arrowElement) {
42
+ const rect = arrowElement.getBoundingClientRect();
43
+ arrowWidth = rect.width;
44
+ arrowHeight = rect.height;
45
+ }
46
+ const middleware = [
47
+ transformOrigin({ arrowHeight, arrowWidth }),
48
+ offset({ mainAxis: sideOffset, alignmentAxis: alignOffset }),
49
+ size({
50
+ apply: ({ elements, rects, availableWidth, availableHeight }) => {
51
+ const { width: anchorWidth, height: anchorHeight } = rects.reference;
52
+ const contentStyle = elements.floating.style;
53
+ contentStyle.setProperty(
54
+ "--radix-popper-available-width",
55
+ `${availableWidth}px`,
56
+ );
57
+ contentStyle.setProperty(
58
+ "--radix-popper-available-height",
59
+ `${availableHeight}px`,
60
+ );
61
+ contentStyle.setProperty(
62
+ "--radix-popper-anchor-width",
63
+ `${anchorWidth}px`,
64
+ );
65
+ contentStyle.setProperty(
66
+ "--radix-popper-anchor-height",
67
+ `${anchorHeight}px`,
68
+ );
69
+ },
70
+ }),
71
+ ];
72
+ const flipMiddleware = flip({
73
+ // Ensure we flip to the perpendicular axis if it doesn't fit
74
+ // on narrow viewports.
75
+ crossAxis: "alignment",
76
+ fallbackAxisSideDirection: "end", // or 'start'
77
+ });
78
+ const shiftMiddleware = shift();
79
+ // Prioritize flip over shift for edge-aligned placements only.
80
+ if (placement.includes("-")) {
81
+ middleware.push(flipMiddleware, shiftMiddleware);
82
+ } else {
83
+ middleware.push(shiftMiddleware, flipMiddleware);
84
+ }
85
+ if (arrowElement) {
86
+ middleware.push(arrow({ element: arrowElement, padding: 0 }));
87
+ }
88
+ return autoUpdate(referenceElement, floatingElement, () => {
89
+ computePosition(referenceElement, floatingElement, {
90
+ placement: placement,
91
+ strategy: "fixed",
92
+ middleware,
93
+ }).then(({ middlewareData, x, y }) => {
94
+ const arrowX = middlewareData.arrow?.x;
95
+ const arrowY = middlewareData.arrow?.y;
96
+ const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
97
+ floatingElement.style.setProperty(
98
+ "--radix-popper-transform-origin",
99
+ `${middlewareData.transformOrigin?.x} ${middlewareData.transformOrigin?.y}`,
100
+ );
101
+ if (arrowElement) {
102
+ const baseSide = OPPOSITE_SIDE[side];
103
+ const arrowStyle = {
104
+ position: "absolute",
105
+ left: arrowX ? `${arrowX}px` : undefined,
106
+ top: arrowY ? `${arrowY}px` : undefined,
107
+ [baseSide]: 0,
108
+ transformOrigin: ARROW_TRANSFORM_ORIGIN[side],
109
+ transform: ARROW_TRANSFORM[side],
110
+ visibility: cannotCenterArrow ? "hidden" : undefined,
111
+ };
112
+ Object.assign(arrowElement.style, arrowStyle);
113
+ }
114
+ Object.assign(floatingElement.style, {
115
+ left: `${x}px`,
116
+ top: `${y}px`,
117
+ });
118
+ });
119
+ });
120
+ };
121
+ const transformOrigin = (options) => {
122
+ return {
123
+ name: "transformOrigin",
124
+ options,
125
+ fn(data) {
126
+ const { placement, rects, middlewareData } = data;
127
+ const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;
128
+ const isArrowHidden = cannotCenterArrow;
129
+ const arrowWidth = isArrowHidden ? 0 : options.arrowWidth;
130
+ const arrowHeight = isArrowHidden ? 0 : options.arrowHeight;
131
+ const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);
132
+ const noArrowAlign = { start: "0%", center: "50%", end: "100%" }[
133
+ placedAlign
134
+ ];
135
+ const arrowXCenter = (middlewareData.arrow?.x ?? 0) + arrowWidth / 2;
136
+ const arrowYCenter = (middlewareData.arrow?.y ?? 0) + arrowHeight / 2;
137
+ let x = "";
138
+ let y = "";
139
+ if (placedSide === "bottom") {
140
+ x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`;
141
+ y = `${-arrowHeight}px`;
142
+ } else if (placedSide === "top") {
143
+ x = isArrowHidden ? noArrowAlign : `${arrowXCenter}px`;
144
+ y = `${rects.floating.height + arrowHeight}px`;
145
+ } else if (placedSide === "right") {
146
+ x = `${-arrowHeight}px`;
147
+ y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`;
148
+ } else if (placedSide === "left") {
149
+ x = `${rects.floating.width + arrowHeight}px`;
150
+ y = isArrowHidden ? noArrowAlign : `${arrowYCenter}px`;
151
+ }
152
+ return { data: { x, y } };
153
+ },
154
+ };
155
+ };
156
+ function getSideAndAlignFromPlacement(placement) {
157
+ const [side, align = "center"] = placement.split("-");
158
+ return [side, align];
159
+ }
160
+ export { initFloatingUi };
@@ -0,0 +1,288 @@
1
+ const ANIMATION_OUT_DELAY = 100;
2
+ const ON_OPEN_FOCUS_DELAY = 100;
3
+ const ON_CLOSE_FOCUS_DELAY = 50;
4
+ const getScrollbarWidth = () => {
5
+ // Create a temporary div container and append it into the body
6
+ const outer = document.createElement("div");
7
+ outer.style.visibility = "hidden";
8
+ outer.style.overflow = "scroll"; // force scrollbars
9
+ outer.style.width = "100px";
10
+ outer.style.position = "absolute";
11
+ outer.style.top = "-9999px";
12
+ document.body.appendChild(outer);
13
+ // Create an inner div and place it inside the outer div
14
+ const inner = document.createElement("div");
15
+ inner.style.width = "100%";
16
+ outer.appendChild(inner);
17
+ // Calculate the scrollbar width
18
+ const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
19
+ // Clean up
20
+ outer.remove();
21
+ return scrollbarWidth;
22
+ };
23
+ const lockScroll = (contentId) => {
24
+ if (window.innerHeight < document.documentElement.scrollHeight) {
25
+ document.body.dataset.scrollLocked = "1";
26
+ document.body.classList.add(
27
+ "data-[scroll-locked]:pointer-events-none",
28
+ "data-[scroll-locked]:!overflow-hidden",
29
+ "data-[scroll-locked]:!relative",
30
+ "data-[scroll-locked]:px-0",
31
+ "data-[scroll-locked]:pt-0",
32
+ "data-[scroll-locked]:ml-0",
33
+ "data-[scroll-locked]:mt-0",
34
+ );
35
+ document.body.style.marginRight = `${getScrollbarWidth()}px`;
36
+ const contentIdsString =
37
+ document.body.dataset.scrollLockedContentIds || "[]";
38
+ const contentIds = JSON.parse(contentIdsString);
39
+ contentIds.push(contentId);
40
+ document.body.dataset.scrollLockedContentIds = JSON.stringify(contentIds);
41
+ }
42
+ };
43
+ const unlockScroll = (contentId) => {
44
+ const contentIdsString = document.body.dataset.scrollLockedContentIds || "[]";
45
+ const contentIds = JSON.parse(contentIdsString);
46
+ const newContentIds = contentIds.filter((id) => id !== contentId);
47
+ document.body.dataset.scrollLockedContentIds = JSON.stringify(newContentIds);
48
+ if (newContentIds.length === 0) {
49
+ delete document.body.dataset.scrollLocked;
50
+ document.body.classList.remove(
51
+ "data-[scroll-locked]:pointer-events-none",
52
+ "data-[scroll-locked]:!overflow-hidden",
53
+ "data-[scroll-locked]:!relative",
54
+ "data-[scroll-locked]:px-0",
55
+ "data-[scroll-locked]:pt-0",
56
+ "data-[scroll-locked]:ml-0",
57
+ "data-[scroll-locked]:mt-0",
58
+ );
59
+ document.body.style.marginRight = "";
60
+ }
61
+ };
62
+ const focusTrigger = (triggerTarget) => {
63
+ setTimeout(() => {
64
+ if (triggerTarget.dataset.asChild === "false") {
65
+ const childElement = triggerTarget.firstElementChild;
66
+ if (childElement) {
67
+ childElement.focus();
68
+ }
69
+ } else {
70
+ triggerTarget.focus();
71
+ }
72
+ }, ON_CLOSE_FOCUS_DELAY);
73
+ };
74
+ const focusElement = (element) => {
75
+ setTimeout(() => {
76
+ if (element) {
77
+ element.focus();
78
+ }
79
+ }, ON_OPEN_FOCUS_DELAY);
80
+ };
81
+ const getFocusableElements = (container) => {
82
+ return Array.from(
83
+ container.querySelectorAll(
84
+ 'button, [href], input:not([type="hidden"]), select:not([tabindex="-1"]), textarea, [tabindex]:not([tabindex="-1"])',
85
+ ),
86
+ );
87
+ };
88
+ const getSameLevelItems = ({ content, items, closestContentSelector }) => {
89
+ const sameLevelItems = [];
90
+ items.forEach((i) => {
91
+ if (
92
+ i.closest(closestContentSelector) === content &&
93
+ i.dataset.disabled === undefined
94
+ ) {
95
+ sameLevelItems.push(i);
96
+ }
97
+ });
98
+ return sameLevelItems;
99
+ };
100
+ const showContent = ({
101
+ trigger,
102
+ content,
103
+ contentContainer,
104
+ setEqualWidth,
105
+ overlay,
106
+ }) => {
107
+ contentContainer.style.display = "";
108
+ if (trigger) {
109
+ if (setEqualWidth) {
110
+ const triggerWidth = trigger.offsetWidth;
111
+ const contentContainerWidth = contentContainer.offsetWidth;
112
+ if (contentContainerWidth < triggerWidth) {
113
+ contentContainer.style.width = `${triggerWidth}px`;
114
+ }
115
+ }
116
+ trigger.ariaExpanded = "true";
117
+ trigger.dataset.state = "open";
118
+ }
119
+ content.dataset.state = "open";
120
+ if (overlay) {
121
+ overlay.style.display = "";
122
+ overlay.dataset.state = "open";
123
+ lockScroll(content.id);
124
+ }
125
+ };
126
+ const hideContent = ({ trigger, content, contentContainer, overlay }) => {
127
+ if (trigger) {
128
+ trigger.ariaExpanded = "false";
129
+ trigger.dataset.state = "closed";
130
+ }
131
+ content.dataset.state = "closed";
132
+ setTimeout(() => {
133
+ contentContainer.style.display = "none";
134
+ if (overlay) {
135
+ overlay.style.display = "none";
136
+ overlay.dataset.state = "closed";
137
+ unlockScroll(content.id);
138
+ }
139
+ }, ANIMATION_OUT_DELAY);
140
+ };
141
+ const getStimulusInstance = (controller, element) => {
142
+ if (!element) return;
143
+ return window.Stimulus.getControllerForElementAndIdentifier(
144
+ element,
145
+ controller,
146
+ );
147
+ };
148
+ const anyNestedComponentsOpen = (element) => {
149
+ const components = [];
150
+ const componentNames = [
151
+ "dialog",
152
+ "alert-dialog",
153
+ "dropdown-menu",
154
+ "popover",
155
+ "select",
156
+ "combobox",
157
+ "command",
158
+ "hover-card",
159
+ "tooltip",
160
+ "date-picker",
161
+ "date-range-picker",
162
+ ];
163
+ componentNames.forEach((name) => {
164
+ const triggers = Array.from(
165
+ element.querySelectorAll(
166
+ `[data-shadcn-phlexcomponents="${name}-trigger"]`,
167
+ ),
168
+ );
169
+ const controllerElements = Array.from(
170
+ element.querySelectorAll(`[data-controller="${name}"]`),
171
+ );
172
+ controllerElements.forEach((controller) => {
173
+ const stimulusInstance = getStimulusInstance(name, controller);
174
+ if (stimulusInstance) {
175
+ components.push(stimulusInstance);
176
+ }
177
+ });
178
+ triggers.forEach((trigger) => {
179
+ const stimulusInstance = getStimulusInstance(
180
+ name,
181
+ document.querySelector(`#${trigger.getAttribute("aria-controls")}`),
182
+ );
183
+ if (stimulusInstance) {
184
+ components.push(stimulusInstance);
185
+ }
186
+ });
187
+ });
188
+ return components.some((c) => c.isOpenValue);
189
+ };
190
+ const onClickOutside = (controller, event) => {
191
+ const target = event.target;
192
+ // Let trigger handle state
193
+ if (target === controller.triggerTarget) return;
194
+ if (controller.triggerTarget.contains(target)) return;
195
+ controller.close();
196
+ };
197
+ const setGroupLabelsId = (controller) => {
198
+ controller.groupTargets.forEach((g) => {
199
+ const label = g.querySelector(
200
+ `[data-shadcn-phlexcomponents="${controller.identifier}-label"]`,
201
+ );
202
+ if (label) {
203
+ label.id = g.getAttribute("aria-labelledby");
204
+ }
205
+ });
206
+ };
207
+ const getNextEnabledIndex = ({ items, currentIndex, wrapAround, filterFn }) => {
208
+ let newIndex = null;
209
+ if (filterFn) {
210
+ newIndex = items.findIndex(
211
+ (item, index) => index > currentIndex && filterFn(item),
212
+ );
213
+ if (newIndex === -1) {
214
+ newIndex = currentIndex;
215
+ }
216
+ } else {
217
+ newIndex = currentIndex + 1;
218
+ }
219
+ if (newIndex > items.length - 1) {
220
+ if (wrapAround) {
221
+ newIndex = 0;
222
+ } else {
223
+ newIndex = items.length - 1;
224
+ }
225
+ }
226
+ return newIndex;
227
+ };
228
+ const getPreviousEnabledIndex = ({
229
+ items,
230
+ currentIndex,
231
+ wrapAround,
232
+ filterFn,
233
+ }) => {
234
+ let newIndex = null;
235
+ if (filterFn) {
236
+ newIndex = items.findLastIndex(
237
+ (item, index) => index < currentIndex && filterFn(item),
238
+ );
239
+ if (newIndex === -1) {
240
+ newIndex = currentIndex;
241
+ }
242
+ } else {
243
+ newIndex = currentIndex - 1;
244
+ }
245
+ if (newIndex < 0) {
246
+ if (wrapAround) {
247
+ newIndex = items.length - 1;
248
+ } else {
249
+ newIndex = 0;
250
+ }
251
+ }
252
+ return newIndex;
253
+ };
254
+ const handleTabNavigation = (element, event) => {
255
+ const focusableElements = getFocusableElements(element);
256
+ const firstElement = focusableElements[0];
257
+ const lastElement = focusableElements[focusableElements.length - 1];
258
+ // If Shift + Tab pressed on first element, go to last element
259
+ if (event.shiftKey && document.activeElement === firstElement) {
260
+ event.preventDefault();
261
+ lastElement.focus();
262
+ }
263
+ // If Tab pressed on last element, go to first element
264
+ else if (!event.shiftKey && document.activeElement === lastElement) {
265
+ event.preventDefault();
266
+ firstElement.focus();
267
+ }
268
+ };
269
+ export {
270
+ ANIMATION_OUT_DELAY,
271
+ ON_CLOSE_FOCUS_DELAY,
272
+ ON_OPEN_FOCUS_DELAY,
273
+ lockScroll,
274
+ unlockScroll,
275
+ focusTrigger,
276
+ focusElement,
277
+ getFocusableElements,
278
+ getSameLevelItems,
279
+ showContent,
280
+ hideContent,
281
+ getStimulusInstance,
282
+ anyNestedComponentsOpen,
283
+ onClickOutside,
284
+ setGroupLabelsId,
285
+ getNextEnabledIndex,
286
+ getPreviousEnabledIndex,
287
+ handleTabNavigation,
288
+ };
@@ -1,3 +1,48 @@
1
+ [data-vc='calendar'] {
2
+ @apply relative flex flex-col outline-none;
3
+ }
4
+
5
+ [data-vc='controls'] {
6
+ @apply absolute z-20 -left-1 -right-1 -top-1.5 flex justify-between items-center pt-1.5 px-1 pointer-events-none box-content;
7
+ }
8
+
9
+ [data-vc='grid'] {
10
+ @apply grid gap-4 grid-cols-1 md:grid-cols-2;
11
+ }
12
+
13
+ [data-vc='column'] {
14
+ @apply flex flex-col;
15
+ }
16
+
17
+ [data-vc='header'] {
18
+ @apply relative flex items-center mb-4;
19
+ }
20
+
21
+ [data-vc-header='content'] {
22
+ @apply grid grid-flow-col gap-x-1 auto-cols-max items-center justify-center px-4 whitespace-pre-wrap grow;
23
+ }
24
+
25
+ [data-vc='month'],
26
+ [data-vc='year'] {
27
+ @apply inline-flex items-center justify-center whitespace-nowrap font-medium transition-all;
28
+ @apply disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4;
29
+ @apply shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px];
30
+ @apply aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive border;
31
+ @apply shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50;
32
+ @apply rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 text-xs h-7 bg-transparent;
33
+ }
34
+
35
+ [data-vc-arrow='prev'],
36
+ [data-vc-arrow='next'] {
37
+ @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium;
38
+ @apply transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none;
39
+ @apply [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring;
40
+ @apply focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20;
41
+ @apply dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive border shadow-xs hover:bg-accent;
42
+ @apply hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 pointer-events-auto;
43
+ @apply size-7 bg-transparent p-0 opacity-50 hover:opacity-100;
44
+ }
45
+
1
46
  [data-vc-arrow='next']:before {
2
47
  @apply bg-current size-4 mask-size-[auto_16px] mask-center mask-no-repeat absolute content-[''];
3
48
  mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>');
@@ -8,6 +53,79 @@
8
53
  mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left-icon lucide-chevron-left"><path d="m15 18-6-6 6-6"/></svg>');
9
54
  }
10
55
 
56
+ [data-vc='wrapper'] {
57
+ @apply flex items-center content-center h-full;
58
+ }
59
+
60
+ [data-vc='content'] {
61
+ @apply flex flex-col grow h-full;
62
+ }
63
+
64
+ [data-vc='months'] {
65
+ @apply grid gap-2 grid-cols-4 items-center grow;
66
+ }
67
+
68
+ [data-vc='years'] {
69
+ @apply grid gap-2 grid-cols-5 items-center grow;
70
+ }
71
+
72
+ [data-vc-months-month],
73
+ [data-vc-years-year] {
74
+ @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all;
75
+ @apply disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4;
76
+ @apply shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px];
77
+ @apply aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive;
78
+ @apply hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 h-9 px-4 py-2 has-[>svg]:px-3;
79
+ @apply aria-[selected=true]:text-primary-foreground aria-[selected=true]:bg-primary;
80
+ @apply aria-[selected=true]:hover:text-primary-foreground aria-[selected=true]:hover:bg-primary;
81
+ }
82
+
83
+ [data-vc='week'] {
84
+ @apply grid mb-2 grid-cols-[repeat(7,_1fr)] justify-items-center items-center text-center;
85
+ }
86
+
87
+ [data-vc-week-day] {
88
+ @apply text-muted-foreground rounded-md w-8 font-normal text-[0.8rem];
89
+ }
90
+
91
+ [data-vc='dates'] {
92
+ @apply grid gap-y-2 grid-cols-[repeat(7,_1fr)] justify-items-center items-center;
93
+ }
94
+
95
+ [data-vc-date-btn] {
96
+ @apply inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm transition-all;
97
+ @apply disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4;
98
+ @apply shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px];
99
+ @apply aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive;
100
+ @apply hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 has-[>svg]:px-3 size-8 p-0 font-normal;
101
+ @apply aria-[disabled]:text-muted-foreground aria-[disabled]:opacity-50 aria-[disabled]:pointer-events-none;
102
+ }
103
+
104
+ [data-vc-date-selected] [data-vc-date-btn] {
105
+ @apply bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground;
106
+ }
107
+
108
+ [data-vc-date-today] [data-vc-date-btn] {
109
+ @apply bg-accent text-accent-foreground;
110
+ }
111
+
112
+ [data-vc-date-month='prev'] [data-vc-date-btn],
113
+ [data-vc-date-month='next'] [data-vc-date-btn] {
114
+ @apply text-muted-foreground;
115
+ }
116
+
117
+ [data-vc-date-selected='middle'][data-vc-date-selected] [data-vc-date-btn] {
118
+ @apply bg-accent text-accent-foreground;
119
+ }
120
+
121
+ [data-vc-date-selected='first'][data-vc-date-selected] [data-vc-date-btn] {
122
+ @apply bg-primary text-primary-foreground;
123
+ }
124
+
125
+ [data-vc-date-hover] [data-vc-date-btn] {
126
+ @apply bg-accent text-accent-foreground;
127
+ }
128
+
11
129
  [data-vc='grid'][data-vc-grid='hidden'] [data-vc='column'] {
12
130
  pointer-events: none;
13
131
  opacity: 0.3;
@@ -0,0 +1,136 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import {
3
+ showContent,
4
+ hideContent,
5
+ getNextEnabledIndex,
6
+ getPreviousEnabledIndex,
7
+ } from '../utils'
8
+
9
+ const AccordionController = class extends Controller<HTMLElement> {
10
+ // targets
11
+ static targets = ['item', 'trigger', 'content']
12
+ declare itemTargets: HTMLElement[]
13
+ declare triggerTargets: HTMLButtonElement[]
14
+ declare contentTargets: HTMLElement[]
15
+
16
+ // values
17
+ static values = { openItems: Array }
18
+ declare openItemsValue: string[]
19
+
20
+ // custom properties
21
+ declare multiple: boolean
22
+
23
+ connect() {
24
+ this.multiple = this.element.dataset.multiple === 'true'
25
+ }
26
+
27
+ contentTargetConnected(content: HTMLElement) {
28
+ setTimeout(() => {
29
+ this.setContentHeight(content)
30
+ }, 100)
31
+ }
32
+
33
+ toggle(event: MouseEvent) {
34
+ const trigger = event.currentTarget as HTMLElement
35
+
36
+ const item = this.itemTargets.find((item) => {
37
+ return item.contains(trigger)
38
+ })
39
+
40
+ if (!item) return
41
+
42
+ const value = item.dataset.value as string
43
+ const isOpen = this.openItemsValue.includes(value)
44
+
45
+ if (isOpen) {
46
+ this.openItemsValue = this.openItemsValue.filter((v) => v !== value)
47
+ } else {
48
+ if (this.multiple) {
49
+ this.openItemsValue = [...this.openItemsValue, value]
50
+ } else {
51
+ this.openItemsValue = [value]
52
+ }
53
+ }
54
+ }
55
+
56
+ focusTrigger(event: KeyboardEvent) {
57
+ const trigger = event.currentTarget as HTMLButtonElement
58
+ const key = event.key
59
+
60
+ const focusableTriggers = this.triggerTargets.filter(
61
+ (trigger) => !trigger.disabled,
62
+ )
63
+
64
+ const index = focusableTriggers.indexOf(trigger)
65
+ let newIndex = 0
66
+
67
+ if (key === 'ArrowUp') {
68
+ newIndex = getPreviousEnabledIndex({
69
+ items: focusableTriggers,
70
+ currentIndex: index,
71
+ wrapAround: true,
72
+ })
73
+ } else {
74
+ newIndex = getNextEnabledIndex({
75
+ items: focusableTriggers,
76
+ currentIndex: index,
77
+ wrapAround: true,
78
+ })
79
+ }
80
+
81
+ focusableTriggers[newIndex].focus()
82
+ }
83
+
84
+ openItemsValueChanged(openItems: string[]) {
85
+ this.itemTargets.forEach((item) => {
86
+ const itemValue = item.dataset.value as string
87
+
88
+ const trigger = this.triggerTargets.find((trigger) =>
89
+ item.contains(trigger),
90
+ ) as HTMLElement
91
+ const content = this.contentTargets.find((content) =>
92
+ item.contains(content),
93
+ ) as HTMLElement
94
+
95
+ if (openItems.includes(itemValue)) {
96
+ showContent({
97
+ trigger,
98
+ content: content,
99
+ contentContainer: content,
100
+ })
101
+ } else {
102
+ hideContent({
103
+ trigger,
104
+ content: content,
105
+ contentContainer: content,
106
+ })
107
+ }
108
+ })
109
+ }
110
+
111
+ protected setContentHeight(element: HTMLElement) {
112
+ const height =
113
+ this.getContentHeight(element) || element.getBoundingClientRect().height
114
+ element.style.setProperty('--radix-accordion-content-height', `${height}px`)
115
+ }
116
+
117
+ getContentHeight(el: HTMLElement) {
118
+ const clone = el.cloneNode(true) as HTMLElement
119
+ Object.assign(clone.style, {
120
+ display: 'block',
121
+ position: 'absolute',
122
+ visibility: 'hidden',
123
+ })
124
+
125
+ document.body.appendChild(clone)
126
+ const height = clone.getBoundingClientRect().height
127
+ document.body.removeChild(clone)
128
+
129
+ return height
130
+ }
131
+ }
132
+
133
+ type Accordion = InstanceType<typeof AccordionController>
134
+
135
+ export { AccordionController }
136
+ export type { Accordion }