@pierdigital/arraia-web 0.0.0-stage → 0.0.1

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.
Files changed (2) hide show
  1. package/README.md +235 -2
  2. package/package.json +100 -4
package/README.md CHANGED
@@ -1,3 +1,236 @@
1
- # Temporary Holding Version
1
+ # @pierdigital/arraia-web
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Componentes React do **Arraia**, o design system da Pier, construídos com [vanilla-extract](https://vanilla-extract.style) (CSS gerado no build, sem runtime) e [Base UI](https://base-ui.com) (comportamento e acessibilidade). Os tokens vêm do [`@pierdigital/arraia-core`](../core/README.md).
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ pnpm add @pierdigital/arraia-web @pierdigital/arraia-core
9
+ ```
10
+
11
+ Carregue as vars do Arraia uma vez, na raiz do app:
12
+
13
+ - **Sem styled-components:** `import '@pierdigital/arraia-core/tokens.css';`
14
+ - **Com styled-components:** monte o `ArraiaGlobalStyles` do `@pierdigital/arraia-core/styled` (mesmo conteúdo).
15
+
16
+ O app **não** precisa do plugin do vanilla-extract: cada componente sai compilado, com o JS importando o próprio CSS. Funciona em Vite e Next (App Router e Pages).
17
+
18
+ ## Uso
19
+
20
+ Um entry por componente, em PascalCase. Não há import raiz.
21
+
22
+ ```tsx
23
+ import { Button } from '@pierdigital/arraia-web/Button';
24
+
25
+ <Button onClick={save}>Salvar</Button>;
26
+ ```
27
+
28
+ | Entry | Componente |
29
+ | --- | --- |
30
+ | `@pierdigital/arraia-web/Button` | `Button` |
31
+ | `@pierdigital/arraia-web/Spinner` | `Spinner` |
32
+
33
+ ## Padrão de API
34
+
35
+ Vale para todos os componentes. A regra do Cursor [`arraia-web.mdc`](../../.cursor/rules/arraia-web.mdc) repete estes pontos para os agentes.
36
+
37
+ 1. **Um entry por componente, em PascalCase**, com named exports e sem default.
38
+ 2. **Simples x composto:** componente de um elemento é um só export (`Button`). Componente com partes é um namespace com os nomes de partes do Base UI (`Select.Root`, `Select.Trigger`, `Select.Popup`, `Select.Item`).
39
+ 3. **Estado controlado ou não, com os nomes do Base UI:** `value`/`defaultValue`/`onValueChange`, `checked`/`defaultChecked`/`onCheckedChange`, `open`/`defaultOpen`/`onOpenChange`, mais `disabled`, `required` e `name`.
40
+ 4. **Variantes só pelas props do Figma** (no Button: `variant`, `size`, `color`), via `recipe` do vanilla-extract. Os tipos saem de `RecipeVariants`, nunca escritos à mão.
41
+ 5. **API fechada: o visual do DS não se altera de fora.**
42
+ - Sem `className`, `style` nem `render` no componente. Os tipos não aceitam essas props, e o runtime também as descarta.
43
+ - Espaçamento externo só por `mt`, `mb`, `ml`, `mr`, com `'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'` (tokens de `space.between`, de 4 a 24 px). São classes geradas no build; valor arbitrário não passa.
44
+ - No **Button**, navegação com `asChild` e um único filho (`<a>` ou componente de link): `href`, `to` e demais props ficam no filho; o Button **clona** esse filho e funde visual/a11y nele (padrão Slot), sem recriar por `type`.
45
+ 6. **Ganchos internos:** cada parte tem `data-slot="<parte>"` (para teste e depuração, não para o app estilizar). Hover, pressed e focus saem do CSS e dos data-attributes do Base UI, nunca de props.
46
+ 7. **Ref e tipos:** `forwardRef` com `displayName` em toda parte; props exportadas como `<Componente><Parte>Props`.
47
+ 8. **CSS:** todo estilo em `@layer arraia.components`, depois de `arraia.tokens`, para o CSS global do app não brigar com o DS.
48
+
49
+ ### Formulários
50
+
51
+ Os campos não conhecem `react-hook-form`, Formik nem outra lib. Integram por contrato: `name`, `ref` encaminhada até o elemento nativo, `value`/`onValueChange` (ou `checked`/`onCheckedChange`), `onBlur` e `invalid` para o erro.
52
+
53
+ - Campos nativos (`Input`, `TextArea`) funcionam com `register()` direto.
54
+ - Campos compostos (`Select`, `Checkbox`, `Combobox`) usam `Controller`:
55
+
56
+ ```tsx
57
+ <Controller
58
+ control={control}
59
+ name="plan"
60
+ render={({ field, fieldState }) => (
61
+ <Select.Root
62
+ invalid={fieldState.invalid}
63
+ name={field.name}
64
+ onValueChange={field.onChange}
65
+ value={field.value}
66
+ >
67
+ <Select.Trigger onBlur={field.onBlur} ref={field.ref} />
68
+ {/* … */}
69
+ </Select.Root>
70
+ )}
71
+ />
72
+ ```
73
+
74
+ Se esse boilerplate pesar, a integração vira um pacote à parte (por exemplo `@pierdigital/arraia-web-rhf`, com `react-hook-form` como peer), nunca dentro do `arraia-web`.
75
+
76
+ ## Button
77
+
78
+ Segue o Figma (DO | Components · Button).
79
+
80
+ | Prop | Valores | Padrão |
81
+ | --- | --- | --- |
82
+ | `variant` | `'contained' \| 'outlined' \| 'ghost'` | `'contained'` |
83
+ | `size` | `'sm' \| 'md' \| 'lg'` (32 / 40 / 48 px) | `'sm'` |
84
+ | `color` | `'pink' \| 'blue'` | `'pink'` |
85
+ | `iconLeft`, `iconRight` | `ReactNode` (20 px em sm/md, 24 px em lg) | — |
86
+ | `loading` | `boolean`: `Spinner` no lugar do conteúdo, fundo da variante com o botão inteiro a 40% (Processing do Figma), `aria-busy`, clique bloqueado e largura mantida | `false` |
87
+ | `disabled`, `focusableWhenDisabled` | `boolean`: com o segundo, vira `aria-disabled` e continua na ordem do Tab | `false` |
88
+ | `fullWidth` | `boolean` | `false` |
89
+ | `asChild` | `true` + um filho (`<a>`, `Link`…); props de rota no filho; merge via `cloneElement` | — |
90
+ | `mt`, `mb`, `ml`, `mr` | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | — |
91
+
92
+ ```tsx
93
+ <Button variant="outlined" iconLeft={<PlusIcon />}>Adicionar</Button>
94
+ <Button aria-label="Adicionar" iconLeft={<PlusIcon />} /> {/* icon-only: aria-label obrigatório */}
95
+ <Button loading>Salvando</Button>
96
+ <Button asChild><a href="https://pier.digital">Site</a></Button>
97
+ <Button asChild><NextLink href="/pedidos">Ver pedidos</NextLink></Button>
98
+ <Button asChild><RouterLink to="/pedidos">Ver pedidos</RouterLink></Button>
99
+ ```
100
+
101
+ - `type="button"` por padrão, para não submeter formulário sem querer.
102
+ - Link com `disabled` ou `loading`: `aria-disabled`, sai da ordem do Tab e o clique é cancelado.
103
+ - Um componente de link já estilizado (`asChild` com `styled(Link)`) traz CSS próprio. O TypeScript não impede; evite no review.
104
+ - **Uso (guideline do Figma):** um `contained` por contexto; no máximo duas ações por grupo; na horizontal, secundário à esquerda e primário à direita; na vertical, primário em cima.
105
+
106
+ **Spinner no loading:** contained usa `onColor`; outlined e ghost usam a cor do botão (`pink` ou `blue`). O tamanho acompanha o ícone: `sm` (20 px) em sm/md e `md` (24 px) em lg.
107
+
108
+ **Movimento:**
109
+
110
+ - Press: escala e overlay de pressed instantâneos, sem transição. A escala é `0.97`, `0.98` no `lg` e `0.99` no `fullWidth`, para o botão largo não encolher demais. Hover só com mouse (`(hover: hover) and (pointer: fine)`).
111
+ - Entrada no `loading`: o conteúdo encolhe para `scale(0.95)` e desfoca (`blur(4px)`) em 200 ms, enquanto o spinner aparece no mesmo espaço (opacidade em 200 ms) e entra em foco mais devagar (`blur(4px)` → nítido em 300 ms, curva que começa lenta), para o desfoque ser visto.
112
+ - Volta assimétrica: o spinner sai em 120 ms e o conteúdo reaparece em 200 ms.
113
+ - Com `prefers-reduced-motion: reduce`: só opacidade, sem escala nem blur.
114
+
115
+ As curvas e durações ficam em [`src/styles/motion.css.ts`](src/styles/motion.css.ts).
116
+
117
+ ## Spinner
118
+
119
+ Segue o Figma (DO | Components · Spinner): track inteiro e arco fixo de um terço da volta, girando em 800 ms (1,6 s com reduced motion).
120
+
121
+ | Prop | Valores | Padrão |
122
+ | --- | --- | --- |
123
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` (16 / 20 / 24 / 32 / 40 px) | `'md'` |
124
+ | `color` | `'pink' \| 'blue' \| 'neutral' \| 'onColor'` | `'pink'` |
125
+ | `label` | `string`, anunciado com `role="status"` | `'Carregando'` |
126
+ | `mt`, `mb`, `ml`, `mr` | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | — |
127
+
128
+ ```tsx
129
+ <Spinner label="Buscando opções" />
130
+ <Spinner color="onColor" size="lg" />
131
+ ```
132
+
133
+ - Só para processamento real (mais de ~300 ms), nunca como decoração.
134
+ - `onColor` vai sobre superfícies sólidas e saturadas; pink, blue e neutral, em superfícies claras.
135
+ - Dentro de um componente que já anuncia o loading (como o Button, com `aria-busy`), use `aria-hidden`.
136
+
137
+ ## Desenvolvimento
138
+
139
+ | Comando | O que faz |
140
+ | --- | --- |
141
+ | `pnpm storybook` | Storybook na porta 6011 |
142
+ | `pnpm test` | Vitest (jsdom, Testing Library) |
143
+ | `pnpm build` | Typecheck e build da lib: `dist/components/<Nome>/index.{js,cjs,d.ts}` e `<Nome>.css`, mais `dist/shared.css` (margens e layers) e os chunks compartilhados |
144
+ | `pnpm lint` / `pnpm lint:fix` | Ultracite (Biome) |
145
+
146
+ ### Estrutura
147
+
148
+ ```text
149
+ src/
150
+ ├── components/<Nome>/
151
+ │ ├── <Nome>.tsx # só orquestra: separa props, monta a classe, escolhe o elemento
152
+ │ ├── <Nome>.types.ts # props exportadas
153
+ │ ├── <Nome>.css.ts # recipe da raiz (vanilla-extract, build time)
154
+ │ ├── index.ts # entry `@pierdigital/arraia-web/<Nome>`
155
+ │ ├── components/ # partes internas, não exportadas (ex.: ButtonContent, ButtonSpinner)
156
+ │ │ └── <Nome><Parte>/{<Nome><Parte>.tsx, <Nome><Parte>.css.ts, __tests__/}
157
+ │ ├── utils/{functions.ts, functions.types.ts, __tests__/}
158
+ │ ├── __tests__/<Nome>.test.tsx
159
+ │ └── stories/<Nome>.stories.tsx
160
+ ├── styles/
161
+ │ ├── layers.css.ts # ordem das layers (`arraia.tokens` / `arraia.components`)
162
+ │ ├── layered.ts # helper `layered(...)` (fora do `.css.ts`: VE não serializa função)
163
+ │ ├── spacing.css.ts # classes de `mt`/`mb`/`ml`/`mr`
164
+ │ └── motion.css.ts # curvas, durações e o helper `transition`
165
+ ├── utils/functions.ts # `cx`, `spacingClassNames`, `omitBlockedProps`
166
+ └── types.ts # contratos de todos os componentes: `SpacingStep`, `SpacingProps`, `BlockedProp`
167
+ ```
168
+
169
+ Cada tipo fica onde está o que ele descreve:
170
+
171
+ - `src/types.ts`: contrato que vale para todos os componentes. Só entra tipo usado por mais de um componente.
172
+ - `<Nome>.types.ts`: props do componente. As públicas saem pelo `index.ts`; as internas (repassadas às partes em `components/`) ficam no mesmo arquivo, derivadas das públicas com `Pick`/`Required`, sem redeclarar campo.
173
+ - `utils/functions.types.ts`: só parâmetros e retornos das funções de `utils/functions.ts`.
174
+
175
+ Uma parte interna vira componente próprio, com entry, quando um segundo componente precisar dela ou quando ela existir como componente no Figma. Foi o caso do `Spinner`: o `ButtonSpinner` virou só a camada do crossfade e usa o `Spinner` público.
176
+
177
+ ### Novo componente
178
+
179
+ 1. Crie `src/components/<Nome>/` com a estrutura acima, sobre o primitivo do Base UI quando existir.
180
+ 2. Adicione o nome em `COMPONENTS` no [`vite.config.ts`](vite.config.ts) e o export `./<Nome>` no [`package.json`](package.json).
181
+ 3. Siga o padrão de API acima: props do Figma, API fechada, `data-slot`, estilos em `layered(...)`.
182
+ 4. No Storybook: `Playground` com `tags: ['test', 'snapshot']` + `play`. Visual novo no PR só entra de propósito (CI falha → olha o diff → `test:visual:update --linux`).
183
+ 5. Figma no mesmo look do snapshot — ver seção abaixo.
184
+
185
+ ### Storybook, Vis e Figma
186
+
187
+ ```bash
188
+ pnpm playwright:install # Chromium (também no postinstall)
189
+ pnpm storybook # 6011 — Interactions, a11y, Vis, Design
190
+ pnpm test:storybook # play + a11y
191
+ pnpm test:visual # compara; falha se o PNG não acompanhou a mudança
192
+ ```
193
+
194
+ O snapshot do componente é o `Playground` (args default). No Figma, o `node-id` / `nodeId` tem que ser **desse** variant — no Button, Contained / sm / Enabled / Pink / text — não o set inteiro nem outro estado. Esse mesmo URL vai em `parameters.design` (aba Design) e em `figma-refs.json` (`fileKey` + `nodeId` + `url`). No URL do Figma o id vem com hífen (`152-1104`); no JSON e na API usa dois-pontos (`152:1104`).
195
+
196
+ ```json
197
+ {
198
+ "Button/playground": {
199
+ "fileKey": "GoNNaKQ3naATm4j03SFvmK",
200
+ "nodeId": "152:1104",
201
+ "url": "https://www.figma.com/design/…?node-id=152-1104"
202
+ }
203
+ }
204
+ ```
205
+
206
+ Mudou o visual de propósito? Aceita o baseline **e** tenta puxar de novo o PNG do Figma (Pixel Tribunal + evidência local). Sem ref, sem URL do node ou sem `FIGMA_TOKEN`, só avisa e o Vis segue:
207
+
208
+ ```bash
209
+ export FIGMA_TOKEN=figd_… # Figma → Settings → Security → PAT, escopo file_content:read (~90 dias)
210
+ pnpm test:visual:update --linux -- src/components/<Nome>/stories
211
+ git add __vis__/local __vis__/linux __figma__
212
+ git commit -m "chore(arraia-web): accept visual baselines"
213
+ ```
214
+
215
+ `--linux` gera o que o CI usa (Docker amd64). `--skip-figma` pula a tentativa. Sem API, exporta o PNG no Figma e troca `__figma__/<Nome>/playground.png` no PR.
216
+
217
+ Só o PNG:
218
+
219
+ ```bash
220
+ pnpm figma:refs # tudo do figma-refs.json
221
+ pnpm figma:refs Button/playground # um entry
222
+ ```
223
+
224
+ A aba Design abre o file no Figma (precisa de acesso). O PAT baixa o PNG versionado; o CI não chama a API.
225
+
226
+ ### Skills no Cursor
227
+
228
+ Quem for escrever story, util ou PR neste pacote: as skills em [`.cursor/skills/`](../../../.cursor/skills/) entram sozinhas quando o pedido casa com a description. As que mais importam aqui:
229
+
230
+ | Skill | Quando |
231
+ | --- | --- |
232
+ | [`arraia-web-stories`](../../../.cursor/skills/arraia-web-stories/SKILL.md) | `*.stories.tsx` — um conceito por story, JSDoc/`@summary` em inglês, `test`+`play` na mesma story, `!manifest` em matrix/demo, `snapshot` no Playground, Figma = variant do snapshot |
233
+ | [`util-functions`](../../../.cursor/skills/util-functions/SKILL.md) | helpers em `**/utils/functions.ts` + testes `functions.<nome>.test.ts` |
234
+ | [`pier-writing-pr`](../../../.cursor/skills/pier-writing-pr/SKILL.md) | abrir PR no template do monorepo, em português, falando do comportamento |
235
+
236
+ A regra [`arraia-web.mdc`](../../.cursor/rules/arraia-web.mdc) continua valendo pro contrato do componente (API fechada, entries, layers).
package/package.json CHANGED
@@ -1,6 +1,102 @@
1
1
  {
2
2
  "name": "@pierdigital/arraia-web",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "description": "Arraia Web — componentes React do design system da Seguradora Pier, com vanilla-extract e Base UI.",
6
+ "sideEffects": [
7
+ "**/*.css"
8
+ ],
9
+ "type": "module",
10
+ "exports": {
11
+ "./Button": {
12
+ "types": "./dist/components/Button/index.d.ts",
13
+ "source": "./src/components/Button/index.ts",
14
+ "import": "./dist/components/Button/index.js",
15
+ "require": "./dist/components/Button/index.cjs"
16
+ },
17
+ "./Spinner": {
18
+ "types": "./dist/components/Spinner/index.d.ts",
19
+ "source": "./src/components/Spinner/index.ts",
20
+ "import": "./dist/components/Spinner/index.js",
21
+ "require": "./dist/components/Spinner/index.cjs"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc --noEmit && vite build",
30
+ "build:watch": "vite build --watch",
31
+ "build-storybook": "storybook build",
32
+ "figma:refs": "node ./scripts/fetch-figma-refs.mjs",
33
+ "postinstall": "playwright install chromium",
34
+ "lint": "ultracite check",
35
+ "lint:fix": "ultracite fix",
36
+ "lint-staged": "lint-staged",
37
+ "playwright:install": "playwright install chromium",
38
+ "storybook": "storybook dev -p 6011",
39
+ "storybook:build": "pnpm build-storybook --quiet",
40
+ "storybook:dev": "pnpm storybook",
41
+ "storybook:move": "mv storybook-static ../../../storybook-static/arraia-web",
42
+ "test": "vitest run --config vitest.unit.config.ts --passWithNoTests",
43
+ "test:storybook": "vitest run --config vitest.storybook.config.ts",
44
+ "test:unit": "vitest run --config vitest.unit.config.ts --passWithNoTests",
45
+ "test:visual": "vitest run --config vitest.visual.config.ts",
46
+ "test:visual:update": "bash ./scripts/update-visual-baselines.sh",
47
+ "test:watch": "vitest --config vitest.unit.config.ts"
48
+ },
49
+ "dependencies": {
50
+ "@base-ui/react": "^1.8.0",
51
+ "@vanilla-extract/recipes": "^0.5.7"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "catalog:",
55
+ "@pierdigital/arraia-core": "workspace:*",
56
+ "@playwright/test": "^1.64.1",
57
+ "@repo/typescript-config": "workspace:*",
58
+ "@storybook/addon-a11y": "catalog:",
59
+ "@storybook/addon-designs": "catalog:",
60
+ "@storybook/addon-docs": "catalog:",
61
+ "@storybook/addon-mcp": "catalog:",
62
+ "@storybook/addon-vitest": "catalog:",
63
+ "@storybook/builder-vite": "catalog:",
64
+ "@storybook/react-vite": "catalog:",
65
+ "@testing-library/jest-dom": "catalog:",
66
+ "@testing-library/react": "catalog:",
67
+ "@testing-library/user-event": "catalog:",
68
+ "@types/react": "catalog:",
69
+ "@types/react-dom": "catalog:",
70
+ "@vanilla-extract/css": "catalog:",
71
+ "@vanilla-extract/vite-plugin": "catalog:",
72
+ "@vitest/browser": "5.0.5",
73
+ "@vitest/browser-playwright": "5.0.5",
74
+ "jsdom": "catalog:",
75
+ "playwright": "^1.64.1",
76
+ "react": "catalog:",
77
+ "react-dom": "catalog:",
78
+ "react-router": "catalog:",
79
+ "storybook": "catalog:",
80
+ "storybook-addon-vis": "^4.4.0",
81
+ "typescript": "catalog:",
82
+ "ultracite": "catalog:",
83
+ "vite": "catalog:",
84
+ "vite-plugin-dts": "catalog:",
85
+ "vite-plugin-lib-inject-css": "^2.2.2",
86
+ "vite-tsconfig-paths": "catalog:",
87
+ "vitest": "catalog:"
88
+ },
89
+ "peerDependencies": {
90
+ "@pierdigital/arraia-core": "workspace:^",
91
+ "react": ">=18.0.0",
92
+ "react-dom": ">=18.0.0"
93
+ },
94
+ "packageManager": "pnpm@10.33.4",
95
+ "engines": {
96
+ "node": "24.x"
97
+ },
98
+ "engineStrict": true,
99
+ "publishConfig": {
100
+ "access": "public"
101
+ }
102
+ }