@viliha/vui-ui 1.1.8 → 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.
- package/AGENT.md +79 -9
- package/README.md +60 -0
- package/bin/vui.mjs +211 -0
- package/package.json +10 -2
- package/template/.env.example +20 -0
- package/template/app/(app)/branches/branches-table.tsx +69 -0
- package/template/app/(app)/branches/page.tsx +13 -0
- package/template/app/(app)/businesses/businesses-table.tsx +39 -0
- package/template/app/(app)/businesses/page.tsx +13 -0
- package/template/app/(app)/calendar/page.tsx +725 -0
- package/template/app/(app)/charts/charts-content.tsx +190 -0
- package/template/app/(app)/charts/layout.tsx +8 -0
- package/template/app/(app)/charts/page.tsx +44 -0
- package/template/app/(app)/chat/layout.tsx +14 -0
- package/template/app/(app)/chat/page.tsx +311 -0
- package/template/app/(app)/components/layout.tsx +8 -0
- package/template/app/(app)/components/page.tsx +464 -0
- package/template/app/(app)/crm/companies/companies-table.tsx +56 -0
- package/template/app/(app)/crm/companies/page.tsx +13 -0
- package/template/app/(app)/crm/opportunities/opportunities-board.tsx +484 -0
- package/template/app/(app)/crm/opportunities/page.tsx +13 -0
- package/template/app/(app)/crm/people/page.tsx +13 -0
- package/template/app/(app)/crm/people/people-table.tsx +50 -0
- package/template/app/(app)/dashboard/layout.tsx +8 -0
- package/template/app/(app)/dashboard/page.tsx +239 -0
- package/template/app/(app)/departments/departments-table.tsx +55 -0
- package/template/app/(app)/departments/page.tsx +13 -0
- package/template/app/(app)/employees/employees-table.tsx +64 -0
- package/template/app/(app)/employees/page.tsx +13 -0
- package/template/app/(app)/forms/layout.tsx +8 -0
- package/template/app/(app)/forms/page.tsx +388 -0
- package/template/app/(app)/layout.tsx +74 -0
- package/template/app/(app)/markets/markets-table.tsx +76 -0
- package/template/app/(app)/markets/page.tsx +13 -0
- package/template/app/(app)/organizations/edit/page.tsx +72 -0
- package/template/app/(app)/organizations/new/page.tsx +46 -0
- package/template/app/(app)/organizations/organizations-config.tsx +97 -0
- package/template/app/(app)/organizations/organizations-table.tsx +42 -0
- package/template/app/(app)/organizations/page.tsx +13 -0
- package/template/app/(app)/settings/layout.tsx +8 -0
- package/template/app/(app)/settings/page.tsx +255 -0
- package/template/app/(app)/steps/page.tsx +263 -0
- package/template/app/(app)/support/layout.tsx +14 -0
- package/template/app/(app)/support/page.tsx +345 -0
- package/template/app/(app)/system/cities/cities-table.tsx +34 -0
- package/template/app/(app)/system/cities/page.tsx +13 -0
- package/template/app/(app)/system/countries/countries-table.tsx +34 -0
- package/template/app/(app)/system/countries/page.tsx +13 -0
- package/template/app/(app)/system/currencies/currencies-table.tsx +34 -0
- package/template/app/(app)/system/currencies/page.tsx +13 -0
- package/template/app/(app)/system/languages/languages-table.tsx +32 -0
- package/template/app/(app)/system/languages/page.tsx +13 -0
- package/template/app/(app)/system/regions/page.tsx +13 -0
- package/template/app/(app)/system/regions/regions-table.tsx +32 -0
- package/template/app/(app)/users/page.tsx +13 -0
- package/template/app/(app)/users/users-table.tsx +97 -0
- package/template/app/_components/app-sidebar.tsx +565 -0
- package/template/app/_components/auth.tsx +184 -0
- package/template/app/_components/breadcrumbs.tsx +26 -0
- package/template/app/_components/global-search.tsx +209 -0
- package/template/app/_components/logo.tsx +49 -0
- package/template/app/_components/nav-config.ts +138 -0
- package/template/app/_components/open-tabs.tsx +530 -0
- package/template/app/_components/quick-actions.tsx +140 -0
- package/template/app/_components/route-meta.ts +137 -0
- package/template/app/_components/set-page-title.tsx +15 -0
- package/template/app/_components/showcase.tsx +29 -0
- package/template/app/_components/stat-card.tsx +56 -0
- package/template/app/_components/theme-toggle.tsx +46 -0
- package/template/app/_components/top-bar.tsx +109 -0
- package/template/app/_components/user-menu.tsx +109 -0
- package/template/app/_components/wordmark.tsx +45 -0
- package/template/app/apple-icon.png +0 -0
- package/template/app/auth/forgot-password/page.tsx +102 -0
- package/template/app/auth/layout.tsx +43 -0
- package/template/app/auth/page.tsx +13 -0
- package/template/app/auth/reset-password/page.tsx +108 -0
- package/template/app/auth/signin/page.tsx +238 -0
- package/template/app/auth/signup/page.tsx +159 -0
- package/template/app/auth/verify/page.tsx +86 -0
- package/template/app/error.tsx +56 -0
- package/template/app/globals.css +6 -0
- package/template/app/icon.png +0 -0
- package/template/app/icon.svg +4 -0
- package/template/app/layout.tsx +103 -0
- package/template/app/not-found.tsx +29 -0
- package/template/app/onboarding/layout.tsx +31 -0
- package/template/app/onboarding/page.tsx +399 -0
- package/template/app/page.tsx +13 -0
- package/template/app/register-business/layout.tsx +30 -0
- package/template/app/register-business/page.tsx +234 -0
- package/template/components/ui/accordion.tsx +66 -0
- package/template/components/ui/alert-dialog.tsx +196 -0
- package/template/components/ui/alert.tsx +66 -0
- package/template/components/ui/aspect-ratio.tsx +11 -0
- package/template/components/ui/avatar.tsx +109 -0
- package/template/components/ui/badge.tsx +48 -0
- package/template/components/ui/breadcrumb.tsx +109 -0
- package/template/components/ui/button.tsx +64 -0
- package/template/components/ui/calendar.tsx +220 -0
- package/template/components/ui/card.tsx +92 -0
- package/template/components/ui/checkbox.tsx +32 -0
- package/template/components/ui/collapsible.tsx +33 -0
- package/template/components/ui/command.tsx +184 -0
- package/template/components/ui/dialog.tsx +158 -0
- package/template/components/ui/dropdown-menu.tsx +257 -0
- package/template/components/ui/form.tsx +167 -0
- package/template/components/ui/hover-card.tsx +44 -0
- package/template/components/ui/input-otp.tsx +77 -0
- package/template/components/ui/input.tsx +21 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/popover.tsx +89 -0
- package/template/components/ui/progress.tsx +31 -0
- package/template/components/ui/radio-group.tsx +45 -0
- package/template/components/ui/scroll-area.tsx +58 -0
- package/template/components/ui/select.tsx +190 -0
- package/template/components/ui/separator.tsx +28 -0
- package/template/components/ui/sheet.tsx +143 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/slider.tsx +63 -0
- package/template/components/ui/sonner.tsx +40 -0
- package/template/components/ui/switch.tsx +35 -0
- package/template/components/ui/table.tsx +116 -0
- package/template/components/ui/tabs.tsx +91 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +83 -0
- package/template/components/ui/toggle.tsx +47 -0
- package/template/components/ui/tooltip.tsx +57 -0
- package/template/lib/auth-demo.ts +94 -0
- package/template/lib/crm-data.ts +80 -0
- package/template/lib/demo-data.ts +267 -0
- package/template/lib/mock-data.ts +169 -0
- package/template/lib/org-store.ts +50 -0
- package/template/lib/seo.ts +188 -0
- package/template/lib/utils.ts +3 -0
- package/template/next.config.mjs +13 -0
- package/template/postcss.config.mjs +7 -0
package/AGENT.md
CHANGED
|
@@ -67,6 +67,23 @@ Never duplicate functionality already provided by VUI.
|
|
|
67
67
|
|
|
68
68
|
# Installation Requirements
|
|
69
69
|
|
|
70
|
+
## Scaffold the shell + demo (`init`)
|
|
71
|
+
|
|
72
|
+
The package ships the components; the **app shell** (layout, sidebar, open tabs,
|
|
73
|
+
command palette, nav config, logo) and **demo pages** are scaffolded with:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx @viliha/vui-ui init # interactive: fresh vs existing, with/without demo
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Files land in the consumer's repo (they own them). **Fresh project:** writes
|
|
80
|
+
config too, runs out of the box. **Existing project:** `init --existing` never
|
|
81
|
+
overwrites config — it adds the shell/pages and prints the merge steps
|
|
82
|
+
(`transpilePackages`, the `theme.css` import, the `@/*` alias, `import
|
|
83
|
+
"./globals.css"`). Flags: `--fresh` / `--existing` / `--demo` / `--no-demo` /
|
|
84
|
+
`--yes` / `--force` / `--dry-run`. If you only need components, skip `init` and
|
|
85
|
+
just do the setup below.
|
|
86
|
+
|
|
70
87
|
VUI ships as TypeScript source.
|
|
71
88
|
|
|
72
89
|
## Next.js
|
|
@@ -207,16 +224,69 @@ One trail, **derived from the route — never hand-written per page.**
|
|
|
207
224
|
|
|
208
225
|
To reorder or rename, edit the nav config; the trail follows automatically.
|
|
209
226
|
|
|
210
|
-
##
|
|
227
|
+
## Sidebar: sections & collapsible groups
|
|
228
|
+
|
|
229
|
+
The sidebar is driven by one `NAV` config (copy `nav-config.ts`). There are
|
|
230
|
+
**two grouping shapes — use them, don't invent a third:**
|
|
231
|
+
|
|
232
|
+
- **Section** (`NavSection`, `{ title?, items }`) — a top-level band with an
|
|
233
|
+
optional `title` heading. Items are **always visible** (no collapse). Use it to
|
|
234
|
+
cluster related pages under a label (e.g. *Records*, *System*). The first
|
|
235
|
+
section usually has no title.
|
|
236
|
+
- **Collapsible group** (`NavGroup`, an entry with `children`) — a parent row with
|
|
237
|
+
a chevron that **hides/unhides** its nested links; it auto-opens when a child is
|
|
238
|
+
the active route. Use it for a set of sub-pages under one parent (e.g. *Auth*,
|
|
239
|
+
*CRM*, *System*) to keep the sidebar short. A group parent has no page of its
|
|
240
|
+
own — its breadcrumb points at its first child.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
export const NAV: NavSection[] = [
|
|
244
|
+
{ items: [ // untitled section
|
|
245
|
+
{ label: "Home", href: "/dashboard", icon: Home },
|
|
246
|
+
{ label: "Auth", icon: Lock, children: [ // collapsible group (hide/unhide)
|
|
247
|
+
{ label: "Sign in", href: "/auth/signin", icon: LogIn },
|
|
248
|
+
]},
|
|
249
|
+
]},
|
|
250
|
+
{ title: "Records", items: [ // titled section (static band)
|
|
251
|
+
{ label: "Organizations", href: "/organizations", icon: Building2 },
|
|
252
|
+
]},
|
|
253
|
+
];
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
When adding a page, always add it to `NAV` and mirror its color in
|
|
257
|
+
`route-meta.ts` — the sidebar, breadcrumbs, and tabs all derive from these.
|
|
258
|
+
|
|
259
|
+
## Open tabs (keep-alive)
|
|
260
|
+
|
|
261
|
+
Enterprise apps keep several pages open at once. The reference app ships a
|
|
262
|
+
browser-style **tab strip** under the top bar — a reference-app pattern (copy
|
|
263
|
+
`open-tabs.tsx`), not a package export. **This is a first-class feature; wire it
|
|
264
|
+
in — a fresh install won't have it.**
|
|
265
|
+
|
|
266
|
+
It is **keep-alive**: every opened page stays mounted (inactive ones hidden), so
|
|
267
|
+
switching is instant (no remount/flash) and each page keeps its live state
|
|
268
|
+
(scroll, form input, filters). Mount the three pieces once in your app layout:
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
<OpenTabsProvider>
|
|
272
|
+
<AppSidebar /> <TopBar />
|
|
273
|
+
<TabStrip /> {/* the strip, under the top bar */}
|
|
274
|
+
<KeepAliveTabs>{children}</KeepAliveTabs> {/* keeps open pages mounted */}
|
|
275
|
+
</OpenTabsProvider>
|
|
276
|
+
```
|
|
211
277
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
278
|
+
- Labels/icons/colors derive from `nav-config.ts` + `route-meta.ts` — no per-tab wiring.
|
|
279
|
+
- The list persists in `sessionStorage`, capped by `NEXT_PUBLIC_MAX_TABS`
|
|
280
|
+
(default 5; oldest FIFO-evicted with a warning).
|
|
281
|
+
- Tabs are drag-reorderable and right-click-taggable with one of seven colors.
|
|
282
|
+
- ⌘/Ctrl-click a nav item opens a **background tab**; call
|
|
283
|
+
`useOpenTabs().openTab(href, { background: true })` for custom "open in new tab"
|
|
284
|
+
buttons. `tabKey` normalizes trailing slashes so `/foo` and `/foo/` are one tab.
|
|
285
|
+
|
|
286
|
+
Keep-alive relies on a static-export (all-client) shell. If your app renders
|
|
287
|
+
server components per route, either adopt the demo's static-export shell or fall
|
|
288
|
+
back to a plain navigation-tab model (router push per tab) — the strip,
|
|
289
|
+
persistence, and nav-config wiring stay identical.
|
|
220
290
|
|
|
221
291
|
---
|
|
222
292
|
|
package/README.md
CHANGED
|
@@ -38,6 +38,66 @@ export default nextConfig;
|
|
|
38
38
|
|
|
39
39
|
(Vite and most bundlers need no extra config.)
|
|
40
40
|
|
|
41
|
+
## Scaffold the full app + demo (`init`)
|
|
42
|
+
|
|
43
|
+
The package ships the **components**; the **app shell** (layout, sidebar,
|
|
44
|
+
browser-style **open tabs**, command palette, nav config, logo) and the **demo
|
|
45
|
+
pages** live in a one-shot scaffolder:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @viliha/vui-ui init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It's **interactive** — it asks whether this is a **fresh** or **existing**
|
|
52
|
+
project and whether to include the **demo pages** (dashboard, CRM, calendar,
|
|
53
|
+
chat, support, settings, auth). Files are copied into *your* repo, so you own and
|
|
54
|
+
edit them. It's **non-destructive**: existing files are skipped.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Flags (for CI / agents, skip the prompts):
|
|
58
|
+
--fresh | --existing project type
|
|
59
|
+
--demo | --no-demo include demo pages (default: with demo)
|
|
60
|
+
--yes, -y accept defaults (fresh, with demo)
|
|
61
|
+
--force overwrite existing files
|
|
62
|
+
--dry-run preview without writing
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
After it runs, install the peer deps it prints, then `npm run dev` → `/dashboard`.
|
|
66
|
+
|
|
67
|
+
### Fresh project (recommended)
|
|
68
|
+
|
|
69
|
+
Start from a `create-next-app` base. `init --fresh` also writes `next.config.mjs`,
|
|
70
|
+
`app/globals.css`, and the root layout, so the demo runs out of the box.
|
|
71
|
+
|
|
72
|
+
### ⚠️ Existing project — read this first
|
|
73
|
+
|
|
74
|
+
Adding VUI to an app you already have needs care. **`init --existing` never
|
|
75
|
+
overwrites your config** — it adds the shell + pages under `app/(app)/` and
|
|
76
|
+
`app/_components/`, then prints the four things to merge yourself:
|
|
77
|
+
|
|
78
|
+
1. **`next.config`** — add `transpilePackages: ["@viliha/vui-ui"]`.
|
|
79
|
+
(Optional, for the open-tabs *keep-alive*: `output: "export"`,
|
|
80
|
+
`images: { unoptimized: true }`, `trailingSlash: true`.)
|
|
81
|
+
2. **`app/globals.css`** — add:
|
|
82
|
+
```css
|
|
83
|
+
@import "tailwindcss";
|
|
84
|
+
@import "@viliha/vui-ui/theme.css";
|
|
85
|
+
```
|
|
86
|
+
3. **`tsconfig.json`** — the scaffold imports via `@/*`; map it to your root:
|
|
87
|
+
`"compilerOptions": { "paths": { "@/*": ["./*"] } }`.
|
|
88
|
+
4. **Root `app/layout.tsx`** — `import "./globals.css"` (and mount fonts to match
|
|
89
|
+
the demo's look).
|
|
90
|
+
|
|
91
|
+
Run **`npx @viliha/vui-ui init --existing --dry-run`** first to see exactly what
|
|
92
|
+
it will add. If you only want the components (not the shell/pages), **skip `init`
|
|
93
|
+
entirely** — the [Setup](#setup) above is all you need, then import from
|
|
94
|
+
`@viliha/vui-ui/*`.
|
|
95
|
+
|
|
96
|
+
> **Note on the theme in an existing app:** VUI owns its design tokens in
|
|
97
|
+
> `theme.css`. If your app already defines shadcn/ui or other CSS variables with
|
|
98
|
+
> the same names, import `theme.css` **last** and remove the duplicates, or the
|
|
99
|
+
> two token sets will fight. On a fresh project this never comes up.
|
|
100
|
+
|
|
41
101
|
## Usage
|
|
42
102
|
|
|
43
103
|
Each component is its own entry point, so you only ship what you use:
|
package/bin/vui.mjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @viliha/vui-ui CLI — scaffolds the app shell + demo pages into a consumer's
|
|
3
|
+
// Next.js project (shadcn-style: the files land in YOUR repo, you own them).
|
|
4
|
+
//
|
|
5
|
+
// npx @viliha/vui-ui init [options]
|
|
6
|
+
//
|
|
7
|
+
// Interactive by default: asks whether this is a fresh or existing project and
|
|
8
|
+
// whether to include the demo pages. Non-interactive with flags (for CI/agents).
|
|
9
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
10
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { createInterface } from "node:readline/promises";
|
|
13
|
+
import { stdin, stdout } from "node:process";
|
|
14
|
+
|
|
15
|
+
const TEMPLATE = fileURLToPath(new URL("../template/", import.meta.url));
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const cmd = args[0];
|
|
18
|
+
const has = (f) => args.includes(f);
|
|
19
|
+
|
|
20
|
+
function usage() {
|
|
21
|
+
console.log(`@viliha/vui-ui
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
npx @viliha/vui-ui init [options]
|
|
25
|
+
|
|
26
|
+
init Scaffold the VUI app shell (+ optional demo pages) into this
|
|
27
|
+
project. Interactive unless flags are given. Non-destructive:
|
|
28
|
+
existing files are skipped (use --force to overwrite).
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--fresh Treat as a brand-new project (copies config too).
|
|
32
|
+
--existing Treat as an existing project (skips config; prints merge steps).
|
|
33
|
+
--demo Include the demo pages (dashboard, CRM, calendar, chat, …).
|
|
34
|
+
--no-demo Shell + dashboard only, no other demo pages.
|
|
35
|
+
--yes, -y Accept defaults without prompting (fresh, with demo).
|
|
36
|
+
--force Overwrite files that already exist.
|
|
37
|
+
--dry-run Show what would be written without touching the disk.
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (cmd !== "init") {
|
|
42
|
+
usage();
|
|
43
|
+
process.exit(cmd ? 1 : 0);
|
|
44
|
+
}
|
|
45
|
+
if (!existsSync(TEMPLATE)) {
|
|
46
|
+
console.error(
|
|
47
|
+
"vui: bundled template not found. Reinstall @viliha/vui-ui (the template " +
|
|
48
|
+
"ships with the published package).",
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dry = has("--dry-run");
|
|
54
|
+
const force = has("--force");
|
|
55
|
+
|
|
56
|
+
async function ask(question, def) {
|
|
57
|
+
if (has("--yes") || has("-y") || !stdin.isTTY) return def;
|
|
58
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
59
|
+
const a = (await rl.question(question)).trim().toLowerCase();
|
|
60
|
+
rl.close();
|
|
61
|
+
return a || def;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Config/root files that an existing project almost certainly already has —
|
|
65
|
+
// copying them would collide, so in "existing" mode we skip + print merge steps.
|
|
66
|
+
const CONFIG_FILES = new Set([
|
|
67
|
+
"next.config.mjs",
|
|
68
|
+
"postcss.config.mjs",
|
|
69
|
+
"app/globals.css",
|
|
70
|
+
"app/layout.tsx",
|
|
71
|
+
"app/page.tsx",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// A file is a "demo page" (optional) vs the shell (always). The dashboard stays
|
|
75
|
+
// in the shell so `/` has a landing page.
|
|
76
|
+
function isDemo(rel) {
|
|
77
|
+
const p = rel.split(sep).join("/");
|
|
78
|
+
if (p.startsWith("app/auth/")) return true;
|
|
79
|
+
if (p.startsWith("app/onboarding/")) return true;
|
|
80
|
+
if (p.startsWith("app/register-business/")) return true;
|
|
81
|
+
if (p.startsWith("app/(app)/")) {
|
|
82
|
+
return !(p === "app/(app)/layout.tsx" || p.startsWith("app/(app)/dashboard/"));
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
// 1. Fresh vs existing.
|
|
89
|
+
let mode = has("--existing") ? "existing" : has("--fresh") ? "fresh" : null;
|
|
90
|
+
if (!mode) {
|
|
91
|
+
const a = await ask(
|
|
92
|
+
"Is this a fresh project or an existing one? [F(resh)/e(xisting)] ",
|
|
93
|
+
"f",
|
|
94
|
+
);
|
|
95
|
+
mode = a.startsWith("e") ? "existing" : "fresh";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Include the demo pages?
|
|
99
|
+
let demo = has("--demo") ? true : has("--no-demo") ? false : null;
|
|
100
|
+
if (demo === null) {
|
|
101
|
+
const a = await ask(
|
|
102
|
+
"Install the demo pages (dashboard, CRM, calendar, chat, support…)? [Y/n] ",
|
|
103
|
+
"y",
|
|
104
|
+
);
|
|
105
|
+
demo = !a.startsWith("n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
let created = 0;
|
|
110
|
+
let skipped = 0;
|
|
111
|
+
const configSkipped = [];
|
|
112
|
+
|
|
113
|
+
const files = [];
|
|
114
|
+
(function walk(dir) {
|
|
115
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
116
|
+
const abs = join(dir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) walk(abs);
|
|
118
|
+
else files.push(abs);
|
|
119
|
+
}
|
|
120
|
+
})(TEMPLATE);
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`\nScaffolding VUI (${mode}${demo ? ", with demo" : ", no demo"}) into ${cwd}${
|
|
124
|
+
dry ? " [dry run]" : ""
|
|
125
|
+
}\n`,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
for (const abs of files) {
|
|
129
|
+
const rel = relative(TEMPLATE, abs);
|
|
130
|
+
const norm = rel.split(sep).join("/");
|
|
131
|
+
|
|
132
|
+
if (isDemo(rel) && !demo) continue; // demo not selected
|
|
133
|
+
if (mode === "existing" && CONFIG_FILES.has(norm)) {
|
|
134
|
+
configSkipped.push(norm);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const dest = join(cwd, rel);
|
|
139
|
+
if (existsSync(dest) && !force) {
|
|
140
|
+
skipped++;
|
|
141
|
+
console.log(` skip ${rel}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!dry) {
|
|
145
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
146
|
+
copyFileSync(abs, dest);
|
|
147
|
+
}
|
|
148
|
+
created++;
|
|
149
|
+
console.log(` ${dry ? "would add" : "added "} ${rel}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(
|
|
153
|
+
`\nDone — ${created} file(s) ${dry ? "would be added" : "added"}, ${skipped} skipped.`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const DEPS = [
|
|
157
|
+
"@radix-ui/react-icons",
|
|
158
|
+
"@radix-ui/react-slot",
|
|
159
|
+
"@tanstack/react-table",
|
|
160
|
+
"class-variance-authority",
|
|
161
|
+
"clsx",
|
|
162
|
+
"cmdk",
|
|
163
|
+
"date-fns",
|
|
164
|
+
"input-otp",
|
|
165
|
+
"lucide-react",
|
|
166
|
+
"next-themes",
|
|
167
|
+
"radix-ui",
|
|
168
|
+
"react-day-picker",
|
|
169
|
+
"react-hook-form",
|
|
170
|
+
"@hookform/resolvers",
|
|
171
|
+
"recharts",
|
|
172
|
+
"sonner",
|
|
173
|
+
"tailwind-merge",
|
|
174
|
+
"zod",
|
|
175
|
+
].join(" ");
|
|
176
|
+
|
|
177
|
+
if (mode === "existing") {
|
|
178
|
+
console.log(`
|
|
179
|
+
⚠ Existing project — config files were NOT written to avoid clobbering yours.
|
|
180
|
+
Merge these into your project manually:
|
|
181
|
+
|
|
182
|
+
• next.config: add
|
|
183
|
+
transpilePackages: ["@viliha/vui-ui"]
|
|
184
|
+
(for the demo's tab keep-alive: output:"export", images:{unoptimized:true},
|
|
185
|
+
trailingSlash:true — optional, only if you want static-export keep-alive)
|
|
186
|
+
• app/globals.css: add
|
|
187
|
+
@import "tailwindcss";
|
|
188
|
+
@import "@viliha/vui-ui/theme.css";
|
|
189
|
+
• tsconfig.json: map the alias the scaffold uses
|
|
190
|
+
"compilerOptions": { "paths": { "@/*": ["./*"] } }
|
|
191
|
+
• Root app/layout.tsx: import "./globals.css" and mount fonts if you want the
|
|
192
|
+
demo's exact look (see the scaffolded app/(app)/layout.tsx for the shell).
|
|
193
|
+
|
|
194
|
+
The shell + pages were added under app/(app)/ and app/_components/ — review
|
|
195
|
+
them (re-run with --dry-run first if unsure) before committing.
|
|
196
|
+
`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`Next steps:
|
|
200
|
+
1. Install peer dependencies:
|
|
201
|
+
npm i ${DEPS}
|
|
202
|
+
2. Start the app:
|
|
203
|
+
npm run dev -> http://localhost:3000 (redirects to /dashboard)
|
|
204
|
+
|
|
205
|
+
Everything landed in YOUR repo — edit app/_components/nav-config.ts, set your
|
|
206
|
+
logo (NEXT_PUBLIC_LOGO_URL in .env), and delete demo pages you don't need.
|
|
207
|
+
Docs: /docs/navigation (sidebar, tabs) and /docs/configuration (logo, env).
|
|
208
|
+
`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viliha/vui-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -33,9 +33,15 @@
|
|
|
33
33
|
"src",
|
|
34
34
|
"!src/**/*.test.ts",
|
|
35
35
|
"!src/**/*.test.tsx",
|
|
36
|
+
"bin",
|
|
37
|
+
"template",
|
|
36
38
|
"AGENT.md",
|
|
37
39
|
"CLAUDE.template.md"
|
|
38
40
|
],
|
|
41
|
+
"bin": {
|
|
42
|
+
"vui-ui": "bin/vui.mjs",
|
|
43
|
+
"vui": "bin/vui.mjs"
|
|
44
|
+
},
|
|
39
45
|
"exports": {
|
|
40
46
|
"./theme.css": "./src/theme.css",
|
|
41
47
|
"./utils": "./src/utils.ts",
|
|
@@ -48,7 +54,9 @@
|
|
|
48
54
|
"lint": "eslint . --max-warnings 0",
|
|
49
55
|
"generate:component": "turbo gen react-component",
|
|
50
56
|
"check-types": "tsgo --noEmit",
|
|
51
|
-
"test": "vitest run"
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"sync-template": "node scripts/sync-template.mjs",
|
|
59
|
+
"prepublishOnly": "node scripts/sync-template.mjs"
|
|
52
60
|
},
|
|
53
61
|
"peerDependencies": {
|
|
54
62
|
"react": "^18 || ^19",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Footer & brand identity — copy to .env.local and edit per deployment.
|
|
2
|
+
# All are optional; defaults are used when unset. NEXT_PUBLIC_ vars are read at
|
|
3
|
+
# build time and inlined into the exported site.
|
|
4
|
+
|
|
5
|
+
NEXT_PUBLIC_COMPANY_NAME="VILIHA PTE. LTD."
|
|
6
|
+
# Optional — links the company name in the footer:
|
|
7
|
+
NEXT_PUBLIC_COMPANY_URL="https://viliha.com"
|
|
8
|
+
NEXT_PUBLIC_LICENSE="MIT Licensed"
|
|
9
|
+
# The copyright year is automatic (always the current/build year) — no env needed.
|
|
10
|
+
|
|
11
|
+
# Optional — your logo image, served from /public; falls back to the built-in
|
|
12
|
+
# mark. e.g. drop public/logo.svg and set:
|
|
13
|
+
# NEXT_PUBLIC_LOGO_URL="/logo.svg"
|
|
14
|
+
|
|
15
|
+
# Override the entire footer line at once (takes precedence over the vars above):
|
|
16
|
+
# NEXT_PUBLIC_FOOTER_NOTICE="© 2026 Acme Inc. · All rights reserved"
|
|
17
|
+
|
|
18
|
+
# Max pages kept open in the tab strip (client-side). Default 5, min 1. Opening
|
|
19
|
+
# more evicts the oldest tab with a warning.
|
|
20
|
+
NEXT_PUBLIC_MAX_TABS="5"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CodeIcon as Hash,
|
|
5
|
+
CubeIcon as Building,
|
|
6
|
+
DotFilledIcon as CircleDot,
|
|
7
|
+
EnvelopeClosedIcon as Mail,
|
|
8
|
+
MobileIcon as Phone,
|
|
9
|
+
SewingPinFilledIcon as MapPin,
|
|
10
|
+
Share2Icon as Network,
|
|
11
|
+
} from "@radix-ui/react-icons";
|
|
12
|
+
|
|
13
|
+
import { usePathname } from "next/navigation";
|
|
14
|
+
|
|
15
|
+
import { Badge } from "@viliha/vui-ui/badge";
|
|
16
|
+
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
|
|
17
|
+
import { branches, type Branch } from "@/lib/mock-data";
|
|
18
|
+
|
|
19
|
+
const fields: RecordField<Branch>[] = [
|
|
20
|
+
{ key: "name", label: "Name", editable: true, required: true, group: "General", hideInTable: true },
|
|
21
|
+
{ key: "organization", label: "Organization", icon: Building, editable: true, width: 180, group: "General" },
|
|
22
|
+
{ key: "code", label: "Code", icon: Hash, editable: true, group: "General" },
|
|
23
|
+
{ key: "email", label: "Email", icon: Mail, editable: true, copyable: true, width: 220, group: "General" },
|
|
24
|
+
{ key: "phone", label: "Phone", icon: Phone, editable: true, copyable: true, width: 160, group: "General" },
|
|
25
|
+
{ key: "city", label: "City", icon: MapPin, editable: true, group: "General" },
|
|
26
|
+
{
|
|
27
|
+
key: "isHeadquarters",
|
|
28
|
+
label: "HQ",
|
|
29
|
+
icon: CircleDot,
|
|
30
|
+
// Wide enough for the "Headquarters" badge so it never spills into Actions.
|
|
31
|
+
width: 150,
|
|
32
|
+
group: "System",
|
|
33
|
+
render: (row) =>
|
|
34
|
+
row.isHeadquarters ? (
|
|
35
|
+
<Badge variant="secondary">Headquarters</Badge>
|
|
36
|
+
) : (
|
|
37
|
+
<Badge variant="muted">Branch</Badge>
|
|
38
|
+
),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export function BranchesTable() {
|
|
43
|
+
const pathname = usePathname();
|
|
44
|
+
return (
|
|
45
|
+
<RecordView
|
|
46
|
+
persistKey={pathname}
|
|
47
|
+
title="Branches"
|
|
48
|
+
singular="Branch"
|
|
49
|
+
icon={Network}
|
|
50
|
+
fields={fields}
|
|
51
|
+
initialData={branches}
|
|
52
|
+
getPrimary={(row) => ({
|
|
53
|
+
title: row.name,
|
|
54
|
+
subtitle: row.organization,
|
|
55
|
+
initials: (row.code || row.name).slice(0, 2).toUpperCase(),
|
|
56
|
+
})}
|
|
57
|
+
makeEmptyRow={() => ({
|
|
58
|
+
id: Date.now(),
|
|
59
|
+
organization: "",
|
|
60
|
+
name: "",
|
|
61
|
+
code: "",
|
|
62
|
+
email: "",
|
|
63
|
+
phone: "",
|
|
64
|
+
city: "",
|
|
65
|
+
isHeadquarters: false,
|
|
66
|
+
})}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pageMeta } from "@/lib/seo";
|
|
2
|
+
|
|
3
|
+
import { BranchesTable } from "./branches-table";
|
|
4
|
+
|
|
5
|
+
export const metadata = pageMeta("/branches");
|
|
6
|
+
|
|
7
|
+
export default function BranchesPage() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="h-full">
|
|
10
|
+
<BranchesTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BackpackIcon as Briefcase,
|
|
5
|
+
CodeIcon as Hash,
|
|
6
|
+
TextAlignLeftIcon as AlignLeft,
|
|
7
|
+
} from "@radix-ui/react-icons";
|
|
8
|
+
|
|
9
|
+
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
|
|
10
|
+
import { businesses, type Business } from "@/lib/mock-data";
|
|
11
|
+
|
|
12
|
+
const fields: RecordField<Business>[] = [
|
|
13
|
+
{ key: "title", label: "Title", editable: true, required: true, group: "General", hideInTable: true },
|
|
14
|
+
{ key: "code", label: "Code", icon: Hash, editable: true, group: "General" },
|
|
15
|
+
{ key: "description", label: "Description", icon: AlignLeft, editable: true, width: 360, group: "General" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function BusinessesTable() {
|
|
19
|
+
return (
|
|
20
|
+
<RecordView
|
|
21
|
+
title="Businesses"
|
|
22
|
+
singular="Business"
|
|
23
|
+
icon={Briefcase}
|
|
24
|
+
fields={fields}
|
|
25
|
+
initialData={businesses}
|
|
26
|
+
getPrimary={(row) => ({
|
|
27
|
+
title: row.title,
|
|
28
|
+
subtitle: row.code,
|
|
29
|
+
initials: row.code.slice(0, 2).toUpperCase(),
|
|
30
|
+
})}
|
|
31
|
+
makeEmptyRow={() => ({
|
|
32
|
+
id: Date.now(),
|
|
33
|
+
title: "",
|
|
34
|
+
code: "",
|
|
35
|
+
description: "",
|
|
36
|
+
})}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pageMeta } from "@/lib/seo";
|
|
2
|
+
|
|
3
|
+
import { BusinessesTable } from "./businesses-table";
|
|
4
|
+
|
|
5
|
+
export const metadata = pageMeta("/businesses");
|
|
6
|
+
|
|
7
|
+
export default function BusinessesPage() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="h-full">
|
|
10
|
+
<BusinessesTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|