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.
- checksums.yaml +4 -4
- data/app/javascript/controllers/accordion_controller.js +107 -0
- data/app/javascript/controllers/alert_dialog_controller.js +7 -0
- data/app/javascript/controllers/avatar_controller.js +14 -0
- data/app/javascript/controllers/checkbox_controller.js +29 -0
- data/app/javascript/controllers/collapsible_controller.js +39 -0
- data/app/javascript/controllers/combobox_controller.js +278 -0
- data/app/javascript/controllers/command_controller.js +207 -0
- data/app/javascript/controllers/date_picker_controller.js +258 -0
- data/app/javascript/controllers/date_range_picker_controller.js +200 -0
- data/app/javascript/controllers/dialog_controller.js +83 -0
- data/app/javascript/controllers/dropdown_menu_controller.js +238 -0
- data/app/javascript/controllers/dropdown_menu_sub_controller.js +118 -0
- data/app/javascript/controllers/form_field_controller.js +20 -0
- data/app/javascript/controllers/hover_card_controller.js +73 -0
- data/app/javascript/controllers/loading_button_controller.js +14 -0
- data/app/javascript/controllers/popover_controller.js +90 -0
- data/app/javascript/controllers/progress_controller.js +14 -0
- data/app/javascript/controllers/radio_group_controller.js +80 -0
- data/app/javascript/controllers/select_controller.js +265 -0
- data/app/javascript/controllers/sidebar_controller.js +29 -0
- data/app/javascript/controllers/sidebar_trigger_controller.js +15 -0
- data/app/javascript/controllers/slider_controller.js +82 -0
- data/app/javascript/controllers/switch_controller.js +26 -0
- data/app/javascript/controllers/tabs_controller.js +66 -0
- data/app/javascript/controllers/theme_switcher_controller.js +32 -0
- data/app/javascript/controllers/toast_container_controller.js +48 -0
- data/app/javascript/controllers/toast_controller.js +22 -0
- data/app/javascript/controllers/toggle_controller.js +20 -0
- data/app/javascript/controllers/toggle_group_controller.js +20 -0
- data/app/javascript/controllers/tooltip_controller.js +79 -0
- data/app/javascript/shadcn_phlexcomponents.js +60 -0
- data/app/javascript/utils/command.js +448 -0
- data/app/javascript/utils/floating_ui.js +160 -0
- data/app/javascript/utils/index.js +288 -0
- data/app/stylesheets/date_picker.css +118 -0
- data/app/typescript/controllers/accordion_controller.ts +136 -0
- data/app/typescript/controllers/alert_dialog_controller.ts +12 -0
- data/app/{javascript → typescript}/controllers/avatar_controller.ts +7 -2
- data/app/{javascript → typescript}/controllers/checkbox_controller.ts +11 -4
- data/app/{javascript → typescript}/controllers/collapsible_controller.ts +12 -5
- data/app/typescript/controllers/combobox_controller.ts +376 -0
- data/app/typescript/controllers/command_controller.ts +301 -0
- data/app/{javascript → typescript}/controllers/date_picker_controller.ts +185 -125
- data/app/{javascript → typescript}/controllers/date_range_picker_controller.ts +89 -79
- data/app/{javascript → typescript}/controllers/dialog_controller.ts +59 -57
- data/app/typescript/controllers/dropdown_menu_controller.ts +309 -0
- data/app/{javascript → typescript}/controllers/dropdown_menu_sub_controller.ts +31 -29
- data/app/{javascript → typescript}/controllers/form_field_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/hover_card_controller.ts +36 -26
- data/app/{javascript → typescript}/controllers/loading_button_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/popover_controller.ts +42 -65
- data/app/{javascript → typescript}/controllers/progress_controller.ts +9 -3
- data/app/{javascript → typescript}/controllers/radio_group_controller.ts +16 -9
- data/app/typescript/controllers/select_controller.ts +341 -0
- data/app/{javascript → typescript}/controllers/slider_controller.ts +23 -16
- data/app/{javascript → typescript}/controllers/switch_controller.ts +11 -4
- data/app/{javascript → typescript}/controllers/tabs_controller.ts +26 -18
- data/app/{javascript → typescript}/controllers/theme_switcher_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/toast_container_controller.ts +6 -1
- data/app/{javascript → typescript}/controllers/toast_controller.ts +7 -1
- data/app/typescript/controllers/toggle_controller.ts +28 -0
- data/app/typescript/controllers/toggle_group_controller.ts +28 -0
- data/app/{javascript → typescript}/controllers/tooltip_controller.ts +43 -31
- data/app/typescript/shadcn_phlexcomponents.ts +61 -0
- data/app/typescript/utils/command.ts +544 -0
- data/app/typescript/utils/floating_ui.ts +196 -0
- data/app/typescript/utils/index.ts +424 -0
- data/lib/install/install_shadcn_phlexcomponents.rb +10 -3
- data/lib/shadcn_phlexcomponents/alias.rb +3 -0
- data/lib/shadcn_phlexcomponents/components/accordion.rb +2 -1
- data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +18 -15
- data/lib/shadcn_phlexcomponents/components/base.rb +14 -0
- data/lib/shadcn_phlexcomponents/components/collapsible.rb +1 -2
- data/lib/shadcn_phlexcomponents/components/combobox.rb +87 -57
- data/lib/shadcn_phlexcomponents/components/command.rb +77 -47
- data/lib/shadcn_phlexcomponents/components/date_picker.rb +25 -81
- data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +21 -4
- data/lib/shadcn_phlexcomponents/components/dialog.rb +14 -12
- data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +5 -4
- data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +2 -1
- data/lib/shadcn_phlexcomponents/components/form/form_combobox.rb +64 -0
- data/lib/shadcn_phlexcomponents/components/form.rb +14 -0
- data/lib/shadcn_phlexcomponents/components/hover_card.rb +3 -2
- data/lib/shadcn_phlexcomponents/components/popover.rb +3 -3
- data/lib/shadcn_phlexcomponents/components/select.rb +10 -25
- data/lib/shadcn_phlexcomponents/components/sheet.rb +15 -11
- data/lib/shadcn_phlexcomponents/components/table.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/tabs.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/toast_container.rb +1 -1
- data/lib/shadcn_phlexcomponents/components/toggle.rb +54 -0
- data/lib/shadcn_phlexcomponents/components/tooltip.rb +3 -2
- data/lib/shadcn_phlexcomponents/engine.rb +1 -5
- data/lib/shadcn_phlexcomponents/version.rb +1 -1
- metadata +71 -32
- data/app/javascript/controllers/accordion_controller.ts +0 -133
- data/app/javascript/controllers/combobox_controller.ts +0 -145
- data/app/javascript/controllers/command_controller.ts +0 -129
- data/app/javascript/controllers/command_root_controller.ts +0 -355
- data/app/javascript/controllers/dropdown_menu_controller.ts +0 -133
- data/app/javascript/controllers/dropdown_menu_root_controller.ts +0 -234
- data/app/javascript/controllers/select_controller.ts +0 -200
- data/app/javascript/shadcn_phlexcomponents.ts +0 -57
- data/app/javascript/utils.ts +0 -437
- /data/app/{javascript → typescript}/controllers/sidebar_controller.ts +0 -0
- /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 }
|