create-quadrokit 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/templates/README.md +4 -4
  3. package/templates/admin-shell/src/components/layout/AppShell.tsx +55 -0
  4. package/templates/admin-shell/src/components/layout/LanguageSwitcher.tsx +37 -0
  5. package/templates/admin-shell/src/components/layout/NotificationsMenu.tsx +79 -0
  6. package/templates/admin-shell/src/components/layout/PageHeading.tsx +40 -0
  7. package/templates/admin-shell/src/components/layout/Sidebar.tsx +65 -0
  8. package/templates/admin-shell/src/components/layout/ThemeMenu.tsx +90 -0
  9. package/templates/admin-shell/src/components/layout/TopHeader.tsx +78 -0
  10. package/templates/admin-shell/src/i18n.ts +16 -2
  11. package/templates/admin-shell/src/locales/en.json +48 -1
  12. package/templates/admin-shell/src/locales/fr.json +62 -0
  13. package/templates/admin-shell/src/main.tsx +8 -2
  14. package/templates/admin-shell/src/pages/HomePage.tsx +1 -2
  15. package/templates/admin-shell/src/pages/SampleDataPage.tsx +37 -0
  16. package/templates/admin-shell/src/router.tsx +13 -4
  17. package/templates/admin-shell/src/types/router.ts +4 -0
  18. package/templates/dashboard/package.json +1 -0
  19. package/templates/dashboard/src/components/dashboard/ChartCard.tsx +29 -0
  20. package/templates/dashboard/src/components/dashboard/DashboardOverview.tsx +79 -0
  21. package/templates/dashboard/src/components/dashboard/KpiCard.tsx +31 -0
  22. package/templates/dashboard/src/components/dashboard/RegionsBarChart.tsx +45 -0
  23. package/templates/dashboard/src/components/dashboard/SeriesBarChart.tsx +55 -0
  24. package/templates/dashboard/src/components/dashboard/TrendLineChart.tsx +55 -0
  25. package/templates/dashboard/src/components/dashboard/chartTheme.ts +18 -0
  26. package/templates/dashboard/src/components/dashboard/dashboardMockData.ts +51 -0
  27. package/templates/dashboard/src/components/dashboard/index.ts +1 -0
  28. package/templates/dashboard/src/components/layout/AppShell.tsx +55 -0
  29. package/templates/dashboard/src/components/layout/LanguageSwitcher.tsx +37 -0
  30. package/templates/dashboard/src/components/layout/NotificationsMenu.tsx +79 -0
  31. package/templates/dashboard/src/components/layout/PageHeading.tsx +40 -0
  32. package/templates/dashboard/src/components/layout/Sidebar.tsx +65 -0
  33. package/templates/dashboard/src/components/layout/ThemeMenu.tsx +90 -0
  34. package/templates/dashboard/src/components/layout/TopHeader.tsx +78 -0
  35. package/templates/dashboard/src/i18n.ts +16 -2
  36. package/templates/dashboard/src/locales/en.json +66 -1
  37. package/templates/dashboard/src/locales/fr.json +80 -0
  38. package/templates/dashboard/src/main.tsx +8 -2
  39. package/templates/dashboard/src/pages/HomePage.tsx +17 -2
  40. package/templates/dashboard/src/pages/SampleDataPage.tsx +37 -0
  41. package/templates/dashboard/src/router.tsx +13 -4
  42. package/templates/dashboard/src/types/router.ts +4 -0
  43. package/templates/ecommerce/src/components/layout/AppShell.tsx +55 -0
  44. package/templates/ecommerce/src/components/layout/LanguageSwitcher.tsx +37 -0
  45. package/templates/ecommerce/src/components/layout/NotificationsMenu.tsx +79 -0
  46. package/templates/ecommerce/src/components/layout/PageHeading.tsx +40 -0
  47. package/templates/ecommerce/src/components/layout/Sidebar.tsx +65 -0
  48. package/templates/ecommerce/src/components/layout/ThemeMenu.tsx +90 -0
  49. package/templates/ecommerce/src/components/layout/TopHeader.tsx +78 -0
  50. package/templates/ecommerce/src/i18n.ts +16 -2
  51. package/templates/ecommerce/src/locales/en.json +43 -1
  52. package/templates/ecommerce/src/locales/fr.json +62 -0
  53. package/templates/ecommerce/src/main.tsx +8 -2
  54. package/templates/ecommerce/src/pages/HomePage.tsx +1 -2
  55. package/templates/ecommerce/src/pages/SampleDataPage.tsx +37 -0
  56. package/templates/ecommerce/src/router.tsx +13 -4
  57. package/templates/ecommerce/src/types/router.ts +4 -0
  58. package/templates/website/src/components/layout/AppShell.tsx +55 -0
  59. package/templates/website/src/components/layout/LanguageSwitcher.tsx +37 -0
  60. package/templates/website/src/components/layout/NotificationsMenu.tsx +79 -0
  61. package/templates/website/src/components/layout/PageHeading.tsx +40 -0
  62. package/templates/website/src/components/layout/Sidebar.tsx +65 -0
  63. package/templates/website/src/components/layout/ThemeMenu.tsx +90 -0
  64. package/templates/website/src/components/layout/TopHeader.tsx +78 -0
  65. package/templates/website/src/i18n.ts +16 -2
  66. package/templates/website/src/locales/en.json +43 -1
  67. package/templates/website/src/locales/fr.json +63 -0
  68. package/templates/website/src/main.tsx +8 -2
  69. package/templates/website/src/pages/HomePage.tsx +1 -4
  70. package/templates/website/src/pages/SampleDataPage.tsx +37 -0
  71. package/templates/website/src/router.tsx +13 -4
  72. package/templates/website/src/types/router.ts +4 -0
  73. package/templates/admin-shell/src/components/AppShell.tsx +0 -68
  74. package/templates/admin-shell/src/pages/AgenciesPage.tsx +0 -83
  75. package/templates/dashboard/src/components/AppShell.tsx +0 -44
  76. package/templates/dashboard/src/pages/AgenciesPage.tsx +0 -83
  77. package/templates/ecommerce/src/components/AppShell.tsx +0 -44
  78. package/templates/ecommerce/src/pages/AgenciesPage.tsx +0 -83
  79. package/templates/website/src/components/AppShell.tsx +0 -44
  80. package/templates/website/src/pages/AgenciesPage.tsx +0 -83
