@thecodecodex/tools 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/package.json +46 -4
- package/src/base64.ts +36 -0
- package/src/br/cnpj.ts +55 -0
- package/src/br/cpf.ts +61 -0
- package/src/cli/main.ts +93 -0
- package/src/cli/tools.ts +643 -0
- package/src/compensation-comparator.ts +124 -0
- package/src/css-playground.ts +699 -0
- package/src/dev-ref.ts +295 -0
- package/src/diff.ts +301 -0
- package/src/finance/clt.ts +189 -0
- package/src/finance/compound.ts +78 -0
- package/src/finance/pj.ts +146 -0
- package/src/fov.ts +145 -0
- package/src/hash.ts +13 -0
- package/src/image-to-pdf.ts +299 -0
- package/src/invoice-pdf.ts +134 -0
- package/src/invoice.ts +94 -0
- package/src/jwt.ts +44 -0
- package/src/market.ts +49 -0
- package/src/password.ts +51 -0
- package/src/pdf-studio.ts +453 -0
- package/src/random/index.ts +19 -0
- package/src/regex.ts +426 -0
- package/src/skills.json +60 -0
- package/src/skills.ts +52 -0
- package/src/state-holidays.ts +94 -0
- package/src/table-convert/code.ts +99 -0
- package/src/table-convert/index.ts +42 -0
- package/src/table-convert/model.ts +72 -0
- package/src/table-convert/parse.ts +242 -0
- package/src/table-convert/serialize.ts +435 -0
- package/src/table-convert/transform.ts +133 -0
- package/src/table-convert/xlsx.ts +262 -0
- package/src/table-convert/zip.ts +127 -0
- package/src/timestamp.ts +50 -0
- package/src/url-codec.ts +25 -0
- package/src/uuid.ts +53 -0
- package/src/vacation.ts +171 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
export type Lang = "pt" | "en"
|
|
2
|
+
|
|
3
|
+
// Playground de CSS: cada demo é dado puro (HTML, CSS, controles), renderizado num <iframe sandbox> sem scripts.
|
|
4
|
+
// Os controles viram variáveis CSS (`--gap: 16px`), e o CSS da demo as usa com var().
|
|
5
|
+
|
|
6
|
+
export const CATEGORIES = ["layout", "selectors", "interaction", "visual"] as const
|
|
7
|
+
export type DemoCategory = (typeof CATEGORIES)[number]
|
|
8
|
+
|
|
9
|
+
// Baseline (web.dev): "widely" = suportado há 30+ meses; "newly" = em todos os navegadores principais há pouco; "limited" = falta algum
|
|
10
|
+
export type SupportLevel = "widely" | "newly" | "limited"
|
|
11
|
+
|
|
12
|
+
export interface SelectControl {
|
|
13
|
+
kind: "select"
|
|
14
|
+
name: string
|
|
15
|
+
options: readonly string[]
|
|
16
|
+
initial: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RangeControl {
|
|
20
|
+
kind: "range"
|
|
21
|
+
name: string
|
|
22
|
+
min: number
|
|
23
|
+
max: number
|
|
24
|
+
step: number
|
|
25
|
+
unit: string
|
|
26
|
+
initial: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ColorControl {
|
|
30
|
+
kind: "color"
|
|
31
|
+
name: string
|
|
32
|
+
initial: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type Control = SelectControl | RangeControl | ColorControl
|
|
36
|
+
|
|
37
|
+
export interface Demo {
|
|
38
|
+
id: string
|
|
39
|
+
category: DemoCategory
|
|
40
|
+
support: { level: SupportLevel, since?: number }
|
|
41
|
+
controls: Control[]
|
|
42
|
+
// mostra o controle de largura da prévia (para demos que reagem ao tamanho)
|
|
43
|
+
resizable: boolean
|
|
44
|
+
height: number
|
|
45
|
+
// condição de @supports: sem suporte, a prévia mostra um aviso em vez de quebrar em silêncio
|
|
46
|
+
check?: string
|
|
47
|
+
html: (lang: Lang) => string
|
|
48
|
+
css: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const t = (lang: Lang, pt: string, en: string) => (lang === "pt" ? pt : en)
|
|
52
|
+
|
|
53
|
+
const select = (name: string, options: readonly string[], initial = options[0] ?? ""): SelectControl => ({ kind: "select", name, options, initial })
|
|
54
|
+
const range = (name: string, min: number, max: number, initial: number, unit = "px", step = 1): RangeControl => ({ kind: "range", name, min, max, step, unit, initial })
|
|
55
|
+
const color = (name: string, initial: string): ColorControl => ({ kind: "color", name, initial })
|
|
56
|
+
|
|
57
|
+
export const DEMOS: Demo[] = [
|
|
58
|
+
{
|
|
59
|
+
id: "flexbox",
|
|
60
|
+
category: "layout",
|
|
61
|
+
support: { level: "widely" },
|
|
62
|
+
controls: [
|
|
63
|
+
select("direction", ["row", "column", "row-reverse"]),
|
|
64
|
+
select("justify", ["flex-start", "center", "space-between", "space-around"]),
|
|
65
|
+
select("align", ["stretch", "flex-start", "center", "flex-end"]),
|
|
66
|
+
select("wrap", ["nowrap", "wrap"]),
|
|
67
|
+
range("gap", 0, 48, 12),
|
|
68
|
+
],
|
|
69
|
+
resizable: true,
|
|
70
|
+
height: 300,
|
|
71
|
+
html: () => `<div class="row">
|
|
72
|
+
<div class="item">1</div>
|
|
73
|
+
<div class="item">2</div>
|
|
74
|
+
<div class="item">3</div>
|
|
75
|
+
<div class="item">4</div>
|
|
76
|
+
<div class="item">5</div>
|
|
77
|
+
</div>`,
|
|
78
|
+
css: `.row {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: var(--direction);
|
|
81
|
+
justify-content: var(--justify);
|
|
82
|
+
align-items: var(--align);
|
|
83
|
+
flex-wrap: var(--wrap);
|
|
84
|
+
gap: var(--gap);
|
|
85
|
+
min-height: 240px;
|
|
86
|
+
padding: 12px;
|
|
87
|
+
border: 2px dashed #9ca3af;
|
|
88
|
+
border-radius: 12px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.item {
|
|
92
|
+
padding: 12px 20px;
|
|
93
|
+
border-radius: 10px;
|
|
94
|
+
background: #0e7490;
|
|
95
|
+
color: white;
|
|
96
|
+
font-weight: 700;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* tamanhos diferentes para o alinhamento ficar visível || different sizes so the alignment is visible */
|
|
100
|
+
.item:nth-child(2) { padding-block: 32px; }
|
|
101
|
+
.item:nth-child(4) { padding-inline: 48px; }`,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "grid",
|
|
105
|
+
category: "layout",
|
|
106
|
+
support: { level: "widely" },
|
|
107
|
+
controls: [range("min", 80, 260, 140), range("gap", 0, 40, 12)],
|
|
108
|
+
resizable: true,
|
|
109
|
+
height: 340,
|
|
110
|
+
html: (lang) => `<div class="cards">
|
|
111
|
+
${[1, 2, 3, 4, 5, 6, 7, 8].map((n) => ` <div class="card">${t(lang, "Card", "Card")} ${n}</div>`).join("\n")}
|
|
112
|
+
</div>`,
|
|
113
|
+
css: `/* quantas colunas couberem, cada uma com no mínimo --min: sem media query || as many columns as fit, each at least --min wide: no media query */
|
|
114
|
+
.cards {
|
|
115
|
+
display: grid;
|
|
116
|
+
grid-template-columns: repeat(auto-fill, minmax(var(--min), 1fr));
|
|
117
|
+
gap: var(--gap);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.card {
|
|
121
|
+
display: grid;
|
|
122
|
+
place-items: center; /* centraliza nos dois eixos || centers on both axes */
|
|
123
|
+
aspect-ratio: 4 / 3;
|
|
124
|
+
border-radius: 12px;
|
|
125
|
+
background: #1d4ed8;
|
|
126
|
+
color: white;
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
}`,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "container",
|
|
132
|
+
category: "layout",
|
|
133
|
+
support: { level: "widely" },
|
|
134
|
+
controls: [],
|
|
135
|
+
resizable: true,
|
|
136
|
+
height: 360,
|
|
137
|
+
html: (lang) => {
|
|
138
|
+
const card = `<article class="card">
|
|
139
|
+
<div class="thumb"></div>
|
|
140
|
+
<div>
|
|
141
|
+
<h3>${t(lang, "Mesmo componente", "Same component")}</h3>
|
|
142
|
+
<p>${t(lang, "O layout depende do espaço do container, não da tela.", "The layout depends on the container's space, not the screen.")}</p>
|
|
143
|
+
</div>
|
|
144
|
+
</article>`
|
|
145
|
+
return `<div class="layout">
|
|
146
|
+
<aside class="slot">
|
|
147
|
+
${card}
|
|
148
|
+
</aside>
|
|
149
|
+
<main class="slot">
|
|
150
|
+
${card}
|
|
151
|
+
</main>
|
|
152
|
+
</div>`
|
|
153
|
+
},
|
|
154
|
+
css: `.layout {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: 1fr 2fr;
|
|
157
|
+
gap: 16px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* cada slot vira um container que os filhos podem consultar || each slot becomes a container its children can query */
|
|
161
|
+
.slot { container-type: inline-size; }
|
|
162
|
+
|
|
163
|
+
.card {
|
|
164
|
+
display: grid;
|
|
165
|
+
gap: 12px;
|
|
166
|
+
padding: 12px;
|
|
167
|
+
border: 2px solid #e5e7eb;
|
|
168
|
+
border-radius: 14px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.thumb {
|
|
172
|
+
aspect-ratio: 16 / 9;
|
|
173
|
+
border-radius: 10px;
|
|
174
|
+
background: linear-gradient(135deg, #0e7490, #a5f3fc);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
h3 { margin: 0; font-size: 18px; }
|
|
178
|
+
p { margin: 4px 0 0; color: #4b5563; }
|
|
179
|
+
|
|
180
|
+
/* vale para a largura do CONTAINER, não da janela || applies to the CONTAINER width, not the window */
|
|
181
|
+
@container (min-width: 360px) {
|
|
182
|
+
.card { grid-template-columns: 140px 1fr; align-items: center; }
|
|
183
|
+
.thumb { aspect-ratio: 1; }
|
|
184
|
+
}`,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "clamp",
|
|
188
|
+
category: "layout",
|
|
189
|
+
support: { level: "widely" },
|
|
190
|
+
controls: [range("min", 16, 32, 24), range("max", 32, 80, 56)],
|
|
191
|
+
resizable: true,
|
|
192
|
+
height: 320,
|
|
193
|
+
html: (lang) => `<div class="box">
|
|
194
|
+
<h1>${t(lang, "Tipografia fluida", "Fluid typography")}</h1>
|
|
195
|
+
<p>${t(lang, "Mude a largura da prévia: o título cresce com a tela, mas nunca passa dos limites.", "Change the preview width: the heading grows with the screen, but never past the limits.")}</p>
|
|
196
|
+
</div>`,
|
|
197
|
+
css: `/* clamp(mínimo, ideal, máximo) || clamp(minimum, preferred, maximum) */
|
|
198
|
+
h1 {
|
|
199
|
+
font-size: clamp(var(--min), 1rem + 5vw, var(--max));
|
|
200
|
+
line-height: 1.05;
|
|
201
|
+
margin: 0 0 12px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
p { font-size: clamp(15px, 0.8rem + 0.6vw, 19px); }
|
|
205
|
+
|
|
206
|
+
.box {
|
|
207
|
+
padding: clamp(12px, 5vw, 48px);
|
|
208
|
+
border-radius: 16px;
|
|
209
|
+
background: #fed7aa;
|
|
210
|
+
}`,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "sticky",
|
|
214
|
+
category: "layout",
|
|
215
|
+
support: { level: "widely" },
|
|
216
|
+
controls: [range("top", 0, 48, 0)],
|
|
217
|
+
resizable: false,
|
|
218
|
+
height: 320,
|
|
219
|
+
html: (lang) => ["A", "B", "C", "D"]
|
|
220
|
+
.map((letter) => `<section>
|
|
221
|
+
<h3>${letter}</h3>
|
|
222
|
+
<p>${t(lang, "Role a prévia: o título gruda no topo enquanto a seção dele estiver visível.", "Scroll the preview: the heading sticks to the top while its section is visible.")}</p>
|
|
223
|
+
<p>Ana · Bruno · Carla · Diego · Elisa · Fábio</p>
|
|
224
|
+
</section>`)
|
|
225
|
+
.join("\n"),
|
|
226
|
+
css: `h3 {
|
|
227
|
+
position: sticky;
|
|
228
|
+
top: var(--top);
|
|
229
|
+
margin: 0;
|
|
230
|
+
padding: 8px 12px;
|
|
231
|
+
background: #12151e;
|
|
232
|
+
color: white;
|
|
233
|
+
border-radius: 8px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
section { padding-bottom: 24px; }
|
|
237
|
+
p { margin: 8px 12px; line-height: 2; }`,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "has",
|
|
241
|
+
category: "selectors",
|
|
242
|
+
support: { level: "widely" },
|
|
243
|
+
controls: [],
|
|
244
|
+
resizable: false,
|
|
245
|
+
height: 380,
|
|
246
|
+
html: (lang) => `<label class="toggle"><input type="checkbox" id="dark"> ${t(lang, "Tema escuro (só CSS)", "Dark theme (CSS only)")}</label>
|
|
247
|
+
|
|
248
|
+
<div class="plans">
|
|
249
|
+
<label class="plan"><input type="radio" name="plan"> ${t(lang, "Básico", "Basic")}</label>
|
|
250
|
+
<label class="plan"><input type="radio" name="plan"> Pro</label>
|
|
251
|
+
<p class="hint">${t(lang, "Escolha um plano", "Pick a plan")}</p>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<form>
|
|
255
|
+
<input type="email" required placeholder="email@example.com">
|
|
256
|
+
<button>${t(lang, "Enviar", "Send")}</button>
|
|
257
|
+
</form>`,
|
|
258
|
+
css: `/* o card que CONTÉM o radio marcado || the card that CONTAINS the checked radio */
|
|
259
|
+
.plan:has(input:checked) {
|
|
260
|
+
border-color: #0e7490;
|
|
261
|
+
background: #cffafe;
|
|
262
|
+
color: #164e63;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* esconde a dica quando qualquer plano foi escolhido || hide the hint once any plan is picked */
|
|
266
|
+
.plans:has(input:checked) .hint { display: none; }
|
|
267
|
+
|
|
268
|
+
/* desabilita o botão enquanto o e-mail for inválido || disable the button while the email is invalid */
|
|
269
|
+
form:has(input:invalid) button { opacity: 0.4; pointer-events: none; }
|
|
270
|
+
|
|
271
|
+
/* o body reage a um checkbox lá dentro || the body reacts to a checkbox inside it */
|
|
272
|
+
body:has(#dark:checked) { background: #12151e; color: #f3f4f6; }
|
|
273
|
+
|
|
274
|
+
.plans { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; }
|
|
275
|
+
.plan { padding: 12px 16px; border: 2px solid #9ca3af; border-radius: 10px; cursor: pointer; }
|
|
276
|
+
form { display: flex; gap: 8px; }
|
|
277
|
+
input[type="email"] { flex: 1; padding: 8px; }
|
|
278
|
+
.hint { width: 100%; margin: 0; color: #9a3412; }`,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "sibling-index",
|
|
282
|
+
category: "selectors",
|
|
283
|
+
support: { level: "newly", since: 2026 },
|
|
284
|
+
controls: [range("step", 20, 120, 80, "ms", 10)],
|
|
285
|
+
resizable: false,
|
|
286
|
+
height: 320,
|
|
287
|
+
check: "width: calc(sibling-index() * 1px)",
|
|
288
|
+
html: () => `<div class="bars">
|
|
289
|
+
${Array.from({ length: 8 }, () => ` <div class="bar"></div>`).join("\n")}
|
|
290
|
+
</div>`,
|
|
291
|
+
css: `/* cada barra sabe sua posição entre os irmãos, sem :nth-child para cada uma || each bar knows its position among its siblings, no :nth-child for each one */
|
|
292
|
+
.bar {
|
|
293
|
+
height: 24px;
|
|
294
|
+
margin-bottom: 8px;
|
|
295
|
+
border-radius: 6px;
|
|
296
|
+
width: calc(sibling-index() * 11%);
|
|
297
|
+
background: hsl(calc(sibling-index() * 40) 70% 45%);
|
|
298
|
+
transform-origin: left;
|
|
299
|
+
animation: grow 0.6s both;
|
|
300
|
+
animation-delay: calc(sibling-index() * var(--step));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@keyframes grow {
|
|
304
|
+
from { transform: scaleX(0); }
|
|
305
|
+
}`,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "popover",
|
|
309
|
+
category: "interaction",
|
|
310
|
+
support: { level: "newly", since: 2026 },
|
|
311
|
+
controls: [select("area", ["bottom span-right", "top span-right", "right", "bottom"])],
|
|
312
|
+
resizable: false,
|
|
313
|
+
height: 300,
|
|
314
|
+
check: "anchor-name: --a",
|
|
315
|
+
html: (lang) => `<button popovertarget="menu" class="trigger">Menu ▾</button>
|
|
316
|
+
|
|
317
|
+
<div id="menu" popover>
|
|
318
|
+
<a href="#">${t(lang, "Perfil", "Profile")}</a>
|
|
319
|
+
<a href="#">${t(lang, "Configurações", "Settings")}</a>
|
|
320
|
+
<a href="#">${t(lang, "Sair", "Sign out")}</a>
|
|
321
|
+
</div>`,
|
|
322
|
+
css: `/* popover: abre, fecha com Esc ou clique fora, sem uma linha de JavaScript || popover: opens, closes on Esc or outside click, without a line of JavaScript */
|
|
323
|
+
body { display: grid; place-items: center; min-height: 260px; }
|
|
324
|
+
|
|
325
|
+
/* anchor positioning: o menu se posiciona em relação ao botão || anchor positioning: the menu positions itself relative to the button */
|
|
326
|
+
.trigger { anchor-name: --menu; padding: 10px 16px; font: inherit; font-weight: 700; }
|
|
327
|
+
|
|
328
|
+
#menu {
|
|
329
|
+
position-anchor: --menu;
|
|
330
|
+
position-area: var(--area);
|
|
331
|
+
inset: auto;
|
|
332
|
+
margin: 6px;
|
|
333
|
+
display: grid;
|
|
334
|
+
min-width: 160px;
|
|
335
|
+
padding: 6px;
|
|
336
|
+
border: 2px solid #12151e;
|
|
337
|
+
border-radius: 12px;
|
|
338
|
+
opacity: 1;
|
|
339
|
+
transform: translateY(0);
|
|
340
|
+
transition: opacity 0.2s, transform 0.2s, display 0.2s allow-discrete, overlay 0.2s allow-discrete;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#menu:not(:popover-open) { opacity: 0; transform: translateY(-6px); }
|
|
344
|
+
|
|
345
|
+
/* @starting-style: o estado de partida da animação de entrada || @starting-style: the starting state of the entry animation */
|
|
346
|
+
@starting-style {
|
|
347
|
+
#menu:popover-open { opacity: 0; transform: translateY(-6px); }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#menu a { padding: 8px 10px; border-radius: 8px; color: inherit; text-decoration: none; }
|
|
351
|
+
#menu a:hover { background: #e5e7eb; }`,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "accordion",
|
|
355
|
+
category: "interaction",
|
|
356
|
+
support: { level: "newly", since: 2024 },
|
|
357
|
+
controls: [],
|
|
358
|
+
resizable: false,
|
|
359
|
+
height: 320,
|
|
360
|
+
html: (lang) => [
|
|
361
|
+
[t(lang, "O que é o atributo name?", "What does the name attribute do?"), t(lang, "Vários <details> com o mesmo name formam um grupo: abrir um fecha os outros.", "Several <details> with the same name form a group: opening one closes the others.")],
|
|
362
|
+
[t(lang, "Precisa de JavaScript?", "Does it need JavaScript?"), t(lang, "Não. É comportamento nativo do HTML.", "No. It is native HTML behavior.")],
|
|
363
|
+
[t(lang, "E a acessibilidade?", "What about accessibility?"), t(lang, "Teclado e leitores de tela funcionam sem nada extra.", "Keyboard and screen readers work with nothing extra.")],
|
|
364
|
+
]
|
|
365
|
+
.map(([question, answer], index) => `<details name="faq"${index === 0 ? " open" : ""}>
|
|
366
|
+
<summary>${question}</summary>
|
|
367
|
+
<p>${answer?.replace(/</g, "<")}</p>
|
|
368
|
+
</details>`)
|
|
369
|
+
.join("\n"),
|
|
370
|
+
css: `details {
|
|
371
|
+
border: 2px solid #e5e7eb;
|
|
372
|
+
border-radius: 12px;
|
|
373
|
+
margin-bottom: 8px;
|
|
374
|
+
padding: 0 14px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
details[open] { border-color: #0e7490; }
|
|
378
|
+
|
|
379
|
+
summary {
|
|
380
|
+
display: flex;
|
|
381
|
+
justify-content: space-between;
|
|
382
|
+
padding: 12px 0;
|
|
383
|
+
font-weight: 700;
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
list-style: none; /* esconde o triângulo padrão || hides the default triangle */
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
summary::after { content: "+"; transition: rotate 0.2s; }
|
|
389
|
+
details[open] summary::after { rotate: 45deg; }
|
|
390
|
+
|
|
391
|
+
p { margin: 0 0 12px; color: #4b5563; }`,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: "scroll-snap",
|
|
395
|
+
category: "interaction",
|
|
396
|
+
support: { level: "widely" },
|
|
397
|
+
controls: [select("type", ["mandatory", "proximity"]), select("align", ["center", "start"])],
|
|
398
|
+
resizable: false,
|
|
399
|
+
height: 300,
|
|
400
|
+
html: (lang) => `<p>${t(lang, "Arraste ou role para o lado:", "Drag or scroll sideways:")}</p>
|
|
401
|
+
<div class="track">
|
|
402
|
+
${[1, 2, 3, 4, 5].map((n) => ` <div class="slide">${n}</div>`).join("\n")}
|
|
403
|
+
</div>`,
|
|
404
|
+
css: `.track {
|
|
405
|
+
display: flex;
|
|
406
|
+
gap: 12px;
|
|
407
|
+
overflow-x: auto;
|
|
408
|
+
scroll-snap-type: x var(--type);
|
|
409
|
+
scroll-padding-inline: 16px;
|
|
410
|
+
padding-bottom: 12px;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.slide {
|
|
414
|
+
flex: 0 0 70%;
|
|
415
|
+
scroll-snap-align: var(--align); /* onde cada slide "encaixa" ao soltar || where each slide "snaps" when released */
|
|
416
|
+
display: grid;
|
|
417
|
+
place-items: center;
|
|
418
|
+
height: 180px;
|
|
419
|
+
border-radius: 14px;
|
|
420
|
+
background: #9a3412;
|
|
421
|
+
color: white;
|
|
422
|
+
font-size: 48px;
|
|
423
|
+
font-weight: 800;
|
|
424
|
+
}`,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "field-sizing",
|
|
428
|
+
category: "interaction",
|
|
429
|
+
support: { level: "newly", since: 2026 },
|
|
430
|
+
controls: [],
|
|
431
|
+
resizable: false,
|
|
432
|
+
height: 300,
|
|
433
|
+
check: "field-sizing: content",
|
|
434
|
+
html: (lang) => `<p>${t(lang, "Digite nos campos: eles crescem com o conteúdo.", "Type in the fields: they grow with the content.")}</p>
|
|
435
|
+
<textarea>${t(lang, "Este textarea cresce a cada nova linha, sem JavaScript.", "This textarea grows with every new line, without JavaScript.")}</textarea>
|
|
436
|
+
<p><input value="${t(lang, "Nome", "Name")}"> <select><option>${t(lang, "Curta", "Short")}</option><option>${t(lang, "Uma opção bem mais longa", "A much longer option")}</option></select></p>`,
|
|
437
|
+
css: `textarea, input, select {
|
|
438
|
+
field-sizing: content; /* o tamanho acompanha o conteúdo || the size follows the content */
|
|
439
|
+
min-width: 8ch;
|
|
440
|
+
max-width: 100%;
|
|
441
|
+
padding: 8px 10px;
|
|
442
|
+
font: inherit;
|
|
443
|
+
border: 2px solid #9ca3af;
|
|
444
|
+
border-radius: 8px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
textarea { min-height: 2lh; width: auto; }`,
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
id: "property",
|
|
451
|
+
category: "visual",
|
|
452
|
+
support: { level: "newly", since: 2024 },
|
|
453
|
+
controls: [range("speed", 1, 10, 4, "s")],
|
|
454
|
+
resizable: false,
|
|
455
|
+
height: 280,
|
|
456
|
+
html: (lang) => `<div class="card">${t(lang, "Borda animada com @property", "Animated border with @property")}</div>`,
|
|
457
|
+
css: `/* registrar a variável com um tipo permite animá-la || registering the variable with a type makes it animatable */
|
|
458
|
+
@property --angle {
|
|
459
|
+
syntax: "<angle>";
|
|
460
|
+
inherits: false;
|
|
461
|
+
initial-value: 0deg;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
body { display: grid; place-items: center; min-height: 240px; }
|
|
465
|
+
|
|
466
|
+
.card {
|
|
467
|
+
padding: 32px 40px;
|
|
468
|
+
font-weight: 700;
|
|
469
|
+
border: 5px solid transparent;
|
|
470
|
+
border-radius: 18px;
|
|
471
|
+
background:
|
|
472
|
+
linear-gradient(white, white) padding-box,
|
|
473
|
+
conic-gradient(from var(--angle), #0e7490, #a5f3fc, #9a3412, #0e7490) border-box;
|
|
474
|
+
animation: spin var(--speed) linear infinite;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@keyframes spin {
|
|
478
|
+
to { --angle: 360deg; }
|
|
479
|
+
}`,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "color-mix",
|
|
483
|
+
category: "visual",
|
|
484
|
+
support: { level: "newly", since: 2024 },
|
|
485
|
+
controls: [color("brand", "#0e7490"), select("scheme", ["light", "dark"])],
|
|
486
|
+
resizable: false,
|
|
487
|
+
height: 320,
|
|
488
|
+
check: "color: light-dark(red, red)",
|
|
489
|
+
html: (lang) => `<div class="swatches">
|
|
490
|
+
<div></div><div></div><div></div><div></div><div></div>
|
|
491
|
+
</div>
|
|
492
|
+
<div class="surface">${t(lang, "Uma cor de marca gera a paleta inteira.", "One brand color generates the whole palette.")}</div>
|
|
493
|
+
<button class="btn">${t(lang, "Botão", "Button")}</button>`,
|
|
494
|
+
css: `:root { color-scheme: var(--scheme); }
|
|
495
|
+
|
|
496
|
+
/* light-dark(): um valor para cada tema, sem media query || light-dark(): one value per theme, no media query */
|
|
497
|
+
body {
|
|
498
|
+
background: light-dark(#ffffff, #12151e);
|
|
499
|
+
color: light-dark(#12151e, #f3f4f6);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* color-mix(): tons derivados da mesma cor || color-mix(): shades derived from the same color */
|
|
503
|
+
.swatches { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
504
|
+
.swatches div { flex: 1; height: 56px; border-radius: 10px; }
|
|
505
|
+
.swatches div:nth-child(1) { background: color-mix(in oklch, var(--brand), white 75%); }
|
|
506
|
+
.swatches div:nth-child(2) { background: color-mix(in oklch, var(--brand), white 40%); }
|
|
507
|
+
.swatches div:nth-child(3) { background: var(--brand); }
|
|
508
|
+
.swatches div:nth-child(4) { background: color-mix(in oklch, var(--brand), black 30%); }
|
|
509
|
+
.swatches div:nth-child(5) { background: color-mix(in oklch, var(--brand), black 60%); }
|
|
510
|
+
|
|
511
|
+
.surface {
|
|
512
|
+
padding: 16px;
|
|
513
|
+
border-radius: 12px;
|
|
514
|
+
background: color-mix(in srgb, var(--brand) 15%, transparent);
|
|
515
|
+
border: 2px solid color-mix(in srgb, var(--brand) 50%, transparent);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.btn {
|
|
519
|
+
margin-top: 16px;
|
|
520
|
+
padding: 10px 18px;
|
|
521
|
+
border: 0;
|
|
522
|
+
border-radius: 10px;
|
|
523
|
+
background: var(--brand);
|
|
524
|
+
color: white;
|
|
525
|
+
font: inherit;
|
|
526
|
+
font-weight: 700;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.btn:hover { background: color-mix(in oklch, var(--brand), black 20%); }`,
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "backdrop",
|
|
533
|
+
category: "visual",
|
|
534
|
+
support: { level: "newly", since: 2024 },
|
|
535
|
+
controls: [range("blur", 0, 24, 12)],
|
|
536
|
+
resizable: false,
|
|
537
|
+
height: 300,
|
|
538
|
+
html: (lang) => `<div class="blob one"></div>
|
|
539
|
+
<div class="blob two"></div>
|
|
540
|
+
<div class="glass">${t(lang, "Vidro fosco: o filtro vale para o que está ATRÁS do elemento.", "Frosted glass: the filter applies to what is BEHIND the element.")}</div>`,
|
|
541
|
+
css: `body { position: relative; min-height: 260px; overflow: hidden; background: #fef3c7; }
|
|
542
|
+
|
|
543
|
+
.blob { position: absolute; width: 200px; height: 200px; border-radius: 50%; }
|
|
544
|
+
.one { top: 10px; left: 15%; background: #0e7490; }
|
|
545
|
+
.two { bottom: 0; right: 15%; background: #9a3412; }
|
|
546
|
+
|
|
547
|
+
.glass {
|
|
548
|
+
position: absolute;
|
|
549
|
+
inset: 60px 40px;
|
|
550
|
+
display: grid;
|
|
551
|
+
place-items: center;
|
|
552
|
+
padding: 16px;
|
|
553
|
+
text-align: center;
|
|
554
|
+
font-weight: 700;
|
|
555
|
+
border-radius: 18px;
|
|
556
|
+
border: 1px solid rgb(255 255 255 / 0.7);
|
|
557
|
+
background: rgb(255 255 255 / 0.3);
|
|
558
|
+
box-shadow: 0 8px 32px rgb(0 0 0 / 0.15);
|
|
559
|
+
backdrop-filter: blur(var(--blur)) saturate(160%);
|
|
560
|
+
}`,
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
id: "text-wrap",
|
|
564
|
+
category: "visual",
|
|
565
|
+
support: { level: "newly", since: 2024 },
|
|
566
|
+
controls: [select("wrap", ["wrap", "balance", "pretty"], "balance")],
|
|
567
|
+
resizable: true,
|
|
568
|
+
height: 300,
|
|
569
|
+
check: "text-wrap: balance",
|
|
570
|
+
html: (lang) => `<h2>${t(lang, "Títulos equilibrados em várias linhas, sem palavra sozinha no final", "Balanced headings across lines, with no lonely word at the end")}</h2>
|
|
571
|
+
<p>${t(lang, "Compare wrap, balance e pretty e mude a largura da prévia. balance distribui o texto por igual entre as linhas (ideal para títulos); pretty evita uma palavra solitária na última linha de parágrafos.", "Compare wrap, balance and pretty, and change the preview width. balance spreads text evenly across lines (great for headings); pretty avoids a single word on the last line of paragraphs.")}</p>`,
|
|
572
|
+
css: `h2, p {
|
|
573
|
+
text-wrap: var(--wrap);
|
|
574
|
+
outline: 1px dashed #9ca3af; /* mostra a caixa do texto || shows the text box */
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
h2 { font-size: 30px; line-height: 1.15; }
|
|
578
|
+
p { line-height: 1.6; }`,
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
id: "scroll-driven",
|
|
582
|
+
category: "visual",
|
|
583
|
+
support: { level: "limited" },
|
|
584
|
+
controls: [],
|
|
585
|
+
resizable: false,
|
|
586
|
+
height: 340,
|
|
587
|
+
check: "animation-timeline: scroll()",
|
|
588
|
+
html: (lang) => `<div class="progress"></div>
|
|
589
|
+
${[1, 2, 3, 4, 5, 6].map((n) => `<div class="reveal">${t(lang, "Bloco", "Block")} ${n}</div>`).join("\n")}`,
|
|
590
|
+
css: `/* barra de progresso de leitura: avança com a rolagem da página || reading progress bar: advances as the page scrolls */
|
|
591
|
+
.progress {
|
|
592
|
+
position: fixed;
|
|
593
|
+
inset: 0 0 auto;
|
|
594
|
+
height: 6px;
|
|
595
|
+
background: #0e7490;
|
|
596
|
+
transform-origin: left;
|
|
597
|
+
animation: progress linear both;
|
|
598
|
+
animation-timeline: scroll(root);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
@keyframes progress {
|
|
602
|
+
from { transform: scaleX(0); }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/* cada bloco aparece ao entrar na área visível || each block appears as it enters the viewport */
|
|
606
|
+
.reveal {
|
|
607
|
+
margin: 24px 0;
|
|
608
|
+
padding: 40px;
|
|
609
|
+
border-radius: 14px;
|
|
610
|
+
background: #e5e7eb;
|
|
611
|
+
font-weight: 700;
|
|
612
|
+
animation: reveal linear both;
|
|
613
|
+
animation-timeline: view();
|
|
614
|
+
animation-range: entry 0% cover 30%;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
@keyframes reveal {
|
|
618
|
+
from { opacity: 0; translate: 0 40px; }
|
|
619
|
+
}`,
|
|
620
|
+
},
|
|
621
|
+
]
|
|
622
|
+
|
|
623
|
+
// Comentários das demos são escritos nas duas línguas: /* português || english */
|
|
624
|
+
export function localizeComments(css: string, lang: Lang): string {
|
|
625
|
+
return css.replace(/\/\*\s*(.*?)\s*\|\|\s*(.*?)\s*\*\//g, (_, pt: string, en: string) => `/* ${lang === "pt" ? pt : en} */`)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function demoCss(demo: Demo, lang: Lang): string {
|
|
629
|
+
return localizeComments(demo.css, lang)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export const DEMO_IDS = DEMOS.map((demo) => demo.id)
|
|
633
|
+
|
|
634
|
+
export function findDemo(id: string): Demo {
|
|
635
|
+
return DEMOS.find((demo) => demo.id === id) ?? (DEMOS[0] as Demo)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function initialValues(demo: Demo): string[] {
|
|
639
|
+
return demo.controls.map((control) => String(control.initial))
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function isValid(control: Control, value: string | undefined): value is string {
|
|
643
|
+
if (value === undefined) return false
|
|
644
|
+
if (control.kind === "select") return control.options.includes(value)
|
|
645
|
+
if (control.kind === "color") return /^#[0-9a-f]{6}$/i.test(value)
|
|
646
|
+
const number = Number(value)
|
|
647
|
+
return value !== "" && Number.isFinite(number) && number >= control.min && number <= control.max
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Valores vindos da URL: cada um é validado; inválido ou ausente volta ao padrão
|
|
651
|
+
export function normalizeValues(demo: Demo, values: readonly string[]): string[] {
|
|
652
|
+
return demo.controls.map((control, index) => {
|
|
653
|
+
const value = values[index]
|
|
654
|
+
return isValid(control, value) ? value : String(control.initial)
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function cssValue(control: Control, value: string): string {
|
|
659
|
+
return control.kind === "range" ? `${value}${control.unit}` : value
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Bloco :root com as variáveis dos controles (vazio se a demo não tem controles)
|
|
663
|
+
export function variablesCss(demo: Demo, values: readonly string[]): string {
|
|
664
|
+
if (demo.controls.length === 0) return ""
|
|
665
|
+
const normalized = normalizeValues(demo, values)
|
|
666
|
+
const lines = demo.controls.map((control, index) => ` --${control.name}: ${cssValue(control, normalized[index] ?? String(control.initial))};`)
|
|
667
|
+
return `:root {\n${lines.join("\n")}\n}`
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const BASE_CSS = `*, *::before, *::after { box-sizing: border-box; }
|
|
671
|
+
body { margin: 0; padding: 16px; font: 16px/1.5 system-ui, sans-serif; color: #12151e; background: #ffffff; }
|
|
672
|
+
.css-playground-warning { display: none; margin: 16px 0 0; padding: 10px 12px; border-radius: 8px; background: #fff7ed; color: #9a3412; font-size: 14px; }`
|
|
673
|
+
|
|
674
|
+
// Documento completo da prévia. "</style" no CSS editado é neutralizado para não fechar a tag antes da hora.
|
|
675
|
+
export function previewDocument(options: { demo: Demo, css: string, values: readonly string[], lang: Lang, warning: string }): string {
|
|
676
|
+
const { demo, css, values, lang, warning } = options
|
|
677
|
+
const safe = (text: string) => text.replace(/<\/style/gi, "<\\/style")
|
|
678
|
+
const supports = demo.check ? `@supports not (${demo.check}) { .css-playground-warning { display: block; } }` : ""
|
|
679
|
+
const note = demo.check ? `\n<p class="css-playground-warning">${warning}</p>` : ""
|
|
680
|
+
return `<!doctype html>
|
|
681
|
+
<html lang="${lang === "pt" ? "pt-BR" : "en"}">
|
|
682
|
+
<head>
|
|
683
|
+
<meta charset="utf-8">
|
|
684
|
+
<style>${BASE_CSS}
|
|
685
|
+
${supports}</style>
|
|
686
|
+
<style>${safe(variablesCss(demo, values))}</style>
|
|
687
|
+
<style>${safe(css)}</style>
|
|
688
|
+
</head>
|
|
689
|
+
<body>
|
|
690
|
+
${demo.html(lang)}${note}
|
|
691
|
+
</body>
|
|
692
|
+
</html>`
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Aviso exibido na prévia quando o navegador não suporta o recurso da demo
|
|
696
|
+
export const CSS_WARNING: Record<Lang, string> = {
|
|
697
|
+
pt: "Seu navegador ainda não suporta este recurso, então a prévia não mostra o efeito completo.",
|
|
698
|
+
en: "Your browser doesn't support this feature yet, so the preview doesn't show the full effect.",
|
|
699
|
+
}
|