@nexub/patterns 1.1.0 → 1.3.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.
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/components/SettingsLayout/SettingsLayout.tsx
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Card, IconButton, Sheet, Sidebar, TopNav, cn } from "@nexub/react";
|
|
6
|
+
import { Menu } from "@nexub/icons";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var isPlainClick = (e) => e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey;
|
|
9
|
+
function SectionNav({
|
|
10
|
+
items,
|
|
11
|
+
activeId,
|
|
12
|
+
onChoose,
|
|
13
|
+
label,
|
|
14
|
+
className
|
|
15
|
+
}) {
|
|
16
|
+
return /* @__PURE__ */ jsx(Sidebar.Root, { "aria-label": label, className, children: /* @__PURE__ */ jsx(Sidebar.Group, { children: items.map((item) => /* @__PURE__ */ jsx(
|
|
17
|
+
Sidebar.Item,
|
|
18
|
+
{
|
|
19
|
+
href: item.href,
|
|
20
|
+
icon: item.icon,
|
|
21
|
+
active: item.id === activeId,
|
|
22
|
+
onClick: (e) => onChoose(e, item),
|
|
23
|
+
children: item.label
|
|
24
|
+
},
|
|
25
|
+
item.id
|
|
26
|
+
)) }) });
|
|
27
|
+
}
|
|
28
|
+
var Root = React.forwardRef(
|
|
29
|
+
({ items, activeId, onSelect, logo, actions, title, description, navLabel = "Settings", className, children, ...props }, ref) => {
|
|
30
|
+
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
31
|
+
const onChoose = (e, item) => {
|
|
32
|
+
if (!isPlainClick(e)) return;
|
|
33
|
+
if (onSelect) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
onSelect(item);
|
|
36
|
+
}
|
|
37
|
+
setMenuOpen(false);
|
|
38
|
+
};
|
|
39
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex min-h-screen flex-col bg-[var(--color-bg-default)]", className), ...props, children: [
|
|
40
|
+
/* @__PURE__ */ jsxs(TopNav.Root, { children: [
|
|
41
|
+
/* @__PURE__ */ jsx(IconButton, { label: `Open ${navLabel.toLowerCase()} menu`, className: "-ml-2 md:hidden", onClick: () => setMenuOpen(true), children: /* @__PURE__ */ jsx(Menu, { size: "md", "aria-hidden": "true" }) }),
|
|
42
|
+
logo && /* @__PURE__ */ jsx(TopNav.Logo, { className: "min-w-0 shrink", children: logo }),
|
|
43
|
+
actions && /* @__PURE__ */ jsx(TopNav.Actions, { children: actions })
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-1", children: [
|
|
46
|
+
/* @__PURE__ */ jsx(
|
|
47
|
+
SectionNav,
|
|
48
|
+
{
|
|
49
|
+
items,
|
|
50
|
+
activeId,
|
|
51
|
+
onChoose,
|
|
52
|
+
label: navLabel,
|
|
53
|
+
className: "sticky top-[var(--size-nav-header,64px)] hidden h-[calc(100vh-var(--size-nav-header,64px))] shrink-0 md:flex"
|
|
54
|
+
}
|
|
55
|
+
),
|
|
56
|
+
/* @__PURE__ */ jsx("main", { className: "min-w-0 flex-1 px-4 py-6 md:px-8 md:py-10", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-6", children: [
|
|
57
|
+
(title || description) && /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-1", children: [
|
|
58
|
+
title && /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight text-[var(--color-text-primary)]", children: title }),
|
|
59
|
+
description && /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--color-text-muted)]", children: description })
|
|
60
|
+
] }),
|
|
61
|
+
children
|
|
62
|
+
] }) })
|
|
63
|
+
] }),
|
|
64
|
+
/* @__PURE__ */ jsx(Sheet.Root, { open: menuOpen, onOpenChange: setMenuOpen, direction: "left", scrollLock: "class", children: /* @__PURE__ */ jsxs(Sheet.Portal, { children: [
|
|
65
|
+
/* @__PURE__ */ jsx(Sheet.Overlay, { className: "md:hidden" }),
|
|
66
|
+
/* @__PURE__ */ jsxs(Sheet.Content, { side: "left", "aria-describedby": void 0, className: "md:hidden", children: [
|
|
67
|
+
/* @__PURE__ */ jsx(Sheet.Title, { className: "px-5 pb-2 pt-5 text-base", children: navLabel }),
|
|
68
|
+
/* @__PURE__ */ jsx(SectionNav, { items, activeId, onChoose, label: navLabel, className: "w-full border-r-0" })
|
|
69
|
+
] })
|
|
70
|
+
] }) })
|
|
71
|
+
] });
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
Root.displayName = "SettingsLayout";
|
|
75
|
+
var Section = React.forwardRef(
|
|
76
|
+
({ title, description, className, children, ...props }, ref) => /* @__PURE__ */ jsx(Card, { asChild: true, className: cn("px-5 py-1", className), children: /* @__PURE__ */ jsxs("section", { ref, ...props, children: [
|
|
77
|
+
(title || description) && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 border-b border-[var(--color-border-default)] pb-4 pt-4", children: [
|
|
78
|
+
title && /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-[var(--color-text-primary)]", children: title }),
|
|
79
|
+
description && /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--color-text-muted)]", children: description })
|
|
80
|
+
] }),
|
|
81
|
+
children
|
|
82
|
+
] }) })
|
|
83
|
+
);
|
|
84
|
+
Section.displayName = "SettingsLayout.Section";
|
|
85
|
+
var SettingsRow = React.forwardRef(
|
|
86
|
+
({ label, description, value, action, className, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
87
|
+
"div",
|
|
88
|
+
{
|
|
89
|
+
ref,
|
|
90
|
+
className: cn(
|
|
91
|
+
"flex flex-col gap-2 border-t border-[var(--color-border-default)] py-4 first:border-t-0 sm:flex-row sm:items-center sm:gap-4",
|
|
92
|
+
className
|
|
93
|
+
),
|
|
94
|
+
...props,
|
|
95
|
+
children: [
|
|
96
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5 sm:w-1/3 sm:shrink-0", children: [
|
|
97
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--color-text-primary)]", children: label }),
|
|
98
|
+
description && /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--color-text-muted)]", children: description })
|
|
99
|
+
] }),
|
|
100
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 break-words text-sm text-[var(--color-text-secondary)]", children: value }),
|
|
101
|
+
action && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-2", children: action })
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
SettingsRow.displayName = "SettingsRow";
|
|
107
|
+
var SettingsLayout = Object.assign(Root, { Section, Row: SettingsRow });
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
SettingsRow,
|
|
111
|
+
SettingsLayout
|
|
112
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/components/AppLauncher/AppLauncher.tsx
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Avatar, IconButton, Popover, cn } from "@nexub/react";
|
|
6
|
+
import { Apps } from "@nexub/icons";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
var COLUMNS = 3;
|
|
9
|
+
function AppIcon({ app }) {
|
|
10
|
+
const [failed, setFailed] = React.useState(false);
|
|
11
|
+
if (app.icon && !failed) {
|
|
12
|
+
return /* @__PURE__ */ jsx(
|
|
13
|
+
"img",
|
|
14
|
+
{
|
|
15
|
+
src: app.icon,
|
|
16
|
+
alt: "",
|
|
17
|
+
width: 40,
|
|
18
|
+
height: 40,
|
|
19
|
+
className: "h-10 w-10 rounded-xl object-cover",
|
|
20
|
+
onError: () => setFailed(true)
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return /* @__PURE__ */ jsx(Avatar, { name: app.name, size: "md", className: "rounded-xl", "aria-hidden": "true" });
|
|
25
|
+
}
|
|
26
|
+
var AppLauncher = React.forwardRef(
|
|
27
|
+
({ apps, onNavigate, surface = "solid", label = "Nexub apps", className }, ref) => {
|
|
28
|
+
const [open, setOpen] = React.useState(false);
|
|
29
|
+
const listRef = React.useRef(null);
|
|
30
|
+
if (apps.length === 0) return null;
|
|
31
|
+
const onOpenAutoFocus = (e) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
const list = listRef.current;
|
|
34
|
+
(list?.querySelector("a[aria-current]") ?? list?.querySelector("a"))?.focus();
|
|
35
|
+
};
|
|
36
|
+
const onKeyDown = (e) => {
|
|
37
|
+
const links = [...e.currentTarget.querySelectorAll("a")];
|
|
38
|
+
const i = links.indexOf(document.activeElement);
|
|
39
|
+
if (i < 0) return;
|
|
40
|
+
const step = {
|
|
41
|
+
ArrowRight: 1,
|
|
42
|
+
ArrowLeft: -1,
|
|
43
|
+
ArrowDown: COLUMNS,
|
|
44
|
+
ArrowUp: -COLUMNS,
|
|
45
|
+
Home: -i,
|
|
46
|
+
End: links.length - 1 - i
|
|
47
|
+
};
|
|
48
|
+
const delta = step[e.key];
|
|
49
|
+
if (delta === void 0) return;
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
links[Math.min(links.length - 1, Math.max(0, i + delta))]?.focus();
|
|
52
|
+
};
|
|
53
|
+
const onClick = (e, app) => {
|
|
54
|
+
const plain = e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey;
|
|
55
|
+
if (onNavigate && plain) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
onNavigate(app);
|
|
58
|
+
}
|
|
59
|
+
if (plain) setOpen(false);
|
|
60
|
+
};
|
|
61
|
+
return /* @__PURE__ */ jsxs(Popover.Root, { open, onOpenChange: setOpen, children: [
|
|
62
|
+
/* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { ref, label, className, children: /* @__PURE__ */ jsx(Apps, { size: "md", "aria-hidden": "true" }) }) }),
|
|
63
|
+
/* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
|
|
64
|
+
Popover.Content,
|
|
65
|
+
{
|
|
66
|
+
align: "end",
|
|
67
|
+
surface,
|
|
68
|
+
"aria-label": label,
|
|
69
|
+
onOpenAutoFocus,
|
|
70
|
+
className: "w-80 max-w-[calc(100vw-2rem)] p-2",
|
|
71
|
+
children: /* @__PURE__ */ jsx("ul", { ref: listRef, className: "grid grid-cols-3 gap-1", onKeyDown, children: apps.map((app) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
72
|
+
"a",
|
|
73
|
+
{
|
|
74
|
+
href: app.href,
|
|
75
|
+
"aria-current": app.current ? "page" : void 0,
|
|
76
|
+
onClick: (e) => onClick(e, app),
|
|
77
|
+
className: cn(
|
|
78
|
+
"flex flex-col items-center gap-1.5 rounded-lg px-1 py-3 text-center text-xs text-[var(--color-text-primary)] transition-colors",
|
|
79
|
+
"hover:bg-[var(--color-bg-subtle)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-ring-default)]",
|
|
80
|
+
app.current && "bg-[var(--color-bg-subtle)] font-semibold"
|
|
81
|
+
),
|
|
82
|
+
children: [
|
|
83
|
+
/* @__PURE__ */ jsx(AppIcon, { app }),
|
|
84
|
+
/* @__PURE__ */ jsx("span", { className: "w-full truncate", children: app.name })
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
) }, app.id)) })
|
|
88
|
+
}
|
|
89
|
+
) })
|
|
90
|
+
] });
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
AppLauncher.displayName = "AppLauncher";
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
AppLauncher
|
|
97
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -116,4 +116,66 @@ interface NotificationItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
116
116
|
}
|
|
117
117
|
declare const NotificationItem: React.ForwardRefExoticComponent<NotificationItemProps & React.RefAttributes<HTMLDivElement>>;
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
interface AppLauncherApp {
|
|
120
|
+
id: string;
|
|
121
|
+
name: string;
|
|
122
|
+
href: string;
|
|
123
|
+
/** Tile image URL (e.g. `@nexub/brand/app-icon-account.svg`). Falls back to initials. */
|
|
124
|
+
icon?: string;
|
|
125
|
+
/** The app the user is in now. */
|
|
126
|
+
current?: boolean;
|
|
127
|
+
}
|
|
128
|
+
interface AppLauncherProps {
|
|
129
|
+
apps: AppLauncherApp[];
|
|
130
|
+
/** Client-side routing. When set, plain clicks call this instead of following `href`. */
|
|
131
|
+
onNavigate?: (app: AppLauncherApp) => void;
|
|
132
|
+
surface?: 'solid' | 'glass';
|
|
133
|
+
/** Accessible name of the trigger and the menu. */
|
|
134
|
+
label?: string;
|
|
135
|
+
className?: string;
|
|
136
|
+
}
|
|
137
|
+
declare const AppLauncher: React.ForwardRefExoticComponent<AppLauncherProps & React.RefAttributes<HTMLButtonElement>>;
|
|
138
|
+
|
|
139
|
+
interface SettingsLayoutItem {
|
|
140
|
+
id: string;
|
|
141
|
+
label: string;
|
|
142
|
+
icon?: React.ReactNode;
|
|
143
|
+
href: string;
|
|
144
|
+
}
|
|
145
|
+
interface SettingsLayoutProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title' | 'onSelect'> {
|
|
146
|
+
/** Sections in the sidebar (and the phone menu). Each one is a page. */
|
|
147
|
+
items: SettingsLayoutItem[];
|
|
148
|
+
activeId?: string;
|
|
149
|
+
/** Router hook-in. When set, plain clicks call this instead of following `href`. */
|
|
150
|
+
onSelect?: (item: SettingsLayoutItem) => void;
|
|
151
|
+
/** Top bar, left: usually `<Logo product="Account" />`. */
|
|
152
|
+
logo?: React.ReactNode;
|
|
153
|
+
/** Top bar, right: theme switch, `<AppLauncher>`, account menu. */
|
|
154
|
+
actions?: React.ReactNode;
|
|
155
|
+
/** Page header. */
|
|
156
|
+
title?: React.ReactNode;
|
|
157
|
+
description?: React.ReactNode;
|
|
158
|
+
/** Accessible name of the section navigation. */
|
|
159
|
+
navLabel?: string;
|
|
160
|
+
}
|
|
161
|
+
interface SettingsLayoutSectionProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
|
|
162
|
+
title?: React.ReactNode;
|
|
163
|
+
description?: React.ReactNode;
|
|
164
|
+
}
|
|
165
|
+
interface SettingsRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
166
|
+
label: React.ReactNode;
|
|
167
|
+
/** Optional hint under the label. */
|
|
168
|
+
description?: React.ReactNode;
|
|
169
|
+
/** The current value, e.g. an email address. */
|
|
170
|
+
value?: React.ReactNode;
|
|
171
|
+
/** Usually a Button or Switch. */
|
|
172
|
+
action?: React.ReactNode;
|
|
173
|
+
}
|
|
174
|
+
/** Label | value | action. Stacks on phones. */
|
|
175
|
+
declare const SettingsRow: React.ForwardRefExoticComponent<SettingsRowProps & React.RefAttributes<HTMLDivElement>>;
|
|
176
|
+
declare const SettingsLayout: React.ForwardRefExoticComponent<SettingsLayoutProps & React.RefAttributes<HTMLDivElement>> & {
|
|
177
|
+
Section: React.ForwardRefExoticComponent<SettingsLayoutSectionProps & React.RefAttributes<HTMLElement>>;
|
|
178
|
+
Row: React.ForwardRefExoticComponent<SettingsRowProps & React.RefAttributes<HTMLDivElement>>;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export { ActivityFeed, type ActivityItemProps, AppLauncher, type AppLauncherApp, type AppLauncherProps, AuthCard, type AuthCardProps, FilterBar, type FilterBarProps, InfoList, type InfoListItemProps, type InfoListRootProps, NotificationItem, type NotificationItemProps, PlanCard, type PlanCardProps, SettingsLayout, type SettingsLayoutItem, type SettingsLayoutProps, type SettingsLayoutSectionProps, SettingsRow, type SettingsRowProps, SettingsSection, type SettingsSectionProps, StatCard, type StatCardProps, StepIndicator, type StepIndicatorProps, type StepIndicatorStep, UserCard, type UserCardProps };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import {
|
|
3
|
+
SettingsLayout,
|
|
4
|
+
SettingsRow
|
|
5
|
+
} from "./chunk-7UPV63EW.js";
|
|
6
|
+
import {
|
|
7
|
+
SettingsSection
|
|
8
|
+
} from "./chunk-OTNHVOPC.js";
|
|
2
9
|
import {
|
|
3
10
|
StatCard
|
|
4
11
|
} from "./chunk-7GYTYFUD.js";
|
|
@@ -11,31 +18,34 @@ import {
|
|
|
11
18
|
import {
|
|
12
19
|
ActivityFeed
|
|
13
20
|
} from "./chunk-YX7UO577.js";
|
|
21
|
+
import {
|
|
22
|
+
AppLauncher
|
|
23
|
+
} from "./chunk-WQX5FFVK.js";
|
|
14
24
|
import {
|
|
15
25
|
AuthCard
|
|
16
26
|
} from "./chunk-AOE6GQSH.js";
|
|
17
|
-
import {
|
|
18
|
-
FilterBar
|
|
19
|
-
} from "./chunk-EOA5C3Q4.js";
|
|
20
27
|
import {
|
|
21
28
|
InfoList
|
|
22
29
|
} from "./chunk-74JVLMLE.js";
|
|
30
|
+
import {
|
|
31
|
+
FilterBar
|
|
32
|
+
} from "./chunk-EOA5C3Q4.js";
|
|
23
33
|
import {
|
|
24
34
|
NotificationItem
|
|
25
35
|
} from "./chunk-Y3TNGDKC.js";
|
|
26
36
|
import {
|
|
27
37
|
PlanCard
|
|
28
38
|
} from "./chunk-QMYBXQG4.js";
|
|
29
|
-
import {
|
|
30
|
-
SettingsSection
|
|
31
|
-
} from "./chunk-OTNHVOPC.js";
|
|
32
39
|
export {
|
|
33
40
|
ActivityFeed,
|
|
41
|
+
AppLauncher,
|
|
34
42
|
AuthCard,
|
|
35
43
|
FilterBar,
|
|
36
44
|
InfoList,
|
|
37
45
|
NotificationItem,
|
|
38
46
|
PlanCard,
|
|
47
|
+
SettingsLayout,
|
|
48
|
+
SettingsRow,
|
|
39
49
|
SettingsSection,
|
|
40
50
|
StatCard,
|
|
41
51
|
StepIndicator,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexub/patterns",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Composite UI patterns for Nexub Design System — pre-built, production-ready layouts and widgets built on @nexub/react.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": ">=18 <20",
|
|
38
38
|
"react-dom": ">=18 <20",
|
|
39
|
-
"@nexub/react": "^1.
|
|
40
|
-
"@nexub/icons": "^1.
|
|
41
|
-
"@nexub/tokens": "^1.
|
|
39
|
+
"@nexub/react": "^1.3.0",
|
|
40
|
+
"@nexub/icons": "^1.3.0",
|
|
41
|
+
"@nexub/tokens": "^1.3.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@nexub/icons": {
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"tsup": "^8.3.5",
|
|
59
59
|
"typescript": "^5.6.3",
|
|
60
60
|
"vitest": "^2.1.8",
|
|
61
|
-
"@nexub/icons": "1.
|
|
62
|
-
"@nexub/react": "1.
|
|
63
|
-
"@nexub/tokens": "1.
|
|
61
|
+
"@nexub/icons": "1.3.0",
|
|
62
|
+
"@nexub/react": "1.3.0",
|
|
63
|
+
"@nexub/tokens": "1.3.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsup",
|