@nexub/patterns 1.7.0 → 1.8.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.
@@ -14,6 +14,7 @@ import {
14
14
  Logo,
15
15
  TopNav,
16
16
  cn,
17
+ useNexubMessages,
17
18
  useTheme
18
19
  } from "@nexub/react";
19
20
  import { Building2, ChevronDown, Moon, Search, Sun } from "@nexub/icons";
@@ -26,6 +27,7 @@ function HeaderSearch({
26
27
  items,
27
28
  onSearch
28
29
  }) {
30
+ const t = useNexubMessages();
29
31
  const [open, setOpen] = React.useState(false);
30
32
  const [query, setQuery] = React.useState("");
31
33
  const [results, setResults] = React.useState([]);
@@ -88,11 +90,7 @@ function HeaderSearch({
88
90
  "aria-haspopup": "dialog"
89
91
  };
90
92
  return /* @__PURE__ */ jsxs(Fragment, { children: [
91
- /* @__PURE__ */ jsxs("span", { id: hintId, className: "sr-only", children: [
92
- "Keyboard shortcut: ",
93
- isMac ? "Command" : "Control",
94
- " K"
95
- ] }),
93
+ /* @__PURE__ */ jsx("span", { id: hintId, className: "sr-only", children: t.keyboardShortcut(`${isMac ? t.commandKey : t.controlKey} K`) }),
96
94
  /* @__PURE__ */ jsxs(
97
95
  "button",
98
96
  {
@@ -118,8 +116,8 @@ function HeaderSearch({
118
116
  children: [
119
117
  /* @__PURE__ */ jsx(CommandPalette.Input, { placeholder, value: query, onValueChange: setQuery }),
120
118
  /* @__PURE__ */ jsxs(CommandPalette.List, { children: [
121
- loading && /* @__PURE__ */ jsx("div", { role: "status", className: "px-3 py-2 text-sm text-[var(--color-on-surface-muted)]", children: "Searching\u2026" }),
122
- !loading && /* @__PURE__ */ jsx(CommandPalette.Empty, { children: failed ? "Search failed. Try again." : query ? `No results for \u201C${query}\u201D` : "Type to search" }),
119
+ loading && /* @__PURE__ */ jsx("div", { role: "status", className: "px-3 py-2 text-sm text-[var(--color-on-surface-muted)]", children: t.searching }),
120
+ !loading && /* @__PURE__ */ jsx(CommandPalette.Empty, { children: failed ? t.searchFailed : query ? t.noResultsFor(query) : t.typeToSearch }),
123
121
  groups.map((group) => /* @__PURE__ */ jsx(CommandPalette.Group, { heading: group.label, children: group.items.map((item) => /* @__PURE__ */ jsxs(
124
122
  CommandPalette.Item,
125
123
  {
@@ -149,6 +147,7 @@ function OrgSwitcher({
149
147
  currentOrgId,
150
148
  onOrgChange
151
149
  }) {
150
+ const t = useNexubMessages();
152
151
  const current = orgs.find((o) => o.id === currentOrgId) ?? orgs[0];
153
152
  if (!current) return null;
154
153
  if (orgs.length === 1) {
@@ -161,7 +160,7 @@ function OrgSwitcher({
161
160
  "button",
162
161
  {
163
162
  type: "button",
164
- "aria-label": `${current.name}, switch organization`,
163
+ "aria-label": t.switchOrganization(current.name),
165
164
  title: current.name,
166
165
  className: "nx-state-layer inline-flex h-9 min-w-0 shrink-0 items-center gap-1 rounded-shape-control px-2 text-sm sm:shrink font-medium text-[var(--color-on-surface)]",
167
166
  children: [
@@ -172,24 +171,32 @@ function OrgSwitcher({
172
171
  }
173
172
  ) }),
174
173
  /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", className: "max-w-[calc(100vw-1rem)]", children: [
175
- /* @__PURE__ */ jsx(DropdownMenu.Label, { children: "Organizations" }),
174
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: t.organizations }),
176
175
  /* @__PURE__ */ jsx(DropdownMenu.RadioGroup, { value: current.id, onValueChange: (id) => onOrgChange?.(id), children: orgs.map((org) => /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: org.id, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: org.name }) }, org.id)) })
177
176
  ] }) })
178
177
  ] })
179
178
  );
180
179
  }
181
180
  function ThemeSwitch() {
181
+ const t = useNexubMessages();
182
182
  const { resolvedTheme, setTheme } = useTheme();
183
183
  const dark = resolvedTheme === "dark";
184
- return /* @__PURE__ */ jsx(IconButton, { label: dark ? "Switch to light theme" : "Switch to dark theme", onClick: () => setTheme(dark ? "light" : "dark"), children: dark ? /* @__PURE__ */ jsx(Sun, { size: "md", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Moon, { size: "md", "aria-hidden": "true" }) });
184
+ return /* @__PURE__ */ jsx(IconButton, { label: dark ? t.switchToLightTheme : t.switchToDarkTheme, onClick: () => setTheme(dark ? "light" : "dark"), children: dark ? /* @__PURE__ */ jsx(Sun, { size: "md", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Moon, { size: "md", "aria-hidden": "true" }) });
185
185
  }
186
- var GLASS_LABELS = { clear: "Clear", balanced: "Balanced", tinted: "Tinted", off: "Solid" };
186
+ var GLASS_MODES = ["clear", "balanced", "tinted", "off"];
187
187
  function GlassItems() {
188
+ const t = useNexubMessages();
188
189
  const { glass, setGlass } = useTheme();
190
+ const labels = {
191
+ clear: t.glassClear,
192
+ balanced: t.glassBalanced,
193
+ tinted: t.glassTinted,
194
+ off: t.glassSolid
195
+ };
189
196
  return /* @__PURE__ */ jsxs(Fragment, { children: [
190
197
  /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
191
- /* @__PURE__ */ jsx(DropdownMenu.Label, { children: "Glass" }),
192
- /* @__PURE__ */ jsx(DropdownMenu.RadioGroup, { value: glass, onValueChange: (v) => setGlass(v), children: Object.keys(GLASS_LABELS).map((mode) => /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: mode, onSelect: (e) => e.preventDefault(), children: GLASS_LABELS[mode] }, mode)) })
198
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: t.glass }),
199
+ /* @__PURE__ */ jsx(DropdownMenu.RadioGroup, { value: glass, onValueChange: (v) => setGlass(v), children: GLASS_MODES.map((mode) => /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: mode, onSelect: (e) => e.preventDefault(), children: labels[mode] }, mode)) })
193
200
  ] });
194
201
  }
195
202
  function AccountMenu({
@@ -201,6 +208,7 @@ function AccountMenu({
201
208
  accountMenuItems,
202
209
  glassControl
203
210
  }) {
211
+ const t = useNexubMessages();
204
212
  const [open, setOpen] = React.useState(false);
205
213
  const [signingOut, setSigningOut] = React.useState(false);
206
214
  const signOut = (e) => {
@@ -214,23 +222,23 @@ function AccountMenu({
214
222
  };
215
223
  const link = (href, label) => /* @__PURE__ */ jsx(DropdownMenu.Item, { asChild: true, children: /* @__PURE__ */ jsx("a", { href, children: label }) });
216
224
  const legal = (privacyHref || termsHref) && /* @__PURE__ */ jsxs(Fragment, { children: [
217
- privacyHref && link(privacyHref, "Privacy policy"),
218
- termsHref && link(termsHref, "Terms of service")
225
+ privacyHref && link(privacyHref, t.privacyPolicy),
226
+ termsHref && link(termsHref, t.termsOfService)
219
227
  ] });
220
228
  const last = (accountMenuItems || onSignOut) && /* @__PURE__ */ jsxs(Fragment, { children: [
221
229
  accountMenuItems,
222
- onSignOut && /* @__PURE__ */ jsx(DropdownMenu.Item, { disabled: signingOut, onSelect: signOut, children: signingOut ? "Signing out\u2026" : "Sign out" })
230
+ onSignOut && /* @__PURE__ */ jsx(DropdownMenu.Item, { disabled: signingOut, onSelect: signOut, children: signingOut ? t.signingOut : t.signOut })
223
231
  ] });
224
232
  return (
225
233
  // modal={false}: Radix's modal menu injects a <style> scroll lock (strict CSP).
226
234
  /* @__PURE__ */ jsxs(DropdownMenu.Root, { open, onOpenChange: setOpen, modal: false, children: [
227
- /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Account: ${user.name}`, className: "ms-1 shrink-0 rounded-full", children: /* @__PURE__ */ jsx(Avatar, { name: user.name, src: user.avatarUrl, size: "sm", "aria-hidden": "true" }) }) }),
235
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx("button", { type: "button", "aria-label": t.accountMenu(user.name), className: "ms-1 shrink-0 rounded-full", children: /* @__PURE__ */ jsx(Avatar, { name: user.name, src: user.avatarUrl, size: "sm", "aria-hidden": "true" }) }) }),
228
236
  /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", className: "w-72 max-w-[calc(100vw-1rem)]", children: [
229
237
  /* @__PURE__ */ jsxs(DropdownMenu.Label, { className: "flex flex-col gap-0.5 py-2", children: [
230
238
  /* @__PURE__ */ jsx("span", { className: "truncate text-title-sm text-[var(--color-on-surface)]", children: user.name }),
231
239
  user.email && /* @__PURE__ */ jsx("span", { className: "truncate text-body-sm text-[var(--color-on-surface-muted)]", children: user.email })
232
240
  ] }),
233
- accountHref && link(accountHref, "Manage your Nexub Account"),
241
+ accountHref && link(accountHref, t.manageAccount),
234
242
  glassControl && /* @__PURE__ */ jsx(GlassItems, {}),
235
243
  legal && /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
236
244
  legal,
@@ -262,10 +270,12 @@ var AppHeader = React.forwardRef(
262
270
  accountMenuItems,
263
271
  surface = "solid",
264
272
  condense = false,
273
+ position = "fixed",
265
274
  className,
266
275
  ...props
267
276
  }, ref) => {
268
- const placeholder = searchPlaceholder ?? (productName ? `Search ${productName}` : "Search");
277
+ const t = useNexubMessages();
278
+ const placeholder = searchPlaceholder ?? (productName ? t.searchProduct(productName) : t.search);
269
279
  return /* @__PURE__ */ jsxs(Fragment, { children: [
270
280
  /* @__PURE__ */ jsx(
271
281
  TopNav.Root,
@@ -274,7 +284,13 @@ var AppHeader = React.forwardRef(
274
284
  role: "banner",
275
285
  surface,
276
286
  "data-condense": condense ? "" : void 0,
277
- className: cn("fixed inset-x-0 top-0 h-[var(--app-header-height,64px)] px-2 sm:px-4", className),
287
+ className: cn(
288
+ "h-[var(--app-header-height,64px)] px-2 sm:px-4",
289
+ // TopNav.Root is sticky by default.
290
+ position === "fixed" && "fixed inset-x-0 top-0",
291
+ position === "static" && "static",
292
+ className
293
+ ),
278
294
  ...props,
279
295
  children: /* @__PURE__ */ jsxs("div", { className: "flex w-full min-w-0 items-center gap-1 sm:gap-2", children: [
280
296
  menu && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 md:hidden", children: menu }),
@@ -293,7 +309,7 @@ var AppHeader = React.forwardRef(
293
309
  }
294
310
  ),
295
311
  /* @__PURE__ */ jsx("div", { className: "ms-auto flex shrink-0 md:ms-0 md:min-w-0 md:flex-1 md:justify-center md:px-2 lg:px-4", children: (onSearch || searchItems) && /* @__PURE__ */ jsx(HeaderSearch, { placeholder, items: searchItems, onSearch }) }),
296
- /* @__PURE__ */ jsxs("nav", { "aria-label": "Account and apps", className: "flex shrink-0 items-center gap-0.5 sm:gap-1", children: [
312
+ /* @__PURE__ */ jsxs("nav", { "aria-label": t.accountAndApps, className: "flex shrink-0 items-center gap-0.5 sm:gap-1", children: [
297
313
  orgs.length > 0 && /* @__PURE__ */ jsx(OrgSwitcher, { orgs, currentOrgId, onOrgChange }),
298
314
  showThemeSwitch && /* @__PURE__ */ jsx(ThemeSwitch, {}),
299
315
  apps && /* @__PURE__ */ jsx(AppLauncher, { apps, surface, onNavigate: onNavigate && ((app) => onNavigate(app.href)) }),
@@ -313,7 +329,7 @@ var AppHeader = React.forwardRef(
313
329
  ] })
314
330
  }
315
331
  ),
316
- /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "h-[var(--app-header-height,64px)] shrink-0" })
332
+ position === "fixed" && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "h-[var(--app-header-height,64px)] shrink-0" })
317
333
  ] });
318
334
  }
319
335
  );
@@ -11,13 +11,15 @@ var StatCard = React.forwardRef(
11
11
  value,
12
12
  delta,
13
13
  deltaType = "neutral",
14
+ sentiment,
14
15
  icon,
15
16
  description,
16
17
  animate = false,
17
18
  className,
18
19
  ...props
19
20
  }, ref) => {
20
- const deltaColor = deltaType === "increase" ? "text-[var(--color-success)]" : deltaType === "decrease" ? "text-[var(--color-error)]" : "text-[var(--color-on-surface-muted)]";
21
+ const tone = sentiment ?? (deltaType === "increase" ? "positive" : deltaType === "decrease" ? "negative" : "neutral");
22
+ const deltaColor = tone === "positive" ? "text-[var(--color-success)]" : tone === "negative" ? "text-[var(--color-error)]" : "text-[var(--color-on-surface-muted)]";
21
23
  const DeltaIcon = deltaType === "increase" ? TrendingUp : deltaType === "decrease" ? TrendingDown : Minus;
22
24
  return /* @__PURE__ */ jsxs(
23
25
  "div",
@@ -40,6 +42,7 @@ var StatCard = React.forwardRef(
40
42
  {
41
43
  className: cn("inline-flex items-center gap-1 text-xs font-medium", deltaColor),
42
44
  "data-delta-type": deltaType,
45
+ "data-sentiment": tone,
43
46
  children: [
44
47
  /* @__PURE__ */ jsx(DeltaIcon, { size: "xs", "aria-hidden": "true" }),
45
48
  delta
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  AppHeader
4
- } from "../../chunk-LAYYOVIK.js";
4
+ } from "../../chunk-6PV4NM55.js";
5
5
  import "../../chunk-H5UTW5ML.js";
6
6
  export {
7
7
  AppHeader
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  StatCard
4
- } from "../../chunk-K5EFLRKU.js";
4
+ } from "../../chunk-INKX2GYA.js";
5
5
  export {
6
6
  StatCard
7
7
  };
package/dist/index.d.ts CHANGED
@@ -29,7 +29,14 @@ interface StatCardProps extends React.HTMLAttributes<HTMLDivElement> {
29
29
  label: string;
30
30
  value: React.ReactNode;
31
31
  delta?: string;
32
+ /** Direction of the change: sets the arrow (and, by default, the colour). */
32
33
  deltaType?: 'increase' | 'decrease' | 'neutral';
34
+ /**
35
+ * Whether the change is good or bad, independent of its direction — e.g. churn going
36
+ * down is `deltaType="decrease"` + `sentiment="positive"`.
37
+ * @default derived from `deltaType` (increase → positive, decrease → negative)
38
+ */
39
+ sentiment?: 'positive' | 'negative' | 'neutral';
33
40
  icon?: React.ReactNode;
34
41
  description?: string;
35
42
  /** Roll digits when a number/string `value` changes (NumberTicker); announced politely. */
@@ -268,10 +275,17 @@ interface AppHeaderProps extends Omit<React.HTMLAttributes<HTMLElement>, 'childr
268
275
  scrollEdge?: 'soft' | 'hard';
269
276
  /** Shrink 64 → 52px and lift onto elevation 1 over the first 64px of page scroll (CSS scroll timeline; static where unsupported). */
270
277
  condense?: boolean;
278
+ /**
279
+ * `fixed` pins it to the viewport and renders a spacer after it. `sticky` and `static`
280
+ * keep it in flow (no spacer), for layouts, embeds and previews.
281
+ * @default 'fixed'
282
+ */
283
+ position?: 'fixed' | 'sticky' | 'static';
271
284
  }
272
285
  /**
273
- * The one top header every Nexub app shares. Fixed to the top; renders a
274
- * spacer of `--app-header-height` after itself, so content starts below it.
286
+ * The one top header every Nexub app shares. Fixed to the top by default and
287
+ * renders a spacer of `--app-header-height` after itself, so content starts
288
+ * below it; `position="sticky" | "static"` keeps it in flow without a spacer.
275
289
  */
276
290
  declare const AppHeader: React.ForwardRefExoticComponent<AppHeaderProps & React.RefAttributes<HTMLElement>>;
277
291
 
package/dist/index.js CHANGED
@@ -1,13 +1,11 @@
1
1
  'use client';
2
- import {
3
- StepIndicator
4
- } from "./chunk-L456OB3Q.js";
5
2
  import {
6
3
  UserCard
7
4
  } from "./chunk-7NYGYETG.js";
8
5
  import {
9
- FilterBar
10
- } from "./chunk-Z6VMCMSW.js";
6
+ ErrorSummary,
7
+ useFormErrors
8
+ } from "./chunk-RWPFCNMJ.js";
11
9
  import {
12
10
  InfoList
13
11
  } from "./chunk-WPNT5JOU.js";
@@ -29,13 +27,16 @@ import {
29
27
  } from "./chunk-U3FT7XAU.js";
30
28
  import {
31
29
  StatCard
32
- } from "./chunk-K5EFLRKU.js";
30
+ } from "./chunk-INKX2GYA.js";
31
+ import {
32
+ StepIndicator
33
+ } from "./chunk-L456OB3Q.js";
33
34
  import {
34
35
  ActivityFeed
35
36
  } from "./chunk-HYC5N5MO.js";
36
37
  import {
37
38
  AppHeader
38
- } from "./chunk-LAYYOVIK.js";
39
+ } from "./chunk-6PV4NM55.js";
39
40
  import "./chunk-VYMTTYPR.js";
40
41
  import {
41
42
  AppLauncher
@@ -51,9 +52,8 @@ import {
51
52
  Conversation
52
53
  } from "./chunk-WRP73TXG.js";
53
54
  import {
54
- ErrorSummary,
55
- useFormErrors
56
- } from "./chunk-RWPFCNMJ.js";
55
+ FilterBar
56
+ } from "./chunk-Z6VMCMSW.js";
57
57
  export {
58
58
  ActivityFeed,
59
59
  AppHeader,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexub/patterns",
3
- "version": "1.7.0",
3
+ "version": "1.8.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/icons": "^1.7.0",
40
- "@nexub/react": "^1.7.0",
41
- "@nexub/tokens": "^1.7.0"
39
+ "@nexub/icons": "^1.8.0",
40
+ "@nexub/react": "^1.8.0",
41
+ "@nexub/tokens": "^1.8.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@nexub/icons": {
@@ -59,9 +59,9 @@
59
59
  "typescript": "^5.9.3",
60
60
  "vite": "^7.3.7",
61
61
  "vitest": "^5.0.5",
62
- "@nexub/icons": "1.7.0",
63
- "@nexub/react": "1.7.0",
64
- "@nexub/tokens": "1.7.0"
62
+ "@nexub/icons": "1.8.0",
63
+ "@nexub/react": "1.8.0",
64
+ "@nexub/tokens": "1.8.0"
65
65
  },
66
66
  "scripts": {
67
67
  "build": "tsup",