forlogic-core 1.8.11 → 1.8.12
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/README.md +266 -0
- package/dist/README.md +266 -0
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -652,6 +652,272 @@ import { TrainingMultiSelect } from './components/TrainingMultiSelect';
|
|
|
652
652
|
}
|
|
653
653
|
```
|
|
654
654
|
|
|
655
|
+
#### ⚠️ MultiSelect dentro de Dialog
|
|
656
|
+
|
|
657
|
+
**Problema Comum:**
|
|
658
|
+
|
|
659
|
+
Quando o `MultiSelect` é usado dentro de um `Dialog`, o popover pode ser cortado ou ter problemas de scroll.
|
|
660
|
+
|
|
661
|
+
**Solução:**
|
|
662
|
+
|
|
663
|
+
Use a prop `popoverContainer` para portalizar o popover para dentro do próprio `DialogContent`:
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
import { useState, useRef, useCallback } from 'react';
|
|
667
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger, Button, MultiSelect } from 'forlogic-core';
|
|
668
|
+
|
|
669
|
+
function MyComponent() {
|
|
670
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
671
|
+
const [open, setOpen] = useState(false);
|
|
672
|
+
const dialogContentRef = useRef<HTMLDivElement>(null);
|
|
673
|
+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
674
|
+
|
|
675
|
+
// useCallback para capturar a ref do DialogContent
|
|
676
|
+
const setDialogContentRef = useCallback((node: HTMLDivElement | null) => {
|
|
677
|
+
dialogContentRef.current = node;
|
|
678
|
+
setPortalContainer(node);
|
|
679
|
+
}, []);
|
|
680
|
+
|
|
681
|
+
const options = [
|
|
682
|
+
{ id: '1', name: 'React' },
|
|
683
|
+
{ id: '2', name: 'Vue' },
|
|
684
|
+
{ id: '3', name: 'Angular' },
|
|
685
|
+
// ... mais opções
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
690
|
+
<DialogTrigger asChild>
|
|
691
|
+
<Button>Abrir MultiSelect em Dialog</Button>
|
|
692
|
+
</DialogTrigger>
|
|
693
|
+
<DialogContent ref={setDialogContentRef} className="max-h-[90vh] overflow-visible">
|
|
694
|
+
<DialogHeader>
|
|
695
|
+
<DialogTitle>Selecione Frameworks</DialogTitle>
|
|
696
|
+
<DialogDescription>
|
|
697
|
+
O Popover é portalizado para o próprio Dialog.Content e o conteúdo rola em um wrapper interno,
|
|
698
|
+
evitando corte e permitindo scroll desde a primeira abertura.
|
|
699
|
+
</DialogDescription>
|
|
700
|
+
</DialogHeader>
|
|
701
|
+
{/* Wrapper interno com scroll - permite que DialogContent tenha overflow-visible */}
|
|
702
|
+
<div className="mt-4 max-h-[60vh] overflow-auto pr-1">
|
|
703
|
+
<MultiSelect
|
|
704
|
+
options={options}
|
|
705
|
+
value={selected}
|
|
706
|
+
onChange={setSelected}
|
|
707
|
+
getOptionValue={(opt) => opt.id}
|
|
708
|
+
getOptionLabel={(opt) => opt.name}
|
|
709
|
+
placeholder="Selecione frameworks..."
|
|
710
|
+
label="Frameworks"
|
|
711
|
+
popoverContainer={portalContainer}
|
|
712
|
+
disabled={!portalContainer}
|
|
713
|
+
/>
|
|
714
|
+
</div>
|
|
715
|
+
</DialogContent>
|
|
716
|
+
</Dialog>
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**Pontos-chave:**
|
|
722
|
+
|
|
723
|
+
1. **useCallback + useState**: Captura a ref do `DialogContent` e guarda no state `portalContainer`
|
|
724
|
+
2. **overflow-visible**: Permite que o popover seja visível fora do dialog
|
|
725
|
+
3. **Wrapper interno com scroll**: Um `div` interno com `overflow-auto` para scroll do conteúdo
|
|
726
|
+
4. **popoverContainer**: Passa o container para o `MultiSelect` para portalizar o popover
|
|
727
|
+
5. **disabled={!portalContainer}**: Desabilita até o container estar disponível (evita erros na primeira renderização)
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
#### 🔧 MultiSelect em BaseForm/createCrudPage
|
|
732
|
+
|
|
733
|
+
**Situação:**
|
|
734
|
+
|
|
735
|
+
Quando você usa `createCrudPage` ou `BaseForm` diretamente, o Dialog é criado automaticamente e você não tem acesso direto à ref do `DialogContent`.
|
|
736
|
+
|
|
737
|
+
**Solução Automática:**
|
|
738
|
+
|
|
739
|
+
O `BaseForm` agora passa **automaticamente** a prop `popoverContainer` para **todos** os componentes custom. Você não precisa fazer nada especial!
|
|
740
|
+
|
|
741
|
+
**Como usar:**
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
// 1. Criar componente custom que aceita popoverContainer
|
|
745
|
+
interface TrainingMultiSelectProps {
|
|
746
|
+
value: string[];
|
|
747
|
+
onChange: (value: string[]) => void;
|
|
748
|
+
disabled?: boolean;
|
|
749
|
+
error?: string;
|
|
750
|
+
popoverContainer?: HTMLElement | null; // ✅ Recebe automaticamente do BaseForm
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function TrainingMultiSelect({
|
|
754
|
+
value,
|
|
755
|
+
onChange,
|
|
756
|
+
disabled,
|
|
757
|
+
error,
|
|
758
|
+
popoverContainer // ✅ Prop passada automaticamente pelo BaseForm
|
|
759
|
+
}: TrainingMultiSelectProps) {
|
|
760
|
+
const { data: trainings = [], isLoading } = useQuery({
|
|
761
|
+
queryKey: ['trainings'],
|
|
762
|
+
queryFn: async () => {
|
|
763
|
+
const { data } = await supabase.schema('common').from('trainings').select('*');
|
|
764
|
+
return data || [];
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
return (
|
|
769
|
+
<MultiSelect
|
|
770
|
+
data={trainings}
|
|
771
|
+
value={value}
|
|
772
|
+
onChange={onChange}
|
|
773
|
+
getId={(item) => item.id}
|
|
774
|
+
getLabel={(item) => item.title}
|
|
775
|
+
placeholder="Selecione treinamentos"
|
|
776
|
+
searchPlaceholder="Buscar..."
|
|
777
|
+
emptyMessage="Nenhum treinamento encontrado"
|
|
778
|
+
disabled={disabled}
|
|
779
|
+
error={error}
|
|
780
|
+
isLoading={isLoading}
|
|
781
|
+
popoverContainer={popoverContainer} // ✅ Repassa para o MultiSelect
|
|
782
|
+
/>
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// 2. Usar no campo do formulário - NENHUMA CONFIGURAÇÃO EXTRA!
|
|
787
|
+
const formFields: FormSection[] = [
|
|
788
|
+
{
|
|
789
|
+
id: 'basic',
|
|
790
|
+
title: 'Informações',
|
|
791
|
+
fields: [
|
|
792
|
+
{
|
|
793
|
+
name: 'training_ids',
|
|
794
|
+
label: 'Treinamentos',
|
|
795
|
+
type: 'custom' as const,
|
|
796
|
+
component: TrainingMultiSelect, // ✅ Receberá popoverContainer automaticamente
|
|
797
|
+
required: true
|
|
798
|
+
}
|
|
799
|
+
]
|
|
800
|
+
}
|
|
801
|
+
];
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Importante:**
|
|
805
|
+
|
|
806
|
+
- O `BaseForm` passa `popoverContainer` para **todos** os componentes custom
|
|
807
|
+
- Se seu componente não precisa, pode simplesmente ignorar a prop
|
|
808
|
+
- Componentes que usam `MultiSelect`, `Select`, `Popover` ou outros overlays devem repassar essa prop
|
|
809
|
+
- **Não** é necessário configurar nada no `componentProps` - é automático!
|
|
810
|
+
|
|
811
|
+
**Exemplo com componentProps (se precisar passar outras props):**
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
{
|
|
815
|
+
name: 'training_ids',
|
|
816
|
+
label: 'Treinamentos',
|
|
817
|
+
type: 'custom' as const,
|
|
818
|
+
component: TrainingMultiSelect,
|
|
819
|
+
componentProps: {
|
|
820
|
+
maxDisplayedBadges: 3, // Props customizadas
|
|
821
|
+
enableSearch: true
|
|
822
|
+
// popoverContainer é passado automaticamente, não precisa incluir aqui
|
|
823
|
+
},
|
|
824
|
+
required: true
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
**Checklist:**
|
|
829
|
+
|
|
830
|
+
- ✅ Componente custom aceita `popoverContainer?: HTMLElement | null`
|
|
831
|
+
- ✅ `popoverContainer` é repassado para `MultiSelect`, `Popover`, etc.
|
|
832
|
+
- ✅ Usar `type: 'custom'` no field config
|
|
833
|
+
- ✅ Não incluir `popoverContainer` em `componentProps` (é automático)
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
#### 🔧 AppSidebar Redimensionável
|
|
838
|
+
|
|
839
|
+
**Comportamento Padrão:**
|
|
840
|
+
|
|
841
|
+
Por padrão, a `AppSidebar` tem largura fixa e **não** é redimensionável:
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
import { AppSidebar } from 'forlogic-core';
|
|
845
|
+
|
|
846
|
+
// Sidebar padrão (largura fixa, apenas collapse/expand com pin)
|
|
847
|
+
<AppSidebar config={sidebarConfig} />
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Habilitando Redimensionamento:**
|
|
851
|
+
|
|
852
|
+
Para habilitar o redimensionamento via drag na borda direita, use a prop `resizable={true}`:
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
import { AppSidebar } from 'forlogic-core';
|
|
856
|
+
|
|
857
|
+
// Sidebar redimensionável com limites customizados
|
|
858
|
+
<AppSidebar
|
|
859
|
+
config={sidebarConfig}
|
|
860
|
+
resizable={true}
|
|
861
|
+
minWidth={280} // Opcional (padrão: 224px/14rem)
|
|
862
|
+
maxWidth={500} // Opcional (padrão: 384px/24rem)
|
|
863
|
+
/>
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Exemplo com Custom Content:**
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
import { AppLayout, AppSidebar } from 'forlogic-core';
|
|
870
|
+
import { FolderTree } from './components/FolderTree';
|
|
871
|
+
|
|
872
|
+
function DocsPage() {
|
|
873
|
+
return (
|
|
874
|
+
<AppLayout
|
|
875
|
+
sidebar={
|
|
876
|
+
<AppSidebar
|
|
877
|
+
customContent={<FolderTree folders={folders} />}
|
|
878
|
+
resizable={true}
|
|
879
|
+
minWidth={280}
|
|
880
|
+
maxWidth={500}
|
|
881
|
+
/>
|
|
882
|
+
}
|
|
883
|
+
>
|
|
884
|
+
<div className="p-6">
|
|
885
|
+
{/* Conteúdo da página */}
|
|
886
|
+
</div>
|
|
887
|
+
</AppLayout>
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Props de Redimensionamento:**
|
|
893
|
+
|
|
894
|
+
| Prop | Tipo | Padrão | Descrição |
|
|
895
|
+
| ---------- | --------- | -------- | ------------------------------------------ |
|
|
896
|
+
| `resizable` | `boolean` | `false` | Habilita/desabilita redimensionamento |
|
|
897
|
+
| `minWidth` | `number` | `224` | Largura mínima em pixels (14rem) |
|
|
898
|
+
| `maxWidth` | `number` | `384` | Largura máxima em pixels (24rem) |
|
|
899
|
+
|
|
900
|
+
**Recursos:**
|
|
901
|
+
|
|
902
|
+
- ✅ Drag-to-resize com mouse na borda direita
|
|
903
|
+
- ✅ Persistência automática da largura (localStorage)
|
|
904
|
+
- ✅ Limites min/max configuráveis
|
|
905
|
+
- ✅ Feedback visual durante drag (hover e cursor)
|
|
906
|
+
- ✅ Funciona independente do pin/hover da sidebar
|
|
907
|
+
- ✅ Storage key: `app-sidebar-width`
|
|
908
|
+
|
|
909
|
+
**Quando Usar:**
|
|
910
|
+
|
|
911
|
+
- 📄 Páginas com conteúdo lateral extenso (documentação, navegação de arquivos)
|
|
912
|
+
- 📊 Dashboards com painéis laterais customizáveis
|
|
913
|
+
- 🗂️ Interfaces com árvores de navegação profundas
|
|
914
|
+
- ⚙️ Configurações onde o usuário prefere mais espaço para a sidebar
|
|
915
|
+
|
|
916
|
+
**Quando NÃO Usar:**
|
|
917
|
+
|
|
918
|
+
- ❌ Sidebars com navegação simples/curta (usa largura fixa padrão)
|
|
919
|
+
- ❌ Aplicações mobile-first (redimensionamento só funciona em desktop)
|
|
920
|
+
|
|
655
921
|
---
|
|
656
922
|
|
|
657
923
|
### 🎨 Campos Customizados no BaseForm
|
package/dist/README.md
CHANGED
|
@@ -652,6 +652,272 @@ import { TrainingMultiSelect } from './components/TrainingMultiSelect';
|
|
|
652
652
|
}
|
|
653
653
|
```
|
|
654
654
|
|
|
655
|
+
#### ⚠️ MultiSelect dentro de Dialog
|
|
656
|
+
|
|
657
|
+
**Problema Comum:**
|
|
658
|
+
|
|
659
|
+
Quando o `MultiSelect` é usado dentro de um `Dialog`, o popover pode ser cortado ou ter problemas de scroll.
|
|
660
|
+
|
|
661
|
+
**Solução:**
|
|
662
|
+
|
|
663
|
+
Use a prop `popoverContainer` para portalizar o popover para dentro do próprio `DialogContent`:
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
import { useState, useRef, useCallback } from 'react';
|
|
667
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger, Button, MultiSelect } from 'forlogic-core';
|
|
668
|
+
|
|
669
|
+
function MyComponent() {
|
|
670
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
671
|
+
const [open, setOpen] = useState(false);
|
|
672
|
+
const dialogContentRef = useRef<HTMLDivElement>(null);
|
|
673
|
+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
674
|
+
|
|
675
|
+
// useCallback para capturar a ref do DialogContent
|
|
676
|
+
const setDialogContentRef = useCallback((node: HTMLDivElement | null) => {
|
|
677
|
+
dialogContentRef.current = node;
|
|
678
|
+
setPortalContainer(node);
|
|
679
|
+
}, []);
|
|
680
|
+
|
|
681
|
+
const options = [
|
|
682
|
+
{ id: '1', name: 'React' },
|
|
683
|
+
{ id: '2', name: 'Vue' },
|
|
684
|
+
{ id: '3', name: 'Angular' },
|
|
685
|
+
// ... mais opções
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
690
|
+
<DialogTrigger asChild>
|
|
691
|
+
<Button>Abrir MultiSelect em Dialog</Button>
|
|
692
|
+
</DialogTrigger>
|
|
693
|
+
<DialogContent ref={setDialogContentRef} className="max-h-[90vh] overflow-visible">
|
|
694
|
+
<DialogHeader>
|
|
695
|
+
<DialogTitle>Selecione Frameworks</DialogTitle>
|
|
696
|
+
<DialogDescription>
|
|
697
|
+
O Popover é portalizado para o próprio Dialog.Content e o conteúdo rola em um wrapper interno,
|
|
698
|
+
evitando corte e permitindo scroll desde a primeira abertura.
|
|
699
|
+
</DialogDescription>
|
|
700
|
+
</DialogHeader>
|
|
701
|
+
{/* Wrapper interno com scroll - permite que DialogContent tenha overflow-visible */}
|
|
702
|
+
<div className="mt-4 max-h-[60vh] overflow-auto pr-1">
|
|
703
|
+
<MultiSelect
|
|
704
|
+
options={options}
|
|
705
|
+
value={selected}
|
|
706
|
+
onChange={setSelected}
|
|
707
|
+
getOptionValue={(opt) => opt.id}
|
|
708
|
+
getOptionLabel={(opt) => opt.name}
|
|
709
|
+
placeholder="Selecione frameworks..."
|
|
710
|
+
label="Frameworks"
|
|
711
|
+
popoverContainer={portalContainer}
|
|
712
|
+
disabled={!portalContainer}
|
|
713
|
+
/>
|
|
714
|
+
</div>
|
|
715
|
+
</DialogContent>
|
|
716
|
+
</Dialog>
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**Pontos-chave:**
|
|
722
|
+
|
|
723
|
+
1. **useCallback + useState**: Captura a ref do `DialogContent` e guarda no state `portalContainer`
|
|
724
|
+
2. **overflow-visible**: Permite que o popover seja visível fora do dialog
|
|
725
|
+
3. **Wrapper interno com scroll**: Um `div` interno com `overflow-auto` para scroll do conteúdo
|
|
726
|
+
4. **popoverContainer**: Passa o container para o `MultiSelect` para portalizar o popover
|
|
727
|
+
5. **disabled={!portalContainer}**: Desabilita até o container estar disponível (evita erros na primeira renderização)
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
#### 🔧 MultiSelect em BaseForm/createCrudPage
|
|
732
|
+
|
|
733
|
+
**Situação:**
|
|
734
|
+
|
|
735
|
+
Quando você usa `createCrudPage` ou `BaseForm` diretamente, o Dialog é criado automaticamente e você não tem acesso direto à ref do `DialogContent`.
|
|
736
|
+
|
|
737
|
+
**Solução Automática:**
|
|
738
|
+
|
|
739
|
+
O `BaseForm` agora passa **automaticamente** a prop `popoverContainer` para **todos** os componentes custom. Você não precisa fazer nada especial!
|
|
740
|
+
|
|
741
|
+
**Como usar:**
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
// 1. Criar componente custom que aceita popoverContainer
|
|
745
|
+
interface TrainingMultiSelectProps {
|
|
746
|
+
value: string[];
|
|
747
|
+
onChange: (value: string[]) => void;
|
|
748
|
+
disabled?: boolean;
|
|
749
|
+
error?: string;
|
|
750
|
+
popoverContainer?: HTMLElement | null; // ✅ Recebe automaticamente do BaseForm
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function TrainingMultiSelect({
|
|
754
|
+
value,
|
|
755
|
+
onChange,
|
|
756
|
+
disabled,
|
|
757
|
+
error,
|
|
758
|
+
popoverContainer // ✅ Prop passada automaticamente pelo BaseForm
|
|
759
|
+
}: TrainingMultiSelectProps) {
|
|
760
|
+
const { data: trainings = [], isLoading } = useQuery({
|
|
761
|
+
queryKey: ['trainings'],
|
|
762
|
+
queryFn: async () => {
|
|
763
|
+
const { data } = await supabase.schema('common').from('trainings').select('*');
|
|
764
|
+
return data || [];
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
return (
|
|
769
|
+
<MultiSelect
|
|
770
|
+
data={trainings}
|
|
771
|
+
value={value}
|
|
772
|
+
onChange={onChange}
|
|
773
|
+
getId={(item) => item.id}
|
|
774
|
+
getLabel={(item) => item.title}
|
|
775
|
+
placeholder="Selecione treinamentos"
|
|
776
|
+
searchPlaceholder="Buscar..."
|
|
777
|
+
emptyMessage="Nenhum treinamento encontrado"
|
|
778
|
+
disabled={disabled}
|
|
779
|
+
error={error}
|
|
780
|
+
isLoading={isLoading}
|
|
781
|
+
popoverContainer={popoverContainer} // ✅ Repassa para o MultiSelect
|
|
782
|
+
/>
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// 2. Usar no campo do formulário - NENHUMA CONFIGURAÇÃO EXTRA!
|
|
787
|
+
const formFields: FormSection[] = [
|
|
788
|
+
{
|
|
789
|
+
id: 'basic',
|
|
790
|
+
title: 'Informações',
|
|
791
|
+
fields: [
|
|
792
|
+
{
|
|
793
|
+
name: 'training_ids',
|
|
794
|
+
label: 'Treinamentos',
|
|
795
|
+
type: 'custom' as const,
|
|
796
|
+
component: TrainingMultiSelect, // ✅ Receberá popoverContainer automaticamente
|
|
797
|
+
required: true
|
|
798
|
+
}
|
|
799
|
+
]
|
|
800
|
+
}
|
|
801
|
+
];
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Importante:**
|
|
805
|
+
|
|
806
|
+
- O `BaseForm` passa `popoverContainer` para **todos** os componentes custom
|
|
807
|
+
- Se seu componente não precisa, pode simplesmente ignorar a prop
|
|
808
|
+
- Componentes que usam `MultiSelect`, `Select`, `Popover` ou outros overlays devem repassar essa prop
|
|
809
|
+
- **Não** é necessário configurar nada no `componentProps` - é automático!
|
|
810
|
+
|
|
811
|
+
**Exemplo com componentProps (se precisar passar outras props):**
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
{
|
|
815
|
+
name: 'training_ids',
|
|
816
|
+
label: 'Treinamentos',
|
|
817
|
+
type: 'custom' as const,
|
|
818
|
+
component: TrainingMultiSelect,
|
|
819
|
+
componentProps: {
|
|
820
|
+
maxDisplayedBadges: 3, // Props customizadas
|
|
821
|
+
enableSearch: true
|
|
822
|
+
// popoverContainer é passado automaticamente, não precisa incluir aqui
|
|
823
|
+
},
|
|
824
|
+
required: true
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
**Checklist:**
|
|
829
|
+
|
|
830
|
+
- ✅ Componente custom aceita `popoverContainer?: HTMLElement | null`
|
|
831
|
+
- ✅ `popoverContainer` é repassado para `MultiSelect`, `Popover`, etc.
|
|
832
|
+
- ✅ Usar `type: 'custom'` no field config
|
|
833
|
+
- ✅ Não incluir `popoverContainer` em `componentProps` (é automático)
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
#### 🔧 AppSidebar Redimensionável
|
|
838
|
+
|
|
839
|
+
**Comportamento Padrão:**
|
|
840
|
+
|
|
841
|
+
Por padrão, a `AppSidebar` tem largura fixa e **não** é redimensionável:
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
import { AppSidebar } from 'forlogic-core';
|
|
845
|
+
|
|
846
|
+
// Sidebar padrão (largura fixa, apenas collapse/expand com pin)
|
|
847
|
+
<AppSidebar config={sidebarConfig} />
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Habilitando Redimensionamento:**
|
|
851
|
+
|
|
852
|
+
Para habilitar o redimensionamento via drag na borda direita, use a prop `resizable={true}`:
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
import { AppSidebar } from 'forlogic-core';
|
|
856
|
+
|
|
857
|
+
// Sidebar redimensionável com limites customizados
|
|
858
|
+
<AppSidebar
|
|
859
|
+
config={sidebarConfig}
|
|
860
|
+
resizable={true}
|
|
861
|
+
minWidth={280} // Opcional (padrão: 224px/14rem)
|
|
862
|
+
maxWidth={500} // Opcional (padrão: 384px/24rem)
|
|
863
|
+
/>
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Exemplo com Custom Content:**
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
import { AppLayout, AppSidebar } from 'forlogic-core';
|
|
870
|
+
import { FolderTree } from './components/FolderTree';
|
|
871
|
+
|
|
872
|
+
function DocsPage() {
|
|
873
|
+
return (
|
|
874
|
+
<AppLayout
|
|
875
|
+
sidebar={
|
|
876
|
+
<AppSidebar
|
|
877
|
+
customContent={<FolderTree folders={folders} />}
|
|
878
|
+
resizable={true}
|
|
879
|
+
minWidth={280}
|
|
880
|
+
maxWidth={500}
|
|
881
|
+
/>
|
|
882
|
+
}
|
|
883
|
+
>
|
|
884
|
+
<div className="p-6">
|
|
885
|
+
{/* Conteúdo da página */}
|
|
886
|
+
</div>
|
|
887
|
+
</AppLayout>
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Props de Redimensionamento:**
|
|
893
|
+
|
|
894
|
+
| Prop | Tipo | Padrão | Descrição |
|
|
895
|
+
| ---------- | --------- | -------- | ------------------------------------------ |
|
|
896
|
+
| `resizable` | `boolean` | `false` | Habilita/desabilita redimensionamento |
|
|
897
|
+
| `minWidth` | `number` | `224` | Largura mínima em pixels (14rem) |
|
|
898
|
+
| `maxWidth` | `number` | `384` | Largura máxima em pixels (24rem) |
|
|
899
|
+
|
|
900
|
+
**Recursos:**
|
|
901
|
+
|
|
902
|
+
- ✅ Drag-to-resize com mouse na borda direita
|
|
903
|
+
- ✅ Persistência automática da largura (localStorage)
|
|
904
|
+
- ✅ Limites min/max configuráveis
|
|
905
|
+
- ✅ Feedback visual durante drag (hover e cursor)
|
|
906
|
+
- ✅ Funciona independente do pin/hover da sidebar
|
|
907
|
+
- ✅ Storage key: `app-sidebar-width`
|
|
908
|
+
|
|
909
|
+
**Quando Usar:**
|
|
910
|
+
|
|
911
|
+
- 📄 Páginas com conteúdo lateral extenso (documentação, navegação de arquivos)
|
|
912
|
+
- 📊 Dashboards com painéis laterais customizáveis
|
|
913
|
+
- 🗂️ Interfaces com árvores de navegação profundas
|
|
914
|
+
- ⚙️ Configurações onde o usuário prefere mais espaço para a sidebar
|
|
915
|
+
|
|
916
|
+
**Quando NÃO Usar:**
|
|
917
|
+
|
|
918
|
+
- ❌ Sidebars com navegação simples/curta (usa largura fixa padrão)
|
|
919
|
+
- ❌ Aplicações mobile-first (redimensionamento só funciona em desktop)
|
|
920
|
+
|
|
655
921
|
---
|
|
656
922
|
|
|
657
923
|
### 🎨 Campos Customizados no BaseForm
|