@softize/opus 12.2.1 → 12.4.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/bin/cli.mjs +2 -2
  3. package/bin/lib/copy.mjs +4 -0
  4. package/docs/elevation-scale.md +2 -0
  5. package/docs/radius-scale.md +1 -0
  6. package/docs/relative-unit-scale.md +55 -0
  7. package/docs/releasing.md +2 -2
  8. package/package.json +3 -3
  9. package/registry/skills/build-opus-ui/SKILL.md +8 -4
  10. package/registry/skills/build-opus-ui/references/ui-patterns.md +2 -0
  11. package/registry/templates/app/package.json +1 -1
  12. package/src/ui/components/patterns/dock.tsx +182 -0
  13. package/src/ui/components/patterns/shell-nav.tsx +1 -0
  14. package/src/ui/components/patterns/sidebar.tsx +56 -13
  15. package/src/ui/components/primitives/accordion.tsx +1 -1
  16. package/src/ui/components/primitives/button.tsx +4 -4
  17. package/src/ui/components/primitives/calendar.tsx +2 -2
  18. package/src/ui/components/primitives/chat.tsx +1 -1
  19. package/src/ui/components/primitives/checkbox.tsx +1 -1
  20. package/src/ui/components/primitives/command.tsx +1 -1
  21. package/src/ui/components/primitives/dialog.tsx +1 -1
  22. package/src/ui/components/primitives/dot.tsx +39 -0
  23. package/src/ui/components/primitives/input-group.tsx +4 -4
  24. package/src/ui/components/primitives/input-otp.tsx +1 -1
  25. package/src/ui/components/primitives/input.tsx +4 -5
  26. package/src/ui/components/primitives/item.tsx +1 -1
  27. package/src/ui/components/primitives/radio-group.tsx +1 -1
  28. package/src/ui/components/primitives/scroll-area.tsx +1 -1
  29. package/src/ui/components/primitives/select.tsx +7 -4
  30. package/src/ui/components/primitives/switch.tsx +1 -1
  31. package/src/ui/components/primitives/table.tsx +14 -4
  32. package/src/ui/components/primitives/tabs.tsx +3 -3
  33. package/src/ui/components/primitives/textarea.tsx +1 -1
  34. package/src/ui/components/primitives/toggle.tsx +1 -1
  35. package/src/ui/components/primitives/tooltip.tsx +1 -1
  36. package/src/ui/components/primitives/truncate.tsx +9 -20
  37. package/src/ui/docs/DocBrowser.tsx +1 -1
  38. package/src/ui/docs/content/button.md +2 -2
  39. package/src/ui/docs/content/customization.md +5 -0
  40. package/src/ui/docs/content/dock.md +69 -0
  41. package/src/ui/docs/content/dot.md +17 -0
  42. package/src/ui/docs/content/select.md +1 -0
  43. package/src/ui/docs/content/sidebar.md +21 -0
  44. package/src/ui/docs/content/table.md +25 -24
  45. package/src/ui/docs/content/tabs.md +1 -1
  46. package/src/ui/docs/content/tokens.md +9 -9
  47. package/src/ui/docs/content/truncate.md +26 -0
  48. package/src/ui/docs/doc.tsx +1 -1
  49. package/src/ui/docs/registry.tsx +4 -0
  50. package/src/ui/lib/overflow.ts +34 -0
  51. package/src/ui/meta.ts +16 -4
  52. package/src/ui/react.tsx +12 -1
  53. package/src/ui/theme.css +27 -24
@@ -101,11 +101,11 @@ render(
101
101
  )
