Katalog Kind — Tier Component
Version: 0.1.0 · Status: Draft
Draft: isi di bawah kontrak yang berlaku. Setiap kind di sini adalah instance VisualSpecKind
tier: component(02-visual-spec-kind.md).
1. Base Component Library
Shell resmi wajib menyediakan pustaka component dasar yang closed, themeable. Registry widget dasar adalah himpunan tertutup yang dienumerasi eksplisit — bukan daftar terbuka yang boleh tumbuh informal.
Dua himpunan terpisah, sesuai permukaan tempat widget itu dipakai di manifest — bukan satu daftar gabungan, sebab widget form pada kolom tabel diabaikan renderer sel dan nilainya tercetak mentah:
Form field — FormField.widget (juga field langkah Wizard):
input, textarea, richtext, number, decimalinput, select, switch, radio-group, combobox, password, slider, tags, select-multi-tag, uuid, json, fileinput (§1.1), relation-picker, datepicker, datetimeinput, child-grid, grants-editor, qrcode, moneyinput, timeinput.
Table/Listing cell — TableColumn.widget:
badge, boolean, image, qrcode.
image merender nilai file/attachment sebagai gambar inline; src-nya adalah route unduh entity (lihat §1.1), jadi kolom tabel tidak butuh widget preview tersendiri. Nilai bukan-gambar (PDF, dokumen) jatuh ke tautan unduh — perilaku yang sama seperti sebelum widget ini ada. Renderer juga menurunkan image secara otomatis untuk field file yang storage.allowed_types-nya memuat gambar, sehingga spec tidak wajib menulisnya.
Halaman detail memperlakukan hal yang sama: nilai gambar dirender sebagai <img>, dan hanya file non-gambar yang tampil sebagai tautan berikon. Mengklik gambar membuka dialog pratinjau di dalam App — bukan tab peramban baru — sehingga permukaan, navigasi, dan record yang sedang dibuka tidak hilang. Tautan unduh berkas non-gambar tetap membuka tab: di situlah tempat sebuah unduhan. Salinan besar dibatasi viewport (max-h-[80vh], rasio asli dipertahankan) dan ditutup dengan Escape/klik luar/tombol tutup.
qrcode (gap #3/S4) merender nilai field sebagai QR yang bisa dipindai — tersedia di kedua permukaan dengan nama yang sama, karena artinya sama: "string ini, scannable". Nilainya ADALAH payload, jadi widget ini selalu read-only: di form ia menggantikan input (tidak ada yang bisa diketik), di sel tabel/listing ia menggantikan teks. Yang perlu diperhatikan penulis spec: QR berisi string apa adanya, sehingga QR yang bisa dipindai ponsel memerlukan URL absolut (mis. https://kafe.example/app/menu/meja-a01). Menyusun URL absolut itu — termasuk menyuntikkan origin aplikasi saat cetak — adalah pekerjaan pemanggil, bukan widget. Renderer memakai SVG (bukan canvas) supaya tajam saat dicetak di struk thermal atau kartu meja.
Aturan yang mengikat seluruh himpunan di atas:
- Setiap nama terimplementasi. Himpunan ini adalah enum di JSON Schema (
$defs/FormWidget,$defs/TableCellWidget, digenerate daripkg/spec/widget.go), sehingga salah ketik (relaion-picker) gagalformspec validate— bukan lolos lalu diam-diam dirender sebagai input teks. Editor YAML juga memakai enum ini untuk autocomplete. - Satu nama per widget. Nama tipe field (
relation,date,child,boolean, …) pernah diterima router sebagai alias; itu bukan bagian katalog dan validator menolaknya dengan petunjuk nama kanonik. widgetyang dihilangkan itu sah — renderer menurunkan widget dari tipe field (mis.enum→select,relation→relation-picker). Menuliswidget:hanya perlu untuk mengganti turunan itu.- Field type yang belum punya widget khusus: tidak ada lagi —
moneydantime, dua yang terakhir, kini punyamoneyinputdantimeinput(§1.2).
1.3 select-multi-tag — tag dari pilihan, bukan dari ketikan
Saudara tags untuk field yang memegang himpunan nilai yang dideklarasikan (Field.options + multiple: true, ../backend/05-field-types.md §1.1.1; fallback enum_values bila options tidak ada). tags menerima apa pun yang diketik, jadi himpunan yang ditetapkan spec (1=Senin … 7=Minggu) tidak bisa ditegakkan lewatnya — 9 tetap tersimpan.
Yang menjadi kontrak widget ini:
- Pilihan yang sudah dipilih tidak ditawarkan lagi, sehingga nilai yang sama tidak bisa masuk dua kali.
- Urutan chip mengikuti urutan deklarasi, bukan urutan klik — himpunan terurut (
Senin..Jumat) terbaca alami. Ini urutan tampilan saja: array tersimpan mempertahankan urutan pengisian, jadi membuka lalu menyimpan ulang form tidak menulis ulang isi record (reorder senyap akan muncul sebagai perubahan di tiap diff/audit). - Nilai di luar deklarasi tetap ditampilkan, ditandai sebagai tidak dikenal — bukan dibuang senyap saat save. Data lama, atau spec yang himpunannya menyusut, tetap terlihat dan bisa dihapus pengguna.
- Nilai non-daftar (mis. object pada field
json) memunculkan error yang terlihat, bukan diganti[]yang menyembunyikan data. - Bentuk nilai dipertahankan:
jsonmasuk array → keluar array;stringmasuk daftar dipisah koma → keluar string. Tipe skalar mengikuti deklarasi, jadi himpunan angka tetap tersimpan sebagai angka.
Halaman detail dan sel tabel/listing merender himpunan yang sama sebagai chip berlabel ("Senin, Selasa") — bukan [1,2] mentah — dari satu resolver bersama, supaya kedua permukaan tidak bisa berbeda.
1.4 Cardinality: Form mengikuti Entity
Single vs multi bukan keputusan Form. Cardinality dideklarasikan pada Entity (Field.multiple), dan renderer menurunkan widgetnya — sehingga field yang sama tidak bisa jadi tag picker di satu form dan picker satu-nilai di form lain.
| Deklarasi Entity | Widget yang diturunkan |
|---|---|
options + multiple: true | select-multi-tag |
options + multiple: false (atau skalar) | select |
enum_values (tanpa options) | select |
tanpa options | seperti tipe field-nya |
Aturan yang mengikat:
- Form yang ditulis tidak perlu menulis
widget:untuk field ber-options; widget diturunkan dari deklarasi Entity — jalur derivasi dan jalur manifest memakai fungsi yang sama, jadi keduanya tidak bisa berbeda. - Caption ikut deklarasi. Picker pilihan tunggal (
select,radio-group,combobox) menampilkanoptions[].labelbila ada, bukan nilai mentah — jadi1tampil "Senin" danqristampil "QRIS". Nilai yang dikirim tetap nilai deklarasi (bukan caption), dengan tipe skalarnya. - Widget yang bertentangan ditolak di deploy, bukan di browser:
formspec checkmenolakselect-multi-tagpada field bernilai tunggal danselect/radio-group/comboboxpada himpunan. - Permukaan baca ikut aturan yang sama: sel tabel, halaman detail, dan filter
selectmerender label dari deklarasi (nilai tunggal tampil sebagai badge ber-caption di tabel).
1.2 moneyinput / timeinput
Dua widget yang menutup kelas gap "field-nya ada, tapi yang bisa dilakukan hanyalah mengetik teks bebas":
moneyinputuntuktype: money(05-field-types.md §2). Yang dijaga: mata uang ikut terbawa — nilai yang dikirim adalah bentuk kanonik{amount, currency}, bukan angka telanjang; jumlahnya disimpan sebagai teks selama mengetik, sehingga tidak dibulatkan diam-diam sebelum pengguna selesai (uang eksak, tidak pernah float — §2.1); dan tampilannya mengikutisettings.currency/settings.locale(tidak menebak simbol atau skala).inputMode: decimalmemberi numpad di perangkat sentuh, dengan pratinjau terformat di bawahnya. Skala/mata uang dapat dioverride per field (currency,decimal_placespada field) sebagaimana §2.- Parity katalog ↔ schema ↔ implementasi dijaga test (
renderers/react-shadcn/src/widgets/catalog.test.tsx).
Di luar widget input, pustaka dasar juga menyediakan tabs, badge, card, empty-state, breadcrumb, skeleton/loading, dan pagination. Himpunan dasar ini tidak tumbuh secara informal: widget baru ditambahkan dengan mendaftarkan VisualSpecKind baru ber-tier: component (02-visual-spec-kind.md §2, §6) — bukan dengan memperluas daftar di atas secara ad-hoc. Component custom (../frontend/07-component-kinds.md §4 di bawah) boleh menyusun ulang lewat formspec.components — bukan menulis ulang widget dasar dari nol. Restyle tampilan pustaka ini adalah urusan kind: Theme (05-app-kinds.md §6) — Theme tidak pernah mengubah semantik layout atau melewati visibilitas berbasis permission (§ Spec Resolution API — 04-spec-resolution-api.md §4).
1.1 fileinput — Upload / Attachment
Widget untuk field bertipe file/attachment (single atau multi — ditentukan field spec Entity, ../backend/01-core-basic.md §1). Upload mengalir ke primitive storage (ctx.storage, dilayani Datastore ber-serves: [storage] — garage/s3/minio/fs, ../platform/06-datastore.md §2); file tenant-isolated seperti semua data.
- Preview per tipe umum: gambar inline (thumbnail), PDF viewer embed, lainnya jadi tombol download.
- Batas ukuran & tipe yang diizinkan dibaca dari field rules — ditegakkan client untuk UX, server tetap otoritas (
../backend/01-core-basic.md§3). - Tray upload/download disediakan renderer (
formspec.files, §4) —fileinputadalah widget dasar, bukan kind tersendiri.
1.2 richtext — Rich Text
Widget untuk field bertipe richtext. Disimpan sebagai HTML tersanitasi: sanitisasi server-side bersifat normatif — backend melucuti script/markup berbahaya saat tulis, terlepas dari klien mana yang mengirim (payload dari klien tak jujur tetap tersanitasi server, ../backend/01-core-basic.md §3).
- Toolbar dasar: bold/italic, list (ordered/unordered), link, heading. Bukan page builder — tanpa layout multi-kolom, embed, atau blok kompleks.
- HTML yang dirender ke pembaca sudah tersanitasi server; klien tak pernah mempercayai HTML mentah.
2. widget — Component Pengisi Slot
Component yang mengisi slot widget milik Page tier (mis. Dashboard — 06-page-kinds.md § Dashboard), dideklarasikan sebagai VisualSpecKind tier: component dengan implements_slot: widget (02-visual-spec-kind.md §4):
apiVersion: formspec.dev/v1
kind: Widget
metadata:
name: gl-cashflow-chart
module: gl
spec:
size: { w: 2, h: 1 }
chart:
{ type: line, entity: gl-cashflow-summary, x: date, y: net, range: 30d }Widget bisa dikontribusikan module manapun (bukan cuma module pemilik Dashboard yang memasangnya) — konsisten dengan prinsip "write once, siapa saja bisa menyediakan implementasi". Visibilitas di katalog widget diturunkan otomatis: user melihat sebuah widget di katalog hanya kalau ia punya permission list/view atas entity/action yang mendasarinya — sama seperti aturan visibilitas Spec Resolution API (04-spec-resolution-api.md §4), bukan flag visibilitas terpisah yang ditulis manual.
Widget bawaan (stat, chart) membaca summary entity atau list action saja — agregasi custom jadi summary entity yang diisi event durable (../backend/02-core-extended.md §6), bukan query ad-hoc dari widget.
3. Slot Filling di Instance
Instance Page mereferensikan Widget ke posisi slot lewat layout/widgets milik Page tersebut (lihat 06-page-kinds.md § Dashboard untuk kontrak lengkap Dashboard sebagai penerima slot, dan 02-visual-spec-kind.md §4 untuk kontrak slot system-nya):
kind: Dashboard
spec:
widgets:
- stat:
{ title: "Today's Revenue", entity: sales-daily-summary, field: total }
- chart:
{
type: line,
entity: sales-daily-summary,
x: date,
y: total,
range: 30d,
}
- component: { asset: billing/assets/heatmap.js } # §4 — full-custom widgetDashboard customizable: kalau spec.customizable: true, layout user (tambah/hapus/urutkan dari katalog widget) tersimpan sebagai runtime preference di formspec.core — manifest mendefinisikan apa yang mungkin; preference mencatat apa yang dipilih. Tidak pernah ditulis balik ke YAML.
4. asset — Escape Hatch Component
Untuk ~20% UI yang tidak berpola. Component adalah ES module di assets/, kontrak mount framework-agnostic:
// modules/billing/assets/payment-timeline.js
export default {
mount(el, props, formspec) {
/* render ke el */
},
unmount(el) {},
}formspec adalah client yang di-inject: formspec.api (generated, typed — berjalan sebagai user yang login, seluruh keamanan tetap server-side), formspec.subscribe(entity, cb) (realtime, 04-spec-resolution-api.md §5), formspec.navigate(page, params), formspec.theme (token), formspec.ui (toast, dialog, confirm, drawer), dan formspec.files (upload/download tray — infrastruktur renderer, bukan kind tersendiri).
needs:— uses-nya frontend. Component itu opaque bagi derivasi footprint, jadi component yang memanggil formspec.api wajib mendeklarasikan apa yang ia sentuh di tempat ia dipasang:
- component:
asset: billing/assets/checkout-wizard.js
needs:
actions: [order.create, order.checkout, customer.find]
subscribe: [billing.order]Open —
needs:belum didukung skemaBlockRef. Deklarasineedsbelum ada dipkg/spec(fieldBlockRefsaat ini:ref/asset/id/mode/param/props) — ditracking didocs_internal/plan/todo.md. Enforcement footprintusesdi sisi backend sudah berjalan; deklarasineedsdi manifest adalah target kontrak berikutnya.
Panggilan formspec.api di luar needs gagal client-side (dan memang tidak pernah diotorisasi server-side juga). formspec validate memperingatkan deklarasi yang tidak dipakai.
Batas sandbox CSP (normatif, bukan anjuran). Component custom yang dimuat lewat escape hatch asset dikungkung Content-Security-Policy: connect-src dibatasi hanya ke origin App itu sendiri. Konsekuensi yang mengikat: component dilarang membaca state global window/document di luar container-nya sendiri, dan dilarang melakukan fetch/connect ke endpoint apa pun selain lewat client formspec.api yang di-inject. Ini batas keamanan atas escape hatch, bukan pedoman opsional — jalur data satu-satunya keluar dari component adalah formspec.*.
CSS bundle scoped ke container. Bundle CSS sebuah component custom di-inject scoped ke container-nya sendiri (mis. CSS Modules, Shadow DOM, atau mekanisme scoping setara) — CSS component tidak pernah boleh bocor ke chrome Page/App di sekitarnya atau ke component lain. Ini konsisten dengan prinsip styling terpusat di kind: Theme (05-app-kinds.md §6).
Headless Form Engine. formspec.form(entity, { mode, id? }) mengembalikan instance form headless: field state, dirty tracking, validasi client dari field rules, evaluasi FormSpecExpr (08-formspec-expr.md), dan submit() yang sudah terhubung ke action yang tepat (create/update, dengan CAS version). Tanpa layout, tanpa widget — developer menguasai 100% markup. Tangga kontrol penuh: Form terkelola → custom widget → component → full-custom Page → headless → raw formspec.api.
Unmanaged client (Flutter, native, SPA lain) adalah konsumen API kelas satu hari ini: HTTP, realtime WebSocket, permission server-enforced, typed client tergenerate (target codegen resmi: TypeScript dan Dart). Tidak ada satupun di dokumen ini yang wajib dipenuhi client semacam itu.
5. Menambah Component Kind Baru
Lewat VisualSpecKind tier: component (02-visual-spec-kind.md); formspec apply menolak implements_slot dari tier selain component. Distribusi lewat marketplace (../platform/07-marketplace.md), sama seperti menambah VisualSpecKind tier lain.