@syncedco/flow 0.3.2 → 0.5.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.
- package/CHANGELOG.md +51 -0
- package/CONTRIBUTING.md +18 -5
- package/README.md +32 -9
- package/bin/synced-flow.mjs +515 -83
- 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 -23
- 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) {
|
|
@@ -3385,6 +3618,12 @@ function rawDeclarationsFor(base) {
|
|
|
3385
3618
|
[/^overflow-x-(hidden|visible|auto|scroll|clip)$/, 'overflow-x'],
|
|
3386
3619
|
[/^overflow-y-(hidden|visible|auto|scroll|clip)$/, 'overflow-y'],
|
|
3387
3620
|
[/^object-(cover|contain|fill|none|scale-down)$/, 'object-fit'],
|
|
3621
|
+
[/^object-(center|top|bottom|left|right|left-top|left-bottom|right-top|right-bottom)$/, 'object-position'],
|
|
3622
|
+
[/^bg-(cover|contain|auto)$/, 'background-size'],
|
|
3623
|
+
[/^bg-(center|top|bottom|left|right|left-top|left-bottom|right-top|right-bottom)$/, 'background-position'],
|
|
3624
|
+
[/^bg-(repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, 'background-repeat'],
|
|
3625
|
+
[/^bg-(fixed|local|scroll)$/, 'background-attachment'],
|
|
3626
|
+
[/^origin-(center|top|bottom|left|right|top-left|top-right|bottom-left|bottom-right)$/, 'transform-origin'],
|
|
3388
3627
|
[/^whitespace-(normal|nowrap|pre|pre-line|pre-wrap|break-spaces)$/, 'white-space'],
|
|
3389
3628
|
[/^break-(normal|words|all|keep)$/, 'overflow-wrap'],
|
|
3390
3629
|
[/^cursor-(pointer|default|not-allowed|grab|grabbing|text)$/, 'cursor'],
|
|
@@ -3394,11 +3633,17 @@ function rawDeclarationsFor(base) {
|
|
|
3394
3633
|
]
|
|
3395
3634
|
for (const [pattern, property] of simpleMaps) {
|
|
3396
3635
|
const match = base.match(pattern)
|
|
3397
|
-
if (match)
|
|
3636
|
+
if (match) {
|
|
3637
|
+
if (match[1] === 'words') return [[property, 'break-word']]
|
|
3638
|
+
// Positions are written `left-top` in the class and `left top` in CSS.
|
|
3639
|
+
return [[property, /-position$|-origin$/.test(property) ? match[1].replace('-', ' ') : match[1]]]
|
|
3640
|
+
}
|
|
3398
3641
|
}
|
|
3399
3642
|
|
|
3400
3643
|
if (base === 'appearance-none') return [['appearance', 'none']]
|
|
3401
3644
|
if (base === 'resize') return [['resize', 'both']]
|
|
3645
|
+
if (base === 'text-ellipsis') return [['text-overflow', 'ellipsis']]
|
|
3646
|
+
if (base === 'text-clip') return [['text-overflow', 'clip']]
|
|
3402
3647
|
if (base === 'truncate') return [['overflow', 'hidden'], ['text-overflow', 'ellipsis'], ['white-space', 'nowrap']]
|
|
3403
3648
|
const lineClamp = base.match(/^line-clamp-(\d+)$/)
|
|
3404
3649
|
if (lineClamp) return [['overflow', 'hidden'], ['display', '-webkit-box'], ['-webkit-box-orient', 'vertical'], ['-webkit-line-clamp', lineClamp[1]]]
|
|
@@ -3446,7 +3691,8 @@ function sideUtility(base, prefixes) {
|
|
|
3446
3691
|
const match = base.match(new RegExp(`^-?${prefix}-(.+)$`))
|
|
3447
3692
|
if (!match) continue
|
|
3448
3693
|
const negative = base.startsWith('-')
|
|
3449
|
-
const
|
|
3694
|
+
const axis = prefix === 'top' || prefix === 'bottom' ? 'y' : 'x'
|
|
3695
|
+
const value = spacingUnit(match[1]) ?? sizeKeyword(match[1], axis)
|
|
3450
3696
|
if (!value) continue
|
|
3451
3697
|
const resolved = negative ? `calc(${value} * -1)` : value
|
|
3452
3698
|
if (prefix === 'inset') return [['inset', resolved]]
|
|
@@ -3487,18 +3733,22 @@ function spacingUtility(base) {
|
|
|
3487
3733
|
'gap-x': ['column-gap'],
|
|
3488
3734
|
'gap-y': ['row-gap'],
|
|
3489
3735
|
}
|
|
3490
|
-
if (prefix === 'space-x') return [['
|
|
3491
|
-
if (prefix === 'space-y') return [['
|
|
3736
|
+
if (prefix === 'space-x') return [['margin-inline-start', resolved]]
|
|
3737
|
+
if (prefix === 'space-y') return [['margin-block-start', resolved]]
|
|
3492
3738
|
return map[prefix].map((property) => [property, resolved])
|
|
3493
3739
|
}
|
|
3494
3740
|
|
|
3495
|
-
|
|
3496
|
-
|
|
3741
|
+
// Keyword sizes. `screen` is swapped for the viewport height on the y axis.
|
|
3742
|
+
const sizeKeywords = {
|
|
3497
3743
|
auto: 'auto',
|
|
3498
3744
|
full: '100%',
|
|
3499
3745
|
screen: '100svw',
|
|
3500
3746
|
svw: '100svw',
|
|
3501
3747
|
svh: '100svh',
|
|
3748
|
+
dvw: '100dvw',
|
|
3749
|
+
dvh: '100dvh',
|
|
3750
|
+
lvw: '100lvw',
|
|
3751
|
+
lvh: '100lvh',
|
|
3502
3752
|
min: 'min-content',
|
|
3503
3753
|
max: 'max-content',
|
|
3504
3754
|
fit: 'fit-content',
|
|
@@ -3525,15 +3775,21 @@ function sizeKeyword(valueName) {
|
|
|
3525
3775
|
'4/5': '80%',
|
|
3526
3776
|
'1/6': '16.666667%',
|
|
3527
3777
|
'5/6': '83.333333%',
|
|
3528
|
-
|
|
3529
|
-
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
function sizeKeyword(valueName, axis = 'x') {
|
|
3781
|
+
if (valueName === 'screen' && axis === 'y') return '100svh'
|
|
3782
|
+
return Object.hasOwn(sizeKeywords, valueName) ? sizeKeywords[valueName] : null
|
|
3530
3783
|
}
|
|
3531
3784
|
|
|
3532
3785
|
function sizingUtility(base) {
|
|
3533
3786
|
const match = base.match(/^(w|h|min-w|min-h|max-w|max-h|size|basis)-(.+)$/)
|
|
3534
3787
|
if (!match) return null
|
|
3535
3788
|
const [, prefix, valueName] = match
|
|
3536
|
-
|
|
3789
|
+
if ((prefix === 'max-w' || prefix === 'max-h') && valueName === 'none') return [[prefix === 'max-w' ? 'max-width' : 'max-height', 'none']]
|
|
3790
|
+
if (prefix === 'size' && valueName === 'screen') return [['width', '100svw'], ['height', '100svh']]
|
|
3791
|
+
const axis = prefix === 'h' || prefix === 'min-h' || prefix === 'max-h' ? 'y' : 'x'
|
|
3792
|
+
const value = spacingUnit(valueName) ?? sizeKeyword(valueName, axis)
|
|
3537
3793
|
if (!value) return null
|
|
3538
3794
|
const props = {
|
|
3539
3795
|
w: ['width'],
|
|
@@ -3559,6 +3815,7 @@ function flexGridUtility(base) {
|
|
|
3559
3815
|
'flex-1': [['flex', '1 1 0%']],
|
|
3560
3816
|
'flex-auto': [['flex', '1 1 auto']],
|
|
3561
3817
|
'flex-none': [['flex', 'none']],
|
|
3818
|
+
'flex-initial': [['flex', '0 1 auto']],
|
|
3562
3819
|
grow: [['flex-grow', '1']],
|
|
3563
3820
|
'grow-0': [['flex-grow', '0']],
|
|
3564
3821
|
shrink: [['flex-shrink', '1']],
|
|
@@ -3585,6 +3842,12 @@ function flexGridUtility(base) {
|
|
|
3585
3842
|
const placeMap = { center: 'center', start: 'start', end: 'end', stretch: 'stretch' }
|
|
3586
3843
|
let match = base.match(/^items-(start|end|center|baseline|stretch)$/)
|
|
3587
3844
|
if (match) return [['align-items', alignMap[match[1]]]]
|
|
3845
|
+
match = base.match(/^justify-items-(start|end|center|stretch)$/)
|
|
3846
|
+
if (match) return [['justify-items', match[1]]]
|
|
3847
|
+
match = base.match(/^justify-self-(auto|start|end|center|stretch)$/)
|
|
3848
|
+
if (match) return [['justify-self', match[1]]]
|
|
3849
|
+
match = base.match(/^order-(\d+|first|last|none)$/)
|
|
3850
|
+
if (match) return [['order', { first: '-9999', last: '9999', none: '0' }[match[1]] ?? match[1]]]
|
|
3588
3851
|
match = base.match(/^justify-(start|end|center|between|around|evenly|stretch)$/)
|
|
3589
3852
|
if (match) return [['justify-content', justifyMap[match[1]]]]
|
|
3590
3853
|
match = base.match(/^content-(start|end|center|between|around|evenly|stretch)$/)
|
|
@@ -3603,6 +3866,8 @@ function flexGridUtility(base) {
|
|
|
3603
3866
|
match = base.match(/^row-span-(\d+)$/)
|
|
3604
3867
|
if (match) return [['grid-row', `span ${match[1]} / span ${match[1]}`]]
|
|
3605
3868
|
if (base === 'col-span-full') return [['grid-column', '1 / -1']]
|
|
3869
|
+
match = base.match(/^(col|row)-(start|end)-(\d+|auto)$/)
|
|
3870
|
+
if (match) return [[`grid-${match[1] === 'col' ? 'column' : 'row'}-${match[2]}`, match[3]]]
|
|
3606
3871
|
if (base === 'grid-flow-col') return [['grid-auto-flow', 'column']]
|
|
3607
3872
|
if (base === 'grid-flow-row') return [['grid-auto-flow', 'row']]
|
|
3608
3873
|
|
|
@@ -3610,10 +3875,11 @@ function flexGridUtility(base) {
|
|
|
3610
3875
|
}
|
|
3611
3876
|
|
|
3612
3877
|
function typographyUtility(base) {
|
|
3613
|
-
let match = base.match(/^font-(sans|display|mono)$/)
|
|
3878
|
+
let match = base.match(/^font-(sans|serif|display|mono)$/)
|
|
3614
3879
|
if (match) {
|
|
3615
3880
|
const map = {
|
|
3616
3881
|
sans: 'var(--font-inter, var(--sf-font-sans, ui-sans-serif, system-ui, sans-serif))',
|
|
3882
|
+
serif: 'var(--font-serif, var(--sf-font-serif, ui-serif, Georgia, Cambria, "Times New Roman", serif))',
|
|
3617
3883
|
display: 'var(--font-display, var(--sf-font-display, ui-serif, Georgia, serif))',
|
|
3618
3884
|
mono: 'var(--font-mono, var(--sf-font-mono, ui-monospace, monospace))',
|
|
3619
3885
|
}
|
|
@@ -3661,22 +3927,33 @@ function colourUtility(base) {
|
|
|
3661
3927
|
}
|
|
3662
3928
|
|
|
3663
3929
|
function borderUtility(base) {
|
|
3664
|
-
|
|
3930
|
+
// A border width draws nothing without a line style, so width utilities set both.
|
|
3931
|
+
if (base === 'border') return [['border-width', '1px'], ['border-style', 'solid']]
|
|
3665
3932
|
if (base === 'border-0') return [['border-width', '0']]
|
|
3666
|
-
if (base === 'border-2') return [['border-width', '0.125rem']]
|
|
3667
|
-
if (base === 'border-t') return [['border-top-width', '1px']]
|
|
3668
|
-
if (base === 'border-r') return [['border-right-width', '1px']]
|
|
3669
|
-
if (base === 'border-b') return [['border-bottom-width', '1px']]
|
|
3670
|
-
if (base === 'border-l') return [['border-left-width', '1px']]
|
|
3671
|
-
if (base === 'border-y') return [['border-top-width', '1px'], ['border-bottom-width', '1px']]
|
|
3672
|
-
if (base === 'border-x') return [['border-left-width', '1px'], ['border-right-width', '1px']]
|
|
3933
|
+
if (base === 'border-2') return [['border-width', '0.125rem'], ['border-style', 'solid']]
|
|
3934
|
+
if (base === 'border-t') return [['border-top-width', '1px'], ['border-top-style', 'solid']]
|
|
3935
|
+
if (base === 'border-r') return [['border-right-width', '1px'], ['border-right-style', 'solid']]
|
|
3936
|
+
if (base === 'border-b') return [['border-bottom-width', '1px'], ['border-bottom-style', 'solid']]
|
|
3937
|
+
if (base === 'border-l') return [['border-left-width', '1px'], ['border-left-style', 'solid']]
|
|
3938
|
+
if (base === 'border-y') return [['border-top-width', '1px'], ['border-bottom-width', '1px'], ['border-top-style', 'solid'], ['border-bottom-style', 'solid']]
|
|
3939
|
+
if (base === 'border-x') return [['border-left-width', '1px'], ['border-right-width', '1px'], ['border-left-style', 'solid'], ['border-right-style', 'solid']]
|
|
3673
3940
|
const sideWidth = base.match(/^border-(t|r|b|l)-(\d+)$/)
|
|
3674
3941
|
if (sideWidth) {
|
|
3675
3942
|
const side = { t: 'top', r: 'right', b: 'bottom', l: 'left' }[sideWidth[1]]
|
|
3676
|
-
return [[`border-${side}-width`, borderWidthUnit(sideWidth[2])]]
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
|
-
if (
|
|
3943
|
+
return [[`border-${side}-width`, borderWidthUnit(sideWidth[2])], [`border-${side}-style`, 'solid']]
|
|
3944
|
+
}
|
|
3945
|
+
const borderStyle = base.match(/^border-(solid|dashed|dotted|double|hidden|none)$/)
|
|
3946
|
+
if (borderStyle) return [['border-style', borderStyle[1]]]
|
|
3947
|
+
const divide = base.match(/^divide-(x|y)(?:-(\d+))?$/)
|
|
3948
|
+
if (divide) {
|
|
3949
|
+
const side = divide[1] === 'y' ? 'block' : 'inline'
|
|
3950
|
+
const width = divide[2] === undefined ? '1px' : borderWidthUnit(divide[2])
|
|
3951
|
+
return [[`border-${side}-start-width`, width], [`border-${side}-start-style`, 'solid']]
|
|
3952
|
+
}
|
|
3953
|
+
const divideStyle = base.match(/^divide-(solid|dashed|dotted|double|none)$/)
|
|
3954
|
+
if (divideStyle) return [['border-style', divideStyle[1]]]
|
|
3955
|
+
const divideColour = base.match(/^divide-(.+)$/)
|
|
3956
|
+
if (divideColour && colourValue(divideColour[1])) return [['border-color', colourValue(divideColour[1])]]
|
|
3680
3957
|
const roundedSide = base.match(/^rounded-(t|r|b|l|tr|tl|br|bl)-(none|xs|sm|md|lg|xl|2xl|3xl|full)$/)
|
|
3681
3958
|
if (roundedSide) {
|
|
3682
3959
|
const radius = `var(--radius-${roundedSide[2]})`
|
|
@@ -3714,6 +3991,9 @@ function borderUtility(base) {
|
|
|
3714
3991
|
}
|
|
3715
3992
|
|
|
3716
3993
|
function effectUtility(base) {
|
|
3994
|
+
const linear = base.match(/^bg-linear-to-(t|b|l|r|tl|tr|bl|br)$/)
|
|
3995
|
+
if (linear) return effectUtility(`bg-gradient-to-${linear[1]}`)
|
|
3996
|
+
if (base === 'bg-none') return [['background-image', 'none']]
|
|
3717
3997
|
if (base === 'bg-gradient-to-r') return [['background-image', 'linear-gradient(to right, var(--sf-gradient-stops))']]
|
|
3718
3998
|
if (base === 'bg-gradient-to-l') return [['background-image', 'linear-gradient(to left, var(--sf-gradient-stops))']]
|
|
3719
3999
|
if (base === 'bg-gradient-to-t') return [['background-image', 'linear-gradient(to top, var(--sf-gradient-stops))']]
|
|
@@ -3735,6 +4015,7 @@ function effectUtility(base) {
|
|
|
3735
4015
|
|
|
3736
4016
|
let match = base.match(/^opacity-(\d+)$/)
|
|
3737
4017
|
if (match) return [['opacity', String(Number(match[1]) / 100)]]
|
|
4018
|
+
if (base === 'blur') return [['filter', 'blur(0.5rem)']]
|
|
3738
4019
|
match = base.match(/^blur-(none|sm|md|lg|xl|2xl|3xl)$/)
|
|
3739
4020
|
if (match) {
|
|
3740
4021
|
const map = { none: '0', sm: '0.25rem', md: '0.75rem', lg: '1rem', xl: '1.5rem', '2xl': '2.5rem', '3xl': '4rem' }
|
|
@@ -3765,6 +4046,9 @@ function effectUtility(base) {
|
|
|
3765
4046
|
if (base === 'transition') return [['transition-property', 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
|
|
3766
4047
|
if (base === 'transition-all') return [['transition-property', 'all'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
|
|
3767
4048
|
if (base === 'transition-colors') return [['transition-property', 'color, background-color, border-color, text-decoration-color, fill, stroke'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
|
|
4049
|
+
if (base === 'transition-opacity') return [['transition-property', 'opacity'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
|
|
4050
|
+
if (base === 'transition-shadow') return [['transition-property', 'box-shadow'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
|
|
4051
|
+
if (base === 'transition-none') return [['transition-property', 'none']]
|
|
3768
4052
|
if (base === 'transition-transform') return [['transition-property', 'transform'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
|
|
3769
4053
|
match = base.match(/^transition-\[(.+)\]$/)
|
|
3770
4054
|
if (match) return [['transition-property', arbitraryValue(match[1])], ['transition-duration', 'var(--duration-normal)']]
|
|
@@ -3777,6 +4061,9 @@ function effectUtility(base) {
|
|
|
3777
4061
|
if (base === 'ease-in-out') return [['transition-timing-function', 'cubic-bezier(0.4, 0, 0.2, 1)']]
|
|
3778
4062
|
if (base === 'animate-pulse') return [['animation', 'sf-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite']]
|
|
3779
4063
|
if (base === 'animate-spin') return [['animation', 'sf-spin 1s linear infinite']]
|
|
4064
|
+
if (base === 'animate-ping') return [['animation', 'sf-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite']]
|
|
4065
|
+
if (base === 'animate-bounce') return [['animation', 'sf-bounce 1s infinite']]
|
|
4066
|
+
if (base === 'animate-none') return [['animation', 'none']]
|
|
3780
4067
|
if (base === 'animate-in') return [['animation', 'sf-enter var(--duration-normal) var(--ease-standard) both']]
|
|
3781
4068
|
if (base === 'animate-out') return [['animation', 'sf-exit var(--duration-fast) var(--ease-standard) both']]
|
|
3782
4069
|
return null
|
|
@@ -3787,7 +4074,7 @@ function transformUtility(base) {
|
|
|
3787
4074
|
let match = base.match(/^(-?)translate-x-(.+)$/)
|
|
3788
4075
|
if (match) declarations.push(['--sf-translate-x', `${match[1] ? '-' : ''}${spacingUnit(match[2]) ?? sizeKeyword(match[2])}`])
|
|
3789
4076
|
match = base.match(/^(-?)translate-y-(.+)$/)
|
|
3790
|
-
if (match) declarations.push(['--sf-translate-y', `${match[1] ? '-' : ''}${spacingUnit(match[2]) ?? sizeKeyword(match[2])}`])
|
|
4077
|
+
if (match) declarations.push(['--sf-translate-y', `${match[1] ? '-' : ''}${spacingUnit(match[2]) ?? sizeKeyword(match[2], 'y')}`])
|
|
3791
4078
|
match = base.match(/^scale-\[(.+)\]$/)
|
|
3792
4079
|
if (match) declarations.push(['--sf-scale-x', arbitraryValue(match[1])], ['--sf-scale-y', arbitraryValue(match[1])])
|
|
3793
4080
|
match = base.match(/^scale-(\d+)$/)
|
|
@@ -3812,7 +4099,9 @@ function cssRuleFor(token) {
|
|
|
3812
4099
|
const selectorResult = selectorFor(token, variants)
|
|
3813
4100
|
if (!selectorResult) return null
|
|
3814
4101
|
|
|
3815
|
-
const {
|
|
4102
|
+
const { wrappers, needsPseudoContent } = selectorResult
|
|
4103
|
+
// space-* and divide-* style the gaps between children, not the element itself.
|
|
4104
|
+
const selector = /^-?(space-[xy]-|divide-)/.test(base) ? `${selectorResult.selector} > * + *` : selectorResult.selector
|
|
3816
4105
|
const allDeclarations = needsPseudoContent ? [['content', '""'], ...declarations] : declarations
|
|
3817
4106
|
let rule = `${selector}{${allDeclarations.map(([property, value]) => `${property}:${value}`).join(';')}}`
|
|
3818
4107
|
|
|
@@ -4480,16 +4769,6 @@ function buildLayoutCss() {
|
|
|
4480
4769
|
}
|
|
4481
4770
|
}
|
|
4482
4771
|
|
|
4483
|
-
:where(.space-y-1, .space-y-2, .space-y-3, .space-y-4, .space-y-5, .space-y-6, .space-y-8, .space-y-10, .space-y-12) > * + * {
|
|
4484
|
-
margin-block-start: var(--sf-space-y);
|
|
4485
|
-
}
|
|
4486
|
-
|
|
4487
|
-
:where(.space-x-1, .space-x-2, .space-x-3, .space-x-4, .space-x-5, .space-x-6, .space-x-8) > * + * {
|
|
4488
|
-
margin-inline-start: var(--sf-space-x);
|
|
4489
|
-
}
|
|
4490
|
-
|
|
4491
|
-
.divide-y > * + * { border-block-start-width: var(--sf-divide-y); }
|
|
4492
|
-
.divide-x > * + * { border-inline-start-width: var(--sf-divide-x); }
|
|
4493
4772
|
}`
|
|
4494
4773
|
}
|
|
4495
4774
|
|
|
@@ -4795,6 +5074,8 @@ function buildComponentCss() {
|
|
|
4795
5074
|
|
|
4796
5075
|
.sf-data-table :where(th button, .sf-table-sort) {
|
|
4797
5076
|
align-items: center;
|
|
5077
|
+
background: transparent;
|
|
5078
|
+
border: 0;
|
|
4798
5079
|
color: inherit;
|
|
4799
5080
|
display: inline-flex;
|
|
4800
5081
|
font: inherit;
|
|
@@ -4803,14 +5084,14 @@ function buildComponentCss() {
|
|
|
4803
5084
|
padding: 0;
|
|
4804
5085
|
}
|
|
4805
5086
|
|
|
4806
|
-
.sf-data-table :where(
|
|
5087
|
+
.sf-data-table th[aria-sort] :where(button, .sf-table-sort)::after {
|
|
4807
5088
|
color: var(--color-muted-foreground);
|
|
4808
5089
|
content: "\\2195";
|
|
4809
5090
|
font-size: var(--step--2);
|
|
4810
5091
|
}
|
|
4811
5092
|
|
|
4812
|
-
.sf-data-table :where(
|
|
4813
|
-
.sf-data-table :where(
|
|
5093
|
+
.sf-data-table th[aria-sort="ascending"] :where(button, .sf-table-sort)::after { content: "\\2191"; }
|
|
5094
|
+
.sf-data-table th[aria-sort="descending"] :where(button, .sf-table-sort)::after { content: "\\2193"; }
|
|
4814
5095
|
|
|
4815
5096
|
.sf-data-table tr[data-clickable="true"] {
|
|
4816
5097
|
cursor: pointer;
|
|
@@ -4885,7 +5166,7 @@ function buildComponentCss() {
|
|
|
4885
5166
|
}
|
|
4886
5167
|
|
|
4887
5168
|
.sf-status::before {
|
|
4888
|
-
background: currentColor;
|
|
5169
|
+
background: var(--status-dot, currentColor);
|
|
4889
5170
|
block-size: .42em;
|
|
4890
5171
|
border-radius: var(--radius-full);
|
|
4891
5172
|
content: "";
|
|
@@ -4895,25 +5176,29 @@ function buildComponentCss() {
|
|
|
4895
5176
|
.sf-status[data-tone="success"] {
|
|
4896
5177
|
--status-bg: var(--sf-colour-success-soft, oklch(62% 0.13 150 / 0.12));
|
|
4897
5178
|
--status-border: color-mix(in oklch, var(--sf-colour-success, oklch(62% 0.13 150)) 34%, transparent);
|
|
4898
|
-
--status-color:
|
|
5179
|
+
--status-color: var(--color-foreground);
|
|
5180
|
+
--status-dot: var(--sf-colour-success, oklch(62% 0.13 150));
|
|
4899
5181
|
}
|
|
4900
5182
|
|
|
4901
5183
|
.sf-status[data-tone="warning"] {
|
|
4902
5184
|
--status-bg: var(--sf-colour-warning-soft, oklch(78% 0.15 82 / 0.16));
|
|
4903
5185
|
--status-border: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 38%, transparent);
|
|
4904
|
-
--status-color:
|
|
5186
|
+
--status-color: var(--color-foreground);
|
|
5187
|
+
--status-dot: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 60%, var(--color-foreground));
|
|
4905
5188
|
}
|
|
4906
5189
|
|
|
4907
5190
|
.sf-status[data-tone="danger"] {
|
|
4908
5191
|
--status-bg: var(--sf-colour-danger-soft, oklch(56% 0.2 28 / 0.12));
|
|
4909
5192
|
--status-border: color-mix(in oklch, var(--sf-colour-danger, var(--color-destructive)) 34%, transparent);
|
|
4910
|
-
--status-color:
|
|
5193
|
+
--status-color: var(--color-foreground);
|
|
5194
|
+
--status-dot: var(--sf-colour-danger, var(--color-destructive));
|
|
4911
5195
|
}
|
|
4912
5196
|
|
|
4913
5197
|
.sf-status[data-tone="info"] {
|
|
4914
5198
|
--status-bg: var(--sf-colour-info-soft, oklch(70% 0.12 205 / 0.14));
|
|
4915
5199
|
--status-border: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 34%, transparent);
|
|
4916
|
-
--status-color:
|
|
5200
|
+
--status-color: var(--color-foreground);
|
|
5201
|
+
--status-dot: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 68%, var(--color-foreground));
|
|
4917
5202
|
}
|
|
4918
5203
|
|
|
4919
5204
|
.sf-data-list {
|
|
@@ -4961,9 +5246,16 @@ function buildComponentCss() {
|
|
|
4961
5246
|
background: var(--color-surface-alt);
|
|
4962
5247
|
}
|
|
4963
5248
|
|
|
5249
|
+
.sf-form {
|
|
5250
|
+
display: grid;
|
|
5251
|
+
gap: var(--form-gap, var(--space-s));
|
|
5252
|
+
grid-template-columns: minmax(0, 1fr);
|
|
5253
|
+
}
|
|
5254
|
+
|
|
4964
5255
|
.sf-field {
|
|
4965
5256
|
display: grid;
|
|
4966
5257
|
gap: var(--space-2xs);
|
|
5258
|
+
grid-template-columns: minmax(0, 1fr);
|
|
4967
5259
|
}
|
|
4968
5260
|
|
|
4969
5261
|
.sf-form[data-density="compact"],
|
|
@@ -5026,6 +5318,130 @@ function buildComponentCss() {
|
|
|
5026
5318
|
outline: 0.125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
|
|
5027
5319
|
outline-offset: 0;
|
|
5028
5320
|
}
|
|
5321
|
+
|
|
5322
|
+
.sf-spinner {
|
|
5323
|
+
animation: sf-spinner-rotate .75s linear infinite;
|
|
5324
|
+
block-size: var(--spinner-size, 1em);
|
|
5325
|
+
border: .125em solid color-mix(in oklch, currentColor 28%, transparent);
|
|
5326
|
+
border-block-start-color: currentColor;
|
|
5327
|
+
border-radius: var(--radius-full);
|
|
5328
|
+
display: inline-block;
|
|
5329
|
+
flex: 0 0 auto;
|
|
5330
|
+
inline-size: var(--spinner-size, 1em);
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
@keyframes sf-spinner-rotate { to { transform: rotate(1turn); } }
|
|
5334
|
+
|
|
5335
|
+
.sf-input-group {
|
|
5336
|
+
align-items: stretch;
|
|
5337
|
+
background: var(--color-card);
|
|
5338
|
+
border: 1px solid var(--color-border);
|
|
5339
|
+
border-radius: var(--input-radius, var(--radius-md));
|
|
5340
|
+
display: flex;
|
|
5341
|
+
inline-size: 100%;
|
|
5342
|
+
min-block-size: var(--input-block-size, 2.75rem);
|
|
5343
|
+
overflow: clip;
|
|
5344
|
+
}
|
|
5345
|
+
|
|
5346
|
+
.sf-input-group:focus-within {
|
|
5347
|
+
border-color: var(--color-ring);
|
|
5348
|
+
outline: .125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
.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; }
|
|
5352
|
+
.sf-input-group__leading,
|
|
5353
|
+
.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; }
|
|
5354
|
+
.sf-input-group__leading { border-inline-end: 1px solid var(--color-border); }
|
|
5355
|
+
.sf-input-group__trailing,
|
|
5356
|
+
.sf-input-group__action { border-inline-start: 1px solid var(--color-border); }
|
|
5357
|
+
.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); }
|
|
5358
|
+
.sf-input-group[data-invalid="true"],
|
|
5359
|
+
.sf-field[data-invalid="true"] .sf-input-group { border-color: var(--color-destructive, var(--color-danger)); }
|
|
5360
|
+
.sf-input-group:has(:disabled) { background: var(--color-surface-alt); opacity: .72; }
|
|
5361
|
+
|
|
5362
|
+
.sf-file-input {
|
|
5363
|
+
background: var(--color-card);
|
|
5364
|
+
border: 1px solid var(--color-border);
|
|
5365
|
+
border-radius: var(--input-radius, var(--radius-md));
|
|
5366
|
+
color: var(--color-muted-foreground);
|
|
5367
|
+
inline-size: 100%;
|
|
5368
|
+
min-block-size: var(--input-block-size, 2.75rem);
|
|
5369
|
+
padding: var(--space-3xs);
|
|
5370
|
+
}
|
|
5371
|
+
|
|
5372
|
+
.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); }
|
|
5373
|
+
.sf-range { accent-color: var(--color-primary); inline-size: 100%; min-block-size: var(--input-block-size, 2.75rem); }
|
|
5374
|
+
.sf-range:focus-visible,
|
|
5375
|
+
.sf-file-input:focus-visible { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
|
|
5376
|
+
.sf-file-input[aria-invalid="true"],
|
|
5377
|
+
.sf-field[data-invalid="true"] .sf-file-input { border-color: var(--color-destructive, var(--color-danger)); }
|
|
5378
|
+
:where(.sf-file-input, .sf-range):disabled { opacity: .55; }
|
|
5379
|
+
|
|
5380
|
+
.sf-switch { align-items: center; display: inline-grid; gap: var(--space-xs); grid-template-columns: auto 1fr; position: relative; }
|
|
5381
|
+
.sf-switch > input[type="checkbox"],
|
|
5382
|
+
.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; }
|
|
5383
|
+
.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; }
|
|
5384
|
+
.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); }
|
|
5385
|
+
.sf-switch > input:checked + .sf-switch__control { background: var(--color-primary); border-color: var(--color-primary); }
|
|
5386
|
+
.sf-switch > input:checked + .sf-switch__control::before { background: var(--color-primary-foreground); transform: translateX(1.1875rem); }
|
|
5387
|
+
.sf-switch > input:focus-visible + .sf-switch__control { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
|
|
5388
|
+
.sf-switch__label { color: var(--color-foreground); font-weight: 700; }
|
|
5389
|
+
.sf-switch:has(input:disabled) { cursor: not-allowed; opacity: .55; }
|
|
5390
|
+
|
|
5391
|
+
.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); }
|
|
5392
|
+
.sf-segmented-control__option { cursor: pointer; position: relative; }
|
|
5393
|
+
.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); }
|
|
5394
|
+
.sf-segmented-control__option > input:checked + .sf-segmented-control__label { background: var(--color-card); border-color: var(--color-border); color: var(--color-foreground); }
|
|
5395
|
+
.sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
|
|
5396
|
+
.sf-segmented-control__option:has(input:disabled) { cursor: not-allowed; opacity: .55; }
|
|
5397
|
+
|
|
5398
|
+
.sf-combobox { position: relative; }
|
|
5399
|
+
.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; }
|
|
5400
|
+
.sf-combobox__option { border-radius: var(--radius-sm); cursor: pointer; padding: var(--space-2xs) var(--space-xs); }
|
|
5401
|
+
.sf-combobox__option:hover,
|
|
5402
|
+
.sf-combobox__option[aria-selected="true"],
|
|
5403
|
+
.sf-combobox__option[data-active="true"] { background: var(--color-surface-alt); }
|
|
5404
|
+
.sf-combobox__empty { color: var(--color-muted-foreground); padding: var(--space-xs); }
|
|
5405
|
+
|
|
5406
|
+
.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); }
|
|
5407
|
+
.sf-bulk-actions__count { font-size: var(--step--1); font-weight: 800; }
|
|
5408
|
+
.sf-bulk-actions__controls,
|
|
5409
|
+
.sf-pagination__controls,
|
|
5410
|
+
.sf-pagination__pages,
|
|
5411
|
+
.sf-pagination__jump { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
|
|
5412
|
+
|
|
5413
|
+
.sf-pagination { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); list-style: none; padding-inline-start: 0; }
|
|
5414
|
+
.sf-pagination[data-layout="complete"] { justify-content: space-between; row-gap: var(--space-xs); }
|
|
5415
|
+
.sf-pagination__summary,
|
|
5416
|
+
.sf-pagination__jump { color: var(--color-muted-foreground); font-size: var(--step--1); }
|
|
5417
|
+
.sf-pagination__jump .sf-input { inline-size: 5rem; }
|
|
5418
|
+
|
|
5419
|
+
.sf-stepper { counter-reset: sf-step; display: grid; gap: var(--space-xs); list-style: none; padding-inline-start: 0; }
|
|
5420
|
+
.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; }
|
|
5421
|
+
.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; }
|
|
5422
|
+
.sf-stepper__marker:empty::before { content: counter(sf-step); }
|
|
5423
|
+
.sf-stepper__step[aria-current="step"] { color: var(--color-foreground); }
|
|
5424
|
+
.sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-foreground); }
|
|
5425
|
+
.sf-stepper__label { font-weight: 700; }
|
|
5426
|
+
.sf-wizard-actions { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-xs); justify-content: space-between; }
|
|
5427
|
+
|
|
5428
|
+
@media (prefers-reduced-motion: reduce) {
|
|
5429
|
+
.sf-spinner { animation: none; border-color: currentColor; border-inline-end-color: transparent; }
|
|
5430
|
+
.sf-switch__control::before { transition: none; }
|
|
5431
|
+
}
|
|
5432
|
+
|
|
5433
|
+
@media (forced-colors: active) {
|
|
5434
|
+
:where(.sf-input-group, .sf-file-input, .sf-combobox__listbox, .sf-bulk-actions) { border-color: ButtonText; forced-color-adjust: auto; }
|
|
5435
|
+
:where(.sf-switch__control, .sf-segmented-control__label, .sf-stepper__marker) { background: Canvas; border-color: ButtonText; color: ButtonText; forced-color-adjust: none; }
|
|
5436
|
+
.sf-switch > input:focus-visible + .sf-switch__control,
|
|
5437
|
+
.sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid Highlight; outline-offset: .125rem; }
|
|
5438
|
+
.sf-switch > input:checked + .sf-switch__control,
|
|
5439
|
+
.sf-segmented-control__option > input:checked + .sf-segmented-control__label,
|
|
5440
|
+
.sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: Highlight; border-color: Highlight; color: HighlightText; }
|
|
5441
|
+
.sf-switch__control::before { background: ButtonText; }
|
|
5442
|
+
.sf-switch > input:checked + .sf-switch__control::before { background: HighlightText; }
|
|
5443
|
+
.sf-stepper__step[data-complete="true"] .sf-stepper__marker { background: Canvas; border-color: LinkText; color: LinkText; }
|
|
5444
|
+
}
|
|
5029
5445
|
}`
|
|
5030
5446
|
}
|
|
5031
5447
|
|
|
@@ -5180,6 +5596,8 @@ function buildUsedKeyframesCss(bases) {
|
|
|
5180
5596
|
|
|
5181
5597
|
if (bases.has('animate-pulse')) keyframes.push('@keyframes sf-pulse { 50% { opacity: .5; } }')
|
|
5182
5598
|
if (bases.has('animate-spin')) keyframes.push('@keyframes sf-spin { to { transform: rotate(360deg); } }')
|
|
5599
|
+
if (bases.has('animate-ping')) keyframes.push('@keyframes sf-ping { 75%, 100% { transform: scale(2); opacity: 0; } }')
|
|
5600
|
+
if (bases.has('animate-bounce')) keyframes.push('@keyframes sf-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); } 50% { transform: none; animation-timing-function: cubic-bezier(0, 0, 0.2, 1); } }')
|
|
5183
5601
|
if (bases.has('animate-in')) keyframes.push('@keyframes sf-enter { from { opacity: 0; transform: translate3d(0, .5rem, 0) scale(.98); } to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } }')
|
|
5184
5602
|
if (bases.has('animate-out')) keyframes.push('@keyframes sf-exit { from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } to { opacity: 0; transform: translate3d(0, .25rem, 0) scale(.98); } }')
|
|
5185
5603
|
|
|
@@ -5221,11 +5639,25 @@ function utilitySuggestionClasses() {
|
|
|
5221
5639
|
|
|
5222
5640
|
function nearestClass(token) {
|
|
5223
5641
|
const { base } = splitVariants(token)
|
|
5224
|
-
const
|
|
5225
|
-
|
|
5642
|
+
const known = knownPublicClasses()
|
|
5643
|
+
// Flow's own version of a common class, e.g. `prose` -> `sf-prose`.
|
|
5644
|
+
if (known.includes(`sf-${base}`)) return `sf-${base}`
|
|
5645
|
+
const scored = [...siblingUtilities(base), ...known]
|
|
5226
5646
|
.map((candidate) => ({ candidate, score: levenshtein(base, candidate) }))
|
|
5227
5647
|
.sort((a, b) => a.score - b.score)
|
|
5228
|
-
|
|
5648
|
+
// Only near typos: a looser match often names a class that does something else.
|
|
5649
|
+
return scored[0]?.score <= Math.max(1, Math.floor(base.length / 5)) ? scored[0].candidate : null
|
|
5650
|
+
}
|
|
5651
|
+
|
|
5652
|
+
// The same utility with other keyword or spacing values, e.g. `h-scren` -> `h-screen`.
|
|
5653
|
+
function siblingUtilities(base) {
|
|
5654
|
+
const cut = base.lastIndexOf('-')
|
|
5655
|
+
if (cut <= 0) return []
|
|
5656
|
+
const prefix = base.slice(0, cut)
|
|
5657
|
+
const values = [...Object.keys(sizeKeywords), 'none', 'first', 'last', 'center', 'start', 'end', '0', 'px', '1', '2', '3', '4', '6', '8', '10', '12', '16', '24']
|
|
5658
|
+
return values
|
|
5659
|
+
.map((value) => `${prefix}-${value}`)
|
|
5660
|
+
.filter((candidate) => candidate !== base && declarationsFor(candidate))
|
|
5229
5661
|
}
|
|
5230
5662
|
|
|
5231
5663
|
function lintSourceFiles() {
|