@@ -0,0 +1,65 @@
1
+ import { cn } from '@quadrokit/ui'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { Link, NavLink } from 'react-router-dom'
4
+
5
+ export type SidebarProps = {
6
+ className?: string
7
+ onNavigate?: () => void
8
+ }
9
+
10
+ export function Sidebar({ className, onNavigate }: SidebarProps) {
11
+ const { t } = useTranslation()
12
+
13
+ const linkClass = ({ isActive }: { isActive: boolean }) =>
14
+ cn(
15
+ 'rounded-md px-3 py-2 text-sm transition-colors',
16
+ isActive
17
+ ? 'bg-primary/15 font-medium text-foreground'
18
+ : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
19
+ )
20
+
21
+ return (
22
+ <aside className={cn('flex h-full flex-col border-border bg-card/50', className)}>
23
+ <div className="flex h-14 items-center border-b border-border px-4">
24
+ <Link to="/" className="text-lg font-semibold tracking-tight" onClick={onNavigate}>
25
+ {t('app.title')}
26
+ </Link>
27
+ </div>
28
+ <nav className="flex-1 space-y-6 overflow-y-auto p-3" aria-label={t('layout.sidebar_nav')}>
29
+ <div>
30
+ <p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
31
+ {t('layout.nav_section_overview')}
32
+ </p>
33
+ <div className="flex flex-col gap-0.5">
34
+ <NavLink to="/" end className={linkClass} onClick={onNavigate}>
35
+ {t('app.nav_home')}
36
+ </NavLink>
37
+ <NavLink to="/sample-data" className={linkClass} onClick={onNavigate}>
38
+ {t('app.nav_sample_data')}
39
+ </NavLink>
40
+ </div>
41
+ </div>
42
+ <div>
43
+ <p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
44
+ {t('layout.nav_section_apps')}
45
+ </p>
46
+ <p className="px-3 text-sm text-muted-foreground">{t('layout.nav_apps_placeholder')}</p>
47
+ </div>
48
+ </nav>
49
+ <div className="border-t border-border p-3">
50
+ <div className="flex items-center gap-3 rounded-lg border border-border bg-background/80 p-2">
51
+ <div
52
+ className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/20 text-xs font-semibold text-primary"
53
+ aria-hidden
54
+ >
55
+ {t('layout.user_initials')}
56
+ </div>
57
+ <div className="min-w-0 flex-1">
58
+ <p className="truncate text-sm font-medium leading-tight">{t('layout.user_name')}</p>
59
+ <p className="truncate text-xs text-muted-foreground">{t('layout.user_role')}</p>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </aside>
64
+ )
65
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ Button,
3
+ type QuadroThemePreset,
4
+ quadroThemes,
5
+ themeLabels,
6
+ useDismissOnInteractOutside,
7
+ useTheme,
8
+ } from '@quadrokit/ui'
9
+ import { useRef, useState } from 'react'
10
+ import { useTranslation } from 'react-i18next'
11
+
12
+ /** Compact theme + color mode controls for the header (same behavior as `ThemeToolbar`). */
13
+ export function ThemeMenu() {
14
+ const { t } = useTranslation()
15
+ const { preset, setPreset, colorMode, setColorMode, resolvedMode } = useTheme()
16
+ const [open, setOpen] = useState(false)
17
+ const rootRef = useRef<HTMLDivElement>(null)
18
+
19
+ useDismissOnInteractOutside({
20
+ enabled: open,
21
+ containerRef: rootRef,
22
+ onDismiss: () => setOpen(false),
23
+ })
24
+
25
+ return (
26
+ <div ref={rootRef} className="relative">
27
+ <Button
28
+ type="button"
29
+ variant="outline"
30
+ className="h-9 px-3"
31
+ aria-expanded={open}
32
+ aria-haspopup="dialog"
33
+ onClick={() => setOpen((o) => !o)}
34
+ >
35
+ {t('layout.theme')}
36
+ </Button>
37
+ {open ? (
38
+ <div
39
+ className="absolute right-0 z-50 mt-1 w-72 rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-lg"
40
+ role="dialog"
41
+ aria-label={t('layout.theme')}
42
+ >
43
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
44
+ {t('layout.theme_presets')}
45
+ </p>
46
+ <div className="flex flex-wrap gap-1">
47
+ {quadroThemes.map((p: QuadroThemePreset) => (
48
+ <Button
49
+ key={p}
50
+ type="button"
51
+ size="sm"
52
+ variant={preset === p ? 'default' : 'outline'}
53
+ onClick={() => setPreset(p)}
54
+ >
55
+ {themeLabels[p]}
56
+ </Button>
57
+ ))}
58
+ </div>
59
+ <p className="mb-2 mt-3 text-xs font-medium text-muted-foreground">
60
+ {t('layout.theme_mode')}
61
+ </p>
62
+ <div className="flex flex-wrap gap-1">
63
+ {(
64
+ [
65
+ ['light', 'layout.mode_light'],
66
+ ['dark', 'layout.mode_dark'],
67
+ ['system', 'layout.mode_system'],
68
+ ] as const
69
+ ).map(([m, labelKey]) => (
70
+ <Button
71
+ key={m}
72
+ type="button"
73
+ size="sm"
74
+ variant={colorMode === m ? 'default' : 'ghost'}
75
+ onClick={() => setColorMode(m)}
76
+ >
77
+ {t(labelKey)}
78
+ </Button>
79
+ ))}
80
+ </div>
81
+ <p className="mt-2 text-xs text-muted-foreground">
82
+ {t('layout.theme_resolved', {
83
+ mode: resolvedMode === 'dark' ? t('layout.mode_dark') : t('layout.mode_light'),
84
+ })}
85
+ </p>
86
+ </div>
87
+ ) : null}
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,78 @@
1
+ import { Button, Input } from '@quadrokit/ui'
2
+ import { useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { LanguageSwitcher } from './LanguageSwitcher'
5
+ import { NotificationsMenu } from './NotificationsMenu'
6
+ import { ThemeMenu } from './ThemeMenu'
7
+
8
+ export type TopHeaderProps = {
9
+ onMenuClick: () => void
10
+ }
11
+
12
+ export function TopHeader({ onMenuClick }: TopHeaderProps) {
13
+ const { t } = useTranslation()
14
+ const [searchQuery, setSearchQuery] = useState('')
15
+
16
+ return (
17
+ <header className="sticky top-0 z-30 flex h-14 shrink-0 items-center gap-3 border-b border-border bg-background/95 px-3 backdrop-blur supports-[backdrop-filter]:bg-background/80 md:px-4">
18
+ <Button
19
+ type="button"
20
+ variant="outline"
21
+ size="icon"
22
+ className="h-9 w-9 md:hidden"
23
+ aria-label={t('layout.open_menu')}
24
+ onClick={onMenuClick}
25
+ >
26
+ <MenuIcon className="h-4 w-4" />
27
+ </Button>
28
+ <div className="min-w-0 flex-1 max-w-md">
29
+ <Input
30
+ type="search"
31
+ placeholder={t('layout.search_placeholder')}
32
+ className="h-9 bg-card"
33
+ aria-label={t('layout.search_placeholder')}
34
+ value={searchQuery}
35
+ onChange={(e) => setSearchQuery(e.target.value)}
36
+ autoComplete="off"
37
+ />
38
+ </div>
39
+ <div className="ml-auto flex shrink-0 items-center gap-2">
40
+ <LanguageSwitcher />
41
+ <ThemeMenu />
42
+ <NotificationsMenu />
43
+ <div className="hidden items-center gap-2 border-l border-border pl-3 sm:flex">
44
+ <div
45
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/15 text-xs font-semibold text-primary"
46
+ aria-hidden
47
+ >
48
+ {t('layout.user_initials')}
49
+ </div>
50
+ <div className="hidden leading-tight lg:block">
51
+ <p className="text-sm font-medium">{t('layout.user_name')}</p>
52
+ <p className="text-xs text-muted-foreground">{t('layout.user_role')}</p>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </header>
57
+ )
58
+ }
59
+
60
+ function MenuIcon({ className }: { className?: string }) {
61
+ return (
62
+ <svg
63
+ xmlns="http://www.w3.org/2000/svg"
64
+ viewBox="0 0 24 24"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ strokeWidth="2"
68
+ className={className}
69
+ aria-hidden="true"
70
+ focusable="false"
71
+ >
72
+ <title>Menu</title>
73
+ <line x1="4" x2="20" y1="6" y2="6" />
74
+ <line x1="4" x2="20" y1="12" y2="12" />
75
+ <line x1="4" x2="20" y1="18" y2="18" />
76
+ </svg>
77
+ )
78
+ }
@@ -1,10 +1,24 @@
1
1
  import i18n from 'i18next'
2
2
  import { initReactI18next } from 'react-i18next'
3
3
  import en from './locales/en.json'
4
+ import fr from './locales/fr.json'
5
+
6
+ function storedLng(): string | undefined {
7
+ try {
8
+ const v = localStorage.getItem('quadrokit:lng')
9
+ if (v === 'en' || v === 'fr') return v
10
+ } catch {
11
+ /* ignore */
12
+ }
13
+ return undefined
14
+ }
4
15
 
5
16
  void i18n.use(initReactI18next).init({
6
- resources: { en: { translation: en } },
7
- lng: 'en',
17
+ resources: {
18
+ en: { translation: en },
19
+ fr: { translation: fr },
20
+ },
21
+ lng: storedLng() ?? 'en',
8
22
  fallbackLng: 'en',
9
23
  interpolation: { escapeValue: false },
10
24
  })
@@ -3,9 +3,74 @@
3
3
  "title": "QuadroKit",
4
4
  "tagline": "Dashboard template — Vite, React, 4D REST, themes, i18n.",
5
5
  "nav_home": "Home",
6
- "nav_agencies": "Agencies",
6
+ "nav_sample_data": "Sample data",
7
7
  "session_cookie": "Expected 4D session cookie: {{name}}"
8
8
  },
9
+ "pages": {
10
+ "home": {
11
+ "title": "Business overview"
12
+ }
13
+ },
14
+ "breadcrumb": {
15
+ "app": "QuadroKit",
16
+ "segment_home": "Home",
17
+ "segment_sample_data": "Sample data",
18
+ "segment_unknown": "Page"
19
+ },
20
+ "layout": {
21
+ "sidebar_nav": "Main navigation",
22
+ "nav_section_overview": "Overview",
23
+ "nav_section_apps": "Apps",
24
+ "nav_apps_placeholder": "More apps can be linked here.",
25
+ "breadcrumb_label": "Breadcrumb",
26
+ "search_placeholder": "Search…",
27
+ "open_menu": "Open menu",
28
+ "close_menu": "Close menu",
29
+ "close": "Close",
30
+ "language": "Language",
31
+ "lang_en": "EN",
32
+ "lang_fr": "FR",
33
+ "theme": "Theme",
34
+ "theme_presets": "Presets",
35
+ "theme_mode": "Mode",
36
+ "mode_light": "Light",
37
+ "mode_dark": "Dark",
38
+ "mode_system": "System",
39
+ "theme_resolved": "Active: {{mode}}",
40
+ "notifications": "Notifications",
41
+ "notif_1": "New deployment finished successfully.",
42
+ "notif_2": "A comment was added to your report.",
43
+ "notif_time_1": "2 min ago",
44
+ "notif_time_2": "1 hour ago",
45
+ "user_name": "Denish N",
46
+ "user_role": "Team",
47
+ "user_initials": "DN"
48
+ },
49
+ "dashboard": {
50
+ "loading": "Loading charts…",
51
+ "footnote": "Charts use static sample data — wire your 4D aggregates or entity sets when you are ready.",
52
+ "kpi_revenue": "Revenue",
53
+ "kpi_sales": "Sales",
54
+ "kpi_customers": "Customers",
55
+ "kpi_orders": "Orders",
56
+ "trend_vs": "vs. last period",
57
+ "chart_bars_title": "Series comparison",
58
+ "chart_lines_title": "Trend lines",
59
+ "chart_regions_title": "By region",
60
+ "chart_subtitle": "Demo data only",
61
+ "series_a": "Series A",
62
+ "series_b": "Series B",
63
+ "series_u": "Signal U",
64
+ "series_v": "Signal V",
65
+ "count": "Count"
66
+ },
67
+ "sample_data": {
68
+ "title": "Sample data",
69
+ "intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
70
+ "hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
71
+ "card_title": "Example rows",
72
+ "count": "{{count}} placeholder rows"
73
+ },
9
74
  "form": {
10
75
  "demo_title": "react-hook-form + zod",
11
76
  "label": "Label",
@@ -0,0 +1,80 @@
1
+ {
2
+ "app": {
3
+ "title": "QuadroKit",
4
+ "tagline": "Modèle de tableau de bord — Vite, React, 4D REST, thèmes, i18n.",
5
+ "nav_home": "Accueil",
6
+ "nav_sample_data": "Données d'exemple",
7
+ "session_cookie": "Cookie de session 4D attendu : {{name}}"
8
+ },
9
+ "pages": {
10
+ "home": {
11
+ "title": "Vue d'ensemble"
12
+ }
13
+ },
14
+ "breadcrumb": {
15
+ "app": "QuadroKit",
16
+ "segment_home": "Accueil",
17
+ "segment_sample_data": "Données d'exemple",
18
+ "segment_unknown": "Page"
19
+ },
20
+ "layout": {
21
+ "sidebar_nav": "Navigation principale",
22
+ "nav_section_overview": "Vue d'ensemble",
23
+ "nav_section_apps": "Applications",
24
+ "nav_apps_placeholder": "D'autres applications peuvent être ajoutées ici.",
25
+ "breadcrumb_label": "Fil d'Ariane",
26
+ "search_placeholder": "Rechercher…",
27
+ "open_menu": "Ouvrir le menu",
28
+ "close_menu": "Fermer le menu",
29
+ "close": "Fermer",
30
+ "language": "Langue",
31
+ "lang_en": "EN",
32
+ "lang_fr": "FR",
33
+ "theme": "Thème",
34
+ "theme_presets": "Préréglages",
35
+ "theme_mode": "Mode",
36
+ "mode_light": "Clair",
37
+ "mode_dark": "Sombre",
38
+ "mode_system": "Système",
39
+ "theme_resolved": "Actif : {{mode}}",
40
+ "notifications": "Notifications",
41
+ "notif_1": "Nouveau déploiement terminé avec succès.",
42
+ "notif_2": "Un commentaire a été ajouté à votre rapport.",
43
+ "notif_time_1": "Il y a 2 min",
44
+ "notif_time_2": "Il y a 1 h",
45
+ "user_name": "Denish N",
46
+ "user_role": "Équipe",
47
+ "user_initials": "DN"
48
+ },
49
+ "dashboard": {
50
+ "loading": "Chargement des graphiques…",
51
+ "footnote": "Les graphiques utilisent des données d'exemple statiques — branchez vos agrégats 4D ou entity sets quand vous êtes prêt.",
52
+ "kpi_revenue": "Revenus",
53
+ "kpi_sales": "Ventes",
54
+ "kpi_customers": "Clients",
55
+ "kpi_orders": "Commandes",
56
+ "trend_vs": "par rapport à la période précédente",
57
+ "chart_bars_title": "Comparaison de séries",
58
+ "chart_lines_title": "Courbes de tendance",
59
+ "chart_regions_title": "Par région",
60
+ "chart_subtitle": "Données de démo uniquement",
61
+ "series_a": "Série A",
62
+ "series_b": "Série B",
63
+ "series_u": "Signal U",
64
+ "series_v": "Signal V",
65
+ "count": "Nombre"
66
+ },
67
+ "sample_data": {
68
+ "title": "Données d'exemple",
69
+ "intro": "Cette page utilise des lignes statiques. Elle ne suppose aucune table 4D particulière.",
70
+ "hint": "Après `quadrokit-client generate`, demandez à votre agent de charger les bonnes entités depuis votre catalogue (par ex. `quadro.<Entité>.all({ ... })`).",
71
+ "card_title": "Lignes d'exemple",
72
+ "count": "{{count}} lignes d'exemple"
73
+ },
74
+ "form": {
75
+ "demo_title": "react-hook-form + zod",
76
+ "label": "Libellé",
77
+ "placeholder": "Saisissez quelque chose…",
78
+ "submit": "Envoyer"
79
+ }
80
+ }
@@ -1,4 +1,4 @@
1
- import { StrictMode } from 'react'
1
+ import { StrictMode, Suspense } from 'react'
2
2
  import { createRoot } from 'react-dom/client'
3
3
  import { RouterProvider } from 'react-router-dom'
4
4
  import '@quadrokit/ui/styles.css'
@@ -9,7 +9,13 @@ import { router } from './router'
9
9
  createRoot(document.getElementById('root')!).render(
10
10
  <StrictMode>
11
11
  <ThemeProvider>
12
- <RouterProvider router={router} />
12
+ <Suspense
13
+ fallback={
14
+ <div className="flex min-h-dvh items-center justify-center text-muted-foreground">…</div>
15
+ }
16
+ >
17
+ <RouterProvider router={router} />
18
+ </Suspense>
13
19
  </ThemeProvider>
14
20
  </StrictMode>
15
21
  )
@@ -9,12 +9,18 @@ import {
9
9
  Input,
10
10
  Label,
11
11
  } from '@quadrokit/ui'
12
+ import { lazy, Suspense } from 'react'
12
13
  import { useForm } from 'react-hook-form'
13
14
  import { useTranslation } from 'react-i18next'
14
15
  import { z } from 'zod'
15
16
  import { quadro } from '@/lib/quadro-client'
16
17
  import { useSidebarHint } from '@/stores/useSidebarHint'
17
18
 
19
+ const DashboardOverview = lazy(async () => {
20
+ const m = await import('@/components/dashboard')
21
+ return { default: m.DashboardOverview }
22
+ })
23
+
18
24
  const schema = z.object({
19
25
  label: z.string().min(1, 'Required'),
20
26
  })
@@ -34,13 +40,22 @@ export function HomePage() {
34
40
  return (
35
41
  <div className="space-y-8">
36
42
  <div>
37
- <h1 className="text-3xl font-semibold tracking-tight">{t('app.title')}</h1>
38
- <p className="mt-2 max-w-2xl text-muted-foreground">{t('app.tagline')}</p>
43
+ <p className="max-w-2xl text-muted-foreground">{t('app.tagline')}</p>
39
44
  <p className="mt-2 text-sm text-muted-foreground">
40
45
  {t('app.session_cookie', { name: quadro.sessionCookieName })}
41
46
  </p>
42
47
  </div>
43
48
 
49
+ <Suspense
50
+ fallback={
51
+ <div className="flex min-h-[200px] items-center justify-center rounded-xl border border-dashed border-border text-sm text-muted-foreground">
52
+ {t('dashboard.loading')}
53
+ </div>
54
+ }
55
+ >
56
+ <DashboardOverview />
57
+ </Suspense>
58
+
44
59
  <Card>
45
60
  <CardHeader>
46
61
  <CardTitle>{t('form.demo_title')}</CardTitle>
@@ -0,0 +1,37 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ /** Static placeholder — swap for `quadro.<YourEntity>.all(...)` wired to your catalog. */
5
+ const PLACEHOLDER_ROWS = [
6
+ { id: '1', label: 'Example row A' },
7
+ { id: '2', label: 'Example row B' },
8
+ { id: '3', label: 'Example row C' },
9
+ ] as const
10
+
11
+ export function SampleDataPage() {
12
+ const { t } = useTranslation()
13
+
14
+ return (
15
+ <div className="space-y-6">
16
+ <div>
17
+ <p className="max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
18
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
19
+ </div>
20
+ <Card>
21
+ <CardHeader>
22
+ <CardTitle>{t('sample_data.card_title')}</CardTitle>
23
+ <CardDescription>
24
+ {t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
25
+ </CardDescription>
26
+ </CardHeader>
27
+ <CardContent>
28
+ <ul className="list-inside list-disc space-y-1 text-sm">
29
+ {PLACEHOLDER_ROWS.map((row) => (
30
+ <li key={row.id}>{row.label}</li>
31
+ ))}
32
+ </ul>
33
+ </CardContent>
34
+ </Card>
35
+ </div>
36
+ )
37
+ }
@@ -1,15 +1,24 @@
1
1
  import { createBrowserRouter } from 'react-router-dom'
2
- import { AppShell } from './components/AppShell'
3
- import { AgenciesPage } from './pages/AgenciesPage'
2
+ import { AppShell } from './components/layout/AppShell'
4
3
  import { HomePage } from './pages/HomePage'
4
+ import { SampleDataPage } from './pages/SampleDataPage'
5
+ import type { AppRouteHandle } from './types/router'
5
6
 
6
7
  export const router = createBrowserRouter([
7
8
  {
8
9
  path: '/',
9
10
  element: <AppShell />,
10
11
  children: [
11
- { index: true, element: <HomePage /> },
12
- { path: 'agencies', element: <AgenciesPage /> },
12
+ {
13
+ index: true,
14
+ element: <HomePage />,
15
+ handle: { pageTitleKey: 'pages.home.title' } satisfies AppRouteHandle,
16
+ },
17
+ {
18
+ path: 'sample-data',
19
+ element: <SampleDataPage />,
20
+ handle: { pageTitleKey: 'sample_data.title' } satisfies AppRouteHandle,
21
+ },
13
22
  ],
14
23
  },
15
24
  ])
@@ -0,0 +1,4 @@
1
+ export type AppRouteHandle = {
2
+ /** i18n key for `<h1>` in the main layout */
3
+ pageTitleKey: string
4
+ }
@@ -0,0 +1,55 @@
1
+ import { cn } from '@quadrokit/ui'
2
+ import { useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { Outlet } from 'react-router-dom'
5
+ import { PageHeading } from './PageHeading'
6
+ import { Sidebar } from './Sidebar'
7
+ import { TopHeader } from './TopHeader'
8
+
9
+ export function AppShell() {
10
+ const { t } = useTranslation()
11
+ const [mobileNavOpen, setMobileNavOpen] = useState(false)
12
+
13
+ return (
14
+ <div className="flex min-h-dvh bg-background text-foreground">
15
+ <div className="hidden w-56 shrink-0 border-r border-border md:block">
16
+ <Sidebar className="sticky top-0 h-dvh" />
17
+ </div>
18
+
19
+ <div
20
+ className={cn(
21
+ 'fixed inset-0 z-50 md:hidden',
22
+ mobileNavOpen ? 'pointer-events-auto' : 'pointer-events-none'
23
+ )}
24
+ aria-hidden={!mobileNavOpen}
25
+ >
26
+ <button
27
+ type="button"
28
+ className={cn(
29
+ 'absolute inset-0 bg-background/80 backdrop-blur-sm transition-opacity',
30
+ mobileNavOpen ? 'opacity-100' : 'opacity-0'
31
+ )}
32
+ aria-label={t('layout.close_menu')}
33
+ tabIndex={mobileNavOpen ? 0 : -1}
34
+ onClick={() => setMobileNavOpen(false)}
35
+ />
36
+ <div
37
+ className={cn(
38
+ 'absolute left-0 top-0 h-full w-56 max-w-[85vw] border-r border-border bg-card shadow-lg transition-transform',
39
+ mobileNavOpen ? 'translate-x-0' : '-translate-x-full'
40
+ )}
41
+ >
42
+ <Sidebar onNavigate={() => setMobileNavOpen(false)} />
43
+ </div>
44
+ </div>
45
+
46
+ <div className="flex min-w-0 flex-1 flex-col">
47
+ <TopHeader onMenuClick={() => setMobileNavOpen(true)} />
48
+ <main className="flex-1 overflow-auto px-4 py-6 md:px-6 lg:px-8">
49
+ <PageHeading />
50
+ <Outlet />
51
+ </main>
52
+ </div>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,37 @@
1
+ import { Button } from '@quadrokit/ui'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ const LOCALES = [
5
+ { code: 'en', labelKey: 'layout.lang_en' as const },
6
+ { code: 'fr', labelKey: 'layout.lang_fr' as const },
7
+ ] as const
8
+
9
+ export function LanguageSwitcher() {
10
+ const { i18n, t } = useTranslation()
11
+ const lng = i18n.resolvedLanguage ?? i18n.language
12
+
13
+ return (
14
+ <fieldset className="m-0 flex min-w-0 items-center gap-1 rounded-md border border-border bg-card p-0.5">
15
+ <legend className="sr-only">{t('layout.language')}</legend>
16
+ {LOCALES.map(({ code, labelKey }) => (
17
+ <Button
18
+ key={code}
19
+ type="button"
20
+ size="sm"
21
+ variant={lng.startsWith(code) ? 'default' : 'ghost'}
22
+ className="h-8 px-2 text-xs"
23
+ onClick={() => {
24
+ void i18n.changeLanguage(code)
25
+ try {
26
+ localStorage.setItem('quadrokit:lng', code)
27
+ } catch {
28
+ /* ignore */
29
+ }
30
+ }}
31
+ >
32
+ {t(labelKey)}
33
+ </Button>
34
+ ))}
35
+ </fieldset>
36
+ )
37
+ }