102
102
  ```
103
103
 
104
- ## Densidade
104
+ ## Escala relativa
105
105
 
106
- > A base é 15px: o `html` da casa fixa o rem, e TODA a escala (texto, spacing, radius) deriva
107
- > dele a UI densifica ~6% vs o default do browser (16px). Nada de font-size solto em px nos
108
- > componentes; quem quiser outra densidade sobrescreve o `html` no app.
106
+ > O Opus não define a fonte raiz. Cada `rem` acompanha o documento, portanto o navegador e a
107
+ > aplicação continuam responsáveis pela densidade. Uma aplicação pode ajustar `font-size` em
108
+ > `html`; texto, espaçamento, radius, foco e elevação acompanham a mesma decisão.
109
109
 
110
110
  ## Radius
111
111
 
@@ -118,12 +118,12 @@ render(
118
118
  render(
119
119
  <div className="flex flex-wrap gap-4">
120
120
  {[
121
- ['rounded-xs', 'radius − 6px'],
122
- ['rounded-sm', 'radius − 4px'],
123
- ['rounded-md', 'radius − 2px'],
121
+ ['rounded-xs', 'radius − 0.375rem'],
122
+ ['rounded-sm', 'radius − 0.25rem'],
123
+ ['rounded-md', 'radius − 0.125rem'],
124
124
  ['rounded-lg', 'radius (0.75rem)'],
125
- ['rounded-xl', 'radius + 4px'],
126
- ['rounded-2xl', 'radius + 8px'],
125
+ ['rounded-xl', 'radius + 0.25rem'],
126
+ ['rounded-2xl', 'radius + 0.5rem'],
127
127
  ].map(([cls, calc]) => (
128
128
  <div key={cls} className="flex items-center gap-3">
129
129
  <div className={'h-12 w-12 border-2 border-foreground/30 bg-muted ' + cls} />
@@ -20,6 +20,32 @@ O mesmo componente, com espaço de sobra: sem tooltip, sem atributo, só o texto
20
20
  </div>
21
21
  ```
22
22
 
23
+ ## Corte por esmaecimento
24
+
25
+ `fade` troca as reticências por um gradiente: o texto segue até a borda da caixa e desaparece
26
+ ali. A medição é a mesma, então o esmaecimento e a dica aparecem juntos, só quando há texto
27
+ escondido.
28
+
29
+ ```tsx preview
30
+ <div className="w-48 rounded-md border p-2">
31
+ <Truncate fade>relatorio-consolidado-vendas-pos-vendas-2026-julho-final-v3.xlsx</Truncate>
32
+ </div>
33
+ ```
34
+
35
+ Uma composição que precisa do esmaecimento sem o tooltip aplica a utilitária `truncate-fade`,
36
+ que cobre o último `1.5rem` da caixa, e decide quando ligá-la com o hook `useOverflowing` — a
37
+ mesma medição que este componente usa. A máscara pressupõe corte real: em texto que cabe
38
+ inteiro, ela esmaeceria o fim de uma palavra visível. É assim que o rótulo do `SidebarItem`
39
+ funciona.
40
+
41
+ ```tsx
42
+ const { ref, overflowing } = useOverflowing<HTMLSpanElement>(label)
43
+
44
+ <span ref={ref} className={cn('overflow-hidden whitespace-nowrap', overflowing && 'truncate-fade')}>
45
+ {label}
46
+ </span>
47
+ ```
48
+
23
49
  ## Conteúdo da dica
24
50
 
25
51
  `tooltip` sobrepõe o conteúdo mostrado no hover (default: os próprios children) — útil
