@hed-hog/core 0.0.327 → 0.0.328

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.
@@ -1410,7 +1410,7 @@
1410
1410
  icon: folder
1411
1411
  name:
1412
1412
  en: File Storage
1413
- pt: Armazenamento de Arquivos
1413
+ pt: Arquivos
1414
1414
  description:
1415
1415
  en: Configure file storage backend, accepted file types, and size limits for uploads
1416
1416
  pt: Configure backend de armazenamento de arquivos, tipos de arquivo aceitos e limites de tamanho para uploads
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { Badge } from '@/components/ui/badge';
3
4
  import {
4
5
  Card,
5
6
  CardContent,
@@ -143,7 +144,7 @@ export default function Page() {
143
144
  {section.settings.map((setting: Setting) => (
144
145
  <div
145
146
  key={setting.id}
146
- className="flex min-w-0 flex-col gap-3 border-b border-border pb-4 last:border-0 last:pb-0 md:flex-row md:items-center md:justify-between md:gap-4"
147
+ className="group flex min-w-0 flex-col gap-3 border-b border-border pb-4 last:border-0 last:pb-0 md:flex-row md:items-center md:justify-between md:gap-4"
147
148
  >
148
149
  <div className="min-w-0 flex-1">
149
150
  <label className="block font-semibold">
@@ -151,6 +152,12 @@ export default function Page() {
151
152
  </label>
152
153
  <p className="text-muted-foreground text-sm wrap-break-word">
153
154
  {setting.description}
155
+ <Badge
156
+ variant="outline"
157
+ className="ml-1 align-middle font-mono text-xs opacity-0 transition-opacity group-hover:opacity-100"
158
+ >
159
+ {setting.slug}
160
+ </Badge>
154
161
  </p>
155
162
  </div>
156
163
 
@@ -18,7 +18,6 @@ import {
18
18
  DropdownMenuItem,
19
19
  DropdownMenuTrigger,
20
20
  } from '@/components/ui/dropdown-menu';
21
- import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
22
21
  import {
23
22
  Select,
24
23
  SelectContent,
@@ -30,7 +29,7 @@ import { cn } from '@/lib/utils';
30
29
  import { PaginatedResult } from '@/types/pagination-result';
31
30
  import { SettingGroup } from '@hed-hog/api-types';
32
31
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
33
- import { Download, MenuIcon, Upload } from 'lucide-react';
32
+ import { Check, ChevronDown, Download, MenuIcon, Upload } from 'lucide-react';
34
33
  import { useTranslations } from 'next-intl';
35
34
  import Link from 'next/link';
36
35
  import { usePathname, useRouter } from 'next/navigation';
@@ -71,7 +70,10 @@ export default function ConfigurationsLayout({ children }: IProps) {
71
70
  const [importDialogOpen, setImportDialogOpen] = useState(false);
72
71
  const [importData, setImportData] = useState<SettingsValidation | null>(null);
73
72
  const fileInputRef = useRef<HTMLInputElement>(null);
74
- const tabsContainerRef = useRef<HTMLDivElement>(null);
73
+ const navContainerRef = useRef<HTMLDivElement>(null);
74
+ const tabRefs = useRef<(HTMLAnchorElement | null)[]>([]);
75
+ const tabWidths = useRef<number[]>([]);
76
+ const [visibleCount, setVisibleCount] = useState(Infinity);
75
77
 
76
78
  const groups = settingGroups?.data || [];
77
79
  const activeGroup = groups.find(
@@ -79,16 +81,50 @@ export default function ConfigurationsLayout({ children }: IProps) {
79
81
  );
80
82
 
81
83
  useEffect(() => {
82
- const activeTab = tabsContainerRef.current?.querySelector<HTMLElement>(
83
- '[data-active="true"]'
84
- );
84
+ const container = navContainerRef.current;
85
+ if (!container) return;
85
86
 
86
- activeTab?.scrollIntoView({
87
- behavior: 'smooth',
88
- inline: 'center',
89
- block: 'nearest',
90
- });
91
- }, [pathname, groups.length]);
87
+ const OVERFLOW_BTN_WIDTH = 96;
88
+ const GAP_WIDTH = 8;
89
+
90
+ const calculate = () => {
91
+ const available = container.clientWidth;
92
+
93
+ tabRefs.current.forEach((el, idx) => {
94
+ if (el && el.offsetWidth > 0) {
95
+ tabWidths.current[idx] = el.offsetWidth;
96
+ }
97
+ });
98
+
99
+ const total = groups.length;
100
+ if (total === 0) return;
101
+
102
+ let used = 0;
103
+ let count = 0;
104
+
105
+ for (let i = 0; i < total; i++) {
106
+ const w = (tabWidths.current[i] ?? 100) + (count > 0 ? GAP_WIDTH : 0);
107
+ const allFit = count + 1 === total;
108
+ const budget = allFit
109
+ ? available
110
+ : available - OVERFLOW_BTN_WIDTH - GAP_WIDTH;
111
+
112
+ if (used + w <= budget) {
113
+ used += w;
114
+ count++;
115
+ } else {
116
+ break;
117
+ }
118
+ }
119
+
120
+ setVisibleCount(count);
121
+ };
122
+
123
+ const observer = new ResizeObserver(calculate);
124
+ observer.observe(container);
125
+ calculate();
126
+ return () => observer.disconnect();
127
+ }, [groups.length]);
92
128
 
93
129
  const handleExport = (includeSecrets: boolean) => {
94
130
  request<Blob>({
@@ -170,6 +206,31 @@ export default function ConfigurationsLayout({ children }: IProps) {
170
206
  }
171
207
  }, [importData, request, refetch, showToastHandler, t]);
172
208
 
209
+ const activeIndex = groups.findIndex(
210
+ (item) => pathname === `/core/configurations/${item.slug}`
211
+ );
212
+
213
+ const rawVisible = isFinite(visibleCount)
214
+ ? Math.min(Math.max(0, visibleCount), groups.length)
215
+ : groups.length;
216
+
217
+ let visibleIdxs: number[];
218
+ if (rawVisible >= groups.length) {
219
+ visibleIdxs = groups.map((_, i) => i);
220
+ } else {
221
+ visibleIdxs = Array.from({ length: rawVisible }, (_, i) => i);
222
+ if (activeIndex >= rawVisible && activeIndex >= 0) {
223
+ visibleIdxs[rawVisible - 1] = activeIndex;
224
+ visibleIdxs.sort((a, b) => a - b);
225
+ }
226
+ }
227
+
228
+ const visibleSet = new Set(visibleIdxs);
229
+ const overflowIdxs = groups
230
+ .map((_, i) => i)
231
+ .filter((i) => !visibleSet.has(i));
232
+ const activeInOverflow = activeIndex >= 0 && !visibleSet.has(activeIndex);
233
+
173
234
  return (
174
235
  <div className="flex min-w-0 flex-col overflow-x-hidden px-4">
175
236
  <PageHeader
@@ -329,36 +390,93 @@ export default function ConfigurationsLayout({ children }: IProps) {
329
390
  </Select>
330
391
  </div>
331
392
 
332
- <div className="hidden min-w-0 overflow-x-hidden md:block">
333
- <ScrollArea className="w-full min-w-0" type="auto">
334
- <nav
335
- ref={tabsContainerRef}
336
- className="flex min-w-max gap-2"
337
- aria-label="Configuration tabs"
338
- >
339
- {groups.map((item: SettingGroup) => {
340
- const isActive =
341
- pathname === `/core/configurations/${item.slug}`;
393
+ <div className="hidden min-w-0 md:block">
394
+ <nav
395
+ ref={navContainerRef}
396
+ className="flex w-full items-end gap-2"
397
+ aria-label="Configuration tabs"
398
+ >
399
+ {groups.map((item: SettingGroup, idx: number) => {
400
+ const isActive = idx === activeIndex;
401
+ const isOverflow = !visibleSet.has(idx);
342
402
 
403
+ if (isOverflow) {
343
404
  return (
344
- <Link
405
+ <span
345
406
  key={item.slug}
346
- href={item.slug}
347
- data-active={isActive}
407
+ ref={(el) => {
408
+ tabRefs.current[idx] =
409
+ el as unknown as HTMLAnchorElement;
410
+ }}
411
+ aria-hidden="true"
412
+ className="invisible absolute shrink-0 whitespace-nowrap px-3 py-3 text-sm font-medium"
413
+ >
414
+ {item.name}
415
+ </span>
416
+ );
417
+ }
418
+
419
+ return (
420
+ <Link
421
+ key={item.slug}
422
+ href={item.slug}
423
+ ref={(el) => {
424
+ tabRefs.current[idx] = el;
425
+ }}
426
+ data-active={isActive}
427
+ className={cn(
428
+ 'shrink-0 border-b-2 px-3 py-3 text-sm font-medium whitespace-nowrap transition-colors',
429
+ isActive
430
+ ? 'border-primary text-primary'
431
+ : 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground'
432
+ )}
433
+ >
434
+ {item.name}
435
+ </Link>
436
+ );
437
+ })}
438
+
439
+ {overflowIdxs.length > 0 && (
440
+ <DropdownMenu>
441
+ <DropdownMenuTrigger asChild>
442
+ <Button
443
+ variant="ghost"
348
444
  className={cn(
349
- 'shrink-0 border-b-2 px-3 py-3 text-sm font-medium whitespace-nowrap transition-colors',
350
- isActive
445
+ 'ml-auto h-auto shrink-0 cursor-pointer rounded-none border-b-2 px-3 py-3 text-sm font-medium whitespace-nowrap transition-colors',
446
+ activeInOverflow
351
447
  ? 'border-primary text-primary'
352
448
  : 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground'
353
449
  )}
354
450
  >
355
- {item.name}
356
- </Link>
357
- );
358
- })}
359
- </nav>
360
- <ScrollBar orientation="horizontal" />
361
- </ScrollArea>
451
+ {overflowIdxs.length} mais
452
+ <ChevronDown className="ml-1 h-3 w-3" />
453
+ </Button>
454
+ </DropdownMenuTrigger>
455
+ <DropdownMenuContent align="end">
456
+ {overflowIdxs.map((idx) => {
457
+ const item = groups[idx];
458
+ if (!item) return null;
459
+ const isActive = idx === activeIndex;
460
+ return (
461
+ <DropdownMenuItem
462
+ key={item.slug}
463
+ onClick={() =>
464
+ router.push(`/core/configurations/${item.slug}`)
465
+ }
466
+ className={cn(
467
+ 'cursor-pointer',
468
+ isActive && 'text-primary'
469
+ )}
470
+ >
471
+ {item.name}
472
+ {isActive && <Check className="ml-auto h-4 w-4" />}
473
+ </DropdownMenuItem>
474
+ );
475
+ })}
476
+ </DropdownMenuContent>
477
+ </DropdownMenu>
478
+ )}
479
+ </nav>
362
480
  </div>
363
481
  </div>
364
482
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.327",
3
+ "version": "0.0.328",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -30,12 +30,12 @@
30
30
  "sharp": "^0.34.2",
31
31
  "speakeasy": "^2.0.0",
32
32
  "uuid": "^11.1.0",
33
- "@hed-hog/api": "0.0.8",
34
- "@hed-hog/api-types": "0.0.1",
35
33
  "@hed-hog/api-locale": "0.0.14",
34
+ "@hed-hog/api": "0.0.8",
35
+ "@hed-hog/api-pagination": "0.0.7",
36
36
  "@hed-hog/api-prisma": "0.0.6",
37
- "@hed-hog/api-mail": "0.0.9",
38
- "@hed-hog/api-pagination": "0.0.7"
37
+ "@hed-hog/api-types": "0.0.1",
38
+ "@hed-hog/api-mail": "0.0.9"
39
39
  },
40
40
  "exports": {
41
41
  ".": {