@assembleo/ui 0.1.0 → 0.2.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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ComponentProps, HTMLAttributes, ReactNode, ButtonHTMLAttributes, ComponentPropsWithoutRef, InputHTMLAttributes, TextareaHTMLAttributes, MouseEventHandler } from 'react';
2
+ import { ComponentProps, HTMLAttributes, ReactNode, ButtonHTMLAttributes, ComponentPropsWithoutRef, TextareaHTMLAttributes, MouseEventHandler, InputHTMLAttributes } from 'react';
3
3
  import * as class_variance_authority_types from 'class-variance-authority/types';
4
4
  import { VariantProps, cx } from 'class-variance-authority';
5
5
  import { Avatar as Avatar$1, Checkbox as Checkbox$1, Dialog as Dialog$1, Popover as Popover$1, RadioGroup as RadioGroup$1, Select as Select$1, Separator as Separator$1, Switch as Switch$1, Tabs as Tabs$1 } from 'radix-ui';
@@ -110,6 +110,44 @@ interface CheckboxProps extends Omit<ComponentProps<typeof Checkbox$1.Root>, 'cl
110
110
  }
111
111
  declare function Checkbox({ label, className, id, ...props }: CheckboxProps): react.JSX.Element;
112
112
 
113
+ type InputProps = ComponentProps<'input'>;
114
+ /**
115
+ * Os campos com máscara (CPF, WhatsApp, código) controlam `value` e `onChange` e
116
+ * fixam `type`, `inputMode` e `autoComplete`; o resto é do `Input`, `ref` incluído.
117
+ */
118
+ type MaskedInputProps = Omit<InputProps, 'value' | 'onChange' | 'type' | 'inputMode' | 'autoComplete'> & {
119
+ value: string;
120
+ /** Recebe a string já mascarada. */
121
+ onChange: (value: string) => void;
122
+ };
123
+ declare function Input({ className, id, type, ...props }: InputProps): react.JSX.Element;
124
+
125
+ type CodeInputProps = Omit<MaskedInputProps, 'maxLength'> & {
126
+ /** Chamado quando o último dígito entra — por digitação ou por colagem. */
127
+ onComplete?: (code: string) => void;
128
+ /** Quantidade de dígitos do código. */
129
+ length?: number;
130
+ };
131
+ /**
132
+ * Os dígitos do código, submetidos sozinhos.
133
+ *
134
+ * Um campo só, e não uma caixinha por dígito: colar é o caminho real. O botão do
135
+ * template de autenticação da Meta copia o código para a área de transferência, e
136
+ * quem usa o app instalado como PWA volta do WhatsApp e cola. Caixinhas quebram a
137
+ * colagem em navegador e atrapalham leitor de tela para não ganhar nada.
138
+ */
139
+ declare function CodeInput({ value, onChange, onComplete, length, className, ...props }: CodeInputProps): react.JSX.Element;
140
+
141
+ /** `000.000.000-00` enquanto se digita; para nos onze dígitos. */
142
+ declare function formatCpf(input: string): string;
143
+ /**
144
+ * Controle de CPF: só máscara. Não valida o dígito verificador, não se marca
145
+ * inválido e não anuncia nada — um CPF incompleto ainda não é um CPF errado.
146
+ * Quem usa valida, no envio ou no `onBlur` (repassado ao `<input>`, assim como o
147
+ * `ref`, para focar o campo), e entrega o erro ao `Field`, que o anuncia.
148
+ */
149
+ declare function CpfInput({ value, onChange, ...props }: MaskedInputProps): react.JSX.Element;
150
+
113
151
  /**
114
152
  * O modal de confirmação: uma pergunta, uma decisão. Radix cuida do portal,
115
153
  * do foco preso e do Escape; aqui entram só a pele e a composição.
@@ -215,11 +253,6 @@ interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>,
215
253
  }
216
254
  declare function IconButton({ label, variant, size, className, type, children, ...props }: IconButtonProps): react.JSX.Element;
217
255
 
218
- interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
219
- className?: string;
220
- }
221
- declare function Input({ className, id, type, ...props }: InputProps): react.JSX.Element;
222
-
223
256
  interface TextareaProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className'> {
224
257
  className?: string;
225
258
  }
@@ -483,13 +516,21 @@ interface TabsContentProps extends Omit<ComponentPropsWithoutRef<typeof Tabs$1.C
483
516
  declare function TabsContent({ className, children, ...props }: TabsContentProps): react.JSX.Element;
484
517
 
485
518
  type ToastVariant = 'default' | 'success' | 'danger';
519
+ interface ToastAction {
520
+ label: string;
521
+ onClick: () => void;
522
+ }
486
523
  interface ToastOptions {
487
524
  title: string;
488
525
  description?: string;
489
526
  variant?: ToastVariant;
527
+ /** Botão do aviso. Presente, o aviso vira acionável. */
528
+ action?: ToastAction;
529
+ /** Quanto o aviso fica na tela; `null` o mantém até o usuário fechar. Ausente, 5 s. */
530
+ durationMs?: number | null;
490
531
  }
