@webjsdev/ui 0.3.1 → 0.3.2
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/package.json +4 -3
- package/packages/registry/README.md +35 -0
- package/packages/registry/components/accordion.ts +74 -0
- package/packages/registry/components/alert-dialog.ts +359 -0
- package/packages/registry/components/alert.ts +51 -0
- package/packages/registry/components/aspect-ratio.ts +22 -0
- package/packages/registry/components/avatar.ts +52 -0
- package/packages/registry/components/badge.ts +40 -0
- package/packages/registry/components/breadcrumb.ts +43 -0
- package/packages/registry/components/button.ts +72 -0
- package/packages/registry/components/card.ts +86 -0
- package/packages/registry/components/checkbox.ts +97 -0
- package/packages/registry/components/collapsible.ts +60 -0
- package/packages/registry/components/dialog.ts +378 -0
- package/packages/registry/components/dropdown-menu.ts +591 -0
- package/packages/registry/components/hover-card.ts +175 -0
- package/packages/registry/components/input.ts +36 -0
- package/packages/registry/components/kbd.ts +25 -0
- package/packages/registry/components/label.ts +23 -0
- package/packages/registry/components/native-select.ts +110 -0
- package/packages/registry/components/pagination.ts +45 -0
- package/packages/registry/components/popover.ts +260 -0
- package/packages/registry/components/progress.ts +46 -0
- package/packages/registry/components/radio-group.ts +113 -0
- package/packages/registry/components/separator.ts +30 -0
- package/packages/registry/components/skeleton.ts +16 -0
- package/packages/registry/components/sonner.ts +240 -0
- package/packages/registry/components/switch.ts +52 -0
- package/packages/registry/components/table.ts +58 -0
- package/packages/registry/components/tabs.ts +271 -0
- package/packages/registry/components/textarea.ts +27 -0
- package/packages/registry/components/toggle-group.ts +236 -0
- package/packages/registry/components/toggle.ts +118 -0
- package/packages/registry/components/tooltip.ts +195 -0
- package/packages/registry/lib/utils.ts +241 -0
- package/packages/registry/package.json +7 -0
- package/packages/registry/registry.json +479 -0
- package/packages/registry/themes/base-colors.js +193 -0
- package/packages/registry/themes/index.css +141 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny class-name merger. Drop-in replacement for the clsx + tailwind-merge
|
|
3
|
+
* pair used in shadcn.
|
|
4
|
+
*
|
|
5
|
+
* - Concatenates truthy arguments separated by spaces.
|
|
6
|
+
* - Later Tailwind utilities win when they target the same property, mimicking
|
|
7
|
+
* `tailwind-merge`'s behaviour for the cases components actually hit
|
|
8
|
+
* (background colour, text colour, padding, margin, width, height, border,
|
|
9
|
+
* rounded, opacity, display).
|
|
10
|
+
*
|
|
11
|
+
* For projects that want the full tailwind-merge behaviour, install
|
|
12
|
+
* `clsx` + `tailwind-merge` and replace this file:
|
|
13
|
+
*
|
|
14
|
+
* import { clsx, type ClassValue } from 'clsx';
|
|
15
|
+
* import { twMerge } from 'tailwind-merge';
|
|
16
|
+
* export function cn(...inputs: ClassValue[]) {
|
|
17
|
+
* return twMerge(clsx(inputs));
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export type ClassValue = string | number | null | false | undefined | ClassValue[] | Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
23
|
+
const flat: string[] = [];
|
|
24
|
+
walk(inputs, flat);
|
|
25
|
+
return dedupeUtilities(flat.join(' ')).trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function walk(value: ClassValue, out: string[]): void {
|
|
29
|
+
if (!value) return;
|
|
30
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
31
|
+
out.push(String(value));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
for (const v of value) walk(v, out);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'object') {
|
|
39
|
+
for (const [k, v] of Object.entries(value)) {
|
|
40
|
+
if (v) out.push(k);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Conflict groups: classes with the same group key: last one wins.
|
|
46
|
+
// Covers ~95% of in-component overrides the registry exposes.
|
|
47
|
+
//
|
|
48
|
+
// IMPORTANT: text-size (text-sm, text-xs, text-base, text-lg, …) and
|
|
49
|
+
// text-color (text-primary, text-foreground, …) are DIFFERENT properties
|
|
50
|
+
// and must be in different groups. Same for bg-size vs bg-color etc.
|
|
51
|
+
const GROUPS: Array<[RegExp, string]> = [
|
|
52
|
+
[/^p-/, 'p'], [/^px-/, 'px'], [/^py-/, 'py'], [/^pt-/, 'pt'], [/^pr-/, 'pr'], [/^pb-/, 'pb'], [/^pl-/, 'pl'],
|
|
53
|
+
[/^m-/, 'm'], [/^mx-/, 'mx'], [/^my-/, 'my'], [/^mt-/, 'mt'], [/^mr-/, 'mr'], [/^mb-/, 'mb'], [/^ml-/, 'ml'],
|
|
54
|
+
[/^w-/, 'w'], [/^h-/, 'h'], [/^size-/, 'size'],
|
|
55
|
+
[/^bg-(linear|gradient|conic|radial|none)/, 'bg-image'],
|
|
56
|
+
[/^bg-(no-repeat|repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, 'bg-repeat'],
|
|
57
|
+
[/^bg-(fixed|local|scroll)$/, 'bg-attach'],
|
|
58
|
+
[/^bg-(auto|cover|contain)$/, 'bg-size'],
|
|
59
|
+
[/^bg-(bottom|center|left|right|top)$/, 'bg-position'],
|
|
60
|
+
[/^bg-/, 'bg-color'],
|
|
61
|
+
// Font size: explicit list of Tailwind size scale.
|
|
62
|
+
[/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/, 'text-size'],
|
|
63
|
+
// Text color: anything else starting with text- that isn't alignment / wrap / overflow.
|
|
64
|
+
[/^text-(?!align-|left$|right$|center$|justify$|start$|end$|wrap$|nowrap$|balance$|pretty$|clip$|ellipsis$|xs$|sm$|base$|lg$|xl$|\d?xl$)/, 'text-color'],
|
|
65
|
+
[/^border(-[trblxy])?-?\d/, 'border-w'],
|
|
66
|
+
[/^rounded(-[a-z]+)?$/, 'rounded'],
|
|
67
|
+
[/^rounded-/, 'rounded'],
|
|
68
|
+
[/^opacity-/, 'opacity'],
|
|
69
|
+
[/^font-(thin|light|normal|medium|semibold|bold|black|extralight|extrabold)$/, 'font-weight'],
|
|
70
|
+
[/^shadow(-|$)/, 'shadow'],
|
|
71
|
+
[/^z-/, 'z'],
|
|
72
|
+
[/^flex(-|$)/, 'flex'],
|
|
73
|
+
[/^grid(-|$)/, 'grid'],
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function dedupeUtilities(input: string): string {
|
|
77
|
+
const tokens = input.split(/\s+/).filter(Boolean);
|
|
78
|
+
const seen = new Map<string, number>();
|
|
79
|
+
const result: Array<string | null> = [];
|
|
80
|
+
|
|
81
|
+
for (const token of tokens) {
|
|
82
|
+
let key: string | null = null;
|
|
83
|
+
// Strip variant prefix (`hover:`, `dark:md:`, …) before testing each
|
|
84
|
+
// dedupe regex so `hover:bg-red-500` still matches the `bg-color` group.
|
|
85
|
+
const prefix = variantPrefix(token);
|
|
86
|
+
const bare = prefix ? token.slice(prefix.length) : token;
|
|
87
|
+
for (const [re, gk] of GROUPS) {
|
|
88
|
+
if (re.test(bare)) { key = `${prefix}::${gk}`; break; }
|
|
89
|
+
}
|
|
90
|
+
if (key && seen.has(key)) result[seen.get(key)!] = null;
|
|
91
|
+
if (key) seen.set(key, result.length);
|
|
92
|
+
result.push(token);
|
|
93
|
+
}
|
|
94
|
+
return result.filter(Boolean).join(' ');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function variantPrefix(token: string): string {
|
|
98
|
+
// capture leading variants like `hover:`, `dark:`, `md:`: overrides only conflict within the same variant set
|
|
99
|
+
const i = token.lastIndexOf(':');
|
|
100
|
+
return i === -1 ? '' : token.slice(0, i + 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Custom-element base: SSR-safe. In the browser `Base = HTMLElement`. On
|
|
105
|
+
// the server (Node, during SSR) `HTMLElement` is undefined; we substitute
|
|
106
|
+
// a stub class so that `class Foo extends Base { … }` doesn't throw at
|
|
107
|
+
// module-load time. The stub's methods are never actually called server-side
|
|
108
|
+
// because connectedCallback/lifecycle hooks only run when the element is
|
|
109
|
+
// inserted into a live DOM, which doesn't happen during webjs SSR.
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
const HasHTMLElement = typeof HTMLElement !== 'undefined';
|
|
113
|
+
|
|
114
|
+
class ServerHTMLElementStub {
|
|
115
|
+
// Minimal surface so attribute reads/writes inside synchronous code paths
|
|
116
|
+
// that DO execute during SSR (e.g. attribute reflection) don't throw.
|
|
117
|
+
_ssrAttrs: Record<string, string> = {};
|
|
118
|
+
getAttribute(name: string): string | null {
|
|
119
|
+
return this._ssrAttrs[name] ?? null;
|
|
120
|
+
}
|
|
121
|
+
setAttribute(name: string, value: string): void {
|
|
122
|
+
this._ssrAttrs[name] = String(value);
|
|
123
|
+
}
|
|
124
|
+
hasAttribute(name: string): boolean {
|
|
125
|
+
return name in this._ssrAttrs;
|
|
126
|
+
}
|
|
127
|
+
removeAttribute(name: string): void {
|
|
128
|
+
delete this._ssrAttrs[name];
|
|
129
|
+
}
|
|
130
|
+
toggleAttribute(name: string, force?: boolean): boolean {
|
|
131
|
+
const want = force === undefined ? !this.hasAttribute(name) : force;
|
|
132
|
+
if (want) this.setAttribute(name, '');
|
|
133
|
+
else this.removeAttribute(name);
|
|
134
|
+
return want;
|
|
135
|
+
}
|
|
136
|
+
addEventListener(): void {}
|
|
137
|
+
removeEventListener(): void {}
|
|
138
|
+
dispatchEvent(): boolean {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
// Tree-walk APIs no-op into null: components that call them server-side
|
|
142
|
+
// simply see "no children / no siblings", which is acceptable for SSR.
|
|
143
|
+
closest(): null {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
querySelector(): null {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
querySelectorAll(): never[] {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
focus(): void {}
|
|
153
|
+
blur(): void {}
|
|
154
|
+
contains(): boolean {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
insertBefore<T>(node: T): T {
|
|
158
|
+
return node;
|
|
159
|
+
}
|
|
160
|
+
appendChild<T>(node: T): T {
|
|
161
|
+
return node;
|
|
162
|
+
}
|
|
163
|
+
replaceChildren(): void {}
|
|
164
|
+
get firstChild(): null {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
get classList() {
|
|
168
|
+
return { add: () => {}, remove: () => {}, toggle: () => false, contains: () => false };
|
|
169
|
+
}
|
|
170
|
+
get className(): string {
|
|
171
|
+
return this._ssrAttrs.class ?? '';
|
|
172
|
+
}
|
|
173
|
+
set className(v: string) {
|
|
174
|
+
this._ssrAttrs.class = v;
|
|
175
|
+
}
|
|
176
|
+
get style(): Record<string, string> {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** SSR-safe base class: `HTMLElement` in browser, a thin stub in Node. */
|
|
182
|
+
export const Base: typeof HTMLElement = (HasHTMLElement
|
|
183
|
+
? HTMLElement
|
|
184
|
+
: (ServerHTMLElementStub as unknown as typeof HTMLElement)) as typeof HTMLElement;
|
|
185
|
+
|
|
186
|
+
/** Register a custom element. No-op on server (no `customElements` global). */
|
|
187
|
+
export function defineElement(name: string, cls: CustomElementConstructor): void {
|
|
188
|
+
if (typeof customElements === 'undefined') return;
|
|
189
|
+
if (customElements.get(name)) return; // already defined (HMR / double-import)
|
|
190
|
+
customElements.define(name, cls);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Layout helpers: encode the design-system rhythm (spacing between label /
|
|
195
|
+
// input / hint, between form fields, between sections). Change one helper to
|
|
196
|
+
// retune the whole app: call sites stay readable inline Tailwind.
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/** Vertical rhythm inside a single form field: label ↔ control ↔ hint/error. */
|
|
200
|
+
export const fieldClass = () => 'grid gap-2';
|
|
201
|
+
|
|
202
|
+
/** Horizontal field layout: label on the left, control on the right. */
|
|
203
|
+
export const fieldRowClass = () => 'flex items-center gap-3';
|
|
204
|
+
|
|
205
|
+
/** Gap step for `stackClass({ gap })`. */
|
|
206
|
+
export type StackGap = 'sm' | 'md' | 'lg';
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Stack of form fields. `sm` for tight groupings, `lg` for spaced-out sections.
|
|
210
|
+
*
|
|
211
|
+
* Object-arg shape matches the rest of the kit (`buttonClass({ variant, size })`,
|
|
212
|
+
* `badgeClass({ variant })`, etc.): predictable across all helpers and
|
|
213
|
+
* extensible if a second dimension (e.g. `direction`) is ever added.
|
|
214
|
+
*/
|
|
215
|
+
export const stackClass = (opts: { gap?: StackGap } = {}): string => {
|
|
216
|
+
const gap = opts.gap ?? 'md';
|
|
217
|
+
return gap === 'sm' ? 'grid gap-3' : gap === 'lg' ? 'grid gap-8' : 'grid gap-6';
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** Form body: same rhythm as a `lg` stack; semantic name for `<form>` content. */
|
|
221
|
+
export const formClass = () => 'grid gap-6';
|
|
222
|
+
|
|
223
|
+
/** Top-level section separation (between form groups, between sections of a page). */
|
|
224
|
+
export const sectionClass = () => 'grid gap-8';
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Typography helpers: fixed text styles used across the design system.
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/** Form-field label: `<label>` text style. */
|
|
231
|
+
export const fieldLabelClass = () =>
|
|
232
|
+
'text-sm leading-none font-medium select-none group-data-[disabled=true]/field:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50';
|
|
233
|
+
|
|
234
|
+
/** Subdued helper / hint text below a form field. */
|
|
235
|
+
export const hintClass = () => 'text-sm text-muted-foreground';
|
|
236
|
+
|
|
237
|
+
/** Tertiary help text (smaller than hint). */
|
|
238
|
+
export const helpClass = () => 'text-xs text-muted-foreground';
|
|
239
|
+
|
|
240
|
+
/** Validation error text: replaces hint when the field is invalid. */
|
|
241
|
+
export const errorClass = () => 'text-sm font-medium text-destructive';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webjsdev/ui-registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Source registry for @webjsdev/ui - component sources, themes, lib. Read on demand by @webjsdev/ui-website to compose registry JSON at request time."
|
|
7
|
+
}
|