ajo-ui 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/LICENSE +15 -0
- package/README.md +126 -0
- package/dist/accordion.js +130 -0
- package/dist/avatar.js +54 -0
- package/dist/calendar.js +2 -0
- package/dist/carousel.js +239 -0
- package/dist/chart.js +790 -0
- package/dist/checkbox-group.js +70 -0
- package/dist/checkbox.js +77 -0
- package/dist/chunks/bar-CVafh6C1.js +99 -0
- package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
- package/dist/chunks/collection-DtRB63U4.js +111 -0
- package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
- package/dist/chunks/menu-B45IyHHC.js +704 -0
- package/dist/chunks/native-BJdhd9XJ.js +20 -0
- package/dist/chunks/popup-C8Bb3l_g.js +459 -0
- package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
- package/dist/chunks/position-D6_i_SRn.js +434 -0
- package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
- package/dist/collapsible.js +106 -0
- package/dist/command.js +263 -0
- package/dist/context-menu.js +112 -0
- package/dist/data-table.js +5 -0
- package/dist/dialog.js +207 -0
- package/dist/direction.js +22 -0
- package/dist/drawer.js +139 -0
- package/dist/field.js +26 -0
- package/dist/index.js +38 -0
- package/dist/input-date.js +994 -0
- package/dist/input-group.js +52 -0
- package/dist/input-otp.js +179 -0
- package/dist/menu.js +2 -0
- package/dist/menubar.js +236 -0
- package/dist/message-scroller.js +446 -0
- package/dist/navigation-menu.js +330 -0
- package/dist/popover.js +307 -0
- package/dist/progress.js +39 -0
- package/dist/radio-group.js +107 -0
- package/dist/resizable.js +172 -0
- package/dist/select.js +961 -0
- package/dist/sidebar.js +343 -0
- package/dist/slider.js +259 -0
- package/dist/switch.js +53 -0
- package/dist/tabs.js +182 -0
- package/dist/toast.js +492 -0
- package/dist/toggle-group.js +111 -0
- package/dist/toggle.js +52 -0
- package/dist/toolbar.js +127 -0
- package/dist/tooltip.js +196 -0
- package/dist/utils.js +104 -0
- package/dist/virtual-list.js +2 -0
- package/package.json +250 -0
- package/src/accordion.tsx +261 -0
- package/src/availability.ts +261 -0
- package/src/avatar.tsx +99 -0
- package/src/bar.ts +156 -0
- package/src/calendar.tsx +1441 -0
- package/src/carousel.tsx +424 -0
- package/src/chart.tsx +1194 -0
- package/src/checkbox-group.tsx +132 -0
- package/src/checkbox.tsx +130 -0
- package/src/collapsible.tsx +188 -0
- package/src/collection.ts +154 -0
- package/src/command.tsx +511 -0
- package/src/context-menu.tsx +233 -0
- package/src/data-table-contract.ts +143 -0
- package/src/data-table-model.ts +760 -0
- package/src/data-table.tsx +475 -0
- package/src/dialog.tsx +393 -0
- package/src/direction.tsx +45 -0
- package/src/drawer.tsx +251 -0
- package/src/field.tsx +61 -0
- package/src/index.ts +37 -0
- package/src/input-date.tsx +1539 -0
- package/src/input-group.tsx +142 -0
- package/src/input-otp.tsx +324 -0
- package/src/menu-cluster.ts +124 -0
- package/src/menu.tsx +1095 -0
- package/src/menubar.tsx +459 -0
- package/src/message-scroller.tsx +732 -0
- package/src/native.ts +26 -0
- package/src/navigation-menu.tsx +578 -0
- package/src/popover.tsx +519 -0
- package/src/popup-surface.tsx +31 -0
- package/src/popup.ts +569 -0
- package/src/position.ts +523 -0
- package/src/progress.tsx +70 -0
- package/src/radio-group.tsx +186 -0
- package/src/resizable.tsx +310 -0
- package/src/segments.ts +922 -0
- package/src/select.tsx +1501 -0
- package/src/sidebar.tsx +683 -0
- package/src/slider.tsx +424 -0
- package/src/switch.tsx +104 -0
- package/src/tabs.tsx +314 -0
- package/src/toast.tsx +923 -0
- package/src/toggle-group.tsx +249 -0
- package/src/toggle.tsx +91 -0
- package/src/toolbar.tsx +212 -0
- package/src/tooltip.tsx +359 -0
- package/src/utils.ts +204 -0
- package/src/virtual-list.tsx +205 -0
- package/src/virtual.ts +385 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2026, Cristian Falcone
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
14
|
+
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ajo-ui
|
|
2
|
+
|
|
3
|
+
Unstyled, accessible component families for Ajo applications.
|
|
4
|
+
|
|
5
|
+
Components provide semantic markup, ARIA relationships, keyboard behavior,
|
|
6
|
+
stable styling hooks, and controlled or uncontrolled state. Use them directly
|
|
7
|
+
to build an application UI or a reusable theme.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add ajo ajo-ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`ajo-ui` requires `ajo ^0.1.35`.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Import a component family from its subpath:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'ajo-ui/accordion'
|
|
23
|
+
import { Popover, PopoverContent, PopoverTrigger } from 'ajo-ui/popover'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The package root exports every component family:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ajo-ui'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ajo-ui/tabs'
|
|
34
|
+
|
|
35
|
+
export default () => (
|
|
36
|
+
<Tabs defaultValue="overview">
|
|
37
|
+
<TabsList aria-label="Project">
|
|
38
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
39
|
+
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
40
|
+
</TabsList>
|
|
41
|
+
|
|
42
|
+
<TabsContent value="overview">Project overview</TabsContent>
|
|
43
|
+
<TabsContent value="activity">Recent activity</TabsContent>
|
|
44
|
+
</Tabs>
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Components
|
|
49
|
+
|
|
50
|
+
| Group | Family subpaths |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Foundations | `direction`, `field`, `input-group` |
|
|
53
|
+
| Disclosure and layout | `accordion`, `collapsible`, `dialog`, `drawer`, `resizable`, `sidebar` |
|
|
54
|
+
| Navigation and menus | `command`, `context-menu`, `menu`, `menubar`, `navigation-menu`, `tabs`, `toolbar` |
|
|
55
|
+
| Inputs and selection | `calendar`, `checkbox`, `checkbox-group`, `input-date`, `input-otp`, `radio-group`, `select`, `slider`, `switch`, `toggle`, `toggle-group` |
|
|
56
|
+
| Overlays and feedback | `popover`, `progress`, `toast`, `tooltip` |
|
|
57
|
+
| Data and display | `avatar`, `carousel`, `chart`, `data-table`, `message-scroller`, `virtual-list` |
|
|
58
|
+
|
|
59
|
+
## Component Utilities
|
|
60
|
+
|
|
61
|
+
`ajo-ui/utils` provides helpers and types for custom components, themes, and
|
|
62
|
+
adapters:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import {
|
|
66
|
+
ariaChecked,
|
|
67
|
+
bool,
|
|
68
|
+
clx,
|
|
69
|
+
flag,
|
|
70
|
+
popupStyle,
|
|
71
|
+
stlx,
|
|
72
|
+
text,
|
|
73
|
+
triggerAttrs,
|
|
74
|
+
withSlot,
|
|
75
|
+
} from 'ajo-ui/utils'
|
|
76
|
+
import type {
|
|
77
|
+
CheckedState,
|
|
78
|
+
FixedArgs,
|
|
79
|
+
OmitArg,
|
|
80
|
+
PopupPlacement,
|
|
81
|
+
PopupPosition,
|
|
82
|
+
StyleInput,
|
|
83
|
+
StyleObject,
|
|
84
|
+
StyleValue,
|
|
85
|
+
} from 'ajo-ui/utils'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Area | Exports |
|
|
89
|
+
|---|---|
|
|
90
|
+
| Popup composition | `PopupPlacement`, `PopupPosition`, `triggerAttrs`, `popupStyle` |
|
|
91
|
+
| Component adapters | `OmitArg`, `FixedArgs`, `withSlot` |
|
|
92
|
+
| Checked state | `CheckedState`, `ariaChecked`, `syncCheckedState` |
|
|
93
|
+
| Values and filtering | `bool`, `flag`, `text`, `strings`, `matchesTokens`, `defaultResultsLabel`, `resolveFilter`, `toNumber`, `emptyChildren` |
|
|
94
|
+
| Classes and styles | `clx`, `stlx`, `StyleValue`, `StyleObject`, `StyleInput` |
|
|
95
|
+
|
|
96
|
+
`OmitArg` removes named properties while preserving Ajo's open argument
|
|
97
|
+
index. `FixedArgs` marks properties supplied by an adapter.
|
|
98
|
+
|
|
99
|
+
## Styling
|
|
100
|
+
|
|
101
|
+
Components render semantic elements, ARIA attributes, `data-slot` markers, and
|
|
102
|
+
state attributes. Apply visual styles through `class`, family class maps, or
|
|
103
|
+
state-aware class callbacks such as `dayClassName`.
|
|
104
|
+
|
|
105
|
+
Boolean state attributes use `data-x="true"` when active. Common
|
|
106
|
+
`data-state` values include `open`, `closed`, `checked`, `unchecked`,
|
|
107
|
+
`active`, `inactive`, `on`, and `off`.
|
|
108
|
+
|
|
109
|
+
## State
|
|
110
|
+
|
|
111
|
+
Controlled and uncontrolled families use matching prop groups:
|
|
112
|
+
|
|
113
|
+
- `value`, `defaultValue`, `onValueChange(value, event)`
|
|
114
|
+
- `open`, `defaultOpen`, `onOpenChange(open, event)`
|
|
115
|
+
- `checked`, `defaultChecked`, `onCheckedChange(checked, event)`
|
|
116
|
+
|
|
117
|
+
## Localization and Direction
|
|
118
|
+
|
|
119
|
+
User-visible and assistive-technology strings have English defaults and
|
|
120
|
+
component args for replacement.
|
|
121
|
+
|
|
122
|
+
`DirectionProvider` supplies the default text direction. Components with
|
|
123
|
+
horizontal keyboard navigation also accept a `dir` override.
|
|
124
|
+
|
|
125
|
+
Component-family subpaths and root imports are side-effect-free and
|
|
126
|
+
tree-shakeable.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { strings } from "./utils.js";
|
|
2
|
+
import { Collapsible, CollapsibleContent, CollapsibleContext, CollapsibleTrigger } from "./collapsible.js";
|
|
3
|
+
import { controlled, listen, roving, statefulRootAttrs } from "ajo-cloves";
|
|
4
|
+
import { context } from "ajo/context";
|
|
5
|
+
import { Fragment, jsx } from "ajo/jsx-runtime";
|
|
6
|
+
//#region packages/ajo-ui/src/accordion.tsx
|
|
7
|
+
var AccordionContext = context(null);
|
|
8
|
+
var sameArray = (first, second) => first.length === second.length && first.every((value, index) => value === second[index]);
|
|
9
|
+
var AccordionRoot = function* ({ defaultValue, type, value }) {
|
|
10
|
+
let collapsible = false;
|
|
11
|
+
let disabled = false;
|
|
12
|
+
let onValueChange;
|
|
13
|
+
let mode = type;
|
|
14
|
+
const multipleState = controlled(this, {
|
|
15
|
+
fallback: type === "multiple" ? strings(value ?? defaultValue) : [],
|
|
16
|
+
onChange: (next, event) => {
|
|
17
|
+
onValueChange?.(next, event);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
const singleState = controlled(this, {
|
|
21
|
+
fallback: type === "single" ? String(value ?? defaultValue ?? "") : "",
|
|
22
|
+
onChange: (next, event) => {
|
|
23
|
+
onValueChange?.(next, event);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
let multiple = multipleState.value;
|
|
27
|
+
let single = singleState.value;
|
|
28
|
+
const isOpen = (itemValue) => mode === "multiple" ? multiple.includes(itemValue) : single === itemValue;
|
|
29
|
+
const setSingle = (next, event) => {
|
|
30
|
+
if (next === single) return;
|
|
31
|
+
singleState.set(next, event);
|
|
32
|
+
};
|
|
33
|
+
const setMultiple = (next, event) => {
|
|
34
|
+
if (sameArray(next, multiple)) return;
|
|
35
|
+
multipleState.set(next, event);
|
|
36
|
+
};
|
|
37
|
+
const toggle = (itemValue, event) => {
|
|
38
|
+
if (disabled) return;
|
|
39
|
+
if (mode === "multiple") {
|
|
40
|
+
setMultiple(multiple.includes(itemValue) ? multiple.filter((value) => value !== itemValue) : [...multiple, itemValue], event);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (single === itemValue) {
|
|
44
|
+
if (collapsible) setSingle("", event);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setSingle(itemValue, event);
|
|
48
|
+
};
|
|
49
|
+
const nav = roving(this, {
|
|
50
|
+
items: () => Array.from(this.querySelectorAll("[data-slot=\"accordion-trigger\"]")).filter((trigger) => trigger.getAttribute("aria-disabled") !== "true" && trigger.offsetParent !== null),
|
|
51
|
+
onMove: (target) => target.focus()
|
|
52
|
+
});
|
|
53
|
+
listen(this, "keydown", (event) => {
|
|
54
|
+
if (!event.target?.matches("[data-slot=\"accordion-trigger\"]")) return;
|
|
55
|
+
nav.handle(event);
|
|
56
|
+
});
|
|
57
|
+
for (const args of this) {
|
|
58
|
+
mode = args.type ?? "single";
|
|
59
|
+
collapsible = mode === "single" && Boolean(args.collapsible);
|
|
60
|
+
disabled = Boolean(args.disabled);
|
|
61
|
+
onValueChange = args.onValueChange;
|
|
62
|
+
multipleState.sync(mode === "multiple" && args.value != null ? strings(args.value) : void 0);
|
|
63
|
+
singleState.sync(mode === "single" && args.value != null ? String(args.value ?? "") : void 0);
|
|
64
|
+
multiple = mode === "multiple" ? multipleState.value : [];
|
|
65
|
+
single = mode === "single" ? singleState.value : "";
|
|
66
|
+
AccordionContext({
|
|
67
|
+
collapsible,
|
|
68
|
+
disabled,
|
|
69
|
+
isOpen,
|
|
70
|
+
toggle,
|
|
71
|
+
type: mode
|
|
72
|
+
});
|
|
73
|
+
yield /* @__PURE__ */ jsx(Fragment, { children: args.children });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
/** Unstyled root provider for accordion state. */
|
|
77
|
+
var Accordion = ({ children, collapsible, defaultValue, disabled, onValueChange, type = "single", value, ...attrs }) => /* @__PURE__ */ jsx(AccordionRoot, {
|
|
78
|
+
...statefulRootAttrs(attrs),
|
|
79
|
+
collapsible: collapsible === true,
|
|
80
|
+
defaultValue,
|
|
81
|
+
disabled: Boolean(disabled),
|
|
82
|
+
onValueChange,
|
|
83
|
+
type,
|
|
84
|
+
value,
|
|
85
|
+
"attr:data-slot": "accordion",
|
|
86
|
+
children
|
|
87
|
+
});
|
|
88
|
+
/** Unstyled accordion section: a group-controlled collapsible details element. */
|
|
89
|
+
var AccordionItem = ({ children, disabled, open: _open, value, ...attrs }) => {
|
|
90
|
+
const accordion = AccordionContext();
|
|
91
|
+
const itemValue = String(value);
|
|
92
|
+
const opened = Boolean(accordion?.isOpen(itemValue));
|
|
93
|
+
const disabledFlag = Boolean(disabled ?? accordion?.disabled);
|
|
94
|
+
return /* @__PURE__ */ jsx(Collapsible, {
|
|
95
|
+
"data-orientation": "vertical",
|
|
96
|
+
"data-slot": "accordion-item",
|
|
97
|
+
"data-value": value,
|
|
98
|
+
...attrs,
|
|
99
|
+
disabled: disabledFlag,
|
|
100
|
+
onOpenChange: (_next, event) => accordion?.toggle(itemValue, event),
|
|
101
|
+
open: opened,
|
|
102
|
+
children
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
/** Unstyled accordion heading trigger: a collapsible summary locked by group rules. */
|
|
106
|
+
var AccordionTrigger = ({ children, ...attrs }) => {
|
|
107
|
+
const accordion = AccordionContext();
|
|
108
|
+
const item = CollapsibleContext();
|
|
109
|
+
const locked = Boolean(item?.open && accordion?.type === "single" && !accordion.collapsible);
|
|
110
|
+
return /* @__PURE__ */ jsx(CollapsibleTrigger, {
|
|
111
|
+
"data-orientation": "vertical",
|
|
112
|
+
"data-slot": "accordion-trigger",
|
|
113
|
+
...attrs,
|
|
114
|
+
locked,
|
|
115
|
+
children
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
/** Unstyled accordion panel content. */
|
|
119
|
+
var AccordionContent = ({ children, innerClass, ...attrs }) => /* @__PURE__ */ jsx(CollapsibleContent, {
|
|
120
|
+
"data-orientation": "vertical",
|
|
121
|
+
"data-slot": "accordion-content",
|
|
122
|
+
role: "region",
|
|
123
|
+
...attrs,
|
|
124
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
125
|
+
class: innerClass,
|
|
126
|
+
children
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
//#endregion
|
|
130
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
package/dist/avatar.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { callHandler } from "ajo-cloves";
|
|
2
|
+
import { jsx } from "ajo/jsx-runtime";
|
|
3
|
+
//#region packages/ajo-ui/src/avatar.tsx
|
|
4
|
+
/** Unstyled avatar root that exposes image, fallback, and badge slots. */
|
|
5
|
+
var Avatar = ({ children, size = "default", ...attrs }) => /* @__PURE__ */ jsx("div", {
|
|
6
|
+
...attrs,
|
|
7
|
+
"data-size": size,
|
|
8
|
+
"data-slot": "avatar",
|
|
9
|
+
children
|
|
10
|
+
});
|
|
11
|
+
/** Unstyled avatar image that hides itself after load errors. */
|
|
12
|
+
var AvatarImage = ({ alt = "", decoding = "async", loading = "lazy", "set:onerror": onerror, "set:onload": onload, ...attrs }) => /* @__PURE__ */ jsx("img", {
|
|
13
|
+
...attrs,
|
|
14
|
+
alt,
|
|
15
|
+
"data-slot": "avatar-image",
|
|
16
|
+
decoding,
|
|
17
|
+
loading,
|
|
18
|
+
"set:onerror": (event) => {
|
|
19
|
+
if (event instanceof Event) {
|
|
20
|
+
event.currentTarget.hidden = true;
|
|
21
|
+
callHandler(onerror, event);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"set:onload": (event) => {
|
|
25
|
+
event.currentTarget.hidden = false;
|
|
26
|
+
callHandler(onload, event);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
/** Unstyled fallback content for an Avatar. */
|
|
30
|
+
var AvatarFallback = ({ children, ...attrs }) => /* @__PURE__ */ jsx("span", {
|
|
31
|
+
...attrs,
|
|
32
|
+
"data-slot": "avatar-fallback",
|
|
33
|
+
children
|
|
34
|
+
});
|
|
35
|
+
/** Unstyled badge anchored to an Avatar. */
|
|
36
|
+
var AvatarBadge = ({ children, ...attrs }) => /* @__PURE__ */ jsx("span", {
|
|
37
|
+
...attrs,
|
|
38
|
+
"data-slot": "avatar-badge",
|
|
39
|
+
children
|
|
40
|
+
});
|
|
41
|
+
/** Unstyled container for a group of avatars. */
|
|
42
|
+
var AvatarGroup = ({ children, ...attrs }) => /* @__PURE__ */ jsx("div", {
|
|
43
|
+
...attrs,
|
|
44
|
+
"data-slot": "avatar-group",
|
|
45
|
+
children
|
|
46
|
+
});
|
|
47
|
+
/** Unstyled count for avatars omitted from a visible group. */
|
|
48
|
+
var AvatarGroupCount = ({ children, ...attrs }) => /* @__PURE__ */ jsx("div", {
|
|
49
|
+
...attrs,
|
|
50
|
+
"data-slot": "avatar-group-count",
|
|
51
|
+
children
|
|
52
|
+
});
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage };
|
package/dist/calendar.js
ADDED
package/dist/carousel.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { callHandler, clamp, listen, resize, scrolling, statefulRootAttrs } from "ajo-cloves";
|
|
2
|
+
import { context } from "ajo/context";
|
|
3
|
+
import { Fragment, jsx } from "ajo/jsx-runtime";
|
|
4
|
+
//#region packages/ajo-ui/src/carousel.tsx
|
|
5
|
+
/** Read the observable state and controls inherited from the nearest Carousel. */
|
|
6
|
+
var CarouselContext = context(null);
|
|
7
|
+
var CarouselPartsContext = context(null);
|
|
8
|
+
var carousel = () => {
|
|
9
|
+
const value = CarouselContext();
|
|
10
|
+
if (!value) throw new Error("Carousel parts must be used within a <Carousel />");
|
|
11
|
+
return value;
|
|
12
|
+
};
|
|
13
|
+
var carouselParts = () => {
|
|
14
|
+
const value = CarouselPartsContext();
|
|
15
|
+
if (!value) throw new Error("Carousel parts must be used within a <Carousel />");
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
var axis = (orientation) => orientation === "horizontal" ? "x" : "y";
|
|
19
|
+
var alignBlock = (orientation) => orientation === "vertical" ? "start" : "nearest";
|
|
20
|
+
var alignInline = (orientation) => orientation === "horizontal" ? "start" : "nearest";
|
|
21
|
+
var itemPosition = (item, orientation) => orientation === "horizontal" ? item.offsetLeft : item.offsetTop;
|
|
22
|
+
var viewportPosition = (viewport, orientation) => orientation === "horizontal" ? viewport.scrollLeft : viewport.scrollTop;
|
|
23
|
+
var viewportSize = (viewport, orientation) => orientation === "horizontal" ? viewport.clientWidth : viewport.clientHeight;
|
|
24
|
+
var itemSize = (item, orientation) => orientation === "horizontal" ? item.offsetWidth : item.offsetHeight;
|
|
25
|
+
var CarouselRoot = function* () {
|
|
26
|
+
let viewport = null;
|
|
27
|
+
let orientation = "horizontal";
|
|
28
|
+
let loop = false;
|
|
29
|
+
let selected = 0;
|
|
30
|
+
let count = 0;
|
|
31
|
+
let canScrollPrev = false;
|
|
32
|
+
let canScrollNext = false;
|
|
33
|
+
let apiReceiver;
|
|
34
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
35
|
+
const items = () => viewport ? Array.from(viewport.querySelectorAll("[data-slot=\"carousel-item\"]")) : [];
|
|
36
|
+
const emit = (event) => {
|
|
37
|
+
for (const listener of listeners.get(event) ?? []) listener();
|
|
38
|
+
};
|
|
39
|
+
const syncScrollability = () => {
|
|
40
|
+
canScrollPrev = Boolean(count) && (loop || selected > 0);
|
|
41
|
+
canScrollNext = Boolean(count) && (loop || selected < count - 1);
|
|
42
|
+
};
|
|
43
|
+
const sync = () => {
|
|
44
|
+
const nodes = items();
|
|
45
|
+
const previousSelected = selected;
|
|
46
|
+
const previousCount = count;
|
|
47
|
+
const previousCanScrollPrev = canScrollPrev;
|
|
48
|
+
const previousCanScrollNext = canScrollNext;
|
|
49
|
+
count = nodes.length;
|
|
50
|
+
if (!viewport || !count) selected = 0;
|
|
51
|
+
else {
|
|
52
|
+
const center = viewportPosition(viewport, orientation) + viewportSize(viewport, orientation) / 2;
|
|
53
|
+
let nextSelected = 0;
|
|
54
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
55
|
+
nodes.forEach((item, index) => {
|
|
56
|
+
const next = Math.abs(itemPosition(item, orientation) + itemSize(item, orientation) / 2 - center);
|
|
57
|
+
if (next < distance) {
|
|
58
|
+
distance = next;
|
|
59
|
+
nextSelected = index;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
selected = nextSelected;
|
|
63
|
+
}
|
|
64
|
+
syncScrollability();
|
|
65
|
+
const changed = previousSelected !== selected || previousCount !== count || previousCanScrollPrev !== canScrollPrev || previousCanScrollNext !== canScrollNext;
|
|
66
|
+
if (previousCount !== count) emit("reInit");
|
|
67
|
+
if (previousSelected !== selected) emit("select");
|
|
68
|
+
return changed;
|
|
69
|
+
};
|
|
70
|
+
const invalidate = () => {
|
|
71
|
+
if (sync()) this.next();
|
|
72
|
+
};
|
|
73
|
+
const scroll = scrolling(this, {
|
|
74
|
+
target: () => viewport,
|
|
75
|
+
onScroll: invalidate
|
|
76
|
+
});
|
|
77
|
+
const size = resize(this, {
|
|
78
|
+
target: () => viewport,
|
|
79
|
+
onResize: invalidate
|
|
80
|
+
});
|
|
81
|
+
const scrollTo = (index) => {
|
|
82
|
+
const nodes = items();
|
|
83
|
+
if (!nodes.length) return;
|
|
84
|
+
nodes[clamp(index, 0, nodes.length - 1)]?.scrollIntoView({
|
|
85
|
+
behavior: "smooth",
|
|
86
|
+
block: alignBlock(orientation),
|
|
87
|
+
inline: alignInline(orientation)
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const scrollPrev = () => {
|
|
91
|
+
if (!count) return;
|
|
92
|
+
scrollTo(selected <= 0 && loop ? count - 1 : selected - 1);
|
|
93
|
+
};
|
|
94
|
+
const scrollNext = () => {
|
|
95
|
+
if (!count) return;
|
|
96
|
+
scrollTo(selected >= count - 1 && loop ? 0 : selected + 1);
|
|
97
|
+
};
|
|
98
|
+
const api = {
|
|
99
|
+
canScrollNext: () => canScrollNext,
|
|
100
|
+
canScrollPrev: () => canScrollPrev,
|
|
101
|
+
off: (event, listener) => listeners.get(event)?.delete(listener),
|
|
102
|
+
on: (event, listener) => {
|
|
103
|
+
const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
104
|
+
set.add(listener);
|
|
105
|
+
listeners.set(event, set);
|
|
106
|
+
},
|
|
107
|
+
reInit: () => {
|
|
108
|
+
this.next(sync);
|
|
109
|
+
},
|
|
110
|
+
scrollNext,
|
|
111
|
+
scrollPrev,
|
|
112
|
+
scrollSnapList: () => items().map((_, index) => index),
|
|
113
|
+
scrollTo,
|
|
114
|
+
selectedScrollSnap: () => selected
|
|
115
|
+
};
|
|
116
|
+
const setViewport = (element) => {
|
|
117
|
+
if (viewport === element) return;
|
|
118
|
+
viewport = element;
|
|
119
|
+
scroll.sync();
|
|
120
|
+
size.sync();
|
|
121
|
+
this.next(sync);
|
|
122
|
+
};
|
|
123
|
+
const parts = { setViewport };
|
|
124
|
+
const keydown = (event) => {
|
|
125
|
+
if (orientation === "horizontal") {
|
|
126
|
+
if (event.key === "ArrowLeft") {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
scrollPrev();
|
|
129
|
+
} else if (event.key === "ArrowRight") {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
scrollNext();
|
|
132
|
+
}
|
|
133
|
+
} else if (event.key === "ArrowUp") {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
scrollPrev();
|
|
136
|
+
} else if (event.key === "ArrowDown") {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
scrollNext();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
listen(this, "keydown", keydown);
|
|
142
|
+
for (const { children, opts, orientation: nextOrientation, setApi } of this) {
|
|
143
|
+
orientation = nextOrientation ?? (opts?.axis === "y" ? "vertical" : "horizontal");
|
|
144
|
+
loop = Boolean(opts?.loop);
|
|
145
|
+
syncScrollability();
|
|
146
|
+
if (setApi && setApi !== apiReceiver) {
|
|
147
|
+
apiReceiver = setApi;
|
|
148
|
+
setApi(api);
|
|
149
|
+
}
|
|
150
|
+
scroll.sync();
|
|
151
|
+
size.sync();
|
|
152
|
+
CarouselContext({
|
|
153
|
+
canScrollNext,
|
|
154
|
+
canScrollPrev,
|
|
155
|
+
count,
|
|
156
|
+
orientation,
|
|
157
|
+
scrollNext,
|
|
158
|
+
scrollPrev,
|
|
159
|
+
selected
|
|
160
|
+
});
|
|
161
|
+
CarouselPartsContext(parts);
|
|
162
|
+
yield /* @__PURE__ */ jsx(Fragment, { children });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
/** Unstyled native scroll-snap carousel root. */
|
|
166
|
+
var Carousel = ({ children, dir, opts, orientation, role = "region", setApi, ...attrs }) => /* @__PURE__ */ jsx(CarouselRoot, {
|
|
167
|
+
...statefulRootAttrs(attrs),
|
|
168
|
+
opts,
|
|
169
|
+
orientation,
|
|
170
|
+
setApi,
|
|
171
|
+
"attr:aria-roledescription": "carousel",
|
|
172
|
+
"attr:data-axis": axis(orientation ?? (opts?.axis === "y" ? "vertical" : "horizontal")),
|
|
173
|
+
"attr:data-slot": "carousel",
|
|
174
|
+
"attr:dir": opts?.direction ?? dir,
|
|
175
|
+
"attr:role": role,
|
|
176
|
+
children
|
|
177
|
+
});
|
|
178
|
+
/** Unstyled scroll viewport and track for carousel slides. */
|
|
179
|
+
var CarouselContent = ({ children, viewportClass, ...attrs }) => {
|
|
180
|
+
const state = carousel();
|
|
181
|
+
const parts = carouselParts();
|
|
182
|
+
return /* @__PURE__ */ jsx("div", {
|
|
183
|
+
class: viewportClass,
|
|
184
|
+
"data-slot": "carousel-content",
|
|
185
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
186
|
+
...attrs,
|
|
187
|
+
"data-axis": axis(state.orientation),
|
|
188
|
+
"data-slot": "carousel-track",
|
|
189
|
+
ref: parts.setViewport,
|
|
190
|
+
children
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
/** Unstyled carousel slide item. */
|
|
195
|
+
var CarouselItem = ({ children, role = "group", ...attrs }) => /* @__PURE__ */ jsx("div", {
|
|
196
|
+
...attrs,
|
|
197
|
+
"aria-roledescription": "slide",
|
|
198
|
+
"data-slot": "carousel-item",
|
|
199
|
+
role,
|
|
200
|
+
children
|
|
201
|
+
});
|
|
202
|
+
/** Unstyled previous slide button. */
|
|
203
|
+
var CarouselPrevious = ({ children, "aria-label": label = "Previous slide", disabled, type = "button", "set:onclick": onClick, ...attrs }) => {
|
|
204
|
+
const { canScrollPrev, scrollPrev } = carousel();
|
|
205
|
+
const click = (event) => {
|
|
206
|
+
callHandler(onClick, event);
|
|
207
|
+
if (event.defaultPrevented || disabled || !canScrollPrev) return;
|
|
208
|
+
scrollPrev();
|
|
209
|
+
};
|
|
210
|
+
return /* @__PURE__ */ jsx("button", {
|
|
211
|
+
...attrs,
|
|
212
|
+
"aria-label": label,
|
|
213
|
+
"data-slot": "carousel-previous",
|
|
214
|
+
disabled: disabled || !canScrollPrev,
|
|
215
|
+
"set:onclick": click,
|
|
216
|
+
type,
|
|
217
|
+
children
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
/** Unstyled next slide button. */
|
|
221
|
+
var CarouselNext = ({ children, "aria-label": label = "Next slide", disabled, type = "button", "set:onclick": onClick, ...attrs }) => {
|
|
222
|
+
const { canScrollNext, scrollNext } = carousel();
|
|
223
|
+
const click = (event) => {
|
|
224
|
+
callHandler(onClick, event);
|
|
225
|
+
if (event.defaultPrevented || disabled || !canScrollNext) return;
|
|
226
|
+
scrollNext();
|
|
227
|
+
};
|
|
228
|
+
return /* @__PURE__ */ jsx("button", {
|
|
229
|
+
...attrs,
|
|
230
|
+
"aria-label": label,
|
|
231
|
+
"data-slot": "carousel-next",
|
|
232
|
+
disabled: disabled || !canScrollNext,
|
|
233
|
+
"set:onclick": click,
|
|
234
|
+
type,
|
|
235
|
+
children
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
export { Carousel, CarouselContent, CarouselContext, CarouselItem, CarouselNext, CarouselPrevious };
|