@viliha/vui-ui 1.5.4 → 1.6.1

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/AGENT.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Using VUI in your project (AI agent guide)
2
2
 
3
- > **Audience:** developers and their AI agents **consuming** `@viliha/vui-ui`
3
+ > **Audience:** developers (and their AI agents) **consuming** `@viliha/vui-ui`
4
4
  > in a downstream app. This file ships with the npm package. To auto-load these
5
- > rules, copy the ready-made pointer to your project root it `@`-imports this
5
+ > rules, copy the ready-made pointer to your project root; it `@`-imports this
6
6
  > file, so you never duplicate the rules:
7
7
  >
8
8
  > ```bash
@@ -14,13 +14,13 @@
14
14
  > [CONTRIBUTING.md](https://github.com/myviliha/vui-starter/blob/main/CONTRIBUTING.md)
15
15
  > and [AGENTS.md](https://github.com/myviliha/vui-starter/blob/main/AGENTS.md).
16
16
 
17
- You are a frontend architect building enterprise applications on **VUI Starter**. Your job is to ship apps that are consistent, maintainable, accessible, and production-ready by leaning fully on the VUI Design System. Don't reinvent the framework build with it.
17
+ You are a frontend architect building enterprise applications on **VUI Starter**. Your job is to ship apps that are consistent, maintainable, accessible, and production-ready by leaning fully on the VUI Design System. Don't reinvent the framework; build with it.
18
18
 
19
19
  > **What you get from the package vs. what to copy.** `@viliha/vui-ui` ships the
20
20
  > component primitives (`Button`, `Input`, `Select`, `Dialog`, `Menu`,
21
21
  > `RecordView`, `ChartContainer`, `Breadcrumbs`, `theme.css`, …). App-shell
22
- > patterns referenced below `SetPageTitle`, the sidebar, and the `AuthCard*`
23
- > auth screens are **reference-app patterns**, not package exports.
22
+ > patterns referenced below (`SetPageTitle`, the sidebar, and the `AuthCard*`
23
+ > auth screens) are **reference-app patterns**, not package exports.
24
24
  > The breadcrumb *trail-building logic* (`crumbsFor` + `nav-config` +
25
25
  > `route-meta`) is also a reference-app pattern: the package exports the
26
26
  > presentational `Breadcrumbs` component; the app derives the trail from the
@@ -49,7 +49,7 @@ When several approaches would work, pick the one that aligns best with VUI.
49
49
 
50
50
  # Think Before You Build
51
51
 
52
- Before you create anything, search for what already exists a VUI component, a page pattern, a layout, a variant, or a utility. Never duplicate functionality VUI already provides.
52
+ Before you create anything, search for what already exists: a VUI component, a page pattern, a layout, a variant, or a utility. Never duplicate functionality VUI already provides.
53
53
 
54
54
  ---
55
55
 
@@ -66,7 +66,7 @@ npx @viliha/vui-ui init # interactive decision tree
66
66
 
67
67
  Files land in the consumer's repo (they own them). Questions:
68
68
  0. **Next.js or Turborepo?** (`--nextjs` / `--turbo` + `--dir <path>`, default
69
- `apps/web` turbo scaffolds into that app directory)
69
+ `apps/web`; turbo scaffolds into that app directory)
70
70
  1. **Fresh project?** (`--fresh` / `--existing`)
71
71
  2. **Pre-built theme?** (`--prebuilt` = shell + demo pages / `--theme-only` = just
72
72
  the theme wiring you configure)
@@ -78,8 +78,8 @@ dependencies** with the package manager it detects from the lockfile (npm / pnpm
78
78
 
79
79
  - **fresh + prebuilt** → full runnable app (config + shell + demo).
80
80
  - **fresh + theme-only** → just `globals.css` + `next.config` wiring; build your own.
81
- - **existing + prebuilt** → shell + demo added; config **never** overwritten
82
- prints the merge steps (`transpilePackages`, the `theme.css` import, the `@/*`
81
+ - **existing + prebuilt** → shell + demo added; config **never** overwritten, and
82
+ it prints the merge steps (`transpilePackages`, the `theme.css` import, the `@/*`
83
83
  alias, `import "./globals.css"`).
84
84
  - **existing + theme-only** → nothing copied; prints the wiring steps.
85
85
 
@@ -88,7 +88,7 @@ Other flags: `--yes` / `--force` / `--dry-run`. If you only need the components,
88
88
 
89
89
  ### Inside a Turborepo / monorepo
90
90
 
91
- **Never scaffold into the repo root** a monorepo root has no `app/` and no Next
91
+ **Never scaffold into the repo root**; a monorepo root has no `app/` and no Next
92
92
  app. Target the specific application, e.g. `apps/web`. Two equivalent ways:
93
93
 
94
94
  ```bash
@@ -115,14 +115,14 @@ dev`. The theme import, `transpilePackages`, and the `@/*` alias all belong in
115
115
 
116
116
  ### End-to-end walkthroughs
117
117
 
118
- - **New standalone app** `npx create-next-app@latest my-app --ts --tailwind
118
+ - **New standalone app**: `npx create-next-app@latest my-app --ts --tailwind
119
119
  --app --no-src-dir --use-npm`, then `cd my-app && npx @viliha/vui-ui init`
120
120
  (fresh + prebuilt). It scaffolds config + shell + demo, installs deps, and you
121
121
  run `npm run dev` → `/dashboard`.
122
- - **New app in a monorepo** scaffold your app under `apps/<name>` (e.g. with
122
+ - **New app in a monorepo**: scaffold your app under `apps/<name>` (e.g. with
123
123
  `create-next-app`), then from that app dir run `npx @viliha/vui-ui init` (or
124
124
  `--turbo --dir apps/<name>` from the root) and install deps in that app.
125
- - **Existing app** run `npx @viliha/vui-ui init --existing`. It never
125
+ - **Existing app**: run `npx @viliha/vui-ui init --existing`. It never
126
126
  overwrites your config; wire up the four things it prints (`transpilePackages`,
127
127
  the `theme.css` import, the `@/*` alias, `import "./globals.css"`). Prefer only
128
128
  the components? Use `--theme-only` (copies nothing) and follow the setup below.
@@ -152,7 +152,7 @@ Import the theme once. No extra transpilation needed.
152
152
 
153
153
  # Upgrading
154
154
 
155
- When you bump `@viliha/vui-ui`, follow these steps the package version and the
155
+ When you bump `@viliha/vui-ui`, follow these steps. The package version and the
156
156
  scaffolded files in the repo are two separate things.
157
157
 
158
158
  1. **Read the [CHANGELOG](./CHANGELOG.md) for the target version.** Note anything
@@ -165,7 +165,7 @@ scaffolded files in the repo are two separate things.
165
165
  In a monorepo, install in the specific app (or with a workspace filter, e.g.
166
166
  `pnpm --filter <app> up @viliha/vui-ui`), never at the repo root.
167
167
  3. **No rebuild needed.** The package ships TypeScript source, so there's no
168
- build/dts step just restart the dev server. If a new version adds a peer
168
+ build/dts step; just restart the dev server. If a new version adds a peer
169
169
  dependency, install it (the CHANGELOG and the "Module not found" error name it).
170
170
  4. **Scaffolded files are yours.** `init` copies the shell and demo pages into
171
171
  the repo once; upgrading the package does **not** touch them. To pull
@@ -181,7 +181,7 @@ scaffolded files in the repo are two separate things.
181
181
 
182
182
  # Design Tokens
183
183
 
184
- VUI is entirely token-driven. Never hardcode colors, spacing, radius, typography, shadows, or borders reach for the semantic design token instead. For example: `--button-primary`, `--button-primary-hover`, `--background`, `--foreground`, `--border`, `--ring`, `--chart-1`, `--sidebar-primary`.
184
+ VUI is entirely token-driven. Never hardcode colors, spacing, radius, typography, shadows, or borders; reach for the semantic design token instead. For example: `--button-primary`, `--button-primary-hover`, `--background`, `--foreground`, `--border`, `--ring`, `--chart-1`, `--sidebar-primary`.
185
185
 
186
186
  Avoid arbitrary values unless there's truly no token for the job.
187
187
 
@@ -239,11 +239,11 @@ Group content into bordered cards, and avoid deeply nested layouts. Reach for sp
239
239
 
240
240
  # Navigation
241
241
 
242
- Navigate with the three provided pieces breadcrumbs, sidebar, and top navigation. Don't build a custom navigation system unless the project explicitly demands one.
242
+ Navigate with the three provided pieces: breadcrumbs, sidebar, and top navigation. Don't build a custom navigation system unless the project explicitly demands one.
243
243
 
244
244
  ## Breadcrumbs
245
245
 
246
- One trail, **derived from the route never hand-written per page.**
246
+ One trail, **derived from the route, never hand-written per page.**
247
247
 
248
248
  - Render with the shared `Breadcrumbs` component (`@viliha/vui-ui/breadcrumbs`):
249
249
  last crumb is the current page (bold, non-interactive), earlier crumbs are
@@ -251,7 +251,7 @@ One trail, **derived from the route — never hand-written per page.**
251
251
  - Build the trail from the URL against a single nav config (copy `crumbsFor` +
252
252
  `nav-config` + `route-meta` from the reference app). Root it at **Home**
253
253
  (your dashboard route). A section/group parent has no page of its own, so its
254
- crumb links to its **first child** clicking a section lands on that
254
+ crumb links to its **first child**; clicking a section lands on that
255
255
  section's first page.
256
256
  - Ship **one** landing page, labeled "Home". Don't split "Home" and "Dashboard"
257
257
  into two routes.
@@ -261,17 +261,17 @@ To reorder or rename, edit the nav config; the trail follows automatically.
261
261
  ## Sidebar: sections & collapsible groups
262
262
 
263
263
  The sidebar is driven by one `NAV` config (copy `nav-config.ts`). There are
264
- **two grouping shapes use them, don't invent a third:**
264
+ **two grouping shapes: use them, don't invent a third:**
265
265
 
266
- - **Section** (`NavSection`, `{ title?, items }`) a top-level band with an
266
+ - **Section** (`NavSection`, `{ title?, items }`): a top-level band with an
267
267
  optional `title` heading. Items are **always visible** (no collapse). Use it to
268
268
  cluster related pages under a label (e.g. *Records*, *System*). The first
269
269
  section usually has no title.
270
- - **Collapsible group** (`NavGroup`, an entry with `children`) a parent row with
270
+ - **Collapsible group** (`NavGroup`, an entry with `children`): a parent row with
271
271
  a chevron that **hides/unhides** its nested links; it auto-opens when a child is
272
272
  the active route. Use it for a set of sub-pages under one parent (e.g. *Auth*,
273
273
  *CRM*, *System*) to keep the sidebar short. A group parent has no page of its
274
- own its breadcrumb points at its first child.
274
+ own; its breadcrumb points at its first child.
275
275
 
276
276
  ```ts
277
277
  export const NAV: NavSection[] = [
@@ -288,14 +288,14 @@ export const NAV: NavSection[] = [
288
288
  ```
289
289
 
290
290
  When adding a page, always add it to `NAV` and mirror its color in
291
- `route-meta.ts` the sidebar, breadcrumbs, and tabs all derive from these.
291
+ `route-meta.ts`; the sidebar, breadcrumbs, and tabs all derive from these.
292
292
 
293
293
  ## Open tabs (keep-alive)
294
294
 
295
295
  Enterprise apps keep several pages open at once. The reference app ships a
296
- browser-style **tab strip** under the top bar a reference-app pattern (copy
296
+ browser-style **tab strip** under the top bar, a reference-app pattern (copy
297
297
  `open-tabs.tsx`), not a package export. **This is a first-class feature; wire it
298
- in a fresh install won't have it.**
298
+ in, because a fresh install won't have it.**
299
299
 
300
300
  It is **keep-alive**: every opened page stays mounted (inactive ones hidden), so
301
301
  switching is instant (no remount/flash) and each page keeps its live state
@@ -309,7 +309,7 @@ switching is instant (no remount/flash) and each page keeps its live state
309
309
  </OpenTabsProvider>
310
310
  ```
311
311
 
312
- - Labels/icons/colors derive from `nav-config.ts` + `route-meta.ts` no per-tab wiring.
312
+ - Labels/icons/colors derive from `nav-config.ts` + `route-meta.ts`, with no per-tab wiring.
313
313
  - The list persists in `sessionStorage`, capped by `NEXT_PUBLIC_MAX_TABS`
314
314
  (default 5; oldest FIFO-evicted with a warning).
315
315
  - Tabs are drag-reorderable and right-click-taggable with one of seven colors.
@@ -319,27 +319,27 @@ switching is instant (no remount/flash) and each page keeps its live state
319
319
 
320
320
  Keep-alive relies on a static-export (all-client) shell. If your app renders
321
321
  server components per route, either adopt the demo's static-export shell or fall
322
- back to a plain navigation-tab model (router push per tab) the strip,
322
+ back to a plain navigation-tab model (router push per tab); the strip,
323
323
  persistence, and nav-config wiring stay identical.
324
324
 
325
325
  ---
326
326
 
327
327
  # Forms
328
328
 
329
- Build forms with VUI and shadcn/ui together, reaching for shadcn Form, React Hook Form, and Zod. Every form handles the full lifecycle validation, loading, success, error, disabled, and keyboard navigation. Never use a placeholder in place of a label.
329
+ Build forms with VUI and shadcn/ui together, reaching for shadcn Form, React Hook Form, and Zod. Every form handles the full lifecycle: validation, loading, success, error, disabled, and keyboard navigation. Never use a placeholder in place of a label.
330
330
 
331
331
  ---
332
332
 
333
333
  # Tables
334
334
 
335
- Never hand-build an HTML table always use `RecordView`. Configure columns through its field props (`editable`, `required`, `copyable`, `options`, `render`), and let `RecordView` own the rest: sorting, filtering, pagination, bulk actions, and import/export.
335
+ Never hand-build an HTML table; always use `RecordView`. Configure columns through its field props (`editable`, `required`, `copyable`, `options`, `render`), and let `RecordView` own the rest: sorting, filtering, pagination, bulk actions, and import/export.
336
336
 
337
337
  ## Add / edit form
338
338
 
339
339
  The buffered add/edit form renders in one of two layouts:
340
340
 
341
- - **Slide-over panel** the default; nothing to configure.
342
- - **Full-page form** set `formMode="page"` (with `formColumns={1 | 2}`). Add
341
+ - **Slide-over panel**: the default; nothing to configure.
342
+ - **Full-page form**: set `formMode="page"` (with `formColumns={1 | 2}`). Add
343
343
  `formDescription` and a per-field `description` to show an AWS-style help
344
344
  panel beside the form.
345
345
 
@@ -352,7 +352,7 @@ shared `@viliha/vui-ui/breadcrumbs` component.
352
352
 
353
353
  # Charts
354
354
 
355
- Build every chart with `ChartContainer` plus Recharts. Never hardcode chart colors map them through the chart tokens.
355
+ Build every chart with `ChartContainer` plus Recharts. Never hardcode chart colors; map them through the chart tokens.
356
356
 
357
357
  ---
358
358
 
@@ -361,10 +361,10 @@ Build every chart with `ChartContainer` plus Recharts. Never hardcode chart colo
361
361
  The auth screens are a **demo pattern in the reference app**, not exports of the
362
362
  `@viliha/vui-ui` package. `AuthCard`, `AuthCardHeader`, `AuthCardBody`,
363
363
  `AuthCardFooter` and `AuthCardAside` live in
364
- `apps/backoffice/app/_components/auth.tsx` **copy and adapt them** into your
364
+ `apps/backoffice/app/_components/auth.tsx`; **copy and adapt them** into your
365
365
  app (they are built from published primitives: `Button`, `Input`, tokens).
366
366
 
367
- Do not `import … from "@viliha/vui-ui/auth"` no such entry point exists.
367
+ Do not `import … from "@viliha/vui-ui/auth"`; no such entry point exists.
368
368
 
369
369
  Wrap the forms in semantic HTML.
370
370
 
@@ -374,7 +374,7 @@ Wrap the forms in semantic HTML.
374
374
 
375
375
  VUI and shadcn/ui complement each other, so split the work along their strengths. Reach for shadcn for forms, dialogs, sheets, tabs, popovers, and accordions; reach for VUI for layouts, `RecordView`, enterprise components, charts, and navigation patterns.
376
376
 
377
- VUI owns the design tokens delete any duplicate token definitions shadcn generates.
377
+ VUI owns the design tokens, so delete any duplicate token definitions shadcn generates.
378
378
 
379
379
  ---
380
380
 
@@ -398,7 +398,7 @@ Default to Server Components, streaming, lazy loading, and dynamic imports. Avoi
398
398
 
399
399
  # UX Standards
400
400
 
401
- Every feature communicates its current state loading, success, empty, and error. Give long-running actions visible progress, and never leave users wondering what happened.
401
+ Every feature communicates its current state: loading, success, empty, and error. Give long-running actions visible progress, and never leave users wondering what happened.
402
402
 
403
403
  ---
404
404
 
@@ -436,7 +436,7 @@ Panel
436
436
 
437
437
  # AI Development Rules
438
438
 
439
- Before you write code, reuse what's already there components, layouts, utilities, and variants and extend an existing component before creating a new one. Never duplicate code or styling. Keep APIs simple and components focused.
439
+ Before you write code, reuse what's already there (components, layouts, utilities, and variants), and extend an existing component before creating a new one. Never duplicate code or styling. Keep APIs simple and components focused.
440
440
 
441
441
  ---
442
442
 
package/CHANGELOG.md CHANGED
@@ -7,13 +7,41 @@ backward-compatible features, **major** for breaking changes.
7
7
 
8
8
  To upgrade, see [Upgrading](./AGENT.md#upgrading) in the agent guide.
9
9
 
10
+ ## 1.6.1 — 2026-07-25
11
+
12
+ ### Added
13
+
14
+ - `NEXT_PUBLIC_APP_URL` sets the deploy origin (`SITE.url`), so `metadataBase`,
15
+ canonical URLs, and Open Graph URLs resolve against your domain instead of the
16
+ hard-coded demo host. Falls back to the demo URL when unset. Completes the
17
+ env-driven brand set (`APP_NAME` / `APP_TAGLINE` / `APP_DESCRIPTION` / `APP_URL`).
18
+
19
+ ## 1.6.0 — 2026-07-25
20
+
21
+ ### Changed
22
+
23
+ - Documentation polish: package-manager commands in the docs now use tabs
24
+ (npm / pnpm / yarn / bun), and the prose across the README, `AGENT.md`, and the
25
+ docs site was cleaned of em-dashes for a more natural read.
26
+
27
+ ### Added
28
+
29
+ - **Runtime branding via `BrandProvider` / `useBrand()`** (in the scaffold) — for
30
+ white-label / multi-tenant apps that get their branding from an API. Env vars
31
+ are the defaults; override them live from a `NEXT_PUBLIC_BRAND_URL` JSON
32
+ endpoint, a `<BrandProvider initial={…}>` seed, or `useBrand().setBrand(…)`.
33
+ The name, tagline, description, logo, and the browser-tab title all update at
34
+ runtime — no rebuild. Also routes the last hard-coded name spots (onboarding /
35
+ register-business headers, the signin aside) through the runtime brand.
36
+
10
37
  ## 1.5.4 — 2026-07-24
11
38
 
12
39
  ### Added
13
40
 
14
- - `NEXT_PUBLIC_APP_NAME` env var (in the scaffold) renames the app — the sidebar,
15
- wordmark, auth screens, and page titles now read from one place (`SITE.name`)
16
- instead of a hard-coded "Vui Starter".
41
+ - `NEXT_PUBLIC_APP_NAME` renames the app — the sidebar, wordmark, auth screens,
42
+ and browser-tab metadata read from one place (`SITE.name`) instead of a
43
+ hard-coded "Vui Starter". `NEXT_PUBLIC_APP_TAGLINE` and
44
+ `NEXT_PUBLIC_APP_DESCRIPTION` complete the tab-title/meta rebrand.
17
45
 
18
46
  ### Fixed
19
47
 
package/README.md CHANGED
@@ -52,14 +52,14 @@ npx @viliha/vui-ui init
52
52
  It's **interactive** and walks through a short decision tree:
53
53
 
54
54
  1. **Fresh project?** (fresh vs. existing)
55
- 2. **Pre-built theme?** the full shell plus demo pages, or just the theme wiring
55
+ 2. **Pre-built theme?** The full shell plus demo pages, or just the theme wiring
56
56
  for you to build on.
57
57
 
58
58
  Everything it generates is copied into *your* repo, so you own and edit it.
59
59
 
60
60
  | | **Pre-built** (shell + demo) | **Theme-only** (you configure) |
61
61
  | --- | --- | --- |
62
- | **Fresh** | full runnable app: config + shell + demo pages | just the theme wiring (`globals.css`, `next.config`) build your own pages |
62
+ | **Fresh** | full runnable app: config + shell + demo pages | just the theme wiring (`globals.css`, `next.config`). Build your own pages |
63
63
  | **Existing** | shell + demo added; your config is **never** overwritten (prints merge steps) | nothing copied; prints the wiring steps |
64
64
 
65
65
  Before that, it asks whether this is a standalone **Next.js** app or a
@@ -83,8 +83,8 @@ from your lockfile (npm, pnpm, yarn, or bun). It prompts before doing so; pass
83
83
 
84
84
  ### Fresh + pre-built (recommended for new apps)
85
85
 
86
- Start from a `create-next-app` base **without `--src-dir`** the scaffold expects
87
- a root `app/` with `@/*` → `./*` then run `init`. It scaffolds the app, installs
86
+ Start from a `create-next-app` base **without `--src-dir`** (the scaffold expects
87
+ a root `app/` with `@/*` → `./*`), then run `init`. It scaffolds the app, installs
88
88
  the dependencies, and leaves you ready to run `dev`. Any package manager works:
89
89
 
90
90
  ```bash
@@ -111,8 +111,8 @@ dependencies. The demo then runs out of the box at `/dashboard`.
111
111
 
112
112
  ### Turborepo / monorepo
113
113
 
114
- Scaffold **inside the target app** (e.g. `apps/web`), never at the repo root
115
- the root has no `app/` and no Next.js app. Run `init` from the app, or name it
114
+ Scaffold **inside the target app** (e.g. `apps/web`), never at the repo root; the
115
+ root has no `app/` and no Next.js app. Run `init` from the app, or name it
116
116
  from the root with `--dir`:
117
117
 
118
118
  ```bash
@@ -124,29 +124,29 @@ npx @viliha/vui-ui init --turbo --dir apps/web
124
124
  ```
125
125
 
126
126
  In monorepo mode `init` does **not** auto-install (installs are
127
- workspace-specific), so add the deps to that app afterward `cd apps/web && pnpm
127
+ workspace-specific), so add the deps to that app afterward: `cd apps/web && pnpm
128
128
  add @viliha/vui-ui …`, or from the root `pnpm --filter web add @viliha/vui-ui …`.
129
129
  The `transpilePackages` entry, the `theme.css` import, and the `@/*` alias all
130
130
  belong to **that app**, not the root.
131
131
 
132
- ### ⚠️ Existing project read this first
132
+ ### ⚠️ Existing project: read this first
133
133
 
134
134
  Adding VUI to an app you already have takes a little care. **`init --existing`
135
135
  never overwrites your config.** Choose pre-built to add the shell and pages under
136
136
  `app/(app)/` and `app/_components/`, or theme-only to copy nothing. Either way, it
137
137
  prints the four things you need to wire up:
138
138
 
139
- 1. **`next.config`** add `transpilePackages: ["@viliha/vui-ui"]`.
139
+ 1. **`next.config`**: add `transpilePackages: ["@viliha/vui-ui"]`.
140
140
  (Optional, for the open-tabs *keep-alive*: `output: "export"`,
141
141
  `images: { unoptimized: true }`, `trailingSlash: true`.)
142
- 2. **`app/globals.css`** add:
142
+ 2. **`app/globals.css`**: add:
143
143
  ```css
144
144
  @import "tailwindcss";
145
145
  @import "@viliha/vui-ui/theme.css";
146
146
  ```
147
- 3. **`tsconfig.json`** the scaffold imports via `@/*`; map it to your root:
147
+ 3. **`tsconfig.json`**: the scaffold imports via `@/*`; map it to your root:
148
148
  `"compilerOptions": { "paths": { "@/*": ["./*"] } }`.
149
- 4. **Root `app/layout.tsx`** `import "./globals.css"` (and mount fonts to match
149
+ 4. **Root `app/layout.tsx`**: `import "./globals.css"` (and mount fonts to match
150
150
  the demo's look).
151
151
 
152
152
  Run **`npx @viliha/vui-ui init --existing --prebuilt --dry-run`** first to preview
@@ -214,15 +214,15 @@ See the [Data table docs](https://vui.viliha.com/docs/data-table) for the detail
214
214
  The reference app composes these primitives into the conventions documented at
215
215
  [vui.viliha.com/docs/layout](https://vui.viliha.com/docs/layout):
216
216
 
217
- - **Five page types** data table, record form (Add / Edit / View), dashboard,
217
+ - **Five page types**: data table, record form (Add / Edit / View), dashboard,
218
218
  settings, and kanban board.
219
- - **Command palette** Quick actions (`⌘K`, navigate pages) and Global search
219
+ - **Command palette**: Quick actions (`⌘K`, navigate pages) and Global search
220
220
  (`⌘⌥K`, find records), both built on the exported `CommandPalette`.
221
- - **Open tabs** a browser-style strip of opened pages under the top bar
221
+ - **Open tabs**: a browser-style strip of opened pages under the top bar
222
222
  (⌘-click a nav item for a background tab), persisted across reloads.
223
- - **Multi-step wizard** the exported `Steps` indicator + your step state for
223
+ - **Multi-step wizard**: the exported `Steps` indicator + your step state for
224
224
  guided flows (see the `/register-business` demo).
225
- - **Breadcrumbs** the exported `Breadcrumbs` component fed a route-derived
225
+ - **Breadcrumbs**: the exported `Breadcrumbs` component fed a route-derived
226
226
  trail.
227
227
 
228
228
  Copy them from the
@@ -257,7 +257,7 @@ wrapper) · `checkbox` · `command-palette` (⌘K launcher) · `dialog` ·
257
257
 
258
258
  ## Theming
259
259
 
260
- Every design decision lives in `@viliha/vui-ui/theme.css` as CSS variables
260
+ Every design decision lives in `@viliha/vui-ui/theme.css` as CSS variables:
261
261
  colors, radius, typography, and dark mode. Override any token **after** the import
262
262
  to rebrand the entire system:
263
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viliha/vui-ui",
3
- "version": "1.5.4",
3
+ "version": "1.6.1",
4
4
  "description": "Vui UI — a clean, token-driven React admin/CRM component library built on Tailwind CSS v4, shadcn-style patterns, and Radix Icons. Ships as TypeScript source (Just-in-Time), compiled by the consuming app.",
5
5
  "license": "MIT",
6
6
  "author": "Suman Bonakurthi",
@@ -2,8 +2,21 @@
2
2
  # All are optional; defaults are used when unset. NEXT_PUBLIC_ vars are read at
3
3
  # build time and inlined into the exported site.
4
4
 
5
- # App / brand name shown in the sidebar, wordmark, auth screens, and page titles.
5
+ # App / brand identity shown in the sidebar, wordmark, auth screens, and the
6
+ # browser-tab metadata. The tab title is "<APP_NAME> — <APP_TAGLINE>".
6
7
  NEXT_PUBLIC_APP_NAME="Vui Starter"
8
+ NEXT_PUBLIC_APP_TAGLINE="React Admin & CRM Design System"
9
+ NEXT_PUBLIC_APP_DESCRIPTION="Your app description for search engines and social cards."
10
+ # Deploy origin (no trailing slash) — drives metadataBase, canonical + OG URLs.
11
+ # Set this to your domain in production.
12
+ NEXT_PUBLIC_APP_URL="https://vui.viliha.com"
13
+
14
+ # Runtime branding (multi-tenant / white-label). Point this at a JSON endpoint
15
+ # returning any of { name, tagline, description, logoUrl, company, companyUrl };
16
+ # BrandProvider fetches it on load and overrides the values above — including the
17
+ # tab title — live, no rebuild. (Or seed <BrandProvider initial={…}> from a
18
+ # loader, or call useBrand().setBrand(…) once your API responds.)
19
+ # NEXT_PUBLIC_BRAND_URL="https://api.example.com/branding"
7
20
 
8
21
  NEXT_PUBLIC_COMPANY_NAME="VILIHA PTE. LTD."
9
22
  # Optional — links the company name in the footer:
@@ -13,7 +13,7 @@ import {
13
13
  } from "@radix-ui/react-icons";
14
14
 
15
15
  import { cn } from "@/lib/utils";
16
- import { SITE } from "@/lib/seo";
16
+ import { useBrand } from "./brand";
17
17
  import { Menu as MenuPanel } from "@viliha/vui-ui/menu";
18
18
  import { Logo } from "./logo";
19
19
  import { QuickActionsLauncher } from "./quick-actions";
@@ -102,6 +102,7 @@ function SidebarBody({
102
102
  headerAction?: React.ReactNode;
103
103
  }) {
104
104
  const pathname = usePathname();
105
+ const { brand } = useBrand();
105
106
  const [openGroups, setOpenGroups] = React.useState<Set<string>>(() => {
106
107
  const open = new Set<string>();
107
108
  for (const section of NAV) {
@@ -370,13 +371,13 @@ function SidebarBody({
370
371
  collapsed ? "w-9 shrink-0 justify-center px-0" : "flex-1",
371
372
  )}
372
373
  aria-label="Switch workspace"
373
- title={collapsed ? SITE.name : undefined}
374
+ title={collapsed ? brand.name : undefined}
374
375
  >
375
376
  <Logo variant="mark" className="h-6 w-6 shrink-0" />
376
377
  {!collapsed && (
377
378
  <>
378
379
  <span className="min-w-0 flex-1 truncate text-lg font-bold tracking-tight text-foreground">
379
- {SITE.name}
380
+ {brand.name}
380
381
  </span>
381
382
  <ChevronsUpDown className="size-4 shrink-0 text-muted-foreground" />
382
383
  </>
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ import { SITE } from "@/lib/seo";
7
+
8
+ /**
9
+ * Runtime brand configuration.
10
+ *
11
+ * The `NEXT_PUBLIC_*` env vars (see lib/seo.ts) are build-time DEFAULTS. Some
12
+ * apps — white-label / multi-tenant — need branding at RUNTIME from an API. This
13
+ * provider seeds from the env defaults, then lets you override them live:
14
+ *
15
+ * - pass `initial` (e.g. from a server/loader response), and/or
16
+ * - set `NEXT_PUBLIC_BRAND_URL` (or the `configUrl` prop) to a JSON endpoint the
17
+ * provider fetches on mount, and/or
18
+ * - call `useBrand().setBrand(patch)` from anywhere once your API responds.
19
+ *
20
+ * It also keeps the browser-tab title in sync (the app is a static export, so
21
+ * the server-rendered `<title>` is the env default until the client updates it).
22
+ */
23
+ export type Brand = {
24
+ name: string;
25
+ tagline: string;
26
+ description: string;
27
+ /** Overrides NEXT_PUBLIC_LOGO_URL at runtime; unset → the built-in mark. */
28
+ logoUrl?: string;
29
+ company: string;
30
+ companyUrl: string;
31
+ };
32
+
33
+ const DEFAULT_BRAND: Brand = {
34
+ name: SITE.name,
35
+ tagline: SITE.tagline,
36
+ description: SITE.description,
37
+ logoUrl: process.env.NEXT_PUBLIC_LOGO_URL || undefined,
38
+ company: SITE.company,
39
+ companyUrl: SITE.companyUrl,
40
+ };
41
+
42
+ type BrandContextValue = {
43
+ brand: Brand;
44
+ /** Merge a partial update — typically the JSON from your branding API. */
45
+ setBrand: (patch: Partial<Brand>) => void;
46
+ };
47
+
48
+ const BrandContext = React.createContext<BrandContextValue | null>(null);
49
+
50
+ export function BrandProvider({
51
+ children,
52
+ initial,
53
+ configUrl,
54
+ }: {
55
+ children: React.ReactNode;
56
+ initial?: Partial<Brand>;
57
+ configUrl?: string;
58
+ }) {
59
+ const [brand, setBrandState] = React.useState<Brand>({
60
+ ...DEFAULT_BRAND,
61
+ ...initial,
62
+ });
63
+ const setBrand = React.useCallback(
64
+ (patch: Partial<Brand>) => setBrandState((b) => ({ ...b, ...patch })),
65
+ [],
66
+ );
67
+
68
+ // Fetch runtime branding from an API (multi-tenant / white-label). Only the
69
+ // keys present in the response override the defaults; failures keep the
70
+ // defaults so the app never renders unbranded.
71
+ const url = configUrl ?? process.env.NEXT_PUBLIC_BRAND_URL;
72
+ React.useEffect(() => {
73
+ if (!url) return;
74
+ let alive = true;
75
+ fetch(url)
76
+ .then((r) => (r.ok ? (r.json() as Promise<Partial<Brand>>) : null))
77
+ .then((data) => {
78
+ if (alive && data) setBrand(data);
79
+ })
80
+ .catch(() => {
81
+ // network/API unavailable — keep the env defaults (deliberate no-op).
82
+ });
83
+ return () => {
84
+ alive = false;
85
+ };
86
+ }, [url, setBrand]);
87
+
88
+ // Keep the tab title branded. Next bakes/sets per-route titles that contain
89
+ // the default name (and, on the home page, the default tagline); swap those
90
+ // for the active brand after each navigation.
91
+ const pathname = usePathname();
92
+ React.useEffect(() => {
93
+ if (
94
+ brand.name === DEFAULT_BRAND.name &&
95
+ brand.tagline === DEFAULT_BRAND.tagline
96
+ ) {
97
+ return; // nothing overridden — leave Next's titles as-is
98
+ }
99
+ const id = requestAnimationFrame(() => {
100
+ document.title = document.title
101
+ .split(DEFAULT_BRAND.name)
102
+ .join(brand.name)
103
+ .split(DEFAULT_BRAND.tagline)
104
+ .join(brand.tagline);
105
+ });
106
+ return () => cancelAnimationFrame(id);
107
+ }, [pathname, brand.name, brand.tagline]);
108
+
109
+ return (
110
+ <BrandContext.Provider value={{ brand, setBrand }}>
111
+ {children}
112
+ </BrandContext.Provider>
113
+ );
114
+ }
115
+
116
+ /** Read the active brand + `setBrand`. Falls back to the env defaults if no
117
+ * provider is mounted, so components work even outside a BrandProvider. */
118
+ export function useBrand(): BrandContextValue {
119
+ return (
120
+ React.useContext(BrandContext) ?? {
121
+ brand: DEFAULT_BRAND,
122
+ setBrand: () => {},
123
+ }
124
+ );
125
+ }
126
+
127
+ /** The active brand name as text — drop it wherever the app name is shown. */
128
+ export function BrandName() {
129
+ return <>{useBrand().brand.name}</>;
130
+ }
@@ -1,7 +1,9 @@
1
+ "use client";
2
+
1
3
  import Image from "next/image";
