🍴 Meu Garfo é uma visualização em grafo dos CNPJs
cuducos.tngl.io/meu-garfo
1<!DOCTYPE html>
2<html lang="pt-br" data-theme="light">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <title>Meu Garfo</title>
7 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
8 <style>
9 :root {
10 --pico-primary: #784c87;
11 --pico-primary-hover: #603d6c;
12 --pico-primary-focus: rgba(120, 76, 135, 0.25);
13 --pico-primary-inverse: #fff;
14 --pico-h1-color: #784c87;
15 }
16
17 h1 {
18 color: #784c87 !important;
19 }
20
21 body {
22 margin: 0;
23 padding: 0;
24 height: 100vh;
25 width: 100vw;
26 display: flex;
27 flex-direction: column;
28 overflow: hidden;
29 }
30
31 #elm {
32 flex: 1;
33 display: flex;
34 flex-direction: column;
35 height: 100vh;
36 }
37
38 .app-root {
39 display: flex;
40 flex-direction: column;
41 height: 100vh;
42 width: 100vw;
43 }
44
45 /* Header & Navigation */
46 .app-header {
47 flex: 0 0 auto;
48 padding: 0.25rem 1rem;
49 border-bottom: 1px solid var(--pico-muted-border-color);
50 background-color: var(--pico-card-background-color);
51 margin-bottom: 0;
52 z-index: 10;
53 }
54
55 /* Footer */
56 .app-footer {
57 flex: 0 0 auto;
58 width: 100%;
59 padding: 0.25rem 0;
60 border-top: 1px solid var(--pico-muted-border-color);
61 background-color: var(--pico-card-background-color);
62 text-align: center;
63 margin: 0;
64 z-index: 10;
65 }
66
67 .app-footer small {
68 display: inline-block;
69 margin: 0 auto;
70 }
71
72 .app-header h1 {
73 margin: 0;
74 font-size: 1.1rem;
75 color: #784c87 !important;
76 white-space: nowrap;
77 }
78
79 /* Search Controls */
80 .controls {
81 display: flex;
82 gap: 1rem;
83 align-items: center;
84 margin: 0;
85 padding: 0;
86 list-style: none;
87 }
88
89 .controls li {
90 display: flex;
91 align-items: center;
92 gap: 0.5rem;
93 padding: 0;
94 }
95
96 .controls input,
97 .controls button {
98 margin-bottom: 0;
99 font-size: 0.9rem;
100 padding: 0.25rem 0.75rem;
101 }
102
103 .cnpj-input {
104 width: 180px;
105 }
106
107 .depth-input {
108 width: 70px;
109 }
110
111 /* Graph Container */
112 .graph-viewport {
113 flex: 1;
114 position: relative;
115 overflow: hidden;
116 background-color: #fafafa;
117 }
118
119 .graph-svg {
120 display: block;
121 width: 100%;
122 height: 100%;
123 }
124
125 /* Status & Feedback */
126 .status-overlay {
127 position: absolute;
128 bottom: 1rem;
129 right: 1rem;
130 background: rgba(255, 255, 255, 0.85);
131 padding: 0.25rem 0.75rem;
132 border-radius: var(--pico-border-radius);
133 box-shadow: var(--pico-card-box-shadow);
134 pointer-events: none;
135 }
136
137 .error-overlay {
138 position: absolute;
139 top: 1rem;
140 left: 50%;
141 transform: translateX(-50%);
142 z-index: 100;
143 max-width: 90%;
144 background: rgba(255, 0, 0, 0.05);
145 border: 1px solid #ff4136;
146 color: #ff4136;
147 padding: 0.25rem 0.75rem;
148 border-radius: var(--pico-border-radius);
149 font-size: 10px;
150 line-height: 1.2;
151 }
152
153 .error-overlay small {
154 color: #ff4136;
155 font-size: inherit !important;
156 }
157
158 /* Graph Elements */
159 .edge {
160 stroke: #89a1c9;
161 stroke-opacity: 0.4;
162 stroke-width: 1;
163 }
164
165 .node-label {
166 font-size: 10px;
167 fill: var(--pico-color);
168 user-select: text;
169 }
170
171 .node-company {
172 stroke: #fff;
173 stroke-width: 1.5;
174 }
175
176 .company-active {
177 fill: #784c87;
178 }
179
180 .company-inactive {
181 fill: #b0a0b8;
182 }
183
184 .company-unknown {
185 fill: #ff4136;
186 }
187
188 .node-person {
189 fill: #89a1c9;
190 stroke: #fff;
191 stroke-width: 1.5;
192 }
193
194 .visited {
195 opacity: 0.6;
196 stroke-width: 1;
197 }
198
199 .expandable {
200 cursor: pointer;
201 stroke: #fff;
202 stroke-width: 2.5;
203 }
204
205 .expandable:hover {
206 stroke: #784c87;
207 filter: brightness(1.1);
208 }
209
210 .node-error {
211 font-size: 10px;
212 fill: #ff4136;
213 font-style: italic;
214 user-select: text;
215 }
216
217 .errored {
218 fill: #ff4136 !important;
219 stroke: #ff4136;
220 opacity: 0.5;
221 }
222
223 .node-group.root .node-label {
224 font-size: 13px;
225 font-weight: 600;
226 }
227
228 circle.root {
229 stroke-width: 3;
230 }
231
232 .external-link-icon {
233 fill: #784c87;
234 cursor: pointer;
235 font-size: 10px;
236 }
237
238 .external-link-icon:hover {
239 fill: #603d6c;
240 text-decoration: underline;
241 }
242 .loading-indicator {
243 color: var(--pico-primary);
244 font-weight: bold;
245 margin-left: 5px;
246 }
247
248 .info-panel {
249 position: absolute;
250 top: 1rem;
251 right: 1rem;
252 max-width: 360px;
253 background: rgba(255, 255, 255, 0.92);
254 border: 1px solid var(--pico-muted-border-color);
255 border-radius: var(--pico-border-radius);
256 box-shadow: var(--pico-card-box-shadow);
257 padding: 0.4rem 0.75rem;
258 font-size: 0.82rem;
259 line-height: 1.35;
260 }
261
262 .info-panel summary {
263 cursor: pointer;
264 color: var(--pico-primary);
265 font-weight: 600;
266 list-style: none;
267 margin-bottom: 0;
268 }
269
270 .info-panel summary::-webkit-details-marker { display: none; }
271 .info-panel summary::marker { content: ""; }
272
273 .info-panel[open] summary { margin-bottom: 0.5rem; }
274
275 .info-panel small {
276 font-size: 0.78rem;
277 color: var(--pico-muted-color);
278 }
279
280 .legend {
281 display: flex;
282 flex-wrap: wrap;
283 gap: 0.5rem 0.75rem;
284 margin-bottom: 0.5rem;
285 }
286
287 .legend-item {
288 display: inline-flex;
289 align-items: center;
290 gap: 0.3rem;
291 }
292
293 .info-panel-divider {
294 border-top: 1px solid var(--pico-muted-border-color);
295 margin: 0 0 0.5rem 0;
296 }
297
298 .legend-dot {
299 display: inline-block;
300 width: 10px;
301 height: 10px;
302 border-radius: 50%;
303 border: 1.5px solid #fff;
304 }
305
306 .legend-company-active { background: #784c87; }
307 .legend-company-inactive { background: #b0a0b8; }
308 .legend-company-unknown { background: #ff4136; }
309 .legend-person { background: #89a1c9; }
310 </style>
311 <script src="main.js"></script>
312</head>
313<body>
314 <div id="elm"></div>
315 <script>
316 var app = Elm.Main.init({
317 node: document.getElementById('elm'),
318 flags: {
319 graphApi: "https://grafo.minhareceita.org",
320 jsonApi: "https://minhareceita.org"
321 }
322 });
323 </script>
324</body>
325</html>