@syncedco/flow 0.3.2 → 0.4.1
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/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +18 -5
- package/README.md +32 -9
- package/bin/synced-flow.mjs +416 -46
- package/components.css +440 -9
- package/dist/index.d.ts +69 -0
- package/dist/index.js +69 -0
- package/docs/accessibility-css.md +14 -3
- package/docs/ai-usage.md +22 -1
- package/docs/api-contract.md +10 -2
- package/docs/audit-2026-06.md +13 -12
- package/docs/cli-reference.md +9 -6
- package/docs/config-reference.md +2 -2
- package/docs/css-optimisation.md +10 -10
- package/docs/native-components.md +121 -0
- package/docs/patterns.md +18 -0
- package/docs/quick-start.md +6 -4
- package/docs/recipes.md +12 -0
- package/docs/release-readiness.md +32 -7
- package/docs/system-primitives.md +9 -2
- package/docs/tokens.md +4 -1
- package/examples/README.md +2 -2
- package/examples/next/app/synced-flow.generated.css +5 -5
- package/examples/plain-html/synced-flow.generated.css +5 -5
- package/examples/templates/README.md +2 -0
- package/examples/templates/app-settings-workflow.html +81 -0
- package/examples/vite/src/synced-flow.generated.css +5 -5
- package/examples/wordpress/assets/css/synced-flow.css +150 -13
- package/package.json +14 -3
- package/skills/synced-flow/SKILL.md +20 -3
- package/src/index.ts +69 -0
- package/src/presets.mjs +5 -5
- package/src/tokens.mjs +10 -8
- package/styles.css +452 -19
- package/tokens.css +12 -10
package/bin/synced-flow.mjs
CHANGED
|
@@ -1025,7 +1025,7 @@ function getTokenSummary() {
|
|
|
1025
1025
|
themePresets: presetNames.map(kebabThemeName),
|
|
1026
1026
|
starterClasses: {
|
|
1027
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'],
|
|
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-spinner', '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-input-group', 'sf-input-group__leading', 'sf-input-group__trailing', 'sf-input-group__action', 'sf-file-input', 'sf-range', 'sf-switch', 'sf-switch__control', 'sf-switch__label', 'sf-segmented-control', 'sf-segmented-control__option', 'sf-segmented-control__label', 'sf-combobox', 'sf-combobox__listbox', 'sf-combobox__option', 'sf-combobox__empty', 'sf-bulk-actions', 'sf-bulk-actions__count', 'sf-bulk-actions__controls', 'sf-stepper', 'sf-stepper__step', 'sf-stepper__marker', 'sf-stepper__label', 'sf-wizard-actions', '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-pagination__summary', 'sf-pagination__controls', 'sf-pagination__pages', 'sf-pagination__jump', '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
|
},
|
|
@@ -1082,7 +1082,7 @@ function getPublicPatterns() {
|
|
|
1082
1082
|
<a class="sf-nav__link" href="#contact">Contact</a>
|
|
1083
1083
|
</nav>
|
|
1084
1084
|
</header>
|
|
1085
|
-
<main class="sf-scroll-viewport sf-scroll-snap-y" id="main">
|
|
1085
|
+
<main class="sf-scroll-viewport sf-scroll-snap-y" id="main" tabindex="0">
|
|
1086
1086
|
<section class="sf-scroll-panel" id="intro"><div class="sf-container sf-stack"><h1 class="sf-text-display">Intro panel</h1></div></section>
|
|
1087
1087
|
<section class="sf-scroll-panel" id="work"><div class="sf-container sf-stack"><h2 class="sf-text-h1">Work panel</h2></div></section>
|
|
1088
1088
|
<section class="sf-scroll-panel" id="contact"><div class="sf-container sf-stack"><h2 class="sf-text-h1">Contact panel</h2></div></section>
|
|
@@ -1100,24 +1100,65 @@ function getPublicPatterns() {
|
|
|
1100
1100
|
a11y: ['Update aria-current on the active nav link.', 'Do not rely on colour alone for current state.', 'Respect prefers-reduced-motion for scripted scrolling.'],
|
|
1101
1101
|
gotchas: ['Keep the observer script in the consuming app or example, not the Synced Flow package.', 'Anchor links still work without the optional script.'],
|
|
1102
1102
|
markup: {
|
|
1103
|
-
html: `<nav class="sf-container sf-nav sf-sticky-top" aria-label="Section navigation">
|
|
1103
|
+
html: `<nav class="sf-container sf-nav sf-sticky-top" aria-label="Section navigation" data-sf-scroll-spy-nav>
|
|
1104
1104
|
<a class="sf-nav__link" href="#one" aria-current="location">One</a>
|
|
1105
1105
|
<a class="sf-nav__link" href="#two">Two</a>
|
|
1106
1106
|
</nav>
|
|
1107
|
-
<main class="sf-scroll-viewport sf-scroll-snap-y" data-sf-scroll-spy>
|
|
1107
|
+
<main class="sf-scroll-viewport sf-scroll-snap-y" data-sf-scroll-spy tabindex="0">
|
|
1108
1108
|
<section class="sf-scroll-panel" id="one"><div class="sf-container"><h1 class="sf-text-display">One</h1></div></section>
|
|
1109
1109
|
<section class="sf-scroll-panel" id="two"><div class="sf-container"><h2 class="sf-text-h1">Two</h2></div></section>
|
|
1110
1110
|
</main>
|
|
1111
1111
|
<script>
|
|
1112
|
-
const
|
|
1113
|
-
const
|
|
1112
|
+
const viewport = document.querySelector('[data-sf-scroll-spy]')
|
|
1113
|
+
const links = [...document.querySelectorAll('[data-sf-scroll-spy-nav] a[href^="#"]')]
|
|
1114
|
+
const panels = [...viewport.querySelectorAll('.sf-scroll-panel[id]')]
|
|
1114
1115
|
const observer = new IntersectionObserver((entries) => {
|
|
1115
1116
|
const active = entries.find((entry) => entry.isIntersecting)
|
|
1116
1117
|
if (!active) return
|
|
1117
|
-
for (const link of links)
|
|
1118
|
+
for (const link of links) {
|
|
1119
|
+
if (link.hash === '#' + active.target.id) link.setAttribute('aria-current', 'location')
|
|
1120
|
+
else link.removeAttribute('aria-current')
|
|
1121
|
+
}
|
|
1118
1122
|
}, { threshold: 0.6 })
|
|
1119
1123
|
for (const panel of panels) observer.observe(panel)
|
|
1120
1124
|
</script>`,
|
|
1125
|
+
next: `'use client'
|
|
1126
|
+
|
|
1127
|
+
import { useEffect, useRef } from 'react'
|
|
1128
|
+
|
|
1129
|
+
export default function ScrollViewportWithSpy() {
|
|
1130
|
+
const navRef = useRef<HTMLElement>(null)
|
|
1131
|
+
const viewportRef = useRef<HTMLElement>(null)
|
|
1132
|
+
|
|
1133
|
+
useEffect(() => {
|
|
1134
|
+
const links = Array.from(navRef.current?.querySelectorAll<HTMLAnchorElement>('a[href^="#"]') ?? [])
|
|
1135
|
+
const panels = Array.from(viewportRef.current?.querySelectorAll<HTMLElement>('.sf-scroll-panel[id]') ?? [])
|
|
1136
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1137
|
+
const active = entries.find((entry) => entry.isIntersecting)
|
|
1138
|
+
if (!active) return
|
|
1139
|
+
for (const link of links) {
|
|
1140
|
+
if (link.hash === '#' + active.target.id) link.setAttribute('aria-current', 'location')
|
|
1141
|
+
else link.removeAttribute('aria-current')
|
|
1142
|
+
}
|
|
1143
|
+
}, { threshold: 0.6 })
|
|
1144
|
+
|
|
1145
|
+
for (const panel of panels) observer.observe(panel)
|
|
1146
|
+
return () => observer.disconnect()
|
|
1147
|
+
}, [])
|
|
1148
|
+
|
|
1149
|
+
return (
|
|
1150
|
+
<>
|
|
1151
|
+
<nav ref={navRef} className="sf-container sf-nav sf-sticky-top" aria-label="Section navigation">
|
|
1152
|
+
<a className="sf-nav__link" href="#one" aria-current="location">One</a>
|
|
1153
|
+
<a className="sf-nav__link" href="#two">Two</a>
|
|
1154
|
+
</nav>
|
|
1155
|
+
<main ref={viewportRef} className="sf-scroll-viewport sf-scroll-snap-y" data-sf-scroll-spy tabIndex={0}>
|
|
1156
|
+
<section className="sf-scroll-panel" id="one"><div className="sf-container"><h1 className="sf-text-display">One</h1></div></section>
|
|
1157
|
+
<section className="sf-scroll-panel" id="two"><div className="sf-container"><h2 className="sf-text-h1">Two</h2></div></section>
|
|
1158
|
+
</main>
|
|
1159
|
+
</>
|
|
1160
|
+
)
|
|
1161
|
+
}`,
|
|
1121
1162
|
},
|
|
1122
1163
|
keywords: ['scroll', 'spy', 'intersectionobserver', 'portfolio', 'active', 'nav', 'sections'],
|
|
1123
1164
|
},
|
|
@@ -1142,7 +1183,7 @@ function getPublicPatterns() {
|
|
|
1142
1183
|
import { useRef } from 'react'
|
|
1143
1184
|
|
|
1144
1185
|
export default function DialogExample() {
|
|
1145
|
-
const dialogRef = useRef(null)
|
|
1186
|
+
const dialogRef = useRef<HTMLDialogElement>(null)
|
|
1146
1187
|
|
|
1147
1188
|
return (
|
|
1148
1189
|
<>
|
|
@@ -1228,7 +1269,7 @@ export default function DialogExample() {
|
|
|
1228
1269
|
name: 'Scroll snap page sections',
|
|
1229
1270
|
whenToUse: 'Portfolio panels, product storytelling, presentation-style pages, and full-page feature sections.',
|
|
1230
1271
|
classes: ['sf-scroll-viewport', 'sf-scroll-snap-y', 'sf-scroll-panel', 'sf-sticky-top', 'sf-section', 'sf-container'],
|
|
1231
|
-
markup: '<main class="sf-scroll-viewport" data-snap="mandatory"><section class="sf-scroll-panel">...</section></main>',
|
|
1272
|
+
markup: '<main class="sf-scroll-viewport" data-snap="mandatory" tabindex="0"><section class="sf-scroll-panel">...</section></main>',
|
|
1232
1273
|
keywords: ['scroll', 'snap', 'panel', 'portfolio', 'presentation', 'full-page', 'sections'],
|
|
1233
1274
|
},
|
|
1234
1275
|
{
|
|
@@ -1285,12 +1326,12 @@ export default function DialogExample() {
|
|
|
1285
1326
|
whenToUse: 'Filtered app views, inboxes, queues, and reports where each tab needs a compact count.',
|
|
1286
1327
|
classes: ['sf-tabs', 'sf-tab-list', 'sf-tab', 'sf-tab__count', 'sf-tab-panel'],
|
|
1287
1328
|
requiresJs: false,
|
|
1288
|
-
requiresJsNotes: 'CSS styles state. The app owns
|
|
1289
|
-
a11y: ['Use role="tablist" only when implementing full keyboard tab behavior.', 'Use aria-
|
|
1329
|
+
requiresJsNotes: 'CSS styles state. The app owns routing or tab panel switching for dynamic interfaces.',
|
|
1330
|
+
a11y: ['Use role="tablist" and aria-selected only when implementing full keyboard tab behavior.', 'Use aria-current="page" for the active route-backed link.', 'Keep counts textual and not colour-only.'],
|
|
1290
1331
|
gotchas: ['Use data-variant="pill" for pill tabs.', 'Use links for route-backed tabs and buttons for in-page tab switching.'],
|
|
1291
1332
|
markup: {
|
|
1292
1333
|
html: `<nav class="sf-tab-list" aria-label="Views">
|
|
1293
|
-
<a class="sf-tab" data-variant="pill" href="?view=open" aria-
|
|
1334
|
+
<a class="sf-tab" data-variant="pill" href="?view=open" aria-current="page">Open <span class="sf-tab__count">12</span></a>
|
|
1294
1335
|
<a class="sf-tab" data-variant="pill" href="?view=closed">Closed <span class="sf-tab__count">8</span></a>
|
|
1295
1336
|
</nav>`,
|
|
1296
1337
|
},
|
|
@@ -1306,9 +1347,9 @@ export default function DialogExample() {
|
|
|
1306
1347
|
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
1348
|
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
1349
|
markup: {
|
|
1309
|
-
html: `<div class="sf-table-wrap" data-view="cards">
|
|
1350
|
+
html: `<div class="sf-table-wrap" data-view="cards" tabindex="0">
|
|
1310
1351
|
<table class="sf-data-table">
|
|
1311
|
-
<thead><tr><th><button class="sf-table-sort" type="button"
|
|
1352
|
+
<thead><tr><th aria-sort="ascending"><button class="sf-table-sort" type="button">Name</button></th><th>Status</th></tr></thead>
|
|
1312
1353
|
<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
1354
|
</table>
|
|
1314
1355
|
</div>`,
|
|
@@ -1345,7 +1386,7 @@ export default function DialogExample() {
|
|
|
1345
1386
|
html: `<dialog class="sf-dialog" data-layout="form" id="edit-dialog" aria-labelledby="edit-title">
|
|
1346
1387
|
<header class="sf-dialog__header"><h2 id="edit-title">Edit record</h2></header>
|
|
1347
1388
|
<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>
|
|
1389
|
+
<footer class="sf-dialog__footer"><button class="sf-button sf-button--outline" type="submit" form="edit-form" formmethod="dialog">Cancel</button><button class="sf-button" type="submit" form="edit-form">Save</button></footer>
|
|
1349
1390
|
</dialog>`,
|
|
1350
1391
|
},
|
|
1351
1392
|
keywords: ['dialog', 'modal', 'form', 'drawer', 'actions', 'footer'],
|
|
@@ -1439,7 +1480,7 @@ export default function DialogExample() {
|
|
|
1439
1480
|
a11y: ['Use a caption or labelled section.', 'Use real table headers.', 'Do not replace table markup with divs for tabular data.'],
|
|
1440
1481
|
gotchas: ['Wrap wide tables in sf-table-wrap.', 'Use sf-status for display only; keep actual status text in the element.'],
|
|
1441
1482
|
markup: {
|
|
1442
|
-
html: `<div class="sf-table-wrap">
|
|
1483
|
+
html: `<div class="sf-table-wrap" tabindex="0">
|
|
1443
1484
|
<table class="sf-data-table" aria-label="Customers">
|
|
1444
1485
|
<thead><tr><th>Customer</th><th>Status</th><th>Plan</th><th>Updated</th></tr></thead>
|
|
1445
1486
|
<tbody>
|
|
@@ -1493,6 +1534,132 @@ export default function DialogExample() {
|
|
|
1493
1534
|
},
|
|
1494
1535
|
keywords: ['saas', 'settings', 'section', 'billing', 'team', 'security', 'account', 'detail'],
|
|
1495
1536
|
},
|
|
1537
|
+
{
|
|
1538
|
+
id: 'switch-control',
|
|
1539
|
+
name: 'Binary switch control',
|
|
1540
|
+
whenToUse: 'A setting that takes effect as a strict on or off value and has a stable visible label.',
|
|
1541
|
+
classes: ['sf-switch', 'sf-switch__control', 'sf-switch__label'],
|
|
1542
|
+
requiresJs: false,
|
|
1543
|
+
requiresJsNotes: 'The native checkbox owns keyboard and form behavior. App logic is only needed for immediate persistence.',
|
|
1544
|
+
a11y: ['Keep the visible label stable when state changes.', 'Use checked for state and disabled when unavailable.', 'Do not use indeterminate or mixed state for a switch.'],
|
|
1545
|
+
gotchas: ['Checkboxes have no readonly state; use disabled, or implement a separately explained app-owned restriction.', 'Place the control span immediately after the checkbox.'],
|
|
1546
|
+
markup: {
|
|
1547
|
+
html: `<label class="sf-switch"><input type="checkbox" role="switch" name="notifications" checked><span class="sf-switch__control" aria-hidden="true"></span><span class="sf-switch__label">Email notifications</span></label>`,
|
|
1548
|
+
},
|
|
1549
|
+
keywords: ['switch', 'toggle', 'settings', 'binary', 'checkbox', 'on', 'off'],
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
id: 'input-group',
|
|
1553
|
+
name: 'Input group',
|
|
1554
|
+
whenToUse: 'Text inputs with a unit, currency prefix, domain suffix, or compact inline action.',
|
|
1555
|
+
classes: ['sf-field', 'sf-label', 'sf-input-group', 'sf-input-group__leading', 'sf-input', 'sf-input-group__trailing', 'sf-input-group__action'],
|
|
1556
|
+
requiresJs: false,
|
|
1557
|
+
requiresJsNotes: 'The group is CSS-only. Inline action behavior belongs to the app.',
|
|
1558
|
+
a11y: ['Keep one explicit label for the input.', 'Mark decorative prefixes and suffixes aria-hidden.', 'Give inline actions an accessible name.'],
|
|
1559
|
+
gotchas: ['Use sf-search for ordinary search fields.', 'Keep exactly one flexible form control in each group.'],
|
|
1560
|
+
markup: {
|
|
1561
|
+
html: `<label class="sf-field"><span class="sf-label">Monthly budget</span><span class="sf-input-group"><span class="sf-input-group__leading" aria-hidden="true">£</span><input class="sf-input" name="budget" inputmode="decimal"><span class="sf-input-group__trailing" aria-hidden="true">GBP</span></span></label>`,
|
|
1562
|
+
},
|
|
1563
|
+
keywords: ['input', 'group', 'prefix', 'suffix', 'currency', 'unit', 'action'],
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
id: 'native-file-and-range',
|
|
1567
|
+
name: 'Native file and range inputs',
|
|
1568
|
+
whenToUse: 'File selection and bounded numeric values that can use browser-native controls.',
|
|
1569
|
+
classes: ['sf-form', 'sf-field', 'sf-label', 'sf-help', 'sf-file-input', 'sf-range'],
|
|
1570
|
+
requiresJs: false,
|
|
1571
|
+
requiresJsNotes: 'Native inputs submit normally. Upload processing, previews, and live range value output belong to the app.',
|
|
1572
|
+
a11y: ['Label both inputs.', 'Describe accepted file types in visible help text.', 'Provide min, max and a visible current value for range controls.'],
|
|
1573
|
+
gotchas: ['File picker presentation varies by browser.', 'Do not use range where an exact typed value is required.'],
|
|
1574
|
+
markup: {
|
|
1575
|
+
html: `<div class="sf-form"><label class="sf-field"><span class="sf-label">Supporting document</span><input class="sf-file-input" type="file" name="document" accept=".pdf,.doc,.docx"><span class="sf-help">PDF or Word, up to 10 MB.</span></label><label class="sf-field"><span class="sf-label">Team size: <output for="team-size">12</output></span><input class="sf-range" id="team-size" name="team_size" type="range" min="1" max="50" value="12"></label></div>`,
|
|
1576
|
+
},
|
|
1577
|
+
keywords: ['file', 'upload', 'range', 'slider', 'input', 'native'],
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
id: 'segmented-control',
|
|
1581
|
+
name: 'Segmented single choice',
|
|
1582
|
+
whenToUse: 'A small set of mutually exclusive form choices displayed as adjacent options.',
|
|
1583
|
+
classes: ['sf-segmented-control', 'sf-segmented-control__option', 'sf-segmented-control__label'],
|
|
1584
|
+
requiresJs: false,
|
|
1585
|
+
requiresJsNotes: 'Native radio inputs own keyboard, form, and checked state.',
|
|
1586
|
+
a11y: ['Use a fieldset and legend to name the choice.', 'Keep every option label visible.', 'Use tabs for panel navigation and switches for independent on/off settings.'],
|
|
1587
|
+
gotchas: ['All radios must share a name.', 'Keep the number of options small enough to scan.'],
|
|
1588
|
+
markup: {
|
|
1589
|
+
html: `<fieldset class="sf-fieldset"><legend>Billing cycle</legend><div class="sf-segmented-control"><label class="sf-segmented-control__option"><input type="radio" name="cycle" value="monthly" checked><span class="sf-segmented-control__label">Monthly</span></label><label class="sf-segmented-control__option"><input type="radio" name="cycle" value="annual"><span class="sf-segmented-control__label">Annual</span></label></div></fieldset>`,
|
|
1590
|
+
},
|
|
1591
|
+
keywords: ['segmented', 'radio', 'choice', 'single', 'options', 'billing'],
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
id: 'loading-button',
|
|
1595
|
+
name: 'Loading button',
|
|
1596
|
+
whenToUse: 'A compact pending state for an action while the app completes asynchronous work.',
|
|
1597
|
+
classes: ['sf-button', 'sf-spinner', 'sr-only'],
|
|
1598
|
+
requiresJs: true,
|
|
1599
|
+
requiresJsNotes: 'The app owns pending state, prevents activation while aria-disabled, updates visible text, and announces completion or failure.',
|
|
1600
|
+
a11y: ['Set aria-busy="true" while pending.', 'Keep a visible pending label such as Saving… or expose an sr-only status.', 'Announce the result in a nearby live region.'],
|
|
1601
|
+
gotchas: ['A spinner alone is not an accessible status.', 'aria-disabled keeps the button focusable but does not prevent activation; the app must guard the handler.', 'Use native disabled instead when removing the action from focus order is preferable.'],
|
|
1602
|
+
markup: {
|
|
1603
|
+
html: `<button class="sf-button" type="submit" aria-busy="true" aria-disabled="true"><span class="sf-spinner" aria-hidden="true"></span><span>Saving…</span></button><p class="sr-only" role="status" aria-live="polite">Saving changes</p>`,
|
|
1604
|
+
},
|
|
1605
|
+
keywords: ['loading', 'spinner', 'button', 'busy', 'pending', 'saving'],
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
id: 'searchable-select',
|
|
1609
|
+
name: 'Searchable select shell',
|
|
1610
|
+
whenToUse: 'A large option set that needs app-owned filtering and complete combobox keyboard behavior.',
|
|
1611
|
+
classes: ['sf-field', 'sf-label', 'sf-combobox', 'sf-input', 'sf-combobox__listbox', 'sf-combobox__option', 'sf-combobox__empty'],
|
|
1612
|
+
requiresJs: true,
|
|
1613
|
+
requiresJsNotes: 'The app must implement the ARIA combobox contract, active option, filtering, selection, focus, Escape, and outside-click behavior. Synced Flow supplies presentation only.',
|
|
1614
|
+
a11y: ['Follow the ARIA combobox pattern completely.', 'Keep aria-expanded, aria-controls, aria-activedescendant and aria-selected synchronized.', 'Test with keyboard and assistive technology.'],
|
|
1615
|
+
gotchas: ['This shell is not a working combobox without app code.', 'Prefer native select for short option lists.'],
|
|
1616
|
+
markup: {
|
|
1617
|
+
html: `<div class="sf-field"><label class="sf-label" for="customer-search">Customer</label><div class="sf-combobox"><input class="sf-input" id="customer-search" role="combobox" aria-autocomplete="list" aria-expanded="true" aria-controls="customer-options" aria-activedescendant="customer-option-2"><ul class="sf-combobox__listbox" id="customer-options" role="listbox"><li class="sf-combobox__option" id="customer-option-1" role="option" aria-selected="false">Northstar Labs</li><li class="sf-combobox__option" id="customer-option-2" role="option" aria-selected="true">Willow Group</li></ul></div></div>`,
|
|
1618
|
+
},
|
|
1619
|
+
keywords: ['combobox', 'searchable', 'select', 'autocomplete', 'listbox', 'customer'],
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
id: 'bulk-actions-toolbar',
|
|
1623
|
+
name: 'Bulk actions toolbar',
|
|
1624
|
+
whenToUse: 'A table or list selection that exposes actions and a clear selected-item count.',
|
|
1625
|
+
classes: ['sf-bulk-actions', 'sf-bulk-actions__count', 'sf-bulk-actions__controls', 'sf-button', 'sf-button--sm', 'sf-button--destructive'],
|
|
1626
|
+
requiresJs: true,
|
|
1627
|
+
requiresJsNotes: 'The app owns row selection, selected count, action availability, confirmation, and focus after updates.',
|
|
1628
|
+
a11y: ['Announce selection count changes in a polite live region.', 'Name destructive actions clearly.', 'Keep selection checkboxes associated with their rows.'],
|
|
1629
|
+
gotchas: ['Hide the toolbar when nothing is selected.', 'Do not infer selection from row colour alone.'],
|
|
1630
|
+
markup: {
|
|
1631
|
+
html: `<div class="sf-bulk-actions" aria-label="Bulk actions"><p class="sf-bulk-actions__count" aria-live="polite">3 customers selected</p><div class="sf-bulk-actions__controls"><button class="sf-button sf-button--sm" type="button">Assign owner</button><button class="sf-button sf-button--destructive sf-button--sm" type="button">Delete selected</button></div></div>`,
|
|
1632
|
+
},
|
|
1633
|
+
keywords: ['bulk', 'actions', 'toolbar', 'selection', 'table', 'list'],
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
id: 'complete-pagination',
|
|
1637
|
+
name: 'Complete pagination',
|
|
1638
|
+
whenToUse: 'Server or app pagination that needs result context, page links, page size, and direct page entry.',
|
|
1639
|
+
classes: ['sf-pagination', 'sf-pagination__summary', 'sf-pagination__controls', 'sf-pagination__pages', 'sf-pagination__jump', 'sf-input', 'sf-select', 'sf-button'],
|
|
1640
|
+
requiresJs: false,
|
|
1641
|
+
requiresJsNotes: 'Links and GET forms work without JavaScript. Client-side data grids may synchronize the same controls with app state.',
|
|
1642
|
+
a11y: ['Label the navigation and current page.', 'Use a real label for page size and page entry.', 'Announce updated result context when content changes dynamically.'],
|
|
1643
|
+
gotchas: ['Keep URL parameters shareable for server pagination.', 'Validate page numbers on the server or in the app.'],
|
|
1644
|
+
markup: {
|
|
1645
|
+
html: `<nav class="sf-pagination" data-layout="complete" aria-label="Customer pages"><p class="sf-pagination__summary">Showing 21–40 of 126 customers</p><div class="sf-pagination__controls"><div class="sf-pagination__pages"><a href="?page=1">Previous</a><a href="?page=1">1</a><span aria-current="page">2</span><a href="?page=3">3</a><a href="?page=3">Next</a></div><form class="sf-pagination__jump" method="get"><label for="page-size">Rows</label><select class="sf-select" id="page-size" name="size"><option>20</option><option>50</option></select><label for="page-number">Page</label><input class="sf-input" id="page-number" name="page" type="number" min="1" max="7" value="2"><button class="sf-button sf-button--sm" type="submit">Go</button></form></div></nav>`,
|
|
1646
|
+
},
|
|
1647
|
+
keywords: ['pagination', 'pages', 'page size', 'jump', 'results', 'table'],
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
id: 'multi-step-form',
|
|
1651
|
+
name: 'Multi-step form',
|
|
1652
|
+
whenToUse: 'Long forms that need clear progress, a current step, completed steps, and predictable navigation.',
|
|
1653
|
+
classes: ['sf-stepper', 'sf-stepper__step', 'sf-stepper__marker', 'sf-stepper__label', 'sf-form', 'sf-wizard-actions', 'sf-button', 'sf-button--outline'],
|
|
1654
|
+
requiresJs: true,
|
|
1655
|
+
requiresJsNotes: 'The app owns step validation, persistence, navigation, focus placement, and URL or history state.',
|
|
1656
|
+
a11y: ['Use an ordered list and aria-current="step".', 'Prefix completed step labels with sr-only text such as "Completed:" so completion is not visual-only.', 'Move focus to the new step heading after navigation.', 'Preserve entered data and expose validation before advancing.'],
|
|
1657
|
+
gotchas: ['Do not make step markers tabs unless they control tab panels with full tab behavior.', 'Allow review and correction before final submission.'],
|
|
1658
|
+
markup: {
|
|
1659
|
+
html: `<nav aria-label="Application progress"><ol class="sf-stepper"><li class="sf-stepper__step" data-complete="true"><span class="sf-stepper__marker" aria-hidden="true">✓</span><span class="sf-stepper__label"><span class="sr-only">Completed: </span>Account</span></li><li class="sf-stepper__step" aria-current="step"><span class="sf-stepper__marker" aria-hidden="true"></span><span class="sf-stepper__label">Profile</span></li><li class="sf-stepper__step"><span class="sf-stepper__marker" aria-hidden="true"></span><span class="sf-stepper__label">Review</span></li></ol></nav><form class="sf-form" aria-labelledby="profile-step-title"><h2 id="profile-step-title">Profile</h2><label class="sf-field"><span class="sf-label">Role</span><input class="sf-input" name="role"></label><div class="sf-wizard-actions"><button class="sf-button sf-button--outline" type="button">Back</button><button class="sf-button" type="submit">Continue</button></div></form>`,
|
|
1660
|
+
},
|
|
1661
|
+
keywords: ['stepper', 'wizard', 'multi-step', 'form', 'onboarding', 'progress'],
|
|
1662
|
+
},
|
|
1496
1663
|
{
|
|
1497
1664
|
id: 'contact-form',
|
|
1498
1665
|
name: 'Accessible contact form',
|
|
@@ -1593,7 +1760,7 @@ function getPublicRecipes() {
|
|
|
1593
1760
|
<button class="sf-nav__link" type="submit" formaction="#contact" popovertarget="portfolio-mobile-menu" popovertargetaction="hide">Contact</button>
|
|
1594
1761
|
</form>
|
|
1595
1762
|
</nav>
|
|
1596
|
-
<main class="sf-scroll-viewport" data-snap="mandatory">
|
|
1763
|
+
<main class="sf-scroll-viewport" data-snap="mandatory" tabindex="0">
|
|
1597
1764
|
<section class="sf-scroll-panel" id="intro">
|
|
1598
1765
|
<div class="sf-container sf-stack">
|
|
1599
1766
|
<p class="sf-kicker">Selected work</p>
|
|
@@ -1733,7 +1900,7 @@ function getPublicRecipes() {
|
|
|
1733
1900
|
<button class="sf-button" type="submit">Apply</button>
|
|
1734
1901
|
</form>
|
|
1735
1902
|
<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>
|
|
1736
|
-
<div class="sf-table-wrap">
|
|
1903
|
+
<div class="sf-table-wrap" tabindex="0">
|
|
1737
1904
|
<table class="sf-data-table" aria-label="Recent activity">
|
|
1738
1905
|
<thead><tr><th>Item</th><th>Status</th><th>Value</th><th>Date</th></tr></thead>
|
|
1739
1906
|
<tbody>
|
|
@@ -1746,7 +1913,7 @@ function getPublicRecipes() {
|
|
|
1746
1913
|
</article>
|
|
1747
1914
|
<article class="sf-card sf-stack sf-detail-panel" id="activity">
|
|
1748
1915
|
<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>
|
|
1749
|
-
<div class="sf-skeleton" aria-hidden="true">Loading customer details</
|
|
1916
|
+
<div class="sf-skeleton" aria-hidden="true"></div><span class="sr-only">Loading customer details</span>
|
|
1750
1917
|
<ul class="sf-list-reset sf-data-list">
|
|
1751
1918
|
<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
1919
|
<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>
|
|
@@ -1769,6 +1936,36 @@ function getPublicRecipes() {
|
|
|
1769
1936
|
</section>
|
|
1770
1937
|
</main>
|
|
1771
1938
|
</div>`,
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
id: 'app-settings-workflow',
|
|
1942
|
+
name: 'Application settings workflow',
|
|
1943
|
+
whenToUse: 'Production app settings and onboarding forms that need richer native controls and explicit progress.',
|
|
1944
|
+
sections: ['settings-section', 'input-group', 'switch-control', 'segmented-control', 'native-file-and-range', 'searchable-select', 'loading-button', 'multi-step-form'],
|
|
1945
|
+
classes: ['sf-skip-link', 'sf-section', 'sf-container', 'sf-split', 'sf-card', 'sf-stack', 'sf-form', 'sf-field', 'sf-label', 'sf-help', 'sf-input', 'sf-input-group', 'sf-input-group__leading', 'sf-switch', 'sf-switch__control', 'sf-switch__label', 'sf-file-input', 'sf-range', 'sf-segmented-control', 'sf-segmented-control__option', 'sf-segmented-control__label', 'sf-combobox', 'sf-combobox__listbox', 'sf-combobox__option', 'sf-stepper', 'sf-stepper__step', 'sf-stepper__marker', 'sf-stepper__label', 'sf-wizard-actions', 'sf-button', 'sf-spinner'],
|
|
1946
|
+
keywords: ['app', 'settings', 'workflow', 'onboarding', 'form', 'switch', 'input group', 'file', 'range', 'segmented', 'combobox', 'stepper', 'wizard', 'loading'],
|
|
1947
|
+
markup: `<a class="sf-skip-link" href="#settings-main">Skip to settings</a>
|
|
1948
|
+
<main class="sf-section" id="settings-main">
|
|
1949
|
+
<div class="sf-container sf-split">
|
|
1950
|
+
<nav aria-label="Setup progress">
|
|
1951
|
+
<ol class="sf-stepper">
|
|
1952
|
+
<li class="sf-stepper__step" data-complete="true"><span class="sf-stepper__marker" aria-hidden="true">✓</span><span class="sf-stepper__label"><span class="sr-only">Completed: </span>Workspace</span></li>
|
|
1953
|
+
<li class="sf-stepper__step" aria-current="step"><span class="sf-stepper__marker" aria-hidden="true"></span><span class="sf-stepper__label">Preferences</span></li>
|
|
1954
|
+
<li class="sf-stepper__step"><span class="sf-stepper__marker" aria-hidden="true"></span><span class="sf-stepper__label">Review</span></li>
|
|
1955
|
+
</ol>
|
|
1956
|
+
</nav>
|
|
1957
|
+
<form class="sf-card sf-form" aria-labelledby="preferences-title">
|
|
1958
|
+
<header class="sf-stack"><p class="sf-kicker">Step 2 of 3</p><h1 id="preferences-title">Workspace preferences</h1></header>
|
|
1959
|
+
<label class="sf-field"><span class="sf-label">Workspace URL</span><span class="sf-input-group"><span class="sf-input-group__leading" aria-hidden="true">https://</span><input class="sf-input" name="workspace" autocomplete="organization"><span class="sf-input-group__trailing" aria-hidden="true">.example.com</span></span></label>
|
|
1960
|
+
<fieldset class="sf-fieldset"><legend>Billing cycle</legend><div class="sf-segmented-control"><label class="sf-segmented-control__option"><input type="radio" name="cycle" value="monthly" checked><span class="sf-segmented-control__label">Monthly</span></label><label class="sf-segmented-control__option"><input type="radio" name="cycle" value="annual"><span class="sf-segmented-control__label">Annual</span></label></div></fieldset>
|
|
1961
|
+
<label class="sf-switch"><input type="checkbox" role="switch" name="weekly_summary" checked><span class="sf-switch__control" aria-hidden="true"></span><span class="sf-switch__label">Email weekly summary</span></label>
|
|
1962
|
+
<label class="sf-field"><span class="sf-label" id="settings-team-size-label">Team size: <output for="settings-team-size">12</output></span><input class="sf-range" id="settings-team-size" type="range" name="team_size" min="1" max="50" value="12" aria-labelledby="settings-team-size-label"></label>
|
|
1963
|
+
<label class="sf-field"><span class="sf-label">Brand mark</span><input class="sf-file-input" type="file" name="brand_mark" accept="image/png,image/svg+xml"><span class="sf-help">PNG or SVG.</span></label>
|
|
1964
|
+
<div class="sf-field"><label class="sf-label" for="owner-search">Workspace owner</label><div class="sf-combobox"><input class="sf-input" id="owner-search" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="owner-options"><ul class="sf-combobox__listbox" id="owner-options" role="listbox" hidden><li class="sf-combobox__option" role="option">Alex Morgan</li></ul></div><span class="sf-help">The app must implement combobox keyboard and state behavior.</span></div>
|
|
1965
|
+
<div class="sf-wizard-actions"><button class="sf-button sf-button--outline" type="button">Back</button><button class="sf-button" type="submit"><span class="sf-spinner" aria-hidden="true" hidden></span><span>Continue</span></button></div>
|
|
1966
|
+
</form>
|
|
1967
|
+
</div>
|
|
1968
|
+
</main>`,
|
|
1772
1969
|
},
|
|
1773
1970
|
{
|
|
1774
1971
|
id: 'agency-home',
|
|
@@ -1823,7 +2020,7 @@ function getPublicRecipes() {
|
|
|
1823
2020
|
<figure class="sf-figure"><div class="sf-frame"></div><figcaption class="sf-caption">Use real project imagery here.</figcaption></figure>
|
|
1824
2021
|
<h2>Start with primitives</h2>
|
|
1825
2022
|
<p>Use semantic colours, fluid spacing, and named components before adding custom CSS.</p>
|
|
1826
|
-
<div class="sf-table-wrap"><table><thead><tr><th>Layer</th><th>Use</th></tr></thead><tbody><tr><td>Tokens</td><td>Brand decisions</td></tr><tr><td>Components</td><td>Common UI</td></tr></tbody></table></div>
|
|
2023
|
+
<div class="sf-table-wrap" tabindex="0"><table><thead><tr><th>Layer</th><th>Use</th></tr></thead><tbody><tr><td>Tokens</td><td>Brand decisions</td></tr><tr><td>Components</td><td>Common UI</td></tr></tbody></table></div>
|
|
1827
2024
|
</article>
|
|
1828
2025
|
</main>`,
|
|
1829
2026
|
},
|
|
@@ -2054,6 +2251,7 @@ function runPattern() {
|
|
|
2054
2251
|
|
|
2055
2252
|
function patternMarkupFor(pattern, framework = 'html') {
|
|
2056
2253
|
if (typeof pattern.markup === 'string') return pattern.markup
|
|
2254
|
+
if (framework === 'react') return pattern.markup?.react ?? pattern.markup?.next ?? pattern.markup?.html ?? ''
|
|
2057
2255
|
return pattern.markup?.[framework] ?? pattern.markup?.html ?? ''
|
|
2058
2256
|
}
|
|
2059
2257
|
|
|
@@ -2339,25 +2537,60 @@ ${indentMarkup(htmlToJsx(markup), 6)}
|
|
|
2339
2537
|
}
|
|
2340
2538
|
|
|
2341
2539
|
function htmlToJsx(markup) {
|
|
2540
|
+
const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
|
|
2541
|
+
const attributeNames = new Map([
|
|
2542
|
+
['class', 'className'],
|
|
2543
|
+
['for', 'htmlFor'],
|
|
2544
|
+
['autocomplete', 'autoComplete'],
|
|
2545
|
+
['inputmode', 'inputMode'],
|
|
2546
|
+
['readonly', 'readOnly'],
|
|
2547
|
+
['tabindex', 'tabIndex'],
|
|
2548
|
+
['formaction', 'formAction'],
|
|
2549
|
+
['formmethod', 'formMethod'],
|
|
2550
|
+
['formenctype', 'formEncType'],
|
|
2551
|
+
['formnovalidate', 'formNoValidate'],
|
|
2552
|
+
['formtarget', 'formTarget'],
|
|
2553
|
+
['accept-charset', 'acceptCharset'],
|
|
2554
|
+
['charset', 'charSet'],
|
|
2555
|
+
['colspan', 'colSpan'],
|
|
2556
|
+
['rowspan', 'rowSpan'],
|
|
2557
|
+
['maxlength', 'maxLength'],
|
|
2558
|
+
['minlength', 'minLength'],
|
|
2559
|
+
['contenteditable', 'contentEditable'],
|
|
2560
|
+
['crossorigin', 'crossOrigin'],
|
|
2561
|
+
['referrerpolicy', 'referrerPolicy'],
|
|
2562
|
+
['usemap', 'useMap'],
|
|
2563
|
+
['datetime', 'dateTime'],
|
|
2564
|
+
['popovertarget', 'popoverTarget'],
|
|
2565
|
+
['popovertargetaction', 'popoverTargetAction'],
|
|
2566
|
+
['commandfor', 'commandFor'],
|
|
2567
|
+
['stroke-linecap', 'strokeLinecap'],
|
|
2568
|
+
['stroke-linejoin', 'strokeLinejoin'],
|
|
2569
|
+
['stroke-width', 'strokeWidth'],
|
|
2570
|
+
['fill-rule', 'fillRule'],
|
|
2571
|
+
['clip-rule', 'clipRule'],
|
|
2572
|
+
])
|
|
2573
|
+
|
|
2342
2574
|
return markup
|
|
2343
|
-
.replace(
|
|
2344
|
-
.replace(
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2575
|
+
.replace(/<!--([\s\S]*?)-->/g, '{/*$1*/}')
|
|
2576
|
+
.replace(/<([A-Za-z][\w:-]*)(\s[^<>]*?)?\s*(\/?)>/g, (match, rawTagName, rawAttributes = '', selfClosing = '') => {
|
|
2577
|
+
const tagName = rawTagName.toLowerCase()
|
|
2578
|
+
let attributes = rawAttributes.replace(/\s+$/, '')
|
|
2579
|
+
|
|
2580
|
+
for (const [htmlName, jsxName] of attributeNames) {
|
|
2581
|
+
attributes = attributes.replace(new RegExp(`(\\s)${htmlName}(?=\\s*=)`, 'gi'), `$1${jsxName}`)
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
attributes = attributes.replace(/(\s)checked(?=\s|$)/gi, '$1defaultChecked')
|
|
2585
|
+
attributes = attributes.replace(/(\s)popover(?=\s|$)/gi, '$1popover=""')
|
|
2586
|
+
attributes = attributes.replace(/(\s)(rows|cols|size|span|tabIndex)=["'](\d+)["']/gi, '$1$2={$3}')
|
|
2587
|
+
if (['input', 'select', 'textarea'].includes(tagName) && !/\stype\s*=\s*["'](?:checkbox|radio)["']/i.test(attributes)) {
|
|
2588
|
+
attributes = attributes.replace(/(\s)value(?=\s*=)/gi, '$1defaultValue')
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const close = voidElements.has(tagName) || selfClosing ? ' />' : '>'
|
|
2592
|
+
return `<${rawTagName}${attributes}${close}`
|
|
2593
|
+
})
|
|
2361
2594
|
}
|
|
2362
2595
|
|
|
2363
2596
|
function indentMarkup(markup, spaces) {
|
|
@@ -4795,6 +5028,8 @@ function buildComponentCss() {
|
|
|
4795
5028
|
|
|
4796
5029
|
.sf-data-table :where(th button, .sf-table-sort) {
|
|
4797
5030
|
align-items: center;
|
|
5031
|
+
background: transparent;
|
|
5032
|
+
border: 0;
|
|
4798
5033
|
color: inherit;
|
|
4799
5034
|
display: inline-flex;
|
|
4800
5035
|
font: inherit;
|
|
@@ -4803,14 +5038,14 @@ function buildComponentCss() {
|
|
|
4803
5038
|
padding: 0;
|
|
4804
5039
|
}
|
|
4805
5040
|
|
|
4806
|
-
.sf-data-table :where(
|
|
5041
|
+
.sf-data-table th[aria-sort] :where(button, .sf-table-sort)::after {
|
|
4807
5042
|
color: var(--color-muted-foreground);
|
|
4808
5043
|
content: "\\2195";
|
|
4809
5044
|
font-size: var(--step--2);
|
|
4810
5045
|
}
|
|
4811
5046
|
|
|
4812
|
-
.sf-data-table :where(
|
|
4813
|
-
.sf-data-table :where(
|
|
5047
|
+
.sf-data-table th[aria-sort="ascending"] :where(button, .sf-table-sort)::after { content: "\\2191"; }
|
|
5048
|
+
.sf-data-table th[aria-sort="descending"] :where(button, .sf-table-sort)::after { content: "\\2193"; }
|
|
4814
5049
|
|
|
4815
5050
|
.sf-data-table tr[data-clickable="true"] {
|
|
4816
5051
|
cursor: pointer;
|
|
@@ -4885,7 +5120,7 @@ function buildComponentCss() {
|
|
|
4885
5120
|
}
|
|
4886
5121
|
|
|
4887
5122
|
.sf-status::before {
|
|
4888
|
-
background: currentColor;
|
|
5123
|
+
background: var(--status-dot, currentColor);
|
|
4889
5124
|
block-size: .42em;
|
|
4890
5125
|
border-radius: var(--radius-full);
|
|
4891
5126
|
content: "";
|
|
@@ -4895,25 +5130,29 @@ function buildComponentCss() {
|
|
|
4895
5130
|
.sf-status[data-tone="success"] {
|
|
4896
5131
|
--status-bg: var(--sf-colour-success-soft, oklch(62% 0.13 150 / 0.12));
|
|
4897
5132
|
--status-border: color-mix(in oklch, var(--sf-colour-success, oklch(62% 0.13 150)) 34%, transparent);
|
|
4898
|
-
--status-color:
|
|
5133
|
+
--status-color: var(--color-foreground);
|
|
5134
|
+
--status-dot: var(--sf-colour-success, oklch(62% 0.13 150));
|
|
4899
5135
|
}
|
|
4900
5136
|
|
|
4901
5137
|
.sf-status[data-tone="warning"] {
|
|
4902
5138
|
--status-bg: var(--sf-colour-warning-soft, oklch(78% 0.15 82 / 0.16));
|
|
4903
5139
|
--status-border: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 38%, transparent);
|
|
4904
|
-
--status-color:
|
|
5140
|
+
--status-color: var(--color-foreground);
|
|
5141
|
+
--status-dot: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 60%, var(--color-foreground));
|
|
4905
5142
|
}
|
|
4906
5143
|
|
|
4907
5144
|
.sf-status[data-tone="danger"] {
|
|
4908
5145
|
--status-bg: var(--sf-colour-danger-soft, oklch(56% 0.2 28 / 0.12));
|
|
4909
5146
|
--status-border: color-mix(in oklch, var(--sf-colour-danger, var(--color-destructive)) 34%, transparent);
|
|
4910
|
-
--status-color:
|
|
5147
|
+
--status-color: var(--color-foreground);
|
|
5148
|
+
--status-dot: var(--sf-colour-danger, var(--color-destructive));
|
|
4911
5149
|
}
|
|
4912
5150
|
|
|
4913
5151
|
.sf-status[data-tone="info"] {
|
|
4914
5152
|
--status-bg: var(--sf-colour-info-soft, oklch(70% 0.12 205 / 0.14));
|
|
4915
5153
|
--status-border: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 34%, transparent);
|
|
4916
|
-
--status-color:
|
|
5154
|
+
--status-color: var(--color-foreground);
|
|
5155
|
+
--status-dot: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 68%, var(--color-foreground));
|
|
4917
5156
|
}
|
|
4918
5157
|
|
|
4919
5158
|
.sf-data-list {
|
|
@@ -4961,9 +5200,16 @@ function buildComponentCss() {
|
|
|
4961
5200
|
background: var(--color-surface-alt);
|
|
4962
5201
|
}
|
|
4963
5202
|
|
|
5203
|
+
.sf-form {
|
|
5204
|
+
display: grid;
|
|
5205
|
+
gap: var(--form-gap, var(--space-s));
|
|
5206
|
+
grid-template-columns: minmax(0, 1fr);
|
|
5207
|
+
}
|
|
5208
|
+
|
|
4964
5209
|
.sf-field {
|
|
4965
5210
|
display: grid;
|
|
4966
5211
|
gap: var(--space-2xs);
|
|
5212
|
+
grid-template-columns: minmax(0, 1fr);
|
|
4967
5213
|
}
|
|
4968
5214
|
|
|
4969
5215
|
.sf-form[data-density="compact"],
|
|
@@ -5026,6 +5272,130 @@ function buildComponentCss() {
|
|
|
5026
5272
|
outline: 0.125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
|
|
5027
5273
|
outline-offset: 0;
|
|
5028
5274
|
}
|
|
5275
|
+
|
|
5276
|
+
.sf-spinner {
|
|
5277
|
+
animation: sf-spinner-rotate .75s linear infinite;
|
|
5278
|
+
block-size: var(--spinner-size, 1em);
|
|
5279
|
+
border: .125em solid color-mix(in oklch, currentColor 28%, transparent);
|
|
5280
|
+
border-block-start-color: currentColor;
|
|
5281
|
+
border-radius: var(--radius-full);
|
|
5282
|
+
display: inline-block;
|
|
5283
|
+
flex: 0 0 auto;
|
|
5284
|
+
inline-size: var(--spinner-size, 1em);
|
|
5285
|
+
}
|
|
5286
|
+
|
|
5287
|
+
@keyframes sf-spinner-rotate { to { transform: rotate(1turn); } }
|
|
5288
|
+
|
|
5289
|
+
.sf-input-group {
|
|
5290
|
+
align-items: stretch;
|
|
5291
|
+
background: var(--color-card);
|
|
5292
|
+
border: 1px solid var(--color-border);
|
|
5293
|
+
border-radius: var(--input-radius, var(--radius-md));
|
|
5294
|
+
display: flex;
|
|
5295
|
+
inline-size: 100%;
|
|
5296
|
+
min-block-size: var(--input-block-size, 2.75rem);
|
|
5297
|
+
overflow: clip;
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
.sf-input-group:focus-within {
|
|
5301
|
+
border-color: var(--color-ring);
|
|
5302
|
+
outline: .125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
.sf-input-group > .sf-input { background: transparent; border: 0; border-radius: 0; flex: 1 1 0%; inline-size: auto; min-inline-size: 0; outline: 0; }
|
|
5306
|
+
.sf-input-group__leading,
|
|
5307
|
+
.sf-input-group__trailing { align-items: center; color: var(--color-muted-foreground); display: inline-flex; flex: 0 0 auto; padding-inline: var(--space-xs); white-space: nowrap; }
|
|
5308
|
+
.sf-input-group__leading { border-inline-end: 1px solid var(--color-border); }
|
|
5309
|
+
.sf-input-group__trailing,
|
|
5310
|
+
.sf-input-group__action { border-inline-start: 1px solid var(--color-border); }
|
|
5311
|
+
.sf-input-group__action { background: transparent; border-block: 0; border-inline-end: 0; color: var(--color-foreground); font-weight: 700; padding-inline: var(--space-xs); }
|
|
5312
|
+
.sf-input-group[data-invalid="true"],
|
|
5313
|
+
.sf-field[data-invalid="true"] .sf-input-group { border-color: var(--color-destructive, var(--color-danger)); }
|
|
5314
|
+
.sf-input-group:has(:disabled) { background: var(--color-surface-alt); opacity: .72; }
|
|
5315
|
+
|
|
5316
|
+
.sf-file-input {
|
|
5317
|
+
background: var(--color-card);
|
|
5318
|
+
border: 1px solid var(--color-border);
|
|
5319
|
+
border-radius: var(--input-radius, var(--radius-md));
|
|
5320
|
+
color: var(--color-muted-foreground);
|
|
5321
|
+
inline-size: 100%;
|
|
5322
|
+
min-block-size: var(--input-block-size, 2.75rem);
|
|
5323
|
+
padding: var(--space-3xs);
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
.sf-file-input::file-selector-button { background: var(--color-surface-alt); border: 1px solid var(--color-border); border-radius: var(--radius-sm); color: var(--color-foreground); font: inherit; font-weight: 700; margin-inline-end: var(--space-xs); min-block-size: 2.25rem; padding-inline: var(--space-xs); }
|
|
5327
|
+
.sf-range { accent-color: var(--color-primary); inline-size: 100%; min-block-size: var(--input-block-size, 2.75rem); }
|
|
5328
|
+
.sf-range:focus-visible,
|
|
5329
|
+
.sf-file-input:focus-visible { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
|
|
5330
|
+
.sf-file-input[aria-invalid="true"],
|
|
5331
|
+
.sf-field[data-invalid="true"] .sf-file-input { border-color: var(--color-destructive, var(--color-danger)); }
|
|
5332
|
+
:where(.sf-file-input, .sf-range):disabled { opacity: .55; }
|
|
5333
|
+
|
|
5334
|
+
.sf-switch { align-items: center; display: inline-grid; gap: var(--space-xs); grid-template-columns: auto 1fr; position: relative; }
|
|
5335
|
+
.sf-switch > input[type="checkbox"],
|
|
5336
|
+
.sf-segmented-control__option > input[type="radio"] { block-size: .0625rem; clip: rect(0 0 0 0); clip-path: inset(50%); inline-size: .0625rem; overflow: hidden; position: absolute; white-space: nowrap; }
|
|
5337
|
+
.sf-switch__control { background: var(--color-surface-alt); border: 1px solid var(--color-border); border-radius: var(--radius-full); block-size: 1.5rem; display: inline-flex; inline-size: 2.75rem; padding: .1875rem; }
|
|
5338
|
+
.sf-switch__control::before { background: var(--color-foreground); border-radius: var(--radius-full); content: ""; inline-size: 1rem; transition: transform var(--duration-fast) var(--ease-standard); }
|
|
5339
|
+
.sf-switch > input:checked + .sf-switch__control { background: var(--color-primary); border-color: var(--color-primary); }
|
|
5340
|
+
.sf-switch > input:checked + .sf-switch__control::before { background: var(--color-primary-foreground); transform: translateX(1.1875rem); }
|
|
5341
|
+
.sf-switch > input:focus-visible + .sf-switch__control { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
|
|
5342
|
+
.sf-switch__label { color: var(--color-foreground); font-weight: 700; }
|
|
5343
|
+
.sf-switch:has(input:disabled) { cursor: not-allowed; opacity: .55; }
|
|
5344
|
+
|
|
5345
|
+
.sf-segmented-control { background: var(--color-surface-alt); border: 1px solid var(--color-border); border-radius: var(--radius-md); display: inline-flex; flex-wrap: wrap; gap: var(--space-3xs); padding: var(--space-3xs); }
|
|
5346
|
+
.sf-segmented-control__option { cursor: pointer; position: relative; }
|
|
5347
|
+
.sf-segmented-control__label { align-items: center; border: 1px solid transparent; border-radius: var(--radius-sm); color: var(--color-muted-foreground); display: inline-flex; font-size: var(--step--1); font-weight: 700; min-block-size: 2.25rem; padding-inline: var(--space-xs); }
|
|
5348
|
+
.sf-segmented-control__option > input:checked + .sf-segmented-control__label { background: var(--color-card); border-color: var(--color-border); color: var(--color-foreground); }
|
|
5349
|
+
.sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
|
|
5350
|
+
.sf-segmented-control__option:has(input:disabled) { cursor: not-allowed; opacity: .55; }
|
|
5351
|
+
|
|
5352
|
+
.sf-combobox { position: relative; }
|
|
5353
|
+
.sf-combobox__listbox { background: var(--color-card); border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); display: grid; gap: var(--space-3xs); inset-block-start: calc(100% + var(--space-3xs)); inset-inline: 0; list-style: none; max-block-size: min(18rem, 50dvh); overflow: auto; padding: var(--space-2xs); position: absolute; z-index: 40; }
|
|
5354
|
+
.sf-combobox__option { border-radius: var(--radius-sm); cursor: pointer; padding: var(--space-2xs) var(--space-xs); }
|
|
5355
|
+
.sf-combobox__option:hover,
|
|
5356
|
+
.sf-combobox__option[aria-selected="true"],
|
|
5357
|
+
.sf-combobox__option[data-active="true"] { background: var(--color-surface-alt); }
|
|
5358
|
+
.sf-combobox__empty { color: var(--color-muted-foreground); padding: var(--space-xs); }
|
|
5359
|
+
|
|
5360
|
+
.sf-bulk-actions { align-items: center; background: color-mix(in oklch, var(--color-primary) 12%, transparent); border: 1px solid color-mix(in oklch, var(--color-primary) 30%, var(--color-border)); border-radius: var(--radius-md); display: flex; flex-wrap: wrap; gap: var(--space-xs); justify-content: space-between; padding: var(--space-xs); }
|
|
5361
|
+
.sf-bulk-actions__count { font-size: var(--step--1); font-weight: 800; }
|
|
5362
|
+
.sf-bulk-actions__controls,
|
|
5363
|
+
.sf-pagination__controls,
|
|
5364
|
+
.sf-pagination__pages,
|
|
5365
|
+
.sf-pagination__jump { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
|
|
5366
|
+
|
|
5367
|
+
.sf-pagination { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); list-style: none; padding-inline-start: 0; }
|
|
5368
|
+
.sf-pagination[data-layout="complete"] { justify-content: space-between; row-gap: var(--space-xs); }
|
|
5369
|
+
.sf-pagination__summary,
|
|
5370
|
+
.sf-pagination__jump { color: var(--color-muted-foreground); font-size: var(--step--1); }
|
|
5371
|
+
.sf-pagination__jump .sf-input { inline-size: 5rem; }
|
|
5372
|
+
|
|
5373
|
+
.sf-stepper { counter-reset: sf-step; display: grid; gap: var(--space-xs); list-style: none; padding-inline-start: 0; }
|
|
5374
|
+
.sf-stepper__step { align-items: center; color: var(--color-muted-foreground); counter-increment: sf-step; display: grid; gap: var(--space-xs); grid-template-columns: auto 1fr; }
|
|
5375
|
+
.sf-stepper__marker { align-items: center; border: 1px solid var(--color-border); border-radius: var(--radius-full); block-size: 2rem; display: inline-flex; font-size: var(--step--1); font-weight: 800; inline-size: 2rem; justify-content: center; }
|
|
5376
|
+
.sf-stepper__marker:empty::before { content: counter(sf-step); }
|
|
5377
|
+
.sf-stepper__step[aria-current="step"] { color: var(--color-foreground); }
|
|
5378
|
+
.sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-foreground); }
|
|
5379
|
+
.sf-stepper__label { font-weight: 700; }
|
|
5380
|
+
.sf-wizard-actions { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-xs); justify-content: space-between; }
|
|
5381
|
+
|
|
5382
|
+
@media (prefers-reduced-motion: reduce) {
|
|
5383
|
+
.sf-spinner { animation: none; border-color: currentColor; border-inline-end-color: transparent; }
|
|
5384
|
+
.sf-switch__control::before { transition: none; }
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5387
|
+
@media (forced-colors: active) {
|
|
5388
|
+
:where(.sf-input-group, .sf-file-input, .sf-combobox__listbox, .sf-bulk-actions) { border-color: ButtonText; forced-color-adjust: auto; }
|
|
5389
|
+
:where(.sf-switch__control, .sf-segmented-control__label, .sf-stepper__marker) { background: Canvas; border-color: ButtonText; color: ButtonText; forced-color-adjust: none; }
|
|
5390
|
+
.sf-switch > input:focus-visible + .sf-switch__control,
|
|
5391
|
+
.sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid Highlight; outline-offset: .125rem; }
|
|
5392
|
+
.sf-switch > input:checked + .sf-switch__control,
|
|
5393
|
+
.sf-segmented-control__option > input:checked + .sf-segmented-control__label,
|
|
5394
|
+
.sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: Highlight; border-color: Highlight; color: HighlightText; }
|
|
5395
|
+
.sf-switch__control::before { background: ButtonText; }
|
|
5396
|
+
.sf-switch > input:checked + .sf-switch__control::before { background: HighlightText; }
|
|
5397
|
+
.sf-stepper__step[data-complete="true"] .sf-stepper__marker { background: Canvas; border-color: LinkText; color: LinkText; }
|
|
5398
|
+
}
|
|
5029
5399
|
}`
|
|
5030
5400
|
}
|
|
5031
5401
|
|