@esuiss/ui 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ 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. Sets up the Inter font: with `next/font/google` in the root layout (Next.js), or with `@fontsource-variable/inter` imported from the stylesheet (Vite).
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
51
  6. Installs [`@esuiss/tailwind`](https://www.npmjs.com/package/@esuiss/tailwind), which provides the variants, animations, and utilities the components rely on.
52
52
  7. Adds the `button` component.
53
53
 
@@ -63,9 +63,11 @@ npx @esuiss/ui@latest init [options]
63
63
 
64
64
  ```bash
65
65
  npx @esuiss/ui@latest add <component...> [options]
66
+ npx @esuiss/ui@latest add patterns <pattern...> [options]
67
+ npx @esuiss/ui@latest add interactions <interaction...> [options]
66
68
  ```
67
69
 
68
- 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).
70
+ Components are written to `components/ui/`, patterns to `components/patterns/`, interactions to `components/interactions/` 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
71
 
70
72
  ### Options
71
73
 
@@ -73,7 +75,7 @@ Components are written to `components/ui/` (and hooks to `hooks/`) under your `@
73
75
  | --- | --- |
74
76
  | `-o, --overwrite` | Replace local files that differ from the registry |
75
77
  | `--diff` | Show how your local files differ from the registry |
76
- | `-a, --all` | Add every component |
78
+ | `-a, --all` | Add every component, or every pattern or interaction with `add patterns` / `add interactions` |
77
79
  | `-y, --yes` | Skip confirmation prompts |
78
80
  | `-c, --cwd <dir>` | Run in another project directory |
79
81
  | `-h, --help` | Show help |
@@ -113,7 +115,7 @@ Your stylesheet holds the whole theme:
113
115
  }
114
116
 
115
117
  @theme inline {
116
- --font-sans: "Inter Variable", sans-serif;
118
+ --font-sans: -apple-system, BlinkMacSystemFont, "Inter Variable", sans-serif;
117
119
  --color-surface: var(--surface);
118
120
  --color-label: var(--label);
119
121
  --color-accent: var(--accent);
@@ -126,13 +128,14 @@ Colors are named by role, not by component:
126
128
  - **Text:** `label`, `label-secondary`, `label-tertiary`, `label-quaternary`.
127
129
  - **Control fills:** `fill`, `fill-secondary`, `fill-tertiary`.
128
130
  - **Lines:** `separator`, `separator-strong`.
131
+ - **Scrim:** `scrim`, the dimmed layer behind dialogs, sheets and drawers.
129
132
  - **Accent:** `accent`, `accent-hover`, `accent-pressed`, `on-accent` (text on the accent).
130
133
  - **Links, focus and states:** `link`, `focus`, `danger`, `success`, `warning` and their `-surface` tints.
131
134
  - **Palette:** `blue`, `green`, `orange`, `red`, `yellow`, `purple`, `pink`, `indigo`, `teal`, `mint`, `cyan`, `brown`, `gray`, used by charts.
132
135
 
133
136
  - **Change a color:** edit its value under `:root` (light) and `.dark` (dark).
134
137
  - **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.
135
- - **Change the font:** set `--font-sans` in `@theme inline`. Inter is loaded by default.
138
+ - **Change the font:** set `--font-sans` in `@theme inline`. By default it uses the system font on macOS and iOS and Inter elsewhere.
136
139
  - **Corner radius:** `--radius` scales every rounded component.
137
140
 
138
141
  ## Dark mode
@@ -157,9 +160,31 @@ export default function RootLayout({ children }: { children: React.ReactNode })
157
160
 
158
161
  ## Components
159
162
 
160
- 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
163
+ accordion, alert, alert-dialog, alert-sheet, 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, fullscreen-menu, 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, tab-bar, table, tabs, textarea, toast, toggle, toggle-group, tooltip, wheel-picker
161
164
 
162
- Hooks: use-message-scroller, use-mobile, use-questionnaire
165
+ Hooks: use-message-scroller, use-mobile, use-platform, use-questionnaire
166
+
167
+ ## Patterns
168
+
169
+ 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:
170
+
171
+ ```bash
172
+ npx @esuiss/ui add patterns date-time-picker
173
+ npx @esuiss/ui add patterns --all
174
+ ```
175
+
176
+ The components a pattern uses are added with it.
177
+
178
+ ## Interactions
179
+
180
+ Interactions add a behavior to something you already have, such as swiping a row to reveal actions. They are optional extras, so they are not added by `init` or `add --all`; add them by name, or all at once:
181
+
182
+ ```bash
183
+ npx @esuiss/ui add interactions swipe-actions
184
+ npx @esuiss/ui add interactions --all
185
+ ```
186
+
187
+ The components an interaction uses are added with it.
163
188
 
164
189
  ## Troubleshooting
165
190
 
package/index.mjs CHANGED
@@ -15,11 +15,14 @@ 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-picker
19
+ npx ${name} add interactions <name...> add interactions, e.g. add interactions swipe-actions
18
20
 
19
21
  Options:
20
22
  -o, --overwrite replace local files that differ from the registry
21
23
  --diff show how local files differ from the registry
22
- -a, --all add every component
24
+ -a, --all add every component, or every pattern or interaction
25
+ after "patterns" or "interactions"
23
26
  -y, --yes skip confirmation prompts
24
27
  -c, --cwd <dir> project directory (defaults to the current directory)
25
28
  -h, --help show this help
@@ -27,6 +30,11 @@ Options:
27
30
 
28
31
  Supports Next.js and React (Vite) projects with TypeScript and Tailwind CSS v4.`
