@majapisoftwares/ui 5.0.0 → 6.0.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.
Files changed (66) hide show
  1. package/AGENTS.md +11 -1
  2. package/README.md +19 -11
  3. package/THIRD_PARTY_NOTICES.md +25 -1
  4. package/components.json +1 -1
  5. package/docs/MIGRATION-6.0.md +33 -0
  6. package/docs/STANDARDS.md +11 -6
  7. package/docs/VERSIONING.md +28 -0
  8. package/package.json +19 -10
  9. package/scripts/check-standards.mjs +2 -0
  10. package/src/components/ui/accordion.tsx +82 -0
  11. package/src/components/ui/alert-dialog.tsx +187 -0
  12. package/src/components/ui/alert.tsx +2 -2
  13. package/src/components/ui/avatar.tsx +108 -0
  14. package/src/components/ui/badge.tsx +1 -1
  15. package/src/components/ui/breadcrumb.tsx +125 -0
  16. package/src/components/ui/button-group.tsx +89 -0
  17. package/src/components/ui/button.tsx +12 -16
  18. package/src/components/ui/card.tsx +101 -0
  19. package/src/components/ui/checkbox.tsx +27 -0
  20. package/src/components/ui/collapsible.tsx +21 -0
  21. package/src/components/ui/context-menu.tsx +271 -0
  22. package/src/components/ui/dialog.tsx +161 -0
  23. package/src/components/ui/dropdown-menu.tsx +271 -0
  24. package/src/components/ui/empty.tsx +4 -4
  25. package/src/components/ui/field.tsx +6 -6
  26. package/src/components/ui/hover-card.tsx +50 -0
  27. package/src/components/ui/input-group.tsx +155 -0
  28. package/src/components/ui/input.tsx +1 -1
  29. package/src/components/ui/kbd.tsx +28 -0
  30. package/src/components/ui/orb.css +1002 -0
  31. package/src/components/ui/orb.tsx +752 -0
  32. package/src/components/ui/popover.tsx +89 -0
  33. package/src/components/ui/progress.tsx +82 -0
  34. package/src/components/ui/radio-group.tsx +37 -0
  35. package/src/components/ui/scroll-area.tsx +54 -0
  36. package/src/components/ui/select.tsx +204 -0
  37. package/src/components/ui/sheet.tsx +8 -6
  38. package/src/components/ui/sidebar.tsx +1035 -0
  39. package/src/components/ui/skeleton.tsx +1 -1
  40. package/src/components/ui/slider.tsx +53 -0
  41. package/src/components/ui/sonner.tsx +44 -0
  42. package/src/components/ui/spinner.tsx +32 -0
  43. package/src/components/ui/switch.tsx +31 -0
  44. package/src/components/ui/table.tsx +10 -4
  45. package/src/components/ui/tabs.tsx +2 -2
  46. package/src/components/ui/textarea.tsx +19 -0
  47. package/src/components/ui/toggle-group.tsx +89 -0
  48. package/src/components/ui/toggle.tsx +44 -0
  49. package/src/components/ui/tooltip.tsx +65 -0
  50. package/src/hooks/use-mobile.ts +20 -0
  51. package/src/lib/i18n.tsx +109 -0
  52. package/src/patterns/app-shell.tsx +158 -53
  53. package/src/patterns/data-table.tsx +2441 -180
  54. package/src/patterns/details-sheet.tsx +8 -6
  55. package/src/patterns/filter-tabs.tsx +53 -0
  56. package/src/patterns/majapi-logo.tsx +32 -0
  57. package/src/patterns/page-header.tsx +44 -22
  58. package/src/patterns/preferences.tsx +172 -0
  59. package/src/patterns/status-badge.tsx +4 -3
  60. package/src/styles.css +41 -341
  61. package/src/templates/admin-dashboard.tsx +14 -33
  62. package/src/templates/auth-layout.tsx +18 -24
  63. package/src/templates/login-form.tsx +97 -65
  64. package/src/templates/signup-form.tsx +139 -99
  65. package/src/components/ui/native-select.tsx +0 -66
  66. package/src/vite-env.d.ts +0 -1
@@ -3,13 +3,33 @@
3
3
  import { useId, type ReactNode } from "react";
4
4
  import { Button } from "../components/ui/button";
5
5
  import { Input } from "../components/ui/input";
