@caqophony/ui 0.3.1 → 0.5.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 +58 -5
- package/dist/chunk-44GEHNZP.js +126 -0
- package/dist/chunk-DP5WQAPO.js +23 -0
- package/dist/chunk-I5DQCG6D.js +378 -0
- package/dist/chunk-ID7VBGJA.js +233 -0
- package/dist/{chunk-4YA6BZDJ.js → chunk-ODRGJV46.js} +3 -10
- package/dist/icon.d-DQqyySj2.d.mts +82 -0
- package/dist/index.d.mts +14 -82
- package/dist/index.js +803 -1351
- package/dist/markdown.js +2 -1
- package/dist/marketing.d.mts +444 -0
- package/dist/marketing.js +1439 -0
- package/dist/material-art.d.mts +137 -0
- package/dist/material-art.js +210 -0
- package/dist/material-tones.d.mts +23 -0
- package/dist/material-tones.js +66 -0
- package/dist/material.d.mts +615 -0
- package/dist/material.js +1066 -0
- package/package.json +19 -2
- package/styles.css +358 -3
package/README.md
CHANGED
|
@@ -48,11 +48,48 @@ Tailwind v4 colour/radius/z-index/opacity/duration utilities and registers the
|
|
|
48
48
|
package's built files as a Tailwind `@source`. Override any `--token` after the
|
|
49
49
|
import to re-theme.
|
|
50
50
|
|
|
51
|
-
Tailwind v3
|
|
52
|
-
`content` and map the same variables in `theme.extend.colors`.
|
|
51
|
+
## Styles (Tailwind v3)
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
Inside the Caqophony monorepo, an app takes the Gossamer preset and adds the
|
|
54
|
+
library to `content`. Do not map the tokens by hand in `theme.extend.colors`:
|
|
55
|
+
that left `bg-primary-solid` with no rule, or with a rule reading a variable
|
|
56
|
+
nothing declares, and neither one raises an error.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { gossamerPreset } from '@caqophony/tokens/tailwind-preset';
|
|
60
|
+
|
|
61
|
+
export default {
|
|
62
|
+
presets: [gossamerPreset],
|
|
63
|
+
darkMode: ['class'],
|
|
64
|
+
content: ['./app/**/*.{ts,tsx}', '../../packages/ui/src/**/*.{ts,tsx}'],
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The preset maps the library's tokens and declares its variables (light, and
|
|
69
|
+
dark under `.dark`), and adds the utilities and the `not-first:` variant a v3
|
|
70
|
+
theme cannot express. It only adds names: stock Tailwind keeps its values, your
|
|
71
|
+
`theme` wins over the preset, and a variable your CSS declares wins over the
|
|
72
|
+
preset's.
|
|
73
|
+
|
|
74
|
+
`pnpm exec tsx scripts/check-ui-classes-compile.ts` covers the two surfaces that
|
|
75
|
+
draw the library: the monorepo's product app on Tailwind v3, and a generated
|
|
76
|
+
app's `globals.css` plus `styles.css` on Tailwind v4, each against the classes
|
|
77
|
+
that surface can reach. It fails on a class Tailwind generates no rule for, or a
|
|
78
|
+
rule reading a variable that does not reach the element by default. It runs in
|
|
79
|
+
pre-push.
|
|
80
|
+
|
|
81
|
+
The preset is not published. Outside the monorepo, a Tailwind v3 app needs
|
|
82
|
+
`./node_modules/@caqophony/ui/dist/**/*.js` in `content` and the same mapping,
|
|
83
|
+
variables, utilities and variant in its own config, or Tailwind v4 and
|
|
84
|
+
`styles.css` above.
|
|
85
|
+
|
|
86
|
+
## Animation and prose
|
|
87
|
+
|
|
88
|
+
On Tailwind v4, `styles.css` defines the `animate-in` / `animate-out` utilities
|
|
89
|
+
the modal and tooltip use, and loads `@tailwindcss/typography` for
|
|
90
|
+
`MarkdownRenderer`, which `@caqophony/ui` depends on. There is nothing to add.
|
|
91
|
+
On Tailwind v3, install `tailwindcss-animate` and `@tailwindcss/typography` as
|
|
92
|
+
plugins.
|
|
56
93
|
|
|
57
94
|
## Use
|
|
58
95
|
|
|
@@ -60,9 +97,25 @@ Tailwind v3 consumers: add `./node_modules/@caqophony/ui/dist/**/*.js` to
|
|
|
60
97
|
import { Button, Card, PageContent } from '@caqophony/ui';
|
|
61
98
|
import { BarChart } from '@caqophony/ui/charts';
|
|
62
99
|
import { MarkdownRenderer } from '@caqophony/ui/markdown';
|
|
100
|
+
import { MarketingCta, MarketingFooter } from '@caqophony/ui/marketing';
|
|
101
|
+
import { Band, Display, Stack, Text } from '@caqophony/ui/material';
|
|
63
102
|
```
|
|
64
103
|
|
|
65
|
-
|
|
104
|
+
`@caqophony/ui/material` holds what a designed page is composed from: bands,
|
|
105
|
+
display type, layout, and flat illustration pieces. Each colour is a token, so a
|
|
106
|
+
theme change reaches all of it. Set a `Band`'s `surface` to `night` and every
|
|
107
|
+
library component inside reads on the dark ground, with no prop of its own.
|
|
108
|
+
|
|
109
|
+
`@caqophony/ui/material-tones` holds `FILL` and `STROKE`, the classes art draws
|
|
110
|
+
in. It and `@caqophony/ui/material-art` are the entries with no `'use client'`
|
|
111
|
+
directive, so a server component can use them. Every other entry point carries
|
|
112
|
+
the directive.
|
|
113
|
+
|
|
114
|
+
An app's own art is data, not code: a closed tree of SVG elements in tones, in a
|
|
115
|
+
`*.art.json` file in `art/` beside `app/`, outside the library. `Art` from
|
|
116
|
+
`@caqophony/ui/material-art` draws it, as a `Figure`'s one child, and nothing else
|
|
117
|
+
makes elements in the slot. Shade `paper` with a held step at low opacity: `paper`
|
|
118
|
+
keeps its colour in dark mode and `mist-*` does not.
|
|
66
119
|
|
|
67
120
|
## Licence
|
|
68
121
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
Icon
|
|
4
|
+
} from "./chunk-ID7VBGJA.js";
|
|
5
|
+
import {
|
|
6
|
+
cn
|
|
7
|
+
} from "./chunk-DP5WQAPO.js";
|
|
8
|
+
|
|
9
|
+
// src/empty-value.tsx
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
var EMPTY_VALUE_GLYPH = "\u2014";
|
|
12
|
+
function isEmptyValue(value) {
|
|
13
|
+
return value === null || value === void 0 || value === false || typeof value === "string" && value.trim() === "";
|
|
14
|
+
}
|
|
15
|
+
function EmptyValue({ className }) {
|
|
16
|
+
return /* @__PURE__ */ jsxs("span", { "data-empty-value": true, className: cn("text-muted-foreground", className), children: [
|
|
17
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: EMPTY_VALUE_GLYPH }),
|
|
18
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: "Not set" })
|
|
19
|
+
] });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/alert.tsx
|
|
23
|
+
import * as React from "react";
|
|
24
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
25
|
+
var INTENT_SUBTLE_STYLES = {
|
|
26
|
+
success: "bg-intent-success/10 text-intent-success-foreground border-intent-success/20",
|
|
27
|
+
warning: "bg-intent-warning/10 text-intent-warning-foreground border-intent-warning/20",
|
|
28
|
+
error: "bg-intent-error/10 text-intent-error-foreground border-intent-error/20",
|
|
29
|
+
info: "bg-intent-info/10 text-intent-info-foreground border-intent-info/20"
|
|
30
|
+
};
|
|
31
|
+
var SOLID_STYLES = {
|
|
32
|
+
success: "border-transparent bg-intent-success-solid text-intent-success-solid-fg",
|
|
33
|
+
warning: "border-transparent bg-intent-warning-solid text-intent-warning-solid-fg",
|
|
34
|
+
error: "border-transparent bg-intent-error-solid text-intent-error-solid-fg",
|
|
35
|
+
info: "border-transparent bg-intent-info-solid text-intent-info-solid-fg"
|
|
36
|
+
};
|
|
37
|
+
var OUTLINE_STYLES = {
|
|
38
|
+
success: "border-intent-success-border text-intent-success-foreground bg-background",
|
|
39
|
+
warning: "border-intent-warning-border text-intent-warning-foreground bg-background",
|
|
40
|
+
error: "border-intent-error-border text-intent-error-foreground bg-background",
|
|
41
|
+
info: "border-intent-info-border text-intent-info-foreground bg-background"
|
|
42
|
+
};
|
|
43
|
+
var ICON_MAP = {
|
|
44
|
+
success: "circle-check",
|
|
45
|
+
warning: "triangle-alert",
|
|
46
|
+
error: "circle-alert",
|
|
47
|
+
info: "info"
|
|
48
|
+
};
|
|
49
|
+
var Alert = React.forwardRef(
|
|
50
|
+
({
|
|
51
|
+
className,
|
|
52
|
+
intent = "info",
|
|
53
|
+
variant = "subtle",
|
|
54
|
+
title,
|
|
55
|
+
dismissible,
|
|
56
|
+
onDismiss,
|
|
57
|
+
children,
|
|
58
|
+
...props
|
|
59
|
+
}, ref) => {
|
|
60
|
+
const styles = variant === "solid" ? SOLID_STYLES[intent] : variant === "outline" ? OUTLINE_STYLES[intent] : INTENT_SUBTLE_STYLES[intent];
|
|
61
|
+
return /* @__PURE__ */ jsxs2(
|
|
62
|
+
"div",
|
|
63
|
+
{
|
|
64
|
+
ref,
|
|
65
|
+
role: "alert",
|
|
66
|
+
className: cn("flex gap-3 rounded-lg border p-4", styles, className),
|
|
67
|
+
...props,
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx2("span", { className: "flex-shrink-0 pt-0.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(Icon, { name: ICON_MAP[intent], size: "md" }) }),
|
|
70
|
+
/* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
|
|
71
|
+
title && /* @__PURE__ */ jsx2("p", { className: "font-semibold", children: title }),
|
|
72
|
+
children && /* @__PURE__ */ jsx2("div", { className: cn(title && "mt-1", "text-sm"), children })
|
|
73
|
+
] }),
|
|
74
|
+
dismissible && /* @__PURE__ */ jsx2(
|
|
75
|
+
"button",
|
|
76
|
+
{
|
|
77
|
+
type: "button",
|
|
78
|
+
onClick: onDismiss,
|
|
79
|
+
className: "flex-shrink-0 hover:opacity-80",
|
|
80
|
+
"aria-label": "Dismiss",
|
|
81
|
+
children: "\u2715"
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
Alert.displayName = "Alert";
|
|
90
|
+
|
|
91
|
+
// src/intent-badge.tsx
|
|
92
|
+
import * as React2 from "react";
|
|
93
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
94
|
+
var INTENT_CLASSES = {
|
|
95
|
+
neutral: "bg-muted text-muted-foreground border-border",
|
|
96
|
+
// the primary tint is lighter than the intents': `primary-text` on a 10%
|
|
97
|
+
// tint over the page grey drew 4.46:1 (W1 sheet), under AA
|
|
98
|
+
primary: "bg-primary/5 text-primary-text border-primary/30",
|
|
99
|
+
...INTENT_SUBTLE_STYLES
|
|
100
|
+
};
|
|
101
|
+
var IntentBadge = React2.forwardRef(
|
|
102
|
+
({ className, intent = "neutral", children, ...props }, ref) => /* @__PURE__ */ jsx3(
|
|
103
|
+
"span",
|
|
104
|
+
{
|
|
105
|
+
ref,
|
|
106
|
+
title: typeof children === "string" ? children : void 0,
|
|
107
|
+
className: cn(
|
|
108
|
+
"inline-flex max-w-full items-center rounded-full border px-2.5 py-0.5 text-xs font-medium",
|
|
109
|
+
INTENT_CLASSES[intent],
|
|
110
|
+
className
|
|
111
|
+
),
|
|
112
|
+
...props,
|
|
113
|
+
children: /* @__PURE__ */ jsx3("span", { className: "truncate", children: isEmptyValue(children) ? /* @__PURE__ */ jsx3(EmptyValue, {}) : children })
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
IntentBadge.displayName = "IntentBadge";
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
EMPTY_VALUE_GLYPH,
|
|
121
|
+
isEmptyValue,
|
|
122
|
+
EmptyValue,
|
|
123
|
+
INTENT_SUBTLE_STYLES,
|
|
124
|
+
Alert,
|
|
125
|
+
IntentBadge
|
|
126
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/utils.ts
|
|
4
|
+
import { clsx } from "clsx";
|
|
5
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
6
|
+
var merge = extendTailwindMerge({
|
|
7
|
+
extend: {
|
|
8
|
+
classGroups: {
|
|
9
|
+
"font-size": [
|
|
10
|
+
{
|
|
11
|
+
text: ["display-poster", "display-headline", "display-title", "lede"]
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
function cn(...inputs) {
|
|
18
|
+
return merge(clsx(inputs));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
cn
|
|
23
|
+
};
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
CONTENT_GUTTER,
|
|
4
|
+
Icon
|
|
5
|
+
} from "./chunk-ID7VBGJA.js";
|
|
6
|
+
import {
|
|
7
|
+
cn
|
|
8
|
+
} from "./chunk-DP5WQAPO.js";
|
|
9
|
+
|
|
10
|
+
// src/spinner.tsx
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
function Spinner(props) {
|
|
13
|
+
const { size = "md", className } = props;
|
|
14
|
+
const glyph = /* @__PURE__ */ jsx(Icon, { name: "loader", size, className: "animate-spin" });
|
|
15
|
+
if (props.decorative)
|
|
16
|
+
return /* @__PURE__ */ jsx(
|
|
17
|
+
"span",
|
|
18
|
+
{
|
|
19
|
+
"aria-hidden": "true",
|
|
20
|
+
className: cn("inline-flex shrink-0 text-current", className),
|
|
21
|
+
children: glyph
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
return /* @__PURE__ */ jsxs(
|
|
25
|
+
"span",
|
|
26
|
+
{
|
|
27
|
+
role: "status",
|
|
28
|
+
className: cn("inline-flex shrink-0 text-current", className),
|
|
29
|
+
children: [
|
|
30
|
+
glyph,
|
|
31
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: props.label })
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/button.tsx
|
|
38
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
39
|
+
import { cva } from "class-variance-authority";
|
|
40
|
+
import * as React from "react";
|
|
41
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
42
|
+
var buttonVariants = cva(
|
|
43
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
44
|
+
{
|
|
45
|
+
variants: {
|
|
46
|
+
variant: {
|
|
47
|
+
default: "bg-primary-solid text-primary-foreground shadow hover:bg-[color:color-mix(in_oklab,hsl(var(--primary-solid))_85%,black)]",
|
|
48
|
+
destructive: "bg-intent-error-solid text-intent-error-solid-fg shadow-sm hover:bg-[color:color-mix(in_oklab,hsl(var(--intent-error-solid))_85%,black)]",
|
|
49
|
+
outline: "border border-border dark:border-border bg-background dark:bg-background text-foreground dark:text-foreground shadow-sm hover:bg-muted dark:hover:bg-muted hover:text-foreground dark:hover:text-foreground",
|
|
50
|
+
secondary: "bg-muted dark:bg-muted text-muted-foreground dark:text-muted-foreground shadow-sm hover:bg-muted/80 dark:hover:bg-muted/80",
|
|
51
|
+
ghost: "hover:bg-muted dark:hover:bg-muted hover:text-foreground dark:hover:text-foreground",
|
|
52
|
+
link: "text-primary-text dark:text-primary-text underline-offset-4 hover:underline"
|
|
53
|
+
},
|
|
54
|
+
size: {
|
|
55
|
+
default: "h-10 px-4 py-2",
|
|
56
|
+
sm: "h-9 rounded-md px-3",
|
|
57
|
+
lg: "h-11 rounded-md px-8",
|
|
58
|
+
icon: "size-10"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
defaultVariants: {
|
|
62
|
+
variant: "default",
|
|
63
|
+
size: "default"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
var Button = React.forwardRef(
|
|
68
|
+
({
|
|
69
|
+
className,
|
|
70
|
+
variant,
|
|
71
|
+
size,
|
|
72
|
+
asChild = false,
|
|
73
|
+
loading = false,
|
|
74
|
+
loadingText,
|
|
75
|
+
children,
|
|
76
|
+
disabled,
|
|
77
|
+
...props
|
|
78
|
+
}, ref) => {
|
|
79
|
+
const Comp = asChild && !loading ? Slot : "button";
|
|
80
|
+
const isDisabled = disabled || loading;
|
|
81
|
+
return /* @__PURE__ */ jsx2(
|
|
82
|
+
Comp,
|
|
83
|
+
{
|
|
84
|
+
className: cn(buttonVariants({ variant, size, className })),
|
|
85
|
+
ref,
|
|
86
|
+
disabled: isDisabled,
|
|
87
|
+
"aria-busy": loading,
|
|
88
|
+
...props,
|
|
89
|
+
children: loading ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
90
|
+
/* @__PURE__ */ jsx2(Spinner, { decorative: true, size: "sm" }),
|
|
91
|
+
/* @__PURE__ */ jsx2("span", { children: loadingText || children })
|
|
92
|
+
] }) : children
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
Button.displayName = "Button";
|
|
98
|
+
|
|
99
|
+
// src/container.tsx
|
|
100
|
+
import * as React2 from "react";
|
|
101
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
102
|
+
var SIZE_CLASSES = {
|
|
103
|
+
sm: "max-w-content-sm",
|
|
104
|
+
md: "max-w-content-md",
|
|
105
|
+
lg: "max-w-content-lg",
|
|
106
|
+
xl: "max-w-content-xl",
|
|
107
|
+
full: "max-w-full"
|
|
108
|
+
};
|
|
109
|
+
var Container = React2.forwardRef(
|
|
110
|
+
({ className, size = "xl", padded = true, ...props }, ref) => /* @__PURE__ */ jsx3(
|
|
111
|
+
"div",
|
|
112
|
+
{
|
|
113
|
+
ref,
|
|
114
|
+
className: cn(
|
|
115
|
+
"mx-auto w-full",
|
|
116
|
+
SIZE_CLASSES[size],
|
|
117
|
+
padded && CONTENT_GUTTER,
|
|
118
|
+
className
|
|
119
|
+
),
|
|
120
|
+
...props
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
Container.displayName = "Container";
|
|
125
|
+
|
|
126
|
+
// src/modal-surface.ts
|
|
127
|
+
import * as React3 from "react";
|
|
128
|
+
import { createPortal } from "react-dom";
|
|
129
|
+
var TABBABLE = "a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]";
|
|
130
|
+
var neverChanges = () => () => void 0;
|
|
131
|
+
var tabStops = (panel) => [...panel.querySelectorAll(TABBABLE)].filter(
|
|
132
|
+
(el) => el.tabIndex >= 0 && el.getClientRects().length > 0 && getComputedStyle(el).visibility === "visible"
|
|
133
|
+
);
|
|
134
|
+
function holdFocus(panel) {
|
|
135
|
+
if (!panel.hasAttribute("tabindex")) panel.tabIndex = -1;
|
|
136
|
+
panel.focus();
|
|
137
|
+
}
|
|
138
|
+
var surfaces = [];
|
|
139
|
+
function onKey(e) {
|
|
140
|
+
const top = surfaces[surfaces.length - 1];
|
|
141
|
+
const box = top?.panel.current;
|
|
142
|
+
if (!top || !box) return;
|
|
143
|
+
const at = document.activeElement;
|
|
144
|
+
if (at && !box.contains(at) && box.compareDocumentPosition(at) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
145
|
+
return;
|
|
146
|
+
if (e.key === "Escape" && !e.defaultPrevented) top.escape.current?.();
|
|
147
|
+
if (e.key !== "Tab") return;
|
|
148
|
+
const stops = tabStops(box);
|
|
149
|
+
const first = stops[0];
|
|
150
|
+
const last = stops[stops.length - 1];
|
|
151
|
+
if (!first || !last) {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
holdFocus(box);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const inside = box.contains(at);
|
|
157
|
+
if (e.shiftKey && (at === first || !inside)) {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
last.focus();
|
|
160
|
+
} else if (!e.shiftKey && (at === last || !inside)) {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
first.focus();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function enter(surface) {
|
|
166
|
+
if (surfaces.length === 0) document.addEventListener("keydown", onKey);
|
|
167
|
+
surfaces.push(surface);
|
|
168
|
+
return () => {
|
|
169
|
+
surfaces.splice(surfaces.indexOf(surface), 1);
|
|
170
|
+
if (surfaces.length === 0) document.removeEventListener("keydown", onKey);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function usePanelRef(forwarded) {
|
|
174
|
+
const panel = React3.useRef(null);
|
|
175
|
+
const setPanel = React3.useCallback(
|
|
176
|
+
(el) => {
|
|
177
|
+
panel.current = el;
|
|
178
|
+
if (typeof forwarded === "function") forwarded(el);
|
|
179
|
+
else if (forwarded) forwarded.current = el;
|
|
180
|
+
},
|
|
181
|
+
[forwarded]
|
|
182
|
+
);
|
|
183
|
+
return [panel, setPanel];
|
|
184
|
+
}
|
|
185
|
+
function useModalSurface(open, panel, onEscape) {
|
|
186
|
+
const hydrated = React3.useSyncExternalStore(
|
|
187
|
+
neverChanges,
|
|
188
|
+
() => true,
|
|
189
|
+
() => false
|
|
190
|
+
);
|
|
191
|
+
const escape = React3.useRef(onEscape);
|
|
192
|
+
React3.useEffect(() => {
|
|
193
|
+
escape.current = onEscape;
|
|
194
|
+
});
|
|
195
|
+
const active = open && hydrated;
|
|
196
|
+
const opener = React3.useRef(null);
|
|
197
|
+
React3.useInsertionEffect(() => {
|
|
198
|
+
if (active) opener.current = document.activeElement;
|
|
199
|
+
}, [active]);
|
|
200
|
+
React3.useEffect(() => {
|
|
201
|
+
const box = panel.current;
|
|
202
|
+
if (!active || !box) return;
|
|
203
|
+
if (!box.contains(document.activeElement)) {
|
|
204
|
+
const first = tabStops(box)[0];
|
|
205
|
+
if (first) first.focus();
|
|
206
|
+
else holdFocus(box);
|
|
207
|
+
}
|
|
208
|
+
const leave = enter({ panel, escape });
|
|
209
|
+
return () => {
|
|
210
|
+
leave();
|
|
211
|
+
const nowhere = !document.activeElement || document.activeElement === document.body;
|
|
212
|
+
const back = opener.current;
|
|
213
|
+
if (nowhere && back instanceof HTMLElement) back.focus();
|
|
214
|
+
};
|
|
215
|
+
}, [active, panel]);
|
|
216
|
+
return (surface) => hydrated ? createPortal(surface, document.body) : surface;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/nav-drawer.tsx
|
|
220
|
+
import * as React4 from "react";
|
|
221
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
222
|
+
function NavDrawer({
|
|
223
|
+
label,
|
|
224
|
+
side,
|
|
225
|
+
closedFrom,
|
|
226
|
+
defaultOpen,
|
|
227
|
+
children,
|
|
228
|
+
triggerClassName,
|
|
229
|
+
surface
|
|
230
|
+
}) {
|
|
231
|
+
const [open, setOpen] = React4.useState(defaultOpen);
|
|
232
|
+
const id = React4.useId();
|
|
233
|
+
const menuButton = React4.useRef(null);
|
|
234
|
+
const overlay = React4.useRef(null);
|
|
235
|
+
const panel = React4.useRef(null);
|
|
236
|
+
const close = React4.useCallback(() => {
|
|
237
|
+
setOpen(false);
|
|
238
|
+
menuButton.current?.focus();
|
|
239
|
+
}, []);
|
|
240
|
+
const place = useModalSurface(open, panel, close);
|
|
241
|
+
React4.useEffect(() => {
|
|
242
|
+
if (!open) return;
|
|
243
|
+
const onClick = (e) => {
|
|
244
|
+
const el = panel.current;
|
|
245
|
+
if (el && e.composedPath().includes(el) && e.target instanceof Element && e.target.closest("a"))
|
|
246
|
+
close();
|
|
247
|
+
};
|
|
248
|
+
document.addEventListener("click", onClick);
|
|
249
|
+
return () => document.removeEventListener("click", onClick);
|
|
250
|
+
}, [open, close]);
|
|
251
|
+
React4.useEffect(() => {
|
|
252
|
+
if (!open || closedFrom === null) return;
|
|
253
|
+
const onResize = () => {
|
|
254
|
+
if (overlay.current && getComputedStyle(overlay.current).display === "none")
|
|
255
|
+
setOpen(false);
|
|
256
|
+
};
|
|
257
|
+
onResize();
|
|
258
|
+
window.addEventListener("resize", onResize);
|
|
259
|
+
return () => window.removeEventListener("resize", onResize);
|
|
260
|
+
}, [open, closedFrom]);
|
|
261
|
+
const left = side === "left";
|
|
262
|
+
const drawer = open ? /* @__PURE__ */ jsxs3(
|
|
263
|
+
"div",
|
|
264
|
+
{
|
|
265
|
+
ref: overlay,
|
|
266
|
+
className: cn(
|
|
267
|
+
"z-overlay fixed inset-0",
|
|
268
|
+
closedFrom === "md" && "md:hidden",
|
|
269
|
+
closedFrom === "lg" && "lg:hidden"
|
|
270
|
+
),
|
|
271
|
+
children: [
|
|
272
|
+
/* @__PURE__ */ jsx4(
|
|
273
|
+
"div",
|
|
274
|
+
{
|
|
275
|
+
"aria-hidden": "true",
|
|
276
|
+
className: "absolute inset-0 bg-foreground/40",
|
|
277
|
+
onClick: close
|
|
278
|
+
}
|
|
279
|
+
),
|
|
280
|
+
/* @__PURE__ */ jsxs3(
|
|
281
|
+
"div",
|
|
282
|
+
{
|
|
283
|
+
ref: panel,
|
|
284
|
+
id,
|
|
285
|
+
role: "dialog",
|
|
286
|
+
"aria-modal": "true",
|
|
287
|
+
"aria-label": label,
|
|
288
|
+
"data-surface": surface,
|
|
289
|
+
className: cn(
|
|
290
|
+
"absolute inset-y-0 flex w-72 max-w-[calc(100%-3rem)] flex-col overflow-y-auto bg-card shadow-lg",
|
|
291
|
+
left ? "left-0 border-r" : "right-0 border-l",
|
|
292
|
+
"border-border"
|
|
293
|
+
),
|
|
294
|
+
children: [
|
|
295
|
+
/* @__PURE__ */ jsx4(
|
|
296
|
+
"div",
|
|
297
|
+
{
|
|
298
|
+
className: cn(
|
|
299
|
+
"flex px-2 py-2",
|
|
300
|
+
left ? "justify-end" : "justify-start"
|
|
301
|
+
),
|
|
302
|
+
children: /* @__PURE__ */ jsx4(
|
|
303
|
+
Button,
|
|
304
|
+
{
|
|
305
|
+
variant: "ghost",
|
|
306
|
+
size: "icon",
|
|
307
|
+
"aria-label": "Close",
|
|
308
|
+
onClick: close,
|
|
309
|
+
children: /* @__PURE__ */ jsx4(Icon, { name: "x", size: "sm" })
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
),
|
|
314
|
+
children
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
) : null;
|
|
321
|
+
return /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
322
|
+
/* @__PURE__ */ jsx4(
|
|
323
|
+
Button,
|
|
324
|
+
{
|
|
325
|
+
ref: menuButton,
|
|
326
|
+
variant: "ghost",
|
|
327
|
+
size: "icon",
|
|
328
|
+
"aria-label": label,
|
|
329
|
+
"aria-expanded": open,
|
|
330
|
+
"aria-controls": open ? id : void 0,
|
|
331
|
+
className: triggerClassName,
|
|
332
|
+
onClick: () => setOpen(!open),
|
|
333
|
+
children: /* @__PURE__ */ jsx4(Icon, { name: "menu", size: "sm" })
|
|
334
|
+
}
|
|
335
|
+
),
|
|
336
|
+
drawer && place(drawer)
|
|
337
|
+
] });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/navbar-menu.tsx
|
|
341
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
342
|
+
function NavbarMenu({
|
|
343
|
+
search,
|
|
344
|
+
links,
|
|
345
|
+
actions,
|
|
346
|
+
defaultOpen = false,
|
|
347
|
+
triggerClassName,
|
|
348
|
+
folded = false,
|
|
349
|
+
surface
|
|
350
|
+
}) {
|
|
351
|
+
return /* @__PURE__ */ jsx5("div", { className: folded ? "ml-auto" : "ml-auto md:hidden", children: /* @__PURE__ */ jsxs4(
|
|
352
|
+
NavDrawer,
|
|
353
|
+
{
|
|
354
|
+
label: "Navigation",
|
|
355
|
+
side: "right",
|
|
356
|
+
closedFrom: folded ? null : "md",
|
|
357
|
+
defaultOpen,
|
|
358
|
+
triggerClassName,
|
|
359
|
+
surface,
|
|
360
|
+
children: [
|
|
361
|
+
search && /* @__PURE__ */ jsx5("div", { className: "px-4 pb-3", children: search }),
|
|
362
|
+
links && /* @__PURE__ */ jsx5("nav", { className: "flex flex-col px-4 pb-3 [&>a]:whitespace-normal [&>a]:py-2.5 [&>a]:text-base", children: links }),
|
|
363
|
+
actions && /* @__PURE__ */ jsx5("div", { className: "flex flex-col gap-2 border-t border-border p-4 [&>*]:w-full", children: actions })
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
) });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export {
|
|
370
|
+
Spinner,
|
|
371
|
+
buttonVariants,
|
|
372
|
+
Button,
|
|
373
|
+
usePanelRef,
|
|
374
|
+
useModalSurface,
|
|
375
|
+
NavDrawer,
|
|
376
|
+
NavbarMenu,
|
|
377
|
+
Container
|
|
378
|
+
};
|