@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 +31 -14
- package/dist/chunk-F5RMHSPS.js +433 -0
- package/dist/index.d.ts +34 -104
- package/dist/index.js +1300 -1896
- package/dist/router.d.ts +101 -0
- package/dist/router.js +254 -0
- package/dist/styles-federated.css +105 -0
- package/dist/styles.css +106 -1
- package/dist/tailwind-preset.js +23 -1
- package/package.json +11 -3
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
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
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
|
-
/*
|
|
62
|
-
|
|
63
|
-
`
|
|
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
|
-
|
|
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
|
+
};
|