491
532
  interface ToastContextValue {
492
- /** Dispara um aviso transitório; some sozinho depois de 5 segundos. */
533
+ /** Dispara um aviso; some sozinho em 5 segundos, salvo `durationMs`. */
493
534
  toast: (options: ToastOptions) => void;
494
535
  }
495
536
  /**
@@ -533,6 +574,17 @@ interface TooltipProps {
533
574
  */
534
575
  declare function Tooltip({ content, side, className, children }: TooltipProps): react.JSX.Element;
535
576
 
577
+ /**
578
+ * O número como o brasileiro escreve, enquanto ele digita.
579
+ *
580
+ * A máscara é do campo e a validação é de quem o usa: o momento de dizer "número
581
+ * inválido" é uma decisão da tela — no envio, e não a cada tecla, porque um número
582
+ * incompleto ainda não é um número errado.
583
+ */
584
+ declare function formatWhatsapp(input: string): string;
585
+ /** Controle de WhatsApp: vive dentro de um `Field`, que dá rótulo, dica e erro. */
586
+ declare function WhatsappInput({ value, onChange, ...props }: MaskedInputProps): react.JSX.Element;
587
+
536
588
  /**
537
589
  * O crachá — como um associado aparece na listagem.
538
590
  *
@@ -673,4 +725,4 @@ interface ThemeToggleProps {
673
725
  */
674
726
  declare function ThemeToggle({ width, className }: ThemeToggleProps): react.JSX.Element;
675
727
 
676
- export { Avatar, AvatarGroup, Badge, Banner, Button, Card, Checkbox, Cracha, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, EmptyState, Field, IconButton, Input, List, ListItem, Livreto, Popover, PopoverContent, PopoverTrigger, Predio, Progress, RadioGroup, RadioGroupItem, SearchField, SegmentedControl, type SegmentedControlOption, Select, SelectItem, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Spinner, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type Theme, ThemeProvider, ThemeToggle, type ToastOptions, ToastProvider, type ToastVariant, Tooltip, VisuallyHidden, cn, initTheme, useFieldContext, useTheme, useToast };
728
+ export { Avatar, AvatarGroup, Badge, Banner, Button, Card, Checkbox, CodeInput, CpfInput, Cracha, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, EmptyState, Field, IconButton, Input, List, ListItem, Livreto, Popover, PopoverContent, PopoverTrigger, Predio, Progress, RadioGroup, RadioGroupItem, SearchField, SegmentedControl, type SegmentedControlOption, Select, SelectItem, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Spinner, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type Theme, ThemeProvider, ThemeToggle, type ToastAction, type ToastOptions, ToastProvider, type ToastVariant, Tooltip, VisuallyHidden, WhatsappInput, cn, formatCpf, formatWhatsapp, initTheme, useFieldContext, useTheme, useToast };
package/dist/index.js CHANGED
@@ -483,6 +483,90 @@ function Checkbox({ label, className, id, ...props }) {
483
483
  /* @__PURE__ */ jsx("span", { className: "text-body text-text", children: label })
484
484
  ] });
485
485
  }
