@dtcode/ui 0.3.0 → 0.4.0

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/dist/sheet.js ADDED
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as RadixDialog from "@radix-ui/react-dialog";
4
+ import { cn } from "./cn.js";
5
+ import { overlayClasses } from "./dialog.js";
6
+ import { Icon } from "./icon.js";
7
+ export const Sheet = RadixDialog.Root;
8
+ export const SheetTrigger = RadixDialog.Trigger;
9
+ export const SheetClose = RadixDialog.Close;
10
+ /** Painel lateral à direita (até 520px) para ver ou editar um item sem sair da lista. */
11
+ export function SheetContent({ title, description, footer, className, children, ...props }) {
12
+ return (_jsxs(RadixDialog.Portal, { children: [_jsx(RadixDialog.Overlay, { className: overlayClasses }), _jsxs(RadixDialog.Content, { ...(description ? {} : { "aria-describedby": undefined }), className: cn("fixed inset-y-0 right-0 z-50 flex w-full max-w-[520px] animate-slide-in-right flex-col border-l border-border bg-surface text-fg shadow-4 focus:outline-none", className), ...props, children: [_jsxs("div", { className: "flex items-start gap-3 border-b border-border px-6 py-5", children: [_jsxs("div", { className: "grid min-w-0 flex-1 gap-1", children: [_jsx(RadixDialog.Title, { className: "font-display text-lg leading-snug font-semibold", children: title }), description && _jsx(RadixDialog.Description, { className: "text-sm text-fg-2", children: description })] }), _jsx(RadixDialog.Close, { "aria-label": "Fechar", className: "-mt-1 -mr-2 grid size-9 flex-none place-items-center rounded-md text-muted transition-colors hover:bg-surface-subtle hover:text-fg", children: _jsx(Icon, { name: "close", size: 20 }) })] }), _jsx("div", { className: "grid flex-1 content-start gap-7 overflow-y-auto px-6 py-6", children: children }), footer && _jsx("div", { className: "flex flex-wrap justify-end gap-2 border-t border-border px-6 py-4", children: footer })] })] }));
13
+ }
14
+ /** Bloco com título dentro do painel. */
15
+ export function SheetSection({ title, className, children }) {
16
+ return (_jsxs("section", { className: cn("grid gap-3", className), children: [_jsx("h3", { className: "font-display text-xs font-semibold tracking-[.1em] text-fg-2 uppercase", children: title }), children] }));
17
+ }
18
+ /** Lista de rótulo e valor (ex.: dados de um registro). */
19
+ export function DefinitionList({ items, className }) {
20
+ return (_jsx("dl", { className: cn("grid grid-cols-[minmax(0,140px)_minmax(0,1fr)] gap-x-4 gap-y-2.5 text-sm", className), children: items.map((item, i) => (_jsxs("div", { className: "contents", children: [_jsx("dt", { className: "text-fg-2", children: item.label }), _jsx("dd", { className: "min-w-0 break-words text-fg", children: item.value })] }, i))) }));
21
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Bloco de carregamento no formato do conteúdo (listas, tabelas, números).
3
+ * Use quando a estrutura da tela já é conhecida; para uma ação em andamento, use o Loader.
4
+ */
5
+ export declare function Skeleton({ className, ...props }: React.ComponentProps<"div">): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "./cn.js";
3
+ /**
4
+ * Bloco de carregamento no formato do conteúdo (listas, tabelas, números).
5
+ * Use quando a estrutura da tela já é conhecida; para uma ação em andamento, use o Loader.
6
+ */
7
+ export function Skeleton({ className, ...props }) {
8
+ return _jsx("div", { "aria-hidden": true, className: cn("h-4 animate-pulse rounded-md bg-border", className), ...props });
9
+ }
package/dist/styles.css CHANGED
@@ -263,7 +263,7 @@
263
263
 
264
264
  /* Base do Design System dtcode: tema escuro, fontes, movimento, estilos de base e ícones.
265
265
  Vai no dist/styles.css do pacote @dtcode/ui, no globals.css do site e no ds.css do kit para copiar (scripts/build-templates.mjs).
266
- Só entra aqui o que os componentes do pacote precisam; estilos do app ficam em globals.css. */
266
+ Só entra aqui o que os componentes e as classes utilitárias do pacote precisam; estilos do app ficam em globals.css. */
267
267
 
268
268
  @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
269
269
 
@@ -278,6 +278,7 @@
278
278
  --animate-fade-up: fade-up 0.28s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
279
279
  --animate-scale-in: scale-in 0.18s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
280
280
  --animate-slide-in-left: slide-in-left 0.28s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
281
+ --animate-slide-in-right: slide-in-right 0.28s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
281
282
  --animate-pop: pop 0.32s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
282
283
  --animate-spin-in: spin-in 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
283
284
  --animate-shake: shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) backwards;
