create-quadrokit 0.2.10 → 0.2.12

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 (34) hide show
  1. package/package.json +1 -1
  2. package/templates/README.md +4 -4
  3. package/templates/admin-shell/src/components/AppShell.tsx +4 -4
  4. package/templates/admin-shell/src/locales/en.json +8 -1
  5. package/templates/admin-shell/src/pages/SampleDataPage.tsx +38 -0
  6. package/templates/admin-shell/src/router.tsx +2 -2
  7. package/templates/dashboard/package.json +1 -0
  8. package/templates/dashboard/src/components/AppShell.tsx +2 -2
  9. package/templates/dashboard/src/components/dashboard/ChartCard.tsx +29 -0
  10. package/templates/dashboard/src/components/dashboard/DashboardOverview.tsx +79 -0
  11. package/templates/dashboard/src/components/dashboard/KpiCard.tsx +31 -0
  12. package/templates/dashboard/src/components/dashboard/RegionsBarChart.tsx +45 -0
  13. package/templates/dashboard/src/components/dashboard/SeriesBarChart.tsx +55 -0
  14. package/templates/dashboard/src/components/dashboard/TrendLineChart.tsx +55 -0
  15. package/templates/dashboard/src/components/dashboard/chartTheme.ts +18 -0
  16. package/templates/dashboard/src/components/dashboard/dashboardMockData.ts +51 -0
  17. package/templates/dashboard/src/components/dashboard/index.ts +1 -0
  18. package/templates/dashboard/src/locales/en.json +26 -1
  19. package/templates/dashboard/src/pages/HomePage.tsx +16 -0
  20. package/templates/dashboard/src/pages/SampleDataPage.tsx +38 -0
  21. package/templates/dashboard/src/router.tsx +2 -2
  22. package/templates/ecommerce/src/components/AppShell.tsx +2 -2
  23. package/templates/ecommerce/src/locales/en.json +8 -1
  24. package/templates/ecommerce/src/pages/SampleDataPage.tsx +38 -0
  25. package/templates/ecommerce/src/router.tsx +2 -2
  26. package/templates/website/src/components/AppShell.tsx +2 -2
  27. package/templates/website/src/locales/en.json +8 -1
  28. package/templates/website/src/pages/HomePage.tsx +1 -1
  29. package/templates/website/src/pages/SampleDataPage.tsx +38 -0
  30. package/templates/website/src/router.tsx +2 -2
  31. package/templates/admin-shell/src/pages/AgenciesPage.tsx +0 -77
  32. package/templates/dashboard/src/pages/AgenciesPage.tsx +0 -77
  33. package/templates/ecommerce/src/pages/AgenciesPage.tsx +0 -77
  34. package/templates/website/src/pages/AgenciesPage.tsx +0 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-quadrokit",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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
 
