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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -0
  3. package/bin/index.js +229 -0
  4. package/bin/sync-templates.js +100 -0
  5. package/package.json +40 -0
  6. package/templates/default/.env.example +1 -0
  7. package/templates/default/index.html +13 -0
  8. package/templates/default/package.json +49 -0
  9. package/templates/default/postcss.config.cjs +6 -0
  10. package/templates/default/src/App.tsx +42 -0
  11. package/templates/default/src/components/AppShell.tsx +23 -0
  12. package/templates/default/src/components/BulkActionBar.tsx +47 -0
  13. package/templates/default/src/components/RelatedCreateModal.tsx +91 -0
  14. package/templates/default/src/components/RelatedList.tsx +70 -0
  15. package/templates/default/src/components/ResourceForm.tsx +311 -0
  16. package/templates/default/src/components/ResourceTable.tsx +475 -0
  17. package/templates/default/src/components/RowActions.tsx +54 -0
  18. package/templates/default/src/components/Sidebar.tsx +171 -0
  19. package/templates/default/src/components/ui/button.tsx +43 -0
  20. package/templates/default/src/components/ui/card.tsx +47 -0
  21. package/templates/default/src/components/ui/checkbox.tsx +24 -0
  22. package/templates/default/src/components/ui/command.tsx +117 -0
  23. package/templates/default/src/components/ui/dialog.tsx +95 -0
  24. package/templates/default/src/components/ui/dropdown-menu.tsx +78 -0
  25. package/templates/default/src/components/ui/input.tsx +18 -0
  26. package/templates/default/src/components/ui/label.tsx +17 -0
  27. package/templates/default/src/components/ui/popover.tsx +27 -0
  28. package/templates/default/src/components/ui/select.tsx +83 -0
  29. package/templates/default/src/components/ui/switch.tsx +21 -0
  30. package/templates/default/src/components/ui/table.tsx +66 -0
  31. package/templates/default/src/components/ui/tabs.tsx +53 -0
  32. package/templates/default/src/components/ui/textarea.tsx +17 -0
  33. package/templates/default/src/davepi-ui.config.ts +14 -0
  34. package/templates/default/src/index.css +55 -0
  35. package/templates/default/src/lib/utils.ts +10 -0
  36. package/templates/default/src/main.tsx +34 -0
  37. package/templates/default/src/pages/DashboardPage.tsx +42 -0
  38. package/templates/default/src/pages/LoginScreen.tsx +77 -0
  39. package/templates/default/src/pages/ResourceCreatePage.tsx +58 -0
  40. package/templates/default/src/pages/ResourceDetailPage.tsx +171 -0
  41. package/templates/default/src/pages/ResourceEditPage.tsx +52 -0
  42. package/templates/default/src/pages/ResourceListPage.tsx +8 -0
  43. package/templates/default/src/resourceOverrides.ts +34 -0
  44. package/templates/default/src/resources/account.ts +25 -0
  45. package/templates/default/src/resources/category.ts +7 -0
  46. package/templates/default/src/resources/contact.ts +40 -0
  47. package/templates/default/src/resources/product.ts +7 -0
  48. package/templates/default/src/resources/project.ts +7 -0
  49. package/templates/default/src/resources/quote.ts +12 -0
  50. package/templates/default/src/vite-env.d.ts +9 -0
  51. package/templates/default/src/widgets/CurrencyInput.tsx +44 -0
  52. package/templates/default/src/widgets/DateInput.tsx +36 -0
  53. package/templates/default/src/widgets/EmailInput.tsx +28 -0
  54. package/templates/default/src/widgets/EnumSelect.tsx +35 -0
  55. package/templates/default/src/widgets/FileUploaderStub.tsx +9 -0
  56. package/templates/default/src/widgets/JsonEditor.tsx +64 -0
  57. package/templates/default/src/widgets/NumberInput.tsx +36 -0
  58. package/templates/default/src/widgets/RelationPicker.tsx +349 -0
  59. package/templates/default/src/widgets/SwitchWidget.tsx +20 -0
  60. package/templates/default/src/widgets/TagInput.tsx +83 -0
  61. package/templates/default/src/widgets/TextAreaWidget.tsx +27 -0
  62. package/templates/default/src/widgets/TextInput.tsx +32 -0
  63. package/templates/default/src/widgets/UrlInput.tsx +27 -0
  64. package/templates/default/src/widgets/registry.ts +51 -0
  65. package/templates/default/src/widgets/types.ts +26 -0
  66. package/templates/default/tailwind.config.ts +54 -0
  67. package/templates/default/tsconfig.json +40 -0
  68. package/templates/default/vite.config.ts +16 -0
  69. 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,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -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
+ }