2
4
 
3
5
  import { cn } from "@/lib/utils";
4
- import { SITE } from "@/lib/seo";
6
+ import { useBrand } from "./brand";
5
7
 
6
8
  type LogoProps = {
7
9
  /** Kept for call-site compatibility; the mark is the same either way. */
@@ -9,19 +11,18 @@ type LogoProps = {
9
11
  className?: string;
10
12
  };
11
13
 
12
- /** Optional custom logo: drop an image in /public and set NEXT_PUBLIC_LOGO_URL
13
- * (e.g. "/logo.svg"). Unset falls back to the built-in mark below. */
14
- const LOGO_URL = process.env.NEXT_PUBLIC_LOGO_URL;
15
-
16
14
  /**
17
- * App logo your image via NEXT_PUBLIC_LOGO_URL, else a self-contained rounded
18
- * blue badge with a stylised "V". Scales with the `className` size (default 24px).
15
+ * App logo. Uses the runtime brand's `logoUrl` (from an API or
16
+ * NEXT_PUBLIC_LOGO_URL, via BrandProvider); when unset, falls back to a
17
+ * self-contained rounded blue badge with a stylised "V". Scales with the
18
+ * `className` size (default 24px).
19
19
  */
20
20
  export function Logo({ className }: LogoProps) {
21
- if (LOGO_URL) {
21
+ const { brand } = useBrand();
22
+ if (brand.logoUrl) {
22
23
  return (
23
24
  <Image
24
- src={LOGO_URL}
25
+ src={brand.logoUrl}
25
26
  alt="Logo"
26
27
  width={24}
27
28
  height={24}
@@ -33,7 +34,7 @@ export function Logo({ className }: LogoProps) {
33
34
  <svg
34
35
  viewBox="0 0 24 24"
35
36
  role="img"
36
- aria-label={SITE.name}
37
+ aria-label={brand.name}
37
38
  className={cn("h-6 w-6", className)}
38
39
  >
39
40
  <rect width="24" height="24" rx="6" fill="var(--brand-indigo)" />
@@ -1,7 +1,9 @@
1
+ "use client";
2
+
1
3
  import Link from "next/link";
2
4
 
3
5
  import { cn } from "@/lib/utils";
4
- import { SITE } from "@/lib/seo";
6
+ import { useBrand } from "./brand";
5
7
  import { Logo } from "./logo";
6
8
 
7
9
  /**
@@ -21,6 +23,7 @@ export function Wordmark({
21
23
  logoClassName?: string;
22
24
  textClassName?: string;
23
25
  }) {
26
+ const { brand } = useBrand();
24
27
  const content = (
25
28
  <>
26
29
  <Logo className={cn("h-6 w-6 shrink-0", logoClassName)} />
@@ -30,14 +33,14 @@ export function Wordmark({
30
33
  textClassName,
31
34
  )}
32
35
  >
33
- {SITE.name}
36
+ {brand.name}
34
37
  </span>
35
38
  </>
36
39
  );
37
40
  const classes = cn("flex items-center gap-2", className);
38
41
 
39
42
  return href ? (
40
- <Link href={href} aria-label={SITE.name} className={classes}>
43
+ <Link href={href} aria-label={brand.name} className={classes}>
41
44
  {content}
42
45
  </Link>
43
46
  ) : (
@@ -13,6 +13,7 @@ import {
13
13
 
14
14
  import { Button } from "@viliha/vui-ui/button";
15
15
  import { Input } from "@viliha/vui-ui/input";
16
+ import { BrandName } from "@/app/_components/brand";
16
17
  import {
17
18
  AuthCard,
18
19
  AuthCardAside,
@@ -212,7 +213,7 @@ export default function SignInPage() {
212
213
  Email me a magic link
213
214
  </Button>
214
215
  <AuthCardAside>
215
- New to Vui Starter?{" "}
216
+ New to <BrandName />?{" "}
216
217
  <Link href="/auth/signup" className="font-medium text-primary hover:underline">
217
218
  Create an account
218
219
  </Link>
@@ -3,6 +3,7 @@ import { Inter, JetBrains_Mono } from "next/font/google";
3
3
 
4
4
  import "./globals.css";
5
5
  import { themeInitScript } from "./_components/theme-toggle";
6
+ import { BrandProvider } from "./_components/brand";
6
7
  import { SITE } from "@/lib/seo";
7
8
 
8
9
  const inter = Inter({
@@ -96,7 +97,7 @@ export default function RootLayout({
96
97
  <body
97
98
  className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}
98
99
  >
99
- {children}
100
+ <BrandProvider>{children}</BrandProvider>
100
101
  </body>
101
102
  </html>
102
103
  );
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
2
2
 
3
3
  import { Logo } from "@/app/_components/logo";
4
4
  import { ThemeToggle } from "@/app/_components/theme-toggle";
5
+ import { BrandName } from "@/app/_components/brand";
5
6
 
6
7
  // Onboarding is a post-signup flow — keep it out of the index.
7
8
  export const metadata: Metadata = { robots: { index: false, follow: false } };
@@ -17,7 +18,7 @@ export default function OnboardingLayout({
17
18
  <div className="flex items-center gap-2">
18
19
  <Logo className="h-7 w-7" />
19
20
  <span className="bg-gradient-to-r from-brand-indigo to-brand-violet bg-clip-text text-sm font-bold tracking-tight text-transparent">
20
- Vui Starter
21
+ <BrandName />
21
22
  </span>
22
23
  </div>
23
24
  <div className="flex items-center gap-3 text-muted-foreground">
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
2
2
 
3
3
  import { Logo } from "@/app/_components/logo";
4
4
  import { ThemeToggle } from "@/app/_components/theme-toggle";
5
+ import { BrandName } from "@/app/_components/brand";
5
6
 
6
7
  export const metadata: Metadata = {
7
8
  title: "Register your business",
@@ -19,7 +20,7 @@ export default function RegisterBusinessLayout({
19
20
  <div className="flex items-center gap-2">
20
21
  <Logo className="h-7 w-7" />
21
22
  <span className="bg-gradient-to-r from-brand-indigo to-brand-violet bg-clip-text text-sm font-bold tracking-tight text-transparent">
22
- Vui Starter
23
+ <BrandName />
23
24
  </span>
24
25
  </div>
25
26
  <ThemeToggle />
@@ -3,12 +3,17 @@ import type { Metadata } from "next";
3
3
  /** Single source of truth for site-wide SEO. Deployed as a static export to a
4
4
  * custom domain (see ../CNAME), so URLs are absolute against SITE.url. */
5
5
  export const SITE = {
6
- // App/brand name shown in the sidebar, wordmark, auth screens, and page
7
- // titles. Override per deployment via env (NEXT_PUBLIC_ = inlined at build).
6
+ // App/brand identity shown in the sidebar, wordmark, auth screens, and the
7
+ // browser-tab metadata (title/description). Override per deployment via env
8
+ // (NEXT_PUBLIC_ = inlined at build). The tab title is `${name} — ${tagline}`.
8
9
  name: process.env.NEXT_PUBLIC_APP_NAME ?? "Vui Starter",
9
- tagline: "React Admin & CRM Design System",
10
- url: "https://vui.viliha.com",
10
+ tagline:
11
+ process.env.NEXT_PUBLIC_APP_TAGLINE ?? "React Admin & CRM Design System",
12
+ // Deploy origin — drives metadataBase, canonical URLs, and OG image URLs.
13
+ // Set NEXT_PUBLIC_APP_URL to your domain (no trailing slash) per deployment.
14
+ url: process.env.NEXT_PUBLIC_APP_URL ?? "https://vui.viliha.com",
11
15
  description:
16
+ process.env.NEXT_PUBLIC_APP_DESCRIPTION ??
12
17
  "Vui Starter is a free, open-source React admin & CRM design system — a token-driven component library (@viliha/vui-ui) plus a full backoffice demo.",
13
18
  ogImage: "/brand/pulse-wordmark.png",
14
19
  author: "Suman Bonakurthi",