@syncedco/flow 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,8 +7,8 @@ These examples show the intended starting point for new projects.
7
7
  - `vite` - Vite React setup
8
8
  - `plain-html` - no framework setup with a complete website demo using only
9
9
  Synced Flow primitives for header, hero, content, cards, form, notice, and footer
10
- - `templates` - copy-ready full-page HTML templates for SaaS, portfolio, blog,
11
- and coming soon pages
10
+ - `templates` - copy-ready full-page HTML templates for SaaS, app settings,
11
+ portfolio, blog, and coming soon pages
12
12
  - `wordpress` - block theme style setup with enqueue code, templates, parts,
13
13
  and PHP patterns
14
14
 
@@ -184,19 +184,19 @@
184
184
  --color-surface-elevated: var(--sf-colour-surface-elevated);
185
185
  --sf-colour-border: oklch(18% 0.026 250 / 0.12);
186
186
  --color-border: var(--sf-colour-border);
187
- --sf-colour-primary: oklch(68% 0.18 44);
187
+ --sf-colour-primary: oklch(50% 0.16 40);
188
188
  --color-primary: var(--sf-colour-primary);
189
- --sf-colour-primary-hover: oklch(60% 0.17 42);
189
+ --sf-colour-primary-hover: oklch(44% 0.14 40);
190
190
  --color-primary-hover: var(--sf-colour-primary-hover);
191
191
  --sf-colour-primary-foreground: oklch(100% 0 0);
192
192
  --color-primary-foreground: var(--sf-colour-primary-foreground);
193
193
  --sf-colour-accent: oklch(70% 0.12 205);
194
194
  --color-accent: var(--sf-colour-accent);
195
- --sf-colour-link: oklch(58% 0.17 42);
195
+ --sf-colour-link: oklch(50% 0.16 40);
196
196
  --color-link: var(--sf-colour-link);
197
- --sf-colour-link-hover: oklch(68% 0.18 44);
197
+ --sf-colour-link-hover: oklch(44% 0.14 40);
198
198
  --color-link-hover: var(--sf-colour-link-hover);
199
- --sf-colour-ring: oklch(68% 0.18 44);
199
+ --sf-colour-ring: oklch(50% 0.16 40);
200
200
  --color-ring: var(--sf-colour-ring);
201
201
  --sf-button-radius: 0.5rem;
202
202
  --button-radius: 0.5rem;
@@ -184,19 +184,19 @@
184
184
  --color-surface-elevated: var(--sf-colour-surface-elevated);
185
185
  --sf-colour-border: oklch(18% 0.026 250 / 0.12);
186
186
  --color-border: var(--sf-colour-border);
187
- --sf-colour-primary: oklch(68% 0.18 44);
187
+ --sf-colour-primary: oklch(50% 0.16 40);
188
188
  --color-primary: var(--sf-colour-primary);
189
- --sf-colour-primary-hover: oklch(60% 0.17 42);
189
+ --sf-colour-primary-hover: oklch(44% 0.14 40);
190
190
  --color-primary-hover: var(--sf-colour-primary-hover);
191
191
  --sf-colour-primary-foreground: oklch(100% 0 0);
192
192
  --color-primary-foreground: var(--sf-colour-primary-foreground);
193
193
  --sf-colour-accent: oklch(70% 0.12 205);
194
194
  --color-accent: var(--sf-colour-accent);
195
- --sf-colour-link: oklch(58% 0.17 42);
195
+ --sf-colour-link: oklch(50% 0.16 40);
196
196
  --color-link: var(--sf-colour-link);
197
- --sf-colour-link-hover: oklch(68% 0.18 44);
197
+ --sf-colour-link-hover: oklch(44% 0.14 40);
198
198
  --color-link-hover: var(--sf-colour-link-hover);
199
- --sf-colour-ring: oklch(68% 0.18 44);
199
+ --sf-colour-ring: oklch(50% 0.16 40);
200
200
  --color-ring: var(--sf-colour-ring);
201
201
  --sf-button-radius: 0.5rem;
202
202
  --button-radius: 0.5rem;
@@ -10,6 +10,7 @@ Use the CLI for framework-specific output:
10
10
  pnpm exec synced-flow recipe saas-landing --markup
11
11
  pnpm exec synced-flow recipe saas-landing --framework next --markup
