From 257e67d6b405787f4e954f759d341ded6968aa03 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 21:01:51 +0000 Subject: [PATCH 1/6] feat(host): add LacoWiki migration wireframes browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the LacoWiki design handoff (claude.ai/design) into a single Inertia page at /lacowiki/wireframes — three tabs (Overview, Design system, Wireframes) covering all eight migration modules and the focused single-sample validation layout. Self-contained under host/client_app/lib/lacowiki/* with all rules scoped to .lw-app so the wireframes' raw 1px-line aesthetic doesn't leak into the rest of the host UI. Adds a biome override for the same paths to permit the array-index keys, anchor-as-button, and untitled SVGs that are idiomatic in mid-fidelity wireframes. https://claude.ai/code/session_01AnJvhmDHXftJPBCL29eyhC --- biome.json | 23 + host/client_app/lib/lacowiki/chrome.tsx | 159 +++++ .../lib/lacowiki/design-system-blocks.tsx | 187 ++++++ .../client_app/lib/lacowiki/design-system.tsx | 227 +++++++ host/client_app/lib/lacowiki/overview.tsx | 96 +++ host/client_app/lib/lacowiki/primitives.tsx | 141 ++++ .../lib/lacowiki/spatial-preview.tsx | 175 +++++ host/client_app/lib/lacowiki/styles.css | 624 ++++++++++++++++++ host/client_app/lib/lacowiki/wf/account.tsx | 87 +++ host/client_app/lib/lacowiki/wf/dashboard.tsx | 153 +++++ host/client_app/lib/lacowiki/wf/datasets.tsx | 325 +++++++++ host/client_app/lib/lacowiki/wf/legends.tsx | 245 +++++++ host/client_app/lib/lacowiki/wf/marketing.tsx | 232 +++++++ .../lib/lacowiki/wf/report-matrix.tsx | 235 +++++++ host/client_app/lib/lacowiki/wf/reports.tsx | 82 +++ .../lib/lacowiki/wf/sampling-generator.tsx | 245 +++++++ .../lib/lacowiki/wf/sampling-strategies.tsx | 148 +++++ host/client_app/lib/lacowiki/wf/sampling.tsx | 202 ++++++ .../lacowiki/wf/validation-create-cards.tsx | 286 ++++++++ .../lib/lacowiki/wf/validation-create.tsx | 30 + .../lib/lacowiki/wf/validation-shared.tsx | 228 +++++++ .../client_app/lib/lacowiki/wf/validation.tsx | 55 ++ host/client_app/lib/lacowiki/wireframes.tsx | 80 +++ host/client_app/pages/LacoWikiWireframes.tsx | 68 ++ host/routes.py | 6 + 25 files changed, 4339 insertions(+) create mode 100644 host/client_app/lib/lacowiki/chrome.tsx create mode 100644 host/client_app/lib/lacowiki/design-system-blocks.tsx create mode 100644 host/client_app/lib/lacowiki/design-system.tsx create mode 100644 host/client_app/lib/lacowiki/overview.tsx create mode 100644 host/client_app/lib/lacowiki/primitives.tsx create mode 100644 host/client_app/lib/lacowiki/spatial-preview.tsx create mode 100644 host/client_app/lib/lacowiki/styles.css create mode 100644 host/client_app/lib/lacowiki/wf/account.tsx create mode 100644 host/client_app/lib/lacowiki/wf/dashboard.tsx create mode 100644 host/client_app/lib/lacowiki/wf/datasets.tsx create mode 100644 host/client_app/lib/lacowiki/wf/legends.tsx create mode 100644 host/client_app/lib/lacowiki/wf/marketing.tsx create mode 100644 host/client_app/lib/lacowiki/wf/report-matrix.tsx create mode 100644 host/client_app/lib/lacowiki/wf/reports.tsx create mode 100644 host/client_app/lib/lacowiki/wf/sampling-generator.tsx create mode 100644 host/client_app/lib/lacowiki/wf/sampling-strategies.tsx create mode 100644 host/client_app/lib/lacowiki/wf/sampling.tsx create mode 100644 host/client_app/lib/lacowiki/wf/validation-create-cards.tsx create mode 100644 host/client_app/lib/lacowiki/wf/validation-create.tsx create mode 100644 host/client_app/lib/lacowiki/wf/validation-shared.tsx create mode 100644 host/client_app/lib/lacowiki/wf/validation.tsx create mode 100644 host/client_app/lib/lacowiki/wireframes.tsx create mode 100644 host/client_app/pages/LacoWikiWireframes.tsx diff --git a/biome.json b/biome.json index 74eadfcb..c30ee5c7 100644 --- a/biome.json +++ b/biome.json @@ -42,6 +42,29 @@ } } } + }, + { + "includes": [ + "host/client_app/lib/lacowiki/**", + "host/client_app/pages/LacoWikiWireframes.tsx" + ], + "linter": { + "rules": { + "a11y": { + "noSvgWithoutTitle": "off", + "useKeyWithClickEvents": "off", + "useSemanticElements": "off", + "noStaticElementInteractions": "off", + "useValidAnchor": "off" + }, + "correctness": { + "useJsxKeyInIterable": "off" + }, + "suspicious": { + "noArrayIndexKey": "off" + } + } + } } ], "javascript": { diff --git a/host/client_app/lib/lacowiki/chrome.tsx b/host/client_app/lib/lacowiki/chrome.tsx new file mode 100644 index 00000000..a3e0048f --- /dev/null +++ b/host/client_app/lib/lacowiki/chrome.tsx @@ -0,0 +1,159 @@ +import type { CSSProperties, ReactNode } from 'react'; +import { Fragment } from 'react'; + +export const ProductSidebar = ({ active = 'Datasets' }: { active?: string }) => { + const items = [ + { name: 'Dashboard', mod: '' }, + { name: 'Datasets', mod: 'datasets' }, + { name: 'Legends', mod: 'legends' }, + { name: 'Sampling', mod: 'sampling' }, + { name: 'Validation', mod: 'validation' }, + { name: 'Reports', mod: 'reports' }, + { name: 'Notifications', mod: 'notif' }, + ]; + return ( +
+
+
+ L +
+ LacoWiki +
+ {items.map((it) => ( +
+ {it.mod && } + {!it.mod && ( + + )} + {it.name} +
+ ))} +
+ v2.0 · python · alpha +
+
+ ); +}; + +export const ProductTopbar = ({ + title, + crumbs = [], + actions, +}: { + title: string; + crumbs?: string[]; + actions?: ReactNode; +}) => ( +
+
+ {crumbs.map((c, i) => ( + + {c} + {i < crumbs.length - 1 && /} + + ))} + {crumbs.length > 0 && /} + {title} +
+
+ {actions} +
+
+
+); + +export const Chrome = ({ + active, + title, + crumbs, + actions, + children, + contentStyle, +}: { + active?: string; + title: string; + crumbs?: string[]; + actions?: ReactNode; + children?: ReactNode; + contentStyle?: CSSProperties; +}) => ( +
+ +
+ +
{children}
+
+
+); diff --git a/host/client_app/lib/lacowiki/design-system-blocks.tsx b/host/client_app/lib/lacowiki/design-system-blocks.tsx new file mode 100644 index 00000000..dcf1e8df --- /dev/null +++ b/host/client_app/lib/lacowiki/design-system-blocks.tsx @@ -0,0 +1,187 @@ +import type { ReactNode } from 'react'; + +import { Pill } from './primitives'; + +export { SpatialLegend, SpatialMap } from './spatial-preview'; + +export const Swatch = ({ + name, + token, + value, + css, +}: { + name: string; + token: string; + value: string; + css: string; +}) => ( +
+
+
+ {name} + {token} +
+ {value} +
+
+); + +export const NEUTRALS: [string, string, string][] = [ + ['bg', '--bg', '#fafbfc'], + ['bg-alt', '--bg-alt', '#f3f5f7'], + ['surface', '--surface', '#ffffff'], + ['line', '--line', '#dfe3e8'], + ['ink-3', '--ink-3', '#6b7480'], + ['ink-1', '--ink-1', '#0e1116'], +]; + +export const ACCENTS: [string, string, string][] = [ + ['accent', '--accent', 'oklch(.62 .12 200)'], + ['accent-soft', '--accent-soft', 'oklch(.94 .04 200)'], + ['accent-ink', '--accent-ink', 'oklch(.34 .08 200)'], + ['ok', '--ok', 'oklch(.62 .12 155)'], + ['warn', '--warn', 'oklch(.72 .13 75)'], + ['err', '--err', 'oklch(.58 .16 25)'], +]; + +export const MODULES: [string, string][] = [ + ['Datasets', 'datasets'], + ['Legends', 'legends'], + ['Sampling', 'sampling'], + ['Validation', 'validation'], + ['Reports', 'reports'], + ['Notifications', 'notif'], +]; + +export const TYPE_SPECS: [string, ReactNode][] = [ + [ + 'Display · 44/1.05 · -0.02em', + + Map. Sample. Validate. + , + ], + [ + 'H1 · 28/1.15 · -0.01em', + + Land cover datasets + , + ], + ['H2 · 20/1.25', Sampling design], + [ + 'Body · 14/1.5', + + Stratified random sampling allocates points proportionally across legend classes. + , + ], + [ + 'Small · 12/1.4 · ink-3', + + Last updated 2 hours ago by m.lesiv@iiasa.ac.at + , + ], + [ + 'Mono · 12 · uppercase', + + EPSG:4326 · 14,328 PIXELS · COG + , + ], + [ + 'Hand · annotation', + + ← classify here + , + ], +]; + +export const SPACING: [string, number][] = [ + ['s-1', 4], + ['s-2', 8], + ['s-3', 12], + ['s-4', 16], + ['s-5', 24], + ['s-6', 32], + ['s-7', 48], + ['s-8', 64], +]; + +export const SampleCard = () => ( +
+
+ + + DATASET · COG + + ready +
+
Africa LC 2020 v3
+
+ Sentinel-2 derived classification, 10m resolution +
+
+ 4.2 GB + EPSG:4326 + shared · 3 +
+
+); + +export const SampleTable = () => ( +
+ {( + [ + ['Africa LC 2020', 'ready', '4.2 GB'], + ['Tropical Forest 2024', 'processing', '1.8 GB'], + ['Urban Extents EU', 'ready', '850 MB'], + ] as [string, 'ready' | 'processing', string][] + ).map((r, i) => ( +
+ {r[0]} + {r[1]} + + {r[2]} + +
+ ))} +
+); diff --git a/host/client_app/lib/lacowiki/design-system.tsx b/host/client_app/lib/lacowiki/design-system.tsx new file mode 100644 index 00000000..f61e2edb --- /dev/null +++ b/host/client_app/lib/lacowiki/design-system.tsx @@ -0,0 +1,227 @@ +import { + ACCENTS, + MODULES, + NEUTRALS, + SampleCard, + SampleTable, + SPACING, + SpatialLegend, + SpatialMap, + Swatch, + TYPE_SPECS, +} from './design-system-blocks'; +import { Btn, Pill, Placeholder, Stack } from './primitives'; + +export const DesignSystem = () => ( +
+
+
01 — System
+

Design system

+

+ A neutral, technical palette built around precise typography and one accent. Tokens are + intentional and few — the aim is restraint, so map data and validation imagery can carry the + visual weight. +

+
+ +
+
+
Neutrals
+ Cool grays, near-zero saturation. Used for surfaces, ink, and lines. +
+
+ {NEUTRALS.map(([n, t, v]) => ( + + ))} +
+
+ +
+
+
Accent
A single teal-cyan family for selected state, + links, and primary CTAs. Same chroma across the family. +
+
+ {ACCENTS.map(([n, t, v]) => ( + + ))} +
+
+ +
+
+
Module keys
+ Tiny color dots for sidebar items and badges, drawn from a neutral hue family at fixed + chroma. +
+
+ {MODULES.map(([n, c]) => ( +
+ + {n} + + mod-{c} + +
+ ))} +
+
+ +
+
+
Type
+ Inter for UI. JetBrains Mono for tabular data, IDs, code, technical labels. Caveat for + hand-drawn margin annotations only. +
+
+ {TYPE_SPECS.map(([meta, sample], i) => ( +
+
{meta}
+
{sample}
+
+ ))} +
+
+ +
+
+
Spacing & radius
+ 4px base. Components round to --r-2 (4px) by default; cards to{' '} + --r-3 (6px). Avoid heavy radii. +
+
+ {SPACING.map(([n, v]) => ( +
+
+
+
+
+ --{n} · {v}px +
+
+ ))} +
+
+ +
+
+
Buttons & badges
+ One primary (ink-1), accent for spatial actions, ghost for secondary. Mono badges for + status. +
+
+ Run sampling + Open on map + Cancel + Export CSV + + draft + ready + processing + shared · 3 +
+
+ +
+
+
Cards, tables, lists
+ Cards on --surface, 1px --line, no shadow. Tables zebra-free; row + hover only. +
+
+ + +
+
+ +
+
+
Spatial UI
+ The map components are the heart of the product. Restrained chrome — controls float on the + map; legend collapses; sample markers are 6–10px discs. +
+
+ + +
+
+ +
+
+
Forms & inputs
+ Native-feeling text inputs, dotted borders for drop zones. Labels live above; helper text + below in --ink-3. +
+
+ +
+
Dataset name
+
+ Africa LC 2020 v3 +
+
+ Used as the title across the workspace. +
+
+
+
Coordinate system
+
+ EPSG:4326 + ▾ +
+
+
+ +
+
+
+); diff --git a/host/client_app/lib/lacowiki/overview.tsx b/host/client_app/lib/lacowiki/overview.tsx new file mode 100644 index 00000000..8a1e70aa --- /dev/null +++ b/host/client_app/lib/lacowiki/overview.tsx @@ -0,0 +1,96 @@ +import { Row, Stack } from './primitives'; + +const MODS: [string, string, string][] = [ + ['datasets', 'Datasets', '8 tables · COG/PMTiles · sharing'], + ['legends', 'Legends', '4 tables · 10 templates · CRUD'], + ['sampling', 'Sampling', '4 tables · 7 strategies · jobs'], + ['validation', 'Validation', '6 tables · classify page'], + ['reports', 'Reports', '4 tables · matrix · sharing'], + ['notif', 'Notifications', '3 tables · 6 events · in-app feed'], +]; + +export const Overview = ({ setTab }: { setTab: (t: string) => void }) => ( +
+
LacoWiki · Python rewrite
+

+ Wireframes & design system +
+ for the v2 migration. +

+

+ A starting point for the new LacoWiki: an opinionated design system in the modern + scientific/technical mold, plus mid-fidelity wireframes covering all eight modules from the + rewrite spec — datasets, legends, sampling, validation, reports, and account. +

+ + + +
+
+
Design direction
+

Restrained chrome, data-forward

+

+ Cool neutrals, one teal-cyan accent, Inter for UI and JetBrains Mono for data. Map and + validation imagery carry the visual weight; the chrome stays out of the way. Inspired by + the working language of Linear, Mapbox, Earth Engine — not a re-skin of any one of them. +

+
    +
  • 1px lines, no shadows on chrome
  • +
  • Status uses small mono pills, never icon-only
  • +
  • Map controls float on the map; never wrap it
  • +
  • Tabular numerals everywhere they appear in tables
  • +
+
+
+
Migration scope
+

Eight modules, one shell

+ + {MODS.map((r) => ( + + + {r[1]} + + {r[2]} + + + ))} + +
+
+
+); diff --git a/host/client_app/lib/lacowiki/primitives.tsx b/host/client_app/lib/lacowiki/primitives.tsx new file mode 100644 index 00000000..5fedf28a --- /dev/null +++ b/host/client_app/lib/lacowiki/primitives.tsx @@ -0,0 +1,141 @@ +import type { CSSProperties, ReactNode } from 'react'; + +type LineWidth = 'short' | 'med' | 'long' | 'title' | 'head'; +type PillTone = 'default' | 'accent' | 'ok' | 'warn'; +type BtnKind = 'default' | 'primary' | 'accent'; + +export const Frame = ({ + name, + dim, + children, + padded = false, + style, +}: { + name: string; + dim: string; + children?: ReactNode; + padded?: boolean; + style?: CSSProperties; +}) => ( +
+
+ {name} + {dim} +
+
{children}
+
+); + +export const Line = ({ + w = 'long', + dark = false, + style, +}: { + w?: LineWidth; + dark?: boolean; + style?: CSSProperties; +}) =>
; + +export const Stack = ({ + gap = 8, + children, + style, +}: { + gap?: number; + children?: ReactNode; + style?: CSSProperties; +}) =>
{children}
; + +export const Row = ({ + gap = 8, + align = 'center', + justify = 'flex-start', + children, + style, +}: { + gap?: number; + align?: CSSProperties['alignItems']; + justify?: CSSProperties['justifyContent']; + children?: ReactNode; + style?: CSSProperties; +}) => ( +
+ {children} +
+); + +export const Pill = ({ children, tone = 'default' }: { children?: ReactNode; tone?: PillTone }) => ( + {children} +); + +export const Btn = ({ + children, + kind = 'default', + icon, +}: { + children?: ReactNode; + kind?: BtnKind; + icon?: string; +}) => ( + + {icon ? {icon} : null} + {children} + +); + +export const Placeholder = ({ + label, + h = 100, + style, +}: { + label: string; + h?: number; + style?: CSSProperties; +}) => ( +
+ {label} +
+); + +export const MapPh = ({ + h = 280, + style, + children, +}: { + h?: number; + style?: CSSProperties; + children?: ReactNode; +}) => ( +
+ {children} +
+); + +export const SectionHead = ({ + id, + label, + children, +}: { + id: string; + label: string; + children?: ReactNode; +}) => ( +
+
+
+ {label} +
+

{children}

+
+
#{id}
+
+); diff --git a/host/client_app/lib/lacowiki/spatial-preview.tsx b/host/client_app/lib/lacowiki/spatial-preview.tsx new file mode 100644 index 00000000..bc2c6a68 --- /dev/null +++ b/host/client_app/lib/lacowiki/spatial-preview.tsx @@ -0,0 +1,175 @@ +import { MapPh, Row } from './primitives'; + +const LEGEND_CLASSES: [string, string][] = [ + ['Forest land', '#3a7d44'], + ['Cropland', '#d6a456'], + ['Grassland', '#bcc972'], + ['Wetlands', '#5b8aa8'], + ['Settlements', '#9a3b3b'], + ['Other', '#7c7c7c'], +]; + +const SAMPLE_POINTS: [number, number][] = [ + [35, 45], + [55, 60], + [70, 35], + [25, 65], + [80, 70], +]; + +export const SpatialMap = () => ( +
+ +
+ + + Land cover + + + + Sentinel-2 RGB + + + + Bing aerial + +
+
+
+ + +
+
+ − +
+
+ {SAMPLE_POINTS.map(([x, y], i) => ( +
+ ))} +
+ 500 m ━━━━ +
+
+); + +export const SpatialLegend = () => ( +
+
+ IPCC LULUCF (6) + + v1.2 + +
+ {LEGEND_CLASSES.map(([n, c], i) => ( +
+ + {n} + + {i + 1} + +
+ ))} +
+); diff --git a/host/client_app/lib/lacowiki/styles.css b/host/client_app/lib/lacowiki/styles.css new file mode 100644 index 00000000..b53baa58 --- /dev/null +++ b/host/client_app/lib/lacowiki/styles.css @@ -0,0 +1,624 @@ +/* ============================================================ + LacoWiki migration wireframes + design system + Modern scientific/technical aesthetic + Ported from the design handoff bundle. All rules are scoped + under .lw-app so they don't leak into the rest of the host UI. + ============================================================ */ + +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Caveat:wght@500;600&display=swap"); + +.lw-app { + /* Type */ + --lw-font-ui: "Inter", -apple-system, system-ui, sans-serif; + --lw-font-mono: "JetBrains Mono", ui-monospace, monospace; + --lw-font-hand: "Caveat", "Kalam", cursive; + + /* Neutrals — cool gray, low saturation */ + --lw-bg: #fafbfc; + --lw-bg-alt: #f3f5f7; + --lw-bg-sunk: #eceff2; + --lw-surface: #ffffff; + --lw-ink-1: #0e1116; + --lw-ink-2: #3a424c; + --lw-ink-3: #6b7480; + --lw-ink-4: #9aa3ad; + --lw-ink-5: #c8cdd3; + --lw-line: #dfe3e8; + --lw-line-soft: #ecedf0; + + /* Accent — precise teal-cyan */ + --lw-accent: oklch(0.62 0.12 200); + --lw-accent-soft: oklch(0.94 0.04 200); + --lw-accent-ink: oklch(0.34 0.08 200); + + /* Status */ + --lw-ok: oklch(0.62 0.12 155); + --lw-warn: oklch(0.72 0.13 75); + --lw-err: oklch(0.58 0.16 25); + + /* Spacing scale */ + --lw-s-1: 4px; + --lw-s-2: 8px; + --lw-s-3: 12px; + --lw-s-4: 16px; + --lw-s-5: 24px; + --lw-s-6: 32px; + --lw-s-7: 48px; + --lw-s-8: 64px; + + /* Radii */ + --lw-r-1: 2px; + --lw-r-2: 4px; + --lw-r-3: 6px; + --lw-r-4: 10px; + + /* Density */ + --lw-row-h: 36px; + --lw-pad-card: 16px; + + /* Wireframe placeholder */ + --lw-ph-stroke: #cdd2d7; + --lw-ph-fill: #f5f6f8; + + background: var(--lw-bg); + color: var(--lw-ink-1); + font-family: var(--lw-font-ui); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + min-height: 100vh; +} + +.lw-app[data-density="compact"] { + --lw-row-h: 28px; + --lw-pad-card: 12px; +} +.lw-app[data-density="comfy"] { + --lw-row-h: 44px; + --lw-pad-card: 20px; +} + +.lw-app *, +.lw-app *::before, +.lw-app *::after { + box-sizing: border-box; +} + +/* ===== App shell ===== */ +.lw-app .topbar { + position: sticky; + top: 0; + z-index: 50; + display: flex; + align-items: center; + gap: var(--lw-s-5); + padding: var(--lw-s-3) var(--lw-s-5); + background: rgba(250, 251, 252, 0.92); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--lw-line); +} +.lw-app .topbar-brand { + display: flex; + align-items: center; + gap: var(--lw-s-3); + font-weight: 600; + letter-spacing: -0.01em; +} +.lw-app .brand-mark { + width: 22px; + height: 22px; + display: grid; + place-items: center; +} +.lw-app .tabs { + display: flex; + gap: 2px; + padding: 2px; + background: var(--lw-bg-sunk); + border-radius: var(--lw-r-3); +} +.lw-app .tab { + appearance: none; + border: 0; + background: transparent; + font: inherit; + font-size: 13px; + font-weight: 500; + color: var(--lw-ink-3); + padding: 6px 12px; + border-radius: var(--lw-r-2); + cursor: pointer; +} +.lw-app .tab[data-active="true"] { + background: var(--lw-surface); + color: var(--lw-ink-1); + box-shadow: 0 1px 2px rgba(14, 17, 22, 0.06); +} +.lw-app .topbar-meta { + margin-left: auto; + font-family: var(--lw-font-mono); + font-size: 11px; + color: var(--lw-ink-4); + letter-spacing: 0.02em; +} + +.lw-app main { + padding: var(--lw-s-7) var(--lw-s-7) var(--lw-s-8); + max-width: 1600px; + margin: 0 auto; +} + +/* ===== Section / hero typography ===== */ +.lw-app .eyebrow { + font-family: var(--lw-font-mono); + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--lw-ink-4); + margin-bottom: var(--lw-s-3); +} +.lw-app .h-display { + font-size: 44px; + line-height: 1.05; + letter-spacing: -0.02em; + font-weight: 600; + margin: 0 0 var(--lw-s-3); + text-wrap: balance; +} +.lw-app .h-section { + font-size: 22px; + letter-spacing: -0.01em; + font-weight: 600; + margin: 0 0 var(--lw-s-2); +} +.lw-app .h-sub { + font-size: 16px; + font-weight: 600; + margin: 0 0 var(--lw-s-2); +} +.lw-app .lede { + font-size: 16px; + color: var(--lw-ink-2); + max-width: 62ch; + margin: 0 0 var(--lw-s-6); + text-wrap: pretty; +} + +/* ===== Index / overview ===== */ +.lw-app .overview-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--lw-s-5); + margin-top: var(--lw-s-6); +} +.lw-app .idx-card { + border: 1px solid var(--lw-line); + background: var(--lw-surface); + padding: var(--lw-s-5); + border-radius: var(--lw-r-3); + text-decoration: none; + color: inherit; + display: flex; + flex-direction: column; + gap: var(--lw-s-3); + transition: + border-color 0.15s, + transform 0.15s; + cursor: pointer; +} +.lw-app .idx-card:hover { + border-color: var(--lw-accent); + transform: translateY(-1px); +} +.lw-app .idx-card .num { + font-family: var(--lw-font-mono); + font-size: 11px; + color: var(--lw-accent-ink); +} +.lw-app .idx-card h3 { + margin: 0; + font-size: 16px; + font-weight: 600; +} +.lw-app .idx-card p { + margin: 0; + font-size: 13px; + color: var(--lw-ink-3); +} + +/* ===== Design System tab ===== */ +.lw-app .ds-grid { + display: grid; + gap: var(--lw-s-6); +} +.lw-app .ds-row { + display: grid; + grid-template-columns: 220px 1fr; + gap: var(--lw-s-6); + padding: var(--lw-s-5) 0; + border-top: 1px solid var(--lw-line-soft); +} +.lw-app .ds-row:first-child { + border-top: 0; +} +.lw-app .ds-row .label { + color: var(--lw-ink-3); + font-size: 13px; +} +.lw-app .ds-row .label .label-title { + color: var(--lw-ink-1); + font-weight: 600; + font-size: 14px; + margin-bottom: 4px; +} + +.lw-app .swatch-row { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: var(--lw-s-3); +} +.lw-app .swatch { + border: 1px solid var(--lw-line); + border-radius: var(--lw-r-3); + overflow: hidden; + background: var(--lw-surface); +} +.lw-app .swatch .chip { + height: 64px; +} +.lw-app .swatch .meta { + padding: 8px 10px; + font-family: var(--lw-font-mono); + font-size: 11px; + color: var(--lw-ink-3); +} +.lw-app .swatch .meta b { + display: block; + color: var(--lw-ink-1); + font-weight: 600; +} + +.lw-app .type-stack > * + * { + margin-top: var(--lw-s-4); +} +.lw-app .type-spec { + display: flex; + align-items: baseline; + gap: var(--lw-s-5); + padding-bottom: var(--lw-s-3); + border-bottom: 1px dashed var(--lw-line); +} +.lw-app .type-spec .meta { + font-family: var(--lw-font-mono); + font-size: 11px; + color: var(--lw-ink-4); + min-width: 140px; +} + +.lw-app .token-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--lw-s-3); +} +.lw-app .token { + border: 1px solid var(--lw-line); + border-radius: var(--lw-r-3); + padding: var(--lw-s-3); + background: var(--lw-surface); + display: flex; + flex-direction: column; + gap: var(--lw-s-2); +} +.lw-app .token .demo { + height: 48px; + background: var(--lw-bg-sunk); + border-radius: var(--lw-r-2); + position: relative; + overflow: hidden; +} +.lw-app .token .name { + font-family: var(--lw-font-mono); + font-size: 11px; + color: var(--lw-ink-3); +} +.lw-app .token .name b { + color: var(--lw-ink-1); +} + +/* ===== Wireframe canvas ===== */ +.lw-app .wf-toc { + position: sticky; + top: 56px; + background: var(--lw-bg); + padding: var(--lw-s-3) 0; + border-bottom: 1px solid var(--lw-line); + z-index: 5; + display: flex; + flex-wrap: wrap; + gap: var(--lw-s-2); + margin-bottom: var(--lw-s-6); +} +.lw-app .wf-toc a { + font-family: var(--lw-font-mono); + font-size: 11px; + text-decoration: none; + color: var(--lw-ink-3); + padding: 4px 8px; + border: 1px solid var(--lw-line); + border-radius: var(--lw-r-2); + background: var(--lw-surface); +} +.lw-app .wf-toc a:hover { + color: var(--lw-accent-ink); + border-color: var(--lw-accent); +} + +.lw-app .wf-section { + margin: var(--lw-s-8) 0; + scroll-margin-top: 110px; +} +.lw-app .wf-section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--lw-s-5); + margin-bottom: var(--lw-s-5); + padding-bottom: var(--lw-s-3); + border-bottom: 1px solid var(--lw-line); +} +.lw-app .wf-section-head h2 { + margin: 0; + font-size: 24px; + letter-spacing: -0.01em; +} +.lw-app .wf-section-head .id { + font-family: var(--lw-font-mono); + font-size: 11px; + color: var(--lw-ink-4); +} + +.lw-app .wf-grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--lw-s-5); +} +.lw-app .wf-grid-3 { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--lw-s-5); +} +.lw-app .wf-grid-4 { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--lw-s-4); +} + +/* Frame = a wireframe artboard */ +.lw-app .frame { + background: var(--lw-surface); + border: 1px solid var(--lw-line); + border-radius: var(--lw-r-3); + overflow: hidden; + position: relative; +} +.lw-app .frame-cap { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid var(--lw-line-soft); + background: var(--lw-bg-alt); +} +.lw-app .frame-cap .name { + font-size: 12px; + font-weight: 600; +} +.lw-app .frame-cap .dim { + font-family: var(--lw-font-mono); + font-size: 10px; + color: var(--lw-ink-4); +} +.lw-app .frame-body { + padding: 0; + position: relative; +} +.lw-app .frame-body.padded { + padding: var(--lw-s-4); +} + +/* Generic wireframe building blocks */ +.lw-app .wf-line { + height: 8px; + background: var(--lw-bg-sunk); + border-radius: 4px; +} +.lw-app .wf-line.dark { + background: var(--lw-ink-5); +} +.lw-app .wf-line.short { + width: 40%; +} +.lw-app .wf-line.med { + width: 65%; +} +.lw-app .wf-line.long { + width: 90%; +} +.lw-app .wf-line.title { + height: 14px; + width: 50%; + background: var(--lw-ink-3); +} +.lw-app .wf-line.head { + height: 10px; + width: 30%; + background: var(--lw-ink-2); +} + +.lw-app .wf-stack { + display: flex; + flex-direction: column; + gap: 8px; +} +.lw-app .wf-row { + display: flex; + align-items: center; + gap: 8px; +} + +.lw-app .wf-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + font-size: 11px; + font-family: var(--lw-font-mono); + background: var(--lw-bg-alt); + border: 1px solid var(--lw-line); + border-radius: 999px; + color: var(--lw-ink-2); +} +.lw-app .wf-pill.accent { + background: var(--lw-accent-soft); + border-color: var(--lw-accent); + color: var(--lw-accent-ink); +} +.lw-app .wf-pill.ok { + background: oklch(0.95 0.04 155); + border-color: oklch(0.78 0.08 155); + color: oklch(0.32 0.08 155); +} +.lw-app .wf-pill.warn { + background: oklch(0.96 0.05 75); + border-color: oklch(0.82 0.1 75); + color: oklch(0.42 0.12 75); +} + +.lw-app .wf-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + font-size: 12px; + font-weight: 500; + border: 1px solid var(--lw-line); + border-radius: var(--lw-r-2); + background: var(--lw-surface); + color: var(--lw-ink-1); + cursor: default; +} +.lw-app .wf-btn.primary { + background: var(--lw-ink-1); + color: white; + border-color: var(--lw-ink-1); +} +.lw-app .wf-btn.accent { + background: var(--lw-accent); + color: white; + border-color: var(--lw-accent); +} + +.lw-app .placeholder { + background: repeating-linear-gradient( + -45deg, + var(--lw-ph-fill) 0, + var(--lw-ph-fill) 10px, + transparent 10px, + transparent 12px + ); + border: 1px dashed var(--lw-ph-stroke); + border-radius: var(--lw-r-2); + display: grid; + place-items: center; + font-family: var(--lw-font-mono); + font-size: 11px; + color: var(--lw-ink-4); + text-align: center; + padding: 12px; +} + +/* Map placeholder = subtle terrain texture */ +.lw-app .map-ph { + background: + radial-gradient(ellipse at 30% 40%, oklch(0.92 0.03 155) 0%, transparent 35%), + radial-gradient(ellipse at 70% 60%, oklch(0.88 0.04 200) 0%, transparent 40%), + repeating-linear-gradient(45deg, transparent 0 14px, rgba(0, 0, 0, 0.025) 14px 15px), + repeating-linear-gradient(-45deg, transparent 0 14px, rgba(0, 0, 0, 0.025) 14px 15px), + var(--lw-bg-sunk); + position: relative; + overflow: hidden; +} +.lw-app .map-ph::after { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient( + circle at 25% 35%, + transparent 0 2px, + rgba(0, 0, 0, 0.12) 2px 3px, + transparent 3px + ), + radial-gradient( + circle at 60% 50%, + transparent 0 2px, + rgba(0, 0, 0, 0.12) 2px 3px, + transparent 3px + ), + radial-gradient( + circle at 78% 30%, + transparent 0 2px, + rgba(0, 0, 0, 0.12) 2px 3px, + transparent 3px + ), + radial-gradient( + circle at 40% 70%, + transparent 0 2px, + rgba(0, 0, 0, 0.12) 2px 3px, + transparent 3px + ), + radial-gradient( + circle at 85% 80%, + transparent 0 2px, + rgba(0, 0, 0, 0.12) 2px 3px, + transparent 3px + ); +} + +.lw-app .anno { + font-family: var(--lw-font-hand); + font-size: 16px; + color: var(--lw-accent-ink); + line-height: 1.1; +} +.lw-app .anno-arrow { + stroke: var(--lw-accent-ink); + stroke-width: 1.4; + fill: none; +} + +/* Module color dots */ +.lw-app .mod-dot { + width: 8px; + height: 8px; + border-radius: 2px; + display: inline-block; +} +.lw-app .mod-datasets { + background: oklch(0.62 0.12 200); +} +.lw-app .mod-legends { + background: oklch(0.62 0.13 75); +} +.lw-app .mod-sampling { + background: oklch(0.55 0.14 285); +} +.lw-app .mod-validation { + background: oklch(0.58 0.14 155); +} +.lw-app .mod-reports { + background: oklch(0.58 0.14 25); +} +.lw-app .mod-notif { + background: oklch(0.55 0.04 250); +} diff --git a/host/client_app/lib/lacowiki/wf/account.tsx b/host/client_app/lib/lacowiki/wf/account.tsx new file mode 100644 index 00000000..2dd74d22 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/account.tsx @@ -0,0 +1,87 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, Row, Stack } from '../primitives'; + +const SECTIONS = ['Profile', 'Workspaces', 'API tokens', 'Notifications', 'Billing', 'Danger zone']; + +const FIELDS: [string, string][] = [ + ['Display name', 'Myroslava Lesiv'], + ['Email', 'm.lesiv@iiasa.ac.at'], + ['ORCID', '0000-0001-9856-1804'], + ['Affiliation', 'IIASA · Novel Data Ecosystems'], +]; + +export const WfAccount = () => ( + +
+ +
+ + {SECTIONS.map((s, i) => ( +
+ {s} +
+ ))} +
+ +
+ Profile +
+ Visible to your collaborators across LacoWiki. +
+
+ +
+ + Upload photo + PNG/JPG · 1MB max + + +
+ {FIELDS.map(([l, v]) => ( + + {l} +
+ {v} +
+
+ ))} +
+ + Cancel + Save changes + + +
+ +
+ +); diff --git a/host/client_app/lib/lacowiki/wf/dashboard.tsx b/host/client_app/lib/lacowiki/wf/dashboard.tsx new file mode 100644 index 00000000..877f7834 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/dashboard.tsx @@ -0,0 +1,153 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, Pill, Row, Stack } from '../primitives'; + +const STATS: [string, string][] = [ + ['Active projects', '4'], + ['Datasets', '12'], + ['Pending validation', '218'], + ['Reports this month', '3'], +]; + +const PROJECTS = [ + 'Africa LC 2020 — accuracy assessment', + 'Tropical forest change 2024', + 'EU urban extents validation', + 'Sahel crop mask v2', +]; +const PROJECT_MODS = ['validation', 'sampling', 'datasets', 'reports']; +const PROJECT_TONES: ('warn' | 'ok' | 'default')[] = ['warn', 'ok', 'default', 'default']; +const PROJECT_LABELS = ['218 to validate', 'sampling done', 'ready', 'draft']; +const PROJECT_AGES = ['2h', '1d', '3d', '1w']; + +const ACTIVITY = [ + 'm.lesiv shared Africa LC 2020 with you', + 'Sampling generated 500 points', + 'ConvertDatasetJob completed', + 'New comment on EU urban v2', +]; +const ACTIVITY_AGES = ['12m', '1h', '3h', 'yesterday']; + +export const WfDashboard = () => ( + +
+ + Invite + + New project + + } + > + + + {STATS.map(([l, v]) => ( +
+
+ {l} +
+
{v}
+
+ ))} +
+
+
+ + Recent projects + View all → + + {PROJECTS.map((n, i) => ( + + + {n} + {PROJECT_LABELS[i]} + + {PROJECT_AGES[i]} ago + + + ))} +
+
+ Activity + + {ACTIVITY.map((t, i) => ( + +
+
+
{t}
+
+ {ACTIVITY_AGES[i]} +
+
+ + ))} + +
+
+ + +
+ +); diff --git a/host/client_app/lib/lacowiki/wf/datasets.tsx b/host/client_app/lib/lacowiki/wf/datasets.tsx new file mode 100644 index 00000000..57d56221 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/datasets.tsx @@ -0,0 +1,325 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, MapPh, Pill, Placeholder, Row, Stack } from '../primitives'; + +const COLS_GRID = '2fr 90px 100px 90px 90px 80px 60px'; + +type Status = 'ready' | 'processing' | 'failed'; + +const ROWS: [string, string, Status, string, string, string][] = [ + ['Africa LC 2020 v3', 'COG', 'ready', '4.2 GB', '3', '2h'], + ['Tropical Forest 2024', 'PMTiles', 'processing', '1.8 GB', '—', '12h'], + ['Urban Extents EU', 'COG', 'ready', '850 MB', '8', '2d'], + ['Sahel Cropmask v2', 'COG', 'failed', '—', '—', '3d'], + ['WorldPop 2020 mask', 'PMTiles', 'ready', '2.1 GB', '12', '1w'], +]; + +export const WfDatasetsList = () => ( + +
+ + Filter + + Upload + + } + > + + +
+ Search 12 datasets… +
+ Type ▾ + Owner ▾ + Status ▾ +
+
+
+ Name + Type + Status + Size + Shared + Updated + +
+ {ROWS.map((r, i) => ( +
+ + + {r[0]} + + + {r[1]} + + {r[2]} + + {r[3]} + + + {r[4]} + + + {r[5]} + + ⋯ +
+ ))} +
+
+
+
+ +); + +export const WfDatasetUpload = () => ( + +
+ + Upload dataset + ✕ + + + 1 · file + → + 2 · metadata + → + 3 · convert + + + + + africa_lc_2020.tif + + 4.2 GB · uploading 64% + + +
+
+
+ + auto-detected EPSG:4326 · COG · 14328×11200 + + + + Cancel + Continue → + +
+ +); + +const META: [string, string][] = [ + ['CRS', 'EPSG:4326'], + ['Resolution', '10 m'], + ['Extent', 'Africa'], + ['Format', 'COG'], +]; + +const SHARES: [string, string][] = [ + ['m.lesiv@iiasa', 'owner'], + ['d.fritz@iiasa', 'editor'], + ['public link', 'viewer'], +]; + +const DETAIL_TABS = ['Overview', 'Sampling (3)', 'Validations (2)', 'Reports (1)', 'Activity']; + +export const WfDatasetDetail = () => ( + +
+ + Share + Download + Open on map + + } + > +
+ + + {DETAIL_TABS.map((t, i) => ( + + {t} + + ))} + + +
+ {META.map(([l, v]) => ( +
+
+ {l} +
+
{v}
+
+ ))} +
+
+ +
+ Sharing + + {SHARES.map(([who, role]) => ( + +
+ {who} + {role} + + ))} + + Invite people + +
+
+ Conversion log + +
✓ uploaded · 4.2 GB
+
✓ tippecanoe → PMTiles
+
✓ rio-cogeo → COG
+
✓ ready · 2h ago
+
+
+
+
+ +
+ +); diff --git a/host/client_app/lib/lacowiki/wf/legends.tsx b/host/client_app/lib/lacowiki/wf/legends.tsx new file mode 100644 index 00000000..c264e78f --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/legends.tsx @@ -0,0 +1,245 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, Pill, Placeholder, Row, Stack } from '../primitives'; + +const LEGEND_CARDS: [string, string, 'template' | 'custom' | 'draft', string][] = [ + ['IPCC LULUCF', '6 classes', 'template', '#3a7d44|#d6a456|#bcc972|#5b8aa8|#9a3b3b|#7c7c7c'], + ['CORINE L1', '5 classes', 'template', '#9a3b3b|#d6a456|#3a7d44|#5b8aa8|#bcc972'], + [ + 'FAO LCCS', + '8 classes', + 'template', + '#3a7d44|#7ba368|#bcc972|#d6a456|#a8895a|#5b8aa8|#9a3b3b|#7c7c7c', + ], + ['Sahel cropmask', '3 classes', 'custom', '#d6a456|#bcc972|#7c7c7c'], + ['EU urban extents', '4 classes', 'custom', '#9a3b3b|#c47171|#bcc972|#7c7c7c'], + [ + 'Forest types v2', + '7 classes', + 'draft', + '#2d6034|#3a7d44|#5e9c6a|#7ba368|#a3c08e|#7c7c7c|#bcc972', + ], +]; + +export const WfLegendsList = () => ( + +
+ + Import + + New legend + + } + > +
+ {LEGEND_CARDS.map(([name, count, kind, swatches]) => ( +
+ + + + {name} + + {kind} + +
+ {count} +
+ + {swatches.split('|').map((c, j) => ( +
+ ))} + +
+ ))} +
+ +
+ +); + +const CLASSES: [string, string, string, string][] = [ + ['1', 'Forest land', '#3a7d44', 'FRST'], + ['2', 'Cropland', '#d6a456', 'CROP'], + ['3', 'Grassland', '#bcc972', 'GRSS'], + ['4', 'Wetlands', '#5b8aa8', 'WTLD'], + ['5', 'Settlements', '#9a3b3b', 'STTL'], + ['6', 'Other', '#7c7c7c', 'OTHR'], +]; + +const COLOR_OPTIONS = ['#bcc972', '#9aaa55', '#d4dc9a', '#7d8b46', '#e8eed0', '#6b7a35']; + +export const WfLegendBuilder = () => ( + +
+ + Discard + Save legend + + } + > +
+
+ + Classes (6) + + Add class + + {CLASSES.map((r, i) => ( + + + {r[0]} + + + {r[1]} + + {r[3]} + + ⋮ + + ))} +
+ +
+ Class details + + + Label +
+ Grassland +
+
+ + Code +
+ GRSS +
+
+ + Color + + {COLOR_OPTIONS.map((c) => ( +
+ ))} + + + +
+
+ Description + +
+
+
+ +
+ +); diff --git a/host/client_app/lib/lacowiki/wf/marketing.tsx b/host/client_app/lib/lacowiki/wf/marketing.tsx new file mode 100644 index 00000000..729e7768 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/marketing.tsx @@ -0,0 +1,232 @@ +import { Btn, Frame, MapPh, Pill, Row, Stack } from '../primitives'; + +export const WfLanding = () => ( + +
+
+ +
+ LacoWiki + + + Product + Datasets + Methodology + Docs + + + Sign in + Request access + +
+
+ + Open platform · IIASA +
+ Validate land cover +
+ maps with confidence. +
+
+ Upload classifications, design sampling, validate against high-resolution imagery, and + publish accuracy reports — all in one workspace. +
+ + Get started + Watch demo · 2 min + + + 14k+ datasets + · + 2.4M validated points + · + open source + +
+
+ + {( + [ + [30, 40], + [60, 55], + [75, 30], + [45, 70], + [20, 55], + ] as [number, number][] + ).map(([x, y], i) => ( +
+ ))} + +
+
+ OVERALL ACCURACY +
+
87.3%
+
±2.1% at 95% CI
+
+
+
+
+ {( + [ + ['Datasets', 'Upload COG/PMTiles'], + ['Sampling', '7 strategies'], + ['Validation', 'Map-side classify'], + ['Reports', 'Confusion matrix'], + ] as [string, string][] + ).map(([t, s]) => ( +
+
{t}
+
{s}
+
+ ))} +
+
+ +); + +const inputCss = { + height: 36, + border: '1px solid var(--lw-line)', + borderRadius: 4, + padding: '0 10px', + fontSize: 13, + display: 'flex', + alignItems: 'center', + color: 'var(--lw-ink-3)', +} as const; + +export const WfLogin = () => ( + +
+
+ +
+ LacoWiki + +
Welcome back
+
+ Sign in to continue your validation work. +
+ + + Email +
you@institution.org
+
+ + + Password + Forgot? + +
••••••••
+
+ Sign in +
+ + OR + +
+ Continue with ORCID + Continue with institutional SSO +
+
+
+ +
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/report-matrix.tsx b/host/client_app/lib/lacowiki/wf/report-matrix.tsx new file mode 100644 index 00000000..c85148cb --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/report-matrix.tsx @@ -0,0 +1,235 @@ +import { Fragment } from 'react'; + +import { Chrome } from '../chrome'; +import { Btn, Frame, Row, Stack } from '../primitives'; + +const CLASSES = ['Forest', 'Crop', 'Grass', 'Wet', 'Settl', 'Other']; +const MATRIX: number[][] = [ + [82, 3, 1, 0, 0, 2], + [4, 76, 5, 1, 0, 3], + [2, 6, 71, 2, 0, 4], + [1, 1, 2, 42, 0, 1], + [0, 1, 0, 0, 33, 2], + [3, 2, 4, 1, 1, 15], +]; +const UA = [93, 82, 75, 89, 89, 58]; +const PA = [89, 84, 84, 91, 97, 57]; +const MAX = 82; + +const monoLabel: React.CSSProperties = { + fontFamily: 'var(--lw-font-mono)', + fontSize: 10, + color: 'var(--lw-ink-4)', + textAlign: 'right', + padding: '4px 8px', +}; + +const MatrixGrid = () => ( +
+ + Confusion matrix · classes + + rows = reference · cols = predicted + + +
+ + {CLASSES.map((c) => ( + + {c} + + ))} + UA + {MATRIX.map((row, i) => ( + + + {CLASSES[i]} + + {row.map((v, j) => { + const intensity = v / MAX; + return ( +
0.6 ? 'white' : 'var(--lw-ink-1)', + }} + > + {v} +
+ ); + })} + + {UA[i]}% + +
+ ))} + PA + {PA.map((v, i) => ( + + {v}% + + ))} + +
+
+); + +const MatrixSidebar = () => ( + +
+ + Overall accuracy + +
+ 87.3% +
+
+ ±2.1% · 95% CI +
+
+
+ + Cohen's κ + 0.81 + + + Quantity disagreement + 4.1% + + + Allocation disagreement + 8.6% + +
+
+
+ Methodology +
+
n = 500 stratified random
+
seed = 4831
+
est. = Olofsson 2014
+
computed = 2h ago
+
+
+); + +export const WfReportMatrix = () => ( + +
+ + Export CSV + Export PDF + Share + + } + > +
+ + +
+
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/reports.tsx b/host/client_app/lib/lacowiki/wf/reports.tsx new file mode 100644 index 00000000..e1a2f186 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/reports.tsx @@ -0,0 +1,82 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, Pill, Row, Stack } from '../primitives'; + +export { WfReportMatrix } from './report-matrix'; + +const REPORT_CARDS: [string, string, string, string][] = [ + ['Africa LC 2020 — accuracy', '87.3%', '±2.1', '2h'], + ['Tropical forest 2024', '91.2%', '±1.4', '1d'], + ['EU urban v2', '78.5%', '±3.0', '3d'], + ['Sahel cropmask v1', '82.1%', '±2.8', '1w'], +]; + +const monoUpper: React.CSSProperties = { + fontFamily: 'var(--lw-font-mono)', + fontSize: 10, + color: 'var(--lw-ink-4)', + textTransform: 'uppercase', +}; + +export const WfReportsList = () => ( + +
+ + New report} + > +
+ {REPORT_CARDS.map((r) => ( +
+ + + Accuracy report + computed + +
{r[0]}
+ + + OA + {r[1]} + + {r[2]}% CI + + + + κ + 0.81 + + + + {r[3]} ago + + + +
+ ))} +
+
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/sampling-generator.tsx b/host/client_app/lib/lacowiki/wf/sampling-generator.tsx new file mode 100644 index 00000000..d9e10ce2 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/sampling-generator.tsx @@ -0,0 +1,245 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, MapPh, Row, Stack } from '../primitives'; + +const STRATEGIES = [ + 'Simple random', + 'Stratified random', + 'Systematic grid', + 'Cluster', + 'Two-stage', + 'Equal allocation', + 'Proportional allocation', +]; + +const ALLOCATIONS: [string, string, number, number][] = [ + ['Forest land', '#3a7d44', 180, 36], + ['Cropland', '#d6a456', 120, 24], + ['Grassland', '#bcc972', 90, 18], + ['Wetlands', '#5b8aa8', 50, 10], + ['Settlements', '#9a3b3b', 40, 8], + ['Other', '#7c7c7c', 20, 4], +]; + +const DOT_COLORS = ['#3a7d44', '#d6a456', '#bcc972', '#5b8aa8', '#9a3b3b', '#7c7c7c']; + +const FieldRow = ({ + label, + value, + mono, + caret, +}: { + label: string; + value: string; + mono?: boolean; + caret?: boolean; +}) => ( + + {label} +
+ {value} + {caret ? ▾ : null} +
+
+); + +const StrategyList = () => ( +
+ 1 · Strategy + + {STRATEGIES.map((s, i) => ( + + + {s} + + ))} + +
+); + +const ParametersBox = () => ( +
+ 2 · Parameters + + + + + + +
+); + +const AllocationBox = () => ( +
+ + 3 · Allocation per class + + Σ = 500 + + + + {ALLOCATIONS.map(([n, c, count, pct]) => ( + + + {n} +
+
+
+ + {count} + + + {pct}% + + + ))} + +
+); + +const PreviewMap = () => ( +
+ + {Array.from({ length: 60 }).map((_, i) => ( +
+ ))} + +
+ preview · 60 / 500 +
+
+); + +export const WfSamplingGenerator = () => ( + +
+ + Save draft + Generate + + } + > +
+ + + + + + + + +
+
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/sampling-strategies.tsx b/host/client_app/lib/lacowiki/wf/sampling-strategies.tsx new file mode 100644 index 00000000..d5def8ee --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/sampling-strategies.tsx @@ -0,0 +1,148 @@ +import { Frame, Row, Stack } from '../primitives'; + +type Field = { l: string; v: string; mono?: boolean }; +type Strategy = { label: string; note: string; fields: Field[] }; + +const StrategyParams: Record = { + simple: { + label: 'Simple random', + note: 'Points uniformly at random across the study area.', + fields: [ + { l: 'Total points (n)', v: '100', mono: true }, + { l: 'Study area', v: 'Africa LC 2020 v3 extent' }, + { l: 'Min spacing', v: '0 m (none)', mono: true }, + { l: 'Random seed', v: '4831', mono: true }, + ], + }, + stratified: { + label: 'Stratified random', + note: 'Points allocated per legend class. See allocation panel →', + fields: [ + { l: 'Total points (n)', v: '500', mono: true }, + { l: 'Stratification', v: 'IPCC LULUCF (6)' }, + { l: 'Allocation method', v: 'proportional ▾' }, + { l: 'Min per stratum', v: '20', mono: true }, + { l: 'Random seed', v: '4831', mono: true }, + ], + }, + systematic: { + label: 'Systematic grid', + note: 'Regular lattice. Origin offset randomised by seed.', + fields: [ + { l: 'Grid spacing', v: '1.0 km', mono: true }, + { l: 'Grid type', v: 'square ▾' }, + { l: 'Random origin', v: '✓ enabled' }, + { l: 'Approx. n', v: '≈ 1240', mono: true }, + { l: 'Random seed', v: '4831', mono: true }, + ], + }, + cluster: { + label: 'Cluster', + note: 'Primary clusters drawn first, then sites within each.', + fields: [ + { l: 'Clusters (PSU)', v: '20', mono: true }, + { l: 'Cluster shape', v: '10 km square ▾' }, + { l: 'Sites per cluster', v: '10', mono: true }, + { l: 'Total points (n)', v: '200', mono: true }, + { l: 'Random seed', v: '4831', mono: true }, + ], + }, + twostage: { + label: 'Two-stage', + note: 'Strata → clusters → sites. Useful for fieldwork logistics.', + fields: [ + { l: 'Stage 1 — strata', v: 'IPCC LULUCF (6)' }, + { l: 'Stage 1 — clusters/stratum', v: '5', mono: true }, + { l: 'Stage 2 — sites/cluster', v: '10', mono: true }, + { l: 'Total points (n)', v: '300', mono: true }, + { l: 'Random seed', v: '4831', mono: true }, + ], + }, + equal: { + label: 'Equal allocation', + note: 'Same n per legend class regardless of area.', + fields: [ + { l: 'Stratification', v: 'IPCC LULUCF (6)' }, + { l: 'Points per class', v: '50', mono: true }, + { l: 'Total points (n)', v: '300', mono: true }, + { l: 'Random seed', v: '4831', mono: true }, + ], + }, + proportional: { + label: 'Proportional allocation', + note: 'n per class scaled to mapped area share.', + fields: [ + { l: 'Stratification', v: 'IPCC LULUCF (6)' }, + { l: 'Total points (n)', v: '500', mono: true }, + { l: 'Min per class', v: '20', mono: true }, + { l: 'Random seed', v: '4831', mono: true }, + ], + }, +}; + +const StrategyCard = ({ k }: { k: string }) => { + const s = StrategyParams[k]; + return ( +
+ + {s.label} + + {k} + + +
{s.note}
+ + {s.fields.map((f, i) => ( + + {f.l} +
+ {f.v} +
+
+ ))} +
+
+ ); +}; + +export const WfSamplingStrategies = () => ( + +
+
+ Each strategy swaps the right-hand panel of the generator. Below: all 7 cards side-by-side. +
+
+ {Object.keys(StrategyParams).map((k) => ( + + ))} +
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/sampling.tsx b/host/client_app/lib/lacowiki/wf/sampling.tsx new file mode 100644 index 00000000..02dd9158 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/sampling.tsx @@ -0,0 +1,202 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, MapPh, Pill, Row, Stack } from '../primitives'; + +const LIST_GRID = '2fr 130px 90px 90px 100px 80px 60px'; + +type SampleStatus = 'ready' | 'generating' | 'draft'; + +const SAMPLING_ROWS: [string, string, string, SampleStatus, string, string][] = [ + ['Africa LC stratified 500', 'stratified random', '500', 'ready', 'Africa LC 2020', '2h'], + ['Tropical systematic 1km', 'systematic grid', '1240', 'ready', 'Tropical Forest 2024', '1d'], + ['EU urban cluster 200', 'cluster', '200', 'generating', 'Urban Extents EU', '12m'], + ['Sahel simple random 100', 'simple random', '100', 'draft', 'Sahel Cropmask v2', '3d'], +]; + +export const WfSamplingList = () => ( + +
+ + New design} + > +
+
+ Design + Strategy + n + Status + Dataset + Updated + +
+ {SAMPLING_ROWS.map((r, i) => ( +
+ + + {r[0]} + + + {r[1]} + + {r[2]} + + {r[3]} + + {r[4]} + + {r[5]} + + ⋯ +
+ ))} +
+
+
+ +); + +const DOT_COLORS = ['#3a7d44', '#d6a456', '#bcc972', '#5b8aa8', '#9a3b3b', '#7c7c7c']; + +const SUMMARY: [string, string][] = [ + ['strategy', 'stratified random'], + ['n', '500'], + ['seed', '4831'], + ['legend', 'IPCC LULUCF'], + ['dataset', 'Africa LC 2020 v3'], + ['created', 'by m.lesiv · 2h'], +]; +const PER_CLASS: [string, string, string][] = [ + ['Forest', '180', '42%'], + ['Cropland', '120', '18%'], + ['Grassland', '90', '60%'], + ['Wetlands', '50', '0%'], + ['Settlements', '40', '12%'], + ['Other', '20', '0%'], +]; + +export const WfSamplingDetail = () => ( + +
+ + Export CSV + Start validation + + } + > +
+ + {Array.from({ length: 80 }).map((_, i) => ( +
+ ))} + + +
+ Summary + + {SUMMARY.map(([k, v]) => ( + + {k} + {v} + + ))} + +
+
+ Per-class progress + + {PER_CLASS.map((r) => ( + + {r[0]} + + {r[2]} + + {r[1]} + + ))} + +
+
+
+ +
+ +); diff --git a/host/client_app/lib/lacowiki/wf/validation-create-cards.tsx b/host/client_app/lib/lacowiki/wf/validation-create-cards.tsx new file mode 100644 index 00000000..41c03250 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/validation-create-cards.tsx @@ -0,0 +1,286 @@ +import { MapPh, Pill, Row, Stack } from '../primitives'; + +const IMAGERY: [string, boolean][] = [ + ['Sentinel-2', true], + ['Bing aerial', true], + ['PlanetScope', false], + ['Sentinel-1', false], + ['Google Earth', true], +]; + +const MODES: [string, string, boolean?][] = [ + ['single', 'Single rater', true], + ['consensus', 'Consensus (k≥2)'], + ['expert', 'Expert review'], +]; + +const CONFIDENCES: [string, string, boolean?][] = [ + ['off', 'Off'], + ['3', '3-step', true], + ['5', '5-step'], +]; + +const REVIEWERS: [string, string, number][] = [ + ['m.lesiv@iiasa', 'owner', 125], + ['d.fritz@iiasa', 'reviewer', 125], + ['a.subash@iiasa', 'reviewer', 125], + ['+ invite reviewer', '', 125], +]; + +const SUMMARY: [string, string][] = [ + ['samples', '500'], + ['reviewers', '3'], + ['per reviewer', '≈ 167'], + ['mode', 'single rater'], + ['buffer', '30 m'], + ['est. time', '≈ 6 h'], +]; + +const cardCss = { + border: '1px solid var(--lw-line)', + borderRadius: 6, + background: 'var(--lw-surface)', + padding: 14, +} as const; + +const fieldCss = { + height: 32, + border: '1px solid var(--lw-line)', + borderRadius: 4, + padding: '0 10px', + fontSize: 12, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +} as const; + +const compactFieldCss = { + height: 28, + border: '1px solid var(--lw-line)', + borderRadius: 4, + padding: '0 10px', + fontSize: 12, + display: 'flex', + alignItems: 'center', +} as const; + +export const SourceCard = () => ( +
+ 1 · Source + + + Sampling design +
+ Africa LC stratified 500 + ▾ +
+
+ + Legend (classification options) +
+ IPCC LULUCF (6) + ▾ +
+
+ + Reference imagery layers + + {IMAGERY.map(([n, on]) => ( + + {on ? '✓ ' : ''} + {n} + + ))} + + +
+
+); + +export const WorkflowCard = () => ( +
+ 2 · Workflow + + + Mode + + {MODES.map(([k, l, on]) => ( + + {l} + + ))} + + + + + Buffer radius +
30 m
+
+ + Sample order +
random ▾
+
+ + Allow skip +
✓ with reason
+
+
+ + Confidence capture + + {CONFIDENCES.map(([k, l, on]) => ( + + {l} + + ))} + + +
+
+); + +export const ReviewersCard = () => ( +
+ 3 · Reviewers + + {REVIEWERS.map(([who, role, n], i) => ( + +
+ + {who} + + {role && {role}} + {role && ( + + {n} samples + + )} + + ))} + +
+); + +export const SummarySidebar = () => ( + +
+ + Summary + + + {SUMMARY.map(([k, v]) => ( + + {k} + {v} + + ))} + +
+
+ + {Array.from({ length: 50 }).map((_, i) => ( +
+ ))} + +
+ 500 samples +
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/validation-create.tsx b/host/client_app/lib/lacowiki/wf/validation-create.tsx new file mode 100644 index 00000000..341b2b1d --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/validation-create.tsx @@ -0,0 +1,30 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, Stack } from '../primitives'; +import { ReviewersCard, SourceCard, SummarySidebar, WorkflowCard } from './validation-create-cards'; + +export const WfValidationCreate = () => ( + +
+ + Cancel + Create campaign + + } + > +
+ + + + + + +
+
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/validation-shared.tsx b/host/client_app/lib/lacowiki/wf/validation-shared.tsx new file mode 100644 index 00000000..b352e3e3 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/validation-shared.tsx @@ -0,0 +1,228 @@ +import type { ReactNode } from 'react'; + +import { Btn, Frame, Placeholder, Row, Stack } from '../primitives'; + +export const ValidationCommon = ({ children, label }: { children: ReactNode; label: string }) => ( + +
+
+ + Validation / + Africa LC stratified 500 / + Sample 47 / 500 + + + + ← prev · next → + + + Save & next + Submit + +
+ {children} +
+ +); + +const CLASSIFY_OPTIONS: [string, string, string, boolean?][] = [ + ['1', 'Forest land', '#3a7d44', true], + ['2', 'Cropland', '#d6a456'], + ['3', 'Grassland', '#bcc972'], + ['4', 'Wetlands', '#5b8aa8'], + ['5', 'Settlements', '#9a3b3b'], + ['6', 'Other', '#7c7c7c'], +]; + +export const ClassifyPanel = ({ compact = false }: { compact?: boolean }) => ( + +
+ Classify · sample #47 +
+
+ Pick the dominant land cover within the buffer. +
+ {CLASSIFY_OPTIONS.map(([num, label, color, selected]) => ( + + + {num} + + + {label} + {selected && ( + + ✓ + + )} + + ))} + + Confidence + + {(['low', 'med', 'high'] as const).map((c, i) => ( + + {c} + + ))} + + + {!compact && } +
+); + +export const ActiveSample = ({ kind = 'point' }: { kind?: 'point' | 'polygon' }) => ( + <> +
+
+ {kind === 'point' ? ( + <> +
+
+
+ + ) : ( + + + + )} +
+ 14.32°N, 38.21°E · 30m +
+ +); diff --git a/host/client_app/lib/lacowiki/wf/validation.tsx b/host/client_app/lib/lacowiki/wf/validation.tsx new file mode 100644 index 00000000..e44fe1ed --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/validation.tsx @@ -0,0 +1,55 @@ +import { MapPh } from '../primitives'; +import { ActiveSample, ClassifyPanel, ValidationCommon } from './validation-shared'; + +const BASEMAP_TABS = ['Sentinel-2', 'Bing', 'PlanetScope', 'Sentinel-1']; + +export const WfValidationB = () => ( + +
+
+ + + +
+ {BASEMAP_TABS.map((b, i) => ( + + {b} + + ))} +
+
+
+ +
+ 47 / 500 · 9.4% +
+
+
+
+); diff --git a/host/client_app/lib/lacowiki/wireframes.tsx b/host/client_app/lib/lacowiki/wireframes.tsx new file mode 100644 index 00000000..8f4759dd --- /dev/null +++ b/host/client_app/lib/lacowiki/wireframes.tsx @@ -0,0 +1,80 @@ +import type { ReactNode } from 'react'; + +import { SectionHead } from './primitives'; +import { WfAccount } from './wf/account'; +import { WfDashboard } from './wf/dashboard'; +import { WfDatasetDetail, WfDatasetsList, WfDatasetUpload } from './wf/datasets'; +import { WfLegendBuilder, WfLegendsList } from './wf/legends'; +import { WfLanding, WfLogin } from './wf/marketing'; +import { WfReportMatrix, WfReportsList } from './wf/reports'; +import { WfSamplingDetail, WfSamplingList } from './wf/sampling'; +import { WfSamplingGenerator } from './wf/sampling-generator'; +import { WfSamplingStrategies } from './wf/sampling-strategies'; +import { WfValidationB } from './wf/validation'; +import { WfValidationCreate } from './wf/validation-create'; + +type Section = { id: string; label: string; items: ReactNode[] }; + +const SECTIONS: Section[] = [ + { id: 'marketing', label: 'Marketing & auth', items: [, ] }, + { id: 'dashboard', label: 'Dashboard', items: [] }, + { + id: 'datasets', + label: 'Datasets', + items: [, , ], + }, + { id: 'legends', label: 'Legends', items: [, ] }, + { + id: 'sampling', + label: 'Sampling', + items: [ + , + , + , + , + ], + }, + { id: 'validation', label: 'Validation', items: [, ] }, + { id: 'reports', label: 'Reports', items: [, ] }, + { id: 'account', label: 'Account', items: [] }, +]; + +const sectionPad = (i: number) => (i + 1 < 10 ? `0${i + 1}` : String(i + 1)); + +const gridClassFor = (s: Section) => { + if (s.id === 'validation') return 'wf-grid-2'; + if (s.items.length >= 2) return 'wf-grid-2'; + return ''; +}; + +export const Wireframes = () => ( +
+
02 — Wireframes
+

Screens.

+

+ Mid-fidelity, one accent color, tabular numerals. Validation focuses on one sample at a time — + a single classify panel beside the map. +

+ +
+ {SECTIONS.map((s) => ( + + ↳ {s.label} + + ))} +
+ + {SECTIONS.map((s, i) => ( +
+ + {s.label} + +
+ {s.items.map((Item, idx) => ( +
{Item}
+ ))} +
+
+ ))} +
+); diff --git a/host/client_app/pages/LacoWikiWireframes.tsx b/host/client_app/pages/LacoWikiWireframes.tsx new file mode 100644 index 00000000..6337243d --- /dev/null +++ b/host/client_app/pages/LacoWikiWireframes.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; + +import '../lib/lacowiki/styles.css'; +import { DesignSystem } from '../lib/lacowiki/design-system'; +import { Overview } from '../lib/lacowiki/overview'; +import { Wireframes } from '../lib/lacowiki/wireframes'; + +const TABS: [string, string][] = [ + ['overview', 'Overview'], + ['system', 'Design system'], + ['wireframes', 'Wireframes'], +]; + +function LacoWikiWireframes() { + const [tab, setTab] = useState('overview'); + + return ( +
+
+
+
+ + + + + +
+ LacoWiki{' '} + + / migration wireframes + +
+
+ {TABS.map(([id, label]) => ( + + ))} +
+
v0.1 · 8 modules · 23 frames
+
+ +
+ {tab === 'overview' && } + {tab === 'system' && } + {tab === 'wireframes' && } +
+
+ ); +} + +LacoWikiWireframes.layout = (page: React.ReactNode) => page; + +export default LacoWikiWireframes; diff --git a/host/routes.py b/host/routes.py index dd425bcd..38b95ac6 100644 --- a/host/routes.py +++ b/host/routes.py @@ -24,3 +24,9 @@ async def landing(request: Request, inertia: InertiaDep) -> InertiaResponse: "isAuthenticated": is_authenticated, }, ) + + +@router.get("/lacowiki/wireframes", response_model=None) +async def lacowiki_wireframes(inertia: InertiaDep) -> InertiaResponse: + """LacoWiki migration wireframes & design system browser.""" + return await inertia.render("LacoWikiWireframes", {}) From e409cc8869acafe273895381dee394989736888a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 21:04:54 +0000 Subject: [PATCH 2/6] fix(lacowiki): split dataset detail to keep datasets.tsx under cap Biome's formatter pushed wf/datasets.tsx to 325 lines after the initial commit, breaking the 300-line cap. Extract WfDatasetDetail into wf/dataset-detail.tsx (147 lines) and re-export from datasets.tsx. https://claude.ai/code/session_01AnJvhmDHXftJPBCL29eyhC --- .../lib/lacowiki/wf/dataset-detail.tsx | 147 ++++++++++++++++++ host/client_app/lib/lacowiki/wf/datasets.tsx | 140 +---------------- 2 files changed, 150 insertions(+), 137 deletions(-) create mode 100644 host/client_app/lib/lacowiki/wf/dataset-detail.tsx diff --git a/host/client_app/lib/lacowiki/wf/dataset-detail.tsx b/host/client_app/lib/lacowiki/wf/dataset-detail.tsx new file mode 100644 index 00000000..dc052947 --- /dev/null +++ b/host/client_app/lib/lacowiki/wf/dataset-detail.tsx @@ -0,0 +1,147 @@ +import { Chrome } from '../chrome'; +import { Btn, Frame, MapPh, Pill, Row, Stack } from '../primitives'; + +const META: [string, string][] = [ + ['CRS', 'EPSG:4326'], + ['Resolution', '10 m'], + ['Extent', 'Africa'], + ['Format', 'COG'], +]; + +const SHARES: [string, string][] = [ + ['m.lesiv@iiasa', 'owner'], + ['d.fritz@iiasa', 'editor'], + ['public link', 'viewer'], +]; + +const DETAIL_TABS = ['Overview', 'Sampling (3)', 'Validations (2)', 'Reports (1)', 'Activity']; + +const cardCss = { + border: '1px solid var(--lw-line)', + borderRadius: 6, + background: 'var(--lw-surface)', + padding: 12, +} as const; + +const TabBar = () => ( + + {DETAIL_TABS.map((t, i) => ( + + {t} + + ))} + +); + +const MetaGrid = () => ( +
+ {META.map(([l, v]) => ( +
+
+ {l} +
+
{v}
+
+ ))} +
+); + +const SharingCard = () => ( +
+ Sharing + + {SHARES.map(([who, role]) => ( + +
+ {who} + {role} + + ))} + + Invite people + +
+); + +const ConversionLog = () => ( +
+ Conversion log + +
✓ uploaded · 4.2 GB
+
✓ tippecanoe → PMTiles
+
✓ rio-cogeo → COG
+
✓ ready · 2h ago
+
+
+); + +export const WfDatasetDetail = () => ( + +
+ + Share + Download + Open on map + + } + > +
+ + + + + + + + + +
+
+
+ +); diff --git a/host/client_app/lib/lacowiki/wf/datasets.tsx b/host/client_app/lib/lacowiki/wf/datasets.tsx index 57d56221..a377bef4 100644 --- a/host/client_app/lib/lacowiki/wf/datasets.tsx +++ b/host/client_app/lib/lacowiki/wf/datasets.tsx @@ -1,5 +1,7 @@ import { Chrome } from '../chrome'; -import { Btn, Frame, MapPh, Pill, Placeholder, Row, Stack } from '../primitives'; +import { Btn, Frame, Placeholder, Row, Stack } from '../primitives'; + +export { WfDatasetDetail } from './dataset-detail'; const COLS_GRID = '2fr 90px 100px 90px 90px 80px 60px'; @@ -187,139 +189,3 @@ export const WfDatasetUpload = () => (
); - -const META: [string, string][] = [ - ['CRS', 'EPSG:4326'], - ['Resolution', '10 m'], - ['Extent', 'Africa'], - ['Format', 'COG'], -]; - -const SHARES: [string, string][] = [ - ['m.lesiv@iiasa', 'owner'], - ['d.fritz@iiasa', 'editor'], - ['public link', 'viewer'], -]; - -const DETAIL_TABS = ['Overview', 'Sampling (3)', 'Validations (2)', 'Reports (1)', 'Activity']; - -export const WfDatasetDetail = () => ( - -
- - Share - Download - Open on map - - } - > -
- - - {DETAIL_TABS.map((t, i) => ( - - {t} - - ))} - - -
- {META.map(([l, v]) => ( -
-
- {l} -
-
{v}
-
- ))} -
-
- -
- Sharing - - {SHARES.map(([who, role]) => ( - -
- {who} - {role} - - ))} - + Invite people - -
-
- Conversion log - -
✓ uploaded · 4.2 GB
-
✓ tippecanoe → PMTiles
-
✓ rio-cogeo → COG
-
✓ ready · 2h ago
-
-
-
-
- -
- -); From 5197735bd864739867febf93a1c26e9bfb459ba8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 21:07:43 +0000 Subject: [PATCH 3/6] fix(lacowiki): restore Pill import in datasets list The earlier split kept the dataset list table (which uses Pill for status badges) in datasets.tsx but accidentally dropped Pill from the import after the formatter rewrote the import line. https://claude.ai/code/session_01AnJvhmDHXftJPBCL29eyhC --- host/client_app/lib/lacowiki/wf/datasets.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host/client_app/lib/lacowiki/wf/datasets.tsx b/host/client_app/lib/lacowiki/wf/datasets.tsx index a377bef4..03078465 100644 --- a/host/client_app/lib/lacowiki/wf/datasets.tsx +++ b/host/client_app/lib/lacowiki/wf/datasets.tsx @@ -1,5 +1,5 @@ import { Chrome } from '../chrome'; -import { Btn, Frame, Placeholder, Row, Stack } from '../primitives'; +import { Btn, Frame, Pill, Placeholder, Row, Stack } from '../primitives'; export { WfDatasetDetail } from './dataset-detail'; From 5c6821d26352a7e3a7f3b9b49e9663ec5808de9c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 21:17:39 +0000 Subject: [PATCH 4/6] refactor(lacowiki): split into a proper module; host stays a shell Move the LacoWiki UI out of the host into a real plugin module so the host is just a shell. - Shared lacowiki primitives, chrome, design-system blocks, and styles.css move from host/client_app/lib/lacowiki/* to packages/ui/src/lacowiki/* and are exposed via subpath exports (./lacowiki/*, ./lacowiki/wf/*, ./lacowiki/styles.css). - New modules/lacowiki/ plugin owns its own Inertia pages (Home/Datasets/Legends/Sampling/Validation/Reports/Account), view endpoints under /lacowiki/*, sidebar menu entries, and locale dir. Registered in host/pyproject.toml + root pyproject.toml. - Host loses the /lacowiki/wireframes route and the LacoWikiWireframes.tsx page; only the public landing remains. - Smoke test: each /lacowiki/* page returns 200 via the authenticated client. Plus all existing checks (file-size cap, biome, ruff, ty, vite build, tsc) still pass. https://claude.ai/code/session_01AnJvhmDHXftJPBCL29eyhC --- biome.json | 5 +- host/pyproject.toml | 2 + host/routes.py | 6 --- modules/lacowiki/README.md | 12 +++++ modules/lacowiki/lacowiki/__init__.py | 0 .../lacowiki/lacowiki/endpoints/__init__.py | 0 modules/lacowiki/lacowiki/endpoints/views.py | 45 +++++++++++++++++ modules/lacowiki/lacowiki/locales/en.json | 10 ++++ modules/lacowiki/lacowiki/module.py | 50 +++++++++++++++++++ modules/lacowiki/lacowiki/pages/Account.tsx | 19 +++++++ modules/lacowiki/lacowiki/pages/Datasets.tsx | 25 ++++++++++ .../lacowiki/lacowiki/pages/Home.tsx | 14 +++--- modules/lacowiki/lacowiki/pages/Legends.tsx | 23 +++++++++ modules/lacowiki/lacowiki/pages/Reports.tsx | 24 +++++++++ modules/lacowiki/lacowiki/pages/Sampling.tsx | 29 +++++++++++ .../lacowiki/lacowiki/pages/Validation.tsx | 26 ++++++++++ modules/lacowiki/lacowiki/py.typed | 0 modules/lacowiki/package.json | 15 ++++++ modules/lacowiki/pyproject.toml | 46 +++++++++++++++++ modules/lacowiki/tests/test_lacowiki.py | 23 +++++++++ modules/lacowiki/tsconfig.json | 11 ++++ packages/ui/package.json | 11 +++- .../ui/src}/lacowiki/chrome.tsx | 0 .../ui/src}/lacowiki/design-system-blocks.tsx | 0 .../ui/src}/lacowiki/design-system.tsx | 0 .../ui/src}/lacowiki/overview.tsx | 0 .../ui/src}/lacowiki/primitives.tsx | 0 .../ui/src}/lacowiki/spatial-preview.tsx | 0 .../ui/src}/lacowiki/styles.css | 0 .../ui/src}/lacowiki/wf/account.tsx | 0 .../ui/src}/lacowiki/wf/dashboard.tsx | 0 .../ui/src}/lacowiki/wf/dataset-detail.tsx | 0 .../ui/src}/lacowiki/wf/datasets.tsx | 0 .../ui/src}/lacowiki/wf/legends.tsx | 0 .../ui/src}/lacowiki/wf/marketing.tsx | 0 .../ui/src}/lacowiki/wf/report-matrix.tsx | 0 .../ui/src}/lacowiki/wf/reports.tsx | 0 .../src}/lacowiki/wf/sampling-generator.tsx | 0 .../src}/lacowiki/wf/sampling-strategies.tsx | 0 .../ui/src}/lacowiki/wf/sampling.tsx | 0 .../lacowiki/wf/validation-create-cards.tsx | 0 .../ui/src}/lacowiki/wf/validation-create.tsx | 0 .../ui/src}/lacowiki/wf/validation-shared.tsx | 0 .../ui/src}/lacowiki/wf/validation.tsx | 0 .../ui/src}/lacowiki/wireframes.tsx | 0 pyproject.toml | 3 +- 46 files changed, 380 insertions(+), 19 deletions(-) create mode 100644 modules/lacowiki/README.md create mode 100644 modules/lacowiki/lacowiki/__init__.py create mode 100644 modules/lacowiki/lacowiki/endpoints/__init__.py create mode 100644 modules/lacowiki/lacowiki/endpoints/views.py create mode 100644 modules/lacowiki/lacowiki/locales/en.json create mode 100644 modules/lacowiki/lacowiki/module.py create mode 100644 modules/lacowiki/lacowiki/pages/Account.tsx create mode 100644 modules/lacowiki/lacowiki/pages/Datasets.tsx rename host/client_app/pages/LacoWikiWireframes.tsx => modules/lacowiki/lacowiki/pages/Home.tsx (81%) create mode 100644 modules/lacowiki/lacowiki/pages/Legends.tsx create mode 100644 modules/lacowiki/lacowiki/pages/Reports.tsx create mode 100644 modules/lacowiki/lacowiki/pages/Sampling.tsx create mode 100644 modules/lacowiki/lacowiki/pages/Validation.tsx create mode 100644 modules/lacowiki/lacowiki/py.typed create mode 100644 modules/lacowiki/package.json create mode 100644 modules/lacowiki/pyproject.toml create mode 100644 modules/lacowiki/tests/test_lacowiki.py create mode 100644 modules/lacowiki/tsconfig.json rename {host/client_app/lib => packages/ui/src}/lacowiki/chrome.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/design-system-blocks.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/design-system.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/overview.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/primitives.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/spatial-preview.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/styles.css (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/account.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/dashboard.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/dataset-detail.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/datasets.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/legends.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/marketing.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/report-matrix.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/reports.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/sampling-generator.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/sampling-strategies.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/sampling.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/validation-create-cards.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/validation-create.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/validation-shared.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wf/validation.tsx (100%) rename {host/client_app/lib => packages/ui/src}/lacowiki/wireframes.tsx (100%) diff --git a/biome.json b/biome.json index c30ee5c7..07eec2af 100644 --- a/biome.json +++ b/biome.json @@ -44,10 +44,7 @@ } }, { - "includes": [ - "host/client_app/lib/lacowiki/**", - "host/client_app/pages/LacoWikiWireframes.tsx" - ], + "includes": ["packages/ui/src/lacowiki/**", "modules/lacowiki/lacowiki/pages/**"], "linter": { "rules": { "a11y": { diff --git a/host/pyproject.toml b/host/pyproject.toml index 35bce23f..b66c1fda 100644 --- a/host/pyproject.toml +++ b/host/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "simple_module_file_storage", "simple_module_settings", "simple_module_feature_flags", + "simple_module_lacowiki", "python-multipart>=0.0.6", ] @@ -24,3 +25,4 @@ simple_module_background_tasks = { workspace = true } simple_module_file_storage = { workspace = true } simple_module_settings = { workspace = true } simple_module_feature_flags = { workspace = true } +simple_module_lacowiki = { workspace = true } diff --git a/host/routes.py b/host/routes.py index 38b95ac6..dd425bcd 100644 --- a/host/routes.py +++ b/host/routes.py @@ -24,9 +24,3 @@ async def landing(request: Request, inertia: InertiaDep) -> InertiaResponse: "isAuthenticated": is_authenticated, }, ) - - -@router.get("/lacowiki/wireframes", response_model=None) -async def lacowiki_wireframes(inertia: InertiaDep) -> InertiaResponse: - """LacoWiki migration wireframes & design system browser.""" - return await inertia.render("LacoWikiWireframes", {}) diff --git a/modules/lacowiki/README.md b/modules/lacowiki/README.md new file mode 100644 index 00000000..234b3147 --- /dev/null +++ b/modules/lacowiki/README.md @@ -0,0 +1,12 @@ +# LacoWiki module + +Migration wireframes for the LacoWiki land-cover validation platform. + +This module ships mid-fidelity Inertia pages for the eight LacoWiki domains (datasets, legends, sampling, validation, reports, account) plus the design-system browser. Pages are display-only — no real data models yet. + +Routes registered under `/lacowiki/`: + +- `/lacowiki/` — wireframes browser (Overview / Design system / Wireframes) +- `/lacowiki/datasets`, `/lacowiki/legends`, `/lacowiki/sampling`, `/lacowiki/validation`, `/lacowiki/reports`, `/lacowiki/account` + +Shared primitives, chrome, and styles live in `@simple-module-py/ui/lacowiki/*`. diff --git a/modules/lacowiki/lacowiki/__init__.py b/modules/lacowiki/lacowiki/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/lacowiki/lacowiki/endpoints/__init__.py b/modules/lacowiki/lacowiki/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/lacowiki/lacowiki/endpoints/views.py b/modules/lacowiki/lacowiki/endpoints/views.py new file mode 100644 index 00000000..b68d41ef --- /dev/null +++ b/modules/lacowiki/lacowiki/endpoints/views.py @@ -0,0 +1,45 @@ +"""Inertia view endpoints for the LacoWiki migration wireframes.""" + +from __future__ import annotations + +from fastapi import APIRouter +from inertia import InertiaResponse +from simple_module_hosting.inertia_deps import InertiaDep + +router = APIRouter() + + +@router.get("/", response_model=None) +async def home(inertia: InertiaDep) -> InertiaResponse: + """Wireframes browser — Overview / Design system / Wireframes tabs.""" + return await inertia.render("LacoWiki/Home", {}) + + +@router.get("/datasets", response_model=None) +async def datasets(inertia: InertiaDep) -> InertiaResponse: + return await inertia.render("LacoWiki/Datasets", {}) + + +@router.get("/legends", response_model=None) +async def legends(inertia: InertiaDep) -> InertiaResponse: + return await inertia.render("LacoWiki/Legends", {}) + + +@router.get("/sampling", response_model=None) +async def sampling(inertia: InertiaDep) -> InertiaResponse: + return await inertia.render("LacoWiki/Sampling", {}) + + +@router.get("/validation", response_model=None) +async def validation(inertia: InertiaDep) -> InertiaResponse: + return await inertia.render("LacoWiki/Validation", {}) + + +@router.get("/reports", response_model=None) +async def reports(inertia: InertiaDep) -> InertiaResponse: + return await inertia.render("LacoWiki/Reports", {}) + + +@router.get("/account", response_model=None) +async def account(inertia: InertiaDep) -> InertiaResponse: + return await inertia.render("LacoWiki/Account", {}) diff --git a/modules/lacowiki/lacowiki/locales/en.json b/modules/lacowiki/lacowiki/locales/en.json new file mode 100644 index 00000000..69275872 --- /dev/null +++ b/modules/lacowiki/lacowiki/locales/en.json @@ -0,0 +1,10 @@ +{ + "menu": { + "lacowiki": "LacoWiki", + "datasets": "Datasets", + "legends": "Legends", + "sampling": "Sampling", + "validation": "Validation", + "reports": "Reports" + } +} diff --git a/modules/lacowiki/lacowiki/module.py b/modules/lacowiki/lacowiki/module.py new file mode 100644 index 00000000..d2585619 --- /dev/null +++ b/modules/lacowiki/lacowiki/module.py @@ -0,0 +1,50 @@ +"""LacoWiki module — migration wireframes for the LacoWiki rewrite.""" + +from __future__ import annotations + +import importlib.resources +from pathlib import Path + +from fastapi import APIRouter +from simple_module_core.menu import MenuItem, MenuRegistry, MenuSection +from simple_module_core.module import ModuleBase, ModuleMeta + +_MODULE_USERS = "Users" +_ICON_DEFAULT = "layout-grid" + + +class LacoWikiModule(ModuleBase): + meta = ModuleMeta( + name="LacoWiki", + route_prefix="/api/lacowiki", + view_prefix="/lacowiki", + depends_on=[_MODULE_USERS], + ) + + def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None: + from lacowiki.endpoints.views import router as views + + view_router.include_router(views) + + def register_menu_items(self, registry: MenuRegistry) -> None: + items = [ + ("Datasets", "/lacowiki/datasets", 20), + ("Legends", "/lacowiki/legends", 21), + ("Sampling", "/lacowiki/sampling", 22), + ("Validation", "/lacowiki/validation", 23), + ("Reports", "/lacowiki/reports", 24), + ("LacoWiki", "/lacowiki/", 19), + ] + for label, url, order in items: + registry.add( + MenuItem( + label=label, + url=url, + icon=_ICON_DEFAULT, + order=order, + section=MenuSection.SIDEBAR, + ) + ) + + def locale_dirs(self) -> dict[str, Path]: + return {"lacowiki": Path(str(importlib.resources.files(__package__) / "locales"))} diff --git a/modules/lacowiki/lacowiki/pages/Account.tsx b/modules/lacowiki/lacowiki/pages/Account.tsx new file mode 100644 index 00000000..e0d4d60e --- /dev/null +++ b/modules/lacowiki/lacowiki/pages/Account.tsx @@ -0,0 +1,19 @@ +import '@simple-module-py/ui/lacowiki/styles.css'; +import { WfAccount } from '@simple-module-py/ui/lacowiki/wf/account'; + +function LacoWikiAccount() { + return ( +
+
+
Account
+

