@azmr/ui 0.0.1 → 0.2.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 (92) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +244 -73
  3. package/dist/avatar.d.ts +58 -0
  4. package/dist/avatar.js +84 -0
  5. package/dist/breadcrumb.d.ts +47 -0
  6. package/dist/breadcrumb.js +120 -0
  7. package/dist/button-CYt2Zw3D.d.ts +64 -0
  8. package/dist/button.d.ts +54 -0
  9. package/dist/button.js +19 -0
  10. package/dist/chunk-2RIWL363.js +27 -0
  11. package/dist/chunk-2TQSHVXA.js +12 -0
  12. package/dist/chunk-7EOSDFS3.js +10 -0
  13. package/dist/chunk-C2HKIPLQ.js +118 -0
  14. package/dist/chunk-CAJLW6K2.js +16 -0
  15. package/dist/chunk-CZJZYN54.js +36 -0
  16. package/dist/chunk-EE4V6XMB.js +35 -0
  17. package/dist/chunk-EFHRRLAW.js +211 -0
  18. package/dist/chunk-GLB75NT3.js +21 -0
  19. package/dist/chunk-GX2YEJ43.js +36 -0
  20. package/dist/chunk-LTOLV67A.js +19 -0
  21. package/dist/chunk-LZMX27IZ.js +10 -0
  22. package/dist/chunk-NIIDYIZA.js +1 -0
  23. package/dist/chunk-Q4PPM2SU.js +97 -0
  24. package/dist/chunk-WVE2E37B.js +20 -0
  25. package/dist/chunk-XRZHP6BD.js +92 -0
  26. package/dist/disclosure.d.ts +72 -0
  27. package/dist/disclosure.js +221 -0
  28. package/dist/display.d.ts +111 -0
  29. package/dist/display.js +237 -0
  30. package/dist/floating.d.ts +81 -0
  31. package/dist/floating.js +231 -0
  32. package/dist/hooks.d.ts +33 -0
  33. package/dist/hooks.js +55 -0
  34. package/dist/icons/lucide.d.ts +77 -0
  35. package/dist/icons/lucide.js +135 -0
  36. package/dist/icons/mui-map.d.ts +1268 -0
  37. package/dist/icons/mui-map.js +352 -0
  38. package/dist/icons.d.ts +48 -0
  39. package/dist/icons.js +19 -0
  40. package/dist/index.d.ts +240 -5
  41. package/dist/index.js +242 -15
  42. package/dist/input.d.ts +14 -0
  43. package/dist/input.js +8 -0
  44. package/dist/layout/mui-map.d.ts +80 -0
  45. package/dist/layout/mui-map.js +104 -0
  46. package/dist/layout.d.ts +89 -0
  47. package/dist/layout.js +455 -0
  48. package/dist/lib.d.ts +11 -0
  49. package/dist/lib.js +6 -0
  50. package/dist/menu.d.ts +105 -0
  51. package/dist/menu.js +56 -0
  52. package/dist/modal.d.ts +105 -0
  53. package/dist/modal.js +273 -0
  54. package/dist/motion.css +295 -0
  55. package/dist/pagination.d.ts +82 -0
  56. package/dist/pagination.js +211 -0
  57. package/dist/presets.d.ts +97 -0
  58. package/dist/presets.js +94 -0
  59. package/dist/progress.d.ts +70 -0
  60. package/dist/progress.js +138 -0
  61. package/dist/react.d.ts +81 -0
  62. package/dist/react.js +281 -0
  63. package/dist/responsive-YJBnvsjt.d.ts +69 -0
  64. package/dist/scroll-area.d.ts +30 -0
  65. package/dist/scroll-area.js +80 -0
  66. package/dist/shared-Fkx-sjPt.d.ts +15 -0
  67. package/dist/spinner.d.ts +18 -0
  68. package/dist/spinner.js +39 -0
  69. package/dist/split-button.d.ts +32 -0
  70. package/dist/split-button.js +54 -0
  71. package/dist/stepper.d.ts +27 -0
  72. package/dist/stepper.js +104 -0
  73. package/dist/styles.css +24 -0
  74. package/dist/table.d.ts +52 -0
  75. package/dist/table.js +124 -0
  76. package/dist/theme.d.ts +111 -0
  77. package/dist/theme.js +414 -0
  78. package/dist/toast.d.ts +77 -0
  79. package/dist/toast.js +203 -0
  80. package/dist/toggle.d.ts +55 -0
  81. package/dist/toggle.js +88 -0
  82. package/dist/tokens.css +337 -0
  83. package/dist/transition.d.ts +51 -0
  84. package/dist/transition.js +79 -0
  85. package/dist/types-BBudf463.d.ts +39 -0
  86. package/dist/typography/mui-map.d.ts +91 -0
  87. package/dist/typography/mui-map.js +62 -0
  88. package/dist/typography.d.ts +90 -0
  89. package/dist/typography.js +193 -0
  90. package/dist/useSignal-C-3ZGURN.d.ts +21 -0
  91. package/dist/variants-De0c0ucO.d.ts +60 -0
  92. package/package.json +173 -18
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Alshafaraz Gazi / Azmara Technologies
3
+ Copyright (c) 2026 Azmara Labs
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,73 +1,244 @@
1
- # @azmr/ui
2
-
3
- React components wired to reactive Signals. The `Grid` component re-renders automatically when its Signal changes — no state management needed.
4
-
5
- ## Install
6
-
7
- ```bash
8
- pnpm add @azmr/ui @azmr/core react react-dom
9
- # or
10
- npm install @azmr/ui @azmr/core react react-dom
11
- ```
12
-
13
- ## Grid
14
-
15
- ```tsx
16
- import { Signal } from "@azmr/core";
17
- import { Grid } from "@azmr/ui";
18
-
19
- const customers = new Signal([
20
- { name: "Aroha", balance: 150 },
21
- { name: "Mere", balance: 320 },
22
- ]);
23
-
24
- function App() {
25
- return (
26
- <Grid
27
- signal={customers}
28
- labels={{ name: "Customer", balance: "Balance ($)" }}
29
- />
30
- );
31
- }
32
-
33
- // Grid updates automatically
34
- customers.set([...customers.peek(), { name: "Tane", balance: 0 }]);
35
- ```
36
-
37
- ## useSignal
38
-
39
- Hook to subscribe any React component to a Signal.
40
-
41
- ```tsx
42
- import { useSignal } from "@azmr/ui";
43
-
44
- function Counter({ count }: { count: Signal<number> }) {
45
- const value = useSignal(count); // push-based, no polling
46
- return <span>{value}</span>;
47
- }
48
- ```
49
-
50
- ## Grid Props
51
-
52
- | Prop | Type | Description |
53
- |---|---|---|
54
- | `signal` | `Signal<T[]>` | Reactive data source |
55
- | `columns` | `(keyof T)[]` | Optional column order |
56
- | `labels` | `Partial<Record<keyof T, string>>` | Optional column label overrides |
57
- | `emptyMessage` | `string` | Message shown when Signal is empty |
58
-
59
- All cell values are rendered through React's JSX escaping — `dangerouslySetInnerHTML` is never used.
60
-
61
- ## Requirements
62
-
63
- - Node.js ≥ 18
64
- - React 18 or 19
65
- - TypeScript ≥ 5 (types included)
66
-
67
- ## Documentation
68
-
69
- Full docs at [docs.azmara.io](https://docs.azmara.io)
70
-
71
- ## License
72
-
73
- MIT © [Azmara Technologies](https://azmara.io)
1
+ # @azmr/ui
2
+
3
+ React components wired to reactive Signals. The `Grid` component re-renders automatically when its Signal changes — no state management needed.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @azmr/ui @azmr/core react react-dom
9
+ # or
10
+ npm install @azmr/ui @azmr/core react react-dom
11
+ ```
12
+
13
+ `@azmr/core` is a **peer dependency**: install it yourself so your app and `@azmr/ui` share one copy (two copies give incompatible `Signal` types).
14
+
15
+ ## Styles and design tokens
16
+
17
+ `@azmr/ui` ships the design tokens and the Tailwind v4 theme mapping in two stylesheets. In your global CSS, pick one:
18
+
19
+ ```css
20
+ @import "tailwindcss";
21
+ @import "@azmr/ui/styles.css"; /* tokens + global base rules (new app) */
22
+ ```
23
+
24
+ ```css
25
+ @import "tailwindcss";
26
+ @import "@azmr/ui/tokens.css"; /* tokens only (app that already has global styles) */
27
+ ```
28
+
29
+ - **`styles.css`** is `tokens.css` plus two global base rules: the page background and text colour on `body`, and our border and outline colours as the default for every element. Use it in a new app.
30
+ - **`tokens.css`** has the same tokens, themes, density, motion and utilities and **changes nothing outside our own classes**. Use it when the app already has global styles: another UI library mid-migration, or a documentation site. Components look right either way.
31
+
32
+ - **Token names follow shadcn/ui** (`--background`, `--primary`, `--ring`, `--radius`, `--sidebar-*`, ...), so shadcn components from any registry look right next to ours. Azmara adds `--success`, `--warning`, `--info`, `--chart-1` to `--chart-8`, `--motion-*`, `--elevation-*`, `--z-*` and `--density-*`.
33
+ - **Utilities** such as `bg-background`, `text-muted-foreground`, `rounded-lg`, `shadow-elevation-2` and `ease-standard` work out of the box. The stylesheets include an `@source` rule, so Tailwind scans the package's classes inside `node_modules`.
34
+ - **Light and dark** use OKLCH colours with `light-dark()`. The page follows the system by default; force a mode with `class="dark"` / `class="light"` or `data-theme="dark|light"` on any ancestor. No JavaScript, no flash.
35
+ - **Density:** set `data-density="compact"` or `"spacious"` on any ancestor to change control heights, padding and row height.
36
+ - **Reduced motion:** with `prefers-reduced-motion: reduce`, every `--motion-duration-*` token collapses to `0.01ms` and a safety net shortens other animations. Mark genuinely essential motion with `data-motion="essential"`.
37
+ - **Accessibility is tested:** every foreground/background token pair meets WCAG AA text contrast (4.5:1) in both modes, the focus ring and chart colours meet 3:1, and the eight chart colours stay distinguishable under protanopia, deuteranopia and tritanopia simulation.
38
+
39
+
40
+ ## Theme, density and locale
41
+
42
+ **Server layout (Next.js App Router).** Read the stored preference from the cookie on the server and render it into `<html>`, so the first paint is already correct, with no inline script (works under a strict CSP):
43
+
44
+ ```tsx
45
+ // app/layout.tsx
46
+ import { cookies } from "next/headers";
47
+ import { htmlAttributes, parseDensity, parseThemePreference, THEME_COOKIE, DENSITY_COOKIE } from "@azmr/ui/theme";
48
+ import { LocaleProvider, ThemeProvider } from "@azmr/ui/react";
49
+
50
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
51
+ const jar = await cookies();
52
+ const theme = parseThemePreference(jar.get(THEME_COOKIE)?.value);
53
+ const density = parseDensity(jar.get(DENSITY_COOKIE)?.value);
54
+ return (
55
+ <html lang="en-NZ" {...htmlAttributes({ theme, density })}>
56
+ <body>
57
+ <ThemeProvider defaultTheme={theme} defaultDensity={density}>
58
+ <LocaleProvider locale="en-NZ" currency="NZD">{children}</LocaleProvider>
59
+ </ThemeProvider>
60
+ </body>
61
+ </html>
62
+ );
63
+ }
64
+ ```
65
+
66
+ - `system` (the default) needs no attribute and no JavaScript: the tokens use `light-dark()`, so the page follows the operating system.
67
+ - `useTheme()` returns `{ theme, resolvedTheme, setTheme, density, setDensity }`. Changes update `<html>` immediately and are remembered in a cookie (`persist={false}` turns that off).
68
+ - Static sites that cannot read cookies on the server can inline `themeInitScript()` instead (add your CSP nonce to the tag).
69
+ - `createTheme({ primary, secondary, success, warning, info, destructive, radius, font, density })` turns brand colours (`#rrggbb`, `oklch(L C H)` or `{ l, c, h }`) into token overrides. Every colour option is optional and keeps its default when omitted (`destructive` is the "danger" colour). It guarantees WCAG AA in both modes, adjusting lightness where needed and keeping the hue:
70
+ - each `-foreground` is at least 4.5:1 on its colour, **including on a solid button's hover surface** (a button blends with the page on hover, which can pull a colour that only just passes below 4.5:1; the derivation checks the blend);
71
+ - `success`, `info` and `destructive` are also readable as **text** on the page and on cards (`Text tone`, form errors), and `warning` gets a separate readable `--warning-text`;
72
+ - the focus ring is at least 3:1.
73
+ Input is validated strictly because the output is CSS, and an error names the option that was wrong.
74
+
75
+ ```tsx
76
+ import { createTheme } from "@azmr/ui/theme";
77
+ const theme = createTheme({
78
+ primary: "#1E88E5",
79
+ success: "#2E7D32",
80
+ destructive: "#D32F2F",
81
+ radius: "0.5rem",
82
+ });
83
+ // <style>{theme.css}</style> or <html style={theme.cssVariables}>
84
+ ```
85
+
86
+ **Locale.** `LocaleProvider` defaults to `en-NZ`, `NZD` and a Monday week start. `useLocale()` gives cached `Intl` formatters (`formatNumber`, `formatCurrency`, `formatDate`, `formatDateTime`) and the strings components show (`messages`, overridable). Date-only strings such as `2026-10-15` print that calendar day in every time zone; missing or invalid values format to an empty string instead of throwing.
87
+
88
+ ## Components
89
+
90
+ Components read the design tokens only (no raw colours), accept `className` (merged last, so it can override defaults), forward `ref` to the real DOM element, and follow the density tokens for height and padding. Import from the root or from a subpath; subpaths and the root are both tree-shakeable.
91
+
92
+ ### Button
93
+
94
+ ```tsx
95
+ import { Button } from "@azmr/ui/button";
96
+
97
+ <Button>Save</Button>
98
+ <Button variant="destructive" loading loadingText="Deleting">Delete</Button>
99
+ <Button variant="outline" startIcon={<PlusIcon />}>New customer</Button>
100
+ <Button size="icon" aria-label="Close"><XIcon /></Button> {/* icon-only needs a name */}
101
+ <Button nativeButton={false} render={<a href="/docs" />}>Docs</Button> {/* renders a link */}
102
+ ```
103
+
104
+ - **Variants:** `default`, `secondary`, `outline`, `ghost`, `destructive`, `success`, `warning`, `info`, `link`. **Sizes:** `sm`, `md` (follows the density token), `lg`, `icon`.
105
+ - **`loading`:** shows a spinner, sets `aria-busy`, ignores clicks and keyboard activation, **stays focusable** (keyboard users keep their place). `loadingText` replaces the label while loading; `startIcon` is replaced by the spinner, `endIcon` stays.
106
+ - **`type` is `"button"` by default**, so a button inside a form never submits by accident; pass `type="submit"` explicitly.
107
+ - **Icon-only buttons are type-checked:** `size="icon"` requires `aria-label` or `aria-labelledby`.
108
+
109
+ ### Input
110
+
111
+ ```tsx
112
+ import { Input } from "@azmr/ui/input";
113
+ import { useForm } from "react-hook-form";
114
+
115
+ const { register } = useForm();
116
+ <label htmlFor="email">Email</label>
117
+ <Input id="email" type="email" {...register("email")} /> {/* no Controller needed */}
118
+ ```
119
+
120
+ `ref` is the real `<input>`, so `register()` works directly. Native `onChange` and the typed `onValueChange(value)` both fire. `aria-invalid="true"` styles the invalid state; focus uses a full-strength ring (WCAG 2.2 focus appearance).
121
+
122
+ ### Spinner
123
+
124
+ `<Spinner />` is decorative; `<Spinner label="Loading customers" />` is announced as a status. It keeps spinning under `prefers-reduced-motion` because a frozen spinner reads as a hung page.
125
+
126
+ ## Icons
127
+
128
+ `Icon` renders any icon component with consistent size, stroke, motion and accessibility. It is a **Server Component** (no hooks, no `"use client"`), so it works in `loading.tsx`, layouts and Suspense fallbacks.
129
+
130
+ ```tsx
131
+ import { Icon } from "@azmr/ui/icons";
132
+ import { Search } from "lucide-react";
133
+ import { IconBell } from "@tabler/icons-react"; // optional: any set, or your own SVG component
134
+
135
+ <Icon icon={Search} /> {/* decorative: aria-hidden */}
136
+ <Icon icon={Search} aria-label="Search" size="lg" /> {/* meaningful: role="img" + name */}
137
+ <Icon icon={IconBell} strokeWidth={1.5} />
138
+ <Icon icon={Loader} spin /> {/* keeps spinning under reduced motion */}
139
+ ```
140
+
141
+ - **Sizes:** `xs`, `sm`, `md` (default), `lg`, `xl` follow the size tokens; a number is a pixel size. `className` merges last, so `className="size-8"` wins.
142
+ - **Accessible by default:** decorative (`aria-hidden`) unless you pass `aria-label` or `aria-labelledby`. An icon-only button is named by a labelled icon.
143
+ - **Bring any set.** Lucide (ISC) is the default; Tabler (MIT) and your own SVG components work through the same `icon` prop with no adapter. `react-icons` is deliberately not bundled: it mixes licences and only tree-shakes through exact subpath imports.
144
+ - **Tree-shaking is tested:** the Icon core bundles no icon at all, and an app that imports one icon bundles that icon only.
145
+
146
+ **By name.** Three ways, from most to least isolated:
147
+
148
+ ```tsx
149
+ // 1. The curated Lucide set, with typed names (about 60 everyday icons; Lucide's own kebab-case names)
150
+ import { Icon } from "@azmr/ui/icons/lucide";
151
+ <Icon name="circle-alert" />
152
+
153
+ // 2. Your own typed set: nothing global, only the icons you list are bundled
154
+ import { createIconSet } from "@azmr/ui/icons";
155
+ export const { Icon } = createIconSet({ check: Check, "my-logo": MyLogo });
156
+
157
+ // 3. A global registry for custom SVGs (names are untyped strings)
158
+ import { registerIcons } from "@azmr/ui/icons";
159
+ registerIcons({ "my-logo": MyLogo }); // then <Icon name="my-logo" />
160
+ ```
161
+
162
+ An unknown name renders nothing and logs one development warning; it never crashes the page.
163
+
164
+ **Migrating from `@mui/icons-material`.** `@azmr/ui/icons/mui-map` is plain data: `resolveMuiIcon("SearchOutlined")` returns `{ name: "Search", variant: "outlined", lucide: "search", tabler: "IconSearch" }`. It covers over 300 common Material icons, strips the `Outlined` / `Rounded` / `Sharp` / `TwoTone` variants (and the legacy `Outline` spelling older MUI versions export), maps brand icons to Tabler only (Lucide has none), and lists the common names it deliberately does not map in `unmappedMuiIcons`. Every name in it is checked by tests against the real `@mui/icons-material`, `lucide-react` and `@tabler/icons-react`. The migration codemod uses it and reports anything unmapped instead of guessing.
165
+
166
+ ## Entry points
167
+
168
+ | Import | Contents | Directive |
169
+ |---|---|---|
170
+ | `@azmr/ui` | everything (`Grid`, `useSignal`, `cn`) | `"use client"` |
171
+ | `@azmr/ui/hooks` | React hooks (`useSignal`) | `"use client"` |
172
+ | `@azmr/ui/theme` | server-safe theme helpers: `createTheme`, cookies, `htmlAttributes`, `createLocale` | none, importable from Server Components and middleware |
173
+ | `@azmr/ui/react` | `ThemeProvider`, `useTheme`, `LocaleProvider`, `useLocale` | `"use client"` |
174
+ | `@azmr/ui/button` | `Button`, `buttonVariants` | `"use client"` |
175
+ | `@azmr/ui/input` | `Input` | `"use client"` |
176
+ | `@azmr/ui/spinner` | `Spinner` | none, importable from Server Components |
177
+ | `@azmr/ui/icons` | `Icon`, `createIconSet`, `registerIcons` | none, importable from Server Components |
178
+ | `@azmr/ui/icons/lucide` | `Icon` bound to the curated Lucide set, `lucideIcons` | none, importable from Server Components |
179
+ | `@azmr/ui/icons/mui-map` | `resolveMuiIcon`, `muiIconMap`, `unmappedMuiIcons` (migration data) | none, pure data |
180
+ | `@azmr/ui/lib` | `cn()` and other server-safe helpers | none, importable from Server Components |
181
+
182
+ `cn(...classes)` merges class names and resolves conflicting Tailwind utilities, last one wins: `cn("p-2", "p-4")` → `"p-4"`.
183
+
184
+ ## Grid
185
+
186
+ ```tsx
187
+ import { Signal } from "@azmr/core";
188
+ import { Grid } from "@azmr/ui";
189
+
190
+ const customers = new Signal([
191
+ { name: "Aroha", balance: 150 },
192
+ { name: "Mere", balance: 320 },
193
+ ]);
194
+
195
+ function App() {
196
+ return (
197
+ <Grid
198
+ signal={customers}
199
+ labels={{ name: "Customer", balance: "Balance ($)" }}
200
+ />
201
+ );
202
+ }
203
+
204
+ // Grid updates automatically
205
+ customers.set([...customers.peek(), { name: "Tane", balance: 0 }]);
206
+ ```
207
+
208
+ ## useSignal
209
+
210
+ Hook to subscribe any React component to a Signal.
211
+
212
+ ```tsx
213
+ import { useSignal } from "@azmr/ui";
214
+
215
+ function Counter({ count }: { count: Signal<number> }) {
216
+ const value = useSignal(count); // push-based, no polling
217
+ return <span>{value}</span>;
218
+ }
219
+ ```
220
+
221
+ ## Grid Props
222
+
223
+ | Prop | Type | Description |
224
+ |---|---|---|
225
+ | `signal` | `Signal<T[]>` | Reactive data source |
226
+ | `columns` | `(keyof T)[]` | Optional column order |
227
+ | `labels` | `Partial<Record<keyof T, string>>` | Optional column label overrides |
228
+ | `emptyMessage` | `string` | Message shown when Signal is empty |
229
+
230
+ All cell values are rendered through React's JSX escaping — `dangerouslySetInnerHTML` is never used.
231
+
232
+ ## Requirements
233
+
234
+ - Node.js ≥ 18
235
+ - React 18 or 19
236
+ - TypeScript ≥ 5 (types included)
237
+
238
+ ## Documentation
239
+
240
+ Full docs at [docs.azmara.io](https://docs.azmara.io)
241
+
242
+ ## License
243
+
244
+ MIT © [Azmara Labs](https://azmara.io)
@@ -0,0 +1,58 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, HTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * Up to two initials from a name: the first character of the first two words, uppercased.
6
+ * Counts whole characters (so a surrogate pair or an emoji is never cut in half).
7
+ */
8
+ declare function getInitials(name: string): string;
9
+ declare const SIZE: {
10
+ readonly sm: "size-6 text-[0.625rem]";
11
+ readonly md: "size-10 text-sm";
12
+ readonly lg: "size-14 text-base";
13
+ readonly xl: "size-20 text-xl";
14
+ };
15
+ declare const SHAPE: {
16
+ readonly circle: "rounded-full";
17
+ readonly square: "rounded-md";
18
+ };
19
+ type AvatarSize = keyof typeof SIZE;
20
+ interface AvatarOwnProps {
21
+ /** The image. When it fails to load (or is absent) the fallback is shown. */
22
+ src?: string;
23
+ size?: AvatarSize;
24
+ shape?: keyof typeof SHAPE;
25
+ /** What to show instead of the image. Default: the initials of `name`, or a placeholder. */
26
+ fallback?: ReactNode;
27
+ /** Wait this long before showing the fallback, to avoid a flash while the image loads. */
28
+ delayMs?: number;
29
+ className?: string;
30
+ }
31
+ /** An avatar must say who it is, or be explicitly decorative. */
32
+ type Identity = {
33
+ name: string;
34
+ alt?: string;
35
+ decorative?: boolean;
36
+ } | {
37
+ alt: string;
38
+ name?: string;
39
+ decorative?: boolean;
40
+ } | {
41
+ decorative: true;
42
+ name?: string;
43
+ alt?: string;
44
+ };
45
+ type AvatarProps = AvatarOwnProps & Identity & Omit<HTMLAttributes<HTMLSpanElement>, keyof AvatarOwnProps | "role" | "aria-label">;
46
+ /** A person or entity as an image with initials as the fallback. Requires a name, alt or `decorative`. */
47
+ declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
48
+ interface AvatarGroupProps extends HTMLAttributes<HTMLDivElement> {
49
+ /** Show at most this many avatars, then a `+N` for the rest. */
50
+ max?: number;
51
+ /** The accessible name of the `+N` avatar. Default `N more`. */
52
+ overflowLabel?: (hidden: number) => string;
53
+ children?: ReactNode;
54
+ }
55
+ /** Overlapping avatars with a labelled `+N` for the rest. */
56
+ declare const AvatarGroup: react.ForwardRefExoticComponent<AvatarGroupProps & react.RefAttributes<HTMLDivElement>>;
57
+
58
+ export { Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, type AvatarSize, getInitials };
package/dist/avatar.js ADDED
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import {
3
+ cn
4
+ } from "./chunk-2TQSHVXA.js";
5
+
6
+ // src/components/avatar/avatar.tsx
7
+ import { Children, forwardRef } from "react";
8
+
9
+ // src/primitives/avatar.ts
10
+ import { Avatar } from "@base-ui/react/avatar";
11
+
12
+ // src/components/avatar/avatar.tsx
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+ function getInitials(name) {
15
+ const words = name.trim().split(/\s+/).filter(Boolean);
16
+ const first = (word) => Array.from(word)[0] ?? "";
17
+ return words.slice(0, 2).map(first).join("").toLocaleUpperCase();
18
+ }
19
+ var SIZE = {
20
+ sm: "size-6 text-[0.625rem]",
21
+ md: "size-10 text-sm",
22
+ lg: "size-14 text-base",
23
+ xl: "size-20 text-xl"
24
+ };
25
+ var SHAPE = { circle: "rounded-full", square: "rounded-md" };
26
+ var AvatarImpl = forwardRef(function Avatar2({
27
+ src,
28
+ name,
29
+ alt,
30
+ decorative = false,
31
+ size = "md",
32
+ shape = "circle",
33
+ fallback,
34
+ delayMs,
35
+ className,
36
+ ...props
37
+ }, ref) {
38
+ const label = alt ?? name;
39
+ return /* @__PURE__ */ jsxs(
40
+ Avatar.Root,
41
+ {
42
+ ref,
43
+ ...decorative ? { "aria-hidden": true } : { role: "img", "aria-label": label },
44
+ className: cn(
45
+ "relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-muted font-medium text-muted-foreground",
46
+ SIZE[size],
47
+ SHAPE[shape],
48
+ className
49
+ ),
50
+ ...props,
51
+ children: [
52
+ src && /* @__PURE__ */ jsx(Avatar.Image, { src, alt: "", className: "aspect-square size-full object-cover" }),
53
+ /* @__PURE__ */ jsx(Avatar.Fallback, { delay: delayMs, "aria-hidden": "true", children: fallback ?? (name ? getInitials(name) : "?") })
54
+ ]
55
+ }
56
+ );
57
+ });
58
+ var Avatar3 = AvatarImpl;
59
+ var AvatarGroup = forwardRef(function AvatarGroup2({ max, overflowLabel = (n) => `${n} more`, className, children, ...props }, ref) {
60
+ const all = Children.toArray(children);
61
+ const visible = max === void 0 ? all : all.slice(0, Math.max(0, max));
62
+ const hidden = all.length - visible.length;
63
+ return (
64
+ // biome-ignore lint/a11y/useSemanticElements: a fieldset groups form controls; this groups images
65
+ /* @__PURE__ */ jsxs(
66
+ "div",
67
+ {
68
+ ref,
69
+ role: "group",
70
+ className: cn("flex -space-x-2 [&>*]:ring-2 [&>*]:ring-background", className),
71
+ ...props,
72
+ children: [
73
+ visible,
74
+ hidden > 0 && /* @__PURE__ */ jsx(Avatar3, { alt: overflowLabel(hidden), fallback: `+${hidden}` })
75
+ ]
76
+ }
77
+ )
78
+ );
79
+ });
80
+ export {
81
+ Avatar3 as Avatar,
82
+ AvatarGroup,
83
+ getInitials
84
+ };
@@ -0,0 +1,47 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode, LiHTMLAttributes, AnchorHTMLAttributes, ReactElement, OlHTMLAttributes } from 'react';
3
+
4
+ interface BreadcrumbProps extends HTMLAttributes<HTMLElement> {
5
+ /** The landmark's name. Default "Breadcrumb"; change it for translation, or with two trails on a page. */
6
+ label?: string;
7
+ }
8
+ /** A navigation landmark holding the trail. Server-safe. */
9
+ declare const Breadcrumb: react.ForwardRefExoticComponent<BreadcrumbProps & react.RefAttributes<HTMLElement>>;
10
+ declare const BreadcrumbList: react.ForwardRefExoticComponent<OlHTMLAttributes<HTMLOListElement> & react.RefAttributes<HTMLOListElement>>;
11
+ declare const BreadcrumbItem: react.ForwardRefExoticComponent<LiHTMLAttributes<HTMLLIElement> & react.RefAttributes<HTMLLIElement>>;
12
+ interface BreadcrumbLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
13
+ /** Render your router's `Link` instead of an `<a>`: `render={<NextLink href="/x" />}`. */
14
+ render?: ReactElement;
15
+ }
16
+ declare const BreadcrumbLink: react.ForwardRefExoticComponent<BreadcrumbLinkProps & react.RefAttributes<HTMLAnchorElement>>;
17
+ /** Where you are. Plain text, not a link, marked `aria-current="page"`. */
18
+ declare const BreadcrumbPage: react.ForwardRefExoticComponent<HTMLAttributes<HTMLSpanElement> & react.RefAttributes<HTMLSpanElement>>;
19
+ /** The mark between items. Decorative: hidden from assistive technology. A chevron by default. */
20
+ declare function BreadcrumbSeparator({ children, className, ...props }: HTMLAttributes<HTMLLIElement>): react.JSX.Element;
21
+ /** Stands for items left out of a long trail. For a menu of the hidden pages, wrap it in a `DropdownMenu`. */
22
+ declare function BreadcrumbEllipsis({ label, className, ...props }: HTMLAttributes<HTMLSpanElement> & {
23
+ label?: string;
24
+ }): react.JSX.Element;
25
+ interface BreadcrumbEntry {
26
+ label: ReactNode;
27
+ /** Where the item links. The last item is always shown as the current page, whatever it has. */
28
+ href?: string;
29
+ }
30
+ interface BreadcrumbsProps extends Omit<BreadcrumbProps, "children"> {
31
+ items: readonly BreadcrumbEntry[];
32
+ /**
33
+ * Collapse a longer trail: keep the first item, an ellipsis, and the last items, `maxItems` in all
34
+ * (the ellipsis counts as one). Default: never collapse.
35
+ */
36
+ maxItems?: number;
37
+ /** Replace the chevron between items. */
38
+ separator?: ReactNode;
39
+ /** Render your router's `Link`: `renderLink={(href) => <NextLink href={href} />}`. */
40
+ renderLink?: (href: string) => ReactElement;
41
+ /** The ellipsis's spoken name. Default "More pages". */
42
+ ellipsisLabel?: string;
43
+ }
44
+ /** A trail from data. Built from the parts above; use those directly for custom layouts. */
45
+ declare function Breadcrumbs({ items, maxItems, separator, renderLink, ellipsisLabel, ...props }: BreadcrumbsProps): react.JSX.Element | null;
46
+
47
+ export { Breadcrumb, BreadcrumbEllipsis, type BreadcrumbEntry, BreadcrumbItem, BreadcrumbLink, type BreadcrumbLinkProps, BreadcrumbList, BreadcrumbPage, type BreadcrumbProps, BreadcrumbSeparator, Breadcrumbs, type BreadcrumbsProps };
@@ -0,0 +1,120 @@
1
+ import {
2
+ renderElement
3
+ } from "./chunk-LTOLV67A.js";
4
+ import {
5
+ Icon
6
+ } from "./chunk-Q4PPM2SU.js";
7
+ import {
8
+ cn
9
+ } from "./chunk-7EOSDFS3.js";
10
+
11
+ // src/components/breadcrumb/breadcrumb.tsx
12
+ import { ChevronRight, Ellipsis } from "lucide-react";
13
+ import {
14
+ Fragment,
15
+ forwardRef
16
+ } from "react";
17
+ import { jsx, jsxs } from "react/jsx-runtime";
18
+ var Breadcrumb = forwardRef(function Breadcrumb2({ label = "Breadcrumb", ...props }, ref) {
19
+ return /* @__PURE__ */ jsx("nav", { ref, "aria-label": label, ...props });
20
+ });
21
+ var BreadcrumbList = forwardRef(
22
+ function BreadcrumbList2({ className, ...props }, ref) {
23
+ return /* @__PURE__ */ jsx(
24
+ "ol",
25
+ {
26
+ ref,
27
+ className: cn(
28
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground",
29
+ className
30
+ ),
31
+ ...props
32
+ }
33
+ );
34
+ }
35
+ );
36
+ var BreadcrumbItem = forwardRef(
37
+ function BreadcrumbItem2({ className, ...props }, ref) {
38
+ return /* @__PURE__ */ jsx("li", { ref, className: cn("inline-flex items-center gap-1.5", className), ...props });
39
+ }
40
+ );
41
+ var LINK = "rounded-sm text-muted-foreground transition-colors duration-(--motion-duration-fast) hover:text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
42
+ var BreadcrumbLink = forwardRef(
43
+ function BreadcrumbLink2({ render, className, children, ...props }, ref) {
44
+ return renderElement(render, "a", { ref, ...props, className: cn(LINK, className) }, children);
45
+ }
46
+ );
47
+ var BreadcrumbPage = forwardRef(
48
+ function BreadcrumbPage2({ className, ...props }, ref) {
49
+ return /* @__PURE__ */ jsx(
50
+ "span",
51
+ {
52
+ ref,
53
+ "aria-current": "page",
54
+ className: cn("font-medium text-foreground", className),
55
+ ...props
56
+ }
57
+ );
58
+ }
59
+ );
60
+ function BreadcrumbSeparator({
61
+ children,
62
+ className,
63
+ ...props
64
+ }) {
65
+ return /* @__PURE__ */ jsx(
66
+ "li",
67
+ {
68
+ role: "presentation",
69
+ "aria-hidden": "true",
70
+ "data-azmr-separator": "",
71
+ className: cn("[&>svg]:size-3.5", className),
72
+ ...props,
73
+ children: children ?? /* @__PURE__ */ jsx(Icon, { icon: ChevronRight })
74
+ }
75
+ );
76
+ }
77
+ function BreadcrumbEllipsis({
78
+ label = "More pages",
79
+ className,
80
+ ...props
81
+ }) {
82
+ return /* @__PURE__ */ jsxs("span", { className: cn("flex size-9 items-center justify-center", className), ...props, children: [
83
+ /* @__PURE__ */ jsx(Icon, { icon: Ellipsis }),
84
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: label })
85
+ ] });
86
+ }
87
+ function Breadcrumbs({
88
+ items,
89
+ maxItems,
90
+ separator,
91
+ renderLink,
92
+ ellipsisLabel,
93
+ ...props
94
+ }) {
95
+ if (items.length === 0) return null;
96
+ const collapse = maxItems !== void 0 && items.length > Math.max(maxItems, 2);
97
+ const lastCount = Math.max(1, (maxItems ?? 0) - 2);
98
+ const visible = collapse ? [items[0], "ellipsis", ...items.slice(items.length - lastCount)] : [...items];
99
+ return /* @__PURE__ */ jsx(Breadcrumb, { ...props, children: /* @__PURE__ */ jsx(BreadcrumbList, { children: visible.map((entry, index) => {
100
+ const isLast = index === visible.length - 1;
101
+ return (
102
+ // The position is the identity: the list is static and never reordered.
103
+ // biome-ignore lint/suspicious/noArrayIndexKey: static, never reordered
104
+ /* @__PURE__ */ jsxs(Fragment, { children: [
105
+ /* @__PURE__ */ jsx(BreadcrumbItem, { children: entry === "ellipsis" ? /* @__PURE__ */ jsx(BreadcrumbEllipsis, { label: ellipsisLabel }) : isLast ? /* @__PURE__ */ jsx(BreadcrumbPage, { children: entry.label }) : entry.href ? /* @__PURE__ */ jsx(BreadcrumbLink, { href: entry.href, render: renderLink?.(entry.href), children: entry.label }) : /* @__PURE__ */ jsx("span", { children: entry.label }) }),
106
+ !isLast && /* @__PURE__ */ jsx(BreadcrumbSeparator, { children: separator })
107
+ ] }, index)
108
+ );
109
+ }) }) });
110
+ }
111
+ export {
112
+ Breadcrumb,
113
+ BreadcrumbEllipsis,
114
+ BreadcrumbItem,
115
+ BreadcrumbLink,
116
+ BreadcrumbList,
117
+ BreadcrumbPage,
118
+ BreadcrumbSeparator,
119
+ Breadcrumbs
120
+ };