@lessly/ui 1.6.0 → 2.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.
package/README.md CHANGED
@@ -5,12 +5,11 @@ and Tailwind preset. This package is the shared singleton the Lessly workspace
5
5
  Module Federation host provides to federation extension remotes at runtime, and
6
6
  it is published to the public npm registry (npmjs.com).
7
7
 
8
- > **Contract:** the package name (`@lessly/ui`), its exports (`.`,
9
- > `./tailwind-preset`, `./styles.css`, `./styles-federated.css`), and its peer
10
- > dependencies (React 19,
11
- > react-router 7, optional `tailwindcss >=3.4`) are a cross-project contract. Do
12
- > not change them without coordinating the workspace host and every federation
13
- > remote.
8
+ > **Contract:** the package name, its exports and its peer dependencies — all
9
+ > declared in `package.json` — are a cross-project contract. Do not change them
10
+ > without coordinating the workspace host and every federation remote.
11
+ > `scripts/check-package-contract.mjs` refuses a change to any of them, and says
12
+ > the same thing when it does.
14
13
 
15
14
  ## What's inside
16
15
 
@@ -24,7 +23,8 @@ it is published to the public npm registry (npmjs.com).
24
23
  [`@lessly/tokens`](https://www.npmjs.com/package/@lessly/tokens) (generated
25
24
  upstream in `lessly-hub/design-system`) with `darkMode: 'class'` added, and
26
25
  `styles.css` bundles the package's `theme.css` (CSS variables, `.light`
27
- overrides, typography classes) plus this library's fonts and base layer.
26
+ overrides, typography classes) plus this library's fonts and base layer, and a
27
+ `@theme` block that declares every colour token to Tailwind v4.
28
28
  `styles-federated.css` is the same sheet without the `@font-face` rules and
29
29
  the `@layer base` block, and with its variables declared on `:where(:root)` /
30
30
  `:where(.light)` so a shell's own tokens outrank the remote's whatever loaded
@@ -47,6 +47,16 @@ import { Button, useTheme } from '@lessly/ui';
47
47
  import '@lessly/ui/styles.css';
48
48
  ```
49
49
 
50
+ The four components that need a router — `AppShell`, `AppSidebar`, `SidebarNav`
51
+ and `ExtensionLink` — come from `@lessly/ui/router` instead, and only that entry
52
+ imports `react-router`. The main entry never does, which is what makes
53
+ `react-router` an optional peer: an app that navigates with plain `href`s can
54
+ use the rest of the kit without installing one.
55
+
56
+ ```ts
57
+ import { AppShell, SidebarNav } from '@lessly/ui/router';
58
+ ```
59
+
50
60
  Tailwind is what turns the kit's class names into CSS. The package ships **no
51
61
  compiled utility CSS** — every consumer runs its own Tailwind over the token
52
62
  preset (`tailwindcss >= 3.4`, an optional peer dependency, required only if you
@@ -54,15 +64,13 @@ consume the preset).
54
64
 
55
65
  ### Tailwind v4 (CSS-first)
56
66
 
57
- Everything is declared in your CSS entry:
67
+ Everything is declared in your CSS entry. The minimum is three lines:
58
68
 
59
69
  ```css
60
70
  @import 'tailwindcss';
61
- /* The token preset. v4 has no `presets:` key of its own, so the preset is
62
- loaded from a JS/TS config file — without this, `bg-bg-surface`,
63
- `text-text-primary` and `gap-gutter` emit nothing at all. */
64
- @config './tailwind.config.ts';
65
- /* Token variables, `.light` overrides, fonts, base layer. */
71
+ /* Token variables, `.light` overrides, fonts, base layer and a `@theme` block
72
+ declaring every colour token, so `bg-bg-surface`, `text-text-primary` and
73
+ `ring-border-selected` compile from this import alone. */
66
74
  @import '@lessly/ui/styles.css';
67
75
  /* Scan the library. v4 does not scan `node_modules` on its own, and the kit's
68
76
  components (Grid, Col, …) carry literal utilities (`md:col-span-5`,
@@ -72,7 +80,16 @@ Everything is declared in your CSS entry:
72
80
  @source './node_modules/@lessly/ui/dist/**/*.js';
