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 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