create-sdd-ai-stack 0.1.17
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/AGENTS.md +164 -0
- package/APP-STACK.md +31 -0
- package/APP.md +46 -0
- package/ARCHITECTURE.md +115 -0
- package/DESIGN.md +219 -0
- package/LICENSE +21 -0
- package/NEXT.md +345 -0
- package/NODE.md +107 -0
- package/REACT.md +92 -0
- package/README.md +260 -0
- package/SKILLS/check-docs/SKILL.md +24 -0
- package/SKILLS/check-docs/check-docs.mjs +56 -0
- package/SKILLS/create-feature/SKILL.md +40 -0
- package/SKILLS/create-feature/create-feature.mjs +128 -0
- package/SKILLS/create-feature.sh +112 -0
- package/SKILLS/install-submodule/SKILL.md +42 -0
- package/SKILLS/install-submodule/install-submodule.mjs +119 -0
- package/bin/create-sdd-ai-stack.mjs +93 -0
- package/docs/CHANGELOG.md +209 -0
- package/docs/PLANNING.md +68 -0
- package/docs/PRODUCT.md +76 -0
- package/docs/RELEASE.md +332 -0
- package/lib/check-links.mjs +42 -0
- package/lib/constants.mjs +60 -0
- package/lib/scaffold.mjs +253 -0
- package/package.json +80 -0
- package/specs/BACKLOG.md +24 -0
- package/specs/PLAN.md +57 -0
- package/specs/ROADMAP.md +35 -0
- package/specs/history/phases/phase-0-bootstrap.md +67 -0
- package/specs/tasks/TASK_TEMPLATE.md +46 -0
- package/src/cli.mjs +119 -0
- package/stacks/README.md +33 -0
- package/stacks/ai.md +52 -0
- package/stacks/ci.md +59 -0
- package/stacks/database.md +56 -0
- package/stacks/git.md +51 -0
- package/stacks/shadcn.md +52 -0
- package/stacks/tailwind.md +65 -0
- package/stacks/testing.md +74 -0
- package/stacks/typescript.md +58 -0
- package/template/next/.env.example +4 -0
- package/template/next/README.md +47 -0
- package/template/next/biome.json +36 -0
- package/template/next/gitignore +32 -0
- package/template/next/next.config.ts +12 -0
- package/template/next/package.json +47 -0
- package/template/next/playwright.config.ts +21 -0
- package/template/next/postcss.config.mjs +7 -0
- package/template/next/src/app/(app)/app/page.tsx +18 -0
- package/template/next/src/app/(app)/error.tsx +30 -0
- package/template/next/src/app/(app)/layout.tsx +25 -0
- package/template/next/src/app/(marketing)/page.tsx +73 -0
- package/template/next/src/app/globals.css +437 -0
- package/template/next/src/app/layout.tsx +41 -0
- package/template/next/src/app/not-found.tsx +11 -0
- package/template/next/src/features/example/actions.ts +43 -0
- package/template/next/src/features/example/application/create-example.usecase.ts +26 -0
- package/template/next/src/features/example/container.ts +16 -0
- package/template/next/src/features/example/domain/IExampleRepository.ts +11 -0
- package/template/next/src/features/example/domain/example.schema.ts +18 -0
- package/template/next/src/features/example/infrastructure/example.repository.ts +26 -0
- package/template/next/src/features/example/ui/create-example-form.tsx +56 -0
- package/template/next/src/proxy.ts +23 -0
- package/template/next/src/shared/lib/cn.ts +6 -0
- package/template/next/src/shared/server/auth.ts +32 -0
- package/template/next/src/shared/server/env.ts +14 -0
- package/template/next/src/shared/ui/index.ts +3 -0
- package/template/next/tests/e2e/routes.spec.ts +34 -0
- package/template/next/tests/unit/create-example.test.ts +55 -0
- package/template/next/tsconfig.json +37 -0
- package/template/next/vitest.config.ts +19 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
4
|
+
EXECUTIVE ENGINEERING — DESIGN TOKENS
|
|
5
|
+
Lei: SDD/DESIGN.md. NUNCA edite token fora daqui.
|
|
6
|
+
═══════════════════════════════════════════════════════════════ */
|
|
7
|
+
|
|
8
|
+
@theme {
|
|
9
|
+
/* ── Superfícies ── */
|
|
10
|
+
--color-surface: #111319;
|
|
11
|
+
--color-surface-base: #0a0d12;
|
|
12
|
+
--color-surface-raised: #11151c;
|
|
13
|
+
--color-surface-overlay: #181e27;
|
|
14
|
+
--color-surface-container-low: #191c21;
|
|
15
|
+
--color-surface-container: #1d2025;
|
|
16
|
+
--color-surface-container-high: #272a30;
|
|
17
|
+
--color-surface-container-highest: #32353b;
|
|
18
|
+
--color-surface-bright: #36393f;
|
|
19
|
+
|
|
20
|
+
/* ── Texto ── */
|
|
21
|
+
--color-on-surface: #e1e2ea;
|
|
22
|
+
--color-text-primary: #f4f1ea;
|
|
23
|
+
--color-text-secondary: #94a3b8;
|
|
24
|
+
--color-text-muted: #56657a;
|
|
25
|
+
|
|
26
|
+
/* ── Bordas ── */
|
|
27
|
+
--color-border-subtle: #1e2633;
|
|
28
|
+
--color-border-prominent: #2d384b;
|
|
29
|
+
--color-outline: #8d937b;
|
|
30
|
+
--color-outline-variant: #434935;
|
|
31
|
+
|
|
32
|
+
/* ── Acentos ── */
|
|
33
|
+
--color-primary: #baf336;
|
|
34
|
+
--color-primary-dim: #a1d811;
|
|
35
|
+
--color-on-primary: #0a0d12;
|
|
36
|
+
--color-primary-container: #b4f230;
|
|
37
|
+
--color-on-primary-container: #253600;
|
|
38
|
+
--color-secondary: #45dfa4;
|
|
39
|
+
--color-secondary-dim: #34d399;
|
|
40
|
+
--color-on-secondary: #003825;
|
|
41
|
+
--color-secondary-container: #00bd85;
|
|
42
|
+
--color-on-secondary-container: #00452e;
|
|
43
|
+
--color-tertiary: #9cc5fd;
|
|
44
|
+
--color-on-tertiary: #003257;
|
|
45
|
+
--color-tertiary-container: #cce2ff;
|
|
46
|
+
--color-on-tertiary-container: #306698;
|
|
47
|
+
--color-error: #ffb4ab;
|
|
48
|
+
--color-on-error: #690005;
|
|
49
|
+
--color-error-container: #93000a;
|
|
50
|
+
--color-on-error-container: #ffdad6;
|
|
51
|
+
|
|
52
|
+
/* ── Glows ── */
|
|
53
|
+
--kpi-accent-glow: rgba(186, 243, 54, 0.12);
|
|
54
|
+
--mint-accent-glow: rgba(52, 211, 153, 0.12);
|
|
55
|
+
|
|
56
|
+
/* ── Tipografia: famílias ── */
|
|
57
|
+
--font-sans: var(--font-manrope), ui-sans-serif, system-ui, sans-serif;
|
|
58
|
+
--font-mono: var(--font-jetbrains-mono), ui-monospace, monospace;
|
|
59
|
+
--font-editorial: var(--font-playfair), ui-serif, Georgia, serif;
|
|
60
|
+
|
|
61
|
+
/* ── Tipografia: escala ── */
|
|
62
|
+
--text-display-hero: 64px;
|
|
63
|
+
--text-display-hero--line-height: 72px;
|
|
64
|
+
--text-display-hero--letter-spacing: -0.035em;
|
|
65
|
+
--text-display-hero--font-weight: 800;
|
|
66
|
+
|
|
67
|
+
--text-display-hero-mobile: 38px;
|
|
68
|
+
--text-display-hero-mobile--line-height: 44px;
|
|
69
|
+
--text-display-hero-mobile--letter-spacing: -0.025em;
|
|
70
|
+
--text-display-hero-mobile--font-weight: 800;
|
|
71
|
+
|
|
72
|
+
--text-metric-stat: 48px;
|
|
73
|
+
--text-metric-stat--line-height: 52px;
|
|
74
|
+
--text-metric-stat--letter-spacing: -0.03em;
|
|
75
|
+
--text-metric-stat--font-weight: 700;
|
|
76
|
+
|
|
77
|
+
--text-metric-stat-mobile: 32px;
|
|
78
|
+
--text-metric-stat-mobile--line-height: 36px;
|
|
79
|
+
--text-metric-stat-mobile--letter-spacing: -0.02em;
|
|
80
|
+
--text-metric-stat-mobile--font-weight: 700;
|
|
81
|
+
|
|
82
|
+
--text-headline-lg: 32px;
|
|
83
|
+
--text-headline-lg--line-height: 40px;
|
|
84
|
+
--text-headline-lg--letter-spacing: -0.02em;
|
|
85
|
+
--text-headline-lg--font-weight: 700;
|
|
86
|
+
|
|
87
|
+
--text-headline-md: 24px;
|
|
88
|
+
--text-headline-md--line-height: 32px;
|
|
89
|
+
--text-headline-md--letter-spacing: -0.015em;
|
|
90
|
+
--text-headline-md--font-weight: 600;
|
|
91
|
+
|
|
92
|
+
--text-headline-sm: 18px;
|
|
93
|
+
--text-headline-sm--line-height: 26px;
|
|
94
|
+
--text-headline-sm--letter-spacing: -0.01em;
|
|
95
|
+
--text-headline-sm--font-weight: 600;
|
|
96
|
+
|
|
97
|
+
--text-body-lg: 17px;
|
|
98
|
+
--text-body-lg--line-height: 28px;
|
|
99
|
+
|
|
100
|
+
--text-body-md: 15px;
|
|
101
|
+
--text-body-md--line-height: 24px;
|
|
102
|
+
|
|
103
|
+
--text-body-sm: 13px;
|
|
104
|
+
--text-body-sm--line-height: 20px;
|
|
105
|
+
|
|
106
|
+
--text-editorial-quote: 26px;
|
|
107
|
+
--text-editorial-quote--line-height: 36px;
|
|
108
|
+
--text-editorial-quote--letter-spacing: -0.01em;
|
|
109
|
+
--text-editorial-quote--font-weight: 500;
|
|
110
|
+
|
|
111
|
+
--text-code-inline: 13px;
|
|
112
|
+
--text-code-inline--line-height: 18px;
|
|
113
|
+
--text-code-inline--letter-spacing: -0.01em;
|
|
114
|
+
--text-code-inline--font-weight: 500;
|
|
115
|
+
|
|
116
|
+
--text-label-mono: 11px;
|
|
117
|
+
--text-label-mono--line-height: 16px;
|
|
118
|
+
--text-label-mono--letter-spacing: 0.06em;
|
|
119
|
+
--text-label-mono--font-weight: 600;
|
|
120
|
+
|
|
121
|
+
/* ── Raios ── */
|
|
122
|
+
--radius-sm: 0.125rem;
|
|
123
|
+
--radius-DEFAULT: 0.25rem;
|
|
124
|
+
--radius-md: 0.375rem;
|
|
125
|
+
--radius-lg: 0.5rem;
|
|
126
|
+
--radius-xl: 0.75rem;
|
|
127
|
+
|
|
128
|
+
/* ── Espaçamento ── */
|
|
129
|
+
--spacing-gutter: 1.5rem;
|
|
130
|
+
--spacing-gutter-desktop: 2rem;
|
|
131
|
+
--spacing-margin: 1rem;
|
|
132
|
+
--spacing-margin-tablet: 2rem;
|
|
133
|
+
--spacing-margin-desktop: 3rem;
|
|
134
|
+
--spacing-space-xs: 0.25rem;
|
|
135
|
+
--spacing-space-sm: 0.5rem;
|
|
136
|
+
--spacing-space-md: 1rem;
|
|
137
|
+
--spacing-space-lg: 1.5rem;
|
|
138
|
+
--spacing-space-xl: 2.5rem;
|
|
139
|
+
--spacing-space-2xl: 4rem;
|
|
140
|
+
--spacing-space-3xl: 6rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
144
|
+
BASE
|
|
145
|
+
═══════════════════════════════════════════════════════════════ */
|
|
146
|
+
|
|
147
|
+
@layer base {
|
|
148
|
+
* {
|
|
149
|
+
border-color: var(--color-border-subtle);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
html {
|
|
153
|
+
scroll-behavior: smooth;
|
|
154
|
+
color-scheme: dark;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
body {
|
|
158
|
+
background-color: var(--color-surface-base);
|
|
159
|
+
color: var(--color-text-primary);
|
|
160
|
+
font-family: var(--font-sans);
|
|
161
|
+
-webkit-font-smoothing: antialiased;
|
|
162
|
+
/* Tier 0: micro-dot grid estrutural */
|
|
163
|
+
background-image: radial-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
|
164
|
+
background-size: 24px 24px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
:focus-visible {
|
|
168
|
+
outline: 1px solid var(--color-primary);
|
|
169
|
+
outline-offset: 2px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
::selection {
|
|
173
|
+
background: rgba(186, 243, 54, 0.25);
|
|
174
|
+
color: var(--color-text-primary);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
::-webkit-scrollbar {
|
|
178
|
+
width: 10px;
|
|
179
|
+
height: 10px;
|
|
180
|
+
}
|
|
181
|
+
::-webkit-scrollbar-track {
|
|
182
|
+
background: var(--color-surface-base);
|
|
183
|
+
}
|
|
184
|
+
::-webkit-scrollbar-thumb {
|
|
185
|
+
background: var(--color-surface-container-high);
|
|
186
|
+
border-radius: 9999px;
|
|
187
|
+
}
|
|
188
|
+
::-webkit-scrollbar-thumb:hover {
|
|
189
|
+
background: var(--color-border-prominent);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
194
|
+
COMPONENTES (próximo de shadcn — edite aqui, não em componente)
|
|
195
|
+
═══════════════════════════════════════════════════════════════ */
|
|
196
|
+
|
|
197
|
+
@layer components {
|
|
198
|
+
/* Botões */
|
|
199
|
+
.btn-primary {
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
gap: 0.5rem;
|
|
204
|
+
border-radius: var(--radius-DEFAULT);
|
|
205
|
+
background-color: var(--color-primary);
|
|
206
|
+
color: var(--color-on-primary);
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
padding: 0.625rem 1.25rem;
|
|
209
|
+
transition:
|
|
210
|
+
background-color 150ms ease,
|
|
211
|
+
box-shadow 150ms ease,
|
|
212
|
+
transform 100ms ease;
|
|
213
|
+
}
|
|
214
|
+
.btn-primary:hover {
|
|
215
|
+
background-color: #c8f75a;
|
|
216
|
+
box-shadow: 0 0 16px rgba(186, 243, 54, 0.3);
|
|
217
|
+
}
|
|
218
|
+
.btn-primary:active {
|
|
219
|
+
transform: scale(0.98);
|
|
220
|
+
}
|
|
221
|
+
.btn-primary:disabled {
|
|
222
|
+
opacity: 0.5;
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.btn-secondary {
|
|
227
|
+
display: inline-flex;
|
|
228
|
+
align-items: center;
|
|
229
|
+
justify-content: center;
|
|
230
|
+
gap: 0.5rem;
|
|
231
|
+
border-radius: var(--radius-DEFAULT);
|
|
232
|
+
border: 1px solid var(--color-border-subtle);
|
|
233
|
+
background-color: transparent;
|
|
234
|
+
color: var(--color-text-primary);
|
|
235
|
+
font-weight: 500;
|
|
236
|
+
padding: 0.625rem 1.25rem;
|
|
237
|
+
transition:
|
|
238
|
+
border-color 150ms ease,
|
|
239
|
+
background-color 150ms ease;
|
|
240
|
+
}
|
|
241
|
+
.btn-secondary:hover {
|
|
242
|
+
border-color: var(--color-text-secondary);
|
|
243
|
+
background-color: rgba(255, 255, 255, 0.04);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.btn-ghost {
|
|
247
|
+
display: inline-flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 0.375rem;
|
|
250
|
+
border-radius: var(--radius-sm);
|
|
251
|
+
font-family: var(--font-mono);
|
|
252
|
+
font-size: 11px;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
letter-spacing: 0.06em;
|
|
255
|
+
text-transform: uppercase;
|
|
256
|
+
color: var(--color-text-secondary);
|
|
257
|
+
padding: 0.375rem 0.5rem;
|
|
258
|
+
transition: color 150ms ease;
|
|
259
|
+
}
|
|
260
|
+
.btn-ghost:hover {
|
|
261
|
+
color: var(--color-primary);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Cards */
|
|
265
|
+
.card {
|
|
266
|
+
border-radius: var(--radius-lg);
|
|
267
|
+
border: 1px solid var(--color-border-subtle);
|
|
268
|
+
background-color: var(--color-surface-raised);
|
|
269
|
+
padding: 1.25rem;
|
|
270
|
+
transition:
|
|
271
|
+
border-color 200ms ease,
|
|
272
|
+
box-shadow 200ms ease;
|
|
273
|
+
}
|
|
274
|
+
.card:hover {
|
|
275
|
+
border-color: var(--color-border-prominent);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.card-metric {
|
|
279
|
+
position: relative;
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
border-radius: var(--radius-lg);
|
|
282
|
+
border: 1px solid var(--color-border-subtle);
|
|
283
|
+
background-color: var(--color-surface-raised);
|
|
284
|
+
padding: 1.25rem;
|
|
285
|
+
transition: box-shadow 250ms ease;
|
|
286
|
+
}
|
|
287
|
+
.card-metric::after {
|
|
288
|
+
content: "";
|
|
289
|
+
position: absolute;
|
|
290
|
+
top: 0;
|
|
291
|
+
right: 0;
|
|
292
|
+
width: 3rem;
|
|
293
|
+
height: 3rem;
|
|
294
|
+
background: radial-gradient(
|
|
295
|
+
circle at top right,
|
|
296
|
+
var(--kpi-accent-glow),
|
|
297
|
+
transparent 70%
|
|
298
|
+
);
|
|
299
|
+
opacity: 0;
|
|
300
|
+
transition: opacity 250ms ease;
|
|
301
|
+
}
|
|
302
|
+
.card-metric:hover::after {
|
|
303
|
+
opacity: 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* Chips / badges de taxonomia */
|
|
307
|
+
.chip {
|
|
308
|
+
display: inline-flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
border-radius: var(--radius-sm);
|
|
311
|
+
border: 1px solid var(--color-border-subtle);
|
|
312
|
+
background-color: var(--color-surface-raised);
|
|
313
|
+
color: var(--color-text-secondary);
|
|
314
|
+
font-family: var(--font-mono);
|
|
315
|
+
font-size: 11px;
|
|
316
|
+
font-weight: 600;
|
|
317
|
+
letter-spacing: 0.06em;
|
|
318
|
+
text-transform: uppercase;
|
|
319
|
+
padding: 4px 8px;
|
|
320
|
+
transition:
|
|
321
|
+
border-color 150ms ease,
|
|
322
|
+
color 150ms ease;
|
|
323
|
+
}
|
|
324
|
+
.chip:hover {
|
|
325
|
+
border-color: var(--color-border-prominent);
|
|
326
|
+
}
|
|
327
|
+
.chip-active {
|
|
328
|
+
background-color: rgba(186, 243, 54, 0.08);
|
|
329
|
+
border-color: rgba(186, 243, 54, 0.4);
|
|
330
|
+
color: var(--color-primary);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Campos */
|
|
334
|
+
.field-label {
|
|
335
|
+
display: block;
|
|
336
|
+
margin-bottom: 0.375rem;
|
|
337
|
+
font-family: var(--font-mono);
|
|
338
|
+
font-size: 11px;
|
|
339
|
+
font-weight: 600;
|
|
340
|
+
letter-spacing: 0.06em;
|
|
341
|
+
text-transform: uppercase;
|
|
342
|
+
color: var(--color-text-secondary);
|
|
343
|
+
}
|
|
344
|
+
.field {
|
|
345
|
+
width: 100%;
|
|
346
|
+
border-radius: var(--radius-md);
|
|
347
|
+
border: 1px solid var(--color-border-subtle);
|
|
348
|
+
background-color: var(--color-surface-base);
|
|
349
|
+
color: var(--color-text-primary);
|
|
350
|
+
padding: 0.625rem 0.75rem;
|
|
351
|
+
font-size: 15px;
|
|
352
|
+
transition: border-color 150ms ease;
|
|
353
|
+
}
|
|
354
|
+
.field::placeholder {
|
|
355
|
+
color: var(--color-text-muted);
|
|
356
|
+
}
|
|
357
|
+
.field:hover {
|
|
358
|
+
border-color: var(--color-border-prominent);
|
|
359
|
+
}
|
|
360
|
+
.field:focus {
|
|
361
|
+
outline: none;
|
|
362
|
+
border-color: var(--color-primary);
|
|
363
|
+
}
|
|
364
|
+
.field[aria-invalid="true"] {
|
|
365
|
+
border-color: var(--color-error);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Label mono / section eyebrow */
|
|
369
|
+
.label-mono {
|
|
370
|
+
font-family: var(--font-mono);
|
|
371
|
+
font-size: 11px;
|
|
372
|
+
font-weight: 600;
|
|
373
|
+
line-height: 16px;
|
|
374
|
+
letter-spacing: 0.06em;
|
|
375
|
+
text-transform: uppercase;
|
|
376
|
+
color: var(--color-text-muted);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Status dot pulsante */
|
|
380
|
+
.status-dot {
|
|
381
|
+
display: inline-block;
|
|
382
|
+
width: 6px;
|
|
383
|
+
height: 6px;
|
|
384
|
+
border-radius: 9999px;
|
|
385
|
+
background-color: var(--color-secondary);
|
|
386
|
+
box-shadow: 0 0 0 0 var(--mint-accent-glow);
|
|
387
|
+
animation: status-pulse 2s infinite;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Container de largura máxima */
|
|
391
|
+
.container-grid {
|
|
392
|
+
margin-inline: auto;
|
|
393
|
+
width: 100%;
|
|
394
|
+
max-width: 1320px;
|
|
395
|
+
padding-inline: var(--spacing-gutter);
|
|
396
|
+
}
|
|
397
|
+
@media (min-width: 1024px) {
|
|
398
|
+
.container-grid {
|
|
399
|
+
padding-inline: var(--spacing-gutter-desktop);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@layer utilities {
|
|
405
|
+
@keyframes status-pulse {
|
|
406
|
+
0% {
|
|
407
|
+
box-shadow: 0 0 0 0 var(--mint-accent-glow);
|
|
408
|
+
}
|
|
409
|
+
70% {
|
|
410
|
+
box-shadow: 0 0 0 6px rgba(52, 211, 153, 0);
|
|
411
|
+
}
|
|
412
|
+
100% {
|
|
413
|
+
box-shadow: 0 0 0 0 rgba(52, 211, 153, 0);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.text-balance {
|
|
418
|
+
text-wrap: balance;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
423
|
+
A11Y — respeitar preferências do usuário
|
|
424
|
+
═══════════════════════════════════════════════════════════════ */
|
|
425
|
+
|
|
426
|
+
/* biome-ignore-start lint/complexity/noImportantStyles: !important é o único jeito de respeitar prefers-reduced-motion sobre transition inline */
|
|
427
|
+
@media (prefers-reduced-motion: reduce) {
|
|
428
|
+
*,
|
|
429
|
+
*::before,
|
|
430
|
+
*::after {
|
|
431
|
+
animation-duration: 0.01ms !important;
|
|
432
|
+
animation-iteration-count: 1 !important;
|
|
433
|
+
transition-duration: 0.01ms !important;
|
|
434
|
+
scroll-behavior: auto !important;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/* biome-ignore-end lint/complexity/noImportantStyles: fim do bloco prefers-reduced-motion */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { JetBrains_Mono, Manrope, Playfair_Display } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const manrope = Manrope({
|
|
6
|
+
subsets: ["latin"],
|
|
7
|
+
variable: "--font-manrope",
|
|
8
|
+
display: "swap",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const jetbrainsMono = JetBrains_Mono({
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
variable: "--font-jetbrains-mono",
|
|
14
|
+
display: "swap",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const playfair = Playfair_Display({
|
|
18
|
+
subsets: ["latin"],
|
|
19
|
+
variable: "--font-playfair",
|
|
20
|
+
display: "swap",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const metadata: Metadata = {
|
|
24
|
+
title: { default: "[NOME DO APP]", template: "%s | [NOME DO APP]" },
|
|
25
|
+
description: "[DESCRIÇÃO DE UMA LINHA]",
|
|
26
|
+
metadataBase: new URL("https://example.com"),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default function RootLayout({
|
|
30
|
+
children,
|
|
31
|
+
}: Readonly<{ children: React.ReactNode }>) {
|
|
32
|
+
return (
|
|
33
|
+
<html lang="pt-BR" data-scroll-behavior="smooth">
|
|
34
|
+
<body
|
|
35
|
+
className={`${manrope.variable} ${jetbrainsMono.variable} ${playfair.variable} antialiased`}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default function NotFound() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="container-grid flex min-h-screen flex-col items-start justify-center py-24">
|
|
4
|
+
<p className="label-mono">404</p>
|
|
5
|
+
<h1 className="mt-4 text-headline-lg">Recurso não encontrado.</h1>
|
|
6
|
+
<a href="/" className="btn-secondary mt-8">
|
|
7
|
+
Voltar ao início
|
|
8
|
+
</a>
|
|
9
|
+
</main>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { revalidatePath, updateTag } from "next/cache";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createExampleUseCases } from "./container";
|
|
5
|
+
import { CreateExampleSchema } from "./domain/example.schema";
|
|
6
|
+
|
|
7
|
+
export type ExampleActionState = {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
errors?: Record<string, string[]>;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function createExampleAction(
|
|
14
|
+
_prev: ExampleActionState,
|
|
15
|
+
formData: FormData,
|
|
16
|
+
): Promise<ExampleActionState> {
|
|
17
|
+
// 1. AUTENTICAÇÃO (ver SDD/NEXT.md §5)
|
|
18
|
+
// const user = await requireUser();
|
|
19
|
+
|
|
20
|
+
// 2. VALIDAÇÃO — Zod no server é a única fonte de verdade
|
|
21
|
+
const parsed = CreateExampleSchema.safeParse({
|
|
22
|
+
title: formData.get("title"),
|
|
23
|
+
body: formData.get("body"),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
return { ok: false, errors: z.flattenError(parsed.error).fieldErrors };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 3. MUTAÇÃO via use case
|
|
32
|
+
const { createExample } = createExampleUseCases();
|
|
33
|
+
await createExample.execute(parsed.data);
|
|
34
|
+
|
|
35
|
+
// 4. CACHE
|
|
36
|
+
updateTag("examples");
|
|
37
|
+
revalidatePath("/home");
|
|
38
|
+
|
|
39
|
+
return { ok: true };
|
|
40
|
+
} catch {
|
|
41
|
+
return { ok: false, error: "Não foi possível criar. Tente novamente." };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CreateExampleInput } from "../domain/example.schema";
|
|
2
|
+
import type { Example, IExampleRepository } from "../domain/IExampleRepository";
|
|
3
|
+
|
|
4
|
+
export class AppError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
readonly code: string,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "AppError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class CreateExampleUseCase {
|
|
15
|
+
constructor(private readonly repo: IExampleRepository) {}
|
|
16
|
+
|
|
17
|
+
async execute(input: CreateExampleInput): Promise<Example> {
|
|
18
|
+
const title = input.title.trim();
|
|
19
|
+
|
|
20
|
+
if (title.length < 3) {
|
|
21
|
+
throw new AppError("Título inválido", "EXAMPLE_INVALID_TITLE");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return this.repo.create({ title, body: input.body.trim() });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { CreateExampleUseCase } from "./application/create-example.usecase";
|
|
3
|
+
import type { IExampleRepository } from "./domain/IExampleRepository";
|
|
4
|
+
import { InMemoryExampleRepository } from "./infrastructure/example.repository";
|
|
5
|
+
|
|
6
|
+
export interface IExampleUseCases {
|
|
7
|
+
createExample: CreateExampleUseCase;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createExampleUseCases(
|
|
11
|
+
repo: IExampleRepository = new InMemoryExampleRepository(),
|
|
12
|
+
): IExampleUseCases {
|
|
13
|
+
return {
|
|
14
|
+
createExample: new CreateExampleUseCase(repo),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface IExampleRepository {
|
|
2
|
+
create(input: { title: string; body: string }): Promise<Example>;
|
|
3
|
+
findById(id: string): Promise<Example | null>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Example {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
body: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const ExampleTitleSchema = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(3, "Título precisa ter ao menos 3 caracteres")
|
|
6
|
+
.max(120, "Título muito longo");
|
|
7
|
+
|
|
8
|
+
export const ExampleBodySchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.min(10, "Conteúdo precisa ter ao menos 10 caracteres")
|
|
11
|
+
.max(5000, "Conteúdo muito longo");
|
|
12
|
+
|
|
13
|
+
export const CreateExampleSchema = z.object({
|
|
14
|
+
title: ExampleTitleSchema,
|
|
15
|
+
body: ExampleBodySchema,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type CreateExampleInput = z.infer<typeof CreateExampleSchema>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { Example, IExampleRepository } from "../domain/IExampleRepository";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adaptador em memória — SUBSTITUA pelo Prisma/Seu DB.
|
|
7
|
+
* Ver SDD/stacks/database.md. A interface do domínio NÃO muda.
|
|
8
|
+
*/
|
|
9
|
+
export class InMemoryExampleRepository implements IExampleRepository {
|
|
10
|
+
private readonly store = new Map<string, Example>();
|
|
11
|
+
|
|
12
|
+
async create(input: { title: string; body: string }): Promise<Example> {
|
|
13
|
+
const example: Example = {
|
|
14
|
+
id: randomUUID(),
|
|
15
|
+
title: input.title,
|
|
16
|
+
body: input.body,
|
|
17
|
+
createdAt: new Date(),
|
|
18
|
+
};
|
|
19
|
+
this.store.set(example.id, example);
|
|
20
|
+
return example;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async findById(id: string): Promise<Example | null> {
|
|
24
|
+
return this.store.get(id) ?? null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useActionState } from "react";
|
|
3
|
+
import { useFormStatus } from "react-dom";
|
|
4
|
+
import { createExampleAction, type ExampleActionState } from "../actions";
|
|
5
|
+
|
|
6
|
+
const initialState: ExampleActionState = { ok: false };
|
|
7
|
+
|
|
8
|
+
export function CreateExampleForm() {
|
|
9
|
+
const [state, formAction] = useActionState(createExampleAction, initialState);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<form action={formAction} className="card flex flex-col gap-4">
|
|
13
|
+
<div>
|
|
14
|
+
<label htmlFor="title" className="field-label">
|
|
15
|
+
Título
|
|
16
|
+
</label>
|
|
17
|
+
<input
|
|
18
|
+
id="title"
|
|
19
|
+
name="title"
|
|
20
|
+
className="field"
|
|
21
|
+
placeholder="Ex: Migração do ledger"
|
|
22
|
+
aria-invalid={state.errors?.title ? true : undefined}
|
|
23
|
+
aria-describedby={state.errors?.title ? "title-error" : undefined}
|
|
24
|
+
/>
|
|
25
|
+
{state.errors?.title ? (
|
|
26
|
+
<p id="title-error" className="mt-1 text-body-sm text-error">
|
|
27
|
+
{state.errors.title[0]}
|
|
28
|
+
</p>
|
|
29
|
+
) : null}
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div>
|
|
33
|
+
<label htmlFor="body" className="field-label">
|
|
34
|
+
Conteúdo
|
|
35
|
+
</label>
|
|
36
|
+
<textarea id="body" name="body" rows={4} className="field" />
|
|
37
|
+
{state.errors?.body ? (
|
|
38
|
+
<p className="mt-1 text-body-sm text-error">{state.errors.body[0]}</p>
|
|
39
|
+
) : null}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{state.error ? <p className="text-body-sm text-error">{state.error}</p> : null}
|
|
43
|
+
|
|
44
|
+
<SubmitButton />
|
|
45
|
+
</form>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function SubmitButton() {
|
|
50
|
+
const { pending } = useFormStatus();
|
|
51
|
+
return (
|
|
52
|
+
<button type="submit" className="btn-primary self-start" disabled={pending}>
|
|
53
|
+
{pending ? "Processando..." : "Criar exemplo"}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|