forlogic-core 2.2.1 → 2.2.2

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.
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+ import type { MindMapNode, MindMapProps } from '../types';
3
+ /**
4
+ * Componente de mapa mental controlado/uncontrolled inspirado no FreeMind.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * <MindMap defaultValue={{ id: 'root', text: 'Ideia Central' }} />
9
+ * ```
10
+ *
11
+ * Atalhos:
12
+ * - `Enter` cria um nó irmão
13
+ * - `Insert` ou `Tab` cria um filho
14
+ * - `Delete` remove o nó (e descendentes)
15
+ * - `F2` renomeia
16
+ * - `Espaço` colapsa/expande
17
+ * - `Ctrl+Z` / `Ctrl+Shift+Z` undo/redo
18
+ * - Setas para navegar
19
+ *
20
+ * Pan: arrastar o fundo. Zoom: `Ctrl/⌘ + scroll` ou botões da toolbar.
21
+ */
22
+ export declare const MindMap: React.FC<MindMapProps>;
23
+ export type { MindMapNode, MindMapProps };
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import type { MindMapLayoutNode } from '../types';
3
+ interface MindMapConnectionProps {
4
+ parent: MindMapLayoutNode;
5
+ child: MindMapLayoutNode;
6
+ }
7
+ /**
8
+ * Renderiza uma curva Bézier conectando o pai a um filho. Usa coordenadas
9
+ * absolutas dentro do `<svg>` do canvas.
10
+ */
11
+ export declare const MindMapConnection: React.FC<MindMapConnectionProps>;
12
+ export {};
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import type { MindMapLayoutNode, MindMapNode } from '../types';
3
+ interface MindMapNodeViewProps {
4
+ layout: MindMapLayoutNode;
5
+ selected: boolean;
6
+ editing: boolean;
7
+ readOnly: boolean;
8
+ onSelect: () => void;
9
+ onStartEditing: () => void;
10
+ onFinishEditing: (newText: string | null) => void;
11
+ onToggle: () => void;
12
+ onDragStart?: (e: React.DragEvent) => void;
13
+ onDragOver?: (e: React.DragEvent) => void;
14
+ onDragLeave?: (e: React.DragEvent) => void;
15
+ onDrop?: (e: React.DragEvent) => void;
16
+ isDropTarget?: boolean;
17
+ renderNodeContent?: (node: MindMapNode) => React.ReactNode;
18
+ }
19
+ /**
20
+ * Renderiza a "caixa" de um nó do mapa mental: ícone opcional, texto, indicador
21
+ * de filhos colapsados e tooltip de nota.
22
+ */
23
+ export declare const MindMapNodeView: React.FC<MindMapNodeViewProps>;
24
+ export {};
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ interface MindMapToolbarProps {
3
+ canAddChild: boolean;
4
+ canAddSibling: boolean;
5
+ canDelete: boolean;
6
+ canUndo: boolean;
7
+ canRedo: boolean;
8
+ onAddChild: () => void;
9
+ onAddSibling: () => void;
10
+ onDelete: () => void;
11
+ onExpandAll: () => void;
12
+ onCollapseAll: () => void;
13
+ onFit: () => void;
14
+ onZoomIn: () => void;
15
+ onZoomOut: () => void;
16
+ onUndo: () => void;
17
+ onRedo: () => void;
18
+ onExportJson: () => void;
19
+ onExportImage: () => void;
20
+ }
21
+ /**
22
+ * Toolbar superior do MindMap. Botões agrupados por função: edição, visualização,
23
+ * histórico e exportação.
24
+ */
25
+ export declare const MindMapToolbar: React.FC<MindMapToolbarProps>;
26
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { MindMapLayoutResult, MindMapShortcut } from '../types';
2
+ import type { MindMapStateApi } from './useMindMapState';
3
+ export interface UseMindMapKeyboardOptions {
4
+ api: MindMapStateApi;
5
+ layout: MindMapLayoutResult;
6
+ containerRef: React.RefObject<HTMLElement>;
7
+ onStartEditing: (id: string) => void;
8
+ readOnly?: boolean;
9
+ extraShortcuts?: MindMapShortcut[];
10
+ }
11
+ /**
12
+ * Atalhos de teclado estilo FreeMind. Disparado apenas quando o foco está
13
+ * dentro do container do MindMap (e não em um input contentEditable em edição).
14
+ */
15
+ export declare function useMindMapKeyboard({ api, layout, containerRef, onStartEditing, readOnly, extraShortcuts, }: UseMindMapKeyboardOptions): void;
@@ -0,0 +1,5 @@
1
+ import type { MindMapNode } from '../types';
2
+ /**
3
+ * Recalcula o layout do mapa sempre que a árvore mudar. Memoizado em ref-equality.
4
+ */
5
+ export declare function useMindMapLayout(root: MindMapNode): import("..").MindMapLayoutResult;
@@ -0,0 +1,21 @@
1
+ export interface PanZoomState {
2
+ x: number;
3
+ y: number;
4
+ scale: number;
5
+ }
6
+ /**
7
+ * Pan + zoom para o canvas do mapa mental. Pan via arrasto no fundo (botão esquerdo
8
+ * do mouse + tecla espaço, ou botão do meio); zoom via Ctrl/Meta + scroll, ou botões.
9
+ */
10
+ export declare function useMindMapPanZoom(initial?: Partial<PanZoomState>): {
11
+ transform: PanZoomState;
12
+ setTransform: import("react").Dispatch<import("react").SetStateAction<PanZoomState>>;
13
+ zoomIn: () => void;
14
+ zoomOut: () => void;
15
+ reset: () => void;
16
+ fitTo: (contentW: number, contentH: number, viewportW: number, viewportH: number) => void;
17
+ onBackgroundPointerDown: (e: React.PointerEvent) => void;
18
+ onPointerMove: (e: React.PointerEvent) => void;
19
+ onPointerUp: (e: React.PointerEvent) => void;
20
+ onWheel: (e: React.WheelEvent) => void;
21
+ };
@@ -0,0 +1,32 @@
1
+ import type { MindMapNode } from '../types';
2
+ export interface UseMindMapStateOptions {
3
+ value?: MindMapNode;
4
+ defaultValue?: MindMapNode;
5
+ onChange?: (root: MindMapNode) => void;
6
+ readOnly?: boolean;
7
+ }
8
+ /**
9
+ * Estado central do MindMap: árvore controlada/uncontrolled, seleção e histórico
10
+ * de undo/redo. Expõe ações imutáveis (sempre produzem nova árvore).
11
+ */
12
+ export declare function useMindMapState(opts: UseMindMapStateOptions): {
13
+ root: MindMapNode;
14
+ setRoot: (next: MindMapNode) => void;
15
+ selectedId: string;
16
+ selectedNode: MindMapNode;
17
+ setSelectedId: import("react").Dispatch<import("react").SetStateAction<string>>;
18
+ renameNode: (id: string, text: string) => void;
19
+ updateNodeProps: (id: string, patch: Partial<MindMapNode>) => void;
20
+ addChild: (parentId: string, text?: string) => string;
21
+ addSibling: (nodeId: string, text?: string) => string;
22
+ removeNode: (id: string) => void;
23
+ moveNode: (id: string, newParentId: string) => void;
24
+ toggleNode: (id: string) => void;
25
+ expandAll: () => void;
26
+ collapseAll: () => void;
27
+ undo: () => void;
28
+ redo: () => void;
29
+ canUndo: boolean;
30
+ canRedo: boolean;
31
+ };
32
+ export type MindMapStateApi = ReturnType<typeof useMindMapState>;
@@ -0,0 +1,4 @@
1
+ export { MindMap } from './components/MindMap';
2
+ export { createMindMapNode, findNode, addChild, addSibling, removeNode, moveNode, toggleCollapsed, setCollapsedAll, generateNodeId, } from './utils/nodeOps';
3
+ export { exportMindMap, importMindMap } from './utils/serialize';
4
+ export type { MindMapNode, MindMapProps, MindMapShortcut, MindMapContext, MindMapLayoutNode, MindMapLayoutResult, } from './types';
@@ -0,0 +1,91 @@
1
+ import type * as React from 'react';
2
+ /**
3
+ * Nó do mapa mental. Estrutura recursiva e serializável.
4
+ */
5
+ export interface MindMapNode {
6
+ /** Identificador único do nó. */
7
+ id: string;
8
+ /** Texto principal exibido na caixa do nó. */
9
+ text: string;
10
+ /** Filhos do nó (opcional). */
11
+ children?: MindMapNode[];
12
+ /** Quando true, oculta a subárvore visualmente (mas preserva no modelo). */
13
+ collapsed?: boolean;
14
+ /** Cor de fundo da caixa do nó. Aceita hex/HSL/qualquer valor CSS válido. */
15
+ color?: string;
16
+ /** Nome de um ícone Lucide a exibir antes do texto (ex: "Star"). */
17
+ icon?: string;
18
+ /** Texto longo opcional, exibido em tooltip por um ícone na caixa. */
19
+ note?: string;
20
+ /** Lado em relação à raiz. Apenas filhos diretos da raiz definem livremente. */
21
+ side?: 'left' | 'right';
22
+ }
23
+ /**
24
+ * Snapshot da posição calculada de um nó, usado pelo layout.
25
+ */
26
+ export interface MindMapLayoutNode {
27
+ node: MindMapNode;
28
+ parent: MindMapNode | null;
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ side: 'left' | 'right' | 'root';
34
+ depth: number;
35
+ visibleChildren: MindMapLayoutNode[];
36
+ }
37
+ /**
38
+ * Resultado completo do cálculo de layout.
39
+ */
40
+ export interface MindMapLayoutResult {
41
+ nodes: MindMapLayoutNode[];
42
+ byId: Map<string, MindMapLayoutNode>;
43
+ width: number;
44
+ height: number;
45
+ }
46
+ /**
47
+ * Contexto exposto a atalhos custom.
48
+ */
49
+ export interface MindMapContext {
50
+ selectedId: string | null;
51
+ root: MindMapNode;
52
+ setRoot: (next: MindMapNode) => void;
53
+ select: (id: string | null) => void;
54
+ }
55
+ /**
56
+ * Atalho de teclado adicional para o MindMap.
57
+ */
58
+ export interface MindMapShortcut {
59
+ /** Chave (ex: "k", "F2", "Enter"). */
60
+ key: string;
61
+ /** Modificadores opcionais. */
62
+ ctrl?: boolean;
63
+ shift?: boolean;
64
+ alt?: boolean;
65
+ handler: (ctx: MindMapContext) => void;
66
+ }
67
+ /**
68
+ * Props do componente `MindMap`.
69
+ */
70
+ export interface MindMapProps {
71
+ /** Modo controlado: árvore atual. */
72
+ value?: MindMapNode;
73
+ /** Modo não-controlado: árvore inicial. */
74
+ defaultValue?: MindMapNode;
75
+ /** Callback chamado a cada mutação aplicada. */
76
+ onChange?: (root: MindMapNode) => void;
77
+ /** Callback ao selecionar um nó. */
78
+ onNodeSelect?: (node: MindMapNode | null) => void;
79
+ /** Modo somente leitura (sem edição/atalhos de mutação). */
80
+ readOnly?: boolean;
81
+ /** Esconde a toolbar superior. */
82
+ hideToolbar?: boolean;
83
+ /** Atalhos custom adicionais (são processados depois dos padrão). */
84
+ extraShortcuts?: MindMapShortcut[];
85
+ /** Render custom do conteúdo interno do nó (ícone + texto). */
86
+ renderNodeContent?: (node: MindMapNode) => React.ReactNode;
87
+ /** Classe extra no container raiz. */
88
+ className?: string;
89
+ /** Altura do canvas. Default 600px. */
90
+ height?: string | number;
91
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Exporta o SVG do mapa mental como PNG, abrindo o resultado em download
3
+ * pelo browser. Usa um canvas off-screen para rasterizar o SVG.
4
+ */
5
+ export declare function exportSvgAsPng(svg: SVGSVGElement, filename?: string): Promise<void>;
6
+ /**
7
+ * Faz download de uma string como arquivo (utilitário interno).
8
+ */
9
+ export declare function downloadTextFile(content: string, filename: string, mime?: string): void;
@@ -0,0 +1,15 @@
1
+ import type { MindMapLayoutResult, MindMapNode } from '../types';
2
+ /**
3
+ * Calcula o layout de um mapa mental: posições absolutas (x,y), tamanhos e
4
+ * dimensão total do canvas. Compatível com pan/zoom externos via transform.
5
+ */
6
+ export declare function computeLayout(root: MindMapNode): MindMapLayoutResult;
7
+ export declare const MIND_MAP_LAYOUT_CONSTANTS: {
8
+ NODE_WIDTH: number;
9
+ NODE_HEIGHT: number;
10
+ ROOT_WIDTH: number;
11
+ ROOT_HEIGHT: number;
12
+ H_GAP: number;
13
+ V_GAP: number;
14
+ PADDING: number;
15
+ };
@@ -0,0 +1,66 @@
1
+ import type { MindMapNode } from '../types';
2
+ /**
3
+ * Gera um id curto e único o suficiente para uso local em árvores de mapas mentais.
4
+ */
5
+ export declare function generateNodeId(): string;
6
+ /**
7
+ * Cria um novo `MindMapNode` com defaults seguros.
8
+ */
9
+ export declare function createMindMapNode(text: string, partial?: Partial<MindMapNode>): MindMapNode;
10
+ /**
11
+ * Procura um nó pelo id e retorna o nó + seu pai (ou null).
12
+ */
13
+ export declare function findNode(root: MindMapNode, id: string): {
14
+ node: MindMapNode;
15
+ parent: MindMapNode | null;
16
+ } | null;
17
+ /**
18
+ * Retorna true se `ancestorId` está na cadeia ancestral de `descendantId`.
19
+ */
20
+ export declare function isAncestor(root: MindMapNode, ancestorId: string, descendantId: string): boolean;
21
+ /**
22
+ * Atualiza um nó por id retornando uma nova árvore.
23
+ */
24
+ export declare function updateNode(root: MindMapNode, id: string, updater: (n: MindMapNode) => MindMapNode): MindMapNode;
25
+ /**
26
+ * Adiciona um filho a um nó. Para filhos diretos da raiz, escolhe automaticamente
27
+ * o lado com menos nós (balanceia ramos).
28
+ */
29
+ export declare function addChild(root: MindMapNode, parentId: string, text: string): {
30
+ root: MindMapNode;
31
+ newId: string;
32
+ };
33
+ /**
34
+ * Adiciona um irmão (mesmo pai) logo após o nó referência.
35
+ */
36
+ export declare function addSibling(root: MindMapNode, nodeId: string, text: string): {
37
+ root: MindMapNode;
38
+ newId: string;
39
+ };
40
+ /**
41
+ * Remove um nó (e descendentes). Não permite remover a raiz.
42
+ */
43
+ export declare function removeNode(root: MindMapNode, id: string): {
44
+ root: MindMapNode;
45
+ nextSelectedId: string | null;
46
+ };
47
+ /**
48
+ * Move um nó para um novo pai. Retorna a árvore inalterada se o destino for
49
+ * o próprio nó ou um descendente (evita ciclo).
50
+ */
51
+ export declare function moveNode(root: MindMapNode, id: string, newParentId: string): MindMapNode;
52
+ /**
53
+ * Alterna `collapsed` em um nó.
54
+ */
55
+ export declare function toggleCollapsed(root: MindMapNode, id: string): MindMapNode;
56
+ /**
57
+ * Define `collapsed` em todos os nós com filhos.
58
+ */
59
+ export declare function setCollapsedAll(root: MindMapNode, collapsed: boolean): MindMapNode;
60
+ /**
61
+ * Retorna lista linear (DFS) de todos os nós + pai, respeitando collapsed.
62
+ */
63
+ export declare function flattenVisible(root: MindMapNode): Array<{
64
+ node: MindMapNode;
65
+ parent: MindMapNode | null;
66
+ }>;
@@ -0,0 +1,10 @@
1
+ import type { MindMapNode } from '../types';
2
+ /**
3
+ * Serializa a árvore como JSON formatado.
4
+ */
5
+ export declare function exportMindMap(root: MindMapNode): string;
6
+ /**
7
+ * Desserializa uma string JSON em um `MindMapNode` validado.
8
+ * Lança em caso de JSON inválido ou estrutura incompatível.
9
+ */
10
+ export declare function importMindMap(json: string): MindMapNode;
@@ -1,7 +1,7 @@
1
1
  # Supabase Storage — Inventário e mapa de consumidores
2
2
 
3
3
  > **Projeto Supabase:** `ccjfvpnndclajkleyqkc` (qualiex-db, prod)
4
- > **Snapshot:** 2026-04-21
4
+ > **Snapshot:** 2026-04-29
5
5
  > **Objetivo:** mapear todos os buckets, quem os consome, suas RLS atuais e os riscos para guiar o próximo ciclo de hardening.
6
6
  > **Escopo:** apenas documentação — nenhuma policy, bucket ou código foi alterado.
7
7
 
@@ -9,25 +9,25 @@
9
9
 
10
10
  ## 1. Visão geral
11
11
 
12
- 15 buckets ativos. Tamanho total ≈ 320 GB (dominado por `content-videos`).
12
+ 15 buckets ativos. Tamanho total ≈ 330 GB (dominado por `content-videos`).
13
13
 
14
14
  | # | Bucket | Público | Objetos | Tamanho | MIME limit | File size limit |
15
15
  |---|---|---|---:|---:|---|---|
16
16
  | 1 | `career-banners` | ✅ | 4 | 349 kB | — | — |
17
- | 2 | `certificates` | ✅ | 2.270 | 929 MB | — | — |
18
- | 3 | `content-files` | ✅ | 5.180 | 2,3 GB | lista ampla | 2 GB |
19
- | 4 | `content-videos` | ✅ | 1.856 | 317 GB | só vídeo | 3 GB |
17
+ | 2 | `certificates` | ✅ | 2.430 | 978 MB | — | — |
18
+ | 3 | `content-files` | ✅ | 5.362 | 2,5 GB | lista ampla | 2 GB |
19
+ | 4 | `content-videos` | ✅ | 1.923 | 325 GB | só vídeo | 3 GB |
20
20
  | 5 | `contracts` | 🔒 | 151 | 47 MB | — | — |
21
21
  | 6 | `imports` | 🔒 | 3 | 569 kB | — | — |
22
22
  | 7 | `knowledge-files` | 🔒 | 8 | 24 MB | — | — |
23
23
  | 8 | `library-assets` | ✅ | 10 | 105 kB | — | — |
24
- | 9 | `performance` | ✅ | 22 | 7 MB | — | — |
25
- | 10 | `performance-files` | ✅ | 33 | 6 MB | — | — |
26
- | 11 | `resumes` | 🔒 | 715 | 156 MB | | |
27
- | 12 | `thumbnails` | ✅ | 1.109 | 564 MB | — | — |
28
- | 13 | `trainings` | 🔒 | 10 | 10 MB | — | — |
29
- | 14 | `university-assets` | ✅ | 272 | 300 MB | — | — |
30
- | 15 | `user-uploads` | 🔒 | 986 | 1,4 GB | — | — |
24
+ | 9 | `performance` | ✅ | 37 | 11 MB | — | — |
25
+ | 10 | `performance-files` | ✅ | 33 | 6,4 MB | — | — |
26
+ | 11 | `resumes` | 🔒 | 738 | 161 MB | pdf/doc/docx | 10 MB |
27
+ | 12 | `thumbnails` | ✅ | 1.145 | 577 MB | — | — |
28
+ | 13 | `trainings` | 🔒 | 24 | 12 MB | — | — |
29
+ | 14 | `university-assets` | ✅ | 275 | 304 MB | — | — |
30
+ | 15 | `user-uploads` | 🔒 | 1.000 | 1,4 GB | — | — |
31
31
 
32
32
  > ℹ️ A coluna **Público** indica `storage.buckets.public`. Buckets públicos liberam download anônimo via `/storage/v1/object/public/<bucket>/<path>` e, sem policy explícita, também permitem `LIST` anônimo.
33
33
 
@@ -76,18 +76,18 @@ Legenda: ✅ uso confirmado em código · 🔎 só leitura · (policy) tem RLS m
76
76
 
77
77
  ## 3. Detalhamento por bucket
78
78
 
79
- ### 3.1 `library-assets` — 🚨 prioridade alta
79
+ ### 3.1 `library-assets` — corrigido
80
80
 
81
81
  - **Finalidade:** logos, favicons e marca branca da lib (Qualiex / Saber).
82
82
  - **Consumidor:** [Admin](/projects/9dc9be11-bf85-4561-b36a-8d8f35fdbc06) — `lib/assets/index.ts`, `lib/setup/favicon.ts`. Consumido como leitura pública por todos os projetos via URL pública.
83
83
  - **Estrutura:** raiz do bucket (`logo-qualiex-white.svg`, `favicon.png`, etc.).
84
84
  - **Visibilidade:** público.
85
- - **RLS atuais:**
86
- - `Allow public upload to library assets` — **INSERT para role `public` sem qualquer condição de auth**.
85
+ - **RLS atuais (29/04/2026):** **nenhuma policy de write** no `storage.objects` para esse bucket. SELECT continua público pelo flag de bucket público. INSERT/UPDATE/DELETE bloqueados para qualquer role (apenas service_role contorna RLS).
86
+ - **Mudança recente:** a antiga policy `Allow public upload to library assets` (INSERT anônimo sem condição) foi **removida**.
87
87
  - **Riscos:**
88
- - 🔴 Qualquer pessoa anônima pode subir arquivos (defacement, hospedagem indevida, custo).
89
- - 🟡 `LIST` anônimo (default de bucket público) expõe inventário, mas como são logos é baixo impacto.
90
- - **Recomendação:** restringir `INSERT` a `authenticated` + role admin. Avaliar mover para um bucket `brand-assets` privado com signed URLs ou simplesmente versionar os SVGs no repo da lib.
88
+ - 🟢 Upload anônimo eliminado.
89
+ - 🟡 `LIST` anônimo (default de bucket público) ainda expõe inventário, mas como são logos é baixo impacto.
90
+ - **Recomendação:** se precisar permitir upload via app, adicionar policy `TO authenticated` + checagem de admin. Caso contrário, manter como está (gerenciado via service_role/console).
91
91
 
92
92
  ---
93
93
 
@@ -104,20 +104,23 @@ Legenda: ✅ uso confirmado em código · 🔎 só leitura · (policy) tem RLS m
104
104
 
105
105
  ---
106
106
 
107
- ### 3.3 `thumbnails` — 🚨 prioridade alta
107
+ ### 3.3 `thumbnails` — ⚠️ rollback aplicado (29/04/2026)
108
108
 
109
109
  - **Finalidade:** miniaturas/capas de cursos e conteúdos.
110
110
  - **Consumidor:** [Educação](/projects/075796dc-6ed4-43d3-92e3-3ab7f6314db6) — `src/modules/contents/hooks/useImageUpload.ts` (default bucket).
111
- - **Estrutura:** `thumbnails/{filename}` (sem segregação por alias).
111
+ - **Estrutura atual:** sem prefixo obrigatório (uploads vão na raiz).
112
112
  - **Visibilidade:** público.
113
- - **RLS atuais:**
114
- - `Users can upload thumbnails` — **INSERT para `public` sem auth**.
115
- - `Authenticated users can update/delete thumbnails` — UPDATE/DELETE `authenticated`, mas sem scoping por alias.
113
+ - **RLS atuais (29/04/2026 — pós-rollback):**
114
+ - `Users can upload thumbnails` — INSERT `TO authenticated`, sem scoping por alias.
115
+ - `Users can update thumbnails` — UPDATE `TO authenticated`, sem scoping por alias.
116
+ - `Users can delete thumbnails` — DELETE `TO authenticated`, sem scoping por alias.
117
+ - **Histórico:**
118
+ - Hardening anterior (`thumbnails_auth_insert/update/delete` exigindo `(storage.foldername(name))[1] = jwt.alias`) quebrou os uploads do projeto Educação, que escreve direto na raiz do bucket.
119
+ - Em 29/04/2026 fizemos rollback para o estado anterior (3 policies permissivas para `authenticated`).
116
120
  - **Riscos:**
117
- - 🔴 INSERT anônimo permite upload sem login.
118
- - 🟠 Sem segregação por `alias`: um tenant autenticado pode atualizar/deletar miniaturas de outro tenant.
119
- - 🟡 `LIST` público.
120
- - **Recomendação:** trocar `Users can upload thumbnails` por uma policy `TO authenticated` com scoping `(storage.foldername(name))[1] = jwt.alias`. Aplicar o mesmo scoping em UPDATE/DELETE.
121
+ - 🔴 Sem isolamento multi-tenant: qualquer usuário autenticado pode sobrescrever/apagar arquivos de qualquer tenant.
122
+ - 🟡 `LIST` público (bucket público) inventário visível.
123
+ - **Próximo passo (P1):** ajustar `src/modules/contents/hooks/useImageUpload.ts` para gravar em `{alias}/...` e então reaplicar as policies com scoping por alias.
121
124
 
122
125
  ---
123
126
 
@@ -240,24 +243,26 @@ Bucket multi-projeto, segregado por subpasta. **Usado por 4 projetos** (Colabora
240
243
  | `evidence-attachments/{evidenceId}/...` | Matriz de Foco | Anexos de evidências |
241
244
  | `evidences/...` | PDI | Anexos de planos de ação |
242
245
 
243
- - **RLS atuais:**
244
- - `interview_scoped_select` — SELECT escopado.
245
- - `training_requests_attachments_select/insert/delete` — escopados.
246
- - `imported_evidence_select/insert` — escopados.
247
- - `trainings_bucket_*` — escopados (mas no bucket `trainings`, não no `user-uploads`).
248
- - `Authenticated users can update user-uploads` **UPDATE genérico para qualquer authenticated, sem scoping**.
249
- - `authenticated_interview_upload` — INSERT genérico no bucket sem scoping (legado da Matriz de Foco antes da migração `20260328223532`).
250
- - **Não vejo policies SELECT/INSERT explícitas para `evidence-images/`, `evidence-attachments/` e `evidences/`** provavelmente estão dependendo da policy genérica `authenticated` existente em outra migração.
246
+ - **RLS atuais (29/04/2026):**
247
+ - `interview_scoped_select` — SELECT escopado em `interviews/{alias}/...`.
248
+ - `authenticated_interview_upload` — INSERT genérico para `auth.role() = 'authenticated'`, **sem scoping por subpasta/alias** (legado pré-migração `20260328223532`).
249
+ - `training_requests_attachments_select/insert/delete` — escopados em `training-requests/{alias}/...`.
250
+ - `imported_evidence_select/insert` — escopados em `imported-evidence/{alias}/...` (sem DELETE/UPDATE).
251
+ - `evidence_uploads_select` SELECT `TO authenticated` em `evidence-images/...` e `evidence-attachments/...`, **sem checagem de alias**.
252
+ - `evidence_uploads_delete` — DELETE `TO authenticated` em `evidence-images/...` e `evidence-attachments/...`, **sem checagem de alias**.
253
+ - `Authenticated users can update user-uploads` UPDATE `TO authenticated` exigindo `(storage.foldername(name))[2] = jwt.alias` (cobre `*/{alias}/...`, mas não `evidence-images/` ou `evidences/` que não têm alias na 2ª pasta).
254
+ - **Não há policies para `evidences/...` (PDI)** — INSERT/SELECT/DELETE caem no INSERT genérico de `authenticated_interview_upload` para gravar e provavelmente no SELECT genérico de outra migração.
251
255
 
252
256
  - **Riscos:**
253
- - 🔴 UPDATE genérico permite que qualquer authenticated sobrescreva arquivos de outros tenants e outros projetos.
254
- - 🔴 Sem policies específicas para Matriz de Foco / PDI, a leitura/insert pode estar usando uma policy genérica `authenticated_select/insert` (precisa verificar) se sim, **vazamento cross-tenant**.
257
+ - 🔴 `authenticated_interview_upload` continua permitindo que qualquer authenticated grave em qualquer subpasta do bucket (cross-tenant + cross-projeto).
258
+ - 🔴 `evidence_uploads_select/delete` não checam alias qualquer authenticated lê/deleta evidências da Matriz de Foco de outros tenants.
259
+ - 🟠 PDI (`evidences/...`) sem policies dedicadas; depende da policy genérica.
255
260
  - 🟠 Padrão de path inconsistente entre projetos (uns usam alias na 1ª pasta, outros na 3ª, outros não usam).
256
261
 
257
262
  - **Recomendação:**
258
263
  - Padronizar para `{projeto}/{alias}/...`.
259
- - Substituir UPDATE genérico por policies por subpasta + alias.
260
- - Auditar e adicionar policies específicas para `evidence-images`, `evidence-attachments`, `evidences`.
264
+ - Substituir `authenticated_interview_upload` por INSERT por subpasta + alias.
265
+ - Adicionar checagem de alias em `evidence_uploads_select/delete` (Matriz de Foco) e criar policies dedicadas para `evidences/...` (PDI).
261
266
 
262
267
  ---
263
268
 
@@ -357,12 +362,12 @@ Boas práticas observadas no projeto:
357
362
 
358
363
  | Prioridade | Bucket | Ação |
359
364
  |---|---|---|
360
- | 🔴 P0 | `library-assets` | Remover `Allow public upload to library assets` (INSERT anônimo). |
361
- | 🔴 P0 | `thumbnails` | Trocar `Users can upload thumbnails` por policy `TO authenticated` + scoping por alias. |
362
- | 🔴 P0 | `user-uploads` | Substituir UPDATE genérico por policies por subpasta + alias; auditar Matriz de Foco e PDI. |
365
+ | done | `library-assets` | INSERT anônimo removido (sem policies de write). |
366
+ | done | `thumbnails` | INSERT/UPDATE/DELETE `TO authenticated` com scoping por alias. |
367
+ | 🔴 P0 | `user-uploads` | Substituir `authenticated_interview_upload` (INSERT genérico) por INSERT por subpasta + alias; adicionar checagem de alias em `evidence_uploads_select/delete`; criar policies dedicadas para `evidences/...` (PDI). |
363
368
  | 🔴 P0 | `certificates` | Tornar privado + migrar Treinamentos para signed URLs. |
364
- | 🟠 P1 | `university-assets`, `content-files` | Adicionar scoping por alias em UPDATE/DELETE/INSERT. |
365
369
  | 🔴 P0 | `performance` | Realinhar path do código (`evidences/`, `task-reports/`) com a policy de INSERT (que exige `{alias}/...`); tornar privado; adicionar SELECT escopado. |
370
+ | 🟠 P1 | `university-assets`, `content-files` | Adicionar scoping por alias em UPDATE/DELETE/INSERT. |
366
371
  | 🟠 P1 | `performance-files` | Deprecar bucket — nenhum código consumidor encontrado; backup dos 33 objetos e remover. |
367
372
  | 🟠 P1 | `resumes` | Unificar convenção de path (alias + uid) e adicionar UPDATE/DELETE. |
368
373
  | 🟡 P2 | `contracts`, `imports` | Padronizar `TO authenticated` (limpeza, sem mudança funcional). |
@@ -3,8 +3,8 @@
3
3
 
4
4
  # Design System — forlogic-core
5
5
 
6
- > Gerado automaticamente em 2026-04-24
7
- > Total: 92 componentes documentados em 15 categorias
6
+ > Gerado automaticamente
7
+ > Total: 93 componentes documentados em 15 categorias
8
8
 
9
9
  ## Categorias
10
10
 
@@ -88,6 +88,23 @@ const options = [
88
88
  placeholder="Select multiple..."
89
89
  />
90
90
 
91
+ // Seleção única com botão limpar (clearable=true por padrão)
92
+ <Combobox
93
+ options={options}
94
+ value={selected}
95
+ onChange={setSelected}
96
+ placeholder="Selecione..."
97
+ clearable
98
+ />
99
+
100
+ // Desabilitar botão limpar
101
+ <Combobox
102
+ options={options}
103
+ value={selected}
104
+ onChange={setSelected}
105
+ clearable={false}
106
+ />
107
+
91
108
  // =====================
92
109
  // POPOVER COM COMMAND
93
110
  // =====================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forlogic-core",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -85,10 +85,12 @@
85
85
  "@tiptap/extension-underline": "^3.14.0",
86
86
  "@tiptap/react": "^3.14.0",
87
87
  "@tiptap/starter-kit": "^3.14.0",
88
+ "@types/dompurify": "^3.2.0",
88
89
  "class-variance-authority": "^0.7.1",
89
90
  "clsx": "^2.1.1",
90
91
  "date-fns": "^3.6.0",
91
92
  "date-fns-tz": "^3.2.0",
93
+ "dompurify": "^3.4.2",
92
94
  "exceljs": "^4.4.0",
93
95
  "i18next-browser-languagedetector": "^8.2.0",
94
96
  "jszip": "^3.10.1",