@dataif/cli 0.1.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/README.md +16 -0
- package/bin/dataif.js +623 -0
- package/package.json +26 -0
- package/scripts/build-template.mjs +72 -0
- package/templates/dataif/README.md +157 -0
- package/templates/dataif/infra/.env.example +119 -0
- package/templates/dataif/infra/.env.stg.example +119 -0
- package/templates/dataif/infra/airflow/Dockerfile +11 -0
- package/templates/dataif/infra/airflow/Dockerfile.release +17 -0
- package/templates/dataif/infra/airflow/requirements.txt +3 -0
- package/templates/dataif/infra/docker-compose.yml +306 -0
- package/templates/dataif/infra/init-db/01-init-dataif.sh +129 -0
- package/templates/dataif/infra/init-db/pnp-curated-views.sqlinc +444 -0
- package/templates/dataif/infra/init-db/pnp-raw-staging-curated.sqlinc +701 -0
- package/templates/dataif/infra/keycloak/Dockerfile +4 -0
- package/templates/dataif/infra/keycloak/realm-dataif.json +73 -0
- package/templates/dataif/infra/ollama/Dockerfile +9 -0
- package/templates/dataif/infra/ollama/bootstrap-model.sh +100 -0
- package/templates/dataif/infra/ollama/sabia-7b.Modelfile +14 -0
- package/templates/dataif/infra/postgres/Dockerfile +4 -0
- package/templates/dataif/pipelines/airflow/dags/generated/.gitkeep +1 -0
- package/templates/dataif/pipelines/airflow/dags/generated/2020_financeiro_fcc6f1f3_sync.py +9 -0
- package/templates/dataif/pipelines/dataif_pipelines/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/airflow/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/airflow/pnp_pipeline_factory.py +167 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/base/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/base/connector.py +28 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/base/types.py +14 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/config.py +19 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/connector.py +558 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/powerbi_microdados.py +728 -0
- package/templates/dataif/pipelines/dataif_pipelines/connectors/nilo_pecanha/transform.py +296 -0
- package/templates/dataif/pipelines/dataif_pipelines/jobs/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/jobs/nilo_pipeline.py +112 -0
- package/templates/dataif/pipelines/dataif_pipelines/orchestration/__init__.py +21 -0
- package/templates/dataif/pipelines/dataif_pipelines/orchestration/pnp_workflow.py +783 -0
- package/templates/dataif/pipelines/dataif_pipelines/repositories/__init__.py +1 -0
- package/templates/dataif/pipelines/dataif_pipelines/repositories/pnp_raw_repository.py +860 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/__init__.py +19 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_curated_service.py +66 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_download_service.py +534 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_quality_service.py +9 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_raw_ingestion_service.py +124 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/pnp_staging_service.py +271 -0
- package/templates/dataif/pipelines/dataif_pipelines/services/powerbi_catalog_service.py +159 -0
- package/templates/dataif/pipelines/sql/staging/020_pnp_matriculas.sql +112 -0
- package/templates/dataif/pipelines/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
- package/templates/dataif/pipelines/sql/staging/040_pnp_servidores.sql +90 -0
- package/templates/dataif/pipelines/sql/staging/050_pnp_financeiro.sql +72 -0
- package/templates/dataif/pipelines/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
- package/templates/dataif/pipelines/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
- package/templates/dataif/pipelines/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
- package/templates/dataif/pipelines/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
- package/templates/dataif/pipelines/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
- package/templates/dataif/pipelines/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
- package/templates/dataif/pipelines/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
- package/templates/dataif/pipelines/sql/views_curated/070_vw_pnp_vanna.sql +115 -0
- package/templates/dataif/scripts/configure-env.sh +149 -0
- package/templates/dataif/scripts/create_metabase_pnp_dashboard.py +943 -0
- package/templates/dataif/scripts/create_metabase_pnp_matriculas_dashboard.py +580 -0
- package/templates/dataif/scripts/deploy.sh +79 -0
- package/templates/dataif/scripts/fix_metabase_template_tag_ids.py +91 -0
- package/templates/dataif/scripts/pnp_powerbi_microdados_probe.py +14 -0
- package/templates/dataif/scripts/pnp_validate_raw_run.py +330 -0
- package/templates/dataif/scripts/publish-images.sh +31 -0
- package/templates/dataif/scripts/sync_metabase_dashboard_field_filters.py +241 -0
- package/templates/dataif/scripts/use-vanna-ollama.sh +139 -0
- package/templates/dataif/services/api/.dockerignore +18 -0
- package/templates/dataif/services/api/Dockerfile +12 -0
- package/templates/dataif/services/api/app/__init__.py +1 -0
- package/templates/dataif/services/api/app/auth.py +48 -0
- package/templates/dataif/services/api/app/config.py +59 -0
- package/templates/dataif/services/api/app/keycloak_admin.py +215 -0
- package/templates/dataif/services/api/app/main.py +2432 -0
- package/templates/dataif/services/api/app/metabase_admin.py +191 -0
- package/templates/dataif/services/api/app/metabase_bootstrap.py +44 -0
- package/templates/dataif/services/api/app/metabase_embed.py +15 -0
- package/templates/dataif/services/api/app/pnp_dag_provisioner.py +113 -0
- package/templates/dataif/services/api/app/pnp_instance_repository.py +951 -0
- package/templates/dataif/services/api/app/pnp_powerbi.py +438 -0
- package/templates/dataif/services/api/app/vanna_client.py +32 -0
- package/templates/dataif/services/api/requirements.txt +9 -0
- package/templates/dataif/services/vanna/.dockerignore +18 -0
- package/templates/dataif/services/vanna/Dockerfile +12 -0
- package/templates/dataif/services/vanna/app/config.py +57 -0
- package/templates/dataif/services/vanna/app/main.py +108 -0
- package/templates/dataif/services/vanna/app/runtime_config.py +114 -0
- package/templates/dataif/services/vanna/app/sql_guard.py +123 -0
- package/templates/dataif/services/vanna/app/vanna_engine.py +382 -0
- package/templates/dataif/services/vanna/requirements.txt +8 -0
- package/templates/dataif/services/web/.dockerignore +13 -0
- package/templates/dataif/services/web/Dockerfile +16 -0
- package/templates/dataif/services/web/index.html +12 -0
- package/templates/dataif/services/web/nginx.conf +74 -0
- package/templates/dataif/services/web/package-lock.json +4397 -0
- package/templates/dataif/services/web/package.json +32 -0
- package/templates/dataif/services/web/postcss.config.mjs +5 -0
- package/templates/dataif/services/web/src/App.jsx +2817 -0
- package/templates/dataif/services/web/src/adminAuth.js +245 -0
- package/templates/dataif/services/web/src/assets/avatar_placeholder.png +0 -0
- package/templates/dataif/services/web/src/assets/github_logo_icon_229278.svg +1 -0
- package/templates/dataif/services/web/src/assets/if-logo.png +0 -0
- package/templates/dataif/services/web/src/assets/if.svg +0 -0
- package/templates/dataif/services/web/src/assets/pnp-horizontal.svg +1 -0
- package/templates/dataif/services/web/src/components/AppHeader.jsx +233 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/mobile-header.tsx +56 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-account-card.tsx +209 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item-button.tsx +67 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-item.tsx +108 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/base-components/nav-list.tsx +83 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/config.ts +23 -0
- package/templates/dataif/services/web/src/components/application/app-navigation/header-navigation.tsx +240 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination-base.tsx +376 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination-dot.tsx +52 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination-line.tsx +48 -0
- package/templates/dataif/services/web/src/components/application/pagination/pagination.tsx +328 -0
- package/templates/dataif/services/web/src/components/application/tabs/tabs.tsx +223 -0
- package/templates/dataif/services/web/src/components/base/avatar/avatar-label-group.tsx +28 -0
- package/templates/dataif/services/web/src/components/base/avatar/avatar.tsx +129 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-add-button.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-company-icon.tsx +24 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/avatar-online-indicator.tsx +29 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/index.tsx +4 -0
- package/templates/dataif/services/web/src/components/base/avatar/base-components/verified-tick.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/badges/badge-types.ts +264 -0
- package/templates/dataif/services/web/src/components/base/badges/badges.tsx +415 -0
- package/templates/dataif/services/web/src/components/base/button-group/button-group.tsx +104 -0
- package/templates/dataif/services/web/src/components/base/buttons/button.tsx +267 -0
- package/templates/dataif/services/web/src/components/base/input/hint-text.tsx +31 -0
- package/templates/dataif/services/web/src/components/base/input/input.tsx +269 -0
- package/templates/dataif/services/web/src/components/base/input/label.tsx +48 -0
- package/templates/dataif/services/web/src/components/base/radio-buttons/radio-buttons.tsx +127 -0
- package/templates/dataif/services/web/src/components/base/select/combobox.tsx +150 -0
- package/templates/dataif/services/web/src/components/base/select/multi-select.tsx +361 -0
- package/templates/dataif/services/web/src/components/base/select/popover.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/select/select-item.tsx +95 -0
- package/templates/dataif/services/web/src/components/base/select/select-native.tsx +67 -0
- package/templates/dataif/services/web/src/components/base/select/select.tsx +144 -0
- package/templates/dataif/services/web/src/components/base/tags/base-components/tag-close-x.tsx +32 -0
- package/templates/dataif/services/web/src/components/base/tooltip/tooltip.tsx +107 -0
- package/templates/dataif/services/web/src/components/foundations/dot-icon.tsx +22 -0
- package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo-minimal.tsx +170 -0
- package/templates/dataif/services/web/src/components/foundations/logo/untitledui-logo.tsx +58 -0
- package/templates/dataif/services/web/src/hooks/use-breakpoint.ts +34 -0
- package/templates/dataif/services/web/src/hooks/use-resize-observer.ts +67 -0
- package/templates/dataif/services/web/src/main.jsx +14 -0
- package/templates/dataif/services/web/src/providers/theme-provider.jsx +62 -0
- package/templates/dataif/services/web/src/styles/globals.css +60 -0
- package/templates/dataif/services/web/src/styles/theme.css +1326 -0
- package/templates/dataif/services/web/src/styles/typography.css +430 -0
- package/templates/dataif/services/web/src/styles.css +1287 -0
- package/templates/dataif/services/web/src/utils/cx.ts +24 -0
- package/templates/dataif/services/web/src/utils/is-react-component.ts +33 -0
- package/templates/dataif/services/web/vite.config.js +14 -0
- package/templates/dataif/sql/ddl/001_schemas.sql +6 -0
- package/templates/dataif/sql/ddl/003_pnp_raw_staging_curated.sql +699 -0
- package/templates/dataif/sql/migrations/001_pnp_phase1_backfill.sql +3 -0
- package/templates/dataif/sql/migrations/002_pnp_phase2_admin_config_backfill.sql +184 -0
- package/templates/dataif/sql/migrations/003_pnp_phase3_raw_tabular_backfill.sql +3 -0
- package/templates/dataif/sql/migrations/004_pnp_phase3_raw_backfill_support_index.sql +3 -0
- package/templates/dataif/sql/migrations/005_pnp_phase7_staging_support_indexes.sql +2 -0
- package/templates/dataif/sql/migrations/006_pnp_phase7_staging_autovacuum_tuning.sql +2 -0
- package/templates/dataif/sql/migrations/007_pnp_phase7b_run_packages.sql +20 -0
- package/templates/dataif/sql/migrations/008_pnp_phase7a_pipeline_endpoints.sql +169 -0
- package/templates/dataif/sql/migrations/009_pnp_phase8_curated.sql +35 -0
- package/templates/dataif/sql/migrations/010_pnp_phase10_staging_incremental_upsert.sql +3 -0
- package/templates/dataif/sql/migrations/010_pnp_pipeline_uuid.sql +51 -0
- package/templates/dataif/sql/migrations/011_app_settings.sql +7 -0
- package/templates/dataif/sql/staging/020_pnp_matriculas.sql +112 -0
- package/templates/dataif/sql/staging/030_pnp_eficiencia_academica.sql +83 -0
- package/templates/dataif/sql/staging/040_pnp_servidores.sql +90 -0
- package/templates/dataif/sql/staging/050_pnp_financeiro.sql +72 -0
- package/templates/dataif/sql/views_curated/003_vw_pnp_microdados_admin.sql +160 -0
- package/templates/dataif/sql/views_curated/004_mv_pnp_dashboard_fast.sql +204 -0
- package/templates/dataif/sql/views_curated/010_vw_pnp_admin_ingestao.sql +51 -0
- package/templates/dataif/sql/views_curated/020_vw_pnp_qualidade_dados.sql +114 -0
- package/templates/dataif/sql/views_curated/030_vw_pnp_matriculas.sql +67 -0
- package/templates/dataif/sql/views_curated/040_vw_pnp_eficiencia.sql +33 -0
- package/templates/dataif/sql/views_curated/050_vw_pnp_servidores.sql +30 -0
- package/templates/dataif/sql/views_curated/060_vw_pnp_financeiro.sql +22 -0
- package/templates/dataif/sql/views_curated/070_vw_pnp_vanna.sql +115 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Moon01, Sun } from "@untitledui/icons";
|
|
4
|
+
|
|
5
|
+
import avatarPlaceholder from "@/assets/avatar_placeholder.png";
|
|
6
|
+
import githubLogo from "@/assets/github_logo_icon_229278.svg";
|
|
7
|
+
import ifLogo from "@/assets/if-logo.png";
|
|
8
|
+
import { useTheme } from "@/providers/theme-provider";
|
|
9
|
+
|
|
10
|
+
function routeToHref(path) {
|
|
11
|
+
return `#${path}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ThemeToggle() {
|
|
15
|
+
const { theme, setTheme } = useTheme();
|
|
16
|
+
const isDark = theme === "dark";
|
|
17
|
+
const Icon = isDark ? Sun : Moon01;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
className="icon-button"
|
|
23
|
+
onClick={() => setTheme(isDark ? "light" : "dark")}
|
|
24
|
+
title={isDark ? "Ativar modo claro" : "Ativar modo escuro"}
|
|
25
|
+
aria-label={isDark ? "Ativar modo claro" : "Ativar modo escuro"}
|
|
26
|
+
>
|
|
27
|
+
<Icon className="size-5" />
|
|
28
|
+
</button>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function HeaderLogo() {
|
|
33
|
+
return (
|
|
34
|
+
<a className="app-brand" href={routeToHref("/")}>
|
|
35
|
+
<span className="app-brand-mark">
|
|
36
|
+
<img src={ifLogo} alt="Instituto Federal" />
|
|
37
|
+
</span>
|
|
38
|
+
<span className="app-brand-label">DataIF</span>
|
|
39
|
+
</a>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function NavLink({ item, active }) {
|
|
44
|
+
return (
|
|
45
|
+
<a className={`nav-link${active ? " active" : ""}`} href={routeToHref(item.path)}>
|
|
46
|
+
{item.label}
|
|
47
|
+
</a>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function AccountMenu({ adminNavItems, auth, onAdminSettings, onLogout, onRequestLogin, onSelectRoute }) {
|
|
52
|
+
const accountLabel =
|
|
53
|
+
auth.claims?.preferred_username ||
|
|
54
|
+
auth.claims?.email ||
|
|
55
|
+
(auth.status === "authenticated" ? "Administrador" : "Convidado");
|
|
56
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
57
|
+
const menuRef = useRef(null);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
function handlePointerDown(event) {
|
|
61
|
+
if (!menuRef.current?.contains(event.target)) {
|
|
62
|
+
setIsOpen(false);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleEscape(event) {
|
|
67
|
+
if (event.key === "Escape") {
|
|
68
|
+
setIsOpen(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
window.addEventListener("mousedown", handlePointerDown);
|
|
73
|
+
window.addEventListener("keydown", handleEscape);
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
window.removeEventListener("mousedown", handlePointerDown);
|
|
77
|
+
window.removeEventListener("keydown", handleEscape);
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="account-menu" ref={menuRef}>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
className="icon-button avatar-button"
|
|
86
|
+
onClick={() => setIsOpen((current) => !current)}
|
|
87
|
+
aria-label="Abrir menu do usuario"
|
|
88
|
+
aria-expanded={isOpen}
|
|
89
|
+
>
|
|
90
|
+
<img src={avatarPlaceholder} alt="" />
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
{isOpen ? (
|
|
94
|
+
<div className="account-dropdown">
|
|
95
|
+
<div className="account-dropdown-head">
|
|
96
|
+
<strong>{accountLabel}</strong>
|
|
97
|
+
<span>{auth.status === "authenticated" ? "Sessão ativa" : "Acesso visitante"}</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{auth.status === "authenticated" ? (
|
|
101
|
+
<>
|
|
102
|
+
<div className="account-dropdown-group">
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className="account-dropdown-item"
|
|
106
|
+
onClick={() => {
|
|
107
|
+
setIsOpen(false);
|
|
108
|
+
onSelectRoute("/configurações");
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
Conta
|
|
112
|
+
</button>
|
|
113
|
+
{adminNavItems.map((item) => (
|
|
114
|
+
<button
|
|
115
|
+
key={item.path}
|
|
116
|
+
type="button"
|
|
117
|
+
className="account-dropdown-item"
|
|
118
|
+
onClick={() => {
|
|
119
|
+
setIsOpen(false);
|
|
120
|
+
onSelectRoute(item.path);
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{item.label}
|
|
124
|
+
</button>
|
|
125
|
+
))}
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
className="account-dropdown-item"
|
|
129
|
+
onClick={() => {
|
|
130
|
+
setIsOpen(false);
|
|
131
|
+
onAdminSettings();
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
Configurações Admin
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className="account-dropdown-item account-dropdown-item-danger"
|
|
141
|
+
onClick={() => {
|
|
142
|
+
setIsOpen(false);
|
|
143
|
+
onLogout();
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
Sair
|
|
147
|
+
</button>
|
|
148
|
+
</>
|
|
149
|
+
) : (
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
className="account-dropdown-item"
|
|
153
|
+
onClick={() => {
|
|
154
|
+
setIsOpen(false);
|
|
155
|
+
onRequestLogin();
|
|
156
|
+
}}
|
|
157
|
+
disabled={auth.status === "loading"}
|
|
158
|
+
>
|
|
159
|
+
{auth.status === "loading" ? "Verificando..." : "Login"}
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
) : null}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function HeaderActions({ adminNavItems, auth, githubRepoUrl, onAdminSettings, onLogout, onRequestLogin, onSelectRoute }) {
|
|
169
|
+
return (
|
|
170
|
+
<>
|
|
171
|
+
<ThemeToggle />
|
|
172
|
+
<a
|
|
173
|
+
className="icon-button"
|
|
174
|
+
href={githubRepoUrl}
|
|
175
|
+
target="_blank"
|
|
176
|
+
rel="noreferrer"
|
|
177
|
+
aria-label="GitHub"
|
|
178
|
+
title="GitHub"
|
|
179
|
+
>
|
|
180
|
+
<img src={githubLogo} alt="" className="github-icon" />
|
|
181
|
+
</a>
|
|
182
|
+
<AccountMenu
|
|
183
|
+
adminNavItems={adminNavItems}
|
|
184
|
+
auth={auth}
|
|
185
|
+
onAdminSettings={onAdminSettings}
|
|
186
|
+
onLogout={onLogout}
|
|
187
|
+
onRequestLogin={onRequestLogin}
|
|
188
|
+
onSelectRoute={onSelectRoute}
|
|
189
|
+
/>
|
|
190
|
+
</>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default function AppHeader({
|
|
195
|
+
auth,
|
|
196
|
+
githubRepoUrl,
|
|
197
|
+
adminNavItems,
|
|
198
|
+
onAdminSettings,
|
|
199
|
+
onLogout,
|
|
200
|
+
onRequestLogin,
|
|
201
|
+
onSelectRoute,
|
|
202
|
+
publicNavItems,
|
|
203
|
+
route,
|
|
204
|
+
}) {
|
|
205
|
+
return (
|
|
206
|
+
<header className="app-header">
|
|
207
|
+
<div className="app-header-inner">
|
|
208
|
+
<div className="app-header-top">
|
|
209
|
+
<HeaderLogo />
|
|
210
|
+
<div className="app-header-actions">
|
|
211
|
+
<HeaderActions
|
|
212
|
+
adminNavItems={adminNavItems}
|
|
213
|
+
auth={auth}
|
|
214
|
+
githubRepoUrl={githubRepoUrl}
|
|
215
|
+
onAdminSettings={onAdminSettings}
|
|
216
|
+
onLogout={onLogout}
|
|
217
|
+
onRequestLogin={onRequestLogin}
|
|
218
|
+
onSelectRoute={onSelectRoute}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<div className="app-header-navs">
|
|
224
|
+
<nav className="nav-row" aria-label="Navegacao principal">
|
|
225
|
+
{publicNavItems.map((item) => (
|
|
226
|
+
<NavLink key={item.path} item={item} active={route === item.path} />
|
|
227
|
+
))}
|
|
228
|
+
</nav>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</header>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PropsWithChildren, ReactNode } from "react";
|
|
2
|
+
import { X as CloseIcon, Menu02 } from "@untitledui/icons";
|
|
3
|
+
import {
|
|
4
|
+
Button as AriaButton,
|
|
5
|
+
Dialog as AriaDialog,
|
|
6
|
+
DialogTrigger as AriaDialogTrigger,
|
|
7
|
+
Modal as AriaModal,
|
|
8
|
+
ModalOverlay as AriaModalOverlay,
|
|
9
|
+
} from "react-aria-components";
|
|
10
|
+
import { UntitledLogo } from "@/components/foundations/logo/untitledui-logo";
|
|
11
|
+
import { cx } from "@/utils/cx";
|
|
12
|
+
|
|
13
|
+
export const MobileNavigationHeader = ({ children, logo }: PropsWithChildren<{ logo?: ReactNode }>) => {
|
|
14
|
+
return (
|
|
15
|
+
<AriaDialogTrigger>
|
|
16
|
+
<header className="flex h-16 items-center justify-between border-b border-secondary bg-primary py-3 pr-2 pl-4 lg:hidden">
|
|
17
|
+
{logo || <UntitledLogo />}
|
|
18
|
+
|
|
19
|
+
<AriaButton
|
|
20
|
+
aria-label="Expand navigation menu"
|
|
21
|
+
className="group flex items-center justify-center rounded-lg bg-primary p-2 text-fg-secondary outline-focus-ring hover:bg-primary_hover hover:text-fg-secondary_hover focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
22
|
+
>
|
|
23
|
+
<Menu02 className="size-6 transition duration-200 ease-in-out group-aria-expanded:opacity-0" />
|
|
24
|
+
<CloseIcon className="absolute size-6 opacity-0 transition duration-200 ease-in-out group-aria-expanded:opacity-100" />
|
|
25
|
+
</AriaButton>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<AriaModalOverlay
|
|
29
|
+
isDismissable
|
|
30
|
+
className={({ isEntering, isExiting }) =>
|
|
31
|
+
cx(
|
|
32
|
+
"fixed inset-0 z-50 cursor-pointer bg-overlay/70 pr-16 backdrop-blur-md lg:hidden",
|
|
33
|
+
isEntering && "duration-300 ease-in-out animate-in fade-in",
|
|
34
|
+
isExiting && "duration-200 ease-in-out animate-out fade-out",
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{({ state }) => (
|
|
39
|
+
<>
|
|
40
|
+
<AriaButton
|
|
41
|
+
aria-label="Close navigation menu"
|
|
42
|
+
onPress={() => state.close()}
|
|
43
|
+
className="fixed top-3 right-2 flex cursor-pointer items-center justify-center rounded-lg p-2 text-fg-white/70 outline-focus-ring hover:bg-white/10 hover:text-fg-white focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
44
|
+
>
|
|
45
|
+
<CloseIcon className="size-6" />
|
|
46
|
+
</AriaButton>
|
|
47
|
+
|
|
48
|
+
<AriaModal className="w-full cursor-auto will-change-transform">
|
|
49
|
+
<AriaDialog className="h-dvh outline-hidden focus:outline-hidden">{children}</AriaDialog>
|
|
50
|
+
</AriaModal>
|
|
51
|
+
</>
|
|
52
|
+
)}
|
|
53
|
+
</AriaModalOverlay>
|
|
54
|
+
</AriaDialogTrigger>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { FC, HTMLAttributes } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import type { Placement } from "@react-types/overlays";
|
|
4
|
+
import { BookOpen01, ChevronSelectorVertical, LogOut01, Plus, Settings01, User01 } from "@untitledui/icons";
|
|
5
|
+
import { useFocusManager } from "react-aria";
|
|
6
|
+
import type { DialogProps as AriaDialogProps } from "react-aria-components";
|
|
7
|
+
import { Button as AriaButton, Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Popover as AriaPopover } from "react-aria-components";
|
|
8
|
+
import { AvatarLabelGroup } from "@/components/base/avatar/avatar-label-group";
|
|
9
|
+
import { Button } from "@/components/base/buttons/button";
|
|
10
|
+
import { RadioButtonBase } from "@/components/base/radio-buttons/radio-buttons";
|
|
11
|
+
import { useBreakpoint } from "@/hooks/use-breakpoint";
|
|
12
|
+
import { cx } from "@/utils/cx";
|
|
13
|
+
|
|
14
|
+
type NavAccountType = {
|
|
15
|
+
/** Unique identifier for the nav item. */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Name of the account holder. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Email address of the account holder. */
|
|
20
|
+
email: string;
|
|
21
|
+
/** Avatar image URL. */
|
|
22
|
+
avatar: string;
|
|
23
|
+
/** Online status of the account holder. This is used to display the online status indicator. */
|
|
24
|
+
status: "online" | "offline";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const placeholderAccounts: NavAccountType[] = [
|
|
28
|
+
{
|
|
29
|
+
id: "olivia",
|
|
30
|
+
name: "Olivia Rhye",
|
|
31
|
+
email: "olivia@untitledui.com",
|
|
32
|
+
avatar: "https://www.untitledui.com/images/avatars/olivia-rhye?fm=webp&q=80",
|
|
33
|
+
status: "online",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "sienna",
|
|
37
|
+
name: "Sienna Hewitt",
|
|
38
|
+
email: "sienna@untitledui.com",
|
|
39
|
+
avatar: "https://www.untitledui.com/images/avatars/transparent/sienna-hewitt?bg=%23E0E0E0",
|
|
40
|
+
status: "online",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const NavAccountMenu = ({
|
|
45
|
+
className,
|
|
46
|
+
selectedAccountId = "olivia",
|
|
47
|
+
...dialogProps
|
|
48
|
+
}: AriaDialogProps & { className?: string; accounts?: NavAccountType[]; selectedAccountId?: string }) => {
|
|
49
|
+
const focusManager = useFocusManager();
|
|
50
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
|
|
52
|
+
const onKeyDown = useCallback(
|
|
53
|
+
(e: KeyboardEvent) => {
|
|
54
|
+
switch (e.key) {
|
|
55
|
+
case "ArrowDown":
|
|
56
|
+
focusManager?.focusNext({ tabbable: true, wrap: true });
|
|
57
|
+
break;
|
|
58
|
+
case "ArrowUp":
|
|
59
|
+
focusManager?.focusPrevious({ tabbable: true, wrap: true });
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[focusManager],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const element = dialogRef.current;
|
|
68
|
+
if (element) {
|
|
69
|
+
element.addEventListener("keydown", onKeyDown);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
if (element) {
|
|
74
|
+
element.removeEventListener("keydown", onKeyDown);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}, [onKeyDown]);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<AriaDialog
|
|
81
|
+
{...dialogProps}
|
|
82
|
+
ref={dialogRef}
|
|
83
|
+
className={cx("w-66 rounded-xl bg-secondary_alt shadow-lg ring ring-secondary_alt outline-hidden", className)}
|
|
84
|
+
>
|
|
85
|
+
<div className="rounded-xl bg-primary ring-1 ring-secondary">
|
|
86
|
+
<div className="flex flex-col gap-0.5 py-1.5">
|
|
87
|
+
<NavAccountCardMenuItem label="View profile" icon={User01} shortcut="⌘K->P" />
|
|
88
|
+
<NavAccountCardMenuItem label="Account settings" icon={Settings01} shortcut="⌘S" />
|
|
89
|
+
<NavAccountCardMenuItem label="Documentation" icon={BookOpen01} />
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex flex-col gap-0.5 border-t border-secondary py-1.5">
|
|
92
|
+
<div className="px-3 pt-1.5 pb-1 text-xs font-semibold text-tertiary">Switch account</div>
|
|
93
|
+
|
|
94
|
+
<div className="flex flex-col gap-0.5 px-1.5">
|
|
95
|
+
{placeholderAccounts.map((account) => (
|
|
96
|
+
<button
|
|
97
|
+
key={account.id}
|
|
98
|
+
className={cx(
|
|
99
|
+
"relative w-full cursor-pointer rounded-md px-2 py-1.5 text-left outline-focus-ring hover:bg-primary_hover focus:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
100
|
+
account.id === selectedAccountId && "bg-primary_hover",
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<AvatarLabelGroup status="online" size="md" src={account.avatar} title={account.name} subtitle={account.email} />
|
|
104
|
+
|
|
105
|
+
<RadioButtonBase isSelected={account.id === selectedAccountId} className="absolute top-2 right-2" />
|
|
106
|
+
</button>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex flex-col gap-2 px-2 pt-0.5 pb-2">
|
|
111
|
+
<Button iconLeading={Plus} color="secondary" size="sm">
|
|
112
|
+
Add account
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="pt-1 pb-1.5">
|
|
118
|
+
<NavAccountCardMenuItem label="Sign out" icon={LogOut01} shortcut="⌥⇧Q" />
|
|
119
|
+
</div>
|
|
120
|
+
</AriaDialog>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const NavAccountCardMenuItem = ({
|
|
125
|
+
icon: Icon,
|
|
126
|
+
label,
|
|
127
|
+
shortcut,
|
|
128
|
+
...buttonProps
|
|
129
|
+
}: {
|
|
130
|
+
icon?: FC<{ className?: string }>;
|
|
131
|
+
label: string;
|
|
132
|
+
shortcut?: string;
|
|
133
|
+
} & HTMLAttributes<HTMLButtonElement>) => {
|
|
134
|
+
return (
|
|
135
|
+
<button {...buttonProps} className={cx("group/item w-full cursor-pointer px-1.5 focus:outline-hidden", buttonProps.className)}>
|
|
136
|
+
<div
|
|
137
|
+
className={cx(
|
|
138
|
+
"flex w-full items-center justify-between gap-3 rounded-md p-2 group-hover/item:bg-primary_hover",
|
|
139
|
+
// Focus styles.
|
|
140
|
+
"outline-focus-ring group-focus-visible/item:outline-2 group-focus-visible/item:outline-offset-2",
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
<div className="flex gap-2 text-sm font-semibold text-secondary group-hover/item:text-secondary_hover">
|
|
144
|
+
{Icon && <Icon className="size-5 text-fg-quaternary" />} {label}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{shortcut && (
|
|
148
|
+
<kbd className="flex rounded px-1 py-px font-body text-xs font-medium text-tertiary ring-1 ring-secondary ring-inset">{shortcut}</kbd>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const NavAccountCard = ({
|
|
156
|
+
popoverPlacement,
|
|
157
|
+
selectedAccountId = "olivia",
|
|
158
|
+
items = placeholderAccounts,
|
|
159
|
+
}: {
|
|
160
|
+
popoverPlacement?: Placement;
|
|
161
|
+
selectedAccountId?: string;
|
|
162
|
+
items?: NavAccountType[];
|
|
163
|
+
}) => {
|
|
164
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
165
|
+
const isDesktop = useBreakpoint("lg");
|
|
166
|
+
|
|
167
|
+
const selectedAccount = placeholderAccounts.find((account) => account.id === selectedAccountId);
|
|
168
|
+
|
|
169
|
+
if (!selectedAccount) {
|
|
170
|
+
console.warn(`Account with ID ${selectedAccountId} not found in <NavAccountCard />`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div ref={triggerRef} className="relative flex items-center gap-3 rounded-xl p-3 ring-1 ring-secondary ring-inset">
|
|
176
|
+
<AvatarLabelGroup
|
|
177
|
+
size="md"
|
|
178
|
+
src={selectedAccount.avatar}
|
|
179
|
+
title={selectedAccount.name}
|
|
180
|
+
subtitle={selectedAccount.email}
|
|
181
|
+
status={selectedAccount.status}
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
<div className="absolute top-1.5 right-1.5">
|
|
185
|
+
<AriaDialogTrigger>
|
|
186
|
+
<AriaButton className="flex cursor-pointer items-center justify-center rounded-md p-1.5 text-fg-quaternary outline-focus-ring transition duration-100 ease-linear hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-2 focus-visible:outline-offset-2 pressed:bg-primary_hover pressed:text-fg-quaternary_hover">
|
|
187
|
+
<ChevronSelectorVertical className="size-4 shrink-0" />
|
|
188
|
+
</AriaButton>
|
|
189
|
+
<AriaPopover
|
|
190
|
+
placement={popoverPlacement ?? (isDesktop ? "right bottom" : "top right")}
|
|
191
|
+
triggerRef={triggerRef}
|
|
192
|
+
offset={8}
|
|
193
|
+
className={({ isEntering, isExiting }) =>
|
|
194
|
+
cx(
|
|
195
|
+
"origin-(--trigger-anchor-point) will-change-transform",
|
|
196
|
+
isEntering &&
|
|
197
|
+
"duration-150 ease-out animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5",
|
|
198
|
+
isExiting &&
|
|
199
|
+
"duration-100 ease-in animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5",
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
>
|
|
203
|
+
<NavAccountMenu selectedAccountId={selectedAccountId} accounts={items} />
|
|
204
|
+
</AriaPopover>
|
|
205
|
+
</AriaDialogTrigger>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FC, MouseEventHandler } from "react";
|
|
2
|
+
import { Pressable } from "react-aria-components";
|
|
3
|
+
import { Tooltip } from "@/components/base/tooltip/tooltip";
|
|
4
|
+
import { cx } from "@/utils/cx";
|
|
5
|
+
|
|
6
|
+
const styles = {
|
|
7
|
+
md: {
|
|
8
|
+
root: "size-10",
|
|
9
|
+
icon: "size-5",
|
|
10
|
+
},
|
|
11
|
+
lg: {
|
|
12
|
+
root: "size-12",
|
|
13
|
+
icon: "size-6",
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface NavItemButtonProps {
|
|
18
|
+
/** Whether the collapsible nav item is open. */
|
|
19
|
+
open?: boolean;
|
|
20
|
+
/** URL to navigate to when the button is clicked. */
|
|
21
|
+
href?: string;
|
|
22
|
+
/** Label text for the button. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Icon component to display. */
|
|
25
|
+
icon: FC<{ className?: string }>;
|
|
26
|
+
/** Whether the button is currently active. */
|
|
27
|
+
current?: boolean;
|
|
28
|
+
/** Size of the button. */
|
|
29
|
+
size?: "md" | "lg";
|
|
30
|
+
/** Handler for click events. */
|
|
31
|
+
onClick?: MouseEventHandler;
|
|
32
|
+
/** Additional CSS classes to apply to the button. */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Placement of the tooltip. */
|
|
35
|
+
tooltipPlacement?: "top" | "right" | "bottom" | "left";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const NavItemButton = ({
|
|
39
|
+
current: current,
|
|
40
|
+
label,
|
|
41
|
+
href,
|
|
42
|
+
icon: Icon,
|
|
43
|
+
size = "md",
|
|
44
|
+
className,
|
|
45
|
+
tooltipPlacement = "right",
|
|
46
|
+
onClick,
|
|
47
|
+
}: NavItemButtonProps) => {
|
|
48
|
+
return (
|
|
49
|
+
<Tooltip title={label} placement={tooltipPlacement}>
|
|
50
|
+
<Pressable>
|
|
51
|
+
<a
|
|
52
|
+
href={href}
|
|
53
|
+
aria-label={label}
|
|
54
|
+
onClick={onClick}
|
|
55
|
+
className={cx(
|
|
56
|
+
"relative flex w-full cursor-pointer items-center justify-center rounded-md bg-primary p-2 text-fg-quaternary outline-focus-ring transition duration-100 ease-linear select-none hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
57
|
+
current && "bg-active text-fg-quaternary_hover hover:bg-secondary_hover",
|
|
58
|
+
styles[size].root,
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<Icon aria-hidden="true" className={cx("shrink-0 transition-inherit-all", styles[size].icon)} />
|
|
63
|
+
</a>
|
|
64
|
+
</Pressable>
|
|
65
|
+
</Tooltip>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { FC, HTMLAttributes, MouseEventHandler, ReactNode } from "react";
|
|
2
|
+
import { ChevronDown, Share04 } from "@untitledui/icons";
|
|
3
|
+
import { Link as AriaLink } from "react-aria-components";
|
|
4
|
+
import { Badge } from "@/components/base/badges/badges";
|
|
5
|
+
import { cx, sortCx } from "@/utils/cx";
|
|
6
|
+
|
|
7
|
+
const styles = sortCx({
|
|
8
|
+
root: "group relative flex w-full cursor-pointer items-center rounded-md bg-primary outline-focus-ring transition duration-100 ease-linear select-none hover:bg-primary_hover focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
9
|
+
rootSelected: "bg-active hover:bg-secondary_hover",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
interface NavItemBaseProps {
|
|
13
|
+
/** Whether the nav item shows only an icon. */
|
|
14
|
+
iconOnly?: boolean;
|
|
15
|
+
/** Whether the collapsible nav item is open. */
|
|
16
|
+
open?: boolean;
|
|
17
|
+
/** URL to navigate to when the nav item is clicked. */
|
|
18
|
+
href?: string;
|
|
19
|
+
/** Type of the nav item. */
|
|
20
|
+
type: "link" | "collapsible" | "collapsible-child";
|
|
21
|
+
/** Icon component to display. */
|
|
22
|
+
icon?: FC<HTMLAttributes<HTMLOrSVGElement>>;
|
|
23
|
+
/** Badge to display. */
|
|
24
|
+
badge?: ReactNode;
|
|
25
|
+
/** Whether the nav item is currently active. */
|
|
26
|
+
current?: boolean;
|
|
27
|
+
/** Whether to truncate the label text. */
|
|
28
|
+
truncate?: boolean;
|
|
29
|
+
/** Handler for click events. */
|
|
30
|
+
onClick?: MouseEventHandler;
|
|
31
|
+
/** Content to display. */
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const NavItemBase = ({ current, type, badge, href, icon: Icon, children, truncate = true, onClick }: NavItemBaseProps) => {
|
|
36
|
+
const iconElement = Icon && <Icon aria-hidden="true" className="mr-2 size-5 shrink-0 text-fg-quaternary transition-inherit-all" />;
|
|
37
|
+
|
|
38
|
+
const badgeElement =
|
|
39
|
+
badge && (typeof badge === "string" || typeof badge === "number") ? (
|
|
40
|
+
<Badge className="ml-3" color="gray" type="pill-color" size="sm">
|
|
41
|
+
{badge}
|
|
42
|
+
</Badge>
|
|
43
|
+
) : (
|
|
44
|
+
badge
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const labelElement = (
|
|
48
|
+
<span
|
|
49
|
+
className={cx(
|
|
50
|
+
"flex-1 text-md font-semibold text-secondary transition-inherit-all group-hover:text-secondary_hover",
|
|
51
|
+
truncate && "truncate",
|
|
52
|
+
current && "text-secondary_hover",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</span>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const isExternal = href && href.startsWith("http");
|
|
60
|
+
const externalIcon = isExternal && <Share04 className="size-4 stroke-[2.5px] text-fg-quaternary" />;
|
|
61
|
+
|
|
62
|
+
if (type === "collapsible") {
|
|
63
|
+
return (
|
|
64
|
+
<summary className={cx("px-3 py-2", styles.root, current && styles.rootSelected)} onClick={onClick}>
|
|
65
|
+
{iconElement}
|
|
66
|
+
|
|
67
|
+
{labelElement}
|
|
68
|
+
|
|
69
|
+
{badgeElement}
|
|
70
|
+
|
|
71
|
+
<ChevronDown aria-hidden="true" className="ml-3 size-4 shrink-0 stroke-[2.5px] text-fg-quaternary in-open:-scale-y-100" />
|
|
72
|
+
</summary>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (type === "collapsible-child") {
|
|
77
|
+
return (
|
|
78
|
+
<AriaLink
|
|
79
|
+
href={href!}
|
|
80
|
+
target={isExternal ? "_blank" : "_self"}
|
|
81
|
+
rel="noopener noreferrer"
|
|
82
|
+
className={cx("py-2 pr-3 pl-10", styles.root, current && styles.rootSelected)}
|
|
83
|
+
onClick={onClick}
|
|
84
|
+
aria-current={current ? "page" : undefined}
|
|
85
|
+
>
|
|
86
|
+
{labelElement}
|
|
87
|
+
{externalIcon}
|
|
88
|
+
{badgeElement}
|
|
89
|
+
</AriaLink>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<AriaLink
|
|
95
|
+
href={href!}
|
|
96
|
+
target={isExternal ? "_blank" : "_self"}
|
|
97
|
+
rel="noopener noreferrer"
|
|
98
|
+
className={cx("px-3 py-2", styles.root, current && styles.rootSelected)}
|
|
99
|
+
onClick={onClick}
|
|
100
|
+
aria-current={current ? "page" : undefined}
|
|
101
|
+
>
|
|
102
|
+
{iconElement}
|
|
103
|
+
{labelElement}
|
|
104
|
+
{externalIcon}
|
|
105
|
+
{badgeElement}
|
|
106
|
+
</AriaLink>
|
|
107
|
+
);
|
|
108
|
+
};
|