@buildpad/cli 0.1.4
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/README.md +557 -0
- package/dist/chunk-J4KKVECI.js +365 -0
- package/dist/chunk-J4KKVECI.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2582 -0
- package/dist/index.js.map +1 -0
- package/dist/outdated-JMAYAZ7W.js +110 -0
- package/dist/outdated-JMAYAZ7W.js.map +1 -0
- package/dist/templates/api/auth-callback-route.ts +36 -0
- package/dist/templates/api/auth-headers.ts +72 -0
- package/dist/templates/api/auth-login-route.ts +63 -0
- package/dist/templates/api/auth-logout-route.ts +41 -0
- package/dist/templates/api/auth-user-route.ts +71 -0
- package/dist/templates/api/collections-route.ts +54 -0
- package/dist/templates/api/fields-route.ts +44 -0
- package/dist/templates/api/files-id-route.ts +116 -0
- package/dist/templates/api/files-route.ts +83 -0
- package/dist/templates/api/items-id-route.ts +120 -0
- package/dist/templates/api/items-route.ts +88 -0
- package/dist/templates/api/login-page.tsx +142 -0
- package/dist/templates/api/permissions-me-route.ts +72 -0
- package/dist/templates/api/relations-route.ts +46 -0
- package/dist/templates/app/content/[collection]/[id]/page.tsx +35 -0
- package/dist/templates/app/content/[collection]/page.tsx +65 -0
- package/dist/templates/app/content/layout.tsx +64 -0
- package/dist/templates/app/content/page.tsx +66 -0
- package/dist/templates/app/design-tokens.css +183 -0
- package/dist/templates/app/globals.css +58 -0
- package/dist/templates/app/layout.tsx +49 -0
- package/dist/templates/app/page.tsx +23 -0
- package/dist/templates/components/ColorSchemeToggle.tsx +35 -0
- package/dist/templates/lib/common-utils.ts +156 -0
- package/dist/templates/lib/hooks/index.ts +98 -0
- package/dist/templates/lib/services/index.ts +31 -0
- package/dist/templates/lib/theme.ts +241 -0
- package/dist/templates/lib/types/index.ts +10 -0
- package/dist/templates/lib/utils-index.ts +32 -0
- package/dist/templates/lib/utils.ts +14 -0
- package/dist/templates/lib/vform/index.ts +24 -0
- package/dist/templates/middleware/middleware.ts +29 -0
- package/dist/templates/supabase/client.ts +25 -0
- package/dist/templates/supabase/middleware.ts +66 -0
- package/dist/templates/supabase/server.ts +45 -0
- package/package.json +61 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Module Layout
|
|
3
|
+
*
|
|
4
|
+
* Provides a content management shell with sidebar collection navigation,
|
|
5
|
+
* header bar, and main content area. Wraps all /content/* pages.
|
|
6
|
+
*
|
|
7
|
+
* Generated by @buildpad/cli
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use client";
|
|
11
|
+
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
14
|
+
import { ContentLayout } from '@/components/ui/content-layout';
|
|
15
|
+
import { ContentNavigation } from '@/components/ui/content-navigation';
|
|
16
|
+
import { useCollections } from '@/lib/buildpad/hooks';
|
|
17
|
+
|
|
18
|
+
export default function ContentModuleLayout({
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
const pathname = usePathname();
|
|
25
|
+
|
|
26
|
+
// Extract current collection from URL: /content/[collection]/...
|
|
27
|
+
const segments = pathname.split('/');
|
|
28
|
+
const currentCollection = segments.length > 2 ? segments[2] : undefined;
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
rootCollections,
|
|
32
|
+
activeGroups,
|
|
33
|
+
toggleGroup,
|
|
34
|
+
showHidden,
|
|
35
|
+
setShowHidden,
|
|
36
|
+
hasHiddenCollections,
|
|
37
|
+
showSearch,
|
|
38
|
+
dense,
|
|
39
|
+
loading,
|
|
40
|
+
} = useCollections({ currentCollection });
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ContentLayout
|
|
44
|
+
breadcrumbs={[{ label: 'Content', href: '/content' }]}
|
|
45
|
+
sidebar={
|
|
46
|
+
<ContentNavigation
|
|
47
|
+
rootCollections={rootCollections}
|
|
48
|
+
activeGroups={activeGroups}
|
|
49
|
+
onToggleGroup={toggleGroup}
|
|
50
|
+
currentCollection={currentCollection}
|
|
51
|
+
onNavigate={(collection) => router.push(`/content/${collection}`)}
|
|
52
|
+
showHidden={showHidden}
|
|
53
|
+
onToggleHidden={() => setShowHidden(!showHidden)}
|
|
54
|
+
hasHiddenCollections={hasHiddenCollections}
|
|
55
|
+
showSearch={showSearch}
|
|
56
|
+
dense={dense}
|
|
57
|
+
loading={loading}
|
|
58
|
+
/>
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</ContentLayout>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Module Index Page
|
|
3
|
+
*
|
|
4
|
+
* Redirects to the first available collection, or shows an empty state
|
|
5
|
+
* if there are no collections yet.
|
|
6
|
+
*
|
|
7
|
+
* Generated by @buildpad/cli
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use client";
|
|
11
|
+
|
|
12
|
+
import React, { useEffect } from 'react';
|
|
13
|
+
import { useRouter } from 'next/navigation';
|
|
14
|
+
import { Stack, Text, Button, Center } from '@mantine/core';
|
|
15
|
+
import { IconBox, IconPlus } from '@tabler/icons-react';
|
|
16
|
+
import { useCollections } from '@/lib/buildpad/hooks';
|
|
17
|
+
import { useLocalStorage } from '@/lib/buildpad/hooks';
|
|
18
|
+
|
|
19
|
+
export default function ContentIndexPage() {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const { visibleCollections, loading } = useCollections();
|
|
22
|
+
const { value: lastAccessed } = useLocalStorage<string>('last-accessed-collection');
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (loading) return;
|
|
26
|
+
|
|
27
|
+
// Redirect to last accessed collection if it still exists
|
|
28
|
+
if (lastAccessed) {
|
|
29
|
+
const exists = visibleCollections.find((c) => c.collection === lastAccessed);
|
|
30
|
+
if (exists) {
|
|
31
|
+
router.replace(`/content/${lastAccessed}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Redirect to first available collection
|
|
37
|
+
if (visibleCollections.length > 0) {
|
|
38
|
+
const first = visibleCollections.find((c) => !!c.schema);
|
|
39
|
+
if (first) {
|
|
40
|
+
router.replace(`/content/${first.collection}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, [loading, lastAccessed, visibleCollections, router]);
|
|
45
|
+
|
|
46
|
+
if (loading) return null;
|
|
47
|
+
|
|
48
|
+
// No collections — show empty state
|
|
49
|
+
if (visibleCollections.length === 0) {
|
|
50
|
+
return (
|
|
51
|
+
<Center style={{ minHeight: '60vh' }}>
|
|
52
|
+
<Stack align="center" gap="md">
|
|
53
|
+
<IconBox size={64} color="var(--mantine-color-gray-5)" stroke={1.5} />
|
|
54
|
+
<Text size="xl" fw={600}>
|
|
55
|
+
No Collections
|
|
56
|
+
</Text>
|
|
57
|
+
<Text c="dimmed" ta="center" maw={400}>
|
|
58
|
+
There are no collections available. Create your first collection in the data model settings.
|
|
59
|
+
</Text>
|
|
60
|
+
</Stack>
|
|
61
|
+
</Center>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* Design system tokens */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
/* Colors - full shade scale */
|
|
5
|
+
--ds-primary-100: #ece6fb;
|
|
6
|
+
--ds-primary-200: #cfcbfd;
|
|
7
|
+
--ds-primary-300: #a29bfb;
|
|
8
|
+
--ds-primary-400: #7857ff;
|
|
9
|
+
--ds-primary: #5925dc;
|
|
10
|
+
--ds-primary-600: #491db6;
|
|
11
|
+
--ds-primary-700: #39178e;
|
|
12
|
+
--ds-primary-800: #291167;
|
|
13
|
+
--ds-primary-900: #190a3f;
|
|
14
|
+
--ds-primary-950: #0f0629;
|
|
15
|
+
|
|
16
|
+
--ds-secondary-100: #ebf1ff;
|
|
17
|
+
--ds-secondary-200: #d3e2ff;
|
|
18
|
+
--ds-secondary-300: #99bbff;
|
|
19
|
+
--ds-secondary-400: #70a0ff;
|
|
20
|
+
--ds-secondary: #1f69ff;
|
|
21
|
+
--ds-secondary-600: #004ff0;
|
|
22
|
+
--ds-secondary-700: #0040c2;
|
|
23
|
+
--ds-secondary-800: #003194;
|
|
24
|
+
--ds-secondary-900: #002266;
|
|
25
|
+
--ds-secondary-950: #001a4d;
|
|
26
|
+
|
|
27
|
+
--ds-success-100: #ecfbee;
|
|
28
|
+
--ds-success-200: #c4e8c8;
|
|
29
|
+
--ds-success-300: #9dd9a3;
|
|
30
|
+
--ds-success-400: #58be62;
|
|
31
|
+
--ds-success-500: #3bb346;
|
|
32
|
+
--ds-success: #198754;
|
|
33
|
+
--ds-success-700: #2da337;
|
|
34
|
+
--ds-success-800: #196f25;
|
|
35
|
+
--ds-success-900: #0d4f15;
|
|
36
|
+
--ds-success-950: #0a3e11;
|
|
37
|
+
|
|
38
|
+
--ds-info-100: #e6f3fb;
|
|
39
|
+
--ds-info-200: #b9d8ee;
|
|
40
|
+
--ds-info-300: #90c1e4;
|
|
41
|
+
--ds-info-400: #58a1d4;
|
|
42
|
+
--ds-info-500: #59a1d4;
|
|
43
|
+
--ds-info: #0f71bb;
|
|
44
|
+
--ds-info-700: #0c5b97;
|
|
45
|
+
--ds-info-800: #0a4776;
|
|
46
|
+
--ds-info-900: #08395e;
|
|
47
|
+
--ds-info-950: #062b47;
|
|
48
|
+
|
|
49
|
+
--ds-warning-100: #fffaeb;
|
|
50
|
+
--ds-warning-200: #fef0c7;
|
|
51
|
+
--ds-warning-300: #fedf89;
|
|
52
|
+
--ds-warning-400: #fec84b;
|
|
53
|
+
--ds-warning-500: #fdb022;
|
|
54
|
+
--ds-warning: #f79009;
|
|
55
|
+
--ds-warning-700: #dc6803;
|
|
56
|
+
--ds-warning-800: #b7571e;
|
|
57
|
+
--ds-warning-900: #8f4419;
|
|
58
|
+
--ds-warning-950: #6d3314;
|
|
59
|
+
|
|
60
|
+
--ds-danger-100: #fff4f3;
|
|
61
|
+
--ds-danger-200: #ffcfc8;
|
|
62
|
+
--ds-danger-300: #fc9c90;
|
|
63
|
+
--ds-danger-400: #fb7463;
|
|
64
|
+
--ds-danger-500: #fa5741;
|
|
65
|
+
--ds-danger: #d7260f;
|
|
66
|
+
--ds-danger-700: #f8331c;
|
|
67
|
+
--ds-danger-800: #c4281a;
|
|
68
|
+
--ds-danger-900: #9a1f15;
|
|
69
|
+
--ds-danger-950: #72170f;
|
|
70
|
+
|
|
71
|
+
--ds-gray-100: #f7f7f9;
|
|
72
|
+
--ds-gray-200: #e4e7ec;
|
|
73
|
+
--ds-gray-300: #d0d5dd;
|
|
74
|
+
--ds-gray-400: #98a2b3;
|
|
75
|
+
--ds-gray-500: #667085;
|
|
76
|
+
--ds-gray-600: #344054;
|
|
77
|
+
--ds-gray-700: #1d2939;
|
|
78
|
+
--ds-gray-800: #101828;
|
|
79
|
+
--ds-gray-900: #000000;
|
|
80
|
+
--ds-gray-950: #000000;
|
|
81
|
+
|
|
82
|
+
/* Spacing (8px base) */
|
|
83
|
+
--ds-spacing-1: 0.25rem;
|
|
84
|
+
--ds-spacing-2: 0.5rem;
|
|
85
|
+
--ds-spacing-3: 0.75rem;
|
|
86
|
+
--ds-spacing-4: 1rem;
|
|
87
|
+
--ds-spacing-5: 1.25rem;
|
|
88
|
+
--ds-spacing-6: 1.5rem;
|
|
89
|
+
--ds-spacing-8: 2rem;
|
|
90
|
+
--ds-spacing-10: 2.5rem;
|
|
91
|
+
--ds-spacing-12: 3rem;
|
|
92
|
+
--ds-spacing-16: 4rem;
|
|
93
|
+
|
|
94
|
+
/* Typography */
|
|
95
|
+
--ds-font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
96
|
+
--ds-font-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
97
|
+
--ds-font-size-xs: 0.75rem;
|
|
98
|
+
--ds-font-size-sm: 0.875rem;
|
|
99
|
+
--ds-font-size-base: 1rem;
|
|
100
|
+
--ds-body-font-size: 1rem;
|
|
101
|
+
--ds-font-size-lg: 1.125rem;
|
|
102
|
+
--ds-font-size-xl: 1.25rem;
|
|
103
|
+
--ds-font-size-2xl: 1.5rem;
|
|
104
|
+
--ds-font-size-3xl: 1.875rem;
|
|
105
|
+
--ds-font-size-4xl: 2.25rem;
|
|
106
|
+
|
|
107
|
+
--ds-font-weight-normal: 400;
|
|
108
|
+
--ds-font-weight-base: 400;
|
|
109
|
+
--ds-font-weight-medium: 500;
|
|
110
|
+
--ds-font-weight-semibold: 600;
|
|
111
|
+
--ds-font-weight-bold: 700;
|
|
112
|
+
|
|
113
|
+
--ds-line-height-tight: 1.25;
|
|
114
|
+
--ds-line-height-normal: 1.5;
|
|
115
|
+
--ds-line-height-base: 1.5;
|
|
116
|
+
--ds-line-height-relaxed: 1.75;
|
|
117
|
+
|
|
118
|
+
/* Radius */
|
|
119
|
+
--ds-radius-sm: 0.25rem;
|
|
120
|
+
--ds-radius: 0.375rem;
|
|
121
|
+
--ds-radius-md: 0.5rem;
|
|
122
|
+
--ds-radius-lg: 0.75rem;
|
|
123
|
+
--ds-radius-xl: 1rem;
|
|
124
|
+
|
|
125
|
+
/* Shadows */
|
|
126
|
+
--ds-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
127
|
+
--ds-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
|
128
|
+
--ds-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
129
|
+
--ds-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
130
|
+
--ds-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
131
|
+
|
|
132
|
+
/* Base colors */
|
|
133
|
+
--ds-body-bg: #ffffff;
|
|
134
|
+
--ds-body-color: var(--ds-gray-600);
|
|
135
|
+
--ds-body-color-rgb: 52, 64, 84;
|
|
136
|
+
--ds-primary-rgb: 89, 37, 220;
|
|
137
|
+
|
|
138
|
+
/* States */
|
|
139
|
+
--ds-hover-opacity: 0.8;
|
|
140
|
+
--ds-focus-ring: 0 0 0 2px var(--ds-primary);
|
|
141
|
+
--ds-disabled-opacity: 0.6;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[data-mantine-color-scheme="dark"] {
|
|
145
|
+
--ds-primary: #7857ff;
|
|
146
|
+
--ds-primary-100: #291167;
|
|
147
|
+
--ds-primary-200: #39178e;
|
|
148
|
+
--ds-primary-300: #491db6;
|
|
149
|
+
--ds-primary-400: #5925dc;
|
|
150
|
+
--ds-primary-600: #a29bfb;
|
|
151
|
+
--ds-primary-700: #cfcbfd;
|
|
152
|
+
--ds-primary-800: #ece6fb;
|
|
153
|
+
--ds-primary-900: #f5f0ff;
|
|
154
|
+
--ds-primary-950: #faf8ff;
|
|
155
|
+
|
|
156
|
+
--ds-secondary: #70a0ff;
|
|
157
|
+
--ds-success: #58be62;
|
|
158
|
+
--ds-info: #58a1d4;
|
|
159
|
+
--ds-warning: #fec84b;
|
|
160
|
+
--ds-danger: #fb7463;
|
|
161
|
+
|
|
162
|
+
--ds-gray-100: #1a1b1e;
|
|
163
|
+
--ds-gray-200: #25262b;
|
|
164
|
+
--ds-gray-300: #2c2e33;
|
|
165
|
+
--ds-gray-400: #373a40;
|
|
166
|
+
--ds-gray-500: #909296;
|
|
167
|
+
--ds-gray-600: #c1c2c5;
|
|
168
|
+
--ds-gray-700: #dee2e6;
|
|
169
|
+
--ds-gray-800: #e9ecef;
|
|
170
|
+
--ds-gray-900: #f8f9fa;
|
|
171
|
+
--ds-gray-950: #ffffff;
|
|
172
|
+
|
|
173
|
+
--ds-body-bg: #1a1b1e;
|
|
174
|
+
--ds-body-color: #c1c2c5;
|
|
175
|
+
--ds-body-color-rgb: 193, 194, 197;
|
|
176
|
+
--ds-primary-rgb: 120, 87, 255;
|
|
177
|
+
|
|
178
|
+
--ds-shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.3);
|
|
179
|
+
--ds-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.4), 0 1px 3px -1px rgba(0, 0, 0, 0.3);
|
|
180
|
+
--ds-shadow-md: 0 4px 8px -1px rgba(0, 0, 0, 0.4), 0 2px 6px -2px rgba(0, 0, 0, 0.3);
|
|
181
|
+
--ds-shadow-lg: 0 10px 20px -3px rgba(0, 0, 0, 0.4), 0 4px 8px -4px rgba(0, 0, 0, 0.3);
|
|
182
|
+
--ds-shadow-xl: 0 20px 30px -5px rgba(0, 0, 0, 0.4), 0 8px 12px -6px rgba(0, 0, 0, 0.3);
|
|
183
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Global styles using design tokens */
|
|
2
|
+
body {
|
|
3
|
+
background: var(--ds-body-bg);
|
|
4
|
+
color: var(--ds-body-color);
|
|
5
|
+
font-family: var(--ds-font-family);
|
|
6
|
+
font-size: var(--ds-body-font-size);
|
|
7
|
+
line-height: var(--ds-line-height-base);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.emoji-icon {
|
|
11
|
+
font-size: 1.2rem;
|
|
12
|
+
line-height: 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.mantine-Tabs-tab:hover {
|
|
16
|
+
background-color: var(--ds-gray-100);
|
|
17
|
+
color: var(--ds-gray-700);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.mantine-Tabs-tab[data-active],
|
|
21
|
+
.mantine-Tabs-tab[data-active]:hover {
|
|
22
|
+
color: var(--ds-primary);
|
|
23
|
+
border-bottom-color: var(--ds-primary);
|
|
24
|
+
font-weight: 600;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.mantine-Notification-root[data-variant="success"] {
|
|
28
|
+
border-left-color: var(--ds-success);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.mantine-Notification-root[data-variant="error"] {
|
|
32
|
+
border-left-color: var(--ds-danger);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.mantine-Notification-root[data-variant="warning"] {
|
|
36
|
+
border-left-color: var(--ds-warning);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.mantine-Notification-root[data-variant="info"] {
|
|
40
|
+
border-left-color: var(--ds-info);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.mantine-Input-input:focus {
|
|
44
|
+
border-color: var(--mantine-color-primary-6);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.mantine-Modal-close:hover {
|
|
48
|
+
background-color: var(--ds-gray-100);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.mantine-Anchor-root:hover {
|
|
52
|
+
text-decoration: underline;
|
|
53
|
+
color: var(--mantine-color-primary-6);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.mantine-ActionIcon-root:hover {
|
|
57
|
+
background-color: var(--ds-gray-100);
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import {
|
|
3
|
+
ColorSchemeScript,
|
|
4
|
+
MantineProvider,
|
|
5
|
+
mantineHtmlProps
|
|
6
|
+
} from "@mantine/core";
|
|
7
|
+
import { ModalsProvider } from "@mantine/modals";
|
|
8
|
+
import { Notifications } from "@mantine/notifications";
|
|
9
|
+
import { Inter } from "next/font/google";
|
|
10
|
+
import "@mantine/core/styles.css";
|
|
11
|
+
import "@mantine/notifications/styles.css";
|
|
12
|
+
import "./design-tokens.css";
|
|
13
|
+
import "./globals.css";
|
|
14
|
+
import { theme } from "@/lib/theme";
|
|
15
|
+
|
|
16
|
+
const inter = Inter({
|
|
17
|
+
variable: "--font-inter",
|
|
18
|
+
subsets: ["latin"]
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const metadata: Metadata = {
|
|
22
|
+
title: "Buildpad App",
|
|
23
|
+
description: "DaaS-ready Next.js app with a token-based design system"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function RootLayout({
|
|
27
|
+
children
|
|
28
|
+
}: Readonly<{ children: React.ReactNode }>) {
|
|
29
|
+
return (
|
|
30
|
+
<html lang="en" {...mantineHtmlProps}>
|
|
31
|
+
<head>
|
|
32
|
+
<ColorSchemeScript />
|
|
33
|
+
<link rel="shortcut icon" href="/favicon.ico" />
|
|
34
|
+
<meta
|
|
35
|
+
name="viewport"
|
|
36
|
+
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
|
37
|
+
/>
|
|
38
|
+
</head>
|
|
39
|
+
<body className={inter.variable}>
|
|
40
|
+
<MantineProvider theme={theme} defaultColorScheme="auto">
|
|
41
|
+
<ModalsProvider>
|
|
42
|
+
<Notifications position="top-right" />
|
|
43
|
+
{children}
|
|
44
|
+
</ModalsProvider>
|
|
45
|
+
</MantineProvider>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Button, Group, Stack, Text, Title } from "@mantine/core";
|
|
2
|
+
import { ColorSchemeToggle } from "@/components/ColorSchemeToggle";
|
|
3
|
+
|
|
4
|
+
export default function HomePage() {
|
|
5
|
+
return (
|
|
6
|
+
<Stack gap="lg" p="xl">
|
|
7
|
+
<Group justify="space-between">
|
|
8
|
+
<Title order={2}>Buildpad Starter</Title>
|
|
9
|
+
<ColorSchemeToggle />
|
|
10
|
+
</Group>
|
|
11
|
+
<Text c="dimmed">
|
|
12
|
+
This starter uses token-based theming with Mantine and is ready to
|
|
13
|
+
consume Buildpad UI components.
|
|
14
|
+
</Text>
|
|
15
|
+
<Group>
|
|
16
|
+
<Button>Primary Action</Button>
|
|
17
|
+
<Button variant="light" color="secondary">
|
|
18
|
+
Secondary Action
|
|
19
|
+
</Button>
|
|
20
|
+
</Group>
|
|
21
|
+
</Stack>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ActionIcon,
|
|
5
|
+
Tooltip,
|
|
6
|
+
useComputedColorScheme,
|
|
7
|
+
useMantineColorScheme
|
|
8
|
+
} from "@mantine/core";
|
|
9
|
+
import { IconMoon, IconSun } from "@tabler/icons-react";
|
|
10
|
+
|
|
11
|
+
export function ColorSchemeToggle() {
|
|
12
|
+
const { setColorScheme } = useMantineColorScheme();
|
|
13
|
+
const computedColorScheme = useComputedColorScheme("light", {
|
|
14
|
+
getInitialValueInEffect: true
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Tooltip label={computedColorScheme === "dark" ? "Light mode" : "Dark mode"}>
|
|
19
|
+
<ActionIcon
|
|
20
|
+
variant="subtle"
|
|
21
|
+
size="lg"
|
|
22
|
+
onClick={() =>
|
|
23
|
+
setColorScheme(computedColorScheme === "light" ? "dark" : "light")
|
|
24
|
+
}
|
|
25
|
+
aria-label="Toggle color scheme"
|
|
26
|
+
>
|
|
27
|
+
{computedColorScheme === "dark" ? (
|
|
28
|
+
<IconSun size={20} />
|
|
29
|
+
) : (
|
|
30
|
+
<IconMoon size={20} />
|
|
31
|
+
)}
|
|
32
|
+
</ActionIcon>
|
|
33
|
+
</Tooltip>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buildpad Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utility functions for Buildpad components.
|
|
5
|
+
* This file is copied to your project and can be customized.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
9
|
+
import { twMerge } from 'tailwind-merge';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Merge class names with Tailwind CSS conflict resolution
|
|
13
|
+
* Like shadcn's cn() utility
|
|
14
|
+
*/
|
|
15
|
+
export function cn(...inputs: ClassValue[]) {
|
|
16
|
+
return twMerge(clsx(inputs));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format file size from bytes to human-readable string
|
|
21
|
+
*/
|
|
22
|
+
export function formatFileSize(bytes: number): string {
|
|
23
|
+
if (bytes === 0) return '0 B';
|
|
24
|
+
|
|
25
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
26
|
+
const k = 1024;
|
|
27
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
28
|
+
|
|
29
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${units[i]}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get file category based on MIME type
|
|
34
|
+
*/
|
|
35
|
+
export function getFileCategory(mimeType: string | null): 'image' | 'video' | 'audio' | 'document' | 'other' {
|
|
36
|
+
if (!mimeType) return 'other';
|
|
37
|
+
|
|
38
|
+
if (mimeType.startsWith('image/')) return 'image';
|
|
39
|
+
if (mimeType.startsWith('video/')) return 'video';
|
|
40
|
+
if (mimeType.startsWith('audio/')) return 'audio';
|
|
41
|
+
if (
|
|
42
|
+
mimeType.startsWith('text/') ||
|
|
43
|
+
mimeType.includes('pdf') ||
|
|
44
|
+
mimeType.includes('document') ||
|
|
45
|
+
mimeType.includes('spreadsheet') ||
|
|
46
|
+
mimeType.includes('presentation')
|
|
47
|
+
) {
|
|
48
|
+
return 'document';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return 'other';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get asset URL for a file
|
|
56
|
+
*/
|
|
57
|
+
export function getAssetUrl(fileId: string, options?: {
|
|
58
|
+
width?: number;
|
|
59
|
+
height?: number;
|
|
60
|
+
fit?: 'cover' | 'contain' | 'inside' | 'outside';
|
|
61
|
+
quality?: number;
|
|
62
|
+
format?: 'auto' | 'webp' | 'avif' | 'jpg' | 'png';
|
|
63
|
+
}): string {
|
|
64
|
+
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '/api';
|
|
65
|
+
let url = `${baseUrl}/assets/${fileId}`;
|
|
66
|
+
|
|
67
|
+
if (options) {
|
|
68
|
+
const params = new URLSearchParams();
|
|
69
|
+
if (options.width) params.set('width', options.width.toString());
|
|
70
|
+
if (options.height) params.set('height', options.height.toString());
|
|
71
|
+
if (options.fit) params.set('fit', options.fit);
|
|
72
|
+
if (options.quality) params.set('quality', options.quality.toString());
|
|
73
|
+
if (options.format) params.set('format', options.format);
|
|
74
|
+
|
|
75
|
+
const queryString = params.toString();
|
|
76
|
+
if (queryString) url += `?${queryString}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return url;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Debounce a function call
|
|
84
|
+
*/
|
|
85
|
+
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
86
|
+
func: T,
|
|
87
|
+
wait: number
|
|
88
|
+
): (...args: Parameters<T>) => void {
|
|
89
|
+
let timeout: NodeJS.Timeout;
|
|
90
|
+
|
|
91
|
+
return function executedFunction(...args: Parameters<T>) {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
timeout = setTimeout(() => func(...args), wait);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a value is a valid primary key
|
|
99
|
+
*/
|
|
100
|
+
export function isValidPrimaryKey(value: unknown): value is string | number {
|
|
101
|
+
if (typeof value === 'number') return !isNaN(value);
|
|
102
|
+
if (typeof value === 'string') return value.length > 0 && value !== '+';
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Deep merge two objects
|
|
108
|
+
*/
|
|
109
|
+
export function deepMerge<T extends Record<string, unknown>>(
|
|
110
|
+
target: T,
|
|
111
|
+
source: Partial<T>
|
|
112
|
+
): T {
|
|
113
|
+
const result = { ...target };
|
|
114
|
+
|
|
115
|
+
for (const key in source) {
|
|
116
|
+
const sourceValue = source[key];
|
|
117
|
+
const targetValue = result[key];
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
sourceValue &&
|
|
121
|
+
typeof sourceValue === 'object' &&
|
|
122
|
+
!Array.isArray(sourceValue) &&
|
|
123
|
+
targetValue &&
|
|
124
|
+
typeof targetValue === 'object' &&
|
|
125
|
+
!Array.isArray(targetValue)
|
|
126
|
+
) {
|
|
127
|
+
result[key] = deepMerge(
|
|
128
|
+
targetValue as Record<string, unknown>,
|
|
129
|
+
sourceValue as Record<string, unknown>
|
|
130
|
+
) as T[Extract<keyof T, string>];
|
|
131
|
+
} else {
|
|
132
|
+
result[key] = sourceValue as T[Extract<keyof T, string>];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convert a string to slug format
|
|
141
|
+
*/
|
|
142
|
+
export function slugify(text: string): string {
|
|
143
|
+
return text
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.trim()
|
|
146
|
+
.replace(/[^\w\s-]/g, '')
|
|
147
|
+
.replace(/[\s_-]+/g, '-')
|
|
148
|
+
.replace(/^-+|-+$/g, '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate a unique ID
|
|
153
|
+
*/
|
|
154
|
+
export function generateId(): string {
|
|
155
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`;
|
|
156
|
+
}
|