@microbuild/cli 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/README.md +555 -0
- package/dist/chunk-6YA3DSAE.js +362 -0
- package/dist/chunk-6YA3DSAE.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2279 -0
- package/dist/index.js.map +1 -0
- package/dist/outdated-TV5ERBNC.js +110 -0
- package/dist/outdated-TV5ERBNC.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/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/relations-route.ts +46 -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 +26 -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,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login Page Template
|
|
3
|
+
*
|
|
4
|
+
* Server-side proxy login page that uses the /api/auth/login proxy route
|
|
5
|
+
* instead of calling Supabase directly from the browser.
|
|
6
|
+
* This avoids CORS issues in the two-tier architecture.
|
|
7
|
+
*
|
|
8
|
+
* Pattern: Browser → /api/auth/login (same origin) → Supabase Auth (server-side)
|
|
9
|
+
*
|
|
10
|
+
* @microbuild/origin: pages/login
|
|
11
|
+
* @microbuild/version: 1.0.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import {
|
|
18
|
+
Paper,
|
|
19
|
+
TextInput,
|
|
20
|
+
PasswordInput,
|
|
21
|
+
Button,
|
|
22
|
+
Title,
|
|
23
|
+
Text,
|
|
24
|
+
Container,
|
|
25
|
+
Stack,
|
|
26
|
+
Box,
|
|
27
|
+
Group,
|
|
28
|
+
Anchor,
|
|
29
|
+
} from '@mantine/core';
|
|
30
|
+
import { useForm } from '@mantine/form';
|
|
31
|
+
import { notifications } from '@mantine/notifications';
|
|
32
|
+
import { useRouter } from 'next/navigation';
|
|
33
|
+
|
|
34
|
+
export default function LoginPage() {
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
|
|
38
|
+
const form = useForm({
|
|
39
|
+
initialValues: {
|
|
40
|
+
email: '',
|
|
41
|
+
password: '',
|
|
42
|
+
},
|
|
43
|
+
validate: {
|
|
44
|
+
email: (value) => (!value ? 'Email is required' : /^\S+@\S+$/.test(value) ? null : 'Invalid email'),
|
|
45
|
+
password: (value) => (!value ? 'Password is required' : null),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const handleLogin = async (values: { email: string; password: string }) => {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Use the proxy route — NOT the Supabase client directly
|
|
54
|
+
// This avoids CORS issues because the request stays same-origin
|
|
55
|
+
const response = await fetch('/api/auth/login', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify(values),
|
|
59
|
+
credentials: 'include', // Include cookies
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(data.errors?.[0]?.message || 'Login failed');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
notifications.show({
|
|
69
|
+
title: 'Success',
|
|
70
|
+
message: 'Logged in successfully',
|
|
71
|
+
color: 'green',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
router.push('/');
|
|
75
|
+
router.refresh();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Login error:', error);
|
|
78
|
+
notifications.show({
|
|
79
|
+
title: 'Error',
|
|
80
|
+
message: error instanceof Error ? error.message : 'Failed to login',
|
|
81
|
+
color: 'red',
|
|
82
|
+
});
|
|
83
|
+
} finally {
|
|
84
|
+
setLoading(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Box
|
|
90
|
+
style={{
|
|
91
|
+
position: 'fixed',
|
|
92
|
+
top: 0,
|
|
93
|
+
left: 0,
|
|
94
|
+
right: 0,
|
|
95
|
+
bottom: 0,
|
|
96
|
+
display: 'flex',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
background: 'linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%)',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<Container size={420}>
|
|
103
|
+
<Title ta="center" mb="md">
|
|
104
|
+
Welcome back
|
|
105
|
+
</Title>
|
|
106
|
+
<Text c="dimmed" size="sm" ta="center" mb="xl">
|
|
107
|
+
Sign in to your account
|
|
108
|
+
</Text>
|
|
109
|
+
|
|
110
|
+
<Paper withBorder shadow="md" p={30} radius="md">
|
|
111
|
+
<form onSubmit={form.onSubmit(handleLogin)}>
|
|
112
|
+
<Stack>
|
|
113
|
+
<TextInput
|
|
114
|
+
label="Email"
|
|
115
|
+
placeholder="you@example.com"
|
|
116
|
+
required
|
|
117
|
+
{...form.getInputProps('email')}
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<PasswordInput
|
|
121
|
+
label="Password"
|
|
122
|
+
placeholder="Your password"
|
|
123
|
+
required
|
|
124
|
+
{...form.getInputProps('password')}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
<Group justify="flex-end">
|
|
128
|
+
<Anchor component="button" type="button" c="dimmed" size="xs">
|
|
129
|
+
Forgot password?
|
|
130
|
+
</Anchor>
|
|
131
|
+
</Group>
|
|
132
|
+
|
|
133
|
+
<Button type="submit" fullWidth loading={loading}>
|
|
134
|
+
Sign in
|
|
135
|
+
</Button>
|
|
136
|
+
</Stack>
|
|
137
|
+
</form>
|
|
138
|
+
</Paper>
|
|
139
|
+
</Container>
|
|
140
|
+
</Box>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relations API Route
|
|
3
|
+
*
|
|
4
|
+
* Proxies relation schema requests to the DaaS backend.
|
|
5
|
+
* Required for M2O, M2M, O2M, and M2A relation components.
|
|
6
|
+
*
|
|
7
|
+
* @microbuild/origin: api-routes/relations
|
|
8
|
+
* @microbuild/version: 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
12
|
+
import { getAuthHeaders, getDaasUrl } from '@/lib/api/auth-headers';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GET /api/relations
|
|
16
|
+
* Get all relation definitions
|
|
17
|
+
*/
|
|
18
|
+
export async function GET(request: NextRequest) {
|
|
19
|
+
try {
|
|
20
|
+
const headers = await getAuthHeaders();
|
|
21
|
+
const daasUrl = getDaasUrl();
|
|
22
|
+
|
|
23
|
+
// Forward query parameters (e.g., filter by collection)
|
|
24
|
+
const searchParams = request.nextUrl.searchParams.toString();
|
|
25
|
+
const url = `${daasUrl}/api/relations${searchParams ? `?${searchParams}` : ''}`;
|
|
26
|
+
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
headers,
|
|
29
|
+
cache: 'no-store',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const error = await response.json().catch(() => ({ errors: [{ message: 'Request failed' }] }));
|
|
34
|
+
return NextResponse.json(error, { status: response.status });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
return NextResponse.json(data);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Relations API error:', error);
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ errors: [{ message: error instanceof Error ? error.message : 'Internal server error' }] },
|
|
43
|
+
{ status: 500 }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -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: "Microbuild 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}>Microbuild 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 Microbuild 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
|
+
* Microbuild Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utility functions for Microbuild 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
|
+
}
|