create-davepi-ui 0.1.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/LICENSE +21 -0
- package/README.md +29 -0
- package/bin/index.js +229 -0
- package/bin/sync-templates.js +100 -0
- package/package.json +40 -0
- package/templates/default/.env.example +1 -0
- package/templates/default/index.html +13 -0
- package/templates/default/package.json +49 -0
- package/templates/default/postcss.config.cjs +6 -0
- package/templates/default/src/App.tsx +42 -0
- package/templates/default/src/components/AppShell.tsx +23 -0
- package/templates/default/src/components/BulkActionBar.tsx +47 -0
- package/templates/default/src/components/RelatedCreateModal.tsx +91 -0
- package/templates/default/src/components/RelatedList.tsx +70 -0
- package/templates/default/src/components/ResourceForm.tsx +311 -0
- package/templates/default/src/components/ResourceTable.tsx +475 -0
- package/templates/default/src/components/RowActions.tsx +54 -0
- package/templates/default/src/components/Sidebar.tsx +171 -0
- package/templates/default/src/components/ui/button.tsx +43 -0
- package/templates/default/src/components/ui/card.tsx +47 -0
- package/templates/default/src/components/ui/checkbox.tsx +24 -0
- package/templates/default/src/components/ui/command.tsx +117 -0
- package/templates/default/src/components/ui/dialog.tsx +95 -0
- package/templates/default/src/components/ui/dropdown-menu.tsx +78 -0
- package/templates/default/src/components/ui/input.tsx +18 -0
- package/templates/default/src/components/ui/label.tsx +17 -0
- package/templates/default/src/components/ui/popover.tsx +27 -0
- package/templates/default/src/components/ui/select.tsx +83 -0
- package/templates/default/src/components/ui/switch.tsx +21 -0
- package/templates/default/src/components/ui/table.tsx +66 -0
- package/templates/default/src/components/ui/tabs.tsx +53 -0
- package/templates/default/src/components/ui/textarea.tsx +17 -0
- package/templates/default/src/davepi-ui.config.ts +14 -0
- package/templates/default/src/index.css +55 -0
- package/templates/default/src/lib/utils.ts +10 -0
- package/templates/default/src/main.tsx +34 -0
- package/templates/default/src/pages/DashboardPage.tsx +42 -0
- package/templates/default/src/pages/LoginScreen.tsx +77 -0
- package/templates/default/src/pages/ResourceCreatePage.tsx +58 -0
- package/templates/default/src/pages/ResourceDetailPage.tsx +171 -0
- package/templates/default/src/pages/ResourceEditPage.tsx +52 -0
- package/templates/default/src/pages/ResourceListPage.tsx +8 -0
- package/templates/default/src/resourceOverrides.ts +34 -0
- package/templates/default/src/resources/account.ts +25 -0
- package/templates/default/src/resources/category.ts +7 -0
- package/templates/default/src/resources/contact.ts +40 -0
- package/templates/default/src/resources/product.ts +7 -0
- package/templates/default/src/resources/project.ts +7 -0
- package/templates/default/src/resources/quote.ts +12 -0
- package/templates/default/src/vite-env.d.ts +9 -0
- package/templates/default/src/widgets/CurrencyInput.tsx +44 -0
- package/templates/default/src/widgets/DateInput.tsx +36 -0
- package/templates/default/src/widgets/EmailInput.tsx +28 -0
- package/templates/default/src/widgets/EnumSelect.tsx +35 -0
- package/templates/default/src/widgets/FileUploaderStub.tsx +9 -0
- package/templates/default/src/widgets/JsonEditor.tsx +64 -0
- package/templates/default/src/widgets/NumberInput.tsx +36 -0
- package/templates/default/src/widgets/RelationPicker.tsx +349 -0
- package/templates/default/src/widgets/SwitchWidget.tsx +20 -0
- package/templates/default/src/widgets/TagInput.tsx +83 -0
- package/templates/default/src/widgets/TextAreaWidget.tsx +27 -0
- package/templates/default/src/widgets/TextInput.tsx +32 -0
- package/templates/default/src/widgets/UrlInput.tsx +27 -0
- package/templates/default/src/widgets/registry.ts +51 -0
- package/templates/default/src/widgets/types.ts +26 -0
- package/templates/default/tailwind.config.ts +54 -0
- package/templates/default/tsconfig.json +40 -0
- package/templates/default/vite.config.ts +16 -0
- package/templates/pinned-versions.json +5 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Switch } from '@/components/ui/switch';
|
|
2
|
+
import type { WidgetComponentProps } from './types';
|
|
3
|
+
|
|
4
|
+
export function SwitchWidget({
|
|
5
|
+
value,
|
|
6
|
+
onChange,
|
|
7
|
+
disabled,
|
|
8
|
+
id,
|
|
9
|
+
name,
|
|
10
|
+
}: WidgetComponentProps<boolean | undefined>) {
|
|
11
|
+
return (
|
|
12
|
+
<Switch
|
|
13
|
+
id={id}
|
|
14
|
+
name={name}
|
|
15
|
+
checked={value ?? false}
|
|
16
|
+
onCheckedChange={(next) => onChange(next)}
|
|
17
|
+
disabled={disabled}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useState, type KeyboardEvent } from 'react';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import { Input } from '@/components/ui/input';
|
|
4
|
+
import type { WidgetComponentProps } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Comma-delimited / Enter-delimited tag input. Backing value is a string
|
|
8
|
+
* array. Empty state renders just the input; values render as chips with
|
|
9
|
+
* an X to remove.
|
|
10
|
+
*/
|
|
11
|
+
export function TagInput({
|
|
12
|
+
value,
|
|
13
|
+
onChange,
|
|
14
|
+
onBlur,
|
|
15
|
+
disabled,
|
|
16
|
+
readOnly,
|
|
17
|
+
id,
|
|
18
|
+
name,
|
|
19
|
+
placeholder,
|
|
20
|
+
}: WidgetComponentProps<string[] | undefined>) {
|
|
21
|
+
const tags = value ?? [];
|
|
22
|
+
const [pending, setPending] = useState('');
|
|
23
|
+
|
|
24
|
+
function commit(raw: string) {
|
|
25
|
+
const next = raw.trim();
|
|
26
|
+
if (!next) return;
|
|
27
|
+
if (tags.includes(next)) return;
|
|
28
|
+
onChange([...tags, next]);
|
|
29
|
+
setPending('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function remove(index: number) {
|
|
33
|
+
onChange(tags.filter((_, i) => i !== index));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleKey(e: KeyboardEvent<HTMLInputElement>) {
|
|
37
|
+
if (e.key === 'Enter' || e.key === ',') {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
commit(pending);
|
|
40
|
+
} else if (e.key === 'Backspace' && pending === '' && tags.length) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
remove(tags.length - 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex flex-wrap items-center gap-1 rounded-md border border-input bg-background p-1">
|
|
48
|
+
{tags.map((t, i) => (
|
|
49
|
+
<span
|
|
50
|
+
key={`${t}:${i}`}
|
|
51
|
+
className="inline-flex items-center gap-1 rounded-sm bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
|
|
52
|
+
>
|
|
53
|
+
{t}
|
|
54
|
+
{!readOnly && !disabled ? (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
aria-label={`Remove ${t}`}
|
|
58
|
+
className="rounded-sm hover:bg-muted"
|
|
59
|
+
onClick={() => remove(i)}
|
|
60
|
+
>
|
|
61
|
+
<X className="h-3 w-3" />
|
|
62
|
+
</button>
|
|
63
|
+
) : null}
|
|
64
|
+
</span>
|
|
65
|
+
))}
|
|
66
|
+
<Input
|
|
67
|
+
id={id}
|
|
68
|
+
name={name}
|
|
69
|
+
value={pending}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
readOnly={readOnly}
|
|
72
|
+
placeholder={tags.length ? undefined : (placeholder ?? 'Add tag…')}
|
|
73
|
+
className="h-7 flex-1 border-0 bg-transparent p-0 shadow-none focus-visible:ring-0"
|
|
74
|
+
onChange={(e) => setPending(e.target.value)}
|
|
75
|
+
onKeyDown={handleKey}
|
|
76
|
+
onBlur={() => {
|
|
77
|
+
commit(pending);
|
|
78
|
+
onBlur?.();
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
2
|
+
import type { WidgetComponentProps } from './types';
|
|
3
|
+
|
|
4
|
+
export function TextAreaWidget({
|
|
5
|
+
value,
|
|
6
|
+
onChange,
|
|
7
|
+
onBlur,
|
|
8
|
+
disabled,
|
|
9
|
+
readOnly,
|
|
10
|
+
id,
|
|
11
|
+
name,
|
|
12
|
+
placeholder,
|
|
13
|
+
}: WidgetComponentProps<string | undefined>) {
|
|
14
|
+
return (
|
|
15
|
+
<Textarea
|
|
16
|
+
id={id}
|
|
17
|
+
name={name}
|
|
18
|
+
value={value ?? ''}
|
|
19
|
+
onChange={(e) => onChange(e.target.value)}
|
|
20
|
+
onBlur={onBlur}
|
|
21
|
+
disabled={disabled}
|
|
22
|
+
readOnly={readOnly}
|
|
23
|
+
placeholder={placeholder}
|
|
24
|
+
rows={4}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Input } from '@/components/ui/input';
|
|
2
|
+
import type { WidgetComponentProps } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Single-line text widget. Used for plain String fields.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <TextInput spec={spec} value={value ?? ''} onChange={onChange} />
|
|
9
|
+
*/
|
|
10
|
+
export function TextInput({
|
|
11
|
+
value,
|
|
12
|
+
onChange,
|
|
13
|
+
onBlur,
|
|
14
|
+
disabled,
|
|
15
|
+
readOnly,
|
|
16
|
+
id,
|
|
17
|
+
name,
|
|
18
|
+
placeholder,
|
|
19
|
+
}: WidgetComponentProps<string | undefined>) {
|
|
20
|
+
return (
|
|
21
|
+
<Input
|
|
22
|
+
id={id}
|
|
23
|
+
name={name}
|
|
24
|
+
value={value ?? ''}
|
|
25
|
+
onChange={(e) => onChange(e.target.value)}
|
|
26
|
+
onBlur={onBlur}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
readOnly={readOnly}
|
|
29
|
+
placeholder={placeholder}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Input } from '@/components/ui/input';
|
|
2
|
+
import type { WidgetComponentProps } from './types';
|
|
3
|
+
|
|
4
|
+
export function UrlInput({
|
|
5
|
+
value,
|
|
6
|
+
onChange,
|
|
7
|
+
onBlur,
|
|
8
|
+
disabled,
|
|
9
|
+
readOnly,
|
|
10
|
+
id,
|
|
11
|
+
name,
|
|
12
|
+
placeholder,
|
|
13
|
+
}: WidgetComponentProps<string | undefined>) {
|
|
14
|
+
return (
|
|
15
|
+
<Input
|
|
16
|
+
id={id}
|
|
17
|
+
name={name}
|
|
18
|
+
type="url"
|
|
19
|
+
value={value ?? ''}
|
|
20
|
+
onChange={(e) => onChange(e.target.value || undefined)}
|
|
21
|
+
onBlur={onBlur}
|
|
22
|
+
disabled={disabled}
|
|
23
|
+
readOnly={readOnly}
|
|
24
|
+
placeholder={placeholder ?? 'https://…'}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { WidgetKind } from '@davepi/ui-core';
|
|
3
|
+
import type { WidgetComponentProps } from './types';
|
|
4
|
+
|
|
5
|
+
import { TextInput } from './TextInput';
|
|
6
|
+
import { TextAreaWidget } from './TextAreaWidget';
|
|
7
|
+
import { NumberInput } from './NumberInput';
|
|
8
|
+
import { SwitchWidget } from './SwitchWidget';
|
|
9
|
+
import { DateInput } from './DateInput';
|
|
10
|
+
import { EnumSelect } from './EnumSelect';
|
|
11
|
+
import { TagInput } from './TagInput';
|
|
12
|
+
import { EmailInput } from './EmailInput';
|
|
13
|
+
import { UrlInput } from './UrlInput';
|
|
14
|
+
import { CurrencyInput } from './CurrencyInput';
|
|
15
|
+
import { JsonEditor } from './JsonEditor';
|
|
16
|
+
import { MultiRelationPicker, RelationPicker } from './RelationPicker';
|
|
17
|
+
import { FileUploaderStub } from './FileUploaderStub';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maps every `WidgetKind` to its React implementation.
|
|
21
|
+
*
|
|
22
|
+
* Override at runtime by passing a custom registry to `<ResourceForm>`.
|
|
23
|
+
* Unknown kinds fall back to `TextInput` rather than throwing — the UI
|
|
24
|
+
* stays functional even if a future widget hint isn't recognised yet.
|
|
25
|
+
*/
|
|
26
|
+
export const widgetRegistry: Record<WidgetKind, ComponentType<WidgetComponentProps<any>>> = {
|
|
27
|
+
TextInput,
|
|
28
|
+
TextArea: TextAreaWidget,
|
|
29
|
+
NumberInput,
|
|
30
|
+
Switch: SwitchWidget,
|
|
31
|
+
DatePicker: DateInput,
|
|
32
|
+
EnumSelect,
|
|
33
|
+
TagInput,
|
|
34
|
+
EmailInput,
|
|
35
|
+
UrlInput,
|
|
36
|
+
CurrencyInput,
|
|
37
|
+
RelationPicker,
|
|
38
|
+
MultiRelationPicker,
|
|
39
|
+
FileUploader: FileUploaderStub,
|
|
40
|
+
JsonEditor,
|
|
41
|
+
RichTextEditor: TextAreaWidget,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function resolveWidgetComponent(
|
|
45
|
+
kind: WidgetKind,
|
|
46
|
+
overrides?: Partial<typeof widgetRegistry>
|
|
47
|
+
): ComponentType<WidgetComponentProps<any>> {
|
|
48
|
+
return overrides?.[kind] ?? widgetRegistry[kind] ?? TextInput;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type { WidgetComponentProps } from './types';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { WidgetSpec } from '@davepi/ui-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contract every davepi-ui widget implements.
|
|
6
|
+
*
|
|
7
|
+
* Widgets are *controlled* — they receive a value + onChange so they can
|
|
8
|
+
* plug into react-hook-form via the Controller render prop. Errors come
|
|
9
|
+
* from RHF and are presented inline by the parent FormField wrapper.
|
|
10
|
+
*/
|
|
11
|
+
export interface WidgetComponentProps<T = unknown> {
|
|
12
|
+
spec: WidgetSpec;
|
|
13
|
+
value: T;
|
|
14
|
+
onChange: (next: T) => void;
|
|
15
|
+
onBlur?: () => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
id?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
/** Field-level validation error message. */
|
|
21
|
+
error?: string;
|
|
22
|
+
/** Auxiliary helper text rendered below the field. */
|
|
23
|
+
description?: ReactNode;
|
|
24
|
+
/** Placeholder rendered inside the input where supported. */
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
darkMode: ['class'],
|
|
5
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
6
|
+
theme: {
|
|
7
|
+
container: {
|
|
8
|
+
center: true,
|
|
9
|
+
padding: '2rem',
|
|
10
|
+
screens: { '2xl': '1400px' },
|
|
11
|
+
},
|
|
12
|
+
extend: {
|
|
13
|
+
colors: {
|
|
14
|
+
border: 'hsl(var(--border))',
|
|
15
|
+
input: 'hsl(var(--input))',
|
|
16
|
+
ring: 'hsl(var(--ring))',
|
|
17
|
+
background: 'hsl(var(--background))',
|
|
18
|
+
foreground: 'hsl(var(--foreground))',
|
|
19
|
+
primary: {
|
|
20
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
21
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
22
|
+
},
|
|
23
|
+
secondary: {
|
|
24
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
25
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
26
|
+
},
|
|
27
|
+
destructive: {
|
|
28
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
29
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
30
|
+
},
|
|
31
|
+
muted: {
|
|
32
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
33
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
34
|
+
},
|
|
35
|
+
accent: {
|
|
36
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
37
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
38
|
+
},
|
|
39
|
+
card: {
|
|
40
|
+
DEFAULT: 'hsl(var(--card))',
|
|
41
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
borderRadius: {
|
|
45
|
+
lg: 'var(--radius)',
|
|
46
|
+
md: 'calc(var(--radius) - 2px)',
|
|
47
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
plugins: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default config;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2022",
|
|
6
|
+
"DOM",
|
|
7
|
+
"DOM.Iterable"
|
|
8
|
+
],
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noImplicitOverride": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"declarationMap": true,
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"jsx": "react-jsx",
|
|
23
|
+
"outDir": "dist",
|
|
24
|
+
"rootDir": "src",
|
|
25
|
+
"noEmit": true,
|
|
26
|
+
"allowImportingTsExtensions": false,
|
|
27
|
+
"types": [
|
|
28
|
+
"vite/client"
|
|
29
|
+
],
|
|
30
|
+
"baseUrl": ".",
|
|
31
|
+
"paths": {
|
|
32
|
+
"@/*": [
|
|
33
|
+
"src/*"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"include": [
|
|
38
|
+
"src/**/*"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'@': path.resolve(__dirname, './src'),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
server: {
|
|
13
|
+
port: 5173,
|
|
14
|
+
strictPort: false,
|
|
15
|
+
},
|
|
16
|
+
});
|