asjs-express 1.7.0 → 1.8.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.
|
@@ -50,47 +50,86 @@
|
|
|
50
50
|
|
|
51
51
|
--asjs-progress-gradient: linear-gradient(90deg, var(--accent), #a78bfa);
|
|
52
52
|
--asjs-progress-shadow: 0 0 14px rgba(var(--accent-rgb), 0.55);
|
|
53
|
+
|
|
54
|
+
/* Bootstrap light bridge — binds BS components to our palette */
|
|
55
|
+
--bs-body-bg: #ffffff;
|
|
56
|
+
--bs-body-bg-rgb: 255,255,255;
|
|
57
|
+
--bs-body-color: #0d0d18;
|
|
58
|
+
--bs-body-color-rgb: 13,13,24;
|
|
59
|
+
--bs-secondary-color: #52526e;
|
|
60
|
+
--bs-border-color: rgba(13,13,50,0.14);
|
|
61
|
+
--bs-border-color-translucent: rgba(13,13,50,0.10);
|
|
62
|
+
--bs-card-bg: #ffffff;
|
|
63
|
+
--bs-card-border-color: rgba(13,13,50,0.10);
|
|
64
|
+
--bs-secondary-bg: #f6f7fb;
|
|
65
|
+
--bs-tertiary-bg: #eceef6;
|
|
66
|
+
--bs-primary: #7c3aed;
|
|
67
|
+
--bs-primary-rgb: 124,58,237;
|
|
68
|
+
--bs-btn-bg: #7c3aed;
|
|
69
|
+
--bs-link-color: #7c3aed;
|
|
70
|
+
--bs-link-color-rgb: 124,58,237;
|
|
71
|
+
--bs-link-hover-color: #6d28d9;
|
|
72
|
+
--bs-focus-ring-color: rgba(124,58,237,0.25);
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
/* ─── DARK TOKENS ───────────────────────────────────────────── */
|
|
56
76
|
[data-theme="dark"] {
|
|
57
|
-
--bg: #
|
|
58
|
-
--bg-2: #
|
|
59
|
-
--bg-3: #
|
|
60
|
-
--surface: #
|
|
77
|
+
--bg: #0d0d18;
|
|
78
|
+
--bg-2: #161625;
|
|
79
|
+
--bg-3: #1e1e30;
|
|
80
|
+
--surface: #131320;
|
|
61
81
|
|
|
62
|
-
--border: rgba(255, 255, 255, 0.
|
|
63
|
-
--border-2: rgba(255, 255, 255, 0.
|
|
82
|
+
--border: rgba(255, 255, 255, 0.10);
|
|
83
|
+
--border-2: rgba(255, 255, 255, 0.19);
|
|
64
84
|
|
|
65
|
-
--text: #
|
|
66
|
-
--text-2: #
|
|
67
|
-
--text-3: #
|
|
85
|
+
--text: #ededf8;
|
|
86
|
+
--text-2: #8888ac;
|
|
87
|
+
--text-3: #52527a;
|
|
68
88
|
|
|
69
89
|
--accent: #a78bfa;
|
|
70
90
|
--accent-h: #c4b5fd;
|
|
71
|
-
--accent-soft: #
|
|
91
|
+
--accent-soft: #1a1132;
|
|
72
92
|
--accent-rgb: 167, 139, 250;
|
|
73
93
|
|
|
74
94
|
--sky: #38bdf8;
|
|
75
|
-
--sky-soft: #
|
|
95
|
+
--sky-soft: #0a1f30;
|
|
76
96
|
--sky-rgb: 56, 189, 248;
|
|
77
97
|
|
|
78
98
|
--ok: #34d399;
|
|
79
|
-
--ok-bg: #
|
|
80
|
-
--ok-border: rgba(52, 211, 153, 0.
|
|
99
|
+
--ok-bg: #071a12;
|
|
100
|
+
--ok-border: rgba(52, 211, 153, 0.22);
|
|
81
101
|
--err: #f87171;
|
|
82
|
-
--err-bg: #
|
|
83
|
-
--err-border: rgba(248, 113, 113, 0.
|
|
84
|
-
--warn-bg: #
|
|
85
|
-
--warn-border:rgba(251, 191, 36, 0.
|
|
102
|
+
--err-bg: #1a0707;
|
|
103
|
+
--err-border: rgba(248, 113, 113, 0.22);
|
|
104
|
+
--warn-bg: #1a1300;
|
|
105
|
+
--warn-border:rgba(251, 191, 36, 0.22);
|
|
86
106
|
|
|
87
|
-
--sh-xs: 0 1px
|
|
88
|
-
--sh-sm: 0 2px
|
|
89
|
-
--sh-md: 0 8px
|
|
90
|
-
--sh-accent: 0 6px 24px rgba(var(--accent-rgb), 0.
|
|
107
|
+
--sh-xs: 0 1px 3px rgba(0,0,0,0.65);
|
|
108
|
+
--sh-sm: 0 2px 10px rgba(0,0,0,0.75), 0 1px 3px rgba(0,0,0,0.5);
|
|
109
|
+
--sh-md: 0 8px 36px rgba(0,0,0,0.85), 0 2px 8px rgba(0,0,0,0.55);
|
|
110
|
+
--sh-accent: 0 6px 24px rgba(var(--accent-rgb), 0.22);
|
|
91
111
|
|
|
92
112
|
--asjs-progress-gradient: linear-gradient(90deg, var(--accent), #c4b5fd);
|
|
93
113
|
--asjs-progress-shadow: 0 0 14px rgba(var(--accent-rgb), 0.5);
|
|
114
|
+
|
|
115
|
+
/* Bootstrap dark bridge */
|
|
116
|
+
--bs-body-bg: #0d0d18;
|
|
117
|
+
--bs-body-bg-rgb: 13,13,24;
|
|
118
|
+
--bs-body-color: #ededf8;
|
|
119
|
+
--bs-body-color-rgb: 237,237,248;
|
|
120
|
+
--bs-secondary-color: #8888ac;
|
|
121
|
+
--bs-border-color: rgba(255,255,255,0.13);
|
|
122
|
+
--bs-border-color-translucent: rgba(255,255,255,0.09);
|
|
123
|
+
--bs-card-bg: #131320;
|
|
124
|
+
--bs-card-border-color: rgba(255,255,255,0.09);
|
|
125
|
+
--bs-secondary-bg: #1e1e30;
|
|
126
|
+
--bs-tertiary-bg: #26263c;
|
|
127
|
+
--bs-primary: #a78bfa;
|
|
128
|
+
--bs-primary-rgb: 167,139,250;
|
|
129
|
+
--bs-link-color: #a78bfa;
|
|
130
|
+
--bs-link-color-rgb: 167,139,250;
|
|
131
|
+
--bs-link-hover-color: #c4b5fd;
|
|
132
|
+
--bs-focus-ring-color: rgba(167,139,250,0.25);
|
|
94
133
|
}
|
|
95
134
|
|
|
96
135
|
/* ─── BASE ──────────────────────────────────────────────────── */
|
|
@@ -1000,6 +1039,49 @@ code {
|
|
|
1000
1039
|
100% { background-position: -200% 0; }
|
|
1001
1040
|
}
|
|
1002
1041
|
|
|
1042
|
+
/* ─── DARK COMPONENT OVERRIDES ──────────────────────────────── */
|
|
1043
|
+
[data-theme="dark"] .brand-mark {
|
|
1044
|
+
background: linear-gradient(135deg, var(--accent) 0%, var(--sky) 100%);
|
|
1045
|
+
}
|
|
1046
|
+
[data-theme="dark"] .section-card,
|
|
1047
|
+
[data-theme="dark"] .hero-copy,
|
|
1048
|
+
[data-theme="dark"] .hero-aside {
|
|
1049
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.05) inset,
|
|
1050
|
+
0 4px 28px rgba(0,0,0,0.75);
|
|
1051
|
+
}
|
|
1052
|
+
[data-theme="dark"] .render-summary-grid,
|
|
1053
|
+
[data-theme="dark"] .card-grid,
|
|
1054
|
+
[data-theme="dark"] .stats-grid,
|
|
1055
|
+
[data-theme="dark"] .metric-grid {
|
|
1056
|
+
background: rgba(255,255,255,0.06);
|
|
1057
|
+
}
|
|
1058
|
+
[data-theme="dark"] .info-card,
|
|
1059
|
+
[data-theme="dark"] .stat-card,
|
|
1060
|
+
[data-theme="dark"] .metric-panel,
|
|
1061
|
+
[data-theme="dark"] .render-item {
|
|
1062
|
+
background: #161625;
|
|
1063
|
+
}
|
|
1064
|
+
[data-theme="dark"] .info-card:hover,
|
|
1065
|
+
[data-theme="dark"] .render-item:hover { background: #1e1e30; }
|
|
1066
|
+
[data-theme="dark"] .check-list li {
|
|
1067
|
+
background: #1e1e30;
|
|
1068
|
+
border-color: rgba(255,255,255,0.12);
|
|
1069
|
+
}
|
|
1070
|
+
[data-theme="dark"] .button-secondary {
|
|
1071
|
+
background: #1e1e30;
|
|
1072
|
+
border-color: rgba(255,255,255,0.16);
|
|
1073
|
+
}
|
|
1074
|
+
[data-theme="dark"] .button-secondary:hover {
|
|
1075
|
+
background: #26263c;
|
|
1076
|
+
border-color: var(--accent);
|
|
1077
|
+
color: var(--accent);
|
|
1078
|
+
}
|
|
1079
|
+
[data-theme="dark"] .route-loader-card { background: rgba(19,19,34,0.97); }
|
|
1080
|
+
[data-theme="dark"] .theme-toggle { background: #1e1e30; }
|
|
1081
|
+
[data-theme="dark"] .section-label,
|
|
1082
|
+
[data-theme="dark"] .hero-badge { background: #1a1132; }
|
|
1083
|
+
[data-theme="dark"] .render-band { background: #161625; }
|
|
1084
|
+
|
|
1003
1085
|
/* ─── TABLET (≤ 1024px) ──────────────────────────────────────── */
|
|
1004
1086
|
@media (max-width: 1024px) {
|
|
1005
1087
|
.header-main { grid-template-columns: auto 1fr; }
|
|
@@ -1043,4 +1125,4 @@ code {
|
|
|
1043
1125
|
.theme-toggle { bottom: 16px; left: 16px; width: 38px; height: 38px; }
|
|
1044
1126
|
|
|
1045
1127
|
.footer { flex-direction: column; align-items: flex-start; }
|
|
1046
|
-
}
|
|
1128
|
+
}
|
package/package.json
CHANGED
|
@@ -92,18 +92,25 @@ app.post('/form', function(req, res) {
|
|
|
92
92
|
if (!subject) errors.subject = 'Please select a subject.';
|
|
93
93
|
if (message.length < 10) errors.message = 'Message must be at least 10 characters.';
|
|
94
94
|
|
|
95
|
+
var formData = {
|
|
96
|
+
title: 'Contact — __APP_TITLE__',
|
|
97
|
+
headline: 'Get in touch',
|
|
98
|
+
description: 'Have a question or want to work together? Send us a message.',
|
|
99
|
+
values: body,
|
|
100
|
+
errors: errors,
|
|
101
|
+
submitted: false
|
|
102
|
+
};
|
|
103
|
+
|
|
95
104
|
if (Object.keys(errors).length > 0) {
|
|
96
|
-
return asjs.render(res, 'form',
|
|
97
|
-
title: 'Contact — __APP_TITLE__',
|
|
98
|
-
headline: 'Get in touch',
|
|
99
|
-
description: 'Have a question or want to work together? Send us a message.',
|
|
100
|
-
submitted: false,
|
|
101
|
-
values: body,
|
|
102
|
-
errors: errors
|
|
103
|
-
});
|
|
105
|
+
return asjs.render(res, 'form', formData);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
return asjs.render(res, 'form', Object.assign({}, formData, {
|
|
109
|
+
headline: 'Message sent!',
|
|
110
|
+
submitted: true,
|
|
111
|
+
values: {},
|
|
112
|
+
errors: {}
|
|
113
|
+
}));
|
|
107
114
|
});
|
|
108
115
|
|
|
109
116
|
app.use(asjs.errors());
|
|
@@ -1,76 +1,111 @@
|
|
|
1
|
-
<div class="page-intro">
|
|
2
|
-
<span class="section-label">Contact</span>
|
|
3
|
-
<h1><%= headline %></h1>
|
|
4
|
-
<p><%= description %></p>
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
<% if (submitted) { %>
|
|
8
|
-
|
|
9
|
-
<div class="
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<div class="
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<div
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
1
|
+
<div class="page-intro">
|
|
2
|
+
<span class="section-label">Contact</span>
|
|
3
|
+
<h1><%= headline %></h1>
|
|
4
|
+
<p><%= description %></p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<% if (submitted) { %>
|
|
8
|
+
|
|
9
|
+
<div class="card shadow-sm" style="border-radius:var(--r-xl);border-color:var(--ok-border);background:var(--ok-bg);">
|
|
10
|
+
<div class="card-body p-4 p-md-5">
|
|
11
|
+
<div class="d-flex align-items-start gap-4">
|
|
12
|
+
<div style="width:52px;height:52px;flex-shrink:0;border-radius:50%;background:rgba(52,211,153,0.15);border:1px solid var(--ok-border);display:flex;align-items:center;justify-content:center;">
|
|
13
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--ok)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<h5 class="fw-bold mb-1" style="color:var(--ok);">Message received!</h5>
|
|
17
|
+
<p class="mb-3" style="color:var(--text-2);font-size:0.92rem;">Thanks for reaching out. We typically respond within one business day.</p>
|
|
18
|
+
<a href="/form" class="button button-primary" data-asjs-transition="fade">Send another message</a>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<% } else { %>
|
|
25
|
+
|
|
26
|
+
<% if (Object.keys(errors).length > 0) { %>
|
|
27
|
+
<div class="alert alert-danger d-flex align-items-start gap-3 mb-3" role="alert" style="border-radius:var(--r-lg);">
|
|
28
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:2px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
29
|
+
<div>
|
|
30
|
+
<strong>Please fix the errors below</strong>
|
|
31
|
+
<div class="mt-1" style="font-size:0.85rem;">Some fields need attention before the form can be submitted.</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<% } %>
|
|
35
|
+
|
|
36
|
+
<div class="card shadow-sm" style="border-radius:var(--r-xl);">
|
|
37
|
+
<div class="card-body p-4 p-lg-5">
|
|
38
|
+
|
|
39
|
+
<div class="d-flex align-items-center gap-3 mb-4 pb-3" style="border-bottom:1px solid var(--border);">
|
|
40
|
+
<div style="width:40px;height:40px;border-radius:var(--r-md);background:var(--accent-soft);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
|
41
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<h5 class="mb-0 fw-bold" style="letter-spacing:-0.02em;color:var(--text);">Send us a message</h5>
|
|
45
|
+
<p class="mb-0" style="font-size:0.8rem;color:var(--text-3);">We respond to every message within 24 hours</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<form action="/form" method="POST" data-asjs-form>
|
|
50
|
+
<div class="row g-4">
|
|
51
|
+
|
|
52
|
+
<div class="col-md-6">
|
|
53
|
+
<label class="form-label fw-semibold" for="f-name" style="font-size:0.84rem;color:var(--text-2);">Full name</label>
|
|
54
|
+
<input type="text" id="f-name" name="name"
|
|
55
|
+
class="form-control form-control-lg<% if(errors.name){ %> is-invalid<% } %>"
|
|
56
|
+
value="<%= values.name||'' %>"
|
|
57
|
+
placeholder="Jane Smith"
|
|
58
|
+
autocomplete="name" required>
|
|
59
|
+
<% if(errors.name){ %><div class="invalid-feedback fw-medium"><%= errors.name %></div><% } %>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="col-md-6">
|
|
63
|
+
<label class="form-label fw-semibold" for="f-email" style="font-size:0.84rem;color:var(--text-2);">Email address</label>
|
|
64
|
+
<input type="email" id="f-email" name="email"
|
|
65
|
+
class="form-control form-control-lg<% if(errors.email){ %> is-invalid<% } %>"
|
|
66
|
+
value="<%= values.email||'' %>"
|
|
67
|
+
placeholder="jane@example.com"
|
|
68
|
+
autocomplete="email" required>
|
|
69
|
+
<% if(errors.email){ %><div class="invalid-feedback fw-medium"><%= errors.email %></div><% } %>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="col-12">
|
|
73
|
+
<label class="form-label fw-semibold" for="f-subject" style="font-size:0.84rem;color:var(--text-2);">Subject</label>
|
|
74
|
+
<select id="f-subject" name="subject"
|
|
75
|
+
class="form-select form-select-lg<% if(errors.subject){ %> is-invalid<% } %>">
|
|
76
|
+
<option value="">Select a topic…</option>
|
|
77
|
+
<option value="general" <%= values.subject==='general' ? 'selected' : '' %>>General enquiry</option>
|
|
78
|
+
<option value="project" <%= values.subject==='project' ? 'selected' : '' %>>Project collaboration</option>
|
|
79
|
+
<option value="support" <%= values.subject==='support' ? 'selected' : '' %>>Technical support</option>
|
|
80
|
+
<option value="other" <%= values.subject==='other' ? 'selected' : '' %>>Other</option>
|
|
81
|
+
</select>
|
|
82
|
+
<% if(errors.subject){ %><div class="invalid-feedback fw-medium"><%= errors.subject %></div><% } %>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="col-12">
|
|
86
|
+
<label class="form-label fw-semibold" for="f-message" style="font-size:0.84rem;color:var(--text-2);">Message</label>
|
|
87
|
+
<textarea id="f-message" name="message" rows="6"
|
|
88
|
+
class="form-control form-control-lg<% if(errors.message){ %> is-invalid<% } %>"
|
|
89
|
+
placeholder="Tell us what's on your mind…" required><%= values.message||'' %></textarea>
|
|
90
|
+
<% if(errors.message){ %><div class="invalid-feedback fw-medium"><%= errors.message %></div><% } %>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="col-12">
|
|
94
|
+
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap pt-1" style="border-top:1px solid var(--border);padding-top:20px!important;">
|
|
95
|
+
<div style="font-size:0.82rem;color:var(--text-3);display:flex;align-items:center;gap:7px;">
|
|
96
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
97
|
+
Your data is never shared with third parties.
|
|
98
|
+
</div>
|
|
99
|
+
<button type="submit" class="button button-primary" style="height:44px;padding:0 24px;font-size:0.9rem;">
|
|
100
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2L11 13M22 2L15 22l-4-9-9-4 20-7z"/></svg>
|
|
101
|
+
Send message
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
</div>
|
|
107
|
+
</form>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
76
111
|
<% } %>
|
|
@@ -1,48 +1,51 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" data-theme="light">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title><%= title %></title>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<span
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="light" data-bs-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= title %></title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
|
|
8
|
+
<%- asjs.clientTags({ preload: true, theme: true }) %>
|
|
9
|
+
<script>(function(){var t=localStorage.getItem('asjs-theme')||(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);document.documentElement.setAttribute('data-bs-theme',t);})();</script>
|
|
10
|
+
</head>
|
|
11
|
+
<body<%- asjs.bodyAttrs() %>>
|
|
12
|
+
<%- asjs.progressMarkup() %>
|
|
13
|
+
<div class="page-shell">
|
|
14
|
+
<%- asjs.header() %>
|
|
15
|
+
<main class="view-frame"<%- asjs.viewAttrs() %>>
|
|
16
|
+
<%- body %>
|
|
17
|
+
</main>
|
|
18
|
+
<footer class="footer">
|
|
19
|
+
<span>Built with <strong>ASJS __ASJS_VERSION__</strong> + Express</span>
|
|
20
|
+
<span>© __YEAR__ __APP_TITLE__</span>
|
|
21
|
+
</footer>
|
|
22
|
+
</div>
|
|
23
|
+
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme" title="Toggle light / dark">
|
|
24
|
+
<span class="toggle-icon toggle-icon--light" style="display:none">
|
|
25
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
|
26
|
+
</span>
|
|
27
|
+
<span class="toggle-icon toggle-icon--dark" style="display:none">
|
|
28
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
29
|
+
</span>
|
|
30
|
+
</button>
|
|
31
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
|
32
|
+
<script>
|
|
33
|
+
(function(){
|
|
34
|
+
var btn = document.getElementById('theme-toggle');
|
|
35
|
+
if (!btn) return;
|
|
36
|
+
function sync(t) {
|
|
37
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
38
|
+
document.documentElement.setAttribute('data-bs-theme', t);
|
|
39
|
+
btn.querySelector('.toggle-icon--light').style.display = t === 'dark' ? 'flex' : 'none';
|
|
40
|
+
btn.querySelector('.toggle-icon--dark').style.display = t === 'light' ? 'flex' : 'none';
|
|
41
|
+
}
|
|
42
|
+
sync(document.documentElement.getAttribute('data-theme') || 'light');
|
|
43
|
+
btn.addEventListener('click', function() {
|
|
44
|
+
var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
45
|
+
localStorage.setItem('asjs-theme', next);
|
|
46
|
+
sync(next);
|
|
47
|
+
});
|
|
48
|
+
})();
|
|
49
|
+
</script>
|
|
50
|
+
</body>
|
|
48
51
|
</html>
|