diff --git a/apps/clients/index.html b/apps/clients/index.html
new file mode 100644
index 0000000..9fb3e81
--- /dev/null
+++ b/apps/clients/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Xtablo — Client Portal
+
+
+
+
+
+
diff --git a/apps/clients/package.json b/apps/clients/package.json
new file mode 100644
index 0000000..088606a
--- /dev/null
+++ b/apps/clients/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@xtablo/clients",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 5175",
+ "build": "tsc -b && vite build --mode production",
+ "build:staging": "tsc -b && vite build --mode staging",
+ "build:prod": "tsc -b && vite build --mode production",
+ "deploy": "wrangler deploy",
+ "typecheck": "tsc -b",
+ "lint": "biome check .",
+ "lint:fix": "biome check --write .",
+ "format": "biome format --write .",
+ "preview": "vite preview",
+ "clean": "rm -rf dist .vite tsconfig.tsbuildinfo node_modules/.vite"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "2.2.5",
+ "@cloudflare/vite-plugin": "^1.9.4",
+ "@tailwindcss/vite": "^4.0.14",
+ "@types/react": "19.0.10",
+ "@types/react-dom": "19.0.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "tailwindcss": "^4.0.14",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^5.7.0",
+ "vite": "^6.2.2",
+ "vite-tsconfig-paths": "^5.1.4",
+ "wrangler": "^4.24.3"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.69.0",
+ "@xtablo/shared": "workspace:*",
+ "@xtablo/shared-types": "workspace:*",
+ "@xtablo/tablo-views": "workspace:*",
+ "@xtablo/ui": "workspace:*",
+ "@xtablo/chat-ui": "workspace:*",
+ "i18next": "^25.6.0",
+ "i18next-browser-languagedetector": "^8.2.0",
+ "lucide-react": "^0.460.0",
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "react-i18next": "^16.2.0",
+ "react-router-dom": "^7.9.4",
+ "tailwind-merge": "^3.0.2",
+ "zustand": "^5.0.5"
+ }
+}
diff --git a/apps/clients/src/App.tsx b/apps/clients/src/App.tsx
new file mode 100644
index 0000000..3683784
--- /dev/null
+++ b/apps/clients/src/App.tsx
@@ -0,0 +1,9 @@
+import AppRoutes from "./routes";
+
+export default function App() {
+ return (
+
+ );
+}
diff --git a/apps/clients/src/i18n.ts b/apps/clients/src/i18n.ts
new file mode 100644
index 0000000..334b18e
--- /dev/null
+++ b/apps/clients/src/i18n.ts
@@ -0,0 +1,31 @@
+import i18n from "i18next";
+import LanguageDetector from "i18next-browser-languagedetector";
+import { initReactI18next } from "react-i18next";
+import bookingEn from "./locales/en/booking.json";
+// Import translation files
+import bookingFr from "./locales/fr/booking.json";
+
+i18n
+ .use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ resources: {
+ fr: {
+ booking: bookingFr,
+ },
+ en: {
+ booking: bookingEn,
+ },
+ },
+ fallbackLng: "fr",
+ defaultNS: "booking",
+ interpolation: {
+ escapeValue: false,
+ },
+ detection: {
+ order: ["localStorage", "navigator"],
+ caches: ["localStorage"],
+ },
+ });
+
+export default i18n;
diff --git a/apps/clients/src/locales/en/booking.json b/apps/clients/src/locales/en/booking.json
new file mode 100644
index 0000000..5c560df
--- /dev/null
+++ b/apps/clients/src/locales/en/booking.json
@@ -0,0 +1,3 @@
+{
+ "welcome": "Welcome"
+}
diff --git a/apps/clients/src/locales/fr/booking.json b/apps/clients/src/locales/fr/booking.json
new file mode 100644
index 0000000..ead2829
--- /dev/null
+++ b/apps/clients/src/locales/fr/booking.json
@@ -0,0 +1,3 @@
+{
+ "welcome": "Bienvenue"
+}
diff --git a/apps/clients/src/main.css b/apps/clients/src/main.css
new file mode 100644
index 0000000..a896ff7
--- /dev/null
+++ b/apps/clients/src/main.css
@@ -0,0 +1,1266 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.145 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.145 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.985 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.396 0.141 25.723);
+ --destructive-foreground: oklch(0.637 0.237 25.331);
+ --border: oklch(0.269 0 0);
+ --input: oklch(0.269 0 0);
+ --ring: oklch(0.439 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(0.269 0 0);
+ --sidebar-ring: oklch(0.439 0 0);
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-navbar-background: #292e39;
+ --color-navbar-darker: #171920;
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
+.str-chat {
+ --str-chat__primary-color: #8b7396;
+ --str-chat__active-primary-color: #6e5c7d;
+ --str-chat__surface-color: #f5f3f7;
+ --str-chat__secondary-surface-color: #e8e4ec;
+ --str-chat__primary-surface-color: #ebe7f0;
+ --str-chat__primary-surface-color-low-emphasis: #f2f0f5;
+ --str-chat__border-radius-circle: 6px;
+}
+
+.dark .str-chat {
+ --str-chat__primary-color: #a68bb5;
+ --str-chat__active-primary-color: #8b7396;
+ --str-chat__surface-color: rgba(120, 107, 130, 0.25);
+ --str-chat__secondary-surface-color: rgba(140, 130, 150, 0.18);
+ --str-chat__primary-surface-color: rgba(166, 139, 181, 0.12);
+ --str-chat__primary-surface-color-low-emphasis: rgba(166, 139, 181, 0.06);
+ --str-chat__background-color: rgba(110, 100, 120, 0.2);
+ --str-chat__secondary-background-color: rgba(80, 72, 88, 0.35);
+ --str-chat__border-color: rgba(120, 107, 130, 0.28);
+ --str-chat__text-color: #f5f3f7;
+ --str-chat__text-low-emphasis-color: #b8b0c0;
+ --str-chat__disabled-color: rgba(155, 143, 165, 0.35);
+}
+
+@keyframes gradient-x {
+ 0%,
+ 100% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+}
+
+.animate-gradient-x {
+ animation: gradient-x 15s ease infinite;
+}
+
+@keyframes wave-float {
+ 0%,
+ 100% {
+ transform: translateY(0px) rotate(0deg);
+ }
+ 25% {
+ transform: translateY(-20px) rotate(1deg);
+ }
+ 50% {
+ transform: translateY(-10px) rotate(-1deg);
+ }
+ 75% {
+ transform: translateY(-15px) rotate(0.5deg);
+ }
+}
+
+@keyframes wave-pulse {
+ 0%,
+ 100% {
+ transform: scale(1) rotateZ(0deg);
+ opacity: 0.3;
+ }
+ 25% {
+ transform: scale(1.05) rotateZ(1deg);
+ opacity: 0.4;
+ }
+ 50% {
+ transform: scale(0.95) rotateZ(-1deg);
+ opacity: 0.5;
+ }
+ 75% {
+ transform: scale(1.02) rotateZ(0.5deg);
+ opacity: 0.35;
+ }
+}
+
+.animate-wave-float {
+ animation: wave-float 8s ease-in-out infinite;
+}
+
+.animate-wave-float-delayed {
+ animation: wave-float 10s ease-in-out infinite 2s;
+}
+
+.animate-wave-float-slow {
+ animation: wave-float 12s ease-in-out infinite 4s;
+}
+
+.animate-wave-pulse {
+ animation: wave-pulse 6s ease-in-out infinite;
+}
+
+.animate-wave-pulse-delayed {
+ animation: wave-pulse 8s ease-in-out infinite 3s;
+}
+
+.animate-wave-pulse-slow {
+ animation: wave-pulse 10s ease-in-out infinite 1s;
+}
+
+/* Moving Animations */
+@keyframes move-right-slow {
+ 0% {
+ transform: translateX(-100px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 100px));
+ }
+}
+
+@keyframes move-right-medium {
+ 0% {
+ transform: translateX(-80px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 80px));
+ }
+}
+
+@keyframes move-right-fast {
+ 0% {
+ transform: translateX(-120px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 120px));
+ }
+}
+
+@keyframes move-down-slow {
+ 0% {
+ transform: translateY(-100px);
+ }
+ 100% {
+ transform: translateY(calc(100vh + 100px));
+ }
+}
+
+@keyframes move-down-medium {
+ 0% {
+ transform: translateY(-80px);
+ }
+ 100% {
+ transform: translateY(calc(100vh + 80px));
+ }
+}
+
+@keyframes move-diagonal-1 {
+ 0% {
+ transform: translate(-100px, -100px);
+ }
+ 100% {
+ transform: translate(calc(100vw + 100px), calc(100vh + 100px));
+ }
+}
+
+@keyframes move-diagonal-2 {
+ 0% {
+ transform: translate(-80px, -50px);
+ }
+ 100% {
+ transform: translate(calc(100vw + 80px), calc(100vh + 50px));
+ }
+}
+
+@keyframes move-diagonal-3 {
+ 0% {
+ transform: translate(-60px, -80px);
+ }
+ 100% {
+ transform: translate(calc(100vw + 60px), calc(100vh + 80px));
+ }
+}
+
+@keyframes orbit-1 {
+ 0% {
+ transform: translate(-50%, -50%) rotate(0deg) translateX(150px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-50%, -50%) rotate(360deg) translateX(150px) rotate(-360deg);
+ }
+}
+
+@keyframes orbit-2 {
+ 0% {
+ transform: translate(-50%, -50%) rotate(0deg) translateX(200px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-50%, -50%) rotate(-360deg) translateX(200px) rotate(360deg);
+ }
+}
+
+@keyframes orbit-3 {
+ 0% {
+ transform: translate(-50%, -50%) rotate(0deg) translateX(100px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-50%, -50%) rotate(360deg) translateX(100px) rotate(-360deg);
+ }
+}
+
+/* Gentle Animations */
+@keyframes spin-slow {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes spin-reverse {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(-360deg);
+ }
+}
+
+@keyframes bounce-gentle {
+ 0%,
+ 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+}
+
+@keyframes bounce-soft {
+ 0%,
+ 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-8px);
+ }
+}
+
+@keyframes pulse-gentle {
+ 0%,
+ 100% {
+ transform: scale(1);
+ opacity: 0.4;
+ }
+ 50% {
+ transform: scale(1.1);
+ opacity: 0.6;
+ }
+}
+
+@keyframes wiggle {
+ 0%,
+ 100% {
+ transform: rotate(0deg);
+ }
+ 25% {
+ transform: rotate(3deg);
+ }
+ 75% {
+ transform: rotate(-3deg);
+ }
+}
+
+@keyframes float-gentle {
+ 0%,
+ 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-5px);
+ }
+}
+
+@keyframes scale-gentle {
+ 0%,
+ 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+}
+
+@keyframes rotate-gentle {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(180deg);
+ }
+}
+
+@keyframes sway {
+ 0%,
+ 100% {
+ transform: translateX(0px);
+ }
+ 50% {
+ transform: translateX(10px);
+ }
+}
+
+/* Animation Classes */
+.animate-move-right-slow {
+ animation: move-right-slow 25s linear infinite;
+}
+.animate-move-right-medium {
+ animation: move-right-medium 20s linear infinite;
+}
+.animate-move-right-fast {
+ animation: move-right-fast 15s linear infinite;
+}
+.animate-move-down-slow {
+ animation: move-down-slow 30s linear infinite;
+}
+.animate-move-down-medium {
+ animation: move-down-medium 25s linear infinite;
+}
+.animate-move-diagonal-1 {
+ animation: move-diagonal-1 35s linear infinite;
+}
+.animate-move-diagonal-2 {
+ animation: move-diagonal-2 28s linear infinite;
+}
+.animate-move-diagonal-3 {
+ animation: move-diagonal-3 32s linear infinite;
+}
+.animate-orbit-1 {
+ animation: orbit-1 20s linear infinite;
+}
+.animate-orbit-2 {
+ animation: orbit-2 25s linear infinite reverse;
+}
+.animate-orbit-3 {
+ animation: orbit-3 15s linear infinite;
+}
+.animate-spin-slow {
+ animation: spin-slow 8s linear infinite;
+}
+.animate-spin-reverse {
+ animation: spin-reverse 6s linear infinite;
+}
+.animate-bounce-gentle {
+ animation: bounce-gentle 3s ease-in-out infinite;
+}
+.animate-bounce-soft {
+ animation: bounce-soft 4s ease-in-out infinite;
+}
+.animate-pulse-gentle {
+ animation: pulse-gentle 4s ease-in-out infinite;
+}
+.animate-wiggle {
+ animation: wiggle 2s ease-in-out infinite;
+}
+.animate-float-gentle {
+ animation: float-gentle 5s ease-in-out infinite;
+}
+.animate-scale-gentle {
+ animation: scale-gentle 6s ease-in-out infinite;
+}
+.animate-rotate-gentle {
+ animation: rotate-gentle 8s ease-in-out infinite;
+}
+.animate-sway {
+ animation: sway 3s ease-in-out infinite;
+}
+
+/* Enhanced Animations */
+@keyframes orbit-4 {
+ 0% {
+ transform: translate(-50%, -50%) rotate(0deg) translateX(250px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-50%, -50%) rotate(360deg) translateX(250px) rotate(-360deg);
+ }
+}
+
+@keyframes orbit-5 {
+ 0% {
+ transform: translate(-50%, -50%) rotate(0deg) translateX(120px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-50%, -50%) rotate(-360deg) translateX(120px) rotate(360deg);
+ }
+}
+
+@keyframes zigzag-1 {
+ 0% {
+ transform: translateX(-100px) translateY(0px);
+ }
+ 25% {
+ transform: translateX(25vw) translateY(-50px);
+ }
+ 50% {
+ transform: translateX(50vw) translateY(50px);
+ }
+ 75% {
+ transform: translateX(75vw) translateY(-30px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 100px)) translateY(20px);
+ }
+}
+
+@keyframes zigzag-2 {
+ 0% {
+ transform: translateX(-80px) translateY(0px);
+ }
+ 20% {
+ transform: translateX(20vw) translateY(40px);
+ }
+ 40% {
+ transform: translateX(40vw) translateY(-60px);
+ }
+ 60% {
+ transform: translateX(60vw) translateY(30px);
+ }
+ 80% {
+ transform: translateX(80vw) translateY(-40px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 80px)) translateY(0px);
+ }
+}
+
+@keyframes zigzag-3 {
+ 0% {
+ transform: translateX(-120px) translateY(0px);
+ }
+ 16% {
+ transform: translateX(16vw) translateY(-70px);
+ }
+ 33% {
+ transform: translateX(33vw) translateY(80px);
+ }
+ 50% {
+ transform: translateX(50vw) translateY(-50px);
+ }
+ 66% {
+ transform: translateX(66vw) translateY(60px);
+ }
+ 83% {
+ transform: translateX(83vw) translateY(-40px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 120px)) translateY(0px);
+ }
+}
+
+@keyframes spiral-1 {
+ 0% {
+ transform: translate(0px, 0px) rotate(0deg) scale(0.5);
+ }
+ 25% {
+ transform: translate(25vw, 25vh) rotate(90deg) scale(1);
+ }
+ 50% {
+ transform: translate(50vw, 50vh) rotate(180deg) scale(1.5);
+ }
+ 75% {
+ transform: translate(75vw, 75vh) rotate(270deg) scale(1);
+ }
+ 100% {
+ transform: translate(100vw, 100vh) rotate(360deg) scale(0.5);
+ }
+}
+
+@keyframes spiral-2 {
+ 0% {
+ transform: translate(0px, 0px) rotate(0deg) scale(1.5);
+ }
+ 25% {
+ transform: translate(-25vw, 25vh) rotate(-90deg) scale(0.8);
+ }
+ 50% {
+ transform: translate(-50vw, 50vh) rotate(-180deg) scale(0.5);
+ }
+ 75% {
+ transform: translate(-75vw, 75vh) rotate(-270deg) scale(1.2);
+ }
+ 100% {
+ transform: translate(-100vw, 100vh) rotate(-360deg) scale(1.5);
+ }
+}
+
+@keyframes float-random-1 {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 25% {
+ transform: translate(50px, -30px) rotate(45deg);
+ }
+ 50% {
+ transform: translate(-30px, 40px) rotate(-30deg);
+ }
+ 75% {
+ transform: translate(40px, 20px) rotate(60deg);
+ }
+}
+
+@keyframes float-random-2 {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 20% {
+ transform: translate(-40px, -50px) rotate(-45deg);
+ }
+ 40% {
+ transform: translate(60px, -20px) rotate(90deg);
+ }
+ 60% {
+ transform: translate(-20px, 60px) rotate(-60deg);
+ }
+ 80% {
+ transform: translate(30px, -40px) rotate(120deg);
+ }
+}
+
+@keyframes float-random-3 {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 33% {
+ transform: translate(70px, 30px) rotate(180deg);
+ }
+ 66% {
+ transform: translate(-50px, -40px) rotate(-90deg);
+ }
+}
+
+@keyframes float-random-4 {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 25% {
+ transform: translate(-60px, 50px) rotate(270deg);
+ }
+ 50% {
+ transform: translate(80px, -30px) rotate(180deg);
+ }
+ 75% {
+ transform: translate(-40px, -60px) rotate(90deg);
+ }
+}
+
+@keyframes wave-1 {
+ 0% {
+ transform: translateX(-100px) translateY(0px);
+ }
+ 25% {
+ transform: translateX(25vw) translateY(-80px);
+ }
+ 50% {
+ transform: translateX(50vw) translateY(0px);
+ }
+ 75% {
+ transform: translateX(75vw) translateY(80px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 100px)) translateY(0px);
+ }
+}
+
+@keyframes wave-2 {
+ 0% {
+ transform: translateX(-100px) translateY(0px);
+ }
+ 20% {
+ transform: translateX(20vw) translateY(60px);
+ }
+ 40% {
+ transform: translateX(40vw) translateY(-60px);
+ }
+ 60% {
+ transform: translateX(60vw) translateY(60px);
+ }
+ 80% {
+ transform: translateX(80vw) translateY(-60px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 100px)) translateY(0px);
+ }
+}
+
+@keyframes wave-3 {
+ 0% {
+ transform: translateX(-100px) translateY(0px);
+ }
+ 33% {
+ transform: translateX(33vw) translateY(-100px);
+ }
+ 66% {
+ transform: translateX(66vw) translateY(100px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 100px)) translateY(0px);
+ }
+}
+
+@keyframes wave-4 {
+ 0% {
+ transform: translateX(-100px) translateY(0px);
+ }
+ 16% {
+ transform: translateX(16vw) translateY(40px);
+ }
+ 33% {
+ transform: translateX(33vw) translateY(-80px);
+ }
+ 50% {
+ transform: translateX(50vw) translateY(40px);
+ }
+ 66% {
+ transform: translateX(66vw) translateY(-80px);
+ }
+ 83% {
+ transform: translateX(83vw) translateY(40px);
+ }
+ 100% {
+ transform: translateX(calc(100vw + 100px)) translateY(0px);
+ }
+}
+
+@keyframes corner-shoot-1 {
+ 0% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(100vw, 100vh) rotate(720deg);
+ }
+}
+
+@keyframes corner-shoot-2 {
+ 0% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-100vw, 100vh) rotate(-720deg);
+ }
+}
+
+@keyframes corner-shoot-3 {
+ 0% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(100vw, -100vh) rotate(720deg);
+ }
+}
+
+@keyframes corner-shoot-4 {
+ 0% {
+ transform: translate(0px, 0px) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-100vw, -100vh) rotate(-720deg);
+ }
+}
+
+@keyframes bounce-ball-1 {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px);
+ }
+ 25% {
+ transform: translate(200px, -150px);
+ }
+ 50% {
+ transform: translate(400px, 0px);
+ }
+ 75% {
+ transform: translate(600px, -100px);
+ }
+}
+
+@keyframes bounce-ball-2 {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px);
+ }
+ 33% {
+ transform: translate(-300px, -200px);
+ }
+ 66% {
+ transform: translate(-600px, 0px);
+ }
+}
+
+@keyframes bounce-ball-3 {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px);
+ }
+ 20% {
+ transform: translate(150px, -100px);
+ }
+ 40% {
+ transform: translate(300px, 50px);
+ }
+ 60% {
+ transform: translate(150px, -80px);
+ }
+ 80% {
+ transform: translate(-150px, 30px);
+ }
+}
+
+/* Crazy Animations */
+@keyframes spin-fast {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(720deg);
+ }
+}
+@keyframes pulse-fast {
+ 0%,
+ 100% {
+ transform: scale(0.8);
+ opacity: 0.3;
+ }
+ 50% {
+ transform: scale(1.3);
+ opacity: 0.8;
+ }
+}
+@keyframes wobble {
+ 0%,
+ 100% {
+ transform: rotate(0deg) scale(1);
+ }
+ 25% {
+ transform: rotate(5deg) scale(1.1);
+ }
+ 50% {
+ transform: rotate(-5deg) scale(0.9);
+ }
+ 75% {
+ transform: rotate(3deg) scale(1.05);
+ }
+}
+@keyframes shake {
+ 0%,
+ 100% {
+ transform: translateX(0px);
+ }
+ 25% {
+ transform: translateX(-10px);
+ }
+ 75% {
+ transform: translateX(10px);
+ }
+}
+@keyframes bounce-crazy {
+ 0%,
+ 100% {
+ transform: translateY(0px) scale(1);
+ }
+ 50% {
+ transform: translateY(-50px) scale(1.2);
+ }
+}
+@keyframes spin-wobble {
+ 0% {
+ transform: rotate(0deg) scale(1);
+ }
+ 25% {
+ transform: rotate(90deg) scale(1.1);
+ }
+ 50% {
+ transform: rotate(180deg) scale(0.9);
+ }
+ 75% {
+ transform: rotate(270deg) scale(1.05);
+ }
+ 100% {
+ transform: rotate(360deg) scale(1);
+ }
+}
+@keyframes flip {
+ 0% {
+ transform: rotateY(0deg);
+ }
+ 50% {
+ transform: rotateY(180deg);
+ }
+ 100% {
+ transform: rotateY(360deg);
+ }
+}
+@keyframes twirl {
+ 0% {
+ transform: rotate(0deg) translateX(0px);
+ }
+ 25% {
+ transform: rotate(90deg) translateX(20px);
+ }
+ 50% {
+ transform: rotate(180deg) translateX(0px);
+ }
+ 75% {
+ transform: rotate(270deg) translateX(-20px);
+ }
+ 100% {
+ transform: rotate(360deg) translateX(0px);
+ }
+}
+@keyframes dance {
+ 0%,
+ 100% {
+ transform: translateY(0px) rotate(0deg);
+ }
+ 25% {
+ transform: translateY(-20px) rotate(10deg);
+ }
+ 50% {
+ transform: translateY(10px) rotate(-5deg);
+ }
+ 75% {
+ transform: translateY(-15px) rotate(8deg);
+ }
+}
+@keyframes jiggle {
+ 0%,
+ 100% {
+ transform: rotate(0deg);
+ }
+ 25% {
+ transform: rotate(2deg) translateX(2px);
+ }
+ 50% {
+ transform: rotate(-2deg) translateX(-2px);
+ }
+ 75% {
+ transform: rotate(1deg) translateX(1px);
+ }
+}
+@keyframes vibrate {
+ 0%,
+ 100% {
+ transform: translate(0px, 0px);
+ }
+ 25% {
+ transform: translate(2px, -2px);
+ }
+ 50% {
+ transform: translate(-2px, 2px);
+ }
+ 75% {
+ transform: translate(2px, 2px);
+ }
+}
+@keyframes swing {
+ 0%,
+ 100% {
+ transform: rotate(0deg);
+ }
+ 25% {
+ transform: rotate(15deg);
+ }
+ 75% {
+ transform: rotate(-15deg);
+ }
+}
+@keyframes pendulum {
+ 0%,
+ 100% {
+ transform: rotate(0deg);
+ }
+ 50% {
+ transform: rotate(30deg);
+ }
+}
+@keyframes elastic {
+ 0%,
+ 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.3) rotate(180deg);
+ }
+}
+@keyframes rubber {
+ 0%,
+ 100% {
+ transform: scaleX(1) scaleY(1);
+ }
+ 25% {
+ transform: scaleX(1.2) scaleY(0.8);
+ }
+ 75% {
+ transform: scaleX(0.8) scaleY(1.2);
+ }
+}
+@keyframes rocket {
+ 0% {
+ transform: scale(0.5) rotate(0deg);
+ }
+ 100% {
+ transform: scale(2) rotate(360deg);
+ }
+}
+@keyframes comet {
+ 0% {
+ transform: scale(1) rotate(0deg);
+ opacity: 1;
+ }
+ 100% {
+ transform: scale(0.2) rotate(720deg);
+ opacity: 0.2;
+ }
+}
+@keyframes meteor {
+ 0% {
+ transform: scale(0.2) rotate(0deg);
+ opacity: 0.2;
+ }
+ 100% {
+ transform: scale(1.5) rotate(-720deg);
+ opacity: 1;
+ }
+}
+@keyframes blast {
+ 0%,
+ 100% {
+ transform: scale(1) rotate(0deg);
+ }
+ 50% {
+ transform: scale(2) rotate(180deg);
+ }
+}
+@keyframes spin-bounce {
+ 0%,
+ 100% {
+ transform: rotate(0deg) translateY(0px);
+ }
+ 50% {
+ transform: rotate(180deg) translateY(-30px);
+ }
+}
+@keyframes flip-bounce {
+ 0%,
+ 100% {
+ transform: rotateX(0deg) translateY(0px);
+ }
+ 50% {
+ transform: rotateX(180deg) translateY(-25px);
+ }
+}
+@keyframes scale-bounce {
+ 0%,
+ 100% {
+ transform: scale(1) translateY(0px);
+ }
+ 50% {
+ transform: scale(1.5) translateY(-40px);
+ }
+}
+
+/* New Animation Classes */
+.animate-orbit-4 {
+ animation: orbit-4 18s linear infinite;
+}
+.animate-orbit-5 {
+ animation: orbit-5 22s linear infinite reverse;
+}
+.animate-zigzag-1 {
+ animation: zigzag-1 18s linear infinite;
+}
+.animate-zigzag-2 {
+ animation: zigzag-2 22s linear infinite;
+}
+.animate-zigzag-3 {
+ animation: zigzag-3 16s linear infinite;
+}
+.animate-spiral-1 {
+ animation: spiral-1 30s linear infinite;
+}
+.animate-spiral-2 {
+ animation: spiral-2 25s linear infinite;
+}
+.animate-float-random-1 {
+ animation: float-random-1 8s ease-in-out infinite;
+}
+.animate-float-random-2 {
+ animation: float-random-2 10s ease-in-out infinite;
+}
+.animate-float-random-3 {
+ animation: float-random-3 12s ease-in-out infinite;
+}
+.animate-float-random-4 {
+ animation: float-random-4 9s ease-in-out infinite;
+}
+.animate-wave-1 {
+ animation: wave-1 20s linear infinite;
+}
+.animate-wave-2 {
+ animation: wave-2 24s linear infinite;
+}
+.animate-wave-3 {
+ animation: wave-3 18s linear infinite;
+}
+.animate-wave-4 {
+ animation: wave-4 26s linear infinite;
+}
+.animate-corner-shoot-1 {
+ animation: corner-shoot-1 15s linear infinite;
+}
+.animate-corner-shoot-2 {
+ animation: corner-shoot-2 18s linear infinite;
+}
+.animate-corner-shoot-3 {
+ animation: corner-shoot-3 20s linear infinite;
+}
+.animate-corner-shoot-4 {
+ animation: corner-shoot-4 16s linear infinite;
+}
+.animate-bounce-ball-1 {
+ animation: bounce-ball-1 12s ease-in-out infinite;
+}
+.animate-bounce-ball-2 {
+ animation: bounce-ball-2 14s ease-in-out infinite;
+}
+.animate-bounce-ball-3 {
+ animation: bounce-ball-3 10s ease-in-out infinite;
+}
+.animate-spin-fast {
+ animation: spin-fast 2s linear infinite;
+}
+.animate-pulse-fast {
+ animation: pulse-fast 1.5s ease-in-out infinite;
+}
+.animate-wobble {
+ animation: wobble 2s ease-in-out infinite;
+}
+.animate-shake {
+ animation: shake 0.5s ease-in-out infinite;
+}
+.animate-bounce-crazy {
+ animation: bounce-crazy 1s ease-in-out infinite;
+}
+.animate-spin-wobble {
+ animation: spin-wobble 4s ease-in-out infinite;
+}
+.animate-flip {
+ animation: flip 3s ease-in-out infinite;
+}
+.animate-twirl {
+ animation: twirl 5s ease-in-out infinite;
+}
+.animate-dance {
+ animation: dance 3s ease-in-out infinite;
+}
+.animate-jiggle {
+ animation: jiggle 1s ease-in-out infinite;
+}
+.animate-vibrate {
+ animation: vibrate 0.3s ease-in-out infinite;
+}
+.animate-swing {
+ animation: swing 4s ease-in-out infinite;
+}
+.animate-pendulum {
+ animation: pendulum 6s ease-in-out infinite;
+}
+.animate-elastic {
+ animation: elastic 4s ease-in-out infinite;
+}
+.animate-rubber {
+ animation: rubber 2s ease-in-out infinite;
+}
+.animate-rocket {
+ animation: rocket 8s ease-in-out infinite;
+}
+.animate-comet {
+ animation: comet 12s ease-in-out infinite;
+}
+.animate-meteor {
+ animation: meteor 10s ease-in-out infinite;
+}
+.animate-blast {
+ animation: blast 3s ease-in-out infinite;
+}
+.animate-spin-bounce {
+ animation: spin-bounce 4s ease-in-out infinite;
+}
+.animate-flip-bounce {
+ animation: flip-bounce 5s ease-in-out infinite;
+}
+.animate-scale-bounce {
+ animation: scale-bounce 3s ease-in-out infinite;
+}
+
+/* Animated Border Light */
+@keyframes border-light {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.animate-border-light {
+ position: relative;
+ border-radius: 1rem;
+}
+
+.animate-border-light::before {
+ content: "";
+ position: absolute;
+ inset: -2px;
+ background: conic-gradient(
+ from 0deg,
+ transparent 0deg,
+ transparent 270deg,
+ rgba(168, 85, 247, 0.8) 300deg,
+ rgba(147, 51, 234, 1) 330deg,
+ rgba(168, 85, 247, 0.8) 360deg,
+ transparent 30deg,
+ transparent 360deg
+ );
+ border-radius: inherit;
+ animation: border-light 3s linear infinite;
+ z-index: -1;
+}
+
+.animate-border-light::after {
+ content: "";
+ position: absolute;
+ inset: 2px;
+ background: inherit;
+ border-radius: inherit;
+ z-index: -1;
+}
diff --git a/apps/clients/src/main.tsx b/apps/clients/src/main.tsx
new file mode 100644
index 0000000..70ff399
--- /dev/null
+++ b/apps/clients/src/main.tsx
@@ -0,0 +1,28 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { queryClient } from "@xtablo/shared";
+import { SessionProvider } from "@xtablo/shared/contexts/SessionContext";
+import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
+import { Toaster } from "@xtablo/ui/components/sonner";
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { BrowserRouter as Router } from "react-router-dom";
+import App from "./App";
+
+import "@xtablo/ui/styles/globals.css";
+import "./main.css";
+import "./i18n";
+
+createRoot(document.getElementById("client-root")!).render(
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/apps/clients/src/routes.tsx b/apps/clients/src/routes.tsx
new file mode 100644
index 0000000..4f94f7f
--- /dev/null
+++ b/apps/clients/src/routes.tsx
@@ -0,0 +1,11 @@
+import { Route, Routes } from "react-router-dom";
+
+export default function AppRoutes() {
+ return (
+
+ Auth callback placeholder} />
+ Tablo view placeholder} />
+ Client portal placeholder} />
+
+ );
+}
diff --git a/apps/clients/tsconfig.json b/apps/clients/tsconfig.json
new file mode 100644
index 0000000..64a1401
--- /dev/null
+++ b/apps/clients/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@xtablo/ui": ["../../packages/ui/src"],
+ "@xtablo/ui/*": ["../../packages/ui/src/*"],
+ "@xtablo/shared": ["../../packages/shared/src"],
+ "@xtablo/shared/*": ["../../packages/shared/src/*"],
+ "@xtablo/tablo-views": ["../../packages/tablo-views/src"],
+ "@xtablo/tablo-views/*": ["../../packages/tablo-views/src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": []
+}
diff --git a/apps/clients/vite.config.ts b/apps/clients/vite.config.ts
new file mode 100644
index 0000000..f9f5138
--- /dev/null
+++ b/apps/clients/vite.config.ts
@@ -0,0 +1,22 @@
+import { cloudflare } from "@cloudflare/vite-plugin";
+import tailwindcss from "@tailwindcss/vite";
+import react from "@vitejs/plugin-react";
+import { defineConfig, type PluginOption } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig(({ mode }) => {
+ const plugins: PluginOption[] = [
+ react(),
+ tailwindcss(),
+ tsconfigPaths(),
+ ];
+
+ if (mode !== "test" && process.env.VITEST !== "true") {
+ plugins.push(cloudflare());
+ }
+
+ return {
+ plugins,
+ server: { cors: false },
+ };
+});
diff --git a/apps/clients/worker/index.ts b/apps/clients/worker/index.ts
new file mode 100644
index 0000000..0dcbb86
--- /dev/null
+++ b/apps/clients/worker/index.ts
@@ -0,0 +1,9 @@
+export default {
+ fetch(request: Request) {
+ const url = new URL(request.url);
+ if (url.pathname.startsWith("/api/")) {
+ return Response.json({ name: "Cloudflare" });
+ }
+ return new Response(null, { status: 404 });
+ },
+};
diff --git a/apps/clients/wrangler.toml b/apps/clients/wrangler.toml
new file mode 100644
index 0000000..13aff9a
--- /dev/null
+++ b/apps/clients/wrangler.toml
@@ -0,0 +1,16 @@
+name = "xtablo-clients"
+main = "worker/index.ts"
+compatibility_date = "2025-07-09"
+
+[assets]
+directory = "./dist/"
+not_found_handling = "single-page-application"
+
+[observability]
+enabled = true
+
+[env.staging]
+route = { pattern = "clients-staging.xtablo.com", custom_domain = true }
+
+[env.production]
+route = { pattern = "clients.xtablo.com", custom_domain = true }
diff --git a/package.json b/package.json
index 50b96ef..bf99826 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"dev": "turbo dev",
"dev:main": "turbo dev --filter=@xtablo/main",
"dev:external": "turbo dev --filter=@xtablo/external",
+ "dev:clients": "turbo dev --filter=@xtablo/clients",
"dev:api": "turbo dev --filter=@xtablo/api",
"deploy:main:staging": "turbo deploy:staging --filter=@xtablo/main",
"deploy:main:prod": "turbo deploy:prod --filter=@xtablo/main",