6
+ import { Field, FieldGroup, FieldLabel } from "../components/ui/field";
7
+ import { Alert, AlertDescription } from "../components/ui/alert";
6
8
  import {
7
- Field,
8
- FieldGroup,
9
- FieldLabel,
10
- FieldDescription,
11
- } from "../components/ui/field";
12
- import { Alert, AlertTitle, AlertDescription } from "../components/ui/alert";
9
+ Card,
10
+ CardContent,
11
+ CardDescription,
12
+ CardHeader,
13
+ CardTitle,
14
+ } from "../components/ui/card";
15
+ import { defineMessages, useMessages } from "../lib/i18n";
16
+
17
+ const messages = defineMessages({
18
+ en: {
19
+ title: "Welcome back",
20
+ email: "Email",
21
+ password: "Password",
22
+ submit: "Log in",
23
+ pending: "Logging in…",
24
+ },
25
+ "pt-BR": {
26
+ title: "Bem-vindo de volta",
27
+ email: "E-mail",
28
+ password: "Senha",
29
+ submit: "Entrar",
30
+ pending: "Entrando…",
31
+ },
32
+ });
13
33
 
14
34
  export interface LoginValues {
15
35
  email: string;
@@ -22,76 +42,88 @@ export interface LoginFormProps {
22
42
  title?: string;
23
43
  description?: string;
24
44
  submitLabel?: string;
25
- footer?: ReactNode;
45
+ /** Shown next to the password label, e.g. a "Forgot password?" link. */
26
46
  recovery?: ReactNode;
47
+ /** Shown above the fields, e.g. OAuth buttons. */
27
48
  alternative?: ReactNode;
49
+ footer?: ReactNode;
28
50
  }
29
51
  export function LoginForm({
30
52
  onSubmit,
31
53
  pending = false,
32
54
  error,
33
- title = "Entrar na sua conta",
34
- description = "Use seu e-mail e senha para continuar.",
35
- submitLabel = "Entrar",
36
- footer,
55
+ title,
56
+ description,
57
+ submitLabel,
37
58
  recovery,
38
59
  alternative,
60
+ footer,
39
61
  }: LoginFormProps) {
40
62
  const id = useId();
63
+ const t = useMessages(messages);
41
64
  return (
42
- <form
43
- className="wb-auth-form"
44
- onSubmit={(event) => {
45
- event.preventDefault();
46
- const data = new FormData(event.currentTarget);
47
- onSubmit({
48
- email: String(data.get("email") ?? ""),
49
- password: String(data.get("password") ?? ""),
50
- });
51
- }}
52
- aria-busy={pending}
53
- >
54
- <header>
55
- <h2>{title}</h2>
56
- <p>{description}</p>
57
- </header>
58
- {error ? (
59
- <Alert variant="destructive">
60
- <AlertTitle>Não foi possível entrar</AlertTitle>
61
- <AlertDescription>{error}</AlertDescription>
62
- </Alert>
63
- ) : null}
64
- <FieldGroup>
65
- <Field>
66
- <FieldLabel htmlFor={`${id}-email`}>E-mail</FieldLabel>
67
- <Input
68
- id={`${id}-email`}
69
- name="email"
70
- type="email"
71
- autoComplete="username"
72
- placeholder="voce@empresa.com"
73
- required
74
- disabled={pending}
75
- />
76
- </Field>
77
- <Field>
78
- <FieldLabel htmlFor={`${id}-password`}>Senha</FieldLabel>
79
- <Input
80
- id={`${id}-password`}
81
- name="password"
82
- type="password"
83
- autoComplete="current-password"
84
- required
85
- disabled={pending}
86
- />
87
- {recovery ? <FieldDescription>{recovery}</FieldDescription> : null}
88
- </Field>
89
- <Button type="submit" disabled={pending}>
90
- {pending ? "Entrando…" : submitLabel}
91
- </Button>
92
- </FieldGroup>
93
- {alternative}
94
- {footer ? <footer>{footer}</footer> : null}
95
- </form>
65
+ <Card>
66
+ <CardHeader className="text-center">
67
+ <CardTitle className="text-xl">{title ?? t.title}</CardTitle>
68
+ {description ? <CardDescription>{description}</CardDescription> : null}
69
+ </CardHeader>
70
+ <CardContent>
71
+ <form
72
+ aria-busy={pending}
73
+ onSubmit={(event) => {
74
+ event.preventDefault();
75
+ const data = new FormData(event.currentTarget);
76
+ onSubmit({
77
+ email: String(data.get("email") ?? ""),
78
+ password: String(data.get("password") ?? ""),
79
+ });
80
+ }}
81
+ >
82
+ <FieldGroup>
83
+ {alternative}
84
+ {error ? (
85
+ <Alert variant="destructive">
86
+ <AlertDescription>{error}</AlertDescription>
87
+ </Alert>
88
+ ) : null}
89
+ <Field>
90
+ <FieldLabel htmlFor={`${id}-email`}>{t.email}</FieldLabel>
91
+ <Input
92
+ id={`${id}-email`}
93
+ name="email"
94
+ type="email"
95
+ autoComplete="username"
96
+ required
97
+ disabled={pending}
98
+ />
99
+ </Field>
100
+ <Field>
101
+ <div className="flex items-center">
102
+ <FieldLabel htmlFor={`${id}-password`}>{t.password}</FieldLabel>
103
+ {recovery ? (
104
+ <div className="ml-auto text-sm">{recovery}</div>
105
+ ) : null}
106
+ </div>
107
+ <Input
108
+ id={`${id}-password`}
109
+ name="password"
110
+ type="password"
111
+ autoComplete="current-password"
112
+ required
113
+ disabled={pending}
114
+ />
115
+ </Field>
116
+ <Button type="submit" disabled={pending}>
117
+ {pending ? t.pending : (submitLabel ?? t.submit)}
118
+ </Button>
119
+ {footer ? (
120
+ <div className="text-center text-sm text-muted-foreground">
121
+ {footer}
122
+ </div>
123
+ ) : null}
124
+ </FieldGroup>
125
+ </form>
126
+ </CardContent>
127
+ </Card>
96
128
  );
97
129
  }
@@ -5,11 +5,44 @@ import { Button } from "../components/ui/button";
5
5
  import { Input } from "../components/ui/input";
6
6
  import {
7
7
  Field,
8
+ FieldDescription,
8
9
  FieldGroup,
9
10
  FieldLabel,
10
- FieldDescription,
11
11
  } from "../components/ui/field";
12
- import { Alert, AlertTitle, AlertDescription } from "../components/ui/alert";
12
+ import { Alert, AlertDescription } from "../components/ui/alert";
13
+ import {
14
+ Card,
15
+ CardContent,
16
+ CardDescription,
17
+ CardHeader,
18
+ CardTitle,
19
+ } from "../components/ui/card";
20
+ import { defineMessages, useMessages } from "../lib/i18n";
21
+
22
+ const messages = defineMessages({
23
+ en: {
24
+ title: "Create an account",
25
+ name: "Name",
26
+ email: "Email",
27
+ password: "Password",
28
+ confirm: "Confirm password",
29
+ minLength: (n: number) => `At least ${n} characters.`,
30
+ mismatch: "Passwords don't match.",
31
+ submit: "Create account",
32
+ pending: "Creating account…",
33
+ },
34
+ "pt-BR": {
35
+ title: "Criar conta",
36
+ name: "Nome",
37
+ email: "E-mail",
38
+ password: "Senha",
39
+ confirm: "Confirmar senha",
40
+ minLength: (n: number) => `Pelo menos ${n} caracteres.`,
41
+ mismatch: "As senhas não coincidem.",
42
+ submit: "Criar conta",
43
+ pending: "Criando conta…",
44
+ },
45
+ });
13
46
 
14
47
  export interface SignupValues {
15
48
  name: string;
@@ -24,114 +57,121 @@ export interface SignupFormProps {
24
57
  description?: string;
25
58
  submitLabel?: string;
26
59
  minPasswordLength?: number;
27
- footer?: ReactNode;
28
60
  terms?: ReactNode;
61
+ footer?: ReactNode;
29
62
  }
30
63
  export function SignupForm({
31
64
  onSubmit,
32
65
  pending = false,
33
66
  error,
34
- title = "Criar sua conta",
35
- description = "Comece a organizar seu trabalho em um só lugar.",
36
- submitLabel = "Criar conta",
67
+ title,
68
+ description,
69
+ submitLabel,
37
70
  minPasswordLength = 12,
38
- footer,
39
71
  terms,
72
+ footer,
40
73
  }: SignupFormProps) {
41
74
  const id = useId(),
75
+ t = useMessages(messages),
42
76
  [mismatch, setMismatch] = useState(false);
43
77
  return (
44
- <form
45
- className="wb-auth-form"
46
- aria-busy={pending}
47
- onSubmit={(event) => {
48
- event.preventDefault();
49
- const data = new FormData(event.currentTarget);
50
- const password = String(data.get("password") ?? "");
51
- if (password !== String(data.get("confirmPassword") ?? "")) {
52
- setMismatch(true);
53
- return;
54
- }
55
- setMismatch(false);
56
- onSubmit({
57
- name: String(data.get("name") ?? ""),
58
- email: String(data.get("email") ?? ""),
59
- password,
60
- });
61
- }}
62
- >
63
- <header>
64
- <h2>{title}</h2>
65
- <p>{description}</p>
66
- </header>
67
- {error ? (
68
- <Alert variant="destructive">
69
- <AlertTitle>Não foi possível criar a conta</AlertTitle>
70
- <AlertDescription>{error}</AlertDescription>
71
- </Alert>
72
- ) : null}
73
- <FieldGroup>
74
- <Field>
75
- <FieldLabel htmlFor={`${id}-name`}>Nome completo</FieldLabel>
76
- <Input
77
- id={`${id}-name`}
78
- name="name"
79
- autoComplete="name"
80
- required
81
- disabled={pending}
82
- />
83
- </Field>
84
- <Field>
85
- <FieldLabel htmlFor={`${id}-email`}>E-mail</FieldLabel>
86
- <Input
87
- id={`${id}-email`}
88
- name="email"
89
- type="email"
90
- autoComplete="email"
91
- required
92
- disabled={pending}
93
- />
94
- </Field>
95
- <Field>
96
- <FieldLabel htmlFor={`${id}-password`}>Senha</FieldLabel>
97
- <Input
98
- id={`${id}-password`}
99
- name="password"
100
- type="password"
101
- autoComplete="new-password"
102
- required
103
- minLength={minPasswordLength}
104
- disabled={pending}
105
- />
106
- <FieldDescription>
107
- Use pelo menos {minPasswordLength} caracteres.
108
- </FieldDescription>
109
- </Field>
110
- <Field data-invalid={mismatch || undefined}>
111
- <FieldLabel htmlFor={`${id}-confirm`}>Confirmar senha</FieldLabel>
112
- <Input
113
- id={`${id}-confirm`}
114
- name="confirmPassword"
115
- type="password"
116
- autoComplete="new-password"
117
- required
118
- aria-invalid={mismatch || undefined}
119
- aria-describedby={mismatch ? `${id}-mismatch` : undefined}
120
- disabled={pending}
121
- onChange={() => setMismatch(false)}
122
- />
123
- {mismatch ? (
124
- <FieldDescription id={`${id}-mismatch`} role="alert">
125
- As senhas precisam ser iguais.
126
- </FieldDescription>
127
- ) : null}
128
- </Field>
129
- {terms ? <FieldDescription>{terms}</FieldDescription> : null}
130
- <Button type="submit" disabled={pending}>
131
- {pending ? "Criando conta…" : submitLabel}
132
- </Button>
133
- </FieldGroup>
134
- {footer ? <footer>{footer}</footer> : null}
135
- </form>
78
+ <Card>
79
+ <CardHeader className="text-center">
80
+ <CardTitle className="text-xl">{title ?? t.title}</CardTitle>
81
+ {description ? <CardDescription>{description}</CardDescription> : null}
82
+ </CardHeader>
83
+ <CardContent>
84
+ <form
85
+ aria-busy={pending}
86
+ onSubmit={(event) => {
87
+ event.preventDefault();
88
+ const data = new FormData(event.currentTarget);
89
+ const password = String(data.get("password") ?? "");
90
+ if (password !== String(data.get("confirmPassword") ?? "")) {
91
+ setMismatch(true);
92
+ return;
93
+ }
94
+ setMismatch(false);
95
+ onSubmit({
96
+ name: String(data.get("name") ?? ""),
97
+ email: String(data.get("email") ?? ""),
98
+ password,
99
+ });
100
+ }}
101
+ >
102
+ <FieldGroup>
103
+ {error ? (
104
+ <Alert variant="destructive">
105
+ <AlertDescription>{error}</AlertDescription>
106
+ </Alert>
107
+ ) : null}
108
+ <Field>
109
+ <FieldLabel htmlFor={`${id}-name`}>{t.name}</FieldLabel>
110
+ <Input
111
+ id={`${id}-name`}
112
+ name="name"
113
+ autoComplete="name"
114
+ required
115
+ disabled={pending}
116
+ />
117
+ </Field>
118
+ <Field>
119
+ <FieldLabel htmlFor={`${id}-email`}>{t.email}</FieldLabel>
120
+ <Input
121
+ id={`${id}-email`}
122
+ name="email"
123
+ type="email"
124
+ autoComplete="email"
125
+ required
126
+ disabled={pending}
127
+ />
128
+ </Field>
129
+ <Field>
130
+ <FieldLabel htmlFor={`${id}-password`}>{t.password}</FieldLabel>
131
+ <Input
132
+ id={`${id}-password`}
133
+ name="password"
134
+ type="password"
135
+ autoComplete="new-password"
136
+ required
137
+ minLength={minPasswordLength}
138
+ disabled={pending}
139
+ />
140
+ <FieldDescription>
141
+ {t.minLength(minPasswordLength)}
142
+ </FieldDescription>
143
+ </Field>
144
+ <Field data-invalid={mismatch || undefined}>
145
+ <FieldLabel htmlFor={`${id}-confirm`}>{t.confirm}</FieldLabel>
146
+ <Input
147
+ id={`${id}-confirm`}
148
+ name="confirmPassword"
149
+ type="password"
150
+ autoComplete="new-password"
151
+ required
152
+ aria-invalid={mismatch || undefined}
153
+ aria-describedby={mismatch ? `${id}-mismatch` : undefined}
154
+ disabled={pending}
155
+ onChange={() => setMismatch(false)}
156
+ />
157
+ {mismatch ? (
158
+ <FieldDescription id={`${id}-mismatch`} role="alert">
159
+ {t.mismatch}
160
+ </FieldDescription>
161
+ ) : null}
162
+ </Field>
163
+ {terms ? <FieldDescription>{terms}</FieldDescription> : null}
164
+ <Button type="submit" disabled={pending}>
165
+ {pending ? t.pending : (submitLabel ?? t.submit)}
166
+ </Button>
167
+ {footer ? (
168
+ <div className="text-center text-sm text-muted-foreground">
169
+ {footer}
170
+ </div>
171
+ ) : null}
172
+ </FieldGroup>
173
+ </form>
174
+ </CardContent>
175
+ </Card>
136
176
  );
137
177
  }
@@ -1,66 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { cn } from "cn";
5
- import { ChevronDownIcon } from "lucide-react";
6
-
7
- type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
8
- size?: "sm" | "default";
9
- };
10
-
11
- function NativeSelect({
12
- className,
13
- size = "default",
14
- ...props
15
- }: NativeSelectProps) {
16
- return (
17
- <div
18
- className={cn(
19
- "group/native-select relative w-fit has-[select:disabled]:opacity-50",
20
- className,
21
- )}
22
- data-slot="native-select-wrapper"
23
- data-size={size}
24
- >
25
- <select
26
- data-slot="native-select"
27
- data-size={size}
28
- className="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
29
- {...props}
30
- />
31
- <ChevronDownIcon
32
- className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
33
- aria-hidden="true"
34
- data-slot="native-select-icon"
35
- />
36
- </div>
37
- );
38
- }
39
-
40
- function NativeSelectOption({
41
- className,
42
- ...props
43
- }: React.ComponentProps<"option">) {
44
- return (
45
- <option
46
- data-slot="native-select-option"
47
- className={cn("bg-[Canvas] text-[CanvasText]", className)}
48
- {...props}
49
- />
50
- );
51
- }
52
-
53
- function NativeSelectOptGroup({
54
- className,
55
- ...props
56
- }: React.ComponentProps<"optgroup">) {
57
- return (
58
- <optgroup
59
- data-slot="native-select-optgroup"
60
- className={cn("bg-[Canvas] text-[CanvasText]", className)}
61
- {...props}
62
- />
63
- );
64
- }
65
-
66
- export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
package/src/vite-env.d.ts DELETED
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />