@seedgrid/fe-components 2026.11.3 → 2026.11.5
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/dist/ai/seedgrid-components.manifest.json +101 -11
- package/dist/buttons/SgButton.d.ts +41 -3
- package/dist/buttons/SgButton.d.ts.map +1 -1
- package/dist/buttons/SgButton.js +51 -5
- package/dist/buttons/SgButton.meta.d.ts.map +1 -1
- package/dist/buttons/SgButton.meta.js +27 -5
- package/dist/gadgets/calendar/SgCalendar.d.ts +6 -0
- package/dist/gadgets/calendar/SgCalendar.d.ts.map +1 -1
- package/dist/gadgets/calendar/SgCalendar.js +9 -5
- package/dist/gadgets/calendar/SgCalendar.meta.d.ts.map +1 -1
- package/dist/gadgets/calendar/SgCalendar.meta.js +1 -0
- package/dist/gadgets/clock/SgClock.d.ts +25 -1
- package/dist/gadgets/clock/SgClock.d.ts.map +1 -1
- package/dist/gadgets/clock/SgClock.js +18 -3
- package/dist/gadgets/clock/SgClock.meta.d.ts.map +1 -1
- package/dist/gadgets/clock/SgClock.meta.js +2 -1
- package/dist/generated/compiledStyles.d.ts +1 -1
- package/dist/generated/compiledStyles.d.ts.map +1 -1
- package/dist/generated/compiledStyles.js +1 -1
- package/dist/i18n/SgComponentsI18nProvider.meta.d.ts.map +1 -1
- package/dist/i18n/SgComponentsI18nProvider.meta.js +6 -3
- package/dist/i18n/useComponentsI18n.meta.d.ts.map +1 -1
- package/dist/i18n/useComponentsI18n.meta.js +8 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/inputs/SgDatatable.d.ts +7 -0
- package/dist/inputs/SgDatatable.d.ts.map +1 -1
- package/dist/inputs/SgDatatable.js +23 -11
- package/dist/inputs/SgDatatable.meta.d.ts.map +1 -1
- package/dist/inputs/SgDatatable.meta.js +1 -0
- package/dist/layout/SgToolBar.d.ts +6 -0
- package/dist/layout/SgToolBar.d.ts.map +1 -1
- package/dist/layout/SgToolBar.js +107 -5
- package/dist/layout/SgToolBar.meta.d.ts.map +1 -1
- package/dist/layout/SgToolBar.meta.js +8 -0
- package/dist/layout/toolbar-logic.d.ts +11 -0
- package/dist/layout/toolbar-logic.d.ts.map +1 -1
- package/dist/layout/toolbar-logic.js +25 -0
- package/dist/sandbox.cjs +55 -55
- package/dist/style.css +2 -2
- package/dist/wizard/SgWizard.d.ts +7 -0
- package/dist/wizard/SgWizard.d.ts.map +1 -1
- package/dist/wizard/SgWizard.js +11 -2
- package/dist/wizard/SgWizard.meta.d.ts.map +1 -1
- package/dist/wizard/SgWizard.meta.js +1 -0
- package/package.json +3 -3
|
@@ -3141,6 +3141,15 @@
|
|
|
3141
3141
|
"semanticRole": "appearance",
|
|
3142
3142
|
"bindable": true
|
|
3143
3143
|
},
|
|
3144
|
+
{
|
|
3145
|
+
"name": "enabled",
|
|
3146
|
+
"type": "boolean",
|
|
3147
|
+
"required": false,
|
|
3148
|
+
"default": true,
|
|
3149
|
+
"description": "Quando false, bloqueia as acoes que mudam estado: ordenacao, selecao de linhas, filtros (global e por coluna), limpar filtros e gerenciador de colunas. Paginacao, rolagem e carga de mais linhas continuam funcionando. Aparencia de desabilitado e aria-disabled.",
|
|
3150
|
+
"semanticRole": "behavior",
|
|
3151
|
+
"bindable": true
|
|
3152
|
+
},
|
|
3144
3153
|
{
|
|
3145
3154
|
"name": "mobileMode",
|
|
3146
3155
|
"type": "\"scroll\" | \"cards\"",
|
|
@@ -10484,7 +10493,7 @@
|
|
|
10484
10493
|
"type": "boolean",
|
|
10485
10494
|
"required": false,
|
|
10486
10495
|
"default": false,
|
|
10487
|
-
"description": "Exibe spinner e bloqueia
|
|
10496
|
+
"description": "Exibe spinner e bloqueia o clique. Continua valendo e vence o loading automático do onClick: o loading efetivo é loading ou operação pendente (Promise/Future devolvida pelo onClick).",
|
|
10488
10497
|
"semanticRole": "behavior",
|
|
10489
10498
|
"bindable": true
|
|
10490
10499
|
},
|
|
@@ -10515,9 +10524,9 @@
|
|
|
10515
10524
|
},
|
|
10516
10525
|
{
|
|
10517
10526
|
"name": "onClick",
|
|
10518
|
-
"type": "
|
|
10527
|
+
"type": "(event: MouseEvent<HTMLButtonElement>) => unknown",
|
|
10519
10528
|
"required": false,
|
|
10520
|
-
"description": "
|
|
10529
|
+
"description": "Clique do botão; pode ser síncrono ou assíncrono. Síncrono (abrir modal, alternar): nada muda. Se DEVOLVER uma Promise/Future, como async () => { await salvar(); } ou () => salvar(), o botão entra em loading sozinho (spinner, desabilitado, aria-busy na web), ignora cliques até terminar (inclusive cliques seguidos antes de a tela atualizar) e volta ao normal ao terminar ou falhar. Protege contra duplo clique e reenvio e ajuda na idempotência da ação na tela (não substitui a chave de idempotência no servidor). Cuidado: a função precisa devolver a operação; com chaves e sem return (ou sem async), como () => { salvar(); } ou () { salvar(); }, não protege; já async () {...} sempre devolve. O erro não é engolido: o botão sai do loading e relança como rejeição não tratada; trate com try/catch dentro do onClick e avise o usuário (sgWhistle.error). Use async só quando a ação puder demorar ou falhar (salvar, enviar, pagar).",
|
|
10521
10530
|
"semanticRole": "event",
|
|
10522
10531
|
"bindable": false
|
|
10523
10532
|
},
|
|
@@ -10554,6 +10563,24 @@
|
|
|
10554
10563
|
"file": "apps/showcase/src/app/components/sg-button/samples/basico.tsx.sample",
|
|
10555
10564
|
"kind": "sample"
|
|
10556
10565
|
},
|
|
10566
|
+
{
|
|
10567
|
+
"id": "auto-loading",
|
|
10568
|
+
"title": "Loading automático com Future",
|
|
10569
|
+
"file": "apps/showcase/src/app/components/sg-button/samples/loading-automatico-com-future.tsx.sample",
|
|
10570
|
+
"kind": "sample"
|
|
10571
|
+
},
|
|
10572
|
+
{
|
|
10573
|
+
"id": "sync-vs-async",
|
|
10574
|
+
"title": "onClick síncrono x assíncrono",
|
|
10575
|
+
"file": "apps/showcase/src/app/components/sg-button/samples/onclick-sincrono-x-assincrono.tsx.sample",
|
|
10576
|
+
"kind": "sample"
|
|
10577
|
+
},
|
|
10578
|
+
{
|
|
10579
|
+
"id": "async-error",
|
|
10580
|
+
"title": "Tratando erro no onClick assíncrono",
|
|
10581
|
+
"file": "apps/showcase/src/app/components/sg-button/samples/tratando-erro-no-onclick-assincrono.tsx.sample",
|
|
10582
|
+
"kind": "sample"
|
|
10583
|
+
},
|
|
10557
10584
|
{
|
|
10558
10585
|
"id": "playground",
|
|
10559
10586
|
"title": "Playground",
|
|
@@ -10581,7 +10608,8 @@
|
|
|
10581
10608
|
"preferredUseCases": [
|
|
10582
10609
|
"Acoes primarias e secundarias em formularios e telas CRUD.",
|
|
10583
10610
|
"Submit, confirmacao, salvar, cancelar e outras acoes explicitas do usuario.",
|
|
10584
|
-
"Chamadas para acao com feedback visual de loading."
|
|
10611
|
+
"Chamadas para acao com feedback visual de loading.",
|
|
10612
|
+
"Salvar, enviar ou pagar: use onClick assíncrono (async () => { await salvar(); } ou () => salvar()) para o botão entrar em loading sozinho e ignorar duplo clique; acoes instantaneas (abrir modal, alternar) ficam sincronas."
|
|
10585
10613
|
],
|
|
10586
10614
|
"avoidUseCases": [
|
|
10587
10615
|
"Agrupar conteudo ou estruturar layout; nesses casos prefira SgPanel, SgCard ou SgStack.",
|
|
@@ -10603,7 +10631,10 @@
|
|
|
10603
10631
|
],
|
|
10604
10632
|
"compositionHints": [
|
|
10605
10633
|
"Combinar com SgInputText e outros campos em formularios.",
|
|
10606
|
-
"Usar dentro de SgPanel ou SgDialog para acoes contextuais."
|
|
10634
|
+
"Usar dentro de SgPanel ou SgDialog para acoes contextuais.",
|
|
10635
|
+
"O onClick precisa DEVOLVER a Promise/Future para o loading automatico funcionar: onClick={() => { salvar(); }} (chaves sem return) nao protege; use () => salvar() ou async () => { await salvar(); }. No Flutter, async () {...} sempre devolve Future; () { salvar(); } nao.",
|
|
10636
|
+
"O botao nao engole o erro do onClick assincrono: sai do loading e relanca como rejeicao nao tratada. Trate com try/catch dentro do onClick e avise o usuario com sgWhistle.error (Flutter: sgWhistle.error(SgWhistleOptions(...))).",
|
|
10637
|
+
"O loading automatico protege a acao na tela (duplo clique/reenvio), mas nao substitui a chave de idempotencia no servidor. A prop loading manual vence: loading efetivo = loading ou operacao pendente."
|
|
10607
10638
|
],
|
|
10608
10639
|
"rankingSignals": {
|
|
10609
10640
|
"freeText": 0,
|
|
@@ -18099,6 +18130,15 @@
|
|
|
18099
18130
|
"semanticRole": "behavior",
|
|
18100
18131
|
"bindable": true
|
|
18101
18132
|
},
|
|
18133
|
+
{
|
|
18134
|
+
"name": "overflow",
|
|
18135
|
+
"type": "boolean",
|
|
18136
|
+
"required": false,
|
|
18137
|
+
"default": false,
|
|
18138
|
+
"description": "Move para um menu '...' os botoes que nao cabem na largura disponivel; abaixo de 600px abre como painel inferior.",
|
|
18139
|
+
"semanticRole": "behavior",
|
|
18140
|
+
"bindable": true
|
|
18141
|
+
},
|
|
18102
18142
|
{
|
|
18103
18143
|
"name": "children",
|
|
18104
18144
|
"type": "ReactNode",
|
|
@@ -18542,6 +18582,15 @@
|
|
|
18542
18582
|
"semanticRole": "behavior",
|
|
18543
18583
|
"bindable": true
|
|
18544
18584
|
},
|
|
18585
|
+
{
|
|
18586
|
+
"name": "enabled",
|
|
18587
|
+
"type": "boolean",
|
|
18588
|
+
"required": false,
|
|
18589
|
+
"default": true,
|
|
18590
|
+
"description": "Quando false, o wizard inteiro fica desabilitado: Anterior, Proximo, Concluir e os passos do stepper deixam de responder; a etapa atual segue visivel. Diferente de nextDisabled, que so bloqueia o avanco.",
|
|
18591
|
+
"semanticRole": "behavior",
|
|
18592
|
+
"bindable": true
|
|
18593
|
+
},
|
|
18545
18594
|
{
|
|
18546
18595
|
"name": "onStepChange",
|
|
18547
18596
|
"type": "(index: number) => void",
|
|
@@ -18901,6 +18950,15 @@
|
|
|
18901
18950
|
"semanticRole": "validation",
|
|
18902
18951
|
"bindable": true
|
|
18903
18952
|
},
|
|
18953
|
+
{
|
|
18954
|
+
"name": "enabled",
|
|
18955
|
+
"type": "boolean",
|
|
18956
|
+
"required": false,
|
|
18957
|
+
"default": true,
|
|
18958
|
+
"description": "Quando false, os dias, os botoes de mes anterior/proximo e o atalho hoje ficam desabilitados (nao focaveis); a data e o mes atuais continuam legiveis.",
|
|
18959
|
+
"semanticRole": "behavior",
|
|
18960
|
+
"bindable": true
|
|
18961
|
+
},
|
|
18904
18962
|
{
|
|
18905
18963
|
"name": "onValueChange",
|
|
18906
18964
|
"type": "(date: Date) => void",
|
|
@@ -19087,13 +19145,21 @@
|
|
|
19087
19145
|
},
|
|
19088
19146
|
{
|
|
19089
19147
|
"name": "digitalStyle",
|
|
19090
|
-
"type": "\"default\" | \"segment\" | \"sevenSegment\" | \"roller3d\" | \"flip\" | \"fade\" | \"matrix\" | \"neon\" | \"discard\"",
|
|
19148
|
+
"type": "\"default\" | \"segment\" | \"sevenSegment\" | \"roller3d\" | \"flip\" | \"fade\" | \"matrix\" | \"neon\" | \"discard\" | \"custom\"",
|
|
19091
19149
|
"required": false,
|
|
19092
19150
|
"default": "default",
|
|
19093
19151
|
"description": "Estilo visual do relogio digital.",
|
|
19094
19152
|
"semanticRole": "appearance",
|
|
19095
19153
|
"bindable": true
|
|
19096
19154
|
},
|
|
19155
|
+
{
|
|
19156
|
+
"name": "digitBuilder",
|
|
19157
|
+
"type": "(parts: SgClockDigitalParts) => ReactNode",
|
|
19158
|
+
"required": false,
|
|
19159
|
+
"description": "Desenha o relogio digital quando digitalStyle e custom; sem ele, usa o estilo default.",
|
|
19160
|
+
"semanticRole": "appearance",
|
|
19161
|
+
"bindable": false
|
|
19162
|
+
},
|
|
19097
19163
|
{
|
|
19098
19164
|
"name": "timezone",
|
|
19099
19165
|
"type": "string",
|
|
@@ -23718,20 +23784,38 @@
|
|
|
23718
23784
|
"examples": [
|
|
23719
23785
|
{
|
|
23720
23786
|
"id": "basic",
|
|
23721
|
-
"title": "Uso
|
|
23787
|
+
"title": "Uso básico",
|
|
23722
23788
|
"file": "apps/showcase/src/app/components/providers/sg-components-i18n-provider/samples/uso-basico.tsx.sample",
|
|
23723
23789
|
"kind": "sample"
|
|
23724
23790
|
},
|
|
23725
23791
|
{
|
|
23726
23792
|
"id": "dynamic",
|
|
23727
|
-
"title": "Troca
|
|
23793
|
+
"title": "Troca dinâmica de locale",
|
|
23728
23794
|
"file": "apps/showcase/src/app/components/providers/sg-components-i18n-provider/samples/troca-dinamica-de-locale.tsx.sample",
|
|
23729
23795
|
"kind": "sample"
|
|
23796
|
+
},
|
|
23797
|
+
{
|
|
23798
|
+
"id": "global",
|
|
23799
|
+
"title": "API imperativa (setComponentsI18n)",
|
|
23800
|
+
"file": "apps/showcase/src/app/components/providers/sg-components-i18n-provider/samples/api-imperativa-setcomponentsi18n.tsx.sample",
|
|
23801
|
+
"kind": "sample"
|
|
23802
|
+
},
|
|
23803
|
+
{
|
|
23804
|
+
"id": "custom",
|
|
23805
|
+
"title": "Sobrescrita de mensagens",
|
|
23806
|
+
"file": "apps/showcase/src/app/components/providers/sg-components-i18n-provider/samples/sobrescrita-de-mensagens.tsx.sample",
|
|
23807
|
+
"kind": "sample"
|
|
23808
|
+
},
|
|
23809
|
+
{
|
|
23810
|
+
"id": "playground",
|
|
23811
|
+
"title": "Playground",
|
|
23812
|
+
"file": "apps/showcase/src/app/components/providers/sg-components-i18n-provider/sg-components-i18n-provider.tsx.playground",
|
|
23813
|
+
"kind": "playground"
|
|
23730
23814
|
}
|
|
23731
23815
|
],
|
|
23732
23816
|
"showcase": {
|
|
23733
23817
|
"route": "/components/providers/sg-components-i18n-provider",
|
|
23734
|
-
"hasPlayground":
|
|
23818
|
+
"hasPlayground": true,
|
|
23735
23819
|
"hasPropsTable": true
|
|
23736
23820
|
},
|
|
23737
23821
|
"sdui": {
|
|
@@ -23843,14 +23927,20 @@
|
|
|
23843
23927
|
},
|
|
23844
23928
|
{
|
|
23845
23929
|
"id": "locale-aware",
|
|
23846
|
-
"title": "
|
|
23930
|
+
"title": "Construindo componentes cientes de locale",
|
|
23847
23931
|
"file": "apps/showcase/src/app/components/hooks/use-components-i18n/samples/componentes-conscientes-do-locale.tsx.sample",
|
|
23848
23932
|
"kind": "sample"
|
|
23933
|
+
},
|
|
23934
|
+
{
|
|
23935
|
+
"id": "playground",
|
|
23936
|
+
"title": "Playground",
|
|
23937
|
+
"file": "apps/showcase/src/app/components/hooks/use-components-i18n/use-components-i18n.tsx.playground",
|
|
23938
|
+
"kind": "playground"
|
|
23849
23939
|
}
|
|
23850
23940
|
],
|
|
23851
23941
|
"showcase": {
|
|
23852
23942
|
"route": "/components/hooks/use-components-i18n",
|
|
23853
|
-
"hasPlayground":
|
|
23943
|
+
"hasPlayground": true,
|
|
23854
23944
|
"hasPropsTable": true
|
|
23855
23945
|
}
|
|
23856
23946
|
},
|
|
@@ -14,7 +14,24 @@ export type SgButtonCustomColors = {
|
|
|
14
14
|
activeBg?: string;
|
|
15
15
|
ring?: string;
|
|
16
16
|
};
|
|
17
|
-
export type SgButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color" | "disabled"> & {
|
|
17
|
+
export type SgButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color" | "disabled" | "onClick"> & {
|
|
18
|
+
/**
|
|
19
|
+
* Clique do botão. Pode ser síncrono ou assíncrono (`(event) => unknown`).
|
|
20
|
+
*
|
|
21
|
+
* - Síncrono (abrir modal, alternar painel): funciona como sempre, nada muda.
|
|
22
|
+
* - Se DEVOLVER uma Promise (`async () => { await salvar(); }` ou `() => salvar()`), o botão entra em loading
|
|
23
|
+
* sozinho (spinner, desabilitado, aria-busy), ignora cliques até terminar (inclusive cliques seguidos antes do
|
|
24
|
+
* re-render) e volta ao normal ao terminar ou falhar. Protege contra duplo clique e reenvio e ajuda na
|
|
25
|
+
* idempotência da ação na tela (não substitui a chave de idempotência no servidor).
|
|
26
|
+
* - Cuidado: precisa DEVOLVER a Promise. `onClick={() => { salvar(); }}` (chaves sem return) NÃO protege.
|
|
27
|
+
* - Erro: o botão não engole o erro (sai do loading e relança como rejeição não tratada). Trate com try/catch
|
|
28
|
+
* dentro do onClick e avise o usuário (ex.: `sgWhistle.error`).
|
|
29
|
+
* - Use async só quando a ação puder demorar ou falhar (salvar, enviar, pagar); ações instantâneas ficam síncronas.
|
|
30
|
+
* - A prop `loading` manual continua valendo e vence (loading efetivo = `loading` || Promise pendente).
|
|
31
|
+
*
|
|
32
|
+
* O retorno é `unknown` só para não quebrar handlers existentes de corpo-expressão (`() => lista.push(x)`).
|
|
33
|
+
*/
|
|
34
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => unknown;
|
|
18
35
|
/** Quando false, desabilita o botão. Padrão: true. */
|
|
19
36
|
enabled?: boolean;
|
|
20
37
|
severity?: Severity;
|
|
@@ -26,11 +43,31 @@ export type SgButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
|
26
43
|
iconOnly?: boolean;
|
|
27
44
|
leftIcon?: React.ReactNode;
|
|
28
45
|
rightIcon?: React.ReactNode;
|
|
46
|
+
/** Exibe spinner e bloqueia o clique. Vence o loading automático do onClick: loading efetivo = `loading` || Promise pendente. */
|
|
29
47
|
loading?: boolean;
|
|
30
48
|
customColors?: SgButtonCustomColors;
|
|
31
49
|
};
|
|
32
|
-
export declare function resolveButtonColors(severity: Severity, custom?: SgButtonCustomColors): Required<SgButtonCustomColors
|
|
33
|
-
|
|
50
|
+
export declare function resolveButtonColors(severity: Severity, custom?: SgButtonCustomColors): Required<SgButtonCustomColors> & {
|
|
51
|
+
activeFg: string;
|
|
52
|
+
};
|
|
53
|
+
export declare const SgButton: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "disabled" | "color" | "onClick"> & {
|
|
54
|
+
/**
|
|
55
|
+
* Clique do botão. Pode ser síncrono ou assíncrono (`(event) => unknown`).
|
|
56
|
+
*
|
|
57
|
+
* - Síncrono (abrir modal, alternar painel): funciona como sempre, nada muda.
|
|
58
|
+
* - Se DEVOLVER uma Promise (`async () => { await salvar(); }` ou `() => salvar()`), o botão entra em loading
|
|
59
|
+
* sozinho (spinner, desabilitado, aria-busy), ignora cliques até terminar (inclusive cliques seguidos antes do
|
|
60
|
+
* re-render) e volta ao normal ao terminar ou falhar. Protege contra duplo clique e reenvio e ajuda na
|
|
61
|
+
* idempotência da ação na tela (não substitui a chave de idempotência no servidor).
|
|
62
|
+
* - Cuidado: precisa DEVOLVER a Promise. `onClick={() => { salvar(); }}` (chaves sem return) NÃO protege.
|
|
63
|
+
* - Erro: o botão não engole o erro (sai do loading e relança como rejeição não tratada). Trate com try/catch
|
|
64
|
+
* dentro do onClick e avise o usuário (ex.: `sgWhistle.error`).
|
|
65
|
+
* - Use async só quando a ação puder demorar ou falhar (salvar, enviar, pagar); ações instantâneas ficam síncronas.
|
|
66
|
+
* - A prop `loading` manual continua valendo e vence (loading efetivo = `loading` || Promise pendente).
|
|
67
|
+
*
|
|
68
|
+
* O retorno é `unknown` só para não quebrar handlers existentes de corpo-expressão (`() => lista.push(x)`).
|
|
69
|
+
*/
|
|
70
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => unknown;
|
|
34
71
|
/** Quando false, desabilita o botão. Padrão: true. */
|
|
35
72
|
enabled?: boolean;
|
|
36
73
|
severity?: Severity;
|
|
@@ -42,6 +79,7 @@ export declare const SgButton: React.ForwardRefExoticComponent<Omit<React.Button
|
|
|
42
79
|
iconOnly?: boolean;
|
|
43
80
|
leftIcon?: React.ReactNode;
|
|
44
81
|
rightIcon?: React.ReactNode;
|
|
82
|
+
/** Exibe spinner e bloqueia o clique. Vence o loading automático do onClick: loading efetivo = `loading` || Promise pendente. */
|
|
45
83
|
loading?: boolean;
|
|
46
84
|
customColors?: SgButtonCustomColors;
|
|
47
85
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgButton.d.ts","sourceRoot":"","sources":["../../src/buttons/SgButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,KAAK,QAAQ,GACT,SAAS,GACT,WAAW,GACX,SAAS,GACT,MAAM,GACN,SAAS,GACT,MAAM,GACN,QAAQ,CAAC;AAEb,KAAK,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhD,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/B,KAAK,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,KAAK,SAAS,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtC,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;
|
|
1
|
+
{"version":3,"file":"SgButton.d.ts","sourceRoot":"","sources":["../../src/buttons/SgButton.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,KAAK,QAAQ,GACT,SAAS,GACT,WAAW,GACX,SAAS,GACT,MAAM,GACN,SAAS,GACT,MAAM,GACN,QAAQ,CAAC;AAEb,KAAK,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhD,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/B,KAAK,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,KAAK,SAAS,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtC,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC,GAAG;IAClH;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,OAAO,CAAC;IAClE,sDAAsD;IACtD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,iIAAiI;IACjI,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAoFF,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,MAAM,CAAC,EAAE,oBAAoB,GAC5B,QAAQ,CAAC,oBAAoB,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAcvD;AAqDD,eAAO,MAAM,QAAQ;IAzLnB;;;;;;;;;;;;;;;OAeG;cACO,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,OAAO;IACjE,sDAAsD;cAC5C,OAAO;eACN,QAAQ;iBACN,UAAU;WAChB,IAAI;YACH,KAAK;gBACD,SAAS;YACb,KAAK,CAAC,SAAS;eACZ,OAAO;eACP,KAAK,CAAC,SAAS;gBACd,KAAK,CAAC,SAAS;IAC3B,iIAAiI;cACvH,OAAO;mBACF,oBAAoB;2CAsQpC,CAAC"}
|
package/dist/buttons/SgButton.js
CHANGED
|
@@ -10,6 +10,8 @@ const PRESET = {
|
|
|
10
10
|
fg: "var(--sg-btn-primary-fg, hsl(var(--primary-foreground)))",
|
|
11
11
|
border: "var(--sg-btn-primary-border, hsl(var(--primary)))",
|
|
12
12
|
hoverBg: "var(--sg-btn-primary-hover-bg, hsl(var(--primary)))",
|
|
13
|
+
hoverFg: "var(--sg-btn-primary-hover-fg, var(--sg-btn-primary-fg, hsl(var(--primary-foreground))))",
|
|
14
|
+
activeFg: "var(--sg-btn-primary-active-fg, var(--sg-btn-primary-fg, hsl(var(--primary-foreground))))",
|
|
13
15
|
ring: "var(--sg-btn-primary-ring, hsl(var(--primary)/0.35))"
|
|
14
16
|
},
|
|
15
17
|
secondary: {
|
|
@@ -17,6 +19,8 @@ const PRESET = {
|
|
|
17
19
|
fg: "var(--sg-btn-secondary-fg, hsl(var(--secondary-foreground)))",
|
|
18
20
|
border: "var(--sg-btn-secondary-border, hsl(var(--secondary)))",
|
|
19
21
|
hoverBg: "var(--sg-btn-secondary-hover-bg, hsl(var(--secondary)))",
|
|
22
|
+
hoverFg: "var(--sg-btn-secondary-hover-fg, var(--sg-btn-secondary-fg, hsl(var(--secondary-foreground))))",
|
|
23
|
+
activeFg: "var(--sg-btn-secondary-active-fg, var(--sg-btn-secondary-fg, hsl(var(--secondary-foreground))))",
|
|
20
24
|
ring: "var(--sg-btn-secondary-ring, hsl(var(--secondary)/0.35))"
|
|
21
25
|
},
|
|
22
26
|
success: {
|
|
@@ -24,6 +28,8 @@ const PRESET = {
|
|
|
24
28
|
fg: "var(--sg-btn-success-fg, hsl(var(--tertiary-foreground, var(--accent-foreground, 0 0% 100%))))",
|
|
25
29
|
border: "var(--sg-btn-success-border, hsl(var(--tertiary, var(--accent))))",
|
|
26
30
|
hoverBg: "var(--sg-btn-success-hover-bg, hsl(var(--tertiary, var(--accent))))",
|
|
31
|
+
hoverFg: "var(--sg-btn-success-hover-fg, var(--sg-btn-success-fg, hsl(var(--tertiary-foreground, var(--accent-foreground, 0 0% 100%)))))",
|
|
32
|
+
activeFg: "var(--sg-btn-success-active-fg, var(--sg-btn-success-fg, hsl(var(--tertiary-foreground, var(--accent-foreground, 0 0% 100%)))))",
|
|
27
33
|
ring: "var(--sg-btn-success-ring, hsl(var(--tertiary, var(--accent))/0.35))"
|
|
28
34
|
},
|
|
29
35
|
info: {
|
|
@@ -31,6 +37,8 @@ const PRESET = {
|
|
|
31
37
|
fg: "var(--sg-btn-info-fg, hsl(var(--secondary-foreground, var(--primary-foreground))))",
|
|
32
38
|
border: "var(--sg-btn-info-border, hsl(var(--secondary, var(--primary))))",
|
|
33
39
|
hoverBg: "var(--sg-btn-info-hover-bg, hsl(var(--secondary, var(--primary))))",
|
|
40
|
+
hoverFg: "var(--sg-btn-info-hover-fg, var(--sg-btn-info-fg, hsl(var(--secondary-foreground, var(--primary-foreground)))))",
|
|
41
|
+
activeFg: "var(--sg-btn-info-active-fg, var(--sg-btn-info-fg, hsl(var(--secondary-foreground, var(--primary-foreground)))))",
|
|
34
42
|
ring: "var(--sg-btn-info-ring, hsl(var(--secondary, var(--primary))/0.35))"
|
|
35
43
|
},
|
|
36
44
|
warning: {
|
|
@@ -38,6 +46,8 @@ const PRESET = {
|
|
|
38
46
|
fg: "var(--sg-btn-warning-fg, hsl(var(--secondary-foreground, var(--accent-foreground, 0 0% 100%))))",
|
|
39
47
|
border: "var(--sg-btn-warning-border, hsl(var(--secondary, var(--accent))))",
|
|
40
48
|
hoverBg: "var(--sg-btn-warning-hover-bg, hsl(var(--secondary, var(--accent))))",
|
|
49
|
+
hoverFg: "var(--sg-btn-warning-hover-fg, var(--sg-btn-warning-fg, hsl(var(--secondary-foreground, var(--accent-foreground, 0 0% 100%)))))",
|
|
50
|
+
activeFg: "var(--sg-btn-warning-active-fg, var(--sg-btn-warning-fg, hsl(var(--secondary-foreground, var(--accent-foreground, 0 0% 100%)))))",
|
|
41
51
|
ring: "var(--sg-btn-warning-ring, hsl(var(--secondary, var(--accent))/0.35))"
|
|
42
52
|
},
|
|
43
53
|
help: {
|
|
@@ -45,6 +55,8 @@ const PRESET = {
|
|
|
45
55
|
fg: "var(--sg-btn-help-fg, hsl(var(--secondary-foreground, var(--primary-foreground))))",
|
|
46
56
|
border: "var(--sg-btn-help-border, hsl(var(--secondary, var(--primary))))",
|
|
47
57
|
hoverBg: "var(--sg-btn-help-hover-bg, hsl(var(--secondary, var(--primary))))",
|
|
58
|
+
hoverFg: "var(--sg-btn-help-hover-fg, var(--sg-btn-help-fg, hsl(var(--secondary-foreground, var(--primary-foreground)))))",
|
|
59
|
+
activeFg: "var(--sg-btn-help-active-fg, var(--sg-btn-help-fg, hsl(var(--secondary-foreground, var(--primary-foreground)))))",
|
|
48
60
|
ring: "var(--sg-btn-help-ring, hsl(var(--secondary, var(--primary))/0.35))"
|
|
49
61
|
},
|
|
50
62
|
danger: {
|
|
@@ -52,6 +64,8 @@ const PRESET = {
|
|
|
52
64
|
fg: "var(--sg-btn-danger-fg, hsl(var(--destructive-foreground)))",
|
|
53
65
|
border: "var(--sg-btn-danger-border, hsl(var(--destructive)))",
|
|
54
66
|
hoverBg: "var(--sg-btn-danger-hover-bg, hsl(var(--destructive)))",
|
|
67
|
+
hoverFg: "var(--sg-btn-danger-hover-fg, var(--sg-btn-danger-fg, hsl(var(--destructive-foreground))))",
|
|
68
|
+
activeFg: "var(--sg-btn-danger-active-fg, var(--sg-btn-danger-fg, hsl(var(--destructive-foreground))))",
|
|
55
69
|
ring: "var(--sg-btn-danger-ring, hsl(var(--destructive)/0.35))"
|
|
56
70
|
},
|
|
57
71
|
};
|
|
@@ -67,9 +81,11 @@ export function resolveButtonColors(severity, custom) {
|
|
|
67
81
|
fg: custom?.fg ?? base.fg,
|
|
68
82
|
border: custom?.border ?? base.border,
|
|
69
83
|
hoverBg: custom?.hoverBg ?? base.hoverBg,
|
|
70
|
-
|
|
84
|
+
// Sem cor customizada, cada estado usa o texto escolhido por contraste sobre o proprio fundo (tokens *-hover-fg/*-active-fg).
|
|
85
|
+
hoverFg: custom?.hoverFg ?? custom?.fg ?? base.hoverFg,
|
|
71
86
|
hoverBorder: custom?.hoverBorder ?? (custom?.border ?? base.border),
|
|
72
87
|
activeBg: custom?.activeBg ?? (custom?.hoverBg ?? base.hoverBg),
|
|
88
|
+
activeFg: custom?.hoverFg ?? custom?.fg ?? base.activeFg,
|
|
73
89
|
ring: custom?.ring ?? base.ring
|
|
74
90
|
};
|
|
75
91
|
}
|
|
@@ -84,6 +100,7 @@ function buildVars(severity, custom) {
|
|
|
84
100
|
["--sg-btn-hover-fg"]: merged.hoverFg,
|
|
85
101
|
["--sg-btn-hover-border"]: merged.hoverBorder,
|
|
86
102
|
["--sg-btn-active-bg"]: merged.activeBg,
|
|
103
|
+
["--sg-btn-active-fg"]: merged.activeFg,
|
|
87
104
|
["--sg-btn-ring"]: merged.ring,
|
|
88
105
|
["--sg-btn-tint"]: `var(--sg-${toneKey}-600)`
|
|
89
106
|
};
|
|
@@ -91,7 +108,7 @@ function buildVars(severity, custom) {
|
|
|
91
108
|
function appearanceClass(appearance) {
|
|
92
109
|
switch (appearance) {
|
|
93
110
|
case "solid":
|
|
94
|
-
return cn("bg-[var(--sg-btn-bg)] text-[var(--sg-btn-fg)] border border-[var(--sg-btn-border)]", "hover:bg-[var(--sg-btn-hover-bg)] hover:text-[var(--sg-btn-hover-fg)] hover:border-[var(--sg-btn-hover-border)]", "active:bg-[var(--sg-btn-active-bg)]");
|
|
111
|
+
return cn("bg-[var(--sg-btn-bg)] text-[var(--sg-btn-fg)] border border-[var(--sg-btn-border)]", "hover:bg-[var(--sg-btn-hover-bg)] hover:text-[var(--sg-btn-hover-fg)] hover:border-[var(--sg-btn-hover-border)]", "active:bg-[var(--sg-btn-active-bg)] active:text-[var(--sg-btn-active-fg)]");
|
|
95
112
|
case "outline":
|
|
96
113
|
return cn("bg-transparent text-[var(--sg-btn-bg)] border border-[var(--sg-btn-border)]", "hover:bg-[rgb(var(--sg-btn-tint)/0.12)]", "active:bg-[rgb(var(--sg-btn-tint)/0.18)]");
|
|
97
114
|
case "ghost":
|
|
@@ -108,16 +125,45 @@ function elevationClass(elevation) {
|
|
|
108
125
|
return "";
|
|
109
126
|
}
|
|
110
127
|
}
|
|
111
|
-
export const SgButton = React.forwardRef(({ severity = "primary", appearance = "solid", size, shape, elevation = "none", label, iconOnly, leftIcon, rightIcon, loading = false, enabled = true, className, children, style, customColors, type = "button", ...rest }, ref) => {
|
|
128
|
+
export const SgButton = React.forwardRef(({ severity = "primary", appearance = "solid", size, shape, elevation = "none", label, iconOnly, leftIcon, rightIcon, loading = false, enabled = true, className, children, style, customColors, type = "button", onClick, ...rest }, ref) => {
|
|
129
|
+
const [pending, setPending] = React.useState(false);
|
|
130
|
+
const pendingRef = React.useRef(false);
|
|
131
|
+
const mountedRef = React.useRef(true);
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
mountedRef.current = true;
|
|
134
|
+
return () => {
|
|
135
|
+
mountedRef.current = false;
|
|
136
|
+
};
|
|
137
|
+
}, []);
|
|
138
|
+
const isLoading = loading || pending;
|
|
139
|
+
const handleClick = React.useCallback((event) => {
|
|
140
|
+
if (!onClick || !enabled || loading || pendingRef.current)
|
|
141
|
+
return;
|
|
142
|
+
const result = onClick(event);
|
|
143
|
+
if (!result || typeof result.then !== "function")
|
|
144
|
+
return;
|
|
145
|
+
pendingRef.current = true;
|
|
146
|
+
setPending(true);
|
|
147
|
+
const done = () => {
|
|
148
|
+
pendingRef.current = false;
|
|
149
|
+
if (mountedRef.current)
|
|
150
|
+
setPending(false);
|
|
151
|
+
};
|
|
152
|
+
// O erro da Promise nao e engolido: segue como rejeicao sem tratamento, como antes.
|
|
153
|
+
void result.then(done, (error) => {
|
|
154
|
+
done();
|
|
155
|
+
throw error;
|
|
156
|
+
});
|
|
157
|
+
}, [onClick, enabled, loading]);
|
|
112
158
|
const resolvedChildren = children ?? label;
|
|
113
|
-
const isDisabled = Boolean(!enabled ||
|
|
159
|
+
const isDisabled = Boolean(!enabled || isLoading);
|
|
114
160
|
const inferredIconOnly = !resolvedChildren && (leftIcon || rightIcon);
|
|
115
161
|
const isIconOnly = iconOnly ?? inferredIconOnly;
|
|
116
162
|
const resolvedSize = size ?? (isIconOnly ? "sm" : "md");
|
|
117
163
|
const resolvedShape = shape ?? (isIconOnly ? "circle" : "default");
|
|
118
164
|
const s = SIZE[resolvedSize];
|
|
119
165
|
const contentClass = cn("inline-flex min-w-0 items-center justify-center", !isIconOnly ? "w-full" : undefined, s.gap);
|
|
120
|
-
return (_jsx("button", { ref: ref, type: type, disabled: isDisabled, style: { ...buildVars(severity, customColors), ...style }, className: cn("inline-flex min-w-0 max-w-full items-center justify-center select-none whitespace-normal break-words text-center", "font-medium", "transition-[background-color,color,border-color,box-shadow,transform] duration-150", "focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--sg-btn-ring)]", "disabled:opacity-60 disabled:cursor-not-allowed", isIconOnly ? cn("aspect-square", s.h) : cn(s.minH, s.py, s.px), s.text, resolvedShape === "rounded" ? "rounded-full" : resolvedShape === "circle" ? "rounded-full" : resolvedShape === "square" ? "rounded-none" : s.radius, appearanceClass(appearance), elevationClass(elevation), !isDisabled ? "active:translate-y-[0.5px]" : false, className), ...rest, children:
|
|
166
|
+
return (_jsx("button", { ref: ref, type: type, disabled: isDisabled, "aria-busy": isLoading ? true : undefined, style: { ...buildVars(severity, customColors), ...style }, className: cn("inline-flex min-w-0 max-w-full items-center justify-center select-none whitespace-normal break-words text-center", "font-medium", "transition-[background-color,color,border-color,box-shadow,transform] duration-150", "focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-[var(--sg-btn-ring)]", "disabled:opacity-60 disabled:cursor-not-allowed", isIconOnly ? cn("aspect-square", s.h) : cn(s.minH, s.py, s.px), s.text, resolvedShape === "rounded" ? "rounded-full" : resolvedShape === "circle" ? "rounded-full" : resolvedShape === "square" ? "rounded-none" : s.radius, appearanceClass(appearance), elevationClass(elevation), !isDisabled ? "active:translate-y-[0.5px]" : false, className), ...rest, onClick: onClick ? handleClick : undefined, children: isLoading ? (_jsxs("span", { className: contentClass, children: [_jsx(Spinner, { className: s.icon }), resolvedChildren ? _jsx("span", { className: "min-w-0 whitespace-normal break-words", children: resolvedChildren }) : null] })) : (_jsxs("span", { className: contentClass, children: [leftIcon ? _jsx("span", { className: cn("shrink-0", s.icon), children: leftIcon }) : null, resolvedChildren ? _jsx("span", { className: "min-w-0 whitespace-normal break-words", children: resolvedChildren }) : null, rightIcon ? _jsx("span", { className: cn("shrink-0", s.icon), children: rightIcon }) : null] })) }));
|
|
121
167
|
});
|
|
122
168
|
SgButton.displayName = "SgButton";
|
|
123
169
|
function Spinner({ className }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgButton.meta.d.ts","sourceRoot":"","sources":["../../src/buttons/SgButton.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"SgButton.meta.d.ts","sourceRoot":"","sources":["../../src/buttons/SgButton.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,QAoIpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WA6BrB,CAAC"}
|
|
@@ -47,7 +47,7 @@ export const sgMeta = {
|
|
|
47
47
|
name: "loading",
|
|
48
48
|
type: "boolean",
|
|
49
49
|
default: false,
|
|
50
|
-
description: "Exibe spinner e bloqueia
|
|
50
|
+
description: "Exibe spinner e bloqueia o clique. Continua valendo e vence o loading automático do onClick: o loading efetivo é loading ou operação pendente (Promise/Future devolvida pelo onClick).",
|
|
51
51
|
semanticRole: "behavior",
|
|
52
52
|
bindable: true
|
|
53
53
|
},
|
|
@@ -75,8 +75,8 @@ export const sgMeta = {
|
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
name: "onClick",
|
|
78
|
-
type: "
|
|
79
|
-
description: "
|
|
78
|
+
type: "(event: MouseEvent<HTMLButtonElement>) => unknown",
|
|
79
|
+
description: "Clique do botão; pode ser síncrono ou assíncrono. Síncrono (abrir modal, alternar): nada muda. Se DEVOLVER uma Promise/Future, como async () => { await salvar(); } ou () => salvar(), o botão entra em loading sozinho (spinner, desabilitado, aria-busy na web), ignora cliques até terminar (inclusive cliques seguidos antes de a tela atualizar) e volta ao normal ao terminar ou falhar. Protege contra duplo clique e reenvio e ajuda na idempotência da ação na tela (não substitui a chave de idempotência no servidor). Cuidado: a função precisa devolver a operação; com chaves e sem return (ou sem async), como () => { salvar(); } ou () { salvar(); }, não protege; já async () {...} sempre devolve. O erro não é engolido: o botão sai do loading e relança como rejeição não tratada; trate com try/catch dentro do onClick e avise o usuário (sgWhistle.error). Use async só quando a ação puder demorar ou falhar (salvar, enviar, pagar).",
|
|
80
80
|
semanticRole: "event",
|
|
81
81
|
bindable: false
|
|
82
82
|
}
|
|
@@ -89,6 +89,24 @@ export const sgMeta = {
|
|
|
89
89
|
file: "apps/showcase/src/app/components/sg-button/samples/basico.tsx.sample",
|
|
90
90
|
kind: "sample"
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
id: "auto-loading",
|
|
94
|
+
title: "Loading automático com Future",
|
|
95
|
+
file: "apps/showcase/src/app/components/sg-button/samples/loading-automatico-com-future.tsx.sample",
|
|
96
|
+
kind: "sample"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "sync-vs-async",
|
|
100
|
+
title: "onClick síncrono x assíncrono",
|
|
101
|
+
file: "apps/showcase/src/app/components/sg-button/samples/onclick-sincrono-x-assincrono.tsx.sample",
|
|
102
|
+
kind: "sample"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "async-error",
|
|
106
|
+
title: "Tratando erro no onClick assíncrono",
|
|
107
|
+
file: "apps/showcase/src/app/components/sg-button/samples/tratando-erro-no-onclick-assincrono.tsx.sample",
|
|
108
|
+
kind: "sample"
|
|
109
|
+
},
|
|
92
110
|
{
|
|
93
111
|
id: "playground",
|
|
94
112
|
title: "Playground",
|
|
@@ -116,7 +134,8 @@ export const aiHints = {
|
|
|
116
134
|
preferredUseCases: [
|
|
117
135
|
"Acoes primarias e secundarias em formularios e telas CRUD.",
|
|
118
136
|
"Submit, confirmacao, salvar, cancelar e outras acoes explicitas do usuario.",
|
|
119
|
-
"Chamadas para acao com feedback visual de loading."
|
|
137
|
+
"Chamadas para acao com feedback visual de loading.",
|
|
138
|
+
"Salvar, enviar ou pagar: use onClick assíncrono (async () => { await salvar(); } ou () => salvar()) para o botão entrar em loading sozinho e ignorar duplo clique; acoes instantaneas (abrir modal, alternar) ficam sincronas."
|
|
120
139
|
],
|
|
121
140
|
avoidUseCases: [
|
|
122
141
|
"Agrupar conteudo ou estruturar layout; nesses casos prefira SgPanel, SgCard ou SgStack.",
|
|
@@ -127,7 +146,10 @@ export const aiHints = {
|
|
|
127
146
|
relatedEntityFields: ["submit", "save", "cancel", "confirm", "delete"],
|
|
128
147
|
compositionHints: [
|
|
129
148
|
"Combinar com SgInputText e outros campos em formularios.",
|
|
130
|
-
"Usar dentro de SgPanel ou SgDialog para acoes contextuais."
|
|
149
|
+
"Usar dentro de SgPanel ou SgDialog para acoes contextuais.",
|
|
150
|
+
"O onClick precisa DEVOLVER a Promise/Future para o loading automatico funcionar: onClick={() => { salvar(); }} (chaves sem return) nao protege; use () => salvar() ou async () => { await salvar(); }. No Flutter, async () {...} sempre devolve Future; () { salvar(); } nao.",
|
|
151
|
+
"O botao nao engole o erro do onClick assincrono: sai do loading e relanca como rejeicao nao tratada. Trate com try/catch dentro do onClick e avise o usuario com sgWhistle.error (Flutter: sgWhistle.error(SgWhistleOptions(...))).",
|
|
152
|
+
"O loading automatico protege a acao na tela (duplo clique/reenvio), mas nao substitui a chave de idempotencia no servidor. A prop loading manual vence: loading efetivo = loading ou operacao pendente."
|
|
131
153
|
],
|
|
132
154
|
rankingSignals: {
|
|
133
155
|
freeText: 0,
|
|
@@ -18,6 +18,12 @@ export type SgCalendarProps = Omit<React.HTMLAttributes<HTMLDivElement>, "childr
|
|
|
18
18
|
minDate?: Date | string;
|
|
19
19
|
maxDate?: Date | string;
|
|
20
20
|
isDateDisabled?: (date: Date) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Habilita o calendario (padrao `true`). Com `false` os dias, os botoes de mes anterior/proximo
|
|
23
|
+
* e o atalho "hoje" ficam desabilitados (nao focaveis, sem clique); a data e o mes atuais seguem
|
|
24
|
+
* legiveis.
|
|
25
|
+
*/
|
|
26
|
+
enabled?: boolean;
|
|
21
27
|
showTodayShortcut?: boolean;
|
|
22
28
|
cardTitle?: React.ReactNode;
|
|
23
29
|
cardProps?: Omit<SgCardProps, "children" | "title">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgCalendar.d.ts","sourceRoot":"","sources":["../../../src/gadgets/calendar/SgCalendar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAuG/D,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC,GAAG;IAClG,KAAK,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC;IACzC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC;CACrD,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"SgCalendar.d.ts","sourceRoot":"","sources":["../../../src/gadgets/calendar/SgCalendar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAuG/D,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC,GAAG;IAClG,KAAK,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC;IACzC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,SAAS,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC;CACrD,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,2CAwU1D;yBAxUe,UAAU"}
|
|
@@ -92,7 +92,7 @@ function capitalizeFirst(value, locale) {
|
|
|
92
92
|
}
|
|
93
93
|
export function SgCalendar(props) {
|
|
94
94
|
const i18n = useComponentsI18n();
|
|
95
|
-
const { value, defaultValue, onValueChange, viewDate, defaultViewDate, onViewDateChange, locale = "pt-BR", weekStartsOn = 0, weekdayFormat = "narrow", numberOfMonths = 1, monthsPerLine = 3, monthMinWidth, showAdjacentMonths = true, minDate, maxDate, isDateDisabled, showTodayShortcut = true, className, style, cardTitle, cardProps, ...rest } = props;
|
|
95
|
+
const { value, defaultValue, onValueChange, viewDate, defaultViewDate, onViewDateChange, locale = "pt-BR", weekStartsOn = 0, weekdayFormat = "narrow", numberOfMonths = 1, monthsPerLine = 3, monthMinWidth, showAdjacentMonths = true, minDate, maxDate, isDateDisabled, enabled = true, showTodayShortcut = true, className, style, cardTitle, cardProps, ...rest } = props;
|
|
96
96
|
const resolvedCardTitle = cardTitle ?? t(i18n, "components.gadgets.calendar.title");
|
|
97
97
|
const today = React.useMemo(() => normalizeDate(new Date()), []);
|
|
98
98
|
const valueControlled = value !== undefined;
|
|
@@ -171,6 +171,8 @@ export function SgCalendar(props) {
|
|
|
171
171
|
return false;
|
|
172
172
|
}, [isDateDisabled, parsedMaxDate, parsedMinDate]);
|
|
173
173
|
const handleSelectDate = React.useCallback((date) => {
|
|
174
|
+
if (!enabled)
|
|
175
|
+
return;
|
|
174
176
|
if (isCellDisabled(date))
|
|
175
177
|
return;
|
|
176
178
|
if (!valueControlled) {
|
|
@@ -180,8 +182,10 @@ export function SgCalendar(props) {
|
|
|
180
182
|
if (!isSameMonth(date, monthViewDate)) {
|
|
181
183
|
setMonthViewDate(date);
|
|
182
184
|
}
|
|
183
|
-
}, [isCellDisabled, monthViewDate, onValueChange, setMonthViewDate, valueControlled]);
|
|
185
|
+
}, [enabled, isCellDisabled, monthViewDate, onValueChange, setMonthViewDate, valueControlled]);
|
|
184
186
|
const handleGoToday = React.useCallback(() => {
|
|
187
|
+
if (!enabled)
|
|
188
|
+
return;
|
|
185
189
|
const now = normalizeDate(new Date());
|
|
186
190
|
if (!isCellDisabled(now)) {
|
|
187
191
|
if (!valueControlled) {
|
|
@@ -190,7 +194,7 @@ export function SgCalendar(props) {
|
|
|
190
194
|
onValueChange?.(new Date(now));
|
|
191
195
|
}
|
|
192
196
|
setMonthViewDate(now);
|
|
193
|
-
}, [isCellDisabled, onValueChange, setMonthViewDate, valueControlled]);
|
|
197
|
+
}, [enabled, isCellDisabled, onValueChange, setMonthViewDate, valueControlled]);
|
|
194
198
|
const monthDates = React.useMemo(() => Array.from({ length: safeNumberOfMonths }, (_, index) => addMonths(monthViewDate, index)), [monthViewDate, safeNumberOfMonths]);
|
|
195
199
|
const selectedLabel = capitalizeFirst(formatSelectedDate.format(selectedDate), locale);
|
|
196
200
|
const visibleColumns = Math.min(safeMonthsPerLine, safeNumberOfMonths);
|
|
@@ -206,7 +210,7 @@ export function SgCalendar(props) {
|
|
|
206
210
|
maxWidth: "100%",
|
|
207
211
|
...(cardProps?.style ?? {})
|
|
208
212
|
};
|
|
209
|
-
return (_jsx(SgCard, { title: resolvedCardTitle, collapsible: true, defaultOpen: true, collapseToggleAlign: "right", collapseIconSize: 20, draggable: true, bgColor: "rgb(var(--sg-surface,var(--sg-bg)))", bgColorTitle: "rgb(var(--sg-surface-2,var(--sg-surface,var(--sg-bg))))", cardStyle: "outlined", ...cardProps, className: mergedCardClassName, bodyClassName: mergedCardBodyClassName, style: mergedCardStyle, children: _jsxs("div", { className: cn("inline-flex max-w-full select-none flex-col gap-3", className), style: style, ...rest, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border/70 bg-background px-4 py-3", children: [_jsx("div", { className: "min-h-6 text-sm font-medium text-foreground", children: selectedLabel }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => setMonthViewDate(addMonths(monthViewDate, -1)), className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "aria-label": uiLabels.previousMonth, title: uiLabels.previousMonth, children: _jsx("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", "aria-hidden": "true", children: _jsx("path", { d: "m12.5 5.8-4.2 4.2 4.2 4.2", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("button", { type: "button", onClick: () => setMonthViewDate(addMonths(monthViewDate, 1)), className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "aria-label": uiLabels.nextMonth, title: uiLabels.nextMonth, children: _jsx("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", "aria-hidden": "true", children: _jsx("path", { d: "m7.5 5.8 4.2 4.2-4.2 4.2", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), showTodayShortcut ? (_jsx("button", { type: "button", onClick: handleGoToday, className: "inline-flex h-8 w-8 items-center justify-center rounded-md border border-border/70 bg-muted/40 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "aria-label": uiLabels.goToday, title: uiLabels.goToday, children: _jsx("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", "aria-hidden": "true", children: _jsx("path", { d: "M5.6 7.6 10 12l4.4-4.4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) })) : null] })] }), _jsx("div", { className: "max-h-full max-w-full overflow-auto", children: _jsx("div", { className: "grid gap-4", style: monthGridStyle, children: monthDates.map((monthDate) => {
|
|
213
|
+
return (_jsx(SgCard, { title: resolvedCardTitle, collapsible: true, defaultOpen: true, collapseToggleAlign: "right", collapseIconSize: 20, draggable: true, bgColor: "rgb(var(--sg-surface,var(--sg-bg)))", bgColorTitle: "rgb(var(--sg-surface-2,var(--sg-surface,var(--sg-bg))))", cardStyle: "outlined", ...cardProps, className: mergedCardClassName, bodyClassName: mergedCardBodyClassName, style: mergedCardStyle, children: _jsxs("div", { className: cn("inline-flex max-w-full select-none flex-col gap-3", !enabled && "opacity-60", className), style: style, "aria-disabled": enabled ? undefined : true, ...rest, children: [_jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border border-border/70 bg-background px-4 py-3", children: [_jsx("div", { className: "min-h-6 text-sm font-medium text-foreground", children: selectedLabel }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => enabled && setMonthViewDate(addMonths(monthViewDate, -1)), disabled: !enabled, className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:hover:bg-transparent", "aria-label": uiLabels.previousMonth, title: uiLabels.previousMonth, children: _jsx("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", "aria-hidden": "true", children: _jsx("path", { d: "m12.5 5.8-4.2 4.2 4.2 4.2", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("button", { type: "button", onClick: () => enabled && setMonthViewDate(addMonths(monthViewDate, 1)), disabled: !enabled, className: "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "aria-label": uiLabels.nextMonth, title: uiLabels.nextMonth, children: _jsx("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", "aria-hidden": "true", children: _jsx("path", { d: "m7.5 5.8 4.2 4.2-4.2 4.2", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), showTodayShortcut ? (_jsx("button", { type: "button", onClick: handleGoToday, disabled: !enabled, className: "inline-flex h-8 w-8 items-center justify-center rounded-md border border-border/70 bg-muted/40 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", "aria-label": uiLabels.goToday, title: uiLabels.goToday, children: _jsx("svg", { viewBox: "0 0 20 20", className: "h-4 w-4", "aria-hidden": "true", children: _jsx("path", { d: "M5.6 7.6 10 12l4.4-4.4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) })) : null] })] }), _jsx("div", { className: "max-h-full max-w-full overflow-auto", children: _jsx("div", { className: "grid gap-4", style: monthGridStyle, children: monthDates.map((monthDate) => {
|
|
210
214
|
const firstDayOfMonth = startOfMonth(monthDate);
|
|
211
215
|
const firstDayOffset = (firstDayOfMonth.getDay() - safeWeekStartsOn + 7) % 7;
|
|
212
216
|
const gridStart = addDays(firstDayOfMonth, -firstDayOffset);
|
|
@@ -215,7 +219,7 @@ export function SgCalendar(props) {
|
|
|
215
219
|
return (_jsxs("div", { className: "rounded-xl border border-border/70 bg-background", children: [_jsx("div", { className: "border-b border-border/70 px-4 py-3", children: _jsx("div", { className: "text-[1.1rem] font-semibold text-foreground", children: monthLabel }) }), _jsxs("div", { className: "px-4 pb-4 pt-3", children: [_jsx("div", { className: "mb-2 grid grid-cols-7 text-center text-[0.79rem] font-medium uppercase tracking-wide text-muted-foreground", children: weekdayLabels.map((label, index) => (_jsx("span", { className: "py-1", children: label }, `${label}-${index}`))) }), _jsx("div", { className: "grid grid-cols-7 gap-y-1", children: days.map((day) => {
|
|
216
220
|
const inCurrentMonth = isSameMonth(day, monthDate);
|
|
217
221
|
const hiddenAdjacentDay = !showAdjacentMonths && !inCurrentMonth;
|
|
218
|
-
const disabled = isCellDisabled(day);
|
|
222
|
+
const disabled = !enabled || isCellDisabled(day);
|
|
219
223
|
const selected = isSameDay(day, selectedDate) && inCurrentMonth;
|
|
220
224
|
const isToday = isSameDay(day, today) && inCurrentMonth;
|
|
221
225
|
if (hiddenAdjacentDay) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SgCalendar.meta.d.ts","sourceRoot":"","sources":["../../../src/gadgets/calendar/SgCalendar.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEjE,eAAO,MAAM,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"SgCalendar.meta.d.ts","sourceRoot":"","sources":["../../../src/gadgets/calendar/SgCalendar.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEjE,eAAO,MAAM,MAAM,EAAE,QAgCpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WAmBrB,CAAC"}
|
|
@@ -19,6 +19,7 @@ export const sgMeta = {
|
|
|
19
19
|
{ name: "numberOfMonths", type: "number", default: 1, description: "Quantidade de meses visiveis ao mesmo tempo.", semanticRole: "appearance", bindable: true },
|
|
20
20
|
{ name: "minDate", type: "Date | string", description: "Limite minimo de selecao.", semanticRole: "validation", bindable: true },
|
|
21
21
|
{ name: "maxDate", type: "Date | string", description: "Limite maximo de selecao.", semanticRole: "validation", bindable: true },
|
|
22
|
+
{ name: "enabled", type: "boolean", default: true, description: "Quando false, os dias, os botoes de mes anterior/proximo e o atalho hoje ficam desabilitados (nao focaveis); a data e o mes atuais continuam legiveis.", semanticRole: "behavior", bindable: true },
|
|
22
23
|
{ name: "onValueChange", type: "(date: Date) => void", description: "Callback disparado ao selecionar uma data.", semanticRole: "event", bindable: false }
|
|
23
24
|
],
|
|
24
25
|
states: ["default", "multi-month", "disabled-dates", "controlled"],
|