@majapisoftwares/ui 6.1.3 → 6.2.1

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/docs/STANDARDS.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Padrão v1 de código e UI/UX
2
2
 
3
+ `LoginForm` aceita `mode="providers"` com `alternative` obrigatório para autenticação exclusiva por OAuth/SSO, sem campos de senha nem callback de credenciais. O modo padrão continua `credentials`. Catálogo e starter demonstram em `/login/provider`. A identidade autenticada usa `UserMenu` no slot `footer` do `AppShell`; tema, idioma e saída permanecem no padrão compartilhado.
4
+
3
5
  Base atual: shadcn base-luma com @base-ui/react. Os componentes interativos expõem fronteiras use client para uso no Next.js App Router. A migração da API Radix está documentada em MIGRATION-0.2.md.
4
6
 
5
7
  ## Arquitetura e código
@@ -1,22 +1,22 @@
1
- # Saved-language titles
2
-
3
- The initial preference state matched server HTML in English. Child title effects
4
- ran before browser preferences were loaded, briefly writing the English title.
5
- The pre-paint visibility mask also expired after two seconds, independently of
6
- whether hydration had completed.
7
-
8
- `Preferences` now exposes internal readiness to `useDocumentTitle`. Titles are
9
- written only after the stored/browser language is resolved. The pre-paint mask
10
- is removed only when React has committed that language. The public API remains
11
- compatible, including `useDocumentTitle` outside Preferences.
12
-
13
- The catalog and generated starter consume the shared implementation. Cloud and
14
- the starter pin the corrected public Git source archive by its full commit SHA;
15
- no registry authentication or mutable dependency ref is required.
16
-
17
- Validation: Portuguese and English hydration tests record every document-title
18
- write and verify that all writes use the selected language. Library typecheck,
19
- lint, standards and build pass. Generated-starter lint, typecheck, UI standards
20
- and build pass. Local Chrome refresh shows `Padrões · Majapi UI` and
21
- `Repositórios · Majapi Cloud`, with visible Portuguese content and the button
22
- `Conectar repositórios`. No production deployment was performed.
1
+ # Saved-language titles
2
+
3
+ The initial preference state matched server HTML in English. Child title effects
4
+ ran before browser preferences were loaded, briefly writing the English title.
5
+ The pre-paint visibility mask also expired after two seconds, independently of
6
+ whether hydration had completed.
7
+
8
+ `Preferences` now exposes internal readiness to `useDocumentTitle`. Titles are
9
+ written only after the stored/browser language is resolved. The pre-paint mask
10
+ is removed only when React has committed that language. The public API remains
11
+ compatible, including `useDocumentTitle` outside Preferences.
12
+
13
+ The catalog and generated starter consume the shared implementation. Cloud and
14
+ the starter pin the corrected public Git source archive by its full commit SHA;
15
+ no registry authentication or mutable dependency ref is required.
16
+
17
+ Validation: Portuguese and English hydration tests record every document-title
18
+ write and verify that all writes use the selected language. Library typecheck,
19
+ lint, standards and build pass. Generated-starter lint, typecheck, UI standards
20
+ and build pass. Local Chrome refresh shows `Padrões · Majapi UI` and
21
+ `Repositórios · Majapi Cloud`, with visible Portuguese content and the button
22
+ `Conectar repositórios`. No production deployment was performed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majapisoftwares/ui",
3
- "version": "6.1.3",
3
+ "version": "6.2.1",
4
4
  "description": "Shared Majapi UI and UX standards based on shadcn/ui",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useState, type ComponentProps } from "react";
