@mlola-ui/registry 1.0.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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/components/accordion.json +54 -0
  4. package/components/activity-heatmap.json +54 -0
  5. package/components/alert.json +56 -0
  6. package/components/app-shell.json +53 -0
  7. package/components/avatar.json +54 -0
  8. package/components/badge.json +62 -0
  9. package/components/breadcrumb.json +49 -0
  10. package/components/button.json +62 -0
  11. package/components/calendar.json +55 -0
  12. package/components/card.json +53 -0
  13. package/components/carousel.json +52 -0
  14. package/components/checkbox.json +50 -0
  15. package/components/color-picker.json +60 -0
  16. package/components/combobox.json +64 -0
  17. package/components/command-menu.json +64 -0
  18. package/components/context-menu.json +61 -0
  19. package/components/date-picker.json +59 -0
  20. package/components/dropdown-menu.json +51 -0
  21. package/components/dropzone.json +45 -0
  22. package/components/empty-state.json +45 -0
  23. package/components/hover-card.json +59 -0
  24. package/components/input.json +56 -0
  25. package/components/kbd.json +45 -0
  26. package/components/modal.json +63 -0
  27. package/components/number-input.json +58 -0
  28. package/components/otp-input.json +49 -0
  29. package/components/pagination.json +50 -0
  30. package/components/popover.json +58 -0
  31. package/components/priority-icon.json +53 -0
  32. package/components/progress.json +57 -0
  33. package/components/radio-group.json +48 -0
  34. package/components/rating.json +48 -0
  35. package/components/resizable.json +46 -0
  36. package/components/segmented-control.json +51 -0
  37. package/components/select.json +67 -0
  38. package/components/sheet.json +66 -0
  39. package/components/skeleton.json +48 -0
  40. package/components/slider.json +53 -0
  41. package/components/status-icon.json +53 -0
  42. package/components/stepper.json +50 -0
  43. package/components/table.json +53 -0
  44. package/components/tabs.json +54 -0
  45. package/components/tag-input.json +53 -0
  46. package/components/textarea.json +48 -0
  47. package/components/time-picker.json +58 -0
  48. package/components/timeline.json +54 -0
  49. package/components/toast.json +56 -0
  50. package/components/toggle.json +52 -0
  51. package/components/tooltip.json +54 -0
  52. package/components/tour.json +62 -0
  53. package/components/virtual-list.json +54 -0
  54. package/generated/catalog.ts +2037 -0
  55. package/index.json +413 -0
  56. package/package.json +47 -0
  57. package/schema/mlola.config.schema.json +51 -0
  58. package/schema/registry-index.schema.json +116 -0
  59. package/schema/registry-item.schema.json +80 -0
  60. package/snapshot/index.json +2938 -0
  61. package/snapshot/items/components/accordion.json +54 -0
  62. package/snapshot/items/components/activity-heatmap.json +54 -0
  63. package/snapshot/items/components/alert.json +56 -0
  64. package/snapshot/items/components/app-shell.json +53 -0
  65. package/snapshot/items/components/avatar.json +54 -0
  66. package/snapshot/items/components/badge.json +62 -0
  67. package/snapshot/items/components/breadcrumb.json +49 -0
  68. package/snapshot/items/components/button.json +62 -0
  69. package/snapshot/items/components/calendar.json +55 -0
  70. package/snapshot/items/components/card.json +53 -0
  71. package/snapshot/items/components/carousel.json +52 -0
  72. package/snapshot/items/components/checkbox.json +50 -0
  73. package/snapshot/items/components/color-picker.json +60 -0
  74. package/snapshot/items/components/combobox.json +64 -0
  75. package/snapshot/items/components/command-menu.json +64 -0
  76. package/snapshot/items/components/context-menu.json +61 -0
  77. package/snapshot/items/components/date-picker.json +59 -0
  78. package/snapshot/items/components/dropdown-menu.json +51 -0
  79. package/snapshot/items/components/dropzone.json +45 -0
  80. package/snapshot/items/components/empty-state.json +45 -0
  81. package/snapshot/items/components/hover-card.json +59 -0
  82. package/snapshot/items/components/input.json +56 -0
  83. package/snapshot/items/components/kbd.json +45 -0
  84. package/snapshot/items/components/modal.json +63 -0
  85. package/snapshot/items/components/number-input.json +58 -0
  86. package/snapshot/items/components/otp-input.json +49 -0
  87. package/snapshot/items/components/pagination.json +50 -0
  88. package/snapshot/items/components/popover.json +58 -0
  89. package/snapshot/items/components/priority-icon.json +53 -0
  90. package/snapshot/items/components/progress.json +57 -0
  91. package/snapshot/items/components/radio-group.json +48 -0
  92. package/snapshot/items/components/rating.json +48 -0
  93. package/snapshot/items/components/resizable.json +46 -0
  94. package/snapshot/items/components/segmented-control.json +51 -0
  95. package/snapshot/items/components/select.json +67 -0
  96. package/snapshot/items/components/sheet.json +66 -0
  97. package/snapshot/items/components/skeleton.json +48 -0
  98. package/snapshot/items/components/slider.json +53 -0
  99. package/snapshot/items/components/status-icon.json +53 -0
  100. package/snapshot/items/components/stepper.json +50 -0
  101. package/snapshot/items/components/table.json +53 -0
  102. package/snapshot/items/components/tabs.json +54 -0
  103. package/snapshot/items/components/tag-input.json +53 -0
  104. package/snapshot/items/components/textarea.json +48 -0
  105. package/snapshot/items/components/time-picker.json +58 -0
  106. package/snapshot/items/components/timeline.json +54 -0
  107. package/snapshot/items/components/toast.json +56 -0
  108. package/snapshot/items/components/toggle.json +52 -0
  109. package/snapshot/items/components/tooltip.json +54 -0
  110. package/snapshot/items/components/tour.json +62 -0
  111. package/snapshot/items/components/virtual-list.json +54 -0
  112. package/snapshot/source/packages/components/_internal/anchor.ts +54 -0
  113. package/snapshot/source/packages/components/_internal/dialog.ts +142 -0
  114. package/snapshot/source/packages/components/_internal/floating.ts +73 -0
  115. package/snapshot/source/packages/components/_internal/match.ts +39 -0
  116. package/snapshot/source/packages/components/_internal/react.ts +40 -0
  117. package/snapshot/source/packages/components/_internal/scroll.ts +15 -0
  118. package/snapshot/source/packages/components/accordion/accordion.tsx +164 -0
  119. package/snapshot/source/packages/components/activity-heatmap/activity-heatmap.tsx +204 -0
  120. package/snapshot/source/packages/components/activity-heatmap/calendar.ts +92 -0
  121. package/snapshot/source/packages/components/alert/alert.tsx +83 -0
  122. package/snapshot/source/packages/components/app-shell/app-shell.tsx +186 -0
  123. package/snapshot/source/packages/components/avatar/avatar.tsx +56 -0
  124. package/snapshot/source/packages/components/badge/badge.tsx +39 -0
  125. package/snapshot/source/packages/components/breadcrumb/breadcrumb.tsx +61 -0
  126. package/snapshot/source/packages/components/button/button.tsx +42 -0
  127. package/snapshot/source/packages/components/calendar/calendar.tsx +309 -0
  128. package/snapshot/source/packages/components/calendar/dates.ts +163 -0
  129. package/snapshot/source/packages/components/card/card.tsx +39 -0
  130. package/snapshot/source/packages/components/carousel/carousel.tsx +248 -0
  131. package/snapshot/source/packages/components/checkbox/checkbox.tsx +66 -0
  132. package/snapshot/source/packages/components/color-picker/color-picker.tsx +261 -0
  133. package/snapshot/source/packages/components/color-picker/color.ts +124 -0
  134. package/snapshot/source/packages/components/combobox/combobox.tsx +320 -0
  135. package/snapshot/source/packages/components/command-menu/command-menu.tsx +199 -0
  136. package/snapshot/source/packages/components/context-menu/context-menu.tsx +192 -0
  137. package/snapshot/source/packages/components/date-picker/date-picker.tsx +185 -0
  138. package/snapshot/source/packages/components/dropdown-menu/dropdown-menu.tsx +157 -0
  139. package/snapshot/source/packages/components/dropzone/dropzone.tsx +185 -0
  140. package/snapshot/source/packages/components/empty-state/empty-state.tsx +32 -0
  141. package/snapshot/source/packages/components/hover-card/hover-card.tsx +103 -0
  142. package/snapshot/source/packages/components/input/input.tsx +101 -0
  143. package/snapshot/source/packages/components/kbd/kbd.tsx +54 -0
  144. package/snapshot/source/packages/components/modal/modal.tsx +100 -0
  145. package/snapshot/source/packages/components/number-input/number-input.tsx +184 -0
  146. package/snapshot/source/packages/components/number-input/number.ts +50 -0
  147. package/snapshot/source/packages/components/otp-input/otp-input.tsx +116 -0
  148. package/snapshot/source/packages/components/pagination/pagination.tsx +65 -0
  149. package/snapshot/source/packages/components/popover/popover.tsx +107 -0
  150. package/snapshot/source/packages/components/priority-icon/labels.ts +13 -0
  151. package/snapshot/source/packages/components/priority-icon/priority-icon.tsx +60 -0
  152. package/snapshot/source/packages/components/progress/progress.tsx +110 -0
  153. package/snapshot/source/packages/components/radio-group/radio-group.tsx +106 -0
  154. package/snapshot/source/packages/components/rating/rating.tsx +94 -0
  155. package/snapshot/source/packages/components/resizable/resizable.tsx +171 -0
  156. package/snapshot/source/packages/components/segmented-control/segmented-control.tsx +78 -0
  157. package/snapshot/source/packages/components/select/select.tsx +332 -0
  158. package/snapshot/source/packages/components/sheet/sheet.tsx +109 -0
  159. package/snapshot/source/packages/components/skeleton/skeleton.tsx +84 -0
  160. package/snapshot/source/packages/components/slider/slider.tsx +99 -0
  161. package/snapshot/source/packages/components/status-icon/labels.ts +14 -0
  162. package/snapshot/source/packages/components/status-icon/status-icon.tsx +76 -0
  163. package/snapshot/source/packages/components/stepper/stepper.tsx +87 -0
  164. package/snapshot/source/packages/components/table/table.tsx +66 -0
  165. package/snapshot/source/packages/components/tabs/tabs.tsx +163 -0
  166. package/snapshot/source/packages/components/tag-input/tag-input.tsx +273 -0
  167. package/snapshot/source/packages/components/textarea/textarea.tsx +70 -0
  168. package/snapshot/source/packages/components/time-picker/time-picker.tsx +286 -0
  169. package/snapshot/source/packages/components/time-picker/times.ts +76 -0
  170. package/snapshot/source/packages/components/timeline/timeline.tsx +115 -0
  171. package/snapshot/source/packages/components/toast/toast.tsx +317 -0
  172. package/snapshot/source/packages/components/toggle/toggle.tsx +80 -0
  173. package/snapshot/source/packages/components/tooltip/tooltip.tsx +70 -0
  174. package/snapshot/source/packages/components/tour/tour.tsx +193 -0
  175. package/snapshot/source/packages/components/virtual-list/virtual-list.tsx +121 -0
  176. package/snapshot/source/packages/components/virtual-list/virtual.ts +33 -0
  177. package/snapshot/source-manifest.json +67 -0
