create-mithaq 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/index.js +127 -0
- package/package.json +19 -4
- package/template/apps/asas-platform/components.json +25 -0
- package/template/apps/asas-platform/gitignore +3 -0
- package/template/apps/asas-platform/package.json +55 -0
- package/template/apps/asas-platform/public/favicon.svg +1 -0
- package/template/apps/asas-platform/src/api.ts +147 -0
- package/template/apps/asas-platform/src/auth-frame.tsx +150 -0
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +686 -0
- package/template/apps/asas-platform/src/components/ui/accordion.tsx +74 -0
- package/template/apps/asas-platform/src/components/ui/alert-dialog.tsx +185 -0
- package/template/apps/asas-platform/src/components/ui/avatar.tsx +106 -0
- package/template/apps/asas-platform/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-platform/src/components/ui/breadcrumb.tsx +124 -0
- package/template/apps/asas-platform/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-platform/src/components/ui/button.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-platform/src/components/ui/card.tsx +102 -0
- package/template/apps/asas-platform/src/components/ui/chart.tsx +371 -0
- package/template/apps/asas-platform/src/components/ui/checkbox.tsx +28 -0
- package/template/apps/asas-platform/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/combobox.tsx +308 -0
- package/template/apps/asas-platform/src/components/ui/dialog.tsx +158 -0
- package/template/apps/asas-platform/src/components/ui/direction.tsx +6 -0
- package/template/apps/asas-platform/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +216 -0
- package/template/apps/asas-platform/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-platform/src/components/ui/field.tsx +236 -0
- package/template/apps/asas-platform/src/components/ui/input-group.tsx +155 -0
- package/template/apps/asas-platform/src/components/ui/input-otp.tsx +84 -0
- package/template/apps/asas-platform/src/components/ui/input.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/label.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/popover.tsx +90 -0
- package/template/apps/asas-platform/src/components/ui/radio-group.tsx +37 -0
- package/template/apps/asas-platform/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-platform/src/components/ui/scroll-area.tsx +54 -0
- package/template/apps/asas-platform/src/components/ui/select.tsx +204 -0
- package/template/apps/asas-platform/src/components/ui/separator.tsx +24 -0
- package/template/apps/asas-platform/src/components/ui/sheet.tsx +139 -0
- package/template/apps/asas-platform/src/components/ui/sidebar.tsx +714 -0
- package/template/apps/asas-platform/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-platform/src/components/ui/slider.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/spinner.tsx +10 -0
- package/template/apps/asas-platform/src/components/ui/switch.tsx +29 -0
- package/template/apps/asas-platform/src/components/ui/table.tsx +115 -0
- package/template/apps/asas-platform/src/components/ui/tabs.tsx +79 -0
- package/template/apps/asas-platform/src/components/ui/textarea.tsx +17 -0
- package/template/apps/asas-platform/src/components/ui/toggle-group.tsx +89 -0
- package/template/apps/asas-platform/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-platform/src/components/ui/tooltip.tsx +63 -0
- package/template/apps/asas-platform/src/copy.ts +1 -0
- package/template/apps/asas-platform/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-platform/src/lib/utils.ts +1 -0
- package/template/apps/asas-platform/src/links.ts +38 -0
- package/template/apps/asas-platform/src/locale-switch.tsx +26 -0
- package/template/apps/asas-platform/src/redirect.ts +1 -0
- package/template/apps/asas-platform/src/request-headers.ts +11 -0
- package/template/apps/asas-platform/src/routeTree.gen.ts +181 -0
- package/template/apps/asas-platform/src/router.tsx +7 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +4 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/screen.json +3609 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/types.ts +145 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.$.tsx +17 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.index.tsx +10 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.tsx +13 -0
- package/template/apps/asas-platform/src/routes/__root.tsx +95 -0
- package/template/apps/asas-platform/src/routes/api/$.ts +30 -0
- package/template/apps/asas-platform/src/routes/auth.sign-in.tsx +72 -0
- package/template/apps/asas-platform/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-platform/src/screen-form.tsx +34 -0
- package/template/apps/asas-platform/src/screen-list.tsx +139 -0
- package/template/apps/asas-platform/src/screen-names.ts +10 -0
- package/template/apps/asas-platform/src/screens.tsx +868 -0
- package/template/apps/asas-platform/src/server.ts +20 -0
- package/template/apps/asas-platform/src/slide.ts +22 -0
- package/template/apps/asas-platform/src/start.ts +42 -0
- package/template/apps/asas-platform/src/styles.css +200 -0
- package/template/apps/asas-platform/src/theme.ts +2 -0
- package/template/apps/asas-platform/tsconfig.json +19 -0
- package/template/apps/asas-platform/vite.config.ts +26 -0
- package/template/apps/asas-tanstack/components.json +25 -0
- package/template/apps/asas-tanstack/gitignore +3 -0
- package/template/apps/asas-tanstack/package.json +56 -0
- package/template/apps/asas-tanstack/public/favicon.svg +1 -0
- package/template/apps/asas-tanstack/src/api.ts +147 -0
- package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
- package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +686 -0
- package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
- package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
- package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
- package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
- package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
- package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
- package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
- package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
- package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
- package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
- package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +215 -0
- package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
- package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
- package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
- package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
- package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
- package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
- package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
- package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
- package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
- package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
- package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
- package/template/apps/asas-tanstack/src/copy.ts +870 -0
- package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
- package/template/apps/asas-tanstack/src/links.ts +38 -0
- package/template/apps/asas-tanstack/src/locale-switch.tsx +26 -0
- package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -0
- package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +382 -0
- package/template/apps/asas-tanstack/src/router.tsx +7 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +3883 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +158 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1800 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +92 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
- package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +101 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +109 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +86 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +122 -0
- package/template/apps/asas-tanstack/src/routes/index.tsx +142 -0
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-tanstack/src/screen-form.tsx +34 -0
- package/template/apps/asas-tanstack/src/screen-list.tsx +139 -0
- package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
- package/template/apps/asas-tanstack/src/screens.tsx +868 -0
- package/template/apps/asas-tanstack/src/server.ts +16 -0
- package/template/apps/asas-tanstack/src/slide.ts +22 -0
- package/template/apps/asas-tanstack/src/start.ts +57 -0
- package/template/apps/asas-tanstack/src/styles.css +159 -0
- package/template/apps/asas-tanstack/src/theme.ts +5 -0
- package/template/apps/asas-tanstack/tsconfig.json +19 -0
- package/template/apps/asas-tanstack/vite.config.ts +28 -0
- package/template/package.json +16 -0
- package/template/templates/asas/.env.example +16 -0
- package/template/templates/asas/components.json +22 -0
- package/template/templates/asas/mithaq.hints.json +25 -0
- package/template/templates/asas/package.json +38 -0
- package/template/templates/asas/src/asas.ts +93 -0
- package/template/templates/asas/src/audit-log/audit-log.ts +187 -0
- package/template/templates/asas/src/audit-log/sink.ts +89 -0
- package/template/templates/asas/src/audit-log/ui/activity.tsx +243 -0
- package/template/templates/asas/src/courses/courses.ts +612 -0
- package/template/templates/asas/src/courses/ui/archive-button.tsx +25 -0
- package/template/templates/asas/src/courses/ui/fee-cell.tsx +19 -0
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +11 -0
- package/template/templates/asas/src/courses/ui/no-courses.tsx +13 -0
- package/template/templates/asas/src/courses/ui/on-offer.ts +6 -0
- package/template/templates/asas/src/courses/ui/status-cell.tsx +31 -0
- package/template/templates/asas/src/enrollments/enrollments.ts +137 -0
- package/template/templates/asas/src/enrollments/ui/session-cell.tsx +65 -0
- package/template/templates/asas/src/kayan/auth/user/account.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/account.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/index.ts +18 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.access.ts +4 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.ts +27 -0
- package/template/templates/asas/src/kayan/auth/user/member.access.ts +10 -0
- package/template/templates/asas/src/kayan/auth/user/member.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/member.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/organization.access.ts +20 -0
- package/template/templates/asas/src/kayan/auth/user/organization.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/organization.ts +24 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/session.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/session.ts +28 -0
- package/template/templates/asas/src/kayan/auth/user/user.access.ts +29 -0
- package/template/templates/asas/src/kayan/auth/user/user.screens.ts +45 -0
- package/template/templates/asas/src/kayan/auth/user/user.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/verification.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/verification.ts +23 -0
- package/template/templates/asas/src/local-users/local-users.ts +143 -0
- package/template/templates/asas/src/media/media.ts +137 -0
- package/template/templates/asas/src/media/ui/preview.tsx +143 -0
- package/template/templates/asas/src/member-roles/member-roles.ts +207 -0
- package/template/templates/asas/src/messages/account.json +106 -0
- package/template/templates/asas/src/messages/auditLog.json +186 -0
- package/template/templates/asas/src/messages/courses.json +270 -0
- package/template/templates/asas/src/messages/enrollments.json +42 -0
- package/template/templates/asas/src/messages/invitation.json +66 -0
- package/template/templates/asas/src/messages/localUsers.json +42 -0
- package/template/templates/asas/src/messages/media.json +66 -0
- package/template/templates/asas/src/messages/member.json +42 -0
- package/template/templates/asas/src/messages/memberRoles.json +42 -0
- package/template/templates/asas/src/messages/mithaq.json +170 -0
- package/template/templates/asas/src/messages/organization.json +50 -0
- package/template/templates/asas/src/messages/organizationRole.json +50 -0
- package/template/templates/asas/src/messages/roles.json +58 -0
- package/template/templates/asas/src/messages/session.json +82 -0
- package/template/templates/asas/src/messages/sessions.json +106 -0
- package/template/templates/asas/src/messages/settings.json +66 -0
- package/template/templates/asas/src/messages/tags.json +38 -0
- package/template/templates/asas/src/messages/user.json +90 -0
- package/template/templates/asas/src/messages/verification.json +50 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
- package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
- package/template/templates/asas/src/migrations/schema.ts +29 -0
- package/template/templates/asas/src/mithaq.ts +406 -0
- package/template/templates/asas/src/pages.ts +74 -0
- package/template/templates/asas/src/platform-http.ts +96 -0
- package/template/templates/asas/src/rich-text-nodes.ts +11 -0
- package/template/templates/asas/src/roles/grants.ts +47 -0
- package/template/templates/asas/src/roles/roles.ts +184 -0
- package/template/templates/asas/src/roles/ui/role-form.tsx +226 -0
- package/template/templates/asas/src/roles/ui/roles.tsx +166 -0
- package/template/templates/asas/src/roles.ts +16 -0
- package/template/templates/asas/src/seed.ts +325 -0
- package/template/templates/asas/src/sessions/sessions.ts +155 -0
- package/template/templates/asas/src/settings/settings.ts +69 -0
- package/template/templates/asas/src/statements.ts +11 -0
- package/template/templates/asas/src/steps/expected-updated-at.ts +41 -0
- package/template/templates/asas/src/storage/defaults.ts +156 -0
- package/template/templates/asas/src/storage/index.ts +95 -0
- package/template/templates/asas/src/storage/local.ts +143 -0
- package/template/templates/asas/src/storage/r2.ts +5 -0
- package/template/templates/asas/src/storage/s3.ts +91 -0
- package/template/templates/asas/src/storage/uploadthing.ts +59 -0
- package/template/templates/asas/src/tags/tags.ts +80 -0
- package/template/templates/asas/src/tenant-access.ts +221 -0
- package/template/templates/asas/src/tenant-roles.ts +262 -0
- package/template/templates/asas/src/ui/app-shell.tsx +260 -0
- package/template/templates/asas/src/ui/kpi-card.tsx +25 -0
- package/template/templates/asas/src/ui/record-page.tsx +50 -0
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +15 -0
- package/template/templates/asas/src/ui/screen-nav.tsx +28 -0
- package/template/templates/asas/src/ui/session-labels.tsx +36 -0
- package/template/templates/asas/src/ui/table-page.tsx +9 -0
- package/template/templates/asas/src/ui/top-shell.tsx +60 -0
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +294 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +630 -0
- package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
- package/template/templates/asas/src/ui-dashboard/versions.tsx +432 -0
- package/template/templates/asas/src/ui-platform/home.tsx +63 -0
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +114 -0
- package/template/templates/asas/src/ui-platform/record-page.tsx +30 -0
- package/template/templates/asas/src/ui-platform/role-cells.tsx +52 -0
- package/template/templates/asas/src/ui-platform/user-actions.tsx +251 -0
- package/template/templates/asas/src/ui-portal/home.tsx +77 -0
- package/template/templates/asas/src/ui-portal/page.tsx +11 -0
- package/template/templates/auth/package.json +14 -0
- package/template/templates/auth/src/index.ts +690 -0
- package/template/templates/auth/src/platform-roles.ts +87 -0
- package/README.md +0 -3
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// The versions are read in the browser, as the viewer, from the kayan's own
|
|
2
|
+
// `findVersions`, which answers only the versions of rows the viewer reads, each
|
|
3
|
+
// cut to the columns it reads. The page draws this only for an actor holding that
|
|
4
|
+
// key, and the restore only for one holding `restoreVersion`; the server refuses
|
|
5
|
+
// either to anyone else whatever the page drew.
|
|
6
|
+
import { useContext, useEffect, useMemo, useState } from 'react'
|
|
7
|
+
import { same, type ClientField, type ComponentProps, type Row } from 'mithaq/admin'
|
|
8
|
+
import { FormContext, useAccess, useClient, useKayan, useLocale, useRecord, useScreen } from 'mithaq/admin/react'
|
|
9
|
+
import { empty } from '../audit-log/sink.ts'
|
|
10
|
+
import {
|
|
11
|
+
AlertDialog,
|
|
12
|
+
AlertDialogAction,
|
|
13
|
+
AlertDialogCancel,
|
|
14
|
+
AlertDialogContent,
|
|
15
|
+
AlertDialogDescription,
|
|
16
|
+
AlertDialogFooter,
|
|
17
|
+
AlertDialogHeader,
|
|
18
|
+
AlertDialogTitle,
|
|
19
|
+
} from '@/components/ui/alert-dialog'
|
|
20
|
+
import { Badge } from '@/components/ui/badge'
|
|
21
|
+
import { Button } from '@/components/ui/button'
|
|
22
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
23
|
+
import { Label } from '@/components/ui/label'
|
|
24
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
25
|
+
import { Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
26
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
27
|
+
import { blocksOf, cellFor, richTextPlainText } from '@/components/mithaq/cells.tsx'
|
|
28
|
+
import { formatDate, labelFor, refusalKeyOf, titleOf, type FindApi, type FoundPage } from '@/components/mithaq/controls.tsx'
|
|
29
|
+
import { endonym } from '@/components/mithaq/nav.tsx'
|
|
30
|
+
import { CloseButton } from '@/components/mithaq/nodes.tsx'
|
|
31
|
+
import { useRelationshipTitles } from '@/components/mithaq/relationship-cell.tsx'
|
|
32
|
+
|
|
33
|
+
/** One version as `findVersions` answers it. */
|
|
34
|
+
export type Version = {
|
|
35
|
+
id: string
|
|
36
|
+
latest: boolean
|
|
37
|
+
autosave: boolean
|
|
38
|
+
updatedAt: string | Date
|
|
39
|
+
version: Row
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** How many versions one read asks for, and how many more each "Show older" adds. */
|
|
43
|
+
const page = 10
|
|
44
|
+
|
|
45
|
+
/** The four status labels, each under its `version` key with the English it falls back to. */
|
|
46
|
+
const labels = {
|
|
47
|
+
currentDraft: 'Current draft',
|
|
48
|
+
draft: 'Draft',
|
|
49
|
+
currentlyPublished: 'Currently published',
|
|
50
|
+
previouslyPublished: 'Previously published',
|
|
51
|
+
} as const
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Which of the four labels a version wears, over the row's versions newest first.
|
|
55
|
+
* A draft is the current draft when it is the latest version, since then no
|
|
56
|
+
* published version is newer than it. A published version is the currently
|
|
57
|
+
* published one when it is the newest published version: the row always holds the
|
|
58
|
+
* last published copy and nothing unpublishes it. The newest page is always read
|
|
59
|
+
* first, so that version is on it whenever any published version is.
|
|
60
|
+
*/
|
|
61
|
+
const labelOf = (version: Version, versions: readonly Version[]): keyof typeof labels | undefined =>
|
|
62
|
+
// A reader without the `_status` column gets it cut from every version, and
|
|
63
|
+
// a status nobody can read is no status to draw.
|
|
64
|
+
!('_status' in version.version)
|
|
65
|
+
? undefined
|
|
66
|
+
: version.version._status === 'draft'
|
|
67
|
+
? version.latest ? 'currentDraft' : 'draft'
|
|
68
|
+
: version.id === versions.find((one) => one.version._status !== 'draft')?.id ? 'currentlyPublished' : 'previouslyPublished'
|
|
69
|
+
|
|
70
|
+
/** The columns every kayan of this project carries and no person edits, and the status the pill already says. */
|
|
71
|
+
const technical = new Set(['id', 'organization', 'createdAt', 'updatedAt', '_status'])
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The words of each block of `side`, and whether `other` holds no block of the
|
|
75
|
+
* same value, as a block is the unit a person reads a rich text change by. A block
|
|
76
|
+
* is held by its whole value, so one whose format or link changed is marked though
|
|
77
|
+
* its words did not.
|
|
78
|
+
*/
|
|
79
|
+
const changedBlocks = ({ side, other }: { side: unknown; other: unknown }) => {
|
|
80
|
+
const left = new Map<string, number>()
|
|
81
|
+
for (const block of blocksOf(other)) {
|
|
82
|
+
const key = JSON.stringify(block)
|
|
83
|
+
left.set(key, (left.get(key) ?? 0) + 1)
|
|
84
|
+
}
|
|
85
|
+
return blocksOf(side).flatMap((block) => {
|
|
86
|
+
const key = JSON.stringify(block)
|
|
87
|
+
const held = left.get(key) ?? 0
|
|
88
|
+
if (held) left.set(key, held - 1)
|
|
89
|
+
const text = richTextPlainText(block).trim()
|
|
90
|
+
return text ? [{ text, changed: held === 0 }] : []
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const entriesOf = (value: unknown) => (value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value) : [])
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The panel an edit page names as a component node. It reads the row the page
|
|
98
|
+
* draws and its kayan off the screen, so one file serves every kayan that keeps
|
|
99
|
+
* drafts.
|
|
100
|
+
*/
|
|
101
|
+
export function Versions(_: ComponentProps) {
|
|
102
|
+
const { t, locale } = useLocale()
|
|
103
|
+
const kayan = useKayan()
|
|
104
|
+
const record = useRecord()
|
|
105
|
+
const client = useClient()
|
|
106
|
+
const id = record?.id === undefined ? undefined : String(record.id)
|
|
107
|
+
// How many saves have landed on the form this panel sits in, an autosave
|
|
108
|
+
// included. A draft save never moves the row's own `updatedAt`, so this is what
|
|
109
|
+
// tells a panel that stayed mounted through one that its list is stale.
|
|
110
|
+
const saves = useContext(FormContext)?.saves ?? 0
|
|
111
|
+
// The row as this page last drew it: a save moves its `updatedAt` or bumps
|
|
112
|
+
// `saves`, and the panel reads its versions again.
|
|
113
|
+
const seen = `${id}@${String(record?.updatedAt ?? '')}@${saves}`
|
|
114
|
+
const [limit, setLimit] = useState(page)
|
|
115
|
+
const [read, setRead] = useState<{ seen: string; docs: Version[]; more: boolean } | { seen: string; failed: true }>()
|
|
116
|
+
const [opened, setOpened] = useState<Version>()
|
|
117
|
+
|
|
118
|
+
// The client is left out of the dependencies: the host makes a new one on every
|
|
119
|
+
// render, so following it would read the same page again on every render.
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!id) return
|
|
122
|
+
let live = true
|
|
123
|
+
;(client({ kayan: kayan.name, operation: 'findVersions', input: { where: { parent: { equals: id } }, sort: '-updatedAt', limit } }) as Promise<{ docs: Version[]; hasNextPage: boolean }>).then(
|
|
124
|
+
(found) => live && setRead({ seen, docs: found.docs, more: found.hasNextPage }),
|
|
125
|
+
() => live && setRead({ seen, failed: true }),
|
|
126
|
+
)
|
|
127
|
+
return () => {
|
|
128
|
+
live = false
|
|
129
|
+
}
|
|
130
|
+
}, [kayan.name, seen, limit])
|
|
131
|
+
|
|
132
|
+
const shown = read?.seen === seen ? read : undefined
|
|
133
|
+
const when = (at: string | Date) => formatDate({ value: at, locale }) ?? String(at)
|
|
134
|
+
return (
|
|
135
|
+
<div data-panel="versions" className="grid gap-4">
|
|
136
|
+
{shown === undefined ? (
|
|
137
|
+
<p className="text-muted-foreground text-sm">{t('version.loading', 'Loading versions…')}</p>
|
|
138
|
+
) : 'failed' in shown ? (
|
|
139
|
+
<p role="alert" className="text-destructive text-sm">{t('version.failed', 'The versions could not be read.')}</p>
|
|
140
|
+
) : !shown.docs.length ? (
|
|
141
|
+
<p className="text-muted-foreground text-sm">{t('version.none', 'No versions yet.')}</p>
|
|
142
|
+
) : (
|
|
143
|
+
<>
|
|
144
|
+
<Table>
|
|
145
|
+
<TableHeader>
|
|
146
|
+
<TableRow>
|
|
147
|
+
<TableHead>{t('version.updatedAt', 'Updated at')}</TableHead>
|
|
148
|
+
<TableHead>{t('version.status', 'Status')}</TableHead>
|
|
149
|
+
</TableRow>
|
|
150
|
+
</TableHeader>
|
|
151
|
+
<TableBody>
|
|
152
|
+
{shown.docs.map((version) => (
|
|
153
|
+
<TableRow key={version.id} data-version={version.id}>
|
|
154
|
+
<TableCell>
|
|
155
|
+
{/* Underlined and in the link colour at rest, since a phone has
|
|
156
|
+
no hover to show that the date opens the comparison, and
|
|
157
|
+
44 pixels tall. */}
|
|
158
|
+
<button type="button" data-button="open-version" className="text-primary inline-flex min-h-11 cursor-pointer items-center underline underline-offset-4" onClick={() => setOpened(version)}>
|
|
159
|
+
<time dateTime={new Date(version.updatedAt).toISOString()}>{when(version.updatedAt)}</time>
|
|
160
|
+
</button>
|
|
161
|
+
</TableCell>
|
|
162
|
+
<TableCell>
|
|
163
|
+
<div className="flex flex-wrap gap-1">
|
|
164
|
+
{version.autosave ? <Badge variant="outline">{t('version.autosave', 'Autosave')}</Badge> : null}
|
|
165
|
+
<Pill version={version} versions={shown.docs} />
|
|
166
|
+
</div>
|
|
167
|
+
</TableCell>
|
|
168
|
+
</TableRow>
|
|
169
|
+
))}
|
|
170
|
+
</TableBody>
|
|
171
|
+
</Table>
|
|
172
|
+
{shown.more ? (
|
|
173
|
+
<Button type="button" variant="outline" size="sm" onClick={() => setLimit(limit + page)}>
|
|
174
|
+
{t('version.more', 'Show older')}
|
|
175
|
+
</Button>
|
|
176
|
+
) : null}
|
|
177
|
+
{opened && id ? <Compare key={opened.id} version={opened} versions={shown.docs} parent={id} onClose={() => setOpened(undefined)} /> : null}
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A version's status pill, the currently published one standing out. */
|
|
185
|
+
function Pill({ version, versions }: { version: Version; versions: readonly Version[] }) {
|
|
186
|
+
const { t } = useLocale()
|
|
187
|
+
const label = labelOf(version, versions)
|
|
188
|
+
return label ? <Badge variant={label === 'currentlyPublished' ? 'default' : 'outline'}>{t(`version.${label}`, labels[label])}</Badge> : null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The sheet over the list: the version, and the one it is compared against, by
|
|
193
|
+
* default the newest one older than it, field by field, each value drawn by the
|
|
194
|
+
* field's own cell and a localized field one row per locale, modified fields only
|
|
195
|
+
* until the viewer asks for all. A column the viewer may not read is in neither
|
|
196
|
+
* snapshot, so it draws no row.
|
|
197
|
+
*/
|
|
198
|
+
function Compare({ version, versions, parent, onClose }: { version: Version; versions: readonly Version[]; parent: string; onClose: () => void }) {
|
|
199
|
+
const { t, locale, dir } = useLocale()
|
|
200
|
+
const kayan = useKayan()
|
|
201
|
+
const client = useClient()
|
|
202
|
+
const screen = useScreen()
|
|
203
|
+
const restores = useAccess().collections?.[kayan.name]?.operations?.restoreVersion === true
|
|
204
|
+
/** Whether the kayan's restore takes `expectedUpdatedAt`, as the form this panel sits in says. */
|
|
205
|
+
const takes = useContext(FormContext)?.expectedUpdatedAtOn?.includes('restoreVersion') === true
|
|
206
|
+
/** The version compared against, or `''` for the one before. */
|
|
207
|
+
const [against, setAgainst] = useState('')
|
|
208
|
+
const [modifiedOnly, setModifiedOnly] = useState(true)
|
|
209
|
+
/** The version before this one, read once: the list holds every other version a person may pick. */
|
|
210
|
+
const [prior, setPrior] = useState<{ version: Version | null } | { failed: true }>()
|
|
211
|
+
/** The restore waiting on its confirm, and whether it restores as a draft. */
|
|
212
|
+
const [confirming, setConfirming] = useState<{ draft: boolean }>()
|
|
213
|
+
const [restoring, setRestoring] = useState(false)
|
|
214
|
+
/** A restore the server refused, drawn in the sheet, which stays open. */
|
|
215
|
+
const [restoreFailed, setRestoreFailed] = useState<unknown>(undefined)
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
let live = true
|
|
219
|
+
const older = { and: [{ parent: { equals: parent } }, { updatedAt: { less_than: new Date(version.updatedAt).toISOString() } }] }
|
|
220
|
+
;(client({ kayan: kayan.name, operation: 'findVersions', input: { where: older, sort: '-updatedAt', limit: 1 } }) as Promise<{ docs: Version[] }>).then(
|
|
221
|
+
(found) => live && setPrior({ version: found.docs[0] ?? null }),
|
|
222
|
+
() => live && setPrior({ failed: true }),
|
|
223
|
+
)
|
|
224
|
+
return () => {
|
|
225
|
+
live = false
|
|
226
|
+
}
|
|
227
|
+
}, [version.id])
|
|
228
|
+
|
|
229
|
+
const previous = prior && 'version' in prior ? prior.version : null
|
|
230
|
+
const from = against ? versions.find((one) => one.id === against) ?? null : previous
|
|
231
|
+
const rows = kayan.fields
|
|
232
|
+
.filter((field) => field.kind !== 'join' && !technical.has(field.name) && (field.name in version.version || (!!from && field.name in from.version)))
|
|
233
|
+
.flatMap((field): { key: string; field: ClientField; before: unknown; after: unknown; code?: string }[] => {
|
|
234
|
+
const before = from?.version[field.name]
|
|
235
|
+
const after = version.version[field.name]
|
|
236
|
+
if (field.rules.localized !== true) return [{ key: field.name, field, before, after }]
|
|
237
|
+
// Each locale's value alone, drawn by the field's cell as a plain value.
|
|
238
|
+
const plain: ClientField = { ...field, rules: { ...field.rules, localized: false } }
|
|
239
|
+
const codes = [...new Set([...entriesOf(before), ...entriesOf(after)])]
|
|
240
|
+
return codes.map((code) => ({
|
|
241
|
+
key: `${field.name}.${code}`,
|
|
242
|
+
field: plain,
|
|
243
|
+
before: (before as Record<string, unknown> | undefined)?.[code],
|
|
244
|
+
after: (after as Record<string, unknown> | undefined)?.[code],
|
|
245
|
+
code,
|
|
246
|
+
}))
|
|
247
|
+
})
|
|
248
|
+
// Whether the two sides differ, once per row, which both the filter and the row's mark read.
|
|
249
|
+
.map((row) => ({ ...row, changed: !(same(row.before, row.after) || (empty(row.before) && empty(row.after))) }))
|
|
250
|
+
.filter((row) => !modifiedOnly || row.changed)
|
|
251
|
+
|
|
252
|
+
// Every kayan this screen crosses, so a relationship's value is named the way
|
|
253
|
+
// the list names it.
|
|
254
|
+
const kayans = useMemo(() => Object.values(screen.kayan), [screen])
|
|
255
|
+
const api = useMemo<FindApi>(
|
|
256
|
+
() => Object.fromEntries(kayans.map((one) => [one.name, { find: (input: Row) => client({ kayan: one.name, operation: 'find', input }) as Promise<FoundPage> }])),
|
|
257
|
+
[kayans],
|
|
258
|
+
)
|
|
259
|
+
const titles = useRelationshipTitles({ fields: kayan.fields, docs: [version.version, from?.version ?? {}], context: { api, client: kayans, locale } })
|
|
260
|
+
const copy = { t, locale }
|
|
261
|
+
// A rich text value whole, a block to a line, where its list cell draws the
|
|
262
|
+
// first block alone: a change further down would read as no change. A block the
|
|
263
|
+
// other side does not hold is marked, as removed (`del`) on the side compared
|
|
264
|
+
// against and as added (`ins`) on the side viewed: struck through and underlined
|
|
265
|
+
// as well as tinted, so the mark does not rest on colour alone.
|
|
266
|
+
const drawn = (field: ClientField, value: unknown, other: unknown, mark: 'del' | 'ins') => {
|
|
267
|
+
if (empty(value)) return '—'
|
|
268
|
+
if (field.kind !== 'richText') return cellFor(field)({ kayan, field, value, t, locale, titles })
|
|
269
|
+
const blocks = changedBlocks({ side: value, other })
|
|
270
|
+
// A value of images or empty blocks alone has no words, and reads as empty.
|
|
271
|
+
if (!blocks.length) return '—'
|
|
272
|
+
return blocks.map(({ text, changed }, at) => {
|
|
273
|
+
const Mark = mark
|
|
274
|
+
return <p key={at} dir="auto" className="whitespace-pre-line">{changed ? <Mark data-block-changed="" className={mark === 'del' ? 'bg-destructive/15 line-through' : 'bg-primary/15 underline'}>{text}</Mark> : text}</p>
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
const when = (at: string | Date) => formatDate({ value: at, locale }) ?? String(at)
|
|
278
|
+
const named = (one: Version) => {
|
|
279
|
+
const label = labelOf(one, versions)
|
|
280
|
+
return label ? `${when(one.updatedAt)} · ${t(`version.${label}`, labels[label])}` : when(one.updatedAt)
|
|
281
|
+
}
|
|
282
|
+
const options = [
|
|
283
|
+
// With its date, so the select says which version it compares against.
|
|
284
|
+
{ value: '', label: previous ? `${t('version.previousVersion', 'Previous version')} · ${when(previous.updatedAt)}` : (t('version.previousVersion', 'Previous version') ?? '') },
|
|
285
|
+
// The version before is offered once, as "Previous version".
|
|
286
|
+
...versions.filter((one) => one.id !== version.id && one.id !== previous?.id).map((one) => ({ value: one.id, label: named(one) })),
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Puts the version back, onto the row or as a new draft, and draws the page
|
|
291
|
+
* again from the server, so the form reads what the restore wrote. A refused or
|
|
292
|
+
* failed restore keeps the sheet open and draws its own sentence and the
|
|
293
|
+
* server's (`refusalKeyOf`, which the form's own banner reads the same way).
|
|
294
|
+
*/
|
|
295
|
+
const restore = async (draft: boolean) => {
|
|
296
|
+
setRestoring(true)
|
|
297
|
+
setRestoreFailed(undefined)
|
|
298
|
+
try {
|
|
299
|
+
// The instant of the latest version this panel read, where the kayan's
|
|
300
|
+
// restore takes it, so a restore over a newer draft another person saved
|
|
301
|
+
// since is refused 409 rather than burying it.
|
|
302
|
+
const read = takes ? versions.find((one) => one.latest)?.version.updatedAt : undefined
|
|
303
|
+
await client({ kayan: kayan.name, operation: 'restoreVersion', input: { id: version.id, ...(read !== undefined && { expectedUpdatedAt: read }) }, draft })
|
|
304
|
+
window.location.reload()
|
|
305
|
+
} catch (thrown) {
|
|
306
|
+
setRestoring(false)
|
|
307
|
+
setConfirming(undefined)
|
|
308
|
+
setRestoreFailed(thrown)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const restoreFailedKey = restoreFailed === undefined ? undefined : refusalKeyOf(restoreFailed)
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<Sheet open onOpenChange={(open) => !open && onClose()}>
|
|
315
|
+
{/* The primitive's `data-[side=*]:w-3/4` is outranked by naming the same
|
|
316
|
+
side, so the sheet is the phone's whole width. */}
|
|
317
|
+
<SheetContent showCloseButton={false} side={dir === 'rtl' ? 'left' : 'right'} className="w-full overflow-y-auto data-[side=left]:w-full data-[side=right]:w-full data-[side=left]:sm:max-w-2xl data-[side=right]:sm:max-w-2xl">
|
|
318
|
+
<SheetHeader>
|
|
319
|
+
<SheetTitle>{t('version.compareVersions', 'Compare versions')}</SheetTitle>
|
|
320
|
+
</SheetHeader>
|
|
321
|
+
<div className="grid gap-4 px-4">
|
|
322
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
323
|
+
<div className="grid gap-1.5">
|
|
324
|
+
<span className="text-muted-foreground text-xs">{t('version.comparingAgainst', 'Comparing against')}</span>
|
|
325
|
+
<Select value={against} disabled={prior === undefined} onValueChange={(value) => setAgainst(String(value ?? ''))}>
|
|
326
|
+
<SelectTrigger className="w-full max-sm:data-[size=default]:h-11"><SelectValue>{options.find((one) => one.value === against)?.label}</SelectValue></SelectTrigger>
|
|
327
|
+
<SelectContent>
|
|
328
|
+
{options.map((one) => (
|
|
329
|
+
<SelectItem key={one.value} value={one.value}>{one.label}</SelectItem>
|
|
330
|
+
))}
|
|
331
|
+
</SelectContent>
|
|
332
|
+
</Select>
|
|
333
|
+
</div>
|
|
334
|
+
<div className="grid gap-1.5">
|
|
335
|
+
<span className="text-muted-foreground text-xs">{t('version.currentlyViewing', 'Currently viewing')}</span>
|
|
336
|
+
<div className="flex flex-wrap items-center gap-1 text-sm">
|
|
337
|
+
<time dateTime={new Date(version.updatedAt).toISOString()}>{when(version.updatedAt)}</time>
|
|
338
|
+
{version.autosave ? <Badge variant="outline">{t('version.autosave', 'Autosave')}</Badge> : null}
|
|
339
|
+
<Pill version={version} versions={versions} />
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
<div className="flex items-center gap-2">
|
|
344
|
+
<Checkbox id={`${version.id}-modified-only`} data-modified-only checked={modifiedOnly} onCheckedChange={(checked) => setModifiedOnly(checked === true)} />
|
|
345
|
+
<Label htmlFor={`${version.id}-modified-only`}>{t('version.modifiedOnly', 'Modified only')}</Label>
|
|
346
|
+
</div>
|
|
347
|
+
{prior === undefined ? (
|
|
348
|
+
<p className="text-muted-foreground text-sm">{t('version.loading', 'Loading versions…')}</p>
|
|
349
|
+
) : 'failed' in prior && !against ? (
|
|
350
|
+
<p role="alert" className="text-destructive text-sm">{t('version.failed', 'The versions could not be read.')}</p>
|
|
351
|
+
) : !rows.length ? (
|
|
352
|
+
<p className="text-muted-foreground text-sm">{t('auditLog.none', 'No field you may read changed.')}</p>
|
|
353
|
+
) : (
|
|
354
|
+
<ul className="grid gap-3 text-sm">
|
|
355
|
+
{/* Which side is which, over the two columns, since on a phone
|
|
356
|
+
the two labels above stack in one. */}
|
|
357
|
+
<li aria-hidden="true" className="text-muted-foreground grid grid-cols-2 gap-2 text-xs">
|
|
358
|
+
<span>{t('version.comparingAgainst', 'Comparing against')}</span>
|
|
359
|
+
<span>{t('version.currentlyViewing', 'Currently viewing')}</span>
|
|
360
|
+
</li>
|
|
361
|
+
{rows.map((row) => {
|
|
362
|
+
// What the "Modified only" filter keeps, so with every field shown
|
|
363
|
+
// the rows that differ say so in words and in a rule at their start.
|
|
364
|
+
const { changed } = row
|
|
365
|
+
return (
|
|
366
|
+
<li key={row.key} data-diff={row.key} data-changed={changed ? '' : undefined} className={changed ? 'border-primary grid gap-1 border-s-2 ps-2' : 'grid gap-1'}>
|
|
367
|
+
<span className="flex items-center gap-1.5 font-medium">
|
|
368
|
+
{labelFor({ kayan, field: row.field, copy })}
|
|
369
|
+
{row.code ? <Badge variant="outline">{endonym(row.code)}</Badge> : null}
|
|
370
|
+
{changed && !modifiedOnly ? <Badge data-badge="changed">{t('version.fieldChanged', 'Changed')}</Badge> : null}
|
|
371
|
+
</span>
|
|
372
|
+
<div className="grid grid-cols-2 gap-2">
|
|
373
|
+
<div data-before className="bg-muted/50 min-w-0 rounded-md px-2 py-1 break-words">{drawn(row.field, row.before, row.after, 'del')}</div>
|
|
374
|
+
<div data-after className="bg-muted min-w-0 rounded-md px-2 py-1 break-words">{drawn(row.field, row.after, row.before, 'ins')}</div>
|
|
375
|
+
</div>
|
|
376
|
+
</li>
|
|
377
|
+
)
|
|
378
|
+
})}
|
|
379
|
+
</ul>
|
|
380
|
+
)}
|
|
381
|
+
{restoreFailed !== undefined ? (
|
|
382
|
+
<>
|
|
383
|
+
<p role="alert" className="rounded-md border border-destructive/50 px-3 py-2 text-sm text-destructive">
|
|
384
|
+
{t('version.problemRestoringVersion', 'There was a problem restoring this version')}
|
|
385
|
+
</p>
|
|
386
|
+
{restoreFailedKey ? (
|
|
387
|
+
<p role="alert" className="rounded-md border border-destructive/50 px-3 py-2 text-sm text-destructive">
|
|
388
|
+
{t(restoreFailedKey, restoreFailedKey)}
|
|
389
|
+
</p>
|
|
390
|
+
) : null}
|
|
391
|
+
</>
|
|
392
|
+
) : null}
|
|
393
|
+
</div>
|
|
394
|
+
{restores && labelOf(version, versions) !== 'currentlyPublished' ? (
|
|
395
|
+
// Not offered for the version the row already holds, where a restore would add a version and an
|
|
396
|
+
// activity line and change nothing. Sticky at the popup's foot, so Restore stays in reach however long the diff is.
|
|
397
|
+
<SheetFooter className="bg-inherit sticky bottom-0 flex-row flex-wrap justify-end border-t">
|
|
398
|
+
{'_status' in version.version && version.version._status !== 'draft' ? (
|
|
399
|
+
<Button type="button" variant="outline" className="max-sm:h-11" data-button="restore-as-draft" disabled={restoring} onClick={() => setConfirming({ draft: true })}>
|
|
400
|
+
{t('version.restoreAsDraft', 'Restore as draft')}
|
|
401
|
+
</Button>
|
|
402
|
+
) : null}
|
|
403
|
+
<Button type="button" className="max-sm:h-11" data-button="restore" disabled={restoring} onClick={() => setConfirming({ draft: false })}>
|
|
404
|
+
{t('version.restoreThisVersion', 'Restore this version')}
|
|
405
|
+
</Button>
|
|
406
|
+
</SheetFooter>
|
|
407
|
+
) : null}
|
|
408
|
+
<AlertDialog open={!!confirming} onOpenChange={(open) => !open && setConfirming(undefined)}>
|
|
409
|
+
<AlertDialogContent>
|
|
410
|
+
<AlertDialogHeader>
|
|
411
|
+
<AlertDialogTitle>{t('version.confirmVersionRestoration', 'Confirm version restoration')}</AlertDialogTitle>
|
|
412
|
+
<AlertDialogDescription>
|
|
413
|
+
{(t('version.aboutToRestore', 'You are about to restore this {label} to the state it was in on {versionDate}.') ?? '')
|
|
414
|
+
.replace('{label}', titleOf({ kayan, copy }))
|
|
415
|
+
.replace('{versionDate}', when(version.updatedAt))}
|
|
416
|
+
</AlertDialogDescription>
|
|
417
|
+
</AlertDialogHeader>
|
|
418
|
+
<AlertDialogFooter className="max-sm:[&_button]:h-11">
|
|
419
|
+
<AlertDialogCancel>{t('admin.cancel', 'Cancel')}</AlertDialogCancel>
|
|
420
|
+
<AlertDialogAction data-button="confirm-restore" disabled={restoring} onClick={() => confirming && void restore(confirming.draft)}>
|
|
421
|
+
{confirming?.draft ? t('version.restoreAsDraft', 'Restore as draft') : t('version.restore', 'Restore')}
|
|
422
|
+
</AlertDialogAction>
|
|
423
|
+
</AlertDialogFooter>
|
|
424
|
+
</AlertDialogContent>
|
|
425
|
+
</AlertDialog>
|
|
426
|
+
{/* The registry's close, named in the page's language, in place of the
|
|
427
|
+
primitive's English one. */}
|
|
428
|
+
<CloseButton Close={SheetClose} label={t('admin.close', 'Close') ?? 'Close'} />
|
|
429
|
+
</SheetContent>
|
|
430
|
+
</Sheet>
|
|
431
|
+
)
|
|
432
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What `/platform` itself draws: where staff start, inside the platform's
|
|
2
|
+
// shell, with the figure card the dashboard and the portal homes draw for each
|
|
3
|
+
// list the nav offers the viewer, with how many rows of it they may read across
|
|
4
|
+
// every organization and a link to it. A list whose count the viewer may not
|
|
5
|
+
// read, or whose count failed, shows a dash.
|
|
6
|
+
import { ArrowRight } from 'lucide-react'
|
|
7
|
+
import type { LoaderTools, PageProps } from 'mithaq/admin'
|
|
8
|
+
import { useLocale, useScreen } from 'mithaq/admin/react'
|
|
9
|
+
import { PlatformShell, iconOf } from '../ui/app-shell.tsx'
|
|
10
|
+
import { KpiCard } from '../ui/kpi-card.tsx'
|
|
11
|
+
import { navLabel } from '../ui/screen-nav.tsx'
|
|
12
|
+
|
|
13
|
+
/** How many rows of each kayan the viewer may count, read as the viewer, keyed by its kayan. */
|
|
14
|
+
export const loader = async ({ client, access }: LoaderTools): Promise<Record<string, number | undefined>> =>
|
|
15
|
+
Object.fromEntries(
|
|
16
|
+
await Promise.all(
|
|
17
|
+
Object.entries(access.collections ?? {})
|
|
18
|
+
.filter(([, one]) => one?.operations?.count === true)
|
|
19
|
+
.map(async ([kayan]) => [kayan, await (client({ kayan, operation: 'count', input: {} }) as Promise<{ totalDocs: number }>).then((found) => found.totalDocs).catch(() => undefined)] as const),
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export function Home({ loaderData: counts }: PageProps<Record<string, number | undefined>>) {
|
|
24
|
+
const { locale, t } = useLocale()
|
|
25
|
+
const screen = useScreen()
|
|
26
|
+
return (
|
|
27
|
+
<PlatformShell
|
|
28
|
+
regions={{
|
|
29
|
+
header: <h1>{t('platform.title', 'Platform')}</h1>,
|
|
30
|
+
main: (
|
|
31
|
+
<section data-dashboard="platform" className="@container grid gap-4">
|
|
32
|
+
<p className="text-muted-foreground">{t('platform.about', 'Every organization and every person on the platform, for its staff.')}</p>
|
|
33
|
+
{/* Cards as tall as their content, the figure and the link: the card's own padding and gap
|
|
34
|
+
are the roomy ones of the platform's style, which a count and one link do not need. */}
|
|
35
|
+
<div className="grid grid-cols-2 items-start gap-4 @2xl:grid-cols-3 @5xl:grid-cols-4 [&_[data-slot=card]]:[--card-spacing:--spacing(4)]">
|
|
36
|
+
{screen.nav.flatMap((entry) => {
|
|
37
|
+
if (!entry.kayan) return []
|
|
38
|
+
const Icon = iconOf(entry.path)
|
|
39
|
+
const counted = counts?.[entry.kayan]
|
|
40
|
+
return [
|
|
41
|
+
<KpiCard
|
|
42
|
+
key={entry.kayan}
|
|
43
|
+
kpi={entry.kayan}
|
|
44
|
+
icon={<Icon className="size-4" />}
|
|
45
|
+
title={navLabel({ entry, screen, locale, t })}
|
|
46
|
+
value={counted === undefined ? '—' : new Intl.NumberFormat(locale).format(counted)}
|
|
47
|
+
// Underlined, with an arrow that follows the reading direction, so it reads as a link.
|
|
48
|
+
footer={
|
|
49
|
+
<a href={`${screen.path}${entry.path}`} className="text-foreground inline-flex items-center gap-1 underline underline-offset-4 max-sm:min-h-11">
|
|
50
|
+
{t('portal.open', 'Open the list')}
|
|
51
|
+
<ArrowRight className="size-3.5 rtl:-scale-x-100" aria-hidden="true" />
|
|
52
|
+
</a>
|
|
53
|
+
}
|
|
54
|
+
/>,
|
|
55
|
+
]
|
|
56
|
+
})}
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
),
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The list's cell for the `slug` column. Rename is drawn only where this actor holds
|
|
2
|
+
// `organization.rename`, and members only where this actor reads them; the route refuses a rename
|
|
3
|
+
// without the key. A rename that lands asks the list behind it to be read again through
|
|
4
|
+
// `mithaq:refresh`, so the list keeps its scroll.
|
|
5
|
+
import { useState, type FormEvent } from 'react'
|
|
6
|
+
import { holds, type CellProps } from 'mithaq/admin'
|
|
7
|
+
import { useAccess, useLocale, useScreen } from 'mithaq/admin/react'
|
|
8
|
+
import { writeQuery } from '@/components/mithaq/list-query.ts'
|
|
9
|
+
import { Button } from '@/components/ui/button'
|
|
10
|
+
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
11
|
+
import { Input } from '@/components/ui/input'
|
|
12
|
+
import { Label } from '@/components/ui/label'
|
|
13
|
+
|
|
14
|
+
const slugShape = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
15
|
+
|
|
16
|
+
export function OrganizationActions({ value, row }: CellProps) {
|
|
17
|
+
const { t } = useLocale()
|
|
18
|
+
const access = useAccess()
|
|
19
|
+
const screen = useScreen()
|
|
20
|
+
const renames = holds(access, 'organization.rename')
|
|
21
|
+
const members = holds(access, 'member.find') && screen.kayan.member?.pages.list !== undefined
|
|
22
|
+
const [open, setOpen] = useState(false)
|
|
23
|
+
const [refusal, setRefusal] = useState<string>()
|
|
24
|
+
const [done, setDone] = useState<string>()
|
|
25
|
+
/** Said here rather than by the browser's own bubble, which is in the browser's language. */
|
|
26
|
+
const [wrong, setWrong] = useState<{ name?: string; slug?: string }>({})
|
|
27
|
+
const id = String(row.id ?? '')
|
|
28
|
+
const save = async (event: FormEvent<HTMLFormElement>) => {
|
|
29
|
+
event.preventDefault()
|
|
30
|
+
const box = (key: string) => (event.currentTarget.elements.namedItem(key) as HTMLInputElement | null)?.value ?? ''
|
|
31
|
+
const name = box('name')
|
|
32
|
+
const slug = box('slug')
|
|
33
|
+
const found = {
|
|
34
|
+
name: name.trim() ? undefined : t('organization.name.required', 'This field is required.'),
|
|
35
|
+
slug: !slug ? t('organization.slug.required', 'This field is required.') : slugShape.test(slug) ? undefined : t('platform.slugPattern', 'A slug is lowercase letters and digits joined by single dashes, such as admin-org.'),
|
|
36
|
+
}
|
|
37
|
+
setWrong(found)
|
|
38
|
+
if (found.name || found.slug) return
|
|
39
|
+
const response = await fetch('/api/platform/organizations/update', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ id, name, slug }),
|
|
43
|
+
})
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const key = ((await response.json().catch(() => null)) as { data?: { key?: string } } | null)?.data?.key
|
|
46
|
+
return setRefusal((key && t(key)) ?? t('platform.refused', 'The platform refused that action.'))
|
|
47
|
+
}
|
|
48
|
+
window.dispatchEvent(new Event('mithaq:refresh'))
|
|
49
|
+
// A function, so a name holding `$&` is put in as written.
|
|
50
|
+
setDone((t('platform.renamed', 'The organization is now named {name}.') ?? '').replace('{name}', () => name))
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<div data-organization={id} className="flex items-center gap-2 whitespace-nowrap">
|
|
54
|
+
{/* An identifier, read left to right in either locale. */}
|
|
55
|
+
<span dir="ltr">{String(value ?? '')}</span>
|
|
56
|
+
{renames ? (
|
|
57
|
+
<Button size="sm" variant="outline" className="max-sm:h-11" data-action="rename" onClick={() => setOpen(true)}>
|
|
58
|
+
{t('platform.rename', 'Rename')}
|
|
59
|
+
</Button>
|
|
60
|
+
) : null}
|
|
61
|
+
{members ? (
|
|
62
|
+
<Button
|
|
63
|
+
size="sm"
|
|
64
|
+
variant="outline"
|
|
65
|
+
className="max-sm:h-11"
|
|
66
|
+
data-action="members"
|
|
67
|
+
nativeButton={false}
|
|
68
|
+
render={<a href={`${screen.path}${screen.kayan.member!.pages.list!.path}?${new URLSearchParams(writeQuery({ where: { or: [{ and: [{ organizationId: { equals: id } }] }] } }))}`} />}
|
|
69
|
+
>
|
|
70
|
+
{t('platform.members', 'Members')}
|
|
71
|
+
</Button>
|
|
72
|
+
) : null}
|
|
73
|
+
{open ? (
|
|
74
|
+
<Dialog open onOpenChange={setOpen}>
|
|
75
|
+
{/* Cancel is the way out, so the primitive's own close, named in
|
|
76
|
+
English alone, is not drawn beside it. */}
|
|
77
|
+
<DialogContent showCloseButton={false}>
|
|
78
|
+
<DialogHeader>
|
|
79
|
+
<DialogTitle>{t('platform.rename', 'Rename')}</DialogTitle>
|
|
80
|
+
<DialogDescription aria-live="polite">{done ?? t('platform.renameAbout', 'The new name and slug take the place of the old ones for everyone in this organization.')}</DialogDescription>
|
|
81
|
+
</DialogHeader>
|
|
82
|
+
{done ? (
|
|
83
|
+
<DialogFooter>
|
|
84
|
+
<DialogClose render={<Button type="button" variant="outline" />}>{t('admin.close', 'Close')}</DialogClose>
|
|
85
|
+
</DialogFooter>
|
|
86
|
+
) : (
|
|
87
|
+
<form noValidate onSubmit={(event) => void save(event)} className="grid gap-4">
|
|
88
|
+
<div className="grid gap-2">
|
|
89
|
+
<Label htmlFor={`organization-name-${id}`}>{t('organization.name', 'Name')}</Label>
|
|
90
|
+
<Input id={`organization-name-${id}`} name="name" required aria-required dir="auto" defaultValue={String(row.name ?? '')} aria-invalid={wrong.name ? true : undefined} aria-describedby={wrong.name ? `organization-name-${id}-error` : undefined} />
|
|
91
|
+
{wrong.name ? <p id={`organization-name-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.name}</p> : null}
|
|
92
|
+
</div>
|
|
93
|
+
<div className="grid gap-2">
|
|
94
|
+
<Label htmlFor={`organization-slug-${id}`}>{t('organization.slug', 'Slug')}</Label>
|
|
95
|
+
<Input id={`organization-slug-${id}`} name="slug" required aria-required dir="ltr" pattern={slugShape.source.slice(1, -1)} defaultValue={String(value ?? '')} aria-invalid={wrong.slug ? true : undefined} aria-describedby={wrong.slug ? `organization-slug-${id}-error` : undefined} />
|
|
96
|
+
{wrong.slug ? <p id={`organization-slug-${id}-error`} role="alert" className="text-sm text-destructive">{wrong.slug}</p> : null}
|
|
97
|
+
</div>
|
|
98
|
+
{refusal ? (
|
|
99
|
+
<p role="alert" className="text-sm text-destructive">
|
|
100
|
+
{refusal}
|
|
101
|
+
</p>
|
|
102
|
+
) : null}
|
|
103
|
+
<DialogFooter>
|
|
104
|
+
<DialogClose render={<Button type="button" variant="outline" />}>{t('admin.cancel', 'Cancel')}</DialogClose>
|
|
105
|
+
<Button type="submit">{t('admin.save', 'Save')}</Button>
|
|
106
|
+
</DialogFooter>
|
|
107
|
+
</form>
|
|
108
|
+
)}
|
|
109
|
+
</DialogContent>
|
|
110
|
+
</Dialog>
|
|
111
|
+
) : null}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The platform's record page: the shared one under a link back to the list the
|
|
2
|
+
// record belongs to, so a person's page, an organization's and a role's are never
|
|
3
|
+
// a dead end. The link is named as the nav names the list, which is already the
|
|
4
|
+
// viewer's own word for it.
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import { ArrowLeft } from 'lucide-react'
|
|
7
|
+
import type { LayoutProps } from 'mithaq/admin'
|
|
8
|
+
import { useKayan, useLocale, useScreen } from 'mithaq/admin/react'
|
|
9
|
+
import { RecordPage } from '../ui/record-page.tsx'
|
|
10
|
+
import { navLabel } from '../ui/screen-nav.tsx'
|
|
11
|
+
|
|
12
|
+
export const regions = ['main', 'side', 'danger'] as const
|
|
13
|
+
|
|
14
|
+
export function PlatformRecordPage(props: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
|
|
15
|
+
const { locale, t } = useLocale()
|
|
16
|
+
const screen = useScreen()
|
|
17
|
+
const kayan = useKayan()
|
|
18
|
+
const list = screen.nav.find((entry) => entry.kayan === kayan.name)
|
|
19
|
+
return (
|
|
20
|
+
<div className="grid gap-4">
|
|
21
|
+
{list ? (
|
|
22
|
+
<a href={`${screen.path}${list.path}`} data-back="" className="inline-flex w-fit items-center gap-1.5 text-sm underline underline-offset-4 max-sm:min-h-11">
|
|
23
|
+
<ArrowLeft className="size-4 rtl:-scale-x-100" aria-hidden="true" />
|
|
24
|
+
<span>{navLabel({ entry: list, screen, locale, t })}</span>
|
|
25
|
+
</a>
|
|
26
|
+
) : null}
|
|
27
|
+
<RecordPage {...props} />
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|