Account & settings.

+

Profile, workspaces, API tokens, notifications, billing.

+ +
+
+ ); +} + +LacoWikiAccount.layout = (page: React.ReactNode) => page; + +export default LacoWikiAccount; diff --git a/modules/lacowiki/lacowiki/pages/Datasets.tsx b/modules/lacowiki/lacowiki/pages/Datasets.tsx new file mode 100644 index 00000000..941a1a17 --- /dev/null +++ b/modules/lacowiki/lacowiki/pages/Datasets.tsx @@ -0,0 +1,25 @@ +import { Stack } from '@simple-module-py/ui/lacowiki/primitives'; +import '@simple-module-py/ui/lacowiki/styles.css'; +import { WfDatasetDetail } from '@simple-module-py/ui/lacowiki/wf/dataset-detail'; +import { WfDatasetsList, WfDatasetUpload } from '@simple-module-py/ui/lacowiki/wf/datasets'; + +function LacoWikiDatasets() { + return ( +
+
+
Datasets
+

Datasets — list, upload, detail.

+

Per-dataset surface: list view, upload flow, detail with sharing.

+ + + + + +
+
+ ); +} + +LacoWikiDatasets.layout = (page: React.ReactNode) => page; + +export default LacoWikiDatasets; diff --git a/host/client_app/pages/LacoWikiWireframes.tsx b/modules/lacowiki/lacowiki/pages/Home.tsx similarity index 81% rename from host/client_app/pages/LacoWikiWireframes.tsx rename to modules/lacowiki/lacowiki/pages/Home.tsx index 6337243d..df852cbd 100644 --- a/host/client_app/pages/LacoWikiWireframes.tsx +++ b/modules/lacowiki/lacowiki/pages/Home.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; -import '../lib/lacowiki/styles.css'; -import { DesignSystem } from '../lib/lacowiki/design-system'; -import { Overview } from '../lib/lacowiki/overview'; -import { Wireframes } from '../lib/lacowiki/wireframes'; +import '@simple-module-py/ui/lacowiki/styles.css'; +import { DesignSystem } from '@simple-module-py/ui/lacowiki/design-system'; +import { Overview } from '@simple-module-py/ui/lacowiki/overview'; +import { Wireframes } from '@simple-module-py/ui/lacowiki/wireframes'; const TABS: [string, string][] = [ ['overview', 'Overview'], @@ -11,7 +11,7 @@ const TABS: [string, string][] = [ ['wireframes', 'Wireframes'], ]; -function LacoWikiWireframes() { +function LacoWikiHome() { const [tab, setTab] = useState('overview'); return ( @@ -63,6 +63,6 @@ function LacoWikiWireframes() { ); } -LacoWikiWireframes.layout = (page: React.ReactNode) => page; +LacoWikiHome.layout = (page: React.ReactNode) => page; -export default LacoWikiWireframes; +export default LacoWikiHome; diff --git a/modules/lacowiki/lacowiki/pages/Legends.tsx b/modules/lacowiki/lacowiki/pages/Legends.tsx new file mode 100644 index 00000000..e108e290 --- /dev/null +++ b/modules/lacowiki/lacowiki/pages/Legends.tsx @@ -0,0 +1,23 @@ +import { Stack } from '@simple-module-py/ui/lacowiki/primitives'; +import '@simple-module-py/ui/lacowiki/styles.css'; +import { WfLegendBuilder, WfLegendsList } from '@simple-module-py/ui/lacowiki/wf/legends'; + +function LacoWikiLegends() { + return ( +
+
+
Legends
+

Legends — list, builder.

+

Templates and custom legends; class editor with color and code.

+ + + + +
+
+ ); +} + +LacoWikiLegends.layout = (page: React.ReactNode) => page; + +export default LacoWikiLegends; diff --git a/modules/lacowiki/lacowiki/pages/Reports.tsx b/modules/lacowiki/lacowiki/pages/Reports.tsx new file mode 100644 index 00000000..e22ef5b5 --- /dev/null +++ b/modules/lacowiki/lacowiki/pages/Reports.tsx @@ -0,0 +1,24 @@ +import { Stack } from '@simple-module-py/ui/lacowiki/primitives'; +import '@simple-module-py/ui/lacowiki/styles.css'; +import { WfReportMatrix } from '@simple-module-py/ui/lacowiki/wf/report-matrix'; +import { WfReportsList } from '@simple-module-py/ui/lacowiki/wf/reports'; + +function LacoWikiReports() { + return ( +
+
+
Reports
+

Reports — list, confusion matrix.

+

Accuracy reports per dataset; confusion matrix with UA/PA per class.

+ + + + +
+
+ ); +} + +LacoWikiReports.layout = (page: React.ReactNode) => page; + +export default LacoWikiReports; diff --git a/modules/lacowiki/lacowiki/pages/Sampling.tsx b/modules/lacowiki/lacowiki/pages/Sampling.tsx new file mode 100644 index 00000000..56547fab --- /dev/null +++ b/modules/lacowiki/lacowiki/pages/Sampling.tsx @@ -0,0 +1,29 @@ +import { Stack } from '@simple-module-py/ui/lacowiki/primitives'; +import '@simple-module-py/ui/lacowiki/styles.css'; +import { WfSamplingDetail, WfSamplingList } from '@simple-module-py/ui/lacowiki/wf/sampling'; +import { WfSamplingGenerator } from '@simple-module-py/ui/lacowiki/wf/sampling-generator'; +import { WfSamplingStrategies } from '@simple-module-py/ui/lacowiki/wf/sampling-strategies'; + +function LacoWikiSampling() { + return ( +
+
+
Sampling
+

Sampling — designs, generator, detail.

+

+ Seven strategies; per-strategy parameter cards swap the generator's right panel. +

+ + + + + + +
+
+ ); +} + +LacoWikiSampling.layout = (page: React.ReactNode) => page; + +export default LacoWikiSampling; diff --git a/modules/lacowiki/lacowiki/pages/Validation.tsx b/modules/lacowiki/lacowiki/pages/Validation.tsx new file mode 100644 index 00000000..88df877e --- /dev/null +++ b/modules/lacowiki/lacowiki/pages/Validation.tsx @@ -0,0 +1,26 @@ +import { Stack } from '@simple-module-py/ui/lacowiki/primitives'; +import '@simple-module-py/ui/lacowiki/styles.css'; +import { WfValidationB } from '@simple-module-py/ui/lacowiki/wf/validation'; +import { WfValidationCreate } from '@simple-module-py/ui/lacowiki/wf/validation-create'; + +function LacoWikiValidation() { + return ( +
+
+
Validation
+

Validation — campaign + classify.

+

+ Single-sample focus: buffer, crosshair guides, classify panel beside the map. +

+ + + + +
+
+ ); +} + +LacoWikiValidation.layout = (page: React.ReactNode) => page; + +export default LacoWikiValidation; diff --git a/modules/lacowiki/lacowiki/py.typed b/modules/lacowiki/lacowiki/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/modules/lacowiki/package.json b/modules/lacowiki/package.json new file mode 100644 index 00000000..a2e87b7b --- /dev/null +++ b/modules/lacowiki/package.json @@ -0,0 +1,15 @@ +{ + "name": "@simple-module-py/lacowiki", + "version": "0.0.8", + "private": true, + "description": "Frontend pages for the LacoWiki module", + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@inertiajs/react": "^2.0.0", + "@simple-module-py/ui": "*" + }, + "devDependencies": { + "@simple-module-py/tsconfig": "*" + } +} diff --git a/modules/lacowiki/pyproject.toml b/modules/lacowiki/pyproject.toml new file mode 100644 index 00000000..6c825daf --- /dev/null +++ b/modules/lacowiki/pyproject.toml @@ -0,0 +1,46 @@ +[project] +name = "simple_module_lacowiki" +version = "0.0.8" +description = "LacoWiki migration: datasets, legends, sampling, validation, reports" +readme = "README.md" +license = "MIT" +requires-python = ">=3.12" +authors = [{ name = "Anto Subash", email = "antosubash@live.com" }] +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: FastAPI", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Internet :: WWW/HTTP", + "Typing :: Typed", +] +dependencies = [ + "simple_module_core==0.0.8", + "simple_module_hosting==0.0.8", + "simple_module_users==0.0.8", +] + +[project.entry-points.simple_module] +lacowiki = "lacowiki.module:LacoWikiModule" + +[project.urls] +Homepage = "https://github.com/antosubash/simple_module_python" +Repository = "https://github.com/antosubash/simple_module_python" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["lacowiki"] + +[tool.hatch.build.targets.wheel.force-include] +"package.json" = "lacowiki/package.json" + +[tool.uv.sources] +simple_module_core = { workspace = true } +simple_module_hosting = { workspace = true } +simple_module_users = { workspace = true } diff --git a/modules/lacowiki/tests/test_lacowiki.py b/modules/lacowiki/tests/test_lacowiki.py new file mode 100644 index 00000000..96d6430b --- /dev/null +++ b/modules/lacowiki/tests/test_lacowiki.py @@ -0,0 +1,23 @@ +"""Smoke test: LacoWiki module pages render via Inertia.""" + +from __future__ import annotations + +import pytest + + +@pytest.mark.parametrize( + "path", + [ + "/lacowiki/", + "/lacowiki/datasets", + "/lacowiki/legends", + "/lacowiki/sampling", + "/lacowiki/validation", + "/lacowiki/reports", + "/lacowiki/account", + ], +) +async def test_lacowiki_pages_render(authenticated_client, path: str) -> None: + """Each page returns 200 (HTML or Inertia JSON, depending on Accept).""" + response = await authenticated_client.get(path) + assert response.status_code == 200 diff --git a/modules/lacowiki/tsconfig.json b/modules/lacowiki/tsconfig.json new file mode 100644 index 00000000..de3ef3e7 --- /dev/null +++ b/modules/lacowiki/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@simple-module-py/tsconfig/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./lacowiki/*"], + "@simple-module-py/ui/*": ["../../packages/ui/src/*"] + } + }, + "include": ["lacowiki/**/*.ts", "lacowiki/**/*.tsx"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 34756133..3a353304 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -50,7 +50,16 @@ "types": "./src/types.ts", "default": "./src/types.ts" }, - "./styles/*": "./src/styles/*" + "./styles/*": "./src/styles/*", + "./lacowiki/*": { + "types": "./src/lacowiki/*.tsx", + "default": "./src/lacowiki/*.tsx" + }, + "./lacowiki/wf/*": { + "types": "./src/lacowiki/wf/*.tsx", + "default": "./src/lacowiki/wf/*.tsx" + }, + "./lacowiki/styles.css": "./src/lacowiki/styles.css" }, "files": [ "src", diff --git a/host/client_app/lib/lacowiki/chrome.tsx b/packages/ui/src/lacowiki/chrome.tsx similarity index 100% rename from host/client_app/lib/lacowiki/chrome.tsx rename to packages/ui/src/lacowiki/chrome.tsx diff --git a/host/client_app/lib/lacowiki/design-system-blocks.tsx b/packages/ui/src/lacowiki/design-system-blocks.tsx similarity index 100% rename from host/client_app/lib/lacowiki/design-system-blocks.tsx rename to packages/ui/src/lacowiki/design-system-blocks.tsx diff --git a/host/client_app/lib/lacowiki/design-system.tsx b/packages/ui/src/lacowiki/design-system.tsx similarity index 100% rename from host/client_app/lib/lacowiki/design-system.tsx rename to packages/ui/src/lacowiki/design-system.tsx diff --git a/host/client_app/lib/lacowiki/overview.tsx b/packages/ui/src/lacowiki/overview.tsx similarity index 100% rename from host/client_app/lib/lacowiki/overview.tsx rename to packages/ui/src/lacowiki/overview.tsx diff --git a/host/client_app/lib/lacowiki/primitives.tsx b/packages/ui/src/lacowiki/primitives.tsx similarity index 100% rename from host/client_app/lib/lacowiki/primitives.tsx rename to packages/ui/src/lacowiki/primitives.tsx diff --git a/host/client_app/lib/lacowiki/spatial-preview.tsx b/packages/ui/src/lacowiki/spatial-preview.tsx similarity index 100% rename from host/client_app/lib/lacowiki/spatial-preview.tsx rename to packages/ui/src/lacowiki/spatial-preview.tsx diff --git a/host/client_app/lib/lacowiki/styles.css b/packages/ui/src/lacowiki/styles.css similarity index 100% rename from host/client_app/lib/lacowiki/styles.css rename to packages/ui/src/lacowiki/styles.css diff --git a/host/client_app/lib/lacowiki/wf/account.tsx b/packages/ui/src/lacowiki/wf/account.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/account.tsx rename to packages/ui/src/lacowiki/wf/account.tsx diff --git a/host/client_app/lib/lacowiki/wf/dashboard.tsx b/packages/ui/src/lacowiki/wf/dashboard.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/dashboard.tsx rename to packages/ui/src/lacowiki/wf/dashboard.tsx diff --git a/host/client_app/lib/lacowiki/wf/dataset-detail.tsx b/packages/ui/src/lacowiki/wf/dataset-detail.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/dataset-detail.tsx rename to packages/ui/src/lacowiki/wf/dataset-detail.tsx diff --git a/host/client_app/lib/lacowiki/wf/datasets.tsx b/packages/ui/src/lacowiki/wf/datasets.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/datasets.tsx rename to packages/ui/src/lacowiki/wf/datasets.tsx diff --git a/host/client_app/lib/lacowiki/wf/legends.tsx b/packages/ui/src/lacowiki/wf/legends.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/legends.tsx rename to packages/ui/src/lacowiki/wf/legends.tsx diff --git a/host/client_app/lib/lacowiki/wf/marketing.tsx b/packages/ui/src/lacowiki/wf/marketing.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/marketing.tsx rename to packages/ui/src/lacowiki/wf/marketing.tsx diff --git a/host/client_app/lib/lacowiki/wf/report-matrix.tsx b/packages/ui/src/lacowiki/wf/report-matrix.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/report-matrix.tsx rename to packages/ui/src/lacowiki/wf/report-matrix.tsx diff --git a/host/client_app/lib/lacowiki/wf/reports.tsx b/packages/ui/src/lacowiki/wf/reports.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/reports.tsx rename to packages/ui/src/lacowiki/wf/reports.tsx diff --git a/host/client_app/lib/lacowiki/wf/sampling-generator.tsx b/packages/ui/src/lacowiki/wf/sampling-generator.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/sampling-generator.tsx rename to packages/ui/src/lacowiki/wf/sampling-generator.tsx diff --git a/host/client_app/lib/lacowiki/wf/sampling-strategies.tsx b/packages/ui/src/lacowiki/wf/sampling-strategies.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/sampling-strategies.tsx rename to packages/ui/src/lacowiki/wf/sampling-strategies.tsx diff --git a/host/client_app/lib/lacowiki/wf/sampling.tsx b/packages/ui/src/lacowiki/wf/sampling.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/sampling.tsx rename to packages/ui/src/lacowiki/wf/sampling.tsx diff --git a/host/client_app/lib/lacowiki/wf/validation-create-cards.tsx b/packages/ui/src/lacowiki/wf/validation-create-cards.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/validation-create-cards.tsx rename to packages/ui/src/lacowiki/wf/validation-create-cards.tsx diff --git a/host/client_app/lib/lacowiki/wf/validation-create.tsx b/packages/ui/src/lacowiki/wf/validation-create.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/validation-create.tsx rename to packages/ui/src/lacowiki/wf/validation-create.tsx diff --git a/host/client_app/lib/lacowiki/wf/validation-shared.tsx b/packages/ui/src/lacowiki/wf/validation-shared.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/validation-shared.tsx rename to packages/ui/src/lacowiki/wf/validation-shared.tsx diff --git a/host/client_app/lib/lacowiki/wf/validation.tsx b/packages/ui/src/lacowiki/wf/validation.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wf/validation.tsx rename to packages/ui/src/lacowiki/wf/validation.tsx diff --git a/host/client_app/lib/lacowiki/wireframes.tsx b/packages/ui/src/lacowiki/wireframes.tsx similarity index 100% rename from host/client_app/lib/lacowiki/wireframes.tsx rename to packages/ui/src/lacowiki/wireframes.tsx diff --git a/pyproject.toml b/pyproject.toml index d84326d5..a0b379de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ extra-paths = [ "modules/file_storage", "modules/settings", "modules/feature_flags", + "modules/lacowiki", "host", "scripts", ] @@ -103,7 +104,7 @@ invalid-argument-type = "ignore" [tool.pytest.ini_options] asyncio_mode = "auto" -testpaths = ["framework/cli/tests", "framework/core/tests", "framework/db/tests", "framework/hosting/tests", "framework/testing/tests", "host/tests", "modules/auth/tests", "modules/dashboard/tests", "modules/users/tests", "modules/permissions/tests", "modules/background_tasks/tests", "modules/file_storage/tests", "modules/settings/tests", "modules/feature_flags/tests", "scripts/tests", "tests/integration", "tests/e2e", "tests/benchmarks"] +testpaths = ["framework/cli/tests", "framework/core/tests", "framework/db/tests", "framework/hosting/tests", "framework/testing/tests", "host/tests", "modules/auth/tests", "modules/dashboard/tests", "modules/users/tests", "modules/permissions/tests", "modules/background_tasks/tests", "modules/file_storage/tests", "modules/settings/tests", "modules/feature_flags/tests", "modules/lacowiki/tests", "scripts/tests", "tests/integration", "tests/e2e", "tests/benchmarks"] markers = [ "e2e: end-to-end tests requiring a live browser", "perf: performance benchmarks (opt-in; run via `make bench`)", From 0e5a73964d5ccdae7b475c04080eb723435ed34d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 21:20:52 +0000 Subject: [PATCH 5/6] chore(lacowiki): update package-lock.json for new workspace The lacowiki module is registered as an npm workspace but the lockfile hadn't been refreshed yet, so 'npm ci' in CI was failing. Run 'npm install' to register it. https://claude.ai/code/session_01AnJvhmDHXftJPBCL29eyhC --- package-lock.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/package-lock.json b/package-lock.json index 523c474c..acfdc06a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -188,6 +188,19 @@ "react-dom": "^19.0.0" } }, + "modules/lacowiki": { + "name": "@simple-module-py/lacowiki", + "version": "0.0.8", + "devDependencies": { + "@simple-module-py/tsconfig": "*" + }, + "peerDependencies": { + "@inertiajs/react": "^2.0.0", + "@simple-module-py/ui": "*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "modules/permissions": { "name": "@simple-module-py/permissions", "version": "0.1.0", @@ -4525,6 +4538,10 @@ "resolved": "packages/i18n", "link": true }, + "node_modules/@simple-module-py/lacowiki": { + "resolved": "modules/lacowiki", + "link": true + }, "node_modules/@simple-module-py/permissions": { "resolved": "modules/permissions", "link": true From 7315d504cb3a2c2f89f2b39c01e442a6445a51f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 6 May 2026 06:02:00 +0000 Subject: [PATCH 6/6] refactor(ui): apply LacoWiki wireframe design to existing pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the new lacowiki module and instead update the existing modules' pages with the wireframe design language. Per user feedback: don't duplicate existing modules, restyle the pages we already have. - packages/ui/src/styles/globals.css — switch to Inter for UI / display, pin primary scale to teal-cyan (oklch hue 200) to match the LacoWiki accent, drop gradient backgrounds, soften shadow tokens, and tighten --radius. Charts now draw from the LacoWiki land-cover palette. - host/templates/index.html — load Inter + JetBrains Mono instead of DM Sans + Outfit so the new --font tokens resolve to real fonts. - packages/ui/src/layouts/AuthCardShell.tsx — replace the minimal centered card with a two-column shell: form on the left, quiet map placeholder on the right. Login/Register/ForgotPassword/ResetPassword/ AcceptInvite/VerifyEmail all drop their nested Card wrappers and render directly inside the shell so the brand mark and layout don't get duplicated. - modules/dashboard/dashboard/pages/Home.tsx — replace gradient stat cards with restrained 1px-bordered tiles, mono uppercase labels, tabular-num values, and split system info into a health checklist column plus a flat module-chip list. - modules/users/users/pages/Profile.tsx — section nav + avatar block + two-column field grid, modeled on the WfAccount frame. - host/client_app/pages/Landing.tsx — light hero with map preview + accuracy callout and a feature row, replacing the dark gradient hero. Keeps existing i18n keys. - modules/lacowiki — removed in full (the previous "single-module" approach is gone). biome.json scope trimmed accordingly. Existing pages benefit from the global token updates without further per-page rewrites. https://claude.ai/code/session_01AnJvhmDHXftJPBCL29eyhC --- biome.json | 2 +- host/client_app/pages/Landing.tsx | 321 +++++++----------- host/pyproject.toml | 2 - host/templates/index.html | 2 +- modules/dashboard/dashboard/pages/Home.tsx | 187 ++++------ modules/lacowiki/README.md | 12 - modules/lacowiki/lacowiki/__init__.py | 0 .../lacowiki/lacowiki/endpoints/__init__.py | 0 modules/lacowiki/lacowiki/endpoints/views.py | 45 --- modules/lacowiki/lacowiki/locales/en.json | 10 - modules/lacowiki/lacowiki/module.py | 50 --- modules/lacowiki/lacowiki/pages/Account.tsx | 19 -- modules/lacowiki/lacowiki/pages/Datasets.tsx | 25 -- modules/lacowiki/lacowiki/pages/Home.tsx | 68 ---- modules/lacowiki/lacowiki/pages/Legends.tsx | 23 -- modules/lacowiki/lacowiki/pages/Reports.tsx | 24 -- modules/lacowiki/lacowiki/pages/Sampling.tsx | 29 -- .../lacowiki/lacowiki/pages/Validation.tsx | 26 -- modules/lacowiki/lacowiki/py.typed | 0 modules/lacowiki/package.json | 15 - modules/lacowiki/pyproject.toml | 46 --- modules/lacowiki/tests/test_lacowiki.py | 23 -- modules/lacowiki/tsconfig.json | 11 - modules/users/users/pages/AcceptInvite.tsx | 79 ++--- modules/users/users/pages/ForgotPassword.tsx | 90 +++-- modules/users/users/pages/Login.tsx | 199 +++++------ modules/users/users/pages/Profile.tsx | 118 +++++-- modules/users/users/pages/Register.tsx | 167 +++++---- modules/users/users/pages/ResetPassword.tsx | 90 +++-- modules/users/users/pages/VerifyEmail.tsx | 17 +- package-lock.json | 5 +- packages/ui/src/layouts/AuthCardShell.tsx | 34 +- packages/ui/src/styles/globals.css | 103 +++--- pyproject.toml | 3 +- 34 files changed, 669 insertions(+), 1176 deletions(-) delete mode 100644 modules/lacowiki/README.md delete mode 100644 modules/lacowiki/lacowiki/__init__.py delete mode 100644 modules/lacowiki/lacowiki/endpoints/__init__.py delete mode 100644 modules/lacowiki/lacowiki/endpoints/views.py delete mode 100644 modules/lacowiki/lacowiki/locales/en.json delete mode 100644 modules/lacowiki/lacowiki/module.py delete mode 100644 modules/lacowiki/lacowiki/pages/Account.tsx delete mode 100644 modules/lacowiki/lacowiki/pages/Datasets.tsx delete mode 100644 modules/lacowiki/lacowiki/pages/Home.tsx delete mode 100644 modules/lacowiki/lacowiki/pages/Legends.tsx delete mode 100644 modules/lacowiki/lacowiki/pages/Reports.tsx delete mode 100644 modules/lacowiki/lacowiki/pages/Sampling.tsx delete mode 100644 modules/lacowiki/lacowiki/pages/Validation.tsx delete mode 100644 modules/lacowiki/lacowiki/py.typed delete mode 100644 modules/lacowiki/package.json delete mode 100644 modules/lacowiki/pyproject.toml delete mode 100644 modules/lacowiki/tests/test_lacowiki.py delete mode 100644 modules/lacowiki/tsconfig.json diff --git a/biome.json b/biome.json index 07eec2af..4f8e6c85 100644 --- a/biome.json +++ b/biome.json @@ -44,7 +44,7 @@ } }, { - "includes": ["packages/ui/src/lacowiki/**", "modules/lacowiki/lacowiki/pages/**"], + "includes": ["packages/ui/src/lacowiki/**"], "linter": { "rules": { "a11y": { diff --git a/host/client_app/pages/Landing.tsx b/host/client_app/pages/Landing.tsx index bbbfcd6f..c9091d8d 100644 --- a/host/client_app/pages/Landing.tsx +++ b/host/client_app/pages/Landing.tsx @@ -1,213 +1,156 @@ -import { usePage } from '@inertiajs/react'; +import { Link, usePage } from '@inertiajs/react'; import { keys, useT } from '@simple-module-py/i18n'; -import { Badge } from '@simple-module-py/ui/components/ui/badge'; -import { Button } from '@simple-module-py/ui/components/ui/button'; -import { Card, CardContent } from '@simple-module-py/ui/components/ui/card'; -import { Separator } from '@simple-module-py/ui/components/ui/separator'; -import { PublicLayout } from '@simple-module-py/ui/layouts/PublicLayout'; interface Props { isAuthenticated: boolean; } +const STATS = ['14k+ datasets', '·', '2.4M validated points', '·', 'open source']; +const FEATURE_GRID: [string, string][] = [ + ['Datasets', 'Upload COG/PMTiles'], + ['Sampling', '7 strategies'], + ['Validation', 'Map-side classify'], + ['Reports', 'Confusion matrix'], +]; + +const SAMPLE_DOTS: [number, number][] = [ + [30, 40], + [60, 55], + [75, 30], + [45, 70], + [20, 55], +]; + +const MAP_BG = `radial-gradient(ellipse at 30% 40%, oklch(0.92 0.03 155) 0%, transparent 35%), + radial-gradient(ellipse at 70% 60%, oklch(0.88 0.04 200) 0%, transparent 40%), + repeating-linear-gradient(45deg, transparent 0 14px, rgba(0,0,0,0.025) 14px 15px), + repeating-linear-gradient(-45deg, transparent 0 14px, rgba(0,0,0,0.025) 14px 15px), + oklch(0.93 0.005 250)`; + function Landing() { const { isAuthenticated } = usePage<{ props: Props }>().props as unknown as Props; const { t } = useT(); - const features = [ - { - icon: ( - - ), - title: t(keys.host.landing.features.module_system_title), - description: t(keys.host.landing.features.module_system_description), - }, - { - icon: ( - - ), - title: t(keys.host.landing.features.auth_title), - description: t(keys.host.landing.features.auth_description), - }, - { - icon: ( - - ), - title: t(keys.host.landing.features.schema_title), - description: t(keys.host.landing.features.schema_description), - }, - { - icon: ( - - ), - title: t(keys.host.landing.features.inertia_title), - description: t(keys.host.landing.features.inertia_description), - }, - { - icon: ( - - ), - title: t(keys.host.landing.features.diagnostics_title), - description: t(keys.host.landing.features.diagnostics_description), - }, - { - icon: ( - - ), - title: t(keys.host.landing.features.devtools_title), - description: t(keys.host.landing.features.devtools_description), - }, - ]; + const ctaHref = isAuthenticated ? '/dashboard/' : '/auth/login'; + const ctaLabel = isAuthenticated + ? t(keys.host.landing.cta_dashboard) + : t(keys.host.landing.cta_get_started); return ( - <> - {/* Hero */} -
- - - {t(keys.host.landing.badge)} - - -

- {t(keys.host.landing.hero_title_line1)} -
- - {t(keys.host.landing.hero_title_line2)} - -

- -

- {t(keys.host.landing.hero_subtitle)} -

- -
- + - + {/* Hero */} +
+
+
+ + {t(keys.host.landing.badge)} + +

+ {t(keys.host.landing.hero_title_line1)} +
+ {t(keys.host.landing.hero_title_line2)} +

+

+ {t(keys.host.landing.hero_subtitle)} +

+ +
+ {STATS.map((s) => ( + {s} + ))} +
+
- {/* Features */} -
-
- {features.map((feature) => ( - +
- -
- {feature.icon} -
-

- {feature.title} -

-

- {feature.description} -

-
- + {SAMPLE_DOTS.map(([x, y]) => ( + + ))} +
+
+
+ Overall accuracy +
+
87.3%
+
±2.1% at 95% CI
+
+
+
+
+ + {/* Feature row */} +
+
+ {FEATURE_GRID.map(([title, sub]) => ( +
+
{title}
+
{sub}
+
))}
- + +
+ SimpleModule — built with FastAPI, Inertia.js, React, Tailwind +
+
); } -Landing.layout = (page: React.ReactNode) => {page}; +Landing.layout = (page: React.ReactNode) => page; export default Landing; diff --git a/host/pyproject.toml b/host/pyproject.toml index b66c1fda..35bce23f 100644 --- a/host/pyproject.toml +++ b/host/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "simple_module_file_storage", "simple_module_settings", "simple_module_feature_flags", - "simple_module_lacowiki", "python-multipart>=0.0.6", ] @@ -25,4 +24,3 @@ simple_module_background_tasks = { workspace = true } simple_module_file_storage = { workspace = true } simple_module_settings = { workspace = true } simple_module_feature_flags = { workspace = true } -simple_module_lacowiki = { workspace = true } diff --git a/host/templates/index.html b/host/templates/index.html index ea8da7ea..41acee4b 100644 --- a/host/templates/index.html +++ b/host/templates/index.html @@ -6,7 +6,7 @@ SimpleModule - + {% inertia_head %} {% if request.app.state.sm.inertia_config.environment == "development" %}