12
12
  pnpm exec synced-flow recipe saas-dashboard --framework next --markup
13
+ pnpm exec synced-flow recipe app-settings-workflow --markup
13
14
  pnpm exec synced-flow recipe portfolio-scroll --framework astro --markup
14
15
  ```
15
16
 
@@ -17,6 +18,7 @@ Templates:
17
18
 
18
19
  - `saas-landing.html`
19
20
  - `saas-dashboard.html`
21
+ - `app-settings-workflow.html`
20
22
  - `portfolio-scroll.html`
21
23
  - `blog-index.html`
22
24
  - `coming-soon.html`
@@ -0,0 +1,81 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Synced Flow Application Settings Template</title>
7
+ <link rel="stylesheet" href="../plain-html/synced-flow.css" />
8
+ </head>
9
+ <body>
10
+ <a class="sf-skip-link" href="#settings-main">Skip to settings</a>
11
+ <main class="sf-section" id="settings-main">
12
+ <div class="sf-container sf-split">
13
+ <nav aria-label="Setup progress">
14
+ <ol class="sf-stepper">
15
+ <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>
16
+ <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>
17
+ <li class="sf-stepper__step"><span class="sf-stepper__marker" aria-hidden="true"></span><span class="sf-stepper__label">Review</span></li>
18
+ </ol>
19
+ </nav>
20
+
21
+ <form class="sf-card sf-form" aria-labelledby="preferences-title">
22
+ <header class="sf-stack">
23
+ <p class="sf-kicker">Step 2 of 3</p>
24
+ <h1 id="preferences-title">Workspace preferences</h1>
25
+ </header>
26
+
27
+ <label class="sf-field">
28
+ <span class="sf-label">Workspace URL</span>
29
+ <span class="sf-input-group">
30
+ <span class="sf-input-group__leading" aria-hidden="true">https://</span>
31
+ <input class="sf-input" name="workspace" autocomplete="organization" />
32
+ <span class="sf-input-group__trailing" aria-hidden="true">.example.com</span>
33
+ </span>
34
+ </label>
35
+
36
+ <fieldset class="sf-fieldset">
37
+ <legend>Billing cycle</legend>
38
+ <div class="sf-segmented-control">
39
+ <label class="sf-segmented-control__option"><input type="radio" name="cycle" value="monthly" checked /><span class="sf-segmented-control__label">Monthly</span></label>
40
+ <label class="sf-segmented-control__option"><input type="radio" name="cycle" value="annual" /><span class="sf-segmented-control__label">Annual</span></label>
41
+ </div>
42
+ </fieldset>
43
+
44
+ <label class="sf-switch">
45
+ <input type="checkbox" role="switch" name="weekly_summary" checked />
46
+ <span class="sf-switch__control" aria-hidden="true"></span>
47
+ <span class="sf-switch__label">Email weekly summary</span>
48
+ </label>
49
+
50
+ <label class="sf-field">
51
+ <span class="sf-label" id="team-size-label">Team size: <output for="team-size">12</output></span>
52
+ <input class="sf-range" id="team-size" type="range" name="team_size" min="1" max="50" value="12" aria-labelledby="team-size-label" />
53
+ </label>
54
+
55
+ <label class="sf-field">
56
+ <span class="sf-label">Brand mark</span>
57
+ <input class="sf-file-input" type="file" name="brand_mark" accept="image/png,image/svg+xml" />
58
+ <span class="sf-help">PNG or SVG.</span>
59
+ </label>
60
+
61
+ <div class="sf-field">
62
+ <label class="sf-label" for="owner-search">Workspace owner</label>
63
+ <div class="sf-combobox">
64
+ <input class="sf-input" id="owner-search" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="owner-options" />
65
+ <ul class="sf-combobox__listbox" id="owner-options" role="listbox" hidden>
66
+ <li class="sf-combobox__option" id="owner-option-1" role="option" aria-selected="true">Alex Morgan</li>
67
+ <li class="sf-combobox__option" id="owner-option-2" role="option" aria-selected="false">Jamie Lee</li>
68
+ </ul>
69
+ </div>
70
+ <span class="sf-help">This static fixture shows presentation. The consuming app must implement combobox behavior.</span>
71
+ </div>
72
+
73
+ <div class="sf-wizard-actions">
74
+ <button class="sf-button sf-button--outline" type="button">Back</button>
75
+ <button class="sf-button" type="submit"><span class="sf-spinner" aria-hidden="true" hidden></span><span>Continue</span></button>
76
+ </div>
77
+ </form>
78
+ </div>
79
+ </main>
80
+ </body>
81
+ </html>
@@ -184,19 +184,19 @@
184
184
  --color-surface-elevated: var(--sf-colour-surface-elevated);
185
185
  --sf-colour-border: oklch(18% 0.026 250 / 0.12);
186
186
  --color-border: var(--sf-colour-border);
187
- --sf-colour-primary: oklch(68% 0.18 44);
187
+ --sf-colour-primary: oklch(50% 0.16 40);
188
188
  --color-primary: var(--sf-colour-primary);
189
- --sf-colour-primary-hover: oklch(60% 0.17 42);
189
+ --sf-colour-primary-hover: oklch(44% 0.14 40);
190
190
  --color-primary-hover: var(--sf-colour-primary-hover);
191
191
  --sf-colour-primary-foreground: oklch(100% 0 0);
192
192
  --color-primary-foreground: var(--sf-colour-primary-foreground);
193
193
  --sf-colour-accent: oklch(70% 0.12 205);
194
194
  --color-accent: var(--sf-colour-accent);
195
- --sf-colour-link: oklch(58% 0.17 42);
195
+ --sf-colour-link: oklch(50% 0.16 40);
196
196
  --color-link: var(--sf-colour-link);
197
- --sf-colour-link-hover: oklch(68% 0.18 44);
197
+ --sf-colour-link-hover: oklch(44% 0.14 40);
198
198
  --color-link-hover: var(--sf-colour-link-hover);
199
- --sf-colour-ring: oklch(68% 0.18 44);
199
+ --sf-colour-ring: oklch(50% 0.16 40);
200
200
  --color-ring: var(--sf-colour-ring);
201
201
  --sf-button-radius: 0.5rem;
202
202
  --button-radius: 0.5rem;
@@ -184,19 +184,19 @@
184
184
  --color-surface-elevated: var(--sf-colour-surface-elevated);
185
185
  --sf-colour-border: oklch(18% 0.026 250 / 0.12);
186
186
  --color-border: var(--sf-colour-border);
187
- --sf-colour-primary: oklch(68% 0.18 44);
187
+ --sf-colour-primary: oklch(50% 0.16 40);
188
188
  --color-primary: var(--sf-colour-primary);
189
- --sf-colour-primary-hover: oklch(60% 0.17 42);
189
+ --sf-colour-primary-hover: oklch(44% 0.14 40);
190
190
  --color-primary-hover: var(--sf-colour-primary-hover);
191
191
  --sf-colour-primary-foreground: oklch(100% 0 0);
192
192
  --color-primary-foreground: var(--sf-colour-primary-foreground);
193
193
  --sf-colour-accent: oklch(70% 0.12 205);
194
194
  --color-accent: var(--sf-colour-accent);
195
- --sf-colour-link: oklch(58% 0.17 42);
195
+ --sf-colour-link: oklch(50% 0.16 40);
196
196
  --color-link: var(--sf-colour-link);
197
- --sf-colour-link-hover: oklch(68% 0.18 44);
197
+ --sf-colour-link-hover: oklch(44% 0.14 40);
198
198
  --color-link-hover: var(--sf-colour-link-hover);
199
- --sf-colour-ring: oklch(68% 0.18 44);
199
+ --sf-colour-ring: oklch(50% 0.16 40);
200
200
  --color-ring: var(--sf-colour-ring);
201
201
  --sf-button-radius: 0.5rem;
202
202
  --button-radius: 0.5rem;
@@ -915,6 +915,8 @@
915
915
 
916
916
  .sf-data-table :where(th button, .sf-table-sort) {
917
917
  align-items: center;
918
+ background: transparent;
919
+ border: 0;
918
920
  color: inherit;
919
921
  display: inline-flex;
920
922
  font: inherit;
@@ -923,14 +925,14 @@
923
925
  padding: 0;
924
926
  }
925
927
 
926
- .sf-data-table :where(th button, .sf-table-sort)[aria-sort]::after {
928
+ .sf-data-table th[aria-sort] :where(button, .sf-table-sort)::after {
927
929
  color: var(--color-muted-foreground);
928
930
  content: "\2195";
929
931
  font-size: var(--step--2);
930
932
  }
931
933
 
932
- .sf-data-table :where(th button, .sf-table-sort)[aria-sort="ascending"]::after { content: "\2191"; }
933
- .sf-data-table :where(th button, .sf-table-sort)[aria-sort="descending"]::after { content: "\2193"; }
934
+ .sf-data-table th[aria-sort="ascending"] :where(button, .sf-table-sort)::after { content: "\2191"; }
935
+ .sf-data-table th[aria-sort="descending"] :where(button, .sf-table-sort)::after { content: "\2193"; }
934
936
 
935
937
  .sf-data-table tr[data-clickable="true"] {
936
938
  cursor: pointer;
@@ -1005,7 +1007,7 @@
1005
1007
  }
1006
1008
 
1007
1009
  .sf-status::before {
1008
- background: currentColor;
1010
+ background: var(--status-dot, currentColor);
1009
1011
  block-size: .42em;
1010
1012
  border-radius: var(--radius-full);
1011
1013
  content: "";
@@ -1015,25 +1017,29 @@
1015
1017
  .sf-status[data-tone="success"] {
1016
1018
  --status-bg: var(--sf-colour-success-soft, oklch(62% 0.13 150 / 0.12));
1017
1019
  --status-border: color-mix(in oklch, var(--sf-colour-success, oklch(62% 0.13 150)) 34%, transparent);
1018
- --status-color: color-mix(in oklch, var(--sf-colour-success, oklch(62% 0.13 150)) 76%, var(--color-foreground));
1020
+ --status-color: var(--color-foreground);
1021
+ --status-dot: var(--sf-colour-success, oklch(62% 0.13 150));
1019
1022
  }
1020
1023
 
1021
1024
  .sf-status[data-tone="warning"] {
1022
1025
  --status-bg: var(--sf-colour-warning-soft, oklch(78% 0.15 82 / 0.16));
1023
1026
  --status-border: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 38%, transparent);
1024
- --status-color: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 70%, var(--color-foreground));
1027
+ --status-color: var(--color-foreground);
1028
+ --status-dot: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 60%, var(--color-foreground));
1025
1029
  }
1026
1030
 
1027
1031
  .sf-status[data-tone="danger"] {
1028
1032
  --status-bg: var(--sf-colour-danger-soft, oklch(56% 0.2 28 / 0.12));
1029
1033
  --status-border: color-mix(in oklch, var(--sf-colour-danger, var(--color-destructive)) 34%, transparent);
1030
- --status-color: color-mix(in oklch, var(--sf-colour-danger, var(--color-destructive)) 76%, var(--color-foreground));
1034
+ --status-color: var(--color-foreground);
1035
+ --status-dot: var(--sf-colour-danger, var(--color-destructive));
1031
1036
  }
1032
1037
 
1033
1038
  .sf-status[data-tone="info"] {
1034
1039
  --status-bg: var(--sf-colour-info-soft, oklch(70% 0.12 205 / 0.14));
1035
1040
  --status-border: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 34%, transparent);
1036
- --status-color: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 76%, var(--color-foreground));
1041
+ --status-color: var(--color-foreground);
1042
+ --status-dot: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 68%, var(--color-foreground));
1037
1043
  }
1038
1044
 
1039
1045
  .sf-data-list {
@@ -1081,9 +1087,16 @@
1081
1087
  background: var(--color-surface-alt);
1082
1088
  }
1083
1089
 
1090
+ .sf-form {
1091
+ display: grid;
1092
+ gap: var(--form-gap, var(--space-s));
1093
+ grid-template-columns: minmax(0, 1fr);
1094
+ }
1095
+
1084
1096
  .sf-field {
1085
1097
  display: grid;
1086
1098
  gap: var(--space-2xs);
1099
+ grid-template-columns: minmax(0, 1fr);
1087
1100
  }
1088
1101
 
1089
1102
  .sf-form[data-density="compact"],
@@ -1146,6 +1159,130 @@
1146
1159
  outline: 0.125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
1147
1160
  outline-offset: 0;
1148
1161
  }
1162
+
1163
+ .sf-spinner {
1164
+ animation: sf-spinner-rotate .75s linear infinite;
1165
+ block-size: var(--spinner-size, 1em);
1166
+ border: .125em solid color-mix(in oklch, currentColor 28%, transparent);
1167
+ border-block-start-color: currentColor;
1168
+ border-radius: var(--radius-full);
1169
+ display: inline-block;
1170
+ flex: 0 0 auto;
1171
+ inline-size: var(--spinner-size, 1em);
1172
+ }
1173
+
1174
+ @keyframes sf-spinner-rotate { to { transform: rotate(1turn); } }
1175
+
1176
+ .sf-input-group {
1177
+ align-items: stretch;
1178
+ background: var(--color-card);
1179
+ border: 1px solid var(--color-border);
1180
+ border-radius: var(--input-radius, var(--radius-md));
1181
+ display: flex;
1182
+ inline-size: 100%;
1183
+ min-block-size: var(--input-block-size, 2.75rem);
1184
+ overflow: clip;
1185
+ }
1186
+
1187
+ .sf-input-group:focus-within {
1188
+ border-color: var(--color-ring);
1189
+ outline: .125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
1190
+ }
1191
+
1192
+ .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; }
1193
+ .sf-input-group__leading,
1194
+ .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; }
1195
+ .sf-input-group__leading { border-inline-end: 1px solid var(--color-border); }
1196
+ .sf-input-group__trailing,
1197
+ .sf-input-group__action { border-inline-start: 1px solid var(--color-border); }
1198
+ .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); }
1199
+ .sf-input-group[data-invalid="true"],
1200
+ .sf-field[data-invalid="true"] .sf-input-group { border-color: var(--color-destructive, var(--color-danger)); }
1201
+ .sf-input-group:has(:disabled) { background: var(--color-surface-alt); opacity: .72; }
1202
+
1203
+ .sf-file-input {
1204
+ background: var(--color-card);
1205
+ border: 1px solid var(--color-border);
1206
+ border-radius: var(--input-radius, var(--radius-md));
1207
+ color: var(--color-muted-foreground);
1208
+ inline-size: 100%;
1209
+ min-block-size: var(--input-block-size, 2.75rem);
1210
+ padding: var(--space-3xs);
1211
+ }
1212
+
1213
+ .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); }
1214
+ .sf-range { accent-color: var(--color-primary); inline-size: 100%; min-block-size: var(--input-block-size, 2.75rem); }
1215
+ .sf-range:focus-visible,
1216
+ .sf-file-input:focus-visible { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
1217
+ .sf-file-input[aria-invalid="true"],
1218
+ .sf-field[data-invalid="true"] .sf-file-input { border-color: var(--color-destructive, var(--color-danger)); }
1219
+ :where(.sf-file-input, .sf-range):disabled { opacity: .55; }
1220
+
1221
+ .sf-switch { align-items: center; display: inline-grid; gap: var(--space-xs); grid-template-columns: auto 1fr; position: relative; }
1222
+ .sf-switch > input[type="checkbox"],
1223
+ .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; }
1224
+ .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; }
1225
+ .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); }
1226
+ .sf-switch > input:checked + .sf-switch__control { background: var(--color-primary); border-color: var(--color-primary); }
1227
+ .sf-switch > input:checked + .sf-switch__control::before { background: var(--color-primary-foreground); transform: translateX(1.1875rem); }
1228
+ .sf-switch > input:focus-visible + .sf-switch__control { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
1229
+ .sf-switch__label { color: var(--color-foreground); font-weight: 700; }
1230
+ .sf-switch:has(input:disabled) { cursor: not-allowed; opacity: .55; }
1231
+
1232
+ .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); }
1233
+ .sf-segmented-control__option { cursor: pointer; position: relative; }
1234
+ .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); }
1235
+ .sf-segmented-control__option > input:checked + .sf-segmented-control__label { background: var(--color-card); border-color: var(--color-border); color: var(--color-foreground); }
1236
+ .sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
1237
+ .sf-segmented-control__option:has(input:disabled) { cursor: not-allowed; opacity: .55; }
1238
+
1239
+ .sf-combobox { position: relative; }
1240
+ .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; }
1241
+ .sf-combobox__option { border-radius: var(--radius-sm); cursor: pointer; padding: var(--space-2xs) var(--space-xs); }
1242
+ .sf-combobox__option:hover,
1243
+ .sf-combobox__option[aria-selected="true"],
1244
+ .sf-combobox__option[data-active="true"] { background: var(--color-surface-alt); }
1245
+ .sf-combobox__empty { color: var(--color-muted-foreground); padding: var(--space-xs); }
1246
+
1247
+ .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); }
1248
+ .sf-bulk-actions__count { font-size: var(--step--1); font-weight: 800; }
1249
+ .sf-bulk-actions__controls,
1250
+ .sf-pagination__controls,
1251
+ .sf-pagination__pages,
1252
+ .sf-pagination__jump { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
1253
+
1254
+ .sf-pagination { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); list-style: none; padding-inline-start: 0; }
1255
+ .sf-pagination[data-layout="complete"] { justify-content: space-between; row-gap: var(--space-xs); }
1256
+ .sf-pagination__summary,
1257
+ .sf-pagination__jump { color: var(--color-muted-foreground); font-size: var(--step--1); }
1258
+ .sf-pagination__jump .sf-input { inline-size: 5rem; }
1259
+
1260
+ .sf-stepper { counter-reset: sf-step; display: grid; gap: var(--space-xs); list-style: none; padding-inline-start: 0; }
1261
+ .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; }
1262
+ .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; }
1263
+ .sf-stepper__marker:empty::before { content: counter(sf-step); }
1264
+ .sf-stepper__step[aria-current="step"] { color: var(--color-foreground); }
1265
+ .sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-foreground); }
1266
+ .sf-stepper__label { font-weight: 700; }
1267
+ .sf-wizard-actions { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-xs); justify-content: space-between; }
1268
+
1269
+ @media (prefers-reduced-motion: reduce) {
1270
+ .sf-spinner { animation: none; border-color: currentColor; border-inline-end-color: transparent; }
1271
+ .sf-switch__control::before { transition: none; }
1272
+ }
1273
+
1274
+ @media (forced-colors: active) {
1275
+ :where(.sf-input-group, .sf-file-input, .sf-combobox__listbox, .sf-bulk-actions) { border-color: ButtonText; forced-color-adjust: auto; }
1276
+ :where(.sf-switch__control, .sf-segmented-control__label, .sf-stepper__marker) { background: Canvas; border-color: ButtonText; color: ButtonText; forced-color-adjust: none; }
1277
+ .sf-switch > input:focus-visible + .sf-switch__control,
1278
+ .sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid Highlight; outline-offset: .125rem; }
1279
+ .sf-switch > input:checked + .sf-switch__control,
1280
+ .sf-segmented-control__option > input:checked + .sf-segmented-control__label,
1281
+ .sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: Highlight; border-color: Highlight; color: HighlightText; }
1282
+ .sf-switch__control::before { background: ButtonText; }
1283
+ .sf-switch > input:checked + .sf-switch__control::before { background: HighlightText; }
1284
+ .sf-stepper__step[data-complete="true"] .sf-stepper__marker { background: Canvas; border-color: LinkText; color: LinkText; }
1285
+ }
1149
1286
  }
1150
1287
 
1151
1288
  @layer utilities {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncedco/flow",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "AI-friendly fluid CSS design system for brand-consistent websites and agency builds.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.2.2",
@@ -77,16 +77,27 @@
77
77
  "scripts": {
78
78
  "build": "node scripts/build-css.mjs && tsc -p tsconfig.build.json",
79
79
  "build:css": "node scripts/build-css.mjs",
80
- "check": "node scripts/build-css.mjs --check && tsc --noEmit && node scripts/guardrails.mjs",
80
+ "build:types": "tsc -p tsconfig.build.json",
81
+ "check": "pnpm check:generated && tsc --noEmit && node scripts/guardrails.mjs",
82
+ "check:generated": "pnpm build:types && node scripts/check-generated-css.mjs",
81
83
  "guardrails": "node scripts/guardrails.mjs",
82
84
  "clean": "rm -rf dist",
83
85
  "prepack": "node scripts/build-css.mjs && tsc -p tsconfig.build.json",
84
86
  "prepare": "node scripts/build-css.mjs && tsc -p tsconfig.build.json",
85
- "test": "pnpm build && node --test tests/*.test.mjs",
87
+ "test": "pnpm check:generated && pnpm build && pnpm test:unit",
88
+ "test:unit": "node --test tests/*.test.mjs",
89
+ "test:browser": "playwright test --project=chromium",
90
+ "test:browser:all": "playwright test",
86
91
  "type-check": "tsc --noEmit"
87
92
  },
88
93
  "devDependencies": {
94
+ "@axe-core/playwright": "^4.13.0",
95
+ "@playwright/test": "^1.63.0",
89
96
  "@types/node": "^22.15.21",
97
+ "@types/react": "^19.2.18",
98
+ "@types/react-test-renderer": "19.1.0",
99
+ "react": "^19.2.8",
100
+ "react-test-renderer": "19.2.8",
90
101
  "typescript": "^5.8.3"
91
102
  },
92
103
  "engines": {
@@ -91,14 +91,24 @@ intact. Leave out `utilities.css` unless static helpers such as `sf-text-*`,
91
91
  `sf-app-backdrop`, `sf-frame`, and `sf-flow`.
92
92
  - Prefer component primitives for common UI: `sf-button`, `sf-card`,
93
93
  `sf-badge`, `sf-status`, `sf-field`, `sf-input`, `sf-search`,
94
- `sf-data-table`, `sf-table-sort`, `sf-tab__count`, and `sf-toast-stack`.
94
+ `sf-input-group`, `sf-file-input`, `sf-range`, `sf-switch`,
95
+ `sf-segmented-control`, `sf-spinner`, `sf-data-table`, `sf-table-sort`,
96
+ `sf-tab__count`, and `sf-toast-stack`.
95
97
  - Use `saas-landing` for public SaaS marketing pages. Use `saas-dashboard` when
96
98
  a full authenticated starter is useful, but prefer neutral app patterns for
97
99
  reusable UI in admin panels, portals, CRMs, analytics dashboards, internal
98
100
  tools, account areas, and product workspaces.
99
101
  - Use app patterns before writing project CSS:
100
102
  `app-shell-layout`, `row-action-menu`, `tabs-with-counts`,
101
- `data-table-actions`, `search-field`, `modal-form`, and `toast-stack`.
103
+ `data-table-actions`, `search-field`, `modal-form`, `toast-stack`,
104
+ `searchable-select`, `bulk-actions-toolbar`, `complete-pagination`, and
105
+ `multi-step-form`.
106
+ - Use `switch-control` for independent binary settings and
107
+ `segmented-control` for one choice from a small set. Do not use either as a
108
+ substitute for tabs.
109
+ - Treat `searchable-select`, async loading, bulk selection, and multi-step
110
+ navigation as app-owned behavior. Flow supplies presentation and accessible
111
+ markup guidance, not JavaScript state machines.
102
112
  - Use app shell states instead of bespoke layout CSS: `data-layout="fixed-header"`
103
113
  for fixed app headers, `data-collapsed="true"` for collapsed sidebars, and
104
114
  `data-mobile="drawer"` with `data-sidebar-open="true"` for mobile drawer
@@ -160,9 +170,16 @@ pnpm flow:doctor
160
170
  For package or repo changes, also run:
161
171
 
162
172
  ```bash
