@waveinno/ui 0.8.0 → 0.9.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/CHANGELOG.md +17 -0
- package/LICENSE +1 -1
- package/README.md +17 -5
- package/bin/starter-files.d.mts +8 -0
- package/bin/starter-files.mjs +136 -0
- package/bin/waveinno-ui.mjs +466 -0
- package/dist/components/ui/nid-input.js +1 -1
- package/dist/components/ui/phone-input.js +1 -1
- package/package.json +19 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ prop, major = a renamed or removed export or prop.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.9.0 — 2026-10-03
|
|
10
|
+
|
|
11
|
+
- **`npx @waveinno/ui init`**: one-command setup.
|
|
12
|
+
- It asks whether you build with Next.js or React + Vite, the currency (taka by default in Bangladesh) and the
|
|
13
|
+
package manager. It can create the app with the official starter.
|
|
14
|
+
- It shows the plan before changing anything, then installs the package and Tailwind CSS 4 and writes the
|
|
15
|
+
stylesheet, fonts, providers and theme script.
|
|
16
|
+
- Starter files are replaced and kept as `.bak`; your own files are left alone, with a note. It also warns when
|
|
17
|
+
your CSS reuses a token name (`--text`, `--border`…).
|
|
18
|
+
- `npx @waveinno/ui doctor` checks a setup. `--yes` and flags for CI.
|
|
19
|
+
- **Wavi**: the design system has a name and a mark. The docs site has a new home page, a "Powered by WAVEINNO" footer,
|
|
20
|
+
and search and AI-friendly metadata: sitemap, robots.txt, llms.txt, Open Graph image, and structured data
|
|
21
|
+
(SoftwareApplication, Organization, FAQPage).
|
|
22
|
+
- **Templates** download as Next.js **or React + Vite** (React Router, a page per route, loaded on demand).
|
|
23
|
+
- PhoneInput and NidInput adapt to their width: in a narrow column the operator or ID-kind label shrinks to its tick,
|
|
24
|
+
so the number keeps its room.
|
|
25
|
+
|
|
9
26
|
## 0.8.0 — 2026-10-03
|
|
10
27
|
|
|
11
28
|
- **Bangladesh kit** (new, free):
|
package/LICENSE
CHANGED
|
@@ -25,4 +25,4 @@ SOFTWARE.
|
|
|
25
25
|
This licence covers the @waveinno/ui package: everything published to npm,
|
|
26
26
|
and the free blocks in playground/blocks/free. The Pro blocks and templates
|
|
27
27
|
(playground/blocks/pro) are not covered; they are licensed separately under
|
|
28
|
-
the
|
|
28
|
+
the Wavi Pro Licence in playground/blocks/pro/LICENSE.md.
|
package/README.md
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
# @waveinno/ui
|
|
1
|
+
# Wavi — @waveinno/ui
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**Wavi** is the React design system from Bangladesh, made by WAVEINNO: 100+ components, an application shell, charts
|
|
4
|
+
and Bangladesh-ready inputs (bKash, taka, +880, Bangla dates), styled with Tailwind CSS 4 tokens, in light and dark, in
|
|
5
|
+
English and Bangla. For Next.js and Vite. Shipped as compiled JavaScript with type definitions.
|
|
6
|
+
|
|
7
|
+
## Set up in one command
|
|
5
8
|
|
|
6
9
|
```bash
|
|
7
|
-
|
|
10
|
+
npx @waveinno/ui init
|
|
8
11
|
```
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
Run it in your app (or in an empty folder to create one). It asks whether you build with **Next.js** or **React +
|
|
14
|
+
Vite**, your currency and package manager, shows every change it will make, then installs the package and Tailwind
|
|
15
|
+
CSS 4 and writes the stylesheet, fonts, providers and theme script. It never overwrites your own files.
|
|
16
|
+
`npx @waveinno/ui doctor` checks a setup. In CI: `npx @waveinno/ui init --yes --framework next --currency BDT`.
|
|
17
|
+
|
|
18
|
+
## Set up by hand
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @waveinno/ui
|
|
22
|
+
```
|
|
11
23
|
|
|
12
24
|
Full guides, with every file: run the docs (`npm run dev`, then http://localhost:4500/docs) and open **Installation →
|
|
13
25
|
Next.js** or **Vite**. In short:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const NEXT_GLOBALS_CSS: string;
|
|
2
|
+
export declare const NEXT_PROVIDERS: string;
|
|
3
|
+
export declare const NEXT_LAYOUT: string;
|
|
4
|
+
export declare const VITE_CONFIG: string;
|
|
5
|
+
export declare const VITE_CSS: string;
|
|
6
|
+
export declare const VITE_MAIN: string;
|
|
7
|
+
export declare const FONTS_HREF: string;
|
|
8
|
+
export declare const VITE_ROUTER: string;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files every app on WAVEINNO UI starts with, in one place: `npx @waveinno/ui init`
|
|
3
|
+
* writes them, the installation guides show them, the template downloads ship them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** app/globals.css of a Next.js app. Its @source is relative to app/; init adjusts it for src/app/. */
|
|
7
|
+
export const NEXT_GLOBALS_CSS = `@import "tailwindcss";
|
|
8
|
+
@import "@waveinno/ui/styles.css";
|
|
9
|
+
|
|
10
|
+
/* Tailwind only generates classes it can see. Point it at the package,
|
|
11
|
+
relative to this file: "../node_modules" for app/globals.css,
|
|
12
|
+
"../../node_modules" for src/app/globals.css. */
|
|
13
|
+
@source "../node_modules/@waveinno/ui/dist";
|
|
14
|
+
|
|
15
|
+
/* The fonts loaded in layout.tsx, in the package's font slots.
|
|
16
|
+
Noto Sans Bengali carries Bangla and the Taka sign (৳), which Inter lacks. */
|
|
17
|
+
:root {
|
|
18
|
+
--font-inter: var(--font-body), var(--font-bangla);
|
|
19
|
+
--font-jakarta: var(--font-display), var(--font-bangla);
|
|
20
|
+
--font-jetbrains: var(--font-code);
|
|
21
|
+
}`;
|
|
22
|
+
|
|
23
|
+
/** app/providers.tsx */
|
|
24
|
+
export const NEXT_PROVIDERS = `"use client";
|
|
25
|
+
|
|
26
|
+
import { Toaster, TooltipProvider } from "@waveinno/ui";
|
|
27
|
+
import { NextRouterProvider } from "@waveinno/ui/next";
|
|
28
|
+
|
|
29
|
+
/** Once, around the whole app: Next.js routing for the package, tooltips, toasts. */
|
|
30
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
31
|
+
return (
|
|
32
|
+
<NextRouterProvider>
|
|
33
|
+
<TooltipProvider delayDuration={300}>
|
|
34
|
+
{children}
|
|
35
|
+
<Toaster />
|
|
36
|
+
</TooltipProvider>
|
|
37
|
+
</NextRouterProvider>
|
|
38
|
+
);
|
|
39
|
+
}`;
|
|
40
|
+
|
|
41
|
+
/** app/layout.tsx */
|
|
42
|
+
export const NEXT_LAYOUT = `import type { Metadata } from "next";
|
|
43
|
+
import { Inter, JetBrains_Mono, Noto_Sans_Bengali, Plus_Jakarta_Sans } from "next/font/google";
|
|
44
|
+
|
|
45
|
+
import { themeBootScript } from "@waveinno/ui";
|
|
46
|
+
|
|
47
|
+
import "./globals.css";
|
|
48
|
+
import { Providers } from "./providers";
|
|
49
|
+
|
|
50
|
+
const body = Inter({ subsets: ["latin"], variable: "--font-body" });
|
|
51
|
+
const display = Plus_Jakarta_Sans({ subsets: ["latin"], weight: ["500", "600", "700"], variable: "--font-display" });
|
|
52
|
+
const code = JetBrains_Mono({ subsets: ["latin"], variable: "--font-code" });
|
|
53
|
+
const bangla = Noto_Sans_Bengali({ subsets: ["bengali"], variable: "--font-bangla" });
|
|
54
|
+
|
|
55
|
+
export const metadata: Metadata = { title: "My app" };
|
|
56
|
+
|
|
57
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
58
|
+
return (
|
|
59
|
+
// The boot script sets data-theme before React runs, so the attribute
|
|
60
|
+
// differs from the server's HTML on purpose.
|
|
61
|
+
<html lang="en" suppressHydrationWarning className={[body.variable, display.variable, code.variable, bangla.variable].join(" ")}>
|
|
62
|
+
<head>
|
|
63
|
+
{/* Applies the saved light/dark theme and density before first paint: no flash. */}
|
|
64
|
+
<script dangerouslySetInnerHTML={{ __html: themeBootScript() }} />
|
|
65
|
+
</head>
|
|
66
|
+
<body className="bg-canvas text-body antialiased">
|
|
67
|
+
<Providers>{children}</Providers>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
);
|
|
71
|
+
}`;
|
|
72
|
+
|
|
73
|
+
/** vite.config.ts */
|
|
74
|
+
export const VITE_CONFIG = `import tailwindcss from "@tailwindcss/vite";
|
|
75
|
+
import react from "@vitejs/plugin-react";
|
|
76
|
+
import { defineConfig } from "vite";
|
|
77
|
+
|
|
78
|
+
export default defineConfig({
|
|
79
|
+
plugins: [react(), tailwindcss()],
|
|
80
|
+
});`;
|
|
81
|
+
|
|
82
|
+
/** src/index.css of a Vite app. */
|
|
83
|
+
export const VITE_CSS = `@import "tailwindcss";
|
|
84
|
+
@import "@waveinno/ui/styles.css";
|
|
85
|
+
|
|
86
|
+
/* Tailwind only generates classes it can see: point it at the package. */
|
|
87
|
+
@source "../node_modules/@waveinno/ui/dist";
|
|
88
|
+
|
|
89
|
+
/* Noto Sans Bengali after each family: it carries Bangla and the Taka sign (৳). */
|
|
90
|
+
:root {
|
|
91
|
+
--font-inter: "Inter", "Noto Sans Bengali";
|
|
92
|
+
--font-jakarta: "Plus Jakarta Sans", "Noto Sans Bengali";
|
|
93
|
+
--font-jetbrains: "JetBrains Mono";
|
|
94
|
+
}`;
|
|
95
|
+
|
|
96
|
+
/** src/main.tsx */
|
|
97
|
+
export const VITE_MAIN = `import { StrictMode } from "react";
|
|
98
|
+
import { createRoot } from "react-dom/client";
|
|
99
|
+
|
|
100
|
+
import { Toaster, TooltipProvider } from "@waveinno/ui";
|
|
101
|
+
|
|
102
|
+
import App from "./App";
|
|
103
|
+
import "./index.css";
|
|
104
|
+
|
|
105
|
+
createRoot(document.getElementById("root")!).render(
|
|
106
|
+
<StrictMode>
|
|
107
|
+
<TooltipProvider delayDuration={300}>
|
|
108
|
+
<App />
|
|
109
|
+
<Toaster />
|
|
110
|
+
</TooltipProvider>
|
|
111
|
+
</StrictMode>,
|
|
112
|
+
);`;
|
|
113
|
+
|
|
114
|
+
/** src/ui-router.tsx: the package's links and navigation through React Router. */
|
|
115
|
+
export const VITE_ROUTER = `import { Link, useLocation, useNavigate } from "react-router";
|
|
116
|
+
|
|
117
|
+
import { RouterProvider, type RouterAdapter } from "@waveinno/ui";
|
|
118
|
+
|
|
119
|
+
// Tell the package to link and navigate through React Router.
|
|
120
|
+
const reactRouter: RouterAdapter = {
|
|
121
|
+
Link: ({ href, ...props }) => <Link to={href} {...props} />,
|
|
122
|
+
usePathname: () => useLocation().pathname,
|
|
123
|
+
useNavigate: () => {
|
|
124
|
+
const navigate = useNavigate();
|
|
125
|
+
return (href) => navigate(href);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Inside <BrowserRouter> (or your data router's root route):
|
|
130
|
+
export function UiRouter({ children }: { children: React.ReactNode }) {
|
|
131
|
+
return <RouterProvider adapter={reactRouter}>{children}</RouterProvider>;
|
|
132
|
+
}`;
|
|
133
|
+
|
|
134
|
+
/** The four families both setups load, for a plain HTML page. */
|
|
135
|
+
export const FONTS_HREF =
|
|
136
|
+
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@500;600;700&family=JetBrains+Mono:wght@400;500&family=Noto+Sans+Bengali:wght@400;500;600;700&display=swap";
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npx @waveinno/ui init set up WAVEINNO UI in a Next.js or React (Vite) app
|
|
4
|
+
* npx @waveinno/ui doctor check a setup and say what is missing
|
|
5
|
+
*
|
|
6
|
+
* init asks what you are building (Next.js or React + Vite), creates the app
|
|
7
|
+
* if there is none, asks the currency and the package manager, shows every
|
|
8
|
+
* change it will make, and only then makes them: the package and Tailwind's
|
|
9
|
+
* plugin, the CSS, the providers, the layout or entry, exactly as the
|
|
10
|
+
* installation guides show.
|
|
11
|
+
*
|
|
12
|
+
* It never overwrites your own code: a file is replaced only while it is still
|
|
13
|
+
* the starter's default (create-next-app, create-vite), and then the old one is
|
|
14
|
+
* kept as <file>.bak. Anything else is left alone and you get the line to add.
|
|
15
|
+
*
|
|
16
|
+
* --yes takes the defaults without asking (for scripts and CI); every question
|
|
17
|
+
* also has a flag: --framework next|vite, --currency BDT, --pm npm|pnpm|yarn|bun,
|
|
18
|
+
* --name my-app. --dry-run shows the plan and stops. --skip-install, --cwd <dir>.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import readline from "node:readline";
|
|
24
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
|
+
|
|
26
|
+
import { FONTS_HREF, NEXT_GLOBALS_CSS, NEXT_LAYOUT, NEXT_PROVIDERS, VITE_CONFIG, VITE_CSS } from "./starter-files.mjs";
|
|
27
|
+
|
|
28
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(HERE, "..", "package.json"), "utf8"));
|
|
30
|
+
const DOCS = "https://www.npmjs.com/package/@waveinno/ui";
|
|
31
|
+
|
|
32
|
+
/* ── output ─────────────────────────────────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
35
|
+
const c = (code) => (s) => (color ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
36
|
+
const bold = c(1);
|
|
37
|
+
const dim = c(2);
|
|
38
|
+
const green = c(32);
|
|
39
|
+
const yellow = c(33);
|
|
40
|
+
const red = c(31);
|
|
41
|
+
const blue = c(34);
|
|
42
|
+
const cyan = c(36);
|
|
43
|
+
const magenta = c(35);
|
|
44
|
+
const say = (s = "") => console.log(s);
|
|
45
|
+
const done = (s) => say(` ${green("✓")} ${s}`);
|
|
46
|
+
const skip = (s) => say(` ${dim("·")} ${dim(s)}`);
|
|
47
|
+
const warn = (s) => say(` ${yellow("!")} ${s}`);
|
|
48
|
+
const heading = (s) => say(`\n ${bold(s)}`);
|
|
49
|
+
const fail = (s) => {
|
|
50
|
+
say(`\n ${red("✗")} ${s}\n`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/* ── args ───────────────────────────────────────────────────────────────── */
|
|
55
|
+
|
|
56
|
+
const argv = process.argv.slice(2);
|
|
57
|
+
const flag = (name) => argv.includes(`--${name}`);
|
|
58
|
+
const opt = (name) => {
|
|
59
|
+
const i = argv.indexOf(`--${name}`);
|
|
60
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
61
|
+
};
|
|
62
|
+
// The first word that is neither a flag nor the value of a flag that takes one.
|
|
63
|
+
const VALUED = new Set(["--cwd", "--pm", "--currency", "--framework", "--name"]);
|
|
64
|
+
const command = argv.find((a, i) => !a.startsWith("-") && !VALUED.has(argv[i - 1] ?? "")) ?? "help";
|
|
65
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !flag("yes");
|
|
66
|
+
let cwd = path.resolve(opt("cwd") ?? process.cwd());
|
|
67
|
+
/** While planning, nothing is written or installed; the same steps print what they would do. */
|
|
68
|
+
let dry = true;
|
|
69
|
+
|
|
70
|
+
/* ── prompts (no dependencies: readline in raw mode) ────────────────────── */
|
|
71
|
+
|
|
72
|
+
/** An arrow-key list. Returns the chosen value. */
|
|
73
|
+
function select(question, choices, initial = 0) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const out = process.stdout;
|
|
76
|
+
const inp = process.stdin;
|
|
77
|
+
let i = Math.max(0, initial);
|
|
78
|
+
const n = choices.length;
|
|
79
|
+
const line = (ch, on) => `\x1b[2K ${on ? cyan("❯") : " "} ${on ? bold(ch.label) : ch.label}${ch.hint ? ` ${dim(ch.hint)}` : ""}`;
|
|
80
|
+
out.write(`\n ${green("?")} ${bold(question)} ${dim("↑ ↓ to move, Enter to choose")}\n`);
|
|
81
|
+
out.write(`${choices.map((ch, k) => line(ch, k === i)).join("\n")}\n`);
|
|
82
|
+
out.write("\x1b[?25l");
|
|
83
|
+
readline.emitKeypressEvents(inp);
|
|
84
|
+
inp.setRawMode(true);
|
|
85
|
+
inp.resume();
|
|
86
|
+
const onKey = (_, key = {}) => {
|
|
87
|
+
if (key.ctrl && key.name === "c") {
|
|
88
|
+
inp.setRawMode(false);
|
|
89
|
+
out.write("\x1b[?25h\n");
|
|
90
|
+
process.exit(130);
|
|
91
|
+
}
|
|
92
|
+
if (key.name === "up" || key.name === "k") i = (i - 1 + n) % n;
|
|
93
|
+
else if (key.name === "down" || key.name === "j" || key.name === "tab") i = (i + 1) % n;
|
|
94
|
+
else if (key.name === "return") {
|
|
95
|
+
inp.off("keypress", onKey);
|
|
96
|
+
inp.setRawMode(false);
|
|
97
|
+
inp.pause();
|
|
98
|
+
// Fold the list into one answered line.
|
|
99
|
+
out.write(`\x1b[${n + 1}A\x1b[0J ${green("✔")} ${bold(question)} ${cyan(choices[i].label)}\n\x1b[?25h`);
|
|
100
|
+
resolve(choices[i].value);
|
|
101
|
+
return;
|
|
102
|
+
} else return;
|
|
103
|
+
out.write(`\x1b[${n}A${choices.map((ch, k) => line(ch, k === i)).join("\n")}\n`);
|
|
104
|
+
};
|
|
105
|
+
inp.on("keypress", onKey);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A line of text, with a default. */
|
|
110
|
+
function ask(question, fallback) {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
113
|
+
rl.question(`\n ${green("?")} ${bold(question)} ${dim(`(${fallback})`)} `, (a) => {
|
|
114
|
+
rl.close();
|
|
115
|
+
resolve(a.trim() || fallback);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── the project ────────────────────────────────────────────────────────── */
|
|
121
|
+
|
|
122
|
+
const rel = (p) => path.relative(cwd, p).split(path.sep).join("/");
|
|
123
|
+
const exists = (p) => fs.existsSync(path.join(cwd, p));
|
|
124
|
+
const read = (p) => fs.readFileSync(path.join(cwd, p), "utf8");
|
|
125
|
+
const first = (...ps) => ps.find(exists);
|
|
126
|
+
|
|
127
|
+
function write(p, content, why = "wrote", note = "") {
|
|
128
|
+
if (dry) return say(` ${blue("+")} ${why} ${bold(p)} ${dim(note)}`);
|
|
129
|
+
fs.mkdirSync(path.dirname(path.join(cwd, p)), { recursive: true });
|
|
130
|
+
fs.writeFileSync(path.join(cwd, p), content);
|
|
131
|
+
done(`${why} ${bold(p)} ${dim(note)}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Replace a starter file, keeping the old one as .bak. */
|
|
135
|
+
function replace(p, content, why) {
|
|
136
|
+
if (!dry) fs.copyFileSync(path.join(cwd, p), path.join(cwd, `${p}.bak`));
|
|
137
|
+
write(p, content, why, `(the old one is kept as ${p}.bak)`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const readPackage = () => (exists("package.json") ? JSON.parse(read("package.json")) : null);
|
|
141
|
+
const deps = (pkg) => ({ ...pkg?.dependencies, ...pkg?.devDependencies });
|
|
142
|
+
const detect = (pkg) => (deps(pkg).next ? "next" : deps(pkg).vite ? "vite" : null);
|
|
143
|
+
|
|
144
|
+
function lockfilePm() {
|
|
145
|
+
if (exists("pnpm-lock.yaml")) return "pnpm";
|
|
146
|
+
if (exists("yarn.lock")) return "yarn";
|
|
147
|
+
if (exists("bun.lockb") || exists("bun.lock")) return "bun";
|
|
148
|
+
if (exists("package-lock.json")) return "npm";
|
|
149
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
150
|
+
for (const m of ["pnpm", "yarn", "bun"]) if (agent.startsWith(m)) return m;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function run(cmd, args, where = cwd) {
|
|
155
|
+
say(dim(` $ ${cmd} ${args.join(" ")}`));
|
|
156
|
+
// On Windows npm is a .cmd, which only runs through a shell; the arguments are names and flags, so one string is safe.
|
|
157
|
+
const r = process.platform === "win32" ? spawnSync(`${cmd} ${args.join(" ")}`, { cwd: where, stdio: "inherit", shell: true }) : spawnSync(cmd, args, { cwd: where, stdio: "inherit" });
|
|
158
|
+
return r.status === 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function install(pm, pkgs, dev) {
|
|
162
|
+
if (!pkgs.length) return;
|
|
163
|
+
const args = [pm === "npm" ? "install" : "add", ...(dev ? [pm === "bun" ? "-d" : "-D"] : []), ...pkgs];
|
|
164
|
+
if (dry) return say(` ${blue("+")} install ${bold(pkgs.join(", "))}${dev ? dim(" (dev)") : ""}`);
|
|
165
|
+
if (flag("skip-install")) return warn(`Run yourself: ${bold(`${pm} ${args.join(" ")}`)}`);
|
|
166
|
+
if (!run(pm, args)) fail(`${pm} ${args.join(" ")} failed. Run it yourself, then run init again.`);
|
|
167
|
+
done(`installed ${pkgs.join(", ")}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The @source line, relative to the CSS file. */
|
|
171
|
+
function sourceLine(cssFile) {
|
|
172
|
+
const from = path.dirname(path.join(cwd, cssFile));
|
|
173
|
+
const to = path.join(cwd, "node_modules", "@waveinno", "ui", "dist");
|
|
174
|
+
return `@source "${path.relative(from, to).split(path.sep).join("/")}";`;
|
|
175
|
+
}
|
|
176
|
+
const withSource = (css, cssFile) => css.replace(/@source "[^"]+";/, sourceLine(cssFile));
|
|
177
|
+
|
|
178
|
+
/** Add the currency line after the imports of a providers or entry file. */
|
|
179
|
+
function withCurrency(src, currency, importLine) {
|
|
180
|
+
if (!currency) return src;
|
|
181
|
+
const lines = src.split("\n");
|
|
182
|
+
let last = -1;
|
|
183
|
+
lines.forEach((l, k) => /^import /.test(l) && (last = k));
|
|
184
|
+
lines.splice(last + 1, 0, "", `// Amounts show in ${currency}. Use another ISO code, or remove this line to follow the app shell.`, `useSettings.setState({ overrideCode: "${currency}" });`);
|
|
185
|
+
return lines.join("\n").replace(importLine, importLine.replace("TooltipProvider }", "TooltipProvider, useSettings }"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ── the steps (the same code plans and applies) ────────────────────────── */
|
|
189
|
+
|
|
190
|
+
function setupCss(file, starter, create) {
|
|
191
|
+
if (!file) return write(create, `${withSource(starter, create)}\n`, "write", "(there was no global stylesheet)");
|
|
192
|
+
const css = read(file);
|
|
193
|
+
if (css.includes("@waveinno/ui/styles.css")) {
|
|
194
|
+
if (!css.includes("@source")) warn(`${file} imports the styles but has no @source line; add: ${sourceLine(file)}`);
|
|
195
|
+
return skip(`${file} already imports @waveinno/ui`);
|
|
196
|
+
}
|
|
197
|
+
// A starter stylesheet (create-next-app, create-vite, old and new) sets fonts, colours and body
|
|
198
|
+
// layout that would fight the design system; the newer Vite one even reuses token names.
|
|
199
|
+
const STARTER = /Arial, Helvetica|--font-geist|#646cff|#aa3bff|--text-h:|--social-bg|place-items: center|^\s*@import "tailwindcss";\s*$/m;
|
|
200
|
+
const isStarter = STARTER.test(css) && css.length < 8000;
|
|
201
|
+
const next = withSource(starter, file);
|
|
202
|
+
if (isStarter || !css.trim()) return replace(file, `${next}\n`, "set up");
|
|
203
|
+
write(file, `${next}\n\n/* ── Your styles ── */\n${css.replace(/^@import ["']tailwindcss["'];\s*/m, "")}`, "add WAVEINNO UI to the top of");
|
|
204
|
+
// Your own variables with a token's name would recolour every component.
|
|
205
|
+
const clash = [...new Set([...css.matchAll(/(--(?:text|body|muted|border2?|primary|surface|canvas|raised|hover|accent|ink|brand-[12]|success|warning|danger|info)):/g)].map((m) => m[1]))];
|
|
206
|
+
if (clash.length) warn(`${file} sets ${clash.join(", ")}: the same names as WAVEINNO UI's tokens, so components change colour. Rename yours.`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stepsNext(pm, currency) {
|
|
210
|
+
const appDir = exists("src/app") ? "src/app" : exists("app") ? "app" : null;
|
|
211
|
+
if (!appDir) fail("This Next.js app has no app/ directory. WAVEINNO UI's setup is for the App Router.");
|
|
212
|
+
const d = deps(readPackage());
|
|
213
|
+
|
|
214
|
+
heading("Packages");
|
|
215
|
+
if (d["@waveinno/ui"]) skip(`@waveinno/ui ${d["@waveinno/ui"]} is installed`);
|
|
216
|
+
install(pm, d["@waveinno/ui"] ? [] : [`@waveinno/ui@^${PKG.version}`], false);
|
|
217
|
+
install(pm, ["tailwindcss", "@tailwindcss/postcss"].filter((x) => !d[x]), true);
|
|
218
|
+
|
|
219
|
+
heading("PostCSS");
|
|
220
|
+
const postcss = first("postcss.config.mjs", "postcss.config.js", "postcss.config.cjs", "postcss.config.ts");
|
|
221
|
+
if (!postcss) write("postcss.config.mjs", `export default { plugins: { "@tailwindcss/postcss": {} } };\n`, "write");
|
|
222
|
+
else if (read(postcss).includes("@tailwindcss/postcss")) skip(`${postcss} loads Tailwind`);
|
|
223
|
+
else warn(`${postcss} does not load Tailwind 4. Add: plugins: { "@tailwindcss/postcss": {} }`);
|
|
224
|
+
|
|
225
|
+
heading("CSS");
|
|
226
|
+
setupCss(first(`${appDir}/globals.css`, `${appDir}/global.css`), NEXT_GLOBALS_CSS, `${appDir}/globals.css`);
|
|
227
|
+
|
|
228
|
+
heading("Providers");
|
|
229
|
+
const providers = first(`${appDir}/providers.tsx`, `${appDir}/providers.jsx`);
|
|
230
|
+
if (!providers) write(`${appDir}/providers.tsx`, `${withCurrency(NEXT_PROVIDERS, currency, 'import { Toaster, TooltipProvider } from "@waveinno/ui";')}\n`, "write", "router, tooltips, toasts" + (currency ? `, amounts in ${currency}` : ""));
|
|
231
|
+
else if (read(providers).includes("@waveinno/ui")) skip(`${providers} already uses @waveinno/ui`);
|
|
232
|
+
else warn(`${providers} is your own: wrap its children in <NextRouterProvider> and <TooltipProvider>, and add <Toaster />.`);
|
|
233
|
+
|
|
234
|
+
heading("Layout");
|
|
235
|
+
const layout = first(`${appDir}/layout.tsx`, `${appDir}/layout.jsx`);
|
|
236
|
+
if (!layout) return write(`${appDir}/layout.tsx`, `${NEXT_LAYOUT}\n`, "write");
|
|
237
|
+
const src = read(layout);
|
|
238
|
+
if (src.includes("themeBootScript") && src.includes("Providers")) return skip(`${layout} is set up`);
|
|
239
|
+
if (/Geist|Create Next App/.test(src) && src.length < 2000) {
|
|
240
|
+
const title = /title:\s*["']([^"']+)["']/.exec(src)?.[1];
|
|
241
|
+
return replace(layout, `${NEXT_LAYOUT.replace('"My app"', JSON.stringify(title && title !== "Create Next App" ? title : "My app"))}\n`, "set up");
|
|
242
|
+
}
|
|
243
|
+
warn(`${layout} is your own, so it stays as it is. Add three things:`);
|
|
244
|
+
say(dim(` import { themeBootScript } from "@waveinno/ui"; import { Providers } from "./providers";`));
|
|
245
|
+
say(dim(` <html suppressHydrationWarning> and <head><script dangerouslySetInnerHTML={{ __html: themeBootScript() }} /></head>`));
|
|
246
|
+
say(dim(` <body><Providers>{children}</Providers></body>`));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function stepsVite(pm, currency) {
|
|
250
|
+
const d = deps(readPackage());
|
|
251
|
+
|
|
252
|
+
heading("Packages");
|
|
253
|
+
if (d["@waveinno/ui"]) skip(`@waveinno/ui ${d["@waveinno/ui"]} is installed`);
|
|
254
|
+
install(pm, d["@waveinno/ui"] ? [] : [`@waveinno/ui@^${PKG.version}`], false);
|
|
255
|
+
install(pm, ["tailwindcss", "@tailwindcss/vite"].filter((x) => !d[x]), true);
|
|
256
|
+
|
|
257
|
+
heading("Vite");
|
|
258
|
+
const config = first("vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs");
|
|
259
|
+
if (!config) write("vite.config.ts", `${VITE_CONFIG}\n`, "write");
|
|
260
|
+
else {
|
|
261
|
+
const src = read(config);
|
|
262
|
+
if (src.includes("@tailwindcss/vite")) skip(`${config} loads Tailwind`);
|
|
263
|
+
else if (/plugins:\s*\[react\(\)\]/.test(src)) write(config, `import tailwindcss from "@tailwindcss/vite";\n${src.replace(/plugins:\s*\[react\(\)\]/, "plugins: [react(), tailwindcss()]")}`, "add Tailwind to");
|
|
264
|
+
else warn(`Add tailwindcss() from "@tailwindcss/vite" to the plugins in ${config}.`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
heading("CSS");
|
|
268
|
+
setupCss(first("src/index.css", "src/main.css", "src/styles.css"), VITE_CSS, "src/index.css");
|
|
269
|
+
const entry = first("src/main.tsx", "src/main.jsx");
|
|
270
|
+
if (entry && !/import ["']\.\/(index|main|styles)\.css["']/.test(read(entry))) warn(`Import the stylesheet once in ${entry}: import "./index.css";`);
|
|
271
|
+
|
|
272
|
+
heading("index.html");
|
|
273
|
+
if (!exists("index.html")) warn("No index.html: add the fonts and the theme script from the guide to your page.");
|
|
274
|
+
else {
|
|
275
|
+
let html = read("index.html");
|
|
276
|
+
if (html.includes("waveinno.theme") || html.includes("Noto+Sans+Bengali")) skip("index.html is set up");
|
|
277
|
+
else {
|
|
278
|
+
// The same script as themeBootScript(), from the package itself.
|
|
279
|
+
const { themeBootScript } = await import(pathToFileURL(path.join(HERE, "..", "dist", "lib", "boot-script.js")).href);
|
|
280
|
+
const head = ` <link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
281
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
282
|
+
<link rel="stylesheet" href="${FONTS_HREF}" />
|
|
283
|
+
<script>${themeBootScript()}</script>
|
|
284
|
+
</head>`;
|
|
285
|
+
html = html.replace(/\s*<\/head>/, `\n${head}`);
|
|
286
|
+
if (/<body(?![^>]*class=)/.test(html)) html = html.replace(/<body/, '<body class="bg-canvas text-body antialiased"');
|
|
287
|
+
write("index.html", html, "add the fonts and the theme script to");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
heading("Entry");
|
|
292
|
+
const main = first("src/main.tsx", "src/main.jsx");
|
|
293
|
+
if (!main) return warn("No src/main.tsx: wrap your app in <TooltipProvider> and add <Toaster /> once.");
|
|
294
|
+
const src = read(main);
|
|
295
|
+
if (src.includes("@waveinno/ui")) return skip(`${main} already uses @waveinno/ui`);
|
|
296
|
+
if (!/<App \/>/.test(src)) return warn(`Wrap your app in <TooltipProvider> and add <Toaster /> once in ${main}.`);
|
|
297
|
+
const imp = 'import { Toaster, TooltipProvider } from "@waveinno/ui";';
|
|
298
|
+
const wrapped = src.replace(/(\s*)<App \/>/, "$1<TooltipProvider delayDuration={300}>$1 <App />$1 <Toaster />$1</TooltipProvider>");
|
|
299
|
+
write(main, withCurrency(`${imp}\n${wrapped}`, currency, imp), "wrap the app with tooltips and toasts in", currency ? `(amounts in ${currency})` : "");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ── init ───────────────────────────────────────────────────────────────── */
|
|
303
|
+
|
|
304
|
+
const FRAMEWORKS = [
|
|
305
|
+
{ value: "next", label: "Next.js", hint: "App Router, server rendering, routing built in" },
|
|
306
|
+
{ value: "vite", label: "React + Vite", hint: "a single-page app, quick to start" },
|
|
307
|
+
];
|
|
308
|
+
const CURRENCIES = [
|
|
309
|
+
{ value: "BDT", label: "৳ BDT", hint: "Bangladeshi taka" },
|
|
310
|
+
{ value: "USD", label: "$ USD", hint: "US dollar" },
|
|
311
|
+
{ value: "INR", label: "₹ INR", hint: "Indian rupee" },
|
|
312
|
+
{ value: "EUR", label: "€ EUR", hint: "Euro" },
|
|
313
|
+
{ value: "", label: "Follow the app shell", hint: "set it per organisation later" },
|
|
314
|
+
];
|
|
315
|
+
const PMS = ["npm", "pnpm", "yarn", "bun"];
|
|
316
|
+
|
|
317
|
+
async function init() {
|
|
318
|
+
say(`\n ${magenta("◆")} ${bold("WAVEINNO UI")} ${dim(`v${PKG.version}`)} ${dim("— the setup")}`);
|
|
319
|
+
say(dim(` ${cwd}`));
|
|
320
|
+
|
|
321
|
+
// 1. What it is, or what to make.
|
|
322
|
+
let pkg = readPackage();
|
|
323
|
+
const found = detect(pkg);
|
|
324
|
+
let framework = opt("framework") ?? found;
|
|
325
|
+
if (!framework && !interactive) fail("No Next.js or Vite app here. Run init in your app's folder, or pass --framework next|vite to create one.");
|
|
326
|
+
if (interactive && !opt("framework")) {
|
|
327
|
+
framework = await select(
|
|
328
|
+
pkg ? "What is this app built with?" : "What would you like to build?",
|
|
329
|
+
FRAMEWORKS.map((f) => ({ ...f, label: f.value === found ? `${f.label} ${green("(detected)")}` : f.label })),
|
|
330
|
+
found ? FRAMEWORKS.findIndex((f) => f.value === found) : 0,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (!["next", "vite"].includes(framework)) fail(`--framework takes next or vite, not "${framework}".`);
|
|
334
|
+
if (found && framework !== found) warn(`This folder is a ${found === "next" ? "Next.js" : "Vite"} app; setting it up as ${framework === "next" ? "Next.js" : "Vite"} may not work.`);
|
|
335
|
+
|
|
336
|
+
// 2. The package manager: from the lockfile, else ask.
|
|
337
|
+
let pm = opt("pm") ?? lockfilePm();
|
|
338
|
+
if (!pm) pm = interactive ? await select("Which package manager?", PMS.map((p) => ({ value: p, label: p }))) : "npm";
|
|
339
|
+
if (!PMS.includes(pm)) fail(`--pm takes ${PMS.join(", ")}.`);
|
|
340
|
+
|
|
341
|
+
// 3. No app yet: make one with the official starter, then set it up.
|
|
342
|
+
if (!pkg) {
|
|
343
|
+
const name = opt("name") ?? (interactive ? await ask("Name of the new app?", "my-app") : "my-app");
|
|
344
|
+
if (fs.existsSync(path.join(cwd, name))) fail(`${name} already exists here. Choose another name, or run init inside it.`);
|
|
345
|
+
if (flag("dry-run")) {
|
|
346
|
+
heading("Plan");
|
|
347
|
+
say(` ${blue("+")} create ${bold(name)} with ${framework === "next" ? "create-next-app" : "create-vite"}, then set it up`);
|
|
348
|
+
return say();
|
|
349
|
+
}
|
|
350
|
+
heading(`Creating ${name}`);
|
|
351
|
+
const ok =
|
|
352
|
+
framework === "next"
|
|
353
|
+
? run("npx", ["--yes", "create-next-app@latest", name, "--ts", "--tailwind", "--app", "--eslint", "--yes", `--use-${pm}`])
|
|
354
|
+
: run("npx", ["--yes", "create-vite@latest", name, "--template", "react-ts"]) && run(pm, ["install"], path.join(cwd, name));
|
|
355
|
+
if (!ok) fail("Creating the app failed; see the output above.");
|
|
356
|
+
cwd = path.join(cwd, name);
|
|
357
|
+
pkg = readPackage();
|
|
358
|
+
if (!pkg) fail(`The starter did not create ${name}/package.json.`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 4. The currency amounts show in.
|
|
362
|
+
const tzBangladesh = Intl.DateTimeFormat().resolvedOptions().timeZone === "Asia/Dhaka";
|
|
363
|
+
let currency = opt("currency");
|
|
364
|
+
if (currency === undefined) {
|
|
365
|
+
currency = interactive ? await select("Which currency do amounts show in?", CURRENCIES, tzBangladesh ? 0 : 1) : tzBangladesh ? "BDT" : "";
|
|
366
|
+
}
|
|
367
|
+
currency = currency.toUpperCase().replace(/[^A-Z]/g, "");
|
|
368
|
+
|
|
369
|
+
// 5. The plan: the same steps, printing instead of doing.
|
|
370
|
+
say(`\n ${bold("The plan")} ${dim(`${framework === "next" ? "Next.js" : "React + Vite"} · ${pm}${currency ? ` · ${currency}` : ""}`)}`);
|
|
371
|
+
dry = true;
|
|
372
|
+
if (framework === "next") stepsNext(pm, currency);
|
|
373
|
+
else await stepsVite(pm, currency);
|
|
374
|
+
if (flag("dry-run")) return say(dim("\n Dry run: nothing was changed.\n"));
|
|
375
|
+
|
|
376
|
+
if (interactive && !(await select("Set it up?", [{ value: true, label: "Yes, make these changes" }, { value: false, label: "No, leave everything as it is" }]))) {
|
|
377
|
+
return say(dim("\n Nothing was changed.\n"));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 6. Do it.
|
|
381
|
+
say(`\n ${bold("Setting up")}`);
|
|
382
|
+
dry = false;
|
|
383
|
+
if (framework === "next") stepsNext(pm, currency);
|
|
384
|
+
else await stepsVite(pm, currency);
|
|
385
|
+
|
|
386
|
+
const runDev = pm === "npm" ? "npm run dev" : `${pm} dev`;
|
|
387
|
+
const into = path.relative(process.cwd(), cwd);
|
|
388
|
+
say(`\n ${green("◆")} ${bold("Ready.")}`);
|
|
389
|
+
say(` ${into ? `cd ${into} && ` : ""}${bold(runDev)}`);
|
|
390
|
+
say(dim(` import { Button, toast } from "@waveinno/ui";`));
|
|
391
|
+
say(dim(` Check the setup any time: npx @waveinno/ui doctor · ${DOCS}\n`));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* ── doctor ─────────────────────────────────────────────────────────────── */
|
|
395
|
+
|
|
396
|
+
function doctor() {
|
|
397
|
+
say(`\n ${magenta("◆")} ${bold("WAVEINNO UI doctor")} ${dim(cwd)}\n`);
|
|
398
|
+
const pkg = readPackage();
|
|
399
|
+
if (!pkg) fail(`No package.json in ${cwd}.`);
|
|
400
|
+
const d = deps(pkg);
|
|
401
|
+
const fw = detect(pkg);
|
|
402
|
+
let bad = 0;
|
|
403
|
+
const check = (ok, good, fix) => {
|
|
404
|
+
if (ok) done(good);
|
|
405
|
+
else {
|
|
406
|
+
bad++;
|
|
407
|
+
say(` ${red("✗")} ${fix}`);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
check(!!fw, `${fw === "next" ? "Next.js" : "Vite"} app`, "Not a Next.js or Vite app");
|
|
411
|
+
check(!!d["@waveinno/ui"], `@waveinno/ui ${d["@waveinno/ui"] ?? ""}`, "@waveinno/ui is not installed: npm install @waveinno/ui");
|
|
412
|
+
check(/^\^?4|^latest/.test(d.tailwindcss ?? ""), `tailwindcss ${d.tailwindcss}`, "Tailwind CSS 4 is not installed");
|
|
413
|
+
if (fw === "next") check(!!d["@tailwindcss/postcss"], "@tailwindcss/postcss", "@tailwindcss/postcss is missing: npm install -D @tailwindcss/postcss");
|
|
414
|
+
if (fw === "vite") check(!!d["@tailwindcss/vite"], "@tailwindcss/vite", "@tailwindcss/vite is missing: npm install -D @tailwindcss/vite");
|
|
415
|
+
|
|
416
|
+
const cssFile = fw === "next" ? first("src/app/globals.css", "app/globals.css") : first("src/index.css", "src/main.css", "src/styles.css");
|
|
417
|
+
const css = cssFile ? read(cssFile) : "";
|
|
418
|
+
check(css.includes("@waveinno/ui/styles.css"), `${cssFile} imports @waveinno/ui/styles.css`, `${cssFile ?? "Your global CSS"} does not import @waveinno/ui/styles.css`);
|
|
419
|
+
const src = /@source "([^"]+)";/.exec(css)?.[1];
|
|
420
|
+
const resolved = src && cssFile ? path.resolve(path.dirname(path.join(cwd, cssFile)), src) : null;
|
|
421
|
+
check(
|
|
422
|
+
!!resolved && fs.existsSync(resolved),
|
|
423
|
+
`@source points at ${resolved ? rel(resolved) : ""}`,
|
|
424
|
+
src ? `@source "${src}" does not exist from ${cssFile}; it should be ${sourceLine(cssFile)}` : `No @source line: add ${cssFile ? sourceLine(cssFile) : '@source "…/node_modules/@waveinno/ui/dist";'}`,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
if (fw === "next") {
|
|
428
|
+
const appDir = exists("src/app") ? "src/app" : "app";
|
|
429
|
+
const layout = first(`${appDir}/layout.tsx`, `${appDir}/layout.jsx`);
|
|
430
|
+
check(!!layout && read(layout).includes("themeBootScript"), "The theme script runs before paint", `${layout ?? "layout"}: add themeBootScript() in <head>, or dark mode flashes`);
|
|
431
|
+
const providers = first(`${appDir}/providers.tsx`, `${appDir}/providers.jsx`);
|
|
432
|
+
const p = providers ? read(providers) : "";
|
|
433
|
+
check(p.includes("Toaster"), "Toaster is mounted", "No <Toaster />: toasts will not show");
|
|
434
|
+
check(p.includes("NextRouterProvider"), "Links use the Next.js router", "No <NextRouterProvider>: links in the shell reload the page");
|
|
435
|
+
} else if (fw === "vite") {
|
|
436
|
+
const html = exists("index.html") ? read("index.html") : "";
|
|
437
|
+
check(html.includes("waveinno.theme"), "The theme script runs before paint", "index.html: add the theme script, or dark mode flashes");
|
|
438
|
+
const main = first("src/main.tsx", "src/main.jsx");
|
|
439
|
+
check(!!main && read(main).includes("Toaster"), "Toaster is mounted", "No <Toaster />: toasts will not show");
|
|
440
|
+
}
|
|
441
|
+
say(bad ? `\n ${yellow(`${bad} to fix.`)} ${dim("npx @waveinno/ui init sets these up.")}\n` : `\n ${green(bold("All good."))}\n`);
|
|
442
|
+
process.exit(bad ? 1 : 0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* ── main ───────────────────────────────────────────────────────────────── */
|
|
446
|
+
|
|
447
|
+
if (flag("version") || command === "version") say(PKG.version);
|
|
448
|
+
else if (command === "init") await init();
|
|
449
|
+
else if (command === "doctor") doctor();
|
|
450
|
+
else {
|
|
451
|
+
say(`
|
|
452
|
+
${magenta("◆")} ${bold("WAVEINNO UI")} ${dim(`v${PKG.version}`)}
|
|
453
|
+
|
|
454
|
+
${bold("npx @waveinno/ui init")} set up a Next.js or React (Vite) app, or create one
|
|
455
|
+
${bold("npx @waveinno/ui doctor")} check the setup
|
|
456
|
+
|
|
457
|
+
${dim("--yes take the defaults, ask nothing")}
|
|
458
|
+
${dim("--framework next next or vite")}
|
|
459
|
+
${dim("--currency BDT the currency amounts show in (BDT is the default in Bangladesh)")}
|
|
460
|
+
${dim("--pm npm npm, pnpm, yarn or bun (default: from the lockfile)")}
|
|
461
|
+
${dim("--name my-app the name of a new app")}
|
|
462
|
+
${dim("--dry-run show the plan, change nothing")}
|
|
463
|
+
${dim("--skip-install print the install commands instead of running them")}
|
|
464
|
+
${dim("--cwd <dir> the folder (default: here)")}
|
|
465
|
+
`);
|
|
466
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as
|
|
1
|
+
"use client";import{jsx as a,jsxs as f}from"react/jsx-runtime";import{Check as y,IdCard as w}from"lucide-react";import*as c from"react";import{cn as g}from"../../lib/utils.js";import{fieldStateClass as C}from"./input.js";const d={10:"smart",13:"old13",17:"old17"},m={smart:"Smart card",old13:"Old NID",old17:"Old NID, with birth year"},p=e=>e.replace(/[০-৯]/g,l=>String("\u09E6\u09E7\u09E8\u09E9\u09EA\u09EB\u09EC\u09ED\u09EE\u09EF".indexOf(l))).replace(/\D/g,"").slice(0,17);function S(e){const l=e.length<=10?[3,3,4]:e.length<=13?[4,4,5]:[4,4,4,5],s=[];let n=0;for(const r of l){if(n>=e.length)break;s.push(e.slice(n,n+r)),n+=r}return s.join(" ")}const K=c.forwardRef(({value:e,defaultValue:l="",onValueChange:s,state:n="default",labels:r,className:h,disabled:u,placeholder:x="123 456 7890",...N},b)=>{const[I,k]=c.useState(()=>p(l)),i=e!==void 0?p(e):I,t=d[i.length]??null;return f("div",{className:g("@container flex h-ctl w-full items-center gap-2 rounded-ctl border bg-raised px-3 shadow-[inset_0_1px_1px_rgba(5,8,22,.04)] transition-[border-color,box-shadow]",C[n],u&&"cursor-not-allowed bg-hover opacity-70",h),children:[a(w,{className:"size-4 shrink-0 text-muted"}),a("input",{ref:b,type:"text",inputMode:"numeric",autoComplete:"off",disabled:u,placeholder:x,value:S(i),onChange:v=>{const o=p(v.target.value);e===void 0&&k(o),s?.(o,{valid:!!d[o.length],kind:d[o.length]??null})},className:"num h-full min-w-[14ch] flex-1 bg-transparent text-[13.5px] tracking-[0.04em] text-text outline-none placeholder:tracking-normal placeholder:text-placeholder",...N}),i?f("span",{title:t?r?.[t]??m[t]:void 0,className:g("inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-medium",t?"bg-success-subtle text-success @max-[420px]:px-1.5":"bg-hover text-muted"),children:[t?a(y,{className:"size-3",strokeWidth:3}):null,t?a("span",{className:"@max-[420px]:sr-only",children:r?.[t]??m[t]}):`${i.length} / ${i.length<10?10:i.length<13?13:17}`]}):null]})});K.displayName="NidInput";export{K as NidInput};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as
|
|
1
|
+
"use client";import{jsx as t,jsxs as d}from"react/jsx-runtime";import{Check as k}from"lucide-react";import*as h from"react";import{BD_OPERATOR_NAMES as b,parseBdPhone as o}from"../../lib/bd-geo.js";import{cn as g}from"../../lib/utils.js";import{fieldStateClass as C}from"./input.js";const M=e=>e.length>4?`${e.slice(0,4)}-${e.slice(4)}`:e,R=h.forwardRef(({value:e,defaultValue:v="",onValueChange:u,showOperator:P=!0,state:x="default",labels:c,className:N,disabled:m,placeholder:w="1712-345678",...I},y)=>{const s=e!==void 0,[_,f]=h.useState(()=>o(v).national),i=s?o(e).national:_,n=o(i),a=i.length>=2&&!n.operator;return d("div",{className:g("@container flex h-ctl w-full items-center overflow-hidden rounded-ctl border bg-raised text-[13px] shadow-[inset_0_1px_1px_rgba(5,8,22,.04)] transition-[border-color,box-shadow]",C[a?"error":x],m&&"cursor-not-allowed bg-hover opacity-70",N),children:[d("span",{className:"flex h-full shrink-0 items-center gap-1.5 border-r border-border bg-surface px-2.5 text-muted @max-[230px]:px-2",children:[t("span",{"aria-hidden":!0,className:"relative h-3 w-[18px] overflow-hidden rounded-[2px] bg-[#006a4e] @max-[230px]:hidden",children:t("span",{className:"absolute left-[38%] top-1/2 size-[7px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#f42a41]"})}),t("span",{className:"num font-medium text-text",children:"+880"})]}),t("input",{ref:y,type:"tel",inputMode:"tel",autoComplete:"tel-national",disabled:m,placeholder:w,"aria-invalid":a||x==="error"||void 0,value:M(i),onChange:l=>{const r=o(l.target.value);s||f(r.national),u?.(r.e164,r)},onPaste:l=>{const r=l.clipboardData.getData("text");if(!r)return;l.preventDefault();const p=o(r);s||f(p.national),u?.(p.e164,p)},className:"num h-full min-w-[10ch] flex-1 bg-transparent px-3 text-text outline-none placeholder:text-placeholder disabled:cursor-not-allowed @max-[230px]:px-2",...I}),P&&(n.operator||a)?d("span",{title:a?c?.invalidPrefix??"Not a mobile prefix":b[n.operator],className:g("mr-2 inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-[11.5px] font-medium @max-[290px]:min-h-5 @max-[290px]:px-1.5",a?"bg-danger-subtle text-danger":n.valid?"bg-success-subtle text-success":"bg-hover text-muted"),children:[n.valid?t(k,{className:"size-3",strokeWidth:3}):t("span",{"aria-hidden":!0,className:"hidden size-1.5 rounded-full bg-current @max-[290px]:block"}),t("span",{className:"@max-[290px]:sr-only",children:a?c?.invalidPrefix??"Not a mobile prefix":b[n.operator]})]}):null]})});R.displayName="PhoneInput";export{R as PhoneInput};
|
package/package.json
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waveinno/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Wavi: the React design system from Bangladesh. 100+ components, app shell, charts, and bKash/taka/Bangla-ready inputs for Next.js and Vite, with Tailwind CSS 4. Set up with npx @waveinno/ui init.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"waveinno",
|
|
7
7
|
"design-system",
|
|
8
8
|
"react",
|
|
9
9
|
"nextjs",
|
|
10
10
|
"tailwindcss",
|
|
11
|
-
"radix-ui"
|
|
11
|
+
"radix-ui",
|
|
12
|
+
"wavi",
|
|
13
|
+
"react-components",
|
|
14
|
+
"component-library",
|
|
15
|
+
"ui-kit",
|
|
16
|
+
"tailwind-css-4",
|
|
17
|
+
"vite",
|
|
18
|
+
"dark-mode",
|
|
19
|
+
"bangladesh",
|
|
20
|
+
"bangla",
|
|
21
|
+
"bkash",
|
|
22
|
+
"taka",
|
|
23
|
+
"dashboard"
|
|
12
24
|
],
|
|
13
25
|
"license": "MIT",
|
|
14
26
|
"author": "WAVEINNO <info@waveinno.com>",
|
|
@@ -40,6 +52,7 @@
|
|
|
40
52
|
},
|
|
41
53
|
"files": [
|
|
42
54
|
"dist",
|
|
55
|
+
"bin",
|
|
43
56
|
"README.md",
|
|
44
57
|
"LICENSE",
|
|
45
58
|
"CHANGELOG.md"
|
|
@@ -113,5 +126,8 @@
|
|
|
113
126
|
"react-dom": "^19.3.0",
|
|
114
127
|
"tailwindcss": "^4.3.3",
|
|
115
128
|
"typescript": "^7.0.2"
|
|
129
|
+
},
|
|
130
|
+
"bin": {
|
|
131
|
+
"waveinno-ui": "bin/waveinno-ui.mjs"
|
|
116
132
|
}
|
|
117
133
|
}
|