4
+ import { CheckIcon, CopyIcon } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+ import {
7
+ InputGroup,
8
+ InputGroupAddon,
9
+ InputGroupButton,
10
+ InputGroupInput,
11
+ } from "../components/ui/input-group";
12
+
13
+ export interface CopyInputProps
14
+ extends Omit<ComponentProps<typeof InputGroupInput>, "value" | "readOnly"> {
15
+ value: string;
16
+ copyLabel?: string;
17
+ copiedLabel?: string;
18
+ className?: string;
19
+ }
20
+
21
+ export function CopyInput({
22
+ value,
23
+ copyLabel = "Copy",
24
+ copiedLabel = "Copied",
25
+ className,
26
+ ...props
27
+ }: CopyInputProps) {
28
+ const [copied, setCopied] = useState(false);
29
+
30
+ const handleCopy = async () => {
31
+ try {
32
+ await navigator.clipboard.writeText(value);
33
+ setCopied(true);
34
+ setTimeout(() => setCopied(false), 2000);
35
+ } catch {
36
+ // ignore clipboard error
37
+ }
38
+ };
39
+
40
+ return (
41
+ <InputGroup className={cn("w-full min-w-0", className)}>
42
+ <InputGroupInput
43
+ readOnly
44
+ value={value}
45
+ className="font-mono text-xs selection:bg-primary/20"
46
+ onFocus={(e) => e.currentTarget.select()}
47
+ {...props}
48
+ />
49
+ <InputGroupAddon align="inline-end">
50
+ <InputGroupButton
51
+ size="icon-xs"
52
+ onClick={() => void handleCopy()}
53
+ aria-label={copied ? copiedLabel : copyLabel}
54
+ title={copied ? copiedLabel : copyLabel}
55
+ >
56
+ {copied ? (
57
+ <CheckIcon className="size-3.5 text-success-foreground" aria-hidden="true" />
58
+ ) : (
59
+ <CopyIcon className="size-3.5" aria-hidden="true" />
60
+ )}
61
+ </InputGroupButton>
62
+ </InputGroupAddon>
63
+ </InputGroup>
64
+ );
65
+ }
@@ -1,54 +1,54 @@
1
- "use client";
2
-
3
- import type { ReactNode } from "react";
4
- import {
5
- Sheet,
6
- SheetContent,
7
- SheetHeader,
8
- SheetTitle,
9
- SheetDescription,
10
- } from "../components/ui/sheet";
11
- export function DetailsSheet({
12
- open,
13
- onOpenChange,
14
- title,
15
- description,
16
- children,
17
- }: {
18
- open: boolean;
19
- onOpenChange: (open: boolean) => void;
20
- title: string;
21
- description?: string;
22
- children: ReactNode;
23
- }) {
24
- return (
25
- <Sheet open={open} onOpenChange={onOpenChange}>
26
- <SheetContent className="overflow-y-auto">
27
- <SheetHeader>
28
- <SheetTitle>{title}</SheetTitle>
29
- {description ? (
30
- <SheetDescription>{description}</SheetDescription>
31
- ) : null}
32
- </SheetHeader>
33
- {/* Same inset as SheetHeader (p-6). */}
34
- <div className="px-6 pb-8">{children}</div>
35
- </SheetContent>
36
- </Sheet>
37
- );
38
- }
39
- export function DetailList({
40
- items,
41
- }: {
42
- items: { label: string; value: ReactNode }[];
43
- }) {
44
- return (
45
- <dl className="grid gap-4 text-sm">
46
- {items.map((item) => (
47
- <div key={item.label} className="grid gap-1">
48
- <dt className="text-muted-foreground">{item.label}</dt>
49
- <dd className="font-medium break-words">{item.value}</dd>
50
- </div>
51
- ))}
52
- </dl>
53
- );
54
- }
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetHeader,
8
+ SheetTitle,
9
+ SheetDescription,
10
+ } from "../components/ui/sheet";
11
+ export function DetailsSheet({
12
+ open,
13
+ onOpenChange,
14
+ title,
15
+ description,
16
+ children,
17
+ }: {
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ title: string;
21
+ description?: string;
22
+ children: ReactNode;
23
+ }) {
24
+ return (
25
+ <Sheet open={open} onOpenChange={onOpenChange}>
26
+ <SheetContent className="overflow-x-hidden overflow-y-auto data-[side=left]:sm:max-w-xl data-[side=right]:sm:max-w-xl">
27
+ <SheetHeader>
28
+ <SheetTitle>{title}</SheetTitle>
29
+ {description ? (
30
+ <SheetDescription>{description}</SheetDescription>
31
+ ) : null}
32
+ </SheetHeader>
33
+ {/* Same inset as SheetHeader (p-6). */}
34
+ <div className="min-w-0 px-6 pb-8">{children}</div>
35
+ </SheetContent>
36
+ </Sheet>
37
+ );
38
+ }
39
+ export function DetailList({
40
+ items,
41
+ }: {
42
+ items: { label: string; value: ReactNode }[];
43
+ }) {
44
+ return (
45
+ <dl className="grid min-w-0 gap-4 text-sm">
46
+ {items.map((item) => (
47
+ <div key={item.label} className="grid min-w-0 gap-1">
48
+ <dt className="text-muted-foreground">{item.label}</dt>
49
+ <dd className="min-w-0 font-medium break-words">{item.value}</dd>
50
+ </div>
51
+ ))}
52
+ </dl>
53
+ );
54
+ }
@@ -90,8 +90,29 @@ export function Preferences({
90
90
  delete root.dataset.localePending;
91
91
  }, [locale, ready]);
