Design system lintas produk Muhammadiyah
LabMu UI
Satu kit untuk SatuMu, SehatMu, Amanah, BukuMu, AiMu, Masa, PayMu, dan EventMu: design tokens, component CSS, icons, JavaScript behavior tanpa dependency, opt-in adapters, dan React + TypeScript components. Satu codebase, tujuh brand; brand dipilih dengan satu attribute.
| Folder | Isi | Dipakai oleh |
|---|---|---|
tokens/ | tokens.json (source), tokens.css, _tokens.scss, tokens.ts | Semua; ubah value hanya di tokens.json |
css/labmu-ui.css | Bundle token + base + app + marketing | Cara tercepat |
css/base.css app.css marketing.css | Layer terpisah | Bila hanya butuh sebagian |
icons/ · js/labmu-icons.js | Sprite 50 line icons (i-*) + 43 duotone icons (d-*) | HTML/server-side |
js/labmu-ui.js | Tabs, dialog, menu, sidebar, toast, confirm dialog, form validation, unsaved-changes guard, copy ke clipboard, page loading, spotlight tour, on-boarding tips | HTML/server-side (Laravel, Django, Go…) |
adapters/ | select2, flatpickr, Quill dengan look LabMu (opt-in) | App yang memakai library itu |
react/ | Typed React 18/19 components | Vite/Next/TanStack |
assets/brand/ | Logo LabMu, 8 produk, dan ChatHPT dari file master, semua SVG (logo, mark, negative, mono) | Header, sidebar, footer, favicon |
assets/backgrounds/ | Background per produk (<produk>-bg.svg), lewat token --brand-bg | Panel login, on-boarding welcome, closing section |
Installation
Setiap rilis punya git tag v<versi>. File per versi ada di https://ui-kit.labmu.id/v<versi>/ (folder css, js, icons, tokens, adapters, assets), tidak pernah berubah, dan di-cache satu tahun. Path tanpa versi (https://ui-kit.labmu.id/css/…) selalu ikut deploy main terakhir: jangan dipakai di production. Naik versi = ganti angka versi di URL atau di dependency.
HTML / server-side rendering
<html lang="id" data-brand="satumu">
<head>
<link rel="stylesheet" href="https://ui-kit.labmu.id/v2.2.1/css/fonts.css">
<link rel="stylesheet" href="https://ui-kit.labmu.id/v2.2.1/css/labmu-ui.css">
</head>
<body> <!-- aplikasi: 14px; <body data-surface="marketing"> untuk landing: 16px -->
<script src="https://ui-kit.labmu.id/v2.2.1/js/labmu-icons.js"></script>
<button class="btn btn-p"><svg class="icon"><use href="#i-plus"/></svg>Ajukan Layanan</button>
<script src="https://ui-kit.labmu.id/v2.2.1/js/labmu-ui.js" defer></script>
</body>
Folder kit juga boleh disalin ke server sendiri (mis. /labmu-ui/). Hanya bila sprite disajikan dari origin yang sama, labmu-icons.js boleh diganti <use href="/labmu-ui/icons/labmu-icons.svg#i-plus"/>: browser tidak memuat <use> dari domain lain.
React (Vite + TypeScript)
npm install "git+https://git.muhammadiyah.or.id/labmu-developer-team/labmu-ui.git#v2.2.1"
Lockfile menyimpan commit tag itu, jadi npm ci selalu memasang isi yang sama. Repo-nya private: di laptop, npm memakai akses git masing-masing. Folder vendor/labmu-ui dan alias Vite @labmu/ui tidak diperlukan lagi.
Di Docker build, image butuh git dan deploy token (group variable LABMU_UI_DEPLOY_USER dan LABMU_UI_DEPLOY_TOKEN) sebagai BuildKit secret, supaya token tidak tersimpan di layer. Job build-image ci-templates harus meneruskan keduanya sebagai --secret.
RUN apk add --no-cache git
RUN --mount=type=secret,id=LABMU_UI_DEPLOY_USER,env=LABMU_UI_DEPLOY_USER \
--mount=type=secret,id=LABMU_UI_DEPLOY_TOKEN,env=LABMU_UI_DEPLOY_TOKEN \
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="url.https://${LABMU_UI_DEPLOY_USER}:${LABMU_UI_DEPLOY_TOKEN}@git.muhammadiyah.or.id/.insteadOf" \
GIT_CONFIG_VALUE_0="https://git.muhammadiyah.or.id/" \
npm ci
import "@labmu/ui/css"; // token + komponen
import "@labmu/ui/css/fonts.css";
document.documentElement.dataset.brand = "amanah";
import { AppShell, PageHead, Button, ToastProvider } from "@labmu/ui";
Metronic 8 / Bootstrap 5 (SCSS)
$labmu-brand: "satumu";
@import "labmu-ui/tokens/tokens"; // menimpa $primary, $border-radius, $font-family-sans-serif, …
@import "metronic/sass/style";
Muat css/labmu-ui.css setelah CSS Metronic. Inline layout memakai .ui-inline, bukan .row.
Brand & surface
| Attribute | Value | Efek |
|---|---|---|
data-brand di <html> atau wrapper | satumu (default) · sehatmu · amanah · bukumu · aimu · masa · paymu · eventmu | Mengganti seluruh palette; bisa nested |
data-surface di <body> | marketing | Base font 16px, outline button, dotted eyebrow |
body.nav-mini | — | Collapsed sidebar: rail 76px (disimpan di localStorage) |
body.nav-open | — | Mobile nav drawer terbuka |
body[data-ui-loading] | — | Loading bar + send-once forms (labmu-ui.js) |
Switch Brand preview di topbar (dan di bawah ini) hanya mengganti data-brand di halaman docs, supaya semua example dan token terlihat dengan warna produk yang dipilih. Di app, brand di-set sekali di <html data-brand="…">.
Brand rules: logo selalu dari file (jangan diketik), header memakai lockup Produk | Powered by LabMu, dark background memakai -negative, button Title Case, heading dan status Sentence case, font Inter.
Site footer
Kiri atas: logo produk -negative, pemisah, logo Majelis mitra produk dalam putih penuh (.footer-logos .majelis). Kanan bawah cukup Powered by + logo LabMu (.endorse). Lihat Landing Example.
Brand panel
.brand-panel memakai background master produk aktif, untuk panel login, on-boarding welcome, closing section, dan promo card. Text putih, logo -negative, small content di .brand-glass. Token: --brand-bg, --brand-gradient (fallback), --brand-bg-shade.
Selamat datang
Satu akun untuk semua layanan
React: BrandPanel (as="section|div|aside")
Color tokens
Value di bawah mengikuti brand yang aktif. Semua warna di CSS app harus var(--…); ubah value hanya di tokens/tokens.json lalu npm run build.
Icons
SVG 24×24. Line icons (i-*, stroke 1.7) untuk action dan teks; duotone icons (d-*, class .icon.duo) untuk navigation dan domain icon. Size: .icon.xs/.sm/.md/.lg.
Line
Duotone
Button
Satu primary action per area. aria-busy="true" menandai button yang request-nya sedang berjalan: spinner, click diabaikan, value-nya tetap ikut terkirim (beda dengan disabled).
React: Button (variant, size, icon, loading), ButtonLink, IconButton (ref), TextLink; ButtonLink dan TextLink lewat LinkProvider
Masuk dengan M-ID
Satu-satunya button login di semua produk, warnanya sama di semua brand (token --mid, --mid-hover). Di .header-actions button mengecil otomatis, dan .mid-with disembunyikan di layar di bawah 400px.
React: MidButton (size="lg")
Halaman login
main.login menaruh satu box (.login-box, maks. 400px) di tengah layar: .login-logo, lalu card berisi judul, catatan dan Masuk dengan M-ID. .login-or memisahkan cara masuk kedua ("atau"), .login-note untuk catatan kecil di bawah card. Tidak ada component khusus: di React pakai markup yang sama dengan Card dan MidButton. Lihat login example.
<main class="login">
<div class="login-box stack">
<img class="login-logo" src="/assets/brand/satumu-logo.svg" alt="SatuMu">
<section class="card"><div class="card-body stack">
<div><h1>Masuk ke SatuMu</h1><p class="caption">Gunakan akun Muhammadiyah ID.</p></div>
<a class="mid-btn mid-btn-lg" href="/auth/mid" aria-label="Masuk dengan Muhammadiyah ID">…</a>
<div class="login-or">atau</div>
<form class="stack" method="post" data-check novalidate>…</form>
</div></section>
<p class="login-note caption">Belum punya akun? <a href="…">Daftar Muhammadiyah ID</a></p>
</div>
</main>
Pill & chip
Tone konsisten: good selesai/terverifikasi · warn menunggu/perlu perbaikan · bad ditolak/error · info sedang diperiksa · neutral draft/nonaktif · primary brand status. Chip untuk category/tag; chip berupa link membuka list yang sudah di-filter.
React: Badge, Pill, Count, Chip (href), Tags
Card & KPI
Semua aktif
6 NBM baru tahun ini
Sudah dipetakan
Kelengkapan organisasi
4 ranting binaan
React: Card, Stat, Attention, Progress
Note & banner
React: Note, Banner
Tabs & segmented
React: Tabs (mode link: href per item, aria-current="page", tanpa onChange), Segmented
Form
Form validation: form[data-check] (dengan novalidate) menampilkan error message bahasa Indonesia di bawah setiap field yang invalid, focus ke field invalid pertama (membuka tab-nya), dan menghapus message saat field diperbaiki. Custom message: data-err atau setCustomValidity pada event check milik form. Server-side error: LabmuUI.fieldError(el, "…"). Group radio/checkbox: <fieldset class="field"><legend>…</legend>, legend-nya tampil seperti label. Coba submit form ini kosong.
React: Field, Input, Select, Textarea, SearchInput (onSearch setelah berhenti mengetik delay ms, langsung saat Enter), InputGroup, PasswordInput, Check, Choice, Switch, FileDrop, ImagePicker (crop, cropSize dengan adapter Cropper.js), Filters, ActiveFilters, FormGrid (three), SaveBar, ColorField (against), contrastRatio, HEX, hooks useDebounced dan useUnsavedGuard; Input, Select, Textarea, PasswordInput meneruskan ref
Form grid, save bar & warna
.form-grid: dua kolom (.three: tiga), satu kolom di ≤900px; .field.full selebar baris; .form-foot menaruh actions di kanan bawah. form[data-ui-guard]: begitu ada isian berubah, .save-bar di dalamnya muncul dan menempel di bawah layar selama form terlihat, dan reload atau tutup tab bertanya dulu; submit dan reset membersihkannya. Form yang disimpan lewat fetch memanggil LabmuUI.guard(form, false) setelah berhasil. .color-pair: color picker browser di samping value HEX yang bisa diketik; pasangan warna dengan kontras di bawah 4,5:1 (WCAG AA teks biasa) diberi .note.warn. Coba ubah isian form ini.
Password: .ui-input-group dengan button Tampilkan/Sembunyikan (i-eye / i-eye-off); label button-nya ikut berganti.
const [form, setForm] = useState(saved);
const dirty = JSON.stringify(form) !== JSON.stringify(saved);
useUnsavedGuard(dirty); // reload dan tutup tab; pindah route di dalam app: blocker router + useConfirm
<form onSubmit={(e) => { e.preventDefault(); save.mutate(form); }}>
<FormGrid>
<Field label="Nama aplikasi"><Input value={form.nama} onChange={(e) => setForm({ ...form, nama: e.target.value })} /></Field>
<Field label="Deskripsi" className="full"><Textarea value={form.deskripsi} onChange={…} /></Field>
<ColorField label="Warna aksen" value={form.aksen} against="#FFFFFF" onChange={(aksen) => setForm({ ...form, aksen })} />
</FormGrid>
<SaveBar dirty={dirty} saving={save.isPending} onReset={() => setForm(saved)} />
</form>
<Field label="Password baru"><PasswordInput autoComplete="new-password" /></Field>
const q = useDebounced(search, 300); // atau langsung: <SearchInput label="Cari" onSearch={setQ} />
Upload
File input menutupi seluruh box (opacity 0), jadi Tab, Enter/Space, dan drag-and-drop file langsung jalan, dan form validation mem-focus input-nya. .ui-file[data-ui-file] menampilkan .ui-file-list (nama, ukuran, button Hapus) di bawah box. .ui-image[data-ui-image] menampilkan preview di .ui-image-frame dengan rasio --ratio; .compact = thumbnail 72px dengan button dan hint di sampingnya. Reset form mengembalikan tampilan awal (termasuk gambar tersimpan). File tetap di input, jadi multipart, required, dan form[data-check] berlaku seperti biasa. Crop sebelum disimpan: adapter Cropper.js.
<FileDrop label="Lampiran" hint="PDF, boleh lebih dari satu" accept=".pdf" multiple onFiles={setFiles} />
const [poster, setPoster] = useState<string | null>(event.posterUrl);
<ImagePicker label="Poster" hint="PNG atau JPG, maks. 2 MB." ratio="4/5" empty="1080 × 1350 px" required
preview={poster} error={errors.poster}
onFile={(f) => { setFile(f); setPoster(f ? URL.createObjectURL(f) : event.posterUrl); }} />
<ImagePicker label="Logo" compact preview={logoUrl} onFile={setLogo} />
Table & pager
.table.stack-sm-rows: di mobile (≤640px) setiap row menjadi card tanpa horizontal scroll; cell pertama jadi judul, td.r (actions) di pojok kanan, td.r kedua (chevron onRowClick dari DataTable) di bawahnya, data-label memberi label cell. .row-menu berisi row actions menu; menunya position: fixed ke viewport (tidak terpotong wrapper table atau dialog), terbuka ke atas bila di bawah tidak muat, dan tertutup saat halaman di-scroll. Server-side pager memakai link; ujung yang nonaktif berupa span.pg[aria-disabled].
| Anggota | NBM | Status | Aksi |
|---|---|---|---|
| Abdul KarimPRM Taman Ilmu | 887.403 | Aktif | |
| Siti MaryamPRM Taman Ilmu | 903.881 | Perlu perbaikan |
React: DataTable (stackOnPhone, page, loading, error + onRetry, selection, bulkActions), LoadMore, RowMenu
Filter aktif, pilihan row & load more
.filter-chips di bawah .filters: satu .chip per filter aktif dengan .chip-x (label "Hapus filter …"), lalu Reset Filter (.btn-link); setelah chip dihapus, focus pindah ke chip berikutnya. Kolom checkbox th.sel / td.sel (label "Pilih <nama>"): row terpilih diberi tint, dan .bulk-bar menggantikan .table-count selama ada yang dipilih. Di ponsel checkbox ada di kiri card; select all tidak tampil karena header table disembunyikan. .card-footer.load-more untuk list berbasis cursor: "Menampilkan X dari Y" (tanpa total: "X atau lebih") dan Muat Lebih Banyak. DataTable loading merender skeleton dengan kolom yang sama, error merender error state dengan Coba Lagi.
| Anggota | NBM | Status | |
|---|---|---|---|
| Abdul KarimPRM Taman Ilmu | 887.403 | Aktif | |
| Siti MaryamPRM Taman Ilmu | 903.881 | Perlu perbaikan |
const list = useInfiniteQuery({ queryKey: ["anggota", q, filters], queryFn, initialPageParam: "", getNextPageParam: (p) => p.next });
const rows = list.data?.pages.flatMap((p) => p.items) ?? [];
const [selected, setSelected] = useState<string[]>([]);
<Filters><SearchInput label="Cari anggota" onSearch={setQ} /></Filters>
<ActiveFilters onReset={clearFilters}
items={filters.map((f) => ({ key: f.key, label: f.label, onRemove: () => removeFilter(f.key) }))} />
<DataTable caption="Anggota" columns={columns} rows={rows} rowKey={(r) => r.id} stackOnPhone
loading={list.isPending} error={list.error} onRetry={() => list.refetch()}
selection={{ selected, onChange: setSelected, rowLabel: (r) => r.nama }}
bulkActions={<Button variant="outline" size="sm" icon="download">Unduh e-KTAM</Button>} />
<LoadMore shown={rows.length} hasMore={!!list.hasNextPage} loading={list.isFetchingNextPage} onMore={() => list.fetchNextPage()} />
Description list & timeline
- Nomor referensi
- ANG-2026-0209
- Ranting
- PRM Taman Ilmu
- Diminta perbaikanVerifikator PCM · 4 Okt 2026, 09.20
Tanggal lahir berbeda dengan KTP.
- DiajukanAdmin PRM · 3 Okt 2026, 13.40
- ✓Anggota
- 2Tujuan
- 3Kirim
React: DescList, CompareRow, Timeline, Stepper, WizardSteps
Empty state
Default: icon polos. .ill: icon di dalam lingkaran (page atau list kosong). .compact: teks saja, untuk card kecil.
Tidak ada data
Ubah filter atau kata kunci pencarian.
Belum ada kegiatan
Kegiatan yang Anda buat muncul di sini.
Belum ada lampiran.
Error state: .empty-state[role="alert"] dengan dua button (jaraknya otomatis). Di React, ErrorBoundary menampilkan ini saat bagian di dalamnya error ketika render: Coba Lagi merender ulang, Muat Ulang Halaman me-reload. Error di event handler atau async tidak tertangkap (batas React): tangani di sana, mis. dengan toast.
Terjadi kesalahan
Bagian ini gagal ditampilkan. Coba lagi, atau muat ulang halaman.
// di layout: error saat render tidak lagi jadi layar kosong; key = pindah halaman ikut mengosongkannya
<ErrorBoundary key={pathname} onError={(e) => report(e)}><Outlet /></ErrorBoundary>
<PageLoading busy={navigation.state === "loading"} saving={save.isPending} />
<EmptyState preset="no-results" action={<Button variant="outline" onClick={clearFilters}>Reset Filter</Button>} />
<EmptyState preset="no-access" circle />
React: EmptyState (preset: empty, no-results, error, no-access; circle, compact), ErrorBoundary (fallback, onError), PageLoading (busy: loading bar; saving: juga overlay dan keyboard ditahan, Esc melepasnya)
Skeleton
Loading state yang bentuknya sama persis dengan halaman setelah data masuk. Render markup halaman yang asli (dengan dummy rows) di dalam container [data-skeleton]: bagian data otomatis jadi bar, chrome (heading, label, header table, button, link, icon) tetap tampil. Tidak ada elemen yang ditambah atau diubah ukurannya, jadi layout tidak loncat saat skeleton diganti data. Container diberi inert (tidak bisa di-klik atau di-focus) dan label role="status" di luarnya untuk screen reader. Dengan prefers-reduced-motion bar tidak berdenyut.
Semua aktif
6 NBM baru tahun ini
Sudah dipetakan
| Anggota | NBM | Status | Aksi |
|---|---|---|---|
| Abdul KarimPRM Taman Ilmu | 887.403 | Aktif | |
| Siti MaryamPRM Taman Ilmu | 903.881 | Perlu perbaikan |
| Jadi bar otomatis | Tetap tampil |
|---|---|
Isi td, .kpi-v, .attention (angka, judul, teks), .work-row (judul, meta, waktu), .table-count, teks pager, .ui-dl dd | Heading, .kpi-l, hint .kpi p, label field, th, label data-label di mobile |
Navigasi: .nav .lbl, .nav .icon, .nav-section, .nav-badge, context button, nama di .who | Logo merek, collapse button, icon button di topbar |
.pill, .chip, .tab-count, .dot-count, .symbol, img, .domain-icon, .progress, button pager | .btn, .btn-link, .text-link, .btn-icon (row menu, chevron) |
| Nilai dan placeholder input/select/textarea (juga select2 dari adapter), checkbox, radio, switch | Box field, icon search, chevron select |
Pengecualian: data-skeleton-bar memaksa elemen jadi bar (teks jadi bar selebar teksnya; elemen kosong jadi box), mis. subjudul halaman. data-skeleton-text membuat target otomatis atau leluhurnya tetap teks, mis. link Bantuan di footer sidebar. Batasan: bar teks bersudut siku dan hanya berdenyut (tanpa shimmer); data-skeleton-text di dalam target bar teks (mis. span di dalam td) tidak berpengaruh; img butuh src yang valid (gambar rusak menampilkan alt text), pakai span.symbol bila belum ada; unit di .kpi-v jadi bar yang lebih tipis.
const rows = loading ? DUMMY_ROWS : members; // dummy rows: bentuk sama dengan data asli
const page = <DataTable columns={columns} rows={rows} rowKey={(m) => m.id} stackOnPhone />;
return loading ? <Skeleton label="Memuat daftar anggota">{page}</Skeleton> : page;
React: Skeleton (label; inert dan status label sudah dipasang)
Avatar & domain icon
Photo mengisi lingkaran (img.symbol, object-fit cover); tanpa photo memakai initials.
React: Avatar (photo), DomainIcon
Dialog & menu
Dialog memakai <dialog class="modal">: focus trap, Esc dan backdrop click menutup. data-blocking: Esc dan backdrop click tidak menutup (dialog hanya selesai lewat action-nya). Close button tetap berukuran sama di samping title yang panjang. Popover umum (ekspor, filter, pilihan): .ui-pop berperilaku seperti .row-menu (menu fixed ke viewport, tidak terpotong card); data-align="start" membukanya rata kiri dengan trigger.
<div class="ui-pop" data-align="start">
<button class="btn btn-o" data-ui-menu="ekspor" aria-expanded="false">Ekspor<svg class="icon sm"><use href="#i-down"/></svg></button>
<div class="ui-menu" id="ekspor" hidden><button>Excel</button><button>PDF</button></div>
</div>
<Popover align="start" trigger={<Button variant="outline" iconEnd="down">Ekspor</Button>}>
<button type="button" onClick={toExcel}>Excel</button>
<button type="button" onClick={toPdf}>PDF</button>
</Popover>
React: Modal (wide, blocking), Popover (trigger, align; klik item, Esc dan klik di luar menutup), RowMenu, useDismiss
Toast & confirm
Toast gelap di kiri bawah (full width di mobile) dengan tone icon dan close button: LabmuUI.toast(message, "success"|"error"|"warning"|"info", ms); default info, 5 detik. Bentuk object LabmuUI.toast({ message, description, tone, action: { label, onClick } }) menambah description di bawah message dan satu action teks (mis. Batalkan); toast dengan action tidak hilang sendiri sampai dipakai atau ditutup. Saat modal <dialog> terbuka, toast tampil di dalam modal teratas (sisa halaman inert, jadi toast di belakangnya tidak terlihat dan tidak bisa ditutup), lalu kembali ke body saat modal tertutup; coba Simpan Draf di Open Dialog. Confirm dialog: data-confirm pada link atau form, atau await LabmuUI.confirm({ title, text, ok, danger }). Batal di kiri, action di kanan (merah bila data-danger).
React: ToastProvider + useToast()(message, tone) atau useToast()({ message, description, tone, action }), useConfirm(), ConfirmDialog (cancel, body, onConfirm)
On-boarding
Lima layer: welcome dialog 3 layar (dialog.onb-dlg), spotlight tour (LabmuUI.tour), contextual tip sekali per halaman (LabmuUI.tip), card Panduan memulai (.onb-card), dan drawer Panduan halaman (dialog.guide-drawer). Markup lengkapnya ada di On-boarding Example.
const st = LabmuUI.onboardingState(user.id, role); // prototype: localStorage; production: simpan di server
if (!st.get().welcomed) document.getElementById("welcome").showModal();
React: WelcomeDialog, Tour, ContextTip, GettingStarted, ProgressRing, ChecklistLauncher, OnboardingHelp, PageGuide, GuideButton
App shell
.sidebar + .main > .topbar + .content, seperti halaman ini dan app example. .nav-section memberi label group menu; di collapsed sidebar berubah jadi divider. Footer sidebar selalu menampilkan Versi X.Y · © tahun Muhammadiyah: hanya major.minor dari versi app (2.3.2 tampil 2.3); di collapsed sidebar teksnya disembunyikan.
Urutan topbar standar: kiri .menu-btn dan .logo-sm (keduanya hanya di layar ≤900px), lalu profil .who yang selalu di kiri; kanan .top-actions berisi search dulu lalu icon tool, hanya kalau ada tool. Profil menggabungkan context (organisasi dan peran, di atas) dengan nama (di bawah); card-nya terbuka ke kanan dan memuat organisasi, nama, email, serta account actions seperti Ganti organisasi. Logo teks yang lebar (bukan mark persegi): tambah .wordmark di .mark-mini (tinggi 12px, menjauhi button collapse) atau di img dalam .logo-sm (tinggi 20px).
<div class="app-sidebar-footer"><small>Versi 2.3 · © 2026 Muhammadiyah</small></div>
<header class="topbar">
<button class="btn-icon menu-btn" data-ui-nav-toggle aria-label="Buka navigasi">…</button>
<a class="logo-sm" href="/" aria-label="Beranda"><img src="mark.svg" alt=""></a> <!-- logo teks: <img class="wordmark" …> -->
<div class="who">
<button class="who-btn" data-ui-menu="who-card" aria-expanded="false" aria-haspopup="dialog">
<img class="symbol" src="foto.jpg" alt="">
<span class="who-ctx"><strong>Admin Utama · PCM Cakrawala</strong><small>Rahmat Hidayat</small></span>
<svg class="icon"><use href="#i-down"/></svg>
</button>
<div class="who-card ui-menu" id="who-card" role="dialog" aria-label="Identitas pengguna" hidden>…</div>
</div>
<div class="top-actions"> <!-- tanpa tool: hilangkan seluruh div ini -->
<div class="search-input">…</div>
<button class="btn-icon" aria-label="Notifikasi">…</button>
</div>
</header>
<nav><div class="nav">
<a href="/" aria-current="page"><svg class="icon duo"><use href="#d-home"/></svg><span class="lbl">Dashboard</span></a>
<div class="nav-section">Admin</div>
<a href="/settings"><svg class="icon duo"><use href="#d-settings"/></svg><span class="lbl">Pengaturan</span></a>
</div></nav>
Dengan tool: profil kiri, search dan notifikasi kanan
Tanpa tool dan tanpa context: profil kiri (nama saja), kanan kosong
<AppShell brand={…} brandMini={…} nav={nav} current="home" version="2.3.2"
account={<AccountMenu name="Rahmat Hidayat" context="Admin Utama · PCM Cakrawala" org="PCM Cakrawala" email="rahmat@contoh.or.id">…</AccountMenu>}
actions={<><SearchInput label="Cari" /><IconButton icon="bell" label="Notifikasi" /></>}>
…
</AppShell>
// tanpa tool: hilangkan actions; logoSm default = brandMini
// main.tsx, sekali: semua link kit memakai Link router (React Router; Link TanStack Router sama)
<LinkProvider render={(href, p) => <Link to={href} {...p} />}><App /></LinkProvider>
// vite.config.ts: version dari package.json
import pkg from "./package.json";
export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(pkg.version) } });
// vite-env.d.ts: declare const __APP_VERSION__: string; lalu <AppShell version={__APP_VERSION__} …>
React: LinkProvider sekali di akar app membuat semua link kit (nav dan brand AppShell, ButtonLink, TextLink, WorkRow, Tabs mode link, AppLink) memakai Link router. Hanya path internal (/…) tanpa target atau download yang lewat router; URL luar, mailto: dan #anchor tetap <a>. renderLink(item, props) di AppShell didahulukan untuk nav (props: NavLinkProps). Di ponsel, klik link apa pun di sidebar dan Esc menutup drawer (focus kembali ke button menu). Judul NavItem.section tampil saat section berganti, jadi isi section di setiap item group supaya judulnya tetap ada setelah item disaring per izin.
Halaman detail: .page-back di atas eyebrow dan judul .page-head kembali ke halaman induknya (lihat settings example).
<div class="page-head">
<div>
<a class="page-back" href="/anggota"><svg class="icon"><use href="#i-back"/></svg>Kembali</a>
<p class="eyebrow">Anggota</p>
<h1>Abdul Karim</h1>
</div>
</div>
<PageHead back={{ href: "/anggota" }} eyebrow="Anggota" title="Abdul Karim" /> // label default "Kembali"
React: AppShell (version, account, actions, logoSm, renderLink, NavItem.section), AccountMenu, PageHead (back), WorkRow, LinkProvider, AppLink
Rich text
.rich untuk menampilkan rich text: HTML hasil editor (Quill adapter) atau Markdown yang sudah di-render, dengan heading, list, bold, link, code, pre, blockquote, table dan hr. Table yang lebar di-scroll di dalam teksnya, tidak mendorong halaman. Sanitize HTML-nya di server sebelum disimpan dan ditampilkan.
Musyawarah Ranting 2026
Dilaksanakan Ahad, 8 November 2026 di aula PCM. Setiap ranting mengirim:
- daftar utusan,
- laporan pertanggungjawaban.
Formulir ada di laman dokumen.
Utusan hadir paling lambat pukul 08.00 untuk registrasi.
| Ranting | Utusan |
|---|---|
| PRM Taman Ilmu | 5 |
| PRM Karya Bersama | 4 |
Kode registrasi: MUSRAN-26
GET /api/musyawarah/2026/utusan
React: Rich (html yang sudah di-sanitize, atau children)
Code & copy
pre.code untuk JSON, log dan snippet (font mono, scroll di dalam box, maks. 420px). .copy-block menaruh .copy-btn di pojok kanan atas. [data-copy="teks"] menyalin teks itu, data-copy="#id" menyalin value (input) atau teks elemen #id; hasilnya diumumkan lewat toast. Clipboard hanya jalan di https atau localhost. Rahasia sekali tampil (kunci API, token baru): input readonly + Salin + .note.warn.
{
"event": "anggota.dibuat",
"nbm": "1.231.044",
"ranting": "PRM Taman Ilmu"
}<CodeBlock>{JSON.stringify(payload, null, 2)}</CodeBlock>
<CopyButton text={member.nbm} label="Salin NBM" />
<SecretReveal label="Kunci API baru" value={created.key} /> // note default: "Simpan sekarang. …"
await copyText(link); // Promise, gagal = throw (http di host non-lokal)
React: CodeBlock (copy), CopyButton (text, label; "Tersalin" selama 2 detik, toast bila gagal), SecretReveal (label, value, note), copyText(text)
labmu-ui.js
Dependency-free; load di akhir <body> atau dengan defer. Semua behavior diaktifkan lewat attribute, jadi markup tetap server-rendered.
| Attribute / API | Behavior |
|---|---|
[data-ui-tabs] | Tabs dengan panel aria-controls; arrow keys, Home, End |
[data-ui-seg] · .switch[role=switch] | Segmented control dan switch toggle |
data-ui-open="id" · data-ui-close | Open/close <dialog>; [data-autofocus] dapat focus |
dialog[data-blocking] | Esc dan backdrop click tidak menutup |
data-ui-menu="id" | Toggle .ui-menu; di .row-menu dan .ui-pop menu fixed ke viewport, terbuka ke atas bila di bawah tidak muat; .ui-pop[data-align=start] rata kiri dengan trigger |
data-ui-collapse · data-ui-nav-toggle | Collapsed sidebar (disimpan) dan mobile nav drawer |
data-ui-brand="amanah" | Ganti brand di <html> |
a[data-confirm] · form[data-confirm] | Confirm dialog dulu; data-confirm-text, data-confirm-ok, data-danger |
form[data-check] | Form validation (message bahasa Indonesia); send-once (submit button aria-busy) |
form[data-ui-guard] | Isian berubah: .save-bar[hidden] di dalamnya muncul, reload/tutup tab bertanya dulu; submit (yang lolos validasi) dan reset membersihkannya. Ketik di input[type=search] tidak dihitung |
[data-copy="teks"] · data-copy="#id" | Salin teks itu, atau value/teks elemen #id, ke clipboard; toast berhasil atau gagal (butuh https atau localhost) |
.ui-file[data-ui-file] | List file terpilih (nama, ukuran, button Hapus) di bawah box; Hapus mengganti input.files dan memicu change |
.ui-image[data-ui-image] | Preview gambar terpilih di .ui-image-frame, button jadi "Ganti Gambar"; reset form mengembalikan tampilan awal |
body[data-ui-loading] | Loading bar + overlay 150 ms setelah page mulai pindah; semua form send-once; Esc menghapus overlay |
LabmuUI.toast(msg, tone, ms) | Toast, di dalam modal teratas bila ada yang terbuka; call lama toast(msg, ms) tetap jalan |
LabmuUI.toast({ message, description, tone, action, ms }) | action: { label, onClick } = satu button teks; toast dengan action tidak hilang sendiri |
LabmuUI.confirm(o) | Promise<boolean> |
LabmuUI.guard(form, dirty) | Set status perubahan form [data-ui-guard] sendiri, mis. false setelah simpan lewat fetch |
LabmuUI.check(form) · LabmuUI.fieldError(el, msg) | Validate form yang di-submit lewat fetch · server-side error message ("" menghapus) |
LabmuUI.selectTab(tab) · LabmuUI.setNavMini(on) | Select tab · collapse sidebar |
LabmuUI.tour.start(steps, opts) · tour.end() · tour.place() | Spotlight tour; steps: [{ target, title, body }], opts: { label, onStep, onEnd }; keyboard → ← Esc |
LabmuUI.tip.show(o) · tip.close() · tip.place() | Contextual tip; o: { anchor, title, body, guide, onAction } |
LabmuUI.onboardingState(userId, role) | On-boarding state per user dan role di localStorage: get, set, reset |
LabmuUI.placePopover(el, rect, pad) | Posisikan popover di kanan, bawah, atau atas target |
dialog .onb-w · [data-onb-next] · [data-onb-back] · .onb-choice | Pindah layar welcome dialog (event onb:step) dan pilihan card |
Adapters
Opt-in: hanya di-load app yang memakai library-nya. Load CSS library, lalu labmu-ui.css, lalu CSS adapter; JS library sebelum JS adapter. Lihat adapters example.
| Adapter | Library | Behavior |
|---|---|---|
adapters/select2.css · .js | jQuery 3 + select2 4.1 | Setiap <select> (kecuali di [data-native] dan [data-ajax]); search box bila lebih dari 8 option; dropdown terbuka di dalam dialog; teks pilihan disembunyikan di dalam [data-skeleton]; select2Field(el, options) |
adapters/flatpickr.css · .js | flatpickr 4.6 + l10n id | Setiap input[type=date|time]: tampil "20 November 2026", "14.30"; tetap submit Y-m-d / H:i; button Hapus untuk optional field; flatpickrField(el) (instance yang ada bila sudah terpasang) |
adapters/quill.css · .js | Quill 1.3.7 | Rich text editor di setiap .rich-text ([data-editor] + hidden input); toolbar label bahasa Indonesia |
adapters/cropper.css · .js | Cropper.js 1.6 | .ui-image[data-crop="4/5"]: gambar terpilih dibuka di dialog "Atur {label}" (Batal / Simpan); hasil crop JPEG (data-crop-size="1080x1350") menggantikan file di input, juga sebagai data URL di hidden input[data-crop-data] bila ada; Batal mempertahankan gambar sebelumnya. React: ImagePicker crop="4/5", onFile menerima hasil crop |
adapters/react.tsx | React + adapter select2 / flatpickr | Select (prop select2, mis. { ajax }) dan DateInput (value "2026-11-20" / "14:30", onChange(value)): widget dipasang di elemen yang dirender React, mengikuti controlled value, dibongkar saat unmount; tanpa adapter fallback ke select/input native. Import @labmu/ui/adapters/react |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css">
<link rel="stylesheet" href="https://ui-kit.labmu.id/v2.2.1/css/labmu-ui.css">
<link rel="stylesheet" href="https://ui-kit.labmu.id/v2.2.1/adapters/flatpickr.css">
…
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/l10n/id.min.js"></script>
<script src="https://ui-kit.labmu.id/v2.2.1/adapters/flatpickr.js"></script>
<script src="https://ui-kit.labmu.id/v2.2.1/js/labmu-ui.js"></script>
React
Typed React 18/19 components, tanpa dependency selain React; npm run typecheck mengecek semuanya.
| CSS class | Component | File |
|---|---|---|
.btn .btn-icon .btn-link [aria-busy] | Button, ButtonLink, IconButton, TextLink | Button.tsx |
.pill .tab-count .chip .tags | Badge, Pill, Count, Chip, Tags | Badge.tsx |
.card · .kpi .attention .progress | Card · Stat, Attention, Progress | Card.tsx · Stat.tsx |
.note .link-banner | Note, Banner | Note.tsx |
.ui-tabs .seg | Tabs, Segmented | Tabs.tsx |
.field .ui-input-group .ui-check .ui-choice .switch .ui-file .ui-file-list .ui-image .filters .filter-chips .chip-x .form-grid .form-foot .save-bar .color-pair | Field, Input, Select, Textarea, SearchInput, InputGroup, PasswordInput, Check, Choice, Switch, FileDrop, ImagePicker, Filters, ActiveFilters, FormGrid, SaveBar, ColorField, contrastRatio, useDebounced, useUnsavedGuard | Form.tsx |
.table .stack-sm-rows .pager td.sel .bulk-bar .load-more | DataTable (loading, error, selection), LoadMore | DataTable.tsx |
.row-menu .ui-pop · .who | RowMenu, Popover, useDismiss · AccountMenu | Menu.tsx |
.ui-dl .cbox · .ui-timeline .ui-stepper | DescList, CompareRow · Timeline, Stepper, WizardSteps | DescList.tsx · Timeline.tsx |
dialog.modal [data-blocking] | Modal (blocking) | Modal.tsx |
.empty-state .toast .toast-desc .toast-action · confirm dialog | EmptyState (preset), ErrorBoundary, PageLoading, ToastProvider/useToast (message atau options), ConfirmDialog/useConfirm (onConfirm) | Feedback.tsx |
pre.code .copy-block .copy-btn | CodeBlock, CopyButton, SecretReveal, copyText | Code.tsx |
[data-skeleton] [data-skeleton-bar] [data-skeleton-text] | Skeleton | Skeleton.tsx |
.symbol .domain-icon | Avatar (photo), DomainIcon | Avatar.tsx |
.sidebar .nav-section .topbar .logo-sm .top-actions .page-head .page-back .work-row | AppShell, AccountMenu, PageHead (back), WorkRow | Shell.tsx |
.brand-panel .brand-glass | BrandPanel | Marketing.tsx |
.mid-btn .mid-btn-lg | MidButton | Button.tsx |
.login .login-box .login-or .login-note | — (markup, dengan Card dan MidButton) | — |
dialog.onb-dlg .onb-tour .onb-tip .onb-card .onb-ring .onb-launch .onb-help dialog.guide-drawer .guide-btn | WelcomeDialog, Tour, ContextTip, GettingStarted, ProgressRing, ChecklistLauncher, OnboardingHelp, PageGuide, GuideButton | Onboarding.tsx |
.stack .spread .ui-inline .grid-* .rich | Stack, Spread, Inline, Grid, Rich | Layout.tsx |
link app (a) | LinkProvider, AppLink | Link.tsx |
| select2 · flatpickr (adapter) | Select, DateInput | adapters/react.tsx |
| format id-ID | fmtDate, fmtDateTime, fmtTime, fmtNumber, fmtPercent, fmtRelative | format.ts |
const toast = useToast();
const [confirm, confirmDialog] = useConfirm();
async function remove() {
// onConfirm: button OK berputar, Batal/Esc tidak menutup sampai selesai; gagal (throw) = dialog tetap terbuka
const yes = await confirm({ title: "Hapus draf ini?", ok: "Hapus", danger: true,
onConfirm: () => api.remove(id).catch((e) => { toast("Gagal menghapus, coba lagi.", "error"); throw e; }) });
if (!yes) return;
toast({ message: "Draf dihapus.", tone: "success", action: { label: "Batalkan", onClick: () => api.restore(id) } });
}
return (<>
<Button variant="light-danger" onClick={remove}>Hapus Draf</Button>
{confirmDialog}
</>);
Format tanggal, angka dan persen untuk table dan halaman detail (Intl id-ID); value kosong atau tidak valid tampil "—".
fmtDate("2026-11-08T09:12") // "8 Nov 2026"
fmtDateTime("2026-11-08T09:12") // "8 Nov 2026, 09.12"
fmtTime("2026-11-08T09:12") // "09.12"
fmtNumber(1231044) // "1.231.044"
fmtPercent(0.756) // "75,6%"
fmtRelative(sentAt) // "baru saja", "5 menit yang lalu", "kemarin"; lewat 7 hari = tanggal
fmtDate(null) // "—" (kosong atau tidak valid, tidak pernah "Invalid Date")
Accessibility
- Keyboard focus selalu terlihat (
--focus, outline 2px). Satu pengecualian: box<dialog>itu sendiri (di-focus olehshowModal) tanpa outline; control di dalamnya tetap punya focus ring. - Form validation mengisi
aria-invaliddanaria-describedby; field invalid pertama di-focus. - Tabs mendukung arrow keys/Home/End. Dialog melakukan focus trap.
IconButtonwajib punya label. data-blockingmengandalkan eventcancel; Chrome tetap menutup dialog dengan Esc bila belum ada interaksi sejak dialog dibuka.- Touch target ≥ 40px;
prefers-reduced-motiondihormati (loading bar dan skeleton jadi statis). - Skeleton: container
[data-skeleton]diberiinert(tidak bisa di-klik, di-focus atau dibaca), dan labelrole="status"ditaruh di luarnya supaya screen reader tetap mengumumkan loading. - Filter aktif:
.chip-xberlabel "Hapus filter …" dan focus pindah ke chip berikutnya (atau Reset Filter) setelah dihapus. Checkbox row berlabel "Pilih <nama>", select all "Pilih semua baris di halaman ini".ColorFieldmemperingatkan kontras di bawah 4,5:1. - Contrast: semua pasangan teks lolos AA kecuali
faint(dekoratif) dan disabled button.
Changelog
Distribusi · 8 November 2026
- Docs pindah ke
https://ui-kit.labmu.id; GitLab Pages lama dimatikan. Rilis 2.0.0 sampai 2.2.1 punya git tag dan folder per versi dihttps://ui-kit.labmu.id/v<versi>/. Adopter React memasang kit lewat git tag, bukan salinan folder. Lihat Installation.
2.2.1 · 8 November 2026
- Fix:
ConfirmDialog/useConfirm(React) kini memberi focus ke button aksi saat terbuka, sepertiLabmuUI.confirm, bukan ke Batal;WelcomeDialogmemberi focus ke Lanjut, bukan Lewati.
2.2.0 · 8 November 2026
- Baru: halaman login
.login(.login-box,.login-logo,.login-or,.login-note) dan login example. - Baru: filter aktif
.filter-chips+.chip-x, ReactActiveFilters;SearchInputonSearch(debouncedelay, Enter langsung) dan hookuseDebounced. - Baru:
DataTableloading(skeleton dengan kolom yang sama),error+onRetry,selection(kolomth.sel/td.sel) danbulkActions(.bulk-bar);LoadMore(.load-more) untuk list berbasis cursor, "X atau lebih" bila total belum diketahui. - Baru:
EmptyStatepreset(empty, no-results, error, no-access),ErrorBoundary,PageLoading(busy,saving); dua button di.empty-stateberjarak otomatis. - Baru:
pre.code,.copy-block,.copy-btn,[data-copy]di labmu-ui.js; ReactCodeBlock,CopyButton,SecretReveal,copyText. - Baru:
.form-grid(.three,.full),.form-foot,.save-baryang menempel di bawah layar,form[data-ui-guard]+LabmuUI.guard; ReactFormGrid,SaveBar,useUnsavedGuard. - Baru:
.color-pairdan ReactColorField(peringatan kontras WCAG di bawah 4,5:1,contrastRatio,HEX);PasswordInputdengan Tampilkan/Sembunyikan. - Baru:
.page-backdanPageHeadback;.richkini menatacode,pre,blockquote,table(di-scroll bila lebar) danhr. - Baru: format id-ID
fmtDate,fmtDateTime,fmtTime,fmtNumber,fmtPercent,fmtRelative(value kosong = "—"). Icons baru:i-copy,i-eye-off,i-link,i-external-link,i-paperclip,i-trash. - Baru: settings example (form grid, save bar, warna, password, kunci API, blok kode).
2.1.0 · 8 November 2026
- Baru React:
LinkProvider+AppLink. Sekali di akar app, semua link kit (nav dan brandAppShell,ButtonLink,TextLink,WorkRow,Tabsmode link) memakai Link router (React Router, TanStack Router); URL luar,targetdandownloadtetap<a>.AppShellrenderLinkmenerimaNavLinkProps(termasuktitledi sidebar ciut). - Baru React: di ponsel, klik link apa pun di sidebar (juga lewat
renderLink) dan Esc menutup drawerAppShell. JudulNavItem.sectionhanya tampil saat section berganti, jadi aman diisi di setiap item group.Tabsmode link (hrefper item).Input,Select,Textarea,IconButtonmeneruskanref.ImagePickercropdancropSize(adapter Cropper.js). - Baru: popover umum
.ui-pop(HTMLdata-ui-menu, rata kiri dengandata-align="start"); ReactPopoverdan hookuseDismiss(Esc dan klik di luar). - Baru: toast dengan description dan satu action (
.toast-desc,.toast-action):LabmuUI.toast({ message, description, tone, action }), ReactuseToast()({ … }); toast dengan action tidak hilang sendiri.ConfirmDialog/useConfirm:cancel,body, danonConfirm(button OK berputar sampai aksi selesai; gagal = dialog tetap terbuka). - Baru:
.wordmarkuntuk logo teks yang lebar di.mark-mini(tinggi 12px, menjauhi button collapse) dan.logo-sm(tinggi 20px). - Baru:
adapters/react.tsx(@labmu/ui/adapters/react) berisiSelect(select2) danDateInput(flatpickr) untuk app React; adapter flatpickr kini menyediakanflatpickrField(el).
2.0.1 · 8 November 2026
- Fix: icon di dalam button (
.btn,.btn-icon,.btn-link) di.card-headerdan.empty-statekini ikut warna dan ukuran button-nya, tidak lagi abu-abu atau 30px. - Fix:
fieldsetdanlegendtanpa border dan padding bawaan browser;fieldset.fielddenganlegendtampil seperti field berlabel. - Fix:
.page-headtanpa margin bawah di dalam.stack,pkosong tidak memakan tempat, dan.head-actionstetap di kanan saat turun ke baris baru..tw-wrapkiniposition:relative. - Fix React:
Modalmemakai id judul unik (dua modal tidak lagi berbagi id);DataTableonRowClicktidak jalan saat klik button, link, input atauRowMenudi dalam cell; klik aksi diAccountMenumenutup card-nya. - Fix: adapter Cropper.js menutup dialog lewat Batal dan klik latar tanpa
labmu-ui.js(app React).
2.0.0 · 8 November 2026
- Baru: app shell standar untuk semua produk. Footer sidebar selalu Versi X.Y · © tahun Muhammadiyah (versi package tetap X.Y.Z, footer hanya major.minor). Topbar: kiri
.menu-btn→.logo-sm(keduanya ≤900px) → profil.who; kanan.top-actions(search dulu, lalu icon tool) hanya kalau ada tool. Class baru.logo-sm. - Breaking:
.context-button,.context-icon,.context-textdan ReactContextButtondihapus. Context (organisasi dan peran) kini masuk profil:.who-ctxberisi context distrong(atas) dan nama dismall(bawah);AccountMenumenampilkancontextdi atas nama, tanpacontexthanya nama. - Breaking:
AppShellproptopbardigantiaccount(profil, kiri) danactions(dibungkus.top-actions, kanan);versionkini wajib (string, mis."2.3.2") dan footer selalu dirender; prop opsionallogoSm(defaultbrandMini). - Breaking:
.top-actionskinimargin-left:auto,.topbar .search-inputtidak lagi; taruh search di dalam.top-actions. Card.whoselalu terbuka ke kanan (aturan.who:last-childdihapus).
1.5.2 · 6 November 2026
- Fix: di Chrome, bar teks skeleton di dalam link yang sudah pernah dibuka (
:visited, mis. label navigasi dan work row) tampil abu-abu gelap. Bar teks di dalam link kini memakai warna tetap dan berdenyut lewatopacitydari animasi yang sama, jadi warnanya sama dengan bar lain.
1.5.1 · 6 November 2026
- Fix: di dalam
[data-skeleton], teks pilihan dan placeholder select2 (adapters/select2.css) kini disembunyikan seperti native select; box field tetap tampil.data-skeleton-textdi select atau leluhurnya menjaga teksnya. Contoh di adapters.html.
1.5.0 · 5 November 2026
- Baru: skeleton mode.
[data-skeleton]pada container membuat bagian data dari markup kit (cell table, KPI, work row, pill, avatar, nilai input, navigasi) jadi bar yang berdenyut, sementara heading, label, button dan icon tetap tampil. Ukuran setiap elemen sama persis dengan halaman setelah loaded. Pengecualian lewatdata-skeleton-bardandata-skeleton-text; ReactSkeleton(label) memasanginertdan status label. CSS saja, tanpa perubahanlabmu-ui.js.
1.4.0 · 4 November 2026
- Fix:
.ui-filekini bisa dicapai keyboard (Tab, Enter/Space), menerima drag-and-drop file, menampilkan focus ring dan state invalid. Form validation mem-focus input-nya dan menulis message di bawah box ("Unggah dokumen wajib diisi."). - Baru:
.ui-file-list(nama, ukuran, button Hapus); HTML lewat.ui-file[data-ui-file], ReactFileDropmembuatnya sendiri.FileDropdapat propname,required,disabled. - Baru: image picker
.ui-image(frame dengan--ratio) dan.ui-image.compact; HTML lewat[data-ui-image], ReactImagePicker. - Baru: adapter
adapters/cropper.css·.js(Cropper.js 1.6): crop dengan rasio tetap sebelum gambar disimpan.
1.3.2 · 3 November 2026
- Fix: di mobile,
DataTabledenganstackOnPhone,onRowClickdan kolomalign: "right"menaruh chevron Buka detail di atas actions, jadi tap ke button actions membuka detail. Chevron kini di bawah actions. Satu card tetap memuat satu kolom actions; dua kolom data rata kanan masih bertumpuk.
1.3.1 · 3 November 2026
- Fix: toast yang muncul saat modal
<dialog>terbuka kini terlihat dan bisa ditutup. Selama tampil, toast tinggal di dalam modal teratas, lalu pindah ke modal berikutnya atau ke body saat modal tertutup atau dilepas dari DOM. Berlaku untukLabmuUI.toastdanToastProvider. - Fix: close button toast di dalam
LabmuUI.confirmtidak lagi dibaca sebagai Batal. - React:
ToastProvidermemakaicreatePortal, jadireact-domkini optional peer dependency.
1.3.0 · 1 November 2026
- Logo: semua logo produk diganti dengan file master, semua SVG. Brand baru
eventmu, logo ChatHPT, dan penulisan AiMu. - Perhatian saat upgrade: file
assets/brand/*.pngdihapus; ganti referensinya ke.svgdengan nama yang sama. - Background:
assets/backgrounds/<produk>-bg.svg, token--brand-bg,--brand-gradient,--brand-bg-shade, class.brand-paneldan.brand-glass..closingkini memakai background brand. - On-boarding:
css/onboarding.css,react/Onboarding.tsx,LabmuUI.tour,LabmuUI.tip,LabmuUI.onboardingState,LabmuUI.placePopover, danexamples/onboarding.html. - Login: semua tombol masuk memakai Masuk dengan M-ID (
.mid-btn,.mid-btn-lg,MidButton, token--middan--mid-hover,assets/brand/mid-mark.svg). - Footer:
.footer-logosuntuk logo produk + Majelis mitra dalam putih penuh (.majelis,assets/brand/majelis-mpi-negative.svg,majelis-mtt-negative.svg,majelis-pp-negative.svg); kanan bawah menjadi Powered by + logo LabMu. - Lockup header menjadi Produk | Powered by LabMu (
.brand-by), dengan contoh fileassets/brand/paymu-logo-powered.svgdanpaymu-logo-powered-negative.svg. - Base:
[hidden]{display:none!important}. - Fix: button
data-ui-branddi dalam[data-ui-seg]kini benar-benar mengganti brand.
1.2.0 · 30 Oktober 2026
- Fixes: focus ring
.ui-input-group, dialog close button tidak mengecil,img.symbolcover,a.chip+.tags,.pagerdengan link,[hidden]selalu menang,.nav-sectionjadi divider di collapsed sidebar, dialog box tanpa focus outline. - labmu-ui.js: confirm dialog (
data-confirm,LabmuUI.confirm),dialog[data-blocking],form[data-check],body[data-ui-loading], busy button, toast tones dengan snackbar look. - New patterns:
.table.stack-sm-rows,.row-menu,.rich,.empty-state .ill/.compact,.who. - React:
Button loading, toast tones,useConfirm/ConfirmDialog,Modal blocking,EmptyState circle/compact,Avatar photo,NavItem.section,DataTable stackOnPhone,RowMenu,AccountMenu,Rich,Chip,Tags. - Opt-in adapters: select2, flatpickr, Quill. Docs site ini, dengan istilah teknis dalam English.
- Diambil dari migrasi EventMu ke LabMu UI.
1.1.0
- Initial package: tokens 7 brand, component CSS (app + marketing), 87 icons, labmu-ui.js, React components, logo produk.