@abydahana/aksara-ui 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/aksara.css CHANGED
@@ -858,7 +858,7 @@ img,svg,video {
858
858
  }
859
859
  .list-group-item {
860
860
  display:block;
861
- padding:.5rem 1rem;
861
+ padding:1rem;
862
862
  border-block-end:1px solid var(--aksara-border-subtle);
863
863
  color:var(--aksara-text-body);
864
864
  text-decoration:none;
@@ -3370,6 +3370,35 @@ img,svg,video {
3370
3370
  min-width:0;
3371
3371
  overflow:auto
3372
3372
  }
3373
+ .settings-group {
3374
+ border-radius:var(--aksara-radius-md);
3375
+ overflow:hidden;
3376
+ border:1px solid var(--aksara-border-subtle)
3377
+ }
3378
+ .settings-group .list-group-item {
3379
+ border-inline:0;
3380
+ border-radius:0;
3381
+ padding:.875rem 1rem
3382
+ }
3383
+ .settings-group .list-group-item:first-child {
3384
+ border-top:0
3385
+ }
3386
+ .settings-group .list-group-item:last-child {
3387
+ border-bottom:0
3388
+ }
3389
+ .settings-group .list-group-item-action:hover,.settings-group .list-group-item-action:focus {
3390
+ background:var(--aksara-bg-subtle)
3391
+ }
3392
+ .settings-item-icon {
3393
+ display:inline-flex;
3394
+ align-items:center;
3395
+ justify-content:center;
3396
+ width:2.25rem;
3397
+ height:2.25rem;
3398
+ border-radius:var(--aksara-radius-sm);
3399
+ font-size:1.2rem;
3400
+ flex-shrink:0
3401
+ }
3373
3402
  .m-0 {
3374
3403
  margin:0 !important
3375
3404
  }
package/dist/aksara.d.ts CHANGED
@@ -286,3 +286,156 @@ declare global {
286
286
  }
287
287
  }
288
288
  export default Aksara;