@@ -30,7 +30,7 @@ function InlineMd({ text }: { text: string }): React.ReactElement {
30
30
  {parts.map((part, i) =>
31
31
  i % 2 === 1 ? (
32
32
  // eslint-disable-next-line react/no-array-index-key
33
- <code key={i} className="rounded-[calc(var(--radius)-4px)] bg-muted px-1.5 py-0.5 font-mono text-[0.85em]">
33
+ <code key={i} className="rounded-[calc(var(--radius)-0.25rem)] bg-muted px-1.5 py-0.5 font-mono text-[0.85em]">
34
34
  {part}
35
35
  </code>
36
36
  ) : (
@@ -50,6 +50,8 @@ import confirmMd from './content/confirm.md?raw'
50
50
  import aspectRatioMd from './content/aspect-ratio.md?raw'
51
51
  import avatarMd from './content/avatar.md?raw'
52
52
  import badgeMd from './content/badge.md?raw'
53
+ import dotMd from './content/dot.md?raw'
54
+ import dockMd from './content/dock.md?raw'
53
55
  import breadcrumbMd from './content/breadcrumb.md?raw'
54
56
  import buttonMd from './content/button.md?raw'
55
57
  import buttonGroupMd from './content/button-group.md?raw'
@@ -357,6 +359,8 @@ export const UI_SECTIONS: DocSection[] = [
357
359
  pages: [
358
360
  { slug: 'avatar', title: 'Avatar', render: comp('Avatar', 'avatar', avatarMd) },
359
361
  { slug: 'badge', title: 'Badge', render: comp('Badge', 'badge', badgeMd) },
362
+ { slug: 'dot', title: 'Dot', render: comp('Dot', 'dot', dotMd) },
363
+ { slug: 'dock', title: 'Dock', render: comp('Dock', 'dock', dockMd) },
360
364
  { slug: 'carousel', title: 'Carousel', render: comp('Carousel', 'carousel', carouselMd) },
361
365
  { slug: 'copyable', title: 'Copyable', render: comp('Copyable', 'copyable', copyableMd) },
362
366
  { slug: 'item', title: 'Item', render: comp('Item', 'item', itemMd) },
@@ -0,0 +1,34 @@
1
+ import * as React from 'react'
2
+
3
+ /**
4
+ * Diz se uma linha única de texto não cabe na própria caixa (`scrollWidth > clientWidth`).
5
+ * A medida é refeita quando a caixa muda de tamanho e quando o conteúdo passado em `content`
6
+ * muda — o ResizeObserver enxerga a CAIXA, não o texto que entra nela.
7
+ *
8
+ * É o que separa o corte real do corte suposto: quem consome liga o sinal de truncamento
9
+ * (reticências, esmaecimento, tooltip) só quando existe texto escondido.
10
+ */
11
+ export function useOverflowing<T extends HTMLElement>(content?: React.ReactNode): {
12
+ ref: React.RefObject<T | null>
13
+ overflowing: boolean
14
+ } {
15
+ const ref = React.useRef<T>(null)
16
+ const [overflowing, setOverflowing] = React.useState(false)
17
+
18
+ const measure = React.useCallback(() => {
19
+ const el = ref.current
20
+ if (el === null) return
21
+ setOverflowing(el.scrollWidth > el.clientWidth)
22
+ }, [])
23
+
24
+ React.useLayoutEffect(() => {
25
+ measure()
26
+ const el = ref.current
27
+ if (el === null || typeof ResizeObserver === 'undefined') return
28
+ const ro = new ResizeObserver(measure)
29
+ ro.observe(el)
30
+ return () => ro.disconnect()
31
+ }, [measure, content])
32
+
33
+ return { ref, overflowing }
34
+ }
package/src/ui/meta.ts CHANGED
@@ -29,11 +29,23 @@ export const componentMeta = {
29
29
  whenToUse:
30
30
  'Rótulo curto de status/categoria. `variant` pra intenção (default/secondary/destructive/outline) ou tom de status (success/warning/info, fill tingido). Não-interativo — pra clique, use Button ou `asChild` num <a>.',
31
31
  },
32
+ 'dot': {
33
+ name: 'dot',
34
+ ancestry: 'opus',
35
+ whenToUse:
36
+ 'Sinal compacto de estado quando o contexto ou o nome acessível já explica o significado. `variant` segue a intenção semântica (default/secondary/destructive/outline/success/warning/info). Passe `label` quando a cor comunicar informação; sem label, o ponto é decorativo. Para texto visível, use Badge.',
37
+ },
38
+ 'dock': {
39
+ name: 'dock',
40
+ ancestry: 'opus',
41
+ whenToUse:
42
+ 'Barra de ferramentas ancorada a uma superfície de trabalho — canvas, editor, preview —, quando um cabeçalho empilharia mais uma faixa de chrome sobre a trilha. Compõe Dock > DockGroup > DockAction; o estado da superfície (salvamento, versão) fica no SurfaceStatus, que flutua num canto e não pertence à barra. `position` escolhe a aresta; a divisória entre grupos é do componente. Requer TooltipProvider na raiz. Para ações de uma PÁGINA, use as `actions` do Page; para um conjunto de toggles exclusivos, ToggleGroup.',
43
+ },
32
44
  'button': {
33
45
  name: 'button',
34
46
  ancestry: 'shadcn',
35
47
  whenToUse:
36
- 'Ação clicável. `variant` define intenção (default/secondary/outline/ghost/destructive/link), `size` define altura (default/sm/lg · icon/icon-sm/icon-xs pros só-ícone, 36/32/24px) e `shape="pill"` troca somente a geometria, sem alterar cor ou hierarquia. `busy` mostra spinner e desabilita. `asChild` renderiza como outro elemento.',
48
+ 'Ação clicável. `variant` define intenção (default/secondary/outline/ghost/destructive/link), `size` define altura (default/sm/lg · icon/icon-sm/icon-xs para os casos só-ícone, 2.25/2/1.5rem) e `shape="pill"` troca somente a geometria, sem alterar cor ou hierarquia. `busy` mostra spinner e desabilita. `asChild` renderiza como outro elemento.',
37
49
  },
38
50
  'card': {
39
51
  name: 'card',
@@ -116,7 +128,7 @@ export const componentMeta = {
116
128
  name: 'select',
117
129
  ancestry: 'opus',
118
130
  whenToUse:
119
- 'O seletor ÚNICO da casa: toda escolha em lista. Data-driven (`options` = { value, label, hint?, content?, group? }), nunca JSX de item. Modos por prop: `searchable`, `multiple`, `onSearch` e `native`. `variant="default"` é campo de formulário; `outline` abraça o conteúdo com borda; `ghost` abraça o conteúdo sem borda. `size` segue a régua inline (default 36px/sm 32px) e `shape="pill"` troca somente a geometria. Também aceita `icon`, `trailing`, `triggerLabel` e `clearable`.',
131
+ 'Seletor para escolhas em lista. Recebe `options` no formato { value, label, hint?, content?, group? }, sem JSX por item. Os modos são definidos por `searchable`, `multiple`, `onSearch` e `native`. `variant="default"` funciona como campo de formulário; `outline` envolve o conteúdo com borda; `ghost` o envolve sem borda. `size` segue a régua inline (default 2.25rem/sm 2rem), e `shape="pill"` altera somente a geometria. Também aceita `icon`, `trailing`, `triggerLabel` e `clearable`.',
120
132
  },
121
133
  'separator': {
122
134
  name: 'separator',
@@ -137,7 +149,7 @@ export const componentMeta = {
137
149
  name: 'table',
138
150
  ancestry: 'shadcn',
139
151
  whenToUse:
140
- 'Tabela de dados. Compõe Table > (TableHeader > TableRow > TableHead, TableBody > TableRow > TableCell). Vem SEM borda externa o datagrid da casa é a Table num wrapper "overflow-hidden rounded-lg border border-border". Pra listagem tabular — pro pattern de search use ActionList (que já monta a tabela emoldurada do spec).',
152
+ 'Tabela de dados. Compõe Table > (TableHeader > TableRow > TableHead, TableBody > TableRow > TableCell). `variant="plain"` vem sem borda externa; `variant="framed"` aplica a moldura canônica, recorta o scroll e destaca o cabeçalho. Pra listagem tabular — pro pattern de search use ActionList.',
141
153
  },
142
154
  'tabs': {
143
155
  name: 'tabs',
@@ -167,7 +179,7 @@ export const componentMeta = {
167
179
  name: 'truncate',
168
180
  ancestry: 'opus',
169
181
  whenToUse:
170
- 'Texto truncado com tooltip SÓ quando transborda (medição do overflow, re-medida em resize) — substitui a composição `block truncate` + `title` sempre presente, que mostra dica até em texto que não corta. `tooltip` sobrepõe o conteúdo da dica (default: os children). Requer TooltipProvider na raiz. Pra célula de tabela, nome de arquivo, URL — qualquer linha única que pode estourar.',
182
+ 'Texto truncado com tooltip SÓ quando transborda (medição do overflow, re-medida em resize) — substitui a composição `block truncate` + `title` sempre presente, que mostra dica até em texto que não corta. `tooltip` sobrepõe o conteúdo da dica (default: os children); `fade` troca as reticências por um esmaecimento até a borda, ligado pela mesma medição. Requer TooltipProvider na raiz. Pra célula de tabela, nome de arquivo, URL — qualquer linha única que pode estourar.',
171
183
  },
172
184
  'accordion': {
173
185
  name: 'accordion',
package/src/ui/react.tsx CHANGED
@@ -135,6 +135,7 @@ export {
135
135
  TableCell,
136
136
  TableCaption,
137
137
  } from './components/primitives/table.tsx'
138
+ export type { TableProps } from './components/primitives/table.tsx'
138
139
 
139
140
  // UM seletor: `native` (menu do SO), `searchable` (busca), `multiple` (chips) e
140
141
  // `variant="ghost"` (barra de composer) são PROPS, não componentes. Ver select.tsx.
@@ -172,6 +173,10 @@ export { ToggleGroup, ToggleGroupItem } from './components/primitives/toggle-gro
172
173
  export { Truncate } from './components/primitives/truncate.tsx'
173
174
  export type { TruncateProps } from './components/primitives/truncate.tsx'
174
175
 
176
+ // A medição de transbordo por trás do Truncate, exposta para composições que precisam
177
+ // do sinal de corte sem o tooltip — por exemplo, esmaecer um rótulo com `truncate-fade`.
178
+ export { useOverflowing } from './lib/overflow.ts'
179
+
175
180
  // Patterns (renderizam actions a partir do spec)
176
181
  export { ActionForm, ActionFormField, useActionFormContext } from './components/patterns/form.tsx'
177
182
  export type { ActionFormProps, ActionFormFieldProps, ActionFormContextValue } from './components/patterns/form.tsx'
@@ -209,8 +214,12 @@ export type { PageProps } from './components/patterns/page.tsx'
209
214
  export { Split, Pane } from './components/patterns/split.tsx'
210
215
  export type { SplitProps, PaneProps, PaneSize, SplitLayout, SplitLayoutChange } from './components/patterns/split.tsx'
211
216
 
217
+ // A barra de ferramentas que flutua sobre uma superfície de trabalho (canvas, editor, preview).
218
+ export { Dock, DockGroup, DockAction, SurfaceStatus } from './components/patterns/dock.tsx'
219
+ export type { DockProps, DockActionProps, SurfaceStatusProps } from './components/patterns/dock.tsx'
220
+
212
221
  // Barra lateral composicional: header/conteúdo/footer e navegação, sem possuir o layout.
213
- export { PaneHeader, PaneContent, PaneFooter, Sidebar, SidebarItem, SidebarNav } from './components/patterns/sidebar.tsx'
222
+ export { PaneHeader, PaneContent, PaneFooter, Sidebar, SidebarItem, SidebarGroupLabel, SidebarNav } from './components/patterns/sidebar.tsx'
214
223
  export type { SidebarProps, SidebarItemProps, SidebarNavGroup, SidebarNavSubgroup, SidebarNavItem } from './components/patterns/sidebar.tsx'
215
224
 
216
225
  // O menu como primitivo pivotável: recolhe quando composto dentro de Sidebar.
@@ -221,3 +230,5 @@ export type { ShellNavProps, ShellNavGroup, ShellNavItem, ShellNavHeadingProps }
221
230
  // reativa da URL + navigate — sem tabela de rotas. Ver src/ui/router.ts.
222
231
  export { navigate, usePathname, useSegments, useSearchParams } from './router.ts'
223
232
  export type { NavigateOptions } from './router.ts'
233
+ export { Dot, dotVariants } from './components/primitives/dot.tsx'
234
+ export type { DotProps } from './components/primitives/dot.tsx'
package/src/ui/theme.css CHANGED
@@ -49,7 +49,7 @@
49
49
  --destructive-foreground: hsl(0 0% 98%);
50
50
  --border: hsl(0 0% 92.5%);
51
51
  --input: hsl(0 0% 92.5%);
52
- /* Ring de foco LEVE (pedido da casa): aro sutil, não um halo. Com ring-[3px] + /50,
52
+ /* Ring de foco LEVE (pedido da casa): aro sutil, não um halo. Com ring-[0.1875rem] + /50,
53
53
  ~86% encosta na borda (89.8%) — um sussurro de foco. */
54
54
  --ring: hsl(0 0% 86%);
55
55
  --radius: 0.75rem;
@@ -134,23 +134,23 @@
134
134
  --color-input: var(--input);
135
135
  --color-ring: var(--ring);
136
136
 
137
- --radius-xs: calc(var(--radius) - 6px);
138
- --radius-sm: calc(var(--radius) - 4px);
139
- --radius-md: calc(var(--radius) - 2px);
137
+ --radius-xs: calc(var(--radius) - 0.375rem);
138
+ --radius-sm: calc(var(--radius) - 0.25rem);
139
+ --radius-md: calc(var(--radius) - 0.125rem);
140
140
  --radius-lg: var(--radius);
141
- --radius-xl: calc(var(--radius) + 4px);
142
- --radius-2xl: calc(var(--radius) + 8px);
141
+ --radius-xl: calc(var(--radius) + 0.25rem);
142
+ --radius-2xl: calc(var(--radius) + 0.5rem);
143
143
 
144
144
  /* Escala Tailwind, com receita direta para o compilador preservar modifiers `/opacity`.
145
145
  `light-dark()` mantém o peso adequado em cada tema sem uma variável intermediária. */
146
- --shadow-2xs: 0 1px 2px -1px light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.12));
147
- --shadow-xs: 0 1px 2px -1px light-dark(rgb(15 15 15 / 0.025), rgb(0 0 0 / 0.12)), 0 2px 4px -2px light-dark(rgb(15 15 15 / 0.035), rgb(0 0 0 / 0.18));
148
- --shadow-sm: 0 2px 4px -2px light-dark(rgb(15 15 15 / 0.025), rgb(0 0 0 / 0.16)), 0 4px 8px -4px light-dark(rgb(15 15 15 / 0.04), rgb(0 0 0 / 0.28));
149
- --shadow: 0 2px 6px -2px light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.16)), 0 6px 14px -6px light-dark(rgb(15 15 15 / 0.05), rgb(0 0 0 / 0.29));
150
- --shadow-md: 0 4px 8px -4px light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.16)), 0 10px 28px -8px light-dark(rgb(15 15 15 / 0.065), rgb(0 0 0 / 0.3));
151
- --shadow-lg: 0 6px 16px -6px light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.18)), 0 18px 48px -12px light-dark(rgb(15 15 15 / 0.07), rgb(0 0 0 / 0.3));
152
- --shadow-xl: 0 8px 24px -10px light-dark(rgb(15 15 15 / 0.04), rgb(0 0 0 / 0.2)), 0 24px 64px -18px light-dark(rgb(15 15 15 / 0.08), rgb(0 0 0 / 0.34));
153
- --shadow-2xl: 0 12px 32px -14px light-dark(rgb(15 15 15 / 0.05), rgb(0 0 0 / 0.22)), 0 32px 88px -24px light-dark(rgb(15 15 15 / 0.1), rgb(0 0 0 / 0.38));
146
+ --shadow-2xs: 0 0.0625rem 0.125rem -0.0625rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.12));
147
+ --shadow-xs: 0 0.0625rem 0.125rem -0.0625rem light-dark(rgb(15 15 15 / 0.025), rgb(0 0 0 / 0.12)), 0 0.125rem 0.25rem -0.125rem light-dark(rgb(15 15 15 / 0.035), rgb(0 0 0 / 0.18));
148
+ --shadow-sm: 0 0.125rem 0.25rem -0.125rem light-dark(rgb(15 15 15 / 0.025), rgb(0 0 0 / 0.16)), 0 0.25rem 0.5rem -0.25rem light-dark(rgb(15 15 15 / 0.04), rgb(0 0 0 / 0.28));
149
+ --shadow: 0 0.125rem 0.375rem -0.125rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.16)), 0 0.375rem 0.875rem -0.375rem light-dark(rgb(15 15 15 / 0.05), rgb(0 0 0 / 0.29));
150
+ --shadow-md: 0 0.25rem 0.5rem -0.25rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.16)), 0 0.625rem 1.75rem -0.5rem light-dark(rgb(15 15 15 / 0.065), rgb(0 0 0 / 0.3));
151
+ --shadow-lg: 0 0.375rem 1rem -0.375rem light-dark(rgb(15 15 15 / 0.03), rgb(0 0 0 / 0.18)), 0 1.125rem 3rem -0.75rem light-dark(rgb(15 15 15 / 0.07), rgb(0 0 0 / 0.3));
152
+ --shadow-xl: 0 0.5rem 1.5rem -0.625rem light-dark(rgb(15 15 15 / 0.04), rgb(0 0 0 / 0.2)), 0 1.5rem 4rem -1.125rem light-dark(rgb(15 15 15 / 0.08), rgb(0 0 0 / 0.34));
153
+ --shadow-2xl: 0 0.75rem 2rem -0.875rem light-dark(rgb(15 15 15 / 0.05), rgb(0 0 0 / 0.22)), 0 2rem 5.5rem -1.5rem light-dark(rgb(15 15 15 / 0.1), rgb(0 0 0 / 0.38));
154
154
  }