73
81
  ```
74
82
 
75
- with the config file next to it holding nothing but the preset:
83
+ The rest of the token vocabulary is not colour and does not travel in CSS: the
84
+ spacing scale, the grid gutter (`gap-gutter`), radii, shadows, z-index and
85
+ durations come from the JS preset, and v4 has no `presets:` key of its own, so a
86
+ config file is what loads it:
87
+
88
+ ```css
89
+ @config './tailwind.config.ts';
90
+ ```
91
+
92
+ with that file holding nothing but the preset:
76
93
 
77
94
  ```ts
78
95
  import type { Config } from 'tailwindcss';
@@ -0,0 +1,433 @@
1
+ // src/lib/utils.ts
2
+ import { clsx } from "clsx";
3
+ import { extendTailwindMerge } from "tailwind-merge";
4
+ var TOKEN_FONT_WEIGHTS = ["regular", "medium", "semibold", "bold", "extrabold"];
5
+ var TOKEN_BORDER_WIDTHS = ["none", "thin", "medium", "thick", "heavy"];
6
+ var twMerge = extendTailwindMerge((config) => {
7
+ config.theme["font-weight"] = [...config.theme["font-weight"] ?? [], ...TOKEN_FONT_WEIGHTS];
8
+ for (const group of Object.keys(config.classGroups)) {
9
+ if (group !== "border-w" && !group.startsWith("border-w-")) continue;
10
+ config.classGroups[group] = [
11
+ ...config.classGroups[group] ?? [],
12
+ { [group.replace("border-w", "border")]: TOKEN_BORDER_WIDTHS }
13
+ ];
14
+ }
15
+ for (const entry of config.classGroups["outline-w"] ?? []) {
16
+ if (typeof entry !== "object" || Array.isArray(entry)) continue;
17
+ for (const [name, members] of Object.entries(entry)) {
18
+ if (Array.isArray(members)) {
19
+ entry[name] = members.filter((m) => m !== "");
20
+ }
21
+ }
22
+ }
23
+ config.conflictingClassGroups["font-size"] = [];
24
+ return config;
25
+ });
26
+ function cn(...inputs) {
27
+ return twMerge(clsx(inputs));
28
+ }
29
+
30
+ // src/components/button.tsx
31
+ import * as React from "react";
32
+ import { Slot } from "@radix-ui/react-slot";
33
+ import { cva } from "class-variance-authority";
34
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
35
+ var buttonVariants = cva(
36
+ "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
37
+ {
38
+ variants: {
39
+ // Every variant names its own `focus-visible:ring-*` colour and the base names none, because
40
+ // `buttonVariants` is exported: called raw it concatenates rather than merging, so a base
41
+ // colour would reach the element beside the variant's and stylesheet order would pick.
42
+ variant: {
43
+ default: "bg-blue-400 text-text-on-brand focus-visible:ring-border-focus hover:bg-blue-350",
44
+ /** The frame carries the danger at rest and the text stays neutral, because a hover-only
45
+ * signal is no signal on a touch device. Hover adds the wash and the red label; keyboard
46
+ * focus reddens the ring alone. */
47
+ destructive: "border border-border-danger bg-transparent text-text-primary focus-visible:ring-border-danger hover:bg-bg-danger-subtle hover:text-text-danger",
48
+ /** The borderless form of `destructive`, for an icon-only revoke repeated down a list — a row
49
+ * of framed ✕ buttons would shout. */
50
+ "destructive-ghost": "text-text-tertiary focus-visible:ring-border-focus hover:bg-bg-danger-subtle hover:text-text-danger",
51
+ /** @deprecated A resting red fill. It belongs on a confirm inside a window that has already
52
+ * stated the stakes, and the kit sanctions two: `ConfirmDialog`'s confirm and a
53
+ * `StepUpChallenge destructive` one. Use `destructive` for any trigger that sits on a page. */
54
+ "destructive-solid": "bg-bg-danger text-text-on-danger focus-visible:ring-border-focus hover:bg-bg-danger-a90",
55
+ outline: "border border-border-default bg-transparent text-text-primary focus-visible:ring-border-focus hover:bg-bg-secondary",
56
+ secondary: "bg-bg-secondary text-text-primary focus-visible:ring-border-focus hover:bg-bg-secondary-a80",
57
+ /** The frameless console tone, and the add affordance — `variant="ghost" size="xs"` with a
58
+ * leading `+`. It rests at `text-text-primary` because a button's variant differs by frame
59
+ * and fill, never by text colour; `destructive-ghost` keeps its quieter rest on purpose. */
60
+ ghost: "text-text-primary focus-visible:ring-border-focus hover:bg-bg-secondary",
61
+ link: "text-text-link underline-offset-4 focus-visible:ring-border-focus hover:underline hover:text-text-link-hover"
62
+ },
63
+ size: {
64
+ default: "h-10 px-4 py-2",
65
+ sm: "h-9 rounded-md px-3",
66
+ /** Card-header and page-header scale — the size an `+ Add` trigger rides at. */
67
+ xs: "h-7 gap-1.5 rounded-md px-2",
68
+ lg: "h-11 rounded-md px-8",
69
+ /**
70
+ * A 40px square. Predates the icon-only form and every call site in this repo overrode it
71
+ * with a `className`. Prefer passing `icon` with no children: the box then squares itself
72
+ * from whatever `size` the button already has, so an icon button lines up with the text
73
+ * button beside it. Kept because `buttonVariants({ size: 'icon' })` is used directly by
74
+ * `Pagination`.
75
+ */
76
+ icon: "h-10 w-10"
77
+ }
78
+ },
79
+ defaultVariants: {
80
+ variant: "default",
81
+ size: "default"
82
+ }
83
+ }
84
+ );
85
+ var ICON_ONLY_BOX = {
86
+ xs: "size-7 p-0",
87
+ sm: "size-9 p-0",
88
+ default: "size-10 p-0",
89
+ lg: "size-11 p-0",
90
+ icon: "size-10 p-0"
91
+ };
92
+ var ICON_GLYPH = {
93
+ xs: "[&>svg]:size-3.5",
94
+ sm: "[&>svg]:size-4",
95
+ default: "[&>svg]:size-4",
96
+ lg: "[&>svg]:size-4",
97
+ icon: "[&>svg]:size-4"
98
+ };
99
+ var Button = React.forwardRef((props, ref) => {
100
+ const {
101
+ className,
102
+ variant,
103
+ size,
104
+ asChild = false,
105
+ icon,
106
+ iconPosition = "leading",
107
+ children,
108
+ type,
109
+ ...rest
110
+ } = props;
111
+ const resolvedSize = size ?? "default";
112
+ const iconOnly = icon != null && children == null;
113
+ if (process.env.NODE_ENV !== "production") {
114
+ const label = rest["aria-label"];
115
+ const labelledBy = rest["aria-labelledby"];
116
+ if (iconOnly && !label?.trim() && !labelledBy?.trim()) {
117
+ console.warn(
118
+ "[Button] an icon-only button needs an accessible name: pass `aria-label` (#269, Guidelines/Icons & tooltips). It owes a tooltip too \u2014 wrap it in <Tooltip>."
119
+ );
120
+ }
121
+ if (asChild && icon != null) {
122
+ console.warn("[Button] `icon` is ignored with `asChild`: the consumer owns the element.");
123
+ }
124
+ }
125
+ const Comp = asChild ? Slot : "button";
126
+ const typeProps = { type: asChild ? type : type ?? "button" };
127
+ const glyph = icon == null ? null : /* @__PURE__ */ jsx(
128
+ "span",
129
+ {
130
+ className: cn("inline-flex shrink-0 items-center justify-center", ICON_GLYPH[resolvedSize]),
131
+ "aria-hidden": true,
132
+ children: icon
133
+ }
134
+ );
135
+ return /* @__PURE__ */ jsx(
136
+ Comp,
137
+ {
138
+ className: cn(
139
+ buttonVariants({ variant, size }),
140
+ iconOnly && ICON_ONLY_BOX[resolvedSize],
141
+ className
142
+ ),
143
+ ref,
144
+ ...typeProps,
145
+ ...rest,
146
+ children: asChild ? children : iconPosition === "trailing" ? /* @__PURE__ */ jsxs(Fragment, { children: [
147
+ children,
148
+ glyph
149
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
150
+ glyph,
151
+ children
152
+ ] })
153
+ }
154
+ );
155
+ });
156
+ Button.displayName = "Button";
157
+
158
+ // src/components/tooltip.tsx
159
+ import * as React2 from "react";
160
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
161
+ import { jsx as jsx2 } from "react/jsx-runtime";
162
+ var TooltipProvider = TooltipPrimitive.Provider;
163
+ var TriggerContext = React2.createContext({ trigger: null, setTrigger: () => {
164
+ } });
165
+ function directionOf(trigger) {
166
+ if (!trigger) return "ltr";
167
+ const declared = trigger.closest('[dir="rtl"], [dir="ltr"]')?.getAttribute("dir");
168
+ if (declared) return declared.toLowerCase() === "rtl" ? "rtl" : "ltr";
169
+ const computed = trigger.ownerDocument.defaultView?.getComputedStyle(trigger).direction;
170
+ return computed === "rtl" ? "rtl" : "ltr";
171
+ }
172
+ var Tooltip = ({
173
+ children,
174
+ ...props
175
+ }) => {
176
+ const [trigger, setTrigger] = React2.useState(null);
177
+ const value = React2.useMemo(() => ({ trigger, setTrigger }), [trigger]);
178
+ return /* @__PURE__ */ jsx2(TriggerContext.Provider, { value, children: /* @__PURE__ */ jsx2(TooltipPrimitive.Root, { ...props, children }) });
179
+ };
180
+ Tooltip.displayName = "Tooltip";
181
+ var TooltipTrigger = React2.forwardRef((props, ref) => {
182
+ const { setTrigger } = React2.useContext(TriggerContext);
183
+ const composedRef = React2.useCallback(
184
+ (node) => {
185
+ setTrigger(node);
186
+ if (typeof ref === "function") ref(node);
187
+ else if (ref) ref.current = node;
188
+ },
189
+ [ref, setTrigger]
190
+ );
191
+ return /* @__PURE__ */ jsx2(TooltipPrimitive.Trigger, { ref: composedRef, ...props });
192
+ });
193
+ TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
194
+ var TooltipContent = React2.forwardRef(({ className, sideOffset = 4, dir, ...props }, ref) => {
195
+ const { trigger } = React2.useContext(TriggerContext);
196
+ return /* @__PURE__ */ jsx2(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx2(
197
+ TooltipPrimitive.Content,
198
+ {
199
+ ref,
200
+ dir: dir ?? directionOf(trigger),
201
+ sideOffset,
202
+ className: cn(
203
+ "z-50 max-w-[min(var(--layout-80),var(--radix-popper-available-width))] overflow-hidden break-words rounded-md border border-border-subtle bg-bg-elevated px-3 py-1.5 text-xs text-text-primary shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
204
+ className
205
+ ),
206
+ ...props
207
+ }
208
+ ) });
209
+ });
210
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
211
+
212
+ // src/components/sheet.tsx
213
+ import * as React3 from "react";
214
+ import * as SheetPrimitive from "@radix-ui/react-dialog";
215
+ import { cva as cva2 } from "class-variance-authority";
216
+ import { X } from "lucide-react";
217
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
218
+ var Sheet = SheetPrimitive.Root;
219
+ var SheetTrigger = SheetPrimitive.Trigger;
220
+ var SheetClose = SheetPrimitive.Close;
221
+ var SheetPortal = SheetPrimitive.Portal;
222
+ var SheetOverlay = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(
223
+ SheetPrimitive.Overlay,
224
+ {
225
+ ref,
226
+ className: cn(
227
+ "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
228
+ className
229
+ ),
230
+ ...props
231
+ }
232
+ ));
233
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
234
+ var sheetVariants = cva2(
235
+ "fixed z-50 gap-4 bg-bg-elevated p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
236
+ {
237
+ variants: {
238
+ side: {
239
+ top: "inset-x-0 top-0 border-b border-border-subtle data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
240
+ bottom: "inset-x-0 bottom-0 border-t border-border-subtle data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
241
+ left: "inset-y-0 left-0 h-full w-3/4 border-r border-border-subtle data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
242
+ right: "inset-y-0 right-0 h-full w-3/4 border-l border-border-subtle data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm"
243
+ }
244
+ },
245
+ defaultVariants: {
246
+ side: "right"
247
+ }
248
+ }
249
+ );
250
+ var SheetContent = React3.forwardRef(({ side = "right", className, children, ...props }, ref) => /* @__PURE__ */ jsxs2(SheetPortal, { children: [
251
+ /* @__PURE__ */ jsx3(SheetOverlay, {}),
252
+ /* @__PURE__ */ jsxs2(
253
+ SheetPrimitive.Content,
254
+ {
255
+ ref,
256
+ className: cn(sheetVariants({ side }), className),
257
+ ...props,
258
+ children: [
259
+ children,
260
+ /* @__PURE__ */ jsxs2(SheetPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm text-text-tertiary opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-border-focus disabled:pointer-events-none", children: [
261
+ /* @__PURE__ */ jsx3(X, { className: "h-4 w-4" }),
262
+ /* @__PURE__ */ jsx3("span", { className: "sr-only", children: "Close" })
263
+ ] })
264
+ ]
265
+ }
266
+ )
267
+ ] }));
268
+ SheetContent.displayName = SheetPrimitive.Content.displayName;
269
+ var SheetHeader = ({ className, ...props }) => /* @__PURE__ */ jsx3(
270
+ "div",
271
+ {
272
+ className: cn("flex flex-col gap-1.5 text-left", className),
273
+ ...props
274
+ }
275
+ );
276
+ SheetHeader.displayName = "SheetHeader";
277
+ var SheetFooter = ({ className, ...props }) => /* @__PURE__ */ jsx3(
278
+ "div",
279
+ {
280
+ className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
281
+ ...props
282
+ }
283
+ );
284
+ SheetFooter.displayName = "SheetFooter";
285
+ var SheetTitle = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(
286
+ SheetPrimitive.Title,
287
+ {
288
+ ref,
289
+ className: cn("text-lg font-semibold text-text-primary", className),
290
+ ...props
291
+ }
292
+ ));
293
+ SheetTitle.displayName = SheetPrimitive.Title.displayName;
294
+ var SheetDescription = React3.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx3(
295
+ SheetPrimitive.Description,
296
+ {
297
+ ref,
298
+ className: cn("text-sm text-text-secondary", className),
299
+ ...props
300
+ }
301
+ ));
302
+ SheetDescription.displayName = SheetPrimitive.Description.displayName;
303
+
304
+ // src/hooks/use-mobile.ts
305
+ import { useEffect, useState as useState2 } from "react";
306
+ var MD_BREAKPOINT = 768;
307
+ function useIsMobile() {
308
+ const [isMobile, setIsMobile] = useState2(
309
+ typeof window !== "undefined" ? window.innerWidth < MD_BREAKPOINT : false
310
+ );
311
+ useEffect(() => {
312
+ const mql = window.matchMedia(`(max-width: ${String(MD_BREAKPOINT - 1)}px)`);
313
+ const handler = (e) => {
314
+ setIsMobile(e.matches);
315
+ };
316
+ mql.addEventListener("change", handler);
317
+ setIsMobile(mql.matches);
318
+ return () => {
319
+ mql.removeEventListener("change", handler);
320
+ };
321
+ }, []);
322
+ return isMobile;
323
+ }
324
+
325
+ // src/hooks/use-sidebar.ts
326
+ import { useCallback as useCallback2, useState as useState3 } from "react";
327
+ function useSidebar(options = {}) {
328
+ const { collapsed: controlled, defaultCollapsed = false, onCollapsedChange } = options;
329
+ const isControlled = controlled !== void 0;
330
+ const [internal, setInternal] = useState3(defaultCollapsed);
331
+ const collapsed = isControlled ? controlled : internal;
332
+ const setCollapsed = useCallback2(
333
+ (next) => {
334
+ if (!isControlled) setInternal(next);
335
+ onCollapsedChange?.(next);
336
+ },
337
+ [isControlled, onCollapsedChange]
338
+ );
339
+ const toggle = useCallback2(() => {
340
+ setCollapsed(!collapsed);
341
+ }, [collapsed, setCollapsed]);
342
+ return { collapsed, setCollapsed, toggle };
343
+ }
344
+
345
+ // src/components/nav-row.tsx
346
+ import * as React4 from "react";
347
+ import { Slot as Slot2 } from "@radix-ui/react-slot";
348
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
349
+ var base = "flex w-full items-center text-left text-sm transition-colors focus-visible:outline-none";
350
+ var ring = "focus-visible:ring-2 focus-visible:ring-border-focus";
351
+ var plate = "relative isolate after:pointer-events-none after:absolute after:inset-x-1.5 after:inset-y-1 after:-z-10 after:rounded-lg";
352
+ var shape = {
353
+ rail: `gap-[14px] rounded-xl px-2.5 py-2 ${ring}`,
354
+ menu: `gap-2 rounded-md px-2 py-1.5 ${ring}`,
355
+ card: `gap-3 px-4 py-3 ${plate} focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-border-focus`
356
+ };
357
+ var state = {
358
+ rail: {
359
+ idle: "font-medium text-text-secondary hover:bg-[var(--hov-bg)] hover:text-text-primary",
360
+ active: "bg-bg-sunken font-semibold text-text-primary"
361
+ },
362
+ menu: {
363
+ idle: "font-medium text-text-secondary hover:bg-[var(--menu-hov)]",
364
+ active: "font-semibold text-text-brand hover:bg-[var(--menu-hov)]"
365
+ },
366
+ // A settings line reads as a label, not as navigation, so it idles at the body weight the rest of
367
+ // the card is set in — and never marks itself current, because it isn't.
368
+ card: {
369
+ idle: "text-text-primary hover:after:bg-bg-elevated",
370
+ active: "text-text-primary hover:after:bg-bg-elevated"
371
+ }
372
+ };
373
+ var NavRow = React4.forwardRef(function NavRow2({ icon, label, sub, active = false, variant, trailing, mutedTrailing = false, hideLabel = false, asChild = false, className, children, ...props }, ref) {
374
+ const card = variant === "card";
375
+ const content = /* @__PURE__ */ jsxs3(Fragment2, { children: [
376
+ Boolean(icon) && /* @__PURE__ */ jsx4(
377
+ "span",
378
+ {
379
+ className: cn(
380
+ "flex shrink-0 items-center justify-center",
381
+ card ? "size-4 text-text-tertiary [&_svg]:size-4" : "size-5 [&_svg]:size-[18px]"
382
+ ),
383
+ "aria-hidden": "true",
384
+ children: icon
385
+ }
386
+ ),
387
+ !hideLabel && (card ? /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1", children: [
388
+ /* @__PURE__ */ jsx4("span", { className: "block truncate", children: label }),
389
+ Boolean(sub) && /* @__PURE__ */ jsx4("span", { className: "block truncate text-xs text-text-secondary", children: sub })
390
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "min-w-0 flex-1 truncate", children: label })),
391
+ Boolean(trailing) && /* @__PURE__ */ jsx4(
392
+ "span",
393
+ {
394
+ className: cn(
395
+ "flex shrink-0 items-center",
396
+ mutedTrailing ? "text-text-tertiary" : card && "text-text-secondary"
397
+ ),
398
+ children: trailing
399
+ }
400
+ ),
401
+ card && /* @__PURE__ */ jsx4("span", { className: "size-4 shrink-0", "aria-hidden": "true" })
402
+ ] });
403
+ const classes = cn(base, shape[variant], state[variant][active ? "active" : "idle"], className);
404
+ if (asChild) {
405
+ const child = React4.Children.only(children);
406
+ return /* @__PURE__ */ jsx4(Slot2, { ref, className: classes, "aria-label": hideLabel ? label : void 0, ...props, children: React4.cloneElement(child, void 0, content) });
407
+ }
408
+ return /* @__PURE__ */ jsx4("button", { ref, type: "button", className: classes, "aria-label": hideLabel ? label : void 0, ...props, children: content });
409
+ });
410
+ NavRow.displayName = "NavRow";
411
+
412
+ export {
413
+ cn,
414
+ buttonVariants,
415
+ Button,
416
+ TooltipProvider,
417
+ Tooltip,
418
+ TooltipTrigger,
419
+ TooltipContent,
420
+ Sheet,
421
+ SheetTrigger,
422
+ SheetClose,
423
+ SheetPortal,
424
+ SheetOverlay,
425
+ SheetContent,
426
+ SheetHeader,
427
+ SheetFooter,
428
+ SheetTitle,
429
+ SheetDescription,
430
+ useIsMobile,
431
+ useSidebar,
432
+ NavRow
433
+ };