486
+
487
+ // src/lib/digitos.ts
488
+ function digitos(input, max) {
489
+ const apenasDigitos = input.replace(/\D/g, "");
490
+ return max === void 0 ? apenasDigitos : apenasDigitos.slice(0, max);
491
+ }
492
+ var fieldControlClasses = [
493
+ "w-full rounded-control squircle",
494
+ "bg-surface-sunken text-text text-body",
495
+ "placeholder:text-text-muted",
496
+ "border border-transparent",
497
+ "transition-[border-color,box-shadow] duration-(--motion-quick) ease-(--ease-out-soft)",
498
+ "outline-none focus-visible:outline-none",
499
+ "focus:border-accent focus:ring-4 focus:ring-accent/15",
500
+ "aria-invalid:border-danger aria-invalid:ring-4 aria-invalid:ring-danger/15",
501
+ "disabled:pointer-events-none disabled:opacity-50"
502
+ ].join(" ");
503
+ function Input({ className, id, type, ...props }) {
504
+ const field = useFieldContext();
505
+ return /* @__PURE__ */ jsx(
506
+ "input",
507
+ {
508
+ id: id ?? field?.controlId,
509
+ type: type ?? "text",
510
+ "aria-invalid": field?.invalid || void 0,
511
+ "aria-describedby": field ? [field.errorId, field.hintId].filter(Boolean).join(" ") || void 0 : void 0,
512
+ className: cn(fieldControlClasses, "min-h-touch px-4", className),
513
+ ...props
514
+ }
515
+ );
516
+ }
517
+ function CodeInput({
518
+ value,
519
+ onChange,
520
+ onComplete,
521
+ length = 6,
522
+ className,
523
+ ...props
524
+ }) {
525
+ return /* @__PURE__ */ jsx(
526
+ Input,
527
+ {
528
+ ...props,
529
+ type: "text",
530
+ inputMode: "numeric",
531
+ autoComplete: "one-time-code",
532
+ value,
533
+ className: cn("text-center font-mono text-title-2 tracking-codigo", className),
534
+ onChange: (event) => {
535
+ const codigo = digitos(event.target.value, length);
536
+ onChange(codigo);
537
+ if (codigo.length === length && codigo !== value) {
538
+ onComplete?.(codigo);
539
+ }
540
+ }
541
+ }
542
+ );
543
+ }
544
+ function formatCpf(input) {
545
+ const numeros = digitos(input, 11);
546
+ if (numeros.length <= 3) {
547
+ return numeros;
548
+ }
549
+ if (numeros.length <= 6) {
550
+ return `${numeros.slice(0, 3)}.${numeros.slice(3)}`;
551
+ }
552
+ if (numeros.length <= 9) {
553
+ return `${numeros.slice(0, 3)}.${numeros.slice(3, 6)}.${numeros.slice(6)}`;
554
+ }
555
+ return `${numeros.slice(0, 3)}.${numeros.slice(3, 6)}.${numeros.slice(6, 9)}-${numeros.slice(9)}`;
556
+ }
557
+ function CpfInput({ value, onChange, ...props }) {
558
+ return /* @__PURE__ */ jsx(
559
+ Input,
560
+ {
561
+ ...props,
562
+ type: "text",
563
+ inputMode: "numeric",
564
+ autoComplete: "off",
565
+ value,
566
+ onChange: (event) => onChange(formatCpf(event.target.value))
567
+ }
568
+ );
569
+ }
486
570
  var Dialog = Dialog$1.Root;
487
571
  function DialogTrigger({ asChild = true, ...props }) {
488
572
  return /* @__PURE__ */ jsx(Dialog$1.Trigger, { asChild, ...props });
@@ -638,31 +722,6 @@ function IconButton({
638
722
  )
639
723
  );
640
724
  }
641
- var fieldControlClasses = [
642
- "w-full rounded-control squircle",
643
- "bg-surface-sunken text-text text-body",
644
- "placeholder:text-text-muted",
645
- "border border-transparent",
646
- "transition-[border-color,box-shadow] duration-(--motion-quick) ease-(--ease-out-soft)",
647
- "outline-none focus-visible:outline-none",
648
- "focus:border-accent focus:ring-4 focus:ring-accent/15",
649
- "aria-invalid:border-danger aria-invalid:ring-4 aria-invalid:ring-danger/15",
650
- "disabled:pointer-events-none disabled:opacity-50"
651
- ].join(" ");
652
- function Input({ className, id, type, ...props }) {
653
- const field = useFieldContext();
654
- return /* @__PURE__ */ jsx(
655
- "input",
656
- {
657
- id: id ?? field?.controlId,
658
- type: type ?? "text",
659
- "aria-invalid": field?.invalid || void 0,
660
- "aria-describedby": field ? [field.errorId, field.hintId].filter(Boolean).join(" ") || void 0 : void 0,
661
- className: cn(fieldControlClasses, "min-h-touch px-4", className),
662
- ...props
663
- }
664
- );
665
- }
666
725
  function Textarea({ className, id, rows, ...props }) {
667
726
  const field = useFieldContext();
668
727
  return /* @__PURE__ */ jsx(
@@ -1215,6 +1274,7 @@ function ToastCard({ toast, onClose }) {
1215
1274
  return /* @__PURE__ */ jsxs(
1216
1275
  Toast.Root,
1217
1276
  {
1277
+ duration: toast.durationMs === null ? Number.POSITIVE_INFINITY : toast.durationMs,
1218
1278
  onOpenChange: (open) => {
1219
1279
  if (!open) onClose();
1220
1280
  },
@@ -1227,10 +1287,27 @@ function ToastCard({ toast, onClose }) {
1227
1287
  ),
1228
1288
  children: [
1229
1289
  visual ? /* @__PURE__ */ jsx(visual.icon, { "aria-hidden": "true", className: cn("mt-0.5 size-5 shrink-0", visual.tint) }) : null,
1230
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
1290
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-0.5", children: [
1231
1291
  /* @__PURE__ */ jsx(Toast.Title, { className: "text-subhead font-semibold", children: toast.title }),
1232
1292
  toast.description ? /* @__PURE__ */ jsx(Toast.Description, { className: "text-footnote text-text-secondary", children: toast.description }) : null
1233
1293
  ] }),
1294
+ toast.action ? (
1295
+ // `altText` é o que o leitor de tela anuncia como alternativa ao aviso, e o
1296
+ // que o Radix exige para aceitar um aviso persistente com botão.
1297
+ /* @__PURE__ */ jsx(
1298
+ Toast.Action,
1299
+ {
1300
+ altText: toast.action.label,
1301
+ onClick: toast.action.onClick,
1302
+ className: cn(
1303
+ "inline-flex min-h-touch shrink-0 items-center self-center rounded-control-sm px-3",
1304
+ "text-subhead font-semibold text-accent",
1305
+ "transition-colors duration-(--motion-quick) ease-(--ease-out-soft) hover:bg-accent/10"
1306
+ ),
1307
+ children: toast.action.label
1308
+ }
1309
+ )
1310
+ ) : null,
1234
1311
  /* @__PURE__ */ jsx(
1235
1312
  Toast.Close,
1236
1313
  {
@@ -1301,6 +1378,32 @@ function Tooltip({ content, side, className, children }) {
1301
1378
  ] }) })
1302
1379
  );
