@nextlyhq/ui 0.0.2-alpha.5 → 0.0.2-alpha.50

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/dist/utils.cjs ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ var clsx = require('clsx');
4
+ var tailwindMerge = require('tailwind-merge');
5
+
6
+ // src/lib/utils.ts
7
+ function cn(...inputs) {
8
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
9
+ }
10
+
11
+ exports.cn = cn;
12
+ //# sourceMappingURL=utils.cjs.map
13
+ //# sourceMappingURL=utils.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/utils.ts"],"names":["twMerge","clsx"],"mappings":";;;;;;AAeO,SAAS,MAAM,MAAA,EAAsB;AAC1C,EAAA,OAAOA,qBAAA,CAAQC,SAAA,CAAK,MAAM,CAAC,CAAA;AAC7B","file":"utils.cjs","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Utility function to merge class names with Tailwind CSS support.\n *\n * Combines clsx for conditional class names with tailwind-merge\n * to properly handle Tailwind CSS class conflicts.\n *\n * @example\n * ```typescript\n * cn('px-2 py-1', 'px-4') // => 'py-1 px-4'\n * cn('text-muted-foreground', condition && 'text-foreground')\n * ```\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import { ClassValue } from 'clsx';
2
+
3
+ /**
4
+ * Utility function to merge class names with Tailwind CSS support.
5
+ *
6
+ * Combines clsx for conditional class names with tailwind-merge
7
+ * to properly handle Tailwind CSS class conflicts.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * cn('px-2 py-1', 'px-4') // => 'py-1 px-4'
12
+ * cn('text-muted-foreground', condition && 'text-foreground')
13
+ * ```
14
+ */
15
+ declare function cn(...inputs: ClassValue[]): string;
16
+
17
+ export { cn };
@@ -0,0 +1,17 @@
1
+ import { ClassValue } from 'clsx';
2
+
3
+ /**
4
+ * Utility function to merge class names with Tailwind CSS support.
5
+ *
6
+ * Combines clsx for conditional class names with tailwind-merge
7
+ * to properly handle Tailwind CSS class conflicts.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * cn('px-2 py-1', 'px-4') // => 'py-1 px-4'
12
+ * cn('text-muted-foreground', condition && 'text-foreground')
13
+ * ```
14
+ */
15
+ declare function cn(...inputs: ClassValue[]): string;
16
+
17
+ export { cn };
package/dist/utils.mjs ADDED
@@ -0,0 +1,11 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ // src/lib/utils.ts
5
+ function cn(...inputs) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ export { cn };
10
+ //# sourceMappingURL=utils.mjs.map
11
+ //# sourceMappingURL=utils.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/utils.ts"],"names":[],"mappings":";;;;AAeO,SAAS,MAAM,MAAA,EAAsB;AAC1C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B","file":"utils.mjs","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Utility function to merge class names with Tailwind CSS support.\n *\n * Combines clsx for conditional class names with tailwind-merge\n * to properly handle Tailwind CSS class conflicts.\n *\n * @example\n * ```typescript\n * cn('px-2 py-1', 'px-4') // => 'py-1 px-4'\n * cn('text-muted-foreground', condition && 'text-foreground')\n * ```\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"]}
@@ -0,0 +1,228 @@
1
+ # Plugin UI authoring guide
2
+
3
+ How to build admin UI — plugin views, custom fields, dashboards — that looks native
4
+ to the Nextly admin and inherits light/dark mode for free.
5
+
6
+ The rules here are a **contract**. A lint guard (`pnpm lint:design`, run in CI) fails the
7
+ build on the most common violations, so following this guide is not optional for code that
8
+ ships in this repo.
9
+
10
+ ---
11
+
12
+ ## 1. Where your styles come from
13
+
14
+ Nextly's design system lives in [`@nextlyhq/ui`](../README.md). It ships two CSS entry
15
+ points:
16
+
17
+ | Entry point | Contents | Use when |
18
+ | ------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------- |
19
+ | `@nextlyhq/ui/theme.css` | Design tokens + `@theme` mappings + `@keyframes`. No reset, no `@import`. | You run your own Tailwind v4 build and want the tokens. |
20
+ | `@nextlyhq/ui/styles.css` | Pre-compiled bundle (Tailwind + tokens + reset). Zero config. | Standalone UI, or a quick embed with no build wiring. |
21
+
22
+ **Inside a Nextly admin plugin you usually need neither.** The admin already loads
23
+ `@nextlyhq/admin/style.css`, which defines every token scoped to `.nextly-admin` (and
24
+ `.nextly-admin.dark` for dark mode). Plugin admin views render inside that scope, so the tokens
25
+ are already on the page. Your plugin CSS should **consume** those tokens, never redefine
26
+ them.
27
+
28
+ Only import a stylesheet yourself when you render outside the admin shell (e.g. a public
29
+ page or a standalone tool) — then import `@nextlyhq/ui/styles.css` once at the root.
30
+
31
+ ---
32
+
33
+ ## 2. The token contract
34
+
35
+ Tokens are **complete color values** (OKLCH). Reference them directly:
36
+
37
+ ```css
38
+ .my-panel {
39
+ background: var(--nx-card);
40
+ color: var(--nx-foreground);
41
+ border: 1px solid var(--nx-border);
42
+ border-radius: var(--radius);
43
+ }
44
+ ```
45
+
46
+ ### Never wrap a token in `hsl()` / `rgb()`
47
+
48
+ Older Nextly builds stored tokens as bare HSL channels (`--nx-primary: 221 83% 53%`) and code
49
+ wrapped them: `hsl(var(--nx-primary))`. **That convention is gone.** Tokens are now full colors
50
+ (`--nx-primary: oklch(0 0 0)`), so `hsl(var(--nx-primary))` becomes `hsl(oklch(0 0 0))`, which is
51
+ invalid CSS — the browser drops the declaration and your element loses its color. Always:
52
+
53
+ ```css
54
+ /* ✗ wrong — produces hsl(oklch(...)), silently dropped */
55
+ color: hsl(var(--nx-foreground));
56
+
57
+ /* ✓ right */
58
+ color: var(--nx-foreground);
59
+ ```
60
+
61
+ ### Alpha / transparency
62
+
63
+ You can no longer do `hsl(var(--nx-primary) / 0.1)`. Use `color-mix`:
64
+
65
+ ```css
66
+ /* 10% primary over transparent */
67
+ background: color-mix(in srgb, var(--nx-primary) 10%, transparent);
68
+
69
+ /* a subtle hover tint from the foreground */
70
+ background: color-mix(in srgb, var(--nx-foreground) 6%, transparent);
71
+ ```
72
+
73
+ ### The tokens you may use
74
+
75
+ Colors: `--nx-background` / `--nx-foreground`, `--nx-card` / `--nx-card-foreground`,
76
+ `--nx-popover` / `--nx-popover-foreground`, `--nx-primary` / `--nx-primary-foreground`,
77
+ `--nx-secondary` / `--nx-secondary-foreground`, `--nx-muted` / `--nx-muted-foreground`,
78
+ `--nx-accent` / `--nx-accent-foreground`, `--nx-destructive`, `--nx-success`, `--nx-warning`,
79
+ `--nx-border`, `--nx-border-strong`, `--nx-input`, `--nx-ring`,
80
+ `--nx-sidebar-background` and the `--nx-sidebar-*` family.
81
+
82
+ Sizing: `--radius` (see the radius tiers below), and the control-height scale
83
+ `--nx-control-height`, `--nx-control-height-sm`, `--nx-control-height-md`,
84
+ `--nx-control-height-lg` for anything that should line up with admin inputs and buttons.
85
+
86
+ Do not hardcode hex, `rgb()`, `rgba()`, or named colors. The only literals allowed are
87
+ `transparent`, `currentColor`, and `inherit`.
88
+
89
+ ### Corner radius: pick a tier, never a value
90
+
91
+ The admin ships square today (`--radius: 0`), but the whole scale derives from that one
92
+ knob, so an operator who sets `--radius: 12px` re-rounds every surface at once. Your plugin
93
+ keeps up with that only if it picks a **tier by element category** rather than writing a
94
+ length. Hardcoding a corner — `rounded-none` just as much as `rounded-[6px]` or
95
+ `border-radius: 4px` — pins your UI at that value and opts it out of the knob.
96
+
97
+ The scale is three steps. These are exactly what `@nextlyhq/ui/tailwind-preset` exports, and
98
+ exactly what the shipped components use, so matching a Nextly component means picking the
99
+ tier it picks.
100
+
101
+ | Tier | Use for |
102
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `rounded-lg` / `--radius-lg` | Floating and grouping containers: cards, dialogs, alert dialogs, popovers, tooltips, dropdown / select / command panels, empty-state panels |
104
+ | `rounded-md` / `--radius-md` | Controls and the inline surfaces among them: buttons, inputs, textareas, select triggers, icon buttons, alerts, table wrappers, image frames |
105
+ | `rounded-sm` / `--radius-sm` | Small controls and adornments: menu and list items, badges, chips, tags, checkboxes, toolbar affordances |
106
+ | `rounded-full` | Circles and pills, outside the scale — avatars, status dots, spinners, progress tracks, switch tracks and thumbs |
107
+
108
+ Four consequences worth internalising:
109
+
110
+ - **`rounded-full` is deliberately outside the scale.** It is `9999px`, not a `--radius`
111
+ step. Those elements are round because of their shape, not because of the theme, so a
112
+ retheme must never flatten them.
113
+ - **`rounded-xl` and `rounded-2xl` are not tiers of this knob.** The preset exports only
114
+ `lg`, `md` and `sm`, so a plugin built against it gets Tailwind's fixed `0.75rem` /
115
+ `1rem` for those two, detached from `--radius`. Inside the admin's own build they are
116
+ defined, but as `--radius + 4px` and `--radius + 8px`, so at the shipped `--radius: 0`
117
+ they resolve to `4px` and `8px` — visibly round inside a square admin. Pick from
118
+ `sm` / `md` / `lg`.
119
+ - **`--radius: 0` is not "every step is zero".** `--radius-lg` is `0`, but `--radius-sm`
120
+ and `--radius-md` compute to `-4px` and `-2px`. They _render_ square only because
121
+ `border-radius` clamps a negative length to `0`. The token keeps its negative value, so
122
+ reading a step for padding, for an inset, or through `getComputedStyle` gives you `-4px`,
123
+ not `0`. Clamp it yourself if you consume a step as a length rather than as a corner.
124
+ - **A rounded container must handle full-bleed children.** If a child paints a background
125
+ all the way to the container's edge (a tinted footer, a sticky header, a hover row), give
126
+ the container `overflow-hidden` or give the child a matching corner with
127
+ `rounded-b-[inherit]` / `rounded-t-[inherit]`. Otherwise the child's fill paints square
128
+ across the parent's curve at any nonzero `--radius`.
129
+
130
+ If a specific element genuinely must stay square, keep `rounded-none` and leave a comment
131
+ saying why (overlapped-border strips, full-bleed fills inside an already-rounded parent,
132
+ underline tabs). Square with a stated reason is a decision; square without one is an element
133
+ nobody wired to the knob. Tabs are square for exactly that reason — the active state is a
134
+ bottom border that has to run the full width of the trigger — which is why they are not in
135
+ the `rounded-sm` tier above. Sheet is the counter-example: it is anchored to the viewport
136
+ edge and carries no radius class at all.
137
+
138
+ ---
139
+
140
+ ## 3. Prefer components and utilities over hand-rolled CSS
141
+
142
+ For new UI, reach for `@nextlyhq/ui` components and Tailwind utility classes before writing
143
+ a stylesheet. They are already token-driven, accessible, and dark-mode-correct.
144
+
145
+ ```tsx
146
+ import { Button, Input, Select, Switch, Badge } from "@nextlyhq/ui";
147
+ import { cn } from "@nextlyhq/ui/utils";
148
+
149
+ <Button variant="outline" size="sm">Add field</Button>
150
+ <Input placeholder="Form name" />
151
+ ```
152
+
153
+ - Merge conditional classes with `cn()` (clsx + tailwind-merge).
154
+ - Express variants with `cva` (class-variance-authority), the way the components do.
155
+ - Target internal parts with the `data-slot` attribute, not by re-styling the component.
156
+
157
+ Hand-written CSS is still fine for bespoke layouts (a drag-and-drop canvas, a box-model
158
+ control). When you write it, it must obey the token contract in section 2 and be scoped
159
+ (section 5).
160
+
161
+ ---
162
+
163
+ ## 4. Dark mode is automatic — don't fight it
164
+
165
+ The admin flips every token under `.nextly-admin.dark`. If you only ever reference tokens, your
166
+ UI switches with it and you never write a dark rule.
167
+
168
+ - **Never** hardcode a light or dark color.
169
+ - **Never** gate admin theming on `@media (prefers-color-scheme: dark)` — admin dark mode is
170
+ class-based (`.dark` / `.nextly-admin.dark`), independent of the OS. A media query makes your
171
+ UI ignore the admin's own theme toggle. Use Tailwind's `dark:` variant or `.nextly-admin.dark`
172
+ descendant selectors if you truly need a mode-specific tweak.
173
+
174
+ ---
175
+
176
+ ## 5. Scope your class names
177
+
178
+ Plugin CSS shares the page with the admin. Prefix every class so nothing leaks:
179
+ `nx-pb-*` (page builder), `field-editor-*` / `form-settings-*` (form builder), etc. Never
180
+ style bare element selectors (`button {}`, `input {}`) or admin utility classes.
181
+
182
+ ---
183
+
184
+ ## 6. Responsive: containers, not the viewport
185
+
186
+ Plugin panels live in variable-width columns, so a media query keyed to the viewport is the
187
+ wrong tool — it fires on window size while your panel might be half that. Use CSS container
188
+ queries (Tailwind v4 core) against the nearest sized ancestor:
189
+
190
+ ```tsx
191
+ <div className="@container/panel">
192
+ <div className="grid grid-cols-1 @2xl/panel:grid-cols-2">...</div>
193
+ </div>
194
+ ```
195
+
196
+ Container breakpoints are **container-sized**, not viewport-sized: `@md` = a 448px container,
197
+ `@2xl` = 672px, `@4xl` = 896px. Pick the threshold by testing the panel, not by mapping from
198
+ `md:`/`lg:`.
199
+
200
+ ---
201
+
202
+ ## 7. The guardrail
203
+
204
+ `pnpm lint:design` (run in CI) scans the admin and plugin packages and fails on:
205
+
206
+ - `hsl(var(--…))` / `rgb(var(--…))` — the dead HSL-channel convention (section 2).
207
+ - Hardcoded colors: hex, `rgb(`, `rgba(`, `hsl(` with literal channels (outside `url(...)`
208
+ data URIs). Allowed literals: `transparent`, `currentColor`, `inherit`.
209
+ - `!important` outside the small, reviewed allowlist.
210
+
211
+ Run it locally before pushing. If a finding is a genuine false positive, add it to the
212
+ allowlist in the script with a one-line reason — don't silence the whole check.
213
+
214
+ ---
215
+
216
+ ## Checklist
217
+
218
+ - [ ] Colors come from `var(--token)` — no `hsl()`/`rgb()` wrappers, no hardcoded values.
219
+ - [ ] Alpha uses `color-mix(in srgb, var(--token) N%, transparent)`.
220
+ - [ ] Corner radius picks a tier (`rounded-lg`/`md`/`sm`), never a hardcoded value; circles
221
+ use `rounded-full`; any `rounded-none` carries a reason; rounded containers clip or
222
+ match their full-bleed children. Control heights use `--nx-control-height*`.
223
+ - [ ] New UI uses `@nextlyhq/ui` components + Tailwind utilities where practical.
224
+ - [ ] No `@media (prefers-color-scheme)`; dark mode inherited from the admin.
225
+ - [ ] Class names are prefixed/scoped; no bare element selectors.
226
+ - [ ] Responsive rules use container queries, tested against the panel width.
227
+ - [ ] `pnpm lint:design` passes.
228
+ - [ ] Verified in the admin in **both** light and dark mode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextlyhq/ui",
3
- "version": "0.0.2-alpha.5",
3
+ "version": "0.0.2-alpha.50",
4
4
  "description": "Nextly UI — Reusable React component library for Nextly plugins and custom admin UIs",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -17,13 +17,45 @@