155
155
 
156
156
  /* Base canônica do consumidor — vem DAQUI, não do entry de cada app. Tailwind v4
@@ -160,15 +160,6 @@
160
160
  * {
161
161
  border-color: var(--border);
162
162
  }
163
- /* Base 15px: o rem da casa — TODA a escala rem (texto, spacing, radius) deriva daqui,
164
- então a UI densifica ~6% vs o default do browser (16px). Histórico (conferido no git):
165
- até a 2.31.4 o pacote NÃO fixava rem (valia o 16px do browser); a 2.31.5 introduziu
166
- 14px; a 2.35.0 assenta em 15px — valor INÉDITO, não é revert. O 15px veio da prática:
167
- a Empresa X escolheu 15px por conta própria em DOIS apps, e a exceção reincidente
168
- virou default. Quem quiser mais denso sobrescreve o `html` no próprio entry. */
169
- html {
170
- font-size: 15px;
171
- }
172
163
  /* Canvas da casa: quase-branco vem do BODY — o shell é transparente e as
173
164
  superfícies (cards, miolo flush) pintam bg-background por cima. Tinte de 25%
174
165
  de muted: separa a superfície do canvas sem virar cinza; segue o tema. */
@@ -177,6 +168,18 @@
177
168
  }
178
169
  }
