From ad1370ea7e712e2ace21e1ae9d9576109cdb280f Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Fri, 12 Jun 2026 19:10:24 +0200 Subject: [PATCH] feat: enhance configuration and add new demo layouts - Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure. --- app/api/agenda/ical/route.ts | 60 ++ app/apple-icon.png | Bin 23130 -> 4538 bytes app/compte/layout.tsx | 7 +- app/demo/agenda/[[...segments]]/page.tsx | 12 + app/demo/agenda/layout.tsx | 22 + app/demo/contacts/layout.tsx | 22 + app/demo/contacts/page.tsx | 4 + app/demo/drive/[[...segments]]/page.tsx | 1 + app/demo/drive/layout.tsx | 28 + app/demo/mail/[[...segments]]/page.tsx | 4 + app/demo/mail/{page.tsx => layout.tsx} | 10 +- app/drive/(browser)/[[...segments]]/page.tsx | 29 +- app/drive/mounts/oauth/callback/page.tsx | 57 ++ app/globals.css | 74 +- app/icon.png | Bin 2036 -> 956 bytes app/layout.tsx | 10 +- app/mail/mail-app-shell.tsx | 12 +- app/meet/[room]/page.tsx | 24 + app/meet/join/page.tsx | 18 + app/meet/layout.tsx | 9 + app/meet/page.tsx | 5 + .../admin/settings/admin-access-guard.tsx | 13 +- .../admin/settings/admin-settings-header.tsx | 19 +- .../settings/admin-settings-section-view.tsx | 4 + .../admin/settings/org-settings-form.tsx | 5 +- .../admin/settings/org-settings-sync.tsx | 19 +- .../settings/sections/agenda-section.tsx | 158 ++++ .../sections/drive-mount-oauth-section.tsx | 154 ++++ .../settings/sections/drive-org-section.tsx | 94 +++ .../sections/file-policies-section.tsx | 11 + .../settings/sections/ultimeet-section.tsx | 347 ++++++++ components/agenda/agenda-app-shell.tsx | 17 +- components/agenda/agenda-calendar-dialog.tsx | 30 +- .../agenda/agenda-calendars-settings.tsx | 629 +++++++++++++++ components/agenda/agenda-event-chip.tsx | 12 +- components/agenda/agenda-event-dialog.tsx | 333 ++++---- components/agenda/agenda-event-popover.tsx | 144 ++-- .../agenda/agenda-event-schedule-fields.tsx | 403 ++++++++++ components/agenda/agenda-guest-picker.tsx | 3 + components/agenda/agenda-header.tsx | 36 +- components/agenda/agenda-mini-month.tsx | 14 +- components/agenda/agenda-org-policy-sync.tsx | 28 + components/agenda/agenda-page.tsx | 206 +++-- components/agenda/agenda-quick-create.tsx | 197 +++-- .../agenda/agenda-quick-settings-panel.tsx | 77 ++ .../agenda/agenda-settings-chip-picker.tsx | 172 ++++ components/agenda/agenda-settings-fields.tsx | 466 +++++++++++ components/agenda/agenda-sidebar.tsx | 120 ++- components/agenda/agenda-step-adjust.tsx | 217 +++++ .../agenda/agenda-video-provider-icon.tsx | 42 + .../agenda-video-provider-select-label.tsx | 26 + components/agenda/agenda-video-toggle.tsx | 100 +++ components/agenda/agenda-view-month.tsx | 282 ++++++- components/agenda/agenda-view-week.tsx | 617 +++++++++++--- components/auth/auth-provider.tsx | 8 +- components/auth/login-chrome.tsx | 61 +- components/auth/session-guard.tsx | 8 +- components/compte/compte-authentik-panel.tsx | 97 +++ components/compte/compte-avatar-field.tsx | 59 ++ components/compte/compte-settings-card.tsx | 14 + components/compte/compte-settings-header.tsx | 16 +- .../compte/sections/compte-home-section.tsx | 43 +- .../sections/compte-personal-info-section.tsx | 57 +- .../sections/compte-security-section.tsx | 133 ++- components/demo/demo-agenda-data.ts | 300 +++++++ components/demo/demo-agenda-shell.tsx | 20 + components/demo/demo-chrome.tsx | 35 + components/demo/demo-contacts-data.ts | 761 ++++++++++++++++++ components/demo/demo-contacts-shell.tsx | 23 + components/demo/demo-docs-editor.tsx | 24 +- components/demo/demo-drive-data.ts | 387 +++++++++ components/demo/demo-drive-shell.tsx | 20 + components/demo/demo-mail-app.tsx | 496 ------------ components/demo/demo-mail-data.ts | 351 +++++++- components/demo/demo-mail-shell.tsx | 19 + components/demo/demo-navigation-guard.tsx | 85 ++ components/drive/breadcrumb-folder-menu.tsx | 8 +- components/drive/breadcrumb-nav.tsx | 29 +- components/drive/drive-add-mount-dialog.tsx | 209 +++++ components/drive/drive-app-shell.tsx | 11 +- components/drive/drive-browser-chrome.tsx | 9 +- components/drive/drive-filter-bar.tsx | 2 +- components/drive/drive-list-modified.tsx | 33 +- components/drive/drive-mobile-bottom-bar.tsx | 15 +- components/drive/drive-route-scope.tsx | 3 +- components/drive/drive-search-bar.tsx | 15 +- components/drive/drive-search-suggestions.tsx | 4 +- components/drive/drive-sidebar-mounts.tsx | 266 ++++++ .../drive/drive-sidebar-org-folders.tsx | 230 ++++++ components/drive/drive-sidebar.tsx | 69 +- components/drive/editor-account-button.tsx | 6 +- components/drive/file-browser.tsx | 14 +- components/drive/file-preview-dialog.tsx | 40 +- components/drive/public-share-view.tsx | 17 +- .../drive/richtext/docs-excalidraw-editor.tsx | 2 +- components/drive/sidebar-folder-tree.tsx | 40 +- components/drive/ultidraw-document.tsx | 2 +- components/first-launch-splash.tsx | 111 ++- .../gmail/calendar-invitation-preview.tsx | 8 +- components/gmail/compose-identities-sync.tsx | 13 +- .../gmail/contacts-page/contacts-sidebar.tsx | 2 - .../gmail/contacts/contacts-panel-logo.tsx | 12 +- .../gmail/email-list/email-list-layout.tsx | 3 +- .../gmail/email-list/email-list-toolbar.tsx | 10 +- .../email-list/hooks/use-email-list-data.ts | 74 +- .../gmail/email-view/sandboxed-content.tsx | 81 +- components/gmail/mail-search-bar.tsx | 21 +- .../mail-search/advanced-search-panel.tsx | 4 +- components/gmail/mail-settings-fields.tsx | 4 +- components/gmail/mail-theme-applier.tsx | 40 +- components/gmail/mobile-search-overlay.tsx | 10 +- .../quick-settings/quick-settings-panel.tsx | 37 +- .../api-token-agenda-scope-editor.tsx | 26 + .../settings/automation/api-tokens-panel.tsx | 18 +- .../automation/automation-domain-mark.tsx | 12 +- .../automation/rule-simulator-panel.tsx | 34 +- .../webhook-agenda-scope-editor.tsx | 86 ++ .../automation/webhook-event-scope-editor.tsx | 15 +- .../webhook-template-variables-panel.tsx | 6 +- .../settings/automation/webhooks-panel.tsx | 6 +- .../automation/workflow-triggers-panel.tsx | 15 +- .../settings/mail-settings-section-view.tsx | 2 + .../sections/agenda-settings-section.tsx | 16 + .../sections/automation-settings-section.tsx | 2 +- components/landing/landing-data.ts | 9 +- components/landing/landing-demo.tsx | 24 + components/landing/landing-header.tsx | 16 +- components/landing/landing-page.tsx | 2 + components/landing/landing-sections.tsx | 33 +- components/landing/landing-theme-applier.tsx | 20 + components/meet/meet-app-shell.tsx | 22 + components/meet/meet-header.tsx | 39 + components/meet/meet-join-client.tsx | 108 +++ components/meet/meet-lobby.tsx | 181 +++++ components/meet/meet-room-client.tsx | 88 ++ components/meet/meet-room-frame.tsx | 25 + components/suite/account-avatar.tsx | 37 +- components/suite/account-switcher-panel.tsx | 6 +- components/suite/agenda-mark.tsx | 32 + components/suite/header-account-actions.tsx | 6 +- components/suite/suite-favorites-menu.tsx | 49 +- components/suite/suite-theme-shell.tsx | 2 +- components/theme-init-script.tsx | 70 +- components/ultimail-logo.tsx | 21 +- hooks/use-build-search-url.ts | 16 + hooks/use-mail-route.ts | 22 +- lib/admin-settings/map-api-org-settings.ts | 42 +- lib/admin-settings/org-settings-store.ts | 38 + lib/admin-settings/org-settings-types.ts | 35 + lib/admin-settings/settings-nav.ts | 18 + lib/agenda/agenda-calendar-visibility.ts | 119 +++ lib/agenda/agenda-chrome-classes.ts | 6 + lib/agenda/agenda-date.ts | 129 ++- .../agenda-destination-identities.test.ts | 98 +++ lib/agenda/agenda-destination-identities.ts | 133 +++ lib/agenda/agenda-event-drag-session.ts | 58 ++ lib/agenda/agenda-ical-parser.ts | 122 +++ lib/agenda/agenda-move-event.ts | 68 ++ lib/agenda/agenda-pending-event.ts | 47 ++ lib/agenda/agenda-route-context.tsx | 25 + lib/agenda/agenda-save-with-video.ts | 122 +++ lib/agenda/agenda-settings-defaults.ts | 31 + lib/agenda/agenda-settings-labels.ts | 36 + lib/agenda/agenda-settings-types.ts | 138 ++++ lib/agenda/agenda-store.ts | 266 +++++- lib/agenda/agenda-types.ts | 2 + lib/agenda/agenda-url.ts | 8 +- lib/agenda/agenda-video-conference.ts | 44 + lib/agenda/use-agenda-event-move.ts | 32 + .../use-agenda-invitation-suggestions.ts | 245 ++++++ lib/agenda/use-effective-agenda-settings.ts | 138 ++++ lib/agenda/use-external-agenda-events.ts | 72 ++ lib/agenda/use-resolved-week-start.ts | 22 + lib/agenda/use-visible-agenda-calendars.ts | 119 +++ lib/api/admin-org-types.ts | 42 + lib/api/drive-roots.ts | 98 +++ lib/api/hooks/use-admin-drive-queries.ts | 52 ++ lib/api/hooks/use-calendar-mutations.ts | 86 +- lib/api/hooks/use-calendar-queries.ts | 46 +- lib/api/hooks/use-compose-mutations.ts | 19 + lib/api/hooks/use-contact-mutations.ts | 44 +- lib/api/hooks/use-contact-queries.ts | 62 +- lib/api/hooks/use-current-user.ts | 5 + lib/api/hooks/use-drive-preview-thumb.ts | 23 +- lib/api/hooks/use-drive-queries.ts | 346 +++++++- lib/api/hooks/use-mail-automation-queries.ts | 1 + lib/api/hooks/use-mail-mutations.ts | 80 +- lib/api/hooks/use-mail-queries.ts | 144 +++- lib/api/hooks/use-meet-queries.ts | 47 ++ lib/api/hooks/use-org-settings.ts | 9 +- lib/api/hooks/use-public-share-mutations.ts | 2 +- lib/api/hooks/use-user-avatar-mutations.ts | 27 + lib/api/query-provider.tsx | 18 +- lib/api/types.ts | 55 ++ lib/api/use-auth-ready.ts | 13 +- lib/api/ws.ts | 8 +- lib/auth/authentik-user-url.ts | 118 ++- lib/auth/handle-unauthorized.ts | 15 + lib/auth/public-paths.ts | 15 + lib/auth/use-platform-admin-access.ts | 19 + lib/calendar-invitation.ts | 3 + lib/compose/identity-map.ts | 17 + lib/contacts-chrome-classes.ts | 3 +- lib/demo/demo-agenda-bootstrap.tsx | 20 + lib/demo/demo-agenda-context.tsx | 62 ++ lib/demo/demo-agenda-store.ts | 75 ++ lib/demo/demo-contacts-bootstrap.tsx | 21 + lib/demo/demo-contacts-context.tsx | 62 ++ lib/demo/demo-contacts-store.ts | 108 +++ lib/demo/demo-drive-api-data.ts | 117 +++ lib/demo/demo-drive-bootstrap.tsx | 21 + lib/demo/demo-drive-context.tsx | 63 ++ lib/demo/demo-drive-preview.ts | 114 +++ lib/demo/demo-drive-store.ts | 163 ++++ lib/demo/demo-mail-api-data.ts | 276 +++++++ lib/demo/demo-mail-bootstrap.tsx | 63 ++ lib/demo/demo-mail-context.tsx | 67 ++ lib/demo/demo-mail-email-map.ts | 34 + lib/demo/demo-mail-nav-data.ts | 78 ++ lib/demo/demo-mail-store.ts | 126 +++ lib/demo/demo-navigation.ts | 45 ++ lib/demo/demo-route.ts | 8 + lib/demo/demo-theme-store.ts | 27 + lib/demo/use-is-demo-app.ts | 8 + lib/demo/use-theme-mode-controls.ts | 24 + lib/drive/drive-chrome-classes.ts | 12 + lib/drive/drive-mount-oauth.ts | 46 ++ lib/drive/drive-open-item.ts | 4 +- lib/drive/drive-route-context.tsx | 30 + lib/drive/drive-search.ts | 4 +- lib/drive/drive-sidebar-tree.ts | 44 +- lib/drive/drive-url.ts | 138 +++- .../extensions/docs-table-column-resizing.ts | 426 ++++++++++ lib/drive/extensions/docs-table.ts | 24 + lib/email-preview-dark-styles.ts | 16 + lib/email-preview-iframe-height.test.ts | 43 + lib/email-preview-iframe-height.ts | 48 +- lib/empty-module.mjs | 1 + lib/hooks/use-chrome-identity.ts | 16 + lib/mail-automation/api-token-permissions.ts | 72 +- lib/mail-automation/condition-helpers.ts | 23 +- lib/mail-automation/defaults.ts | 12 + lib/mail-automation/domains.ts | 39 +- lib/mail-automation/types.ts | 28 + .../use-automation-suggestions.ts | 15 + lib/mail-automation/webhook-config.ts | 17 +- .../webhook-template-variables.ts | 65 +- lib/mail-chrome-classes.ts | 2 +- lib/mail-html-iframe.ts | 2 +- lib/mail-search/navigate.ts | 6 +- lib/mail-search/search-params.ts | 8 +- lib/mail-settings/settings-nav.ts | 10 + lib/mail-settings/settings-search-index.ts | 10 +- lib/mail-url.ts | 5 +- lib/meet/meet-settings-types.ts | 88 ++ lib/meet/meet-url.ts | 43 + lib/suite/drive-route.ts | 9 +- lib/suite/favorite-apps.ts | 11 +- lib/suite/mail-route.ts | 9 + lib/suite/page-metadata.ts | 26 +- lib/suite/suite-app-splash.ts | 94 +++ lib/suite/suite-chrome-classes.ts | 24 + next.config.mjs | 5 + package.json | 8 +- pnpm-lock.yaml | 44 +- public/agenda-mark-dark.svg | 16 + public/agenda-mark.svg | 6 +- public/brand/ultimail-header-icon.png | Bin 10904 -> 13447 bytes public/brand/ultimail-mark.jpg | Bin 8654 -> 8360 bytes public/brand/ultimail-mark.png | Bin 37698 -> 11854 bytes public/brand/ultimail-wordmark-horizontal.jpg | Bin 39331 -> 48452 bytes public/brand/ultimail-wordmark-horizontal.png | Bin 258079 -> 51202 bytes .../brand/ultimail-wordmark-stacked-dark.png | Bin 356898 -> 38719 bytes public/brand/ultimail-wordmark-stacked.jpg | Bin 32226 -> 37704 bytes public/brand/ultimail-wordmark-stacked.png | Bin 353765 -> 38617 bytes public/demo/contacts/avatars/avatar-01.jpg | Bin 0 -> 560453 bytes public/demo/contacts/avatars/avatar-02.jpg | Bin 0 -> 541892 bytes public/demo/contacts/avatars/avatar-03.jpg | Bin 0 -> 607189 bytes public/demo/contacts/avatars/avatar-04.jpg | Bin 0 -> 549586 bytes public/demo/contacts/avatars/avatar-05.jpg | Bin 0 -> 565302 bytes public/demo/contacts/avatars/avatar-06.jpg | Bin 0 -> 529609 bytes public/demo/contacts/avatars/avatar-07.jpg | Bin 0 -> 623630 bytes public/demo/contacts/avatars/avatar-08.jpg | Bin 0 -> 579095 bytes public/demo/contacts/avatars/avatar-09.jpg | Bin 0 -> 534787 bytes public/demo/contacts/avatars/avatar-10.jpg | Bin 0 -> 667544 bytes public/demo/contacts/avatars/avatar-11.jpg | Bin 0 -> 525995 bytes public/demo/contacts/avatars/avatar-12.jpg | Bin 0 -> 491097 bytes public/demo/contacts/avatars/avatar-13.jpg | Bin 0 -> 522516 bytes public/demo/contacts/avatars/avatar-14.jpg | Bin 0 -> 549559 bytes public/drive/ultimail-mark.svg | 9 + public/ultimail-mark.svg | 9 + scripts/emit-ultimail-brand.mjs | 104 +++ scripts/emit-ultimail-header-icon.mjs | 232 ------ scripts/rasterize-ultimail-brand.mjs | 161 ---- scripts/vectorize-ultimail-brand.mjs | 185 ----- styles/excalidraw.css | 1 + styles/landing.css | 4 +- styles/richtext-editor.css | 9 +- suite-icons/admin-mark.svg | 15 + suite-icons/agenda-mark-dark.svg | 16 + suite-icons/agenda-mark.svg | 16 + suite-icons/compte-mark.svg | 16 + suite-icons/contacts-mark.svg | 21 + suite-icons/ground-news-mark.svg | 1 + suite-icons/openstreetmap-mark.svg | 14 + suite-icons/photos-mark.svg | 7 + suite-icons/qwant-mark.svg | 6 + suite-icons/ultiai-mark.svg | 15 + suite-icons/ultidrive-mark.svg | 9 + suite-icons/ultimail-mark.svg | 9 + suite-icons/ultimeet-mark.svg | 7 + suite-icons/ultisuite-mark.svg | 7 + tsconfig.tsbuildinfo | 2 +- 313 files changed, 16203 insertions(+), 2373 deletions(-) create mode 100644 app/api/agenda/ical/route.ts create mode 100644 app/demo/agenda/[[...segments]]/page.tsx create mode 100644 app/demo/agenda/layout.tsx create mode 100644 app/demo/contacts/layout.tsx create mode 100644 app/demo/contacts/page.tsx create mode 100644 app/demo/drive/[[...segments]]/page.tsx create mode 100644 app/demo/drive/layout.tsx create mode 100644 app/demo/mail/[[...segments]]/page.tsx rename app/demo/mail/{page.tsx => layout.tsx} (64%) create mode 100644 app/drive/mounts/oauth/callback/page.tsx create mode 100644 app/meet/[room]/page.tsx create mode 100644 app/meet/join/page.tsx create mode 100644 app/meet/layout.tsx create mode 100644 app/meet/page.tsx create mode 100644 components/admin/settings/sections/agenda-section.tsx create mode 100644 components/admin/settings/sections/drive-mount-oauth-section.tsx create mode 100644 components/admin/settings/sections/drive-org-section.tsx create mode 100644 components/admin/settings/sections/ultimeet-section.tsx create mode 100644 components/agenda/agenda-calendars-settings.tsx create mode 100644 components/agenda/agenda-event-schedule-fields.tsx create mode 100644 components/agenda/agenda-org-policy-sync.tsx create mode 100644 components/agenda/agenda-quick-settings-panel.tsx create mode 100644 components/agenda/agenda-settings-chip-picker.tsx create mode 100644 components/agenda/agenda-settings-fields.tsx create mode 100644 components/agenda/agenda-step-adjust.tsx create mode 100644 components/agenda/agenda-video-provider-icon.tsx create mode 100644 components/agenda/agenda-video-provider-select-label.tsx create mode 100644 components/agenda/agenda-video-toggle.tsx create mode 100644 components/compte/compte-authentik-panel.tsx create mode 100644 components/compte/compte-avatar-field.tsx create mode 100644 components/compte/compte-settings-card.tsx create mode 100644 components/demo/demo-agenda-data.ts create mode 100644 components/demo/demo-agenda-shell.tsx create mode 100644 components/demo/demo-chrome.tsx create mode 100644 components/demo/demo-contacts-data.ts create mode 100644 components/demo/demo-contacts-shell.tsx create mode 100644 components/demo/demo-drive-data.ts create mode 100644 components/demo/demo-drive-shell.tsx delete mode 100644 components/demo/demo-mail-app.tsx create mode 100644 components/demo/demo-mail-shell.tsx create mode 100644 components/demo/demo-navigation-guard.tsx create mode 100644 components/drive/drive-add-mount-dialog.tsx create mode 100644 components/drive/drive-sidebar-mounts.tsx create mode 100644 components/drive/drive-sidebar-org-folders.tsx create mode 100644 components/gmail/settings/automation/api-token-agenda-scope-editor.tsx create mode 100644 components/gmail/settings/automation/webhook-agenda-scope-editor.tsx create mode 100644 components/gmail/settings/sections/agenda-settings-section.tsx create mode 100644 components/landing/landing-theme-applier.tsx create mode 100644 components/meet/meet-app-shell.tsx create mode 100644 components/meet/meet-header.tsx create mode 100644 components/meet/meet-join-client.tsx create mode 100644 components/meet/meet-lobby.tsx create mode 100644 components/meet/meet-room-client.tsx create mode 100644 components/meet/meet-room-frame.tsx create mode 100644 components/suite/agenda-mark.tsx create mode 100644 hooks/use-build-search-url.ts create mode 100644 lib/agenda/agenda-calendar-visibility.ts create mode 100644 lib/agenda/agenda-chrome-classes.ts create mode 100644 lib/agenda/agenda-destination-identities.test.ts create mode 100644 lib/agenda/agenda-destination-identities.ts create mode 100644 lib/agenda/agenda-event-drag-session.ts create mode 100644 lib/agenda/agenda-ical-parser.ts create mode 100644 lib/agenda/agenda-move-event.ts create mode 100644 lib/agenda/agenda-pending-event.ts create mode 100644 lib/agenda/agenda-route-context.tsx create mode 100644 lib/agenda/agenda-save-with-video.ts create mode 100644 lib/agenda/agenda-settings-defaults.ts create mode 100644 lib/agenda/agenda-settings-labels.ts create mode 100644 lib/agenda/agenda-settings-types.ts create mode 100644 lib/agenda/agenda-video-conference.ts create mode 100644 lib/agenda/use-agenda-event-move.ts create mode 100644 lib/agenda/use-agenda-invitation-suggestions.ts create mode 100644 lib/agenda/use-effective-agenda-settings.ts create mode 100644 lib/agenda/use-external-agenda-events.ts create mode 100644 lib/agenda/use-resolved-week-start.ts create mode 100644 lib/agenda/use-visible-agenda-calendars.ts create mode 100644 lib/api/drive-roots.ts create mode 100644 lib/api/hooks/use-admin-drive-queries.ts create mode 100644 lib/api/hooks/use-meet-queries.ts create mode 100644 lib/api/hooks/use-user-avatar-mutations.ts create mode 100644 lib/auth/public-paths.ts create mode 100644 lib/auth/use-platform-admin-access.ts create mode 100644 lib/demo/demo-agenda-bootstrap.tsx create mode 100644 lib/demo/demo-agenda-context.tsx create mode 100644 lib/demo/demo-agenda-store.ts create mode 100644 lib/demo/demo-contacts-bootstrap.tsx create mode 100644 lib/demo/demo-contacts-context.tsx create mode 100644 lib/demo/demo-contacts-store.ts create mode 100644 lib/demo/demo-drive-api-data.ts create mode 100644 lib/demo/demo-drive-bootstrap.tsx create mode 100644 lib/demo/demo-drive-context.tsx create mode 100644 lib/demo/demo-drive-preview.ts create mode 100644 lib/demo/demo-drive-store.ts create mode 100644 lib/demo/demo-mail-api-data.ts create mode 100644 lib/demo/demo-mail-bootstrap.tsx create mode 100644 lib/demo/demo-mail-context.tsx create mode 100644 lib/demo/demo-mail-email-map.ts create mode 100644 lib/demo/demo-mail-nav-data.ts create mode 100644 lib/demo/demo-mail-store.ts create mode 100644 lib/demo/demo-navigation.ts create mode 100644 lib/demo/demo-route.ts create mode 100644 lib/demo/demo-theme-store.ts create mode 100644 lib/demo/use-is-demo-app.ts create mode 100644 lib/demo/use-theme-mode-controls.ts create mode 100644 lib/drive/drive-mount-oauth.ts create mode 100644 lib/drive/drive-route-context.tsx create mode 100644 lib/drive/extensions/docs-table-column-resizing.ts create mode 100644 lib/email-preview-iframe-height.test.ts create mode 100644 lib/empty-module.mjs create mode 100644 lib/meet/meet-settings-types.ts create mode 100644 lib/meet/meet-url.ts create mode 100644 lib/suite/mail-route.ts create mode 100644 lib/suite/suite-app-splash.ts create mode 100644 public/agenda-mark-dark.svg create mode 100644 public/demo/contacts/avatars/avatar-01.jpg create mode 100644 public/demo/contacts/avatars/avatar-02.jpg create mode 100644 public/demo/contacts/avatars/avatar-03.jpg create mode 100644 public/demo/contacts/avatars/avatar-04.jpg create mode 100644 public/demo/contacts/avatars/avatar-05.jpg create mode 100644 public/demo/contacts/avatars/avatar-06.jpg create mode 100644 public/demo/contacts/avatars/avatar-07.jpg create mode 100644 public/demo/contacts/avatars/avatar-08.jpg create mode 100644 public/demo/contacts/avatars/avatar-09.jpg create mode 100644 public/demo/contacts/avatars/avatar-10.jpg create mode 100644 public/demo/contacts/avatars/avatar-11.jpg create mode 100644 public/demo/contacts/avatars/avatar-12.jpg create mode 100644 public/demo/contacts/avatars/avatar-13.jpg create mode 100644 public/demo/contacts/avatars/avatar-14.jpg create mode 100644 public/drive/ultimail-mark.svg create mode 100644 public/ultimail-mark.svg create mode 100644 scripts/emit-ultimail-brand.mjs delete mode 100644 scripts/emit-ultimail-header-icon.mjs delete mode 100644 scripts/rasterize-ultimail-brand.mjs delete mode 100644 scripts/vectorize-ultimail-brand.mjs create mode 100644 styles/excalidraw.css create mode 100644 suite-icons/admin-mark.svg create mode 100644 suite-icons/agenda-mark-dark.svg create mode 100644 suite-icons/agenda-mark.svg create mode 100644 suite-icons/compte-mark.svg create mode 100644 suite-icons/contacts-mark.svg create mode 100644 suite-icons/ground-news-mark.svg create mode 100644 suite-icons/openstreetmap-mark.svg create mode 100644 suite-icons/photos-mark.svg create mode 100644 suite-icons/qwant-mark.svg create mode 100644 suite-icons/ultiai-mark.svg create mode 100644 suite-icons/ultidrive-mark.svg create mode 100644 suite-icons/ultimail-mark.svg create mode 100644 suite-icons/ultimeet-mark.svg create mode 100644 suite-icons/ultisuite-mark.svg diff --git a/app/api/agenda/ical/route.ts b/app/api/agenda/ical/route.ts new file mode 100644 index 0000000..a9e33e7 --- /dev/null +++ b/app/api/agenda/ical/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from "next/server" + +const MAX_ICS_BYTES = 2 * 1024 * 1024 + +function isAllowedIcsUrl(raw: string): boolean { + try { + const url = new URL(raw) + if (url.protocol !== "http:" && url.protocol !== "https:") return false + const host = url.hostname.toLowerCase() + if (host === "localhost" || host === "127.0.0.1" || host === "::1") { + return false + } + return true + } catch { + return false + } +} + +export async function GET(request: Request) { + const url = new URL(request.url).searchParams.get("url")?.trim() + if (!url || !isAllowedIcsUrl(url)) { + return NextResponse.json({ error: "invalid_url" }, { status: 400 }) + } + + try { + const res = await fetch(url, { + headers: { Accept: "text/calendar, text/plain, */*" }, + redirect: "follow", + cache: "no-store", + }) + if (!res.ok) { + return NextResponse.json({ error: "fetch_failed" }, { status: 502 }) + } + + const contentType = res.headers.get("content-type") ?? "" + if ( + contentType && + !contentType.includes("text/calendar") && + !contentType.includes("text/plain") && + !contentType.includes("application/octet-stream") + ) { + return NextResponse.json({ error: "unsupported_content_type" }, { status: 415 }) + } + + const buffer = await res.arrayBuffer() + if (buffer.byteLength > MAX_ICS_BYTES) { + return NextResponse.json({ error: "payload_too_large" }, { status: 413 }) + } + + return new NextResponse(new TextDecoder("utf-8").decode(buffer), { + status: 200, + headers: { + "Content-Type": "text/calendar; charset=utf-8", + "Cache-Control": "private, max-age=300", + }, + }) + } catch { + return NextResponse.json({ error: "fetch_failed" }, { status: 502 }) + } +} diff --git a/app/apple-icon.png b/app/apple-icon.png index c059766d4950e6c79dd34929a74bc37b83749cb4..614f3547c486fcdeb46f70d4b81a352240703234 100644 GIT binary patch literal 4538 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 literal 23130 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 N002ovPDHLkV1jhdfnWdt diff --git a/app/compte/layout.tsx b/app/compte/layout.tsx index b9ee6e4..644e290 100644 --- a/app/compte/layout.tsx +++ b/app/compte/layout.tsx @@ -1,4 +1,5 @@ import { CompteSettingsLayout } from "@/components/compte/compte-settings-layout" +import { SuiteThemeShell } from "@/components/suite/suite-theme-shell" import type { Metadata } from "next" import { suitePageMetadata } from "@/lib/suite/page-metadata" @@ -11,5 +12,9 @@ export default function CompteRootLayout({ }: { children: React.ReactNode }) { - return {children} + return ( + + {children} + + ) } diff --git a/app/demo/agenda/[[...segments]]/page.tsx b/app/demo/agenda/[[...segments]]/page.tsx new file mode 100644 index 0000000..0ac158e --- /dev/null +++ b/app/demo/agenda/[[...segments]]/page.tsx @@ -0,0 +1,12 @@ +"use client" + +import { Suspense } from "react" +import { AgendaPage } from "@/components/agenda/agenda-page" + +export default function DemoAgendaRoutePage() { + return ( + + + + ) +} diff --git a/app/demo/agenda/layout.tsx b/app/demo/agenda/layout.tsx new file mode 100644 index 0000000..6b366f4 --- /dev/null +++ b/app/demo/agenda/layout.tsx @@ -0,0 +1,22 @@ +import { DemoAgendaShell } from "@/components/demo/demo-agenda-shell" +import type { Metadata } from "next" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = { + ...suitePageMetadata({ + app: "agenda", + title: "Démo UltiAgenda", + absoluteTitle: true, + description: + "Essayez l'agenda UltiAgenda sans compte — démo interactive, zéro rétention.", + }), + robots: { index: false }, +} + +export default function DemoAgendaLayout({ + children, +}: { + children: React.ReactNode +}) { + return {children} +} diff --git a/app/demo/contacts/layout.tsx b/app/demo/contacts/layout.tsx new file mode 100644 index 0000000..f0560f2 --- /dev/null +++ b/app/demo/contacts/layout.tsx @@ -0,0 +1,22 @@ +import { DemoContactsShell } from "@/components/demo/demo-contacts-shell" +import type { Metadata } from "next" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = { + ...suitePageMetadata({ + app: "contacts", + title: "Démo Contacts", + absoluteTitle: true, + description: + "Essayez les contacts Ulti Suite sans compte — démo interactive, zéro rétention.", + }), + robots: { index: false }, +} + +export default function DemoContactsLayout({ + children, +}: { + children: React.ReactNode +}) { + return {children} +} diff --git a/app/demo/contacts/page.tsx b/app/demo/contacts/page.tsx new file mode 100644 index 0000000..e5cdc3b --- /dev/null +++ b/app/demo/contacts/page.tsx @@ -0,0 +1,4 @@ +/** Route racine : l'interface contacts est rendue par `app/demo/contacts/layout.tsx`. */ +export default function DemoContactsPage() { + return null +} diff --git a/app/demo/drive/[[...segments]]/page.tsx b/app/demo/drive/[[...segments]]/page.tsx new file mode 100644 index 0000000..4d6ac8d --- /dev/null +++ b/app/demo/drive/[[...segments]]/page.tsx @@ -0,0 +1 @@ +export { default } from "@/app/drive/(browser)/[[...segments]]/page" diff --git a/app/demo/drive/layout.tsx b/app/demo/drive/layout.tsx new file mode 100644 index 0000000..1afb423 --- /dev/null +++ b/app/demo/drive/layout.tsx @@ -0,0 +1,28 @@ +import { DriveRouteScope } from "@/components/drive/drive-route-scope" +import { DemoDriveShell } from "@/components/demo/demo-drive-shell" +import type { Metadata } from "next" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = { + ...suitePageMetadata({ + app: "drive", + title: "Démo UltiDrive", + absoluteTitle: true, + description: + "Essayez le drive UltiDrive sans compte — démo interactive, zéro rétention.", + }), + robots: { index: false }, +} + +export default function DemoDriveLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + <> + + {children} + + ) +} diff --git a/app/demo/mail/[[...segments]]/page.tsx b/app/demo/mail/[[...segments]]/page.tsx new file mode 100644 index 0000000..cbfd623 --- /dev/null +++ b/app/demo/mail/[[...segments]]/page.tsx @@ -0,0 +1,4 @@ +/** Route catch-all : l'interface mail réelle est rendue par `app/demo/mail/layout.tsx`. */ +export default function DemoMailSegmentsPage() { + return null +} diff --git a/app/demo/mail/page.tsx b/app/demo/mail/layout.tsx similarity index 64% rename from app/demo/mail/page.tsx rename to app/demo/mail/layout.tsx index 00dc420..38bc817 100644 --- a/app/demo/mail/page.tsx +++ b/app/demo/mail/layout.tsx @@ -1,5 +1,5 @@ +import { DemoMailShell } from "@/components/demo/demo-mail-shell" import type { Metadata } from "next" -import { DemoMailApp } from "@/components/demo/demo-mail-app" import { suitePageMetadata } from "@/lib/suite/page-metadata" export const metadata: Metadata = { @@ -13,6 +13,10 @@ export const metadata: Metadata = { robots: { index: false }, } -export default function DemoMailPage() { - return +export default function DemoMailLayout({ + children, +}: { + children: React.ReactNode +}) { + return {children} } diff --git a/app/drive/(browser)/[[...segments]]/page.tsx b/app/drive/(browser)/[[...segments]]/page.tsx index cb73ee2..5cdebbc 100644 --- a/app/drive/(browser)/[[...segments]]/page.tsx +++ b/app/drive/(browser)/[[...segments]]/page.tsx @@ -27,12 +27,15 @@ import { import { cn } from "@/lib/utils" import { useDriveList, + useDriveMountList, + useDriveOrgList, useDriveRecent, useDriveSearch, useDriveSharedWithMe, useDriveStarred, useDriveTrash, } from "@/lib/api/hooks/use-drive-queries" +import { pathRefFromRoute } from "@/lib/api/drive-roots" export default function DriveBrowserPage() { const params = useParams() @@ -42,7 +45,13 @@ export default function DriveBrowserPage() { const folderPath = folderPathFromSegments(route.pathSegments) const contextView = - route.view === "shared" ? "shared" : route.view === "search" ? "files" : route.view + route.view === "shared" + ? "shared" + : route.view === "search" + ? "files" + : route.view === "org" || route.view === "mount" + ? route.view + : route.view const fallbackScope = defaultDriveSearchScope( route.view === "shared" ? "shared" : "files", folderPath @@ -78,6 +87,8 @@ export default function DriveBrowserPage() { const folderPlacement = useDriveSettingsStore((s) => s.folderPlacement) const list = useDriveList(folderPath, route.page, "", route.view === "files") + const orgList = useDriveOrgList(route.rootId ?? "", folderPath, route.page, route.view === "org" && Boolean(route.rootId)) + const mountList = useDriveMountList(route.rootId ?? "", folderPath, route.page, route.view === "mount" && Boolean(route.rootId)) const shared = useDriveSharedWithMe( route.page, "", @@ -113,7 +124,11 @@ export default function DriveBrowserPage() { ? route.pathSegments.length === 0 ? shared : sharedFolder - : list + : route.view === "org" + ? orgList + : route.view === "mount" + ? mountList + : list const files = active.data?.files ?? [] @@ -186,6 +201,7 @@ export default function DriveBrowserPage() { 0 ? ( ) : null} diff --git a/app/drive/mounts/oauth/callback/page.tsx b/app/drive/mounts/oauth/callback/page.tsx new file mode 100644 index 0000000..833c35a --- /dev/null +++ b/app/drive/mounts/oauth/callback/page.tsx @@ -0,0 +1,57 @@ +"use client" + +import { Suspense, useEffect, useState } from "react" +import { useSearchParams } from "next/navigation" +import { useDriveMountMutations } from "@/lib/api/hooks/use-drive-queries" +import { Button } from "@/components/ui/button" +import { buildDriveMountOAuthRedirectURI } from "@/lib/drive/drive-mount-oauth" + +function DriveMountOAuthCallbackInner() { + const searchParams = useSearchParams() + const { completeOAuth } = useDriveMountMutations() + const [message, setMessage] = useState("Finalisation de la connexion…") + const [done, setDone] = useState(false) + + useEffect(() => { + const code = searchParams.get("code") + const mountId = searchParams.get("state") ?? searchParams.get("mount_id") + if (!code || !mountId) { + setMessage("Paramètres OAuth manquants.") + setDone(true) + return + } + void completeOAuth + .mutateAsync({ mountId, code, redirectUri: buildDriveMountOAuthRedirectURI() }) + .then(() => { + setMessage("Volume connecté avec succès.") + setDone(true) + if (window.opener) { + window.opener.postMessage({ type: "drive-mount-oauth-complete", mountId }, window.location.origin) + window.setTimeout(() => window.close(), 800) + } + }) + .catch(() => { + setMessage("Échec de la connexion OAuth. Réessayez depuis UltiDrive.") + setDone(true) + }) + }, [completeOAuth, searchParams]) + + return ( +
+

{message}

+ {done && !window.opener ? ( + + ) : null} +
+ ) +} + +export default function DriveMountOAuthCallbackPage() { + return ( + Chargement…}> + + + ) +} diff --git a/app/globals.css b/app/globals.css index 0bdba42..63536f4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -158,8 +158,8 @@ } @theme inline { - --font-sans: 'Geist', 'Geist Fallback'; - --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); @@ -573,6 +573,14 @@ html[data-splash-seen='1'] .app-first-launch-splash { animation: splash-logo-float 2s ease-in-out infinite; } +.app-first-launch-splash__mark { + transform-origin: center center; +} + +.app-first-launch-splash__mark--spin { + animation: splash-logo-spin 0.72s linear infinite; +} + .app-first-launch-splash__loader { width: min(58vw, 230px); height: 4px; @@ -595,6 +603,7 @@ html[data-splash-seen='1'] .app-first-launch-splash { .app-first-launch-splash__grain, .app-first-launch-splash__content, .app-first-launch-splash__logo, + .app-first-launch-splash__mark--spin, .app-first-launch-splash__loader > span { animation: none !important; } @@ -609,7 +618,7 @@ html:has(.ultimail-login) body { background-color: transparent !important; } -/* ── Drive : pas de fond décoratif mail ni splash Ultimail (y compris chargement) ── */ +/* ── Drive : pas de fond décoratif mail ── */ html[data-route-scope='drive']::before, html:has([data-drive-app])::before { opacity: 0 !important; @@ -623,12 +632,6 @@ html[data-route-scope='drive'] body { background-color: var(--app-canvas) !important; } -html[data-route-scope='drive'] .app-first-launch-splash { - opacity: 0; - visibility: hidden; - pointer-events: none; -} - @media (min-width: 640px) { .ultimail-login-card-frame { padding: 3px; @@ -727,9 +730,31 @@ html[data-mail-background]:not([data-mail-background='none']) [data-contacts-app background-color: var(--mail-surface-muted) !important; } -/* Réglages / administration : fond décoratif visible uniquement derrière la sidebar (contenu opaque). */ +/* Agenda : pas de fond décoratif mail — surfaces opaques (carte arrondie + chrome). */ +html[data-mail-background]:not([data-mail-background='none']) [data-agenda-app].ultimail-app { + background-color: var(--app-canvas) !important; +} + +html[data-mail-background]:not([data-mail-background='none']) [data-agenda-app] :where(.bg-app-canvas) { + background-color: var(--app-canvas) !important; +} + +html[data-mail-background]:not([data-mail-background='none']) [data-agenda-app] :where(.bg-mail-surface, .bg-white) { + background-color: var(--mail-surface) !important; +} + +html[data-mail-background]:not([data-mail-background='none']) [data-agenda-app] :where(.bg-mail-surface-elevated) { + background-color: var(--mail-surface-elevated) !important; +} + +html[data-mail-background]:not([data-mail-background='none']) [data-agenda-app] [data-agenda-calendar-card] { + background-color: var(--mail-surface) !important; +} + +/* Réglages / administration / compte : fond décoratif visible uniquement derrière la sidebar (contenu opaque). */ html[data-mail-background]:not([data-mail-background='none']) [data-mail-settings-app].ultimail-app, -html[data-mail-background]:not([data-mail-background='none']) [data-admin-settings-app].ultimail-app { +html[data-mail-background]:not([data-mail-background='none']) [data-admin-settings-app].ultimail-app, +html[data-mail-background]:not([data-mail-background='none']) [data-compte-settings-app].ultimail-app { background-color: var(--app-canvas) !important; } @@ -738,7 +763,10 @@ html[data-mail-background]:not([data-mail-background='none']) [data-mail-settings-sidebar], html[data-mail-background]:not([data-mail-background='none']) [data-admin-settings-app] - [data-admin-settings-sidebar] { + [data-admin-settings-sidebar], +html[data-mail-background]:not([data-mail-background='none']) + [data-compte-settings-app] + [data-compte-settings-sidebar] { background-color: color-mix(in srgb, var(--app-canvas) 72%, transparent) !important; } @@ -747,7 +775,10 @@ html[data-mail-background]:not([data-mail-background='none']) :where([data-mail-settings-main]), html[data-mail-background]:not([data-mail-background='none']) [data-admin-settings-app] - :where([data-admin-settings-main]) { + :where([data-admin-settings-main]), +html[data-mail-background]:not([data-mail-background='none']) + [data-compte-settings-app] + :where([data-compte-settings-main]) { background-color: var(--mail-surface) !important; } @@ -1239,8 +1270,9 @@ html.dark :where([data-contacts-panel] .border-gray-200, [data-contacts-panel] . border-color: var(--border) !important; } -/* Réglages mail : cartes cohérentes en dark mode (fond + bordure plus visible) */ -html.dark [data-mail-settings-main] { +/* Réglages mail / compte : cartes cohérentes en dark mode (fond + bordure plus visible) */ +html.dark [data-mail-settings-main], +html.dark [data-compte-settings-main] { --border: var(--mail-border); } @@ -1270,6 +1302,18 @@ html.dark [data-mail-settings-main] :where( border-color: color-mix(in srgb, var(--mail-border) 72%, transparent) !important; } +html.dark [data-compte-settings-main] :where( + .mail-settings-card, + [data-slot='card'], + [class*='rounded-lg'].border, + [class*='rounded-xl'].border, + [class*='rounded-md'].border, + [class*='rounded-2xl'].border +) { + background-color: var(--mail-surface-elevated) !important; + border-color: var(--mail-border) !important; +} + /* Settings / Drive : cartes et champs internes — gris mail, pas le noir shadcn */ html.dark .ultimail-app :where(.bg-background) { background-color: var(--mail-surface-muted) !important; diff --git a/app/icon.png b/app/icon.png index 7242607e4fc1aba8bdf4a9f80d3d531f3ca8de7e..8e90ab717155ef2d5d6dfe86756740a313cfae99 100644 GIT binary patch delta 927 zcmV;Q17Q6054;DED}OE{001r{0eGc9b^rhaZb?KzRCwC$m0w6xQ5?swZc8KTS%Va$ zB0=!s-s=RHbN5b^NYbr6X@XQ^^bjG{i_nrd-vA|A3X4banA3z@8|dDoQog_Yk$8jw-PT{Ca{(jwY+q% z{!`uQEhWsp7G)n(kW}15;2sZ&12>5W+yr_tTk#sot0gF3fG{(Fpcqd=2sc^L%8Eyf zg`f$nAyaXJ@;5}qcT7bwt)c-miRrljAqWEB_NdTv>57wGJ~b@+CRaQR;Mb=G5H@Us zCefOd%Wn2oL4Pq{ym%G>&S4-NW@c4P zJOco1NdSais>RCUW0huK85E#-qLgnbV*r@(u!q2m{B~e&?mMCGTt5W|?vFsmci{eT zRsifi?0+|}9VE7}OkySK-i;zCGSAPP83mxt~hGd%q9(j z0Nrs__$^k2L@SiNRl69Iy;Vyk4Fr?O@qkn6kAZMX?d%OmS>0&NMJ?JFvEmvA9cwB~ zDL&|sP+Wj0L<*H)Gv|wk06J4n0K7x;A;c69!XaJQo|Ih`0tpXXsOO73af{Y<@>P9a zT7NwRuaQES_0M>SooZ~k9Ypy#_z{OQ+B$faG002ovPDHLkV1fu! Bv+e)@ delta 2015 zcmV<52O#*o2lNk+D}Mv%000B%0kwNlYXATTrb$FWRCwCWR|!zlR~}9Zb=uvT-RTas z)`KGuZjehUXHaedl_PRUKm<9YXb>!-HYxV-0BgI&QE6AFuv2YRs!%y9CDa0@>eBfJ2flP0h(M#akuBxBh=3=xL_>xJaii6D{{Ax4r17@-qrrrQuIbDp>YWy!G4 z4yI!wUrGw*18b5<(IkOPmO!EhXp$49B3G@pOgMkintzW^LI-ZNoHz|(=^+$L{D~$z z91t(L9|57W7HO;s$zCHQKXfgdjL+(caLj&PLV{}qRsvJ9Qptwoa5|R6N}mu6bw)H$ zm|{vGD@=?fvsqv#Lem`?Cc&9NH0e4dD4@@JvXiJ@K(X zTo@$_!+!`97fLj#aX_xfgaDJvDv;(5n&v>53Z&d?{6h#slH8ps|a9=xBiv& zDG-rDucqYCipWGdG=Bl53zGrSa#I3D0x`0k!Q|M2u5uj4{x!28jh{-_c{M=FhgM=q z=vFJJb~%py_dsxo4v<~Om9c6FWqM%Uu0wa+T07_UJELA~gjWG20V~;E=aU9$)sRJe z5q~ptUNXHCxR{bC5s*><69T0yfkFo`1w4>q`(ae4+ZlI8cFbE&DxGf9YAf8Xo=Hab z+v1<~p2yFhOywKb=Nvm>KmRXE`P}LexK)g3y(EC&I(jWZN|v$D7*}d>5o`lRMCF@8 zNN5g_BsNvx^&m0U*n?SZ1Ns_&NL7=ruYUu%-s3J%eU@o``$La+YSj`Gpn z?DO;Cw&2I()G36HM2w_0H8hX;B%h{c^~dw}_Bn zR+&X*rUc#!5M#X^3vLCn(QXK}=@W%$ycQTNc;mA$(Un6k*9VU|;rQq7w6-G>Yk%9< zKv-YRM9Sm{*?aznn0?n$Os!}vM3GWh&nYBhvsVxjP|Uib+|-8KUJ;{$2WF!s=$pJD zl`Q?PN~p7+F7b5Zo85UcdXM?ZzL5K4cSkf+-k!r`w`@nkq58jOw_SQk7bBFhHZ%Ym zKO;B8+Ot@ra$TtvJki86YWX^{1c{#`Y5rvn_dBi(HAlFw=-w$uS;j&8&$8A5s{oRpU#f_N3 zF_jqy?T6I0ja~|(z#P!~@XWW7*UtF2)vhV754!iyX$33H% zTQhvvaBN(TF}u+m5`#uI2@k%Y!Eps5FJFGn^nR%OGvtT zHt}WO!S17PG%owDBdqR5sNOC5DmtM8ZFR222@7bOUr^zs1%|tMCx368iw!ltmVebv zkS)#Oovjg0tzBn$O-k?%oKrb8{0QGqzuj$`oE*~~-t2k*52S$=WI|@%h|xApPrr9P}9GlVO#&=hVuU8hW!4lhNa4t4Ke*|8y1|a zYz_F^&KB3Mk1z5&8-E$=_AeRB#tt>F?IPTcTxgbcrPKxcY zJ5>XljylGsvx%|$x)UDF-E}3c*M4t$Jol5$aK_*cpY}`nv46|<%=c%k$iEeqxX^Eh zQ%YyFoOj3mfw`#uhX9)fqfk7!2$_Sah#SmC$lz+s8Q2JSU%;v z+Y={be-OCoap;PTe%$-)b;Ux@i+usvioUs$6TLo?&-?r(b$x-74-{dN3dKT6i6U8& zrO1>dC{{=!6j-Yzfqfe!A${A~@!sVzio>pQx&D&!z(VyvBCI=}$iE{Us4?3gZ#`Rj xAvBZA1?I*s{Y4i{bm>IbSxk!GeyxA%?SFp)#48WD1xWw^002ovPDHLkV1hgN-sAuP diff --git a/app/layout.tsx b/app/layout.tsx index 25181a2..5f4bdfb 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,8 +10,8 @@ import { SessionGuard } from '@/components/auth/session-guard' import { MailToaster } from '@/components/gmail/mail-toaster' import { suiteRootMetadata } from '@/lib/suite/page-metadata' -const _geist = Geist({ subsets: ["latin"] }); -const _geistMono = Geist_Mono({ subsets: ["latin"] }); +const geistSans = Geist({ subsets: ['latin'], variable: '--font-geist-sans' }) +const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-geist-mono' }) export const metadata: Metadata = suiteRootMetadata() @@ -30,7 +30,11 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - + diff --git a/app/mail/mail-app-shell.tsx b/app/mail/mail-app-shell.tsx index 62b2786..38fd413 100644 --- a/app/mail/mail-app-shell.tsx +++ b/app/mail/mail-app-shell.tsx @@ -7,6 +7,7 @@ import { useLayoutEffect, useState, } from "react" +import { usePathname } from "next/navigation" import { useIsXs } from "@/hooks/use-xs" import { readTouchNavMatches, useTouchNav } from "@/hooks/use-touch-nav" import { useMailSplitView } from "@/hooks/use-mail-split-view" @@ -16,7 +17,6 @@ import { searchParamsToDisplayQuery } from "@/lib/mail-search/search-filter" import { MobileBottomBar } from "@/components/gmail/mobile-bottom-bar" import { MobileSearchOverlay } from "@/components/gmail/mobile-search-overlay" import type { MailXsViewChrome } from "@/lib/mail-xs-view-chrome" -import { useRouter, usePathname } from "next/navigation" import { Sidebar } from "@/components/gmail/sidebar" import { Header } from "@/components/gmail/header" import { EmailList } from "@/components/gmail/email-list" @@ -55,7 +55,6 @@ function isMailSettingsPath(pathname: string | null): boolean { } function MailAppInner() { - const router = useRouter() const { route, navigateRoute, searchParams: currentSearchParams } = useMailRoute() const activeSearchQuery = @@ -204,7 +203,14 @@ function MailAppInner() { xsViewChrome={xsViewChrome} onOpenSearch={() => setMobileSearchOpen(true)} searchQuery={activeSearchQuery} - onClearSearch={() => router.push("/mail/inbox")} + onClearSearch={() => + navigateRoute({ + folderId: "inbox", + inboxTab: DEFAULT_INBOX_TAB, + page: 1, + mailId: null, + }) + } /> ) : null} +}) { + const { room } = await params + + return ( + + + Chargement… + + } + > + + + ) +} diff --git a/app/meet/join/page.tsx b/app/meet/join/page.tsx new file mode 100644 index 0000000..43a6ce5 --- /dev/null +++ b/app/meet/join/page.tsx @@ -0,0 +1,18 @@ +import { Suspense } from "react" +import { Loader2 } from "lucide-react" +import { MeetJoinClient } from "@/components/meet/meet-join-client" + +export default function MeetJoinPage() { + return ( + + + Chargement… + + } + > + + + ) +} diff --git a/app/meet/layout.tsx b/app/meet/layout.tsx new file mode 100644 index 0000000..8a002cd --- /dev/null +++ b/app/meet/layout.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from "react" +import { MeetAppShell } from "@/components/meet/meet-app-shell" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata = suitePageMetadata({ app: "meet" }) + +export default function MeetLayout({ children }: { children: ReactNode }) { + return {children} +} diff --git a/app/meet/page.tsx b/app/meet/page.tsx new file mode 100644 index 0000000..6b44750 --- /dev/null +++ b/app/meet/page.tsx @@ -0,0 +1,5 @@ +import { MeetLobby } from "@/components/meet/meet-lobby" + +export default function MeetPage() { + return +} diff --git a/components/admin/settings/admin-access-guard.tsx b/components/admin/settings/admin-access-guard.tsx index 6ebb100..f85684b 100644 --- a/components/admin/settings/admin-access-guard.tsx +++ b/components/admin/settings/admin-access-guard.tsx @@ -1,16 +1,13 @@ "use client" import Link from "next/link" -import { useAuthStore } from "@/lib/api/auth-store" import { useAuthReady } from "@/lib/api/use-auth-ready" -import { useCurrentUser } from "@/lib/api/hooks/use-current-user" -import { adminScopesFromToken, isPlatformAdminFromToken } from "@/lib/auth/admin" +import { usePlatformAdminAccess } from "@/lib/auth/use-platform-admin-access" import { Button } from "@/components/ui/button" export function AdminAccessGuard({ children }: { children: React.ReactNode }) { const { ready, authenticated } = useAuthReady() - const token = useAuthStore((s) => s.accessToken) - const { data: me, isFetching: meLoading } = useCurrentUser() + const { isAdmin, adminReady } = usePlatformAdminAccess() if (!ready) { return ( @@ -29,16 +26,12 @@ export function AdminAccessGuard({ children }: { children: React.ReactNode }) { ) } - if (meLoading && !me) { + if (!adminReady) { return (

Vérification des droits administrateur…

) } - const scopes = adminScopesFromToken(token) - const isAdmin = - isPlatformAdminFromToken(token) || scopes.read || me?.platform_admin === true - if (!isAdmin) { return (
diff --git a/components/admin/settings/admin-settings-header.tsx b/components/admin/settings/admin-settings-header.tsx index 32bbe3a..4a45fd0 100644 --- a/components/admin/settings/admin-settings-header.tsx +++ b/components/admin/settings/admin-settings-header.tsx @@ -2,6 +2,12 @@ import { AdminLogo } from "@/components/admin/admin-logo" import { HeaderAccountActions } from "@/components/suite/header-account-actions" +import { + SUITE_APP_LOGO_LOCKUP_CLASS, + SUITE_APP_LOGO_MARK_CLASS, + SUITE_APP_LOGO_TEXT_CLASS, +} from "@/lib/suite/suite-chrome-classes" +import { cn } from "@/lib/utils" const SETTINGS_HREF = "/admin/settings" @@ -11,13 +17,18 @@ export function AdminSettingsHeader() { data-admin-settings-chrome-header className="flex h-16 w-full shrink-0 items-center gap-0 bg-app-canvas pr-4 sm:gap-2" > -
- - Administration +
+ + Administration
- +
diff --git a/components/admin/settings/admin-settings-section-view.tsx b/components/admin/settings/admin-settings-section-view.tsx index 0641248..6ffbb89 100644 --- a/components/admin/settings/admin-settings-section-view.tsx +++ b/components/admin/settings/admin-settings-section-view.tsx @@ -21,6 +21,8 @@ import { MailingSection } from "@/components/admin/settings/sections/mailing-sec import { OnlyofficeSection } from "@/components/admin/settings/sections/onlyoffice-section" import { RichtextSection } from "@/components/admin/settings/sections/richtext-section" import { AiAssistantSection } from "@/components/admin/settings/sections/ai-assistant-section" +import { AgendaSection } from "@/components/admin/settings/sections/agenda-section" +import { UltimeetSection } from "@/components/admin/settings/sections/ultimeet-section" import { AuditSection } from "@/components/admin/settings/sections/audit-section" const SECTIONS: Record = { @@ -36,6 +38,8 @@ const SECTIONS: Record = { search: SearchSection, plugins: PluginsSection, nextcloud: NextcloudSection, + agenda: AgendaSection, + ultimeet: UltimeetSection, mailing: MailingSection, onlyoffice: OnlyofficeSection, richtext: RichtextSection, diff --git a/components/admin/settings/org-settings-form.tsx b/components/admin/settings/org-settings-form.tsx index a7de5b4..cde745b 100644 --- a/components/admin/settings/org-settings-form.tsx +++ b/components/admin/settings/org-settings-form.tsx @@ -28,9 +28,10 @@ export function OrgSettingsSection({ }) { const [saved, setSaved] = useState(false) const [error, setError] = useState(null) - const { isFetching, isError, refetch } = useOrgSettings() + const { isFetching, isError, refetch, isFetched } = useOrgSettings() const savePolicy = useSaveOrgPolicy() const apiSynced = useOrgSettingsStore((s) => s.apiSynced) + const showPendingBanner = !apiSynced && !isError && (isFetching || !isFetched) const hasSave = Boolean(policySection) async function handleSave() { @@ -51,7 +52,7 @@ export function OrgSettingsSection({ <> refetch()} /> - {!apiSynced ? : null} + {!showPendingBanner ? null : } {showEffectiveBanner ? : null}
{children}
{hasSave ? ( diff --git a/components/admin/settings/org-settings-sync.tsx b/components/admin/settings/org-settings-sync.tsx index 360fac5..3d46492 100644 --- a/components/admin/settings/org-settings-sync.tsx +++ b/components/admin/settings/org-settings-sync.tsx @@ -14,13 +14,18 @@ export function OrgSettingsSync() { useEffect(() => { if (!data) return - hydratingRef.current = true - const mapped = apiOrgPolicyToStore(data.policy) - const meta = apiOrgSettingsMeta(data) - useOrgSettingsStore.getState().hydrateFromApi(mapped, meta) - queueMicrotask(() => { - hydratingRef.current = false - }) + try { + hydratingRef.current = true + const mapped = apiOrgPolicyToStore(data.policy) + const meta = apiOrgSettingsMeta(data) + useOrgSettingsStore.getState().hydrateFromApi(mapped, meta) + } catch (err) { + console.error("org settings hydrate failed", err) + } finally { + queueMicrotask(() => { + hydratingRef.current = false + }) + } }, [data]) return null diff --git a/components/admin/settings/sections/agenda-section.tsx b/components/admin/settings/sections/agenda-section.tsx new file mode 100644 index 0000000..0200ee6 --- /dev/null +++ b/components/admin/settings/sections/agenda-section.tsx @@ -0,0 +1,158 @@ +"use client" + +import { useEffect, useState } from "react" +import { OrgSettingsSection } from "@/components/admin/settings/org-settings-form" +import { AgendaVideoProviderSelectLabel } from "@/components/agenda/agenda-video-provider-select-label" +import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store" +import { + AGENDA_VIDEO_PROVIDER_LABELS, + AGENDA_VIDEO_PROVIDERS, + type AgendaVideoProvider, +} from "@/lib/agenda/agenda-settings-types" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import type { MailThemeMode } from "@/lib/mail-settings/types" + +const THEME_OPTIONS: { id: MailThemeMode; label: string }[] = [ + { id: "light", label: "Clair" }, + { id: "dark", label: "Sombre" }, + { id: "system", label: "Système" }, +] + +export function AgendaSection() { + const agenda = useOrgSettingsStore((s) => s.agenda) + const setAgenda = useOrgSettingsStore((s) => s.setAgenda) + const [draft, setDraft] = useState(agenda) + + useEffect(() => { + setDraft(agenda) + }, [agenda]) + + const updateApiKey = (provider: AgendaVideoProvider, value: string) => { + setDraft((prev) => ({ + ...prev, + video_provider_api_keys: { + ...prev.video_provider_api_keys, + [provider]: value, + }, + })) + } + + return ( + setAgenda(draft)} + > +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+

Clés API visioconférence (organisation)

+

+ Stockées côté serveur. UltiMeet n'exige pas de clé API. +

+ {(["zoom", "google_meet", "teams", "jitsi"] as AgendaVideoProvider[]).map( + (provider) => ( +
+ + updateApiKey(provider, e.target.value)} + /> +
+ ), + )} +
+
+
+ ) +} diff --git a/components/admin/settings/sections/drive-mount-oauth-section.tsx b/components/admin/settings/sections/drive-mount-oauth-section.tsx new file mode 100644 index 0000000..8f2a69b --- /dev/null +++ b/components/admin/settings/sections/drive-mount-oauth-section.tsx @@ -0,0 +1,154 @@ +"use client" + +import { useEffect, useState } from "react" +import { Check, Copy } from "lucide-react" +import { toast } from "sonner" +import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store" +import type { DriveMountOAuthProvider, DriveMountOAuthSettings } from "@/lib/admin-settings/org-settings-types" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { buildDriveMountOAuthRedirectURI } from "@/lib/drive/drive-mount-oauth" + +const PROVIDERS: { id: DriveMountOAuthProvider; label: string; hint: string }[] = [ + { + id: "google", + label: "Google Drive", + hint: "Console Google Cloud — API Drive, redirect URI ci-dessous", + }, + { + id: "dropbox", + label: "Dropbox", + hint: "App Dropbox — permissions files.metadata.read, files.content.read/write", + }, + { + id: "microsoft", + label: "Microsoft OneDrive", + hint: "Azure AD — Microsoft Graph Files.ReadWrite", + }, +] + +const SECRET_KEYS: Record = { + google: "mount_oauth_google", + dropbox: "mount_oauth_dropbox", + microsoft: "mount_oauth_microsoft", +} + +export function DriveMountOAuthSection({ + draft, + onChange, +}: { + draft: DriveMountOAuthSettings + onChange: (next: DriveMountOAuthSettings) => void +}) { + const secrets = useOrgSettingsStore((s) => s.meta?.secrets) + const [redirectUri, setRedirectUri] = useState("") + const [copied, setCopied] = useState(false) + + useEffect(() => { + setRedirectUri(buildDriveMountOAuthRedirectURI()) + }, []) + + const updateProvider = (provider: DriveMountOAuthProvider, patch: Partial) => { + onChange({ + ...draft, + [provider]: { ...draft[provider], ...patch }, + }) + } + + const copyRedirectUri = async () => { + const uri = redirectUri || buildDriveMountOAuthRedirectURI() + try { + await navigator.clipboard.writeText(uri) + setCopied(true) + toast.success("URI de redirection copiée") + window.setTimeout(() => setCopied(false), 2000) + } catch { + toast.error("Impossible de copier l'URI") + } + } + + return ( +
+
+

Connexion cloud (OAuth)

+

+ Permet aux utilisateurs de monter Google Drive, Dropbox ou OneDrive depuis UltiDrive. +

+
+
+ +
+ + +
+

+ Basée sur l'URL actuelle du navigateur. Enregistrez-la chez chaque fournisseur OAuth (Google, Dropbox, Microsoft). +

+
+
+ {PROVIDERS.map(({ id, label, hint }) => { + const provider = draft[id] + const configured = Boolean(secrets?.[SECRET_KEYS[id]]?.configured) + return ( +
+ + {provider.enabled ? ( +
+
+ + updateProvider(id, { client_id: e.target.value })} + autoComplete="off" + /> +
+
+ + updateProvider(id, { client_secret: e.target.value })} + placeholder={configured ? "•••••••• (laisser vide pour conserver)" : "Coller le secret"} + autoComplete="off" + /> + {configured && !provider.client_secret.trim() ? ( +

Secret configuré

+ ) : null} +
+
+ ) : null} +
+ ) + })} +
+
+ ) +} diff --git a/components/admin/settings/sections/drive-org-section.tsx b/components/admin/settings/sections/drive-org-section.tsx new file mode 100644 index 0000000..3125868 --- /dev/null +++ b/components/admin/settings/sections/drive-org-section.tsx @@ -0,0 +1,94 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + useAdminDriveOrgFolderMutations, + useAdminDriveOrgFolders, +} from "@/lib/api/hooks/use-admin-drive-queries" + +export function DriveOrgFoldersSection() { + const folders = useAdminDriveOrgFolders() + const { create, remove, sync } = useAdminDriveOrgFolderMutations() + const [orgSlug, setOrgSlug] = useState("") + const [mountPoint, setMountPoint] = useState("") + const [syncSlugs, setSyncSlugs] = useState("") + + return ( +
+
+

Dossiers d'organisation

+

+ Group folders Nextcloud liés aux organisations Authentik. +

+
+ +
+
+ + setOrgSlug(e.target.value)} placeholder="acme" /> +
+
+ + setMountPoint(e.target.value)} placeholder="Acme Corp" /> +
+
+ + +
+ + setSyncSlugs(e.target.value)} + placeholder="acme, beta" + /> + +
+ +
    + {(folders.data ?? []).map((folder) => ( +
  • +
    +

    {folder.mount_point}

    +

    {folder.org_slug}

    +
    + +
  • + ))} + {folders.data?.length === 0 ? ( +
  • Aucun dossier d'organisation
  • + ) : null} +
+
+ ) +} diff --git a/components/admin/settings/sections/file-policies-section.tsx b/components/admin/settings/sections/file-policies-section.tsx index 3660159..6f79773 100644 --- a/components/admin/settings/sections/file-policies-section.tsx +++ b/components/admin/settings/sections/file-policies-section.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect, useState } from "react" import { OrgSettingsSection } from "@/components/admin/settings/org-settings-form" import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store" import { Input } from "@/components/ui/input" @@ -13,10 +14,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { DriveOrgFoldersSection } from "@/components/admin/settings/sections/drive-org-section" +import { DriveMountOAuthSection } from "@/components/admin/settings/sections/drive-mount-oauth-section" export function FilePoliciesSection() { const filePolicies = useOrgSettingsStore((s) => s.filePolicies) const setFilePolicies = useOrgSettingsStore((s) => s.setFilePolicies) + const [mountOAuthDraft, setMountOAuthDraft] = useState(filePolicies.mount_oauth) const vtKeyConfigured = useOrgSettingsStore( (s) => s.meta?.secrets?.virustotal_api_key?.configured ?? false ) @@ -25,11 +29,16 @@ export function FilePoliciesSection() { !vtKeyConfigured && !(filePolicies.virustotal_api_key ?? "").trim() + useEffect(() => { + setMountOAuthDraft(filePolicies.mount_oauth) + }, [filePolicies.mount_oauth]) + return ( setFilePolicies({ mount_oauth: mountOAuthDraft })} >
@@ -143,6 +152,8 @@ export function FilePoliciesSection() {
) : null}
+ +
) } diff --git a/components/admin/settings/sections/ultimeet-section.tsx b/components/admin/settings/sections/ultimeet-section.tsx new file mode 100644 index 0000000..4bfa11a --- /dev/null +++ b/components/admin/settings/sections/ultimeet-section.tsx @@ -0,0 +1,347 @@ +"use client" + +import { useEffect, useState } from "react" +import { OrgSettingsSection } from "@/components/admin/settings/org-settings-form" +import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store" +import { + MEET_EMAIL_RECIPIENTS_LABELS, + MEET_EXTERNAL_API_PROVIDER_LABELS, + MEET_TRANSCRIPTION_ENGINE_LABELS, + MEET_TRANSCRIPTION_MODE_LABELS, + type MeetOrgPolicySettings, +} from "@/lib/meet/meet-settings-types" +import { Label } from "@/components/ui/label" +import { Switch } from "@/components/ui/switch" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" + +export function UltimeetSection() { + const meet = useOrgSettingsStore((s) => s.meet) + const llmProviders = useOrgSettingsStore((s) => s.llm.providers) + const setMeet = useOrgSettingsStore((s) => s.setMeet) + const effective = useOrgSettingsStore((s) => s.meta?.effective.jitsi) + const [draft, setDraft] = useState(meet) + + useEffect(() => { + setDraft(meet) + }, [meet]) + + const patch = (next: Partial) => + setDraft((prev) => ({ ...prev, ...next })) + + const patchPost = (next: Partial) => + setDraft((prev) => ({ + ...prev, + post_actions: { ...prev.post_actions, ...next }, + })) + + return ( + setMeet(draft)} + > + + + Infrastructure + + Jitsi {effective?.enabled ? "actif" : "inactif"} + {effective?.public_url ? ` — ${effective.public_url}` : ""} + + + + +
+ + + {draft.transcription_enabled ? ( + <> +
+
+ + +
+ +
+ + +
+
+ + + + {draft.transcription_engine === "faster_whisper_local" ? ( +
+
+ + patch({ skynet_url: e.target.value })} + placeholder="http://skynet:8000" + /> +
+
+ + patch({ whisper_model: e.target.value })} + placeholder="tiny, base, small…" + /> +
+
+ ) : ( +
+
+ + +
+
+ + patch({ external_api_url: e.target.value })} + placeholder="https://api.openai.com/v1/audio/transcriptions" + /> +
+
+ + patch({ external_api_key: e.target.value })} + placeholder="Laisser vide pour conserver la clé enregistrée" + autoComplete="off" + /> +
+
+ )} + + ) : null} +
+ + {draft.transcription_enabled ? ( +
+
+

Après la réunion

+

+ Actions exécutées quand le transcript est reçu par le backend. +

+
+ + + {draft.post_actions.drive_enabled ? ( +
+ + patchPost({ drive_folder_path: e.target.value })} + placeholder="/UltiMeet/Transcripts" + /> +
+ ) : null} + + + {draft.post_actions.email_enabled ? ( +
+
+ + +
+ {draft.post_actions.email_recipients === "custom" ? ( +
+ + patchPost({ email_custom_addresses: e.target.value })} + /> +
+ ) : null} +
+ ) : null} + + + {draft.post_actions.llm_enabled ? ( +
+
+ + +
+
+ +