@syncedco/flow 0.1.4 → 0.3.0

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.
@@ -1024,8 +1024,8 @@ function getTokenSummary() {
1024
1024
  components: componentTokens,
1025
1025
  themePresets: presetNames.map(kebabThemeName),
1026
1026
  starterClasses: {
1027
- layout: ['sf-container', 'sf-container--narrow', 'sf-container--wide', 'sf-container--full', 'sf-rail-shell', 'sf-rail-shell__rail', 'sf-rail-shell__body', 'sf-rail-shell__mark', 'sf-rail-shell__label', 'sf-section', 'sf-section--compact', 'sf-section--spacious', 'sf-stack', 'sf-flow', 'sf-cluster', 'sf-repel', 'sf-toolbar', 'sf-app-shell', 'sf-app-sidebar', 'sf-app-sidebar__brand', 'sf-app-main', 'sf-auto-grid', 'sf-auto-grid--xs', 'sf-auto-grid--sm', 'sf-auto-grid--lg', 'sf-switcher', 'sf-sidebar', 'sf-split', 'sf-split--reverse', 'sf-chapter', 'sf-chapter__copy', 'sf-frame', 'sf-cover', 'sf-panel-grid', 'sf-metric-grid', 'sf-pipeline', 'sf-scroll-viewport', 'sf-scroll-panel', 'sf-scroll-snap-y', 'sf-sticky-top', 'sf-media-object', 'sf-aside-rail'],
1028
- components: ['sf-button', 'sf-button-group', 'sf-button--icon', 'sf-icon-button', 'sf-icon', 'sf-icon--xs', 'sf-icon--sm', 'sf-icon--md', 'sf-icon--lg', 'sf-icon--xl', 'sf-card', 'sf-card--flat', 'sf-card--raised', 'sf-card--compact', 'sf-card--glass', 'sf-card--technical', 'sf-card--accent', 'sf-card--center', 'sf-surface', 'sf-code-window', 'sf-code-window--glow', 'sf-code-window__bar', 'sf-window-dots', 'sf-code-lines', 'sf-code-lines--compact', 'sf-code-block', 'sf-token-strip', 'sf-marquee', 'sf-marquee__track', 'sf-marquee__item', 'sf-command-list', 'sf-command-list__prompt', 'sf-platform-card', 'sf-platform-card__logo', 'sf-hero', 'sf-logo-cloud', 'sf-feature', 'sf-stats', 'sf-stat', 'sf-testimonial', 'sf-pricing-grid', 'sf-price-card', 'sf-price', 'sf-faq', 'sf-cta', 'sf-footer', 'sf-badge', 'sf-avatar', 'sf-chart', 'sf-chart--wide', 'sf-chart__plot', 'sf-chart__svg', 'sf-chart__grid', 'sf-chart__axis', 'sf-chart__area', 'sf-chart__line', 'sf-chart__point', 'sf-chart__legend', 'sf-chart__legend-item', 'sf-chart__swatch', 'sf-meter-list', 'sf-meter-row', 'sf-meter', 'sf-nav', 'sf-nav--mobile', 'sf-nav__list', 'sf-nav__link', 'sf-menu', 'sf-breadcrumb', 'sf-pagination', 'sf-dialog', 'sf-popover', 'sf-tooltip', 'sf-tooltip-trigger', 'sf-menu-popover', 'sf-toast', 'sf-banner', 'sf-drawer', 'sf-drawer--stack', 'sf-disclosure', 'sf-accordion', 'sf-tabs', 'sf-tab-list', 'sf-tab', 'sf-tab-panel', 'sf-form', 'sf-fieldset', 'sf-field', 'sf-label', 'sf-help', 'sf-error', 'sf-input', 'sf-select', 'sf-textarea', 'sf-check', 'sf-alert', 'sf-section-header', 'sf-kicker'],
1027
+ layout: ['sf-container', 'sf-container--narrow', 'sf-container--wide', 'sf-container--full', 'sf-rail-shell', 'sf-rail-shell__rail', 'sf-rail-shell__body', 'sf-rail-shell__mark', 'sf-rail-shell__label', 'sf-section', 'sf-section--compact', 'sf-section--spacious', 'sf-stack', 'sf-flow', 'sf-cluster', 'sf-repel', 'sf-toolbar', 'sf-app-shell', 'sf-app-sidebar', 'sf-app-sidebar__brand', 'sf-app-header', 'sf-app-backdrop', 'sf-app-main', 'sf-auto-grid', 'sf-auto-grid--xs', 'sf-auto-grid--sm', 'sf-auto-grid--lg', 'sf-switcher', 'sf-sidebar', 'sf-split', 'sf-split--reverse', 'sf-chapter', 'sf-chapter__copy', 'sf-frame', 'sf-cover', 'sf-panel-grid', 'sf-metric-grid', 'sf-pipeline', 'sf-scroll-viewport', 'sf-scroll-panel', 'sf-scroll-snap-y', 'sf-sticky-top', 'sf-media-object', 'sf-aside-rail'],
1028
+ components: ['sf-button', 'sf-button-group', 'sf-button--icon', 'sf-icon-button', 'sf-icon', 'sf-icon--xs', 'sf-icon--sm', 'sf-icon--md', 'sf-icon--lg', 'sf-icon--xl', 'sf-card', 'sf-card--flat', 'sf-card--raised', 'sf-card--compact', 'sf-card--glass', 'sf-card--technical', 'sf-card--accent', 'sf-card--center', 'sf-surface', 'sf-code-window', 'sf-code-window--glow', 'sf-code-window__bar', 'sf-window-dots', 'sf-code-lines', 'sf-code-lines--compact', 'sf-code-block', 'sf-token-strip', 'sf-marquee', 'sf-marquee__track', 'sf-marquee__item', 'sf-command-list', 'sf-command-list__prompt', 'sf-platform-card', 'sf-platform-card__logo', 'sf-hero', 'sf-logo-cloud', 'sf-feature', 'sf-stats', 'sf-stat', 'sf-testimonial', 'sf-pricing-grid', 'sf-price-card', 'sf-price', 'sf-faq', 'sf-cta', 'sf-footer', 'sf-badge', 'sf-avatar', 'sf-chart', 'sf-chart--wide', 'sf-chart__plot', 'sf-chart__svg', 'sf-chart__grid', 'sf-chart__axis', 'sf-chart__area', 'sf-chart__line', 'sf-chart__point', 'sf-chart__legend', 'sf-chart__legend-item', 'sf-chart__swatch', 'sf-meter-list', 'sf-meter-row', 'sf-meter', 'sf-progress', 'sf-skeleton', 'sf-filter-bar', 'sf-search', 'sf-data-table', 'sf-table-sort', 'sf-status', 'sf-data-list', 'sf-empty-state', 'sf-settings-section', 'sf-detail-panel', 'sf-nav', 'sf-nav--mobile', 'sf-nav__list', 'sf-nav__link', 'sf-menu', 'sf-breadcrumb', 'sf-pagination', 'sf-dialog', 'sf-popover', 'sf-tooltip', 'sf-tooltip-trigger', 'sf-menu-popover', 'sf-toast', 'sf-toast-stack', 'sf-banner', 'sf-drawer', 'sf-drawer--stack', 'sf-disclosure', 'sf-accordion', 'sf-tabs', 'sf-tab-list', 'sf-tab', 'sf-tab__count', 'sf-tab-panel', 'sf-form', 'sf-fieldset', 'sf-field', 'sf-label', 'sf-help', 'sf-error', 'sf-input', 'sf-select', 'sf-textarea', 'sf-check', 'sf-alert', 'sf-section-header', 'sf-kicker'],
1029
1029
  type: ['sf-text-caption', 'sf-text-body', 'sf-text-lead', 'sf-text-h4', 'sf-text-h3', 'sf-text-h2', 'sf-text-h1', 'sf-text-display'],
1030
1030
  utilities: ['sr-only', 'not-sr-only', 'sf-visually-hidden', 'sf-not-visually-hidden', 'sf-skip-link', 'sf-focus-ring', 'sf-touch-target', 'sf-list-reset', 'sf-list-disc', 'sf-list-decimal', 'sf-link', 'sf-link-subtle', 'sf-link-plain', 'sf-prose', 'sf-prose--blog', 'sf-prose--legal', 'sf-meta', 'sf-figure', 'sf-caption', 'sf-table-wrap', 'sf-push-block-end', 'sf-push-block-start', 'sf-push-inline-end', 'sf-push-inline-start', 'sf-full-bleed', 'sf-text-muted', 'sf-bg-surface', 'sf-border', 'sf-rounded-panel', 'sf-shadow-md', 'sf-animate-fade', 'sf-animate-rise', 'sf-animate-scale', 'sf-animate-slide', 'sf-animate-stagger'],
1031
1031
  },
@@ -1239,6 +1239,260 @@ export default function DialogExample() {
1239
1239
  markup: '<article class="sf-container sf-prose sf-prose--blog">...</article>',
1240
1240
  keywords: ['blog', 'article', 'prose', 'legal', 'docs', 'case-study', 'table', 'figure', 'seo'],
1241
1241
  },
1242
+ {
1243
+ id: 'app-shell-layout',
1244
+ name: 'App shell layout',
1245
+ whenToUse: 'Product apps, portals, admin tools, and dashboards that need a header, sidebar, main workspace, optional collapsed sidebar, and mobile drawer mode.',
1246
+ classes: ['sf-app-shell', 'sf-app-header', 'sf-app-sidebar', 'sf-app-backdrop', 'sf-app-main', 'sf-nav', 'sf-nav__link'],
1247
+ requiresJs: false,
1248
+ requiresJsNotes: 'CSS handles layout states. The consuming app owns toggling data-collapsed and data-sidebar-open attributes.',
1249
+ a11y: ['Keep the sidebar inside a named nav or aside.', 'Use a real button to toggle drawer/collapsed states.', 'Move focus into the drawer when app JavaScript opens it.'],
1250
+ gotchas: ['Use data-mobile="drawer" only for app layouts that need overlay navigation on small screens.', 'Keep data-hide-collapsed text available through aria-labels on icon-only links.'],
1251
+ markup: {
1252
+ html: `<div class="sf-app-shell" data-layout="fixed-header" data-mobile="drawer">
1253
+ <header class="sf-app-header"><button class="sf-button sf-button--ghost" type="button" aria-label="Open navigation">Menu</button><strong>Workspace</strong></header>
1254
+ <aside class="sf-app-sidebar" aria-label="Workspace navigation"><nav class="sf-nav"><a class="sf-nav__link" href="/app" aria-current="page">Dashboard</a></nav></aside>
1255
+ <button class="sf-app-backdrop" type="button" aria-label="Close navigation"></button>
1256
+ <main class="sf-app-main">...</main>
1257
+ </div>`,
1258
+ },
1259
+ keywords: ['app', 'shell', 'layout', 'sidebar', 'header', 'drawer', 'collapsed', 'dashboard', 'admin', 'portal'],
1260
+ },
1261
+ {
1262
+ id: 'row-action-menu',
1263
+ name: 'Row action menu',
1264
+ whenToUse: 'Kebab, overflow, and per-row action menus using native popover markup and destructive item styling.',
1265
+ classes: ['sf-menu-popover', 'sf-menu', 'sf-button', 'sf-button--ghost', 'sf-button--icon'],
1266
+ requiresJs: false,
1267
+ requiresJsNotes: 'Native popover handles open, backdrop dismiss, and light-dismiss where supported. App JavaScript may add keyboard roving for complex menus.',
1268
+ a11y: ['Use a button trigger with an accessible label.', 'Keep destructive actions visibly labelled.', 'Use links for navigation and buttons for actions.'],
1269
+ gotchas: ['Use popovertarget on buttons, not anchors.', 'The app owns confirmation flows for destructive actions.'],
1270
+ markup: {
1271
+ html: `<button class="sf-button sf-button--ghost sf-button--icon" type="button" popovertarget="row-actions-1" aria-label="Open row actions">...</button>
1272
+ <div class="sf-menu-popover" id="row-actions-1" popover="auto">
1273
+ <nav class="sf-menu" aria-label="Row actions">
1274
+ <a href="/items/1">Open</a>
1275
+ <button type="button">Archive</button>
1276
+ <button type="button" data-variant="danger">Delete</button>
1277
+ </nav>
1278
+ </div>`,
1279
+ },
1280
+ keywords: ['menu', 'row', 'actions', 'kebab', 'popover', 'destructive', 'table'],
1281
+ },
1282
+ {
1283
+ id: 'tabs-with-counts',
1284
+ name: 'Tabs with counts',
1285
+ whenToUse: 'Filtered app views, inboxes, queues, and reports where each tab needs a compact count.',
1286
+ classes: ['sf-tabs', 'sf-tab-list', 'sf-tab', 'sf-tab__count', 'sf-tab-panel'],
1287
+ requiresJs: false,
1288
+ requiresJsNotes: 'CSS styles state. The app owns aria-selected, routing, or tab panel switching for dynamic interfaces.',
1289
+ a11y: ['Use role="tablist" only when implementing full keyboard tab behavior.', 'Use aria-selected on active tabs.', 'Keep counts textual and not colour-only.'],
1290
+ gotchas: ['Use data-variant="pill" for pill tabs.', 'Use links for route-backed tabs and buttons for in-page tab switching.'],
1291
+ markup: {
1292
+ html: `<nav class="sf-tab-list" aria-label="Views">
1293
+ <a class="sf-tab" data-variant="pill" href="?view=open" aria-selected="true">Open <span class="sf-tab__count">12</span></a>
1294
+ <a class="sf-tab" data-variant="pill" href="?view=closed">Closed <span class="sf-tab__count">8</span></a>
1295
+ </nav>`,
1296
+ },
1297
+ keywords: ['tabs', 'counts', 'queue', 'views', 'filters', 'pill'],
1298
+ },
1299
+ {
1300
+ id: 'data-table-actions',
1301
+ name: 'Data table actions',
1302
+ whenToUse: 'Operational tables that need sortable header controls, clickable rows, and a responsive card view fallback.',
1303
+ classes: ['sf-table-wrap', 'sf-data-table', 'sf-table-sort', 'sf-status', 'sf-link'],
1304
+ requiresJs: false,
1305
+ requiresJsNotes: 'Sorting and row navigation are app behavior. Synced Flow only styles controls, focus state, and responsive card layout.',
1306
+ a11y: ['Use aria-sort on sorted columns.', 'Keep the row destination as a real link.', 'Add data-label to cells when using card fallback.'],
1307
+ gotchas: ['Set data-view="cards" on sf-table-wrap for the responsive card fallback.', 'Do not put interactive controls inside a stretched row link.'],
1308
+ markup: {
1309
+ html: `<div class="sf-table-wrap" data-view="cards">
1310
+ <table class="sf-data-table">
1311
+ <thead><tr><th><button class="sf-table-sort" type="button" aria-sort="ascending">Name</button></th><th>Status</th></tr></thead>
1312
+ <tbody><tr data-clickable="true"><td data-label="Name"><a class="sf-link" href="/items/1">Northstar</a></td><td data-label="Status"><span class="sf-status" data-size="sm" data-tone="success">Active</span></td></tr></tbody>
1313
+ </table>
1314
+ </div>`,
1315
+ },
1316
+ keywords: ['table', 'sort', 'sortable', 'responsive', 'card', 'clickable', 'data'],
1317
+ },
1318
+ {
1319
+ id: 'search-field',
1320
+ name: 'Search field',
1321
+ whenToUse: 'Search inputs in filters, headers, command surfaces, and dense app interfaces.',
1322
+ classes: ['sf-search', 'sf-field', 'sf-label', 'sf-input'],
1323
+ requiresJs: false,
1324
+ requiresJsNotes: 'Native form submit works without JavaScript. Debounce, suggestions, and command logic belong to the app.',
1325
+ a11y: ['Keep a label available, visible or visually hidden.', 'Use type="search".', 'Do not use placeholder text as the only label.'],
1326
+ gotchas: ['Use sf-field data-density="compact" for dense interfaces.', 'Autocomplete and combobox behavior is app-owned.'],
1327
+ markup: {
1328
+ html: `<label class="sf-field sf-search" data-density="compact">
1329
+ <span class="sf-label">Search</span>
1330
+ <input class="sf-input" type="search" name="q" placeholder="Search records">
1331
+ </label>`,
1332
+ },
1333
+ keywords: ['search', 'input', 'filter', 'command', 'dense'],
1334
+ },
1335
+ {
1336
+ id: 'modal-form',
1337
+ name: 'Modal form',
1338
+ whenToUse: 'Dialog-based create, edit, confirmation, and settings flows with form body and footer actions.',
1339
+ classes: ['sf-dialog', 'sf-dialog__header', 'sf-dialog__body', 'sf-dialog__footer', 'sf-form', 'sf-field', 'sf-input', 'sf-button'],
1340
+ requiresJs: true,
1341
+ requiresJsNotes: 'Native dialog provides modal behavior. Broad support still needs a tiny app handler for showModal() until commandfor is universal.',
1342
+ a11y: ['Use a visible dialog heading.', 'Keep footer actions inside the form or associated with form attribute.', 'Return focus to the trigger after closing.'],
1343
+ gotchas: ['Use data-layout="form" for form spacing.', 'Validation and persistence belong to the app.'],
1344
+ markup: {
1345
+ html: `<dialog class="sf-dialog" data-layout="form" id="edit-dialog" aria-labelledby="edit-title">
1346
+ <header class="sf-dialog__header"><h2 id="edit-title">Edit record</h2></header>
1347
+ <form class="sf-form sf-dialog__body" id="edit-form"><label class="sf-field"><span class="sf-label">Name</span><input class="sf-input" name="name"></label></form>
1348
+ <footer class="sf-dialog__footer"><button class="sf-button sf-button--outline" formmethod="dialog">Cancel</button><button class="sf-button" form="edit-form">Save</button></footer>
1349
+ </dialog>`,
1350
+ },
1351
+ keywords: ['dialog', 'modal', 'form', 'drawer', 'actions', 'footer'],
1352
+ },
1353
+ {
1354
+ id: 'toast-stack',
1355
+ name: 'Toast stack',
1356
+ whenToUse: 'Non-blocking app feedback where multiple toast messages need a consistent fixed stack layout.',
1357
+ classes: ['sf-toast-stack', 'sf-toast', 'sf-alert', 'sf-alert__title'],
1358
+ requiresJs: false,
1359
+ requiresJsNotes: 'Stack layout is CSS. Queuing, dismissal timing, and live-region announcements belong to the app.',
1360
+ a11y: ['Use an aria-live region for dynamic messages.', 'Do not auto-dismiss critical errors too quickly.', 'Provide a close control when messages persist.'],
1361
+ gotchas: ['Use sf-toast-stack for layout and sf-toast or sf-alert for each item.', 'Keep queue state outside Synced Flow.'],
1362
+ markup: {
1363
+ html: `<div class="sf-toast-stack" aria-live="polite" aria-label="Notifications">
1364
+ <section class="sf-toast sf-alert" role="status"><strong class="sf-alert__title">Saved</strong><p>Changes were updated.</p></section>
1365
+ </div>`,
1366
+ },
1367
+ keywords: ['toast', 'notification', 'stack', 'feedback', 'alert'],
1368
+ },
1369
+ {
1370
+ id: 'filter-toolbar',
1371
+ name: 'Filter toolbar',
1372
+ whenToUse: 'Search, status filters, quick actions, and exports above lists, tables, queues, directories, and admin screens.',
1373
+ classes: ['sf-filter-bar', 'sf-field', 'sf-label', 'sf-input', 'sf-select', 'sf-button', 'sf-button--outline'],
1374
+ requiresJs: false,
1375
+ requiresJsNotes: 'Use native form submit, GET params, or app-owned state. Synced Flow only styles the shell and controls.',
1376
+ a11y: ['Keep every field labelled.', 'Use button type="submit" for applied filters.', 'Announce result count in app markup when filtering changes content dynamically.'],
1377
+ gotchas: ['Do not hide labels without keeping them accessible.', 'Use app logic for debounced search, saved views, and live result counts.'],
1378
+ markup: {
1379
+ html: `<form class="sf-filter-bar" role="search" action="/app/customers" method="get">
1380
+ <label class="sf-field">
1381
+ <span class="sf-label">Search</span>
1382
+ <input class="sf-input" name="q" type="search" placeholder="Search customers">
1383
+ </label>
1384
+ <label class="sf-field">
1385
+ <span class="sf-label">Status</span>
1386
+ <select class="sf-select" name="status"><option>Any status</option><option>Healthy</option><option>Needs review</option></select>
1387
+ </label>
1388
+ <button class="sf-button" type="submit">Apply</button>
1389
+ <a class="sf-button sf-button--outline" href="/app/customers/export">Export</a>
1390
+ </form>`,
1391
+ },
1392
+ keywords: ['saas', 'filter', 'toolbar', 'search', 'status', 'table', 'list', 'admin', 'crm'],
1393
+ },
1394
+ {
1395
+ id: 'date-filter-form',
1396
+ name: 'Native date filter',
1397
+ whenToUse: 'Date ranges, billing periods, export windows, audit logs, appointments, and reporting filters that can use browser-native date/time controls.',
1398
+ classes: ['sf-filter-bar', 'sf-field', 'sf-label', 'sf-input', 'sf-select', 'sf-button', 'sf-button--outline'],
1399
+ requiresJs: false,
1400
+ requiresJsNotes: 'Native date, time, month, week, and datetime-local inputs submit through normal forms. Custom calendar widgets and availability logic belong to the app.',
1401
+ a11y: ['Use explicit visible labels for each date or time input.', 'Prefer separate start and end fields for ranges.', 'Use server or app validation for invalid ranges.'],
1402
+ gotchas: ['Native picker UI varies by browser and operating system.', 'Use a dedicated calendar library only when the product needs custom scheduling behavior.'],
1403
+ markup: {
1404
+ html: `<form class="sf-filter-bar" action="/app/reports" method="get">
1405
+ <label class="sf-field"><span class="sf-label">From</span><input class="sf-input" name="from" type="date"></label>
1406
+ <label class="sf-field"><span class="sf-label">To</span><input class="sf-input" name="to" type="date"></label>
1407
+ <label class="sf-field"><span class="sf-label">Period</span><select class="sf-select" name="period"><option>Custom</option><option>This month</option><option>Last month</option></select></label>
1408
+ <button class="sf-button" type="submit">Apply</button>
1409
+ <a class="sf-button sf-button--outline" href="/app/reports">Reset</a>
1410
+ </form>`,
1411
+ },
1412
+ keywords: ['saas', 'date', 'time', 'calendar', 'filter', 'range', 'reports', 'billing', 'native'],
1413
+ },
1414
+ {
1415
+ id: 'active-filter-tags',
1416
+ name: 'Active filter tags',
1417
+ whenToUse: 'Visible selected filters below a search/filter toolbar, with clear-all and individual removal actions owned by the app.',
1418
+ classes: ['sf-cluster', 'sf-badge', 'sf-button', 'sf-button--ghost', 'sf-button--sm', 'sf-link'],
1419
+ requiresJs: false,
1420
+ requiresJsNotes: 'Links can remove filters through URL params without JavaScript. Client-side filter state belongs to the consuming app.',
1421
+ a11y: ['Use clear link text that names the filter being removed.', 'Keep the active filter value visible as text.', 'Provide a clear-all action when several filters can be active.'],
1422
+ gotchas: ['Use buttons only when client-side state changes in place.', 'Use links for URL-driven filters so filtered views remain shareable.'],
1423
+ markup: {
1424
+ html: `<div class="sf-cluster" aria-label="Active filters">
1425
+ <span class="sf-badge">Status: Healthy <a class="sf-link" href="?plan=team" aria-label="Remove status filter">Remove</a></span>
1426
+ <span class="sf-badge">Plan: Team <a class="sf-link" href="?status=healthy" aria-label="Remove plan filter">Remove</a></span>
1427
+ <a class="sf-button sf-button--ghost sf-button--sm" href="/app/customers">Clear filters</a>
1428
+ </div>`,
1429
+ },
1430
+ keywords: ['saas', 'filter', 'filters', 'chips', 'tags', 'badge', 'selected', 'active'],
1431
+ },
1432
+ {
1433
+ id: 'data-table',
1434
+ name: 'Data table',
1435
+ whenToUse: 'Account lists, activity logs, invoices, users, queues, directories, and operational tables that should keep native table semantics.',
1436
+ classes: ['sf-table-wrap', 'sf-data-table', 'sf-status', 'sf-avatar', 'sf-link'],
1437
+ requiresJs: false,
1438
+ requiresJsNotes: 'Sorting, pagination state, and row actions belong to the consuming app.',
1439
+ a11y: ['Use a caption or labelled section.', 'Use real table headers.', 'Do not replace table markup with divs for tabular data.'],
1440
+ gotchas: ['Wrap wide tables in sf-table-wrap.', 'Use sf-status for display only; keep actual status text in the element.'],
1441
+ markup: {
1442
+ html: `<div class="sf-table-wrap">
1443
+ <table class="sf-data-table" aria-label="Customers">
1444
+ <thead><tr><th>Customer</th><th>Status</th><th>Plan</th><th>Updated</th></tr></thead>
1445
+ <tbody>
1446
+ <tr><td><a class="sf-link" href="/app/customers/northstar">Northstar Labs</a></td><td><span class="sf-status" data-tone="success">Healthy</span></td><td>Growth</td><td>Today</td></tr>
1447
+ <tr><td>Willow Group</td><td><span class="sf-status" data-tone="warning">Needs review</span></td><td>Operations</td><td>Yesterday</td></tr>
1448
+ </tbody>
1449
+ </table>
1450
+ </div>`,
1451
+ },
1452
+ keywords: ['saas', 'table', 'data', 'customers', 'accounts', 'admin', 'crm', 'status', 'list'],
1453
+ },
1454
+ {
1455
+ id: 'empty-state',
1456
+ name: 'Empty state',
1457
+ whenToUse: 'Empty queues, no search results, first-run screens, directories, and dashboards before data is available.',
1458
+ classes: ['sf-empty-state', 'sf-button', 'sf-button--outline', 'sf-text-muted'],
1459
+ requiresJs: false,
1460
+ requiresJsNotes: 'No JavaScript is required unless the consuming app opens a creation flow.',
1461
+ a11y: ['Use a heading that explains the missing state.', 'Keep primary action text specific.', 'Do not rely on illustration-only meaning.'],
1462
+ gotchas: ['Keep it short; empty states should unblock the next action.', 'Use sf-card around it only when the surrounding layout needs a card surface.'],
1463
+ markup: {
1464
+ html: `<div class="sf-empty-state">
1465
+ <h2 class="sf-text-h4">No matching customers</h2>
1466
+ <p class="sf-text-muted">Adjust the filters or add the first customer.</p>
1467
+ <div class="sf-cluster">
1468
+ <a class="sf-button" href="/app/customers/new">Add customer</a>
1469
+ <a class="sf-button sf-button--outline" href="/app/customers">Reset filters</a>
1470
+ </div>
1471
+ </div>`,
1472
+ },
1473
+ keywords: ['saas', 'empty', 'state', 'first-run', 'onboarding', 'no-results', 'zero'],
1474
+ },
1475
+ {
1476
+ id: 'settings-section',
1477
+ name: 'Settings section',
1478
+ whenToUse: 'Account, team, billing, security, notification, integration, profile, and site settings screens.',
1479
+ classes: ['sf-settings-section', 'sf-detail-panel', 'sf-data-list', 'sf-form', 'sf-field', 'sf-input', 'sf-check', 'sf-button'],
1480
+ requiresJs: false,
1481
+ requiresJsNotes: 'Form submission, optimistic saves, and validation belong to the consuming app.',
1482
+ a11y: ['Group related settings under a heading.', 'Use fieldsets when settings are tightly related.', 'Describe destructive settings before the action.'],
1483
+ gotchas: ['Avoid putting unrelated settings in one panel.', 'Use sf-detail-panel for read-only metadata next to editable fields.'],
1484
+ markup: {
1485
+ html: `<section class="sf-settings-section" aria-labelledby="workspace-settings-title">
1486
+ <header class="sf-repel"><div><h2 id="workspace-settings-title">Workspace settings</h2><p class="sf-text-muted">Manage visible account details.</p></div><button class="sf-button" type="submit" form="workspace-settings">Save</button></header>
1487
+ <form class="sf-form" id="workspace-settings">
1488
+ <label class="sf-field"><span class="sf-label">Workspace name</span><input class="sf-input" name="name" value="Product Suite"></label>
1489
+ <label class="sf-check"><input type="checkbox" name="weekly_summary" checked> <span>Email weekly summary</span></label>
1490
+ </form>
1491
+ <aside class="sf-detail-panel"><dl class="sf-data-list"><div><dt>Plan</dt><dd>Team</dd></div><div><dt>Seats</dt><dd>12 active</dd></div></dl></aside>
1492
+ </section>`,
1493
+ },
1494
+ keywords: ['saas', 'settings', 'section', 'billing', 'team', 'security', 'account', 'detail'],
1495
+ },
1242
1496
  {
1243
1497
  id: 'contact-form',
1244
1498
  name: 'Accessible contact form',
@@ -1362,8 +1616,8 @@ function getPublicRecipes() {
1362
1616
  id: 'saas-dashboard',
1363
1617
  name: 'SaaS dashboard',
1364
1618
  whenToUse: 'Authenticated SaaS apps, admin panels, customer portals, CRMs, analytics dashboards, and product workspaces.',
1365
- sections: ['sticky-navigation', 'mobile-nav-drawer', 'card-grid', 'contact-form', 'long-form-content'],
1366
- classes: ['sf-skip-link', 'sf-theme-light', 'sf-app-shell', 'sf-app-sidebar', 'sf-app-sidebar__brand', 'sf-app-main', 'sf-toolbar', 'sf-stack', 'sf-stack--tight', 'sf-repel', 'sf-cluster', 'sf-panel-grid', 'sf-metric-grid', 'sf-nav', 'sf-nav__list', 'sf-nav__link', 'sf-icon', 'sf-card', 'sf-surface', 'sf-stat', 'sf-badge', 'sf-avatar', 'sf-chart', 'sf-chart--wide', 'sf-chart__plot', 'sf-chart__svg', 'sf-chart__grid', 'sf-chart__axis', 'sf-chart__area', 'sf-chart__line', 'sf-chart__point', 'sf-chart__legend', 'sf-chart__legend-item', 'sf-chart__swatch', 'sf-meter-list', 'sf-meter-row', 'sf-meter', 'sf-button', 'sf-button--outline', 'sf-button--icon', 'sf-form', 'sf-field', 'sf-input', 'sf-check', 'sf-table-wrap', 'sf-font-sans', 'sf-visually-hidden'],
1619
+ sections: ['sticky-navigation', 'mobile-nav-drawer', 'card-grid', 'contact-form', 'long-form-content', 'filter-toolbar', 'date-filter-form', 'active-filter-tags', 'data-table', 'empty-state', 'settings-section'],
1620
+ classes: ['sf-skip-link', 'sf-theme-light', 'sf-app-shell', 'sf-app-sidebar', 'sf-app-sidebar__brand', 'sf-app-main', 'sf-toolbar', 'sf-stack', 'sf-stack--tight', 'sf-repel', 'sf-cluster', 'sf-panel-grid', 'sf-metric-grid', 'sf-nav', 'sf-nav__list', 'sf-nav__link', 'sf-icon', 'sf-card', 'sf-surface', 'sf-stat', 'sf-badge', 'sf-avatar', 'sf-chart', 'sf-chart--wide', 'sf-chart__plot', 'sf-chart__svg', 'sf-chart__grid', 'sf-chart__axis', 'sf-chart__area', 'sf-chart__line', 'sf-chart__point', 'sf-chart__legend', 'sf-chart__legend-item', 'sf-chart__swatch', 'sf-meter-list', 'sf-meter-row', 'sf-meter', 'sf-progress', 'sf-skeleton', 'sf-filter-bar', 'sf-data-table', 'sf-status', 'sf-data-list', 'sf-empty-state', 'sf-settings-section', 'sf-detail-panel', 'sf-button', 'sf-button--outline', 'sf-button--icon', 'sf-form', 'sf-field', 'sf-input', 'sf-select', 'sf-check', 'sf-table-wrap', 'sf-font-sans', 'sf-visually-hidden'],
1367
1621
  keywords: ['saas', 'dashboard', 'admin', 'panel', 'portal', 'crm', 'analytics', 'app', 'auth', 'login', 'signin', 'sign-in', 'user', 'account', 'metrics', 'pipeline', 'activity', 'customers', 'accounts', 'tasks', 'tickets', 'revenue'],
1368
1622
  markup: `<a class="sf-skip-link" href="#dashboard-main">Skip to dashboard</a>
1369
1623
  <div class="sf-app-shell sf-theme-light">
@@ -1460,38 +1714,47 @@ function getPublicRecipes() {
1460
1714
  <label class="sf-meter-row"><span class="sf-repel"><strong>Accounts</strong><span class="sf-text-muted">82%</span></span><meter class="sf-meter" min="0" max="100" value="82">82%</meter></label>
1461
1715
  <label class="sf-meter-row"><span class="sf-repel"><strong>Automation</strong><span class="sf-text-muted">64%</span></span><meter class="sf-meter" min="0" max="100" value="64">64%</meter></label>
1462
1716
  <label class="sf-meter-row"><span class="sf-repel"><strong>Support</strong><span class="sf-text-muted">48%</span></span><meter class="sf-meter" min="0" max="100" value="48">48%</meter></label>
1717
+ <label class="sf-meter-row"><span class="sf-repel"><strong>Import job</strong><span class="sf-text-muted">36%</span></span><progress class="sf-progress" max="100" value="36">36%</progress></label>
1463
1718
  </div>
1464
1719
  </article>
1465
1720
  </section>
1466
1721
  <section class="sf-metric-grid" id="command">
1467
1722
  <article class="sf-card sf-stack"><div class="sf-repel"><h2 class="sf-card__title sf-font-sans">Command centre</h2><span class="sf-badge">Priority</span></div><p class="sf-card__description">Follow-ups, approvals, and missing next actions stay visible.</p></article>
1468
1723
  <article class="sf-card sf-stack" id="billing"><h2 class="sf-card__title sf-font-sans">Billing watch</h2><p class="sf-card__description">2 overdue payment follow-ups and 1 renewal due this week.</p></article>
1469
- <article class="sf-card sf-stack"><h2 class="sf-card__title sf-font-sans">Relationship pulse</h2><p class="sf-card__description">Three customer timelines need a recent touch point.</p></article>
1724
+ <article class="sf-card sf-stack"><h2 class="sf-card__title sf-font-sans">Relationship pulse</h2><p class="sf-card__description">Three customer timelines need a recent touch point.</p><div class="sf-empty-state"><strong>No urgent escalations</strong><span class="sf-text-muted">New priority work will appear here.</span></div></article>
1470
1725
  </section>
1471
1726
  <section class="sf-panel-grid" id="customers">
1472
1727
  <article class="sf-card sf-stack">
1473
1728
  <div class="sf-repel"><h2 class="sf-card__title sf-font-sans">Recent activity</h2><a class="sf-link" href="/app/activity">View all</a></div>
1729
+ <form class="sf-filter-bar" role="search" action="/app/activity" method="get">
1730
+ <label class="sf-field"><span class="sf-label">Search</span><input class="sf-input" name="q" type="search" placeholder="Search activity"></label>
1731
+ <label class="sf-field"><span class="sf-label">Status</span><select class="sf-select" name="status"><option>Any status</option><option>Active</option><option>Scheduled</option><option>Complete</option></select></label>
1732
+ <label class="sf-field"><span class="sf-label">From</span><input class="sf-input" name="from" type="date"></label>
1733
+ <button class="sf-button" type="submit">Apply</button>
1734
+ </form>
1735
+ <div class="sf-cluster" aria-label="Active filters"><span class="sf-badge">Status: Active <a class="sf-link" href="/app/activity" aria-label="Remove status filter">Remove</a></span><a class="sf-button sf-button--ghost sf-button--sm" href="/app/activity">Clear filters</a></div>
1474
1736
  <div class="sf-table-wrap">
1475
- <table>
1737
+ <table class="sf-data-table" aria-label="Recent activity">
1476
1738
  <thead><tr><th>Item</th><th>Status</th><th>Value</th><th>Date</th></tr></thead>
1477
1739
  <tbody>
1478
- <tr><td>Enterprise onboarding</td><td><span class="sf-badge">Active</span></td><td>£7,800</td><td>Today</td></tr>
1479
- <tr><td>Usage review</td><td><span class="sf-badge">Scheduled</span></td><td>£4,050</td><td>Yesterday</td></tr>
1480
- <tr><td>Renewal workflow</td><td><span class="sf-badge">Complete</span></td><td>£3,250</td><td>17 May</td></tr>
1740
+ <tr><td>Enterprise onboarding</td><td><span class="sf-status" data-tone="success">Active</span></td><td>£7,800</td><td>Today</td></tr>
1741
+ <tr><td>Usage review</td><td><span class="sf-status" data-tone="info">Scheduled</span></td><td>£4,050</td><td>Yesterday</td></tr>
1742
+ <tr><td>Renewal workflow</td><td><span class="sf-status" data-tone="warning">Review</span></td><td>£3,250</td><td>17 May</td></tr>
1481
1743
  </tbody>
1482
1744
  </table>
1483
1745
  </div>
1484
1746
  </article>
1485
- <article class="sf-card sf-stack" id="activity">
1747
+ <article class="sf-card sf-stack sf-detail-panel" id="activity">
1486
1748
  <div class="sf-repel"><h2 class="sf-card__title sf-font-sans">Recent customers</h2><a class="sf-link" href="/app/customers">View all</a></div>
1487
- <ul class="sf-list-reset sf-stack">
1488
- <li class="sf-repel"><span><strong>Northstar Labs</strong><br><span class="sf-text-muted">Growth workspace</span></span><span class="sf-text-muted">Healthy</span></li>
1489
- <li class="sf-repel"><span><strong>Willow Group</strong><br><span class="sf-text-muted">Operations workspace</span></span><span class="sf-text-muted">Needs review</span></li>
1490
- <li class="sf-repel"><span><strong>Harbour Systems</strong><br><span class="sf-text-muted">Platform workspace</span></span><span class="sf-text-muted">Healthy</span></li>
1749
+ <div class="sf-skeleton" aria-hidden="true">Loading customer details</div>
1750
+ <ul class="sf-list-reset sf-data-list">
1751
+ <li><span><strong>Northstar Labs</strong><br><span class="sf-text-muted">Growth workspace</span></span><span class="sf-status" data-tone="success">Healthy</span></li>
1752
+ <li><span><strong>Willow Group</strong><br><span class="sf-text-muted">Operations workspace</span></span><span class="sf-status" data-tone="warning">Needs review</span></li>
1753
+ <li><span><strong>Harbour Systems</strong><br><span class="sf-text-muted">Platform workspace</span></span><span class="sf-status" data-tone="success">Healthy</span></li>
1491
1754
  </ul>
1492
1755
  </article>
1493
1756
  </section>
1494
- <section class="sf-card sf-form" aria-labelledby="signin-title">
1757
+ <section class="sf-card sf-form sf-settings-section" aria-labelledby="signin-title">
1495
1758
  <!-- Example unauthenticated state. Wire this form to your app auth provider. -->
1496
1759
  <div class="sf-repel">
1497
1760
  <div><p class="sf-kicker">Signed out state</p><h2 class="sf-card__title sf-font-sans" id="signin-title">Sign in to your workspace</h2></div>
@@ -1684,7 +1947,7 @@ function runTokens() {
1684
1947
  function runCatalog() {
1685
1948
  const catalog = getPublicCatalog()
1686
1949
  if (args.includes('--json')) {
1687
- console.log(JSON.stringify(catalog, null, 2))
1950
+ writeFileSync(1, `${JSON.stringify(catalog, null, 2)}\n`)
1688
1951
  return
1689
1952
  }
1690
1953
 
@@ -2038,8 +2301,14 @@ function runRecipe() {
2038
2301
 
2039
2302
  function findPattern(value, patterns) {
2040
2303
  const normal = value.toLowerCase()
2304
+ const aliases = {
2305
+ form: 'contact-form',
2306
+ }
2307
+
2041
2308
  return (
2309
+ patterns.find((pattern) => pattern.id === aliases[normal]) ??
2042
2310
  patterns.find((pattern) => pattern.id === normal) ??
2311
+ patterns.find((pattern) => pattern.id.endsWith(`-${normal}`)) ??
2043
2312
  patterns.find((pattern) => pattern.keywords.some((keyword) => keyword === normal)) ??
2044
2313
  patterns.find((pattern) => pattern.name.toLowerCase().includes(normal))
2045
2314
  )
@@ -2549,7 +2818,9 @@ const semanticColours = {
2549
2818
  destructive: 'var(--color-destructive)',
2550
2819
  'destructive-foreground': 'var(--color-destructive-foreground)',
2551
2820
  border: 'var(--color-border)',
2821
+ 'border-subtle': 'var(--color-border-subtle)',
2552
2822
  input: 'var(--color-input)',
2823
+ backdrop: 'var(--color-backdrop)',
2553
2824
  ring: 'var(--color-ring)',
2554
2825
  heading: 'var(--color-heading)',
2555
2826
  'page-text': 'var(--color-page-text)',
@@ -2558,7 +2829,15 @@ const semanticColours = {
2558
2829
  subdued: 'var(--color-subdued)',
2559
2830
  surface: 'var(--color-surface)',
2560
2831
  'surface-alt': 'var(--color-surface-alt)',
2832
+ 'surface-hover': 'var(--color-surface-hover)',
2833
+ 'surface-raised-2': 'var(--color-surface-raised-2)',
2561
2834
  'hero-bg': 'var(--color-hero-bg)',
2835
+ 'categorical-1': 'var(--color-categorical-1)',
2836
+ 'categorical-2': 'var(--color-categorical-2)',
2837
+ 'categorical-3': 'var(--color-categorical-3)',
2838
+ 'categorical-4': 'var(--color-categorical-4)',
2839
+ 'categorical-5': 'var(--color-categorical-5)',
2840
+ 'categorical-6': 'var(--color-categorical-6)',
2562
2841
  green: 'var(--color-green)',
2563
2842
  'green-dark': 'var(--color-green-dark)',
2564
2843
  'green-muted': 'var(--color-green-muted)',
@@ -3664,6 +3943,7 @@ function buildTokensCss() {
3664
3943
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
3665
3944
 
3666
3945
  ${steps.join('\n')}
3946
+ --type-caption-sm: var(--step--2);
3667
3947
 
3668
3948
  ${spaces.join('\n')}
3669
3949
  ${pairs.join('\n')}
@@ -3714,7 +3994,9 @@ ${pairs.join('\n')}
3714
3994
  --color-destructive: var(--sf-colour-destructive, ${colours.red600});
3715
3995
  --color-destructive-foreground: var(--sf-colour-destructive-foreground, oklch(98% 0.01 95));
3716
3996
  --color-border: var(--sf-colour-border, oklch(100% 0 0 / 0.12));
3997
+ --color-border-subtle: var(--sf-colour-border-subtle, oklch(100% 0 0 / 0.08));
3717
3998
  --color-input: var(--sf-colour-input, oklch(100% 0 0 / 0.12));
3999
+ --color-backdrop: var(--sf-colour-backdrop, oklch(0% 0 0 / 0.56));
3718
4000
  --color-ring: var(--sf-colour-ring, ${colours.orange600});
3719
4001
  --color-heading: var(--sf-colour-heading, oklch(20% 0.02 18));
3720
4002
  --color-page-text: var(--sf-colour-page-text, oklch(23% 0.02 18));
@@ -3723,6 +4005,14 @@ ${pairs.join('\n')}
3723
4005
  --color-subdued: var(--sf-colour-subdued, oklch(42% 0.02 20));
3724
4006
  --color-surface: var(--sf-colour-surface, ${colours.surfaceAlt});
3725
4007
  --color-surface-alt: var(--sf-colour-surface-alt, ${colours.paperAlt});
4008
+ --color-surface-hover: var(--sf-colour-surface-hover, color-mix(in oklch, var(--color-surface-alt) 86%, var(--color-primary)));
4009
+ --color-surface-raised-2: var(--sf-colour-surface-raised-2, color-mix(in oklch, var(--color-surface) 86%, white));
4010
+ --color-categorical-1: var(--sf-colour-categorical-1, var(--color-primary));
4011
+ --color-categorical-2: var(--sf-colour-categorical-2, var(--color-green));
4012
+ --color-categorical-3: var(--sf-colour-categorical-3, var(--color-deco-blue));
4013
+ --color-categorical-4: var(--sf-colour-categorical-4, var(--color-deco-lime));
4014
+ --color-categorical-5: var(--sf-colour-categorical-5, var(--color-warm-icon));
4015
+ --color-categorical-6: var(--sf-colour-categorical-6, var(--color-purple-icon));
3726
4016
  --color-hero-bg: oklch(8% 0.02 260);
3727
4017
  --color-green: var(--sf-colour-green, ${colours.green600});
3728
4018
  --color-green-dark: var(--sf-colour-green-dark, ${colours.green700});
@@ -3916,11 +4206,47 @@ function buildLayoutCss() {
3916
4206
  }
3917
4207
 
3918
4208
  .sf-app-shell {
4209
+ --sf-app-header-height: 4rem;
4210
+ --sf-app-sidebar-collapsed-width: 4.75rem;
3919
4211
  background: var(--color-background);
3920
4212
  color: var(--color-foreground);
3921
4213
  display: grid;
3922
4214
  grid-template-columns: minmax(14rem, var(--sf-app-sidebar-width, 16rem)) minmax(0, 1fr);
3923
4215
  min-block-size: 100dvh;
4216
+ position: relative;
4217
+ transition: grid-template-columns var(--duration-normal) var(--ease-standard);
4218
+ }
4219
+
4220
+ .sf-app-header {
4221
+ align-items: center;
4222
+ background: var(--color-card);
4223
+ border-block-end: 1px solid var(--color-border);
4224
+ display: flex;
4225
+ gap: var(--space-s);
4226
+ grid-column: 1 / -1;
4227
+ justify-content: space-between;
4228
+ min-block-size: var(--sf-app-header-height);
4229
+ padding-inline: var(--space-s-m);
4230
+ z-index: 20;
4231
+ }
4232
+
4233
+ .sf-app-shell[data-layout~="fixed-header"] {
4234
+ grid-template-rows: var(--sf-app-header-height) minmax(0, 1fr);
4235
+ }
4236
+
4237
+ .sf-app-shell[data-layout~="fixed-header"] .sf-app-header {
4238
+ inset-block-start: 0;
4239
+ position: sticky;
4240
+ }
4241
+
4242
+ .sf-app-shell[data-layout~="fixed-header"] .sf-app-sidebar {
4243
+ inset-block-start: var(--sf-app-header-height);
4244
+ max-block-size: calc(100dvh - var(--sf-app-header-height));
4245
+ min-block-size: calc(100dvh - var(--sf-app-header-height));
4246
+ }
4247
+
4248
+ .sf-app-shell[data-collapsed="true"] {
4249
+ grid-template-columns: var(--sf-app-sidebar-collapsed-width) minmax(0, 1fr);
3924
4250
  }
3925
4251
 
3926
4252
  .sf-app-sidebar {
@@ -3936,6 +4262,7 @@ function buildLayoutCss() {
3936
4262
  padding: var(--space-s);
3937
4263
  position: sticky;
3938
4264
  inset-block-start: 0;
4265
+ transition: transform var(--duration-normal) var(--ease-standard), inline-size var(--duration-normal) var(--ease-standard);
3939
4266
  }
3940
4267
 
3941
4268
  .sf-app-sidebar__brand {
@@ -3988,6 +4315,58 @@ function buildLayoutCss() {
3988
4315
  padding: var(--sf-app-main-padding, var(--space-m-l));
3989
4316
  }
3990
4317
 
4318
+ .sf-app-shell[data-collapsed="true"] .sf-app-sidebar {
4319
+ overflow-x: hidden;
4320
+ }
4321
+
4322
+ .sf-app-shell[data-collapsed="true"] .sf-app-sidebar__brand,
4323
+ .sf-app-shell[data-collapsed="true"] .sf-app-sidebar .sf-nav__link {
4324
+ justify-content: center;
4325
+ }
4326
+
4327
+ .sf-app-shell[data-collapsed="true"] [data-hide-collapsed="true"] {
4328
+ display: none;
4329
+ }
4330
+
4331
+ .sf-app-backdrop {
4332
+ display: none;
4333
+ }
4334
+
4335
+ @media (max-width: 48rem) {
4336
+ .sf-app-shell[data-mobile="drawer"] {
4337
+ display: block;
4338
+ }
4339
+
4340
+ .sf-app-shell[data-mobile="drawer"] .sf-app-header {
4341
+ position: sticky;
4342
+ inset-block-start: 0;
4343
+ }
4344
+
4345
+ .sf-app-shell[data-mobile="drawer"] .sf-app-sidebar {
4346
+ box-shadow: var(--shadow-xl);
4347
+ inline-size: min(18rem, calc(100vw - var(--space-l)));
4348
+ inset-block: 0;
4349
+ inset-inline-start: 0;
4350
+ max-block-size: none;
4351
+ min-block-size: 100dvh;
4352
+ position: fixed;
4353
+ transform: translateX(-100%);
4354
+ z-index: 30;
4355
+ }
4356
+
4357
+ .sf-app-shell[data-mobile="drawer"][data-sidebar-open="true"] .sf-app-sidebar {
4358
+ transform: translateX(0);
4359
+ }
4360
+
4361
+ .sf-app-shell[data-mobile="drawer"][data-sidebar-open="true"] .sf-app-backdrop {
4362
+ background: var(--color-backdrop);
4363
+ display: block;
4364
+ inset: 0;
4365
+ position: fixed;
4366
+ z-index: 25;
4367
+ }
4368
+ }
4369
+
3991
4370
  .sf-grid {
3992
4371
  display: grid;
3993
4372
  gap: var(--grid-gutter);
@@ -4313,11 +4692,268 @@ function buildComponentCss() {
4313
4692
  overflow: hidden;
4314
4693
  }
4315
4694
 
4695
+ .sf-progress {
4696
+ accent-color: var(--progress-color, var(--color-primary));
4697
+ block-size: var(--progress-size, .625rem);
4698
+ inline-size: 100%;
4699
+ }
4700
+
4701
+ .sf-progress::-webkit-progress-bar {
4702
+ background: var(--color-surface-alt);
4703
+ border-radius: var(--radius-full);
4704
+ }
4705
+
4706
+ .sf-progress::-webkit-progress-value,
4707
+ .sf-progress::-moz-progress-bar {
4708
+ background: var(--progress-color, var(--color-primary));
4709
+ border-radius: var(--radius-full);
4710
+ }
4711
+
4712
+ .sf-skeleton {
4713
+ background: color-mix(in oklch, var(--color-surface-alt) 82%, var(--color-foreground));
4714
+ border-radius: var(--skeleton-radius, var(--radius-md));
4715
+ color: transparent;
4716
+ min-block-size: var(--skeleton-block-size, 1em);
4717
+ }
4718
+
4719
+ @media (prefers-reduced-motion: no-preference) {
4720
+ .sf-skeleton {
4721
+ animation: sf-skeleton-pulse 1.4s var(--ease-standard) infinite alternate;
4722
+ }
4723
+ }
4724
+
4725
+ @keyframes sf-skeleton-pulse {
4726
+ to { opacity: .62; }
4727
+ }
4728
+
4729
+ .sf-filter-bar {
4730
+ align-items: end;
4731
+ background: var(--color-card);
4732
+ border: 1px solid var(--color-border);
4733
+ border-radius: var(--radius-lg);
4734
+ display: flex;
4735
+ flex-wrap: wrap;
4736
+ gap: var(--space-xs);
4737
+ padding: var(--space-s);
4738
+ }
4739
+
4740
+ .sf-filter-bar > :where(.sf-field, form, label) {
4741
+ flex: 1 1 min(100%, 14rem);
4742
+ }
4743
+
4744
+ .sf-filter-bar > :where(.sf-button, button, a) {
4745
+ flex: 0 0 auto;
4746
+ }
4747
+
4748
+ .sf-data-table {
4749
+ border-collapse: collapse;
4750
+ inline-size: 100%;
4751
+ min-inline-size: var(--data-table-min, 44rem);
4752
+ }
4753
+
4754
+ .sf-data-table :where(th, td) {
4755
+ border-block-end: 1px solid var(--color-border);
4756
+ padding-block: var(--data-table-padding-block, var(--space-xs));
4757
+ padding-inline: var(--data-table-padding-inline, var(--space-s));
4758
+ text-align: start;
4759
+ vertical-align: middle;
4760
+ }
4761
+
4762
+ .sf-data-table :where(th) {
4763
+ background: var(--color-surface-alt);
4764
+ color: var(--color-muted-foreground);
4765
+ font-size: var(--step--1);
4766
+ font-weight: 800;
4767
+ }
4768
+
4769
+ .sf-data-table :where(tbody tr:hover) {
4770
+ background: color-mix(in oklch, var(--color-primary) 6%, transparent);
4771
+ }
4772
+
4773
+ .sf-data-table[data-density="compact"] {
4774
+ --data-table-padding-block: var(--space-2xs);
4775
+ --data-table-padding-inline: var(--space-xs);
4776
+ }
4777
+
4778
+ .sf-data-table :where(th button, .sf-table-sort) {
4779
+ align-items: center;
4780
+ color: inherit;
4781
+ display: inline-flex;
4782
+ font: inherit;
4783
+ font-weight: inherit;
4784
+ gap: var(--space-3xs);
4785
+ padding: 0;
4786
+ }
4787
+
4788
+ .sf-data-table :where(th button, .sf-table-sort)[aria-sort]::after {
4789
+ color: var(--color-muted-foreground);
4790
+ content: "\\2195";
4791
+ font-size: var(--step--2);
4792
+ }
4793
+
4794
+ .sf-data-table :where(th button, .sf-table-sort)[aria-sort="ascending"]::after { content: "\\2191"; }
4795
+ .sf-data-table :where(th button, .sf-table-sort)[aria-sort="descending"]::after { content: "\\2193"; }
4796
+
4797
+ .sf-data-table tr[data-clickable="true"] {
4798
+ cursor: pointer;
4799
+ }
4800
+
4801
+ .sf-data-table tr[data-clickable="true"]:focus-within {
4802
+ outline: 0.125rem solid var(--color-ring);
4803
+ outline-offset: -0.125rem;
4804
+ }
4805
+
4806
+ @container (max-width: 38rem) {
4807
+ .sf-table-wrap[data-view="cards"] {
4808
+ border: 0;
4809
+ overflow: visible;
4810
+ }
4811
+
4812
+ .sf-table-wrap[data-view="cards"] .sf-data-table,
4813
+ .sf-table-wrap[data-view="cards"] .sf-data-table :where(thead, tbody, tr, th, td) {
4814
+ display: block;
4815
+ min-inline-size: 0;
4816
+ }
4817
+
4818
+ .sf-table-wrap[data-view="cards"] .sf-data-table thead {
4819
+ display: none;
4820
+ }
4821
+
4822
+ .sf-table-wrap[data-view="cards"] .sf-data-table tr {
4823
+ background: var(--color-card);
4824
+ border: 1px solid var(--color-border);
4825
+ border-radius: var(--radius-lg);
4826
+ margin-block-end: var(--space-xs);
4827
+ padding: var(--space-xs);
4828
+ }
4829
+
4830
+ .sf-table-wrap[data-view="cards"] .sf-data-table td {
4831
+ align-items: center;
4832
+ border: 0;
4833
+ display: flex;
4834
+ gap: var(--space-s);
4835
+ justify-content: space-between;
4836
+ padding-block: var(--space-2xs);
4837
+ padding-inline: 0;
4838
+ }
4839
+
4840
+ .sf-table-wrap[data-view="cards"] .sf-data-table td::before {
4841
+ color: var(--color-muted-foreground);
4842
+ content: attr(data-label);
4843
+ font-size: var(--step--1);
4844
+ font-weight: 800;
4845
+ }
4846
+ }
4847
+
4848
+ .sf-status {
4849
+ align-items: center;
4850
+ background: var(--status-bg, var(--color-surface-alt));
4851
+ border: 1px solid var(--status-border, var(--color-border));
4852
+ border-radius: var(--radius-full);
4853
+ color: var(--status-color, var(--color-foreground));
4854
+ display: inline-flex;
4855
+ font-size: var(--step--1);
4856
+ font-weight: 700;
4857
+ gap: var(--space-3xs);
4858
+ line-height: 1;
4859
+ min-block-size: 1.75rem;
4860
+ padding-inline: .7em;
4861
+ }
4862
+
4863
+ .sf-status[data-size="sm"] {
4864
+ font-size: var(--step--2);
4865
+ min-block-size: 1.35rem;
4866
+ padding-inline: .55em;
4867
+ }
4868
+
4869
+ .sf-status::before {
4870
+ background: currentColor;
4871
+ block-size: .42em;
4872
+ border-radius: var(--radius-full);
4873
+ content: "";
4874
+ inline-size: .42em;
4875
+ }
4876
+
4877
+ .sf-status[data-tone="success"] {
4878
+ --status-bg: var(--sf-colour-success-soft, oklch(62% 0.13 150 / 0.12));
4879
+ --status-border: color-mix(in oklch, var(--sf-colour-success, oklch(62% 0.13 150)) 34%, transparent);
4880
+ --status-color: color-mix(in oklch, var(--sf-colour-success, oklch(62% 0.13 150)) 76%, var(--color-foreground));
4881
+ }
4882
+
4883
+ .sf-status[data-tone="warning"] {
4884
+ --status-bg: var(--sf-colour-warning-soft, oklch(78% 0.15 82 / 0.16));
4885
+ --status-border: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 38%, transparent);
4886
+ --status-color: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 70%, var(--color-foreground));
4887
+ }
4888
+
4889
+ .sf-status[data-tone="danger"] {
4890
+ --status-bg: var(--sf-colour-danger-soft, oklch(56% 0.2 28 / 0.12));
4891
+ --status-border: color-mix(in oklch, var(--sf-colour-danger, var(--color-destructive)) 34%, transparent);
4892
+ --status-color: color-mix(in oklch, var(--sf-colour-danger, var(--color-destructive)) 76%, var(--color-foreground));
4893
+ }
4894
+
4895
+ .sf-status[data-tone="info"] {
4896
+ --status-bg: var(--sf-colour-info-soft, oklch(70% 0.12 205 / 0.14));
4897
+ --status-border: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 34%, transparent);
4898
+ --status-color: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 76%, var(--color-foreground));
4899
+ }
4900
+
4901
+ .sf-data-list {
4902
+ display: grid;
4903
+ }
4904
+
4905
+ .sf-data-list > * {
4906
+ align-items: center;
4907
+ border-block-end: 1px solid var(--color-border);
4908
+ display: flex;
4909
+ flex-wrap: wrap;
4910
+ gap: var(--space-xs);
4911
+ justify-content: space-between;
4912
+ padding-block: var(--space-xs);
4913
+ }
4914
+
4915
+ .sf-data-list > :last-child {
4916
+ border-block-end: 0;
4917
+ }
4918
+
4919
+ .sf-empty-state {
4920
+ align-items: center;
4921
+ background: var(--color-surface-alt);
4922
+ border: 1px dashed var(--color-border);
4923
+ border-radius: var(--radius-lg);
4924
+ color: var(--color-muted-foreground);
4925
+ display: grid;
4926
+ gap: var(--space-xs);
4927
+ justify-items: center;
4928
+ padding: var(--space-l);
4929
+ text-align: center;
4930
+ }
4931
+
4932
+ .sf-settings-section,
4933
+ .sf-detail-panel {
4934
+ background: var(--color-card);
4935
+ border: 1px solid var(--color-border);
4936
+ border-radius: var(--radius-lg);
4937
+ display: grid;
4938
+ gap: var(--space-s);
4939
+ padding: var(--space-m);
4940
+ }
4941
+
4942
+ .sf-detail-panel {
4943
+ background: var(--color-surface-alt);
4944
+ }
4945
+
4316
4946
  .sf-field {
4317
4947
  display: grid;
4318
4948
  gap: var(--space-2xs);
4319
4949
  }
4320
4950
 
4951
+ .sf-form[data-density="compact"],
4952
+ .sf-field[data-density="compact"] {
4953
+ --input-block-size: 2.25rem;
4954
+ --input-padding-inline: var(--space-xs);
4955
+ }
4956
+
4321
4957
  .sf-field > label {
4322
4958
  color: var(--color-muted-foreground);
4323
4959
  font-size: var(--step--1);
@@ -4330,7 +4966,41 @@ function buildComponentCss() {
4330
4966
  border-radius: var(--input-radius, var(--radius-md));
4331
4967
  color: var(--color-foreground);
4332
4968
  min-block-size: var(--input-block-size, 2.75rem);
4333
- padding-inline: var(--space-s);
4969
+ padding-inline: var(--input-padding-inline, var(--space-s));
4970
+ }
4971
+
4972
+ .sf-search {
4973
+ position: relative;
4974
+ }
4975
+
4976
+ .sf-search::before {
4977
+ block-size: .75em;
4978
+ border: 0.125em solid currentColor;
4979
+ border-radius: var(--radius-full);
4980
+ color: var(--color-muted-foreground);
4981
+ content: "";
4982
+ inline-size: .75em;
4983
+ inset-block-start: 50%;
4984
+ inset-inline-start: var(--space-xs);
4985
+ line-height: 1;
4986
+ position: absolute;
4987
+ transform: translateY(-58%);
4988
+ }
4989
+
4990
+ .sf-search::after {
4991
+ background: var(--color-muted-foreground);
4992
+ block-size: .125em;
4993
+ content: "";
4994
+ inline-size: .45em;
4995
+ inset-block-start: 50%;
4996
+ inset-inline-start: calc(var(--space-xs) + .58em);
4997
+ position: absolute;
4998
+ transform: translateY(120%) rotate(45deg);
4999
+ transform-origin: inline-start center;
5000
+ }
5001
+
5002
+ .sf-search > .sf-input {
5003
+ padding-inline-start: var(--space-l);
4334
5004
  }
4335
5005
 
4336
5006
  .sf-input:focus-visible {