@majapisoftwares/ui 6.2.0 → 6.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@majapisoftwares/ui",
3
- "version": "6.2.0",
3
+ "version": "6.2.2",
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
+ }
@@ -1,180 +1,201 @@
1
- "use client";
2
-
3
- import {
4
- createContext,
5
- useContext,
6
- useEffect,
7
- useState,
8
- type ReactNode,
9
- } from "react";
10
- import { LanguagesIcon, MoonIcon, SunIcon } from "lucide-react";
11
- import {
12
- LocaleProvider,
13
- locales,
14
- matchLocale,
15
- useUiMessages,
16
- type Locale,
17
- } from "../lib/i18n";
18
- import { Button } from "../components/ui/button";
19
- import { Toaster } from "../components/ui/sonner";
20
- import {
21
- DropdownMenu,
22
- DropdownMenuContent,
23
- DropdownMenuRadioGroup,
24
- DropdownMenuRadioItem,
25
- DropdownMenuTrigger,
26
- } from "../components/ui/dropdown-menu";
27
-
28
- type Theme = "light" | "dark";
29
- const PreferencesContext = createContext<{
30
- locale: Locale;
31
- setLocale: (locale: Locale) => void;
32
- theme: Theme;
33
- setTheme: (theme: Theme) => void;
34
- } | null>(null);
35
- const PreferencesReadyContext = createContext(true);
36
-
37
- function read(key: string) {
38
- try {
39
- return localStorage.getItem(key);
40
- } catch {
41
- return null;
42
- }
43
- }
44
- function write(key: string, value: string) {
45
- try {
46
- localStorage.setItem(key, value);
47
- } catch {
48
- // Storage can be unavailable (private mode); preferences just won't persist.
49
- }
50
- }
51
-
52
- /**
53
- * Mirrors matchLocale; keep both in sync.
54
- * Inline this in `<head>` (e.g. `<script dangerouslySetInnerHTML>`) so the
55
- * stored theme applies before paint.
56
- */
57
- export const preferencesScript = `try{var d=document.documentElement;if(localStorage.getItem("theme")==="dark")d.classList.add("dark");var l=localStorage.getItem("locale");if(l!=="en"&&l!=="pt-BR"){l="en";var a=navigator.languages||[];for(var i=0;i<a.length;i++){if(/^pt/i.test(a[i])){l="pt-BR";break}if(/^en/i.test(a[i]))break}}if(l!==d.lang){d.lang=l;d.setAttribute("data-locale-pending",l)}}catch(e){}`;
58
-
59
- /**
60
- * Locale + theme for the whole app. Renders `defaultLocale` first (matches
61
- * static/server HTML), then applies the stored or browser locale on mount.
62
- */
63
- export function Preferences({
64
- defaultLocale = "en",
65
- children,
66
- }: {
67
- defaultLocale?: Locale;
68
- children: ReactNode;
69
- }) {
70
- const [locale, setLocale] = useState<Locale>(defaultLocale);
71
- const [theme, setTheme] = useState<Theme>("light");
72
- const [ready, setReady] = useState(false);
73
- useEffect(() => {
74
- const stored = read("locale");
75
- // eslint-disable-next-line react-hooks/set-state-in-effect -- sync from browser-only storage after hydration
76
- setLocale(
77
- locales.includes(stored as Locale)
78
- ? (stored as Locale)
79
- : matchLocale(navigator.languages),
80
- );
81
- setTheme(read("theme") === "dark" ? "dark" : "light");
82
- setReady(true);
83
- }, []);
84
- useEffect(() => {
85
- if (!ready) return;
86
- const root = document.documentElement;
87
- root.lang = locale;
88
- // The pre-paint script hides the page until the saved locale is rendered.
89
- if (root.dataset.localePending === locale)
90
- delete root.dataset.localePending;
91
- }, [locale, ready]);
92
- useEffect(() => {
93
- document.documentElement.classList.toggle("dark", theme === "dark");
94
- }, [theme]);
95
- return (
96
- <PreferencesContext.Provider
97
- value={{
98
- locale,
99
- setLocale: (next) => {
100
- setLocale(next);
101
- write("locale", next);
102
- },
103
- theme,
104
- setTheme: (next) => {
105
- setTheme(next);
106
- write("theme", next);
107
- },
108
- }}
109
- >
110
- <PreferencesReadyContext.Provider value={ready}>
111
- <LocaleProvider locale={locale}>
112
- {children}
113
- <Toaster theme={theme} />
114
- </LocaleProvider>
115
- </PreferencesReadyContext.Provider>
116
- </PreferencesContext.Provider>
117
- );
118
- }
119
-
120
- /**
121
- * Owns `document.title` so it follows the active language. Leave `title` out
122
- * of Next.js metadata, or Next will overwrite it after hydration.
123
- */
124
- export function useDocumentTitle(title: string) {
125
- const ready = useContext(PreferencesReadyContext);
126
- useEffect(() => {
127
- if (!ready) return;
128
- document.title = title;
129
- }, [title, ready]);
130
- }
131
-
132
- export function usePreferences() {
133
- const context = useContext(PreferencesContext);
134
- if (!context)
135
- throw new Error("usePreferences must be used within Preferences.");
136
- return context;
137
- }
138
-
139
- const localeNames: Record<Locale, string> = {
140
- en: "English",
141
- "pt-BR": "Português",
142
- };
143
-
144
- /** Language menu and theme toggle, e.g. for `AppShell` actions. */
145
- export function PreferenceControls() {
146
- const { locale, setLocale, theme, setTheme } = usePreferences();
147
- const t = useUiMessages();
148
- return (
149
- <>
150
- <DropdownMenu>
151
- <DropdownMenuTrigger
152
- render={<Button variant="ghost" size="sm" aria-label={t.language} />}
153
- >
154
- <LanguagesIcon />
155
- {locale === "en" ? "EN" : "PT"}
156
- </DropdownMenuTrigger>
157
- <DropdownMenuContent align="end">
158
- <DropdownMenuRadioGroup
159
- value={locale}
160
- onValueChange={(value) => setLocale(value as Locale)}
161
- >
162
- {locales.map((value) => (
163
- <DropdownMenuRadioItem key={value} value={value}>
164
- {localeNames[value]}
165
- </DropdownMenuRadioItem>
166
- ))}
167
- </DropdownMenuRadioGroup>
168
- </DropdownMenuContent>
169
- </DropdownMenu>
170
- <Button
171
- variant="ghost"
172
- size="icon-sm"
173
- aria-label={t.toggleTheme}
174
- onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
175
- >
176
- {theme === "dark" ? <SunIcon /> : <MoonIcon />}
177
- </Button>
178
- </>
179
- );
180
- }
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ type ReactNode,
9
+ } from "react";
10
+ import { LanguagesIcon, MoonIcon, SunIcon } from "lucide-react";
11
+ import {
12
+ LocaleProvider,
13
+ locales,
14
+ matchLocale,
15
+ useUiMessages,
16
+ type Locale,
17
+ } from "../lib/i18n";
18
+ import { Button } from "../components/ui/button";
19
+ import { Toaster } from "../components/ui/sonner";
20
+ import {
21
+ DropdownMenu,
22
+ DropdownMenuContent,
23
+ DropdownMenuRadioGroup,
24
+ DropdownMenuRadioItem,
25
+ DropdownMenuTrigger,
26
+ } from "../components/ui/dropdown-menu";
27
+
28
+ type Theme = "light" | "dark";
29
+ const PreferencesContext = createContext<{
30
+ locale: Locale;
31
+ setLocale: (locale: Locale) => void;
32
+ theme: Theme;
33
+ setTheme: (theme: Theme) => void;
34
+ } | null>(null);
35
+ const PreferencesReadyContext = createContext(true);
36
+
37
+ function read(key: string) {
38
+ try {
39
+ return localStorage.getItem(key);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function write(key: string, value: string) {
45
+ try {
46
+ localStorage.setItem(key, value);
47
+ } catch {
48
+ // Storage can be unavailable (private mode); preferences just won't persist.
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Mirrors matchLocale; keep both in sync.
54
+ * Inline this in `<head>` (e.g. `<script dangerouslySetInnerHTML>`) so the
55
+ * stored theme applies before paint.
56
+ */
57
+ export const preferencesScript = `try{var d=document.documentElement;if(localStorage.getItem("theme")==="dark")d.classList.add("dark");var l=localStorage.getItem("locale");if(l!=="en"&&l!=="pt-BR"){l="en";var a=navigator.languages||[];for(var i=0;i<a.length;i++){if(/^pt/i.test(a[i])){l="pt-BR";break}if(/^en/i.test(a[i]))break}}if(l!==d.lang){d.lang=l;d.setAttribute("data-locale-pending",l)}}catch(e){}`;
58
+
59
+ /**
60
+ * Locale + theme for the whole app. Renders `defaultLocale` first (matches
61
+ * static/server HTML), then applies the stored or browser locale on mount.
62
+ */
63
+ export function Preferences({
64
+ defaultLocale = "en",
65
+ children,
66
+ }: {
67
+ defaultLocale?: Locale;
68
+ children: ReactNode;
69
+ }) {
70
+ const [locale, setLocale] = useState<Locale>(defaultLocale);
71
+ const [theme, setTheme] = useState<Theme>("light");
72
+ const [ready, setReady] = useState(false);
73
+ useEffect(() => {
74
+ const stored = read("locale");
75
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- sync from browser-only storage after hydration
76
+ setLocale(
77
+ locales.includes(stored as Locale)
78
+ ? (stored as Locale)
79
+ : matchLocale(navigator.languages),
80
+ );
81
+ setTheme(read("theme") === "dark" ? "dark" : "light");
82
+ setReady(true);
83
+ }, []);
84
+ useEffect(() => {
85
+ if (!ready) return;
86
+ const root = document.documentElement;
87
+ root.lang = locale;
88
+ // The pre-paint script hides the page until the saved locale is rendered.
89
+ if (root.dataset.localePending === locale)
90
+ delete root.dataset.localePending;
91
+ }, [locale, ready]);
92
+ useEffect(() => {
93
+ if (!ready) return;
94
+ document.documentElement.classList.toggle("dark", theme === "dark");
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
+ }, []);
116
+ return (
117
+ <PreferencesContext.Provider
118
+ value={{
119
+ locale,
120
+ setLocale: (next) => {
121
+ setLocale(next);
122
+ write("locale", next);
123
+ },
124
+ theme,
125
+ setTheme: (next) => {
126
+ setTheme(next);
127
+ write("theme", next);
128
+ },
129
+ }}
130
+ >
131
+ <PreferencesReadyContext.Provider value={ready}>
132
+ <LocaleProvider locale={locale}>
133
+ {children}
134
+ <Toaster theme={theme} />
135
+ </LocaleProvider>
136
+ </PreferencesReadyContext.Provider>
137
+ </PreferencesContext.Provider>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Owns `document.title` so it follows the active language. Leave `title` out
143
+ * of Next.js metadata, or Next will overwrite it after hydration.
144
+ */
145
+ export function useDocumentTitle(title: string) {
146
+ const ready = useContext(PreferencesReadyContext);
147
+ useEffect(() => {
148
+ if (!ready) return;
149
+ document.title = title;
150
+ }, [title, ready]);
151
+ }
152
+
153
+ export function usePreferences() {
154
+ const context = useContext(PreferencesContext);
155
+ if (!context)
156
+ throw new Error("usePreferences must be used within Preferences.");
157
+ return context;
158
+ }
159
+
160
+ const localeNames: Record<Locale, string> = {
161
+ en: "English",
162
+ "pt-BR": "Português",
163
+ };
164
+
165
+ /** Language menu and theme toggle, e.g. for `AppShell` actions. */
166
+ export function PreferenceControls() {
167
+ const { locale, setLocale, theme, setTheme } = usePreferences();
168
+ const t = useUiMessages();
169
+ return (
170
+ <>
171
+ <DropdownMenu>
172
+ <DropdownMenuTrigger
173
+ render={<Button variant="ghost" size="sm" aria-label={t.language} />}
174
+ >
175
+ <LanguagesIcon />
176
+ {locale === "en" ? "EN" : "PT"}
177
+ </DropdownMenuTrigger>
178
+ <DropdownMenuContent align="end">
179
+ <DropdownMenuRadioGroup
180
+ value={locale}
181
+ onValueChange={(value) => setLocale(value as Locale)}
182
+ >
183
+ {locales.map((value) => (
184
+ <DropdownMenuRadioItem key={value} value={value}>
185
+ {localeNames[value]}
186
+ </DropdownMenuRadioItem>
187
+ ))}
188
+ </DropdownMenuRadioGroup>
189
+ </DropdownMenuContent>
190
+ </DropdownMenu>
191
+ <Button
192
+ variant="ghost"
193
+ size="icon-sm"
194
+ aria-label={t.toggleTheme}
195
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
196
+ >
197
+ {theme === "dark" ? <SunIcon /> : <MoonIcon />}
198
+ </Button>
199
+ </>
200
+ );
201
+ }