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.
- package/README.md +106 -0
- package/js/carousel.js +77 -0
- package/js/combobox.js +125 -0
- package/js/command.js +91 -0
- package/js/context-menu.js +49 -0
- package/js/dialog.js +18 -0
- package/js/dropdown-menu.js +39 -0
- package/js/otp.js +92 -0
- package/js/popover.js +35 -0
- package/js/select.js +56 -0
- package/js/sheet.js +17 -0
- package/js/tabs.js +34 -0
- package/js/toast.js +60 -0
- package/js/toggle.js +26 -0
- package/package.json +53 -0
- package/src/css/ciderui.cdn.css +11 -0
- package/src/css/ciderui.css +324 -0
- package/src/css/components/accordion.css +31 -0
- package/src/css/components/alert.css +18 -0
- package/src/css/components/aspect-ratio.css +7 -0
- package/src/css/components/avatar.css +17 -0
- package/src/css/components/badge.css +20 -0
- package/src/css/components/breadcrumb.css +16 -0
- package/src/css/components/button-group.css +20 -0
- package/src/css/components/button.css +59 -0
- package/src/css/components/card.css +37 -0
- package/src/css/components/carousel.css +60 -0
- package/src/css/components/checkbox.css +16 -0
- package/src/css/components/collapsible.css +25 -0
- package/src/css/components/combobox.css +69 -0
- package/src/css/components/command.css +64 -0
- package/src/css/components/context-menu.css +28 -0
- package/src/css/components/dialog.css +23 -0
- package/src/css/components/drawer.css +69 -0
- package/src/css/components/dropdown-menu.css +28 -0
- package/src/css/components/empty.css +12 -0
- package/src/css/components/field.css +7 -0
- package/src/css/components/file-tree.css +53 -0
- package/src/css/components/form-controls.css +57 -0
- package/src/css/components/form.css +15 -0
- package/src/css/components/hover-card.css +25 -0
- package/src/css/components/input-group.css +48 -0
- package/src/css/components/input-otp.css +22 -0
- package/src/css/components/item.css +43 -0
- package/src/css/components/kbd.css +7 -0
- package/src/css/components/label.css +4 -0
- package/src/css/components/pagination.css +15 -0
- package/src/css/components/popover.css +19 -0
- package/src/css/components/progress.css +18 -0
- package/src/css/components/radio.css +13 -0
- package/src/css/components/scroll-area.css +24 -0
- package/src/css/components/separator.css +19 -0
- package/src/css/components/sheet.css +46 -0
- package/src/css/components/sidebar.css +32 -0
- package/src/css/components/skeleton.css +9 -0
- package/src/css/components/slider.css +48 -0
- package/src/css/components/spinner.css +12 -0
- package/src/css/components/switch.css +28 -0
- package/src/css/components/table.css +39 -0
- package/src/css/components/tabs.css +44 -0
- package/src/css/components/theme-switcher.css +17 -0
- package/src/css/components/toast.css +22 -0
- package/src/css/components/toggle.css +34 -0
- package/src/css/components/tooltip.css +66 -0
- 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
|
+
});
|