go-duck-cli 1.0.8 → 1.1.1
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 +30 -15
- package/generators/ai_docs.js +130 -0
- package/generators/broker.js +63 -0
- package/generators/config.js +149 -7
- package/generators/devops.js +210 -43
- package/generators/docs.js +23 -4
- package/generators/elasticsearch.js +263 -0
- package/generators/kratos.js +229 -41
- package/generators/metering.js +280 -48
- package/generators/migrations.js +92 -198
- package/generators/mqtt.js +2 -39
- package/generators/multitenancy.js +274 -71
- package/generators/nats.js +39 -0
- package/generators/outbox.js +171 -0
- package/generators/postgrest.js +7 -3
- package/generators/postman.js +405 -0
- package/generators/repository.js +27 -0
- package/generators/router.js +27 -0
- package/generators/security.js +95 -14
- package/generators/serverless.js +147 -0
- package/generators/storage.js +589 -0
- package/generators/swagger.js +84 -60
- package/generators/telemetry.js +23 -32
- package/generators/websocket.js +55 -21
- package/index.js +481 -116
- package/package.json +6 -4
- package/parser/gdl.js +163 -24
- package/templates/docs/index.html.hbs +5 -5
- package/templates/docs/layout.hbs +221 -62
- package/templates/docs/pages/audit.hbs +83 -35
- package/templates/docs/pages/cli.hbs +18 -0
- package/templates/docs/pages/configuration.hbs +241 -0
- package/templates/docs/pages/datadog.hbs +46 -0
- package/templates/docs/pages/elasticsearch.hbs +121 -0
- package/templates/docs/pages/federation.hbs +241 -0
- package/templates/docs/pages/gdl-advanced.hbs +91 -0
- package/templates/docs/pages/gdl-annotations.hbs +137 -0
- package/templates/docs/pages/gdl-entities.hbs +134 -0
- package/templates/docs/pages/gdl-relationships.hbs +80 -0
- package/templates/docs/pages/gdl.hbs +60 -204
- package/templates/docs/pages/graphql.hbs +58 -44
- package/templates/docs/pages/grpc.hbs +53 -90
- package/templates/docs/pages/hybrid-store.hbs +127 -0
- package/templates/docs/pages/index.hbs +418 -149
- package/templates/docs/pages/keycloak.hbs +43 -0
- package/templates/docs/pages/legend.hbs +116 -0
- package/templates/docs/pages/mosquitto.hbs +39 -0
- package/templates/docs/pages/multitenancy.hbs +139 -71
- package/templates/docs/pages/otel.hbs +40 -0
- package/templates/docs/pages/realtime.hbs +38 -12
- package/templates/docs/pages/redis.hbs +40 -0
- package/templates/docs/pages/rest.hbs +120 -202
- package/templates/docs/pages/saga.hbs +94 -0
- package/templates/docs/pages/security.hbs +150 -44
- package/templates/docs/pages/serverless.hbs +157 -0
- package/templates/docs/pages/storage.hbs +127 -0
- package/templates/docs/pages/wizard.hbs +683 -0
- package/templates/docs/triple_identity_registry.png +0 -0
- package/templates/go/controller.go.hbs +287 -283
- package/templates/go/entity.go.hbs +17 -15
- package/templates/go/main.go.hbs +47 -180
- package/templates/go/migrator.go.hbs +65 -0
- package/templates/go/router.go.hbs +272 -0
- package/templates/graphql/resolver.go.hbs +53 -34
- package/templates/graphql/schema.graphql.hbs +17 -5
- package/templates/kratos/service.go.hbs +169 -34
- package/templates/proto/entity.proto.hbs +10 -14
- package/test_nested.gdl +21 -0
- package/templates/docs/intro.mp4 +0 -0
- package/test_parser.js +0 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "go-duck-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"chalk": "^4.1.2",
|
|
17
17
|
"commander": "^14.0.3",
|
|
18
|
+
"express": "^5.2.1",
|
|
18
19
|
"fs-extra": "^11.3.4",
|
|
19
20
|
"handlebars": "^4.7.8",
|
|
20
21
|
"inquirer": "^8.2.7",
|
|
21
|
-
"js-yaml": "^4.1.1"
|
|
22
|
+
"js-yaml": "^4.1.1",
|
|
23
|
+
"open": "^11.0.0"
|
|
22
24
|
}
|
|
23
|
-
}
|
|
25
|
+
}
|
package/parser/gdl.js
CHANGED
|
@@ -21,7 +21,10 @@ import path from 'path';
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
export const parseGDL = async (filePath) => {
|
|
24
|
-
const
|
|
24
|
+
const rawContent = await fs.readFile(filePath, 'utf8');
|
|
25
|
+
// Strip single-line (//) and multi-line (/* */) comments to prevent parsing artifacts
|
|
26
|
+
const content = rawContent.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, '');
|
|
27
|
+
|
|
25
28
|
const entities = [];
|
|
26
29
|
const relationships = [];
|
|
27
30
|
const enums = [];
|
|
@@ -35,39 +38,122 @@ export const parseGDL = async (filePath) => {
|
|
|
35
38
|
enums.push({ name, values });
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// 🦆 RECURSIVE BLOCK PARSER: Correctly handles nested braces for @Document entities
|
|
42
|
+
const parseEntityBlocks = (content) => {
|
|
43
|
+
const entityBlocks = [];
|
|
44
|
+
const entityStartRegex = /((?:@\w+\s*)*)entity\s+(\w+)\s*\{/g;
|
|
45
|
+
let match;
|
|
46
|
+
|
|
47
|
+
while ((match = entityStartRegex.exec(content)) !== null) {
|
|
48
|
+
const annotation = match[1]?.trim();
|
|
49
|
+
const name = match[2];
|
|
50
|
+
if (name === 'relationship' || name === 'enum') continue;
|
|
51
|
+
|
|
52
|
+
const startIndex = match.index + match[0].length;
|
|
53
|
+
let braceCount = 1;
|
|
54
|
+
let index = startIndex;
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
while (braceCount > 0 && index < content.length) {
|
|
57
|
+
if (content[index] === '{') braceCount++;
|
|
58
|
+
if (content[index] === '}') braceCount--;
|
|
59
|
+
index++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fieldBlock = content.substring(startIndex, index - 1);
|
|
63
|
+
entityBlocks.push({ name, annotation, fieldBlock });
|
|
64
|
+
}
|
|
65
|
+
return entityBlocks;
|
|
66
|
+
};
|
|
46
67
|
|
|
68
|
+
const parseFields = (block) => {
|
|
47
69
|
const fields = [];
|
|
48
|
-
const
|
|
70
|
+
const lines = block.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
71
|
+
let i = 0;
|
|
72
|
+
|
|
73
|
+
while (i < lines.length) {
|
|
74
|
+
const line = lines[i];
|
|
75
|
+
const parts = line.split(/\s+/).filter(p => p.length > 0);
|
|
76
|
+
if (parts.length < 1) { i++; continue; }
|
|
77
|
+
|
|
78
|
+
// Extract field annotations
|
|
79
|
+
const fieldAnnotations = parts.filter(p => p.startsWith('@'));
|
|
80
|
+
const dataParts = parts.filter(p => !p.startsWith('@'));
|
|
49
81
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
// Check if this line starts a nested block like "clinicalData {"
|
|
83
|
+
if (line.includes('{')) {
|
|
84
|
+
let fieldName = dataParts[0] || 'unknown';
|
|
85
|
+
if (line.includes('{')) {
|
|
86
|
+
const nestedBlockStart = line.indexOf('{');
|
|
87
|
+
fieldName = line.substring(0, nestedBlockStart).trim().split(/\s+/).pop();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Extract the nested block by finding the matching brace in the block
|
|
91
|
+
let nestedBraceCount = 1;
|
|
92
|
+
let nestedContent = '';
|
|
93
|
+
|
|
94
|
+
const fullRemainingBlock = block.substring(block.indexOf(line) + line.indexOf('{') + 1);
|
|
95
|
+
let charIndex = 0;
|
|
96
|
+
while (nestedBraceCount > 0 && charIndex < fullRemainingBlock.length) {
|
|
97
|
+
if (fullRemainingBlock[charIndex] === '{') nestedBraceCount++;
|
|
98
|
+
if (fullRemainingBlock[charIndex] === '}') nestedBraceCount--;
|
|
99
|
+
if (nestedBraceCount > 0) nestedContent += fullRemainingBlock[charIndex];
|
|
100
|
+
charIndex++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fields.push({
|
|
104
|
+
name: fieldName,
|
|
105
|
+
type: 'Object',
|
|
106
|
+
isNested: true,
|
|
107
|
+
annotations: fieldAnnotations,
|
|
108
|
+
children: parseFields(nestedContent)
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const nestedLineCount = nestedContent.split('\n').length;
|
|
112
|
+
i += nestedLineCount + 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
53
115
|
|
|
54
|
-
|
|
55
|
-
let rawType = parts[1];
|
|
116
|
+
if (dataParts.length < 2) { i++; continue; }
|
|
56
117
|
|
|
57
|
-
|
|
118
|
+
let fieldName = dataParts[0];
|
|
119
|
+
let rawType = dataParts[1];
|
|
120
|
+
|
|
121
|
+
// Standard parsing logic for simple fields
|
|
122
|
+
const typeKeywords = ['string', 'text', 'int', 'long', 'float', 'bigdecimal', 'bool', 'boolean', 'datetime', 'localdate', 'json', 'jsonb', 'uuid', 'instant'];
|
|
123
|
+
const isKnownType = typeKeywords.some(tk => dataParts[0].toLowerCase().startsWith(tk));
|
|
124
|
+
const isKnownEnum = enums.some(e => e.name.toLowerCase() === dataParts[0].toLowerCase());
|
|
125
|
+
|
|
126
|
+
if (isKnownType || isKnownEnum) {
|
|
127
|
+
fieldName = dataParts[1];
|
|
128
|
+
rawType = dataParts[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Standardize capitalization for the factory
|
|
132
|
+
rawType = rawType.charAt(0).toUpperCase() + rawType.slice(1).toLowerCase();
|
|
133
|
+
if (rawType.toLowerCase().startsWith('string(')) rawType = 'String' + rawType.slice(6);
|
|
134
|
+
if (rawType.startsWith('Float')) rawType = 'Float';
|
|
135
|
+
if (rawType === 'Int' || rawType.startsWith('Int(')) rawType = 'Integer';
|
|
136
|
+
if (rawType === 'Bool') rawType = 'Boolean';
|
|
137
|
+
|
|
138
|
+
// Support String(N) or Int(N) for custom sizing
|
|
58
139
|
let varcharSize = 255;
|
|
59
|
-
const sizeMatch = rawType.match(/^String\((\d+)\)$/);
|
|
140
|
+
const sizeMatch = rawType.match(/^(?:String|Integer)\((\d+)\)$/i);
|
|
60
141
|
if (sizeMatch) {
|
|
61
142
|
varcharSize = parseInt(sizeMatch[1], 10);
|
|
62
|
-
rawType = 'String';
|
|
143
|
+
rawType = rawType.startsWith('String') ? 'String' : 'Integer';
|
|
63
144
|
}
|
|
64
145
|
|
|
65
146
|
const required = line.includes('required');
|
|
66
147
|
const unique = line.includes('unique');
|
|
67
148
|
const isText = rawType === 'Text';
|
|
149
|
+
const isVersion = fieldAnnotations.includes('@Version');
|
|
68
150
|
|
|
69
151
|
// Check if type is an Enum
|
|
70
|
-
const isEnum = enums.some(e => e.name === rawType);
|
|
152
|
+
const isEnum = enums.some(e => e.name.toLowerCase() === rawType.toLowerCase());
|
|
153
|
+
if (isEnum) {
|
|
154
|
+
const en = enums.find(e => e.name.toLowerCase() === rawType.toLowerCase());
|
|
155
|
+
rawType = en.name; // Use correct case
|
|
156
|
+
}
|
|
71
157
|
|
|
72
158
|
fields.push({
|
|
73
159
|
name: fieldName,
|
|
@@ -75,15 +161,41 @@ export const parseGDL = async (filePath) => {
|
|
|
75
161
|
required,
|
|
76
162
|
unique,
|
|
77
163
|
isEnum,
|
|
164
|
+
isVersion,
|
|
165
|
+
annotations: fieldAnnotations,
|
|
78
166
|
varcharSize: rawType === 'String' ? varcharSize : null,
|
|
79
167
|
});
|
|
168
|
+
i++;
|
|
80
169
|
}
|
|
170
|
+
return fields;
|
|
171
|
+
};
|
|
172
|
+
const entityBlocks = parseEntityBlocks(content);
|
|
173
|
+
for (const block of entityBlocks) {
|
|
174
|
+
const annotation = block.annotation;
|
|
175
|
+
const name = block.name;
|
|
81
176
|
|
|
82
|
-
|
|
177
|
+
const isAudited = annotation?.includes('@Audited');
|
|
178
|
+
const isFederated = annotation?.includes('@Federated');
|
|
179
|
+
const isSearchable = annotation?.includes('@Searchable');
|
|
180
|
+
const isDocument = annotation?.includes('@Document') || annotation?.includes('@isDocument');
|
|
181
|
+
const isEmbedded = annotation?.includes('@Embed');
|
|
182
|
+
|
|
183
|
+
const fields = parseFields(block.fieldBlock);
|
|
184
|
+
|
|
185
|
+
entities.push({
|
|
186
|
+
name,
|
|
187
|
+
annotation,
|
|
188
|
+
isAudited,
|
|
189
|
+
isFederated,
|
|
190
|
+
isSearchable,
|
|
191
|
+
isDocument,
|
|
192
|
+
isEmbedded,
|
|
193
|
+
fields
|
|
194
|
+
});
|
|
83
195
|
}
|
|
84
196
|
|
|
85
|
-
// Parse relationship blocks
|
|
86
|
-
const relRegex = /relationship\s+(\w+)\s*\{([\s\S]*?)\
|
|
197
|
+
// Parse relationship blocks (Robust whitespace handling)
|
|
198
|
+
const relRegex = /relationship\s+(\w+)\s*\{([\s\S]*?)\}/g;
|
|
87
199
|
while ((match = relRegex.exec(content)) !== null) {
|
|
88
200
|
const type = match[1];
|
|
89
201
|
const relBlock = match[2];
|
|
@@ -94,8 +206,12 @@ export const parseGDL = async (filePath) => {
|
|
|
94
206
|
if (relParts.length !== 2) continue;
|
|
95
207
|
|
|
96
208
|
const parseRelPart = (p) => {
|
|
97
|
-
|
|
98
|
-
|
|
209
|
+
// Support both "Parent{id}" and "Parent"
|
|
210
|
+
const mWithBraces = p.match(/(\w+)\s*\{\s*(\w+)\s*\}/);
|
|
211
|
+
if (mWithBraces) return { entity: mWithBraces[1], field: mWithBraces[2] };
|
|
212
|
+
|
|
213
|
+
const mSimple = p.match(/^(\w+)/);
|
|
214
|
+
return mSimple ? { entity: mSimple[1], field: null } : null;
|
|
99
215
|
};
|
|
100
216
|
|
|
101
217
|
// Support required FK: "required" keyword anywhere in the line
|
|
@@ -110,7 +226,30 @@ export const parseGDL = async (filePath) => {
|
|
|
110
226
|
}
|
|
111
227
|
}
|
|
112
228
|
|
|
113
|
-
|
|
229
|
+
// Parse open entities
|
|
230
|
+
const openEntities = [];
|
|
231
|
+
const openRegex = /open\s+(.*)/g;
|
|
232
|
+
while ((match = openRegex.exec(content)) !== null) {
|
|
233
|
+
const val = match[1].trim();
|
|
234
|
+
// Split by comma but ignore commas inside parentheses
|
|
235
|
+
const items = val.split(/,(?![^\(]*\))/).map(v => v.trim()).filter(v => v.length > 0);
|
|
236
|
+
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
const itemMatch = item.match(/^([\w\*]+)(?:\s*\((.*?)\))?$/);
|
|
239
|
+
if (itemMatch) {
|
|
240
|
+
const name = itemMatch[1];
|
|
241
|
+
const actionStr = itemMatch[2];
|
|
242
|
+
let actions = ['read', 'create', 'update', 'delete'];
|
|
243
|
+
if (actionStr) {
|
|
244
|
+
actions = actionStr.split(/[\s,]+/).map(a => a.trim().toLowerCase()).filter(a => a.length > 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
openEntities.push({ name, actions });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { entities, relationships, enums, openEntities };
|
|
114
253
|
};
|
|
115
254
|
|
|
116
255
|
/**
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
<li class="pt-2 pb-1"><span class="font-semibold text-gray-800">5. Observability</span></li>
|
|
42
42
|
<li><a href="#observability" class="text-gray-600 hover:text-indigo-600 hover:font-medium transition-colors pl-4 block">OTel & Datadog</a></li>
|
|
43
43
|
<li class="pt-2 pb-1"><span class="font-semibold text-gray-800">6. Migrations</span></li>
|
|
44
|
-
<li><a href="#
|
|
44
|
+
<li><a href="#goose" class="text-gray-600 hover:text-indigo-600 hover:font-medium transition-colors pl-4 block">Goose SQL & GDL</a></li>
|
|
45
45
|
</ul>
|
|
46
46
|
</aside>
|
|
47
47
|
|
|
@@ -206,12 +206,12 @@ res, err := client.Get{{#if entities.length}}{{capitalize entities.[0].name}}{{e
|
|
|
206
206
|
</ul>
|
|
207
207
|
</section>
|
|
208
208
|
|
|
209
|
-
<!--
|
|
210
|
-
<section id="
|
|
209
|
+
<!-- Goose & GDL -->
|
|
210
|
+
<section id="goose" class="content-section">
|
|
211
211
|
<h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">6. Advanced Migrations & GDL</h2>
|
|
212
|
-
<p class="mb-4">Running <code>go-duck import-gdl</code> calculates stateful differences using the `.go-duck/` snapshot folder. It generates atomic, timestamped
|
|
212
|
+
<p class="mb-4">Running <code>go-duck import-gdl</code> calculates stateful differences using the `.go-duck/` snapshot folder. It generates atomic, Go-embedded timestamped Goose SQL migrations in <code>migrations/sql/</code>.</p>
|
|
213
213
|
<ul class="list-disc pl-6 space-y-2">
|
|
214
|
-
<li>The application
|
|
214
|
+
<li>The application natively compiles SQL inside the Go binary via <code>go:embed</code>.</li>
|
|
215
215
|
<li>Includes smart nullability and automatic indexing for Foreign Keys.</li>
|
|
216
216
|
<li><strong>Needle Support:</strong> We inject `// go-duck-needle-*` markers in <code>main.go</code> and <code>grpc.go</code> for safe evolutionary code additions without destroying manual updates.</li>
|
|
217
217
|
</ul>
|
|
@@ -1,105 +1,264 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
2
|
+
<html lang="en" class="scroll-smooth">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>{{appName}} - {{title}}</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
-
<link href="https://fonts.googleapis.com
|
|
9
|
-
<link rel="
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
11
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
|
10
12
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
11
13
|
<script>
|
|
12
14
|
tailwind.config = {
|
|
13
15
|
theme: {
|
|
14
16
|
extend: {
|
|
15
|
-
fontFamily: {
|
|
17
|
+
fontFamily: {
|
|
18
|
+
sans: ['Inter', 'sans-serif'],
|
|
19
|
+
mono: ['JetBrains Mono', 'monospace'],
|
|
20
|
+
},
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
</script>
|
|
20
25
|
<script>hljs.highlightAll();</script>
|
|
21
26
|
<style>
|
|
22
|
-
.gradient-text { background: linear-gradient(
|
|
27
|
+
.gradient-text { background: linear-gradient(135deg, #6366f1, #a855f7, #ec4899); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
23
28
|
::-webkit-scrollbar { width: 6px; }
|
|
24
|
-
::-webkit-scrollbar-
|
|
29
|
+
::-webkit-scrollbar-track { background: #f1f5f9; }
|
|
30
|
+
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
|
31
|
+
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
|
32
|
+
|
|
33
|
+
.sidebar-link-active {
|
|
34
|
+
background-color: #ebf5ff;
|
|
35
|
+
color: #1e40af;
|
|
36
|
+
border-right: 4px solid #3b82f6;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#sidebar {
|
|
41
|
+
width: 280px;
|
|
42
|
+
height: calc(100vh - 64px);
|
|
43
|
+
position: fixed;
|
|
44
|
+
top: 64px;
|
|
45
|
+
left: 0;
|
|
46
|
+
overflow-y: auto;
|
|
47
|
+
border-right: 1px solid #e2e8f0;
|
|
48
|
+
background: #ffffff;
|
|
49
|
+
z-index: 30;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#content-wrapper {
|
|
53
|
+
margin-left: 280px;
|
|
54
|
+
padding-top: 64px;
|
|
55
|
+
min-height: 100vh;
|
|
56
|
+
background: #ffffff;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (max-width: 1024px) {
|
|
60
|
+
#sidebar {
|
|
61
|
+
transform: translateX(-100%);
|
|
62
|
+
transition: transform 0.3s ease;
|
|
63
|
+
}
|
|
64
|
+
#sidebar.open {
|
|
65
|
+
transform: translateX(0);
|
|
66
|
+
}
|
|
67
|
+
#content-wrapper {
|
|
68
|
+
margin-left: 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.prose pre {
|
|
73
|
+
background-color: #0f172a !important;
|
|
74
|
+
border-radius: 0.75rem;
|
|
75
|
+
border: 1px solid #1e293b;
|
|
76
|
+
color: #00ff41 !important;
|
|
77
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
78
|
+
}
|
|
79
|
+
.prose pre code {
|
|
80
|
+
color: inherit !important;
|
|
81
|
+
text-shadow: 0 0 5px rgba(0, 255, 65, 0.2);
|
|
82
|
+
}
|
|
83
|
+
.prose code:not(pre code) {
|
|
84
|
+
background-color: #f1f5f9;
|
|
85
|
+
color: #0f172a;
|
|
86
|
+
padding: 0.2rem 0.4rem;
|
|
87
|
+
border-radius: 0.375rem;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
}
|
|
90
|
+
@keyframes float {
|
|
91
|
+
0% { transform: translateY(0px); }
|
|
92
|
+
50% { transform: translateY(-15px); }
|
|
93
|
+
100% { transform: translateY(0px); }
|
|
94
|
+
}
|
|
95
|
+
.animate-float {
|
|
96
|
+
animation: float 6s ease-in-out infinite;
|
|
97
|
+
}
|
|
25
98
|
</style>
|
|
26
99
|
</head>
|
|
27
|
-
<body class="bg-
|
|
28
|
-
|
|
29
|
-
|
|
100
|
+
<body class="bg-white text-slate-900 font-sans antialiased selection:bg-blue-100 selection:text-blue-900 overflow-x-hidden">
|
|
101
|
+
|
|
102
|
+
<!-- Header Navbar -->
|
|
103
|
+
<nav class="fixed top-0 left-0 right-0 h-16 bg-white border-b border-slate-200 z-50 flex items-center px-6 justify-between">
|
|
30
104
|
<div class="flex items-center">
|
|
31
|
-
<
|
|
32
|
-
|
|
105
|
+
<button id="mobile-menu-btn" class="lg:hidden mr-4 p-2 text-slate-600 hover:bg-slate-100 rounded-lg">
|
|
106
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
|
107
|
+
</button>
|
|
108
|
+
<div class="relative group cursor-pointer mr-3">
|
|
109
|
+
<div class="absolute -inset-1.5 bg-gradient-to-tr from-indigo-500/20 to-purple-500/20 rounded-xl blur opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
|
|
110
|
+
<img src="logo.png" alt="GO-DUCK Logo" class="relative h-10 w-auto transform hover:scale-105 transition-all duration-300 drop-shadow-sm">
|
|
111
|
+
</div>
|
|
112
|
+
<h1 class="text-xl font-bold text-slate-900 tracking-tight flex items-center">
|
|
113
|
+
GO-DUCK
|
|
114
|
+
<span class="ml-3 px-2 py-0.5 bg-slate-100 text-slate-500 text-[10px] uppercase tracking-widest font-bold rounded-md border border-slate-200">
|
|
115
|
+
V3.0
|
|
116
|
+
</span>
|
|
117
|
+
</h1>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="hidden md:flex items-center space-x-6 text-sm font-medium text-slate-600">
|
|
120
|
+
<a href="https://github.com" target="_blank" class="hover:text-blue-600 transition-colors">GitHub</a>
|
|
121
|
+
<a href="swagger.json" target="_blank" class="hover:text-blue-600 transition-colors">API Docs</a>
|
|
33
122
|
</div>
|
|
34
123
|
</nav>
|
|
35
124
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
<h3 class="font-bold text-slate-400 uppercase
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<ul class="space-y-1 text-sm font-medium text-slate-600">
|
|
44
|
-
<li><a href="index.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'index')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">Home & Quick Start</a></li>
|
|
45
|
-
|
|
46
|
-
<li class="pt-5 pb-1 px-3"><span class="font-bold text-slate-900 uppercase text-[10px] tracking-widest">1. Core Concepts</span></li>
|
|
47
|
-
<li><a href="gdl.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'gdl')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">GDL & Framework</a></li>
|
|
48
|
-
<li><a href="cli.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'cli')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">CLI & Code Injection</a></li>
|
|
49
|
-
|
|
50
|
-
<li class="pt-5 pb-1 px-3"><span class="font-bold text-slate-900 uppercase text-[10px] tracking-widest">2. API Endpoints</span></li>
|
|
51
|
-
<li><a href="rest.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'rest')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">REST & Generic Search</a></li>
|
|
52
|
-
<li><a href="graphql.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'graphql')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">GraphQL Integration</a></li>
|
|
53
|
-
<li><a href="grpc.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'grpc')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">Secured gRPC (Kratos)</a></li>
|
|
54
|
-
|
|
55
|
-
<li class="pt-5 pb-1 px-3"><span class="font-bold text-slate-900 uppercase text-[10px] tracking-widest">3. Real-Time & Audit</span></li>
|
|
56
|
-
<li><a href="realtime.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'realtime')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">WebSockets & MQTT</a></li>
|
|
57
|
-
<li><a href="audit.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'audit')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">Audit & Metering</a></li>
|
|
58
|
-
|
|
59
|
-
<li class="pt-5 pb-1 px-3"><span class="font-bold text-slate-900 uppercase text-[10px] tracking-widest">4. Infrastructure</span></li>
|
|
60
|
-
<li><a href="security.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'security')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">Security & Resilience</a></li>
|
|
61
|
-
<li><a href="observability.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'observability')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">Observability</a></li>
|
|
62
|
-
|
|
63
|
-
<li class="pt-5 pb-1 px-3"><span class="font-bold text-slate-900 uppercase text-[10px] tracking-widest">5. References</span></li>
|
|
64
|
-
<li><a href="integrations.html" class="flex items-center px-3 py-2 rounded-lg transition-all duration-200 {{#if (eq activePage 'integrations')}}bg-white shadow-sm ring-1 ring-slate-200 text-indigo-700{{else}}hover:bg-slate-200/50 hover:text-slate-900{{/if}}">Client Integrations</a></li>
|
|
125
|
+
<!-- Sidebar Navigation -->
|
|
126
|
+
<aside id="sidebar" class="py-6 px-2">
|
|
127
|
+
<div class="mb-8 px-4">
|
|
128
|
+
<h3 class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-4">Documentation</h3>
|
|
129
|
+
<ul class="space-y-1">
|
|
130
|
+
<li><a href="index.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'index')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Introduction</a></li>
|
|
131
|
+
<li><a href="legend.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'legend')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">🦆 The Legend</a></li>
|
|
65
132
|
</ul>
|
|
66
|
-
</
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="mb-8 px-4">
|
|
136
|
+
<h3 class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-4">GDL Language</h3>
|
|
137
|
+
<ul class="space-y-1 border-l-2 border-indigo-100 ml-2">
|
|
138
|
+
<li><a href="gdl.html" class="block px-4 py-2 rounded-r-lg text-sm transition-all {{#if (eq activePage 'gdl')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">⚡ Getting Started</a></li>
|
|
139
|
+
<li><a href="gdl-entities.html" class="block px-4 py-2 rounded-r-lg text-sm transition-all {{#if (eq activePage 'gdl-entities')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">💎 Entities & Fields</a></li>
|
|
140
|
+
<li><a href="gdl-relationships.html" class="block px-4 py-2 rounded-r-lg text-sm transition-all {{#if (eq activePage 'gdl-relationships')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">🧬 Relationships</a></li>
|
|
141
|
+
<li><a href="gdl-annotations.html" class="block px-4 py-2 rounded-r-lg text-sm transition-all {{#if (eq activePage 'gdl-annotations')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">🎯 Power-up Annotations</a></li>
|
|
142
|
+
<li><a href="gdl-advanced.html" class="block px-4 py-2 rounded-r-lg text-sm transition-all {{#if (eq activePage 'gdl-advanced')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">🔮 Advanced Topics</a></li>
|
|
143
|
+
</ul>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="mb-8 px-4">
|
|
147
|
+
<div class="flex items-center justify-between mb-4">
|
|
148
|
+
<h3 class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em]">Generation & CLI</h3>
|
|
149
|
+
<span class="px-2 py-0.5 bg-indigo-600 text-white text-[9px] font-black rounded-lg animate-pulse shadow-lg shadow-indigo-200">NEW</span>
|
|
78
150
|
</div>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
|
|
151
|
+
<ul class="space-y-1">
|
|
152
|
+
<li><a href="wizard.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'wizard')}}sidebar-link-active font-black bg-indigo-50{{else}}text-slate-600 font-bold hover:bg-slate-100 hover:text-slate-900{{/if}}">✨ Config Wizard</a></li>
|
|
153
|
+
<li><a href="cli.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'cli')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">CLI Reference</a></li>
|
|
154
|
+
<li><a href="configuration.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'configuration')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">⚙️ Configuration</a></li>
|
|
155
|
+
</ul>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="mb-8 px-4">
|
|
159
|
+
<h3 class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-4">API Features</h3>
|
|
160
|
+
<ul class="space-y-1">
|
|
161
|
+
<li><a href="rest.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'rest')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">REST & Search</a></li>
|
|
162
|
+
<li><a href="graphql.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'graphql')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">GraphQL Integration</a></li>
|
|
163
|
+
<li><a href="multitenancy.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'multitenancy')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Multi-Tenancy</a></li>
|
|
164
|
+
<li><a href="federation.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'federation')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Federated Architecture</a></li>
|
|
165
|
+
<li><a href="hybrid-store.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'hybrid-store')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">🍀 Hybrid-Store</a></li>
|
|
166
|
+
<li><a href="grpc.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'grpc')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">gRPC Service</a></li>
|
|
167
|
+
</ul>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="mb-8 px-4">
|
|
171
|
+
<h3 class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-4">Monitoring</h3>
|
|
172
|
+
<ul class="space-y-1">
|
|
173
|
+
<li><a href="realtime.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'realtime')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">WebSockets</a></li>
|
|
174
|
+
<li><a href="audit.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'audit')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Audit Logs</a></li>
|
|
175
|
+
<li><a href="observability.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'observability')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Observability</a></li>
|
|
176
|
+
</ul>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="mb-8 px-4">
|
|
180
|
+
<h3 class="text-[11px] font-bold text-slate-400 uppercase tracking-[0.2em] mb-4">Infrastructure</h3>
|
|
181
|
+
<ul class="space-y-1">
|
|
182
|
+
<li><a href="security.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'security')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Security</a></li>
|
|
183
|
+
<li><a href="redis.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'redis')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Redis</a></li>
|
|
184
|
+
<li><a href="keycloak.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'keycloak')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">Keycloak</a></li>
|
|
185
|
+
<li><a href="serverless.html" class="block px-4 py-2 rounded-lg text-sm transition-all {{#if (eq activePage 'serverless')}}sidebar-link-active{{else}}text-slate-600 hover:bg-slate-100 hover:text-slate-900{{/if}}">🚀 Serverless</a></li>
|
|
186
|
+
</ul>
|
|
187
|
+
</div>
|
|
188
|
+
</aside>
|
|
189
|
+
|
|
190
|
+
<!-- Main Content Wrapper -->
|
|
191
|
+
<div id="content-wrapper">
|
|
192
|
+
<main class="w-full px-6 md:px-12 py-10">
|
|
193
|
+
<article class="max-w-none prose prose-slate prose-blue">
|
|
194
|
+
{{{body}}}
|
|
195
|
+
</article>
|
|
196
|
+
|
|
197
|
+
<!-- Consolidated Elite Footer -->
|
|
198
|
+
<footer class="mt-24 py-12 border-t border-slate-100 flex flex-col md:flex-row justify-between items-center gap-12 text-slate-400 text-[10px] font-bold uppercase tracking-[0.3em]">
|
|
199
|
+
<div class="flex flex-col items-center md:items-start gap-4">
|
|
200
|
+
<p>© 2024-2026 GO-DUCK ELITE ECOSYSTEM. ALL RIGHTS RESERVED.</p>
|
|
201
|
+
<div class="flex gap-10">
|
|
202
|
+
<a href="security.html" class="hover:text-indigo-600 transition-colors">Digital Defense</a>
|
|
203
|
+
<a href="audit.html" class="hover:text-indigo-600 transition-colors">Privacy Shield</a>
|
|
204
|
+
<a href="configuration.html" class="hover:text-indigo-600 transition-colors">Architecture Audit</a>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="flex items-center gap-6">
|
|
208
|
+
<div class="flex flex-col items-end gap-1">
|
|
209
|
+
<span class="text-[9px] text-slate-300 uppercase">System Architecture</span>
|
|
210
|
+
<span class="text-slate-400 font-black">INDUSTRIAL GRADE VR.1</span>
|
|
211
|
+
</div>
|
|
212
|
+
<span class="text-4xl filter grayscale opacity-20 transform hover:grayscale-0 hover:opacity-100 transition-all duration-700 cursor-alias">🦆</span>
|
|
213
|
+
</div>
|
|
84
214
|
</footer>
|
|
85
215
|
</main>
|
|
86
216
|
</div>
|
|
217
|
+
|
|
87
218
|
<!-- Lightbox Overlay -->
|
|
88
|
-
<div id="lightbox" class="fixed inset-0 bg-slate-900/
|
|
89
|
-
<div class="relative max-w-
|
|
90
|
-
<img id="lightbox-img" src="" alt="Enlarged View" class="max-w-full max-h-full object-contain rounded-
|
|
91
|
-
<button class="absolute top-
|
|
92
|
-
<svg class="w-
|
|
219
|
+
<div id="lightbox" class="fixed inset-0 bg-slate-900/95 backdrop-blur-md z-[100] hidden flex items-center justify-center p-4 md:p-10 cursor-zoom-out transition-all duration-300 opacity-0" onclick="closeLightbox()">
|
|
220
|
+
<div class="relative max-w-6xl w-full h-full flex flex-col items-center justify-center transform scale-95 transition-transform duration-300" id="lightbox-content">
|
|
221
|
+
<img id="lightbox-img" src="" alt="Enlarged View" class="max-w-full max-h-full object-contain rounded-2xl shadow-2xl ring-1 ring-white/10">
|
|
222
|
+
<button class="absolute top-4 right-4 m-4 text-white/70 hover:text-white bg-black/20 hover:bg-black/40 rounded-full p-2 transition-all p-3" onclick="closeLightbox(event)">
|
|
223
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
93
224
|
</button>
|
|
94
225
|
</div>
|
|
95
226
|
</div>
|
|
96
227
|
|
|
97
228
|
<script>
|
|
229
|
+
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
|
230
|
+
const sidebar = document.getElementById('sidebar');
|
|
231
|
+
|
|
232
|
+
if (mobileMenuBtn && sidebar) {
|
|
233
|
+
mobileMenuBtn.addEventListener('click', () => {
|
|
234
|
+
sidebar.classList.toggle('open');
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
98
238
|
function openLightbox(src) {
|
|
99
239
|
const lb = document.getElementById('lightbox');
|
|
240
|
+
const content = document.getElementById('lightbox-content');
|
|
100
241
|
const img = document.getElementById('lightbox-img');
|
|
101
242
|
img.src = src;
|
|
102
243
|
lb.classList.remove('hidden');
|
|
244
|
+
void lb.offsetWidth;
|
|
245
|
+
lb.classList.remove('opacity-0');
|
|
246
|
+
lb.classList.add('opacity-100');
|
|
247
|
+
content.classList.remove('scale-95');
|
|
248
|
+
content.classList.add('scale-100');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function closeLightbox(e) {
|
|
252
|
+
if (e) e.stopPropagation();
|
|
253
|
+
const lb = document.getElementById('lightbox');
|
|
254
|
+
const content = document.getElementById('lightbox-content');
|
|
255
|
+
lb.classList.remove('opacity-100');
|
|
256
|
+
lb.classList.add('opacity-0');
|
|
257
|
+
content.classList.remove('scale-100');
|
|
258
|
+
content.classList.add('scale-95');
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
lb.classList.add('hidden');
|
|
261
|
+
}, 300);
|
|
103
262
|
}
|
|
104
263
|
</script>
|
|
105
264
|
</body>
|