odac 1.4.4 → 1.4.6
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/.agent/rules/memory.md +10 -1
- package/.releaserc.js +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +2 -1
- package/client/odac.js +228 -62
- package/docs/ai/skills/backend/views.md +102 -30
- package/docs/ai/skills/frontend/core.md +17 -3
- package/docs/ai/skills/frontend/navigation.md +105 -8
- package/docs/backend/07-views/01-the-view-directory.md +28 -6
- package/docs/backend/07-views/02-rendering-a-view.md +16 -23
- package/docs/backend/07-views/03-template-syntax.md +48 -14
- package/docs/backend/07-views/03-variables.md +22 -7
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +22 -0
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +51 -0
- package/package.json +30 -6
- package/src/Request.js +5 -4
- package/src/Route.js +11 -0
- package/src/View.js +54 -9
- package/template/controller/page/about.js +3 -3
- package/template/controller/page/index.js +2 -2
- package/template/public/assets/js/app.js +38 -54
- package/template/skeleton/main.html +4 -4
- package/template/view/content/about.html +64 -60
- package/template/view/content/home.html +148 -175
- package/template/view/css/app.css +46 -0
- package/template/view/footer/main.html +10 -9
- package/template/view/header/main.html +34 -11
- package/test/Client/load.test.js +306 -0
- package/test/Database/ConnectionFactory/buildConnections.test.js +4 -0
- package/test/View/parseOdacTag.test.js +180 -0
- package/template/public/assets/css/style.css +0 -1835
package/src/View.js
CHANGED
|
@@ -103,6 +103,7 @@ class View {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
#part = {}
|
|
106
|
+
#refresh = new Set()
|
|
106
107
|
#odac = null
|
|
107
108
|
|
|
108
109
|
constructor(odac) {
|
|
@@ -148,10 +149,25 @@ class View {
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
//
|
|
152
|
+
// Build current parts manifest (part name → view path)
|
|
153
|
+
const currentParts = {}
|
|
154
|
+
for (let key in this.#part) {
|
|
155
|
+
if (['all', 'skeleton'].includes(key)) continue
|
|
156
|
+
if (this.#part[key]) currentParts[key] = this.#part[key]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Parse client's previous parts to detect unchanged regions
|
|
160
|
+
const clientParts = this.#odac.Request.clientParts || {}
|
|
161
|
+
|
|
162
|
+
// Render requested elements (skip unchanged parts)
|
|
152
163
|
let title = null
|
|
153
164
|
for (let element of this.#odac.Request.ajaxLoad) {
|
|
154
165
|
if (this.#part[element]) {
|
|
166
|
+
// Skip rendering if the part view path has not changed and refresh is not forced
|
|
167
|
+
// Content is always re-rendered since its output depends on the current URL
|
|
168
|
+
if (element !== 'content' && !this.#refresh.has(element) && clientParts[element] && clientParts[element] === this.#part[element])
|
|
169
|
+
continue
|
|
170
|
+
|
|
155
171
|
let viewPath = this.#part[element]
|
|
156
172
|
if (viewPath.includes('.')) viewPath = viewPath.replace(/\./g, '/')
|
|
157
173
|
if (await this.#exists(`./view/${element}/${viewPath}.html`)) {
|
|
@@ -202,6 +218,7 @@ class View {
|
|
|
202
218
|
|
|
203
219
|
this.#odac.Request.end({
|
|
204
220
|
output: output,
|
|
221
|
+
parts: currentParts,
|
|
205
222
|
variables: variables,
|
|
206
223
|
data: this.#odac.Request.sharedData,
|
|
207
224
|
title: title,
|
|
@@ -239,6 +256,9 @@ class View {
|
|
|
239
256
|
}
|
|
240
257
|
}
|
|
241
258
|
}
|
|
259
|
+
|
|
260
|
+
// Clean up unresolved skeleton placeholders
|
|
261
|
+
result = result.replace(/\{\{\s*[A-Z_]+\s*\}\}/g, '')
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
if (result) {
|
|
@@ -294,7 +314,7 @@ class View {
|
|
|
294
314
|
}
|
|
295
315
|
|
|
296
316
|
if (attrs.get) {
|
|
297
|
-
return `{{ get('${attrs.get}') || '' }}`
|
|
317
|
+
return `{{ get('${attrs.get.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}') || '' }}`
|
|
298
318
|
} else if (attrs.var) {
|
|
299
319
|
if (attrs.raw) {
|
|
300
320
|
return `{!! ${attrs.var} !!}`
|
|
@@ -323,7 +343,7 @@ class View {
|
|
|
323
343
|
}
|
|
324
344
|
|
|
325
345
|
if (attrs.get) {
|
|
326
|
-
return `{{ get('${attrs.get}') || '' }}`
|
|
346
|
+
return `{{ get('${attrs.get.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}') || '' }}`
|
|
327
347
|
} else if (attrs.var) {
|
|
328
348
|
if (attrs.raw) {
|
|
329
349
|
return `{!! ${attrs.var} !!}`
|
|
@@ -350,8 +370,9 @@ class View {
|
|
|
350
370
|
return `%s${placeholderIndex++}`
|
|
351
371
|
})
|
|
352
372
|
|
|
373
|
+
const escapedContent = processedContent.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
353
374
|
const translationCall =
|
|
354
|
-
placeholders.length > 0 ? `__('${
|
|
375
|
+
placeholders.length > 0 ? `__('${escapedContent}', ${placeholders.join(', ')})` : `__('${escapedContent}')`
|
|
355
376
|
|
|
356
377
|
if (attrs.raw) {
|
|
357
378
|
return `{!! ${translationCall} !!}`
|
|
@@ -359,7 +380,7 @@ class View {
|
|
|
359
380
|
return `{{ ${translationCall} }}`
|
|
360
381
|
}
|
|
361
382
|
} else {
|
|
362
|
-
return `{{ '${innerContent}' }}`
|
|
383
|
+
return `{{ '${innerContent.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' }}`
|
|
363
384
|
}
|
|
364
385
|
})
|
|
365
386
|
if (before === content) break
|
|
@@ -523,8 +544,12 @@ class View {
|
|
|
523
544
|
|
|
524
545
|
// - SET PARTS
|
|
525
546
|
set(...args) {
|
|
526
|
-
if (args.length === 1 && typeof args[0] === 'object')
|
|
527
|
-
|
|
547
|
+
if (args.length === 1 && typeof args[0] === 'object') {
|
|
548
|
+
for (let key in args[0]) this.#part[key] = args[0][key]
|
|
549
|
+
} else if (args.length >= 2) {
|
|
550
|
+
this.#part[args[0]] = args[1]
|
|
551
|
+
if (args[2]?.refresh) this.#refresh.add(args[0])
|
|
552
|
+
}
|
|
528
553
|
|
|
529
554
|
if (!this.#odac.Request.page) {
|
|
530
555
|
this.#odac.Request.page = this.#part.content || this.#part.all || ''
|
|
@@ -541,12 +566,21 @@ class View {
|
|
|
541
566
|
}
|
|
542
567
|
|
|
543
568
|
#addNavigateAttribute(skeleton) {
|
|
544
|
-
|
|
569
|
+
// Inject data-odac-navigate for placeholders already wrapped in an HTML tag
|
|
570
|
+
skeleton = skeleton.replace(/(<[^>]+>)(\s*\{\{\s*([A-Z_]+)\s*\}\})/g, (match, openTag, content, partName) => {
|
|
571
|
+
const attrName = partName.toLowerCase()
|
|
545
572
|
if (openTag.includes('data-odac-navigate')) return match
|
|
546
|
-
const tagWithAttr = openTag.slice(0, -1) +
|
|
573
|
+
const tagWithAttr = openTag.slice(0, -1) + ` data-odac-navigate="${attrName}">`
|
|
547
574
|
return tagWithAttr + content
|
|
548
575
|
})
|
|
549
576
|
|
|
577
|
+
// Auto-wrap unwrapped placeholders so AJAX navigation can target them
|
|
578
|
+
// Uses display:contents to avoid breaking flex/grid layouts
|
|
579
|
+
skeleton = skeleton.replace(/(?<!>)\s*(\{\{\s*([A-Z_]+)\s*\}\})/g, (match, placeholder, partName) => {
|
|
580
|
+
const attrName = partName.toLowerCase()
|
|
581
|
+
return `<div style="display:contents" data-odac-navigate="${attrName}">${placeholder}</div>`
|
|
582
|
+
})
|
|
583
|
+
|
|
550
584
|
const skeletonName = this.#part.skeleton || 'main'
|
|
551
585
|
const pageName = this.#odac.Request.page || ''
|
|
552
586
|
|
|
@@ -558,6 +592,17 @@ class View {
|
|
|
558
592
|
if (!attrs.includes('data-odac-page')) {
|
|
559
593
|
updates.push(`data-odac-page="${pageName}"`)
|
|
560
594
|
}
|
|
595
|
+
|
|
596
|
+
// Embed current parts manifest for client-side diffing on first load
|
|
597
|
+
const partsMap = {}
|
|
598
|
+
for (let key in this.#part) {
|
|
599
|
+
if (['all', 'skeleton'].includes(key)) continue
|
|
600
|
+
if (this.#part[key]) partsMap[key] = this.#part[key]
|
|
601
|
+
}
|
|
602
|
+
if (!attrs.includes('data-odac-parts') && Object.keys(partsMap).length > 0) {
|
|
603
|
+
updates.push(`data-odac-parts='${JSON.stringify(partsMap)}'`)
|
|
604
|
+
}
|
|
605
|
+
|
|
561
606
|
if (updates.length === 0) return match
|
|
562
607
|
return `<html${attrs} ${updates.join(' ')}>`
|
|
563
608
|
})
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* About Page Controller
|
|
3
3
|
*
|
|
4
|
-
* This controller renders the about page using
|
|
5
|
-
* Provides information about
|
|
4
|
+
* This controller renders the about page using ODAC's skeleton-based view system.
|
|
5
|
+
* Provides information about ODAC and its key components.
|
|
6
6
|
*
|
|
7
7
|
* For AJAX requests, only content is returned. For full page loads, skeleton + content.
|
|
8
8
|
*/
|
|
@@ -11,7 +11,7 @@ module.exports = function (Odac) {
|
|
|
11
11
|
// Set variables for AJAX responses
|
|
12
12
|
Odac.set(
|
|
13
13
|
{
|
|
14
|
-
pageTitle: 'About
|
|
14
|
+
pageTitle: 'About ODAC',
|
|
15
15
|
version: '1.0.0'
|
|
16
16
|
},
|
|
17
17
|
true
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Home Page Controller
|
|
3
3
|
*
|
|
4
|
-
* This controller renders the home page using
|
|
4
|
+
* This controller renders the home page using ODAC's skeleton-based view system.
|
|
5
5
|
* The skeleton provides the layout (header, nav, footer) and the view provides the content.
|
|
6
6
|
*
|
|
7
7
|
* For AJAX requests (odac-link navigation), only the content is returned.
|
|
@@ -18,7 +18,7 @@ module.exports = function (Odac) {
|
|
|
18
18
|
// Set variables that will be available in AJAX responses
|
|
19
19
|
Odac.set(
|
|
20
20
|
{
|
|
21
|
-
welcomeMessage: 'Welcome to
|
|
21
|
+
welcomeMessage: 'Welcome to ODAC!',
|
|
22
22
|
timestamp: Date.now()
|
|
23
23
|
},
|
|
24
24
|
true
|
|
@@ -1,62 +1,21 @@
|
|
|
1
|
+
/* global Odac */
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* ODAC Template - Client-Side Application
|
|
3
4
|
*
|
|
4
5
|
* This file demonstrates odac.js features including:
|
|
5
|
-
* - AJAX page loading with
|
|
6
|
+
* - AJAX page loading with Odac.loader() for smooth navigation
|
|
6
7
|
* - History API integration
|
|
7
8
|
* - Event delegation
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* AJAX Navigation
|
|
13
|
-
* Enables smooth page transitions without full page reloads
|
|
14
|
-
*
|
|
15
|
-
* Minimal usage: navigate: 'main'
|
|
16
|
-
* Medium usage: navigate: {update: 'main', on: callback}
|
|
17
|
-
* Full usage: navigate: {links: 'a[href^="/"]', update: {...}, on: callback}
|
|
18
|
-
*/
|
|
19
|
-
navigate: {
|
|
20
|
-
update: 'main' // Update <main> element
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Custom functions
|
|
25
|
-
* These become available as odac.fn.functionName()
|
|
26
|
-
*/
|
|
27
|
-
function: {
|
|
28
|
-
/**
|
|
29
|
-
* Update active navigation state
|
|
30
|
-
* Highlights the current page in the navigation menu
|
|
31
|
-
*/
|
|
32
|
-
updateActiveNav: function (url) {
|
|
33
|
-
// Remove active class from all navigation links
|
|
34
|
-
const navLinks = document.querySelectorAll('nav a')
|
|
35
|
-
navLinks.forEach(function (link) {
|
|
36
|
-
link.classList.remove('active')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// Add active class to current page link
|
|
40
|
-
const currentLink = document.querySelector(`nav a[href="${url}"]`)
|
|
41
|
-
if (currentLink) {
|
|
42
|
-
currentLink.classList.add('active')
|
|
43
|
-
} else if (url === '/' || url === '') {
|
|
44
|
-
// Handle home page
|
|
45
|
-
const homeLink = document.querySelector('nav a[href="/"]')
|
|
46
|
-
if (homeLink) {
|
|
47
|
-
homeLink.classList.add('active')
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
|
|
11
|
+
Odac.action({
|
|
53
12
|
/**
|
|
54
13
|
* Initialize application on page load
|
|
55
14
|
* This runs once when the page first loads
|
|
56
15
|
*/
|
|
57
16
|
load: function () {
|
|
58
17
|
// Set initial active navigation state
|
|
59
|
-
|
|
18
|
+
Odac.fn.updateActiveNav(window.location.pathname)
|
|
60
19
|
},
|
|
61
20
|
|
|
62
21
|
/**
|
|
@@ -76,15 +35,8 @@ odac.action({
|
|
|
76
35
|
*/
|
|
77
36
|
about: function () {
|
|
78
37
|
console.log('About page loaded')
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Docs page initialization
|
|
83
|
-
*/
|
|
84
|
-
docs: function () {
|
|
85
|
-
console.log('Docs page loaded')
|
|
86
38
|
}
|
|
87
|
-
}
|
|
39
|
+
},
|
|
88
40
|
|
|
89
41
|
// Add your custom event handlers here
|
|
90
42
|
// Example:
|
|
@@ -93,4 +45,36 @@ odac.action({
|
|
|
93
45
|
// console.log('Button clicked')
|
|
94
46
|
// }
|
|
95
47
|
// }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Custom functions
|
|
51
|
+
* These become available as Odac.fn.functionName()
|
|
52
|
+
*/
|
|
53
|
+
function: {
|
|
54
|
+
/**
|
|
55
|
+
* Update active navigation state
|
|
56
|
+
* Highlights the current page in the navigation menu
|
|
57
|
+
*/
|
|
58
|
+
updateActiveNav: function (url) {
|
|
59
|
+
// Remove active class from all navigation links
|
|
60
|
+
const navLinks = document.querySelectorAll('nav a')
|
|
61
|
+
navLinks.forEach(function (link) {
|
|
62
|
+
link.classList.remove('active')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Add active class to current page link
|
|
66
|
+
const currentLinks = document.querySelectorAll(`nav a[href="${url}"]`)
|
|
67
|
+
if (currentLinks.length > 0) {
|
|
68
|
+
currentLinks.forEach(function (link) {
|
|
69
|
+
link.classList.add('active')
|
|
70
|
+
})
|
|
71
|
+
} else if (url === '/' || url === '') {
|
|
72
|
+
// Handle home page
|
|
73
|
+
const homeLinks = document.querySelectorAll('nav a[href="/"]')
|
|
74
|
+
homeLinks.forEach(function (link) {
|
|
75
|
+
link.classList.add('active')
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
96
80
|
})
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
{{ HEAD }}
|
|
5
5
|
</head>
|
|
6
|
-
<body>
|
|
7
|
-
<header>
|
|
6
|
+
<body class="min-h-screen flex flex-col items-center">
|
|
7
|
+
<header class="sticky top-0 w-full z-50 glass h-16 flex items-center">
|
|
8
8
|
{{ HEADER }}
|
|
9
9
|
</header>
|
|
10
10
|
|
|
11
|
-
<main>
|
|
11
|
+
<main class="flex-grow w-full max-w-5xl mx-auto px-6 py-12">
|
|
12
12
|
{{ CONTENT }}
|
|
13
13
|
</main>
|
|
14
14
|
|
|
15
|
-
<footer>
|
|
15
|
+
<footer class="w-full border-t border-brand-200 py-12 mt-auto">
|
|
16
16
|
{{ FOOTER }}
|
|
17
17
|
</footer>
|
|
18
18
|
|
|
@@ -1,65 +1,69 @@
|
|
|
1
|
-
<div class="
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
<div class="space-y-24">
|
|
2
|
+
<!-- About Hero -->
|
|
3
|
+
<section class="text-center space-y-6">
|
|
4
|
+
<h1 class="text-4xl md:text-6xl font-bold tracking-tight text-brand-900 leading-tight">
|
|
5
|
+
About This <br/><span class="text-brand-900/40">Template</span>
|
|
6
|
+
</h1>
|
|
7
|
+
<p class="text-xl text-brand-900/50 max-w-2xl mx-auto font-medium leading-relaxed">
|
|
8
|
+
This is your starting point for building something incredible with ODAC. A modern, high-performance foundation.
|
|
9
|
+
</p>
|
|
10
|
+
</section>
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</section>
|
|
12
|
+
<!-- Template Features -->
|
|
13
|
+
<section class="space-y-12">
|
|
14
|
+
<div class="text-center space-y-2">
|
|
15
|
+
<h2 class="text-3xl font-bold tracking-tight text-brand-900">Template Features</h2>
|
|
16
|
+
<p class="text-brand-900/40">Everything you need, already configured.</p>
|
|
17
|
+
</div>
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</div>
|
|
38
|
-
</section>
|
|
19
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
20
|
+
<div class="p-8 bg-white rounded-[2rem] border border-brand-100 shadow-sm space-y-4">
|
|
21
|
+
<h3 class="text-xl font-bold text-brand-900">Modern UI</h3>
|
|
22
|
+
<p class="text-brand-900/60 leading-relaxed">Design with Apple-style aesthetics using Tailwind CSS. Responsive, minimal, and premium.</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="p-8 bg-white rounded-[2rem] border border-brand-100 shadow-sm space-y-4">
|
|
26
|
+
<h3 class="text-xl font-bold text-brand-900">AJAX Navigation</h3>
|
|
27
|
+
<p class="text-brand-900/60 leading-relaxed">Smooth page transitions without full reloads, already configured and working out of the box.</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="p-8 bg-white rounded-[2rem] border border-brand-100 shadow-sm space-y-4">
|
|
31
|
+
<h3 class="text-xl font-bold text-brand-900">Security</h3>
|
|
32
|
+
<p class="text-brand-900/60 leading-relaxed">CSRF protection, secure sessions, and authentication ready to use for your application.</p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="p-8 bg-white rounded-[2rem] border border-brand-100 shadow-sm space-y-4">
|
|
36
|
+
<h3 class="text-xl font-bold text-brand-900">Responsive</h3>
|
|
37
|
+
<p class="text-brand-900/60 leading-relaxed">Mobile-first design that works perfectly on all devices, from small phones to large desktop displays.</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
<!-- Next Steps -->
|
|
43
|
+
<section class="bg-brand-900 rounded-[3rem] p-12 md:p-20 text-white space-y-12">
|
|
44
|
+
<h2 class="text-3xl font-bold tracking-tight text-center">Next Steps</h2>
|
|
45
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
46
|
+
<div class="space-y-2 border-l-2 border-white/10 pl-6">
|
|
47
|
+
<h4 class="font-bold">Routes</h4>
|
|
48
|
+
<p class="text-white/40 text-sm">Define URL patterns in <code>route/www.js</code></p>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="space-y-2 border-l-2 border-white/10 pl-6">
|
|
51
|
+
<h4 class="font-bold">Controllers</h4>
|
|
52
|
+
<p class="text-white/40 text-sm">Create page logic in <code>controller/</code></p>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="space-y-2 border-l-2 border-white/10 pl-6">
|
|
55
|
+
<h4 class="font-bold">Skeleton</h4>
|
|
56
|
+
<p class="text-white/40 text-sm">Design your layout structure in <code>skeleton/</code></p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</section>
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<a href="https://odac.run" class="link-card" target="_blank" data-navigate="false">
|
|
60
|
-
<h3>🌐 odac.run</h3>
|
|
61
|
-
<p>Official website with examples and community</p>
|
|
62
|
-
</a>
|
|
63
|
-
</div>
|
|
64
|
-
</section>
|
|
61
|
+
<!-- Learn More -->
|
|
62
|
+
<section class="text-center pb-24 space-y-8">
|
|
63
|
+
<h2 class="text-3xl font-bold text-brand-900 tracking-tight">Need help?</h2>
|
|
64
|
+
<div class="flex justify-center gap-4">
|
|
65
|
+
<a href="https://docs.odac.run" target="_blank" rel="noopener" class="px-8 py-3 bg-white border border-brand-100 rounded-full font-bold hover:bg-brand-50 transition-colors" data-navigate="false">Documentation</a>
|
|
66
|
+
<a href="https://odac.run" target="_blank" rel="noopener" class="px-8 py-3 bg-brand-900 text-white rounded-full font-bold hover:opacity-90 transition-opacity" data-navigate="false">Official Site</a>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
65
69
|
</div>
|