@majapisoftwares/ui 6.1.0 → 6.1.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/AGENTS.md +27 -27
- package/README.md +79 -79
- package/THIRD_PARTY_NOTICES.md +39 -39
- package/components.json +25 -25
- package/docs/MIGRATION-0.2.md +20 -20
- package/docs/MIGRATION-6.0.md +33 -33
- package/docs/NEXT-STARTER-VALIDATION.md +14 -14
- package/docs/STANDARDS.md +41 -41
- package/docs/VALIDATION.md +11 -11
- package/docs/VERSIONING.md +28 -28
- package/docs/validation/preference-titles.md +22 -0
- package/package.json +88 -84
- package/scripts/check-standards.mjs +41 -41
- package/src/components/ui/accordion.tsx +82 -82
- package/src/components/ui/alert-dialog.tsx +187 -187
- package/src/components/ui/alert.tsx +77 -77
- package/src/components/ui/avatar.tsx +108 -108
- package/src/components/ui/badge.tsx +55 -55
- package/src/components/ui/breadcrumb.tsx +125 -125
- package/src/components/ui/button-group.tsx +89 -89
- package/src/components/ui/button.tsx +59 -59
- package/src/components/ui/card.tsx +101 -101
- package/src/components/ui/checkbox.tsx +27 -27
- package/src/components/ui/collapsible.tsx +21 -21
- package/src/components/ui/context-menu.tsx +271 -271
- package/src/components/ui/dialog.tsx +161 -161
- package/src/components/ui/dropdown-menu.tsx +271 -271
- package/src/components/ui/empty.tsx +105 -105
- package/src/components/ui/field.tsx +238 -238
- package/src/components/ui/hover-card.tsx +50 -50
- package/src/components/ui/input-group.tsx +155 -155
- package/src/components/ui/input.tsx +21 -21
- package/src/components/ui/kbd.tsx +28 -28
- package/src/components/ui/label.tsx +19 -19
- package/src/components/ui/orb.css +1002 -1002
- package/src/components/ui/orb.tsx +752 -752
- package/src/components/ui/popover.tsx +89 -89
- package/src/components/ui/progress.tsx +82 -82
- package/src/components/ui/radio-group.tsx +37 -37
- package/src/components/ui/scroll-area.tsx +54 -54
- package/src/components/ui/select.tsx +204 -204
- package/src/components/ui/separator.tsx +24 -24
- package/src/components/ui/sheet.tsx +139 -139
- package/src/components/ui/sidebar.tsx +1039 -1035
- package/src/components/ui/skeleton.tsx +15 -15
- package/src/components/ui/slider.tsx +53 -53
- package/src/components/ui/sonner.tsx +44 -44
- package/src/components/ui/spinner.tsx +32 -32
- package/src/components/ui/switch.tsx +31 -31
- package/src/components/ui/table.tsx +121 -121
- package/src/components/ui/tabs.tsx +81 -81
- package/src/components/ui/textarea.tsx +19 -19
- package/src/components/ui/toggle-group.tsx +89 -89
- package/src/components/ui/toggle.tsx +44 -44
- package/src/components/ui/tooltip.tsx +65 -65
- package/src/hooks/use-mobile.ts +20 -20
- package/src/lib/i18n.tsx +109 -109
- package/src/lib/utils.ts +1 -1
- package/src/patterns/app-shell.tsx +177 -179
- package/src/patterns/data-table.tsx +2486 -2486
- package/src/patterns/details-sheet.tsx +54 -54
- package/src/patterns/filter-tabs.tsx +53 -53
- package/src/patterns/majapi-logo.tsx +32 -32
- package/src/patterns/page-header.tsx +62 -62
- package/src/patterns/preferences.tsx +15 -7
- package/src/patterns/status-badge.tsx +24 -24
- package/src/patterns/user-menu.tsx +218 -218
- package/src/patterns/workspace-switcher.tsx +176 -173
- package/src/styles.css +165 -165
- package/src/templates/admin-dashboard.tsx +44 -44
- package/src/templates/auth-layout.tsx +30 -30
- package/src/templates/login-form.tsx +129 -129
- package/src/templates/signup-form.tsx +177 -177
package/src/lib/i18n.tsx
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
-
|
|
5
|
-
export const locales = ["en", "pt-BR"] as const;
|
|
6
|
-
export type Locale = (typeof locales)[number];
|
|
7
|
-
export type Messages<T> = Record<Locale, T>;
|
|
8
|
-
|
|
9
|
-
/** Declares a message catalog; every locale must provide every key. */
|
|
10
|
-
export function defineMessages<T>(messages: Messages<T>) {
|
|
11
|
-
return messages;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const LocaleContext = createContext<Locale>("en");
|
|
15
|
-
|
|
16
|
-
export function LocaleProvider({
|
|
17
|
-
locale,
|
|
18
|
-
children,
|
|
19
|
-
}: {
|
|
20
|
-
locale: Locale;
|
|
21
|
-
children: ReactNode;
|
|
22
|
-
}) {
|
|
23
|
-
return (
|
|
24
|
-
<LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function useLocale() {
|
|
29
|
-
return useContext(LocaleContext);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function useMessages<T>(messages: Messages<T>): T {
|
|
33
|
-
return messages[useLocale()];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Picks the supported locale that best matches a list such as navigator.languages. */
|
|
37
|
-
export function matchLocale(preferred: readonly string[]): Locale {
|
|
38
|
-
for (const tag of preferred) {
|
|
39
|
-
const lower = tag.toLowerCase();
|
|
40
|
-
if (lower.startsWith("pt")) return "pt-BR";
|
|
41
|
-
if (lower.startsWith("en")) return "en";
|
|
42
|
-
}
|
|
43
|
-
return "en";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const uiMessages = defineMessages({
|
|
47
|
-
en: {
|
|
48
|
-
sidebar: "Sidebar",
|
|
49
|
-
toggleSidebar: "Toggle sidebar",
|
|
50
|
-
collapseSidebar: "Collapse sidebar",
|
|
51
|
-
expandSidebar: "Expand sidebar",
|
|
52
|
-
resizeSidebar: "Resize sidebar",
|
|
53
|
-
modeOpen: "Expanded",
|
|
54
|
-
modeCompact: "Compact",
|
|
55
|
-
modeClosed: "Hidden",
|
|
56
|
-
sidebarHint: (shortcut: string) =>
|
|
57
|
-
`Right-click for options · ${shortcut} hides`,
|
|
58
|
-
search: "Search",
|
|
59
|
-
noResults: "No results",
|
|
60
|
-
noData: "Nothing here yet",
|
|
61
|
-
clearSearch: "Clear search",
|
|
62
|
-
retry: "Retry",
|
|
63
|
-
loadFailed: "Couldn't load data",
|
|
64
|
-
staleData: "Showing data from the last successful load",
|
|
65
|
-
previous: "Previous",
|
|
66
|
-
next: "Next",
|
|
67
|
-
open: "Open",
|
|
68
|
-
page: (page: number, total: number) => `Page ${page} of ${total}`,
|
|
69
|
-
rows: (count: number) => `${count} ${count === 1 ? "row" : "rows"}`,
|
|
70
|
-
close: "Close",
|
|
71
|
-
language: "Language",
|
|
72
|
-
toggleTheme: "Toggle theme",
|
|
73
|
-
more: "More",
|
|
74
|
-
loading: "Loading",
|
|
75
|
-
},
|
|
76
|
-
"pt-BR": {
|
|
77
|
-
sidebar: "Barra lateral",
|
|
78
|
-
toggleSidebar: "Alternar barra lateral",
|
|
79
|
-
collapseSidebar: "Recolher barra lateral",
|
|
80
|
-
expandSidebar: "Expandir barra lateral",
|
|
81
|
-
resizeSidebar: "Redimensionar barra lateral",
|
|
82
|
-
modeOpen: "Expandida",
|
|
83
|
-
modeCompact: "Compacta",
|
|
84
|
-
modeClosed: "Escondida",
|
|
85
|
-
sidebarHint: (shortcut: string) =>
|
|
86
|
-
`Clique direito para opções · ${shortcut} esconde`,
|
|
87
|
-
search: "Buscar",
|
|
88
|
-
noResults: "Nenhum resultado",
|
|
89
|
-
noData: "Nada por aqui ainda",
|
|
90
|
-
clearSearch: "Limpar busca",
|
|
91
|
-
retry: "Tentar novamente",
|
|
92
|
-
loadFailed: "Não foi possível carregar os dados",
|
|
93
|
-
staleData: "Exibindo dados do último carregamento",
|
|
94
|
-
previous: "Anterior",
|
|
95
|
-
next: "Próxima",
|
|
96
|
-
open: "Abrir",
|
|
97
|
-
page: (page: number, total: number) => `Página ${page} de ${total}`,
|
|
98
|
-
rows: (count: number) => `${count} ${count === 1 ? "linha" : "linhas"}`,
|
|
99
|
-
close: "Fechar",
|
|
100
|
-
language: "Idioma",
|
|
101
|
-
toggleTheme: "Alternar tema",
|
|
102
|
-
more: "Mais",
|
|
103
|
-
loading: "Carregando",
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
export function useUiMessages() {
|
|
108
|
-
return useMessages(uiMessages);
|
|
109
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export const locales = ["en", "pt-BR"] as const;
|
|
6
|
+
export type Locale = (typeof locales)[number];
|
|
7
|
+
export type Messages<T> = Record<Locale, T>;
|
|
8
|
+
|
|
9
|
+
/** Declares a message catalog; every locale must provide every key. */
|
|
10
|
+
export function defineMessages<T>(messages: Messages<T>) {
|
|
11
|
+
return messages;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LocaleContext = createContext<Locale>("en");
|
|
15
|
+
|
|
16
|
+
export function LocaleProvider({
|
|
17
|
+
locale,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
locale: Locale;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useLocale() {
|
|
29
|
+
return useContext(LocaleContext);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useMessages<T>(messages: Messages<T>): T {
|
|
33
|
+
return messages[useLocale()];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Picks the supported locale that best matches a list such as navigator.languages. */
|
|
37
|
+
export function matchLocale(preferred: readonly string[]): Locale {
|
|
38
|
+
for (const tag of preferred) {
|
|
39
|
+
const lower = tag.toLowerCase();
|
|
40
|
+
if (lower.startsWith("pt")) return "pt-BR";
|
|
41
|
+
if (lower.startsWith("en")) return "en";
|
|
42
|
+
}
|
|
43
|
+
return "en";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const uiMessages = defineMessages({
|
|
47
|
+
en: {
|
|
48
|
+
sidebar: "Sidebar",
|
|
49
|
+
toggleSidebar: "Toggle sidebar",
|
|
50
|
+
collapseSidebar: "Collapse sidebar",
|
|
51
|
+
expandSidebar: "Expand sidebar",
|
|
52
|
+
resizeSidebar: "Resize sidebar",
|
|
53
|
+
modeOpen: "Expanded",
|
|
54
|
+
modeCompact: "Compact",
|
|
55
|
+
modeClosed: "Hidden",
|
|
56
|
+
sidebarHint: (shortcut: string) =>
|
|
57
|
+
`Right-click for options · ${shortcut} hides`,
|
|
58
|
+
search: "Search",
|
|
59
|
+
noResults: "No results",
|
|
60
|
+
noData: "Nothing here yet",
|
|
61
|
+
clearSearch: "Clear search",
|
|
62
|
+
retry: "Retry",
|
|
63
|
+
loadFailed: "Couldn't load data",
|
|
64
|
+
staleData: "Showing data from the last successful load",
|
|
65
|
+
previous: "Previous",
|
|
66
|
+
next: "Next",
|
|
67
|
+
open: "Open",
|
|
68
|
+
page: (page: number, total: number) => `Page ${page} of ${total}`,
|
|
69
|
+
rows: (count: number) => `${count} ${count === 1 ? "row" : "rows"}`,
|
|
70
|
+
close: "Close",
|
|
71
|
+
language: "Language",
|
|
72
|
+
toggleTheme: "Toggle theme",
|
|
73
|
+
more: "More",
|
|
74
|
+
loading: "Loading",
|
|
75
|
+
},
|
|
76
|
+
"pt-BR": {
|
|
77
|
+
sidebar: "Barra lateral",
|
|
78
|
+
toggleSidebar: "Alternar barra lateral",
|
|
79
|
+
collapseSidebar: "Recolher barra lateral",
|
|
80
|
+
expandSidebar: "Expandir barra lateral",
|
|
81
|
+
resizeSidebar: "Redimensionar barra lateral",
|
|
82
|
+
modeOpen: "Expandida",
|
|
83
|
+
modeCompact: "Compacta",
|
|
84
|
+
modeClosed: "Escondida",
|
|
85
|
+
sidebarHint: (shortcut: string) =>
|
|
86
|
+
`Clique direito para opções · ${shortcut} esconde`,
|
|
87
|
+
search: "Buscar",
|
|
88
|
+
noResults: "Nenhum resultado",
|
|
89
|
+
noData: "Nada por aqui ainda",
|
|
90
|
+
clearSearch: "Limpar busca",
|
|
91
|
+
retry: "Tentar novamente",
|
|
92
|
+
loadFailed: "Não foi possível carregar os dados",
|
|
93
|
+
staleData: "Exibindo dados do último carregamento",
|
|
94
|
+
previous: "Anterior",
|
|
95
|
+
next: "Próxima",
|
|
96
|
+
open: "Abrir",
|
|
97
|
+
page: (page: number, total: number) => `Página ${page} de ${total}`,
|
|
98
|
+
rows: (count: number) => `${count} ${count === 1 ? "linha" : "linhas"}`,
|
|
99
|
+
close: "Fechar",
|
|
100
|
+
language: "Idioma",
|
|
101
|
+
toggleTheme: "Alternar tema",
|
|
102
|
+
more: "Mais",
|
|
103
|
+
loading: "Carregando",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export function useUiMessages() {
|
|
108
|
+
return useMessages(uiMessages);
|
|
109
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { cn } from "cn";
|
|
1
|
+
export { cn } from "cn";
|
|
@@ -1,179 +1,177 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
import { cn } from "cn";
|
|
5
|
-
import {
|
|
6
|
-
Sidebar,
|
|
7
|
-
SidebarCollapse,
|
|
8
|
-
SidebarContent,
|
|
9
|
-
SidebarFooter,
|
|
10
|
-
SidebarGroup,
|
|
11
|
-
SidebarGroupLabel,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "../components/ui/
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
logo
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
header
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{/*
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
);
|
|
179
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { cn } from "cn";
|
|
5
|
+
import {
|
|
6
|
+
Sidebar,
|
|
7
|
+
SidebarCollapse,
|
|
8
|
+
SidebarContent,
|
|
9
|
+
SidebarFooter,
|
|
10
|
+
SidebarGroup,
|
|
11
|
+
SidebarGroupLabel,
|
|
12
|
+
SidebarInset,
|
|
13
|
+
SidebarMenu,
|
|
14
|
+
SidebarMenuBadge,
|
|
15
|
+
SidebarMenuButton,
|
|
16
|
+
SidebarMenuItem,
|
|
17
|
+
SidebarProvider,
|
|
18
|
+
SidebarRail,
|
|
19
|
+
SidebarTrigger,
|
|
20
|
+
type SidebarMode,
|
|
21
|
+
} from "../components/ui/sidebar";
|
|
22
|
+
import { Separator } from "../components/ui/separator";
|
|
23
|
+
import { TooltipProvider } from "../components/ui/tooltip";
|
|
24
|
+
|
|
25
|
+
export interface NavigationItem {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
icon?: ReactNode;
|
|
29
|
+
count?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface NavigationGroup {
|
|
32
|
+
label?: string;
|
|
33
|
+
items: NavigationItem[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AppShellProps {
|
|
37
|
+
/** Square logo; stays in place in every sidebar mode. */
|
|
38
|
+
logo: ReactNode;
|
|
39
|
+
/** App name next to the logo; fades out when the sidebar is compact. */
|
|
40
|
+
name: string;
|
|
41
|
+
navigation: NavigationGroup[];
|
|
42
|
+
activeId: string;
|
|
43
|
+
onNavigate: (id: string) => void;
|
|
44
|
+
/** Rendered in the header next to the sidebar trigger, e.g. breadcrumbs. */
|
|
45
|
+
header?: ReactNode;
|
|
46
|
+
/** Rendered at the end of the header, e.g. locale or theme switchers. */
|
|
47
|
+
actions?: ReactNode;
|
|
48
|
+
/** Below the logo in the sidebar header, e.g. `WorkspaceSwitcher`. */
|
|
49
|
+
sidebarHeader?: ReactNode;
|
|
50
|
+
/** Sidebar footer, e.g. `UserMenu`. */
|
|
51
|
+
footer?: ReactNode;
|
|
52
|
+
defaultMode?: SidebarMode;
|
|
53
|
+
defaultWidth?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Locks the shell to the viewport height so content (e.g. a `DataTable
|
|
56
|
+
* fillHeight`) fills the remaining space and scrolls internally.
|
|
57
|
+
*/
|
|
58
|
+
fillHeight?: boolean;
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Items without an icon still need something to show in compact mode.
|
|
63
|
+
function ItemIcon({ item }: { item: NavigationItem }) {
|
|
64
|
+
return (
|
|
65
|
+
item.icon ?? (
|
|
66
|
+
<span
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
className="flex size-4 items-center justify-center text-xs font-semibold"
|
|
69
|
+
>
|
|
70
|
+
{item.label.charAt(0).toUpperCase()}
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The collapse control lives inside the sidebar; reopening needs one outside.
|
|
77
|
+
// Shown on mobile and, on desktop, only while the sidebar is hidden (CSS, so
|
|
78
|
+
// it is right from the first paint).
|
|
79
|
+
function HeaderTrigger() {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex items-center md:hidden md:group-has-data-[collapsible=offcanvas]/sidebar-wrapper:flex">
|
|
82
|
+
<SidebarTrigger className="-ml-1" />
|
|
83
|
+
<Separator
|
|
84
|
+
orientation="vertical"
|
|
85
|
+
className="mr-2 ml-2 h-4 self-center!"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function AppShell({
|
|
92
|
+
logo,
|
|
93
|
+
name,
|
|
94
|
+
navigation,
|
|
95
|
+
activeId,
|
|
96
|
+
onNavigate,
|
|
97
|
+
header,
|
|
98
|
+
actions,
|
|
99
|
+
footer,
|
|
100
|
+
sidebarHeader,
|
|
101
|
+
defaultMode,
|
|
102
|
+
defaultWidth,
|
|
103
|
+
fillHeight = false,
|
|
104
|
+
children,
|
|
105
|
+
}: AppShellProps) {
|
|
106
|
+
return (
|
|
107
|
+
<TooltipProvider delay={600}>
|
|
108
|
+
<SidebarProvider defaultMode={defaultMode} defaultWidth={defaultWidth}>
|
|
109
|
+
<Sidebar>
|
|
110
|
+
{/* Top row: fixed h-14 to match the page header with identical vertical alignment in all apps */}
|
|
111
|
+
{/* Keep the mark anchored to the left while the sidebar width animates. */}
|
|
112
|
+
<div className="flex h-14 shrink-0 items-center gap-2 px-2 overflow-hidden">
|
|
113
|
+
<div className="flex size-8 shrink-0 items-center justify-center [&>*]:shrink-0">
|
|
114
|
+
{logo}
|
|
115
|
+
</div>
|
|
116
|
+
<span className="flex-1 truncate font-semibold whitespace-nowrap transition-opacity duration-200 ease-linear group-data-[collapsible=icon]:hidden">
|
|
117
|
+
{name}
|
|
118
|
+
</span>
|
|
119
|
+
<SidebarCollapse className="group-data-[collapsible=icon]:hidden" />
|
|
120
|
+
</div>
|
|
121
|
+
<div className="hidden group-data-[collapsible=icon]:flex justify-center pb-2">
|
|
122
|
+
<SidebarCollapse />
|
|
123
|
+
</div>
|
|
124
|
+
{sidebarHeader ? (
|
|
125
|
+
<div className="p-2 pt-0">{sidebarHeader}</div>
|
|
126
|
+
) : null}
|
|
127
|
+
<SidebarContent>
|
|
128
|
+
{navigation.map((group, index) => (
|
|
129
|
+
<SidebarGroup key={group.label ?? index}>
|
|
130
|
+
{group.label ? (
|
|
131
|
+
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
132
|
+
) : null}
|
|
133
|
+
<SidebarMenu>
|
|
134
|
+
{group.items.map((item) => (
|
|
135
|
+
<SidebarMenuItem key={item.id}>
|
|
136
|
+
<SidebarMenuButton
|
|
137
|
+
isActive={item.id === activeId}
|
|
138
|
+
tooltip={item.label}
|
|
139
|
+
aria-current={item.id === activeId ? "page" : undefined}
|
|
140
|
+
onClick={() => onNavigate(item.id)}
|
|
141
|
+
>
|
|
142
|
+
<ItemIcon item={item} />
|
|
143
|
+
<span>{item.label}</span>
|
|
144
|
+
</SidebarMenuButton>
|
|
145
|
+
{item.count !== undefined ? (
|
|
146
|
+
<SidebarMenuBadge>{item.count}</SidebarMenuBadge>
|
|
147
|
+
) : null}
|
|
148
|
+
</SidebarMenuItem>
|
|
149
|
+
))}
|
|
150
|
+
</SidebarMenu>
|
|
151
|
+
</SidebarGroup>
|
|
152
|
+
))}
|
|
153
|
+
</SidebarContent>
|
|
154
|
+
{footer ? <SidebarFooter>{footer}</SidebarFooter> : null}
|
|
155
|
+
<SidebarRail />
|
|
156
|
+
</Sidebar>
|
|
157
|
+
<SidebarInset className={fillHeight ? "h-svh min-h-0" : undefined}>
|
|
158
|
+
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b bg-background px-4 md:px-6">
|
|
159
|
+
<HeaderTrigger />
|
|
160
|
+
<div className="min-w-0 flex-1">{header}</div>
|
|
161
|
+
{actions ? (
|
|
162
|
+
<div className="flex items-center gap-2">{actions}</div>
|
|
163
|
+
) : null}
|
|
164
|
+
</header>
|
|
165
|
+
<div
|
|
166
|
+
className={cn(
|
|
167
|
+
"flex flex-1 flex-col gap-4 p-4 md:gap-6 md:p-6",
|
|
168
|
+
fillHeight && "min-h-0 overflow-hidden",
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{children}
|
|
172
|
+
</div>
|
|
173
|
+
</SidebarInset>
|
|
174
|
+
</SidebarProvider>
|
|
175
|
+
</TooltipProvider>
|
|
176
|
+
);
|
|
177
|
+
}
|