289
+ export interface SidebarNavItemProps {
290
+ /** Unique ID (used for tab panel targeting) */
291
+ id?: string;
292
+ /** Label text */
293
+ label: string;
294
+ /** Icon HTML string */
295
+ icon?: string;
296
+ /** Badge text or count */
297
+ badge?: string;
298
+ /** Active state */
299
+ active?: boolean;
300
+ /** Disabled state */
301
+ disabled?: boolean;
302
+ /** Link mode: render as <a href="..."> */
303
+ href?: string;
304
+ /** Tab mode: render as <button data-tabs="#panelId"> */
305
+ panelTarget?: string;
306
+ className?: string;
307
+ attributes?: Record<string, unknown>;
308
+ }
309
+ export interface SidebarNavProps {
310
+ items: SidebarNavItemProps[];
311
+ /** Brand section title at the top */
312
+ title?: string;
313
+ /** Brand section subtitle */
314
+ subtitle?: string;
315
+ /** Brand icon HTML */
316
+ brandIcon?: string;
317
+ className?: string;
318
+ attributes?: Record<string, unknown>;
319
+ }
320
+ /**
321
+ * Sidebar navigation that supports two modes:
322
+ *
323
+ * - **Tab mode** (`panelTarget` prop): renders `<button data-tabs="#panelId">` —
324
+ * integrates with Aksara's tab JS to show/hide content panels.
325
+ * - **Link mode** (`href` prop): renders `<a href="...">` — plain navigation links.
326
+ *
327
+ * @example Tab mode
328
+ * SidebarNav({
329
+ * title: "Settings",
330
+ * items: [
331
+ * { label: "Privacy", icon: '<span class="mdi mdi-lock-outline">', panelTarget: "#panel-privacy", active: true },
332
+ * { label: "Notifications", icon: '<span class="mdi mdi-bell-outline">', panelTarget: "#panel-notif" },
333
+ * ]
334
+ * })
335
+ *
336
+ * @example Link mode
337
+ * SidebarNav({
338
+ * items: [
339
+ * { label: "Dashboard", icon: '<span class="mdi mdi-home-outline">', href: "/dashboard", active: true },
340
+ * { label: "Settings", icon: '<span class="mdi mdi-cog-outline">', href: "/settings" },
341
+ * ]
342
+ * })
343
+ */
344
+ export declare function SidebarNav(props: SidebarNavProps): string;
345
+ export declare const sidebarNav: typeof SidebarNav;
346
+ export interface SidebarNavHeaderProps {
347
+ label: string;
348
+ className?: string;
349
+ }
350
+ /** A group header label inside a SidebarNav */
351
+ export declare function SidebarNavHeader(props: SidebarNavHeaderProps): string;
352
+ export declare const sidebarNavHeader: typeof SidebarNavHeader;
353
+ export interface SidebarLayoutProps {
354
+ sidebar: string;
355
+ content: string;
356
+ className?: string;
357
+ attributes?: Record<string, unknown>;
358
+ }
359
+ /**
360
+ * Two-column layout wrapper: sidebar pane on the left, content area on the right.
361
+ *
362
+ * @example
363
+ * SidebarLayout({
364
+ * sidebar: SidebarNav({ ... }),
365
+ * content: '<div class="p-5">Panel content here</div>'
366
+ * })
367
+ */
368
+ export declare function SidebarLayout(props: SidebarLayoutProps): string;
369
+ export declare const sidebarLayout: typeof SidebarLayout;
370
+ export interface ListGroupProps {
371
+ children?: string;
372
+ flush?: boolean;
373
+ numbered?: boolean;
374
+ horizontal?: boolean | "sm" | "md" | "lg" | "xl" | "xxl";
375
+ className?: string;
376
+ attributes?: Record<string, unknown>;
377
+ }
378
+ export declare function ListGroup(props?: ListGroupProps): string;
379
+ export declare const listGroup: typeof ListGroup;
380
+ export interface ListGroupItemProps {
381
+ content?: string;
382
+ active?: boolean;
383
+ disabled?: boolean;
384
+ action?: boolean;
385
+ href?: string;
386
+ variant?: "primary" | "secondary" | "success" | "danger" | "warning" | "info" | "light" | "dark";
387
+ className?: string;
388
+ attributes?: Record<string, unknown>;
389
+ }
390
+ export declare function ListGroupItem(props?: ListGroupItemProps): string;
391
+ export declare const listGroupItem: typeof ListGroupItem;
392
+ export interface SettingsGroupProps {
393
+ children?: string;
394
+ /** Optional uppercase heading rendered above the group */
395
+ title?: string;
396
+ className?: string;
397
+ attributes?: Record<string, unknown>;
398
+ }
399
+ /**
400
+ * Container for a group of settings-style rows.
401
+ * Renders as `.list-group` with an optional uppercase section heading above.
402
+ */
403
+ export declare function SettingsGroup(props?: SettingsGroupProps): string;
404
+ export declare const settingsGroup: typeof SettingsGroup;
405
+ export interface SettingsItemProps {
406
+ /** Main label text */
407
+ label: string;
408
+ /** Optional supporting description rendered below the label */
409
+ description?: string;
410
+ /** Icon HTML string, e.g. `<span class="mdi mdi-lock-outline">` */
411
+ icon?: string;
412
+ /** Value text shown on the right */
413
+ value?: string;
414
+ /** Arbitrary trailing HTML on the right, e.g. a switch or select */
415
+ trailing?: string;
416
+ /** Renders the item as `<a href>` */
417
+ href?: string;
418
+ /** Renders the item as `<button>` (when no href) */
419
+ action?: boolean;
420
+ /** Shows a `›` chevron on the trailing edge */
421
+ showChevron?: boolean;
422
+ active?: boolean;
423
+ disabled?: boolean;
424
+ className?: string;
425
+ attributes?: Record<string, unknown>;
426
+ }
427
+ /**
428
+ * A settings-style row using `.list-group-item` with an inner flex layout.
429
+ * Composable as a link, button, or static element.
430
+ *
431
+ * @example Link row
432
+ * SettingsItem({ label: "Private profile", icon: '<span class="mdi mdi-lock-outline">', value: "Public", showChevron: true, href: "#privacy" })
433
+ *
434
+ * @example Toggle row
435
+ * SettingsItem({ label: "Floods & Inundations", icon: '<span class="mdi mdi-water-alert">', trailing: '<input type="checkbox" role="switch" class="form-check-input" checked />' })
436
+ *
437
+ * @example Description row
438
+ * SettingsItem({ label: "Verified reports only", description: "Only display verified observations.", trailing: '<input type="checkbox" role="switch" class="form-check-input" />' })
439
+ */
440
+ export declare function SettingsItem(props: SettingsItemProps): string;
441
+ export declare const settingsItem: typeof SettingsItem;
@@ -1737,3 +1737,194 @@ if (typeof document !== "undefined") {
1737
1737
  document.addEventListener("DOMContentLoaded", () => Aksara.init());
1738
1738
  }
