@syncedco/flow 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -602,16 +602,6 @@
602
602
  }
603
603
  }
604
604
 
605
- :where(.space-y-1, .space-y-2, .space-y-3, .space-y-4, .space-y-5, .space-y-6, .space-y-8, .space-y-10, .space-y-12) > * + * {
606
- margin-block-start: var(--sf-space-y);
607
- }
608
-
609
- :where(.space-x-1, .space-x-2, .space-x-3, .space-x-4, .space-x-5, .space-x-6, .space-x-8) > * + * {
610
- margin-inline-start: var(--sf-space-x);
611
- }
612
-
613
- .divide-y > * + * { border-block-start-width: var(--sf-divide-y); }
614
- .divide-x > * + * { border-inline-start-width: var(--sf-divide-x); }
615
605
  }
616
606
 
617
607
  @layer components {
@@ -915,6 +905,8 @@
915
905
 
916
906
  .sf-data-table :where(th button, .sf-table-sort) {
917
907
  align-items: center;
908
+ background: transparent;
909
+ border: 0;
918
910
  color: inherit;
919
911
  display: inline-flex;
920
912
  font: inherit;
@@ -923,14 +915,14 @@
923
915
  padding: 0;
924
916
  }
925
917
 
926
- .sf-data-table :where(th button, .sf-table-sort)[aria-sort]::after {
918
+ .sf-data-table th[aria-sort] :where(button, .sf-table-sort)::after {
927
919
  color: var(--color-muted-foreground);
928
920
  content: "\2195";
929
921
  font-size: var(--step--2);
930
922
  }
931
923
 
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"; }
924
+ .sf-data-table th[aria-sort="ascending"] :where(button, .sf-table-sort)::after { content: "\2191"; }
925
+ .sf-data-table th[aria-sort="descending"] :where(button, .sf-table-sort)::after { content: "\2193"; }
934
926
 
935
927
  .sf-data-table tr[data-clickable="true"] {
936
928
  cursor: pointer;
@@ -1005,7 +997,7 @@
1005
997
  }
1006
998
 
1007
999
  .sf-status::before {
1008
- background: currentColor;
1000
+ background: var(--status-dot, currentColor);
1009
1001
  block-size: .42em;
1010
1002
  border-radius: var(--radius-full);
1011
1003
  content: "";
@@ -1015,25 +1007,29 @@
1015
1007
  .sf-status[data-tone="success"] {
1016
1008
  --status-bg: var(--sf-colour-success-soft, oklch(62% 0.13 150 / 0.12));
1017
1009
  --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));
1010
+ --status-color: var(--color-foreground);
1011
+ --status-dot: var(--sf-colour-success, oklch(62% 0.13 150));
1019
1012
  }
1020
1013
 
1021
1014
  .sf-status[data-tone="warning"] {
1022
1015
  --status-bg: var(--sf-colour-warning-soft, oklch(78% 0.15 82 / 0.16));
1023
1016
  --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));
1017
+ --status-color: var(--color-foreground);
1018
+ --status-dot: color-mix(in oklch, var(--sf-colour-warning, oklch(78% 0.15 82)) 60%, var(--color-foreground));
1025
1019
  }
1026
1020
 
1027
1021
  .sf-status[data-tone="danger"] {
1028
1022
  --status-bg: var(--sf-colour-danger-soft, oklch(56% 0.2 28 / 0.12));
1029
1023
  --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));
1024
+ --status-color: var(--color-foreground);
1025
+ --status-dot: var(--sf-colour-danger, var(--color-destructive));
1031
1026
  }
1032
1027
 
1033
1028
  .sf-status[data-tone="info"] {
1034
1029
  --status-bg: var(--sf-colour-info-soft, oklch(70% 0.12 205 / 0.14));
1035
1030
  --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));
1031
+ --status-color: var(--color-foreground);
1032
+ --status-dot: color-mix(in oklch, var(--sf-colour-info, oklch(70% 0.12 205)) 68%, var(--color-foreground));
1037
1033
  }
1038
1034
 
1039
1035
  .sf-data-list {
@@ -1081,9 +1077,16 @@
1081
1077
  background: var(--color-surface-alt);
1082
1078
  }
1083
1079
 