@@ -23,12 +23,12 @@ export function AppShell() {
23
23
  {t('app.nav_home')}
24
24
  </NavLink>
25
25
  <NavLink
26
- to="/agencies"
26
+ to="/sample-data"
27
27
  className={({ isActive }) =>
28
28
  isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
29
29
  }
30
30
  >
31
- {t('app.nav_agencies')}
31
+ {t('app.nav_sample_data')}
32
32
  </NavLink>
33
33
  </nav>
34
34
  </aside>
@@ -47,10 +47,10 @@ export function AppShell() {
47
47
  {t('app.nav_home')}
48
48
  </NavLink>
49
49
  <NavLink
50
- to="/agencies"
50
+ to="/sample-data"
51
51
  className={({ isActive }) => (isActive ? 'text-foreground' : '')}
52
52
  >
53
- {t('app.nav_agencies')}
53
+ {t('app.nav_sample_data')}
54
54
  </NavLink>
55
55
  </nav>
56
56
  </div>
@@ -3,9 +3,16 @@
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
+ "sample_data": {
10
+ "title": "Sample data",
11
+ "intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
12
+ "hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
13
+ "card_title": "Example rows",
14
+ "count": "{{count}} placeholder rows"
15
+ },
9
16
  "form": {
10
17
  "demo_title": "react-hook-form + zod",
11
18
  "label": "Label",
@@ -0,0 +1,38 @@
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
+ <h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
18
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
19
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
20
+ </div>
21
+ <Card>
22
+ <CardHeader>
23
+ <CardTitle>{t('sample_data.card_title')}</CardTitle>
24
+ <CardDescription>
25
+ {t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
26
+ </CardDescription>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <ul className="list-inside list-disc space-y-1 text-sm">
30
+ {PLACEHOLDER_ROWS.map((row) => (
31
+ <li key={row.id}>{row.label}</li>
32
+ ))}
33
+ </ul>
34
+ </CardContent>
35
+ </Card>
36
+ </div>
37
+ )
38
+ }
@@ -1,6 +1,6 @@
1
1
  import { createBrowserRouter } from 'react-router-dom'
2
2
  import { AppShell } from './components/AppShell'
3
- import { AgenciesPage } from './pages/AgenciesPage'
3
+ import { SampleDataPage } from './pages/SampleDataPage'
4
4
  import { HomePage } from './pages/HomePage'
5
5
 
6
6
  export const router = createBrowserRouter([
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
9
9
  element: <AppShell />,
10
10
  children: [
11
11
  { index: true, element: <HomePage /> },
12
- { path: 'agencies', element: <AgenciesPage /> },
12
+ { path: 'sample-data', element: <SampleDataPage /> },
13
13
  ],
14
14
  },
15
15
  ])
@@ -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
  },
@@ -24,12 +24,12 @@ export function AppShell() {
24
24
  {t('app.nav_home')}
25
25
  </NavLink>
26
26
  <NavLink
27
- to="/agencies"
27
+ to="/sample-data"
28
28
  className={({ isActive }) =>
29
29
  isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
30
30
  }
31
31
  >
32
- {t('app.nav_agencies')}
32
+ {t('app.nav_sample_data')}
33
33
  </NavLink>
34
34
  </nav>
35
35
  </div>
@@ -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'
@@ -3,9 +3,34 @@
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
+ "dashboard": {
10
+ "loading": "Loading charts…",
11
+ "footnote": "Charts use static sample data — wire your 4D aggregates or entity sets when you are ready.",
12
+ "kpi_revenue": "Revenue",
13
+ "kpi_sales": "Sales",
14
+ "kpi_customers": "Customers",
15
+ "kpi_orders": "Orders",
16
+ "trend_vs": "vs. last period",
17
+ "chart_bars_title": "Series comparison",
18
+ "chart_lines_title": "Trend lines",
19
+ "chart_regions_title": "By region",
20
+ "chart_subtitle": "Demo data only",
21
+ "series_a": "Series A",
22
+ "series_b": "Series B",
23
+ "series_u": "Signal U",
24
+ "series_v": "Signal V",
25
+ "count": "Count"
26
+ },
27
+ "sample_data": {
28
+ "title": "Sample data",
29
+ "intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
30
+ "hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
31
+ "card_title": "Example rows",
32
+ "count": "{{count}} placeholder rows"
33
+ },
9
34
  "form": {
10
35
  "demo_title": "react-hook-form + zod",
11
36
  "label": "Label",
@@ -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
  })
@@ -41,6 +47,16 @@ export function HomePage() {
41
47
  </p>
42
48
  </div>
43
49
 
50
+ <Suspense
51
+ fallback={
52
+ <div className="flex min-h-[200px] items-center justify-center rounded-xl border border-dashed border-border text-sm text-muted-foreground">
53
+ {t('dashboard.loading')}
54
+ </div>
55
+ }
56
+ >
57
+ <DashboardOverview />
58
+ </Suspense>
59
+
44
60
  <Card>
45
61
  <CardHeader>
46
62
  <CardTitle>{t('form.demo_title')}</CardTitle>
@@ -0,0 +1,38 @@
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
+ <h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
18
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
19
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
20
+ </div>
21
+ <Card>
22
+ <CardHeader>
23
+ <CardTitle>{t('sample_data.card_title')}</CardTitle>
24
+ <CardDescription>
25
+ {t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
26
+ </CardDescription>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <ul className="list-inside list-disc space-y-1 text-sm">
30
+ {PLACEHOLDER_ROWS.map((row) => (
31
+ <li key={row.id}>{row.label}</li>
32
+ ))}
33
+ </ul>
34
+ </CardContent>
35
+ </Card>
36
+ </div>
37
+ )
38
+ }
@@ -1,7 +1,7 @@
1
1
  import { createBrowserRouter } from 'react-router-dom'
