umpordez 1.2.1 → 1.3.2
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/.claude/settings.local.json +4 -1
- package/README.md +28 -1
- package/cli.mjs +4 -2
- package/create.mjs +86 -7
- package/package.json +1 -1
- package/template/CLAUDE.md +1 -1
- package/template/gitignore +13 -0
- package/template/misc/build-local.sh +82 -0
- package/template/mobile/{{PROJECT_SLUG}}/App.tsx +5 -2
- package/template/mobile/{{PROJECT_SLUG}}/CHANGELOG.md +28 -0
- package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +10 -1
- package/template/mobile/{{PROJECT_SLUG}}/docs/releases.md +78 -0
- package/template/mobile/{{PROJECT_SLUG}}/docs/screenshots.md +84 -0
- package/template/mobile/{{PROJECT_SLUG}}/docs/store-copy.md +122 -0
- package/template/mobile/{{PROJECT_SLUG}}/docs/store-listing.md +107 -0
- package/template/mobile/{{PROJECT_SLUG}}/gitignore +29 -0
- package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +6 -0
- package/template/scripts/README.md +18 -1
- package/template/scripts/appstore-resize.sh +70 -0
- package/template/scripts/frame-shots.mjs +246 -0
- package/template/scripts/gen-mockups.mjs +120 -0
- package/template/scripts/screenshots-capture.sh +50 -0
- package/template/scripts/screenshots.flow.yaml +54 -0
- package/template/ui/site/_variants/app/en.ts +131 -0
- package/template/ui/site/_variants/app/footer.ejs +46 -0
- package/template/ui/site/_variants/app/header.ejs +102 -0
- package/template/ui/site/_variants/app/home.ejs +237 -0
- package/template/ui/site/_variants/app/not-found.ejs +22 -0
- package/template/ui/site/_variants/app/pt-BR.ts +203 -0
- package/template/ui/site/tailwind.config.js +9 -1
- package/template-variables.mjs +34 -0
- package/update.mjs +37 -2
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<%- include('../partials/header') %>
|
|
2
|
+
|
|
3
|
+
<main>
|
|
4
|
+
<!-- Hero: the promise + a live-feeling app mockup (the signature) -->
|
|
5
|
+
<section class="relative overflow-hidden">
|
|
6
|
+
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
|
|
7
|
+
<div class="absolute -top-40 left-1/2 -translate-x-1/2 w-[46rem] h-[46rem] bg-primary/15 rounded-full blur-3xl"></div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24 lg:pt-28 lg:pb-32">
|
|
11
|
+
<div class="grid lg:grid-cols-2 gap-16 items-center">
|
|
12
|
+
<div>
|
|
13
|
+
<p class="inline-flex items-center gap-2 text-primary text-sm font-semibold tracking-wide uppercase mb-6">
|
|
14
|
+
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span>
|
|
15
|
+
<%= t('hero.eyebrow') %>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<h1 class="font-black tracking-tight text-5xl sm:text-6xl leading-[1.05] mb-6">
|
|
19
|
+
<span class="block text-zinc-100"><%= t('hero.title1') %></span>
|
|
20
|
+
<span class="block text-primary"><%= t('hero.title2') %></span>
|
|
21
|
+
</h1>
|
|
22
|
+
|
|
23
|
+
<p class="text-lg text-zinc-400 leading-relaxed max-w-xl mb-10">
|
|
24
|
+
<%= t('hero.subtitle') %>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
<%# Store buttons — point these at your real listings once %>
|
|
28
|
+
<%# the app is live (App Store / Play Store URLs). %>
|
|
29
|
+
<div class="flex flex-col sm:flex-row gap-3 mb-6">
|
|
30
|
+
<a href="#" class="inline-flex items-center justify-center gap-2.5 bg-zinc-100 text-zinc-900 px-7 py-3.5 rounded-2xl hover:bg-white transition-colors font-semibold text-sm">
|
|
31
|
+
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
32
|
+
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
|
|
33
|
+
</svg>
|
|
34
|
+
<%= t('hero.ctaIos') %>
|
|
35
|
+
</a>
|
|
36
|
+
<a href="#" class="inline-flex items-center justify-center gap-2.5 border border-zinc-700 text-zinc-100 px-7 py-3.5 rounded-2xl hover:border-zinc-500 transition-colors font-semibold text-sm">
|
|
37
|
+
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
38
|
+
<path d="M3.61 1.81c-.36.38-.57.97-.57 1.73v16.92c0 .76.21 1.35.57 1.73l.09.08L13.18 12.8v-.22L3.7 1.72l-.09.09zm12.75 7.62l-3.18 3.15v.22l3.18 3.16.07-.04 3.77-2.12c1.08-.6 1.08-1.6 0-2.21l-3.77-2.12-.07-.04zm-.07 6.53L13.18 12.7 3.61 22.19c.36.36.94.4 1.6.03l11.08-6.26zM3.61 1.81l9.57 9.48 3.11-3.09L5.21 1.94c-.66-.37-1.24-.32-1.6.03v-.16z"/>
|
|
39
|
+
</svg>
|
|
40
|
+
<%= t('hero.ctaAndroid') %>
|
|
41
|
+
</a>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<p class="text-sm text-zinc-500"><%= t('hero.noSub') %></p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Signature: a generic-but-real app screen in a phone frame -->
|
|
48
|
+
<div class="relative mx-auto w-full max-w-[19rem]" aria-hidden="true">
|
|
49
|
+
<div class="rounded-[2.5rem] border border-zinc-800 bg-zinc-900 p-3 shadow-2xl shadow-black/50">
|
|
50
|
+
<div class="rounded-[2rem] bg-zinc-950 overflow-hidden">
|
|
51
|
+
<div class="px-5 pt-6 pb-5">
|
|
52
|
+
<div class="flex items-center justify-between mb-6">
|
|
53
|
+
<div>
|
|
54
|
+
<div class="h-2 w-16 rounded bg-zinc-700 mb-2"></div>
|
|
55
|
+
<div class="h-3.5 w-28 rounded bg-zinc-500"></div>
|
|
56
|
+
</div>
|
|
57
|
+
<span class="w-9 h-9 rounded-full bg-primary/20 border border-primary/40"></span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="rounded-2xl bg-primary p-5 mb-5">
|
|
61
|
+
<div class="h-2.5 w-20 rounded bg-zinc-950/40 mb-3"></div>
|
|
62
|
+
<div class="h-6 w-32 rounded bg-zinc-950/60"></div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="space-y-3">
|
|
66
|
+
<% for (var r = 0; r < 3; r++) { %>
|
|
67
|
+
<div class="flex items-center gap-3 rounded-xl border border-zinc-800 bg-zinc-900/60 p-3">
|
|
68
|
+
<span class="w-9 h-9 rounded-lg bg-primary/15"></span>
|
|
69
|
+
<div class="flex-1">
|
|
70
|
+
<div class="h-2.5 w-24 rounded bg-zinc-600 mb-2"></div>
|
|
71
|
+
<div class="h-2 w-16 rounded bg-zinc-800"></div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<% } %>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="flex items-center justify-around border-t border-zinc-800 px-4 py-3">
|
|
79
|
+
<span class="w-6 h-6 rounded-md bg-primary"></span>
|
|
80
|
+
<span class="w-6 h-6 rounded-md bg-zinc-800"></span>
|
|
81
|
+
<span class="w-6 h-6 rounded-md bg-zinc-800"></span>
|
|
82
|
+
<span class="w-6 h-6 rounded-md bg-zinc-800"></span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<!-- Positioning: what it's NOT, then what it IS -->
|
|
92
|
+
<section class="border-y border-zinc-800 bg-zinc-900/40">
|
|
93
|
+
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-20 grid lg:grid-cols-2 gap-12 items-center">
|
|
94
|
+
<div>
|
|
95
|
+
<h2 class="font-bold text-2xl text-zinc-500 mb-6"><%= t('notIs.title') %></h2>
|
|
96
|
+
<ul class="space-y-3">
|
|
97
|
+
<% t('notIs.items', { returnObjects: true }).forEach(function (item) { %>
|
|
98
|
+
<li class="flex items-center gap-3 text-zinc-500">
|
|
99
|
+
<svg class="w-4 h-4 text-zinc-600 shrink-0" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24">
|
|
100
|
+
<path stroke-linecap="round" d="M6 18L18 6M6 6l12 12"/>
|
|
101
|
+
</svg>
|
|
102
|
+
<span class="line-through decoration-zinc-700"><%= item %></span>
|
|
103
|
+
</li>
|
|
104
|
+
<% }); %>
|
|
105
|
+
</ul>
|
|
106
|
+
</div>
|
|
107
|
+
<div>
|
|
108
|
+
<h2 class="font-black text-4xl text-primary mb-4"><%= t('notIs.isTitle') %></h2>
|
|
109
|
+
<p class="text-zinc-400 text-lg leading-relaxed"><%= t('notIs.isBody') %></p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<!-- How it works: three steps -->
|
|
115
|
+
<section id="how-it-works" class="py-20 lg:py-28">
|
|
116
|
+
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
117
|
+
<div class="max-w-2xl mb-16">
|
|
118
|
+
<p class="text-primary text-sm font-semibold tracking-wide uppercase mb-3"><%= t('flow.eyebrow') %></p>
|
|
119
|
+
<h2 class="font-bold text-3xl md:text-4xl text-zinc-100 mb-4"><%= t('flow.title') %></h2>
|
|
120
|
+
<p class="text-lg text-zinc-400"><%= t('flow.sub') %></p>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="grid md:grid-cols-3 gap-6">
|
|
124
|
+
<% t('flow.steps', { returnObjects: true }).forEach(function (step, i) { %>
|
|
125
|
+
<div class="rounded-3xl border border-zinc-800 bg-zinc-900/40 p-8">
|
|
126
|
+
<div class="w-11 h-11 rounded-2xl bg-primary/15 flex items-center justify-center mb-6 font-bold text-primary">
|
|
127
|
+
<%= i + 1 %>
|
|
128
|
+
</div>
|
|
129
|
+
<h3 class="font-bold text-xl text-zinc-100 mb-3"><%= step.title %></h3>
|
|
130
|
+
<p class="text-zinc-400 leading-relaxed text-[15px]"><%= step.body %></p>
|
|
131
|
+
</div>
|
|
132
|
+
<% }); %>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</section>
|
|
136
|
+
|
|
137
|
+
<!-- Features -->
|
|
138
|
+
<section id="features" class="py-20 lg:py-28 border-t border-zinc-800 bg-zinc-900/40">
|
|
139
|
+
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
140
|
+
<div class="max-w-2xl mb-16">
|
|
141
|
+
<p class="text-primary text-sm font-semibold tracking-wide uppercase mb-3"><%= t('features.eyebrow') %></p>
|
|
142
|
+
<h2 class="font-bold text-3xl md:text-4xl text-zinc-100 mb-4"><%= t('features.title') %></h2>
|
|
143
|
+
<p class="text-lg text-zinc-400"><%= t('features.sub') %></p>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<%# Icon paths — order matches features.items. Kept out of i18n %>
|
|
147
|
+
<%# because they're markup, not copy. %>
|
|
148
|
+
<% const featureIcons = [
|
|
149
|
+
'M13 10V3L4 14h7v7l9-11h-7z',
|
|
150
|
+
'M5 13l4 4L19 7',
|
|
151
|
+
'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
|
|
152
|
+
'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
153
|
+
'M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M12 4v12m0 0l-4-4m4 4l4-4',
|
|
154
|
+
'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'
|
|
155
|
+
]; %>
|
|
156
|
+
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
157
|
+
<% t('features.items', { returnObjects: true }).forEach(function (item, i) {
|
|
158
|
+
const path = featureIcons[i] || featureIcons[0]; %>
|
|
159
|
+
<div class="rounded-3xl border border-zinc-800 bg-zinc-950 p-8 hover:border-zinc-700 transition-colors">
|
|
160
|
+
<div class="w-11 h-11 rounded-xl bg-primary/15 flex items-center justify-center mb-5">
|
|
161
|
+
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24">
|
|
162
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="<%= path %>"/>
|
|
163
|
+
</svg>
|
|
164
|
+
</div>
|
|
165
|
+
<h3 class="font-bold text-lg text-zinc-100 mb-2.5"><%= item.title %></h3>
|
|
166
|
+
<p class="text-zinc-400 leading-relaxed text-[15px]"><%= item.body %></p>
|
|
167
|
+
</div>
|
|
168
|
+
<% }); %>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</section>
|
|
172
|
+
|
|
173
|
+
<!-- Pricing: one price, everything included -->
|
|
174
|
+
<section id="pricing" class="py-20 lg:py-28">
|
|
175
|
+
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
176
|
+
<p class="text-primary text-sm font-semibold tracking-wide uppercase mb-3"><%= t('pricing.eyebrow') %></p>
|
|
177
|
+
<h2 class="font-bold text-3xl md:text-4xl text-zinc-100 mb-4"><%= t('pricing.title') %></h2>
|
|
178
|
+
<p class="text-lg text-zinc-400 mb-12"><%= t('pricing.sub') %></p>
|
|
179
|
+
|
|
180
|
+
<div class="rounded-[2rem] border border-primary/25 bg-zinc-900/50 p-10">
|
|
181
|
+
<div class="flex items-baseline justify-center gap-3 mb-2">
|
|
182
|
+
<span class="font-black text-5xl text-zinc-100"><%= t('pricing.price') %></span>
|
|
183
|
+
</div>
|
|
184
|
+
<p class="text-sm text-zinc-500 mb-8"><%= t('pricing.priceNote') %></p>
|
|
185
|
+
|
|
186
|
+
<ul class="max-w-sm mx-auto space-y-3 text-left mb-10">
|
|
187
|
+
<% t('pricing.included', { returnObjects: true }).forEach(function (item) { %>
|
|
188
|
+
<li class="flex items-center gap-3 text-zinc-300">
|
|
189
|
+
<svg class="w-5 h-5 text-primary shrink-0" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24">
|
|
190
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
|
191
|
+
</svg>
|
|
192
|
+
<span><%= item %></span>
|
|
193
|
+
</li>
|
|
194
|
+
<% }); %>
|
|
195
|
+
</ul>
|
|
196
|
+
|
|
197
|
+
<a href="<%= urlForLocale(locale) %>#download"
|
|
198
|
+
class="inline-flex items-center justify-center gap-2 bg-zinc-100 text-zinc-900 px-10 py-4 rounded-2xl hover:bg-white transition-colors font-semibold">
|
|
199
|
+
<%= t('pricing.cta') %>
|
|
200
|
+
</a>
|
|
201
|
+
<p class="text-xs text-zinc-600 mt-6"><%= t('pricing.fine') %></p>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</section>
|
|
205
|
+
|
|
206
|
+
<!-- Final CTA / download anchor -->
|
|
207
|
+
<section id="download" class="border-t border-zinc-800 bg-zinc-900/40">
|
|
208
|
+
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-20 text-center">
|
|
209
|
+
<h2 class="font-bold text-3xl md:text-4xl text-zinc-100 mb-4"><%= t('cta.title') %></h2>
|
|
210
|
+
<p class="text-lg text-zinc-400 mb-10"><%= t('cta.body') %></p>
|
|
211
|
+
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
|
212
|
+
<a href="#" class="inline-flex items-center justify-center gap-2.5 bg-zinc-100 text-zinc-900 px-7 py-3.5 rounded-2xl hover:bg-white transition-colors font-semibold text-sm">
|
|
213
|
+
<%= t('hero.ctaIos') %>
|
|
214
|
+
</a>
|
|
215
|
+
<a href="#" class="inline-flex items-center justify-center gap-2.5 border border-zinc-700 text-zinc-100 px-7 py-3.5 rounded-2xl hover:border-zinc-500 transition-colors font-semibold text-sm">
|
|
216
|
+
<%= t('hero.ctaAndroid') %>
|
|
217
|
+
</a>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</section>
|
|
221
|
+
</main>
|
|
222
|
+
|
|
223
|
+
<%- include('../partials/footer') %>
|
|
224
|
+
|
|
225
|
+
<script>
|
|
226
|
+
document.querySelectorAll('a[href*="#"]').forEach(anchor => {
|
|
227
|
+
anchor.addEventListener('click', function (e) {
|
|
228
|
+
const hash = this.getAttribute('href').split('#')[1];
|
|
229
|
+
if (!hash) { return; }
|
|
230
|
+
const target = document.getElementById(hash);
|
|
231
|
+
if (target) {
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<%- include('../partials/header') %>
|
|
2
|
+
|
|
3
|
+
<main class="min-h-[60vh] flex items-center justify-center px-4">
|
|
4
|
+
<div class="text-center max-w-md">
|
|
5
|
+
<p class="text-sm font-bold text-primary tracking-widest mb-4">404</p>
|
|
6
|
+
<h1 class="text-3xl md:text-4xl font-bold text-zinc-100 mb-4">
|
|
7
|
+
<%= t('notFound.title') %>
|
|
8
|
+
</h1>
|
|
9
|
+
<p class="text-zinc-400 mb-8">
|
|
10
|
+
<%= t('notFound.body') %>
|
|
11
|
+
</p>
|
|
12
|
+
<a href="<%= urlForLocale(locale) %>"
|
|
13
|
+
class="inline-flex items-center gap-2 bg-zinc-100 text-zinc-900 px-6 py-3 rounded-xl hover:bg-white transition-colors text-sm font-semibold">
|
|
14
|
+
<%= t('notFound.cta') %>
|
|
15
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
|
17
|
+
</svg>
|
|
18
|
+
</a>
|
|
19
|
+
</div>
|
|
20
|
+
</main>
|
|
21
|
+
|
|
22
|
+
<%- include('../partials/footer') %>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Source-of-truth shape for the app-marketing landing — EN satisfies
|
|
2
|
+
// the same TranslationShape. This is the app-store flavor (download +
|
|
3
|
+
// pricing), swapped in for mobile presets. Edit the copy freely; the
|
|
4
|
+
// shape is what TypeScript enforces across locales.
|
|
5
|
+
|
|
6
|
+
export type TranslationShape = {
|
|
7
|
+
meta: {
|
|
8
|
+
siteTitle: string;
|
|
9
|
+
siteDescription: string;
|
|
10
|
+
};
|
|
11
|
+
nav: {
|
|
12
|
+
features: string;
|
|
13
|
+
howItWorks: string;
|
|
14
|
+
pricing: string;
|
|
15
|
+
download: string;
|
|
16
|
+
languageToggle: string;
|
|
17
|
+
};
|
|
18
|
+
hero: {
|
|
19
|
+
eyebrow: string;
|
|
20
|
+
title1: string;
|
|
21
|
+
title2: string;
|
|
22
|
+
subtitle: string;
|
|
23
|
+
ctaIos: string;
|
|
24
|
+
ctaAndroid: string;
|
|
25
|
+
noSub: string;
|
|
26
|
+
};
|
|
27
|
+
notIs: {
|
|
28
|
+
title: string;
|
|
29
|
+
items: string[];
|
|
30
|
+
isTitle: string;
|
|
31
|
+
isBody: string;
|
|
32
|
+
};
|
|
33
|
+
flow: {
|
|
34
|
+
eyebrow: string;
|
|
35
|
+
title: string;
|
|
36
|
+
sub: string;
|
|
37
|
+
steps: Array<{ title: string; body: string }>;
|
|
38
|
+
};
|
|
39
|
+
features: {
|
|
40
|
+
eyebrow: string;
|
|
41
|
+
title: string;
|
|
42
|
+
sub: string;
|
|
43
|
+
items: Array<{ title: string; body: string }>;
|
|
44
|
+
};
|
|
45
|
+
pricing: {
|
|
46
|
+
eyebrow: string;
|
|
47
|
+
title: string;
|
|
48
|
+
sub: string;
|
|
49
|
+
price: string;
|
|
50
|
+
priceNote: string;
|
|
51
|
+
included: string[];
|
|
52
|
+
cta: string;
|
|
53
|
+
fine: string;
|
|
54
|
+
};
|
|
55
|
+
cta: {
|
|
56
|
+
title: string;
|
|
57
|
+
body: string;
|
|
58
|
+
};
|
|
59
|
+
footer: {
|
|
60
|
+
tagline: string;
|
|
61
|
+
product: string;
|
|
62
|
+
support: string;
|
|
63
|
+
contact: string;
|
|
64
|
+
rights: string;
|
|
65
|
+
terms: string;
|
|
66
|
+
privacy: string;
|
|
67
|
+
};
|
|
68
|
+
notFound: {
|
|
69
|
+
title: string;
|
|
70
|
+
body: string;
|
|
71
|
+
cta: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const ptBR: TranslationShape = {
|
|
76
|
+
meta: {
|
|
77
|
+
siteTitle: '{{PROJECT_NAME}} — para iOS e Android',
|
|
78
|
+
siteDescription: '{{PROJECT_DESCRIPTION}}'
|
|
79
|
+
},
|
|
80
|
+
nav: {
|
|
81
|
+
features: 'Recursos',
|
|
82
|
+
howItWorks: 'Como funciona',
|
|
83
|
+
pricing: 'Preço',
|
|
84
|
+
download: 'Baixar',
|
|
85
|
+
languageToggle: 'EN'
|
|
86
|
+
},
|
|
87
|
+
hero: {
|
|
88
|
+
eyebrow: 'Agora no iOS e Android',
|
|
89
|
+
title1: 'O {{PROJECT_NAME}}',
|
|
90
|
+
title2: 'no seu bolso.',
|
|
91
|
+
subtitle: '{{PROJECT_DESCRIPTION}}',
|
|
92
|
+
ctaIos: 'Baixar na App Store',
|
|
93
|
+
ctaAndroid: 'Disponível no Google Play',
|
|
94
|
+
noSub: 'Grátis para começar. Sem cadastro.'
|
|
95
|
+
},
|
|
96
|
+
notIs: {
|
|
97
|
+
title: 'O que ele não é',
|
|
98
|
+
items: [
|
|
99
|
+
'Sem anúncios',
|
|
100
|
+
'Sem rastreadores',
|
|
101
|
+
'Sem cadastro obrigatório',
|
|
102
|
+
'Sem assinatura escondida'
|
|
103
|
+
],
|
|
104
|
+
isTitle: 'É só o app que você queria.',
|
|
105
|
+
isBody: 'Rápido, direto ao ponto e feito para usar todo dia. '
|
|
106
|
+
+ 'Abra e comece; a configuração fica para depois, se você '
|
|
107
|
+
+ 'quiser.'
|
|
108
|
+
},
|
|
109
|
+
flow: {
|
|
110
|
+
eyebrow: 'Como funciona',
|
|
111
|
+
title: 'Três passos e pronto',
|
|
112
|
+
sub: 'Nada de tutorial longo. Você entende em segundos.',
|
|
113
|
+
steps: [
|
|
114
|
+
{
|
|
115
|
+
title: 'Baixe',
|
|
116
|
+
body: 'Instale pela App Store ou Google Play. Leva '
|
|
117
|
+
+ 'menos de um minuto.'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
title: 'Abra e use',
|
|
121
|
+
body: 'Sem tela de cadastro no caminho. Comece a usar '
|
|
122
|
+
+ 'na primeira tela.'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
title: 'Leve com você',
|
|
126
|
+
body: 'Funciona quando você precisa, onde você estiver, '
|
|
127
|
+
+ 'no seu ritmo.'
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
features: {
|
|
132
|
+
eyebrow: 'Recursos',
|
|
133
|
+
title: 'Feito para o dia a dia',
|
|
134
|
+
sub: 'O essencial, bem feito. Sem peso, sem enrolação.',
|
|
135
|
+
items: [
|
|
136
|
+
{
|
|
137
|
+
title: 'Rápido de verdade',
|
|
138
|
+
body: 'Abre na hora e responde a cada toque. Nada de '
|
|
139
|
+
+ 'telas de carregamento.'
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
title: 'Funciona offline',
|
|
143
|
+
body: 'Seus dados ficam no aparelho. Use no metrô, no '
|
|
144
|
+
+ 'avião, sem sinal.'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
title: 'Seus dados são seus',
|
|
148
|
+
body: 'Sem rastreadores e sem venda de dados. O que é '
|
|
149
|
+
+ 'seu, continua seu.'
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: 'Simples de aprender',
|
|
153
|
+
body: 'Interface limpa e direta. Você acha tudo sem '
|
|
154
|
+
+ 'precisar de manual.'
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: 'Backup quando quiser',
|
|
158
|
+
body: 'Exporte e restaure com um toque. Troque de '
|
|
159
|
+
+ 'aparelho sem perder nada.'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
title: 'Claro e escuro',
|
|
163
|
+
body: 'Acompanha o tema do sistema ou fixe o seu '
|
|
164
|
+
+ 'favorito. Confortável a qualquer hora.'
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
pricing: {
|
|
169
|
+
eyebrow: 'Preço',
|
|
170
|
+
title: 'Um preço, tudo incluído',
|
|
171
|
+
sub: 'Sem pegadinha, sem plano premium escondido.',
|
|
172
|
+
price: 'Grátis',
|
|
173
|
+
priceNote: 'Baixe e comece agora mesmo.',
|
|
174
|
+
included: [
|
|
175
|
+
'Todos os recursos, sem trava',
|
|
176
|
+
'Funciona offline',
|
|
177
|
+
'Backup e restauração',
|
|
178
|
+
'Atualizações incluídas'
|
|
179
|
+
],
|
|
180
|
+
cta: 'Baixar agora',
|
|
181
|
+
fine: 'Sem cartão de crédito. Sem cadastro.'
|
|
182
|
+
},
|
|
183
|
+
cta: {
|
|
184
|
+
title: 'Pronto para começar?',
|
|
185
|
+
body: 'Baixe o {{PROJECT_NAME}} e veja em menos de um minuto.'
|
|
186
|
+
},
|
|
187
|
+
footer: {
|
|
188
|
+
tagline: '{{PROJECT_DESCRIPTION}}',
|
|
189
|
+
product: 'Produto',
|
|
190
|
+
support: 'Suporte',
|
|
191
|
+
contact: 'Fale com a gente',
|
|
192
|
+
rights: 'Todos os direitos reservados.',
|
|
193
|
+
terms: 'Termos de Uso',
|
|
194
|
+
privacy: 'Privacidade'
|
|
195
|
+
},
|
|
196
|
+
notFound: {
|
|
197
|
+
title: 'Página não encontrada',
|
|
198
|
+
body: 'O conteúdo que você procura não está aqui.',
|
|
199
|
+
cta: 'Voltar para o início'
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default ptBR;
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
export default {
|
|
3
3
|
content: ['./views/**/*.ejs'],
|
|
4
4
|
theme: {
|
|
5
|
-
extend: {
|
|
5
|
+
extend: {
|
|
6
|
+
// Brand accent, injected from the project's primary color at
|
|
7
|
+
// scaffold time. Used by the app-marketing landing
|
|
8
|
+
// (bg-primary / text-primary); harmless for the SaaS landing,
|
|
9
|
+
// which sticks to Tailwind's stock palette.
|
|
10
|
+
colors: {
|
|
11
|
+
primary: '{{PRIMARY_COLOR}}'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
6
14
|
},
|
|
7
15
|
plugins: []
|
|
8
16
|
};
|
package/template-variables.mjs
CHANGED
|
@@ -35,6 +35,38 @@
|
|
|
35
35
|
// mobile for signed content URLs (random
|
|
36
36
|
// per project)
|
|
37
37
|
// {{JWT_TOKEN_SECRET}} — JWT signing secret (random per project)
|
|
38
|
+
// {{SPLASH_ANIMATED}} — "true"/"false" — whether the mobile app
|
|
39
|
+
// plays the AnimatedSplash on boot or shows
|
|
40
|
+
// up instantly (native splash hides on font
|
|
41
|
+
// load). Consumed by mobile src/config.ts.
|
|
42
|
+
|
|
43
|
+
// ─── Site landing variants ────────────────────────────────────────────────────
|
|
44
|
+
// The public site ships two landing flavors. The SaaS landing (CTAs →
|
|
45
|
+
// admin signup) is the canonical set under ui/site/. The app-marketing
|
|
46
|
+
// landing (App Store / Play buttons, pricing, download) lives under
|
|
47
|
+
// ui/site/_variants/app/ and is swapped over the canonical files for
|
|
48
|
+
// presets flagged `appLanding` (see create.mjs / update.mjs). Both tools
|
|
49
|
+
// import these so create-time scaffolding and `umpordez update` stay in
|
|
50
|
+
// lockstep — otherwise update would clobber an app project's landing with
|
|
51
|
+
// the SaaS one.
|
|
52
|
+
|
|
53
|
+
export const SITE_VARIANT_DIR = 'ui/site/_variants';
|
|
54
|
+
|
|
55
|
+
// [variantPath, canonicalPath] — relative to the project root.
|
|
56
|
+
export const APP_LANDING_SWAPS = [
|
|
57
|
+
['ui/site/_variants/app/home.ejs',
|
|
58
|
+
'ui/site/views/pages/home.ejs'],
|
|
59
|
+
['ui/site/_variants/app/header.ejs',
|
|
60
|
+
'ui/site/views/partials/header.ejs'],
|
|
61
|
+
['ui/site/_variants/app/footer.ejs',
|
|
62
|
+
'ui/site/views/partials/footer.ejs'],
|
|
63
|
+
['ui/site/_variants/app/not-found.ejs',
|
|
64
|
+
'ui/site/views/pages/not-found.ejs'],
|
|
65
|
+
['ui/site/_variants/app/pt-BR.ts',
|
|
66
|
+
'ui/site/i18n/locales/pt-BR.ts'],
|
|
67
|
+
['ui/site/_variants/app/en.ts',
|
|
68
|
+
'ui/site/i18n/locales/en.ts'],
|
|
69
|
+
];
|
|
38
70
|
|
|
39
71
|
// ─── Theme Generation ───────────────────────────────────────────────────────
|
|
40
72
|
|
|
@@ -241,5 +273,7 @@ export function buildReplacements(details) {
|
|
|
241
273
|
// URLs already issued by the running app.
|
|
242
274
|
'{{CONTENT_SECRET}}': details.contentSecret ?? randomSecret(),
|
|
243
275
|
'{{JWT_TOKEN_SECRET}}': details.jwtTokenSecret ?? randomSecret(),
|
|
276
|
+
'{{SPLASH_ANIMATED}}':
|
|
277
|
+
details.splashStyle === 'animated' ? 'true' : 'false',
|
|
244
278
|
};
|
|
245
279
|
}
|
package/update.mjs
CHANGED
|
@@ -32,7 +32,10 @@ import crypto from 'node:crypto';
|
|
|
32
32
|
import readline from 'node:readline';
|
|
33
33
|
import { fileURLToPath } from 'node:url';
|
|
34
34
|
import chalk from 'chalk';
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
buildReplacements,
|
|
37
|
+
APP_LANDING_SWAPS
|
|
38
|
+
} from './template-variables.mjs';
|
|
36
39
|
import { generateIcons, ICON_TARGETS } from './icons.mjs';
|
|
37
40
|
|
|
38
41
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -46,11 +49,17 @@ const PRESET_INCLUDES = {
|
|
|
46
49
|
everything: ['server', 'ui/admin', 'ui/site', 'mobile', 'misc',
|
|
47
50
|
'scripts', 'doc'],
|
|
48
51
|
'web-only': ['server', 'ui/admin', 'ui/site', 'misc', 'doc'],
|
|
52
|
+
'mobile-and-site': ['mobile', 'ui/site', 'scripts', 'doc'],
|
|
49
53
|
'mobile-and-server': ['server', 'mobile', 'misc', 'scripts',
|
|
50
54
|
'doc'],
|
|
51
55
|
'just-mobile': ['mobile', 'scripts', 'doc']
|
|
52
56
|
};
|
|
53
57
|
|
|
58
|
+
// Presets that use the app-marketing landing instead of the SaaS one.
|
|
59
|
+
// Mirror of create.mjs's `appLanding` preset flag — kept in sync
|
|
60
|
+
// manually (see PRESET_INCLUDES note above).
|
|
61
|
+
const APP_LANDING_PRESETS = new Set(['mobile-and-site']);
|
|
62
|
+
|
|
54
63
|
const green = chalk.hex('#02e027');
|
|
55
64
|
const cyan = chalk.hex('#00fff9');
|
|
56
65
|
const red = chalk.hex('#ff3c00');
|
|
@@ -134,7 +143,13 @@ function applyPlaceholders(content, replacements) {
|
|
|
134
143
|
|
|
135
144
|
function applyPathPlaceholders(p, slug) {
|
|
136
145
|
// Mirrors create.mjs renameDirs + per-file substitution.
|
|
137
|
-
|
|
146
|
+
let out = p.replaceAll('{{PROJECT_SLUG}}', slug);
|
|
147
|
+
// Gitignore files ship dot-less (npm strips `.gitignore` from
|
|
148
|
+
// published packages) and are restored to `.gitignore` on write.
|
|
149
|
+
if (path.basename(out) === 'gitignore') {
|
|
150
|
+
out = path.join(path.dirname(out), '.gitignore');
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
/**
|
|
@@ -197,6 +212,26 @@ async function loadTemplate(details, preset) {
|
|
|
197
212
|
out.set(rel, content);
|
|
198
213
|
}
|
|
199
214
|
|
|
215
|
+
// Landing-page variant resolution — mirror create.mjs's copyTemplate
|
|
216
|
+
// swap. For app-landing presets the canonical landing files are
|
|
217
|
+
// sourced from ui/site/_variants/app/ so `umpordez update` compares
|
|
218
|
+
// against the SAME flavor the project was scaffolded with (otherwise
|
|
219
|
+
// it would try to push the SaaS landing over an app project). The
|
|
220
|
+
// _variants/ staging files never ship, so drop them from the map for
|
|
221
|
+
// every preset.
|
|
222
|
+
if (APP_LANDING_PRESETS.has(preset)) {
|
|
223
|
+
for (const [variant, canonical] of APP_LANDING_SWAPS) {
|
|
224
|
+
if (out.has(variant)) {
|
|
225
|
+
out.set(canonical, out.get(variant));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const key of [...out.keys()]) {
|
|
230
|
+
if (key.startsWith('ui/site/_variants/')) {
|
|
231
|
+
out.delete(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
200
235
|
return out;
|
|
201
236
|
}
|
|
202
237
|
|