@softize/opus 12.9.0 → 12.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/bin/lib/copy.mjs +63 -6
- package/docs/code-style.md +6 -2
- package/package.json +1 -1
- package/src/schema/drivers/zod.ts +17 -11
- package/src/ui/components/patterns/form.tsx +1 -1
- package/src/ui/components/primitives/alert.tsx +44 -14
- package/src/ui/components/primitives/field.tsx +4 -4
- package/src/ui/components/primitives/tooltip.tsx +1 -1
- package/src/ui/docs/content/alert.md +10 -3
- package/src/ui/docs/content/field.md +1 -1
- package/src/ui/docs/content/tooltip.md +15 -1
- package/src/ui/meta.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 12.10.0 — 2026-09-03
|
|
11
|
+
|
|
12
|
+
`Alert` ganha a variante aditiva `warning` e passa a oferecer realce tonal nativo ao redor do
|
|
13
|
+
ícone. O layout com ícone reserva uma coluna estável; sem ícone, o aviso continua em bloco e não
|
|
14
|
+
cria espaço vazio. `style` permite ajustar as medidas internas quando uma composição exigir.
|
|
15
|
+
|
|
16
|
+
`FieldLabel` adota a mesma hierarquia tipográfica de `DetailField`, com ritmo vertical menor, e o
|
|
17
|
+
estado marcado de uma escolha em cartão deixa de vazar para rótulos comuns com checkbox. Tooltips
|
|
18
|
+
passam a ter largura máxima compartilhada e quebra de linha natural; `ActionForm` apenas consome
|
|
19
|
+
esse padrão, sem decidir a largura do primitivo.
|
|
20
|
+
|
|
21
|
+
`opus copy` passa a inventariar estaticamente `label`, `description` e `doc` declarados em
|
|
22
|
+
`t.dict`, inclusive a documentação do dicionário. Metadados livres não viram copy por inferência;
|
|
23
|
+
declarações dinâmicas continuam falhando no gate em vez de executar código consumidor. Em runtime,
|
|
24
|
+
o dicionário mantém um snapshot imutável das entradas, então alterações posteriores no objeto
|
|
25
|
+
original, em `metaFor` ou na metadata lógica não mudam a copy já declarada.
|
|
26
|
+
|
|
10
27
|
## 12.9.0 — 2026-09-03
|
|
11
28
|
|
|
12
29
|
`Chat` aceita `empty`, um nó React para o estado sem mensagens: o app compõe o vazio com os
|
package/bin/lib/copy.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Inventário semântico de copy dos contratos Opus.
|
|
2
|
+
* Inventário semântico de copy dos contratos e dicionários Opus.
|
|
3
3
|
*
|
|
4
|
-
* O Opus conhece o papel dos textos declarados em defineAction/defineContract;
|
|
4
|
+
* O Opus conhece o papel dos textos declarados em defineAction/defineContract e t.dict;
|
|
5
5
|
* a @softize/base continua dona da política e da validação editorial. Este módulo
|
|
6
6
|
* apenas projeta o protocolo JSON v2 sem executar código do consumidor.
|
|
7
7
|
*/
|
|
@@ -61,6 +61,7 @@ const COPY_ROLES = new Set([
|
|
|
61
61
|
const AUDIT_REFERENCE = /^(?:https?:\/\/\S+|[a-z][a-z0-9-]*:\S+|(?:[A-Za-z0-9._-]+\/)+[A-Za-z0-9._#/-]+|[A-Za-z0-9._-]+\.(?:md|json|ya?ml)(?:#[^\s]+)?)$/u
|
|
62
62
|
const CONTRACT_MODULES = new Set(['@softize/opus', '@softize/opus/core'])
|
|
63
63
|
const CONTRACT_FACTORIES = new Set(['defineAction', 'defineContract'])
|
|
64
|
+
const SCHEMA_ZOD_MODULES = new Set(['@softize/opus/schema/zod'])
|
|
64
65
|
const ACTION_BINDING_KEYS = new Set(['authorize', 'background', 'emits', 'handler', 'idempotency', 'loads'])
|
|
65
66
|
|
|
66
67
|
// Somente exports Opus cujo papel textual é estável por contrato do componente.
|
|
@@ -397,6 +398,18 @@ function knownContractFactory(call, checker) {
|
|
|
397
398
|
return name !== null && CONTRACT_FACTORIES.has(name)
|
|
398
399
|
}
|
|
399
400
|
|
|
401
|
+
function knownDictionaryFactory(call, checker) {
|
|
402
|
+
const expression = resolveBinding(call.expression, checker)
|
|
403
|
+
if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false
|
|
404
|
+
const member = ts.isPropertyAccessExpression(expression)
|
|
405
|
+
? expression.name.text
|
|
406
|
+
: expression.argumentExpression === undefined
|
|
407
|
+
? null
|
|
408
|
+
: staticText(expression.argumentExpression, checker)?.text ?? null
|
|
409
|
+
return member === 'dict' &&
|
|
410
|
+
importedMemberName(expression.expression, checker, (module) => SCHEMA_ZOD_MODULES.has(module)) === 't'
|
|
411
|
+
}
|
|
412
|
+
|
|
400
413
|
function knownBindAction(call, checker) {
|
|
401
414
|
return importedMemberName(call.expression, checker, (module) => CONTRACT_MODULES.has(module)) === 'bindAction'
|
|
402
415
|
}
|
|
@@ -512,7 +525,10 @@ function stableExpressionUse(node, checker, mode, stack) {
|
|
|
512
525
|
ts.isSpreadAssignment(parent) || ts.isSpreadElement(parent) || ts.isArrayLiteralExpression(parent)
|
|
513
526
|
) return consumerMode === 'aggregate' && stableComposition(carrier, checker, stack)
|
|
514
527
|
if (ts.isCallExpression(parent) && parent.arguments.some((argument) => argument === carrier)) {
|
|
515
|
-
if (consumerMode === 'aggregate')
|
|
528
|
+
if (consumerMode === 'aggregate') {
|
|
529
|
+
if (knownDictionaryFactory(parent, checker)) return true
|
|
530
|
+
return knownContractFactory(parent, checker) && stableFactoryResult(parent, checker, stack)
|
|
531
|
+
}
|
|
516
532
|
if (
|
|
517
533
|
consumerMode === 'factory-result' && parent.arguments[0] === carrier &&
|
|
518
534
|
knownBindAction(parent, checker) && safeBindActionBinding(parent, checker)
|
|
@@ -865,14 +881,15 @@ function diagnostic(file, sourceFile, node, field, category = 'content') {
|
|
|
865
881
|
/**
|
|
866
882
|
* Extrai as superfícies humanas de um source isolado.
|
|
867
883
|
*
|
|
868
|
-
* O retorno distingue arquivo sem
|
|
869
|
-
* necessária para hashear todo contrato e detectar a adição posterior de
|
|
884
|
+
* O retorno distingue arquivo sem superfície Opus de contrato sem copy. Essa diferença é
|
|
885
|
+
* necessária para hashear todo contrato ou dicionário e detectar a adição posterior de texto.
|
|
870
886
|
*/
|
|
871
887
|
export function extractCopyFromSource(file, sourceText) {
|
|
872
888
|
const { sourceFile, checker } = parseSource(file, sourceText)
|
|
873
889
|
const entries = []
|
|
874
890
|
const diagnostics = []
|
|
875
891
|
let hasContract = false
|
|
892
|
+
let hasDictionary = false
|
|
876
893
|
const hasUiImport = sourceFile.statements.some(
|
|
877
894
|
(statement) =>
|
|
878
895
|
ts.isImportDeclaration(statement) &&
|
|
@@ -999,6 +1016,42 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
999
1016
|
nestedObject(object, 'errors', (error, prefix) => addProperty(error, 'description', 'error', prefix))
|
|
1000
1017
|
}
|
|
1001
1018
|
|
|
1019
|
+
function extractDictionary(call) {
|
|
1020
|
+
const entriesNode = call.arguments[0]
|
|
1021
|
+
const entries = resolveBinding(entriesNode, checker)
|
|
1022
|
+
if (!ts.isObjectLiteralExpression(entries)) {
|
|
1023
|
+
diagnostics.push(diagnostic(file, sourceFile, entriesNode, 't.dict entries', 'structure'))
|
|
1024
|
+
return
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const listed = properties(entries, sourceFile, checker)
|
|
1028
|
+
for (const opaque of listed.opaque) {
|
|
1029
|
+
diagnostics.push(diagnostic(file, sourceFile, opaque, 't.dict entries', 'structure'))
|
|
1030
|
+
}
|
|
1031
|
+
for (const [key, entry] of listed.items) {
|
|
1032
|
+
const valueNode = propertyValue(entry)
|
|
1033
|
+
const value = resolveBinding(valueNode, checker)
|
|
1034
|
+
if (!ts.isObjectLiteralExpression(value)) {
|
|
1035
|
+
diagnostics.push(diagnostic(file, sourceFile, valueNode, `t.dict.${key}`, 'structure'))
|
|
1036
|
+
continue
|
|
1037
|
+
}
|
|
1038
|
+
addProperty(value, 'label', 'label', `t.dict.${key}.`)
|
|
1039
|
+
addProperty(value, 'description', 'description', `t.dict.${key}.`)
|
|
1040
|
+
addProperty(value, 'doc', 'description', `t.dict.${key}.`)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (call.arguments.length < 2) return
|
|
1044
|
+
const optionsNode = call.arguments[1]
|
|
1045
|
+
const options = resolveBinding(optionsNode, checker)
|
|
1046
|
+
if (ts.isIdentifier(options) && options.text === 'undefined') {
|
|
1047
|
+
const symbol = checker.getSymbolAtLocation(options)
|
|
1048
|
+
const shadowed = symbol?.declarations?.some((declaration) => declaration.getSourceFile() === sourceFile) ?? false
|
|
1049
|
+
if (!shadowed) return
|
|
1050
|
+
}
|
|
1051
|
+
if (ts.isObjectLiteralExpression(options)) addProperty(options, 'doc', 'description', 't.dict.options.')
|
|
1052
|
+
else diagnostics.push(diagnostic(file, sourceFile, optionsNode, 't.dict options', 'structure'))
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1002
1055
|
function importedMember(node, modulePredicate) {
|
|
1003
1056
|
return importedMemberName(node, checker, modulePredicate)
|
|
1004
1057
|
}
|
|
@@ -1790,12 +1843,16 @@ export function extractCopyFromSource(file, sourceText) {
|
|
|
1790
1843
|
else diagnostics.push(diagnostic(file, sourceFile, node.arguments[0], `${factory}(...)`, 'structure'))
|
|
1791
1844
|
}
|
|
1792
1845
|
}
|
|
1846
|
+
if (ts.isCallExpression(node) && knownDictionaryFactory(node, checker) && node.arguments.length > 0) {
|
|
1847
|
+
hasDictionary = true
|
|
1848
|
+
extractDictionary(node)
|
|
1849
|
+
}
|
|
1793
1850
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) extractJsx(node)
|
|
1794
1851
|
ts.forEachChild(node, visit)
|
|
1795
1852
|
}
|
|
1796
1853
|
visit(sourceFile)
|
|
1797
1854
|
|
|
1798
|
-
return { hasContract, hasCopySurface: hasContract || hasUiImport, entries, diagnostics }
|
|
1855
|
+
return { hasContract, hasCopySurface: hasContract || hasDictionary || hasUiImport, entries, diagnostics }
|
|
1799
1856
|
}
|
|
1800
1857
|
|
|
1801
1858
|
function globRegex(pattern) {
|
package/docs/code-style.md
CHANGED
|
@@ -92,16 +92,20 @@ Base instalada é dona do catálogo, dos kinds aceitos e do fundamento de cada r
|
|
|
92
92
|
| `Select.emptyText`, `Select.searchPlaceholder` | `empty-state`, `placeholder` |
|
|
93
93
|
| `Select.options[].hint/triggerLabel/group` | `label`, `label`, `heading` |
|
|
94
94
|
| `ActionTrigger.confirm.*` | papel correspondente do diálogo |
|
|
95
|
+
| `t.dict` — `label`, `description`, `doc` das entradas e `doc` do dicionário | `label`, `description` |
|
|
95
96
|
|
|
96
97
|
## Cobertura e significado do gate verde
|
|
97
98
|
|
|
98
|
-
O extrator cobre
|
|
99
|
+
O extrator cobre três superfícies, sem heurística de nome:
|
|
99
100
|
|
|
100
101
|
1. propriedades estáticas de `defineAction` e `defineContract` descritas acima, somente
|
|
101
102
|
quando a factory resolve por símbolo a um import de `@softize/opus` ou
|
|
102
103
|
`@softize/opus/core`; alias, namespace e destructuring `const` de namespace funcionam,
|
|
103
104
|
homônimo local não é contrato;
|
|
104
|
-
2.
|
|
105
|
+
2. `label`, `description` e `doc` estáticos de `t.dict` importado de
|
|
106
|
+
`@softize/opus/schema/zod`; metadata livre não é classificada por nome, declarações dinâmicas
|
|
107
|
+
reprovam em vez de serem executadas e o dicionário mantém um snapshot imutável das entradas;
|
|
108
|
+
3. texto estático em filhos e props de uma allowlist de componentes importados diretamente
|
|
105
109
|
de `@softize/opus/ui*` — por exemplo, `Button`, `DialogTitle`, `DialogDescription`,
|
|
106
110
|
`FieldLabel`, `TabsTrigger`, `Page`, `Input` e `DataState`. Alias e namespace de import
|
|
107
111
|
continuam rastreáveis. `uppercase`, variantes como `sm:hover:uppercase`, modificadores
|
package/package.json
CHANGED
|
@@ -680,7 +680,7 @@ export interface DictType<K extends string, M extends DictEntryMeta> {
|
|
|
680
680
|
*/
|
|
681
681
|
labelFor(key: K, locale?: string): string
|
|
682
682
|
/** Meta completa da chave (label + extras). */
|
|
683
|
-
metaFor(key: K): M
|
|
683
|
+
metaFor(key: K): Readonly<M>
|
|
684
684
|
/** Lista pronta pra `<Select options={...} />`: `{ value, ...meta }`. */
|
|
685
685
|
options(): DictOption<K, M>[]
|
|
686
686
|
/** True se `key` está no dict. */
|
|
@@ -707,7 +707,7 @@ const dict = <const M extends Record<string, DictEntryMeta>>(
|
|
|
707
707
|
opts?: DictOpts,
|
|
708
708
|
): DictType<Extract<keyof M, string>, M[keyof M]> => {
|
|
709
709
|
type K = Extract<keyof M, string>
|
|
710
|
-
const keys = Object.keys(entries) as K[]
|
|
710
|
+
const keys = Object.freeze(Object.keys(entries)) as readonly K[]
|
|
711
711
|
if (keys.length === 0) {
|
|
712
712
|
throw new Error('t.dict precisa de pelo menos uma entrada')
|
|
713
713
|
}
|
|
@@ -729,16 +729,22 @@ const dict = <const M extends Record<string, DictEntryMeta>>(
|
|
|
729
729
|
)
|
|
730
730
|
}
|
|
731
731
|
}
|
|
732
|
+
// O dicionário é declarativo: mantém um snapshot próprio e imutável para que `metaFor`,
|
|
733
|
+
// a metadata do schema e mutações posteriores no objeto de entrada não alterem a copy
|
|
734
|
+
// ou a apresentação que foram inventariadas no código-fonte.
|
|
735
|
+
const stableEntries = Object.freeze(
|
|
736
|
+
Object.fromEntries(keys.map((key) => [key, Object.freeze({ ...entries[key] })])),
|
|
737
|
+
) as unknown as Readonly<M>
|
|
732
738
|
const zodSchema = z.enum(keys as [K, ...K[]])
|
|
733
|
-
const meta: LogicalTypeMeta = {
|
|
739
|
+
const meta: LogicalTypeMeta = Object.freeze({
|
|
734
740
|
logicalType: 'dict',
|
|
735
|
-
params: {
|
|
741
|
+
params: Object.freeze({
|
|
736
742
|
keys,
|
|
737
|
-
entries,
|
|
743
|
+
entries: stableEntries,
|
|
738
744
|
...(opts?.doc !== undefined ? { doc: opts.doc } : {}),
|
|
739
745
|
...(opts?.presentation !== undefined ? { presentation: opts.presentation } : {}),
|
|
740
|
-
},
|
|
741
|
-
}
|
|
746
|
+
}),
|
|
747
|
+
})
|
|
742
748
|
attachLogicalType(zodSchema as unknown as object, meta)
|
|
743
749
|
|
|
744
750
|
return {
|
|
@@ -749,16 +755,16 @@ const dict = <const M extends Record<string, DictEntryMeta>>(
|
|
|
749
755
|
keys: () => [...keys],
|
|
750
756
|
labelFor: (key, _locale) => {
|
|
751
757
|
// _locale reservado pra i18n futuro; no v1 sempre retorna o label cru.
|
|
752
|
-
return (
|
|
758
|
+
return (stableEntries[key] as M[keyof M]).label
|
|
753
759
|
},
|
|
754
|
-
metaFor: (key) =>
|
|
760
|
+
metaFor: (key) => stableEntries[key] as M[keyof M],
|
|
755
761
|
options: () =>
|
|
756
762
|
keys.map((k) => ({
|
|
757
|
-
...(
|
|
763
|
+
...(stableEntries[k] as M[keyof M]),
|
|
758
764
|
value: k,
|
|
759
765
|
})) as DictOption<K, M[keyof M]>[],
|
|
760
766
|
has: (key: string): key is K =>
|
|
761
|
-
Object.prototype.hasOwnProperty.call(
|
|
767
|
+
Object.prototype.hasOwnProperty.call(stableEntries, key),
|
|
762
768
|
}
|
|
763
769
|
}
|
|
764
770
|
|
|
@@ -197,7 +197,7 @@ function LabelHelp({ help }: { help: string | undefined }): React.ReactElement |
|
|
|
197
197
|
</TooltipTrigger>
|
|
198
198
|
{/* max-w-sm: frase de help típica (~50 caracteres) cabe numa linha; o
|
|
199
199
|
text-balance do primitivo só divide o que realmente transborda. */}
|
|
200
|
-
<TooltipContent
|
|
200
|
+
<TooltipContent>{help}</TooltipContent>
|
|
201
201
|
</Tooltip>
|
|
202
202
|
</TooltipProvider>
|
|
203
203
|
)
|
|
@@ -3,25 +3,38 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
|
|
3
3
|
import { cn } from '../../lib/cn.ts'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Variantes do alert (cva). COM ícone o
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Variantes do alert (cva). COM ícone o componente abre uma coluna de realce tonal;
|
|
7
|
+
* sem ícone é bloco comum. `destructive`, `success` e `warning` mantêm superfície
|
|
8
|
+
* tonalizada + borda (divergência da casa — o shadcn rebaixou destructive pra bg-card,
|
|
9
|
+
* nós preservamos o realce).
|
|
10
10
|
*/
|
|
11
11
|
export const alertVariants = cva(
|
|
12
|
-
|
|
13
|
-
// a 1ª coluna em `0` — e aí qualquer nó de texto solto virava item anônimo NESSA coluna
|
|
14
|
-
// de largura zero, saindo uma palavra por linha. Sem ícone, bloco comum: o `col-start-2`
|
|
15
|
-
// dos slots é inócuo fora de grid.
|
|
16
|
-
"relative w-full rounded-lg border px-4 py-3 text-sm has-[>svg]:grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:items-start has-[>svg]:gap-x-3 has-[>svg]:gap-y-0.5 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
|
12
|
+
'relative w-full rounded-lg border px-4 py-3 text-sm has-[>svg]:grid has-[>svg]:grid-cols-[1rem_minmax(0,1fr)] has-[>svg]:items-start has-[>svg]:gap-x-3 has-[>svg]:gap-y-1 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
|
17
13
|
{
|
|
18
14
|
variants: {
|
|
19
15
|
variant: {
|
|
20
16
|
default: 'bg-card text-card-foreground',
|
|
21
17
|
destructive:
|
|
22
|
-
'border-destructive/50 bg-destructive/5 text-destructive *:data-[slot=alert-description]:text-destructive/90
|
|
18
|
+
'border-destructive/50 bg-destructive/5 text-destructive *:data-[slot=alert-description]:text-destructive/90',
|
|
23
19
|
success:
|
|
24
|
-
'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 *:data-[slot=alert-description]:text-emerald-700/90 dark:*:data-[slot=alert-description]:text-emerald-400/90
|
|
20
|
+
'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 *:data-[slot=alert-description]:text-emerald-700/90 dark:*:data-[slot=alert-description]:text-emerald-400/90',
|
|
21
|
+
warning:
|
|
22
|
+
'border-amber-500/40 bg-amber-500/10 text-foreground *:data-[slot=alert-description]:text-muted-foreground',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: { variant: 'default' },
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const alertIconVariants = cva(
|
|
30
|
+
'flex size-9 shrink-0 items-center justify-center rounded-md [&_svg]:size-5',
|
|
31
|
+
{
|
|
32
|
+
variants: {
|
|
33
|
+
variant: {
|
|
34
|
+
default: 'bg-muted text-muted-foreground',
|
|
35
|
+
destructive: 'bg-destructive/10 text-destructive',
|
|
36
|
+
success: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
|
|
37
|
+
warning: 'bg-amber-500/15 text-amber-700 dark:text-amber-300',
|
|
25
38
|
},
|
|
26
39
|
},
|
|
27
40
|
defaultVariants: { variant: 'default' },
|
|
@@ -49,9 +62,11 @@ export function Alert({
|
|
|
49
62
|
description,
|
|
50
63
|
icon,
|
|
51
64
|
children,
|
|
65
|
+
style,
|
|
52
66
|
...props
|
|
53
67
|
}: AlertProps): React.ReactElement {
|
|
54
68
|
const short = title !== undefined || description !== undefined
|
|
69
|
+
const hasIcon = icon !== undefined && icon !== null
|
|
55
70
|
// Texto CRU como filho (`<Alert>Sincronizado.</Alert>`) vira descrição: no layout com
|
|
56
71
|
// ícone ele cairia na coluna do ícone, espremido. Assim o atalho de sempre continua
|
|
57
72
|
// valendo e cai no slot certo.
|
|
@@ -61,16 +76,31 @@ export function Alert({
|
|
|
61
76
|
<div
|
|
62
77
|
data-slot="alert"
|
|
63
78
|
role="alert"
|
|
64
|
-
className={cn(alertVariants({ variant }), className)}
|
|
79
|
+
className={cn(alertVariants({ variant }), hasIcon && 'grid items-start', className)}
|
|
80
|
+
style={hasIcon ? {
|
|
81
|
+
gridTemplateColumns: '2.25rem minmax(0, 1fr)',
|
|
82
|
+
columnGap: '0.75rem',
|
|
83
|
+
rowGap: '0.25rem',
|
|
84
|
+
...style,
|
|
85
|
+
} : style}
|
|
65
86
|
{...props}
|
|
66
87
|
>
|
|
67
|
-
{
|
|
88
|
+
{hasIcon && (
|
|
89
|
+
<span
|
|
90
|
+
data-slot="alert-icon"
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
className={alertIconVariants({ variant })}
|
|
93
|
+
style={{ gridRow: '1 / span 2' }}
|
|
94
|
+
>
|
|
95
|
+
{icon}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
68
98
|
{short ? (
|
|
69
99
|
<>
|
|
70
100
|
{title !== undefined && <AlertTitle>{title}</AlertTitle>}
|
|
71
101
|
{description !== undefined && <AlertDescription>{description}</AlertDescription>}
|
|
72
102
|
{/* A ação (children DEPOIS da frase) precisa da coluna 2: sem `col-start-2`
|
|
73
|
-
ela cai na coluna do ícone
|
|
103
|
+
ela cai na coluna do ícone e é espremida pra esquerda. O
|
|
74
104
|
`col-start-2` é inócuo sem o grid (forma curta sem ícone). */}
|
|
75
105
|
{children !== undefined && children !== null && (
|
|
76
106
|
<div data-slot="alert-actions" className="col-start-2 mt-1">
|
|
@@ -57,14 +57,14 @@ const fieldVariants = cva(
|
|
|
57
57
|
{
|
|
58
58
|
variants: {
|
|
59
59
|
orientation: {
|
|
60
|
-
vertical: ["flex-col gap-
|
|
60
|
+
vertical: ["flex-col gap-1.5 [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
61
61
|
horizontal: [
|
|
62
62
|
"flex-row items-center gap-3",
|
|
63
63
|
"[&>[data-slot=field-label]]:flex-auto",
|
|
64
64
|
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
65
65
|
],
|
|
66
66
|
responsive: [
|
|
67
|
-
"flex-col gap-
|
|
67
|
+
"flex-col gap-1.5 @md/field-group:flex-row @md/field-group:items-center @md/field-group:gap-3 [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
|
|
68
68
|
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
69
69
|
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
70
70
|
],
|
|
@@ -113,9 +113,9 @@ function FieldLabel({
|
|
|
113
113
|
<Label
|
|
114
114
|
data-slot="field-label"
|
|
115
115
|
className={cn(
|
|
116
|
-
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
116
|
+
"group/field-label peer/field-label flex w-fit gap-2 text-xs leading-snug font-medium text-muted-foreground group-data-[invalid=true]/field:text-destructive group-data-[disabled=true]/field:opacity-50",
|
|
117
117
|
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
118
|
-
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
|
|
118
|
+
"has-[>[data-slot=field]]:has-data-[state=checked]:border-primary has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/5 dark:has-[>[data-slot=field]]:has-data-[state=checked]:bg-primary/10",
|
|
119
119
|
className
|
|
120
120
|
)}
|
|
121
121
|
{...props}
|
|
@@ -40,7 +40,7 @@ function TooltipContent({
|
|
|
40
40
|
data-slot="tooltip-content"
|
|
41
41
|
sideOffset={sideOffset}
|
|
42
42
|
className={cn(
|
|
43
|
-
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-
|
|
43
|
+
"z-50 w-fit max-w-sm origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-pretty text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
44
44
|
className
|
|
45
45
|
)}
|
|
46
46
|
{...props}
|
|
@@ -16,6 +16,12 @@ Quase todo alert é ícone + título + uma frase — então isso é UMA linha: `
|
|
|
16
16
|
title="Skill publicada"
|
|
17
17
|
description="Os agentes do workspace já enxergam a nova versão."
|
|
18
18
|
/>
|
|
19
|
+
<Alert
|
|
20
|
+
variant="warning"
|
|
21
|
+
icon={<CircleAlert />}
|
|
22
|
+
title="Revisão necessária"
|
|
23
|
+
description="Confira os dados antes de continuar."
|
|
24
|
+
/>
|
|
19
25
|
```
|
|
20
26
|
|
|
21
27
|
## Só a frase
|
|
@@ -29,7 +35,7 @@ Título é opcional — o aviso de uma linha dispensa. `destructive` e `success`
|
|
|
29
35
|
|
|
30
36
|
## Ícone é opcional
|
|
31
37
|
|
|
32
|
-
Sem `icon` o alert é bloco comum; com ele, vira grid de duas colunas e o conteúdo se alinha ao lado. Texto solto como filho também vale (`<Alert>Sincronizado.</Alert>`) — cai no slot de descrição sozinho.
|
|
38
|
+
Sem `icon` o alert é bloco comum; com ele, vira grid de duas colunas e o conteúdo se alinha ao lado. O ícone da prop recebe o slot `alert-icon` e um realce tonal coerente com a variante. A composição legada com um SVG direto continua abrindo a coluna do ícone. Texto solto como filho também vale (`<Alert>Sincronizado.</Alert>`) — cai no slot de descrição sozinho.
|
|
33
39
|
|
|
34
40
|
```tsx preview col
|
|
35
41
|
<Alert title="Sem provider próprio" description="As conversas usam o padrão do sistema." />
|
|
@@ -64,6 +70,7 @@ Os dois modos convivem: com `title`/`description` preenchidos, `children` entra
|
|
|
64
70
|
|---|---|---|---|
|
|
65
71
|
| `title` | `React.ReactNode` | | Título do alert (a forma curta). Não é o atributo `title` do HTML — esse é tooltip nativo, banido na casa, e o componente não o aceita. |
|
|
66
72
|
| `description` | `React.ReactNode` | | A frase. Sozinha, dispensa título. |
|
|
67
|
-
| `icon` | `React.ReactNode` | | Ícone à esquerda;
|
|
68
|
-
| `variant` | `'default' \| 'destructive' \| 'success'` | `'default'` | O tom da mensagem. |
|
|
73
|
+
| `icon` | `React.ReactNode` | | Ícone à esquerda; liga o layout de duas colunas e recebe realce tonal no slot `alert-icon`. Decorativo — quem nomeia é o título. |
|
|
74
|
+
| `variant` | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` | O tom da mensagem e do realce do ícone. |
|
|
69
75
|
| `children` | `React.ReactNode` | | Composição (`AlertTitle`/`AlertDescription`), texto cru, ou — junto da forma curta — o que vem depois da frase. |
|
|
76
|
+
| `style` | `React.CSSProperties` | | Ajuste excepcional das medidas internas; com `icon`, pode sobrescrever as colunas e os espaçamentos padrão. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## Campo vertical
|
|
2
2
|
|
|
3
|
-
A composição base: FieldLabel (htmlFor↔id), o controle e FieldDescription como ajuda — empilhados, com o respiro do Field.
|
|
3
|
+
A composição base: FieldLabel (htmlFor↔id), o controle e FieldDescription como ajuda — empilhados, com o respiro do Field. O rótulo usa a mesma hierarquia compacta de `DetailField`; quando o campo é inválido, muda explicitamente para o tom destrutivo.
|
|
4
4
|
|
|
5
5
|
```tsx preview col md
|
|
6
6
|
<Field>
|
|
@@ -41,10 +41,24 @@ O TooltipProvider embrulha o app uma vez (delay compartilhado); cada Tooltip dis
|
|
|
41
41
|
</TooltipProvider>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
## Texto longo
|
|
45
|
+
|
|
46
|
+
O conteúdo tem largura máxima compartilhada e distribui as palavras naturalmente. O consumidor
|
|
47
|
+
pode reduzir ou ampliar esse limite com `className` quando a composição exigir, sem repetir o
|
|
48
|
+
default nos formulários.
|
|
49
|
+
|
|
50
|
+
```tsx preview
|
|
51
|
+
<Tooltip>
|
|
52
|
+
<TooltipTrigger asChild><Button variant="ghost">Como funciona</Button></TooltipTrigger>
|
|
53
|
+
<TooltipContent>Registre a evidência usada para que esta decisão possa ser consultada depois.</TooltipContent>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
```
|
|
56
|
+
|
|
44
57
|
## Props
|
|
45
58
|
|
|
46
59
|
| Prop | Tipo | Default | Descrição |
|
|
47
60
|
|---|---|---|---|
|
|
48
61
|
| `TooltipProvider.delayDuration` | `number` | `0` | Atraso (ms) até aparecer — compartilhado por todos os tooltips do app. |
|
|
49
62
|
| `TooltipContent.side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'top'` | Lado preferido; inverte sozinho sem espaço (Radix). |
|
|
50
|
-
| `TooltipContent.sideOffset` | `number` | `
|
|
63
|
+
| `TooltipContent.sideOffset` | `number` | `0` | Distância (px) entre gatilho e dica. |
|
|
64
|
+
| `TooltipContent.className` | `string` | `max-w-sm text-pretty` | Permite substituir o limite e a distribuição de linha quando necessário. |
|
package/src/ui/meta.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const componentMeta = {
|
|
|
21
21
|
name: 'alert',
|
|
22
22
|
ancestry: 'opus',
|
|
23
23
|
whenToUse:
|
|
24
|
-
'Aviso INLINE no fluxo da página (o erro do login, o "sincronizado" da lista) — `role="alert"`, então o leitor de tela anuncia sozinho. Forma CURTA (o caso comum): `<Alert title description icon variant />` numa linha; composição (`AlertTitle`/`AlertDescription`) só quando o conteúdo é rico (parágrafos, link, ação). `variant` default/destructive/success. Pra INTERROMPER cobrando decisão, use `confirm()`; pra recado passageiro, `toast`.',
|
|
24
|
+
'Aviso INLINE no fluxo da página (o erro do login, o "sincronizado" da lista) — `role="alert"`, então o leitor de tela anuncia sozinho. Forma CURTA (o caso comum): `<Alert title description icon variant />` numa linha; composição (`AlertTitle`/`AlertDescription`) só quando o conteúdo é rico (parágrafos, link, ação). `variant` default/destructive/success/warning; quando informado, o ícone recebe realce tonal nativo. Pra INTERROMPER cobrando decisão, use `confirm()`; pra recado passageiro, `toast`.',
|
|
25
25
|
},
|
|
26
26
|
'badge': {
|
|
27
27
|
name: 'badge',
|