Dokumentasiv2.2.1
Brand preview

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.

FolderIsiDipakai oleh
tokens/tokens.json (source), tokens.css, _tokens.scss, tokens.tsSemua; ubah value hanya di tokens.json
css/labmu-ui.cssBundle token + base + app + marketingCara tercepat
css/base.css app.css marketing.cssLayer terpisahBila hanya butuh sebagian
icons/ · js/labmu-icons.jsSprite 50 line icons (i-*) + 43 duotone icons (d-*)HTML/server-side
js/labmu-ui.jsTabs, dialog, menu, sidebar, toast, confirm dialog, form validation, unsaved-changes guard, copy ke clipboard, page loading, spotlight tour, on-boarding tipsHTML/server-side (Laravel, Django, Go…)
adapters/select2, flatpickr, Quill dengan look LabMu (opt-in)App yang memakai library itu
react/Typed React 18/19 componentsVite/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-bgPanel 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

AttributeValueEfek
data-brand di <html> atau wrappersatumu (default) · sehatmu · amanah · bukumu · aimu · masa · paymu · eventmuMengganti seluruh palette; bisa nested
data-surface di <body>marketingBase 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.

Brand status

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

Masuk dengan akun Muhammadiyah Anda.

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).

Lihat semua

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.

Terverifikasi Perlu perbaikan Ditolak Sedang diperiksa Draf Baru 12

React: Badge, Pill, Count, Chip (href), Tags

Card & KPI

Organisasi binaan
4ranting

Semua aktif

Anggota tercatat
22orang

6 NBM baru tahun ini

Dana siap diajukan
Rp380.000

Sudah dipetakan

Kelengkapan organisasi

4 ranting binaan

React: Card, Stat, Attention, Progress

Note & banner

Menu mengikuti penugasan aktif Anda.
Perlu perbaikan. Tanggal lahir berbeda dengan KTP.

React: Note, Banner

Tabs & segmented

Isi tab Daftar.

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.

Focus field ini: focus ring melingkupi input dan button.
Alasan mutasi

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.

Kontras dengan #FFFFFF hanya 1,5:1. Minimal 4,5:1 supaya terbaca.

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.

4:51080 × 1350 px
PNG atau JPG, maks. 2 MB.
1:1 Persegi 512 × 512 px.
<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].

3 anggota
AnggotaNBMStatusAksi
Abdul KarimPRM Taman Ilmu887.403Aktif
Siti MaryamPRM Taman Ilmu903.881Perlu 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.

Ranting: PRM Taman Ilmu e-KTAM: Diterbitkan
1 dipilih
AnggotaNBMStatus
Abdul KarimPRM Taman Ilmu887.403Aktif
Siti MaryamPRM Taman Ilmu903.881Perlu 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
  1. Diminta perbaikanVerifikator PCM · 4 Okt 2026, 09.20

    Tanggal lahir berbeda dengan KTP.

  2. DiajukanAdmin PRM · 3 Okt 2026, 13.40
  1. ✓Anggota
  2. 2Tujuan
  3. 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.

// 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.

Memuat daftar anggota
Organisasi binaan
4ranting

Semua aktif

Anggota tercatat
22orang

6 NBM baru tahun ini

Dana siap diajukan
Rp380.000

Sudah dipetakan

3 anggota
AnggotaNBMStatusAksi
Abdul KarimPRM Taman Ilmu887.403Aktif
Siti MaryamPRM Taman Ilmu903.881Perlu perbaikan
Jadi bar otomatisTetap tampil
Isi td, .kpi-v, .attention (angka, judul, teks), .work-row (judul, meta, waktu), .table-count, teks pager, .ui-dl ddHeading, .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 .whoLogo 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, switchBox 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.

RH AF

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).

