@thecodecodex/design-system 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/components/Badge.d.ts +5 -0
- package/dist/components/Badge.js +6 -0
- package/dist/components/Checkbox.d.ts +8 -0
- package/dist/components/Checkbox.js +6 -0
- package/dist/components/DataTable.d.ts +13 -0
- package/dist/components/DataTable.js +6 -0
- package/dist/components/KeyValue.d.ts +12 -0
- package/dist/components/KeyValue.js +10 -0
- package/dist/components/NumberField.d.ts +14 -0
- package/dist/components/NumberField.js +6 -0
- package/dist/components/OutputList.d.ts +8 -0
- package/dist/components/OutputList.js +11 -0
- package/dist/components/ParamGroup.d.ts +8 -0
- package/dist/components/ParamGroup.js +13 -0
- package/dist/components/Presets.d.ts +12 -0
- package/dist/components/Presets.js +6 -0
- package/dist/components/RangeField.d.ts +13 -0
- package/dist/components/RangeField.js +12 -0
- package/dist/components/Segmented.d.ts +12 -0
- package/dist/components/Segmented.js +9 -0
- package/dist/components/Stepper.d.ts +11 -0
- package/dist/components/Stepper.js +11 -0
- package/dist/components/TextField.d.ts +15 -0
- package/dist/components/TextField.js +8 -0
- package/dist/components/ToolShell.d.ts +14 -0
- package/dist/components/ToolShell.js +11 -0
- package/dist/components/index.d.ts +17 -0
- package/dist/components/index.js +17 -0
- package/dist/components/labels.d.ts +20 -0
- package/dist/components/labels.js +11 -0
- package/dist/components/panel.d.ts +2 -0
- package/dist/components/panel.js +4 -0
- package/dist/components/textInput.d.ts +10 -0
- package/dist/components/textInput.js +7 -0
- package/dist/components/useCopy.d.ts +4 -0
- package/dist/components/useCopy.js +17 -0
- package/dist/components/useHydrated.d.ts +3 -0
- package/dist/components/useHydrated.js +18 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/inline-script.d.ts +1 -0
- package/dist/theme/inline-script.js +19 -0
- package/dist/theme/theme.d.ts +21 -0
- package/dist/theme/theme.js +92 -0
- package/package.json +47 -4
- package/src/styles/base.css +112 -0
- package/src/styles/index.css +4 -0
- package/src/styles/themes.css +184 -0
- package/src/styles/tokens.css +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Douglas Capeletti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @thecodecodex/design-system
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Design system do [thecodecodex.com](https://thecodecodex.com): tokens e 5 temas para **Tailwind CSS v4** e componentes de formulário em **React** (funcionam também em Preact via `preact/compat`).
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @thecodecodex/design-system
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## CSS
|
|
12
|
+
|
|
13
|
+
No CSS principal do app, depois do Tailwind:
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
@import "tailwindcss";
|
|
17
|
+
@import "@thecodecodex/design-system/styles.css";
|
|
18
|
+
/* para o Tailwind gerar as classes usadas pelos componentes (caminho relativo a este CSS) */
|
|
19
|
+
@source "../node_modules/@thecodecodex/design-system/dist";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`styles.css` = `tokens.css` + `themes.css` + `base.css`. Para não aplicar os estilos base (títulos, foco, tabelas, barras de rolagem), importe só `tokens.css` e `themes.css`.
|
|
23
|
+
|
|
24
|
+
Utilidades geradas: `bg-surface`, `bg-inset`, `bg-ground`, `text-text-primary`, `text-text-secondary`, `bg-btn-primary`, `text-on-accent`, `border-border`, `border-soft`, `text-contrast`, `bg-chart-1`, `bg-diff-add`, `bg-cat-*` etc.
|
|
25
|
+
|
|
26
|
+
### Fontes
|
|
27
|
+
|
|
28
|
+
Os tokens usam `--font-roboto` (display), `--font-poppins` (corpo) e `--font-jetbrains-mono` (mono). Cada app carrega as fontes e define essas variáveis (ex.: `next/font` com `variable: "--font-roboto"`, ou Astro Fonts).
|
|
29
|
+
|
|
30
|
+
## Temas
|
|
31
|
+
|
|
32
|
+
`claro`, `papel`, `crepusculo`, `escuro`, `contraste`, mais as preferências `auto` (por horário) e `sistema`. A classe `.theme-<nome>` vai no `<html>`; temas escuros recebem também `.dark` (variante `dark:` do Tailwind).
|
|
33
|
+
|
|
34
|
+
Script anti-flash no `<head>`, antes do primeiro paint:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Next.js (app/layout.tsx)
|
|
38
|
+
import { themeInitScript } from "@thecodecodex/design-system/theme"
|
|
39
|
+
|
|
40
|
+
<html suppressHydrationWarning>
|
|
41
|
+
<head><script dangerouslySetInnerHTML={{ __html: themeInitScript }} /></head>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```astro
|
|
45
|
+
---
|
|
46
|
+
import { themeInitScript } from "@thecodecodex/design-system/theme"
|
|
47
|
+
---
|
|
48
|
+
<script is:inline set:html={themeInitScript} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
No cliente: `watchTheme()` mantém o tema certo (virada de horário, mudança no SO, outra aba); `setPreference("papel")` troca e salva. O evento `themechange` é disparado em `document`.
|
|
52
|
+
|
|
53
|
+
## Componentes
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { Badge, NumberField, Segmented, ToolShell, ParamGroup } from "@thecodecodex/design-system"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Badge`, `Checkbox`, `DataTable`, `KeyValue`, `NumberField`, `OutputList`, `ParamGroup`, `Presets`, `RangeField`, `Segmented`, `Stepper`, `TextField`, `ToolShell`, as classes `panel`/`panelHeader` e os hooks `useCopy` e `useHydrated`.
|
|
60
|
+
|
|
61
|
+
Textos fixos vêm em pt-BR; passe `labels` para traduzir (ex.: `<OutputList labels={{ copy: "Copy", copied: "Copied!" }} … />`).
|
|
62
|
+
|
|
63
|
+
### Preact
|
|
64
|
+
|
|
65
|
+
Com `preact({ compat: true })` (Astro) ou o alias `react → preact/compat` no bundler, os componentes funcionam sem mudanças.
|
|
66
|
+
|
|
67
|
+
No Astro, empacote o pacote também no SSR/prerender, senão o Node carrega o React real (instalado como peer) e os hooks quebram:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// astro.config.mjs
|
|
71
|
+
const bundled = { resolve: { noExternal: ["@thecodecodex/design-system"] } }
|
|
72
|
+
export default defineConfig({
|
|
73
|
+
integrations: [preact({ compat: true })],
|
|
74
|
+
vite: { environments: { ssr: bundled, prerender: bundled } },
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Desenvolvimento
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pnpm install
|
|
82
|
+
pnpm check # tsc + eslint + vitest
|
|
83
|
+
pnpm build # gera dist/
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Publicação: atualize `version` no `package.json`, faça a tag `vX.Y.Z` e envie; o workflow `publish.yml` publica no npm com provenance.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Selo de status: ok (válido), error (inválido) ou neutral
|
|
3
|
+
export default function Badge({ tone, children }) {
|
|
4
|
+
const styles = { ok: "bg-btn-primary text-on-accent", error: "bg-contrast text-on-accent", neutral: "border-2 border-border" }[tone];
|
|
5
|
+
return _jsx("span", { className: `inline-flex items-center rounded-md px-2.5 py-1 text-[13px] font-bold ${styles}`, children: children });
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Linha inteira clicável, em recuo (bg-inset) para se destacar dos campos de texto
|
|
4
|
+
export default function Checkbox({ label, checked, onChecked }) {
|
|
5
|
+
return (_jsxs("label", { className: "flex min-h-11 cursor-pointer items-center gap-3 rounded-[10px] bg-inset px-3 py-2 text-sm font-semibold has-[:focus-visible]:outline-3 has-[:focus-visible]:outline-contrast", children: [_jsx("input", { type: "checkbox", className: "size-5 flex-none accent-[var(--btn-primary)]", checked: checked, onChange: (e) => onChecked(e.currentTarget.checked) }), label] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface DataTableRow {
|
|
3
|
+
key: string;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
cells: ReactNode[];
|
|
6
|
+
highlight?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
headers: ReactNode[];
|
|
10
|
+
rows: DataTableRow[];
|
|
11
|
+
}
|
|
12
|
+
export default function DataTable({ headers, rows }: Props): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const cellClass = "border-0 px-4 py-3";
|
|
3
|
+
// `plain` desliga o estilo de tabela dos artigos (.post table) definido em global.css
|
|
4
|
+
export default function DataTable({ headers, rows }) {
|
|
5
|
+
return (_jsx("div", { className: "overflow-x-auto rounded-2xl border-2 border-border", children: _jsxs("table", { className: "plain w-full border-collapse whitespace-nowrap text-left text-sm", children: [_jsx("thead", { className: "bg-text-primary text-bg-primary", children: _jsx("tr", { children: headers.map((header, i) => (_jsx("th", { className: `${cellClass} border-0 font-semibold`, children: header }, i))) }) }), _jsx("tbody", { className: "font-mono", children: rows.map((row) => (_jsxs("tr", { className: `border-b border-border/15 last:border-b-0 ${row.highlight ? "bg-cat-note text-cat-fg" : "hover:bg-bg-secondary"}`, children: [_jsx("td", { className: `${cellClass} font-body font-semibold`, children: row.label }), row.cells.map((cell, i) => (_jsx("td", { className: `${cellClass} ${row.highlight ? "font-bold" : ""}`, children: cell }, i)))] }, row.key))) })] }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type CopyLabels } from "./labels.js";
|
|
3
|
+
interface Props {
|
|
4
|
+
labels?: Partial<CopyLabels>;
|
|
5
|
+
rows: {
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
value: string;
|
|
8
|
+
copyable?: boolean;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
export default function KeyValue({ labels, rows }: Props): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { copyLabels } from "./labels.js";
|
|
4
|
+
import { useCopy } from "./useCopy.js";
|
|
5
|
+
// Tabela de rótulo → valor (resultados de conversões), com copiar por linha
|
|
6
|
+
export default function KeyValue({ labels, rows }) {
|
|
7
|
+
const text = { ...copyLabels, ...labels };
|
|
8
|
+
const { copied, copy } = useCopy();
|
|
9
|
+
return (_jsx("dl", { className: "flex flex-col overflow-hidden rounded-[14px] border-2 border-border", children: rows.map((row, i) => (_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 border-border/20 px-4 py-2.5 [&:not(:first-child)]:border-t-[1.5px]", children: [_jsx("dt", { className: "w-40 flex-none text-sm font-semibold text-text-secondary", children: row.label }), _jsx("dd", { className: "m-0 min-w-0 flex-1 break-all font-mono text-[15px]", children: row.value }), row.copyable !== false && row.value && (_jsx("button", { type: "button", className: "text-sm font-bold text-btn-primary hover:text-contrast", onClick: () => copy(row.value, `${i}`), "aria-live": "polite", children: copied === `${i}` ? text.copied : text.copy }))] }, i))) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
value: number;
|
|
5
|
+
onValue: (value: number) => void;
|
|
6
|
+
step?: number;
|
|
7
|
+
prefix?: string;
|
|
8
|
+
suffix?: string;
|
|
9
|
+
hint?: ReactNode;
|
|
10
|
+
large?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export default function NumberField({ label, value, onValue, step, prefix, suffix, hint, large, className }: Props): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { onTextInput } from "./textInput.js";
|
|
4
|
+
export default function NumberField({ label, value, onValue, step = 1, prefix, suffix, hint, large = false, className = "" }) {
|
|
5
|
+
return (_jsxs("label", { className: `flex flex-col gap-1.5 text-sm font-semibold ${className}`, children: [label, _jsxs("span", { className: `flex items-stretch overflow-hidden rounded-[10px] border-2 border-border bg-surface font-mono focus-within:outline-3 focus-within:outline-offset-1 focus-within:outline-contrast ${large ? "h-14 text-[22px] font-bold" : "h-12 text-base"}`, children: [prefix && _jsx("span", { className: "flex flex-none items-center border-r-[1.5px] border-soft bg-inset px-3 font-body text-sm font-semibold text-text-secondary", children: prefix }), _jsx("input", { type: "number", inputMode: "decimal", step: step, className: "min-w-0 flex-1 bg-transparent px-3 text-text-primary outline-none", value: value, ...onTextInput((raw) => onValue(Number(raw) || 0)) }), suffix && _jsx("span", { className: "flex flex-none items-center border-l-[1.5px] border-soft bg-inset px-3 font-body text-sm font-semibold text-text-secondary", children: suffix })] }), hint && _jsx("span", { className: "text-[13px] font-normal text-text-secondary", children: hint })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type OutputListLabels } from "./labels.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
labels?: Partial<OutputListLabels>;
|
|
4
|
+
values: string[];
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
export default function OutputList({ labels, values, label }: Props): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { outputListLabels } from "./labels.js";
|
|
4
|
+
import { useCopy } from "./useCopy.js";
|
|
5
|
+
const button = "inline-flex h-10 flex-none items-center rounded-[10px] border-2 border-border bg-bg-primary px-3 text-sm font-bold hover:border-contrast";
|
|
6
|
+
// Lista de valores gerados, cada um com "copiar", e um "copiar todos" (um por linha)
|
|
7
|
+
export default function OutputList({ labels, values, label }) {
|
|
8
|
+
const text = { ...outputListLabels, ...labels };
|
|
9
|
+
const { copied, copy } = useCopy();
|
|
10
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("h2", { className: "m-0 text-base tracking-[0.04em] sm:text-lg", children: label ?? text.result }), values.length > 1 && (_jsx("button", { type: "button", className: button, onClick: () => copy(values.join("\n"), "__all"), "aria-live": "polite", children: copied === "__all" ? text.copied : text.copyAll }))] }), _jsx("ul", { className: "flex flex-col gap-2", "data-test": "tool-output", children: values.map((value, i) => (_jsxs("li", { className: "flex items-center gap-3 rounded-[14px] border-2 border-border bg-bg-primary py-2 pl-4 pr-2", children: [_jsx("code", { className: "min-w-0 flex-1 break-all font-mono text-[15px] sm:text-base", "data-test": "tool-value", children: value }), _jsx("button", { type: "button", className: button, onClick: () => copy(value, `${i}`), "aria-live": "polite", children: copied === `${i}` ? text.copied : text.copy })] }, `${i}-${value}`))) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Número do bloco: contador CSS zerado no painel de parâmetros (ToolShell), então blocos condicionais não deixam buracos
|
|
3
|
+
const badge = "inline-flex size-5 flex-none items-center justify-center rounded-md bg-text-primary font-mono text-[11px] font-bold not-italic text-bg-primary before:content-[counter(param)]";
|
|
4
|
+
const heading = "flex items-center gap-2 text-xs font-bold uppercase tracking-[0.08em] text-text-secondary";
|
|
5
|
+
const block = "border-soft [counter-increment:param] [&:not(:first-child)]:border-t-[1.5px]";
|
|
6
|
+
const body = "flex flex-col gap-4";
|
|
7
|
+
// Bloco numerado do painel de parâmetros
|
|
8
|
+
export default function ParamGroup({ title, children, collapsible = false }) {
|
|
9
|
+
if (collapsible && title) {
|
|
10
|
+
return (_jsxs("details", { open: true, className: `group ${block}`, children: [_jsxs("summary", { className: `${heading} min-h-[52px] cursor-pointer list-none px-5 sm:px-6 [&::-webkit-details-marker]:hidden`, children: [_jsx("span", { "aria-hidden": "true", className: badge }), _jsx("span", { children: title }), _jsx("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", className: "ml-auto transition-transform group-open:rotate-180", children: _jsx("path", { d: "M6 9l6 6 6-6" }) })] }), _jsx("div", { className: `${body} px-5 pb-5 sm:px-6 sm:pb-6`, children: children })] }));
|
|
11
|
+
}
|
|
12
|
+
return (_jsxs("div", { role: title ? "group" : undefined, "aria-label": title, className: `${block} ${body} p-5 sm:p-6`, children: [title && (_jsxs("span", { className: heading, children: [_jsx("span", { "aria-hidden": "true", className: badge }), title] })), children] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props<T> {
|
|
3
|
+
label?: ReactNode;
|
|
4
|
+
value: T;
|
|
5
|
+
options: {
|
|
6
|
+
value: T;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
onValue: (value: T) => void;
|
|
10
|
+
}
|
|
11
|
+
export default function Presets<T>({ label, value, options, onValue }: Props<T>): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Valores de exemplo em pílulas, logo abaixo do campo que eles preenchem
|
|
4
|
+
export default function Presets({ label, value, options, onValue }) {
|
|
5
|
+
return (_jsxs("div", { className: "-mt-1 flex flex-wrap items-center gap-1.5", children: [label && _jsx("span", { className: "mr-0.5 text-[13px] font-semibold text-text-secondary", children: label }), options.map((option) => (_jsx("button", { type: "button", "aria-pressed": option.value === value, className: "h-8 min-w-10 rounded-full border-[1.5px] border-soft bg-surface px-2.5 font-mono text-xs font-semibold hover:border-contrast aria-pressed:border-text-primary aria-pressed:bg-text-primary aria-pressed:text-bg-primary", onClick: () => onValue(option.value), children: option.label }, String(option.value))))] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
value: number;
|
|
5
|
+
onValue: (value: number) => void;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
unit?: string;
|
|
10
|
+
hint?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export default function RangeField({ label, value, onValue, min, max, step, unit, hint }: Props): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { onTextInput } from "./textInput.js";
|
|
4
|
+
// Slider para ajuste rápido + número para valor exato (o slider sozinho é impreciso, principalmente no celular)
|
|
5
|
+
export default function RangeField({ label, value, onValue, min, max, step = 1, unit, hint }) {
|
|
6
|
+
const set = (raw) => {
|
|
7
|
+
const number = Number(raw);
|
|
8
|
+
if (Number.isFinite(number))
|
|
9
|
+
onValue(number);
|
|
10
|
+
};
|
|
11
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm font-semibold", children: [label, _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("input", { type: "range", min: min, max: max, step: step, value: value, className: "h-11 min-w-0 flex-1 accent-[var(--btn-primary)]", ...onTextInput(set) }), _jsxs("span", { className: "flex h-12 w-28 flex-none items-center gap-1.5 rounded-[10px] border-2 border-border bg-bg-primary px-3 font-mono focus-within:outline-3 focus-within:outline-offset-1 focus-within:outline-contrast", children: [_jsx("input", { type: "number", inputMode: "decimal", min: min, max: max, step: step, value: value, "aria-label": typeof label === "string" ? label : undefined, className: "min-w-0 flex-1 bg-transparent text-base text-text-primary outline-none", ...onTextInput(set) }), unit && _jsx("span", { className: "text-sm font-bold text-text-secondary", children: unit })] })] })] }), hint && _jsx("span", { className: "text-[13px] font-normal leading-snug text-text-secondary", children: hint })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props<T extends string | number> {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
value: T;
|
|
5
|
+
options: {
|
|
6
|
+
value: T;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
}[];
|
|
9
|
+
onValue: (value: T) => void;
|
|
10
|
+
}
|
|
11
|
+
export default function Segmented<T extends string | number>({ label, value, options, onValue }: Props<T>): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// Grupo de opções exclusivas (radio) com cara de botões
|
|
4
|
+
export default function Segmented({ label, value, options, onValue }) {
|
|
5
|
+
return (_jsxs("fieldset", { className: "flex flex-col gap-1.5", children: [_jsx("legend", { className: "mb-1.5 text-sm font-semibold", children: label }), _jsx("div", { className: "flex flex-wrap gap-2", children: options.map((option) => {
|
|
6
|
+
const active = option.value === value;
|
|
7
|
+
return (_jsxs("label", { className: `inline-flex min-h-11 cursor-pointer items-center rounded-[10px] border-2 border-border px-3.5 text-sm font-bold has-[:focus-visible]:outline-3 has-[:focus-visible]:outline-contrast ${active ? "bg-text-primary text-bg-primary" : "bg-bg-primary hover:border-contrast"}`, children: [_jsx("input", { type: "radio", className: "sr-only", checked: active, onChange: () => onValue(option.value) }), option.label] }, String(option.value)));
|
|
8
|
+
}) })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
value: number;
|
|
5
|
+
onValue: (value: number) => void;
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
hint?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export default function Stepper({ label, value, onValue, min, max, hint }: Props): import("react").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { onTextInput } from "./textInput.js";
|
|
4
|
+
import { useId } from "react";
|
|
5
|
+
// Inteiro pequeno (dependentes, quantidade): − valor +, com o campo ainda editável
|
|
6
|
+
export default function Stepper({ label, value, onValue, min = 0, max = Number.MAX_SAFE_INTEGER, hint }) {
|
|
7
|
+
const id = useId();
|
|
8
|
+
const set = (next) => onValue(Math.min(max, Math.max(min, Math.round(next) || min)));
|
|
9
|
+
const button = "flex w-11 flex-none items-center justify-center bg-inset text-lg font-bold hover:text-contrast disabled:cursor-not-allowed disabled:opacity-40";
|
|
10
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("label", { htmlFor: id, className: "flex min-w-0 flex-col gap-0.5 text-sm font-semibold", children: [label, hint && _jsx("span", { className: "text-[13px] font-normal leading-snug text-text-secondary", children: hint })] }), _jsxs("span", { className: "flex h-11 flex-none overflow-hidden rounded-[10px] border-2 border-border focus-within:outline-3 focus-within:outline-offset-1 focus-within:outline-contrast", children: [_jsx("button", { type: "button", className: button, disabled: value <= min, onClick: () => set(value - 1), "aria-label": typeof label === "string" ? `− ${label}` : "−", "aria-controls": id, children: "\u2212" }), _jsx("input", { id: id, type: "number", inputMode: "numeric", min: min, max: max === Number.MAX_SAFE_INTEGER ? undefined : max, step: 1, value: value, ...onTextInput((raw) => set(Number(raw))), className: "w-14 border-x-[1.5px] border-soft bg-surface text-center font-mono text-base font-bold text-text-primary outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" }), _jsx("button", { type: "button", className: button, disabled: value >= max, onClick: () => set(value + 1), "aria-label": typeof label === "string" ? `+ ${label}` : "+", "aria-controls": id, children: "+" })] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
label: ReactNode;
|
|
4
|
+
value: string;
|
|
5
|
+
onValue: (value: string) => void;
|
|
6
|
+
placeholder?: string | undefined;
|
|
7
|
+
multiline?: boolean;
|
|
8
|
+
rows?: number;
|
|
9
|
+
type?: "text" | "datetime-local";
|
|
10
|
+
invalid?: boolean;
|
|
11
|
+
hint?: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
export default function TextField({ label, value, onValue, placeholder, multiline, rows, type, invalid, hint, className }: Props): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { onTextInput } from "./textInput.js";
|
|
4
|
+
const box = "rounded-[10px] border-2 bg-bg-primary px-3 font-mono text-base text-text-primary outline-none focus:outline-3 focus:outline-offset-1 focus:outline-contrast";
|
|
5
|
+
export default function TextField({ label, value, onValue, placeholder, multiline = false, rows = 4, type = "text", invalid = false, hint, className = "" }) {
|
|
6
|
+
const border = invalid ? "border-contrast" : "border-border";
|
|
7
|
+
return (_jsxs("label", { className: `flex flex-col gap-1.5 text-sm font-semibold ${className}`, children: [label, multiline ? (_jsx("textarea", { rows: rows, spellCheck: false, className: `${box} ${border} min-h-24 resize-y py-2.5 leading-relaxed`, value: value, placeholder: placeholder, ...onTextInput(onValue) })) : (_jsx("input", { type: type, step: type === "datetime-local" ? 1 : undefined, spellCheck: false, autoComplete: "off", className: `${box} ${border} h-12`, value: value, placeholder: placeholder, ...onTextInput(onValue) })), hint && _jsx("span", { className: "text-[13px] font-normal text-text-secondary", children: hint })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type ToolShellLabels } from "./labels.js";
|
|
3
|
+
interface Props {
|
|
4
|
+
labels?: Partial<ToolShellLabels>;
|
|
5
|
+
ready: boolean;
|
|
6
|
+
params?: ReactNode;
|
|
7
|
+
paramsTitle?: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
how?: ReactNode;
|
|
10
|
+
sourceUrl?: string;
|
|
11
|
+
notice?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export default function ToolShell({ labels, ready, params, paramsTitle, children, how, sourceUrl, notice }: Props): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { toolShellLabels } from "./labels.js";
|
|
4
|
+
import { useCopy } from "./useCopy.js";
|
|
5
|
+
import { panel, panelHeader } from "./panel.js";
|
|
6
|
+
// Layout comum das ferramentas: parâmetros + resultado, "Como funciona?" e aviso de privacidade
|
|
7
|
+
export default function ToolShell({ labels, ready, params, paramsTitle, children, how, sourceUrl, notice }) {
|
|
8
|
+
const text = { ...toolShellLabels, ...labels };
|
|
9
|
+
const { copied, copy } = useCopy(2000);
|
|
10
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", "data-tool": ready ? "ready" : "loading", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2 md:justify-end", children: [_jsx("span", { className: "text-[13px] text-text-secondary", children: text.shareHint }), _jsxs("button", { type: "button", className: "inline-flex h-11 items-center gap-2 rounded-[10px] bg-text-primary px-4 text-sm font-bold text-bg-primary hover:bg-bg-tertiary-hover", onClick: () => copy(window.location.href, "link"), "aria-live": "polite", "data-test": "copy-link", children: [_jsx("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: _jsx("path", { d: "M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1" }) }), copied === "link" ? text.linkCopied : text.copyLink] })] }), notice && _jsx("p", { className: "m-0 rounded-[14px] border-2 border-dashed border-soft bg-surface px-4 py-3 text-sm leading-relaxed text-text-secondary", children: notice }), _jsxs("div", { className: "flex flex-wrap items-start gap-6", children: [params && (_jsxs("form", { className: `${panel} flex min-w-0 flex-[1_1_320px] flex-col md:max-w-[440px]`, onSubmit: (e) => e.preventDefault(), children: [_jsx("span", { className: panelHeader, children: paramsTitle ?? text.params }), _jsx("div", { className: "flex flex-col [counter-reset:param]", children: params })] })), _jsx("section", { className: `${panel} flex min-w-0 flex-[999_1_480px] flex-col gap-5 p-5 sm:p-6`, children: children })] }), (how || sourceUrl) && (_jsxs("details", { id: "how", className: `${panel} scroll-mt-24 px-5`, children: [_jsx("summary", { className: "flex min-h-[52px] cursor-pointer items-center font-bold", children: text.how }), how && _jsx("div", { className: "mb-3 mt-1 max-w-4xl text-sm leading-relaxed text-text-secondary", children: how }), sourceUrl && (_jsx("a", { href: sourceUrl, className: "mb-4 inline-block font-mono text-sm font-bold text-btn-primary hover:text-contrast", children: text.howCode }))] })), _jsxs("p", { className: "m-0 flex items-center gap-2 text-[13px] text-text-secondary", children: [_jsxs("svg", { "aria-hidden": "true", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("rect", { x: "5", y: "11", width: "14", height: "10", rx: "2" }), _jsx("path", { d: "M8 11V7a4 4 0 0 1 8 0v4" })] }), text.private] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as Badge } from "./Badge.js";
|
|
2
|
+
export { default as Checkbox } from "./Checkbox.js";
|
|
3
|
+
export { default as DataTable, type DataTableRow } from "./DataTable.js";
|
|
4
|
+
export { default as KeyValue } from "./KeyValue.js";
|
|
5
|
+
export { default as NumberField } from "./NumberField.js";
|
|
6
|
+
export { default as OutputList } from "./OutputList.js";
|
|
7
|
+
export { default as ParamGroup } from "./ParamGroup.js";
|
|
8
|
+
export { default as Presets } from "./Presets.js";
|
|
9
|
+
export { default as RangeField } from "./RangeField.js";
|
|
10
|
+
export { default as Segmented } from "./Segmented.js";
|
|
11
|
+
export { default as Stepper } from "./Stepper.js";
|
|
12
|
+
export { default as TextField } from "./TextField.js";
|
|
13
|
+
export { default as ToolShell } from "./ToolShell.js";
|
|
14
|
+
export { panel, panelHeader } from "./panel.js";
|
|
15
|
+
export { useCopy } from "./useCopy.js";
|
|
16
|
+
export { readParam, useHydrated, writeParams } from "./useHydrated.js";
|
|
17
|
+
export * from "./labels.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { default as Badge } from "./Badge.js";
|
|
2
|
+
export { default as Checkbox } from "./Checkbox.js";
|
|
3
|
+
export { default as DataTable } from "./DataTable.js";
|
|
4
|
+
export { default as KeyValue } from "./KeyValue.js";
|
|
5
|
+
export { default as NumberField } from "./NumberField.js";
|
|
6
|
+
export { default as OutputList } from "./OutputList.js";
|
|
7
|
+
export { default as ParamGroup } from "./ParamGroup.js";
|
|
8
|
+
export { default as Presets } from "./Presets.js";
|
|
9
|
+
export { default as RangeField } from "./RangeField.js";
|
|
10
|
+
export { default as Segmented } from "./Segmented.js";
|
|
11
|
+
export { default as Stepper } from "./Stepper.js";
|
|
12
|
+
export { default as TextField } from "./TextField.js";
|
|
13
|
+
export { default as ToolShell } from "./ToolShell.js";
|
|
14
|
+
export { panel, panelHeader } from "./panel.js";
|
|
15
|
+
export { useCopy } from "./useCopy.js";
|
|
16
|
+
export { readParam, useHydrated, writeParams } from "./useHydrated.js";
|
|
17
|
+
export * from "./labels.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CopyLabels {
|
|
2
|
+
copy: string;
|
|
3
|
+
copied: string;
|
|
4
|
+
}
|
|
5
|
+
export interface OutputListLabels extends CopyLabels {
|
|
6
|
+
copyAll: string;
|
|
7
|
+
result: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolShellLabels {
|
|
10
|
+
shareHint: string;
|
|
11
|
+
copyLink: string;
|
|
12
|
+
linkCopied: string;
|
|
13
|
+
params: string;
|
|
14
|
+
how: string;
|
|
15
|
+
howCode: string;
|
|
16
|
+
private: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const copyLabels: CopyLabels;
|
|
19
|
+
export declare const outputListLabels: OutputListLabels;
|
|
20
|
+
export declare const toolShellLabels: ToolShellLabels;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const copyLabels = { copy: "Copiar", copied: "Copiado!" };
|
|
2
|
+
export const outputListLabels = { ...copyLabels, copyAll: "Copiar todos", result: "Resultado" };
|
|
3
|
+
export const toolShellLabels = {
|
|
4
|
+
shareHint: "O link guarda o que está na tela.",
|
|
5
|
+
copyLink: "Copiar link com valores",
|
|
6
|
+
linkCopied: "Link copiado!",
|
|
7
|
+
params: "Parâmetros",
|
|
8
|
+
how: "Como é calculado?",
|
|
9
|
+
howCode: "Ver o código ↗",
|
|
10
|
+
private: "Processado no seu navegador; nada é enviado.",
|
|
11
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const panel = "rounded-[20px] border-[1.5px] border-soft bg-surface";
|
|
2
|
+
export declare const panelHeader = "flex items-center gap-3 rounded-t-[20px] border-b-[1.5px] border-soft bg-inset px-5 py-3.5 font-display text-sm font-extrabold uppercase tracking-[0.08em] sm:px-6";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Painéis das ferramentas: superfície (bg-primary) sobre o fundo da página (--ground), com borda suave
|
|
2
|
+
export const panel = "rounded-[20px] border-[1.5px] border-soft bg-surface";
|
|
3
|
+
// Faixa de título no topo de um painel (ex.: "Parâmetros")
|
|
4
|
+
export const panelHeader = "flex items-center gap-3 rounded-t-[20px] border-b-[1.5px] border-soft bg-inset px-5 py-3.5 font-display text-sm font-extrabold uppercase tracking-[0.08em] sm:px-6";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type TextElement = HTMLInputElement | HTMLTextAreaElement;
|
|
2
|
+
export declare function onTextInput<T extends TextElement>(handler: (value: string) => void): {
|
|
3
|
+
onChange: (event: {
|
|
4
|
+
currentTarget: T;
|
|
5
|
+
}) => void;
|
|
6
|
+
onInput: (event: {
|
|
7
|
+
currentTarget: T;
|
|
8
|
+
}) => void;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Dispara a cada tecla em React e em Preact: o React precisa de onChange (input controlado),
|
|
2
|
+
// e o Preact sem compat (ou com o jsx-runtime puro) só dispara onChange no blur, então registra também onInput.
|
|
3
|
+
// Os dois chamam o mesmo handler com o mesmo valor, o que é idempotente.
|
|
4
|
+
export function onTextInput(handler) {
|
|
5
|
+
const listener = (event) => handler(event.currentTarget.value);
|
|
6
|
+
return { onChange: listener, onInput: listener };
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
// Copia para a área de transferência e mantém o estado "copiado" por alguns segundos
|
|
4
|
+
export function useCopy(timeout = 1500) {
|
|
5
|
+
const [copied, setCopied] = useState(undefined);
|
|
6
|
+
const copy = async (value, id = value) => {
|
|
7
|
+
try {
|
|
8
|
+
await navigator.clipboard.writeText(value);
|
|
9
|
+
setCopied(id);
|
|
10
|
+
window.setTimeout(() => setCopied((current) => (current === id ? undefined : current)), timeout);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// sem permissão de clipboard: o usuário ainda pode selecionar o texto
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return { copied, copy };
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
// false no HTML estático e true após a hidratação: valores aleatórios e leitura da URL só acontecem no cliente
|
|
4
|
+
export function useHydrated() {
|
|
5
|
+
const [hydrated, setHydrated] = useState(false);
|
|
6
|
+
useEffect(() => setHydrated(true), []);
|
|
7
|
+
return hydrated;
|
|
8
|
+
}
|
|
9
|
+
// Lê um parâmetro da URL uma única vez, após hidratar
|
|
10
|
+
export function readParam(name) {
|
|
11
|
+
return typeof window === "undefined" ? null : new URLSearchParams(window.location.search).get(name);
|
|
12
|
+
}
|
|
13
|
+
// Atualiza (ou remove) parâmetros da URL sem recarregar, para links compartilháveis
|
|
14
|
+
export function writeParams(values) {
|
|
15
|
+
const url = new URL(window.location.href);
|
|
16
|
+
Object.entries(values).forEach(([key, value]) => (value ? url.searchParams.set(key, value) : url.searchParams.delete(key)));
|
|
17
|
+
window.history.replaceState(null, "", url);
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const themeInitScript: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AUTO_SCHEDULE, DARK_THEMES, STORAGE_KEY, THEMES } from "./theme.js";
|
|
2
|
+
// Script para o <head>, antes do primeiro paint (sem flash): versão mínima de resolveTheme + applyTheme.
|
|
3
|
+
// Gerado a partir das constantes de theme.ts, então horários e nomes nunca ficam dessincronizados.
|
|
4
|
+
export const themeInitScript = `(() => {
|
|
5
|
+
const themes = ${JSON.stringify(THEMES)}
|
|
6
|
+
const schedule = ${JSON.stringify([...AUTO_SCHEDULE].reverse())}
|
|
7
|
+
let pref = null
|
|
8
|
+
try { pref = localStorage.getItem(${JSON.stringify(STORAGE_KEY)}) } catch { /* modo privado */ }
|
|
9
|
+
if (pref === "light") pref = "claro"
|
|
10
|
+
if (pref === "dark") pref = "escuro"
|
|
11
|
+
if (!["auto", "sistema", ...themes].includes(pref)) pref = "auto"
|
|
12
|
+
const h = new Date().getHours()
|
|
13
|
+
const auto = (schedule.find((slot) => h >= slot.from) || schedule[0]).theme
|
|
14
|
+
const theme = pref === "auto" ? auto : pref === "sistema" ? (matchMedia("(prefers-color-scheme: dark)").matches ? "escuro" : "claro") : pref
|
|
15
|
+
const root = document.documentElement
|
|
16
|
+
root.classList.add("theme-" + theme)
|
|
17
|
+
if (${JSON.stringify([...DARK_THEMES])}.includes(theme)) root.classList.add("dark")
|
|
18
|
+
root.dataset.theme = theme
|
|
19
|
+
})()`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const THEMES: readonly ["claro", "papel", "crepusculo", "escuro", "contraste"];
|
|
2
|
+
export type Theme = (typeof THEMES)[number];
|
|
3
|
+
export declare const THEME_PREFERENCES: readonly ["auto", "sistema", "claro", "papel", "crepusculo", "escuro", "contraste"];
|
|
4
|
+
export type ThemePreference = (typeof THEME_PREFERENCES)[number];
|
|
5
|
+
export declare const DEFAULT_PREFERENCE: ThemePreference;
|
|
6
|
+
export declare const STORAGE_KEY = "theme";
|
|
7
|
+
export declare const DARK_THEMES: ReadonlySet<Theme>;
|
|
8
|
+
export declare const AUTO_SCHEDULE: ReadonlyArray<{
|
|
9
|
+
from: number;
|
|
10
|
+
theme: Theme;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const AUTO_RECHECK_MS: number;
|
|
13
|
+
export declare function themeForHour(hour: number): Theme;
|
|
14
|
+
export declare function normalizePreference(raw: string | null | undefined): ThemePreference;
|
|
15
|
+
export declare function resolveTheme(preference: ThemePreference, hour: number, systemDark: boolean): Theme;
|
|
16
|
+
export declare function isDarkTheme(theme: Theme): boolean;
|
|
17
|
+
export declare function readPreference(): ThemePreference;
|
|
18
|
+
export declare function applyTheme(theme: Theme, root?: HTMLElement): void;
|
|
19
|
+
export declare function currentTheme(preference?: ThemePreference): Theme;
|
|
20
|
+
export declare function setPreference(preference: ThemePreference): Theme;
|
|
21
|
+
export declare function watchTheme(): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Temas do design system: a classe .theme-<nome> vai no <html> (valores em src/styles/themes.css).
|
|
2
|
+
// O script anti-flash (inline-script.ts) é gerado a partir destas constantes.
|
|
3
|
+
export const THEMES = ["claro", "papel", "crepusculo", "escuro", "contraste"];
|
|
4
|
+
export const THEME_PREFERENCES = ["auto", "sistema", ...THEMES];
|
|
5
|
+
export const DEFAULT_PREFERENCE = "auto";
|
|
6
|
+
export const STORAGE_KEY = "theme";
|
|
7
|
+
// Temas que recebem também a classe .dark (variante dark: do Tailwind, Shiki e Mermaid escuros)
|
|
8
|
+
export const DARK_THEMES = new Set(["crepusculo", "escuro", "contraste"]);
|
|
9
|
+
// Modo Auto: cada tema vale a partir da hora indicada (horário local). Antes da primeira faixa (madrugada) vale o tema da noite.
|
|
10
|
+
const NIGHT_THEME = "escuro";
|
|
11
|
+
export const AUTO_SCHEDULE = [
|
|
12
|
+
{ from: 6, theme: "claro" },
|
|
13
|
+
{ from: 12, theme: "papel" },
|
|
14
|
+
{ from: 17, theme: "crepusculo" },
|
|
15
|
+
{ from: 20, theme: NIGHT_THEME },
|
|
16
|
+
];
|
|
17
|
+
export const AUTO_RECHECK_MS = 5 * 60 * 1000;
|
|
18
|
+
export function themeForHour(hour) {
|
|
19
|
+
const current = [...AUTO_SCHEDULE].reverse().find((slot) => hour >= slot.from);
|
|
20
|
+
return current?.theme ?? NIGHT_THEME;
|
|
21
|
+
}
|
|
22
|
+
// Aceita valores antigos ("light"/"dark") e descarta lixo do localStorage
|
|
23
|
+
export function normalizePreference(raw) {
|
|
24
|
+
if (raw === "light")
|
|
25
|
+
return "claro";
|
|
26
|
+
if (raw === "dark")
|
|
27
|
+
return "escuro";
|
|
28
|
+
return THEME_PREFERENCES.includes(raw ?? "") ? raw : DEFAULT_PREFERENCE;
|
|
29
|
+
}
|
|
30
|
+
export function resolveTheme(preference, hour, systemDark) {
|
|
31
|
+
if (preference === "auto")
|
|
32
|
+
return themeForHour(hour);
|
|
33
|
+
if (preference === "sistema")
|
|
34
|
+
return systemDark ? "escuro" : "claro";
|
|
35
|
+
return preference;
|
|
36
|
+
}
|
|
37
|
+
export function isDarkTheme(theme) {
|
|
38
|
+
return DARK_THEMES.has(theme);
|
|
39
|
+
}
|
|
40
|
+
// --- Navegador ---
|
|
41
|
+
export function readPreference() {
|
|
42
|
+
try {
|
|
43
|
+
return normalizePreference(localStorage.getItem(STORAGE_KEY));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return DEFAULT_PREFERENCE;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function systemPrefersDark() {
|
|
50
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
51
|
+
}
|
|
52
|
+
export function applyTheme(theme, root = document.documentElement) {
|
|
53
|
+
THEMES.forEach((name) => root.classList.toggle(`theme-${name}`, name === theme));
|
|
54
|
+
root.classList.toggle("dark", isDarkTheme(theme));
|
|
55
|
+
root.dataset.theme = theme;
|
|
56
|
+
}
|
|
57
|
+
export function currentTheme(preference = readPreference()) {
|
|
58
|
+
return resolveTheme(preference, new Date().getHours(), systemPrefersDark());
|
|
59
|
+
}
|
|
60
|
+
export function setPreference(preference) {
|
|
61
|
+
try {
|
|
62
|
+
localStorage.setItem(STORAGE_KEY, preference);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// modo privado: o tema vale só para esta página
|
|
66
|
+
}
|
|
67
|
+
const theme = currentTheme(preference);
|
|
68
|
+
applyTheme(theme);
|
|
69
|
+
document.dispatchEvent(new CustomEvent("themechange", { detail: { preference, theme } }));
|
|
70
|
+
return theme;
|
|
71
|
+
}
|
|
72
|
+
// Mantém o tema certo enquanto a página fica aberta (virada de horário, mudança no SO, outra aba)
|
|
73
|
+
export function watchTheme() {
|
|
74
|
+
const refresh = () => {
|
|
75
|
+
const preference = readPreference();
|
|
76
|
+
const theme = currentTheme(preference);
|
|
77
|
+
applyTheme(theme);
|
|
78
|
+
document.dispatchEvent(new CustomEvent("themechange", { detail: { preference, theme } }));
|
|
79
|
+
};
|
|
80
|
+
setInterval(() => {
|
|
81
|
+
if (readPreference() === "auto")
|
|
82
|
+
refresh();
|
|
83
|
+
}, AUTO_RECHECK_MS);
|
|
84
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
85
|
+
if (readPreference() === "sistema")
|
|
86
|
+
refresh();
|
|
87
|
+
});
|
|
88
|
+
window.addEventListener("storage", (event) => {
|
|
89
|
+
if (event.key === STORAGE_KEY)
|
|
90
|
+
refresh();
|
|
91
|
+
});
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thecodecodex/design-system",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Design system do thecodecodex.com: tokens e temas para Tailwind CSS v4 e componentes React",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Douglas Capeletti",
|
|
7
|
+
"homepage": "https://github.com/thecodecodex/design-system#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/thecodecodex/design-system.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/thecodecodex/design-system/issues",
|
|
13
|
+
"keywords": ["design-system", "tailwindcss", "react", "preact", "themes"],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": ["**/*.css"],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": { "types": "./dist/components/index.d.ts", "default": "./dist/components/index.js" },
|
|
18
|
+
"./theme": { "types": "./dist/theme/index.d.ts", "default": "./dist/theme/index.js" },
|
|
19
|
+
"./styles.css": "./src/styles/index.css",
|
|
20
|
+
"./tokens.css": "./src/styles/tokens.css",
|
|
21
|
+
"./themes.css": "./src/styles/themes.css",
|
|
22
|
+
"./base.css": "./src/styles/base.css"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src/styles"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
30
|
+
"check": "tsc && eslint && vitest run",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"lint": "eslint",
|
|
33
|
+
"prepublishOnly": "pnpm check && pnpm build"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@stylistic/eslint-plugin": "^2.13.0",
|
|
40
|
+
"@types/react": "^19.3.0",
|
|
41
|
+
"eslint": "^9.39.4",
|
|
42
|
+
"globals": "^15.15.0",
|
|
43
|
+
"react": "^19.3.0",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"typescript-eslint": "^8.59.0",
|
|
46
|
+
"vitest": "^5.0.5"
|
|
47
|
+
},
|
|
48
|
+
"packageManager": "pnpm@12.14.0"
|
|
49
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Base: barras de rolagem, foco, títulos, tabelas e seleção (opcional: importe só tokens.css e themes.css para dispensá-la) */
|
|
2
|
+
@layer base {
|
|
3
|
+
|
|
4
|
+
/* Barras de rolagem: padrão (Firefox, Chrome/Edge 121+); a da página usa o fundo como trilho,
|
|
5
|
+
as internas (tabelas, sumário, diagramas) ficam finas e com trilho transparente */
|
|
6
|
+
* {
|
|
7
|
+
scrollbar-color: var(--scrollbar-thumb) transparent;
|
|
8
|
+
scrollbar-width: thin;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html {
|
|
12
|
+
scrollbar-color: var(--scrollbar-thumb) var(--ground);
|
|
13
|
+
scrollbar-width: auto;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Safari ainda não tem scrollbar-color: mesmo visual via pseudo-elementos */
|
|
17
|
+
@supports not (scrollbar-color: auto) {
|
|
18
|
+
::-webkit-scrollbar {
|
|
19
|
+
width: 10px;
|
|
20
|
+
height: 10px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
::-webkit-scrollbar-track {
|
|
24
|
+
background: transparent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
html::-webkit-scrollbar-track {
|
|
28
|
+
background: var(--ground);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
::-webkit-scrollbar-thumb {
|
|
32
|
+
border: 2px solid transparent;
|
|
33
|
+
border-radius: 999px;
|
|
34
|
+
background: var(--scrollbar-thumb) padding-box;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
::-webkit-scrollbar-thumb:hover {
|
|
38
|
+
background: var(--scrollbar-thumb-hover) padding-box;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
::-webkit-scrollbar-corner {
|
|
42
|
+
background: transparent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* alto contraste: barras na largura normal, mais fáceis de pegar */
|
|
47
|
+
.theme-contraste,
|
|
48
|
+
.theme-contraste * {
|
|
49
|
+
scrollbar-width: auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
body {
|
|
53
|
+
transition: background-color 0.6s ease, color 0.6s ease;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@media (prefers-reduced-motion: reduce) {
|
|
57
|
+
body {
|
|
58
|
+
transition: none;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
button {
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Foco visível por teclado em todo o site */
|
|
67
|
+
:focus-visible {
|
|
68
|
+
outline: 3px solid var(--contrast);
|
|
69
|
+
outline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
h1 {
|
|
73
|
+
@apply font-display uppercase font-black leading-none mb-6 mt-12;
|
|
74
|
+
font-size: clamp(2rem, 4vw, 2.5rem);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
h2 {
|
|
78
|
+
@apply font-display uppercase font-extrabold leading-none mb-5 mt-10;
|
|
79
|
+
font-size: clamp(1.6rem, 3vw, 2rem);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
h3 {
|
|
83
|
+
@apply font-display font-bold leading-none mb-4 mt-8;
|
|
84
|
+
font-size: clamp(1.35rem, 2.5vw, 1.75rem);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
h4 {
|
|
88
|
+
@apply font-display tracking-wide font-semibold leading-none mb-4 mt-6;
|
|
89
|
+
font-size: clamp(1.2rem, 2vw, 1.6rem);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
h5 {
|
|
93
|
+
@apply font-display tracking-wide font-medium leading-none mb-3 mt-5;
|
|
94
|
+
font-size: clamp(1.125rem, 1.5vw, 1.5rem);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
h6 {
|
|
98
|
+
@apply font-display font-normal leading-none mb-2 mt-4;
|
|
99
|
+
font-size: clamp(1.125rem, 1.5vw, 1.5rem);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
table:not(.plain),
|
|
103
|
+
table:not(.plain) th,
|
|
104
|
+
table:not(.plain) td {
|
|
105
|
+
border: 1px solid var(--border);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
::selection {
|
|
109
|
+
color: var(--bg-primary);
|
|
110
|
+
background: var(--contrast);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/* Temas: a classe .theme-* vai no <html> (ver @thecodecodex/design-system/theme) */
|
|
2
|
+
@layer base {
|
|
3
|
+
:root,
|
|
4
|
+
.theme-claro {
|
|
5
|
+
--text-primary: #12151e;
|
|
6
|
+
--text-secondary: #4b5563;
|
|
7
|
+
--text-tertiary: #ffffff;
|
|
8
|
+
--bg-primary: #ffffff;
|
|
9
|
+
--bg-secondary: #f3f4f6;
|
|
10
|
+
--bg-tertiary: #12151e;
|
|
11
|
+
--bg-tertiary-hover: #1f2937;
|
|
12
|
+
--btn-primary: #0e7490;
|
|
13
|
+
--on-accent: #ffffff;
|
|
14
|
+
--border: #12151e;
|
|
15
|
+
--contrast: #fd2d78;
|
|
16
|
+
/* gráficos: slots 1 e 2 da paleta de dataviz, validados (CVD e contraste) contra a superfície deste tema */
|
|
17
|
+
--chart-1: #2a78d6;
|
|
18
|
+
--chart-2: #eb6834;
|
|
19
|
+
/* diff: fundo da linha e realce do trecho alterado; texto do tema ≥ 4.5:1 sobre todos */
|
|
20
|
+
--diff-add: #dcfce7;
|
|
21
|
+
--diff-add-strong: #86efac;
|
|
22
|
+
--diff-del: #fee2e2;
|
|
23
|
+
--diff-del-strong: #fca5a5;
|
|
24
|
+
--cat-guide: #12151e;
|
|
25
|
+
--cat-guide-fg: #ffffff;
|
|
26
|
+
--cat-guide-accent: #22d3ee;
|
|
27
|
+
--cat-note: #fef9c3;
|
|
28
|
+
--cat-tool: #cffafe;
|
|
29
|
+
--cat-project: #ffe4ec;
|
|
30
|
+
--cat-fg: #12151e;
|
|
31
|
+
--cat-dev: #e0e7ff;
|
|
32
|
+
/* fundo da página: contrasta com as superfícies (bg-primary) */
|
|
33
|
+
--ground: #eef0f3;
|
|
34
|
+
/* controles nativos (barras de rolagem, campos de data, select) no mesmo modo do tema */
|
|
35
|
+
color-scheme: light;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.theme-papel {
|
|
39
|
+
--text-primary: #2a2118;
|
|
40
|
+
--text-secondary: #5c4f3f;
|
|
41
|
+
--text-tertiary: #f6f1e7;
|
|
42
|
+
--bg-primary: #f6f1e7;
|
|
43
|
+
--bg-secondary: #ebe3d2;
|
|
44
|
+
--bg-tertiary: #2a2118;
|
|
45
|
+
--bg-tertiary-hover: #3d3226;
|
|
46
|
+
--btn-primary: #9a3412;
|
|
47
|
+
--on-accent: #ffffff;
|
|
48
|
+
--border: #2a2118;
|
|
49
|
+
--contrast: #e8590c;
|
|
50
|
+
/* gráficos: slots 1 e 2 da paleta de dataviz, validados (CVD e contraste) contra a superfície deste tema */
|
|
51
|
+
--chart-1: #2a78d6;
|
|
52
|
+
--chart-2: #eb6834;
|
|
53
|
+
/* diff: fundo da linha e realce do trecho alterado; texto do tema ≥ 4.5:1 sobre todos */
|
|
54
|
+
--diff-add: #dcefd4;
|
|
55
|
+
--diff-add-strong: #9fd18a;
|
|
56
|
+
--diff-del: #f6d9d0;
|
|
57
|
+
--diff-del-strong: #eba58f;
|
|
58
|
+
--cat-guide: #2a2118;
|
|
59
|
+
--cat-guide-fg: #f6f1e7;
|
|
60
|
+
--cat-guide-accent: #f2a65a;
|
|
61
|
+
--cat-note: #f1e2a6;
|
|
62
|
+
--cat-tool: #d6e5dc;
|
|
63
|
+
--cat-project: #f0d5ca;
|
|
64
|
+
--cat-fg: #2a2118;
|
|
65
|
+
--cat-dev: #e2dcef;
|
|
66
|
+
/* fundo da página: contrasta com as superfícies (bg-primary) */
|
|
67
|
+
--ground: #e6dcc6;
|
|
68
|
+
/* controles nativos (barras de rolagem, campos de data, select) no mesmo modo do tema */
|
|
69
|
+
color-scheme: light;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.theme-crepusculo {
|
|
73
|
+
--text-primary: #efe6da;
|
|
74
|
+
--text-secondary: #bcb0a0;
|
|
75
|
+
--text-tertiary: #262130;
|
|
76
|
+
--bg-primary: #262130;
|
|
77
|
+
--bg-secondary: #332c3f;
|
|
78
|
+
--bg-tertiary: #efe6da;
|
|
79
|
+
--bg-tertiary-hover: #ddd2c4;
|
|
80
|
+
--btn-primary: #f2a65a;
|
|
81
|
+
--on-accent: #262130;
|
|
82
|
+
--border: #efe6da;
|
|
83
|
+
--contrast: #ff7aa8;
|
|
84
|
+
/* gráficos: slots 1 e 2 da paleta de dataviz, validados (CVD e contraste) contra a superfície deste tema */
|
|
85
|
+
--chart-1: #3987e5;
|
|
86
|
+
--chart-2: #d95926;
|
|
87
|
+
/* diff: fundo da linha e realce do trecho alterado; texto do tema ≥ 4.5:1 sobre todos */
|
|
88
|
+
--diff-add: #1f3a2a;
|
|
89
|
+
--diff-add-strong: #2f6b45;
|
|
90
|
+
--diff-del: #4a2328;
|
|
91
|
+
--diff-del-strong: #8a3a44;
|
|
92
|
+
--cat-guide: #efe6da;
|
|
93
|
+
--cat-guide-fg: #262130;
|
|
94
|
+
--cat-guide-accent: #9a3412;
|
|
95
|
+
--cat-note: #4a4020;
|
|
96
|
+
--cat-tool: #1e3f45;
|
|
97
|
+
--cat-project: #4d2639;
|
|
98
|
+
--cat-fg: #efe6da;
|
|
99
|
+
--cat-dev: #2e3557;
|
|
100
|
+
/* fundo da página: contrasta com as superfícies (bg-primary) */
|
|
101
|
+
--ground: #1c1824;
|
|
102
|
+
/* controles nativos (barras de rolagem, campos de data, select) no mesmo modo do tema */
|
|
103
|
+
color-scheme: dark;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.theme-escuro {
|
|
107
|
+
--text-primary: #e5e7eb;
|
|
108
|
+
--text-secondary: #9ca3af;
|
|
109
|
+
--text-tertiary: #12151e;
|
|
110
|
+
--bg-primary: #12151e;
|
|
111
|
+
--bg-secondary: #1f2937;
|
|
112
|
+
--bg-tertiary: #e5e7eb;
|
|
113
|
+
--bg-tertiary-hover: #d1d5db;
|
|
114
|
+
--btn-primary: #2dfdfd;
|
|
115
|
+
--on-accent: #12151e;
|
|
116
|
+
--border: #e5e7eb;
|
|
117
|
+
--contrast: #3efd2d;
|
|
118
|
+
/* gráficos: slots 1 e 2 da paleta de dataviz, validados (CVD e contraste) contra a superfície deste tema */
|
|
119
|
+
--chart-1: #3987e5;
|
|
120
|
+
--chart-2: #d95926;
|
|
121
|
+
/* diff: fundo da linha e realce do trecho alterado; texto do tema ≥ 4.5:1 sobre todos */
|
|
122
|
+
--diff-add: #12341f;
|
|
123
|
+
--diff-add-strong: #1d6b3a;
|
|
124
|
+
--diff-del: #3f1a1d;
|
|
125
|
+
--diff-del-strong: #8a2a33;
|
|
126
|
+
--cat-guide: #e5e7eb;
|
|
127
|
+
--cat-guide-fg: #12151e;
|
|
128
|
+
--cat-guide-accent: #0e7490;
|
|
129
|
+
--cat-note: #3d3914;
|
|
130
|
+
--cat-tool: #0f3a40;
|
|
131
|
+
--cat-project: #44182c;
|
|
132
|
+
--cat-fg: #e5e7eb;
|
|
133
|
+
--cat-dev: #252a52;
|
|
134
|
+
/* fundo da página: contrasta com as superfícies (bg-primary) */
|
|
135
|
+
--ground: #0b0e14;
|
|
136
|
+
/* controles nativos (barras de rolagem, campos de data, select) no mesmo modo do tema */
|
|
137
|
+
color-scheme: dark;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.theme-contraste {
|
|
141
|
+
--text-primary: #ffffff;
|
|
142
|
+
--text-secondary: #e6e6e6;
|
|
143
|
+
--text-tertiary: #000000;
|
|
144
|
+
--bg-primary: #000000;
|
|
145
|
+
--bg-secondary: #161616;
|
|
146
|
+
--bg-tertiary: #ffffff;
|
|
147
|
+
--bg-tertiary-hover: #e6e6e6;
|
|
148
|
+
--btn-primary: #ffe500;
|
|
149
|
+
--on-accent: #000000;
|
|
150
|
+
--border: #ffffff;
|
|
151
|
+
--contrast: #00e5ff;
|
|
152
|
+
/* gráficos: slots 1 e 2 da paleta de dataviz, validados (CVD e contraste) contra a superfície deste tema */
|
|
153
|
+
--chart-1: #3987e5;
|
|
154
|
+
--chart-2: #d95926;
|
|
155
|
+
/* diff: fundo da linha e realce do trecho alterado; texto do tema ≥ 4.5:1 sobre todos */
|
|
156
|
+
--diff-add: #003d1a;
|
|
157
|
+
--diff-add-strong: #00692c;
|
|
158
|
+
--diff-del: #4d0010;
|
|
159
|
+
--diff-del-strong: #8a001f;
|
|
160
|
+
--cat-guide: #ffffff;
|
|
161
|
+
--cat-guide-fg: #000000;
|
|
162
|
+
--cat-guide-accent: #000000;
|
|
163
|
+
--cat-note: #ffe500;
|
|
164
|
+
--cat-tool: #00e5ff;
|
|
165
|
+
--cat-project: #ff9ec9;
|
|
166
|
+
--cat-fg: #000000;
|
|
167
|
+
--cat-dev: #ffffff;
|
|
168
|
+
/* fundo da página: contrasta com as superfícies (bg-primary) */
|
|
169
|
+
--ground: #000000;
|
|
170
|
+
/* controles nativos (barras de rolagem, campos de data, select) no mesmo modo do tema */
|
|
171
|
+
color-scheme: dark;
|
|
172
|
+
--scrollbar-thumb: #ffffff;
|
|
173
|
+
--scrollbar-thumb-hover: var(--contrast);
|
|
174
|
+
--soft: #8c8c8c;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* borda suave: derivada da borda do tema (o alto contraste define a sua) */
|
|
178
|
+
:where(:root) {
|
|
179
|
+
--soft: color-mix(in srgb, var(--border) 18%, transparent);
|
|
180
|
+
/* barras de rolagem: derivadas do texto secundário (o alto contraste define as suas) */
|
|
181
|
+
--scrollbar-thumb: color-mix(in srgb, var(--text-secondary) 55%, transparent);
|
|
182
|
+
--scrollbar-thumb-hover: var(--text-secondary);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Tokens do design system: variante dark e mapeamento das variáveis do tema para utilidades do Tailwind */
|
|
2
|
+
/* .dark é aplicada junto com qualquer tema escuro (crepúsculo, escuro, alto contraste) — ver src/utils/theme.ts */
|
|
3
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
4
|
+
|
|
5
|
+
@theme {
|
|
6
|
+
--font-display: var(--font-roboto);
|
|
7
|
+
--font-body: var(--font-poppins);
|
|
8
|
+
--font-mono: var(--font-jetbrains-mono), ui-monospace, monospace;
|
|
9
|
+
--radius-xl: 0.75rem;
|
|
10
|
+
|
|
11
|
+
--color-text-primary: var(--text-primary);
|
|
12
|
+
--color-text-secondary: var(--text-secondary);
|
|
13
|
+
--color-text-tertiary: var(--text-tertiary);
|
|
14
|
+
--color-bg-primary: var(--bg-primary);
|
|
15
|
+
--color-bg-secondary: var(--bg-secondary);
|
|
16
|
+
--color-bg-tertiary: var(--bg-tertiary);
|
|
17
|
+
--color-bg-tertiary-hover: var(--bg-tertiary-hover);
|
|
18
|
+
--color-btn-primary: var(--btn-primary);
|
|
19
|
+
--color-border: var(--border);
|
|
20
|
+
--color-contrast: var(--contrast);
|
|
21
|
+
--color-chart-1: var(--chart-1);
|
|
22
|
+
--color-chart-2: var(--chart-2);
|
|
23
|
+
--color-diff-add: var(--diff-add);
|
|
24
|
+
--color-diff-add-strong: var(--diff-add-strong);
|
|
25
|
+
--color-diff-del: var(--diff-del);
|
|
26
|
+
--color-diff-del-strong: var(--diff-del-strong);
|
|
27
|
+
--color-on-accent: var(--on-accent);
|
|
28
|
+
/* Camadas: fundo da página (ground) < superfície (bg-primary, cards e campos) < recuo (bg-secondary) */
|
|
29
|
+
--color-ground: var(--ground);
|
|
30
|
+
--color-surface: var(--bg-primary);
|
|
31
|
+
--color-inset: var(--bg-secondary);
|
|
32
|
+
--color-soft: var(--soft);
|
|
33
|
+
|
|
34
|
+
/* Cores de categoria (docs/redesign/01-tokens-e-temas.md) */
|
|
35
|
+
--color-cat-guide: var(--cat-guide);
|
|
36
|
+
--color-cat-guide-fg: var(--cat-guide-fg);
|
|
37
|
+
--color-cat-guide-accent: var(--cat-guide-accent);
|
|
38
|
+
--color-cat-note: var(--cat-note);
|
|
39
|
+
--color-cat-tool: var(--cat-tool);
|
|
40
|
+
--color-cat-project: var(--cat-project);
|
|
41
|
+
--color-cat-dev: var(--cat-dev);
|
|
42
|
+
--color-cat-fg: var(--cat-fg);
|
|
43
|
+
}
|