2
2
  import { AppShell } from './components/AppShell'
3
- import { AgenciesPage } from './pages/AgenciesPage'
4
3
  import { HomePage } from './pages/HomePage'
4
+ import { SampleDataPage } from './pages/SampleDataPage'
5
5
 
6
6
  export const router = createBrowserRouter([
7
7
  {
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
9
9
  element: <AppShell />,
10
10
  children: [
11
11
  { index: true, element: <HomePage /> },
12
- { path: 'agencies', element: <AgenciesPage /> },
12
+ { path: 'sample-data', element: <SampleDataPage /> },
13
13
  ],
14
14
  },
15
15
  ])
@@ -24,12 +24,12 @@ export function AppShell() {
24
24
  {t('app.nav_home')}
25
25
  </NavLink>
26
26
  <NavLink
27
- to="/agencies"
27
+ to="/sample-data"
28
28
  className={({ isActive }) =>
29
29
  isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
30
30
  }
31
31
  >
32
- {t('app.nav_agencies')}
32
+ {t('app.nav_sample_data')}
33
33
  </NavLink>
34
34
  </nav>
35
35
  </div>
@@ -3,9 +3,16 @@
3
3
  "title": "QuadroKit",
4
4
  "tagline": "Storefront shell — swap cards for real 4D entities.",
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
+ "sample_data": {
10
+ "title": "Sample data",
11
+ "intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
12
+ "hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
13
+ "card_title": "Example rows",
14
+ "count": "{{count}} placeholder rows"
15
+ },
9
16
  "store": {
10
17
  "title": "Storefront",
11
18
  "sub": "Placeholder tiles — connect `quadro` to your catalog entities when you are ready.",
@@ -0,0 +1,38 @@
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
+ <h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
18
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
19
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
20
+ </div>
21
+ <Card>
22
+ <CardHeader>
23
+ <CardTitle>{t('sample_data.card_title')}</CardTitle>
24
+ <CardDescription>
25
+ {t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
26
+ </CardDescription>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <ul className="list-inside list-disc space-y-1 text-sm">
30
+ {PLACEHOLDER_ROWS.map((row) => (
31
+ <li key={row.id}>{row.label}</li>
32
+ ))}
33
+ </ul>
34
+ </CardContent>
35
+ </Card>
36
+ </div>
37
+ )
38
+ }
@@ -1,6 +1,6 @@
1
1
  import { createBrowserRouter } from 'react-router-dom'
2
2
  import { AppShell } from './components/AppShell'
3
- import { AgenciesPage } from './pages/AgenciesPage'
3
+ import { SampleDataPage } from './pages/SampleDataPage'
4
4
  import { HomePage } from './pages/HomePage'
5
5
 
6
6
  export const router = createBrowserRouter([
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
9
9
  element: <AppShell />,
10
10
  children: [
11
11
  { index: true, element: <HomePage /> },
12
- { path: 'agencies', element: <AgenciesPage /> },
12
+ { path: 'sample-data', element: <SampleDataPage /> },
13
13
  ],
14
14
  },
15
15
  ])