@@ -296,6 +297,9 @@
296
297
  @keyframes slide-in-left {
297
298
  from { translate: -100% 0; }
298
299
  }
300
+ @keyframes slide-in-right {
301
+ from { translate: 100% 0; }
302
+ }
299
303
  @keyframes pop {
300
304
  0% { scale: 0.92; opacity: 0.6; }
301
305
  60% { scale: 1.03; opacity: 1; }
@@ -353,6 +357,71 @@
353
357
  user-select: none;
354
358
  flex: none;
355
359
  }
360
+
361
+ /* Rótulo de seção — padrão das páginas do MIV: RÓTULO ——— */
362
+ .eyebrow {
363
+ display: flex;
364
+ align-items: center;
365
+ gap: 0.875rem;
366
+ font: 600 0.75rem/1 var(--font-display);
367
+ letter-spacing: 0.12em;
368
+ text-transform: uppercase;
369
+ color: var(--fg-2);
370
+ }
371
+ .eyebrow::after {
372
+ content: "";
373
+ flex: 1;
374
+ height: 1px;
375
+ background: var(--border-strong);
376
+ }
377
+
378
+ .text-gradient {
379
+ background: var(--grad-marca);
380
+ -webkit-background-clip: text;
381
+ background-clip: text;
382
+ color: transparent;
383
+ }
384
+
385
+ /* Rolagem sobre fundo escuro (sidebar): quase invisível, aparece ao passar o mouse */
386
+ .scrollbar-dark {
387
+ scrollbar-width: thin;
388
+ scrollbar-color: transparent transparent;
389
+ transition: scrollbar-color 0.2s;
390
+ }
391
+ .scrollbar-dark:hover,
392
+ .scrollbar-dark:focus-within {
393
+ scrollbar-color: rgb(255 255 255 / 0.22) transparent;
394
+ }
395
+ .scrollbar-dark::-webkit-scrollbar-thumb {
396
+ background-color: transparent;
397
+ }
398
+ .scrollbar-dark:hover::-webkit-scrollbar-thumb {
399
+ background-color: rgb(255 255 255 / 0.22);
400
+ }
401
+
402
+ /* Entrada em cascata dos filhos diretos */
403
+ .stagger > * {
404
+ animation: var(--animate-fade-up);
405
+ }
406
+ .stagger > :nth-child(2) { animation-delay: 25ms; }
407
+ .stagger > :nth-child(3) { animation-delay: 50ms; }
408
+ .stagger > :nth-child(4) { animation-delay: 75ms; }
409
+ .stagger > :nth-child(5) { animation-delay: 100ms; }
410
+ .stagger > :nth-child(6) { animation-delay: 125ms; }
411
+ .stagger > :nth-child(n + 7) { animation-delay: 150ms; }
412
+
413
+ .bg-hero {
414
+ background: var(--grad-hero);
415
+ }
416
+
417
+ .bg-dots {
418
+ background-image: radial-gradient(var(--canvas-dot) 1px, transparent 1.2px);
419
+ background-size: 16px 16px;
420
+ }
421
+ .bg-dots-light {
422
+ background-image: radial-gradient(rgb(255 255 255 / 0.14) 1px, transparent 1.2px);
423
+ background-size: 18px 18px;
424
+ }
356
425
  }
357
426
 
358
427
  @media (prefers-reduced-motion: reduce) {
@@ -362,3 +431,228 @@
362
431
  transition-duration: 0.01ms !important;
363
432
  }
364
433
  }
