ciderui 0.1.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 (65) hide show
  1. package/README.md +106 -0
  2. package/js/carousel.js +77 -0
  3. package/js/combobox.js +125 -0
  4. package/js/command.js +91 -0
  5. package/js/context-menu.js +49 -0
  6. package/js/dialog.js +18 -0
  7. package/js/dropdown-menu.js +39 -0
  8. package/js/otp.js +92 -0
  9. package/js/popover.js +35 -0
  10. package/js/select.js +56 -0
  11. package/js/sheet.js +17 -0
  12. package/js/tabs.js +34 -0
  13. package/js/toast.js +60 -0
  14. package/js/toggle.js +26 -0
  15. package/package.json +53 -0
  16. package/src/css/ciderui.cdn.css +11 -0
  17. package/src/css/ciderui.css +324 -0
  18. package/src/css/components/accordion.css +31 -0
  19. package/src/css/components/alert.css +18 -0
  20. package/src/css/components/aspect-ratio.css +7 -0
  21. package/src/css/components/avatar.css +17 -0
  22. package/src/css/components/badge.css +20 -0
  23. package/src/css/components/breadcrumb.css +16 -0
  24. package/src/css/components/button-group.css +20 -0
  25. package/src/css/components/button.css +59 -0
  26. package/src/css/components/card.css +37 -0
  27. package/src/css/components/carousel.css +60 -0
  28. package/src/css/components/checkbox.css +16 -0
  29. package/src/css/components/collapsible.css +25 -0
  30. package/src/css/components/combobox.css +69 -0
  31. package/src/css/components/command.css +64 -0
  32. package/src/css/components/context-menu.css +28 -0
  33. package/src/css/components/dialog.css +23 -0
  34. package/src/css/components/drawer.css +69 -0
  35. package/src/css/components/dropdown-menu.css +28 -0
  36. package/src/css/components/empty.css +12 -0
  37. package/src/css/components/field.css +7 -0
  38. package/src/css/components/file-tree.css +53 -0
  39. package/src/css/components/form-controls.css +57 -0
  40. package/src/css/components/form.css +15 -0
  41. package/src/css/components/hover-card.css +25 -0
  42. package/src/css/components/input-group.css +48 -0
  43. package/src/css/components/input-otp.css +22 -0
  44. package/src/css/components/item.css +43 -0
  45. package/src/css/components/kbd.css +7 -0
  46. package/src/css/components/label.css +4 -0
  47. package/src/css/components/pagination.css +15 -0
  48. package/src/css/components/popover.css +19 -0
  49. package/src/css/components/progress.css +18 -0
  50. package/src/css/components/radio.css +13 -0
  51. package/src/css/components/scroll-area.css +24 -0
  52. package/src/css/components/separator.css +19 -0
  53. package/src/css/components/sheet.css +46 -0
  54. package/src/css/components/sidebar.css +32 -0
  55. package/src/css/components/skeleton.css +9 -0
  56. package/src/css/components/slider.css +48 -0
  57. package/src/css/components/spinner.css +12 -0
  58. package/src/css/components/switch.css +28 -0
  59. package/src/css/components/table.css +39 -0
  60. package/src/css/components/tabs.css +44 -0
  61. package/src/css/components/theme-switcher.css +17 -0
  62. package/src/css/components/toast.css +22 -0
  63. package/src/css/components/toggle.css +34 -0
  64. package/src/css/components/tooltip.css +66 -0
  65. package/src/css/components/typography.css +84 -0
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Cider UI
2
+
3
+ Apple-aesthetic UI components as a Tailwind CSS v4 plugin. Semantic class names like `btn`, `card`, `input` — no utility soup in your markup.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install ciderui@github:newlix/ciderui
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Tailwind CSS v4 plugin
14
+
15
+ ```css
16
+ /* your main CSS file */
17
+ @import "tailwindcss";
18
+ @import "ciderui";
19
+ ```
20
+
21
+ ```html
22
+ <button class="btn">Click me</button>
23
+ <button class="btn-destructive">Delete</button>
24
+ <div class="card">
25
+ <header><h2>Title</h2></header>
26
+ <section>Content</section>
27
+ </div>
28
+ ```
29
+
30
+ ### CDN
31
+
32
+ For quick prototyping without a build step:
33
+
34
+ ```html
35
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/newlix/ciderui@main/dist/ciderui.cdn.min.css" />
36
+ ```
37
+
38
+ ## Components (27)
39
+
40
+ | Component | Class | JS |
41
+ |-----------|-------|----|
42
+ | Button | `btn` `btn-primary` `btn-secondary` `btn-outline` `btn-ghost` `btn-destructive` `btn-pill` `btn-sm` `btn-lg` `btn-icon` | — |
43
+ | Badge | `badge` `badge-primary` `badge-success` `badge-warning` `badge-destructive` `badge-outline` | — |
44
+ | Card | `card` `card-interactive` (children: `header` `section` `footer`) | — |
45
+ | Alert | `alert-info` `alert-success` `alert-warning` `alert-destructive` | — |
46
+ | Input | `input` | — |
47
+ | Textarea | `textarea` | — |
48
+ | Select | `select` (native) | `js/select.js` (custom) |
49
+ | Checkbox | `checkbox` | — |
50
+ | Radio | `radio` | — |
51
+ | Switch | `switch` | — |
52
+ | Label | `label` | — |
53
+ | Field | `field` (auto-styles children) | — |
54
+ | Dialog | `dialog` (children: `header` `section` `footer`) | `js/dialog.js` (optional) |
55
+ | Dropdown | `dropdown-menu` | `js/dropdown-menu.js` |
56
+ | Tabs | `tabs` `tabs-list` `tabs-trigger` `tabs-content` `tabs-pill` | `js/tabs.js` |
57
+ | Table | `table` `table-striped` | — |
58
+ | Accordion | `accordion` (native `<details>`) | — |
59
+ | Avatar | `avatar` `avatar-sm` `avatar-lg` `avatar-group` | — |
60
+ | Breadcrumb | `breadcrumb` | — |
61
+ | Button Group | `button-group` | — |
62
+ | Kbd | `kbd` | — |
63
+ | Pagination | `pagination` `pagination-item` | — |
64
+ | Progress | `progress` > `.progress-bar` `progress-sm` `progress-indeterminate` | — |
65
+ | Skeleton | `skeleton` `skeleton-circle` | — |
66
+ | Slider | `slider` | — |
67
+ | Spinner | `spinner` `spinner-sm` `spinner-lg` | — |
68
+ | Toast | `toast` `toast-container` | `js/toast.js` |
69
+ | Tooltip | `tooltip` `tooltip-content` `tooltip-bottom` | — |
70
+
71
+ ### ES Module / Bundler (Vite, Webpack, etc.)
72
+
73
+ JS files work with bundlers. Import them as side effects in your entry file:
74
+
75
+ ```js
76
+ import "ciderui/dialog.js";
77
+ import "ciderui/tabs.js";
78
+ import "ciderui/select.js";
79
+ import "ciderui/toast.js";
80
+ ```
81
+
82
+ ## Dark Mode
83
+
84
+ Add `class="dark"` to your `<html>` element. All components automatically adapt.
85
+
86
+ ```js
87
+ // Toggle dark mode
88
+ document.documentElement.classList.toggle('dark');
89
+ ```
90
+
91
+ ## Design Tokens
92
+
93
+ CSS variables that you can override:
94
+
95
+ | Variable | Light | Dark |
96
+ |----------|-------|------|
97
+ | `--primary` | Apple Blue | Apple Blue |
98
+ | `--background` | white | gray-950 |
99
+ | `--foreground` | gray-900 | gray-100 |
100
+ | `--border` | gray-200/60 | gray-800 |
101
+ | `--radius` | 0.625rem | 0.625rem |
102
+ | `--apple-press-scale` | 0.97 | 0.97 |
103
+
104
+ ## License
105
+
106
+ MIT
package/js/carousel.js ADDED
@@ -0,0 +1,77 @@
1
+ // Carousel — ciderui
2
+ // Prev/next navigation + indicators for scroll-snap carousels.
3
+ function init() {
4
+ document.querySelectorAll(".carousel").forEach((carousel) => {
5
+ const content = carousel.querySelector(".carousel-content");
6
+ const prevBtn = carousel.querySelector(".carousel-prev");
7
+ const nextBtn = carousel.querySelector(".carousel-next");
8
+ const indicators = carousel.querySelector(".carousel-indicators");
9
+ if (!content) return;
10
+
11
+ function getItems() {
12
+ return content.querySelectorAll(".carousel-item, .carousel-item-half, .carousel-item-third");
13
+ }
14
+
15
+ function getCurrentIndex() {
16
+ const items = getItems();
17
+ const scrollLeft = content.scrollLeft;
18
+ let closest = 0;
19
+ let minDist = Infinity;
20
+ items.forEach((item, i) => {
21
+ const dist = Math.abs(item.offsetLeft - scrollLeft);
22
+ if (dist < minDist) { minDist = dist; closest = i; }
23
+ });
24
+ return closest;
25
+ }
26
+
27
+ function scrollToIndex(index) {
28
+ const items = getItems();
29
+ if (index < 0 || index >= items.length) return;
30
+ content.scrollTo({ left: items[index].offsetLeft, behavior: "smooth" });
31
+ }
32
+
33
+ function updateState() {
34
+ const items = getItems();
35
+ const current = getCurrentIndex();
36
+
37
+ if (prevBtn) prevBtn.disabled = current === 0;
38
+ if (nextBtn) nextBtn.disabled = current >= items.length - 1;
39
+
40
+ if (indicators) {
41
+ indicators.querySelectorAll(".carousel-indicator").forEach((dot, i) => {
42
+ if (i === current) dot.setAttribute("data-active", "");
43
+ else dot.removeAttribute("data-active");
44
+ });
45
+ }
46
+ }
47
+
48
+ // Build indicators if container exists but is empty
49
+ if (indicators && !indicators.children.length) {
50
+ const items = getItems();
51
+ items.forEach((_, i) => {
52
+ const dot = document.createElement("button");
53
+ dot.className = "carousel-indicator";
54
+ dot.setAttribute("aria-label", `Go to slide ${i + 1}`);
55
+ if (i === 0) dot.setAttribute("data-active", "");
56
+ dot.addEventListener("click", () => scrollToIndex(i));
57
+ indicators.appendChild(dot);
58
+ });
59
+ }
60
+
61
+ if (prevBtn) prevBtn.addEventListener("click", () => scrollToIndex(getCurrentIndex() - 1));
62
+ if (nextBtn) nextBtn.addEventListener("click", () => scrollToIndex(getCurrentIndex() + 1));
63
+
64
+ content.addEventListener("scroll", () => {
65
+ clearTimeout(content._scrollTimer);
66
+ content._scrollTimer = setTimeout(updateState, 50);
67
+ });
68
+
69
+ updateState();
70
+ });
71
+ }
72
+
73
+ if (document.readyState === "loading") {
74
+ document.addEventListener("DOMContentLoaded", init);
75
+ } else {
76
+ init();
77
+ }
package/js/combobox.js ADDED
@@ -0,0 +1,125 @@
1
+ // Combobox — ciderui
2
+ // Searchable select dropdown
3
+
4
+ function init() {
5
+ document.querySelectorAll(".combobox").forEach((combobox) => {
6
+ const trigger = combobox.querySelector("button");
7
+ const content = combobox.querySelector("[data-combobox-content]");
8
+ const input = content ? content.querySelector("header input") : null;
9
+ const listbox = content ? content.querySelector("[role='listbox']") : null;
10
+ const valueDisplay = trigger ? trigger.querySelector("span") : null;
11
+ const hiddenInput = combobox.querySelector("input[type='hidden']");
12
+
13
+ if (!trigger || !content || !listbox) return;
14
+
15
+ const options = listbox.querySelectorAll("[role='option']");
16
+ const groups = listbox.querySelectorAll("[role='group']");
17
+
18
+ function open() {
19
+ combobox.setAttribute("data-open", "");
20
+ if (input) {
21
+ input.value = "";
22
+ input.dispatchEvent(new Event("input"));
23
+ requestAnimationFrame(() => input.focus());
24
+ }
25
+ }
26
+
27
+ function close() {
28
+ combobox.removeAttribute("data-open");
29
+ }
30
+
31
+ function isOpen() {
32
+ return combobox.hasAttribute("data-open");
33
+ }
34
+
35
+ // Toggle on trigger click
36
+ trigger.addEventListener("click", () => {
37
+ // Close all other comboboxes
38
+ document.querySelectorAll(".combobox[data-open]").forEach((el) => {
39
+ if (el !== combobox) el.removeAttribute("data-open");
40
+ });
41
+ if (isOpen()) {
42
+ close();
43
+ } else {
44
+ open();
45
+ }
46
+ });
47
+
48
+ // Select an option
49
+ options.forEach((option) => {
50
+ option.addEventListener("click", () => {
51
+ const value = option.dataset.value || option.textContent.trim();
52
+ const text = option.textContent.trim();
53
+
54
+ // Update display
55
+ if (valueDisplay) {
56
+ valueDisplay.textContent = text;
57
+ valueDisplay.removeAttribute("data-placeholder");
58
+ }
59
+
60
+ // Update hidden input
61
+ if (hiddenInput) {
62
+ hiddenInput.value = value;
63
+ }
64
+
65
+ // Mark selected
66
+ options.forEach((o) => o.removeAttribute("data-selected"));
67
+ option.setAttribute("data-selected", "");
68
+
69
+ close();
70
+ });
71
+ });
72
+
73
+ // Search filtering
74
+ if (input) {
75
+ input.addEventListener("input", () => {
76
+ const query = input.value.toLowerCase().trim();
77
+ let anyVisible = false;
78
+
79
+ options.forEach((option) => {
80
+ const text = (option.dataset.value || option.textContent || "").toLowerCase();
81
+ const match = !query || text.includes(query);
82
+ option.hidden = !match;
83
+ if (match) anyVisible = true;
84
+ });
85
+
86
+ // Hide empty groups
87
+ groups.forEach((group) => {
88
+ const visibleOptions = group.querySelectorAll("[role='option']:not([hidden])");
89
+ const heading = group.querySelector("[role='heading']");
90
+ if (heading) heading.hidden = visibleOptions.length === 0;
91
+ group.hidden = visibleOptions.length === 0;
92
+ });
93
+
94
+ // Toggle empty state
95
+ if (listbox.hasAttribute("data-empty")) {
96
+ listbox.classList.toggle("combobox-empty", !anyVisible);
97
+ }
98
+ });
99
+ }
100
+ });
101
+ }
102
+
103
+ if (document.readyState === "loading") {
104
+ document.addEventListener("DOMContentLoaded", init);
105
+ } else {
106
+ init();
107
+ }
108
+
109
+ // Close on outside click
110
+ document.addEventListener("click", (e) => {
111
+ if (!e.target.closest(".combobox")) {
112
+ document.querySelectorAll(".combobox[data-open]").forEach((el) => {
113
+ el.removeAttribute("data-open");
114
+ });
115
+ }
116
+ });
117
+
118
+ // Close on Escape
119
+ document.addEventListener("keydown", (e) => {
120
+ if (e.key === "Escape") {
121
+ document.querySelectorAll(".combobox[data-open]").forEach((el) => {
122
+ el.removeAttribute("data-open");
123
+ });
124
+ }
125
+ });
package/js/command.js ADDED
@@ -0,0 +1,91 @@
1
+ // Command — ciderui
2
+ // Handles search filtering in .command components
3
+
4
+ function init() {
5
+ document.querySelectorAll(".command").forEach((command) => {
6
+ const input = command.querySelector("header input");
7
+ if (!input) return;
8
+
9
+ const menu = command.querySelector("[role='menu'], [role='listbox']");
10
+ if (!menu) return;
11
+
12
+ const items = menu.querySelectorAll("[role='menuitem'], [role='option']");
13
+ const groups = menu.querySelectorAll("[role='group']");
14
+ const separators = menu.querySelectorAll("hr[role='separator'], hr");
15
+ const emptyContainer = menu.hasAttribute("data-empty") ? menu : null;
16
+
17
+ input.addEventListener("input", () => {
18
+ const query = input.value.toLowerCase().trim();
19
+
20
+ let anyVisible = false;
21
+
22
+ items.forEach((item) => {
23
+ const text = (item.getAttribute("data-filter") || item.textContent || "").toLowerCase();
24
+ const match = !query || text.includes(query);
25
+ item.hidden = !match;
26
+ if (match) anyVisible = true;
27
+ });
28
+
29
+ // Hide groups that have no visible items
30
+ groups.forEach((group) => {
31
+ const visibleItems = group.querySelectorAll("[role='menuitem']:not([hidden]), [role='option']:not([hidden])");
32
+ const heading = group.querySelector("[role='heading']");
33
+ if (heading) heading.hidden = visibleItems.length === 0;
34
+ group.hidden = visibleItems.length === 0;
35
+ });
36
+
37
+ // Hide separators when filtering
38
+ separators.forEach((sep) => {
39
+ sep.hidden = !!query;
40
+ });
41
+
42
+ // Toggle empty message via CSS (data-empty::after shows when no items visible)
43
+ if (emptyContainer) {
44
+ emptyContainer.classList.toggle("command-empty", !anyVisible);
45
+ }
46
+ });
47
+ });
48
+
49
+ // Close command dialogs on backdrop click
50
+ document.querySelectorAll(".command-dialog").forEach((dialog) => {
51
+ dialog.addEventListener("click", (e) => {
52
+ if (e.target === dialog) {
53
+ dialog.close();
54
+ }
55
+ });
56
+ });
57
+ }
58
+
59
+ if (document.readyState === "loading") {
60
+ document.addEventListener("DOMContentLoaded", init);
61
+ } else {
62
+ init();
63
+ }
64
+
65
+ // Command dialog — keyboard shortcut (single listener for all dialogs)
66
+ document.addEventListener("keydown", (e) => {
67
+ document.querySelectorAll(".command-dialog[data-shortcut]").forEach((dialog) => {
68
+ const parts = dialog.dataset.shortcut.toLowerCase().split("+");
69
+ const key = parts.pop();
70
+ const needsMeta = parts.includes("meta") || parts.includes("cmd");
71
+ const needsCtrl = parts.includes("ctrl");
72
+
73
+ const metaMatch = needsMeta ? (e.metaKey || e.ctrlKey) : true;
74
+ const ctrlMatch = needsCtrl ? e.ctrlKey : true;
75
+
76
+ if (metaMatch && ctrlMatch && e.key.toLowerCase() === key) {
77
+ e.preventDefault();
78
+ if (dialog.open) {
79
+ dialog.close();
80
+ } else {
81
+ dialog.showModal();
82
+ const input = dialog.querySelector("header input");
83
+ if (input) {
84
+ input.value = "";
85
+ input.dispatchEvent(new Event("input"));
86
+ input.focus();
87
+ }
88
+ }
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,49 @@
1
+ // Context Menu — ciderui
2
+ // Right-click triggered menu, reuses dropdown-menu styling.
3
+ document.addEventListener("contextmenu", (e) => {
4
+ const trigger = e.target.closest(".context-menu-trigger");
5
+ if (!trigger) return;
6
+
7
+ e.preventDefault();
8
+
9
+ // Close all context menus first
10
+ document.querySelectorAll(".context-menu-content[data-open]").forEach((el) => {
11
+ el.removeAttribute("data-open");
12
+ });
13
+
14
+ const menu = trigger.querySelector(".context-menu-content") ||
15
+ trigger.nextElementSibling;
16
+ if (!menu || !menu.classList.contains("context-menu-content")) return;
17
+
18
+ // Position at cursor
19
+ menu.style.left = e.clientX + "px";
20
+ menu.style.top = e.clientY + "px";
21
+ menu.setAttribute("data-open", "");
22
+
23
+ // Adjust if overflowing viewport
24
+ requestAnimationFrame(() => {
25
+ const rect = menu.getBoundingClientRect();
26
+ if (rect.right > window.innerWidth) {
27
+ menu.style.left = (e.clientX - rect.width) + "px";
28
+ }
29
+ if (rect.bottom > window.innerHeight) {
30
+ menu.style.top = (e.clientY - rect.height) + "px";
31
+ }
32
+ });
33
+ });
34
+
35
+ // Close on click anywhere
36
+ document.addEventListener("click", () => {
37
+ document.querySelectorAll(".context-menu-content[data-open]").forEach((el) => {
38
+ el.removeAttribute("data-open");
39
+ });
40
+ });
41
+
42
+ // Close on Escape
43
+ document.addEventListener("keydown", (e) => {
44
+ if (e.key === "Escape") {
45
+ document.querySelectorAll(".context-menu-content[data-open]").forEach((el) => {
46
+ el.removeAttribute("data-open");
47
+ });
48
+ }
49
+ });
package/js/dialog.js ADDED
@@ -0,0 +1,18 @@
1
+ // Dialog — ciderui
2
+ // Uses native <dialog> element — no JS needed for basic open/close.
3
+ // This script adds backdrop-click-to-close behavior.
4
+ function init() {
5
+ document.querySelectorAll("dialog").forEach((dialog) => {
6
+ dialog.addEventListener("click", (e) => {
7
+ if (e.target === dialog) {
8
+ dialog.close();
9
+ }
10
+ });
11
+ });
12
+ }
13
+
14
+ if (document.readyState === "loading") {
15
+ document.addEventListener("DOMContentLoaded", init);
16
+ } else {
17
+ init();
18
+ }
@@ -0,0 +1,39 @@
1
+ // Dropdown Menu — ciderui
2
+ document.addEventListener("click", (e) => {
3
+ const trigger = e.target.closest("[data-dropdown-trigger]");
4
+ if (trigger) {
5
+ const dropdown = trigger.closest("[data-dropdown]");
6
+ const content = dropdown.querySelector("[data-dropdown-content]");
7
+ const isOpen = content.hasAttribute("data-open");
8
+
9
+ // Close all other dropdowns
10
+ document.querySelectorAll("[data-dropdown-content][data-open]").forEach((el) => {
11
+ el.removeAttribute("data-open");
12
+ });
13
+
14
+ if (!isOpen) {
15
+ content.setAttribute("data-open", "");
16
+ }
17
+ return;
18
+ }
19
+
20
+ // Close if clicking an item inside the dropdown
21
+ if (e.target.closest("[data-dropdown-content] button")) {
22
+ e.target.closest("[data-dropdown-content]").removeAttribute("data-open");
23
+ return;
24
+ }
25
+
26
+ // Close all dropdowns on outside click
27
+ document.querySelectorAll("[data-dropdown-content][data-open]").forEach((el) => {
28
+ el.removeAttribute("data-open");
29
+ });
30
+ });
31
+
32
+ // Close on Escape
33
+ document.addEventListener("keydown", (e) => {
34
+ if (e.key === "Escape") {
35
+ document.querySelectorAll("[data-dropdown-content][data-open]").forEach((el) => {
36
+ el.removeAttribute("data-open");
37
+ });
38
+ }
39
+ });
package/js/otp.js ADDED
@@ -0,0 +1,92 @@
1
+ // Input OTP — ciderui
2
+ // Manages hidden input, auto-advance, backspace, paste for OTP slots.
3
+ function init() {
4
+ document.querySelectorAll(".input-otp").forEach((otp) => {
5
+ const slots = otp.querySelectorAll(".input-otp-slot");
6
+ if (!slots.length) return;
7
+
8
+ // Create hidden input for the full value
9
+ let hidden = otp.querySelector("input[type=hidden]");
10
+ if (!hidden) {
11
+ hidden = document.createElement("input");
12
+ hidden.type = "hidden";
13
+ hidden.name = otp.dataset.name || "otp";
14
+ otp.appendChild(hidden);
15
+ }
16
+
17
+ function syncValue() {
18
+ hidden.value = Array.from(slots).map((s) => s.textContent.trim()).join("");
19
+ // Update active state
20
+ slots.forEach((s) => s.removeAttribute("data-active"));
21
+ }
22
+
23
+ function focusSlot(index) {
24
+ if (index >= 0 && index < slots.length) {
25
+ slots[index].focus();
26
+ slots.forEach((s) => s.removeAttribute("data-active"));
27
+ slots[index].setAttribute("data-active", "");
28
+ }
29
+ }
30
+
31
+ slots.forEach((slot, i) => {
32
+ slot.setAttribute("tabindex", "0");
33
+ slot.setAttribute("role", "textbox");
34
+ slot.setAttribute("aria-label", `Digit ${i + 1}`);
35
+
36
+ slot.addEventListener("focus", () => {
37
+ slots.forEach((s) => s.removeAttribute("data-active"));
38
+ slot.setAttribute("data-active", "");
39
+ });
40
+
41
+ slot.addEventListener("blur", () => {
42
+ slot.removeAttribute("data-active");
43
+ });
44
+
45
+ slot.addEventListener("keydown", (e) => {
46
+ if (e.key === "Backspace") {
47
+ e.preventDefault();
48
+ if (slot.textContent.trim()) {
49
+ slot.textContent = "";
50
+ syncValue();
51
+ } else if (i > 0) {
52
+ slots[i - 1].textContent = "";
53
+ focusSlot(i - 1);
54
+ syncValue();
55
+ }
56
+ return;
57
+ }
58
+ if (e.key === "ArrowLeft") { e.preventDefault(); focusSlot(i - 1); return; }
59
+ if (e.key === "ArrowRight") { e.preventDefault(); focusSlot(i + 1); return; }
60
+ if (e.key.length === 1 && /\d/.test(e.key)) {
61
+ e.preventDefault();
62
+ slot.textContent = e.key;
63
+ syncValue();
64
+ focusSlot(i + 1);
65
+ }
66
+ });
67
+
68
+ slot.addEventListener("paste", (e) => {
69
+ e.preventDefault();
70
+ const text = (e.clipboardData || window.clipboardData).getData("text").replace(/\D/g, "");
71
+ for (let j = 0; j < text.length && i + j < slots.length; j++) {
72
+ slots[i + j].textContent = text[j];
73
+ }
74
+ syncValue();
75
+ focusSlot(Math.min(i + text.length, slots.length - 1));
76
+ });
77
+ });
78
+
79
+ // Click the container focuses the first empty slot
80
+ otp.addEventListener("click", (e) => {
81
+ if (e.target.closest(".input-otp-slot")) return;
82
+ const firstEmpty = Array.from(slots).findIndex((s) => !s.textContent.trim());
83
+ focusSlot(firstEmpty >= 0 ? firstEmpty : slots.length - 1);
84
+ });
85
+ });
86
+ }
87
+
88
+ if (document.readyState === "loading") {
89
+ document.addEventListener("DOMContentLoaded", init);
90
+ } else {
91
+ init();
92
+ }
package/js/popover.js ADDED
@@ -0,0 +1,35 @@
1
+ // Popover — ciderui
2
+ document.addEventListener("click", (e) => {
3
+ const trigger = e.target.closest("[data-popover-trigger]");
4
+ if (trigger) {
5
+ const popover = trigger.closest("[data-popover]");
6
+ const content = popover.querySelector(".popover-content");
7
+ const isOpen = content.hasAttribute("data-open");
8
+
9
+ // Close all other popovers
10
+ document.querySelectorAll(".popover-content[data-open]").forEach((el) => {
11
+ el.removeAttribute("data-open");
12
+ });
13
+
14
+ if (!isOpen) {
15
+ content.setAttribute("data-open", "");
16
+ }
17
+ return;
18
+ }
19
+
20
+ // Close all popovers on outside click
21
+ if (!e.target.closest(".popover-content")) {
22
+ document.querySelectorAll(".popover-content[data-open]").forEach((el) => {
23
+ el.removeAttribute("data-open");
24
+ });
25
+ }
26
+ });
27
+
28
+ // Close on Escape
29
+ document.addEventListener("keydown", (e) => {
30
+ if (e.key === "Escape") {
31
+ document.querySelectorAll(".popover-content[data-open]").forEach((el) => {
32
+ el.removeAttribute("data-open");
33
+ });
34
+ }
35
+ });
package/js/select.js ADDED
@@ -0,0 +1,56 @@
1
+ // Custom Select — ciderui
2
+ function init() {
3
+ document.querySelectorAll("[data-select]").forEach((select) => {
4
+ const trigger = select.querySelector("[data-select-trigger]");
5
+ const content = select.querySelector("[data-select-content]");
6
+ const valueEl = select.querySelector("[data-select-value]");
7
+ const items = select.querySelectorAll("[data-select-item]");
8
+
9
+ trigger.addEventListener("click", () => {
10
+ const isOpen = !content.classList.contains("hidden");
11
+ // Close all other selects
12
+ document.querySelectorAll("[data-select-content]").forEach((el) => {
13
+ el.classList.add("hidden");
14
+ });
15
+ if (!isOpen) {
16
+ content.classList.remove("hidden");
17
+ }
18
+ });
19
+
20
+ items.forEach((item) => {
21
+ item.addEventListener("click", () => {
22
+ valueEl.textContent = item.textContent;
23
+ valueEl.removeAttribute("data-placeholder");
24
+ content.classList.add("hidden");
25
+
26
+ // Mark selected
27
+ items.forEach((i) => i.removeAttribute("data-selected"));
28
+ item.setAttribute("data-selected", "");
29
+ });
30
+ });
31
+ });
32
+ }
33
+
34
+ if (document.readyState === "loading") {
35
+ document.addEventListener("DOMContentLoaded", init);
36
+ } else {
37
+ init();
38
+ }
39
+
40
+ // Close on outside click
41
+ document.addEventListener("click", (e) => {
42
+ if (!e.target.closest("[data-select]")) {
43
+ document.querySelectorAll("[data-select-content]").forEach((el) => {
44
+ el.classList.add("hidden");
45
+ });
46
+ }
47
+ });
48
+
49
+ // Close on Escape
50
+ document.addEventListener("keydown", (e) => {
51
+ if (e.key === "Escape") {
52
+ document.querySelectorAll("[data-select-content]").forEach((el) => {
53
+ el.classList.add("hidden");
54
+ });
55
+ }
56
+ });