@remit/ui 0.0.107 → 0.0.109
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/package.json +1 -1
- package/src/components/confirm-dialog.tsx +5 -9
- package/src/components/dialog-backdrop.tsx +30 -0
- package/src/components/dialog.tsx +2 -6
- package/src/components/nav-link-surface.interaction.test.ts +117 -0
- package/src/components/nav-link-surface.render.test.ts +126 -0
- package/src/components/nav-link-surface.tsx +97 -0
- package/src/index.ts +5 -0
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { DialogBackdrop } from "./dialog-backdrop.js";
|
|
3
4
|
|
|
4
5
|
export interface ConfirmDialogProps {
|
|
5
6
|
isOpen: boolean;
|
|
@@ -80,13 +81,10 @@ export const ConfirmDialog = ({
|
|
|
80
81
|
// confirmation is the decision blocking whatever is under it, and a drawer
|
|
81
82
|
// portalled to the body at the same level would cover it.
|
|
82
83
|
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
className="absolute inset-0 bg-canvas/80 backdrop-blur-sm"
|
|
88
|
-
aria-hidden="true"
|
|
89
|
-
onClick={onCancel}
|
|
84
|
+
<DialogBackdrop
|
|
85
|
+
label="Dismiss confirmation"
|
|
86
|
+
onDismiss={onCancel}
|
|
87
|
+
className="backdrop-blur-sm"
|
|
90
88
|
/>
|
|
91
89
|
|
|
92
90
|
{/* Dialog */}
|
|
@@ -99,8 +97,6 @@ export const ConfirmDialog = ({
|
|
|
99
97
|
"bg-surface border border-line rounded-sm shadow-lg",
|
|
100
98
|
"p-6",
|
|
101
99
|
)}
|
|
102
|
-
onClick={(e) => e.stopPropagation()}
|
|
103
|
-
onKeyDown={(e) => e.stopPropagation()}
|
|
104
100
|
>
|
|
105
101
|
<h2 className="text-lg font-semibold">{title}</h2>
|
|
106
102
|
{description && (
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
|
|
3
|
+
export interface DialogBackdropProps {
|
|
4
|
+
/** Names the dismissal to assistive technology, e.g. `"Close"`. */
|
|
5
|
+
label: string;
|
|
6
|
+
onDismiss: () => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The scrim behind a modal and the click-to-dismiss on it, in the shape
|
|
12
|
+
* `BottomSheet` already uses: a real button rather than a click handler on a
|
|
13
|
+
* div, out of the tab order because Escape and the dialog's own Close are the
|
|
14
|
+
* keyboard's way out. It sits as a sibling of the dialog card, never its
|
|
15
|
+
* ancestor, so a click on the card is not a click on the scrim and nothing has
|
|
16
|
+
* to stop propagation to survive.
|
|
17
|
+
*/
|
|
18
|
+
export const DialogBackdrop = ({
|
|
19
|
+
label,
|
|
20
|
+
onDismiss,
|
|
21
|
+
className,
|
|
22
|
+
}: DialogBackdropProps) => (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
aria-label={label}
|
|
26
|
+
tabIndex={-1}
|
|
27
|
+
onClick={onDismiss}
|
|
28
|
+
className={cn("absolute inset-0 bg-canvas/80", className)}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode, useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { DialogBackdrop } from "./dialog-backdrop.js";
|
|
3
4
|
|
|
4
5
|
export interface DialogProps {
|
|
5
6
|
open: boolean;
|
|
@@ -64,7 +65,6 @@ export function Dialog({
|
|
|
64
65
|
const isRight = anchor === "right";
|
|
65
66
|
|
|
66
67
|
return (
|
|
67
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: outer overlay closes dialog on click; role=presentation lets inner role=dialog own the AT semantics
|
|
68
68
|
<div
|
|
69
69
|
className={cn(
|
|
70
70
|
"fixed inset-0 z-50 flex",
|
|
@@ -74,10 +74,8 @@ export function Dialog({
|
|
|
74
74
|
? "items-stretch justify-end"
|
|
75
75
|
: "items-center justify-center px-4",
|
|
76
76
|
)}
|
|
77
|
-
role="presentation"
|
|
78
|
-
onClick={onClose}
|
|
79
77
|
>
|
|
80
|
-
<
|
|
78
|
+
<DialogBackdrop label="Dismiss dialog" onDismiss={onClose} />
|
|
81
79
|
<div
|
|
82
80
|
ref={dialogRef}
|
|
83
81
|
role="dialog"
|
|
@@ -94,8 +92,6 @@ export function Dialog({
|
|
|
94
92
|
: "w-full max-w-lg rounded-md border",
|
|
95
93
|
className,
|
|
96
94
|
)}
|
|
97
|
-
onClick={(e) => e.stopPropagation()}
|
|
98
|
-
onKeyDown={(e) => e.stopPropagation()}
|
|
99
95
|
>
|
|
100
96
|
<h2 id="dialog-title" className="sr-only">
|
|
101
97
|
{title}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounted against jsdom rather than `renderToString`, since focus and clicks
|
|
3
|
+
* need a real `document`. Enter-to-activate is the browser's own behaviour and
|
|
4
|
+
* jsdom does not implement it for anchors; `detail-surface.stories.tsx` covers
|
|
5
|
+
* that in Chromium.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { after, afterEach, before, beforeEach, describe, it } from "node:test";
|
|
9
|
+
import type { JSDOM } from "jsdom";
|
|
10
|
+
import { act, createElement, type MouseEvent } from "react";
|
|
11
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
12
|
+
import { NavLinkSurface } from "./nav-link-surface.js";
|
|
13
|
+
|
|
14
|
+
let dom: JSDOM;
|
|
15
|
+
let container: HTMLElement;
|
|
16
|
+
let root: Root;
|
|
17
|
+
|
|
18
|
+
before(async () => {
|
|
19
|
+
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
20
|
+
dom = new JSDOMCtor(
|
|
21
|
+
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
22
|
+
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
23
|
+
);
|
|
24
|
+
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
25
|
+
globalThis.document = dom.window.document;
|
|
26
|
+
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
27
|
+
globalThis.Element = dom.window.Element;
|
|
28
|
+
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
29
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
30
|
+
value: dom.window.navigator,
|
|
31
|
+
configurable: true,
|
|
32
|
+
});
|
|
33
|
+
(
|
|
34
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
35
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
after(() => {
|
|
39
|
+
dom.window.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
container = dom.window.document.getElementById(
|
|
44
|
+
"root",
|
|
45
|
+
) as unknown as HTMLElement;
|
|
46
|
+
container.innerHTML = "";
|
|
47
|
+
root = createRoot(container);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
act(() => {
|
|
52
|
+
root.unmount();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function mount(props: Parameters<typeof NavLinkSurface>[0]) {
|
|
57
|
+
act(() => {
|
|
58
|
+
root.render(createElement(NavLinkSurface, props, "Daily brief"));
|
|
59
|
+
});
|
|
60
|
+
const link = container.querySelector("a");
|
|
61
|
+
assert.ok(link, "no anchor rendered");
|
|
62
|
+
return link;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function click(target: Element, init: { metaKey?: boolean } = {}) {
|
|
66
|
+
act(() => {
|
|
67
|
+
target.dispatchEvent(
|
|
68
|
+
new dom.window.MouseEvent("click", {
|
|
69
|
+
bubbles: true,
|
|
70
|
+
cancelable: true,
|
|
71
|
+
...init,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("NavLinkSurface interaction", () => {
|
|
78
|
+
it("puts a link with an href in the tab order", () => {
|
|
79
|
+
const link = mount({ href: "/mail/brief" });
|
|
80
|
+
link.focus();
|
|
81
|
+
assert.equal(dom.window.document.activeElement, link);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("keeps a link with no href out of the tab order", () => {
|
|
85
|
+
const link = mount({});
|
|
86
|
+
link.focus();
|
|
87
|
+
assert.notEqual(dom.window.document.activeElement, link);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("passes a modified click through with its modifier intact", () => {
|
|
91
|
+
const seen: boolean[] = [];
|
|
92
|
+
const link = mount({
|
|
93
|
+
href: "/mail/brief",
|
|
94
|
+
onClick: (event: MouseEvent<HTMLAnchorElement>) => {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
seen.push(event.metaKey);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
click(link);
|
|
101
|
+
click(link, { metaKey: true });
|
|
102
|
+
|
|
103
|
+
assert.deepEqual(seen, [false, true]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("adds no click handling of its own when the caller passes none", () => {
|
|
107
|
+
const link = mount({ href: "/mail/brief" });
|
|
108
|
+
const event = new dom.window.MouseEvent("click", {
|
|
109
|
+
bubbles: true,
|
|
110
|
+
cancelable: true,
|
|
111
|
+
});
|
|
112
|
+
act(() => {
|
|
113
|
+
link.dispatchEvent(event);
|
|
114
|
+
});
|
|
115
|
+
assert.equal(event.defaultPrevented, false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
import { NavLinkSurface } from "./nav-link-surface.js";
|
|
6
|
+
|
|
7
|
+
describe("NavLinkSurface", () => {
|
|
8
|
+
it("renders a real anchor carrying the href", () => {
|
|
9
|
+
const html = renderToString(
|
|
10
|
+
createElement(NavLinkSurface, { href: "/mail/brief" }, "Daily brief"),
|
|
11
|
+
);
|
|
12
|
+
assert.match(html, /^<a /);
|
|
13
|
+
assert.match(html, /href="\/mail\/brief"/);
|
|
14
|
+
assert.match(html, /Daily brief/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders no href attribute when given none", () => {
|
|
18
|
+
const html = renderToString(
|
|
19
|
+
createElement(NavLinkSurface, {}, "Daily brief"),
|
|
20
|
+
);
|
|
21
|
+
assert.doesNotMatch(html, /href=/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("marks the current destination and styles it", () => {
|
|
25
|
+
const html = renderToString(
|
|
26
|
+
createElement(
|
|
27
|
+
NavLinkSurface,
|
|
28
|
+
{ href: "/mail/brief", current: "page" },
|
|
29
|
+
"Daily brief",
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
assert.match(html, /aria-current="page"/);
|
|
33
|
+
assert.match(html, /bg-accent-2-soft/);
|
|
34
|
+
assert.doesNotMatch(html, /text-fg-muted/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("styles a caller-supplied aria-current the same way", () => {
|
|
38
|
+
const html = renderToString(
|
|
39
|
+
createElement(
|
|
40
|
+
NavLinkSurface,
|
|
41
|
+
{ href: "/mail/brief", "aria-current": "page" },
|
|
42
|
+
"Daily brief",
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
assert.match(html, /aria-current="page"/);
|
|
46
|
+
assert.match(html, /bg-accent-2-soft/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("treats aria-current=false as not current", () => {
|
|
50
|
+
const html = renderToString(
|
|
51
|
+
createElement(
|
|
52
|
+
NavLinkSurface,
|
|
53
|
+
{ href: "/mail/brief", "aria-current": "false" },
|
|
54
|
+
"Daily brief",
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
assert.match(html, /text-fg-muted/);
|
|
58
|
+
assert.doesNotMatch(html, /bg-accent-2-soft/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("carries a focus ring on every variant", () => {
|
|
62
|
+
for (const variant of ["nav", "row", "inline"] as const) {
|
|
63
|
+
const html = renderToString(
|
|
64
|
+
createElement(NavLinkSurface, { href: "/x", variant }, "x"),
|
|
65
|
+
);
|
|
66
|
+
assert.match(html, /focus-visible:ring-2/, variant);
|
|
67
|
+
assert.match(html, /focus-visible:ring-ring/, variant);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rings a full-bleed row inside its own edge", () => {
|
|
72
|
+
const html = renderToString(
|
|
73
|
+
createElement(NavLinkSurface, { href: "/x", variant: "row" }, "x"),
|
|
74
|
+
);
|
|
75
|
+
assert.match(html, /focus-visible:ring-inset/);
|
|
76
|
+
assert.doesNotMatch(html, /ring-offset/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rings a nav entry outside its own edge", () => {
|
|
80
|
+
const html = renderToString(
|
|
81
|
+
createElement(NavLinkSurface, { href: "/x", variant: "nav" }, "x"),
|
|
82
|
+
);
|
|
83
|
+
assert.match(html, /focus-visible:ring-offset-1/);
|
|
84
|
+
assert.doesNotMatch(html, /ring-inset/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("underlines the inline variant on hover", () => {
|
|
88
|
+
const html = renderToString(
|
|
89
|
+
createElement(NavLinkSurface, { href: "/x", variant: "inline" }, "x"),
|
|
90
|
+
);
|
|
91
|
+
assert.match(html, /hover:underline/);
|
|
92
|
+
assert.match(html, /text-accent/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("lets a caller's className override the variant's own colours", () => {
|
|
96
|
+
const html = renderToString(
|
|
97
|
+
createElement(
|
|
98
|
+
NavLinkSurface,
|
|
99
|
+
{ href: "/x", className: "text-danger" },
|
|
100
|
+
"x",
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
assert.match(html, /text-danger/);
|
|
104
|
+
assert.doesNotMatch(html, /text-fg-muted/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("passes anchor attributes straight through", () => {
|
|
108
|
+
const html = renderToString(
|
|
109
|
+
createElement(
|
|
110
|
+
NavLinkSurface,
|
|
111
|
+
{
|
|
112
|
+
href: "https://example.com",
|
|
113
|
+
target: "_blank",
|
|
114
|
+
rel: "noopener noreferrer",
|
|
115
|
+
"data-testid": "nav-link",
|
|
116
|
+
title: "Example",
|
|
117
|
+
},
|
|
118
|
+
"x",
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
assert.match(html, /target="_blank"/);
|
|
122
|
+
assert.match(html, /rel="noopener noreferrer"/);
|
|
123
|
+
assert.match(html, /data-testid="nav-link"/);
|
|
124
|
+
assert.match(html, /title="Example"/);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AnchorHTMLAttributes, AriaAttributes, Ref } from "react";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
|
|
4
|
+
export type NavLinkSurfaceVariant = "nav" | "row" | "inline";
|
|
5
|
+
|
|
6
|
+
/** Every `aria-current` value ARIA defines, minus the absent case. */
|
|
7
|
+
export type NavLinkCurrent = NonNullable<AriaAttributes["aria-current"]>;
|
|
8
|
+
|
|
9
|
+
export interface NavLinkSurfaceProps
|
|
10
|
+
extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
11
|
+
variant?: NavLinkSurfaceVariant;
|
|
12
|
+
/**
|
|
13
|
+
* Which kind of "current" this destination is. Sets `aria-current` and turns
|
|
14
|
+
* on the current-state styling together, so the two cannot disagree. A
|
|
15
|
+
* caller that already sets `aria-current` — the router binding does — gets
|
|
16
|
+
* the same styling without passing this.
|
|
17
|
+
*/
|
|
18
|
+
current?: NavLinkCurrent;
|
|
19
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
20
|
+
/** `data-status` is what the router's link props mark an active link with. */
|
|
21
|
+
[key: `data-${string}`]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const OFFSET_RING =
|
|
25
|
+
"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-surface";
|
|
26
|
+
|
|
27
|
+
/** A full-bleed row has no margin to spend on an offset ring. */
|
|
28
|
+
const INSET_RING =
|
|
29
|
+
"outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset";
|
|
30
|
+
|
|
31
|
+
const variants: Record<
|
|
32
|
+
NavLinkSurfaceVariant,
|
|
33
|
+
{ base: string; rest: string; current: string }
|
|
34
|
+
> = {
|
|
35
|
+
nav: {
|
|
36
|
+
base: cn(
|
|
37
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors",
|
|
38
|
+
OFFSET_RING,
|
|
39
|
+
),
|
|
40
|
+
rest: "text-fg-muted hover:bg-surface hover:text-fg",
|
|
41
|
+
current: "bg-accent-2-soft font-medium text-accent-2",
|
|
42
|
+
},
|
|
43
|
+
row: {
|
|
44
|
+
base: cn(
|
|
45
|
+
"relative flex w-full items-start text-left transition-colors",
|
|
46
|
+
INSET_RING,
|
|
47
|
+
),
|
|
48
|
+
rest: "hover:bg-surface-sunken",
|
|
49
|
+
current: "bg-accent-2-soft",
|
|
50
|
+
},
|
|
51
|
+
inline: {
|
|
52
|
+
base: cn(
|
|
53
|
+
"rounded-sm underline-offset-2 transition-colors hover:underline",
|
|
54
|
+
OFFSET_RING,
|
|
55
|
+
),
|
|
56
|
+
rest: "text-accent hover:text-accent-hover",
|
|
57
|
+
current: "font-medium text-accent",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function isCurrent(value: NavLinkCurrent | undefined): boolean {
|
|
62
|
+
return value !== undefined && value !== false && value !== "false";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The design system's navigation link: a real `<a>`, so middle-click, cmd-click,
|
|
67
|
+
* "copy link address" and the browser's own Enter handling all work without a
|
|
68
|
+
* single handler of ours. It carries appearance and accessible state only —
|
|
69
|
+
* where the link goes is the router binding's business, and this package imports
|
|
70
|
+
* no router.
|
|
71
|
+
*/
|
|
72
|
+
export function NavLinkSurface({
|
|
73
|
+
variant = "nav",
|
|
74
|
+
current,
|
|
75
|
+
"aria-current": ariaCurrent,
|
|
76
|
+
className,
|
|
77
|
+
children,
|
|
78
|
+
ref,
|
|
79
|
+
...props
|
|
80
|
+
}: NavLinkSurfaceProps) {
|
|
81
|
+
const resolved = current ?? ariaCurrent;
|
|
82
|
+
const style = variants[variant];
|
|
83
|
+
return (
|
|
84
|
+
<a
|
|
85
|
+
ref={ref}
|
|
86
|
+
aria-current={resolved}
|
|
87
|
+
className={cn(
|
|
88
|
+
style.base,
|
|
89
|
+
isCurrent(resolved) ? style.current : style.rest,
|
|
90
|
+
className,
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</a>
|
|
96
|
+
);
|
|
97
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -167,6 +167,10 @@ export {
|
|
|
167
167
|
type DangerZoneSectionProps,
|
|
168
168
|
} from "./components/danger-zone-section.js";
|
|
169
169
|
export { Dialog, type DialogProps } from "./components/dialog.js";
|
|
170
|
+
export {
|
|
171
|
+
DialogBackdrop,
|
|
172
|
+
type DialogBackdropProps,
|
|
173
|
+
} from "./components/dialog-backdrop.js";
|
|
170
174
|
export type { EmailFrameVariant } from "./components/email-frame-css.js";
|
|
171
175
|
export {
|
|
172
176
|
FieldLabel,
|
|
@@ -362,6 +366,7 @@ export {
|
|
|
362
366
|
MobileSearchView,
|
|
363
367
|
type MobileSearchViewProps,
|
|
364
368
|
} from "./components/mobile-search-view.js";
|
|
369
|
+
export { NavLinkSurface } from "./components/nav-link-surface.js";
|
|
365
370
|
export {
|
|
366
371
|
NavSidebar,
|
|
367
372
|
type NavSidebarProps,
|