@@ -0,0 +1,186 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { IconChevronDown, IconMenu } from "@mlola-ui/icons";
5
+ import { Resizable } from "../resizable/resizable";
6
+ import { Sheet } from "../sheet/sheet";
7
+ import { cx } from "../_internal/react";
8
+
9
+ interface ShellContext {
10
+ /** Fold the sidebar on wide screens; open the drawer on narrow ones. */
11
+ toggle: () => void;
12
+ folded: boolean;
13
+ narrow: boolean;
14
+ /** Close the drawer, after a navigation on a phone. */
15
+ closeDrawer: () => void;
16
+ }
17
+
18
+ const Context = React.createContext<ShellContext>({ toggle: () => undefined, folded: false, narrow: false, closeDrawer: () => undefined });
19
+
20
+ /** The shell's controls, for a menu button in a page header or a link that should close the drawer. */
21
+ export const useAppShell = () => React.useContext(Context);
22
+
23
+ function useNarrow(query: string) {
24
+ return React.useSyncExternalStore(
25
+ (notify) => {
26
+ const list = window.matchMedia(query);
27
+ list.addEventListener("change", notify);
28
+ return () => list.removeEventListener("change", notify);
29
+ },
30
+ () => window.matchMedia(query).matches,
31
+ () => false,
32
+ );
33
+ }
34
+
35
+ export interface AppShellProps {
36
+ sidebar: React.ReactNode;
37
+ children: React.ReactNode;
38
+ /** Names the navigation region and the drawer. */
39
+ label?: string;
40
+ defaultWidth?: number;
41
+ minWidth?: number;
42
+ maxWidth?: number;
43
+ /** Remembers the width and the fold between visits. */
44
+ storageKey?: string;
45
+ /** Below this width the sidebar becomes a drawer. */
46
+ drawerQuery?: string;
47
+ className?: string;
48
+ }
49
+
50
+ /**
51
+ * The frame of an app: a sidebar that resizes and folds, and the page beside
52
+ * it; on phones the sidebar becomes a drawer. Anything inside can reach the
53
+ * controls with `useAppShell`, and `AppShellMenuButton` shows itself only
54
+ * when the sidebar is out of view.
55
+ */
56
+ export function AppShell({ sidebar, children, label = "Navigation", defaultWidth = 248, minWidth = 200, maxWidth = 360, storageKey, drawerQuery = "(max-width: 48rem)", className }: AppShellProps) {
57
+ const narrow = useNarrow(drawerQuery);
58
+ const [folded, setFolded] = React.useState(false);
59
+ const [drawer, setDrawer] = React.useState(false);
60
+ const context = React.useMemo<ShellContext>(
61
+ () => ({ toggle: () => (narrow ? setDrawer(true) : setFolded((value) => !value)), folded, narrow, closeDrawer: () => setDrawer(false) }),
62
+ [folded, narrow],
63
+ );
64
+ React.useEffect(() => {
65
+ if (!narrow) setDrawer(false);
66
+ }, [narrow]);
67
+ return (
68
+ <Context.Provider value={context}>
69
+ <div className={cx("ml-app-shell", className)} data-folded={folded || undefined} data-narrow={narrow || undefined}>
70
+ <Resizable
71
+ className="ml-app-shell-frame"
72
+ units="pixels"
73
+ defaultSize={defaultWidth}
74
+ min={minWidth}
75
+ max={maxWidth}
76
+ collapsible
77
+ collapsed={folded}
78
+ onCollapsedChange={setFolded}
79
+ storageKey={storageKey}
80
+ label={`Resize ${label.toLowerCase()}`}
81
+ first={
82
+ <nav className="ml-app-shell-sidebar" aria-label={label}>
83
+ {sidebar}
84
+ </nav>
85
+ }
86
+ second={<div className="ml-app-shell-main">{children}</div>}
87
+ />
88
+ <Sheet open={drawer} onClose={() => setDrawer(false)} side="left" size="sm" title={label} className="ml-app-shell-drawer">
89
+ <nav className="ml-app-shell-sidebar" aria-label={label}>
90
+ {sidebar}
91
+ </nav>
92
+ </Sheet>
93
+ </div>
94
+ </Context.Provider>
95
+ );
96
+ }
97
+
98
+ /** A menu button that appears only when the sidebar is folded or on a phone. */
99
+ export function AppShellMenuButton({ label = "Open navigation", className }: { label?: string; className?: string }) {
100
+ const { toggle, folded, narrow } = useAppShell();
101
+ if (!folded && !narrow) return null;
102
+ return (
103
+ <button type="button" className={cx("ml-app-shell-menu", className)} aria-label={label} onClick={toggle}>
104
+ <IconMenu aria-hidden="true" size="1.05em" />
105
+ </button>
106
+ );
107
+ }
108
+
109
+ export function Sidebar({ header, footer, children, className }: { header?: React.ReactNode; footer?: React.ReactNode; children: React.ReactNode; className?: string }) {
110
+ return (
111
+ <div className={cx("ml-sidebar", className)}>
112
+ {header ? <div className="ml-sidebar-header">{header}</div> : null}
113
+ <div className="ml-sidebar-body">{children}</div>
114
+ {footer ? <div className="ml-sidebar-footer">{footer}</div> : null}
115
+ </div>
116
+ );
117
+ }
118
+
119
+ /** A titled group of items; with `collapsible`, the title folds it. */
120
+ export function SidebarSection({ title, collapsible = false, defaultOpen = true, children }: { title?: React.ReactNode; collapsible?: boolean; defaultOpen?: boolean; children: React.ReactNode }) {
121
+ const [open, setOpen] = React.useState(defaultOpen);
122
+ const id = React.useId();
123
+ return (
124
+ <section className="ml-sidebar-section">
125
+ {title ? (
126
+ collapsible ? (
127
+ <button type="button" className="ml-sidebar-title" data-collapsible="" aria-expanded={open} aria-controls={id} onClick={() => setOpen(!open)}>
128
+ {title}
129
+ <IconChevronDown aria-hidden="true" size="0.85em" className="ml-sidebar-chevron" />
130
+ </button>
131
+ ) : (
132
+ <h2 className="ml-sidebar-title">{title}</h2>
133
+ )
134
+ ) : null}
135
+ <ul id={id} className="ml-sidebar-list" hidden={!open}>
136
+ {children}
137
+ </ul>
138
+ </section>
139
+ );
140
+ }
141
+
142
+ export interface SidebarItemProps {
143
+ label: string;
144
+ icon?: React.ReactNode;
145
+ /** A number at the end: unread, open, due. */
146
+ count?: number;
147
+ /** The page being shown. */
148
+ current?: boolean;
149
+ href?: string;
150
+ onClick?: () => void;
151
+ /** Controls revealed on hover, such as a + or a menu. */
152
+ actions?: React.ReactNode;
153
+ }
154
+
155
+ export function SidebarItem({ label, icon, count, current, href, onClick, actions }: SidebarItemProps) {
156
+ const { narrow, closeDrawer } = useAppShell();
157
+ const content = (
158
+ <>
159
+ {icon ? (
160
+ <span className="ml-sidebar-icon" aria-hidden="true">
161
+ {icon}
162
+ </span>
163
+ ) : null}
164
+ <span className="ml-sidebar-label">{label}</span>
165
+ {count ? <span className="ml-sidebar-count">{count}</span> : null}
166
+ </>
167
+ );
168
+ const choose = () => {
169
+ onClick?.();
170
+ if (narrow) closeDrawer();
171
+ };
172
+ return (
173
+ <li className="ml-sidebar-entry">
174
+ {href ? (
175
+ <a className="ml-sidebar-item" href={href} aria-current={current ? "page" : undefined} onClick={choose}>
176
+ {content}
177
+ </a>
178
+ ) : (
179
+ <button type="button" className="ml-sidebar-item" aria-current={current ? "page" : undefined} onClick={choose}>
180
+ {content}
181
+ </button>
182
+ )}
183
+ {actions ? <span className="ml-sidebar-actions">{actions}</span> : null}
184
+ </li>
185
+ );
186
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cx } from "../_internal/react";
5
+
6
+ export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
7
+ export type AvatarStatus = "online" | "offline" | "away" | "busy";
8
+ const initials = (name?: string) => {
9
+ const words = name?.replace(/[^\p{L}\p{N}\s]/gu, "").trim().split(/\s+/).filter(Boolean) ?? [];
10
+ return words.length > 1 ? `${words[0][0]}${words[words.length - 1][0]}`.toUpperCase() : words[0]?.slice(0, 2).toUpperCase() ?? "?";
11
+ };
12
+ interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
13
+ src?: string;
14
+ alt?: string;
15
+ name?: string;
16
+ size?: AvatarSize;
17
+ status?: AvatarStatus;
18
+ }
19
+ const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
20
+ ({ src, alt, name, size = "md", status, className, ...props }, ref) => {
21
+ const [failed, setFailed] = React.useState(false);
22
+ React.useEffect(() => setFailed(false), [src]);
23
+ const label = alt ?? name ?? "Avatar";
24
+ // alt="" marks the avatar as decorative, as it is for images: the name is
25
+ // already on screen beside it and must not be read twice.
26
+ const decorative = alt === "";
27
+ return (
28
+ <span ref={ref} data-size={size} className={cx("ml-avatar-root", className)} {...props}>
29
+ <span className="ml-avatar">
30
+ {src && !failed
31
+ ? <img src={src} alt={label} loading="lazy" className="ml-avatar-image" onError={() => setFailed(true)} />
32
+ : <span role={decorative ? undefined : "img"} aria-label={decorative ? undefined : label} aria-hidden={decorative || undefined} className="ml-avatar-fallback">{initials(name)}</span>}
33
+ </span>
34
+ {status ? <span role="status" aria-label={`${label} is ${status}`} data-status={status} className="ml-avatar-status" /> : null}
35
+ </span>
36
+ );
37
+ }
38
+ );
39
+ Avatar.displayName = "Avatar";
40
+
41
+ interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> { max?: number; }
42
+ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
43
+ ({ max = 4, className, children, ...props }, ref) => {
44
+ const items = React.Children.toArray(children);
45
+ const visible = items.slice(0, Math.max(0, max));
46
+ const overflow = items.length - visible.length;
47
+ return (
48
+ <div ref={ref} role="group" aria-label={`Avatar group, ${items.length} people`} className={cx("ml-avatar-group", className)} {...props}>
49
+ {visible.map((child, index) => <span key={index} className="ml-avatar-group-item">{child}</span>)}
50
+ {overflow ? <span aria-label={`${overflow} more people`} className="ml-avatar-overflow">+{overflow}</span> : null}
51
+ </div>
52
+ );
53
+ }
54
+ );
55
+ AvatarGroup.displayName = "AvatarGroup";
56
+ export { Avatar, AvatarGroup };
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cx } from "../_internal/react";
5
+ import { IconX } from "@mlola-ui/icons";
6
+
7
+ /** The meaning of the colour, from the theme's roles. */
8
+ export type BadgeTone = "neutral" | "primary" | "info" | "success" | "warning" | "danger";
9
+ /** The form: a tint, a solid fill, or an outline. */
10
+ export type BadgeVariant = "soft" | "solid" | "outline";
11
+ type BadgeSize = "sm" | "md" | "lg";
12
+ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
13
+ tone?: BadgeTone;
14
+ variant?: BadgeVariant;
15
+ size?: BadgeSize;
16
+ dot?: boolean;
17
+ onRemove?: () => void;
18
+ }
19
+ const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
20
+ ({ tone = "neutral", variant = "soft", size = "md", dot = false, onRemove, className, children, ...props }, ref) => (
21
+ <span ref={ref} data-tone={tone} data-variant={variant} data-size={size} className={cx("ml-badge", className)} {...props}>
22
+ {dot ? <span aria-hidden="true" className="ml-badge-dot" /> : null}
23
+ <span className="ml-badge-label">{children}</span>
24
+ {onRemove ? (
25
+ <button
26
+ type="button"
27
+ onClick={onRemove}
28
+ aria-label={`Remove${typeof children === "string" ? ` ${children}` : " badge"}`}
29
+ className="ml-badge-remove"
30
+ >
31
+ <IconX aria-hidden="true" size="0.75em" />
32
+ </button>
33
+ ) : null}
34
+ </span>
35
+ )
36
+ );
37
+ Badge.displayName = "Badge";
38
+ export { Badge };
39
+ export type { BadgeSize };
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cx } from "../_internal/react";
5
+ import { IconChevronRight } from "@mlola-ui/icons";
6
+
7
+ export type BreadcrumbItemType = { label: string; href?: string };
8
+ type BreadcrumbProps = { items: BreadcrumbItemType[]; separator?: React.ReactNode; className?: string };
9
+
10
+ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.OlHTMLAttributes<HTMLOListElement>>(
11
+ ({ className, ...props }, ref) => <ol ref={ref} className={cx("ml-breadcrumb-list", className)} {...props} />
12
+ );
13
+ BreadcrumbList.displayName = "BreadcrumbList";
14
+ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.LiHTMLAttributes<HTMLLIElement>>(
15
+ ({ className, ...props }, ref) => <li ref={ref} className={cx("ml-breadcrumb-item", className)} {...props} />
16
+ );
17
+ BreadcrumbItem.displayName = "BreadcrumbItem";
18
+ const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, React.AnchorHTMLAttributes<HTMLAnchorElement>>(
19
+ ({ className, ...props }, ref) => <a ref={ref} className={cx("ml-breadcrumb-link", className)} {...props} />
20
+ );
21
+ BreadcrumbLink.displayName = "BreadcrumbLink";
22
+ function BreadcrumbPage({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
23
+ return <span aria-current="page" className={cx("ml-breadcrumb-page", className)} {...props} />;
24
+ }
25
+ function BreadcrumbSeparator({ children, className }: { children?: React.ReactNode; className?: string }) {
26
+ return <span aria-hidden="true" className={cx("ml-breadcrumb-separator", className)}>{children ?? <IconChevronRight aria-hidden="true" size="1em" />}</span>;
27
+ }
28
+ function BreadcrumbEllipsis({ className }: { className?: string }) {
29
+ return <span aria-hidden="true" className={cx("ml-breadcrumb-ellipsis", className)}>…</span>;
30
+ }
31
+
32
+ export function Breadcrumb({ items, separator, className }: BreadcrumbProps) {
33
+ const last = items.length - 1;
34
+ const collapse = items.length > 3;
35
+ return (
36
+ <nav aria-label="Breadcrumb" className={cx("ml-breadcrumb", className)}>
37
+ <BreadcrumbList>
38
+ {items.map((item, index) => {
39
+ const current = index === last;
40
+ const middle = index > 0 && index < last;
41
+ return (
42
+ <React.Fragment key={`${item.label}-${index}`}>
43
+ {collapse && index === 1 ? (
44
+ <BreadcrumbItem aria-hidden="true" data-collapse-indicator="">
45
+ <BreadcrumbEllipsis /><BreadcrumbSeparator>{separator}</BreadcrumbSeparator>
46
+ </BreadcrumbItem>
47
+ ) : null}
48
+ <BreadcrumbItem data-collapsible={middle && collapse ? "" : undefined}>
49
+ {current || !item.href
50
+ ? <BreadcrumbPage>{item.label}</BreadcrumbPage>
51
+ : <BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>}
52
+ {!current ? <BreadcrumbSeparator>{separator}</BreadcrumbSeparator> : null}
53
+ </BreadcrumbItem>
54
+ </React.Fragment>
55
+ );
56
+ })}
57
+ </BreadcrumbList>
58
+ </nav>
59
+ );
60
+ }
61
+ export { BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Magnetic } from "@mlola-ui/motion";
5
+ import { cx } from "../_internal/react";
6
+
7
+ export type ButtonVariant = "primary" | "secondary" | "subtle" | "danger" | "outline" | "link";
8
+ type ButtonSize = "sm" | "md" | "lg" | "icon";
9
+
10
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
11
+ variant?: ButtonVariant;
12
+ size?: ButtonSize;
13
+ loading?: boolean;
14
+ magnetic?: boolean;
15
+ }
16
+
17
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
18
+ ({ className, variant = "primary", size = "md", loading = false, magnetic = false, disabled, children, type = "button", ...props }, ref) => {
19
+ const isDisabled = disabled || loading;
20
+ const button = (
21
+ <button
22
+ ref={ref}
23
+ type={type}
24
+ disabled={isDisabled}
25
+ aria-disabled={isDisabled || undefined}
26
+ aria-busy={loading || undefined}
27
+ data-variant={variant}
28
+ data-size={size}
29
+ data-loading={loading ? "" : undefined}
30
+ className={cx("ml-button", className)}
31
+ {...props}
32
+ >
33
+ {loading ? <span aria-hidden="true" className="ml-button-spinner" /> : null}
34
+ <span className="ml-button-label">{children}</span>
35
+ </button>
36
+ );
37
+ return magnetic && !isDisabled ? <Magnetic pull={0.2}>{button}</Magnetic> : button;
38
+ }
39
+ );
40
+ Button.displayName = "Button";
41
+
42
+ export { Button };