Hapus Draf

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.
RantingUtusan
PRM Taman Ilmu5
PRM Karya Bersama4

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"
}
Simpan sekarang. Nilai ini hanya ditampilkan sekali.
<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 / APIBehavior
[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-closeOpen/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-toggleCollapsed 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-choicePindah 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.

AdapterLibraryBehavior
adapters/select2.css · .jsjQuery 3 + select2 4.1Setiap <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 · .jsflatpickr 4.6 + l10n idSetiap 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 · .jsQuill 1.3.7Rich text editor di setiap .rich-text ([data-editor] + hidden input); toolbar label bahasa Indonesia
adapters/cropper.css · .jsCropper.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.tsxReact + adapter select2 / flatpickrSelect (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 classComponentFile
.btn .btn-icon .btn-link [aria-busy]Button, ButtonLink, IconButton, TextLinkButton.tsx
.pill .tab-count .chip .tagsBadge, Pill, Count, Chip, TagsBadge.tsx
.card · .kpi .attention .progressCard · Stat, Attention, ProgressCard.tsx · Stat.tsx
.note .link-bannerNote, BannerNote.tsx
.ui-tabs .segTabs, SegmentedTabs.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-pairField, Input, Select, Textarea, SearchInput, InputGroup, PasswordInput, Check, Choice, Switch, FileDrop, ImagePicker, Filters, ActiveFilters, FormGrid, SaveBar, ColorField, contrastRatio, useDebounced, useUnsavedGuardForm.tsx
.table .stack-sm-rows .pager td.sel .bulk-bar .load-moreDataTable (loading, error, selection), LoadMoreDataTable.tsx
.row-menu .ui-pop · .whoRowMenu, Popover, useDismiss · AccountMenuMenu.tsx
.ui-dl .cbox · .ui-timeline .ui-stepperDescList, CompareRow · Timeline, Stepper, WizardStepsDescList.tsx · Timeline.tsx
dialog.modal [data-blocking]Modal (blocking)Modal.tsx
.empty-state .toast .toast-desc .toast-action · confirm dialogEmptyState (preset), ErrorBoundary, PageLoading, ToastProvider/useToast (message atau options), ConfirmDialog/useConfirm (onConfirm)Feedback.tsx
pre.code .copy-block .copy-btnCodeBlock, CopyButton, SecretReveal, copyTextCode.tsx
[data-skeleton] [data-skeleton-bar] [data-skeleton-text]SkeletonSkeleton.tsx
.symbol .domain-iconAvatar (photo), DomainIconAvatar.tsx
.sidebar .nav-section .topbar .logo-sm .top-actions .page-head .page-back .work-rowAppShell, AccountMenu, PageHead (back), WorkRowShell.tsx
.brand-panel .brand-glassBrandPanelMarketing.tsx
.mid-btn .mid-btn-lgMidButtonButton.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-btnWelcomeDialog, Tour, ContextTip, GettingStarted, ProgressRing, ChecklistLauncher, OnboardingHelp, PageGuide, GuideButtonOnboarding.tsx
.stack .spread .ui-inline .grid-* .richStack, Spread, Inline, Grid, RichLayout.tsx
link app (a)LinkProvider, AppLinkLink.tsx
select2 · flatpickr (adapter)Select, DateInputadapters/react.tsx
format id-IDfmtDate, fmtDateTime, fmtTime, fmtNumber, fmtPercent, fmtRelativeformat.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 oleh showModal) tanpa outline; control di dalamnya tetap punya focus ring.
  • Form validation mengisi aria-invalid dan aria-describedby; field invalid pertama di-focus.
  • Tabs mendukung arrow keys/Home/End. Dialog melakukan focus trap. IconButton wajib punya label.
  • data-blocking mengandalkan event cancel; Chrome tetap menutup dialog dengan Esc bila belum ada interaksi sejak dialog dibuka.
  • Touch target ≥ 40px; prefers-reduced-motion dihormati (loading bar dan skeleton jadi statis).
  • Skeleton: container [data-skeleton] diberi inert (tidak bisa di-klik, di-focus atau dibaca), dan label role="status" ditaruh di luarnya supaya screen reader tetap mengumumkan loading.
  • Filter aktif: .chip-x berlabel "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". ColorField memperingatkan 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 di https://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, seperti LabmuUI.confirm, bukan ke Batal; WelcomeDialog memberi 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, React ActiveFilters; SearchInput onSearch (debounce delay, Enter langsung) dan hook useDebounced.
  • Baru: DataTable loading (skeleton dengan kolom yang sama), error + onRetry, selection (kolom th.sel/td.sel) dan bulkActions (.bulk-bar); LoadMore (.load-more) untuk list berbasis cursor, "X atau lebih" bila total belum diketahui.
  • Baru: EmptyState preset (empty, no-results, error, no-access), ErrorBoundary, PageLoading (busy, saving); dua button di .empty-state berjarak otomatis.
  • Baru: pre.code, .copy-block, .copy-btn, [data-copy] di labmu-ui.js; React CodeBlock, CopyButton, SecretReveal, copyText.
  • Baru: .form-grid (.three, .full), .form-foot, .save-bar yang menempel di bawah layar, form[data-ui-guard] + LabmuUI.guard; React FormGrid, SaveBar, useUnsavedGuard.
  • Baru: .color-pair dan React ColorField (peringatan kontras WCAG di bawah 4,5:1, contrastRatio, HEX); PasswordInput dengan Tampilkan/Sembunyikan.
  • Baru: .page-back dan PageHead back; .rich kini menata code, pre, blockquote, table (di-scroll bila lebar) dan hr.
  • 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 brand AppShell, ButtonLink, TextLink, WorkRow, Tabs mode link) memakai Link router (React Router, TanStack Router); URL luar, target dan download tetap <a>. AppShell renderLink menerima NavLinkProps (termasuk title di sidebar ciut).
  • Baru React: di ponsel, klik link apa pun di sidebar (juga lewat renderLink) dan Esc menutup drawer AppShell. Judul NavItem.section hanya tampil saat section berganti, jadi aman diisi di setiap item group. Tabs mode link (href per item). Input, Select, Textarea, IconButton meneruskan ref. ImagePicker crop dan cropSize (adapter Cropper.js).
  • Baru: popover umum .ui-pop (HTML data-ui-menu, rata kiri dengan data-align="start"); React Popover dan hook useDismiss (Esc dan klik di luar).
  • Baru: toast dengan description dan satu action (.toast-desc, .toast-action): LabmuUI.toast({ message, description, tone, action }), React useToast()({ … }); toast dengan action tidak hilang sendiri. ConfirmDialog / useConfirm: cancel, body, dan onConfirm (button OK berputar sampai aksi selesai; gagal = dialog tetap terbuka).
  • Baru: .wordmark untuk 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) berisi Select (select2) dan DateInput (flatpickr) untuk app React; adapter flatpickr kini menyediakan flatpickrField(el).