179
170
 
171
+ /* Truncamento por esmaecimento: a linha continua em direção à borda e desaparece num
172
+ * gradiente, em vez de terminar em reticências. A máscara ocupa o último 1.5rem da caixa,
173
+ * então só deve ser aplicada quando o texto REALMENTE corta — em texto que cabe, ela
174
+ * esmaeceria o fim de uma palavra inteira. `Truncate fade` e o rótulo do `SidebarItem`
175
+ * medem o transbordo antes de ligá-la. */
176
+ @utility truncate-fade {
177
+ overflow: hidden;
178
+ white-space: nowrap;
179
+ text-overflow: clip;
180
+ mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
181
+ }
182
+
180
183
  /* Scrollbar nativa da casa: todo scroller do documento compartilha o mesmo acabamento,
181
184
  * inclusive conteúdo renderizado por portal. Isso elimina a variação do macOS entre raiz,
182
185
  * elementos aninhados, mouse e trackpad sem trocar o mecanismo nativo nem adicionar trabalho
@@ -241,7 +244,7 @@
241
244
  * regra custom vaza pra dentro do not-prose (o CodeBlock ganhava uma SEGUNDA
242
245
  * borda no <pre>, colada na do contêiner — a "borda duplicada"). */
243
246
  .prose :where(code):not(:where(pre code, [class~='not-prose'], [class~='not-prose'] *)) {
244
- border-radius: calc(var(--radius) - 4px);
247
+ border-radius: calc(var(--radius) - 0.25rem);
245
248
  background: var(--muted);
246
249
  padding: 0.125rem 0.375rem;
247
250
  font-weight: 400;