92
92
  useEffect(() => {
93
+ if (!ready) return;
93
94
  document.documentElement.classList.toggle("dark", theme === "dark");
94
- }, [theme]);
95
+ }, [theme, ready]);
96
+ useEffect(() => {
97
+ if (
98
+ typeof location === "undefined" ||
99
+ !location.hostname.endsWith(".localhost")
100
+ )
101
+ return;
102
+ const link = document.querySelector<HTMLLinkElement>('link[rel*="icon"]');
103
+ const href = link?.getAttribute("href");
104
+ if (!link || !href || href.startsWith("data:")) return;
105
+ void fetch(href)
106
+ .then((res) => (res.ok ? res.text() : ""))
107
+ .then((svg) => {
108
+ if (!svg) return;
109
+ const embeddedPng = svg.match(/data:image\/png;base64,[^"']+/);
110
+ link.href = embeddedPng
111
+ ? embeddedPng[0]
112
+ : `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
113
+ })
114
+ .catch(() => {});
115
+ }, []);
95
116
  return (
96
117
  <PreferencesContext.Provider
97
118
  value={{
@@ -1,129 +1,140 @@
1
- "use client";
2
-
3
- import { useId, type ReactNode } from "react";
4
- import { Button } from "../components/ui/button";
5
- import { Input } from "../components/ui/input";
6
- import { Field, FieldGroup, FieldLabel } from "../components/ui/field";
7
- import { Alert, AlertDescription } from "../components/ui/alert";
8
- import {
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
- });
33
-
34
- export interface LoginValues {
35
- email: string;
36
- password: string;
37
- }
38
- export interface LoginFormProps {
39
- onSubmit: (values: LoginValues) => void;
40
- pending?: boolean;
41
- error?: string;
42
- title?: string;
43
- description?: string;
44
- submitLabel?: string;
45
- /** Shown next to the password label, e.g. a "Forgot password?" link. */
46
- recovery?: ReactNode;
47
- /** Shown above the fields, e.g. OAuth buttons. */
48
- alternative?: ReactNode;
49
- footer?: ReactNode;
50
- }
51
- export function LoginForm({
52
- onSubmit,
53
- pending = false,
54
- error,
55
- title,
56
- description,
57
- submitLabel,
58
- recovery,
59
- alternative,
60
- footer,
61
- }: LoginFormProps) {
62
- const id = useId();
63
- const t = useMessages(messages);
64
- return (
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>
128
- );
129
- }
1
+ "use client";
2
+
3
+ import { useId, type ReactNode } from "react";
4
+ import { Button } from "../components/ui/button";
5
+ import { Input } from "../components/ui/input";
6
+ import { Field, FieldGroup, FieldLabel } from "../components/ui/field";
7
+ import { Alert, AlertDescription } from "../components/ui/alert";
8
+ import {
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
+ });
33
+
34
+ export interface LoginValues {
35
+ email: string;
36
+ password: string;
37
+ }
38
+ interface LoginFormOptions {
39
+ pending?: boolean;
40
+ error?: string;
41
+ title?: string;
42
+ description?: string;
43
+ submitLabel?: string;
44
+ /** Shown next to the password label, e.g. a "Forgot password?" link. */
45
+ recovery?: ReactNode;
46
+ /** Shown above the fields, e.g. OAuth buttons. */
47
+ alternative?: ReactNode;
48
+ footer?: ReactNode;
49
+ }
50
+ export type LoginFormProps = LoginFormOptions &
51
+ (
52
+ | { mode?: "credentials"; onSubmit: (values: LoginValues) => void }
53
+ | { mode: "providers"; onSubmit?: never; alternative: ReactNode }
54
+ );
55
+ export function LoginForm({
56
+ mode = "credentials",
57
+ onSubmit,
58
+ pending = false,
59
+ error,
60
+ title,
61
+ description,
62
+ submitLabel,
63
+ recovery,
64
+ alternative,
65
+ footer,
66
+ }: LoginFormProps) {
67
+ const id = useId();
68
+ const t = useMessages(messages);
69
+ return (
70
+ <Card>
71
+ <CardHeader className="text-center">
72
+ <CardTitle className="text-xl">{title ?? t.title}</CardTitle>
73
+ {description ? <CardDescription>{description}</CardDescription> : null}
74
+ </CardHeader>
75
+ <CardContent>
76
+ <form
77
+ aria-busy={pending}
78
+ onSubmit={(event) => {
79
+ event.preventDefault();
80
+ const data = new FormData(event.currentTarget);
81
+ onSubmit?.({
82
+ email: String(data.get("email") ?? ""),
83
+ password: String(data.get("password") ?? ""),
84
+ });
85
+ }}
86
+ >
87
+ <FieldGroup>
88
+ {alternative}
89
+ {error ? (
90
+ <Alert variant="destructive">
91
+ <AlertDescription>{error}</AlertDescription>
92
+ </Alert>
93
+ ) : null}
94
+ {mode === "credentials" ? (
95
+ <>
96
+ <Field>
97
+ <FieldLabel htmlFor={`${id}-email`}>{t.email}</FieldLabel>
98
+ <Input
99
+ id={`${id}-email`}
100
+ name="email"
101
+ type="email"
102
+ autoComplete="username"
103
+ required
104
+ disabled={pending}
105
+ />
106
+ </Field>
107
+ <Field>
108
+ <div className="flex items-center">
109
+ <FieldLabel htmlFor={`${id}-password`}>
110
+ {t.password}
111
+ </FieldLabel>
112
+ {recovery ? (
113
+ <div className="ml-auto text-sm">{recovery}</div>
114
+ ) : null}
115
+ </div>
116
+ <Input
117
+ id={`${id}-password`}
118
+ name="password"
119
+ type="password"
120
+ autoComplete="current-password"
121
+ required
122
+ disabled={pending}
123
+ />
124
+ </Field>
125
+ <Button type="submit" disabled={pending}>
126
+ {pending ? t.pending : (submitLabel ?? t.submit)}
127
+ </Button>
128
+ </>
129
+ ) : null}
130
+ {footer ? (
131
+ <div className="text-center text-sm text-muted-foreground">
132
+ {footer}
133
+ </div>
134
+ ) : null}
135
+ </FieldGroup>
136
+ </form>
137
+ </CardContent>
138
+ </Card>
139
+ );
140
+ }