1739
1739
  export default Aksara;
1740
+ // ── HTML Component Generators ─────────────────────────────────────────────────
1741
+ // Inline pure-function HTML string generators.
1742
+ // These accept typed props and return HTML strings ready for server-side rendering.
1743
+ // ── Shared utilities ──────────────────────────────────────────────────────────
1744
+ function classNames(...classes) {
1745
+ return classes.filter(Boolean).join(" ").trim();
1746
+ }
1747
+ function toAttributes(attributes) {
1748
+ if (!attributes)
1749
+ return "";
1750
+ const parts = [];
1751
+ for (const [key, value] of Object.entries(attributes)) {
1752
+ if (value === true) {
1753
+ parts.push(key);
1754
+ }
1755
+ else if (value !== false && value !== null && value !== undefined) {
1756
+ parts.push(`${key}="${escapeHtml(value)}"`);
1757
+ }
1758
+ }
1759
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
1760
+ }
1761
+ function escapeHtmlStr(value) {
1762
+ return String(value ?? "")
1763
+ .replace(/&/g, "&amp;")
1764
+ .replace(/</g, "&lt;")
1765
+ .replace(/>/g, "&gt;")
1766
+ .replace(/"/g, "&quot;")
1767
+ .replace(/'/g, "&#39;");
1768
+ }
1769
+ /**
1770
+ * Sidebar navigation that supports two modes:
1771
+ *
1772
+ * - **Tab mode** (`panelTarget` prop): renders `<button data-tabs="#panelId">` —
1773
+ * integrates with Aksara's tab JS to show/hide content panels.
1774
+ * - **Link mode** (`href` prop): renders `<a href="...">` — plain navigation links.
1775
+ *
1776
+ * @example Tab mode
1777
+ * SidebarNav({
1778
+ * title: "Settings",
1779
+ * items: [
1780
+ * { label: "Privacy", icon: '<span class="mdi mdi-lock-outline">', panelTarget: "#panel-privacy", active: true },
1781
+ * { label: "Notifications", icon: '<span class="mdi mdi-bell-outline">', panelTarget: "#panel-notif" },
1782
+ * ]
1783
+ * })
1784
+ *
1785
+ * @example Link mode
1786
+ * SidebarNav({
1787
+ * items: [
1788
+ * { label: "Dashboard", icon: '<span class="mdi mdi-home-outline">', href: "/dashboard", active: true },
1789
+ * { label: "Settings", icon: '<span class="mdi mdi-cog-outline">', href: "/settings" },
1790
+ * ]
1791
+ * })
1792
+ */
1793
+ export function SidebarNav(props) {
1794
+ const { items = [], title = "", subtitle = "", brandIcon = "", className = "", attributes } = props;
1795
+ const baseAttrs = toAttributes(attributes);
1796
+ // Brand section
1797
+ const brandHtml = title
1798
+ ? `<div class="sidebar-nav-brand">${brandIcon ? `<span class="sidebar-nav-icon">${brandIcon}</span>` : ""}<div><span class="sidebar-nav-brand-title">${escapeHtmlStr(title)}</span>${subtitle ? `<span class="sidebar-nav-brand-subtitle">${escapeHtmlStr(subtitle)}</span>` : ""}</div></div>`
1799
+ : "";
1800
+ // Items
1801
+ const itemsHtml = items
1802
+ .map((item) => {
1803
+ const isLink = Boolean(item.href);
1804
+ const isTab = Boolean(item.panelTarget) && !isLink;
1805
+ const Tag = isLink ? "a" : "button";
1806
+ const itemClasses = classNames("sidebar-nav-item", item.active && "active", item.disabled && "disabled", item.className);
1807
+ const itemAttrs = toAttributes({
1808
+ href: isLink ? item.href : undefined,
1809
+ type: !isLink ? "button" : undefined,
1810
+ "data-tabs": isTab ? item.panelTarget : undefined,
1811
+ role: isTab ? "tab" : undefined,
1812
+ "aria-selected": isTab ? (item.active ? "true" : "false") : undefined,
1813
+ tabindex: isTab ? (item.active ? "0" : "-1") : undefined,
1814
+ id: isTab && item.id ? `sidebar-tab-${item.id}` : item.id,
1815
+ "aria-controls": isTab && item.id ? item.id : undefined,
1816
+ disabled: !isLink && item.disabled ? true : undefined,
1817
+ "aria-disabled": item.disabled ? "true" : undefined,
1818
+ "aria-current": isLink && item.active ? "page" : undefined,
1819
+ ...item.attributes
1820
+ });
1821
+ const iconHtml = item.icon ? `<span class="sidebar-nav-icon" aria-hidden="true">${item.icon}</span>` : "";
1822
+ const badgeHtml = item.badge ? `<span class="sidebar-nav-badge">${escapeHtmlStr(item.badge)}</span>` : "";
1823
+ return `<${Tag} class="${itemClasses}"${itemAttrs}>${iconHtml}<span class="sidebar-nav-label">${escapeHtmlStr(item.label)}</span>${badgeHtml}</${Tag}>`;
1824
+ })
1825
+ .join("");
1826
+ return `<nav class="${classNames("sidebar-nav", className)}"${baseAttrs}>${brandHtml}${itemsHtml}</nav>`.trim();
1827
+ }
1828
+ export const sidebarNav = SidebarNav;
1829
+ /** A group header label inside a SidebarNav */
1830
+ export function SidebarNavHeader(props) {
1831
+ const { label, className = "" } = props;
1832
+ return `<span class="${classNames("sidebar-nav-header", className)}">${escapeHtmlStr(label)}</span>`;
1833
+ }
1834
+ export const sidebarNavHeader = SidebarNavHeader;
1835
+ /**
1836
+ * Two-column layout wrapper: sidebar pane on the left, content area on the right.
1837
+ *
1838
+ * @example
1839
+ * SidebarLayout({
1840
+ * sidebar: SidebarNav({ ... }),
1841
+ * content: '<div class="p-5">Panel content here</div>'
1842
+ * })
1843
+ */
1844
+ export function SidebarLayout(props) {
1845
+ const { sidebar, content, className = "", attributes } = props;
1846
+ const baseAttrs = toAttributes(attributes);
1847
+ return `<div class="${classNames("sidebar-layout", className)}"${baseAttrs}><div class="sidebar-pane">${sidebar}</div><div class="sidebar-content">${content}</div></div>`.trim();
1848
+ }
1849
+ export const sidebarLayout = SidebarLayout;
1850
+ export function ListGroup(props = {}) {
1851
+ const { children = "", flush = false, numbered = false, horizontal = false, className = "", attributes } = props;
1852
+ const horizontalClass = horizontal === true
1853
+ ? "list-group-horizontal"
1854
+ : typeof horizontal === "string"
1855
+ ? `list-group-horizontal-${horizontal}`
1856
+ : null;
1857
+ const classes = classNames("list-group", flush && "list-group-flush", numbered && "list-group-numbered", horizontalClass, className);
1858
+ const Tag = numbered ? "ol" : "div";
1859
+ const baseAttrs = toAttributes(attributes);
1860
+ return `<${Tag} class="${classes}"${baseAttrs}>${children}</${Tag}>`.trim();
1861
+ }
1862
+ export const listGroup = ListGroup;
1863
+ export function ListGroupItem(props = {}) {
1864
+ const { content = "", active = false, disabled = false, action = false, href, variant, className = "", attributes } = props;
1865
+ const isLink = Boolean(href);
1866
+ const Tag = isLink ? "a" : action ? "button" : "div";
1867
+ const classes = classNames("list-group-item", (action || isLink) && "list-group-item-action", active && "active", disabled && "disabled", variant && `list-group-item-${variant}`, className);
1868
+ const baseAttrs = toAttributes({
1869
+ href,
1870
+ type: Tag === "button" ? "button" : undefined,
1871
+ disabled: Tag === "button" && disabled ? true : undefined,
1872
+ "aria-current": active ? "true" : undefined,
1873
+ "aria-disabled": disabled ? "true" : undefined,
1874
+ ...attributes
1875
+ });
1876
+ return `<${Tag} class="${classes}"${baseAttrs}>${content}</${Tag}>`.trim();
1877
+ }
1878
+ export const listGroupItem = ListGroupItem;
1879
+ /**
1880
+ * Container for a group of settings-style rows.
1881
+ * Renders as `.list-group` with an optional uppercase section heading above.
1882
+ */
1883
+ export function SettingsGroup(props = {}) {
1884
+ const { children = "", title = "", className = "", attributes } = props;
1885
+ const baseAttrs = toAttributes(attributes);
1886
+ const groupHtml = `<div class="${classNames("list-group", className)}"${baseAttrs}>${children}</div>`;
1887
+ if (!title)
1888
+ return groupHtml.trim();
1889
+ return `<div><p class="text-xs font-800 uppercase tracking-wider text-subtle mb-2 px-1">${escapeHtmlStr(title)}</p><div class="${classNames("list-group", className)}">${children}</div></div>`.trim();
1890
+ }
1891
+ export const settingsGroup = SettingsGroup;
1892
+ /**
1893
+ * A settings-style row using `.list-group-item` with an inner flex layout.
1894
+ * Composable as a link, button, or static element.
1895
+ *
1896
+ * @example Link row
1897
+ * SettingsItem({ label: "Private profile", icon: '<span class="mdi mdi-lock-outline">', value: "Public", showChevron: true, href: "#privacy" })
1898
+ *
1899
+ * @example Toggle row
1900
+ * SettingsItem({ label: "Floods & Inundations", icon: '<span class="mdi mdi-water-alert">', trailing: '<input type="checkbox" role="switch" class="form-check-input" checked />' })
1901
+ *
1902
+ * @example Description row
1903
+ * SettingsItem({ label: "Verified reports only", description: "Only display verified observations.", trailing: '<input type="checkbox" role="switch" class="form-check-input" />' })
1904
+ */
1905
+ export function SettingsItem(props) {
1906
+ const { label, description = "", icon = "", value = "", trailing = "", href, action = false, showChevron = false, active = false, disabled = false, className = "", attributes } = props;
1907
+ const isLink = Boolean(href);
1908
+ const Tag = isLink ? "a" : action ? "button" : "div";
1909
+ const classes = classNames("list-group-item", (action || isLink) && "list-group-item-action", active && "active", disabled && "disabled", className);
1910
+ const baseAttrs = toAttributes({
1911
+ href,
1912
+ type: Tag === "button" ? "button" : undefined,
1913
+ disabled: Tag === "button" && disabled ? true : undefined,
1914
+ "aria-current": active ? "true" : undefined,
1915
+ "aria-disabled": disabled ? "true" : undefined,
1916
+ ...attributes
1917
+ });
1918
+ const iconHtml = icon
1919
+ ? `<span class="flex items-center justify-center w-7 h-7 shrink-0 text-lg" aria-hidden="true">${icon}</span>`
1920
+ : "";
1921
+ const labelBlock = description
1922
+ ? `<span class="flex-1 min-w-0"><span class="font-600 block">${escapeHtmlStr(label)}</span><span class="text-subtle text-sm block mt-1">${escapeHtmlStr(description)}</span></span>`
1923
+ : `<span class="flex-1 font-600">${escapeHtmlStr(label)}</span>`;
1924
+ const valueHtml = value ? `<span class="text-subtle text-sm">${escapeHtmlStr(value)}</span>` : "";
1925
+ const chevronHtml = showChevron
1926
+ ? `<span class="text-subtle opacity-50 text-sm ms-1" aria-hidden="true">&#8250;</span>`
1927
+ : "";
1928
+ return `<${Tag} class="${classes}"${baseAttrs}><div class="flex items-center gap-3">${iconHtml}${labelBlock}${valueHtml}${trailing}${chevronHtml}</div></${Tag}>`.trim();
1929
+ }
1930
+ export const settingsItem = SettingsItem;
package/dist/aksara.js CHANGED
@@ -1738,6 +1738,197 @@ if (typeof document !== "undefined") {
1738
1738
  document.addEventListener("DOMContentLoaded", () => Aksara.init());
1739
1739
  }
1740
1740
 
1741
+ // ── HTML Component Generators ─────────────────────────────────────────────────
1742
+ // Inline pure-function HTML string generators.
1743
+ // These accept typed props and return HTML strings ready for server-side rendering.
1744
+ // ── Shared utilities ──────────────────────────────────────────────────────────
1745
+ function classNames(...classes) {
1746
+ return classes.filter(Boolean).join(" ").trim();
1747
+ }
1748
+ function toAttributes(attributes) {
1749
+ if (!attributes)
1750
+ return "";
1751
+ const parts = [];
1752
+ for (const [key, value] of Object.entries(attributes)) {
1753
+ if (value === true) {
1754
+ parts.push(key);
1755
+ }
1756
+ else if (value !== false && value !== null && value !== undefined) {
1757
+ parts.push(`${key}="${escapeHtml(value)}"`);
1758
+ }
1759
+ }
1760
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
1761
+ }
1762
+ function escapeHtmlStr(value) {
1763
+ return String(value ?? "")
1764
+ .replace(/&/g, "&amp;")
1765
+ .replace(/</g, "&lt;")
1766
+ .replace(/>/g, "&gt;")
1767
+ .replace(/"/g, "&quot;")
1768
+ .replace(/'/g, "&#39;");
1769
+ }
1770
+ /**
1771
+ * Sidebar navigation that supports two modes:
1772
+ *
1773
+ * - **Tab mode** (`panelTarget` prop): renders `<button data-tabs="#panelId">` —
1774
+ * integrates with Aksara's tab JS to show/hide content panels.
1775
+ * - **Link mode** (`href` prop): renders `<a href="...">` — plain navigation links.
1776
+ *
1777
+ * @example Tab mode
1778
+ * SidebarNav({
1779
+ * title: "Settings",
1780
+ * items: [
1781
+ * { label: "Privacy", icon: '<span class="mdi mdi-lock-outline">', panelTarget: "#panel-privacy", active: true },
1782
+ * { label: "Notifications", icon: '<span class="mdi mdi-bell-outline">', panelTarget: "#panel-notif" },
1783
+ * ]
1784
+ * })
1785
+ *
1786
+ * @example Link mode
1787
+ * SidebarNav({
1788
+ * items: [
1789
+ * { label: "Dashboard", icon: '<span class="mdi mdi-home-outline">', href: "/dashboard", active: true },
1790
+ * { label: "Settings", icon: '<span class="mdi mdi-cog-outline">', href: "/settings" },
1791
+ * ]
1792
+ * })
1793
+ */
1794
+ function SidebarNav(props) {
1795
+ const { items = [], title = "", subtitle = "", brandIcon = "", className = "", attributes } = props;
1796
+ const baseAttrs = toAttributes(attributes);
1797
+ // Brand section
1798
+ const brandHtml = title
1799
+ ? `<div class="sidebar-nav-brand">${brandIcon ? `<span class="sidebar-nav-icon">${brandIcon}</span>` : ""}<div><span class="sidebar-nav-brand-title">${escapeHtmlStr(title)}</span>${subtitle ? `<span class="sidebar-nav-brand-subtitle">${escapeHtmlStr(subtitle)}</span>` : ""}</div></div>`
1800
+ : "";
1801
+ // Items
1802
+ const itemsHtml = items
1803
+ .map((item) => {
1804
+ const isLink = Boolean(item.href);
1805
+ const isTab = Boolean(item.panelTarget) && !isLink;
1806
+ const Tag = isLink ? "a" : "button";
1807
+ const itemClasses = classNames("sidebar-nav-item", item.active && "active", item.disabled && "disabled", item.className);
1808
+ const itemAttrs = toAttributes({
1809
+ href: isLink ? item.href : undefined,
1810
+ type: !isLink ? "button" : undefined,
1811
+ "data-tabs": isTab ? item.panelTarget : undefined,
1812
+ role: isTab ? "tab" : undefined,
1813
+ "aria-selected": isTab ? (item.active ? "true" : "false") : undefined,
1814
+ tabindex: isTab ? (item.active ? "0" : "-1") : undefined,
1815
+ id: isTab && item.id ? `sidebar-tab-${item.id}` : item.id,
1816
+ "aria-controls": isTab && item.id ? item.id : undefined,
1817
+ disabled: !isLink && item.disabled ? true : undefined,
1818
+ "aria-disabled": item.disabled ? "true" : undefined,
1819
+ "aria-current": isLink && item.active ? "page" : undefined,
1820
+ ...item.attributes
1821
+ });
1822
+ const iconHtml = item.icon ? `<span class="sidebar-nav-icon" aria-hidden="true">${item.icon}</span>` : "";
1823
+ const badgeHtml = item.badge ? `<span class="sidebar-nav-badge">${escapeHtmlStr(item.badge)}</span>` : "";
1824
+ return `<${Tag} class="${itemClasses}"${itemAttrs}>${iconHtml}<span class="sidebar-nav-label">${escapeHtmlStr(item.label)}</span>${badgeHtml}</${Tag}>`;
1825
+ })
1826
+ .join("");
1827
+ return `<nav class="${classNames("sidebar-nav", className)}"${baseAttrs}>${brandHtml}${itemsHtml}</nav>`.trim();
1828
+ }
1829
+ export const sidebarNav = SidebarNav;
1830
+ /** A group header label inside a SidebarNav */
1831
+ function SidebarNavHeader(props) {
1832
+ const { label, className = "" } = props;
1833
+ return `<span class="${classNames("sidebar-nav-header", className)}">${escapeHtmlStr(label)}</span>`;
1834
+ }
1835
+ export const sidebarNavHeader = SidebarNavHeader;
1836
+ /**
1837
+ * Two-column layout wrapper: sidebar pane on the left, content area on the right.
1838
+ *
1839
+ * @example
1840
+ * SidebarLayout({
1841
+ * sidebar: SidebarNav({ ... }),
1842
+ * content: '<div class="p-5">Panel content here</div>'
1843
+ * })
1844
+ */
1845
+ function SidebarLayout(props) {
1846
+ const { sidebar, content, className = "", attributes } = props;
1847
+ const baseAttrs = toAttributes(attributes);
1848
+ return `<div class="${classNames("sidebar-layout", className)}"${baseAttrs}><div class="sidebar-pane">${sidebar}</div><div class="sidebar-content">${content}</div></div>`.trim();
1849
+ }
1850
+ export const sidebarLayout = SidebarLayout;
1851
+ function ListGroup(props = {}) {
1852
+ const { children = "", flush = false, numbered = false, horizontal = false, className = "", attributes } = props;
1853
+ const horizontalClass = horizontal === true
1854
+ ? "list-group-horizontal"
1855
+ : typeof horizontal === "string"
1856
+ ? `list-group-horizontal-${horizontal}`
1857
+ : null;
1858
+ const classes = classNames("list-group", flush && "list-group-flush", numbered && "list-group-numbered", horizontalClass, className);
1859
+ const Tag = numbered ? "ol" : "div";
1860
+ const baseAttrs = toAttributes(attributes);
1861
+ return `<${Tag} class="${classes}"${baseAttrs}>${children}</${Tag}>`.trim();
1862
+ }
1863
+ export const listGroup = ListGroup;
1864
+ function ListGroupItem(props = {}) {
1865
+ const { content = "", active = false, disabled = false, action = false, href, variant, className = "", attributes } = props;
1866
+ const isLink = Boolean(href);
1867
+ const Tag = isLink ? "a" : action ? "button" : "div";
1868
+ const classes = classNames("list-group-item", (action || isLink) && "list-group-item-action", active && "active", disabled && "disabled", variant && `list-group-item-${variant}`, className);
1869
+ const baseAttrs = toAttributes({
1870
+ href,
1871
+ type: Tag === "button" ? "button" : undefined,
1872
+ disabled: Tag === "button" && disabled ? true : undefined,
1873
+ "aria-current": active ? "true" : undefined,
1874
+ "aria-disabled": disabled ? "true" : undefined,
1875
+ ...attributes
1876
+ });
1877
+ return `<${Tag} class="${classes}"${baseAttrs}>${content}</${Tag}>`.trim();
1878
+ }
1879
+ export const listGroupItem = ListGroupItem;
1880
+ /**
1881
+ * Container for a group of settings-style rows.
1882
+ * Renders as `.list-group` with an optional uppercase section heading above.
1883
+ */
1884
+ function SettingsGroup(props = {}) {
1885
+ const { children = "", title = "", className = "", attributes } = props;
1886
+ const baseAttrs = toAttributes(attributes);
1887
+ const groupHtml = `<div class="${classNames("list-group", className)}"${baseAttrs}>${children}</div>`;
1888
+ if (!title)
1889
+ return groupHtml.trim();
1890
+ return `<div><p class="text-xs font-800 uppercase tracking-wider text-subtle mb-2 px-1">${escapeHtmlStr(title)}</p><div class="${classNames("list-group", className)}">${children}</div></div>`.trim();
1891
+ }
1892
+ export const settingsGroup = SettingsGroup;
1893
+ /**
1894
+ * A settings-style row using `.list-group-item` with an inner flex layout.
1895
+ * Composable as a link, button, or static element.
1896
+ *
1897
+ * @example Link row
1898
+ * SettingsItem({ label: "Private profile", icon: '<span class="mdi mdi-lock-outline">', value: "Public", showChevron: true, href: "#privacy" })
1899
+ *
1900
+ * @example Toggle row
1901
+ * SettingsItem({ label: "Floods & Inundations", icon: '<span class="mdi mdi-water-alert">', trailing: '<input type="checkbox" role="switch" class="form-check-input" checked />' })
1902
+ *
1903
+ * @example Description row
1904
+ * SettingsItem({ label: "Verified reports only", description: "Only display verified observations.", trailing: '<input type="checkbox" role="switch" class="form-check-input" />' })
1905
+ */
1906
+ function SettingsItem(props) {
1907
+ const { label, description = "", icon = "", value = "", trailing = "", href, action = false, showChevron = false, active = false, disabled = false, className = "", attributes } = props;
1908
+ const isLink = Boolean(href);
1909
+ const Tag = isLink ? "a" : action ? "button" : "div";
1910
+ const classes = classNames("list-group-item", (action || isLink) && "list-group-item-action", active && "active", disabled && "disabled", className);
1911
+ const baseAttrs = toAttributes({
1912
+ href,
1913
+ type: Tag === "button" ? "button" : undefined,
1914
+ disabled: Tag === "button" && disabled ? true : undefined,
1915
+ "aria-current": active ? "true" : undefined,
1916
+ "aria-disabled": disabled ? "true" : undefined,
1917
+ ...attributes
1918
+ });
1919
+ const iconHtml = icon
1920
+ ? `<span class="flex items-center justify-center w-7 h-7 shrink-0 text-lg" aria-hidden="true">${icon}</span>`
1921
+ : "";
1922
+ const labelBlock = description
1923
+ ? `<span class="flex-1 min-w-0"><span class="font-600 block">${escapeHtmlStr(label)}</span><span class="text-subtle text-sm block mt-1">${escapeHtmlStr(description)}</span></span>`
1924
+ : `<span class="flex-1 font-600">${escapeHtmlStr(label)}</span>`;
1925
+ const valueHtml = value ? `<span class="text-subtle text-sm">${escapeHtmlStr(value)}</span>` : "";
1926
+ const chevronHtml = showChevron
1927
+ ? `<span class="text-subtle opacity-50 text-sm ms-1" aria-hidden="true">&#8250;</span>`
1928
+ : "";
1929
+ return `<${Tag} class="${classes}"${baseAttrs}><div class="flex items-center gap-3">${iconHtml}${labelBlock}${valueHtml}${trailing}${chevronHtml}</div></${Tag}>`.trim();
1930
+ }
1931
+ export const settingsItem = SettingsItem;
1741
1932
 
1742
1933
  window.Aksara=Aksara;
1743
1934
  })();