17
17
  "types": "dist/index.d.ts",
18
18
  "exports": {
19
19
  ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.mjs",
22
- "require": "./dist/index.cjs"
23
- }
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.mjs"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "./tailwind-preset": {
30
+ "import": {
31
+ "types": "./dist/tailwind-preset.d.ts",
32
+ "default": "./dist/tailwind-preset.mjs"
33
+ },
34
+ "require": {
35
+ "types": "./dist/tailwind-preset.d.cts",
36
+ "default": "./dist/tailwind-preset.cjs"
37
+ }
38
+ },
39
+ "./utils": {
40
+ "import": {
41
+ "types": "./dist/utils.d.ts",
42
+ "default": "./dist/utils.mjs"
43
+ },
44
+ "require": {
45
+ "types": "./dist/utils.d.cts",
46
+ "default": "./dist/utils.cjs"
47
+ }
48
+ },
49
+ "./theme.css": "./dist/theme.css",
50
+ "./styles.css": "./dist/styles.css",
51
+ "./styles.scoped.css": "./dist/styles.scoped.css"
24
52
  },
53
+ "sideEffects": [
54
+ "**/*.css"
55
+ ],
25
56
  "files": [
26
- "dist"
57
+ "dist",
58
+ "docs"
27
59
  ],
28
60
  "engines": {
29
61
  "node": ">=20.0.0"
@@ -34,22 +66,29 @@
34
66
  "react-dom": "^18.0.0 || ^19.0.0"
35
67
  },
