From efaaf16f60aa0f6ddd45fd20fce890c7c4269bb7 Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Fri, 19 Jun 2026 22:11:42 +0200 Subject: [PATCH] feat: update metadata and layout for new product pages - Refactored metadata for contacts, administration, and Ulticards pages to utilize dynamic app names and descriptions. - Introduced new product pages for Ultiai, Ultical, Ulticards, Ultidrive, Ultimail, and Ultimeet with appropriate metadata. - Enhanced layout components to ensure consistent styling and functionality across new product sections. - Updated various components to replace hardcoded labels with dynamic references to improve maintainability and consistency. --- app/contacts/layout.tsx | 6 +- app/demo/contacts/layout.tsx | 6 +- app/onboard/migration/page.tsx | 4 +- app/suite/administration/page.tsx | 14 + app/suite/ultiai/page.tsx | 17 + app/suite/ultical/page.tsx | 14 + app/suite/ulticards/page.tsx | 14 + app/suite/ultidrive/page.tsx | 14 + app/suite/ultimail/page.tsx | 14 + app/suite/ultimeet/page.tsx | 14 + .../sections/migration-projects-panel.tsx | 4 +- .../settings/sections/plugins-section.tsx | 4 +- components/demo/demo-chrome.tsx | 2 +- components/demo/demo-drive-shell.tsx | 2 + components/demo/demo-mail-shell.tsx | 2 + components/drive/drive-app-shell.tsx | 8 +- .../gmail/calendar-invitation-preview.tsx | 5 +- .../gmail/contacts/contacts-panel-logo.tsx | 3 +- components/gmail/contacts/contacts-panel.tsx | 3 +- .../gmail/email-list/email-list-body.tsx | 16 +- .../hooks/use-email-list-reading.ts | 43 +- .../gmail/email-view/email-view-messages.tsx | 2 +- .../gmail/email-view/email-view-toolbar.tsx | 19 +- components/gmail/mail-date-text.tsx | 30 + components/gmail/right-panel.tsx | 3 +- .../automation/rule-simulator-panel.tsx | 3 +- components/landing/landing-data.ts | 68 +- components/landing/landing-demo.tsx | 52 +- components/landing/landing-sections.tsx | 7 +- .../product-cross-platform-section.tsx | 583 +++++ components/landing/product/product-cta.tsx | 53 + components/landing/product/product-data.tsx | 2023 +++++++++++++++++ .../landing/product/product-demo-frame.tsx | 69 + .../product-demos/admin-identity-demo.tsx | 96 + .../product-demos/admin-migration-demo.tsx | 102 + .../product-demos/admin-policies-demo.tsx | 86 + .../product-demos/admin-quotas-demo.tsx | 80 + .../product-demos/admin-users-demo.tsx | 111 + .../product-demos/product-mail-demo-shell.tsx | 42 + .../product-demos/ultiai-chat-demo.tsx | 109 + .../product-demos/ultiai-tools-demo.tsx | 106 + .../product-demos/ultiai-triage-demo.tsx | 97 + .../product-demos/ultical-agenda-demo.tsx | 55 + .../product-demos/ultical-invitation-demo.tsx | 48 + .../product-demos/ultical-scheduling-demo.tsx | 135 ++ .../ulticards-directory-demo.tsx | 55 + .../ulticards-discovery-demo.tsx | 134 ++ .../product-demos/ulticards-merge-demo.tsx | 131 ++ .../product-demos/ultidrive-browser-demo.tsx | 66 + .../product-demos/ultidrive-docs-demo.tsx | 55 + .../product-demos/ultidrive-share-demo.tsx | 16 + .../product-demos/ultidrive-share-preview.tsx | 166 ++ .../ultimail-automation-demo.tsx | 18 + .../product-demos/ultimail-compose-demo.tsx | 74 + .../product-demos/ultimail-demo-workflow.ts | 57 + .../product-demos/ultimail-inbox-demo.tsx | 48 + .../product-demos/ultimeet-collab-demo.tsx | 110 + .../product-demos/ultimeet-lobby-demo.tsx | 107 + .../product-demos/ultimeet-room-demo.tsx | 160 ++ .../product-demos/workflow-flow-preview.tsx | 66 + .../landing/product/product-feature-grid.tsx | 65 + components/landing/product/product-hero.tsx | 100 + .../landing/product/product-highlights.tsx | 69 + .../landing/product/product-integrations.tsx | 87 + .../product/product-interop-section.tsx | 123 + .../landing/product/product-page-shell.tsx | 208 ++ .../product/product-section-heading.tsx | 44 + .../landing/product/product-showcases.tsx | 256 +++ .../landing/product/scaled-preview-iframe.tsx | 139 ++ hooks/use-mail-split-view.ts | 9 + lib/ai/ultiai-tool-groups.ts | 4 +- lib/auth/native-auth.ts | 5 +- lib/auth/public-paths.ts | 1 + lib/contacts/print-contacts.ts | 3 +- .../demo-drive-layout-preview-bootstrap.tsx | 23 + lib/demo/demo-drive-layout-preview.ts | 13 + lib/demo/demo-drive-preview.ts | 2 +- lib/demo/demo-mail-preview-bootstrap.tsx | 31 + lib/demo/demo-mail-preview.ts | 20 + lib/mail-automation/api-token-permissions.ts | 4 +- lib/mail-automation/domains.ts | 4 +- lib/mail-automation/types.ts | 2 +- lib/mail-chrome-classes.ts | 11 + lib/mail-date.ts | 47 +- lib/native/http.ts | 48 + lib/runtime-config/index.ts | 3 +- lib/suite/favorite-apps.ts | 4 +- lib/suite/page-metadata.ts | 7 +- lib/suite/suite-app-splash.ts | 6 +- mobile/Cargo.lock | 277 ++- mobile/Cargo.toml | 1 + .../apps/contacts/src-tauri/tauri.conf.json | 4 +- mobile/crates/ulti-core/Cargo.toml | 1 + mobile/crates/ulti-core/build.rs | 1 + .../autogenerated/commands/http_request.toml | 13 + .../permissions/autogenerated/reference.md | 27 + .../crates/ulti-core/permissions/default.toml | 1 + .../ulti-core/permissions/schemas/schema.json | 16 +- mobile/crates/ulti-core/src/lib.rs | 52 +- out/404.html | 2 +- out/404/index.html | 2 +- out/__next.__PAGE__.txt | 4 +- out/__next._full.txt | 8 +- out/__next._head.txt | 2 +- out/__next._index.txt | 6 +- out/__next._tree.txt | 4 +- .../OFDwMfc5Pr2nZnVdAoyoc/_buildManifest.js | 1 - .../OFDwMfc5Pr2nZnVdAoyoc/_ssgManifest.js | 1 - .../static/chunks/1862.65faf38ba3939d15.js | 1 - .../static/chunks/2268-3e79c752b2ed6430.js | 38 - .../static/chunks/294-d25d4d9e5f45af4c.js | 1 - .../static/chunks/3660-4dea82fc6959c5b8.js | 1 - .../static/chunks/3719-4f12726272c13648.js | 5 - .../static/chunks/4156-126c7d677784214a.js | 1 - .../static/chunks/4498-77a1a5cf57e4efbb.js | 1 - .../static/chunks/5143-db5ae9673c121033.js | 2 - .../static/chunks/563-f13ab691e5395b61.js | 1 - .../static/chunks/7001-9cbc3eb7ee2255af.js | 1 - .../static/chunks/8868-bdd232b0f3ac2ffa.js | 1 - .../static/chunks/9731-4e57aaa90a88eac7.js | 1 - .../static/chunks/9821-63c24d4f4d16de76.js | 1 - .../app/account/layout-402754dc927a4e09.js | 1 - .../[[...segments]]/page-bd2e2ac72698ea50.js | 7 - .../app/agenda/layout-50ab4a0ac8765042.js | 1 - .../demo/contacts/layout-1af3f765b1fb9c85.js | 1 - .../app/demo/docs/page-acad723f594cc8f5.js | 44 - .../[[...segments]]/page-442052f871743fb1.js | 1 - .../(browser)/layout-ae205affaf827be7.js | 1 - .../chunks/app/layout-ff893684d9d60f2f.js | 118 - .../app/login/layout-1714c0f653863ebe.js | 1 - .../chunks/app/login/page-7debb75685146df8.js | 1 - .../app/mail/layout-2c462b8f519b7077.js | 334 --- .../app/meet/layout-333d45e028df760d.js | 1 - .../chunks/app/page-c862aef8c851a5f3.js | 1 - .../[[...section]]/page-ccebb45274be3b02.js | 1 - .../app/settings/layout-769bbb59db434bc8.js | 1 - .../static/chunks/webpack-059392c5e8bbf6a6.js | 1 - out/_next/static/css/6ff56d5b3e82b309.css | 1 - out/_not-found/__next._full.txt | 6 +- out/_not-found/__next._head.txt | 2 +- out/_not-found/__next._index.txt | 6 +- out/_not-found/__next._not-found.__PAGE__.txt | 2 +- out/_not-found/__next._not-found.txt | 2 +- out/_not-found/__next._tree.txt | 4 +- out/_not-found/index.html | 2 +- out/_not-found/index.txt | 6 +- out/account/__next._full.txt | 12 +- out/account/__next._head.txt | 2 +- out/account/__next._index.txt | 6 +- out/account/__next._tree.txt | 4 +- .../__next.account.$oc$section.__PAGE__.txt | 4 +- out/account/__next.account.$oc$section.txt | 2 +- out/account/__next.account.txt | 6 +- out/account/index.html | 2 +- out/account/index.txt | 12 +- out/admin/__next._full.txt | 6 +- out/admin/__next._head.txt | 2 +- out/admin/__next._index.txt | 6 +- out/admin/__next._tree.txt | 4 +- out/admin/__next.admin.__PAGE__.txt | 2 +- out/admin/__next.admin.txt | 2 +- out/admin/index.html | 2 +- out/admin/index.txt | 6 +- out/admin/settings/__next._full.txt | 10 +- out/admin/settings/__next._head.txt | 2 +- out/admin/settings/__next._index.txt | 6 +- out/admin/settings/__next._tree.txt | 4 +- ...xt.admin.settings.$oc$section.__PAGE__.txt | 4 +- .../__next.admin.settings.$oc$section.txt | 2 +- out/admin/settings/__next.admin.settings.txt | 4 +- out/admin/settings/__next.admin.txt | 2 +- out/admin/settings/index.html | 2 +- out/admin/settings/index.txt | 10 +- out/agenda/__next._full.txt | 10 +- out/agenda/__next._head.txt | 2 +- out/agenda/__next._index.txt | 6 +- out/agenda/__next._tree.txt | 4 +- .../__next.agenda.$oc$segments.__PAGE__.txt | 4 +- out/agenda/__next.agenda.$oc$segments.txt | 2 +- out/agenda/__next.agenda.txt | 4 +- out/agenda/index.html | 2 +- out/agenda/index.txt | 10 +- out/auth/complete/__next._full.txt | 6 +- out/auth/complete/__next._head.txt | 2 +- out/auth/complete/__next._index.txt | 6 +- out/auth/complete/__next._tree.txt | 4 +- .../__next.auth.complete.__PAGE__.txt | 2 +- out/auth/complete/__next.auth.complete.txt | 2 +- out/auth/complete/__next.auth.txt | 2 +- out/auth/complete/index.html | 2 +- out/auth/complete/index.txt | 6 +- out/chat/__next._full.txt | 8 +- out/chat/__next._head.txt | 2 +- out/chat/__next._index.txt | 6 +- out/chat/__next._tree.txt | 4 +- out/chat/__next.chat.__PAGE__.txt | 4 +- out/chat/__next.chat.txt | 2 +- out/chat/index.html | 2 +- out/chat/index.txt | 8 +- out/compte/__next._full.txt | 8 +- out/compte/__next._head.txt | 2 +- out/compte/__next._index.txt | 6 +- out/compte/__next._tree.txt | 4 +- .../__next.compte.$oc$section.__PAGE__.txt | 4 +- out/compte/__next.compte.$oc$section.txt | 2 +- out/compte/__next.compte.txt | 2 +- out/compte/index.html | 2 +- out/compte/index.txt | 8 +- out/contacts/__next._full.txt | 8 +- out/contacts/__next._head.txt | 2 +- out/contacts/__next._index.txt | 6 +- out/contacts/__next._tree.txt | 4 +- .../__next.contacts.$oc$slug.__PAGE__.txt | 4 +- out/contacts/__next.contacts.$oc$slug.txt | 2 +- out/contacts/__next.contacts.txt | 2 +- out/contacts/index.html | 2 +- out/contacts/index.txt | 8 +- out/demo/contacts/__next._full.txt | 8 +- out/demo/contacts/__next._head.txt | 2 +- out/demo/contacts/__next._index.txt | 6 +- out/demo/contacts/__next._tree.txt | 4 +- .../__next.demo.contacts.__PAGE__.txt | 2 +- out/demo/contacts/__next.demo.contacts.txt | 4 +- out/demo/contacts/__next.demo.txt | 2 +- out/demo/contacts/index.html | 2 +- out/demo/contacts/index.txt | 8 +- out/demo/docs/__next._full.txt | 8 +- out/demo/docs/__next._head.txt | 2 +- out/demo/docs/__next._index.txt | 6 +- out/demo/docs/__next._tree.txt | 4 +- out/demo/docs/__next.demo.docs.__PAGE__.txt | 4 +- out/demo/docs/__next.demo.docs.txt | 2 +- out/demo/docs/__next.demo.txt | 2 +- out/demo/docs/index.html | 2 +- out/demo/docs/index.txt | 8 +- out/drive/__next._full.txt | 10 +- out/drive/__next._head.txt | 2 +- out/drive/__next._index.txt | 6 +- out/drive/__next._tree.txt | 4 +- ...ve.!KGJyb3dzZXIp.$oc$segments.__PAGE__.txt | 4 +- ..._next.drive.!KGJyb3dzZXIp.$oc$segments.txt | 2 +- out/drive/__next.drive.!KGJyb3dzZXIp.txt | 4 +- out/drive/__next.drive.txt | 2 +- out/drive/index.html | 2 +- out/drive/index.txt | 10 +- .../mounts/oauth/callback/__next._full.txt | 8 +- .../mounts/oauth/callback/__next._head.txt | 2 +- .../mounts/oauth/callback/__next._index.txt | 6 +- .../mounts/oauth/callback/__next._tree.txt | 4 +- ...t.drive.mounts.oauth.callback.__PAGE__.txt | 4 +- .../__next.drive.mounts.oauth.callback.txt | 2 +- .../callback/__next.drive.mounts.oauth.txt | 2 +- .../oauth/callback/__next.drive.mounts.txt | 2 +- .../mounts/oauth/callback/__next.drive.txt | 2 +- out/drive/mounts/oauth/callback/index.html | 2 +- out/drive/mounts/oauth/callback/index.txt | 8 +- out/index.html | 2 +- out/index.txt | 8 +- out/login/__next._full.txt | 10 +- out/login/__next._head.txt | 2 +- out/login/__next._index.txt | 6 +- out/login/__next._tree.txt | 4 +- out/login/__next.login.__PAGE__.txt | 4 +- out/login/__next.login.txt | 4 +- out/login/index.html | 2 +- out/login/index.txt | 10 +- out/mail/__next._full.txt | 8 +- out/mail/__next._head.txt | 2 +- out/mail/__next._index.txt | 6 +- out/mail/__next._tree.txt | 4 +- .../__next.mail.$oc$segments.__PAGE__.txt | 2 +- out/mail/__next.mail.$oc$segments.txt | 2 +- out/mail/__next.mail.txt | 4 +- out/mail/index.html | 2 +- out/mail/index.txt | 8 +- out/meet/__next._full.txt | 10 +- out/meet/__next._head.txt | 2 +- out/meet/__next._index.txt | 6 +- out/meet/__next._tree.txt | 4 +- out/meet/__next.meet.__PAGE__.txt | 4 +- out/meet/__next.meet.txt | 4 +- out/meet/index.html | 2 +- out/meet/index.txt | 10 +- out/meet/join/__next._full.txt | 10 +- out/meet/join/__next._head.txt | 2 +- out/meet/join/__next._index.txt | 6 +- out/meet/join/__next._tree.txt | 4 +- out/meet/join/__next.meet.join.__PAGE__.txt | 4 +- out/meet/join/__next.meet.join.txt | 2 +- out/meet/join/__next.meet.txt | 4 +- out/meet/join/index.html | 2 +- out/meet/join/index.txt | 10 +- out/onboard/claim/__next._full.txt | 8 +- out/onboard/claim/__next._head.txt | 2 +- out/onboard/claim/__next._index.txt | 6 +- out/onboard/claim/__next._tree.txt | 4 +- .../claim/__next.onboard.claim.__PAGE__.txt | 4 +- out/onboard/claim/__next.onboard.claim.txt | 2 +- out/onboard/claim/__next.onboard.txt | 2 +- out/onboard/claim/index.html | 2 +- out/onboard/claim/index.txt | 8 +- out/onboard/migration/__next._full.txt | 8 +- out/onboard/migration/__next._head.txt | 2 +- out/onboard/migration/__next._index.txt | 6 +- out/onboard/migration/__next._tree.txt | 4 +- .../__next.onboard.migration.__PAGE__.txt | 4 +- .../migration/__next.onboard.migration.txt | 2 +- out/onboard/migration/__next.onboard.txt | 2 +- out/onboard/migration/index.html | 2 +- out/onboard/migration/index.txt | 8 +- out/settings/__next._full.txt | 14 +- out/settings/__next._head.txt | 2 +- out/settings/__next._index.txt | 6 +- out/settings/__next._tree.txt | 4 +- .../__next.settings.$oc$section.__PAGE__.txt | 4 +- out/settings/__next.settings.$oc$section.txt | 2 +- out/settings/__next.settings.txt | 8 +- out/settings/index.html | 2 +- out/settings/index.txt | 14 +- tsconfig.tsbuildinfo | 2 +- 320 files changed, 7489 insertions(+), 1112 deletions(-) create mode 100644 app/suite/administration/page.tsx create mode 100644 app/suite/ultiai/page.tsx create mode 100644 app/suite/ultical/page.tsx create mode 100644 app/suite/ulticards/page.tsx create mode 100644 app/suite/ultidrive/page.tsx create mode 100644 app/suite/ultimail/page.tsx create mode 100644 app/suite/ultimeet/page.tsx create mode 100644 components/landing/product/product-cross-platform-section.tsx create mode 100644 components/landing/product/product-cta.tsx create mode 100644 components/landing/product/product-data.tsx create mode 100644 components/landing/product/product-demo-frame.tsx create mode 100644 components/landing/product/product-demos/admin-identity-demo.tsx create mode 100644 components/landing/product/product-demos/admin-migration-demo.tsx create mode 100644 components/landing/product/product-demos/admin-policies-demo.tsx create mode 100644 components/landing/product/product-demos/admin-quotas-demo.tsx create mode 100644 components/landing/product/product-demos/admin-users-demo.tsx create mode 100644 components/landing/product/product-demos/product-mail-demo-shell.tsx create mode 100644 components/landing/product/product-demos/ultiai-chat-demo.tsx create mode 100644 components/landing/product/product-demos/ultiai-tools-demo.tsx create mode 100644 components/landing/product/product-demos/ultiai-triage-demo.tsx create mode 100644 components/landing/product/product-demos/ultical-agenda-demo.tsx create mode 100644 components/landing/product/product-demos/ultical-invitation-demo.tsx create mode 100644 components/landing/product/product-demos/ultical-scheduling-demo.tsx create mode 100644 components/landing/product/product-demos/ulticards-directory-demo.tsx create mode 100644 components/landing/product/product-demos/ulticards-discovery-demo.tsx create mode 100644 components/landing/product/product-demos/ulticards-merge-demo.tsx create mode 100644 components/landing/product/product-demos/ultidrive-browser-demo.tsx create mode 100644 components/landing/product/product-demos/ultidrive-docs-demo.tsx create mode 100644 components/landing/product/product-demos/ultidrive-share-demo.tsx create mode 100644 components/landing/product/product-demos/ultidrive-share-preview.tsx create mode 100644 components/landing/product/product-demos/ultimail-automation-demo.tsx create mode 100644 components/landing/product/product-demos/ultimail-compose-demo.tsx create mode 100644 components/landing/product/product-demos/ultimail-demo-workflow.ts create mode 100644 components/landing/product/product-demos/ultimail-inbox-demo.tsx create mode 100644 components/landing/product/product-demos/ultimeet-collab-demo.tsx create mode 100644 components/landing/product/product-demos/ultimeet-lobby-demo.tsx create mode 100644 components/landing/product/product-demos/ultimeet-room-demo.tsx create mode 100644 components/landing/product/product-demos/workflow-flow-preview.tsx create mode 100644 components/landing/product/product-feature-grid.tsx create mode 100644 components/landing/product/product-hero.tsx create mode 100644 components/landing/product/product-highlights.tsx create mode 100644 components/landing/product/product-integrations.tsx create mode 100644 components/landing/product/product-interop-section.tsx create mode 100644 components/landing/product/product-page-shell.tsx create mode 100644 components/landing/product/product-section-heading.tsx create mode 100644 components/landing/product/product-showcases.tsx create mode 100644 components/landing/product/scaled-preview-iframe.tsx create mode 100644 lib/demo/demo-drive-layout-preview-bootstrap.tsx create mode 100644 lib/demo/demo-drive-layout-preview.ts create mode 100644 lib/demo/demo-mail-preview-bootstrap.tsx create mode 100644 lib/demo/demo-mail-preview.ts create mode 100644 lib/native/http.ts create mode 100644 mobile/crates/ulti-core/permissions/autogenerated/commands/http_request.toml delete mode 100644 out/_next/static/OFDwMfc5Pr2nZnVdAoyoc/_buildManifest.js delete mode 100644 out/_next/static/OFDwMfc5Pr2nZnVdAoyoc/_ssgManifest.js delete mode 100644 out/_next/static/chunks/1862.65faf38ba3939d15.js delete mode 100644 out/_next/static/chunks/2268-3e79c752b2ed6430.js delete mode 100644 out/_next/static/chunks/294-d25d4d9e5f45af4c.js delete mode 100644 out/_next/static/chunks/3660-4dea82fc6959c5b8.js delete mode 100644 out/_next/static/chunks/3719-4f12726272c13648.js delete mode 100644 out/_next/static/chunks/4156-126c7d677784214a.js delete mode 100644 out/_next/static/chunks/4498-77a1a5cf57e4efbb.js delete mode 100644 out/_next/static/chunks/5143-db5ae9673c121033.js delete mode 100644 out/_next/static/chunks/563-f13ab691e5395b61.js delete mode 100644 out/_next/static/chunks/7001-9cbc3eb7ee2255af.js delete mode 100644 out/_next/static/chunks/8868-bdd232b0f3ac2ffa.js delete mode 100644 out/_next/static/chunks/9731-4e57aaa90a88eac7.js delete mode 100644 out/_next/static/chunks/9821-63c24d4f4d16de76.js delete mode 100644 out/_next/static/chunks/app/account/layout-402754dc927a4e09.js delete mode 100644 out/_next/static/chunks/app/agenda/[[...segments]]/page-bd2e2ac72698ea50.js delete mode 100644 out/_next/static/chunks/app/agenda/layout-50ab4a0ac8765042.js delete mode 100644 out/_next/static/chunks/app/demo/contacts/layout-1af3f765b1fb9c85.js delete mode 100644 out/_next/static/chunks/app/demo/docs/page-acad723f594cc8f5.js delete mode 100644 out/_next/static/chunks/app/drive/(browser)/[[...segments]]/page-442052f871743fb1.js delete mode 100644 out/_next/static/chunks/app/drive/(browser)/layout-ae205affaf827be7.js delete mode 100644 out/_next/static/chunks/app/layout-ff893684d9d60f2f.js delete mode 100644 out/_next/static/chunks/app/login/layout-1714c0f653863ebe.js delete mode 100644 out/_next/static/chunks/app/login/page-7debb75685146df8.js delete mode 100644 out/_next/static/chunks/app/mail/layout-2c462b8f519b7077.js delete mode 100644 out/_next/static/chunks/app/meet/layout-333d45e028df760d.js delete mode 100644 out/_next/static/chunks/app/page-c862aef8c851a5f3.js delete mode 100644 out/_next/static/chunks/app/settings/[[...section]]/page-ccebb45274be3b02.js delete mode 100644 out/_next/static/chunks/app/settings/layout-769bbb59db434bc8.js delete mode 100644 out/_next/static/chunks/webpack-059392c5e8bbf6a6.js delete mode 100644 out/_next/static/css/6ff56d5b3e82b309.css diff --git a/app/contacts/layout.tsx b/app/contacts/layout.tsx index 4890e0d..7d2edb1 100644 --- a/app/contacts/layout.tsx +++ b/app/contacts/layout.tsx @@ -1,11 +1,7 @@ import type { Metadata } from "next" import { suitePageMetadata } from "@/lib/suite/page-metadata" -export const metadata: Metadata = suitePageMetadata({ - app: "contacts", - absoluteTitle: true, - title: "Contacts - Ulti Suite", -}) +export const metadata: Metadata = suitePageMetadata({ app: "contacts" }) export default function ContactsLayout({ children, diff --git a/app/demo/contacts/layout.tsx b/app/demo/contacts/layout.tsx index f0560f2..4b9cc6d 100644 --- a/app/demo/contacts/layout.tsx +++ b/app/demo/contacts/layout.tsx @@ -1,14 +1,14 @@ import { DemoContactsShell } from "@/components/demo/demo-contacts-shell" import type { Metadata } from "next" -import { suitePageMetadata } from "@/lib/suite/page-metadata" +import { ULTICARDS_APP_NAME, suitePageMetadata } from "@/lib/suite/page-metadata" export const metadata: Metadata = { ...suitePageMetadata({ app: "contacts", - title: "Démo Contacts", + title: `Démo ${ULTICARDS_APP_NAME}`, absoluteTitle: true, description: - "Essayez les contacts Ulti Suite sans compte — démo interactive, zéro rétention.", + `Essayez ${ULTICARDS_APP_NAME} sans compte — démo interactive, zéro rétention.`, }), robots: { index: false }, } diff --git a/app/onboard/migration/page.tsx b/app/onboard/migration/page.tsx index dd28642..e873377 100644 --- a/app/onboard/migration/page.tsx +++ b/app/onboard/migration/page.tsx @@ -18,11 +18,11 @@ import { useStartMigrationOAuth, } from "@/lib/api/hooks/use-hosted-mail" import { useAuthReady } from "@/lib/api/use-auth-ready" -import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" +import { ULTICAL_APP_NAME, ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" const SERVICE_LABELS: Record = { mail: "Mail", - contacts: "Contacts", + contacts: ULTICARDS_APP_NAME, calendar: ULTICAL_APP_NAME, drive: "Drive", } diff --git a/app/suite/administration/page.tsx b/app/suite/administration/page.tsx new file mode 100644 index 0000000..47ceebe --- /dev/null +++ b/app/suite/administration/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next" +import { ProductPageShell } from "@/components/landing/product/product-page-shell" +import { ADMINISTRATION_PRODUCT } from "@/components/landing/product/product-data" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = suitePageMetadata({ + app: "admin", + title: "Administration — Console souveraine UltiSuite", + description: ADMINISTRATION_PRODUCT.description, +}) + +export default function AdministrationProductPage() { + return +} diff --git a/app/suite/ultiai/page.tsx b/app/suite/ultiai/page.tsx new file mode 100644 index 0000000..0c48143 --- /dev/null +++ b/app/suite/ultiai/page.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from "next" +import { ProductPageShell } from "@/components/landing/product/product-page-shell" +import { ULTIAI_PRODUCT } from "@/components/landing/product/product-data" + +export const metadata: Metadata = { + title: { absolute: "UltiAI — Assistant IA souverain" }, + description: ULTIAI_PRODUCT.description, + icons: { + icon: [{ url: "/ultiai-mark.svg", type: "image/svg+xml" }], + apple: [{ url: "/ultiai-mark.svg", type: "image/svg+xml" }], + shortcut: "/ultiai-mark.svg", + }, +} + +export default function UltiaiProductPage() { + return +} diff --git a/app/suite/ultical/page.tsx b/app/suite/ultical/page.tsx new file mode 100644 index 0000000..84906c3 --- /dev/null +++ b/app/suite/ultical/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next" +import { ProductPageShell } from "@/components/landing/product/product-page-shell" +import { ULTICAL_PRODUCT } from "@/components/landing/product/product-data" +import { ULTICAL_APP_NAME, suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = suitePageMetadata({ + app: "agenda", + title: `${ULTICAL_APP_NAME} — Calendrier partagé souverain`, + description: ULTICAL_PRODUCT.description, +}) + +export default function UlticalProductPage() { + return +} diff --git a/app/suite/ulticards/page.tsx b/app/suite/ulticards/page.tsx new file mode 100644 index 0000000..eb71119 --- /dev/null +++ b/app/suite/ulticards/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next" +import { ProductPageShell } from "@/components/landing/product/product-page-shell" +import { ULTICARDS_PRODUCT } from "@/components/landing/product/product-data" +import { ULTICARDS_APP_NAME, suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = suitePageMetadata({ + app: "contacts", + title: `${ULTICARDS_APP_NAME} — Carnet d'adresses souverain`, + description: ULTICARDS_PRODUCT.description, +}) + +export default function UlticardsProductPage() { + return +} diff --git a/app/suite/ultidrive/page.tsx b/app/suite/ultidrive/page.tsx new file mode 100644 index 0000000..7aebfdc --- /dev/null +++ b/app/suite/ultidrive/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next" +import { ProductPageShell } from "@/components/landing/product/product-page-shell" +import { ULTIDRIVE_PRODUCT } from "@/components/landing/product/product-data" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = suitePageMetadata({ + app: "drive", + title: "UltiDrive — Stockage & documents souverains", + description: ULTIDRIVE_PRODUCT.description, +}) + +export default function UltidriveProductPage() { + return +} diff --git a/app/suite/ultimail/page.tsx b/app/suite/ultimail/page.tsx new file mode 100644 index 0000000..fd60158 --- /dev/null +++ b/app/suite/ultimail/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next" +import { ProductPageShell } from "@/components/landing/product/product-page-shell" +import { ULTIMAIL_PRODUCT } from "@/components/landing/product/product-data" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = suitePageMetadata({ + app: "mail", + title: "Ultimail — Messagerie unifiée souveraine", + description: ULTIMAIL_PRODUCT.description, +}) + +export default function UltimailProductPage() { + return +} diff --git a/app/suite/ultimeet/page.tsx b/app/suite/ultimeet/page.tsx new file mode 100644 index 0000000..c0e5ca3 --- /dev/null +++ b/app/suite/ultimeet/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next" +import { ProductPageShell } from "@/components/landing/product/product-page-shell" +import { ULTIMEET_PRODUCT } from "@/components/landing/product/product-data" +import { suitePageMetadata } from "@/lib/suite/page-metadata" + +export const metadata: Metadata = suitePageMetadata({ + app: "meet", + title: "UltiMeet — Visioconférence chiffrée souveraine", + description: ULTIMEET_PRODUCT.description, +}) + +export default function UltimeetProductPage() { + return +} diff --git a/components/admin/settings/sections/migration-projects-panel.tsx b/components/admin/settings/sections/migration-projects-panel.tsx index e577132..7d2f3b2 100644 --- a/components/admin/settings/sections/migration-projects-panel.tsx +++ b/components/admin/settings/sections/migration-projects-panel.tsx @@ -46,11 +46,11 @@ import { useRetryMigrationFailedJobs, useRetryMigrationJob, } from "@/lib/api/hooks/use-hosted-mail" -import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" +import { ULTICAL_APP_NAME, ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" const SERVICE_LABELS: Record = { mail: "Mail", - contacts: "Contacts", + contacts: ULTICARDS_APP_NAME, calendar: ULTICAL_APP_NAME, drive: "Drive", } diff --git a/components/admin/settings/sections/plugins-section.tsx b/components/admin/settings/sections/plugins-section.tsx index 65f79a6..4a8763e 100644 --- a/components/admin/settings/sections/plugins-section.tsx +++ b/components/admin/settings/sections/plugins-section.tsx @@ -31,7 +31,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" -import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" +import { ULTICAL_APP_NAME, ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" import { cn } from "@/lib/utils" const SIMPLE_PLUGIN_IDS = new Set(["mail-automation", "contact-discovery", "public-share"]) @@ -268,7 +268,7 @@ function NextcloudPluginCard() { onChange={(calendar_enabled) => setNextcloud({ calendar_enabled })} /> setNextcloud({ contacts_enabled })} /> diff --git a/components/demo/demo-chrome.tsx b/components/demo/demo-chrome.tsx index e2c9c4a..c3820c9 100644 --- a/components/demo/demo-chrome.tsx +++ b/components/demo/demo-chrome.tsx @@ -25,7 +25,7 @@ export function DemoChrome({ "max-sm:pt-1" )} > - + Démo interactive — zéro rétention diff --git a/components/demo/demo-drive-shell.tsx b/components/demo/demo-drive-shell.tsx index 2c2c8bb..a2432e3 100644 --- a/components/demo/demo-drive-shell.tsx +++ b/components/demo/demo-drive-shell.tsx @@ -5,6 +5,7 @@ import { DriveAppShell } from "@/components/drive/drive-app-shell" import { DemoChrome } from "@/components/demo/demo-chrome" import { DemoDriveProvider } from "@/lib/demo/demo-drive-context" import { DemoDriveBootstrap } from "@/lib/demo/demo-drive-bootstrap" +import { DemoDriveLayoutPreviewBootstrap } from "@/lib/demo/demo-drive-layout-preview-bootstrap" import { DEMO_DRIVE_ROUTE_ROOT } from "@/lib/demo/demo-drive-context" import { useDemoDriveStore } from "@/lib/demo/demo-drive-store" @@ -12,6 +13,7 @@ export function DemoDriveShell({ children }: { children: ReactNode }) { return ( useDemoDriveStore.getState().reset()}> + {children} diff --git a/components/demo/demo-mail-shell.tsx b/components/demo/demo-mail-shell.tsx index 6591509..2084120 100644 --- a/components/demo/demo-mail-shell.tsx +++ b/components/demo/demo-mail-shell.tsx @@ -5,12 +5,14 @@ import { MailAppShell } from "@/app/mail/mail-app-shell" import { DemoChrome } from "@/components/demo/demo-chrome" import { DemoMailProvider } from "@/lib/demo/demo-mail-context" import { DemoMailBootstrap } from "@/lib/demo/demo-mail-bootstrap" +import { DemoMailPreviewBootstrap } from "@/lib/demo/demo-mail-preview-bootstrap" import { useDemoMailStore } from "@/lib/demo/demo-mail-store" export function DemoMailShell({ children }: { children: ReactNode }) { return ( useDemoMailStore.getState().reset()}> + {children} diff --git a/components/drive/drive-app-shell.tsx b/components/drive/drive-app-shell.tsx index 1327527..9ac450a 100644 --- a/components/drive/drive-app-shell.tsx +++ b/components/drive/drive-app-shell.tsx @@ -7,6 +7,7 @@ import { FilePreviewDialog } from "@/components/drive/file-preview-dialog" import { ShareDialog } from "@/components/drive/share-dialog" import { SuiteThemeShell } from "@/components/suite/suite-theme-shell" import { useIsMobile } from "@/hooks/use-mobile" +import { getDemoDriveLayoutPreview } from "@/lib/demo/demo-drive-layout-preview" import { DriveRouteRootProvider } from "@/lib/drive/drive-route-context" import { useDriveUIStore } from "@/lib/stores/drive-ui-store" @@ -18,17 +19,20 @@ export function DriveAppShell({ routeRoot?: string }) { const isMobile = useIsMobile() + const layoutPreview = getDemoDriveLayoutPreview() const sidebarCollapsed = useDriveUIStore((s) => s.sidebarCollapsed) const setSidebarCollapsed = useDriveUIStore((s) => s.setSidebarCollapsed) const sidebarOpen = !sidebarCollapsed useLayoutEffect(() => { + if (layoutPreview) return if (!isMobile) setSidebarCollapsed(false) - }, [isMobile, setSidebarCollapsed]) + }, [isMobile, layoutPreview, setSidebarCollapsed]) useEffect(() => { + if (layoutPreview) return if (isMobile) setSidebarCollapsed(true) - }, [isMobile, setSidebarCollapsed]) + }, [isMobile, layoutPreview, setSidebarCollapsed]) return ( diff --git a/components/gmail/calendar-invitation-preview.tsx b/components/gmail/calendar-invitation-preview.tsx index 52deddd..8f7b136 100644 --- a/components/gmail/calendar-invitation-preview.tsx +++ b/components/gmail/calendar-invitation-preview.tsx @@ -48,9 +48,12 @@ const RSVP_SECONDARY = export function CalendarInvitationPreview({ invitation, className, + calendarAppName = "Agenda", }: { invitation: ParsedCalendarInvitation className?: string + /** Nom de l'app calendrier affiché dans le bouton « Ouvrir dans … ». */ + calendarAppName?: string }) { ensureVcLogosCollection() @@ -122,7 +125,7 @@ export function CalendarInvitationPreview({ href={`/agenda/day/${format(invitation.start, "yyyy-MM-dd")}`} className={RSVP_SECONDARY} > - Ouvrir dans Agenda + Ouvrir dans {calendarAppName} ) } diff --git a/components/gmail/contacts/contacts-panel.tsx b/components/gmail/contacts/contacts-panel.tsx index d6063a2..30f41af 100644 --- a/components/gmail/contacts/contacts-panel.tsx +++ b/components/gmail/contacts/contacts-panel.tsx @@ -2,6 +2,7 @@ import { useEffect, useCallback } from "react" import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet" +import { ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" import { useContactsStore } from "@/lib/contacts/contacts-store" import { ContactsListView } from "./contacts-list-view" import { ContactFormView } from "./contact-form-view" @@ -40,7 +41,7 @@ export function ContactsPanel() { data-contacts-panel className="w-[360px] sm:max-w-[360px] gap-0 border-border bg-mail-surface p-0 text-foreground" > - Contacts + {ULTICARDS_APP_NAME} {view === "list" && } {view === "view" && } {view === "create" && } diff --git a/components/gmail/email-list/email-list-body.tsx b/components/gmail/email-list/email-list-body.tsx index 9a52adf..525744e 100644 --- a/components/gmail/email-list/email-list-body.tsx +++ b/components/gmail/email-list/email-list-body.tsx @@ -5,7 +5,10 @@ import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { MailFolderStackIndicator } from "@/components/gmail/mail-folder-stack-indicator" import { mailNavVisitKey } from "@/lib/mail-folder-display" -import { MAIL_LIST_ROW_DIVIDER_CLASS } from "@/lib/mail-chrome-classes" +import { + MAIL_LIST_MAIN_SCROLL_CLASS, + MAIL_LIST_ROW_DIVIDER_CLASS, +} from "@/lib/mail-chrome-classes" import { PULL_HOLD_HEIGHT, REFRESH_SPIN_CLASS, @@ -21,15 +24,6 @@ import type { EmailListLabels } from "@/components/gmail/email-list/hooks/use-em import type { EmailListSelection } from "@/components/gmail/email-list/hooks/use-email-list-selection" import type { EmailListReading } from "@/components/gmail/email-list/hooks/use-email-list-reading" -const MAIN_SCROLL_CLASS = - "min-h-0 flex-1 overflow-y-auto overflow-x-hidden border-0 bg-mail-surface shadow-none outline-none sm:rounded-b-2xl " + - "[scrollbar-color:#9aa0a6_#ffffff] [scrollbar-width:auto] " + - "[&::-webkit-scrollbar]:w-2.5 [&::-webkit-scrollbar]:border-0 [&::-webkit-scrollbar]:bg-white " + - "[&::-webkit-scrollbar-track]:border-0 [&::-webkit-scrollbar-track]:bg-white [&::-webkit-scrollbar-track]:shadow-none " + - "[&::-webkit-scrollbar-thumb]:rounded-none [&::-webkit-scrollbar-thumb]:border-0 [&::-webkit-scrollbar-thumb]:shadow-none " + - "[&::-webkit-scrollbar-thumb]:bg-[#9aa0a6] hover:[&::-webkit-scrollbar-thumb]:bg-[#5f6368] " + - "[&::-webkit-scrollbar-corner]:border-0 [&::-webkit-scrollbar-corner]:bg-white" - type EmailListBodyProps = { data: EmailListData labels: EmailListLabels @@ -148,7 +142,7 @@ export function EmailListBody({ "max-sm:pb-16", !splitView && isViewMode && openEmail ? "relative flex min-h-0 flex-1 flex-col overflow-hidden" - : MAIN_SCROLL_CLASS, + : MAIL_LIST_MAIN_SCROLL_CLASS, "relative min-h-0 flex-1 overscroll-y-none" )} > diff --git a/components/gmail/email-list/hooks/use-email-list-reading.ts b/components/gmail/email-list/hooks/use-email-list-reading.ts index 9e9f970..3467c60 100644 --- a/components/gmail/email-list/hooks/use-email-list-reading.ts +++ b/components/gmail/email-list/hooks/use-email-list-reading.ts @@ -97,6 +97,11 @@ export function useEmailListReading( // Guard: emailById/setReadOverrides get new refs after each messages refetch — without // this guard, mark-read → invalidate → refetch → effect re-runs in a loop. const readAppliedForMailRef = useRef(null) + const splitViewScrollTargetRef = useRef({ + openMailId: null as string | null, + listPage: 1, + hadOpenRow: false, + }) useEffect(() => { if (!openMailId) { @@ -482,14 +487,38 @@ export function useEmailListReading( ) useLayoutEffect(() => { - if (!splitView || !openMailId) return + if (!splitView || !openMailId) { + splitViewScrollTargetRef.current = { + openMailId: null, + listPage, + hadOpenRow: false, + } + return + } + + const root = listViewportRef.current + const row = root?.querySelector( + `[data-email-row-id="${openMailId}"]` + ) + const rowInList = row != null + + const prev = splitViewScrollTargetRef.current + const openMailChanged = prev.openMailId !== openMailId + const pageChanged = prev.listPage !== listPage + const rowJustAppeared = rowInList && !prev.hadOpenRow + + splitViewScrollTargetRef.current = { + openMailId, + listPage, + hadOpenRow: rowInList, + } + + // Infinite scroll appends rows → listRowsDep changes; skip scroll unless the + // open mail changed, the page changed, or its row just entered the list. + if (!openMailChanged && !pageChanged && !rowJustAppeared) return + if (!row) return + const scrollActiveRowIntoView = () => { - const root = listViewportRef.current - if (!root) return - const row = root.querySelector( - `[data-email-row-id="${openMailId}"]` - ) - if (!row) return row.scrollIntoView({ block: "nearest", behavior: "smooth" }) } scrollActiveRowIntoView() diff --git a/components/gmail/email-view/email-view-messages.tsx b/components/gmail/email-view/email-view-messages.tsx index 4118e58..009b9b6 100644 --- a/components/gmail/email-view/email-view-messages.tsx +++ b/components/gmail/email-view/email-view-messages.tsx @@ -218,7 +218,7 @@ export function CollapsedMessage({ {name} -
+
{attachmentCount > 0 ? (
-
-
- +
+ +
{onToggleStar ? (
-
diff --git a/components/gmail/mail-date-text.tsx b/components/gmail/mail-date-text.tsx index 65e0713..c84d5e5 100644 --- a/components/gmail/mail-date-text.tsx +++ b/components/gmail/mail-date-text.tsx @@ -3,6 +3,8 @@ import { useEffect, useState } from "react" import { formatMailDate, + formatMailPreviewDatePrimary, + formatMailPreviewDateRelative, type MailDateDisplayVariant, } from "@/lib/mail-date" import { cn } from "@/lib/utils" @@ -13,18 +15,46 @@ type MailDateTextProps = { className?: string } +type PreviewDateLines = { + primary: string + relative: string | null +} + /** Date mail formatée côté client (fuseau navigateur, évite mismatch SSR). */ export function MailDateText({ iso, variant, className }: MailDateTextProps) { const [text, setText] = useState("\u00a0") + const [previewLines, setPreviewLines] = useState(null) useEffect(() => { if (!iso?.trim()) { setText("—") + setPreviewLines(null) return } + if (variant === "preview") { + setPreviewLines({ + primary: formatMailPreviewDatePrimary(iso), + relative: formatMailPreviewDateRelative(iso), + }) + setText("\u00a0") + return + } + setPreviewLines(null) setText(formatMailDate(iso, variant)) }, [iso, variant]) + if (variant === "preview" && previewLines) { + return ( + + {previewLines.primary} + {previewLines.relative ? {previewLines.relative} : null} + + ) + } + return ( {text} diff --git a/components/gmail/right-panel.tsx b/components/gmail/right-panel.tsx index 26eec13..57dde14 100644 --- a/components/gmail/right-panel.tsx +++ b/components/gmail/right-panel.tsx @@ -5,6 +5,7 @@ import { Calendar, Users, CheckSquare, Plus, Sparkles } from "lucide-react" import { Button } from "@/components/ui/button" import { useContactsStore } from "@/lib/contacts/contacts-store" import { useAiPanelStore } from "@/lib/ai/use-ai-panel" +import { ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" import { cn } from "@/lib/utils" export function RightPanel() { @@ -48,7 +49,7 @@ export function RightPanel() { panelOpen ? "bg-blue-100 text-[#1a73e8]" : "text-gray-600" )} onClick={togglePanel} - aria-label="Contacts" + aria-label={ULTICARDS_APP_NAME} > diff --git a/components/gmail/settings/automation/rule-simulator-panel.tsx b/components/gmail/settings/automation/rule-simulator-panel.tsx index cdacb42..67b8cb1 100644 --- a/components/gmail/settings/automation/rule-simulator-panel.tsx +++ b/components/gmail/settings/automation/rule-simulator-panel.tsx @@ -1,5 +1,6 @@ 'use client' +import { ULTICARDS_APP_NAME } from '@/lib/suite/page-metadata' import { useMemo, useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -113,7 +114,7 @@ export function RuleSimulatorPanel({ state, ruleId }: RuleSimulatorPanelProps) { {domains.includes('contacts') ? (
-

Contacts

+

{ULTICARDS_APP_NAME}

diff --git a/components/landing/landing-data.ts b/components/landing/landing-data.ts index 27fc1b7..f3c66bb 100644 --- a/components/landing/landing-data.ts +++ b/components/landing/landing-data.ts @@ -1,6 +1,44 @@ -import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" +import { ULTICAL_APP_NAME, ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" import { suitePublicAsset } from "@/lib/suite/suite-public-asset" +export type LandingAppGradient = { + a: string + b: string + c: string +} + +/** Dégradés de marque — CTA démo, hover cartes, halos. */ +export const LANDING_BRAND_GRADIENTS = { + mail: { a: "#EA4335", b: "#f2783c", c: "#c5221f" }, + drive: { a: "#34A853", b: "#1e8e3e", c: "#81c995" }, + ultical: { a: "#FBBC04", b: "#f9ab00", c: "#fdd663" }, + ulticards: { a: "#4285F4", b: "#1a73e8", c: "#669df6" }, + ultidocs: { a: "#34c77b", b: "#1fb6c9", c: "#2dd4bf" }, + ultiai: { a: "#f2783c", b: "#ea4335", c: "#ff9a56" }, + admin: { a: "#7C3AED", b: "#6D28D9", c: "#A78BFA" }, + ultimeet: { a: "#34A853", b: "#1e8e3e", c: "#81c995" }, +} as const satisfies Record + +export function landingAppGlowVars(gradient: LandingAppGradient) { + return { + "--landing-glow-a": gradient.a, + "--landing-glow-b": gradient.b, + "--landing-glow-c": gradient.c, + } +} + +/** Onglets démo — dégradé + texte sombre si besoin (jaune). */ +export const LANDING_DEMO_TAB_STYLES: Record< + string, + { gradient: LandingAppGradient; primaryTextDark?: boolean } +> = { + mail: { gradient: LANDING_BRAND_GRADIENTS.mail }, + drive: { gradient: LANDING_BRAND_GRADIENTS.drive }, + agenda: { gradient: LANDING_BRAND_GRADIENTS.ultical, primaryTextDark: true }, + contacts: { gradient: LANDING_BRAND_GRADIENTS.ulticards }, + docs: { gradient: LANDING_BRAND_GRADIENTS.ultidocs }, +} + export type LandingApp = { name: string tagline: string @@ -8,9 +46,12 @@ export type LandingApp = { icon: string iconDark?: string href?: string + /** Page produit marketing (ex. /suite/ultimail). */ + productHref?: string /** Application annoncée, pas encore disponible. */ soon?: boolean accent: string + gradient: LandingAppGradient } /** Onglet démo (#demo) associé à une app (dock hero, visiteur non connecté). */ @@ -29,7 +70,9 @@ export const LANDING_APPS: LandingApp[] = [ "Boîte unifiée multi-comptes, libellés intelligents, règles, envoi programmé et tri IA.", icon: suitePublicAsset("/ultimail-mark.svg"), href: "/mail", + productHref: "/suite/ultimail", accent: "#EA4335", + gradient: LANDING_BRAND_GRADIENTS.mail, }, { name: "UltiDrive", @@ -38,16 +81,20 @@ export const LANDING_APPS: LandingApp[] = [ "Stockage, partage par lien, documents texte, dessins et co-édition en temps réel.", icon: suitePublicAsset("/ultidrive-mark.svg"), href: "/drive", - accent: "#4285F4", + productHref: "/suite/ultidrive", + accent: "#34A853", + gradient: LANDING_BRAND_GRADIENTS.drive, }, { - name: "Contacts", + name: ULTICARDS_APP_NAME, tagline: "Carnet d'adresses", description: "Contacts unifiés sur toute la suite, synchronisés avec la messagerie et le partage.", icon: suitePublicAsset("/contacts-mark.svg"), href: "/contacts", + productHref: "/suite/ulticards", accent: "#4285F4", + gradient: LANDING_BRAND_GRADIENTS.ulticards, }, { name: "UltiAI", @@ -56,7 +103,9 @@ export const LANDING_APPS: LandingApp[] = [ "Assistant connecté à vos mails et fichiers, fournisseurs OpenAI-compatibles, quotas maîtrisés.", icon: suitePublicAsset("/ultiai-mark.svg"), href: "/chat", + productHref: "/suite/ultiai", accent: "#f2783c", + gradient: LANDING_BRAND_GRADIENTS.ultiai, }, { name: "Administration", @@ -64,8 +113,10 @@ export const LANDING_APPS: LandingApp[] = [ description: "Gestion de l'organisation, SSO, déploiement, quotas IA et réglages centralisés.", icon: suitePublicAsset("/admin-mark.svg"), - href: "/admin/settings", - accent: "#5a6172", + href: "/admin", + productHref: "/suite/administration", + accent: "#7C3AED", + gradient: LANDING_BRAND_GRADIENTS.admin, }, { name: ULTICAL_APP_NAME, @@ -75,7 +126,9 @@ export const LANDING_APPS: LandingApp[] = [ icon: suitePublicAsset("/agenda-mark.svg"), iconDark: suitePublicAsset("/agenda-mark-dark.svg"), href: "/agenda", - accent: "#34c77b", + productHref: "/suite/ultical", + accent: "#FBBC04", + gradient: LANDING_BRAND_GRADIENTS.ultical, }, { name: "UltiMeet", @@ -84,7 +137,9 @@ export const LANDING_APPS: LandingApp[] = [ "Réunions vidéo chiffrées dans le navigateur, auto-hébergées via Jitsi et liées à UltiCal.", icon: suitePublicAsset("/ultimeet-mark.svg"), href: "/meet", + productHref: "/suite/ultimeet", accent: "#34A853", + gradient: LANDING_BRAND_GRADIENTS.ultimeet, }, { name: "Photos", @@ -94,6 +149,7 @@ export const LANDING_APPS: LandingApp[] = [ icon: suitePublicAsset("/photos-mark.svg"), soon: true, accent: "#FBBC04", + gradient: LANDING_BRAND_GRADIENTS.ultical, }, ] diff --git a/components/landing/landing-demo.tsx b/components/landing/landing-demo.tsx index 256f06d..c334d13 100644 --- a/components/landing/landing-demo.tsx +++ b/components/landing/landing-demo.tsx @@ -3,7 +3,11 @@ import { useEffect, useRef, useState } from "react" import { Icon } from "@iconify/react" import { LandingReveal } from "@/components/landing/landing-reveal" -import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" +import { + LANDING_DEMO_TAB_STYLES, + landingAppGlowVars, +} from "@/components/landing/landing-data" +import { ULTICAL_APP_NAME, ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" import { cn } from "@/lib/utils" type DemoTab = { @@ -42,7 +46,7 @@ const DEMO_TABS: DemoTab[] = [ }, { id: "contacts", - label: "Contacts", + label: ULTICARDS_APP_NAME, icon: "mdi:account-group-outline", src: "/demo/contacts", fakeUrl: "suite.votre-domaine.fr/contacts", @@ -133,23 +137,33 @@ export function LandingDemoSection({ {/* Onglets */}
- {DEMO_TABS.map((tab) => ( - - ))} + {DEMO_TABS.map((tab) => { + const selected = tab.id === activeTab + const tabStyle = LANDING_DEMO_TAB_STYLES[tab.id] + return ( + + ) + })}
{/* Fenêtre virtuelle */} diff --git a/components/landing/landing-sections.tsx b/components/landing/landing-sections.tsx index 035a023..cd2af7b 100644 --- a/components/landing/landing-sections.tsx +++ b/components/landing/landing-sections.tsx @@ -1,5 +1,6 @@ "use client" +import { ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" import Link from "next/link" import { Icon } from "@iconify/react" import { LandingReveal } from "@/components/landing/landing-reveal" @@ -7,6 +8,7 @@ import { LANDING_APPS, LANDING_FEATURES, LANDING_INTEGRATIONS, + landingAppGlowVars, } from "@/components/landing/landing-data" import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity" import { getAuthentikEnrollmentUrl } from "@/lib/auth/oidc-config" @@ -63,6 +65,7 @@ export function LandingAppsSection() { "landing-glass landing-halo-card flex h-full flex-col gap-3 rounded-2xl p-5", app.soon && "opacity-75" )} + style={landingAppGlowVars(app.gradient) as React.CSSProperties} >
{app.href && !app.soon ? ( - + {card} ) : ( @@ -364,7 +367,7 @@ export function LandingFooter() { UltiDrive - Contacts + {ULTICARDS_APP_NAME} UltiAI diff --git a/components/landing/product/product-cross-platform-section.tsx b/components/landing/product/product-cross-platform-section.tsx new file mode 100644 index 0000000..855768e --- /dev/null +++ b/components/landing/product/product-cross-platform-section.tsx @@ -0,0 +1,583 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import { Icon } from "@iconify/react" +import { LandingReveal } from "@/components/landing/landing-reveal" +import { ProductSectionHeading } from "@/components/landing/product/product-section-heading" +import type { ProductCrossPlatformSection as ProductCrossPlatformSectionData } from "@/components/landing/product/product-data" +import { + demoMailPreviewSrc, + type DemoMailPreviewLayout, +} from "@/lib/demo/demo-mail-preview" +import { + demoDriveLayoutPreviewSrc, + type DemoDriveLayoutPreview, +} from "@/lib/demo/demo-drive-layout-preview" +import { ScaledPreviewIframe } from "@/components/landing/product/scaled-preview-iframe" +import { cn } from "@/lib/utils" + +export type ProductCrossPlatformDemoApp = "mail" | "drive" | "meet" + +type DeviceLayout = DemoMailPreviewLayout | DemoDriveLayoutPreview + +const STAGE_DURATION_MS = 4200 +/** Hauteur fixe du socle — évite le saut de layout entre mobile / tablette / bureau. */ +const SHOWCASE_HEIGHT_PX = 620 +/** Padding bezel DeviceShell (`p-2`). */ +const DEVICE_BEZEL_PX = 8 +const MONITOR_STAND_PX = 20 + +type DeviceStage = { + id: DeviceLayout + label: string + platform: string + icon: string + /** Viewport logique rendu dans l'iframe (media queries Ultimail). */ + viewportWidth: number + viewportHeight: number + /** Largeur du cadre affiché sur la page (px). La hauteur est dérivée du ratio. */ + frameWidth: number + radius: number + notch?: boolean + monitor?: boolean +} + +const DEVICE_STAGES: DeviceStage[] = [ + { + id: "phone", + label: "Mobile", + platform: "iOS & Android", + icon: "mdi:cellphone", + viewportWidth: 390, + viewportHeight: 844, + frameWidth: 264, + radius: 40, + notch: true, + }, + { + id: "tablet", + label: "Tablette", + platform: "iPad paysage", + icon: "mdi:tablet", + viewportWidth: 1024, + viewportHeight: 768, + frameWidth: 520, + radius: 24, + }, + { + id: "desktop", + label: "Bureau", + platform: "Écran 4:3", + icon: "mdi:monitor", + viewportWidth: 1280, + viewportHeight: 960, + frameWidth: 640, + radius: 12, + monitor: true, + }, +] + +function stageScale(stage: DeviceStage) { + return stage.frameWidth / stage.viewportWidth +} + +function statusBarHeight(stage: DeviceStage) { + if (!stage.notch) return 0 + const scale = stageScale(stage) + return Math.max(24, Math.round(32 * scale)) +} + +/** Hauteur de la zone iframe — ratio identique au viewport (aucune coupe). */ +function contentAreaHeight(stage: DeviceStage) { + return Math.round((stage.frameWidth * stage.viewportHeight) / stage.viewportWidth) +} + +/** Hauteur totale du cadre = zone iframe + barre de statut éventuelle. */ +function frameHeight(stage: DeviceStage) { + return contentAreaHeight(stage) + statusBarHeight(stage) +} + +function IosBatteryIcon({ size }: { size: number }) { + const width = Math.round(size * 2.1) + return ( + + + + + + ) +} + +function DeviceStatusBar({ stage }: { stage: DeviceStage }) { + if (!stage.notch) return null + + const scale = stageScale(stage) + const height = statusBarHeight(stage) + const fontSize = Math.max(9, Math.round(11 * scale)) + const iconSize = Math.max(9, Math.round(11 * scale)) + const cornerRadius = Math.max(stage.radius - 10, 10) + /** Inset coins arrondis — plus à droite (courbure visible). */ + const padLeft = Math.max(22, Math.round(28 * scale)) + const padRight = Math.max(36, Math.round(cornerRadius * 1.12)) + + return ( +
+ 9:41 +
+ + + +
+
+ ) +} + +function deviceOuterSize(stage: DeviceStage) { + return { + width: stage.frameWidth + DEVICE_BEZEL_PX * 2, + height: + frameHeight(stage) + + DEVICE_BEZEL_PX * 2 + + (stage.monitor ? MONITOR_STAND_PX : 0), + } +} + +function ScaledProductDemoIframe({ + stage, + ready, + active, + demoApp, +}: { + stage: DeviceStage + ready: boolean + active: boolean + demoApp: ProductCrossPlatformDemoApp +}) { + const demoSrc = + demoApp === "drive" + ? demoDriveLayoutPreviewSrc(stage.id) + : demoMailPreviewSrc(stage.id) + + const title = + demoApp === "drive" + ? `UltiDrive — vue ${stage.label}` + : `Ultimail — vue ${stage.label}` + + const iframe = ( + + ) + + // Sans encoche (tablette / bureau) : l'iframe remplit directement la boîte + // écran en absolute inset-0 — même structure que la démo docs (qui est nette), + // sans chaîne flex qui introduirait des sous-pixels (bandes au bord). + if (!stage.notch) { + return ( +
+ {iframe} +
+ ) + } + + // Avec encoche (mobile) : barre de statut en haut, iframe en dessous. + return ( +
+
+ +
+
+ {iframe} +
+
+ ) +} + +const MEET_PEOPLE = [ + { name: "Léa", color: "#34A853" }, + { name: "Vincent", color: "#4285F4" }, + { name: "Thomas", color: "#EA4335" }, + { name: "Camille", color: "#FBBC04" }, + { name: "Sofia", color: "#A142F4" }, + { name: "Karim", color: "#00ACC1" }, +] as const + +const MEET_CONTROLS = ["mdi:microphone", "mdi:video", "mdi:monitor-share", "mdi:message-outline"] as const + +/** Visio placeholder statique (pas d'iframe) rendu dans le device frame pour UltiMeet. */ +function MeetDevicePlaceholder({ + stage, + accent, +}: { + stage: DeviceStage + accent: string +}) { + const cols = stage.id === "phone" ? 2 : 3 + const people = stage.id === "phone" ? MEET_PEOPLE.slice(0, 4) : MEET_PEOPLE + const avatarSize = + stage.id === "desktop" ? "size-14" : stage.id === "tablet" ? "size-12" : "size-9" + const buttonSize = stage.id === "phone" ? "size-7" : "size-9" + const iconSize = stage.id === "phone" ? "size-4" : "size-5" + + return ( +
+
+ + Atelier Nord — point hebdo + 12:47 +
+ +
+ {people.map((person, index) => ( +
+
+ + {person.name.charAt(0)} + + + + {person.name} + +
+ ))} +
+ +
+ {MEET_CONTROLS.map((icon) => ( + + + + ))} + + + +
+
+ ) +} + +function DeviceShell({ + stage, + children, +}: { + stage: DeviceStage + children: React.ReactNode +}) { + if (stage.monitor) { + return ( +
+
+
+ {children} +
+
+
+
+ ) + } + + return ( +
+ {stage.notch ? ( +
+ ) : null} +
+ {children} +
+
+ ) +} + +function MorphingDeviceShowcase({ + accent, + demoApp, +}: { + accent: string + demoApp: ProductCrossPlatformDemoApp +}) { + const sectionRef = useRef(null) + const [visible, setVisible] = useState(false) + const [stageIndex, setStageIndex] = useState(0) + const [autoPlay, setAutoPlay] = useState(true) + const [reduceMotion, setReduceMotion] = useState(false) + + const stage = DEVICE_STAGES[stageIndex]! + + useEffect(() => { + setReduceMotion( + typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches + ) + }, []) + + useEffect(() => { + const node = sectionRef.current + if (!node) return + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setVisible(true) + observer.disconnect() + } + }, + { rootMargin: "120px 0px" } + ) + observer.observe(node) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!visible || reduceMotion || !autoPlay) return + const timer = window.setInterval(() => { + setStageIndex((current) => (current + 1) % DEVICE_STAGES.length) + }, STAGE_DURATION_MS) + return () => window.clearInterval(timer) + }, [visible, reduceMotion, autoPlay]) + + return ( +
+
+ {DEVICE_STAGES.map((item, index) => { + const active = index === stageIndex + const outer = deviceOuterSize(item) + return ( +
+ + {demoApp === "meet" ? ( + + ) : ( + + )} + +
+ ) + })} +
+ +
+ {DEVICE_STAGES.map((item, index) => { + const active = index === stageIndex + return ( + + ) + })} +
+ +

+ {stage.platform} + {" — "} + même interface, adaptée à la taille de l'écran +

+
+ ) +} + +export function ProductCrossPlatformSection({ + section, + accent, + demoApp = "mail", +}: { + section: ProductCrossPlatformSectionData + accent: string + demoApp?: ProductCrossPlatformDemoApp +}) { + return ( +
+
+ + +
+ +
    + {section.features.map((feature) => ( +
  • + + + +
    +

    {feature.title}

    +

    + {feature.description} +

    +
    +
  • + ))} +
+
+ + + + +
+
+
+ ) +} diff --git a/components/landing/product/product-cta.tsx b/components/landing/product/product-cta.tsx new file mode 100644 index 0000000..d50b9c3 --- /dev/null +++ b/components/landing/product/product-cta.tsx @@ -0,0 +1,53 @@ +"use client" + +import Link from "next/link" +import { Icon } from "@iconify/react" +import { LandingReveal } from "@/components/landing/landing-reveal" +import type { ProductPageData } from "@/components/landing/product/product-data" + +export function ProductCta({ + section, + accent, +}: { + section: ProductPageData["ctaSection"] + accent: string +}) { + return ( +
+
+ +

+ {section.title} +

+

+ {section.description} +

+
+ + {section.ctas.primary.label} + + + {section.ctas.secondary ? ( + + {section.ctas.secondary.label} + + ) : null} +
+
+
+
+ ) +} diff --git a/components/landing/product/product-data.tsx b/components/landing/product/product-data.tsx new file mode 100644 index 0000000..a72c96d --- /dev/null +++ b/components/landing/product/product-data.tsx @@ -0,0 +1,2023 @@ +import type { ReactNode } from "react" +import { ULTICAL_APP_NAME, ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata" +import { suitePublicAsset } from "@/lib/suite/suite-public-asset" + +export type ProductFeature = { + title: string + description: string + icon: string +} + +export type ProductShowcaseDemo = + | "ultimail-inbox" + | "ultimail-compose" + | "ultimail-automation" + | "ultidrive-browser" + | "ultidrive-docs" + | "ultidrive-share" + | "ultical-agenda" + | "ultical-invitations" + | "ultical-scheduling" + | "ultiai-chat" + | "ultiai-tools" + | "ultiai-triage" + | "ulticards-directory" + | "ulticards-discovery" + | "ulticards-merge" + | "ultimeet-room" + | "ultimeet-lobby" + | "ultimeet-collab" + | "admin-migration" + | "admin-identity" + | "admin-users" + | "admin-quotas" + | "admin-policies" + +export type ProductShowcase = { + id: string + eyebrow: string + title: ReactNode + description?: string + features: ProductFeature[] + demo: ProductShowcaseDemo + reverse?: boolean +} + +/** @deprecated Préférer `showcases` pour les pages produit. */ +export type ProductFeatureGroup = { + eyebrow: string + title: ReactNode + description?: string + features: (ProductFeature & { wide?: boolean })[] +} + +export type ProductHighlight = { + value: string + label: string +} + +export type ProductIntegration = { + name: string + tagline: string + description: string + icon: string + href?: string + accent: string +} + +export type ProductCtaLink = { + label: string + href: string +} + +export type ProductCrossPlatformSection = { + eyebrow: string + title: ReactNode + description: string + features: ProductFeature[] +} + +export type ProductInteropProvider = { + name: string + tagline: string + icon: string + accent: string + /** Icône logos:* Iconify — couleurs d'origine, sans teinte accent. */ + brandLogo?: boolean + personal: string + enterprise: string +} + +export type ProductInteropSection = { + eyebrow: string + title: ReactNode + description: string + providers: ProductInteropProvider[] + features: ProductFeature[] +} + +export type ProductCrossPlatformDemoApp = "mail" | "drive" | "meet" + +export type ProductPageData = { + name: string + tagline: string + description: string + icon: string + accent: string + heroEyebrow: string + heroTitle: string + heroTitleAccent: string + ctas: { + primary: ProductCtaLink + secondary?: ProductCtaLink + } + highlightsSection: { + eyebrow: string + title: ReactNode + description: string + highlights: ProductHighlight[] + } + showcases: ProductShowcase[] + crossPlatformSection?: ProductCrossPlatformSection + crossPlatformDemo?: ProductCrossPlatformDemoApp + interopSection?: ProductInteropSection + /** Grille classique — fallback si pas de showcases. */ + featureGroups?: ProductFeatureGroup[] + integrations: ProductIntegration[] + ctaSection: { + title: ReactNode + description: string + ctas: { + primary: ProductCtaLink + secondary?: ProductCtaLink + } + } +} + +export const ULTIMAIL_PRODUCT: ProductPageData = { + name: "Ultimail", + tagline: "Messagerie", + description: + "Boîte unifiée multi-comptes, libellés intelligents, règles de tri, envoi programmé et tri IA — l'alternative souveraine à Gmail, hébergée chez vous.", + icon: suitePublicAsset("/ultimail-mark.svg"), + accent: "#EA4335", + heroEyebrow: "Messagerie souveraine", + heroTitle: "Tous vos comptes mail,", + heroTitleAccent: "une seule boîte.", + ctas: { + primary: { label: "Ouvrir Ultimail", href: "/mail/inbox" }, + secondary: { label: "Essayer la démo", href: "/#demo" }, + }, + highlightsSection: { + eyebrow: "Pourquoi Ultimail", + title: ( + <> + Au-delà de Gmail,{" "} + sans quitter vos habitudes + + ), + description: + "Interface familière, automatisations avancées et IA maîtrisée — sans céder vos données.", + highlights: [ + { value: "N→1", label: "comptes dans une boîte unifiée" }, + { value: "∞", label: "libellés cross-comptes" }, + { value: "IA", label: "tri LLM & agents MCP" }, + { value: "0", label: "tracker ou télémétrie" }, + ], + }, + showcases: [ + { + id: "boite-unifiee", + eyebrow: "Boîte unifiée", + title: ( + <> + Multi-comptes,{" "} + une seule vue + + ), + description: + "IMAP/SMTP, libellés unifiés et recherche cross-comptes — tout au même endroit.", + demo: "ultimail-inbox", + features: [ + { + title: "Comptes IMAP & SMTP", + description: "Plusieurs boîtes, synchronisation permanente côté serveur.", + icon: "mdi:email-sync-outline", + }, + { + title: "Identités & catch-all", + description: "Alias, routage et adresses d'envoi distinctes du compte SMTP.", + icon: "mdi:account-badge-outline", + }, + { + title: "Libellés & dossiers unifiés", + description: "Organisez tous vos comptes avec les mêmes libellés et dossiers.", + icon: "mdi:label-multiple-outline", + }, + { + title: "Recherche avancée", + description: "Expéditeur, sujet, PJ — une requête traverse toutes vos boîtes.", + icon: "mdi:file-search-outline", + }, + ], + }, + { + id: "composition", + eyebrow: "Composition & envoi", + title: ( + <> + Rédigez, programmez,{" "} + envoyez + + ), + description: "Éditeur riche, signatures par identité et envoi différé.", + demo: "ultimail-compose", + reverse: true, + features: [ + { + title: "Éditeur riche TipTap", + description: "Mise en forme complète, rendu HTML compatible clients mail.", + icon: "mdi:format-text", + }, + { + title: "Brouillons", + description: "Reprenez vos messages en cours à tout moment.", + icon: "mdi:content-save-outline", + }, + { + title: "Envoi programmé", + description: "Planifiez l'envoi à l'heure qui vous convient.", + icon: "mdi:clock-outline", + }, + { + title: "Pièces jointes & signatures", + description: "PJ locales ou UltiDrive, signature par identité d'envoi.", + icon: "mdi:paperclip", + }, + ], + }, + { + id: "automatisations", + eyebrow: "Automatisations & IA", + title: ( + <> + Règles visuelles,{" "} + zéro code + + ), + description: + "Tri automatique, webhooks, tri LLM et tokens API pour vos agents.", + demo: "ultimail-automation", + features: [ + { + title: "Éditeur no-code", + description: "Déclencheurs, conditions et actions en flux visuel.", + icon: "mdi:filter-cog-outline", + }, + { + title: "Forward & réponses auto", + description: "Transferts et absences configurés par identité.", + icon: "mdi:email-fast-outline", + }, + { + title: "Webhooks à templates", + description: "Slack, Discord, n8n — JSON personnalisable à la réception.", + icon: "mdi:webhook", + }, + { + title: "Tri IA & agents MCP", + description: "LLM OpenAI-compatible et tokens à permissions fines.", + icon: "mdi:creation-outline", + }, + ], + }, + ], + crossPlatformSection: { + eyebrow: "Multi-plateforme", + title: ( + <> + Expérience cohérente,{" "} + même sur mobile + + ), + description: + "Apps iOS et Android natives (Tauri) : la même messagerie Ultimail, adaptée à chaque écran — du téléphone au bureau.", + features: [ + { + title: "Apps iOS & Android", + description: "Clients natifs Tauri, même code et mêmes fonctionnalités que le web.", + icon: "mdi:cellphone-link", + }, + { + title: "Interface responsive", + description: "Liste seule sur mobile, split-view sur tablette, colonnes sur desktop.", + icon: "mdi:responsive", + }, + { + title: "Gestes & raccourcis natifs", + description: "Navigation tactile, notifications push et intégration système.", + icon: "mdi:gesture-tap", + }, + { + title: "Sync unifiée", + description: "Même compte, mêmes libellés et brouillons sur tous vos appareils.", + icon: "mdi:sync", + }, + ], + }, + integrations: [ + { + name: ULTICARDS_APP_NAME, + tagline: "Contacts", + description: "Saisie assistée à la composition et fiches enrichies.", + icon: suitePublicAsset("/contacts-mark.svg"), + href: "/contacts", + accent: "#4285F4", + }, + { + name: "UltiDrive", + tagline: "Fichiers", + description: "PJ depuis le stockage ou partage par lien sécurisé.", + icon: suitePublicAsset("/ultidrive-mark.svg"), + href: "/suite/ultidrive", + accent: "#34A853", + }, + { + name: ULTICAL_APP_NAME, + tagline: "Calendrier", + description: "Invitations et disponibilités synchronisées.", + icon: suitePublicAsset("/agenda-mark.svg"), + href: "/agenda", + accent: "#34c77b", + }, + { + name: "UltiAI", + tagline: "Assistant IA", + description: + "Résumez, rédigez, envoyez ou transférez vos mails sur simple commande.", + icon: suitePublicAsset("/ultiai-mark.svg"), + href: "/chat", + accent: "#f2783c", + }, + ], + ctaSection: { + title: ( + <> + Prêt à unifier{" "} + votre messagerie ? + + ), + description: + "Rattachez vos comptes existants et découvrez une boîte souveraine, sans renoncer à l'ergonomie Gmail.", + ctas: { + primary: { label: "Ouvrir Ultimail", href: "/mail/inbox" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + }, +} + +export const ULTIDRIVE_PRODUCT: ProductPageData = { + name: "UltiDrive", + tagline: "Fichiers & docs", + description: + "Stockage souverain, interface familière type Google Drive, documents texte, bureautique OnlyOffice, partage par lien et montages cloud — hébergé chez vous.", + icon: suitePublicAsset("/ultidrive-mark.svg"), + accent: "#34A853", + heroEyebrow: "Stockage souverain", + heroTitle: "Vos fichiers,", + heroTitleAccent: "sous votre contrôle.", + ctas: { + primary: { label: "Ouvrir UltiDrive", href: "/drive" }, + secondary: { label: "Essayer la démo", href: "/#demo" }, + }, + highlightsSection: { + eyebrow: "Pourquoi UltiDrive", + title: ( + <> + L'ergonomie de Google Drive,{" "} + sans Google + + ), + description: + "Explorateur familier, édition collaborative et partage fin — sur votre infrastructure Nextcloud ou WebDAV.", + highlights: [ + { value: "∞", label: "stockage selon votre backend" }, + { value: "3", label: "éditeurs natifs (docs, office, draw)" }, + { value: "WebDAV", label: "montage local & sync" }, + { value: "0", label: "donnée chez un géant US" }, + ], + }, + showcases: [ + { + id: "explorateur", + eyebrow: "Explorateur de fichiers", + title: ( + <> + Mon Drive,{" "} + comme chez Google + + ), + description: + "Arborescence, favoris, récents, corbeille et recherche — branchés sur Nextcloud ou WebDAV.", + demo: "ultidrive-browser", + features: [ + { + title: "Navigation familière", + description: "Vues Mon Drive, Partagés, Favoris, Récents et Corbeille.", + icon: "mdi:folder-multiple-outline", + }, + { + title: "Upload & glisser-déposer", + description: "Déposez fichiers et dossiers, création de répertoires en un clic.", + icon: "mdi:cloud-upload-outline", + }, + { + title: "Prévisualisation intégrée", + description: "Images, PDF, texte et bureautique — sans quitter l'explorateur.", + icon: "mdi:file-eye-outline", + }, + { + title: "Recherche rapide", + description: "Filtrez par nom, type ou emplacement dans tout le drive.", + icon: "mdi:magnify", + }, + ], + }, + { + id: "documents", + eyebrow: "Documents & bureautique", + title: ( + <> + Rédigez, dessinez,{" "} + co-éditez + + ), + description: + "UltiDocs pour le texte riche, OnlyOffice pour docx/xlsx/pptx, UltiDraw pour les schémas.", + demo: "ultidrive-docs", + reverse: true, + features: [ + { + title: "UltiDocs", + description: "Éditeur riche TipTap — tableaux, images, export PDF.", + icon: "mdi:file-document-edit-outline", + }, + { + title: "OnlyOffice", + description: "Word, Excel et PowerPoint en co-édition temps réel.", + icon: "mdi:microsoft-office", + }, + { + title: "UltiDraw", + description: "Diagrammes et croquis vectoriels intégrés à la suite.", + icon: "mdi:draw", + }, + { + title: "Versioning", + description: "Historique des versions via le backend Nextcloud.", + icon: "mdi:history", + }, + ], + }, + { + id: "partage", + eyebrow: "Partage & collaboration", + title: ( + <> + Permissions simples,{" "} + liens sécurisés + + ), + description: + "Propriétaire, éditeur, lecteur — partage interne, lien public, mot de passe et expiration.", + demo: "ultidrive-share", + features: [ + { + title: "Partage interne", + description: "Utilisateurs et groupes de l'organisation, rôles hérités.", + icon: "mdi:account-group-outline", + }, + { + title: "Lien public", + description: "URL partageable, lecture ou écriture, expiration configurable.", + icon: "mdi:link-variant", + }, + { + title: "Protection par mot de passe", + description: "Sécurisez les liens sensibles avant diffusion externe.", + icon: "mdi:shield-lock-outline", + }, + { + title: "Montages cloud", + description: "Google Drive, Dropbox, OneDrive ou WebDAV org — dans le même explorateur.", + icon: "mdi:cloud-sync-outline", + }, + ], + }, + ], + crossPlatformSection: { + eyebrow: "Sync & montage", + title: ( + <> + Un drive local,{" "} + partout sur vos postes + + ), + description: + "WebDAV natif, montage disque via rclone et client desktop Tauri — vos fichiers restent accessibles hors ligne.", + features: [ + { + title: "WebDAV natif", + description: "Montez UltiDrive comme un disque réseau sur macOS, Windows et Linux.", + icon: "mdi:server-network", + }, + { + title: "Client desktop Tauri", + description: "Sync sélective et accès hors ligne depuis l'app UltiSuite.", + icon: "mdi:desktop-classic", + }, + { + title: "Apps mobile", + description: "Parcourez et partagez depuis iOS et Android — même UX que le web.", + icon: "mdi:cellphone-link", + }, + { + title: "Intégration mail", + description: "Enregistrez les PJ Ultimail directement dans votre drive.", + icon: "mdi:email-arrow-right-outline", + }, + ], + }, + crossPlatformDemo: "drive", + interopSection: { + eyebrow: "Interopérabilité", + title: ( + <> + Vos clouds existants,{" "} + dans le même explorateur + + ), + description: + "Montez vos dossiers personnels ou d'entreprise — Google Drive, OneDrive, Dropbox ou WebDAV — sans quitter UltiDrive.", + providers: [ + { + name: "Google Drive", + tagline: "Drive personnel & Workspace", + icon: "logos:google-drive", + accent: "#4285F4", + brandLogo: true, + personal: + "Connectez votre compte Google et parcourez Mon Drive depuis la barre latérale UltiDrive.", + enterprise: + "Montage Shared Drives (Workspace) pour accéder aux dossiers d'équipe sans migration.", + }, + { + name: "Microsoft OneDrive", + tagline: "OneDrive & SharePoint", + icon: "logos:microsoft-onedrive", + accent: "#0078D4", + brandLogo: true, + personal: + "Rattachez OneDrive perso — fichiers et albums visibles à côté de votre stockage Ulti.", + enterprise: + "OneDrive for Business et bibliothèques SharePoint montés pour les équipes.", + }, + { + name: "Dropbox", + tagline: "Dropbox & Dropbox Business", + icon: "logos:dropbox", + accent: "#0061FF", + brandLogo: true, + personal: + "OAuth Dropbox : votre espace perso apparaît comme un volume dans l'explorateur.", + enterprise: + "Dropbox Business — dossiers d'équipe et espaces partagés sans changer d'interface.", + }, + { + name: "WebDAV", + tagline: "NAS, Nextcloud externe, serveur org", + icon: "mdi:server-network", + accent: "#34A853", + personal: + "Serveur WebDAV perso (NAS Synology, Nextcloud auto-hébergé) monté en quelques clics.", + enterprise: + "Volume WebDAV partagé par l'admin — visible pour toute l'organisation dans UltiDrive.", + }, + ], + features: [ + { + title: "OAuth ou URL", + description: "Connexion guidée pour les clouds grand public, URL + identifiants pour WebDAV.", + icon: "mdi:key-link", + }, + { + title: "Vue unifiée", + description: "Mon Drive, montages perso et volumes org dans la même arborescence.", + icon: "mdi:folder-multiple-outline", + }, + { + title: "Permissions respectées", + description: "Droits hérités du service source — lecture seule ou écriture selon le montage.", + icon: "mdi:shield-check-outline", + }, + { + title: "Sans migration", + description: "Gardez vos fichiers chez le fournisseur d'origine, accédez-y depuis UltiDrive.", + icon: "mdi:cloud-sync-outline", + }, + ], + }, + integrations: [ + { + name: "Ultimail", + tagline: "Messagerie", + description: "Pièces jointes sauvegardées et partage par lien depuis le mail.", + icon: suitePublicAsset("/ultimail-mark.svg"), + href: "/suite/ultimail", + accent: "#EA4335", + }, + { + name: ULTICARDS_APP_NAME, + tagline: "Contacts", + description: "Partage par adresse e-mail avec autocomplétion contacts.", + icon: suitePublicAsset("/contacts-mark.svg"), + href: "/contacts", + accent: "#4285F4", + }, + { + name: ULTICAL_APP_NAME, + tagline: "Calendrier", + description: "Fichiers joints aux événements et invitations.", + icon: suitePublicAsset("/agenda-mark.svg"), + href: "/agenda", + accent: "#34c77b", + }, + { + name: "UltiAI", + tagline: "Assistant IA", + description: + "Résumez vos documents, créez-en et modifiez-les sur simple commande.", + icon: suitePublicAsset("/ultiai-mark.svg"), + href: "/chat", + accent: "#f2783c", + }, + ], + ctaSection: { + title: ( + <> + Prêt à reprendre le contrôle de{" "} + vos fichiers ? + + ), + description: + "Déployez UltiDrive sur votre infrastructure et offrez à vos équipes une expérience Drive familière, sans dépendance cloud US.", + ctas: { + primary: { label: "Ouvrir UltiDrive", href: "/drive" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + }, +} + +export const ULTICAL_PRODUCT: ProductPageData = { + name: ULTICAL_APP_NAME, + tagline: "Calendrier", + description: + "Calendrier partagé multi-agendas, invitations et RSVP liés au mail, disponibilités et prise de rendez-vous — l'alternative souveraine à Google Agenda, basée sur CalDAV et hébergée chez vous.", + icon: suitePublicAsset("/agenda-mark.svg"), + accent: "#FBBC04", + heroEyebrow: "Calendrier souverain", + heroTitle: "Tout votre temps,", + heroTitleAccent: "au même endroit.", + ctas: { + primary: { label: "Ouvrir UltiCal", href: "/agenda" }, + secondary: { label: "Essayer la démo", href: "/demo/agenda" }, + }, + highlightsSection: { + eyebrow: "Pourquoi UltiCal", + title: ( + <> + L'ergonomie de Google Agenda,{" "} + sans Google + + ), + description: + "Vues familières, invitations connectées au mail et standard CalDAV ouvert — sur votre infrastructure, sans céder vos données.", + highlights: [ + { value: "J/S/M", label: "vues jour, semaine, mois" }, + { value: "∞", label: "agendas partagés cross-comptes" }, + { value: "CalDAV", label: "standard ouvert, sans verrou" }, + { value: "0", label: "tracker ou télémétrie" }, + ], + }, + showcases: [ + { + id: "calendrier-partage", + eyebrow: "Calendrier partagé", + title: ( + <> + Tous vos agendas,{" "} + une seule vue + + ), + description: + "Vues jour, semaine et mois, glisser-déposer et agendas multiples — branchés sur votre serveur CalDAV.", + demo: "ultical-agenda", + features: [ + { + title: "Multi-agendas CalDAV", + description: "Perso, équipe et partagés — superposés avec leurs couleurs.", + icon: "mdi:calendar-multiple", + }, + { + title: "Vues jour / semaine / mois", + description: "Basculez d'un coup d'œil entre vos différentes échelles de temps.", + icon: "mdi:calendar-week", + }, + { + title: "Glisser-déposer", + description: "Déplacez et redimensionnez les événements directement à la souris.", + icon: "mdi:cursor-move", + }, + { + title: "Couleurs & catégories", + description: "Un code couleur par agenda pour distinguer pro, perso et projets.", + icon: "mdi:palette-outline", + }, + ], + }, + { + id: "invitations", + eyebrow: "Invitations & RSVP", + title: ( + <> + Invitez, répondez,{" "} + synchronisez + + ), + description: + "Invitations envoyées par mail, RSVP en un clic et visio UltiMeet ajoutée automatiquement.", + demo: "ultical-invitations", + reverse: true, + features: [ + { + title: "Détection depuis le mail", + description: "Ultimail reconnaît les invitations .ics et propose Oui / Non / Peut-être.", + icon: "mdi:email-arrow-right-outline", + }, + { + title: "RSVP en un clic", + description: "Répondez sans quitter le mail — le statut se synchronise dans l'agenda.", + icon: "mdi:check-circle-outline", + }, + { + title: "Visio UltiMeet auto", + description: "Un lien de réunion chiffré généré et joint à chaque événement.", + icon: "mdi:video-outline", + }, + { + title: "Invités & disponibilités", + description: "Voyez qui a accepté et proposez un autre horaire en cas de conflit.", + icon: "mdi:account-group-outline", + }, + ], + }, + { + id: "rendez-vous", + eyebrow: "Disponibilités & rendez-vous", + title: ( + <> + Vos créneaux libres,{" "} + réservables en un lien + + ), + description: + "Partagez une page de réservation, laissez vos contacts choisir un créneau, recevez rappels et notifications.", + demo: "ultical-scheduling", + features: [ + { + title: "Pages de réservation", + description: "Un lien public façon Calendly, calé sur vos vraies disponibilités.", + icon: "mdi:calendar-clock", + }, + { + title: "Free / busy en temps réel", + description: "Les créneaux occupés sont masqués automatiquement, sans double réservation.", + icon: "mdi:clock-check-outline", + }, + { + title: "Rappels & notifications", + description: "Alertes par mail et notifications push avant chaque événement.", + icon: "mdi:bell-outline", + }, + { + title: "Gestion des fuseaux", + description: "Affichage et conversion automatiques selon le fuseau de chaque invité.", + icon: "mdi:earth", + }, + ], + }, + ], + interopSection: { + eyebrow: "Interopérabilité", + title: ( + <> + Vos calendriers existants,{" "} + dans la même vue + + ), + description: + "Abonnez vos agendas personnels ou d'entreprise — Google Agenda, Outlook, Apple ou CalDAV — sans quitter UltiCal.", + providers: [ + { + name: "Google Agenda", + tagline: "Agenda perso & Workspace", + icon: "logos:google-calendar", + accent: "#4285F4", + brandLogo: true, + personal: + "Connectez votre compte Google et superposez vos agendas perso dans la vue UltiCal.", + enterprise: + "Agendas d'équipe Workspace synchronisés pour coordonner les disponibilités sans migration.", + }, + { + name: "Microsoft Outlook", + tagline: "Outlook & Microsoft 365", + icon: "vscode-icons:file-type-outlook", + accent: "#0078D4", + brandLogo: true, + personal: + "Rattachez votre calendrier Outlook.com — événements visibles à côté de vos agendas Ulti.", + enterprise: + "Calendriers Microsoft 365 et salles de réunion partagés pour toute l'organisation.", + }, + { + name: "Apple iCloud", + tagline: "iCloud & Calendrier Apple", + icon: "mdi:apple", + accent: "#9aa0a6", + personal: + "Abonnez vos agendas iCloud via CalDAV — la même base ouverte que vos appareils Apple.", + enterprise: + "Calendriers partagés iCloud accessibles depuis l'explorateur d'agendas UltiCal.", + }, + { + name: "CalDAV", + tagline: "Nextcloud, Radicale, serveur org", + icon: "mdi:calendar-sync", + accent: "#FBBC04", + personal: + "Serveur CalDAV perso (Nextcloud, Radicale auto-hébergé) abonné en quelques clics.", + enterprise: + "Agendas CalDAV partagés par l'admin — visibles pour toute l'organisation dans UltiCal.", + }, + ], + features: [ + { + title: "Standard CalDAV", + description: "Protocole ouvert : aucun verrou propriétaire, vos données restent portables.", + icon: "mdi:calendar-sync-outline", + }, + { + title: "Vue unifiée", + description: "Agendas perso, abonnés et partagés org superposés dans le même calendrier.", + icon: "mdi:calendar-multiple", + }, + { + title: "Sync bidirectionnelle", + description: "Créez ici, retrouvez partout — vos appareils restent à jour automatiquement.", + icon: "mdi:sync", + }, + { + title: "Apps mobile & desktop", + description: "iOS, Android et client desktop Tauri — même agenda, adapté à chaque écran.", + icon: "mdi:cellphone-link", + }, + ], + }, + integrations: [ + { + name: "Ultimail", + tagline: "Messagerie", + description: "Invitations détectées dans le mail et RSVP synchronisés avec l'agenda.", + icon: suitePublicAsset("/ultimail-mark.svg"), + href: "/suite/ultimail", + accent: "#EA4335", + }, + { + name: "UltiMeet", + tagline: "Visio", + description: "Lien de réunion chiffré ajouté automatiquement à chaque événement.", + icon: suitePublicAsset("/ultimeet-mark.svg"), + href: "/meet", + accent: "#34A853", + }, + { + name: ULTICARDS_APP_NAME, + tagline: "Contacts", + description: "Autocomplétion des invités et fiches contacts enrichies.", + icon: suitePublicAsset("/contacts-mark.svg"), + href: "/contacts", + accent: "#4285F4", + }, + { + name: "UltiAI", + tagline: "Assistant IA", + description: + "Créez des créneaux ou déplacez vos rendez-vous sur simple commande.", + icon: suitePublicAsset("/ultiai-mark.svg"), + href: "/chat", + accent: "#f2783c", + }, + ], + ctaSection: { + title: ( + <> + Prêt à reprendre le contrôle de{" "} + votre temps ? + + ), + description: + "Déployez UltiCal sur votre infrastructure et offrez à vos équipes un calendrier partagé familier, sans dépendance cloud US.", + ctas: { + primary: { label: "Ouvrir UltiCal", href: "/agenda" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + }, +} + +export const ULTIAI_PRODUCT: ProductPageData = { + name: "UltiAI", + tagline: "Assistant IA", + description: + "Assistant IA souverain intégré à toute la suite : chat contextuel, actions sur vos mails, fichiers, agenda et contacts via MCP, tri LLM et tokens API fine-grained — vos modèles, votre infrastructure.", + icon: suitePublicAsset("/ultiai-mark.svg"), + accent: "#F59E0B", + heroEyebrow: "Assistant IA souverain", + heroTitle: "Une IA qui agit", + heroTitleAccent: "dans toute votre suite.", + ctas: { + primary: { label: "Ouvrir UltiAI", href: "/chat" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + highlightsSection: { + eyebrow: "Pourquoi UltiAI", + title: ( + <> + La puissance d'un copilote,{" "} + sans céder vos données + + ), + description: + "Branchez le modèle de votre choix, donnez à l'assistant des outils sur votre suite et gardez le contrôle des permissions — sur votre infrastructure.", + highlights: [ + { value: "∞", label: "modèles OpenAI-compatibles" }, + { value: "MCP", label: "outils suite & recherche web" }, + { value: "Tri IA", label: "classement & résumés automatiques" }, + { value: "0", label: "tracker ou télémétrie" }, + ], + }, + showcases: [ + { + id: "assistant", + eyebrow: "Assistant conversationnel", + title: ( + <> + Votre copilote,{" "} + dans toute la suite + + ), + description: + "Un chat intégré qui connaît le contexte de votre mail, vos fichiers et votre agenda — en plein écran ou en panneau latéral.", + demo: "ultiai-chat", + features: [ + { + title: "Chat intégré partout", + description: "Panneau latéral dans Ultimail et UltiDrive, ou plein écran sur /chat.", + icon: "mdi:message-text-outline", + }, + { + title: "Contexte de la suite", + description: "Comprend le mail ouvert, le fichier sélectionné ou l'événement en cours.", + icon: "mdi:text-box-search-outline", + }, + { + title: "Réponses en streaming", + description: "Génération token par token, sources et étapes affichées en direct.", + icon: "mdi:lightning-bolt-outline", + }, + { + title: "Historique & conversations", + description: "Reprenez vos échanges, épinglez les fils utiles, repartez d'un prompt.", + icon: "mdi:history", + }, + ], + }, + { + id: "outils", + eyebrow: "Actions sur votre suite", + title: ( + <> + Plus que répondre,{" "} + elle agit + + ), + description: + "Via MCP, UltiAI utilise des outils sur vos applications — recherche, envoi, classement, déplacement — avec des permissions par groupe.", + demo: "ultiai-tools", + reverse: true, + features: [ + { + title: "Outils Mail", + description: "Recherche, lecture, envoi, libellés et suppression de messages.", + icon: "mdi:email-search-outline", + }, + { + title: "Outils Drive & Contacts", + description: "Fichiers, dossiers, partages, carnets d'adresses et fiches.", + icon: "mdi:folder-account-outline", + }, + { + title: "Outils Agenda", + description: "Événements UltiCal, invitations et visioconférence UltiMeet.", + icon: "mdi:calendar-edit-outline", + }, + { + title: "Recherche unifiée + web", + description: "Index mail/drive/contacts et recherche en ligne (Brave, SearXNG…).", + icon: "mdi:magnify-scan", + }, + ], + }, + { + id: "automatisations", + eyebrow: "Tri IA & automatisations", + title: ( + <> + Triez et résumez,{" "} + sans lever le doigt + + ), + description: + "Branchez l'IA dans vos règles de tri : classement par LLM, résumés, réponses suggérées et tokens API pour vos agents.", + demo: "ultiai-triage", + features: [ + { + title: "Tri LLM par règle", + description: "Prompt et contexte personnalisés par règle — le LLM choisit le libellé.", + icon: "mdi:creation-outline", + }, + { + title: "Résumés & réponses", + description: "Synthèse des longs fils et brouillons de réponse en un clic.", + icon: "mdi:text-short", + }, + { + title: "Tokens API fine-grained", + description: "Permissions partielles pour agents externes et webhooks.", + icon: "mdi:key-chain-variant", + }, + { + title: "Fournisseur par règle", + description: "Choisissez le modèle adapté à chaque automatisation.", + icon: "mdi:tune-variant", + }, + ], + }, + ], + interopSection: { + eyebrow: "Vos modèles", + title: ( + <> + Vos modèles,{" "} + votre choix + + ), + description: + "UltiAI parle l'API OpenAI-compatible : branchez un fournisseur cloud ou un modèle auto-hébergé, et changez quand vous voulez — sans lock-in.", + providers: [ + { + name: "OpenAI", + tagline: "GPT & API compatibles", + icon: "logos:openai-icon", + accent: "#10A37F", + brandLogo: true, + personal: + "Renseignez votre clé API OpenAI et choisissez le modèle pour le chat et le tri.", + enterprise: + "Azure OpenAI ou passerelle d'entreprise via le même endpoint compatible.", + }, + { + name: "Mistral AI", + tagline: "Modèles européens", + icon: "logos:mistral-ai-icon", + accent: "#FA520F", + brandLogo: true, + personal: + "Modèles Mistral hébergés en Europe, configurés en quelques secondes.", + enterprise: + "Déploiement Mistral dédié ou on-premise via endpoint compatible.", + }, + { + name: "Anthropic", + tagline: "Famille Claude", + icon: "logos:anthropic-icon", + accent: "#D97757", + brandLogo: true, + personal: + "Branchez Claude via une passerelle OpenAI-compatible pour le raisonnement.", + enterprise: + "Clés d'organisation et quotas gérés au niveau de l'admin.", + }, + { + name: "Ollama", + tagline: "Modèles locaux & auto-hébergés", + icon: "simple-icons:ollama", + accent: "#F59E0B", + personal: + "Faites tourner Llama, Qwen ou Mistral en local — aucune donnée ne sort.", + enterprise: + "Serveur d'inférence interne (vLLM, Ollama) partagé pour toute l'organisation.", + }, + ], + features: [ + { + title: "API OpenAI-compatible", + description: "Tout endpoint conforme fonctionne : cloud, passerelle ou serveur local.", + icon: "mdi:api", + }, + { + title: "Local & auto-hébergé", + description: "Gardez l'inférence chez vous — idéal pour les données sensibles.", + icon: "mdi:server-security", + }, + { + title: "Permissions par outil", + description: "Activez ou coupez chaque groupe d'outils MCP exposé à l'assistant.", + icon: "mdi:shield-key-outline", + }, + { + title: "Sans lock-in", + description: "Changez de fournisseur ou de modèle sans migrer vos données.", + icon: "mdi:swap-horizontal", + }, + ], + }, + integrations: [ + { + name: "Ultimail", + tagline: "Messagerie", + description: + "Tri IA et résumés, mais aussi envoi et transfert de vos mails sur votre commande.", + icon: suitePublicAsset("/ultimail-mark.svg"), + href: "/suite/ultimail", + accent: "#EA4335", + }, + { + name: "UltiDrive", + tagline: "Fichiers", + description: + "Interrogez et résumez vos documents, créez-en et modifiez-les sur votre commande.", + icon: suitePublicAsset("/ultidrive-mark.svg"), + href: "/suite/ultidrive", + accent: "#34A853", + }, + { + name: ULTICAL_APP_NAME, + tagline: "Calendrier", + description: + "Trouvez un créneau, créez des rendez-vous ou déplacez-les sur votre commande.", + icon: suitePublicAsset("/agenda-mark.svg"), + href: "/suite/ultical", + accent: "#FBBC04", + }, + { + name: ULTICARDS_APP_NAME, + tagline: "Contacts", + description: "Retrouvez et enrichissez vos fiches contacts sur votre commande.", + icon: suitePublicAsset("/contacts-mark.svg"), + href: "/contacts", + accent: "#4285F4", + }, + ], + ctaSection: { + title: ( + <> + Prêt à donner des{" "} + superpouvoirs à votre suite ? + + ), + description: + "Activez UltiAI sur votre infrastructure, branchez votre modèle et gardez le contrôle total des données et des permissions.", + ctas: { + primary: { label: "Ouvrir UltiAI", href: "/chat" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + }, +} + +export const ULTICARDS_PRODUCT: ProductPageData = { + name: ULTICARDS_APP_NAME, + tagline: "Contacts", + description: + "Carnet d'adresses unifié cross-comptes, fiches enrichies, découverte de contacts depuis le mail, fusion de doublons et sync CardDAV — l'alternative souveraine à Google Contacts, hébergée chez vous.", + icon: suitePublicAsset("/contacts-mark.svg"), + accent: "#4285F4", + heroEyebrow: "Carnet d'adresses souverain", + heroTitle: "Tous vos contacts,", + heroTitleAccent: "au même endroit.", + ctas: { + primary: { label: "Ouvrir UltiCards", href: "/contacts" }, + secondary: { label: "Essayer la démo", href: "/demo/contacts" }, + }, + highlightsSection: { + eyebrow: "Pourquoi UltiCards", + title: ( + <> + L'ergonomie de Google Contacts,{" "} + sans Google + + ), + description: + "Carnet familier, enrichissement IA et standard CardDAV ouvert — partagé par toute la suite, sans céder vos données.", + highlights: [ + { value: "1", label: "carnet unifié cross-comptes" }, + { value: "∞", label: "champs personnalisés & groupes" }, + { value: "CardDAV", label: "standard ouvert, sans verrou" }, + { value: "0", label: "tracker ou télémétrie" }, + ], + }, + showcases: [ + { + id: "carnet-unifie", + eyebrow: "Carnet unifié", + title: ( + <> + Toutes vos fiches,{" "} + une seule vue + + ), + description: + "Contacts, groupes, étiquettes et recherche instantanée — branchés sur votre serveur CardDAV.", + demo: "ulticards-directory", + features: [ + { + title: "Fiches enrichies", + description: "Emails, téléphones, adresses, organisation, anniversaire et notes.", + icon: "mdi:card-account-details-outline", + }, + { + title: "Groupes & étiquettes", + description: "Organisez par équipe, projet ou famille — filtres dynamiques.", + icon: "mdi:tag-multiple-outline", + }, + { + title: "Champs personnalisés", + description: "Ajoutez vos propres champs typés, sans limite ni schéma figé.", + icon: "mdi:format-list-bulleted-type", + }, + { + title: "Recherche instantanée", + description: "Nom, e-mail, société — recherche floue dans tout le carnet.", + icon: "mdi:account-search-outline", + }, + ], + }, + { + id: "decouverte", + eyebrow: "Découverte & enrichissement", + title: ( + <> + Vos contacts,{" "} + détectés et complétés + + ), + description: + "UltiCards scanne vos boîtes mail, propose les contacts manquants et enrichit les fiches par IA.", + demo: "ulticards-discovery", + reverse: true, + features: [ + { + title: "Découverte depuis le mail", + description: "Profils détectés dans vos échanges, suggérés à l'ajout en un clic.", + icon: "mdi:email-search-outline", + }, + { + title: "Enrichissement IA", + description: "Société, poste et coordonnées extraits des signatures par LLM.", + icon: "mdi:auto-fix", + }, + { + title: "Tri des indésirables", + description: "Listes de diffusion, jetables et spam écartés automatiquement.", + icon: "mdi:filter-remove-outline", + }, + { + title: "Interactions récentes", + description: "Derniers mails échangés affichés directement dans la fiche.", + icon: "mdi:history", + }, + ], + }, + { + id: "doublons", + eyebrow: "Doublons & import/export", + title: ( + <> + Un carnet propre,{" "} + sans doublons + + ), + description: + "Détection intelligente des doublons, fusion assistée et migration sans douleur depuis vos carnets existants.", + demo: "ulticards-merge", + features: [ + { + title: "Détection de doublons", + description: "Fiches similaires regroupées par e-mail, nom ou téléphone.", + icon: "mdi:account-multiple-outline", + }, + { + title: "Fusion assistée", + description: "Combinez les champs en gardant la meilleure valeur de chaque fiche.", + icon: "mdi:call-merge", + }, + { + title: "Import vCard, CSV & Google", + description: "Récupérez vos carnets existants en quelques clics.", + icon: "mdi:file-import-outline", + }, + { + title: "Export & impression", + description: "Sauvegardez en vCard/CSV ou imprimez une fiche propre.", + icon: "mdi:file-export-outline", + }, + ], + }, + ], + interopSection: { + eyebrow: "Interopérabilité", + title: ( + <> + Vos carnets existants,{" "} + dans la même vue + + ), + description: + "Synchronisez vos contacts personnels ou d'entreprise — Google, iCloud, Outlook ou CardDAV — sans quitter UltiCards.", + providers: [ + { + name: "Google Contacts", + tagline: "Contacts perso & Workspace", + icon: "logos:google-icon", + accent: "#4285F4", + brandLogo: true, + personal: + "Importez vos contacts Google et retrouvez-les unifiés dans le carnet UltiCards.", + enterprise: + "Annuaire d'équipe Workspace synchronisé pour partager les fiches sans migration.", + }, + { + name: "Apple iCloud", + tagline: "iCloud & Contacts Apple", + icon: "mdi:apple", + accent: "#9aa0a6", + personal: + "Synchronisez vos contacts iCloud via CardDAV — la même base ouverte que vos appareils Apple.", + enterprise: + "Carnets partagés iCloud accessibles depuis l'annuaire UltiCards.", + }, + { + name: "Microsoft Outlook", + tagline: "Outlook & Microsoft 365", + icon: "vscode-icons:file-type-outlook", + accent: "#0078D4", + brandLogo: true, + personal: + "Rattachez vos contacts Outlook.com — fiches visibles à côté de vos carnets Ulti.", + enterprise: + "Annuaire Microsoft 365 et listes de distribution partagés pour toute l'organisation.", + }, + { + name: "CardDAV", + tagline: "Nextcloud, Radicale, serveur org", + icon: "mdi:card-account-details-outline", + accent: "#34A853", + personal: + "Serveur CardDAV perso (Nextcloud, Radicale auto-hébergé) synchronisé en quelques clics.", + enterprise: + "Carnets CardDAV partagés par l'admin — visibles pour toute l'organisation dans UltiCards.", + }, + ], + features: [ + { + title: "Standard CardDAV", + description: "Protocole ouvert : aucun verrou propriétaire, vos contacts restent portables.", + icon: "mdi:sync", + }, + { + title: "Vue unifiée", + description: "Carnets perso, importés et partagés org regroupés dans le même annuaire.", + icon: "mdi:account-box-multiple-outline", + }, + { + title: "Sync bidirectionnelle", + description: "Créez ici, retrouvez partout — vos appareils restent à jour automatiquement.", + icon: "mdi:sync-circle", + }, + { + title: "Apps mobile & desktop", + description: "iOS, Android et client desktop Tauri — même carnet, adapté à chaque écran.", + icon: "mdi:cellphone-link", + }, + ], + }, + integrations: [ + { + name: "Ultimail", + tagline: "Messagerie", + description: "Autocomplétion des destinataires et fiches enrichies à la composition.", + icon: suitePublicAsset("/ultimail-mark.svg"), + href: "/suite/ultimail", + accent: "#EA4335", + }, + { + name: ULTICAL_APP_NAME, + tagline: "Calendrier", + description: "Invités suggérés depuis le carnet et anniversaires dans l'agenda.", + icon: suitePublicAsset("/agenda-mark.svg"), + href: "/suite/ultical", + accent: "#FBBC04", + }, + { + name: "UltiDrive", + tagline: "Fichiers", + description: "Partage par adresse e-mail avec autocomplétion des contacts.", + icon: suitePublicAsset("/ultidrive-mark.svg"), + href: "/suite/ultidrive", + accent: "#34A853", + }, + { + name: "UltiAI", + tagline: "Assistant IA", + description: "Retrouvez et enrichissez vos fiches contacts sur simple commande.", + icon: suitePublicAsset("/ultiai-mark.svg"), + href: "/suite/ultiai", + accent: "#f2783c", + }, + ], + ctaSection: { + title: ( + <> + Prêt à reprendre le contrôle de{" "} + votre carnet d'adresses ? + + ), + description: + "Déployez UltiCards sur votre infrastructure et offrez à vos équipes un carnet partagé familier, sans dépendance cloud US.", + ctas: { + primary: { label: "Ouvrir UltiCards", href: "/contacts" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + }, +} + +export const ULTIMEET_PRODUCT: ProductPageData = { + name: "UltiMeet", + tagline: "Visio", + description: + "Lancez une visio en un clic, sans que Google, Microsoft ou Zoom puissent écouter ce qui s'y dit. Vos réunions restent chiffrées et entre vous.", + icon: suitePublicAsset("/ultimeet-mark.svg"), + accent: "#34A853", + heroEyebrow: "Visioconférence souveraine", + heroTitle: "Vos réunions vidéo,", + heroTitleAccent: "à l'abri des GAFAM.", + ctas: { + primary: { label: "Démarrer une réunion", href: "/meet" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + highlightsSection: { + eyebrow: "Pourquoi UltiMeet", + title: ( + <> + Des réunions{" "} + que personne n'écoute + + ), + description: + "Une visio claire et familière, connectée à votre agenda et votre messagerie — sans que les géants du web puissent écouter ni profiler vos échanges.", + highlights: [ + { value: "1 clic", label: "pour lancer une réunion" }, + { value: "E2EE", label: "chiffrement de bout en bout" }, + { value: "0", label: "GAFAM à l'écoute" }, + { value: "0", label: "tracker ou télémétrie" }, + ], + }, + showcases: [ + { + id: "reunions-navigateur", + eyebrow: "Réunions dans le navigateur", + title: ( + <> + Lancez une visio,{" "} + en un clic + + ), + description: + "Vidéo HD WebRTC, grille de participants et contrôles familiers — sans plugin ni application à installer.", + demo: "ultimeet-room", + features: [ + { + title: "Vidéo HD WebRTC", + description: "Flux audio/vidéo temps réel dans le navigateur, sans installation.", + icon: "mdi:video-outline", + }, + { + title: "Chiffrement de bout en bout", + description: "Médias chiffrés (E2EE) — vos réunions restent privées.", + icon: "mdi:lock-check-outline", + }, + { + title: "Salles persistantes", + description: "Une URL stable par équipe ou projet, réutilisable à volonté.", + icon: "mdi:door-open", + }, + { + title: "Grille & vue active", + description: "Mosaïque ou orateur épinglé, détection automatique de la voix.", + icon: "mdi:view-grid-outline", + }, + ], + }, + { + id: "planification-acces", + eyebrow: "Planification & accès", + title: ( + <> + Planifiée dans l'agenda,{" "} + protégée par un lobby + + ), + description: + "Un lien généré depuis UltiCal et Ultimail, une salle d'attente et un contrôle d'accès — sans compte pour vos invités.", + demo: "ultimeet-lobby", + reverse: true, + features: [ + { + title: "Lien depuis UltiCal", + description: "Visio ajoutée automatiquement à chaque événement et invitation.", + icon: "mdi:calendar-plus-outline", + }, + { + title: "Salle d'attente", + description: "Les participants patientent jusqu'à l'admission par l'hôte.", + icon: "mdi:account-clock-outline", + }, + { + title: "Contrôle d'accès", + description: "Verrouillage de salle, mot de passe et expulsion en un clic.", + icon: "mdi:shield-account-outline", + }, + { + title: "Sans compte pour les invités", + description: "Vos contacts rejoignent depuis un simple lien, aucun compte requis.", + icon: "mdi:link-variant", + }, + ], + }, + { + id: "collaboration", + eyebrow: "Collaboration en réunion", + title: ( + <> + Partagez, réagissez,{" "} + transcrivez + + ), + description: + "Partage d'écran, chat, réactions et enregistrement — avec transcription et résumés générés par UltiAI.", + demo: "ultimeet-collab", + features: [ + { + title: "Partage d'écran", + description: "Diffusez une fenêtre, un onglet ou tout l'écran en HD.", + icon: "mdi:monitor-share", + }, + { + title: "Chat & réactions", + description: "Messages, émojis, lever la main et sondages pendant la réunion.", + icon: "mdi:message-reply-text-outline", + }, + { + title: "Enregistrement", + description: "Enregistrez la session sur votre UltiDrive, sous votre contrôle.", + icon: "mdi:record-rec", + }, + { + title: "Transcription IA", + description: "Sous-titres en direct et résumé d'après-réunion via UltiAI.", + icon: "mdi:text-recognition", + }, + ], + }, + ], + crossPlatformSection: { + eyebrow: "Multi-plateforme", + title: ( + <> + Rejoignez depuis{" "} + n'importe quel appareil + + ), + description: + "Navigateur, apps iOS et Android natives (Tauri) ou client desktop — la même réunion UltiMeet, adaptée à chaque écran et à chaque connexion.", + features: [ + { + title: "Navigateur, zéro install", + description: "Chrome, Safari, Firefox ou Edge — un lien suffit pour rejoindre.", + icon: "mdi:web", + }, + { + title: "Apps iOS & Android", + description: "Clients natifs Tauri, même expérience que sur le web.", + icon: "mdi:cellphone-link", + }, + { + title: "Mode économe", + description: "Adaptation du débit et coupure vidéo automatique sur réseau faible.", + icon: "mdi:signal-cellular-2", + }, + { + title: "Notifications & rappels", + description: "Alertes avant chaque réunion, synchronisées avec UltiCal.", + icon: "mdi:bell-outline", + }, + ], + }, + crossPlatformDemo: "meet", + integrations: [ + { + name: ULTICAL_APP_NAME, + tagline: "Calendrier", + description: "Lien de réunion chiffré ajouté automatiquement à chaque événement.", + icon: suitePublicAsset("/agenda-mark.svg"), + href: "/suite/ultical", + accent: "#FBBC04", + }, + { + name: "Ultimail", + tagline: "Messagerie", + description: "Invitations détectées dans le mail et lien visio en un clic.", + icon: suitePublicAsset("/ultimail-mark.svg"), + href: "/suite/ultimail", + accent: "#EA4335", + }, + { + name: ULTICARDS_APP_NAME, + tagline: "Contacts", + description: "Invités suggérés depuis le carnet avec autocomplétion.", + icon: suitePublicAsset("/contacts-mark.svg"), + href: "/suite/ulticards", + accent: "#4285F4", + }, + { + name: "UltiAI", + tagline: "Assistant IA", + description: "Transcription en direct, sous-titres et résumés d'après-réunion.", + icon: suitePublicAsset("/ultiai-mark.svg"), + href: "/suite/ultiai", + accent: "#f2783c", + }, + ], + ctaSection: { + title: ( + <> + Prêt à reprendre le contrôle de{" "} + vos réunions ? + + ), + description: + "Déployez UltiMeet sur votre infrastructure et offrez à vos équipes une visio familière et chiffrée, sans dépendance cloud US.", + ctas: { + primary: { label: "Démarrer une réunion", href: "/meet" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + }, +} + +export const ADMINISTRATION_PRODUCT: ProductPageData = { + name: "Administration", + tagline: "Console d'administration", + description: + "Pilotez toute la suite depuis une console unique : migration depuis Google Workspace et Microsoft 365, gestion d'identité (SSO), utilisateurs, groupes, quotas, politiques de sécurité et audit — souverain, hébergé chez vous.", + icon: suitePublicAsset("/admin-mark.svg"), + accent: "#7C3AED", + heroEyebrow: "Console souveraine", + heroTitle: "Toute votre organisation,", + heroTitleAccent: "sous contrôle.", + ctas: { + primary: { label: "Ouvrir la console", href: "/admin" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + highlightsSection: { + eyebrow: "Pourquoi l'Administration UltiSuite", + title: ( + <> + Quittez Google et Microsoft,{" "} + gardez la maîtrise + + ), + description: + "Migration assistée, identité fédérée, quotas et politiques de sécurité — toute la gouvernance de votre organisation au même endroit, sans dépendance cloud US.", + highlights: [ + { value: "Workspace + 365", label: "migration mail, drive, agenda, contacts" }, + { value: "SSO", label: "OAuth, SAML & LDAP/Active Directory" }, + { value: "RBAC", label: "rôles, groupes & quotas par défaut" }, + { value: "Audit", label: "journal exportable, 0 télémétrie" }, + ], + }, + showcases: [ + { + id: "migration", + eyebrow: "Migration & bascule", + title: ( + <> + Depuis Google & Microsoft,{" "} + sans rien perdre + + ), + description: + "Importez mail, drive, agenda et contacts depuis Google Workspace ou Microsoft 365, suivez chaque job et basculez les MX quand tout est prêt.", + demo: "admin-migration", + features: [ + { + title: "Google Workspace & Microsoft 365", + description: "OAuth utilisateur, Google DWD ou Microsoft app-only — au choix par projet.", + icon: "mdi:swap-horizontal-bold", + }, + { + title: "Invitations & roster", + description: "Invitez par e-mail ou import CSV, pré-provisionnez le roster et les alias SSO.", + icon: "mdi:account-multiple-plus-outline", + }, + { + title: "Suivi des jobs & audit", + description: "Progression par service, relance des échecs et export CSV/NDJSON par utilisateur.", + icon: "mdi:progress-check", + }, + { + title: "Bascule MX guidée", + description: "Pré-vérification DNS (TXT, MX, SPF/DKIM/DMARC) puis cutover en mode delta.", + icon: "mdi:dns-outline", + }, + ], + }, + { + id: "identite", + eyebrow: "Gestion d'identité", + title: ( + <> + Une identité fédérée,{" "} + vos règles d'accès + + ), + description: + "Connectez vos fournisseurs SSO existants via Authentik et gardez la main sur qui accède à quoi.", + demo: "admin-identity", + reverse: true, + features: [ + { + title: "OAuth, SAML & LDAP", + description: "Google, Microsoft Entra, Okta, Active Directory ou OAuth custom.", + icon: "mdi:account-key-outline", + }, + { + title: "Inscription self-service", + description: "Flow d'enrollment Authentik en parallèle du SSO entreprise, activable.", + icon: "mdi:account-plus-outline", + }, + { + title: "Restrictions d'accès", + description: "Domaines e-mail, identités et organisations autorisés par fournisseur.", + icon: "mdi:shield-account-outline", + }, + { + title: "Provisioning de groupes", + description: "Groupes Authentik par défaut et synchronisation des comptes LDAP.", + icon: "mdi:account-sync-outline", + }, + ], + }, + { + id: "utilisateurs", + eyebrow: "Utilisateurs, rôles & groupes", + title: ( + <> + Vos équipes,{" "} + organisées et gouvernées + + ), + description: + "Annuaire complet, rôles fins et groupes pour piloter les permissions à grande échelle.", + demo: "admin-users", + features: [ + { + title: "Rôles admin / utilisateur / invité", + description: "Attribuez les bons droits et suspendez un compte en un clic.", + icon: "mdi:shield-crown-outline", + }, + { + title: "Groupes d'utilisateurs", + description: "Équipes, services et externes — base des partages et des permissions.", + icon: "mdi:account-group-outline", + }, + { + title: "Actions en masse", + description: "Invitation, rôle, quota ou ajout à un groupe sur une sélection.", + icon: "mdi:checkbox-multiple-marked-outline", + }, + { + title: "Fiche utilisateur", + description: "Quotas individuels, plafonds IA et statut depuis chaque profil.", + icon: "mdi:card-account-details-outline", + }, + ], + }, + { + id: "quotas", + eyebrow: "Quotas & supervision", + title: ( + <> + Des ressources cadrées,{" "} + sans surprise + + ), + description: + "Quotas de stockage, plafonds de coût IA et seuils d'alerte appliqués par défaut, ajustables par compte.", + demo: "admin-quotas", + reverse: true, + features: [ + { + title: "Stockage Mail, Drive & Photos", + description: "Quotas par défaut pour l'organisation, surcharge par utilisateur.", + icon: "mdi:gauge", + }, + { + title: "Plafonds de coût IA", + description: "Limites journalières et mensuelles par utilisateur sur les clés org.", + icon: "mdi:robot-outline", + }, + { + title: "Recherche & automatisations", + description: "Recherches web, tokens API et webhooks plafonnés par compte.", + icon: "mdi:tune-variant", + }, + { + title: "Vue d'ensemble & alertes", + description: "Stockage consommé, comptes proches du quota et activité récente.", + icon: "mdi:view-dashboard-outline", + }, + ], + }, + { + id: "securite", + eyebrow: "Sécurité & politiques", + title: ( + <> + Des politiques fermes,{" "} + une traçabilité totale + + ), + description: + "2FA, politiques fichiers et journal d'audit pour répondre à vos exigences de conformité.", + demo: "admin-policies", + features: [ + { + title: "2FA & WebAuthn", + description: "Second facteur obligatoire (tous ou admins), TOTP et clés de sécurité.", + icon: "mdi:cellphone-key", + }, + { + title: "Politiques fichiers", + description: "Partage externe, extensions autorisées, antivirus et rétention UltiDrive.", + icon: "mdi:file-cog-outline", + }, + { + title: "Journal d'audit", + description: "Toutes les actions administratives tracées, export CSV/NDJSON.", + icon: "mdi:clipboard-text-clock-outline", + }, + { + title: "Souveraineté des données", + description: "Hébergement chez vous, aucune télémétrie ni tracker tiers.", + icon: "mdi:shield-lock-outline", + }, + ], + }, + ], + interopSection: { + eyebrow: "Sources & fédération", + title: ( + <> + Vos plateformes actuelles,{" "} + migrées et fédérées + + ), + description: + "Migrez les données depuis vos suites existantes et continuez à fédérer l'identité via SSO — un compte, plusieurs sources.", + providers: [ + { + name: "Google Workspace", + tagline: "Gmail, Drive, Agenda, Contacts", + icon: "logos:google-workspace", + accent: "#4285F4", + brandLogo: true, + personal: + "Migration OAuth utilisateur : chacun rattache son compte Google et lance l'import.", + enterprise: + "Google DWD (service account) pour migrer toute l'organisation, plus SSO OAuth continu.", + }, + { + name: "Microsoft 365", + tagline: "Outlook, OneDrive, Calendrier", + icon: "logos:microsoft-icon", + accent: "#0078D4", + brandLogo: true, + personal: + "Migration OAuth : import du courrier, des fichiers et de l'agenda Microsoft 365.", + enterprise: + "Microsoft app-only (client credentials) avec consentement admin, plus SSO SAML Entra ID.", + }, + { + name: "Okta", + tagline: "SSO SAML & OIDC", + icon: "logos:okta-icon", + accent: "#007DC1", + brandLogo: true, + personal: + "Connexion SAML pour authentifier vos membres avec leurs identifiants Okta.", + enterprise: + "Fédération SAML/OIDC et provisioning des groupes pour toute l'organisation.", + }, + { + name: "Active Directory", + tagline: "LDAP & Azure AD / Entra", + icon: "mdi:server-network", + accent: "#7C3AED", + personal: + "Authentification LDAP contre votre annuaire interne, sans nouvelle base de comptes.", + enterprise: + "Synchronisation des utilisateurs et des groupes AD via LDAP sécurisé (StartTLS).", + }, + ], + features: [ + { + title: "Migration sans coupure", + description: "Mode delta et bascule MX progressive — les utilisateurs gardent leurs données.", + icon: "mdi:transfer", + }, + { + title: "Identité fédérée", + description: "Un compte Ulti relié à vos fournisseurs SSO, avec alias multiples acceptés.", + icon: "mdi:account-key-outline", + }, + { + title: "Standards ouverts", + description: "OAuth, SAML, LDAP, IMAP, CalDAV/CardDAV — aucun verrou propriétaire.", + icon: "mdi:lock-open-variant-outline", + }, + { + title: "Sans dépendance US", + description: "Données et identités hébergées sur votre infrastructure souveraine.", + icon: "mdi:earth", + }, + ], + }, + integrations: [ + { + name: "Ultimail", + tagline: "Messagerie", + description: "Domaines hébergés Stalwart, relais SMTP des notifications et bascule MX.", + icon: suitePublicAsset("/ultimail-mark.svg"), + href: "/suite/ultimail", + accent: "#EA4335", + }, + { + name: "UltiDrive", + tagline: "Fichiers", + description: "Politiques d'upload, partage externe, antivirus et montages d'organisation.", + icon: suitePublicAsset("/ultidrive-mark.svg"), + href: "/suite/ultidrive", + accent: "#34A853", + }, + { + name: "UltiAI", + tagline: "Assistant IA", + description: "Fournisseurs LLM d'organisation, groupes d'outils MCP et plafonds de coût.", + icon: suitePublicAsset("/ultiai-mark.svg"), + href: "/suite/ultiai", + accent: "#f2783c", + }, + { + name: ULTICARDS_APP_NAME, + tagline: "Contacts", + description: "Annuaire d'organisation alimenté par le provisioning LDAP/SCIM.", + icon: suitePublicAsset("/contacts-mark.svg"), + href: "/suite/ulticards", + accent: "#4285F4", + }, + ], + ctaSection: { + title: ( + <> + Prêt à reprendre le contrôle de{" "} + votre organisation ? + + ), + description: + "Déployez UltiSuite sur votre infrastructure, migrez depuis Google ou Microsoft et administrez identité, quotas et sécurité depuis une console unique.", + ctas: { + primary: { label: "Ouvrir la console", href: "/admin" }, + secondary: { label: "Découvrir la suite", href: "/" }, + }, + }, +} diff --git a/components/landing/product/product-demo-frame.tsx b/components/landing/product/product-demo-frame.tsx new file mode 100644 index 0000000..9e6ebeb --- /dev/null +++ b/components/landing/product/product-demo-frame.tsx @@ -0,0 +1,69 @@ +"use client" + +import Link from "next/link" +import { Icon } from "@iconify/react" +import { cn } from "@/lib/utils" + +type ProductDemoFrameProps = { + fakeUrl: string + hint?: string + fullscreenHref?: string + heightClass?: string + /** Rapport largeur/hauteur de la zone contenu (ex. 1440/900). Prioritaire sur heightClass. */ + aspectRatio?: number + children: React.ReactNode + className?: string +} + +export function ProductDemoFrame({ + fakeUrl, + hint, + fullscreenHref, + heightClass = "h-[22rem] sm:h-[26rem] lg:h-[28rem]", + aspectRatio, + children, + className, +}: ProductDemoFrameProps) { + return ( +
+
+
+ + + +
+ + {fakeUrl} +
+ {fullscreenHref ? ( + + + Plein écran + + ) : null} +
+
+ {children} +
+
+ {hint ? ( +

+ + {hint} +

+ ) : null} +
+ ) +} diff --git a/components/landing/product/product-demos/admin-identity-demo.tsx b/components/landing/product/product-demos/admin-identity-demo.tsx new file mode 100644 index 0000000..13b61af --- /dev/null +++ b/components/landing/product/product-demos/admin-identity-demo.tsx @@ -0,0 +1,96 @@ +"use client" + +import { Icon } from "@iconify/react" + +const ACCENT = "#7C3AED" + +type Provider = { + name: string + type: string + icon: string + brandLogo?: boolean + enabled: boolean + status: "synced" | "pending" +} + +const PROVIDERS: Provider[] = [ + { name: "Google Workspace", type: "OAuth", icon: "logos:google-icon", brandLogo: true, enabled: true, status: "synced" }, + { name: "Azure AD / Entra", type: "SAML", icon: "logos:microsoft-icon", brandLogo: true, enabled: true, status: "synced" }, + { name: "Active Directory", type: "LDAP", icon: "mdi:server-network", enabled: true, status: "pending" }, + { name: "Okta", type: "SAML", icon: "logos:okta-icon", brandLogo: true, enabled: false, status: "pending" }, +] + +const RESTRICTIONS = ["acme.com", "filiale.fr", "ulti-users", "ulti-admins"] + +/** Aperçu statique des fournisseurs d'identité (SSO) gérés via Authentik. */ +export function AdminIdentityDemo() { + return ( +
+
+
+ + Fournisseurs d'identité + SSO · Authentik +
+ +
    + {PROVIDERS.map((p) => ( +
  • + + + +
    +

    {p.name}

    +

    {p.type}

    +
    + + {p.status === "synced" ? "Synchronisé" : "En attente"} + + +
  • + ))} +
+ +
+

+ + Restrictions d'accès & groupes par défaut +

+
+ {RESTRICTIONS.map((r) => ( + + {r} + + ))} +
+
+
+

+ + Aperçu statique — OAuth, SAML et LDAP/AD, provisioning des groupes et domaines autorisés. +

+
+ ) +} diff --git a/components/landing/product/product-demos/admin-migration-demo.tsx b/components/landing/product/product-demos/admin-migration-demo.tsx new file mode 100644 index 0000000..558ad90 --- /dev/null +++ b/components/landing/product/product-demos/admin-migration-demo.tsx @@ -0,0 +1,102 @@ +"use client" + +import { Icon } from "@iconify/react" + +const ACCENT = "#7C3AED" + +type ServiceProgress = { + service: string + icon: string + imported: number + total: number +} + +const SERVICES: ServiceProgress[] = [ + { service: "Mail", icon: "mdi:email-outline", imported: 12840, total: 13200 }, + { service: "Drive", icon: "mdi:folder-outline", imported: 3420, total: 3900 }, + { service: "Contacts", icon: "mdi:card-account-details-outline", imported: 512, total: 512 }, + { service: "Agenda", icon: "mdi:calendar-outline", imported: 286, total: 310 }, +] + +/** Aperçu statique d'un projet de migration Google Workspace / Microsoft 365. */ +export function AdminMigrationDemo() { + return ( +
+
+
+ + Migration ACME 2026 + + + En cours + +
+ +
+ + + + + + + +
+

Google Workspace + Microsoft 365

+

OAuth · Google DWD · MS app-only

+
+
+ +
+ {SERVICES.map((s) => { + const pct = Math.round((s.imported / s.total) * 100) + return ( +
+
+ + {s.service} + + {s.imported.toLocaleString("fr-FR")} / {s.total.toLocaleString("fr-FR")} + +
+
+
+
+
+ ) + })} +
+ +
+ + Bascule MX + + + + TXT + + + + MX + + DNS vérifié · prêt au cutover +
+
+

+ + Aperçu statique — import, suivi des jobs, audit par utilisateur et bascule MX progressive. +

+
+ ) +} diff --git a/components/landing/product/product-demos/admin-policies-demo.tsx b/components/landing/product/product-demos/admin-policies-demo.tsx new file mode 100644 index 0000000..fe17cb9 --- /dev/null +++ b/components/landing/product/product-demos/admin-policies-demo.tsx @@ -0,0 +1,86 @@ +"use client" + +import { Icon } from "@iconify/react" + +const ACCENT = "#7C3AED" + +type Policy = { + label: string + icon: string + enabled: boolean +} + +const POLICIES: Policy[] = [ + { label: "2FA obligatoire (admins)", icon: "mdi:cellphone-key", enabled: true }, + { label: "Clés de sécurité WebAuthn", icon: "mdi:key-chain-variant", enabled: true }, + { label: "Partage externe restreint", icon: "mdi:link-lock", enabled: true }, + { label: "Analyse antivirus à l'upload", icon: "mdi:shield-bug-outline", enabled: true }, + { label: "Rétention corbeille 30 j", icon: "mdi:delete-clock-outline", enabled: false }, +] + +const AUDIT = [ + { actor: "alice@acme.com", action: "user.role.update", icon: "mdi:account-edit-outline" }, + { actor: "system", action: "migration.cutover", icon: "mdi:swap-horizontal" }, + { actor: "bob@acme.com", action: "share.link.create", icon: "mdi:link-variant" }, +] + +/** Aperçu statique des politiques de sécurité et du journal d'audit. */ +export function AdminPoliciesDemo() { + return ( +
+
+
+ + Politiques de sécurité +
+ +
    + {POLICIES.map((p) => ( +
  • + + + + + {p.label} + + +
  • + ))} +
+ +
+

+ + Journal d'audit +

+
    + {AUDIT.map((a, i) => ( +
  • + + {a.action} + + {a.actor} + +
  • + ))} +
+
+
+

+ + Aperçu statique — 2FA, politiques fichiers et journal d'audit exportable (CSV/NDJSON). +

+
+ ) +} diff --git a/components/landing/product/product-demos/admin-quotas-demo.tsx b/components/landing/product/product-demos/admin-quotas-demo.tsx new file mode 100644 index 0000000..122602c --- /dev/null +++ b/components/landing/product/product-demos/admin-quotas-demo.tsx @@ -0,0 +1,80 @@ +"use client" + +import { Icon } from "@iconify/react" + +const ACCENT = "#7C3AED" + +type StorageQuota = { + label: string + icon: string + usedGib: number + totalGib: number +} + +const STORAGE: StorageQuota[] = [ + { label: "Mail", icon: "mdi:email-outline", usedGib: 24, totalGib: 30 }, + { label: "Drive", icon: "mdi:folder-outline", usedGib: 78, totalGib: 100 }, + { label: "Photos", icon: "mdi:image-outline", usedGib: 9, totalGib: 30 }, +] + +/** Aperçu statique des quotas de stockage et de coût IA appliqués par défaut. */ +export function AdminQuotasDemo() { + return ( +
+
+
+ + Quotas par défaut + Organisation +
+ +
+ {STORAGE.map((q) => { + const pct = Math.round((q.usedGib / q.totalGib) * 100) + const warn = pct >= 90 + return ( +
+
+ + {q.label} + + {q.usedGib} / {q.totalGib} Go + +
+
+
+
+
+ ) + })} +
+ +
+
+

+ + Coût IA / jour +

+

2,50 €

+

par utilisateur · clés org

+
+
+

+ + Recherches web +

+

50 / jour

+

tokens API & webhooks limités

+
+
+
+

+ + Aperçu statique — quotas stockage, plafonds de coût IA et seuils d'alerte par défaut. +

+
+ ) +} diff --git a/components/landing/product/product-demos/admin-users-demo.tsx b/components/landing/product/product-demos/admin-users-demo.tsx new file mode 100644 index 0000000..0ac79b9 --- /dev/null +++ b/components/landing/product/product-demos/admin-users-demo.tsx @@ -0,0 +1,111 @@ +"use client" + +import { Icon } from "@iconify/react" + +const ACCENT = "#7C3AED" + +type Member = { + initials: string + name: string + email: string + role: string + roleIcon: string + groups: string[] +} + +const MEMBERS: Member[] = [ + { + initials: "AD", + name: "Alice Dupont", + email: "alice@acme.com", + role: "Admin", + roleIcon: "mdi:shield-crown-outline", + groups: ["Direction", "IT"], + }, + { + initials: "BM", + name: "Bob Martin", + email: "bob@acme.com", + role: "Utilisateur", + roleIcon: "mdi:account-outline", + groups: ["Ventes"], + }, + { + initials: "CL", + name: "Chloé Leroy", + email: "chloe@acme.com", + role: "Utilisateur", + roleIcon: "mdi:account-outline", + groups: ["Marketing"], + }, + { + initials: "PR", + name: "Prestataire X", + email: "ext@partenaire.io", + role: "Invité", + roleIcon: "mdi:account-clock-outline", + groups: ["Externes"], + }, +] + +/** Aperçu statique de l'annuaire utilisateurs avec rôles et groupes. */ +export function AdminUsersDemo() { + return ( +
+
+
+ + Utilisateurs & groupes + + + Tous les groupes + +
+ +
    + {MEMBERS.map((m) => ( +
  • + + {m.initials} + +
    +

    {m.name}

    +

    {m.email}

    +
    +
    + {m.groups.map((g) => ( + + {g} + + ))} +
    + + + {m.role} + +
  • + ))} +
+ +
+ + Actions en masse · invitation · rôle · quota · ajout à un groupe +
+
+

+ + Aperçu statique — rôles admin/utilisateur/invité, groupes et actions en lot. +

+
+ ) +} diff --git a/components/landing/product/product-demos/product-mail-demo-shell.tsx b/components/landing/product/product-demos/product-mail-demo-shell.tsx new file mode 100644 index 0000000..3bfc6e8 --- /dev/null +++ b/components/landing/product/product-demos/product-mail-demo-shell.tsx @@ -0,0 +1,42 @@ +"use client" + +import { useLayoutEffect, type ReactNode } from "react" +import { DEMO_USER } from "@/components/demo/demo-mail-data" +import { DEMO_MAIL_ACCOUNT_ID } from "@/lib/demo/demo-mail-api-data" +import { DemoMailProvider } from "@/lib/demo/demo-mail-context" +import { ComposeProvider } from "@/lib/compose-context" +import { ScheduledMailProvider } from "@/lib/scheduled-mail-context" +import { useAccountStore } from "@/lib/stores/account-store" +import { useComposeIdentitiesStore } from "@/lib/stores/compose-identities-store" + +function ProductMailDemoBootstrap() { + useLayoutEffect(() => { + useAccountStore.getState().setActiveAccountId(DEMO_MAIL_ACCOUNT_ID) + useComposeIdentitiesStore.getState().hydrateFromApi([ + { + id: "product-demo-compose-identity", + accountId: DEMO_MAIL_ACCOUNT_ID, + name: DEMO_USER.name, + email: DEMO_USER.email, + defaultSignatureId: null, + signatureHtml: null, + isDefault: true, + }, + ]) + }, []) + return null +} + +/** Providers minimaux pour embarquer le compositeur mail en démo produit. */ +export function ProductMailDemoShell({ children }: { children: ReactNode }) { + return ( + {}}> + + + +
{children}
+
+
+
+ ) +} diff --git a/components/landing/product/product-demos/ultiai-chat-demo.tsx b/components/landing/product/product-demos/ultiai-chat-demo.tsx new file mode 100644 index 0000000..374e06d --- /dev/null +++ b/components/landing/product/product-demos/ultiai-chat-demo.tsx @@ -0,0 +1,109 @@ +"use client" + +import { Icon } from "@iconify/react" +import { cn } from "@/lib/utils" + +const ACCENT = "#F59E0B" + +type ChatMessage = { + role: "user" | "assistant" + text: string + tool?: { icon: string; label: string } +} + +const MESSAGES: ChatMessage[] = [ + { + role: "user", + text: "Résume le fil « Atelier Nord » et prépare une réponse pour décaler à jeudi.", + }, + { + role: "assistant", + tool: { icon: "mdi:email-search-outline", label: "Recherche dans Ultimail" }, + text: "3 messages trouvés. Léa propose mardi 14h, Vincent a un conflit. En résumé : validation du devis OK, reste à caler la date.", + }, + { + role: "assistant", + tool: { icon: "mdi:file-document-edit-outline", label: "Brouillon de réponse" }, + text: "Brouillon prêt : « Bonjour Léa, jeudi 14h vous conviendrait-il ? Je joins le lien UltiMeet. »", + }, +] + +/** Aperçu statique du chat UltiAI avec appels d'outils et streaming. */ +export function UltiaiChatDemo() { + return ( +
+
+
+ + UltiAI + + + Contexte mail actif + +
+ +
+ {MESSAGES.map((message, index) => ( +
+ {message.tool ? ( + + + {message.tool.label} + + ) : null} +
+ {message.text} +
+
+ ))} + +
+ + + +
+
+ +
+
+ + Demandez quelque chose à UltiAI… + + + + +
+
+
+

+ + Aperçu statique — l'assistant lit le contexte et propose, rien n'est envoyé. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ultiai-tools-demo.tsx b/components/landing/product/product-demos/ultiai-tools-demo.tsx new file mode 100644 index 0000000..0dd6b39 --- /dev/null +++ b/components/landing/product/product-demos/ultiai-tools-demo.tsx @@ -0,0 +1,106 @@ +"use client" + +import { Icon } from "@iconify/react" +import { ULTIAI_TOOL_GROUPS } from "@/lib/ai/ultiai-tool-groups" +import { cn } from "@/lib/utils" + +const ACCENT = "#F59E0B" + +const TOOL_ICONS: Record = { + mail: "mdi:email-outline", + drive: "mdi:folder-outline", + contacts: "mdi:card-account-details-outline", + agenda: "mdi:calendar-outline", + search: "mdi:magnify", + web_search: "mdi:web", +} + +// Groupes désactivés dans l'aperçu pour illustrer les permissions fines. +const DISABLED_GROUPS = new Set(["web_search"]) + +const TRACE = [ + { icon: "mdi:email-search-outline", label: "mail.search", detail: "« facture » · 3 résultats" }, + { icon: "mdi:label-outline", label: "mail.addLabel", detail: "Comptabilité" }, + { icon: "mdi:folder-move-outline", label: "drive.move", detail: "→ /Factures/2026" }, +] + +/** Aperçu statique des groupes d'outils MCP et d'une trace d'exécution. */ +export function UltiaiToolsDemo() { + return ( +
+
+
+ + Outils MCP exposés + + X-Ulti-Enabled-Tools + +
+ +
+ {ULTIAI_TOOL_GROUPS.map((group) => { + const enabled = !DISABLED_GROUPS.has(group.id) + return ( +
+ + + + + {group.label} + + +
+ ) + })} +
+ +
+

+ + Trace d'exécution +

+
    + {TRACE.map((step, index) => ( +
  • + + + + {step.label} + + {step.detail} +
  • + ))} +
+
+
+

+ + Aperçu statique — chaque groupe d'outils s'active ou se coupe par compte. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ultiai-triage-demo.tsx b/components/landing/product/product-demos/ultiai-triage-demo.tsx new file mode 100644 index 0000000..c9431c4 --- /dev/null +++ b/components/landing/product/product-demos/ultiai-triage-demo.tsx @@ -0,0 +1,97 @@ +"use client" + +import { Icon } from "@iconify/react" + +const ACCENT = "#F59E0B" + +type TriageItem = { + sender: string + subject: string + label: string + labelColor: string + reason: string +} + +const ITEMS: TriageItem[] = [ + { + sender: "Stripe", + subject: "Votre reçu de paiement — 149 €", + label: "Comptabilité", + labelColor: "#34A853", + reason: "Reçu de paiement → archivé et étiqueté.", + }, + { + sender: "Léa Fontaine", + subject: "Re: Atelier Nord — proposition de date", + label: "À traiter", + labelColor: "#EA4335", + reason: "Demande de réponse → priorité haute.", + }, + { + sender: "Newsletter Produit", + subject: "Les nouveautés de juin sont là", + label: "Veille", + labelColor: "#4285F4", + reason: "Contenu informatif → boîte secondaire.", + }, +] + +/** Aperçu statique du tri IA : règle LLM qui classe et explique son choix. */ +export function UltiaiTriageDemo() { + return ( +
+
+
+ + Règle de tri IA +
+ +
+

+ Prompt de la règle +

+

+ « Classe chaque mail entrant selon le contexte de mon activité : comptabilité, + à traiter ou veille. » +

+
+ +
    + {ITEMS.map((item) => ( +
  • + + {item.sender.charAt(0)} + +
    +
    + + {item.sender} + + + {item.label} + +
    +

    {item.subject}

    +

    + + {item.reason} +

    +
    +
  • + ))} +
+
+

+ + Aperçu statique — le LLM classe à la réception selon votre prompt. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ultical-agenda-demo.tsx b/components/landing/product/product-demos/ultical-agenda-demo.tsx new file mode 100644 index 0000000..f31809e --- /dev/null +++ b/components/landing/product/product-demos/ultical-agenda-demo.tsx @@ -0,0 +1,55 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import { ProductDemoFrame } from "@/components/landing/product/product-demo-frame" +import { ScaledPreviewIframe } from "@/components/landing/product/scaled-preview-iframe" + +// Viewport logique « grand écran » : l'agenda est rendu plus large puis réduit +// pour fit dans le cadre — dézoome un peu le contenu (plus de calendrier visible). +const AGENDA_DEMO_VIEWPORT_WIDTH = 1040 +const AGENDA_DEMO_VIEWPORT_HEIGHT = 700 +const AGENDA_DEMO_ASPECT_RATIO = + AGENDA_DEMO_VIEWPORT_WIDTH / AGENDA_DEMO_VIEWPORT_HEIGHT + +export function UlticalAgendaDemo() { + const ref = useRef(null) + const [visible, setVisible] = useState(false) + + useEffect(() => { + const node = ref.current + if (!node) return + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setVisible(true) + observer.disconnect() + } + }, + { rootMargin: "120px 0px" } + ) + observer.observe(node) + return () => observer.disconnect() + }, []) + + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/components/landing/product/product-demos/ultical-invitation-demo.tsx b/components/landing/product/product-demos/ultical-invitation-demo.tsx new file mode 100644 index 0000000..6ef8ca6 --- /dev/null +++ b/components/landing/product/product-demos/ultical-invitation-demo.tsx @@ -0,0 +1,48 @@ +"use client" + +import { useMemo } from "react" +import { Icon } from "@iconify/react" +import { addDays, addHours, setHours, setMinutes, startOfDay } from "date-fns" +import { CalendarInvitationPreview } from "@/components/gmail/calendar-invitation-preview" +import type { ParsedCalendarInvitation } from "@/lib/calendar-invitation" +import { ULTICAL_APP_NAME } from "@/lib/suite/page-metadata" + +function buildDemoInvitation(): ParsedCalendarInvitation { + const start = setMinutes(setHours(addDays(startOfDay(new Date()), 2), 14), 0) + return { + summary: "Appel client — Atelier Nord", + start, + end: addHours(start, 1), + organizer: { name: "Léa Fontaine", email: "lea.fontaine@atelier-nord.fr" }, + attendees: [ + { name: "Camille Visiteur", email: "camille@demo.ulti" }, + { name: "Vincent Morel", email: "vincent.morel@gmail.com" }, + { name: "Thomas Giraud", email: "thomas.giraud@proton.me" }, + ], + location: "UltiMeet", + description: + "Présentation UltiCal + intégration UltiMeet pour leur équipe.", + conferenceProvider: "ultimeet", + } +} + +/** Aperçu statique d'une invitation détectée dans le mail — réutilise le composant réel. */ +export function UlticalInvitationDemo() { + const invitation = useMemo(buildDemoInvitation, []) + + return ( +
+
+ +
+

+ + Invitation détectée dans Ultimail — RSVP en un clic, rien n'est envoyé. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ultical-scheduling-demo.tsx b/components/landing/product/product-demos/ultical-scheduling-demo.tsx new file mode 100644 index 0000000..7e06af6 --- /dev/null +++ b/components/landing/product/product-demos/ultical-scheduling-demo.tsx @@ -0,0 +1,135 @@ +"use client" + +import { Icon } from "@iconify/react" +import { addDays, format, startOfDay } from "date-fns" +import { fr } from "date-fns/locale" +import { cn } from "@/lib/utils" + +const ACCENT = "#FBBC04" + +const SLOTS = ["09:00", "09:30", "10:00", "11:30", "14:00", "15:30"] as const +const SELECTED_SLOT = "10:00" + +function buildDays() { + const base = startOfDay(new Date()) + return Array.from({ length: 5 }, (_, index) => { + const date = addDays(base, index + 1) + return { + key: format(date, "yyyy-MM-dd"), + weekday: format(date, "EEE", { locale: fr }), + day: format(date, "d", { locale: fr }), + busy: index === 2, + } + }) +} + +/** Page de réservation type Calendly — aperçu statique sur les disponibilités réelles. */ +export function UlticalSchedulingDemo() { + const days = buildDays() + const selectedDay = days.find((day) => !day.busy) ?? days[0]! + + return ( +
+
+
+
+
+ + Camille Visiteur +
+

+ Rendez-vous découverte +

+
    +
  • + + 30 minutes +
  • +
  • + + Visio UltiMeet (lien généré) +
  • +
  • + + Europe/Paris (GMT+2) +
  • +
+

+ Les créneaux occupés sont masqués automatiquement d'après votre + agenda CalDAV — aucune double réservation. +

+
+ +
+

+ Choisissez un créneau +

+ +
+ {days.map((day) => { + const active = day.key === selectedDay.key + return ( +
+ {day.weekday} + {day.day} +
+ ) + })} +
+ +
+ {SLOTS.map((slot) => { + const selected = slot === SELECTED_SLOT + return ( + + ) + })} +
+ + +
+
+
+

+ + Page de réservation publique — disponibilités free/busy, rien n'est réservé. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ulticards-directory-demo.tsx b/components/landing/product/product-demos/ulticards-directory-demo.tsx new file mode 100644 index 0000000..1cc0eb1 --- /dev/null +++ b/components/landing/product/product-demos/ulticards-directory-demo.tsx @@ -0,0 +1,55 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import { ProductDemoFrame } from "@/components/landing/product/product-demo-frame" +import { ScaledPreviewIframe } from "@/components/landing/product/scaled-preview-iframe" + +// Viewport logique « grand écran » : l'annuaire est rendu plus large puis réduit +// pour fit dans le cadre — dézoome un peu le contenu (plus de fiches visibles). +const DIRECTORY_DEMO_VIEWPORT_WIDTH = 1040 +const DIRECTORY_DEMO_VIEWPORT_HEIGHT = 700 +const DIRECTORY_DEMO_ASPECT_RATIO = + DIRECTORY_DEMO_VIEWPORT_WIDTH / DIRECTORY_DEMO_VIEWPORT_HEIGHT + +export function UlticardsDirectoryDemo() { + const ref = useRef(null) + const [visible, setVisible] = useState(false) + + useEffect(() => { + const node = ref.current + if (!node) return + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setVisible(true) + observer.disconnect() + } + }, + { rootMargin: "120px 0px" } + ) + observer.observe(node) + return () => observer.disconnect() + }, []) + + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/components/landing/product/product-demos/ulticards-discovery-demo.tsx b/components/landing/product/product-demos/ulticards-discovery-demo.tsx new file mode 100644 index 0000000..70e3da0 --- /dev/null +++ b/components/landing/product/product-demos/ulticards-discovery-demo.tsx @@ -0,0 +1,134 @@ +"use client" + +import { Icon } from "@iconify/react" +import { cn } from "@/lib/utils" + +const ACCENT = "#4285F4" + +type DiscoveredProfile = { + name: string + email: string + messages: number + enriched: string + status: "suggested" | "filtered" + reason: string +} + +const PROFILES: DiscoveredProfile[] = [ + { + name: "Léa Fontaine", + email: "lea.fontaine@atelier-nord.fr", + messages: 18, + enriched: "Directrice · Atelier Nord", + status: "suggested", + reason: "Société & poste extraits de la signature", + }, + { + name: "Vincent Morel", + email: "vincent.morel@gmail.com", + messages: 7, + enriched: "+33 6 12 34 56 78", + status: "suggested", + reason: "Téléphone détecté dans les échanges", + }, + { + name: "Newsletter Produit", + email: "news@produit.io", + messages: 42, + enriched: "Liste de diffusion", + status: "filtered", + reason: "Écarté automatiquement (mailing-list)", + }, +] + +/** Aperçu statique de la découverte de contacts depuis le mail + enrichissement IA. */ +export function UlticardsDiscoveryDemo() { + return ( +
+
+
+ + Découverte de contacts + + + 3 comptes scannés + +
+ +
+
+ + + Analyse des messages + + 1 248 / 1 248 +
+
+ +
+
+ +
    + {PROFILES.map((profile) => { + const filtered = profile.status === "filtered" + return ( +
  • + + {profile.name.charAt(0)} + +
    +
    + + {profile.name} + + + {profile.messages} messages + +
    +

    {profile.email}

    +

    + + {profile.enriched} + · + {profile.reason} +

    +
    + {filtered ? ( + + ) : ( + + + Ajouter + + )} +
  • + ) + })} +
+
+

+ + Aperçu statique — la découverte tourne sur votre infrastructure, rien n'est partagé. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ulticards-merge-demo.tsx b/components/landing/product/product-demos/ulticards-merge-demo.tsx new file mode 100644 index 0000000..d741c18 --- /dev/null +++ b/components/landing/product/product-demos/ulticards-merge-demo.tsx @@ -0,0 +1,131 @@ +"use client" + +import { Icon } from "@iconify/react" + +const ACCENT = "#4285F4" + +type SourceCard = { + origin: string + originIcon: string + fields: { icon: string; value: string; kept: boolean }[] +} + +const SOURCES: SourceCard[] = [ + { + origin: "Importé de Google", + originIcon: "logos:google-icon", + fields: [ + { icon: "mdi:email-outline", value: "marc.dubois@example.com", kept: true }, + { icon: "mdi:phone-outline", value: "—", kept: false }, + { icon: "mdi:office-building-outline", value: "Studio Lumen", kept: true }, + ], + }, + { + origin: "Découvert dans le mail", + originIcon: "mdi:email-search-outline", + fields: [ + { icon: "mdi:email-outline", value: "m.dubois@studio-lumen.fr", kept: true }, + { icon: "mdi:phone-outline", value: "+33 6 98 76 54 32", kept: true }, + { icon: "mdi:office-building-outline", value: "—", kept: false }, + ], + }, +] + +const MERGED = [ + { icon: "mdi:email-outline", value: "marc.dubois@example.com" }, + { icon: "mdi:email-outline", value: "m.dubois@studio-lumen.fr" }, + { icon: "mdi:phone-outline", value: "+33 6 98 76 54 32" }, + { icon: "mdi:office-building-outline", value: "Studio Lumen" }, +] + +/** Aperçu statique de la fusion de doublons — deux fiches sources combinées en une. */ +export function UlticardsMergeDemo() { + return ( +
+
+
+ + Doublon détecté + + Marc Dubois + +
+ +
+ {SOURCES.map((card) => ( +
+

+ + {card.origin} +

+
    + {card.fields.map((field, index) => ( +
  • + + {field.value} + {field.kept ? ( + + ) : null} +
  • + ))} +
+
+ ))} +
+ +
+ + Fiche fusionnée + +
+ +
+
+ + M + +
+

Marc Dubois

+

Studio Lumen

+
+ + + Fusionner + +
+
    + {MERGED.map((field, index) => ( +
  • + + {field.value} +
  • + ))} +
+
+
+

+ + Aperçu statique — import vCard, CSV ou Google, puis fusion en gardant le meilleur de chaque fiche. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ultidrive-browser-demo.tsx b/components/landing/product/product-demos/ultidrive-browser-demo.tsx new file mode 100644 index 0000000..4f50dae --- /dev/null +++ b/components/landing/product/product-demos/ultidrive-browser-demo.tsx @@ -0,0 +1,66 @@ +"use client" + +import { useLayoutEffect, useRef, useState } from "react" +import { ProductDemoFrame } from "@/components/landing/product/product-demo-frame" +import { ScaledPreviewIframe } from "@/components/landing/product/scaled-preview-iframe" + +// Viewport logique réduit : l'explorateur est rendu plus petit puis agrandi +// dans le cadre — zoom ~25 % par rapport à la référence 1440×900. +export const ULTIDRIVE_BROWSER_DEMO_VIEWPORT_WIDTH = 1080 +export const ULTIDRIVE_BROWSER_DEMO_VIEWPORT_HEIGHT = 675 +export const ULTIDRIVE_BROWSER_DEMO_ASPECT_RATIO = + ULTIDRIVE_BROWSER_DEMO_VIEWPORT_WIDTH / ULTIDRIVE_BROWSER_DEMO_VIEWPORT_HEIGHT + +function isInDemoViewport(node: HTMLElement, margin = 120): boolean { + const rect = node.getBoundingClientRect() + return rect.top < window.innerHeight + margin && rect.bottom > -margin +} + +export function UltidriveBrowserDemo() { + const ref = useRef(null) + const [visible, setVisible] = useState(false) + + useLayoutEffect(() => { + const node = ref.current + if (!node) return + + if (isInDemoViewport(node)) { + setVisible(true) + return + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setVisible(true) + observer.disconnect() + } + }, + { rootMargin: "120px 0px" } + ) + observer.observe(node) + return () => observer.disconnect() + }, []) + + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/components/landing/product/product-demos/ultidrive-docs-demo.tsx b/components/landing/product/product-demos/ultidrive-docs-demo.tsx new file mode 100644 index 0000000..8177918 --- /dev/null +++ b/components/landing/product/product-demos/ultidrive-docs-demo.tsx @@ -0,0 +1,55 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import { ProductDemoFrame } from "@/components/landing/product/product-demo-frame" +import { ScaledPreviewIframe } from "@/components/landing/product/scaled-preview-iframe" + +// Viewport logique « grand écran » réduit de 20 % (1440→1200) pour zoomer le +// contenu de +20 % sans coupe. Le ratio (1.6) reste celui du parent. +export const ULTIDOCS_DEMO_VIEWPORT_WIDTH = 1200 +export const ULTIDOCS_DEMO_VIEWPORT_HEIGHT = 750 +export const ULTIDOCS_DEMO_ASPECT_RATIO = + ULTIDOCS_DEMO_VIEWPORT_WIDTH / ULTIDOCS_DEMO_VIEWPORT_HEIGHT + +export function UltidriveDocsDemo() { + const ref = useRef(null) + const [visible, setVisible] = useState(false) + + useEffect(() => { + const node = ref.current + if (!node) return + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setVisible(true) + observer.disconnect() + } + }, + { rootMargin: "120px 0px" } + ) + observer.observe(node) + return () => observer.disconnect() + }, []) + + return ( +
+ +
+ +
+
+
+ ) +} diff --git a/components/landing/product/product-demos/ultidrive-share-demo.tsx b/components/landing/product/product-demos/ultidrive-share-demo.tsx new file mode 100644 index 0000000..a9b8f78 --- /dev/null +++ b/components/landing/product/product-demos/ultidrive-share-demo.tsx @@ -0,0 +1,16 @@ +"use client" + +import { Icon } from "@iconify/react" +import { UltidriveSharePreview } from "@/components/landing/product/product-demos/ultidrive-share-preview" + +export function UltidriveShareDemo() { + return ( +
+ +

+ + Partage par lien, utilisateurs et groupes — permissions héritées du dossier parent. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ultidrive-share-preview.tsx b/components/landing/product/product-demos/ultidrive-share-preview.tsx new file mode 100644 index 0000000..ca0eec6 --- /dev/null +++ b/components/landing/product/product-demos/ultidrive-share-preview.tsx @@ -0,0 +1,166 @@ +"use client" + +import { + Building2, + Globe, + Link2, + Shield, + Trash2, + UserRound, +} from "lucide-react" +import { + DRIVE_BTN_GHOST, + DRIVE_BTN_PRIMARY, + DRIVE_DIALOG_CONTENT, + DRIVE_DIALOG_FOOTER, + DRIVE_DIALOG_HEADER, + DRIVE_FIELD_CLASS, + DRIVE_TEXT_PRIMARY, + DRIVE_TEXT_SECONDARY, + DRIVE_TEXT_TITLE, +} from "@/lib/drive/drive-dialog-styles" +import { cn } from "@/lib/utils" + +const EXISTING_SHARES = [ + { + id: "alice", + label: "Alice Martin", + role: "Éditeur", + icon: UserRound, + }, + { + id: "team", + label: "Équipe Produit", + role: "Lecteur", + icon: Building2, + }, + { + id: "link", + label: "Lien public", + role: "Lecteur", + icon: Link2, + password: true, + }, +] as const + +/** Modale de partage flottante — aperçu statique, sans overlay ni fenêtre navigateur. */ +export function UltidriveSharePreview() { + return ( +
+
+
+

+ Partager « Roadmap Q3 — Produit » +

+
+ +
+
+ +
+ +
+

Personnes

+
    + {EXISTING_SHARES.map((share) => { + const ShareIcon = share.icon + return ( +
  • +
    + +
    +

    + {share.label} +

    + {"password" in share && share.password ? ( + + ) : null} + + {share.role} + + +
  • + ) + })} +
+
+ +
+

Accès général

+
+
+ +
+
+

Lien public

+

+ Toute personne disposant du lien peut consulter l'élément. +

+
+ + Lecteur + +
+
+
+ +
+ + +
+
+
+ ) +} diff --git a/components/landing/product/product-demos/ultimail-automation-demo.tsx b/components/landing/product/product-demos/ultimail-automation-demo.tsx new file mode 100644 index 0000000..91e53d8 --- /dev/null +++ b/components/landing/product/product-demos/ultimail-automation-demo.tsx @@ -0,0 +1,18 @@ +"use client" + +import { ProductDemoFrame } from "@/components/landing/product/product-demo-frame" +import { WorkflowFlowPreview } from "@/components/landing/product/product-demos/workflow-flow-preview" + +export function UltimailAutomationDemo() { + return ( + +
+ +
+
+ ) +} diff --git a/components/landing/product/product-demos/ultimail-compose-demo.tsx b/components/landing/product/product-demos/ultimail-compose-demo.tsx new file mode 100644 index 0000000..8ccd72f --- /dev/null +++ b/components/landing/product/product-demos/ultimail-compose-demo.tsx @@ -0,0 +1,74 @@ +"use client" + +import { useLayoutEffect, useRef } from "react" +import { Icon } from "@iconify/react" +import { ComposeWindow } from "@/components/gmail/compose/compose-window" +import { ProductMailDemoShell } from "@/components/landing/product/product-demos/product-mail-demo-shell" +import { + type ComposeOpenPreset, + useComposeActions, + useComposeWindows, +} from "@/lib/compose-context" + +const COMPOSE_PRESET: ComposeOpenPreset = { + from: { + name: "Camille Visiteur", + email: "camille@demo.ulti", + defaultSignatureId: null, + }, + to: [{ name: "Alice Martin", email: "alice.martin@yahoo.fr" }], + subject: "Proposition de rendez-vous — mardi 14h", + bodyHtml: + "

Bonjour Alice,

Suite à notre échange, je vous propose un créneau mardi à 14h pour finaliser le projet. Dites-moi si cela vous convient.

— Cordialement,
Jean

", + autoInsertSignature: false, + focusToOnMount: false, + focusBodyOnMount: false, + placement: "dock", +} + +function ProductComposeDemoInner() { + const { openComposeWithInitial } = useComposeActions() + const { composeWindows } = useComposeWindows() + const seeded = useRef(false) + + useLayoutEffect(() => { + if (seeded.current) return + seeded.current = true + openComposeWithInitial(COMPOSE_PRESET) + }, [openComposeWithInitial]) + + useLayoutEffect(() => { + if (!seeded.current || composeWindows.length > 0) return + openComposeWithInitial(COMPOSE_PRESET) + }, [composeWindows.length, openComposeWithInitial]) + + const compose = composeWindows[0] + + if (!compose) { + return ( +
+ Chargement du compositeur… +
+ ) + } + + return ( +
+ +
+ ) +} + +export function UltimailComposeDemo() { + return ( +
+ + + +

+ + Compositeur réel — mise en forme, PJ et envoi programmé. Rien n'est envoyé. +

+
+ ) +} diff --git a/components/landing/product/product-demos/ultimail-demo-workflow.ts b/components/landing/product/product-demos/ultimail-demo-workflow.ts new file mode 100644 index 0000000..a42dd24 --- /dev/null +++ b/components/landing/product/product-demos/ultimail-demo-workflow.ts @@ -0,0 +1,57 @@ +import type { RuleEditorState } from "@/lib/mail-automation/types" + +/** Règle pré-remplie pour la démo produit Ultimail (éditeur no-code). */ +export function createUltimailProductDemoRuleState(): RuleEditorState { + const startId = "product-demo-start" + const condId = "product-demo-cond" + const actionsId = "product-demo-actions" + const endId = "product-demo-end" + + return { + name: "Tri factures → Comptabilité", + priority: 10, + is_active: true, + rule_kind: "rule", + workflow: { + version: 1, + kind: "rule", + triggers: { + operator: "or", + groups: [ + { + operator: "and", + items: [{ type: "message_received" }], + }, + ], + }, + variables: [], + nodes: [ + { id: startId, type: "start", position: { x: 40, y: 160 }, data: {} }, + { + id: condId, + type: "condition", + position: { x: 240, y: 140 }, + data: { field: "subject", operator: "contains", value: "facture" }, + }, + { + id: actionsId, + type: "actions", + position: { x: 480, y: 120 }, + data: { + actions: [ + { type: "label", value: "Comptabilité" }, + { type: "archive", value: "" }, + ], + }, + }, + { id: endId, type: "end", position: { x: 720, y: 160 }, data: {} }, + ], + edges: [ + { id: "product-demo-e1", source: startId, target: condId }, + { id: "product-demo-e2", source: condId, target: actionsId, sourceHandle: "true" }, + { id: "product-demo-e3", source: actionsId, target: endId }, + { id: "product-demo-e4", source: condId, target: endId, sourceHandle: "false" }, + ], + }, + } +} diff --git a/components/landing/product/product-demos/ultimail-inbox-demo.tsx b/components/landing/product/product-demos/ultimail-inbox-demo.tsx new file mode 100644 index 0000000..40ee2f1 --- /dev/null +++ b/components/landing/product/product-demos/ultimail-inbox-demo.tsx @@ -0,0 +1,48 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import { ProductDemoFrame } from "@/components/landing/product/product-demo-frame" + +export function UltimailInboxDemo() { + const ref = useRef(null) + const [visible, setVisible] = useState(false) + + useEffect(() => { + const node = ref.current + if (!node) return + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setVisible(true) + observer.disconnect() + } + }, + { rootMargin: "120px 0px" } + ) + observer.observe(node) + return () => observer.disconnect() + }, []) + + return ( +
+ + {visible ? ( +