2.0.1 · 8 November 2026

  • Fix: icon di dalam button (.btn, .btn-icon, .btn-link) di .card-header dan .empty-state kini ikut warna dan ukuran button-nya, tidak lagi abu-abu atau 30px.
  • Fix: fieldset dan legend tanpa border dan padding bawaan browser; fieldset.field dengan legend tampil seperti field berlabel.
  • Fix: .page-head tanpa margin bawah di dalam .stack, p kosong tidak memakan tempat, dan .head-actions tetap di kanan saat turun ke baris baru. .tw-wrap kini position:relative.
  • Fix React: Modal memakai id judul unik (dua modal tidak lagi berbagi id); DataTable onRowClick tidak jalan saat klik button, link, input atau RowMenu di dalam cell; klik aksi di AccountMenu menutup 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-text dan React ContextButton dihapus. Context (organisasi dan peran) kini masuk profil: .who-ctx berisi context di strong (atas) dan nama di small (bawah); AccountMenu menampilkan context di atas nama, tanpa context hanya nama.
  • Breaking: AppShell prop topbar diganti account (profil, kiri) dan actions (dibungkus .top-actions, kanan); version kini wajib (string, mis. "2.3.2") dan footer selalu dirender; prop opsional logoSm (default brandMini).
  • Breaking: .top-actions kini margin-left:auto, .topbar .search-input tidak lagi; taruh search di dalam .top-actions. Card .who selalu terbuka ke kanan (aturan .who:last-child dihapus).

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 lewat opacity dari 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-text di 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 lewat data-skeleton-bar dan data-skeleton-text; React Skeleton (label) memasang inert dan status label. CSS saja, tanpa perubahan labmu-ui.js.

1.4.0 · 4 November 2026

  • Fix: .ui-file kini 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], React FileDrop membuatnya sendiri. FileDrop dapat prop name, required, disabled.
  • Baru: image picker .ui-image (frame dengan --ratio) dan .ui-image.compact; HTML lewat [data-ui-image], React ImagePicker.
  • 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, DataTable dengan stackOnPhone, onRowClick dan kolom align: "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 untuk LabmuUI.toast dan ToastProvider.
  • Fix: close button toast di dalam LabmuUI.confirm tidak lagi dibaca sebagai Batal.
  • React: ToastProvider memakai createPortal, jadi react-dom kini 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/*.png dihapus; ganti referensinya ke .svg dengan nama yang sama.
  • Background: assets/backgrounds/<produk>-bg.svg, token --brand-bg, --brand-gradient, --brand-bg-shade, class .brand-panel dan .brand-glass. .closing kini memakai background brand.
  • On-boarding: css/onboarding.css, react/Onboarding.tsx, LabmuUI.tour, LabmuUI.tip, LabmuUI.onboardingState, LabmuUI.placePopover, dan examples/onboarding.html.
  • Login: semua tombol masuk memakai Masuk dengan M-ID (.mid-btn, .mid-btn-lg, MidButton, token --mid dan --mid-hover, assets/brand/mid-mark.svg).
  • Footer: .footer-logos untuk 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 file assets/brand/paymu-logo-powered.svg dan paymu-logo-powered-negative.svg.
  • Base: [hidden]{display:none!important}.
  • Fix: button data-ui-brand di 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.symbol cover, a.chip + .tags, .pager dengan link, [hidden] selalu menang, .nav-section jadi 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.