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,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
+ }
@@ -28,6 +28,7 @@
28
28
  "react-hook-form": "^7.72.0",
29
29
  "react-i18next": "^17.0.2",
30
30
  "react-router-dom": "^7.13.2",
31
+ "recharts": "^2.15.0",
31
32
  "zod": "^4.3.6",
32
33
  "zustand": "^5.0.12"
33
34
  },
@@ -0,0 +1,29 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, cn } from '@quadrokit/ui'
2
+ import type { ReactNode } from 'react'
3
+
4
+ export type ChartCardProps = {
5
+ title: string
6
+ description: string
7
+ /** Chart area (e.g. `h-[260px]`) */
8
+ chartClassName: string
9
+ children: ReactNode
10
+ className?: string
11
+ }
12
+
13
+ export function ChartCard({
14
+ title,
15
+ description,
16
+ chartClassName,
17
+ children,
18
+ className,
19
+ }: ChartCardProps) {
20
+ return (
21
+ <Card className={className}>
22
+ <CardHeader>
23
+ <CardTitle className="text-base">{title}</CardTitle>
24
+ <CardDescription>{description}</CardDescription>
25
+ </CardHeader>
26
+ <CardContent className={cn('pt-0', chartClassName)}>{children}</CardContent>
27
+ </Card>
28
+ )
29
+ }
@@ -0,0 +1,79 @@
1
+ import { useMemo } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { ChartCard } from './ChartCard'
4
+ import { MOCK_BARS, MOCK_KPI, MOCK_LINES, MOCK_REGIONS } from './dashboardMockData'
5
+ import { KpiCard } from './KpiCard'
6
+ import { RegionsBarChart } from './RegionsBarChart'
7
+ import { SeriesBarChart } from './SeriesBarChart'
8
+ import { TrendLineChart } from './TrendLineChart'
9
+
10
+ export function DashboardOverview() {
11
+ const { t } = useTranslation()
12
+
13
+ const kpiCards = useMemo(
14
+ () =>
15
+ MOCK_KPI.map((row) => ({
16
+ key: row.key,
17
+ title: t(`dashboard.kpi_${row.key}`),
18
+ value: row.value,
19
+ deltaLine: `${row.delta} · ${t('dashboard.trend_vs')}`,
20
+ trendPositive: row.positive,
21
+ })),
22
+ [t]
23
+ )
24
+
25
+ return (
26
+ <div className="space-y-6">
27
+ <p className="text-sm text-muted-foreground">{t('dashboard.footnote')}</p>
28
+
29
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
30
+ {kpiCards.map((row) => (
31
+ <KpiCard
32
+ key={row.key}
33
+ title={row.title}
34
+ value={row.value}
35
+ deltaLine={row.deltaLine}
36
+ trendPositive={row.trendPositive}
37
+ />
38
+ ))}
39
+ </div>
40
+
41
+ <div className="grid gap-6 lg:grid-cols-2">
42
+ <ChartCard
43
+ className="min-h-[320px] shadow-sm"
44
+ title={t('dashboard.chart_bars_title')}
45
+ description={t('dashboard.chart_subtitle')}
46
+ chartClassName="h-[260px]"
47
+ >
48
+ <SeriesBarChart
49
+ data={MOCK_BARS}
50
+ seriesA={{ key: 'a', label: t('dashboard.series_a') }}
51
+ seriesB={{ key: 'b', label: t('dashboard.series_b') }}
52
+ />
53
+ </ChartCard>
54
+
55
+ <ChartCard
56
+ className="min-h-[320px] shadow-sm"
57
+ title={t('dashboard.chart_lines_title')}
58
+ description={t('dashboard.chart_subtitle')}
59
+ chartClassName="h-[260px]"
60
+ >
61
+ <TrendLineChart
62
+ data={MOCK_LINES}
63
+ lineU={{ key: 'u', label: t('dashboard.series_u') }}
64
+ lineV={{ key: 'v', label: t('dashboard.series_v') }}
65
+ />
66
+ </ChartCard>
67
+ </div>
68
+
69
+ <ChartCard
70
+ className="shadow-sm"
71
+ title={t('dashboard.chart_regions_title')}
72
+ description={t('dashboard.chart_subtitle')}
73
+ chartClassName="h-[220px]"
74
+ >
75
+ <RegionsBarChart data={MOCK_REGIONS} valueLabel={t('dashboard.count')} />
76
+ </ChartCard>
77
+ </div>
78
+ )
79
+ }
@@ -0,0 +1,31 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
2
+
3
+ export type KpiCardProps = {
4
+ title: string
5
+ value: string
6
+ /** Full second line, e.g. "+12% · vs. last period" */
7
+ deltaLine: string
8
+ trendPositive: boolean
9
+ }
10
+
11
+ export function KpiCard({ title, value, deltaLine, trendPositive }: KpiCardProps) {
12
+ return (
13
+ <Card className="shadow-sm">
14
+ <CardHeader className="pb-2">
15
+ <CardDescription>{title}</CardDescription>
16
+ <CardTitle className="text-2xl font-semibold tabular-nums">{value}</CardTitle>
17
+ </CardHeader>
18
+ <CardContent className="pt-0">
19
+ <p
20
+ className={
21
+ trendPositive
22
+ ? 'text-xs font-medium text-emerald-600 dark:text-emerald-400'
23
+ : 'text-xs font-medium text-rose-600 dark:text-rose-400'
24
+ }
25
+ >
26
+ {deltaLine}
27
+ </p>
28
+ </CardContent>
29
+ </Card>
30
+ )
31
+ }
@@ -0,0 +1,45 @@
1
+ import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
2
+ import { chartAxisTick, chartGridStroke, chartTooltipContentStyle } from './chartTheme'
3
+ import type { RegionDatum } from './dashboardMockData'
4
+
5
+ export type RegionsBarChartProps = {
6
+ data: readonly RegionDatum[]
7
+ categoryKey?: keyof RegionDatum & string
8
+ valueKey?: keyof RegionDatum & string
9
+ valueLabel: string
10
+ }
11
+
12
+ export function RegionsBarChart({
13
+ data,
14
+ categoryKey = 'region',
15
+ valueKey = 'n',
16
+ valueLabel,
17
+ }: RegionsBarChartProps) {
18
+ return (
19
+ <ResponsiveContainer width="100%" height="100%">
20
+ <BarChart
21
+ layout="vertical"
22
+ data={[...data]}
23
+ margin={{ top: 8, right: 24, left: 8, bottom: 8 }}
24
+ >
25
+ <CartesianGrid strokeDasharray="3 3" stroke={chartGridStroke} horizontal={false} />
26
+ <XAxis type="number" tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} />
27
+ <YAxis
28
+ type="category"
29
+ dataKey={categoryKey}
30
+ width={72}
31
+ tick={chartAxisTick}
32
+ axisLine={{ stroke: chartGridStroke }}
33
+ />
34
+ <Tooltip contentStyle={chartTooltipContentStyle} />
35
+ <Bar
36
+ dataKey={valueKey}
37
+ name={valueLabel}
38
+ fill="hsl(var(--primary))"
39
+ radius={[0, 4, 4, 0]}
40
+ maxBarSize={18}
41
+ />
42
+ </BarChart>
43
+ </ResponsiveContainer>
44
+ )
45
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ Bar,
3
+ BarChart,
4
+ CartesianGrid,
5
+ Legend,
6
+ ResponsiveContainer,
7
+ Tooltip,
8
+ XAxis,
9
+ YAxis,
10
+ } from 'recharts'
11
+ import {
12
+ chartAxisTick,
13
+ chartGridStroke,
14
+ chartLegendStyle,
15
+ chartTooltipContentStyle,
16
+ } from './chartTheme'
17
+ import type { BarDatum } from './dashboardMockData'
18
+
19
+ export type SeriesBarChartProps = {
20
+ data: readonly BarDatum[]
21
+ seriesA: { key: 'a'; label: string }
22
+ seriesB: { key: 'b'; label: string }
23
+ xKey?: keyof BarDatum & string
24
+ }
25
+
26
+ export function SeriesBarChart({ data, seriesA, seriesB, xKey = 'name' }: SeriesBarChartProps) {
27
+ return (
28
+ <ResponsiveContainer width="100%" height="100%">
29
+ <BarChart data={[...data]} margin={{ top: 8, right: 8, left: -8, bottom: 0 }}>
30
+ <CartesianGrid strokeDasharray="3 3" stroke={chartGridStroke} vertical={false} />
31
+ <XAxis dataKey={xKey} tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} />
32
+ <YAxis tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} width={32} />
33
+ <Tooltip
34
+ cursor={{ fill: 'hsl(var(--muted) / 0.35)' }}
35
+ contentStyle={chartTooltipContentStyle}
36
+ />
37
+ <Legend wrapperStyle={chartLegendStyle} />
38
+ <Bar
39
+ dataKey={seriesA.key}
40
+ name={seriesA.label}
41
+ fill="hsl(var(--primary))"
42
+ radius={[4, 4, 0, 0]}
43
+ maxBarSize={40}
44
+ />
45
+ <Bar
46
+ dataKey={seriesB.key}
47
+ name={seriesB.label}
48
+ fill="hsl(var(--muted-foreground) / 0.35)"
49
+ radius={[4, 4, 0, 0]}
50
+ maxBarSize={40}
51
+ />
52
+ </BarChart>
53
+ </ResponsiveContainer>
54
+ )
55
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ CartesianGrid,
3
+ Legend,
4
+ Line,
5
+ LineChart,
6
+ ResponsiveContainer,
7
+ Tooltip,
8
+ XAxis,
9
+ YAxis,
10
+ } from 'recharts'
11
+ import {
12
+ chartAxisTick,
13
+ chartGridStroke,
14
+ chartLegendStyle,
15
+ chartTooltipContentStyle,
16
+ } from './chartTheme'
17
+ import type { LineDatum } from './dashboardMockData'
18
+
19
+ export type TrendLineChartProps = {
20
+ data: readonly LineDatum[]
21
+ xKey?: keyof LineDatum & string
22
+ lineU: { key: 'u'; label: string }
23
+ lineV: { key: 'v'; label: string }
24
+ }
25
+
26
+ export function TrendLineChart({ data, xKey = 'x', lineU, lineV }: TrendLineChartProps) {
27
+ return (
28
+ <ResponsiveContainer width="100%" height="100%">
29
+ <LineChart data={[...data]} margin={{ top: 8, right: 8, left: -8, bottom: 0 }}>
30
+ <CartesianGrid strokeDasharray="3 3" stroke={chartGridStroke} />
31
+ <XAxis dataKey={xKey} tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} />
32
+ <YAxis tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} width={32} />
33
+ <Tooltip contentStyle={chartTooltipContentStyle} />
34
+ <Legend wrapperStyle={chartLegendStyle} />
35
+ <Line
36
+ type="monotone"
37
+ dataKey={lineU.key}
38
+ name={lineU.label}
39
+ stroke="hsl(var(--primary))"
40
+ strokeWidth={2}
41
+ dot={{ r: 3 }}
42
+ activeDot={{ r: 5 }}
43
+ />
44
+ <Line
45
+ type="monotone"
46
+ dataKey={lineV.key}
47
+ name={lineV.label}
48
+ stroke="hsl(var(--muted-foreground))"
49
+ strokeWidth={2}
50
+ dot={{ r: 3 }}
51
+ />
52
+ </LineChart>
53
+ </ResponsiveContainer>
54
+ )
55
+ }
@@ -0,0 +1,18 @@
1
+ import type { CSSProperties } from 'react'
2
+
3
+ /** Shared Recharts styling aligned with QuadroKit CSS variables (light/dark). */
4
+ export const chartAxisTick = {
5
+ fill: 'hsl(var(--muted-foreground))',
6
+ fontSize: 11,
7
+ } as const
8
+
9
+ export const chartGridStroke = 'hsl(var(--border))'
10
+
11
+ export const chartTooltipContentStyle: CSSProperties = {
12
+ background: 'hsl(var(--card))',
13
+ border: '1px solid hsl(var(--border))',
14
+ borderRadius: '8px',
15
+ fontSize: '12px',
16
+ }
17
+
18
+ export const chartLegendStyle = { fontSize: '12px' } as const
@@ -0,0 +1,51 @@
1
+ /** Static demo data — replace with props from your API / `quadro` client. */
2
+
3
+ export type KpiDatum = {
4
+ key: string
5
+ value: string
6
+ delta: string
7
+ positive: boolean
8
+ }
9
+
10
+ export const MOCK_KPI: readonly KpiDatum[] = [
11
+ { key: 'revenue', value: '$587.54', delta: '+12%', positive: true },
12
+ { key: 'sales', value: '4,500', delta: '+8%', positive: true },
13
+ { key: 'customers', value: '2,242', delta: '-3%', positive: false },
14
+ { key: 'orders', value: '1,128', delta: '+15%', positive: true },
15
+ ]
16
+
17
+ export type BarDatum = {
18
+ name: string
19
+ a: number
20
+ b: number
21
+ }
22
+
23
+ export const MOCK_BARS: readonly BarDatum[] = [
24
+ { name: 'Jan', a: 40, b: 24 },
25
+ { name: 'Feb', a: 30, b: 38 },
26
+ { name: 'Mar', a: 55, b: 42 },
27
+ { name: 'Apr', a: 45, b: 30 },
28
+ { name: 'May', a: 60, b: 48 },
29
+ { name: 'Jun', a: 52, b: 55 },
30
+ ]
31
+
32
+ export type LineDatum = { x: number; u: number; v: number }
33
+
34
+ export const MOCK_LINES: readonly LineDatum[] = [
35
+ { x: 1, u: 12, v: 8 },
36
+ { x: 2, u: 18, v: 14 },
37
+ { x: 3, u: 15, v: 20 },
38
+ { x: 4, u: 22, v: 16 },
39
+ { x: 5, u: 28, v: 24 },
40
+ { x: 6, u: 25, v: 30 },
41
+ ]
42
+
43
+ export type RegionDatum = { region: string; n: number }
44
+
45
+ export const MOCK_REGIONS: readonly RegionDatum[] = [
46
+ { region: 'North', n: 14 },
47
+ { region: 'South', n: 19 },
48
+ { region: 'East', n: 12 },
49
+ { region: 'West', n: 17 },
50
+ { region: 'Other', n: 9 },
51
+ ]
@@ -0,0 +1 @@
1
+ export { DashboardOverview } from './DashboardOverview'
@@ -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
+ }