create-mithaq 0.1.1 → 0.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/LICENSES/openstatus-time-picker.txt +21 -0
- package/index.js +457 -63
- package/package.json +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
- package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
- package/template/apps/asas-backoffice/src/redirect.ts +1 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
- package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
- package/template/apps/asas-backoffice/src/router.tsx +18 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
- package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
- package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
- package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
- package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
- package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
- package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
- package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
- package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
- package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
- package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
- package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
- package/template/apps/asas-tanstack/package.json +4 -2
- package/template/apps/asas-tanstack/src/api.ts +1 -1
- package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +416 -79
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/links.ts +1 -1
- package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -1
- package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
- package/template/apps/asas-tanstack/src/router.tsx +12 -1
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
- package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
- package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
- package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
- package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
- package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
- package/template/apps/asas-tanstack/src/screens.tsx +46 -26
- package/template/apps/asas-tanstack/src/server.ts +10 -0
- package/template/apps/asas-tanstack/src/slide.ts +1 -1
- package/template/apps/asas-tanstack/src/styles.css +7 -3
- package/template/apps/asas-tanstack/tsconfig.json +2 -2
- package/template/apps/asas-tanstack/vite.config.ts +7 -1
- package/template/package.json +3 -3
- package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
- package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
- package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
- package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
- package/template/templates/asas/.env.example +22 -5
- package/template/templates/asas/AGENTS.md +101 -0
- package/template/templates/asas/CLAUDE.md +1 -0
- package/template/templates/asas/README.md +1094 -0
- package/template/templates/asas/package.json +19 -4
- package/template/templates/asas/src/asas.ts +81 -56
- package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
- package/template/templates/asas/src/dev-setup.ts +112 -0
- package/template/templates/asas/src/local-database.ts +22 -0
- package/template/templates/asas/src/messages/account.json +129 -103
- package/template/templates/asas/src/messages/auditLog.json +223 -105
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +256 -214
- package/template/templates/asas/src/messages/enrollments.json +45 -27
- package/template/templates/asas/src/messages/invitation.json +72 -56
- package/template/templates/asas/src/messages/localUsers.json +34 -28
- package/template/templates/asas/src/messages/media.json +31 -21
- package/template/templates/asas/src/messages/member.json +59 -39
- package/template/templates/asas/src/messages/memberRoles.json +37 -31
- package/template/templates/asas/src/messages/mithaq.json +206 -162
- package/template/templates/asas/src/messages/organization.json +63 -41
- package/template/templates/asas/src/messages/organizationRole.json +59 -47
- package/template/templates/asas/src/messages/roles.json +59 -45
- package/template/templates/asas/src/messages/session.json +99 -79
- package/template/templates/asas/src/messages/sessions.json +110 -80
- package/template/templates/asas/src/messages/settings.json +51 -53
- package/template/templates/asas/src/messages/tags.json +31 -21
- package/template/templates/asas/src/messages/user.json +109 -87
- package/template/templates/asas/src/messages/verification.json +59 -47
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
- package/template/templates/asas/src/migrations/schema.ts +1 -0
- package/template/templates/asas/src/mithaq.ts +4 -406
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +147 -45
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
- package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
- package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
- package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
- package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +9 -13
- package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
- package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
- package/template/templates/features/audit-log/index.ts +4 -0
- package/template/templates/features/audit-log/package.json +13 -0
- package/template/templates/features/auth/index.ts +11 -0
- package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
- package/template/templates/{auth → features/auth}/package.json +2 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
- package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
- package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
- package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
- package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
- package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
- package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
- package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
- package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
- package/template/templates/features/courses/courses/cover-reads.ts +33 -0
- package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
- package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
- package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
- package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
- package/template/templates/features/courses/domain/archive.ts +39 -0
- package/template/templates/features/courses/domain/on-offer.ts +6 -0
- package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
- package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
- package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
- package/template/templates/features/courses/index.ts +8 -0
- package/template/templates/features/courses/package.json +24 -0
- package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
- package/template/templates/features/courses/portal/ui/course.tsx +176 -0
- package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
- package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
- package/template/templates/features/courses/portal/ui/reads.ts +76 -0
- package/template/templates/features/courses/portal/ui/rules.ts +87 -0
- package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
- package/template/templates/features/courses/sessions/seats.ts +42 -0
- package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
- package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
- package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
- package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
- package/template/templates/mithaq/package.json +33 -0
- package/template/templates/mithaq/src/mail/index.ts +32 -0
- package/template/templates/mithaq/src/mail/mailers.ts +36 -0
- package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
- package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
- package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
- package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
- package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
- package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
- package/template/templates/mithaq/src/mithaq.ts +593 -0
- package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
- package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
- package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
- package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
- package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
- package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
- package/template/apps/asas-platform/src/redirect.ts +0 -1
- package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
- package/template/apps/asas-platform/src/router.tsx +0 -7
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
- package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
- package/template/apps/asas-platform/src/server.ts +0 -20
- package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
- package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
- package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
- package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
- package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
- package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
- package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
- package/template/templates/asas/src/rich-text-nodes.ts +0 -11
- package/template/templates/asas/src/roles.ts +0 -16
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
- package/template/templates/auth/src/index.ts +0 -690
- /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
- /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
- /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
- /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
- /package/template/templates/{asas → mithaq}/components.json +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
- /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
|
@@ -1,871 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useEffect, useMemo, useState, type ComponentProps, type CSSProperties, type ReactNode } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
BookOpen,
|
|
4
|
-
Building2,
|
|
5
|
-
CalendarDays,
|
|
6
|
-
Check,
|
|
7
|
-
ChevronsUpDown,
|
|
8
|
-
ClipboardList,
|
|
9
|
-
Folder,
|
|
10
|
-
GalleryVerticalEnd,
|
|
11
|
-
HelpCircle,
|
|
12
|
-
Languages,
|
|
13
|
-
LayoutGrid,
|
|
14
|
-
LogOut,
|
|
15
|
-
Mail,
|
|
16
|
-
Monitor,
|
|
17
|
-
Moon,
|
|
18
|
-
MoreHorizontal,
|
|
19
|
-
PlusCircle,
|
|
20
|
-
Search,
|
|
21
|
-
Settings2,
|
|
22
|
-
Share2,
|
|
23
|
-
ShieldCheck,
|
|
24
|
-
Sun,
|
|
25
|
-
Table2,
|
|
26
|
-
Trash2,
|
|
27
|
-
Users,
|
|
28
|
-
type LucideIcon,
|
|
29
|
-
} from 'lucide-react'
|
|
30
|
-
import { Moon02Icon, Sun03Icon } from '@hugeicons/core-free-icons'
|
|
31
|
-
import { HugeiconsIcon } from '@hugeicons/react'
|
|
32
|
-
import type { ClientKayan } from 'mithaq/admin'
|
|
33
|
-
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
34
|
-
import { Button, buttonVariants } from '@/components/ui/button'
|
|
35
|
-
import {
|
|
36
|
-
DropdownMenu,
|
|
37
|
-
DropdownMenuContent,
|
|
38
|
-
DropdownMenuGroup,
|
|
39
|
-
DropdownMenuItem,
|
|
40
|
-
DropdownMenuLabel,
|
|
41
|
-
DropdownMenuRadioGroup,
|
|
42
|
-
DropdownMenuRadioItem,
|
|
43
|
-
DropdownMenuSeparator,
|
|
44
|
-
DropdownMenuTrigger,
|
|
45
|
-
} from '@/components/ui/dropdown-menu'
|
|
46
|
-
import { Separator } from '@/components/ui/separator'
|
|
47
|
-
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
48
|
-
import {
|
|
49
|
-
Sidebar,
|
|
50
|
-
SidebarContent,
|
|
51
|
-
SidebarFooter,
|
|
52
|
-
SidebarGroup,
|
|
53
|
-
SidebarGroupContent,
|
|
54
|
-
SidebarGroupLabel,
|
|
55
|
-
SidebarHeader,
|
|
56
|
-
SidebarInset,
|
|
57
|
-
SidebarMenu,
|
|
58
|
-
SidebarMenuAction,
|
|
59
|
-
SidebarMenuButton,
|
|
60
|
-
SidebarMenuItem,
|
|
61
|
-
SidebarProvider,
|
|
62
|
-
SidebarTrigger,
|
|
63
|
-
useSidebar,
|
|
64
|
-
} from '@/components/ui/sidebar'
|
|
65
|
-
import { titleOf, type Translate } from './controls.tsx'
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* As much of the access report as this screen reads: one `read` per kayan, keyed by its name.
|
|
69
|
-
* `operations` is what a screen gates a control on: an effect is allowed where any one of its
|
|
70
|
-
* operations is, so gate a button on `operations.archive`, never on the effect.
|
|
71
|
-
*/
|
|
72
|
-
export type AccessReport = {
|
|
73
|
-
readonly collections?: Readonly<
|
|
74
|
-
Record<
|
|
75
|
-
string,
|
|
76
|
-
{ readonly read?: boolean; readonly operations?: Readonly<Record<string, boolean>> } | undefined
|
|
77
|
-
>
|
|
78
|
-
>
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The two keys of a kayan this screen reads: its name, and whether it is a global, which picks the
|
|
83
|
-
* singular over the plural. A shell handed only these server-renders no field names or labels.
|
|
84
|
-
*/
|
|
85
|
-
export type NavKayan = Pick<ClientKayan, 'name' | 'single'>
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* One lucide icon per common kayan name, `Table2` for every other kayan. An app gives its own
|
|
89
|
-
* kayan an icon by editing this map.
|
|
90
|
-
*/
|
|
91
|
-
export const navIcons: Record<string, LucideIcon> = {
|
|
92
|
-
courses: BookOpen,
|
|
93
|
-
sessions: CalendarDays,
|
|
94
|
-
enrollments: ClipboardList,
|
|
95
|
-
settings: Settings2,
|
|
96
|
-
user: Users,
|
|
97
|
-
localUsers: Users,
|
|
98
|
-
member: Users,
|
|
99
|
-
organization: Building2,
|
|
100
|
-
invitation: Mail,
|
|
101
|
-
roles: ShieldCheck,
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** The icon a kayan is listed under. `Object.hasOwn` so a kayan named for a key of `Object.prototype` reads nothing off it. */
|
|
105
|
-
export const iconOf = (name: string): LucideIcon => (Object.hasOwn(navIcons, name) ? navIcons[name]! : Table2)
|
|
106
|
-
|
|
107
|
-
/** One entry of the kayan index: what it is called, where it goes and what it is drawn with. */
|
|
108
|
-
export type NavEntry = { kayan: NavKayan; label: string; href: string; Icon: LucideIcon }
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* What the header's `<h1>` says: the kayan's plural on its list, its singular on a row of it, the
|
|
112
|
-
* shell's own heading anywhere else. A row is matched by the rule `NavMain` marks an entry active by.
|
|
113
|
-
*/
|
|
114
|
-
export const titleFor = ({
|
|
115
|
-
entries,
|
|
116
|
-
copy,
|
|
117
|
-
pathname,
|
|
118
|
-
}: {
|
|
119
|
-
entries: readonly NavEntry[]
|
|
120
|
-
copy: { t: Translate; locale: string }
|
|
121
|
-
pathname?: string
|
|
122
|
-
}): string | undefined => {
|
|
123
|
-
const row = pathname === undefined ? undefined : entries.find((one) => pathname.startsWith(`${one.href}/`))
|
|
124
|
-
if (row) return titleOf({ kayan: row.kayan, copy })
|
|
125
|
-
const listed = entries.find((one) => one.href === pathname)
|
|
126
|
-
return listed ? listed.label : copy.t('admin.nav', 'Admin')
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The kayan index: the kayans this request's access report lets the viewer read. */
|
|
130
|
-
export const navEntries = ({
|
|
131
|
-
client,
|
|
132
|
-
access,
|
|
133
|
-
copy,
|
|
134
|
-
href,
|
|
135
|
-
}: {
|
|
136
|
-
client: readonly NavKayan[]
|
|
137
|
-
access: AccessReport
|
|
138
|
-
copy: { t: Translate; locale: string }
|
|
139
|
-
href: (kayan: NavKayan) => string
|
|
140
|
-
}): NavEntry[] =>
|
|
141
|
-
client
|
|
142
|
-
// `=== true` so a kayan the report has no entry for is denied, including one named for a key
|
|
143
|
-
// of `Object.prototype`.
|
|
144
|
-
.filter((kayan) => access.collections?.[kayan.name]?.read === true)
|
|
145
|
-
.map((kayan) => ({
|
|
146
|
-
kayan,
|
|
147
|
-
label: titleOf({ kayan, copy, many: !kayan.single }),
|
|
148
|
-
href: href(kayan),
|
|
149
|
-
Icon: iconOf(kayan.name),
|
|
150
|
-
}))
|
|
151
|
-
|
|
152
|
-
/** The values the theme toggle offers. */
|
|
153
|
-
export type Theme = 'dark' | 'light' | 'system'
|
|
154
|
-
|
|
155
|
-
/** The themes the user menu offers unless the project passes its own to `NavUserMenu`. */
|
|
156
|
-
export const defaultThemes: readonly Theme[] = ['light', 'dark', 'system']
|
|
157
|
-
|
|
158
|
-
type ThemeProviderProps = {
|
|
159
|
-
children: ReactNode
|
|
160
|
-
defaultTheme?: Theme
|
|
161
|
-
storageKey?: string
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
type ThemeProviderState = {
|
|
165
|
-
theme: Theme
|
|
166
|
-
setTheme: (theme: Theme) => void
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const initialState: ThemeProviderState = {
|
|
170
|
-
theme: 'system',
|
|
171
|
-
setTheme: () => null,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** `null` outside a provider, so a menu that offers the choice draws nothing where nothing would keep it. */
|
|
175
|
-
const ThemeProviderContext = createContext<ThemeProviderState | null>(null)
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Keeps the theme in `localStorage` and puts `light` or `dark` on the document element. The stored
|
|
179
|
-
* theme is read in the first browser render and not in an effect: a change after mount re-renders
|
|
180
|
-
* consumers, and inside an unhydrated Suspense boundary that makes React drop the server's HTML.
|
|
181
|
-
*/
|
|
182
|
-
export function ThemeProvider({
|
|
183
|
-
children,
|
|
184
|
-
defaultTheme = 'system',
|
|
185
|
-
storageKey = 'vite-ui-theme',
|
|
186
|
-
}: ThemeProviderProps) {
|
|
187
|
-
const [theme, setTheme] = useState<Theme>(() => {
|
|
188
|
-
if (typeof window === 'undefined') return defaultTheme
|
|
189
|
-
const stored = localStorage.getItem(storageKey)
|
|
190
|
-
// A stray value under this key would hydrate mismatched against the server's default.
|
|
191
|
-
return stored === 'dark' || stored === 'light' || stored === 'system' ? stored : defaultTheme
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
useEffect(() => {
|
|
195
|
-
const root = window.document.documentElement
|
|
196
|
-
root.classList.remove('light', 'dark')
|
|
197
|
-
|
|
198
|
-
if (theme === 'system') {
|
|
199
|
-
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
200
|
-
root.classList.add(systemTheme)
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
root.classList.add(theme)
|
|
205
|
-
}, [theme])
|
|
206
|
-
|
|
207
|
-
const value = useMemo(
|
|
208
|
-
() => ({
|
|
209
|
-
theme,
|
|
210
|
-
setTheme: (newTheme: Theme) => {
|
|
211
|
-
localStorage.setItem(storageKey, newTheme)
|
|
212
|
-
setTheme(newTheme)
|
|
213
|
-
},
|
|
214
|
-
}),
|
|
215
|
-
[theme, storageKey],
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** The theme and its setter, or `null` where no `ThemeProvider` is above. */
|
|
222
|
-
export const useTheme = () => useContext(ThemeProviderContext)
|
|
223
|
-
|
|
224
|
-
/** A theme toggle button whose menu offers light, dark and system. */
|
|
225
|
-
export function ModeToggle({ t }: { t: Translate }) {
|
|
226
|
-
const { setTheme } = useTheme() ?? initialState
|
|
227
|
-
|
|
228
|
-
return (
|
|
229
|
-
<DropdownMenu>
|
|
230
|
-
<DropdownMenuTrigger className={buttonVariants({ variant: 'outline', size: 'icon' })}>
|
|
231
|
-
<HugeiconsIcon
|
|
232
|
-
icon={Sun03Icon}
|
|
233
|
-
className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
|
|
234
|
-
/>
|
|
235
|
-
<HugeiconsIcon
|
|
236
|
-
icon={Moon02Icon}
|
|
237
|
-
className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
|
|
238
|
-
/>
|
|
239
|
-
<span className="sr-only">{t('admin.theme', 'Toggle theme')}</span>
|
|
240
|
-
</DropdownMenuTrigger>
|
|
241
|
-
<DropdownMenuContent align="end" className="max-sm:**:data-[slot$=item]:min-h-11">
|
|
242
|
-
<DropdownMenuItem onClick={() => setTheme('light')}>{t('admin.themeLight', 'Light')}</DropdownMenuItem>
|
|
243
|
-
<DropdownMenuItem onClick={() => setTheme('dark')}>{t('admin.themeDark', 'Dark')}</DropdownMenuItem>
|
|
244
|
-
<DropdownMenuItem onClick={() => setTheme('system')}>
|
|
245
|
-
{t('admin.themeSystem', 'System')}
|
|
246
|
-
</DropdownMenuItem>
|
|
247
|
-
</DropdownMenuContent>
|
|
248
|
-
</DropdownMenu>
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* A language's own name for itself (`ar` reads `العربية`), whatever the viewer reads in. It is the
|
|
254
|
-
* same in every locale, so it carries no copy key. `of` throws a `RangeError` on a malformed tag,
|
|
255
|
-
* so the tag itself is the fallback.
|
|
256
|
-
*/
|
|
257
|
-
export const endonym = (locale: string): string => {
|
|
258
|
-
try {
|
|
259
|
-
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale) ?? locale
|
|
260
|
-
} catch {
|
|
261
|
-
return locale
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** A radio row: a 44 pixel target on a phone, and the tick on the inline end in right-to-left pages, where the primitive draws it at the right. */
|
|
266
|
-
const radioRow = 'max-sm:min-h-11 rtl:pr-3 rtl:pl-8 rtl:[&>[data-slot=dropdown-menu-radio-item-indicator]]:right-auto rtl:[&>[data-slot=dropdown-menu-radio-item-indicator]]:left-2'
|
|
267
|
-
|
|
268
|
-
/** One row of the user menu's locale group: the locale, what it calls itself, and whether it is the one being read in. */
|
|
269
|
-
export const localeEntries = (
|
|
270
|
-
locales: readonly string[],
|
|
271
|
-
current: string,
|
|
272
|
-
): readonly { locale: string; label: string; active: boolean }[] =>
|
|
273
|
-
locales.map((one) => ({ locale: one, label: endonym(one), active: one === current }))
|
|
274
|
-
|
|
275
|
-
/** What the shell is told about the viewer, which is the two strings the user menu draws. */
|
|
276
|
-
export type NavUser = {
|
|
277
|
-
name: string
|
|
278
|
-
email: string
|
|
279
|
-
/** The other screens the host links for this viewer, each a whole address; the user menu draws a link to each. */
|
|
280
|
-
screens?: readonly { name: string; path: string }[]
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* One organization the switcher lists. Two may share a name, so each carries a slug, which the
|
|
285
|
-
* switcher shows beside a name that repeats.
|
|
286
|
-
*/
|
|
287
|
-
export type NavOrganization = { id: string; name: string; slug?: string }
|
|
288
|
-
|
|
289
|
-
export type ShellProps = {
|
|
290
|
-
/** Every kayan and global the app chose to hand down, as little of each as the two keys of `NavKayan`. */
|
|
291
|
-
client: readonly NavKayan[]
|
|
292
|
-
access: AccessReport
|
|
293
|
-
t: Translate
|
|
294
|
-
locale: string
|
|
295
|
-
/** Where an entry goes. A prop so the shell imports no router. */
|
|
296
|
-
href: (kayan: NavKayan) => string
|
|
297
|
-
/** The path the viewer is on, so the entry for it is marked current. A prop for the same reason `href` is one. */
|
|
298
|
-
pathname?: string
|
|
299
|
-
/** Which side the sidebar is on. */
|
|
300
|
-
side?: 'left' | 'right'
|
|
301
|
-
user: NavUser
|
|
302
|
-
organizations: readonly NavOrganization[]
|
|
303
|
-
activeOrganizationId?: string | null
|
|
304
|
-
/** What switching the active organization means, which is the app's business because it is the auth arm's. */
|
|
305
|
-
onSwitchOrganization: (id: string) => void
|
|
306
|
-
onSignOut: () => void
|
|
307
|
-
/** Where the host's permissions screen is. No row is drawn without it. */
|
|
308
|
-
permissionsHref?: string
|
|
309
|
-
/** Which locales the host reads in, so the user menu can offer them. A prop because mithaq resolves no locale. */
|
|
310
|
-
locales?: readonly string[]
|
|
311
|
-
/** What choosing a locale means. Without it the menu has no locale group. */
|
|
312
|
-
onLocale?: (locale: string) => void
|
|
313
|
-
children?: ReactNode
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function Shell({
|
|
317
|
-
access,
|
|
318
|
-
activeOrganizationId,
|
|
319
|
-
children,
|
|
320
|
-
client,
|
|
321
|
-
href,
|
|
322
|
-
locale,
|
|
323
|
-
locales,
|
|
324
|
-
onLocale,
|
|
325
|
-
onSignOut,
|
|
326
|
-
onSwitchOrganization,
|
|
327
|
-
organizations,
|
|
328
|
-
pathname,
|
|
329
|
-
permissionsHref,
|
|
330
|
-
side = 'left',
|
|
331
|
-
t,
|
|
332
|
-
user,
|
|
333
|
-
}: ShellProps) {
|
|
334
|
-
const entries = navEntries({ client, access, copy: { t, locale }, href })
|
|
335
|
-
return (
|
|
336
|
-
<SidebarProvider
|
|
337
|
-
style={
|
|
338
|
-
{
|
|
339
|
-
'--sidebar-width': 'calc(var(--spacing) * 72)',
|
|
340
|
-
'--header-height': 'calc(var(--spacing) * 12)',
|
|
341
|
-
} as CSSProperties
|
|
342
|
-
}
|
|
343
|
-
>
|
|
344
|
-
<AppSidebar
|
|
345
|
-
activeOrganizationId={activeOrganizationId}
|
|
346
|
-
entries={entries}
|
|
347
|
-
locale={locale}
|
|
348
|
-
locales={locales}
|
|
349
|
-
onLocale={onLocale}
|
|
350
|
-
onSignOut={onSignOut}
|
|
351
|
-
onSwitchOrganization={onSwitchOrganization}
|
|
352
|
-
organizations={organizations}
|
|
353
|
-
pathname={pathname}
|
|
354
|
-
permissionsHref={permissionsHref}
|
|
355
|
-
side={side}
|
|
356
|
-
t={t}
|
|
357
|
-
user={user}
|
|
358
|
-
variant="inset"
|
|
359
|
-
/>
|
|
360
|
-
{/* `min-w-0`: the inset is a flex item beside the sidebar, and a table wider than a phone
|
|
361
|
-
would widen the whole document. */}
|
|
362
|
-
<SidebarInset className="min-w-0">
|
|
363
|
-
<SiteHeader t={t} title={titleFor({ entries, copy: { t, locale }, pathname })} />
|
|
364
|
-
<div className="flex flex-1 flex-col px-4 pb-4 lg:px-6 lg:pb-6">
|
|
365
|
-
{/* `min-w-0`: a flex child whose minimum is its content pushes the sidebar off the page
|
|
366
|
-
instead of scrolling a wide table. */}
|
|
367
|
-
<div className="@container/main flex min-w-0 flex-1 flex-col gap-2">{children}</div>
|
|
368
|
-
</div>
|
|
369
|
-
</SidebarInset>
|
|
370
|
-
</SidebarProvider>
|
|
371
|
-
)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* The sidebar: organization switcher, kayan index and user menu. It takes `side` and no `dir`,
|
|
376
|
-
* since the direction is already on the document.
|
|
377
|
-
*/
|
|
378
|
-
export function AppSidebar({
|
|
379
|
-
activeOrganizationId,
|
|
380
|
-
entries,
|
|
381
|
-
locale,
|
|
382
|
-
locales,
|
|
383
|
-
onLocale,
|
|
384
|
-
onSignOut,
|
|
385
|
-
onSwitchOrganization,
|
|
386
|
-
organizations,
|
|
387
|
-
pathname,
|
|
388
|
-
permissionsHref,
|
|
389
|
-
t,
|
|
390
|
-
user,
|
|
391
|
-
...props
|
|
392
|
-
}: {
|
|
393
|
-
activeOrganizationId?: string | null
|
|
394
|
-
entries: readonly NavEntry[]
|
|
395
|
-
locale?: string
|
|
396
|
-
locales?: readonly string[]
|
|
397
|
-
onLocale?: (locale: string) => void
|
|
398
|
-
onSignOut: () => void
|
|
399
|
-
onSwitchOrganization: (id: string) => void
|
|
400
|
-
organizations: readonly NavOrganization[]
|
|
401
|
-
pathname?: string
|
|
402
|
-
permissionsHref?: string
|
|
403
|
-
t: Translate
|
|
404
|
-
user: NavUser
|
|
405
|
-
} & ComponentProps<typeof Sidebar>) {
|
|
406
|
-
return (
|
|
407
|
-
<NavSidebar {...props} t={t}>
|
|
408
|
-
{/* One navigation landmark around the whole sidebar. A box of its own: the sidebar's props
|
|
409
|
-
reach no element on a phone, where it is a sheet. */}
|
|
410
|
-
<nav aria-label={t('admin.navigation', 'Navigation')} className="flex min-h-0 flex-1 flex-col">
|
|
411
|
-
<SidebarHeader>
|
|
412
|
-
<OrganizationSwitcher
|
|
413
|
-
activeOrganizationId={activeOrganizationId}
|
|
414
|
-
onSwitch={onSwitchOrganization}
|
|
415
|
-
organizations={organizations}
|
|
416
|
-
t={t}
|
|
417
|
-
/>
|
|
418
|
-
</SidebarHeader>
|
|
419
|
-
<SidebarContent>
|
|
420
|
-
<NavMain entries={entries} pathname={pathname} t={t} />
|
|
421
|
-
<NavDocuments t={t} />
|
|
422
|
-
<NavSecondary className="mt-auto" permissionsHref={permissionsHref} t={t} />
|
|
423
|
-
</SidebarContent>
|
|
424
|
-
<SidebarFooter>
|
|
425
|
-
<NavUserMenu locale={locale} locales={locales} onLocale={onLocale} onSignOut={onSignOut} t={t} user={user} />
|
|
426
|
-
</SidebarFooter>
|
|
427
|
-
</nav>
|
|
428
|
-
</NavSidebar>
|
|
429
|
-
)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* The kayan index as one sidebar group. An entry is current when the path is its own or a segment
|
|
434
|
-
* under it, so no two entries are current at once. `render` and not `asChild`: base-ui composes a
|
|
435
|
-
* button into an anchor through `useRender`.
|
|
436
|
-
*/
|
|
437
|
-
export function NavMain({ entries, pathname, t }: { entries: readonly NavEntry[]; pathname?: string; t: Translate }) {
|
|
438
|
-
return (
|
|
439
|
-
<SidebarGroup>
|
|
440
|
-
<SidebarGroupContent className="flex flex-col gap-2">
|
|
441
|
-
{/* Quick Create and Inbox are placeholders: this admin has no destination for either. */}
|
|
442
|
-
<SidebarMenu>
|
|
443
|
-
<SidebarMenuItem className="flex items-center gap-2">
|
|
444
|
-
<SidebarMenuButton
|
|
445
|
-
tooltip={t('admin.quickCreate', 'Quick Create')}
|
|
446
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
|
|
447
|
-
>
|
|
448
|
-
<PlusCircle />
|
|
449
|
-
<span>{t('admin.quickCreate', 'Quick Create')}</span>
|
|
450
|
-
</SidebarMenuButton>
|
|
451
|
-
<Button size="icon" className="size-8 group-data-[collapsible=icon]:opacity-0" variant="outline">
|
|
452
|
-
<Mail />
|
|
453
|
-
<span className="sr-only">{t('admin.inbox', 'Inbox')}</span>
|
|
454
|
-
</Button>
|
|
455
|
-
</SidebarMenuItem>
|
|
456
|
-
</SidebarMenu>
|
|
457
|
-
<SidebarMenu>
|
|
458
|
-
{entries.map((entry) => {
|
|
459
|
-
const isActive = pathname !== undefined && (pathname === entry.href || pathname.startsWith(`${entry.href}/`))
|
|
460
|
-
return (
|
|
461
|
-
<SidebarMenuItem key={entry.kayan.name}>
|
|
462
|
-
<SidebarMenuButton
|
|
463
|
-
isActive={isActive}
|
|
464
|
-
render={
|
|
465
|
-
<a href={entry.href}>
|
|
466
|
-
<entry.Icon />
|
|
467
|
-
<span>{entry.label}</span>
|
|
468
|
-
</a>
|
|
469
|
-
}
|
|
470
|
-
/>
|
|
471
|
-
</SidebarMenuItem>
|
|
472
|
-
)
|
|
473
|
-
})}
|
|
474
|
-
</SidebarMenu>
|
|
475
|
-
</SidebarGroupContent>
|
|
476
|
-
</SidebarGroup>
|
|
477
|
-
)
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* The documents group. `items` defaults to none, so only the label and the "More" row draw; the
|
|
482
|
-
* row markup stays for an app that passes its own documents.
|
|
483
|
-
*/
|
|
484
|
-
export function NavDocuments({
|
|
485
|
-
items = [],
|
|
486
|
-
t,
|
|
487
|
-
}: {
|
|
488
|
-
items?: readonly { name: string; url: string; icon: LucideIcon }[]
|
|
489
|
-
t: Translate
|
|
490
|
-
}) {
|
|
491
|
-
const { isMobile } = useSidebar()
|
|
492
|
-
return (
|
|
493
|
-
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
|
494
|
-
<SidebarGroupLabel>{t('admin.documents', 'Documents')}</SidebarGroupLabel>
|
|
495
|
-
<SidebarMenu>
|
|
496
|
-
{items.map((item) => (
|
|
497
|
-
<SidebarMenuItem key={item.name}>
|
|
498
|
-
<SidebarMenuButton render={<a href={item.url} />}>
|
|
499
|
-
<item.icon />
|
|
500
|
-
<span>{item.name}</span>
|
|
501
|
-
</SidebarMenuButton>
|
|
502
|
-
<DropdownMenu>
|
|
503
|
-
<DropdownMenuTrigger
|
|
504
|
-
render={(props) => (
|
|
505
|
-
<SidebarMenuAction showOnHover className="data-[state=open]:bg-accent rounded-sm" {...props}>
|
|
506
|
-
<MoreHorizontal />
|
|
507
|
-
<span className="sr-only">{t('admin.more', 'More')}</span>
|
|
508
|
-
</SidebarMenuAction>
|
|
509
|
-
)}
|
|
510
|
-
/>
|
|
511
|
-
<DropdownMenuContent
|
|
512
|
-
className="w-24 rounded-lg"
|
|
513
|
-
side={isMobile ? 'bottom' : 'right'}
|
|
514
|
-
align={isMobile ? 'end' : 'start'}
|
|
515
|
-
>
|
|
516
|
-
<DropdownMenuItem>
|
|
517
|
-
<Folder className="me-2 h-4 w-4" />
|
|
518
|
-
<span>{t('admin.open', 'Open')}</span>
|
|
519
|
-
</DropdownMenuItem>
|
|
520
|
-
<DropdownMenuItem>
|
|
521
|
-
<Share2 className="me-2 h-4 w-4" />
|
|
522
|
-
<span>{t('admin.share', 'Share')}</span>
|
|
523
|
-
</DropdownMenuItem>
|
|
524
|
-
<DropdownMenuSeparator />
|
|
525
|
-
<DropdownMenuItem variant="destructive">
|
|
526
|
-
<Trash2 className="me-2 h-4 w-4" />
|
|
527
|
-
<span>{t('admin.trash', 'Move to Trash')}</span>
|
|
528
|
-
</DropdownMenuItem>
|
|
529
|
-
</DropdownMenuContent>
|
|
530
|
-
</DropdownMenu>
|
|
531
|
-
</SidebarMenuItem>
|
|
532
|
-
))}
|
|
533
|
-
<SidebarMenuItem>
|
|
534
|
-
<SidebarMenuButton className="text-sidebar-foreground/70">
|
|
535
|
-
<MoreHorizontal className="text-sidebar-foreground/70" />
|
|
536
|
-
<span>{t('admin.more', 'More')}</span>
|
|
537
|
-
</SidebarMenuButton>
|
|
538
|
-
</SidebarMenuItem>
|
|
539
|
-
</SidebarMenu>
|
|
540
|
-
</SidebarGroup>
|
|
541
|
-
)
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* The rows at the foot of the sidebar: a permissions row where the host gives `permissionsHref`,
|
|
546
|
-
* then Get Help and Search, which point at `#`. Their copy keys are quoted literals at the call
|
|
547
|
-
* so the copy sweep can see them.
|
|
548
|
-
*/
|
|
549
|
-
export function NavSecondary({
|
|
550
|
-
className,
|
|
551
|
-
permissionsHref,
|
|
552
|
-
t,
|
|
553
|
-
}: {
|
|
554
|
-
className?: string
|
|
555
|
-
permissionsHref?: string
|
|
556
|
-
t: Translate
|
|
557
|
-
}) {
|
|
558
|
-
return (
|
|
559
|
-
<SidebarGroup className={className}>
|
|
560
|
-
<SidebarGroupContent>
|
|
561
|
-
<SidebarMenu>
|
|
562
|
-
{permissionsHref ? (
|
|
563
|
-
<SidebarMenuItem>
|
|
564
|
-
<SidebarMenuButton render={<a href={permissionsHref} />}>
|
|
565
|
-
<ShieldCheck />
|
|
566
|
-
<span>{t('admin.permissions', 'Permissions')}</span>
|
|
567
|
-
</SidebarMenuButton>
|
|
568
|
-
</SidebarMenuItem>
|
|
569
|
-
) : null}
|
|
570
|
-
<SidebarMenuItem>
|
|
571
|
-
<SidebarMenuButton render={<a href="#" />}>
|
|
572
|
-
<HelpCircle />
|
|
573
|
-
<span>{t('admin.getHelp', 'Get Help')}</span>
|
|
574
|
-
</SidebarMenuButton>
|
|
575
|
-
</SidebarMenuItem>
|
|
576
|
-
<SidebarMenuItem>
|
|
577
|
-
<SidebarMenuButton render={<a href="#" />}>
|
|
578
|
-
<Search />
|
|
579
|
-
<span>{t('admin.search', 'Search')}</span>
|
|
580
|
-
</SidebarMenuButton>
|
|
581
|
-
</SidebarMenuItem>
|
|
582
|
-
</SidebarMenu>
|
|
583
|
-
</SidebarGroupContent>
|
|
584
|
-
</SidebarGroup>
|
|
585
|
-
)
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const initialsOf = (name: string) =>
|
|
589
|
-
name
|
|
590
|
-
.split(/\s+/)
|
|
591
|
-
.filter(Boolean)
|
|
592
|
-
.slice(0, 2)
|
|
593
|
-
.map((word) => word[0]!.toUpperCase())
|
|
594
|
-
.join('') || '?'
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* The organization switcher: a dropdown listing every organization as a radio item, the active
|
|
598
|
-
* one checked.
|
|
599
|
-
*/
|
|
600
|
-
export function OrganizationSwitcher({
|
|
601
|
-
activeOrganizationId,
|
|
602
|
-
onSwitch,
|
|
603
|
-
organizations,
|
|
604
|
-
t,
|
|
605
|
-
}: {
|
|
606
|
-
activeOrganizationId?: string | null
|
|
607
|
-
onSwitch: (id: string) => void
|
|
608
|
-
organizations: readonly NavOrganization[]
|
|
609
|
-
t: Translate
|
|
610
|
-
}) {
|
|
611
|
-
const { isMobile } = useSidebar()
|
|
612
|
-
const active = organizations.find((one) => one.id === activeOrganizationId) ?? organizations[0]
|
|
613
|
-
/** An organization's name, with its slug beside it where another listed shares the name. */
|
|
614
|
-
const named = (one: NavOrganization) =>
|
|
615
|
-
one.slug && organizations.some((other) => other.id !== one.id && other.name === one.name) ? (
|
|
616
|
-
<>
|
|
617
|
-
{one.name} <span dir="ltr" data-slug="" className="text-muted-foreground text-xs font-normal">{one.slug}</span>
|
|
618
|
-
</>
|
|
619
|
-
) : (
|
|
620
|
-
one.name
|
|
621
|
-
)
|
|
622
|
-
const badge = (
|
|
623
|
-
<>
|
|
624
|
-
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
625
|
-
<GalleryVerticalEnd className="size-4" />
|
|
626
|
-
</div>
|
|
627
|
-
<div className="grid flex-1 text-start text-sm leading-tight">
|
|
628
|
-
<span className="truncate font-semibold text-foreground">
|
|
629
|
-
{active ? named(active) : t('admin.noOrganization', 'No organization')}
|
|
630
|
-
</span>
|
|
631
|
-
<span className="truncate text-xs">
|
|
632
|
-
{active ? t('admin.organization', 'Organization') : ''}
|
|
633
|
-
</span>
|
|
634
|
-
</div>
|
|
635
|
-
</>
|
|
636
|
-
)
|
|
637
|
-
// One organization or none is nothing to switch between, so it is drawn as a plain name.
|
|
638
|
-
if (organizations.length < 2) {
|
|
639
|
-
return (
|
|
640
|
-
<SidebarMenu>
|
|
641
|
-
<SidebarMenuItem data-organization="">
|
|
642
|
-
<div className="flex h-12 items-center gap-2 p-2">{badge}</div>
|
|
643
|
-
</SidebarMenuItem>
|
|
644
|
-
</SidebarMenu>
|
|
645
|
-
)
|
|
646
|
-
}
|
|
647
|
-
return (
|
|
648
|
-
<SidebarMenu>
|
|
649
|
-
<SidebarMenuItem>
|
|
650
|
-
<DropdownMenu>
|
|
651
|
-
<DropdownMenuTrigger
|
|
652
|
-
render={(props, { open }) => (
|
|
653
|
-
<SidebarMenuButton
|
|
654
|
-
size="lg"
|
|
655
|
-
data-state={open ? 'open' : 'closed'}
|
|
656
|
-
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
657
|
-
{...props}
|
|
658
|
-
>
|
|
659
|
-
{badge}
|
|
660
|
-
<ChevronsUpDown className="ms-auto" />
|
|
661
|
-
</SidebarMenuButton>
|
|
662
|
-
)}
|
|
663
|
-
/>
|
|
664
|
-
<DropdownMenuContent
|
|
665
|
-
className="min-w-56 rounded-lg max-sm:**:data-[slot$=item]:min-h-11"
|
|
666
|
-
align="start"
|
|
667
|
-
side={isMobile ? 'bottom' : 'right'}
|
|
668
|
-
sideOffset={4}
|
|
669
|
-
>
|
|
670
|
-
<DropdownMenuGroup>
|
|
671
|
-
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
|
672
|
-
{t('admin.organizations', 'Organizations')}
|
|
673
|
-
</DropdownMenuLabel>
|
|
674
|
-
{/* Radio items, so the current organization is `aria-checked`; `closeOnClick`
|
|
675
|
-
because a radio item does not close the menu by itself. */}
|
|
676
|
-
<DropdownMenuRadioGroup value={active?.id} onValueChange={onSwitch}>
|
|
677
|
-
{organizations.map((one) => (
|
|
678
|
-
<DropdownMenuRadioItem key={one.id} value={one.id} closeOnClick className={radioRow}>
|
|
679
|
-
{named(one)}
|
|
680
|
-
</DropdownMenuRadioItem>
|
|
681
|
-
))}
|
|
682
|
-
</DropdownMenuRadioGroup>
|
|
683
|
-
</DropdownMenuGroup>
|
|
684
|
-
</DropdownMenuContent>
|
|
685
|
-
</DropdownMenu>
|
|
686
|
-
</SidebarMenuItem>
|
|
687
|
-
</SidebarMenu>
|
|
688
|
-
)
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* The user menu: avatar, name and email, a link to each of the viewer's other screens, a locale
|
|
693
|
-
* group, a theme group and sign-out. The locale and theme groups draw only where they can work.
|
|
694
|
-
*/
|
|
695
|
-
export function NavUserMenu({
|
|
696
|
-
locale,
|
|
697
|
-
locales,
|
|
698
|
-
onLocale,
|
|
699
|
-
onSignOut,
|
|
700
|
-
t,
|
|
701
|
-
themes = defaultThemes,
|
|
702
|
-
user,
|
|
703
|
-
}: {
|
|
704
|
-
locale?: string
|
|
705
|
-
locales?: readonly string[]
|
|
706
|
-
onLocale?: (locale: string) => void
|
|
707
|
-
onSignOut: () => void
|
|
708
|
-
t: Translate
|
|
709
|
-
/**
|
|
710
|
-
* Which themes the menu offers: `defaultThemes` unless given, none for `[]`. Drawn only where a
|
|
711
|
-
* `ThemeProvider` is above. Rows keep the order light, dark, system.
|
|
712
|
-
*/
|
|
713
|
-
themes?: readonly Theme[]
|
|
714
|
-
user: NavUser
|
|
715
|
-
}) {
|
|
716
|
-
const { isMobile } = useSidebar()
|
|
717
|
-
const provided = useTheme()
|
|
718
|
-
const screens = user.screens
|
|
719
|
-
return (
|
|
720
|
-
<SidebarMenu>
|
|
721
|
-
<SidebarMenuItem>
|
|
722
|
-
<DropdownMenu>
|
|
723
|
-
<DropdownMenuTrigger
|
|
724
|
-
render={(props, { open }) => (
|
|
725
|
-
<SidebarMenuButton
|
|
726
|
-
data-state={open ? 'open' : 'closed'}
|
|
727
|
-
size="lg"
|
|
728
|
-
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
729
|
-
{...props}
|
|
730
|
-
>
|
|
731
|
-
<Avatar className="h-8 w-8 rounded-lg grayscale">
|
|
732
|
-
<AvatarFallback className="rounded-lg">{initialsOf(user.name)}</AvatarFallback>
|
|
733
|
-
</Avatar>
|
|
734
|
-
<div className="grid flex-1 text-start text-sm leading-tight">
|
|
735
|
-
<span className="truncate font-medium">{user.name}</span>
|
|
736
|
-
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
|
|
737
|
-
</div>
|
|
738
|
-
<ChevronsUpDown className="ms-auto size-4" />
|
|
739
|
-
</SidebarMenuButton>
|
|
740
|
-
)}
|
|
741
|
-
/>
|
|
742
|
-
<DropdownMenuContent
|
|
743
|
-
className="min-w-56 rounded-lg w-56 max-sm:**:data-[slot$=item]:min-h-11"
|
|
744
|
-
side={isMobile ? 'bottom' : 'top'}
|
|
745
|
-
align="end"
|
|
746
|
-
sideOffset={4}
|
|
747
|
-
>
|
|
748
|
-
<DropdownMenuGroup>
|
|
749
|
-
<DropdownMenuLabel className="p-0 font-normal">
|
|
750
|
-
<div className="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
|
751
|
-
<Avatar className="h-8 w-8 rounded-lg">
|
|
752
|
-
<AvatarFallback className="rounded-lg">{initialsOf(user.name)}</AvatarFallback>
|
|
753
|
-
</Avatar>
|
|
754
|
-
<div className="grid flex-1 text-start text-sm leading-tight">
|
|
755
|
-
<span className="truncate font-medium">{user.name}</span>
|
|
756
|
-
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
|
|
757
|
-
</div>
|
|
758
|
-
</div>
|
|
759
|
-
</DropdownMenuLabel>
|
|
760
|
-
</DropdownMenuGroup>
|
|
761
|
-
{screens && screens.length > 0 ? (
|
|
762
|
-
<>
|
|
763
|
-
<DropdownMenuSeparator />
|
|
764
|
-
<DropdownMenuGroup>
|
|
765
|
-
{screens.map((one) => (
|
|
766
|
-
<DropdownMenuItem key={one.name} className="max-sm:min-h-11" render={<a href={one.path} />}>
|
|
767
|
-
<LayoutGrid className="me-2 h-4 w-4" />
|
|
768
|
-
{t(`screens.${one.name}`, one.name) ?? one.name}
|
|
769
|
-
</DropdownMenuItem>
|
|
770
|
-
))}
|
|
771
|
-
</DropdownMenuGroup>
|
|
772
|
-
</>
|
|
773
|
-
) : null}
|
|
774
|
-
{locales && locales.length > 0 && onLocale ? (
|
|
775
|
-
<>
|
|
776
|
-
<DropdownMenuSeparator />
|
|
777
|
-
<DropdownMenuGroup>
|
|
778
|
-
{localeEntries(locales, locale ?? '').map((entry) => (
|
|
779
|
-
<DropdownMenuItem key={entry.locale} className="max-sm:min-h-11" onClick={() => onLocale(entry.locale)}>
|
|
780
|
-
<Languages className="me-2 h-4 w-4" />
|
|
781
|
-
{entry.label}
|
|
782
|
-
{entry.active ? <Check className="ms-auto h-4 w-4" /> : null}
|
|
783
|
-
</DropdownMenuItem>
|
|
784
|
-
))}
|
|
785
|
-
</DropdownMenuGroup>
|
|
786
|
-
</>
|
|
787
|
-
) : null}
|
|
788
|
-
{provided && themes.length > 0 ? (
|
|
789
|
-
<>
|
|
790
|
-
<DropdownMenuSeparator />
|
|
791
|
-
<DropdownMenuGroup>
|
|
792
|
-
<DropdownMenuLabel className="text-xs text-muted-foreground">{t('admin.theme', 'Toggle theme')}</DropdownMenuLabel>
|
|
793
|
-
<DropdownMenuRadioGroup value={provided.theme} onValueChange={(next) => provided.setTheme(next as Theme)}>
|
|
794
|
-
{(
|
|
795
|
-
[
|
|
796
|
-
['light', Sun, t('admin.themeLight', 'Light')],
|
|
797
|
-
['dark', Moon, t('admin.themeDark', 'Dark')],
|
|
798
|
-
['system', Monitor, t('admin.themeSystem', 'System')],
|
|
799
|
-
] as const
|
|
800
|
-
)
|
|
801
|
-
.filter(([value]) => themes.includes(value))
|
|
802
|
-
.map(([value, Icon, label]) => (
|
|
803
|
-
<DropdownMenuRadioItem key={value} value={value} closeOnClick className={radioRow}>
|
|
804
|
-
<Icon />
|
|
805
|
-
{label}
|
|
806
|
-
</DropdownMenuRadioItem>
|
|
807
|
-
))}
|
|
808
|
-
</DropdownMenuRadioGroup>
|
|
809
|
-
</DropdownMenuGroup>
|
|
810
|
-
</>
|
|
811
|
-
) : null}
|
|
812
|
-
<DropdownMenuSeparator />
|
|
813
|
-
<DropdownMenuItem className="max-sm:min-h-11" onClick={onSignOut}>
|
|
814
|
-
<LogOut className="me-2 h-4 w-4" />
|
|
815
|
-
{t('admin.signOut', 'Sign out')}
|
|
816
|
-
</DropdownMenuItem>
|
|
817
|
-
</DropdownMenuContent>
|
|
818
|
-
</DropdownMenu>
|
|
819
|
-
</SidebarMenuItem>
|
|
820
|
-
</SidebarMenu>
|
|
821
|
-
)
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
/**
|
|
825
|
-
* `Sidebar`, except that on a phone its sheet is titled and described in the page's language;
|
|
826
|
-
* the primitive's sheet is English only, which a screen reader announces on an Arabic page.
|
|
827
|
-
* It has no close button: the overlay and Escape close it.
|
|
828
|
-
*/
|
|
829
|
-
export function NavSidebar({ children, side = 'left', t, ...props }: ComponentProps<typeof Sidebar> & { t: Translate }) {
|
|
830
|
-
const { isMobile, openMobile, setOpenMobile } = useSidebar()
|
|
831
|
-
if (!isMobile) return <Sidebar {...props} side={side} collapsible="offcanvas">{children}</Sidebar>
|
|
832
|
-
return (
|
|
833
|
-
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
|
|
834
|
-
<SheetContent
|
|
835
|
-
side={side}
|
|
836
|
-
showCloseButton={false}
|
|
837
|
-
data-sidebar="sidebar"
|
|
838
|
-
data-slot="sidebar"
|
|
839
|
-
data-mobile="true"
|
|
840
|
-
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0"
|
|
841
|
-
style={{ '--sidebar-width': '18rem' } as CSSProperties}
|
|
842
|
-
>
|
|
843
|
-
<SheetHeader className="sr-only">
|
|
844
|
-
<SheetTitle>{t('admin.navigation', 'Navigation')}</SheetTitle>
|
|
845
|
-
<SheetDescription>{t('admin.navigationAbout', 'The pages you can open.')}</SheetDescription>
|
|
846
|
-
</SheetHeader>
|
|
847
|
-
<div className="flex h-full w-full flex-col">{children}</div>
|
|
848
|
-
</SheetContent>
|
|
849
|
-
</Sheet>
|
|
850
|
-
)
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* The header: sidebar trigger, the screen's title and the theme toggle. The trigger's
|
|
855
|
-
* `aria-label` is its only name, so an Arabic page carries no English word for it.
|
|
856
|
-
*/
|
|
857
|
-
export function SiteHeader({ t, title }: { t: Translate; title?: string }) {
|
|
858
|
-
return (
|
|
859
|
-
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
|
860
|
-
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
861
|
-
<SidebarTrigger aria-label={t('admin.toggleSidebar', 'Toggle Sidebar')} className="-ms-1" />
|
|
862
|
-
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
|
|
863
|
-
<h1 className="text-base font-medium">{title ?? t('admin.nav', 'Admin')}</h1>
|
|
864
|
-
<div className="ms-auto flex items-center gap-2">
|
|
865
|
-
<ModeToggle t={t} />
|
|
866
|
-
</div>
|
|
867
|
-
</div>
|
|
868
|
-
</header>
|
|
869
|
-
)
|
|
870
|
-
}
|
|
871
|
-
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 b1ad44964867b0d65635652733a4821b190a720b1e283fc2f4aab3f82993effa
|