Skip to content

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 dari pkg/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.
  • widget yang dihilangkan itu sah — renderer menurunkan widget dari tipe field (mis. enum → select, relation → relation-picker). Menulis widget: hanya perlu untuk mengganti turunan itu.
  • Field type yang belum punya widget khusus: tidak ada lagi — money dan time, dua yang terakhir, kini punya moneyinput dan timeinput (§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: json masuk array → keluar array; string masuk 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 EntityWidget yang diturunkan
options + multiple: trueselect-multi-tag
options + multiple: false (atau skalar)select
enum_values (tanpa options)select
tanpa optionsseperti 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) menampilkan options[].label bila ada, bukan nilai mentah — jadi 1 tampil "Senin" dan qris tampil "QRIS". Nilai yang dikirim tetap nilai deklarasi (bukan caption), dengan tipe skalarnya.
  • Widget yang bertentangan ditolak di deploy, bukan di browser: formspec check menolak select-multi-tag pada field bernilai tunggal dan select/radio-group/combobox pada himpunan.
  • Permukaan baca ikut aturan yang sama: sel tabel, halaman detail, dan filter select merender 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":

  • moneyinput untuk type: 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 mengikuti settings.currency/settings.locale (tidak menebak simbol atau skala). inputMode: decimal memberi numpad di perangkat sentuh, dengan pratinjau terformat di bawahnya. Skala/mata uang dapat dioverride per field (currency, decimal_places pada 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) — fileinput adalah 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):

yaml
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):

yaml
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 widget

Dashboard 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:

js
// 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:

yaml
- component:
    asset: billing/assets/checkout-wizard.js
    needs:
      actions: [order.create, order.checkout, customer.find]
      subscribe: [billing.order]

Open — needs: belum didukung skema BlockRef. Deklarasi needs belum ada di pkg/spec (field BlockRef saat ini: ref/asset/id/mode/ param/props) — ditracking di docs_internal/plan/todo.md. Enforcement footprint uses di sisi backend sudah berjalan; deklarasi needs di 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.

Standar terbuka (CC0) dengan implementasi referensi.