@nexub/patterns 1.1.0 → 1.2.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", 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", 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
+ };
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import {
3
+ AppLauncher
4
+ } from "../../chunk-WQX5FFVK.js";
5
+ export {
6
+ AppLauncher
7
+ };
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import {
3
+ SettingsLayout,
4
+ SettingsRow
5
+ } from "../../chunk-QSJUE24Y.js";
6
+ export {
7
+ SettingsLayout,
8
+ SettingsRow
9
+ };
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
- export { ActivityFeed, type ActivityItemProps, AuthCard, type AuthCardProps, FilterBar, type FilterBarProps, InfoList, type InfoListItemProps, type InfoListRootProps, NotificationItem, type NotificationItemProps, PlanCard, type PlanCardProps, SettingsSection, type SettingsSectionProps, StatCard, type StatCardProps, StepIndicator, type StepIndicatorProps, type StepIndicatorStep, UserCard, type UserCardProps };
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-QSJUE24Y.js";
6
+ import {
7
+ SettingsSection
8
+ } from "./chunk-OTNHVOPC.js";
2
9
  import {
3
10
  StatCard
4
11
  } from "./chunk-7GYTYFUD.js";
@@ -8,6 +15,9 @@ import {
8
15
  import {
9
16
  UserCard
10
17
  } from "./chunk-AEYLZU3F.js";
18
+ import {
19
+ AppLauncher
20
+ } from "./chunk-WQX5FFVK.js";
11
21
  import {
12
22
  ActivityFeed
13
23
  } from "./chunk-YX7UO577.js";
@@ -26,16 +36,16 @@ import {
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.1.0",
3
+ "version": "1.2.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.1.0",
40
- "@nexub/icons": "^1.1.0",
41
- "@nexub/tokens": "^1.1.0"
39
+ "@nexub/react": "^1.2.0",
40
+ "@nexub/icons": "^1.2.0",
41
+ "@nexub/tokens": "^1.2.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.1.0",
62
- "@nexub/react": "1.1.0",
63
- "@nexub/tokens": "1.1.0"
61
+ "@nexub/icons": "1.2.0",
62
+ "@nexub/react": "1.2.0",
63
+ "@nexub/tokens": "1.2.0"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsup",