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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-quadrokit",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Scaffold a QuadroKit Vite + React app from a template",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.mjs",
@@ -4,10 +4,10 @@ Vite + React 19 + React Router 7 + **Tailwind 3.4** + **i18next** + **react-hook
4
4
 
5
5
  | Template | Use case |
6
6
  |----------|----------|
7
- | `dashboard` | Default shell, agencies sample page, form demo |
8
- | `website` | Marketing-style hero + same stack |
9
- | `ecommerce` | Product-card style placeholder |
10
- | `admin-shell` | Sidebar + main content layout |
7
+ | `dashboard` | Default shell, **Recharts** overview (static KPIs + graphs), sample-data route, form demo |
8
+ | `website` | Marketing-style hero + same stack (static `/sample-data`) |
9
+ | `ecommerce` | Product-card style placeholder + static `/sample-data` |
10
+ | `admin-shell` | Sidebar + main layout + static `/sample-data` |
11
11
 
12
12
  ## Monorepo
13
13
 
@@ -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
+ }
@@ -0,0 +1,79 @@
1
+ import { Button, useDismissOnInteractOutside } from '@quadrokit/ui'
2
+ import { useRef, useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ /** Static notification list — replace with real data / subscriptions later. */
6
+ const PLACEHOLDER_ITEMS = [
7
+ { id: '1', titleKey: 'layout.notif_1' as const, timeKey: 'layout.notif_time_1' as const },
8
+ { id: '2', titleKey: 'layout.notif_2' as const, timeKey: 'layout.notif_time_2' as const },
9
+ ] as const
10
+
11
+ export function NotificationsMenu() {
12
+ const { t } = useTranslation()
13
+ const [open, setOpen] = useState(false)
14
+ const rootRef = useRef<HTMLDivElement>(null)
15
+
16
+ useDismissOnInteractOutside({
17
+ enabled: open,
18
+ containerRef: rootRef,
19
+ onDismiss: () => setOpen(false),
20
+ })
21
+
22
+ return (
23
+ <div ref={rootRef} className="relative">
24
+ <Button
25
+ type="button"
26
+ variant="outline"
27
+ size="icon"
28
+ className="relative h-9 w-9 shrink-0"
29
+ aria-expanded={open}
30
+ aria-haspopup="dialog"
31
+ aria-label={t('layout.notifications')}
32
+ onClick={() => setOpen((o) => !o)}
33
+ >
34
+ <BellIcon className="h-4 w-4" />
35
+ <span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-destructive" aria-hidden />
36
+ </Button>
37
+ {open ? (
38
+ <div
39
+ className="absolute right-0 z-50 mt-1 w-80 max-w-[calc(100vw-2rem)] rounded-lg border border-border bg-popover p-2 text-sm text-popover-foreground shadow-lg"
40
+ role="dialog"
41
+ aria-label={t('layout.notifications')}
42
+ >
43
+ <p className="border-b border-border px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
44
+ {t('layout.notifications')}
45
+ </p>
46
+ <ul className="max-h-64 overflow-auto py-1">
47
+ {PLACEHOLDER_ITEMS.map((item) => (
48
+ <li key={item.id} className="rounded-md px-2 py-2 hover:bg-muted/60">
49
+ <p className="font-medium leading-snug">{t(item.titleKey)}</p>
50
+ <p className="text-xs text-muted-foreground">{t(item.timeKey)}</p>
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </div>
55
+ ) : null}
56
+ </div>
57
+ )
58
+ }
59
+
60
+ function BellIcon({ 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
+ strokeLinecap="round"
69
+ strokeLinejoin="round"
70
+ className={className}
71
+ aria-hidden="true"
72
+ focusable="false"
73
+ >
74
+ <title>Notifications</title>
75
+ <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
76
+ <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
77
+ </svg>
78
+ )
79
+ }
@@ -0,0 +1,40 @@
1
+ import { useTranslation } from 'react-i18next'
2
+ import { Link, useLocation, useMatches } from 'react-router-dom'
3
+ import type { AppRouteHandle } from '@/types/router'
4
+
5
+ export function PageHeading() {
6
+ const { t } = useTranslation()
7
+ const { pathname } = useLocation()
8
+ const matches = useMatches()
9
+ const leaf = matches[matches.length - 1]
10
+ const handle = leaf?.handle as AppRouteHandle | undefined
11
+
12
+ const segmentKey =
13
+ pathname === '/'
14
+ ? ('breadcrumb.segment_home' as const)
15
+ : pathname === '/sample-data'
16
+ ? ('breadcrumb.segment_sample_data' as const)
17
+ : ('breadcrumb.segment_unknown' as const)
18
+
19
+ return (
20
+ <header className="mb-6">
21
+ <nav
22
+ aria-label={t('layout.breadcrumb_label')}
23
+ className="mb-3 flex flex-wrap items-center gap-2 text-sm text-muted-foreground"
24
+ >
25
+ <Link to="/" className="hover:text-foreground">
26
+ {t('breadcrumb.app')}
27
+ </Link>
28
+ <span aria-hidden className="text-border">
29
+ /
30
+ </span>
31
+ <span className="font-medium text-foreground">{t(segmentKey)}</span>
32
+ </nav>
33
+ {handle?.pageTitleKey ? (
34
+ <h1 className="text-2xl font-semibold tracking-tight md:text-3xl">
35
+ {t(handle.pageTitleKey)}
36
+ </h1>
37
+ ) : null}
38
+ </header>
39
+ )
40
+ }
@@ -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,56 @@
3
3
  "title": "QuadroKit",
4
4
  "tagline": "Admin shell with sidebar — same stack, denser layout.",
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
+ "sample_data": {
50
+ "title": "Sample data",
51
+ "intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
52
+ "hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
53
+ "card_title": "Example rows",
54
+ "count": "{{count}} placeholder rows"
55
+ },
9
56
  "form": {
10
57
  "demo_title": "react-hook-form + zod",
11
58
  "label": "Label",
@@ -0,0 +1,62 @@
1
+ {
2
+ "app": {
3
+ "title": "QuadroKit",
4
+ "tagline": "Coque admin avec barre latérale — même stack, mise en page plus dense.",
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
+ "sample_data": {
50
+ "title": "Données d'exemple",
51
+ "intro": "Cette page utilise des lignes statiques. Elle ne suppose aucune table 4D particulière.",
52
+ "hint": "Après `quadrokit-client generate`, demandez à votre agent de charger les bonnes entités depuis votre catalogue (par ex. `quadro.<Entité>.all({ ... })`).",
53
+ "card_title": "Lignes d'exemple",
54
+ "count": "{{count}} lignes d'exemple"
55
+ },
56
+ "form": {
57
+ "demo_title": "react-hook-form + zod",
58
+ "label": "Libellé",
59
+ "placeholder": "Saisissez quelque chose…",
60
+ "submit": "Envoyer"
61
+ }
62
+ }
@@ -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
  )
@@ -34,8 +34,7 @@ export function HomePage() {
34
34
  return (
35
35
  <div className="space-y-8">
36
36
  <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>
37
+ <p className="max-w-2xl text-muted-foreground">{t('app.tagline')}</p>
39
38
  <p className="mt-2 text-sm text-muted-foreground">
40
39
  {t('app.session_cookie', { name: quadro.sessionCookieName })}
41
40
  </p>