1080
+ .sf-form {
1081
+ display: grid;
1082
+ gap: var(--form-gap, var(--space-s));
1083
+ grid-template-columns: minmax(0, 1fr);
1084
+ }
1085
+
1084
1086
  .sf-field {
1085
1087
  display: grid;
1086
1088
  gap: var(--space-2xs);
1089
+ grid-template-columns: minmax(0, 1fr);
1087
1090
  }
1088
1091
 
1089
1092
  .sf-form[data-density="compact"],
@@ -1146,6 +1149,130 @@
1146
1149
  outline: 0.125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
1147
1150
  outline-offset: 0;
1148
1151
  }
1152
+
1153
+ .sf-spinner {
1154
+ animation: sf-spinner-rotate .75s linear infinite;
1155
+ block-size: var(--spinner-size, 1em);
1156
+ border: .125em solid color-mix(in oklch, currentColor 28%, transparent);
1157
+ border-block-start-color: currentColor;
1158
+ border-radius: var(--radius-full);
1159
+ display: inline-block;
1160
+ flex: 0 0 auto;
1161
+ inline-size: var(--spinner-size, 1em);
1162
+ }
1163
+
1164
+ @keyframes sf-spinner-rotate { to { transform: rotate(1turn); } }
1165
+
1166
+ .sf-input-group {
1167
+ align-items: stretch;
1168
+ background: var(--color-card);
1169
+ border: 1px solid var(--color-border);
1170
+ border-radius: var(--input-radius, var(--radius-md));
1171
+ display: flex;
1172
+ inline-size: 100%;
1173
+ min-block-size: var(--input-block-size, 2.75rem);
1174
+ overflow: clip;
1175
+ }
1176
+
1177
+ .sf-input-group:focus-within {
1178
+ border-color: var(--color-ring);
1179
+ outline: .125rem solid color-mix(in oklch, var(--color-ring) 24%, transparent);
1180
+ }
1181
+
1182
+ .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; }
1183
+ .sf-input-group__leading,
1184
+ .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; }
1185
+ .sf-input-group__leading { border-inline-end: 1px solid var(--color-border); }
1186
+ .sf-input-group__trailing,
1187
+ .sf-input-group__action { border-inline-start: 1px solid var(--color-border); }
1188
+ .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); }
1189
+ .sf-input-group[data-invalid="true"],
1190
+ .sf-field[data-invalid="true"] .sf-input-group { border-color: var(--color-destructive, var(--color-danger)); }
1191
+ .sf-input-group:has(:disabled) { background: var(--color-surface-alt); opacity: .72; }
1192
+
1193
+ .sf-file-input {
1194
+ background: var(--color-card);
1195
+ border: 1px solid var(--color-border);
1196
+ border-radius: var(--input-radius, var(--radius-md));
1197
+ color: var(--color-muted-foreground);
1198
+ inline-size: 100%;
1199
+ min-block-size: var(--input-block-size, 2.75rem);
1200
+ padding: var(--space-3xs);
1201
+ }
1202
+
1203
+ .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); }
1204
+ .sf-range { accent-color: var(--color-primary); inline-size: 100%; min-block-size: var(--input-block-size, 2.75rem); }
1205
+ .sf-range:focus-visible,
1206
+ .sf-file-input:focus-visible { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
1207
+ .sf-file-input[aria-invalid="true"],
1208
+ .sf-field[data-invalid="true"] .sf-file-input { border-color: var(--color-destructive, var(--color-danger)); }
1209
+ :where(.sf-file-input, .sf-range):disabled { opacity: .55; }
1210
+
1211
+ .sf-switch { align-items: center; display: inline-grid; gap: var(--space-xs); grid-template-columns: auto 1fr; position: relative; }
1212
+ .sf-switch > input[type="checkbox"],
1213
+ .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; }
1214
+ .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; }
1215
+ .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); }
1216
+ .sf-switch > input:checked + .sf-switch__control { background: var(--color-primary); border-color: var(--color-primary); }
1217
+ .sf-switch > input:checked + .sf-switch__control::before { background: var(--color-primary-foreground); transform: translateX(1.1875rem); }
1218
+ .sf-switch > input:focus-visible + .sf-switch__control { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
1219
+ .sf-switch__label { color: var(--color-foreground); font-weight: 700; }
1220
+ .sf-switch:has(input:disabled) { cursor: not-allowed; opacity: .55; }
1221
+
1222
+ .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); }
1223
+ .sf-segmented-control__option { cursor: pointer; position: relative; }
1224
+ .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); }
1225
+ .sf-segmented-control__option > input:checked + .sf-segmented-control__label { background: var(--color-card); border-color: var(--color-border); color: var(--color-foreground); }
1226
+ .sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid var(--color-ring); outline-offset: .125rem; }
1227
+ .sf-segmented-control__option:has(input:disabled) { cursor: not-allowed; opacity: .55; }
1228
+
1229
+ .sf-combobox { position: relative; }
1230
+ .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; }
1231
+ .sf-combobox__option { border-radius: var(--radius-sm); cursor: pointer; padding: var(--space-2xs) var(--space-xs); }
1232
+ .sf-combobox__option:hover,
1233
+ .sf-combobox__option[aria-selected="true"],
1234
+ .sf-combobox__option[data-active="true"] { background: var(--color-surface-alt); }
1235
+ .sf-combobox__empty { color: var(--color-muted-foreground); padding: var(--space-xs); }
1236
+
1237
+ .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); }
1238
+ .sf-bulk-actions__count { font-size: var(--step--1); font-weight: 800; }
1239
+ .sf-bulk-actions__controls,
1240
+ .sf-pagination__controls,
1241
+ .sf-pagination__pages,
1242
+ .sf-pagination__jump { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
1243
+
1244
+ .sf-pagination { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-2xs); list-style: none; padding-inline-start: 0; }
1245
+ .sf-pagination[data-layout="complete"] { justify-content: space-between; row-gap: var(--space-xs); }
1246
+ .sf-pagination__summary,
1247
+ .sf-pagination__jump { color: var(--color-muted-foreground); font-size: var(--step--1); }
1248
+ .sf-pagination__jump .sf-input { inline-size: 5rem; }
1249
+
1250
+ .sf-stepper { counter-reset: sf-step; display: grid; gap: var(--space-xs); list-style: none; padding-inline-start: 0; }
1251
+ .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; }
1252
+ .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; }
1253
+ .sf-stepper__marker:empty::before { content: counter(sf-step); }
1254
+ .sf-stepper__step[aria-current="step"] { color: var(--color-foreground); }
1255
+ .sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: var(--color-primary); border-color: var(--color-primary); color: var(--color-primary-foreground); }
1256
+ .sf-stepper__label { font-weight: 700; }
1257
+ .sf-wizard-actions { align-items: center; display: flex; flex-wrap: wrap; gap: var(--space-xs); justify-content: space-between; }
1258
+
1259
+ @media (prefers-reduced-motion: reduce) {
1260
+ .sf-spinner { animation: none; border-color: currentColor; border-inline-end-color: transparent; }
1261
+ .sf-switch__control::before { transition: none; }
1262
+ }
1263
+
1264
+ @media (forced-colors: active) {
1265
+ :where(.sf-input-group, .sf-file-input, .sf-combobox__listbox, .sf-bulk-actions) { border-color: ButtonText; forced-color-adjust: auto; }
1266
+ :where(.sf-switch__control, .sf-segmented-control__label, .sf-stepper__marker) { background: Canvas; border-color: ButtonText; color: ButtonText; forced-color-adjust: none; }
1267
+ .sf-switch > input:focus-visible + .sf-switch__control,
1268
+ .sf-segmented-control__option > input:focus + .sf-segmented-control__label { outline: .125rem solid Highlight; outline-offset: .125rem; }
1269
+ .sf-switch > input:checked + .sf-switch__control,
1270
+ .sf-segmented-control__option > input:checked + .sf-segmented-control__label,
1271
+ .sf-stepper__step[aria-current="step"] .sf-stepper__marker { background: Highlight; border-color: Highlight; color: HighlightText; }
1272
+ .sf-switch__control::before { background: ButtonText; }
1273
+ .sf-switch > input:checked + .sf-switch__control::before { background: HighlightText; }
1274
+ .sf-stepper__step[data-complete="true"] .sf-stepper__marker { background: Canvas; border-color: LinkText; color: LinkText; }
1275
+ }
1149
1276
  }
1150
1277
 
1151
1278
  @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.5.0",
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: {