Arsitektur shadcn-shell
Updated: 2026-07-16 · Status: Draft
Draft: isi di bawah kondisi kode
renderers/react-shadcn/srchari ini, bukan rencana desain. §5 mencatat kesenjangan terhadap kontrakdocs/spec/frontend/— bagian itu boleh berubah tanpa mengubah kontrak.
1. Interpreter Runtime
SPA React yang di-deploy sekali dan me-render App apa pun dari spec saat runtime — bukan build artifact per-app, konsisten dengan ../../spec/frontend/01-visual-hierarchy.md. Dua surface dilayani satu bundle yang sama: /{workspace}/_admin/* (100% derived dari Entity, tanpa manifest UI) dan /{workspace}/app/* (manifest App/Page/dst mengoverride derivasi).
2. Struktur Kode
renderers/react-shadcn/src/
├── App.tsx / main.tsx # bootstrap, routing dua surface
├── lib/api/{client,meta}.ts # ky client, envelope, fetcher _meta/*
├── lib/formspec-expr/ # lexer, parser, eval — interpreter FormSpecExpr
├── types/manifest.ts # mirror pkg/spec/frontend.go + entity schema
├── stores/{session,meta,prefs}.ts # zustand
├── engine/{derive,permissions,lifecycle,entityRef}.ts # lihat §3, 02-derivation-engine.md
├── shell/{SideNavShell,Sidebar,router,OverlayHost,LoginScreen}.tsx
├── kinds/{page,form,table,dashboard,widget,report,wizard,kanban,timeline,print,theme}/
├── widgets/{TextInput,NumberInput,Select,Switch,Badge,RelationPicker}.tsx
├── hooks/{useMediaQuery,useTheme}.ts
└── components/{ThemeSwitcher,ErrorBoundary}.tsx, components/ui/ (primitif shadcn)Tidak ada kinds/menu/ — navigasi bukan kind tersendiri, sudah dihapus mengikuti keputusan App.spec.menu/Module.spec.menu sebagai satu-satunya sumber (lihat komentar types/manifest.ts: "No KIND_MENU — navigation isn't a standalone kind"). engine/registry.tsx (registry kind→component generik) dan renderers/react-shadcn/src/api/, renderers/react-shadcn/src/assets/ (sisa scaffold Vite) ada di repo tapi tidak dipakai — lihat §5.
3. Bootstrap & Resolusi App
Root() (App.tsx) mendaftarkan route framework (/:workspace/_admin/setup, /oauth/*, /change-password) dan /:workspace/* untuk permukaan App. WorkspaceRoute me-resolve App pemilik path, lalu SurfaceShell({app}) menjalankan boot:
SurfaceShellmenjalankan dua hal di tick yang sama, bukan berantai:useSessionStore.boot({workspace, app})(fetch/_meta/medengan sesi App tersebut — satu slot sessionStorage per(workspace, App)) danuseMetaStore.load(workspace, undefined, {appName}). Keduanya bisa paralel karena bundle hanya butuh (a) nama App — sudah diketahuiWorkspaceRoute, yang memang harus me-resolve-nya untuk merutekan request ke sini — dan (b) token, yang ditulisboot()ke store sebelumawaitpertamanya (client bundle membaca token dari store lewatgetToken, bukan dari argumen). Keduanya single-flight per(workspace, App), jadi double-mount React StrictMode di dev tidak menggandakan permintaan.load()tidak lagi memanggilfetchMetaApps()pada jalur boot: nama App dioper masuk.detectAppName()(GET .../_meta/apps, cocokkanroot_urlterpanjang terhadap path) tetap dipakairefresh()(setelah HMR spec-reload, himpunan App bisa berubah) dan layar login — yang memang belum punya sesi. Varian unscoped{admin:true}(_admin.access) sudah tidak ada (planapp-scoped-login.mdD4). Bundle diambil dengan conditional GET:ETagdisimpan per(workspace, App, grants), diputar-balikkan sebagaiIf-None-Match, dan304memakai body yang di-cache — jadi reload tidak mengunduh ulang bundle 47 KB (terukur: 380 byte).buildRoutes()(02-derivation-engine.md§4) membangun route table dari bundle: routekind: Pagedarispec.route, route CRUD turunan per entity, satu route per entry Dashboard/Widget/Wizard/Kanban/Timeline/Report/Print (/dashboard/{name}, dst).RegionShellmembungkus seluruhnya. Path tak cocok dan index jatuh keDefaultRedirect: telusuribundle.menudepth-first (mendarat di item menu authored pertama), fallback ke list derived entity non-summary pertama.- 403 (bundle App ditolak) dan error koneksi adalah layar eksplisit, bukan crash.
Resolusi multi-App-per-workspace (_meta/apps, root_url, detectAppName()) mengonsumsi kontrak App yang lebih baru dari yang didesain semula — lihat docs/spec/platform/02-workspace-app-module.md (masih Outline, menunggu Draft di S8).
4. Konsumsi Spec Resolution API
Endpoint yang dipakai (lihat kontraknya di ../../spec/frontend/04-spec-resolution-api.md §2): _meta/apps, _meta/ui (mode appName; varian admin sudah pensiun), _meta/me, _meta/entities/{module}/{name} (lazy-load, dipanggil fetchEntitySchema). Semuanya di bawah /{workspace}/_ui/_meta/. Client ky memprefiks /{workspace}/_ui untuk meta (lib/api/meta.ts createMetaClient) dan /{workspace}/_ui/entity untuk CRUD entity (lib/api/client.ts createApiClient); keduanya menyuntik Authorization: Bearer, unwrap envelope {data, meta}/{data, meta:{page,...}}, dan melempar FormaApiError typed dari envelope error. _meta/ui dan _meta/apps memakai conditional GET (ETag + 304) dengan cache in-memory per (workspace, App, grants); seluruh respons JSON permukaan ini di-gzip oleh server. CAS: version dikirim sebagai header If-Match pada mutasi — tidak ada percabangan eksplisit untuk status 409 di mana pun; error CAS conflict jatuh ke jalur error generik (toast.error), bukan alur refetch-khusus.
5. Status Implementasi Hari Ini
Bagian yang terbukti belum/tidak sesuai rencana desain awal — dicatat supaya tidak diam-diam diasumsikan bekerja:
OverlayHostsudah terhubung — dipasang di ketiga shell (SideNavShell,TopNavShell, dan otomatis ikutAuthPage) dan dibuka lewat query string?action=&form=&mode=yang dikirimTableRenderer;Form.render: modal|drawerdi manifest karena itu benar-benar mengubah presentasi. Jalur derivasi juga bekerja saat pemanggil mengirimentity=module.namesebagai gantiform(lihatOverlayHost.tsx+formspec-client.ts).engine/registry.tsxsudah dihapus. Wiring aktual memakailazy()map hardcoded dishell/router.tsx; tidak ada file registry generik lagi. (Item ini dulu menyebutnya "kode mati" — sekarang tidak ada sama sekali.)deriveMenuItems()kini TIDAK dipakai lagi. Ia dulu membangun sidebar_admindaribundle.entities; surface itu dipensiunkan (planapp-scoped-login.mdD4), jadi menu selalu berasal dariApp.spec.menuyang authored. Helper-nya tetap ada untuk pemakai yang ingin pohon module→entity mekanis, tapi shell tidak memanggilnya lagi.TableRenderertidak lagi hardcode prefiks/_admin— navigasi memakaiuseSurface().surfacePath, sehingga tabel di surfaceapptetap diapp.- Realtime sudah ada —
hooks/useRealtime.ts(subscriber union + delta) dan sudah dipakai Table, Kanban, Calendar, Dashboard, Timeline, ApprovalInbox, dan NotificationCenter;realtime: truedi manifest dibaca, bukan field mati. - Component contract
assetsudah ada —shell/AssetRenderer.tsxmemuat ES module dari pathasset, memanggilmount(el, props, formspec)/unmount(el), dan menyuntikkanformspecclient sesuai07-component-kinds.md§4 (04-theming-assets.md§2).
Cara memakai section ini: ia memuat divergensi yang masih berlaku. Tiap baris di atas adalah divergensi yang sudah tertutup dan sengaja dipertahankan sebagai jejak singkat — bila Anda menemukan barisnya tidak lagi benar, hapus baris itu (jangan tambahkan baris "dulu X sekarang Y" ke docs/, cukup rujuk changelog penutupnya). Untuk routing dan visibilitas menu, dokumen otoritatifnya 05-routing.md.