@@ -24,12 +24,12 @@ export function AppShell() {
24
24
  {t('app.nav_home')}
25
25
  </NavLink>
26
26
  <NavLink
27
- to="/agencies"
27
+ to="/sample-data"
28
28
  className={({ isActive }) =>
29
29
  isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
30
30
  }
31
31
  >
32
- {t('app.nav_agencies')}
32
+ {t('app.nav_sample_data')}
33
33
  </NavLink>
34
34
  </nav>
35
35
  </div>
@@ -3,9 +3,16 @@
3
3
  "title": "QuadroKit",
4
4
  "tagline": "Marketing-style template — same stack, lighter chrome.",
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
+ "sample_data": {
10
+ "title": "Sample data",
11
+ "intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
12
+ "hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
13
+ "card_title": "Example rows",
14
+ "count": "{{count}} placeholder rows"
15
+ },
9
16
  "hero": {
10
17
  "headline": "Ship a public site on top of your 4D datastore.",
11
18
  "sub": "QuadroKit keeps REST, auth cookies, and typed clients consistent across templates.",
@@ -46,7 +46,7 @@ export function HomePage() {
46
46
  <a href="#contact">{t('hero.cta_primary')}</a>
47
47
  </Button>
48
48
  <Button variant="outline" asChild>
49
- <a href="/agencies">{t('hero.cta_secondary')}</a>
49
+ <a href="/sample-data">{t('hero.cta_secondary')}</a>
50
50
  </Button>
51
51
  </div>
52
52
  </section>
@@ -0,0 +1,38 @@
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
+ <h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
18
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
19
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
20
+ </div>
21
+ <Card>
22
+ <CardHeader>
23
+ <CardTitle>{t('sample_data.card_title')}</CardTitle>
24
+ <CardDescription>
25
+ {t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
26
+ </CardDescription>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <ul className="list-inside list-disc space-y-1 text-sm">
30
+ {PLACEHOLDER_ROWS.map((row) => (
31
+ <li key={row.id}>{row.label}</li>
32
+ ))}
33
+ </ul>
34
+ </CardContent>
35
+ </Card>
36
+ </div>
37
+ )
38
+ }
@@ -1,6 +1,6 @@
1
1
  import { createBrowserRouter } from 'react-router-dom'
2
2
  import { AppShell } from './components/AppShell'
3
- import { AgenciesPage } from './pages/AgenciesPage'
3
+ import { SampleDataPage } from './pages/SampleDataPage'
4
4
  import { HomePage } from './pages/HomePage'
5
5
 
6
6
  export const router = createBrowserRouter([
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
9
9
  element: <AppShell />,
10
10
  children: [
11
11
  { index: true, element: <HomePage /> },
12
- { path: 'agencies', element: <AgenciesPage /> },
12
+ { path: 'sample-data', element: <SampleDataPage /> },
13
13
  ],
14
14
  },
15
15
  ])
@@ -1,77 +0,0 @@
1
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
2
- import { useEffect, useRef, useState } from 'react'
3
- import { quadro } from '@/lib/quadro-client'
4
-
5
- const PAGE_SIZE = 20
6
-
7
- export function AgenciesPage() {
8
- const [names, setNames] = useState<string[]>([])
9
- const [error, setError] = useState<string | null>(null)
10
- const loadSeq = useRef(0)
11
-
12
- useEffect(() => {
13
- const seq = ++loadSeq.current
14
- let cancelled = false
15
- ;(async () => {
16
- try {
17
- const list = quadro.Agency.all({
18
- page: 1,
19
- pageSize: PAGE_SIZE,
20
- select: ['name'],
21
- maxItems: PAGE_SIZE,
22
- })
23
- const acc: string[] = []
24
- for await (const a of list) {
25
- if (cancelled || seq !== loadSeq.current) {
26
- break
27
- }
28
- acc.push(a.name ?? '')
29
- }
30
- if (!cancelled && seq === loadSeq.current) {
31
- setNames(acc)
32
- }
33
- } catch (e) {
34
- if (!cancelled && seq === loadSeq.current) {
35
- setError(e instanceof Error ? e.message : String(e))
36
- }
37
- }
38
- })()
39
- return () => {
40
- cancelled = true
41
- }
42
- }, [])
43
-
44
- return (
45
- <div className="space-y-6">
46
- <div>
47
- <h1 className="text-2xl font-semibold">Agencies</h1>
48
- <p className="text-sm text-muted-foreground">
49
- Sample list from 4D REST (requires running server + auth cookie).
50
- </p>
51
- </div>
52
- {error && (
53
- <Card className="border-destructive/50">
54
- <CardHeader>
55
- <CardTitle className="text-destructive">Request failed</CardTitle>
56
- <CardDescription>{error}</CardDescription>
57
- </CardHeader>
58
- </Card>
59
- )}
60
- <Card>
61
- <CardHeader>
62
- <CardTitle>Names</CardTitle>
63
- <CardDescription>
64
- {names.length ? `${names.length} loaded` : 'No data yet'}
65
- </CardDescription>
66
- </CardHeader>
67
- <CardContent>
68
- <ul className="list-inside list-disc space-y-1 text-sm">
69
- {names.map((n) => (
70
- <li key={n}>{n || '(empty)'}</li>
71
- ))}
72
- </ul>
73
- </CardContent>
74
- </Card>
75
- </div>
76
- )
77
- }
@@ -1,77 +0,0 @@
1
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
2
- import { useEffect, useRef, useState } from 'react'
3
- import { quadro } from '@/lib/quadro-client'
4
-
5
- const PAGE_SIZE = 20
6
-
7
- export function AgenciesPage() {
8
- const [names, setNames] = useState<string[]>([])
9
- const [error, setError] = useState<string | null>(null)
10
- const loadSeq = useRef(0)
11
-
12
- useEffect(() => {
13
- const seq = ++loadSeq.current
14
- let cancelled = false
15
- ;(async () => {
16
- try {
17
- const list = quadro.Agency.all({
18
- page: 1,
19
- pageSize: PAGE_SIZE,
20
- select: ['name'],
21
- maxItems: PAGE_SIZE,
22
- })
23
- const acc: string[] = []
24
- for await (const a of list) {
25
- if (cancelled || seq !== loadSeq.current) {
26
- break
27
- }
28
- acc.push(a.name ?? '')
29
- }
30
- if (!cancelled && seq === loadSeq.current) {
31
- setNames(acc)
32
- }
33
- } catch (e) {
34
- if (!cancelled && seq === loadSeq.current) {
35
- setError(e instanceof Error ? e.message : String(e))
36
- }
37
- }
38
- })()
39
- return () => {
40
- cancelled = true
41
- }
42
- }, [])
43
-
44
- return (
45
- <div className="space-y-6">
46
- <div>
47
- <h1 className="text-2xl font-semibold">Agencies</h1>
48
- <p className="text-sm text-muted-foreground">
49
- Sample list from 4D REST (requires running server + auth cookie).
50
- </p>
51
- </div>
52
- {error && (
53
- <Card className="border-destructive/50">
54
- <CardHeader>
55
- <CardTitle className="text-destructive">Request failed</CardTitle>
56
- <CardDescription>{error}</CardDescription>
57
- </CardHeader>
58
- </Card>
59
- )}
60
- <Card>
61
- <CardHeader>
62
- <CardTitle>Names</CardTitle>
63
- <CardDescription>
64
- {names.length ? `${names.length} loaded` : 'No data yet'}
65
- </CardDescription>
66
- </CardHeader>
67
- <CardContent>
68
- <ul className="list-inside list-disc space-y-1 text-sm">
69
- {names.map((n) => (
70
- <li key={n}>{n || '(empty)'}</li>
71
- ))}
72
- </ul>
73
- </CardContent>
74
- </Card>
75
- </div>
76
- )
77
- }
@@ -1,77 +0,0 @@
1
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
2
- import { useEffect, useRef, useState } from 'react'
3
- import { quadro } from '@/lib/quadro-client'
4
-
5
- const PAGE_SIZE = 20
6
-
7
- export function AgenciesPage() {
8
- const [names, setNames] = useState<string[]>([])
9
- const [error, setError] = useState<string | null>(null)
10
- const loadSeq = useRef(0)
11
-
12
- useEffect(() => {
13
- const seq = ++loadSeq.current
14
- let cancelled = false
15
- ;(async () => {
16
- try {
17
- const list = quadro.Agency.all({
18
- page: 1,
19
- pageSize: PAGE_SIZE,
20
- select: ['name'],
21
- maxItems: PAGE_SIZE,
22
- })
23
- const acc: string[] = []
24
- for await (const a of list) {
25
- if (cancelled || seq !== loadSeq.current) {
26
- break
27
- }
28
- acc.push(a.name ?? '')
29
- }
30
- if (!cancelled && seq === loadSeq.current) {
31
- setNames(acc)
32
- }
33
- } catch (e) {
34
- if (!cancelled && seq === loadSeq.current) {
35
- setError(e instanceof Error ? e.message : String(e))
36
- }
37
- }
38
- })()
39
- return () => {
40
- cancelled = true
41
- }
42
- }, [])
43
-
44
- return (
45
- <div className="space-y-6">
46
- <div>
47
- <h1 className="text-2xl font-semibold">Agencies</h1>
48
- <p className="text-sm text-muted-foreground">
49
- Sample list from 4D REST (requires running server + auth cookie).
50
- </p>
51
- </div>
52
- {error && (
53
- <Card className="border-destructive/50">
54
- <CardHeader>
55
- <CardTitle className="text-destructive">Request failed</CardTitle>
56
- <CardDescription>{error}</CardDescription>
57
- </CardHeader>
58
- </Card>
59
- )}
60
- <Card>
61
- <CardHeader>
62
- <CardTitle>Names</CardTitle>
63
- <CardDescription>
64
- {names.length ? `${names.length} loaded` : 'No data yet'}
65
- </CardDescription>
66
- </CardHeader>
67
- <CardContent>
68
- <ul className="list-inside list-disc space-y-1 text-sm">
69
- {names.map((n) => (
70
- <li key={n}>{n || '(empty)'}</li>
71
- ))}
72
- </ul>
73
- </CardContent>
74
- </Card>
75
- </div>
76
- )
77
- }
@@ -1,77 +0,0 @@
1
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
2
- import { useEffect, useRef, useState } from 'react'
3
- import { quadro } from '@/lib/quadro-client'
4
-
5
- const PAGE_SIZE = 20
6
-
7
- export function AgenciesPage() {
8
- const [names, setNames] = useState<string[]>([])
9
- const [error, setError] = useState<string | null>(null)
10
- const loadSeq = useRef(0)
11
-
12
- useEffect(() => {
13
- const seq = ++loadSeq.current
14
- let cancelled = false
15
- ;(async () => {
16
- try {
17
- const list = quadro.Agency.all({
18
- page: 1,
19
- pageSize: PAGE_SIZE,
20
- select: ['name'],
21
- maxItems: PAGE_SIZE,
22
- })
23
- const acc: string[] = []
24
- for await (const a of list) {
25
- if (cancelled || seq !== loadSeq.current) {
26
- break
27
- }
28
- acc.push(a.name ?? '')
29
- }
30
- if (!cancelled && seq === loadSeq.current) {
31
- setNames(acc)
32
- }
33
- } catch (e) {
34
- if (!cancelled && seq === loadSeq.current) {
35
- setError(e instanceof Error ? e.message : String(e))
36
- }
37
- }
38
- })()
39
- return () => {
40
- cancelled = true
41
- }
42
- }, [])
43
-
44
- return (
45
- <div className="space-y-6">
46
- <div>
47
- <h1 className="text-2xl font-semibold">Agencies</h1>
48
- <p className="text-sm text-muted-foreground">
49
- Sample list from 4D REST (requires running server + auth cookie).
50
- </p>
51
- </div>
52
- {error && (
53
- <Card className="border-destructive/50">
54
- <CardHeader>
55
- <CardTitle className="text-destructive">Request failed</CardTitle>
56
- <CardDescription>{error}</CardDescription>
57
- </CardHeader>
58
- </Card>
59
- )}
60
- <Card>
61
- <CardHeader>
62
- <CardTitle>Names</CardTitle>
63
- <CardDescription>
64
- {names.length ? `${names.length} loaded` : 'No data yet'}
65
- </CardDescription>
66
- </CardHeader>
67
- <CardContent>
68
- <ul className="list-inside list-disc space-y-1 text-sm">
69
- {names.map((n) => (
70
- <li key={n}>{n || '(empty)'}</li>
71
- ))}
72
- </ul>
73
- </CardContent>
74
- </Card>
75
- </div>
76
- )
77
- }