36
68
  "devDependencies": {
69
+ "@tailwindcss/cli": "^4.3.2",
70
+ "@types/culori": "^4.0.1",
37
71
  "@types/react": "19.2.0",
38
72
  "@types/react-dom": "19.2.0",
39
73
  "@vitejs/plugin-react": "^5.1.0",
40
- "@vitest/coverage-v8": "^4.0.8",
41
- "@vitest/ui": "^4.0.8",
42
- "eslint": "^9.34.0",
43
- "lucide-react": "^0.544.0",
74
+ "@vitest/coverage-v8": "^4.1.0",
75
+ "@vitest/ui": "^4.1.0",
76
+ "culori": "^4.0.2",
77
+ "eslint": "^9.39.1",
44
78
  "jsdom": "^27.1.0",
79
+ "lucide-react": "^0.544.0",
80
+ "postcss": "^8.5.6",
45
81
  "react": "19.2.0",
46
82
  "react-dom": "19.2.0",
83
+ "rimraf": "^6.1.3",
84
+ "tailwindcss": "^4.3.2",
47
85
  "tsup": "^8.5.0",
48
86
  "typescript": "^5.9.3",
49
87
  "vite-tsconfig-paths": "^5.1.4",
50
- "vitest": "^4.0.8",
51
- "@nextlyhq/eslint-config": "0.0.2-alpha.4",
52
- "@nextlyhq/tsconfig": "0.0.2-alpha.4"
88
+ "vitest": "^4.1.0",
89
+ "@nextlyhq/admin-css": "0.0.2-alpha.50",
90
+ "@nextlyhq/eslint-config": "0.0.2-alpha.50",
91
+ "@nextlyhq/tsconfig": "0.0.2-alpha.50"
53
92
  },
54
93
  "dependencies": {
55
94
  "@radix-ui/react-accordion": "^1.2.12",
@@ -90,8 +129,10 @@
90
129
  },
91
130
  "author": "Nextly <contact@nextlyhq.com> (https://nextlyhq.com)",
92
131
  "scripts": {
93
- "build": "tsup",
94
- "dev": "tsup --watch",
132
+ "build": "pnpm build:js && node scripts/build-css.mjs",
133
+ "build:js": "rimraf dist && tsup && tsup --config tsup.server-safe.config.ts && node scripts/check-client-directive.mjs",
134
+ "build:css": "node scripts/build-css.mjs",
135
+ "dev": "tsup --watch & tsup --config tsup.server-safe.config.ts --watch & wait",
95
136
  "check-types": "tsc --noEmit",
96
137
  "lint": "eslint . --max-warnings 0",
97
138
  "lint:fix": "eslint . --fix",