@esuiss/ui 0.5.9 → 0.7.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/README.md CHANGED
@@ -47,8 +47,9 @@ export function Example() {
47
47
  2. Adds the `@/*` import alias to `tsconfig.json` if it is missing.
48
48
  3. Installs and wires up Tailwind CSS if needed (`postcss.config.mjs` for Next.js, `vite.config.ts` for Vite).
49
49
  4. Writes the theme stylesheet to `app/globals.css` (Next.js) or `src/index.css` (Vite). It asks before replacing a stylesheet that already has content.
50
- 5. Installs [`@esuiss/tailwind`](https://www.npmjs.com/package/@esuiss/tailwind), which provides the variants, animations, and utilities the components rely on.
51
- 6. Adds the `button` component.
50
+ 5. Sets up the font: the system font on macOS and iOS, Inter everywhere else. Inter is loaded with `next/font/google` in the root layout (Next.js), or with `@fontsource-variable/inter` imported from the stylesheet (Vite).
51
+ 6. Installs [`@esuiss/tailwind`](https://www.npmjs.com/package/@esuiss/tailwind), which provides the variants, animations, and utilities the components rely on.
52
+ 7. Adds the `button` component.
52
53
 
53
54
  ## Commands
54
55
 
@@ -62,9 +63,10 @@ npx @esuiss/ui@latest init [options]
62
63
 
63
64
  ```bash
64
65
  npx @esuiss/ui@latest add <component...> [options]
66
+ npx @esuiss/ui@latest add patterns <pattern...> [options]
65
67
  ```
66
68
 
67
- Components are written to `components/ui/` (and hooks to `hooks/`) under your `@/*` alias root. Components they depend on are added too, and missing npm packages are installed with your package manager (npm, pnpm, yarn, or bun).
69
+ Components are written to `components/ui/`, patterns to `components/patterns/` and hooks to `hooks/` under your `@/*` alias root. Components they depend on are added too, and missing npm packages are installed with your package manager (npm, pnpm, yarn, or bun).
68
70
 
69
71
  ### Options
70
72
 
@@ -72,7 +74,7 @@ Components are written to `components/ui/` (and hooks to `hooks/`) under your `@
72
74
  | --- | --- |
73
75
  | `-o, --overwrite` | Replace local files that differ from the registry |
74
76
  | `--diff` | Show how your local files differ from the registry |
75
- | `-a, --all` | Add every component |
77
+ | `-a, --all` | Add every component, or every pattern with `add patterns` |
76
78
  | `-y, --yes` | Skip confirmation prompts |
77
79
  | `-c, --cwd <dir>` | Run in another project directory |
78
80
  | `-h, --help` | Show help |
@@ -96,28 +98,43 @@ Your stylesheet holds the whole theme:
96
98
  ```css
97
99
  @import "tailwindcss";
98
100
  @import "@esuiss/tailwind";
101
+ @import "@fontsource-variable/inter";
99
102
 
100
103
  :root {
101
- --background: oklch(1 0 0);
102
- --primary: oklch(0.6321 0.2018 254.09);
103
- --radius: 0.75rem;
104
+ --surface: oklch(1 0 0);
105
+ --label: oklch(0.2316 0.0038 286.09);
106
+ --accent: oklch(0.5629 0.1933 256.16);
107
+ --radius: 0.625rem;
104
108
  }
105
109
 
106
110
  .dark {
107
- --background: oklch(0 0 0);
108
- --primary: oklch(0.651471 0.191972 251.4696);
111
+ --surface: oklch(0 0 0);
112
+ --label: oklch(0.9707 0.0026 286.29);
113
+ --accent: oklch(0.5629 0.1933 256.16);
109
114
  }
110
115
 
111
116
  @theme inline {
112
- --font-sans: system-ui, sans-serif;
113
- --color-background: var(--background);
114
- --color-primary: var(--primary);
117
+ --font-sans: -apple-system, BlinkMacSystemFont, "Inter Variable", sans-serif;
118
+ --color-surface: var(--surface);
119
+ --color-label: var(--label);
120
+ --color-accent: var(--accent);
115
121
  }
116
122
  ```
117
123
 
124
+ Colors are named by role, not by component:
125
+
126
+ - **Surfaces:** `surface`, `surface-secondary`, `surface-tertiary`, `surface-raised` (menus, dialogs, cards).
127
+ - **Text:** `label`, `label-secondary`, `label-tertiary`, `label-quaternary`.
128
+ - **Control fills:** `fill`, `fill-secondary`, `fill-tertiary`.
129
+ - **Lines:** `separator`, `separator-strong`.
130
+ - **Scrim:** `scrim`, the dimmed layer behind dialogs, sheets and drawers.
131
+ - **Accent:** `accent`, `accent-hover`, `accent-pressed`, `on-accent` (text on the accent).
132
+ - **Links, focus and states:** `link`, `focus`, `danger`, `success`, `warning` and their `-surface` tints.
133
+ - **Palette:** `blue`, `green`, `orange`, `red`, `yellow`, `purple`, `pink`, `indigo`, `teal`, `mint`, `cyan`, `brown`, `gray`, used by charts.
134
+
118
135
  - **Change a color:** edit its value under `:root` (light) and `.dark` (dark).
119
136
  - **Add a color:** define it under `:root` and `.dark`, then map it in `@theme inline`, for example `--color-brand: var(--brand);`. Classes such as `bg-brand` and `text-brand` become available.
120
- - **Change the font:** set `--font-sans` in `@theme inline`. By default each device's own system font is used, so nothing has to be downloaded.
137
+ - **Change the font:** set `--font-sans` in `@theme inline`. By default it uses the system font on macOS and iOS and Inter elsewhere.
121
138
  - **Corner radius:** `--radius` scales every rounded component.
122
139
 
123
140
  ## Dark mode
@@ -142,10 +159,21 @@ export default function RootLayout({ children }: { children: React.ReactNode })
142
159
 
143
160
  ## Components
144
161
 
145
- accordion, alert, alert-dialog, aspect-ratio, attachment, avatar, badge, breadcrumb, bubble, button, button-group, calendar, card, carousel, chart, checkbox, collapsible, combobox, command, context-menu, dialog, direction, drawer, dropdown-menu, empty, field, hover-card, input, input-group, input-otp, item, kbd, label, marker, menubar, message, message-scroller, native-select, navigation-menu, pagination, popover, progress, questionnaire, radio-group, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, spinner, switch, table, tabs, textarea, toast, toggle, toggle-group, tooltip
162
+ accordion, alert, alert-dialog, aspect-ratio, attachment, avatar, badge, breadcrumb, bubble, button, button-group, calendar, card, carousel, chart, checkbox, collapsible, combobox, command, context-menu, dialog, direction, drawer, dropdown-menu, empty, field, hover-card, input, input-group, input-otp, item, kbd, label, marker, menubar, message, message-scroller, native-select, navigation-menu, pagination, popover, progress, questionnaire, radio-group, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, spinner, switch, table, tabs, textarea, toast, toggle, toggle-group, tooltip, wheel-picker
146
163
 
147
164
  Hooks: use-message-scroller, use-mobile, use-questionnaire
148
165
 
166
+ ## Patterns
167
+
168
+ Patterns combine several components into one ready-made piece, such as a date and time picker. They are not added by `init` or `add --all`; add them by name, or all at once:
169
+
170
+ ```bash
171
+ npx @esuiss/ui add patterns date-time-picker
172
+ npx @esuiss/ui add patterns --all
173
+ ```
174
+
175
+ The components a pattern uses are added with it.
176
+
149
177
  ## Troubleshooting
150
178
 
151
179
  - **"Tailwind CSS 4.2 or later is required"**: upgrade with `npm install tailwindcss@latest` and run `init` again.
package/index.mjs CHANGED
@@ -15,11 +15,12 @@ const usage = `${name} ${version}
15
15
  Usage:
16
16
  npx ${name} init [options] set up Tailwind, the theme, and the button
17
17
  npx ${name} add <component...> [options] add components, e.g. add button sidebar
18
+ npx ${name} add patterns <pattern...> add patterns, e.g. add patterns date-time-picker
18
19
 
19
20
  Options:
20
21
  -o, --overwrite replace local files that differ from the registry
21
22
  --diff show how local files differ from the registry
22
- -a, --all add every component
23
+ -a, --all add every component, or every pattern after "patterns"
23
24
  -y, --yes skip confirmation prompts
24
25
  -c, --cwd <dir> project directory (defaults to the current directory)
25
26
  -h, --help show this help
@@ -79,8 +80,9 @@ async function main() {
79
80
  await init(parseArgs(args).options)
80
81
  break
81
82
  case "add": {
82
- const { options, names } = parseArgs(args)
83
- await add(names, options)
83
+ const patterns = args[0] === "patterns"
84
+ const { options, names } = parseArgs(patterns ? args.slice(1) : args)
85
+ await add(names, { ...options, patterns })
84
86
  break
85
87
  }
86
88
  case "-v":
package/lib/add.mjs CHANGED
@@ -4,7 +4,12 @@ import path from "node:path"
4
4
  import { formatDiff } from "./diff.mjs"
5
5
  import { CliError, color, list, note, step } from "./output.mjs"
6
6
  import { detectProject, findAliasRoot, install } from "./project.mjs"
7
- import { fetchRegistry, fetchText, resolveItems } from "./registry.mjs"
7
+ import {
8
+ fetchRegistry,
9
+ fetchText,
10
+ isPattern,
11
+ resolveItems,
12
+ } from "./registry.mjs"
8
13
 
9
14
  async function fetchFiles(items) {
10
15
  return Promise.all(
@@ -93,6 +98,40 @@ async function showDiff(project, root, registry, names) {
93
98
  if (!changes) step("No differences from the registry.")
94
99
  }
95
100
 
101
+ function selectNames(registry, names, { all, patterns }) {
102
+ const inScope = (item) => isPattern(item) === patterns
103
+ const scoped = registry.items.filter(inScope)
104
+ const kind = patterns ? "pattern" : "component"
105
+ const command = patterns ? "add patterns" : "add"
106
+
107
+ if (all) {
108
+ if (!scoped.length) throw new CliError(`No ${kind}s are available yet.`)
109
+ return scoped.map((item) => item.name)
110
+ }
111
+
112
+ if (!names.length) {
113
+ throw new CliError(
114
+ `Name at least one ${kind}, e.g. npx @esuiss/ui ${command} ${patterns ? "<name>" : "button"}`
115
+ )
116
+ }
117
+
118
+ const byName = new Map(registry.items.map((item) => [item.name, item]))
119
+ for (const name of names) {
120
+ const item = byName.get(name)
121
+ if (item && inScope(item)) continue
122
+ if (item) {
123
+ const other = patterns ? "add" : "add patterns"
124
+ throw new CliError(
125
+ `"${name}" is ${patterns ? "a component" : "a pattern"}. Run npx @esuiss/ui ${other} ${name}`
126
+ )
127
+ }
128
+ throw new CliError(
129
+ `Unknown ${kind}: ${name}.\nAvailable: ${scoped.map((entry) => entry.name).join(", ") || "none yet"}`
130
+ )
131
+ }
132
+ return names
133
+ }
134
+
96
135
  export async function add(names, options) {
97
136
  const project = detectProject(options.cwd)
98
137
  const root = findAliasRoot(project.cwd)
@@ -103,12 +142,7 @@ export async function add(names, options) {
103
142
  }
104
143
 
105
144
  const registry = await fetchRegistry()
106
- const selected = options.all ? registry.items.map((item) => item.name) : names
107
- if (!selected.length) {
108
- throw new CliError(
109
- "Name at least one component, e.g. npx @esuiss/ui add button"
110
- )
111
- }
145
+ const selected = selectNames(registry, names, options)
112
146
 
113
147
  if (options.diff) await showDiff(project, root, registry, selected)
114
148
  else await installItems(project, root, registry, selected, options)
package/lib/init.mjs CHANGED
@@ -39,6 +39,19 @@ const TOOLING = {
39
39
  vite: ["tailwindcss", "@tailwindcss/vite", "@types/node"],
40
40
  }
41
41
  const MINIMUM_TAILWIND = [4, 2]
42
+ const FONT_IMPORT = '@import "@fontsource-variable/inter";\n'
43
+ const FONT_FAMILY =
44
+ '--font-sans: -apple-system, BlinkMacSystemFont, "Inter Variable", sans-serif;'
45
+ const NEXT_FONT_FAMILY =
46
+ "--font-sans: -apple-system, BlinkMacSystemFont, var(--font-inter), sans-serif;"
47
+ const NEXT_LAYOUTS = [
48
+ "src/app/layout.tsx",
49
+ "app/layout.tsx",
50
+ "src/app/layout.jsx",
51
+ "app/layout.jsx",
52
+ ]
53
+ const NEXT_FONT_NOTE =
54
+ 'Load Inter in your root layout with next/font/google: Inter({ subsets: ["latin", "latin-ext"], variable: "--font-inter" }), and add its .variable to the <html> className.'
42
55
 
43
56
  function relative(project, file) {
44
57
  return path.relative(project.cwd, file)
@@ -184,6 +197,62 @@ function configureVite(project) {
184
197
  return manual
185
198
  }
186
199
 
200
+ function stylesheetFor(project, css) {
201
+ if (project.framework !== "next") return css
202
+ return css.replace(FONT_IMPORT, "").replace(FONT_FAMILY, NEXT_FONT_FAMILY)
203
+ }
204
+
205
+ function configureNextFont(project) {
206
+ const file = NEXT_LAYOUTS.map((name) => path.join(project.cwd, name)).find(
207
+ existsSync
208
+ )
209
+ if (!file) return [NEXT_FONT_NOTE]
210
+
211
+ let text = readFileSync(file, "utf8")
212
+ if (/variable:\s*["']--font-inter["']/.test(text)) return []
213
+
214
+ const html = text.match(/<html\b[^>]*>/)
215
+ if (!html) return [NEXT_FONT_NOTE]
216
+ let tag = html[0]
217
+ const literal = tag.match(/className="([^"]*)"/)
218
+ if (literal) {
219
+ tag = tag.replace(
220
+ literal[0],
221
+ `className={\`\${fontInter.variable} ${literal[1]}\`}`
222
+ )
223
+ } else if (!/className=/.test(tag)) {
224
+ tag = tag.replace(/<html\b/, "<html className={fontInter.variable}")
225
+ } else {
226
+ return [NEXT_FONT_NOTE]
227
+ }
228
+ text = text.replace(html[0], tag)
229
+
230
+ const semi = /from\s+["'][^"']+["'];/.test(text) ? ";" : ""
231
+ const fontImport = text.match(
232
+ /import\s*\{([^}]*)\}\s*from\s*["']next\/font\/google["']/
233
+ )
234
+ if (fontImport) {
235
+ text = text.replace(
236
+ fontImport[0],
237
+ fontImport[0].replace(fontImport[1], `${fontImport[1].trimEnd()}, Inter `)
238
+ )
239
+ } else {
240
+ text = `import { Inter } from "next/font/google"${semi}\n${text}`
241
+ }
242
+
243
+ const imports = [
244
+ ...text.matchAll(/^import\s[\s\S]*?["'][^"']+["'];?[ \t]*\n/gm),
245
+ ]
246
+ const last = imports.at(-1)
247
+ const at = last ? last.index + last[0].length : 0
248
+ const declaration = `\nconst fontInter = Inter({\n subsets: ["latin", "latin-ext"],\n variable: "--font-inter",\n})${semi}\n`
249
+ text = text.slice(0, at) + declaration + text.slice(at)
250
+
251
+ writeFileSync(file, text)
252
+ step(`Loaded Inter in ${relative(project, file)}`)
253
+ return []
254
+ }
255
+
187
256
  function configureNext(project) {
188
257
  const existing = POSTCSS_CONFIGS.map((name) =>
189
258
  path.join(project.cwd, name)
@@ -230,7 +299,7 @@ export async function init(options) {
230
299
  }
231
300
 
232
301
  const registry = await fetchRegistry()
233
- const css = await fetchStylesheet(registry)
302
+ const css = stylesheetFor(project, await fetchStylesheet(registry))
234
303
 
235
304
  if (ensureAlias(project, sourceRoot))
236
305
  step("Added the @/* import alias to tsconfig")
@@ -241,7 +310,7 @@ export async function init(options) {
241
310
  const manual =
242
311
  project.framework === "vite"
243
312
  ? configureVite(project)
244
- : configureNext(project)
313
+ : [...configureNext(project), ...configureNextFont(project)]
245
314
 
246
315
  writeFileSync(stylesheet, css)
247
316
  step(`Wrote ${relative(project, stylesheet)}`)
package/lib/registry.mjs CHANGED
@@ -20,14 +20,12 @@ export async function fetchRegistry() {
20
20
  return JSON.parse(await fetchText("registry.json"))
21
21
  }
22
22
 
23
+ export function isPattern(item) {
24
+ return item.type === "pattern"
25
+ }
26
+
23
27
  export function resolveItems(registry, names) {
24
28
  const byName = new Map(registry.items.map((item) => [item.name, item]))
25
- const unknown = names.filter((name) => !byName.has(name))
26
- if (unknown.length) {
27
- throw new CliError(
28
- `Unknown component: ${unknown.join(", ")}.\nAvailable: ${[...byName.keys()].join(", ")}`
29
- )
30
- }
31
29
 
32
30
  const ordered = []
33
31
  const seen = new Set()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esuiss/ui",
3
- "version": "0.5.9",
3
+ "version": "0.7.0",
4
4
  "description": "Copy-paste React components with a clean, minimal design, built on Base UI and Tailwind CSS v4. CLI for Next.js and React (Vite).",
5
5
  "keywords": [
6
6
  "react",