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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Baxter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# create-davepi-ui
|
|
2
|
+
|
|
3
|
+
Scaffolder for new [davepi-ui](https://github.com/projik/davepi-ui) admin projects against a running [davepi](https://github.com/projik/davepi) backend.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-davepi-ui my-admin --api-url http://localhost:4001
|
|
9
|
+
cd my-admin
|
|
10
|
+
pnpm dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Flags
|
|
14
|
+
|
|
15
|
+
| Flag | Default | Purpose |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `--api-url <url>` | `http://localhost:4001` | davepi backend base URL written to `.env` |
|
|
18
|
+
| `--no-install` | off | Skip post-scaffold `pnpm install` |
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
1. Copies the bundled template (Vite + React Router + shadcn + `@davepi/ui-react`).
|
|
23
|
+
2. Rewrites `package.json` — pins `@davepi/ui-*` deps to the published versions matching this scaffolder, sets `private: true`, drops upstream repo metadata.
|
|
24
|
+
3. Writes `.env` with `VITE_API_URL`.
|
|
25
|
+
4. Runs `pnpm install` (unless `--no-install`).
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npx create-davepi-ui <name> [--api-url <url>] [--no-install]`
|
|
4
|
+
*
|
|
5
|
+
* Scaffolds a new davepi-ui admin project against a running davepi
|
|
6
|
+
* backend:
|
|
7
|
+
* - copies the `default` template (a stripped Vite + React Router
|
|
8
|
+
* shell built on @davepi/ui-react + shadcn primitives)
|
|
9
|
+
* - rewrites `package.json` with the new project name + pinned
|
|
10
|
+
* published versions of @davepi/ui-* (no workspace: protocol)
|
|
11
|
+
* - writes `.env` carrying VITE_API_URL
|
|
12
|
+
* - runs `pnpm install` (skip with --no-install)
|
|
13
|
+
* - prints the next three commands
|
|
14
|
+
*
|
|
15
|
+
* No `inquirer`, no progress bars — keeps the failure surface small.
|
|
16
|
+
* Match davepi's own `create-davepi-app` posture.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const DEFAULT_API_URL = 'http://localhost:4001';
|
|
26
|
+
|
|
27
|
+
function out(line) {
|
|
28
|
+
process.stdout.write(line + '\n');
|
|
29
|
+
}
|
|
30
|
+
function err(line) {
|
|
31
|
+
process.stderr.write(line + '\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read a `--name <value>` flag from argv. Returns:
|
|
36
|
+
* - null when flag absent
|
|
37
|
+
* - true when flag present but no value follows (boolean flag)
|
|
38
|
+
* - string when flag has a non-flag value following
|
|
39
|
+
*
|
|
40
|
+
* Throws when the next token starts with `--` so a missing value
|
|
41
|
+
* doesn't silently consume the next flag's name.
|
|
42
|
+
*/
|
|
43
|
+
function flag(args, name) {
|
|
44
|
+
const i = args.indexOf(name);
|
|
45
|
+
if (i === -1) return null;
|
|
46
|
+
const next = args[i + 1];
|
|
47
|
+
if (next === undefined) return true;
|
|
48
|
+
if (typeof next === 'string' && next.startsWith('--')) {
|
|
49
|
+
const e = new Error(`Flag ${name} requires a value (got "${next}" which looks like another flag)`);
|
|
50
|
+
e.usage = true;
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function usage() {
|
|
57
|
+
out('Usage: npx create-davepi-ui <name> [--api-url <url>] [--no-install]');
|
|
58
|
+
out('');
|
|
59
|
+
out('Flags:');
|
|
60
|
+
out(' --api-url <url> davepi backend base URL (default: http://localhost:4001)');
|
|
61
|
+
out(' --no-install skip the post-scaffold `pnpm install`');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function copyTree(src, dst, skip) {
|
|
65
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
66
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
67
|
+
if (skip && skip.has(entry.name)) continue;
|
|
68
|
+
const a = path.join(src, entry.name);
|
|
69
|
+
const b = path.join(dst, entry.name);
|
|
70
|
+
if (entry.isDirectory()) copyTree(a, b, skip);
|
|
71
|
+
else fs.copyFileSync(a, b);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the template root. Two layouts:
|
|
77
|
+
* - published package: `<pkg>/templates/default` (shipped via
|
|
78
|
+
* `prepublishOnly`).
|
|
79
|
+
* - inside the monorepo during dev: fall back to
|
|
80
|
+
* `<pkg>/../ui-app-react` directly.
|
|
81
|
+
*/
|
|
82
|
+
function resolveTemplate() {
|
|
83
|
+
const shipped = path.resolve(__dirname, '..', 'templates', 'default');
|
|
84
|
+
if (fs.existsSync(shipped)) return shipped;
|
|
85
|
+
const monorepoFallback = path.resolve(__dirname, '..', '..', 'ui-app-react');
|
|
86
|
+
if (fs.existsSync(monorepoFallback)) return monorepoFallback;
|
|
87
|
+
err('Could not find scaffolder template. Reinstall create-davepi-ui.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pin versions used in the template's `package.json`. Reads the
|
|
93
|
+
* shipped `pinned-versions.json` if present (written at publish time
|
|
94
|
+
* by `sync-templates.js`); otherwise falls back to the monorepo
|
|
95
|
+
* workspace versions discovered by walking siblings.
|
|
96
|
+
*/
|
|
97
|
+
function resolvePinnedVersions() {
|
|
98
|
+
const pinnedFile = path.resolve(__dirname, '..', 'templates', 'pinned-versions.json');
|
|
99
|
+
if (fs.existsSync(pinnedFile)) {
|
|
100
|
+
return JSON.parse(fs.readFileSync(pinnedFile, 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
const out = {};
|
|
103
|
+
const packagesDir = path.resolve(__dirname, '..', '..');
|
|
104
|
+
for (const name of fs.readdirSync(packagesDir)) {
|
|
105
|
+
const pj = path.join(packagesDir, name, 'package.json');
|
|
106
|
+
if (!fs.existsSync(pj)) continue;
|
|
107
|
+
try {
|
|
108
|
+
const p = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
109
|
+
if (p.name && p.name.startsWith('@davepi/ui-') && p.version) {
|
|
110
|
+
out[p.name] = `^${p.version}`;
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Rewrite a scaffolded `package.json`:
|
|
119
|
+
* - set `name` to the project name
|
|
120
|
+
* - reset to `private: true`, `version: "0.0.0"`
|
|
121
|
+
* - replace `workspace:^` / `workspace:*` references in deps and
|
|
122
|
+
* devDeps with the pinned semver from the published packages
|
|
123
|
+
* - drop the original repo / publish metadata so the user's new
|
|
124
|
+
* project doesn't impersonate davepi-ui
|
|
125
|
+
*/
|
|
126
|
+
function rewritePackageJson(file, projectName, pins) {
|
|
127
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
128
|
+
raw.name = projectName;
|
|
129
|
+
raw.version = '0.0.0';
|
|
130
|
+
raw.private = true;
|
|
131
|
+
delete raw.description;
|
|
132
|
+
delete raw.publishConfig;
|
|
133
|
+
delete raw.repository;
|
|
134
|
+
delete raw.bugs;
|
|
135
|
+
delete raw.homepage;
|
|
136
|
+
delete raw.keywords;
|
|
137
|
+
for (const section of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
138
|
+
const block = raw[section];
|
|
139
|
+
if (!block) continue;
|
|
140
|
+
for (const [dep, spec] of Object.entries(block)) {
|
|
141
|
+
if (typeof spec === 'string' && spec.startsWith('workspace:')) {
|
|
142
|
+
if (pins[dep]) {
|
|
143
|
+
block[dep] = pins[dep];
|
|
144
|
+
} else {
|
|
145
|
+
// Conservative fallback — leave a known-bad spec rather than
|
|
146
|
+
// an unresolvable workspace: marker so the install surface
|
|
147
|
+
// surfaces the gap loudly.
|
|
148
|
+
block[dep] = 'latest';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
fs.writeFileSync(file, JSON.stringify(raw, null, 2) + '\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function main() {
|
|
157
|
+
const args = process.argv.slice(2);
|
|
158
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
159
|
+
usage();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
164
|
+
const name = positional[0];
|
|
165
|
+
if (!name) {
|
|
166
|
+
err('Project name is required.');
|
|
167
|
+
usage();
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
171
|
+
err(`Invalid project name: "${name}". Use kebab-case, no leading dot/dash.`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let apiUrl, skipInstall;
|
|
176
|
+
try {
|
|
177
|
+
const a = flag(args, '--api-url');
|
|
178
|
+
apiUrl = typeof a === 'string' ? a : DEFAULT_API_URL;
|
|
179
|
+
skipInstall = flag(args, '--no-install') === true;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
err(e.message);
|
|
182
|
+
if (e.usage) usage();
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const target = path.resolve(process.cwd(), name);
|
|
187
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
|
|
188
|
+
err(`Target directory "${name}" already exists and is not empty.`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const template = resolveTemplate();
|
|
193
|
+
out(`Scaffolding ${name} from ${template} → ${target}`);
|
|
194
|
+
copyTree(template, target, new Set(['node_modules', 'dist', '.turbo', 'storybook-static', '.astro']));
|
|
195
|
+
|
|
196
|
+
const pkgPath = path.join(target, 'package.json');
|
|
197
|
+
if (fs.existsSync(pkgPath)) {
|
|
198
|
+
rewritePackageJson(pkgPath, name, resolvePinnedVersions());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Write a starter `.env` so the user can run `pnpm dev` immediately.
|
|
202
|
+
fs.writeFileSync(path.join(target, '.env'), `VITE_API_URL=${apiUrl}\n`);
|
|
203
|
+
|
|
204
|
+
if (!skipInstall) {
|
|
205
|
+
out('Installing dependencies…');
|
|
206
|
+
// spawnSync with an argument array — no shell, no interpolation,
|
|
207
|
+
// so the only program ever invoked is the literal `pnpm`.
|
|
208
|
+
const r = spawnSync('pnpm', ['install'], { cwd: target, stdio: 'inherit' });
|
|
209
|
+
if (r.status !== 0) {
|
|
210
|
+
err('pnpm install failed. You can re-run it from the project directory.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
out('');
|
|
215
|
+
out('Done.');
|
|
216
|
+
out('');
|
|
217
|
+
out(` cd ${name}`);
|
|
218
|
+
if (skipInstall) out(' pnpm install');
|
|
219
|
+
out(' pnpm dev');
|
|
220
|
+
out('');
|
|
221
|
+
out(`Backend expected at ${apiUrl}. Edit .env to point elsewhere.`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
main();
|
|
226
|
+
} catch (e) {
|
|
227
|
+
err(e.stack || e.message);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pre-publish hook: copy `packages/ui-app-react/` (sans build
|
|
4
|
+
* artifacts) into this package's `templates/default/` so the
|
|
5
|
+
* published tarball carries the template and consumers don't need
|
|
6
|
+
* the davepi-ui monorepo layout to scaffold.
|
|
7
|
+
*
|
|
8
|
+
* Also writes `templates/pinned-versions.json` capturing each
|
|
9
|
+
* `@davepi/ui-*` workspace package's current version, so the
|
|
10
|
+
* scaffolder can rewrite `workspace:^` references in the template's
|
|
11
|
+
* package.json to the matching `^<version>` semver at the moment the
|
|
12
|
+
* tarball was built.
|
|
13
|
+
*
|
|
14
|
+
* The runtime CLI (`bin/index.js`) prefers `templates/default`
|
|
15
|
+
* inside the package; in dev (running from the monorepo) it falls
|
|
16
|
+
* back to `../ui-app-react` directly and reads versions from the
|
|
17
|
+
* sibling package.json files.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const repoRoot = path.resolve(__dirname, '..', '..', '..');
|
|
26
|
+
const srcApp = path.join(repoRoot, 'packages', 'ui-app-react');
|
|
27
|
+
const dstTemplate = path.resolve(__dirname, '..', 'templates', 'default');
|
|
28
|
+
const dstPins = path.resolve(__dirname, '..', 'templates', 'pinned-versions.json');
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(srcApp)) {
|
|
31
|
+
process.stderr.write(
|
|
32
|
+
`sync-templates: ${srcApp} not found. Run from inside the davepi-ui monorepo.\n`
|
|
33
|
+
);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fs.rmSync(path.resolve(__dirname, '..', 'templates'), { recursive: true, force: true });
|
|
38
|
+
fs.mkdirSync(dstTemplate, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const SKIP = new Set([
|
|
41
|
+
'node_modules',
|
|
42
|
+
'dist',
|
|
43
|
+
'.turbo',
|
|
44
|
+
'storybook-static',
|
|
45
|
+
'.astro',
|
|
46
|
+
'.env',
|
|
47
|
+
'.env.local',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function copyTree(src, dst) {
|
|
51
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
52
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
53
|
+
if (SKIP.has(entry.name)) continue;
|
|
54
|
+
const a = path.join(src, entry.name);
|
|
55
|
+
const b = path.join(dst, entry.name);
|
|
56
|
+
if (entry.isDirectory()) copyTree(a, b);
|
|
57
|
+
else fs.copyFileSync(a, b);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
copyTree(srcApp, dstTemplate);
|
|
62
|
+
|
|
63
|
+
// tsconfig.json in the template extends `../../tsconfig.base.json`,
|
|
64
|
+
// which doesn't exist in a scaffolded project. Inline the base so
|
|
65
|
+
// the scaffolded tsconfig is standalone.
|
|
66
|
+
const baseTs = JSON.parse(fs.readFileSync(path.join(repoRoot, 'tsconfig.base.json'), 'utf8'));
|
|
67
|
+
const tplTsPath = path.join(dstTemplate, 'tsconfig.json');
|
|
68
|
+
if (fs.existsSync(tplTsPath)) {
|
|
69
|
+
const tplTs = JSON.parse(fs.readFileSync(tplTsPath, 'utf8'));
|
|
70
|
+
delete tplTs.extends;
|
|
71
|
+
tplTs.compilerOptions = { ...baseTs.compilerOptions, ...(tplTs.compilerOptions || {}) };
|
|
72
|
+
fs.writeFileSync(tplTsPath, JSON.stringify(tplTs, null, 2) + '\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Capture the live version of every PUBLISHED `@davepi/ui-*` package
|
|
76
|
+
// so the scaffolder can pin them at consume time without a workspace
|
|
77
|
+
// protocol leaking into the user's package.json. `private: true`
|
|
78
|
+
// packages are skipped — they're not on the registry, so a pin
|
|
79
|
+
// against them would 404 at install time. The example app
|
|
80
|
+
// (`@davepi/ui-app-react`) is private for this reason; the template
|
|
81
|
+
// IS that package, so its scaffolded `package.json` doesn't reference
|
|
82
|
+
// it by name anyway.
|
|
83
|
+
const pins = {};
|
|
84
|
+
const packagesDir = path.join(repoRoot, 'packages');
|
|
85
|
+
for (const name of fs.readdirSync(packagesDir)) {
|
|
86
|
+
const pj = path.join(packagesDir, name, 'package.json');
|
|
87
|
+
if (!fs.existsSync(pj)) continue;
|
|
88
|
+
try {
|
|
89
|
+
const p = JSON.parse(fs.readFileSync(pj, 'utf8'));
|
|
90
|
+
if (p.private === true) continue;
|
|
91
|
+
if (p.name && p.name.startsWith('@davepi/ui-') && p.version) {
|
|
92
|
+
pins[p.name] = `^${p.version}`;
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
fs.writeFileSync(dstPins, JSON.stringify(pins, null, 2) + '\n');
|
|
97
|
+
|
|
98
|
+
process.stdout.write(
|
|
99
|
+
`sync-templates: copied ${srcApp} → ${dstTemplate} (pins: ${JSON.stringify(pins)})\n`
|
|
100
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-davepi-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffolder for new davepi-ui projects. Run: npx create-davepi-ui <name> [--api-url <url>]",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "David Baxter <dave@unlockedequity.com>",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-davepi-ui": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"templates/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/projik/davepi-ui#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/projik/davepi-ui.git",
|
|
23
|
+
"directory": "packages/create-davepi-ui"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/projik/davepi-ui/issues"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"davepi",
|
|
30
|
+
"davepi-ui",
|
|
31
|
+
"scaffolder",
|
|
32
|
+
"create-app",
|
|
33
|
+
"admin",
|
|
34
|
+
"template"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {}
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VITE_API_URL=http://localhost:4001
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>davepi-ui</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="bg-background text-foreground">
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@davepi/ui-app-react",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Deployable Vite + React admin shell for davepi-ui (template — clone, don't install)",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"clean": "rm -rf dist .turbo"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@davepi/ui-core": "workspace:*",
|
|
16
|
+
"@davepi/ui-react": "workspace:*",
|
|
17
|
+
"@hookform/resolvers": "^3.9.1",
|
|
18
|
+
"@radix-ui/react-checkbox": "^1.1.3",
|
|
19
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
20
|
+
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
|
21
|
+
"@radix-ui/react-label": "^2.1.1",
|
|
22
|
+
"@radix-ui/react-popover": "^1.1.4",
|
|
23
|
+
"@radix-ui/react-select": "^2.1.4",
|
|
24
|
+
"@radix-ui/react-slot": "^1.1.1",
|
|
25
|
+
"@radix-ui/react-switch": "^1.1.2",
|
|
26
|
+
"@radix-ui/react-tabs": "^1.1.2",
|
|
27
|
+
"cmdk": "^1.0.4",
|
|
28
|
+
"@tanstack/react-query": "^5.62.7",
|
|
29
|
+
"class-variance-authority": "^0.7.1",
|
|
30
|
+
"clsx": "^2.1.1",
|
|
31
|
+
"lucide-react": "^0.469.0",
|
|
32
|
+
"react": "^19.0.0",
|
|
33
|
+
"react-dom": "^19.0.0",
|
|
34
|
+
"react-hook-form": "^7.54.2",
|
|
35
|
+
"react-router-dom": "^7.1.1",
|
|
36
|
+
"tailwind-merge": "^2.5.5",
|
|
37
|
+
"zod": "^3.23.8"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19.0.1",
|
|
41
|
+
"@types/react-dom": "^19.0.2",
|
|
42
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
43
|
+
"autoprefixer": "^10.4.20",
|
|
44
|
+
"postcss": "^8.4.49",
|
|
45
|
+
"tailwindcss": "^3.4.17",
|
|
46
|
+
"typescript": "^5.6.3",
|
|
47
|
+
"vite": "^6.0.7"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Navigate, Route, Routes } from 'react-router-dom';
|
|
2
|
+
import { AuthGuard, useAuth } from '@davepi/ui-react';
|
|
3
|
+
import { AppShell } from './components/AppShell';
|
|
4
|
+
import { LoginScreen } from './pages/LoginScreen';
|
|
5
|
+
import { DashboardPage } from './pages/DashboardPage';
|
|
6
|
+
import { ResourceListPage } from './pages/ResourceListPage';
|
|
7
|
+
import { ResourceCreatePage } from './pages/ResourceCreatePage';
|
|
8
|
+
import { ResourceEditPage } from './pages/ResourceEditPage';
|
|
9
|
+
import { ResourceDetailPage } from './pages/ResourceDetailPage';
|
|
10
|
+
|
|
11
|
+
export function App() {
|
|
12
|
+
const { status } = useAuth();
|
|
13
|
+
|
|
14
|
+
if (status === 'unknown') {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex h-screen items-center justify-center text-muted-foreground">
|
|
17
|
+
Loading…
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Routes>
|
|
24
|
+
<Route path="/login" element={<LoginScreen />} />
|
|
25
|
+
<Route
|
|
26
|
+
path="/"
|
|
27
|
+
element={
|
|
28
|
+
<AuthGuard fallback={<Navigate to="/login" replace />}>
|
|
29
|
+
<AppShell />
|
|
30
|
+
</AuthGuard>
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
<Route index element={<DashboardPage />} />
|
|
34
|
+
<Route path="r/:path" element={<ResourceListPage />} />
|
|
35
|
+
<Route path="r/:path/new" element={<ResourceCreatePage />} />
|
|
36
|
+
<Route path="r/:path/:id" element={<ResourceDetailPage />} />
|
|
37
|
+
<Route path="r/:path/:id/edit" element={<ResourceEditPage />} />
|
|
38
|
+
</Route>
|
|
39
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
40
|
+
</Routes>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Outlet } from 'react-router-dom';
|
|
2
|
+
import { UserMenu } from '@davepi/ui-react';
|
|
3
|
+
import { Sidebar } from './Sidebar';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Two-column shell: sidebar nav (resource list from describe) + main outlet.
|
|
7
|
+
* Tablet/mobile responsive collapsing is M1+ scope.
|
|
8
|
+
*/
|
|
9
|
+
export function AppShell() {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex h-screen bg-background text-foreground">
|
|
12
|
+
<Sidebar />
|
|
13
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
14
|
+
<header className="flex h-14 items-center justify-end gap-3 border-b border-border px-6">
|
|
15
|
+
<UserMenu className="flex items-center gap-3 text-sm" />
|
|
16
|
+
</header>
|
|
17
|
+
<main className="flex-1 overflow-auto p-6">
|
|
18
|
+
<Outlet />
|
|
19
|
+
</main>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { descriptor } from '@davepi/ui-core';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sticky bar shown when one or more table rows are selected. Renders the
|
|
6
|
+
* configured `bulk` actions plus a count and a "clear selection" button.
|
|
7
|
+
*/
|
|
8
|
+
export interface BulkActionBarProps {
|
|
9
|
+
selectedIds: readonly string[];
|
|
10
|
+
actions: descriptor.ActionSpec[];
|
|
11
|
+
onClear: () => void;
|
|
12
|
+
onRun: (action: descriptor.ActionSpec, ids: readonly string[]) => void;
|
|
13
|
+
resourceLabel: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function BulkActionBar({
|
|
17
|
+
selectedIds,
|
|
18
|
+
actions,
|
|
19
|
+
onClear,
|
|
20
|
+
onRun,
|
|
21
|
+
resourceLabel,
|
|
22
|
+
}: BulkActionBarProps) {
|
|
23
|
+
if (!selectedIds.length) return null;
|
|
24
|
+
return (
|
|
25
|
+
<div className="sticky bottom-4 z-10 mx-auto flex w-fit items-center gap-3 rounded-full border border-border bg-card px-4 py-2 shadow-lg">
|
|
26
|
+
<span className="text-sm text-muted-foreground">
|
|
27
|
+
{selectedIds.length} {resourceLabel.toLowerCase()} selected
|
|
28
|
+
</span>
|
|
29
|
+
<div className="flex items-center gap-2">
|
|
30
|
+
{actions.map((action) => (
|
|
31
|
+
<Button
|
|
32
|
+
key={action.id}
|
|
33
|
+
type="button"
|
|
34
|
+
variant={action.kind === 'bulkDelete' ? 'destructive' : 'outline'}
|
|
35
|
+
size="sm"
|
|
36
|
+
onClick={() => onRun(action, selectedIds)}
|
|
37
|
+
>
|
|
38
|
+
{action.label}
|
|
39
|
+
</Button>
|
|
40
|
+
))}
|
|
41
|
+
<Button type="button" variant="ghost" size="sm" onClick={onClear}>
|
|
42
|
+
Clear
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|