29
32
 
33
+ const SUBCOMMANDS = {
34
+ patterns: "pattern",
35
+ interactions: "interaction",
36
+ }
37
+
30
38
  function parseArgs(args) {
31
39
  const options = {
32
40
  cwd: process.cwd(),
@@ -79,8 +87,11 @@ async function main() {
79
87
  await init(parseArgs(args).options)
80
88
  break
81
89
  case "add": {
82
- const { options, names } = parseArgs(args)
83
- await add(names, options)
90
+ const scope = SUBCOMMANDS[args[0]] ?? "ui"
91
+ const { options, names } = parseArgs(
92
+ scope === "ui" ? args : args.slice(1)
93
+ )
94
+ await add(names, { ...options, scope })
84
95
  break
85
96
  }
86
97
  case "-v":
package/lib/add.mjs CHANGED
@@ -4,7 +4,13 @@ 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
+ resolveItems,
11
+ SCOPES,
12
+ scopeOf,
13
+ } from "./registry.mjs"
8
14
 
9
15
  async function fetchFiles(items) {
10
16
  return Promise.all(
@@ -93,6 +99,39 @@ async function showDiff(project, root, registry, names) {
93
99
  if (!changes) step("No differences from the registry.")
94
100
  }
95
101
 
102
+ function selectNames(registry, names, { all, scope = "ui" }) {
103
+ const inScope = (item) => scopeOf(item) === scope
104
+ const scoped = registry.items.filter(inScope)
105
+ const { kind, command, example } = SCOPES[scope]
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} ${example}`
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 = SCOPES[scopeOf(item)]
124
+ throw new CliError(
125
+ `"${name}" is ${other.kind === "interaction" ? "an" : "a"} ${other.kind}. Run npx @esuiss/ui ${other.command} ${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
@@ -40,8 +40,10 @@ const TOOLING = {
40
40
  }
41
41
  const MINIMUM_TAILWIND = [4, 2]
42
42
  const FONT_IMPORT = '@import "@fontsource-variable/inter";\n'
43
- const FONT_FAMILY = '--font-sans: "Inter Variable", sans-serif;'
44
- const NEXT_FONT_FAMILY = "--font-sans: var(--font-sans);"
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;"
45
47
  const NEXT_LAYOUTS = [
46
48
  "src/app/layout.tsx",
47
49
  "app/layout.tsx",
@@ -49,7 +51,7 @@ const NEXT_LAYOUTS = [
49
51
  "app/layout.jsx",
50
52
  ]
51
53
  const NEXT_FONT_NOTE =
52
- 'Load Inter in your root layout with next/font/google: Inter({ subsets: ["latin", "latin-ext"], variable: "--font-sans" }), and add its .variable to the <html> className.'
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.'
53
55
 
54
56
  function relative(project, file) {
55
57
  return path.relative(project.cwd, file)
@@ -207,7 +209,7 @@ function configureNextFont(project) {
207
209
  if (!file) return [NEXT_FONT_NOTE]
208
210
 
209
211
  let text = readFileSync(file, "utf8")
210
- if (/variable:\s*["']--font-sans["']/.test(text)) return []
212
+ if (/variable:\s*["']--font-inter["']/.test(text)) return []
211
213
 
212
214
  const html = text.match(/<html\b[^>]*>/)
213
215
  if (!html) return [NEXT_FONT_NOTE]
@@ -216,10 +218,10 @@ function configureNextFont(project) {
216
218
  if (literal) {
217
219
  tag = tag.replace(
218
220
  literal[0],
219
- `className={\`\${fontSans.variable} ${literal[1]}\`}`
221
+ `className={\`\${fontInter.variable} ${literal[1]}\`}`
220
222
  )
221
223
  } else if (!/className=/.test(tag)) {
222
- tag = tag.replace(/<html\b/, "<html className={fontSans.variable}")
224
+ tag = tag.replace(/<html\b/, "<html className={fontInter.variable}")
223
225
  } else {
224
226
  return [NEXT_FONT_NOTE]
225
227
  }
@@ -243,7 +245,7 @@ function configureNextFont(project) {
243
245
  ]
244
246
  const last = imports.at(-1)
245
247
  const at = last ? last.index + last[0].length : 0
246
- const declaration = `\nconst fontSans = Inter({\n subsets: ["latin", "latin-ext"],\n variable: "--font-sans",\n})${semi}\n`
248
+ const declaration = `\nconst fontInter = Inter({\n subsets: ["latin", "latin-ext"],\n variable: "--font-inter",\n})${semi}\n`
247
249
  text = text.slice(0, at) + declaration + text.slice(at)
248
250
 
249
251
  writeFileSync(file, text)
package/lib/registry.mjs CHANGED
@@ -20,14 +20,22 @@ export async function fetchRegistry() {
20
20
  return JSON.parse(await fetchText("registry.json"))
21
21
  }
22
22
 
23
+ export const SCOPES = {
24
+ ui: { kind: "component", command: "add", example: "button" },
25
+ pattern: { kind: "pattern", command: "add patterns", example: "<name>" },
26
+ interaction: {
27
+ kind: "interaction",
28
+ command: "add interactions",
29
+ example: "<name>",
30
+ },
31
+ }
32
+
33
+ export function scopeOf(item) {
34
+ return item.type in SCOPES ? item.type : "ui"
35
+ }
36
+
23
37
  export function resolveItems(registry, names) {
24
38
  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
39
 
32
40
  const ordered = []
33
41
  const seen = new Set()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esuiss/ui",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",