173
+ pnpm check:generated
163
174
  pnpm check
164
175
  pnpm test
176
+ pnpm test:browser
177
+ pnpm test:browser:all
165
178
  pnpm pack --dry-run
166
179
  ```
167
180
 
168
- If `doctor` warns about stale CSS, run `pnpm flow:build`.
181
+ Run `pnpm check:generated` before a build or lifecycle command can refresh
182
+ tracked CSS. It covers package CSS and the plain HTML, Vite, Next, Astro, and
183
+ WordPress generated outputs. Use the Chromium browser gate during development
184
+ and all three browser engines before a release. If `doctor` warns about stale
185
+ project CSS, run `pnpm flow:build`.
package/src/index.ts CHANGED
@@ -48,10 +48,79 @@ export const fluidSystem = {
48
48
  },
49
49
  components: {
50
50
  button: 'sf-button',
51
+ buttonGroup: 'sf-button-group',
52
+ icon: 'sf-icon',
53
+ iconButton: 'sf-icon-button',
51
54
  card: 'sf-card',
55
+ surface: 'sf-surface',
56
+ hero: 'sf-hero',
57
+ logoCloud: 'sf-logo-cloud',
58
+ feature: 'sf-feature',
59
+ stats: 'sf-stats',
60
+ testimonial: 'sf-testimonial',
61
+ pricingGrid: 'sf-pricing-grid',
62
+ faq: 'sf-faq',
63
+ callToAction: 'sf-cta',
64
+ footer: 'sf-footer',
52
65
  badge: 'sf-badge',
66
+ avatar: 'sf-avatar',
67
+ alert: 'sf-alert',
68
+ status: 'sf-status',
69
+ sectionHeader: 'sf-section-header',
70
+ kicker: 'sf-kicker',
71
+ nav: 'sf-nav',
72
+ form: 'sf-form',
73
+ fieldset: 'sf-fieldset',
53
74
  field: 'sf-field',
75
+ label: 'sf-label',
76
+ help: 'sf-help',
77
+ error: 'sf-error',
54
78
  input: 'sf-input',
79
+ inputGroup: 'sf-input-group',
80
+ select: 'sf-select',
81
+ textarea: 'sf-textarea',
82
+ check: 'sf-check',
83
+ switch: 'sf-switch',
84
+ fileInput: 'sf-file-input',
85
+ range: 'sf-range',
86
+ segmentedControl: 'sf-segmented-control',
87
+ spinner: 'sf-spinner',
88
+ search: 'sf-search',
89
+ combobox: 'sf-combobox',
90
+ filterBar: 'sf-filter-bar',
91
+ bulkActions: 'sf-bulk-actions',
92
+ dataTable: 'sf-data-table',
93
+ tableSort: 'sf-table-sort',
94
+ dataList: 'sf-data-list',
95
+ emptyState: 'sf-empty-state',
96
+ settingsSection: 'sf-settings-section',
97
+ detailPanel: 'sf-detail-panel',
98
+ dialog: 'sf-dialog',
99
+ popover: 'sf-popover',
100
+ drawer: 'sf-drawer',
101
+ tooltip: 'sf-tooltip',
102
+ toast: 'sf-toast',
103
+ toastStack: 'sf-toast-stack',
104
+ banner: 'sf-banner',
105
+ disclosure: 'sf-disclosure',
106
+ accordion: 'sf-accordion',
107
+ tabs: 'sf-tabs',
108
+ tab: 'sf-tab',
109
+ menu: 'sf-menu',
110
+ breadcrumb: 'sf-breadcrumb',
111
+ pagination: 'sf-pagination',
112
+ stepper: 'sf-stepper',
113
+ wizardActions: 'sf-wizard-actions',
114
+ progress: 'sf-progress',
115
+ skeleton: 'sf-skeleton',
116
+ chart: 'sf-chart',
117
+ meter: 'sf-meter',
118
+ codeWindow: 'sf-code-window',
119
+ codeLines: 'sf-code-lines',
120
+ tokenStrip: 'sf-token-strip',
121
+ marquee: 'sf-marquee',
122
+ commandList: 'sf-command-list',
123
+ platformCard: 'sf-platform-card',
55
124
  },
56
125
  utilities: {
57
126
  visuallyHidden: 'sf-visually-hidden',
package/src/presets.mjs CHANGED
@@ -12,13 +12,13 @@ export const themePresets = {
12
12
  surfaceAlt: 'oklch(96.4% 0.011 80)',
13
13
  surfaceElevated: 'var(--sf-colour-surface-raised)',
14
14
  border: 'oklch(18% 0.026 250 / 0.12)',
15
- primary: 'oklch(68% 0.18 44)',
16
- primaryHover: 'oklch(60% 0.17 42)',
15
+ primary: 'oklch(50% 0.16 40)',
16
+ primaryHover: 'oklch(44% 0.14 40)',
17
17
  primaryForeground: 'oklch(100% 0 0)',
18
18
  accent: 'oklch(70% 0.12 205)',
19
- link: 'oklch(58% 0.17 42)',
20
- linkHover: 'oklch(68% 0.18 44)',
21
- ring: 'oklch(68% 0.18 44)',
19
+ link: 'oklch(50% 0.16 40)',
20
+ linkHover: 'oklch(44% 0.14 40)',
21
+ ring: 'oklch(50% 0.16 40)',
22
22
  },
23
23
  components: {
24
24
  button: {