434
+
435
+ /* Símbolo animado (AnimatedSymbol): montagem, brilho, partículas e parallax. Vai no dist/styles.css. */
436
+
437
+ @layer components {
438
+ /*
439
+ * Símbolo animado (tela de login) — componente AnimatedSymbol do pacote.
440
+ * Ordem dos <path> no SVG oficial: losango de cima, do meio, de baixo e o chevron.
441
+ * Linha do tempo: 0–1,3 s peças voam de longe (desfocadas, girando) e se encaixam · ~1,3 s impacto
442
+ * (ondas em losango + aura + halo) · 2 s chevron pisca como cursor de terminal · 2 s+ flutuação, brilho
443
+ * pulsando, reflexo de luz, balanço de cada peça e partículas em losango subindo.
444
+ * Hover: visão explodida (as peças se afastam um pouco; --spread anima de 0 a 1).
445
+ * Parallax: o JS do componente define --px/--py (-1..1) conforme o ponteiro; a inclinação 3D usa
446
+ * `transform` no palco e o deslocamento por peça usa `transform` nos <path>, que se soma às
447
+ * propriedades `translate`/`rotate`/`scale` usadas pelas animações.
448
+ */
449
+ .symbol-stage {
450
+ --px: 0;
451
+ --py: 0;
452
+ transition: --spread 0.6s cubic-bezier(0.22, 1.3, 0.36, 1);
453
+ }
454
+ .symbol-stage:hover {
455
+ --spread: 1;
456
+ }
457
+ .symbol-tilt {
458
+ transform: perspective(800px) rotateX(calc(var(--py) * -14deg)) rotateY(calc(var(--px) * 16deg));
459
+ transform-style: preserve-3d;
460
+ }
461
+
462
+ .symbol-live {
463
+ overflow: visible;
464
+ animation:
465
+ symbol-float 5s ease-in-out 2s infinite,
466
+ symbol-glow 5s ease-in-out 1.3s infinite;
467
+ }
468
+ .symbol-live path {
469
+ transform-box: fill-box;
470
+ transform-origin: center;
471
+ }
472
+ /* unidades em px aqui são do viewBox do SVG (350 de largura) */
473
+ .symbol-live path:nth-of-type(1) {
474
+ --from: -210% -300%;
475
+ --spin: -150deg;
476
+ transform: translate(calc(var(--px) * -18px), calc(var(--py) * -14px - var(--spread) * 22px));
477
+ animation:
478
+ piece-assemble 1.25s cubic-bezier(0.22, 1.25, 0.36, 1) backwards,
479
+ piece-bob 3.4s ease-in-out 2s infinite;
480
+ }
481
+ .symbol-live path:nth-of-type(2) {
482
+ --from: 360% -80%;
483
+ --spin: 170deg;
484
+ transform: translate(calc(var(--px) * 26px + var(--spread) * 24px), calc(var(--py) * 10px));
485
+ animation:
486
+ piece-assemble 1.25s cubic-bezier(0.22, 1.25, 0.36, 1) 0.15s backwards,
487
+ piece-bob 3.9s ease-in-out 2.3s infinite;
488
+ }
489
+ .symbol-live path:nth-of-type(3) {
490
+ --from: -190% 320%;
491
+ --spin: 130deg;
492
+ transform: translate(calc(var(--px) * -10px), calc(var(--py) * 18px + var(--spread) * 22px));
493
+ animation:
494
+ piece-assemble 1.25s cubic-bezier(0.22, 1.25, 0.36, 1) 0.3s backwards,
495
+ piece-bob 3.6s ease-in-out 2.6s infinite;
496
+ }
497
+ .symbol-live path:nth-of-type(4) {
498
+ --from: -700% 0;
499
+ --spin: 0deg;
500
+ transform: translate(calc(var(--px) * 34px - var(--spread) * 30px), 0);
501
+ animation:
502
+ piece-assemble 0.85s cubic-bezier(0.22, 1.5, 0.36, 1) 0.85s backwards,
503
+ chevron-blink 0.9s steps(1) 1.9s 2,
504
+ chevron-nudge 4s ease-in-out 3.8s infinite;
505
+ }
506
+
507
+ /* Reflexo de luz: cópia branca do símbolo, revelada por uma faixa de máscara que atravessa as peças */
508
+ .symbol-live.symbol-shine {
509
+ mix-blend-mode: overlay;
510
+ -webkit-mask-image: linear-gradient(115deg, transparent 42%, #000 50%, transparent 58%);
511
+ mask-image: linear-gradient(115deg, transparent 42%, #000 50%, transparent 58%);
512
+ -webkit-mask-size: 300% 100%;
513
+ mask-size: 300% 100%;
514
+ -webkit-mask-repeat: no-repeat;
515
+ mask-repeat: no-repeat;
516
+ -webkit-mask-position: 130% 0;
517
+ mask-position: 130% 0;
518
+ animation:
519
+ symbol-float 5s ease-in-out 2s infinite,
520
+ shine-sweep 5.5s ease-in-out 2.2s infinite;
521
+ }
522
+
523
+ /* Ondas em losango no momento do encaixe */
524
+ .symbol-ring {
525
+ position: absolute;
526
+ width: 58%;
527
+ aspect-ratio: 1;
528
+ border: 2px solid rgb(255 145 153 / 0.75);
529
+ border-radius: 26%;
530
+ rotate: 45deg;
531
+ opacity: 0;
532
+ pointer-events: none;
533
+ animation: ring-burst 1.2s cubic-bezier(0.2, 0.7, 0.2, 1) 1.2s both;
534
+ }
535
+ .symbol-ring--late {
536
+ border-color: rgb(239 47 177 / 0.55);
537
+ animation-delay: 1.42s;
538
+ }
539
+
540
+ /* Halo de luz atrás do símbolo: dá contraste ao chevron roxo sobre o fundo escuro */
541
+ .symbol-halo {
542
+ position: absolute;
543
+ width: 78%;
544
+ aspect-ratio: 1;
545
+ border-radius: 50%;
546
+ background: radial-gradient(circle, rgb(255 236 246 / 0.55), rgb(255 145 153 / 0.18) 45%, transparent 70%);
547
+ filter: blur(18px);
548
+ opacity: 0;
549
+ pointer-events: none;
550
+ animation:
551
+ aurora-in 1.4s ease-out 1.15s both,
552
+ halo-breathe 5s ease-in-out 2.6s infinite;
553
+ }
554
+
555
+ /* Partículas em losango (motivo da marca) subindo devagar em volta do símbolo */
556
+ .symbol-particles {
557
+ position: absolute;
558
+ inset: -35% -45%;
559
+ pointer-events: none;
560
+ }
561
+ .symbol-particles span {
562
+ position: absolute;
563
+ left: var(--x);
564
+ bottom: 18%;
565
+ width: var(--s);
566
+ aspect-ratio: 1;
567
+ border-radius: 28%;
568
+ rotate: 45deg;
569
+ background: linear-gradient(135deg, #ff9199, #ef2fb1);
570
+ box-shadow: 0 0 12px rgb(239 47 177 / 0.6);
571
+ opacity: 0;
572
+ animation: particle-rise var(--d) ease-in-out var(--delay) infinite;
573
+ }
574
+
575
+ /* Aura colorida atrás do símbolo: acende no impacto e gira devagar */
576
+ .symbol-aurora {
577
+ position: absolute;
578
+ width: 150%;
579
+ aspect-ratio: 1;
580
+ border-radius: 50%;
581
+ background: conic-gradient(from 0deg, #ef2fb1, #6a3dba, #ff9199, #3d1883, #ef2fb1);
582
+ filter: blur(70px);
583
+ opacity: 0;
584
+ pointer-events: none;
585
+ animation:
586
+ aurora-in 1.6s ease-out 1.1s both,
587
+ aurora-spin 16s linear infinite;
588
+ }
589
+
590
+ @keyframes piece-assemble {
591
+ from {
592
+ translate: var(--from);
593
+ rotate: var(--spin);
594
+ scale: 0.3;
595
+ opacity: 0;
596
+ filter: blur(14px);
597
+ }
598
+ 30% {
599
+ opacity: 1;
600
+ }
601
+ 70% {
602
+ filter: blur(0);
603
+ }
604
+ }
605
+ @keyframes piece-bob {
606
+ 0%, 100% { translate: 0 0; }
607
+ 50% { translate: 0 -7%; }
608
+ }
609
+ @keyframes chevron-blink {
610
+ 0% { opacity: 0.15; }
611
+ 50% { opacity: 1; }
612
+ }
613
+ @keyframes halo-breathe {
614
+ 0%, 100% { opacity: 0.5; scale: 1; }
615
+ 50% { opacity: 0.8; scale: 1.08; }
616
+ }
617
+ @keyframes particle-rise {
618
+ 0% { translate: 0 0; opacity: 0; scale: 0.4; }
619
+ 15% { opacity: var(--o, 0.85); scale: 1; }
620
+ 75% { opacity: calc(var(--o, 0.85) * 0.6); }
621
+ 100% { translate: var(--drift) -240px; opacity: 0; scale: 0.6; }
622
+ }
623
+ @keyframes chevron-nudge {
624
+ 0%, 55%, 100% { translate: 0 0; }
625
+ 25% { translate: 30% 0; }
626
+ }
627
+ @keyframes symbol-float {
628
+ 0%, 100% { translate: 0 0; }
629
+ 50% { translate: 0 -16px; }
630
+ }
631
+ @keyframes symbol-glow {
632
+ 0%, 100% { filter: drop-shadow(0 18px 36px rgb(239 47 177 / 0.35)); }
633
+ 50% { filter: drop-shadow(0 26px 60px rgb(239 47 177 / 0.65)); }
634
+ }
635
+ @keyframes shine-sweep {
636
+ 0% { -webkit-mask-position: 130% 0; mask-position: 130% 0; }
637
+ 40%, 100% { -webkit-mask-position: -30% 0; mask-position: -30% 0; }
638
+ }
639
+ @keyframes ring-burst {
640
+ 0% { opacity: 0; scale: 0.45; }
641
+ 8% { opacity: 1; }
642
+ 100% { opacity: 0; scale: 2.6; }
643
+ }
644
+ @keyframes aurora-in {
645
+ from { opacity: 0; scale: 0.3; }
646
+ to { opacity: 0.5; scale: 1; }
647
+ }
648
+ @keyframes aurora-spin {
649
+ to { rotate: 360deg; }
650
+ }
651
+ }
652
+
653
+ /* Permite animar --spread (visão explodida do símbolo do login) com transição */
654
+ @property --spread {
655
+ syntax: "<number>";
656
+ inherits: true;
657
+ initial-value: 0;
658
+ }
@@ -0,0 +1,7 @@
1
+ /** Tabela de dados. A moldura rola na horizontal quando falta espaço. */
2
+ export declare function Table({ className, ...props }: React.ComponentProps<"table">): import("react").JSX.Element;
3
+ export declare function TableHeader({ className, ...props }: React.ComponentProps<"thead">): import("react").JSX.Element;
4
+ export declare function TableBody({ className, ...props }: React.ComponentProps<"tbody">): import("react").JSX.Element;
5
+ export declare function TableRow({ className, ...props }: React.ComponentProps<"tr">): import("react").JSX.Element;
6
+ export declare function TableHead({ className, ...props }: React.ComponentProps<"th">): import("react").JSX.Element;
7
+ export declare function TableCell({ className, ...props }: React.ComponentProps<"td">): import("react").JSX.Element;
package/dist/table.js ADDED
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "./cn.js";
3
+ /** Tabela de dados. A moldura rola na horizontal quando falta espaço. */
4
+ export function Table({ className, ...props }) {
5
+ return (_jsx("div", { className: "w-full overflow-x-auto rounded-lg border border-border bg-surface", children: _jsx("table", { className: cn("w-full border-collapse text-left text-sm", className), ...props }) }));
6
+ }
7
+ export function TableHeader({ className, ...props }) {
8
+ return _jsx("thead", { className: cn("border-b border-border bg-surface-subtle", className), ...props });
9
+ }
10
+ export function TableBody({ className, ...props }) {
11
+ return _jsx("tbody", { className: cn("[&>tr:not(:last-child)]:border-b [&>tr:not(:last-child)]:border-border", className), ...props });
12
+ }
13
+ export function TableRow({ className, ...props }) {
14
+ return _jsx("tr", { className: cn("transition-colors [tbody>&]:hover:bg-surface-subtle", className), ...props });
15
+ }
16
+ export function TableHead({ className, ...props }) {
17
+ return _jsx("th", { scope: "col", className: cn("px-4 py-3 font-display text-xs font-semibold tracking-[.06em] whitespace-nowrap text-fg-2 uppercase", className), ...props });
18
+ }
19
+ export function TableCell({ className, ...props }) {
20
+ return _jsx("td", { className: cn("px-4 py-3 align-middle text-fg", className), ...props });
21
+ }
@@ -0,0 +1,5 @@
1
+ export type TextareaProps = React.ComponentProps<"textarea"> & {
2
+ invalid?: boolean;
3
+ };
4
+ /** Campo de texto longo, com o visual do Input. Use dentro de <Field> para rótulo, dica e erro. */
5
+ export declare function Textarea({ invalid, className, rows, ...props }: TextareaProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "./cn.js";
3
+ import { controlClasses } from "./input.js";
4
+ /** Campo de texto longo, com o visual do Input. Use dentro de <Field> para rótulo, dica e erro. */
5
+ export function Textarea({ invalid, className, rows = 4, ...props }) {
6
+ return (_jsx("textarea", { rows: rows, "aria-invalid": invalid || undefined, className: controlClasses({ invalid, className: cn("min-h-20 resize-y py-2.5 leading-relaxed", className) }), ...props }));
7
+ }
@@ -0,0 +1,11 @@
1
+ export type Theme = "light" | "dark" | "system";
2
+ export declare const DARK = "(prefers-color-scheme: dark)";
3
+ /**
4
+ * Script para o <head>, antes de qualquer CSS: aplica o tema salvo antes da primeira pintura
5
+ * (sem piscar o tema errado). No Next.js, use <script dangerouslySetInnerHTML={{ __html: themeScript() }} />;
6
+ * no Vite, cole o retorno num <script> do index.html.
7
+ */
8
+ export declare function themeScript({ defaultTheme, storageKey }?: {
9
+ defaultTheme?: Theme;
10
+ storageKey?: string;
11
+ }): string;
@@ -0,0 +1,10 @@
1
+ // Fora do arquivo "use client" do tema: o script é montado no servidor (layout do Next.js) ou no build.
2
+ export const DARK = "(prefers-color-scheme: dark)";
3
+ /**
4
+ * Script para o <head>, antes de qualquer CSS: aplica o tema salvo antes da primeira pintura
5
+ * (sem piscar o tema errado). No Next.js, use <script dangerouslySetInnerHTML={{ __html: themeScript() }} />;
6
+ * no Vite, cole o retorno num <script> do index.html.
7
+ */
8
+ export function themeScript({ defaultTheme = "light", storageKey = "theme" } = {}) {
9
+ return `(function(){try{var t=localStorage.getItem(${JSON.stringify(storageKey)});if(t!=="light"&&t!=="dark"&&t!=="system")t=${JSON.stringify(defaultTheme)};if(t==="system")t=matchMedia(${JSON.stringify(DARK)}).matches?"dark":"light";var r=document.documentElement;r.dataset.theme=t;r.style.colorScheme=t}catch(e){}})()`;
10
+ }
@@ -0,0 +1,24 @@
1
+ import { type Theme } from "./theme-script.js";
2
+ export type { Theme };
3
+ type Resolved = "light" | "dark";
4
+ type ThemeApi = {
5
+ /** Escolha salva. undefined até o navegador ler a preferência (no servidor e na hidratação). */
6
+ theme: Theme | undefined;
7
+ /** Tema aplicado de fato ("system" vira light ou dark). */
8
+ resolvedTheme: Resolved | undefined;
9
+ setTheme: (theme: Theme) => void;
10
+ };
11
+ /**
12
+ * Tema claro/escuro para qualquer app React (Next.js, Vite…). Grava a escolha no localStorage e
13
+ * aplica data-theme no <html>, que é o que os tokens do styles.css usam.
14
+ */
15
+ export declare function ThemeProvider({ children, defaultTheme, storageKey, disableTransitionOnChange, }: {
16
+ children: React.ReactNode;
17
+ defaultTheme?: Theme;
18
+ storageKey?: string;
19
+ /** Desliga as transições no instante da troca, para nada animar de uma cor para a outra. */
20
+ disableTransitionOnChange?: boolean;
21
+ }): import("react").JSX.Element;
22
+ export declare function useTheme(): ThemeApi;
23
+ /** Botão que alterna entre claro e escuro. */
24
+ export declare function ThemeToggle({ className, ...props }: Omit<React.ComponentProps<"button">, "onClick" | "children">): import("react").JSX.Element;
package/dist/theme.js ADDED
@@ -0,0 +1,91 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
4
+ import { cn } from "./cn.js";
5
+ import { Icon } from "./icon.js";
6
+ import { DARK } from "./theme-script.js";
7
+ const ThemeContext = createContext(null);
8
+ // Avisa as outras instâncias na mesma aba (o evento "storage" só chega às outras abas).
9
+ const CHANGE = "dtcode-theme-change";
10
+ function applyTheme(resolved, disableTransition) {
11
+ const root = document.documentElement;
12
+ if (root.dataset.theme === resolved)
13
+ return;
14
+ let style;
15
+ if (disableTransition) {
16
+ style = document.createElement("style");
17
+ style.textContent = "*,*::before,*::after{transition:none!important}";
18
+ document.head.appendChild(style);
19
+ }
20
+ root.dataset.theme = resolved;
21
+ root.style.colorScheme = resolved;
22
+ if (style) {
23
+ void getComputedStyle(root).opacity; // força o recálculo antes de devolver as transições
24
+ const el = style;
25
+ setTimeout(() => el.remove(), 1);
26
+ }
27
+ }
28
+ const subscribeSystem = (onChange) => {
29
+ const mq = matchMedia(DARK);
30
+ mq.addEventListener("change", onChange);
31
+ return () => mq.removeEventListener("change", onChange);
32
+ };
33
+ /**
34
+ * Tema claro/escuro para qualquer app React (Next.js, Vite…). Grava a escolha no localStorage e
35
+ * aplica data-theme no <html>, que é o que os tokens do styles.css usam.
36
+ */
37
+ export function ThemeProvider({ children, defaultTheme = "light", storageKey = "theme", disableTransitionOnChange = true, }) {
38
+ const subscribeStored = useCallback((onChange) => {
39
+ const onStorage = (e) => {
40
+ if (e.key === storageKey)
41
+ onChange();
42
+ };
43
+ window.addEventListener("storage", onStorage);
44
+ window.addEventListener(CHANGE, onChange);
45
+ return () => {
46
+ window.removeEventListener("storage", onStorage);
47
+ window.removeEventListener(CHANGE, onChange);
48
+ };
49
+ }, [storageKey]);
50
+ const readStored = useCallback(() => {
51
+ try {
52
+ return localStorage.getItem(storageKey);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }, [storageKey]);
58
+ const stored = useSyncExternalStore(subscribeStored, readStored, () => undefined);
59
+ const systemDark = useSyncExternalStore(subscribeSystem, () => matchMedia(DARK).matches, () => undefined);
60
+ const theme = stored === undefined ? undefined : stored === "light" || stored === "dark" || stored === "system" ? stored : defaultTheme;
61
+ const resolvedTheme = theme === "system" ? (systemDark ? "dark" : "light") : theme;
62
+ useEffect(() => {
63
+ if (resolvedTheme)
64
+ applyTheme(resolvedTheme, disableTransitionOnChange);
65
+ }, [resolvedTheme, disableTransitionOnChange]);
66
+ const setTheme = useCallback((next) => {
67
+ try {
68
+ localStorage.setItem(storageKey, next);
69
+ }
70
+ catch {
71
+ // Sem localStorage (modo privado restrito): aplica só nesta página.
72
+ applyTheme(next === "system" ? (matchMedia(DARK).matches ? "dark" : "light") : next, disableTransitionOnChange);
73
+ }
74
+ window.dispatchEvent(new Event(CHANGE));
75
+ }, [storageKey, disableTransitionOnChange]);
76
+ const api = useMemo(() => ({ theme, resolvedTheme, setTheme }), [theme, resolvedTheme, setTheme]);
77
+ return _jsx(ThemeContext.Provider, { value: api, children: children });
78
+ }
79
+ export function useTheme() {
80
+ const api = useContext(ThemeContext);
81
+ if (!api)
82
+ throw new Error("useTheme precisa estar dentro de <ThemeProvider>.");
83
+ return api;
84
+ }
85
+ /** Botão que alterna entre claro e escuro. */
86
+ export function ThemeToggle({ className, ...props }) {
87
+ const { resolvedTheme, setTheme } = useTheme();
88
+ const mounted = resolvedTheme !== undefined;
89
+ const dark = resolvedTheme === "dark";
90
+ return (_jsx("button", { type: "button", onClick: () => setTheme(dark ? "light" : "dark"), "aria-label": dark ? "Usar tema claro" : "Usar tema escuro", className: cn("grid size-9 place-items-center rounded-md border border-border bg-surface text-fg-2 transition-[color,border-color,scale] hover:border-border-strong hover:text-fg active:scale-90", className), ...props, children: _jsx(Icon, { name: dark ? "light_mode" : "dark_mode", size: 18, className: mounted ? "animate-spin-in" : undefined }, dark ? "sol" : "lua") }));
91
+ }
@@ -0,0 +1,24 @@
1
+ export type ToastTone = "success" | "info" | "danger";
2
+ export type ToastOptions = {
3
+ title: React.ReactNode;
4
+ description?: React.ReactNode;
5
+ tone?: ToastTone;
6
+ /** Botão de ação (ex.: Desfazer). Clicar fecha o aviso. */
7
+ action?: {
8
+ label: string;
9
+ onClick: () => void;
10
+ };
11
+ /** Tempo na tela em ms. Padrão: 5 s, ou 8 s quando há ação. Infinity mantém até fechar. */
12
+ duration?: number;
13
+ };
14
+ type ToastApi = {
15
+ toast: (options: ToastOptions) => number;
16
+ dismiss: (id: number) => void;
17
+ };
18
+ /** Avisos rápidos. Envolva o app uma vez e chame useToast() onde precisar. */
19
+ export declare function ToastProvider({ children }: {
20
+ children: React.ReactNode;
21
+ }): import("react").JSX.Element;
22
+ /** toast({ title, tone, action }) mostra um aviso; dismiss(id) fecha antes do tempo. */
23
+ export declare function useToast(): ToastApi;
24
+ export {};
package/dist/toast.js ADDED
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ import { cn } from "./cn.js";
5
+ import { Icon } from "./icon.js";
6
+ const ToastContext = createContext(null);
7
+ const tones = {
8
+ success: { icon: "check_circle", cls: "text-success" },
9
+ info: { icon: "info", cls: "text-info" },
10
+ danger: { icon: "error", cls: "text-danger" },
11
+ };
12
+ /** Avisos rápidos. Envolva o app uma vez e chame useToast() onde precisar. */
13
+ export function ToastProvider({ children }) {
14
+ const [items, setItems] = useState([]);
15
+ const next = useRef(0);
16
+ const dismiss = useCallback((id) => setItems((list) => list.filter((t) => t.id !== id)), []);
17
+ const toast = useCallback((options) => {
18
+ const id = ++next.current;
19
+ // No máximo 3 na tela: o mais antigo sai.
20
+ setItems((list) => [...list.slice(-2), { ...options, id }]);
21
+ return id;
22
+ }, []);
23
+ const api = useMemo(() => ({ toast, dismiss }), [toast, dismiss]);
24
+ return (_jsxs(ToastContext.Provider, { value: api, children: [children, _jsx("div", { role: "region", "aria-label": "Avisos", "aria-live": "polite", className: "pointer-events-none fixed right-4 bottom-4 z-[60] grid w-[min(380px,calc(100%-32px))] gap-2", children: items.map((t) => (_jsx(ToastCard, { item: t, dismiss: dismiss }, t.id))) })] }));
25
+ }
26
+ function ToastCard({ item, dismiss }) {
27
+ const tone = tones[item.tone ?? "success"];
28
+ const [paused, setPaused] = useState(false);
29
+ const duration = item.duration ?? (item.action ? 8000 : 5000);
30
+ const { id } = item;
31
+ const onDismiss = () => dismiss(id);
32
+ useEffect(() => {
33
+ if (paused || !Number.isFinite(duration))
34
+ return;
35
+ const timer = setTimeout(() => dismiss(id), duration);
36
+ return () => clearTimeout(timer);
37
+ }, [paused, duration, dismiss, id]);
38
+ return (_jsxs("div", { role: item.tone === "danger" ? "alert" : "status", onPointerEnter: () => setPaused(true), onPointerLeave: () => setPaused(false), onFocus: () => setPaused(true), onBlur: () => setPaused(false), className: "pointer-events-auto flex animate-fade-up items-start gap-3 rounded-lg border border-border bg-surface p-4 text-sm text-fg shadow-3", children: [_jsx(Icon, { name: tone.icon, size: 20, className: cn("mt-px", tone.cls) }), _jsxs("div", { className: "grid min-w-0 flex-1 gap-0.5", children: [_jsx("strong", { className: "font-semibold", children: item.title }), item.description && _jsx("span", { className: "text-fg-2", children: item.description })] }), item.action && (_jsx("button", { type: "button", onClick: () => {
39
+ item.action?.onClick();
40
+ onDismiss();
41
+ }, className: "flex-none rounded-md px-2 py-1 font-semibold text-link transition-colors hover:bg-surface-subtle", children: item.action.label })), _jsx("button", { type: "button", onClick: onDismiss, "aria-label": "Fechar aviso", className: "-m-1 grid size-7 flex-none place-items-center rounded-md text-muted hover:bg-surface-subtle hover:text-fg", children: _jsx(Icon, { name: "close", size: 18 }) })] }));
42
+ }
43
+ /** toast({ title, tone, action }) mostra um aviso; dismiss(id) fecha antes do tempo. */
44
+ export function useToast() {
45
+ const api = useContext(ToastContext);
46
+ if (!api)
47
+ throw new Error("useToast precisa estar dentro de <ToastProvider>.");
48
+ return api;
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtcode/ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Componentes React e tokens do Design System dtcode (Tailwind CSS v4).",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -32,6 +32,9 @@
32
32
  "tailwindcss": ">=4"
33
33
  },
34
34
  "dependencies": {
35
+ "@radix-ui/react-checkbox": "^1.4.1",
36
+ "@radix-ui/react-dialog": "^1.3.1",
37
+ "@radix-ui/react-dropdown-menu": "^2.2.1",
35
38
  "@radix-ui/react-switch": "^1.4.1",
36
39
  "@radix-ui/react-tabs": "^1.2.1",
37
40
  "clsx": "^2.1.1"