1303
1380
  }
1381
+ function formatWhatsapp(input) {
1382
+ const numeros = digitos(input, 11);
1383
+ if (numeros.length <= 2) {
1384
+ return numeros;
1385
+ }
1386
+ if (numeros.length <= 6) {
1387
+ return `(${numeros.slice(0, 2)}) ${numeros.slice(2)}`;
1388
+ }
1389
+ if (numeros.length <= 10) {
1390
+ return `(${numeros.slice(0, 2)}) ${numeros.slice(2, 6)}-${numeros.slice(6)}`;
1391
+ }
1392
+ return `(${numeros.slice(0, 2)}) ${numeros.slice(2, 7)}-${numeros.slice(7)}`;
1393
+ }
1394
+ function WhatsappInput({ value, onChange, ...props }) {
1395
+ return /* @__PURE__ */ jsx(
1396
+ Input,
1397
+ {
1398
+ ...props,
1399
+ type: "tel",
1400
+ inputMode: "numeric",
1401
+ autoComplete: "tel",
1402
+ value,
1403
+ onChange: (event) => onChange(formatWhatsapp(event.target.value))
1404
+ }
1405
+ );
1406
+ }
1304
1407
  var band = {
1305
1408
  terracota: "bg-terracota-soft",
1306
1409
  jardim: "bg-jardim-soft",
@@ -1591,4 +1694,4 @@ function ThemeToggle({ width, className }) {
1591
1694
  );
1592
1695
  }
1593
1696
 
1594
- export { Avatar, AvatarGroup, Badge, Banner, Button, Card, Checkbox, Cracha, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, EmptyState, Field, IconButton, Input, List, ListItem, Livreto, Popover, PopoverContent, PopoverTrigger, Predio, Progress, RadioGroup, RadioGroupItem, SearchField, SegmentedControl, Select, SelectItem, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Spinner, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ThemeProvider, ThemeToggle, ToastProvider, Tooltip, VisuallyHidden, cn, initTheme, useFieldContext, useTheme, useToast };
1697
+ export { Avatar, AvatarGroup, Badge, Banner, Button, Card, Checkbox, CodeInput, CpfInput, Cracha, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, EmptyState, Field, IconButton, Input, List, ListItem, Livreto, Popover, PopoverContent, PopoverTrigger, Predio, Progress, RadioGroup, RadioGroupItem, SearchField, SegmentedControl, Select, SelectItem, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Spinner, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ThemeProvider, ThemeToggle, ToastProvider, Tooltip, VisuallyHidden, WhatsappInput, cn, formatCpf, formatWhatsapp, initTheme, useFieldContext, useTheme, useToast };
package/dist/theme.css CHANGED
@@ -325,6 +325,12 @@
325
325
  --text-caption: 0.75rem;
326
326
  --text-caption--line-height: 1.34;
327
327
 
328
+ /*
329
+ * Seis dígitos num campo só precisam respirar para serem conferidos de relance
330
+ * contra o que chegou no WhatsApp. Espaçamento, não cor: sem par no escuro.
331
+ */
332
+ --tracking-codigo: 0.5em;
333
+
328
334
  /*
329
335
  * Raios generosos — a assinatura de forma do Assembleo. `control` para o que
330
336
  * se aperta, `surface` para o que contém, `expressive` para a camada lúdica.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembleo/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Design system do Assembleo: tokens, componentes e a camada expressiva que dão cara ao produto.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,