@syncedco/flow 0.3.1 → 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 +33 -0
- package/CONTRIBUTING.md +18 -5
- package/README.md +32 -9
- package/base.css +2 -1
- package/bin/synced-flow.mjs +446 -58
- 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/base-styling.md +5 -0
- 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 +10 -3
- package/docs/tokens.md +10 -2
- package/examples/README.md +2 -2
- package/examples/astro/src/styles/synced-flow.generated.css +14 -0
- package/examples/next/app/synced-flow.generated.css +19 -5
- package/examples/plain-html/synced-flow.generated.css +19 -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 +19 -5
- package/examples/wordpress/assets/css/synced-flow.css +542 -9
- package/package.json +14 -3
- package/reset.css +1 -1
- 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 +456 -21
- package/tokens.css +13 -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) {
|
|
@@ -2980,6 +3213,19 @@ function pxToRem(value) {
|
|
|
2980
3213
|
})
|
|
2981
3214
|
}
|
|
2982
3215
|
|
|
3216
|
+
function pixelNumberToRem(value) {
|
|
3217
|
+
const number = Number(value)
|
|
3218
|
+
if (Number.isNaN(number)) return null
|
|
3219
|
+
if (number === 0) return '0'
|
|
3220
|
+
return `${round(number / 16)}rem`
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
function borderWidthUnit(value) {
|
|
3224
|
+
if (value === '0') return '0'
|
|
3225
|
+
if (value === '1') return '1px'
|
|
3226
|
+
return pixelNumberToRem(value)
|
|
3227
|
+
}
|
|
3228
|
+
|
|
2983
3229
|
function arbitraryValue(value) {
|
|
2984
3230
|
return pxToRem(value.replace(/_/g, ' '))
|
|
2985
3231
|
}
|
|
@@ -3650,7 +3896,7 @@ function colourUtility(base) {
|
|
|
3650
3896
|
function borderUtility(base) {
|
|
3651
3897
|
if (base === 'border') return [['border-width', '1px']]
|
|
3652
3898
|
if (base === 'border-0') return [['border-width', '0']]
|
|
3653
|
-
if (base === 'border-2') return [['border-width', '
|
|
3899
|
+
if (base === 'border-2') return [['border-width', '0.125rem']]
|
|
3654
3900
|
if (base === 'border-t') return [['border-top-width', '1px']]
|
|
3655
3901
|
if (base === 'border-r') return [['border-right-width', '1px']]
|
|
3656
3902
|
if (base === 'border-b') return [['border-bottom-width', '1px']]
|
|
@@ -3660,7 +3906,7 @@ function borderUtility(base) {
|
|
|
3660
3906
|
const sideWidth = base.match(/^border-(t|r|b|l)-(\d+)$/)
|
|
3661
3907
|
if (sideWidth) {
|
|
3662
3908
|
const side = { t: 'top', r: 'right', b: 'bottom', l: 'left' }[sideWidth[1]]
|
|
3663
|
-
return [[`border-${side}-width`,
|
|
3909
|
+
return [[`border-${side}-width`, borderWidthUnit(sideWidth[2])]]
|
|
3664
3910
|
}
|
|
3665
3911
|
if (base === 'divide-y') return [['--sf-divide-y', '1px']]
|
|
3666
3912
|
if (base === 'divide-x') return [['--sf-divide-x', '1px']]
|
|
@@ -3724,26 +3970,26 @@ function effectUtility(base) {
|
|
|
3724
3970
|
if (match) return [['opacity', String(Number(match[1]) / 100)]]
|
|
3725
3971
|
match = base.match(/^blur-(none|sm|md|lg|xl|2xl|3xl)$/)
|
|
3726
3972
|
if (match) {
|
|
3727
|
-
const map = { none: '0', sm: '
|
|
3973
|
+
const map = { none: '0', sm: '0.25rem', md: '0.75rem', lg: '1rem', xl: '1.5rem', '2xl': '2.5rem', '3xl': '4rem' }
|
|
3728
3974
|
return [['filter', `blur(${map[match[1]]})`]]
|
|
3729
3975
|
}
|
|
3730
3976
|
match = base.match(/^backdrop-blur(?:-(none|sm|md|lg|xl))?$/)
|
|
3731
3977
|
if (match) {
|
|
3732
|
-
const map = { undefined: '
|
|
3978
|
+
const map = { undefined: '0.5rem', none: '0', sm: '0.25rem', md: '0.75rem', lg: '1rem', xl: '1.5rem' }
|
|
3733
3979
|
return [['backdrop-filter', `blur(${map[match[1] ?? 'undefined']})`]]
|
|
3734
3980
|
}
|
|
3735
3981
|
match = base.match(/^ring(?:-(\d+))?$/)
|
|
3736
|
-
if (match) return [['box-shadow', `0 0 0 ${match[1] ?? 3}
|
|
3982
|
+
if (match) return [['box-shadow', `0 0 0 ${pixelNumberToRem(match[1] ?? 3)} var(--sf-ring-color, var(--color-ring))`]]
|
|
3737
3983
|
if (base === 'ring-offset-white') return [['--sf-ring-offset-color', 'var(--color-white)']]
|
|
3738
3984
|
if (base === 'ring-offset-background') return [['--sf-ring-offset-color', 'var(--color-background)']]
|
|
3739
3985
|
match = base.match(/^ring-offset-(\d+)$/)
|
|
3740
|
-
if (match) return [['--sf-ring-offset-width',
|
|
3986
|
+
if (match) return [['--sf-ring-offset-width', pixelNumberToRem(match[1])]]
|
|
3741
3987
|
if (base === 'outline') return [['outline-style', 'solid']]
|
|
3742
|
-
if (base === 'outline-none') return [['outline', '
|
|
3988
|
+
if (base === 'outline-none') return [['outline', '0.125rem solid transparent'], ['outline-offset', '0.125rem']]
|
|
3743
3989
|
match = base.match(/^outline-(\d+)$/)
|
|
3744
|
-
if (match) return [['outline-width',
|
|
3990
|
+
if (match) return [['outline-width', pixelNumberToRem(match[1])]]
|
|
3745
3991
|
match = base.match(/^outline-offset-(\d+)$/)
|
|
3746
|
-
if (match) return [['outline-offset',
|
|
3992
|
+
if (match) return [['outline-offset', pixelNumberToRem(match[1])]]
|
|
3747
3993
|
match = base.match(/^outline-(.+)$/)
|
|
3748
3994
|
if (match) {
|
|
3749
3995
|
const value = colourValue(match[1])
|
|
@@ -3944,6 +4190,7 @@ function buildTokensCss() {
|
|
|
3944
4190
|
|
|
3945
4191
|
${steps.join('\n')}
|
|
3946
4192
|
--type-caption-sm: var(--step--2);
|
|
4193
|
+
--text-base: var(--step-0);
|
|
3947
4194
|
|
|
3948
4195
|
${spaces.join('\n')}
|
|
3949
4196
|
${pairs.join('\n')}
|
|
@@ -4074,7 +4321,7 @@ function buildBaseCss() {
|
|
|
4074
4321
|
return `@layer reset {
|
|
4075
4322
|
*, *::before, *::after { box-sizing: border-box; }
|
|
4076
4323
|
*:where(:not(dialog)) { margin: 0; }
|
|
4077
|
-
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
4324
|
+
html { font-size: 100%; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
4078
4325
|
body { min-block-size: 100%; }
|
|
4079
4326
|
img, picture, video, canvas, svg { display: block; max-inline-size: 100%; }
|
|
4080
4327
|
img, video { block-size: auto; }
|
|
@@ -4084,11 +4331,15 @@ function buildBaseCss() {
|
|
|
4084
4331
|
}
|
|
4085
4332
|
|
|
4086
4333
|
@layer base {
|
|
4334
|
+
html {
|
|
4335
|
+
font-size: 100%;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4087
4338
|
body {
|
|
4088
4339
|
background: var(--color-background);
|
|
4089
4340
|
color: var(--color-foreground);
|
|
4090
4341
|
font-family: var(--font-inter, var(--sf-font-sans, ui-sans-serif, system-ui, sans-serif));
|
|
4091
|
-
font-size: var(--
|
|
4342
|
+
font-size: var(--text-base);
|
|
4092
4343
|
line-height: 1.5;
|
|
4093
4344
|
font-feature-settings: "rlig" 1, "calt" 1;
|
|
4094
4345
|
-webkit-font-smoothing: antialiased;
|
|
@@ -4116,7 +4367,7 @@ function buildBaseCss() {
|
|
|
4116
4367
|
:where(button:disabled, [aria-disabled="true"]) { cursor: not-allowed; }
|
|
4117
4368
|
|
|
4118
4369
|
:where(:focus-visible) {
|
|
4119
|
-
outline:
|
|
4370
|
+
outline: 0.125rem solid var(--color-ring);
|
|
4120
4371
|
outline-offset: .2rem;
|
|
4121
4372
|
}
|
|
4122
4373
|
|
|
@@ -4777,6 +5028,8 @@ function buildComponentCss() {
|
|
|
4777
5028
|
|
|
4778
5029
|
.sf-data-table :where(th button, .sf-table-sort) {
|
|
4779
5030
|
align-items: center;
|
|
5031
|
+
background: transparent;
|
|
5032
|
+
border: 0;
|
|
4780
5033
|
color: inherit;
|
|
4781
5034
|
display: inline-flex;
|
|
4782
5035
|
font: inherit;
|
|
@@ -4785,14 +5038,14 @@ function buildComponentCss() {
|
|
|
4785
5038
|
padding: 0;
|
|
4786
5039
|
}
|
|
4787
5040
|
|
|
4788
|
-
.sf-data-table :where(
|
|
5041
|
+
.sf-data-table th[aria-sort] :where(button, .sf-table-sort)::after {
|
|
4789
5042
|
color: var(--color-muted-foreground);
|
|
4790
5043
|
content: "\\2195";
|
|
4791
5044
|
font-size: var(--step--2);
|
|
4792
5045
|
}
|
|
4793
5046
|
|
|
4794
|
-
.sf-data-table :where(
|
|
4795
|
-
.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"; }
|
|
4796
5049
|
|
|
4797
5050
|
.sf-data-table tr[data-clickable="true"] {
|
|
4798
5051
|
cursor: pointer;
|
|
@@ -4867,7 +5120,7 @@ function buildComponentCss() {
|
|
|
4867
5120
|
}
|
|
4868
5121
|
|
|
4869
5122
|
.sf-status::before {
|
|
4870
|
-
background: currentColor;
|
|
5123
|
+
background: var(--status-dot, currentColor);
|
|
4871
5124
|
block-size: .42em;
|
|
4872
5125
|
border-radius: var(--radius-full);
|
|
4873
5126
|
content: "";
|
|
@@ -4877,25 +5130,29 @@ function buildComponentCss() {
|
|
|
4877
5130
|
.sf-status[data-tone="success"] {
|
|
4878
5131
|
--status-bg: var(--sf-colour-success-soft, oklch(62% 0.13 150 / 0.12));
|
|
4879
5132
|
--status-border: color-mix(in oklch, var(--sf-colour-success, oklch(62% 0.13 150)) 34%, transparent);
|
|
4880
|
-
--status-color:
|
|
5133
|
+
--status-color: var(--color-foreground);
|
|
5134
|
+
--status-dot: var(--sf-colour-success, oklch(62% 0.13 150));
|
|
4881
5135
|
}
|
|
4882
5136
|
|
|
4883
5137
|
.sf-status[data-tone="warning"] {
|
|
4884
5138
|
--status-bg: var(--sf-colour-warning-soft, oklch(78% 0.15 82 / 0.16));
|
|
4885
5139
|
--status-border: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 38%, transparent);
|
|
4886
|
-
--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));
|
|
4887
5142
|
}
|
|
4888
5143
|
|
|
4889
5144
|
.sf-status[data-tone="danger"] {
|
|
4890
5145
|
--status-bg: var(--sf-colour-danger-soft, oklch(56% 0.2 28 / 0.12));
|
|
4891
5146
|
--status-border: color-mix(in oklch, var(--sf-colour-danger, var(--color-destructive)) 34%, transparent);
|
|
4892
|
-
--status-color:
|
|
5147
|
+
--status-color: var(--color-foreground);
|
|
5148
|
+
--status-dot: var(--sf-colour-danger, var(--color-destructive));
|
|
4893
5149
|
}
|
|
4894
5150
|
|
|
4895
5151
|
.sf-status[data-tone="info"] {
|
|
4896
5152
|
--status-bg: var(--sf-colour-info-soft, oklch(70% 0.12 205 / 0.14));
|
|
4897
5153
|
--status-border: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 34%, transparent);
|
|
4898
|
-
--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));
|
|
4899
5156
|
}
|
|
4900
5157
|
|
|
4901
5158
|
.sf-data-list {
|
|
@@ -4943,9 +5200,16 @@ function buildComponentCss() {
|
|
|
4943
5200
|
background: var(--color-surface-alt);
|
|
4944
5201
|
}
|
|
4945
5202
|
|
|
5203
|
+
.sf-form {
|
|
5204
|
+
display: grid;
|
|
5205
|
+
gap: var(--form-gap, var(--space-s));
|
|
5206
|
+
grid-template-columns: minmax(0, 1fr);
|
|
5207
|
+
}
|
|
5208
|
+
|
|
4946
5209
|
.sf-field {
|
|
4947
5210
|
display: grid;
|
|
4948
5211
|
gap: var(--space-2xs);
|
|
5212
|
+
grid-template-columns: minmax(0, 1fr);
|
|
4949
5213
|
}
|
|
4950
5214
|
|
|
4951
5215
|
.sf-form[data-density="compact"],
|
|
@@ -5008,6 +5272,130 @@ function buildComponentCss() {
|
|
|
5008
5272
|
outline: 0.125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
|
|
5009
5273
|
outline-offset: 0;
|
|
5010
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
|
+
}
|
|
5011
5399
|
}`
|
|
5012
5400
|
}
|
|
5013
5401
|
|