altitude-design 0.2.0 → 0.3.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.
Files changed (295) hide show
  1. package/README.md +46 -8
  2. package/dist/adapters/codemirror.cjs +88 -0
  3. package/dist/adapters/codemirror.cjs.map +7 -0
  4. package/dist/adapters/codemirror.js +67 -0
  5. package/dist/adapters/codemirror.js.map +7 -0
  6. package/dist/adapters/react-flow.css +102 -0
  7. package/dist/adapters/tiptap.css +71 -0
  8. package/dist/altitude.css +3165 -246
  9. package/dist/altitude.iife.js +33 -5
  10. package/dist/altitude.no-fonts.css +3165 -246
  11. package/dist/chart-themes.cjs +195 -0
  12. package/dist/chart-themes.cjs.map +7 -0
  13. package/dist/chart-themes.js +165 -0
  14. package/dist/chart-themes.js.map +7 -0
  15. package/dist/cli/altitude.mjs +134 -0
  16. package/dist/guidance/README.md +32 -0
  17. package/dist/guidance/agent-setup.md +22 -0
  18. package/dist/guidance/app-skeleton.md +17 -0
  19. package/dist/guidance/evidence.json +95 -0
  20. package/dist/guidance/index.json +493 -0
  21. package/dist/guidance/llms.txt +20 -0
  22. package/dist/guidance/manifest.json +92 -0
  23. package/dist/guidance/pattern.schema.json +181 -0
  24. package/dist/guidance/patterns/agent-action.json +77 -0
  25. package/dist/guidance/patterns/agent-action.md +39 -0
  26. package/dist/guidance/patterns/assistant-conversation.json +77 -0
  27. package/dist/guidance/patterns/assistant-conversation.md +50 -0
  28. package/dist/guidance/patterns/async-collection.json +71 -0
  29. package/dist/guidance/patterns/async-collection.md +29 -0
  30. package/dist/guidance/patterns/audit-log.json +75 -0
  31. package/dist/guidance/patterns/audit-log.md +27 -0
  32. package/dist/guidance/patterns/auth.json +86 -0
  33. package/dist/guidance/patterns/auth.md +29 -0
  34. package/dist/guidance/patterns/billing.json +80 -0
  35. package/dist/guidance/patterns/billing.md +29 -0
  36. package/dist/guidance/patterns/consequential-action.json +76 -0
  37. package/dist/guidance/patterns/consequential-action.md +27 -0
  38. package/dist/guidance/patterns/developer-settings.json +79 -0
  39. package/dist/guidance/patterns/developer-settings.md +29 -0
  40. package/dist/guidance/patterns/edit-record.json +74 -0
  41. package/dist/guidance/patterns/edit-record.md +51 -0
  42. package/dist/guidance/patterns/import.json +77 -0
  43. package/dist/guidance/patterns/import.md +31 -0
  44. package/dist/guidance/patterns/members.json +77 -0
  45. package/dist/guidance/patterns/members.md +27 -0
  46. package/dist/guidance/patterns/onboarding.json +78 -0
  47. package/dist/guidance/patterns/onboarding.md +31 -0
  48. package/dist/guidance/patterns/review-submit.json +72 -0
  49. package/dist/guidance/patterns/review-submit.md +27 -0
  50. package/dist/guidance/patterns/screen-layout.json +79 -0
  51. package/dist/guidance/patterns/screen-layout.md +40 -0
  52. package/dist/guidance/patterns/settings.json +78 -0
  53. package/dist/guidance/patterns/settings.md +32 -0
  54. package/dist/guidance/patterns/sync.json +77 -0
  55. package/dist/guidance/patterns/sync.md +31 -0
  56. package/dist/guidance/principles.md +23 -0
  57. package/dist/guidance/references/build/checks/agent-action-pattern.mjs +57 -0
  58. package/dist/guidance/references/build/checks/agent-setup.mjs +78 -0
  59. package/dist/guidance/references/build/checks/assistant-conversation-pattern.mjs +49 -0
  60. package/dist/guidance/references/build/checks/audit-log-pattern.mjs +63 -0
  61. package/dist/guidance/references/build/checks/auth-pattern.mjs +67 -0
  62. package/dist/guidance/references/build/checks/billing-pattern.mjs +63 -0
  63. package/dist/guidance/references/build/checks/developer-settings-pattern.mjs +76 -0
  64. package/dist/guidance/references/build/checks/guidance.mjs +71 -0
  65. package/dist/guidance/references/build/checks/import-pattern.mjs +69 -0
  66. package/dist/guidance/references/build/checks/members-pattern.mjs +71 -0
  67. package/dist/guidance/references/build/checks/onboarding-pattern.mjs +54 -0
  68. package/dist/guidance/references/build/checks/pattern-types.mjs +13 -0
  69. package/dist/guidance/references/build/checks/pattern-workflows.mjs +226 -0
  70. package/dist/guidance/references/build/checks/primary-scope.mjs +49 -0
  71. package/dist/guidance/references/build/checks/render-tells.mjs +72 -0
  72. package/dist/guidance/references/build/checks/screen-layout-pattern.mjs +111 -0
  73. package/dist/guidance/references/build/checks/semantics.mjs +72 -0
  74. package/dist/guidance/references/build/checks/settings-pattern.mjs +73 -0
  75. package/dist/guidance/references/build/checks/sync-pattern.mjs +62 -0
  76. package/dist/guidance/references/build/checks/tells-lint.mjs +164 -0
  77. package/dist/guidance/references/build/fixtures/pattern-workflows.tsx +83 -0
  78. package/dist/guidance/references/build/fixtures/patterns/agent-action.jsx +60 -0
  79. package/dist/guidance/references/build/fixtures/patterns/app-skeleton.jsx +94 -0
  80. package/dist/guidance/references/build/fixtures/patterns/assistant-conversation.jsx +79 -0
  81. package/dist/guidance/references/build/fixtures/patterns/audit-log.jsx +81 -0
  82. package/dist/guidance/references/build/fixtures/patterns/auth.jsx +150 -0
  83. package/dist/guidance/references/build/fixtures/patterns/billing.jsx +96 -0
  84. package/dist/guidance/references/build/fixtures/patterns/developer-settings.jsx +95 -0
  85. package/dist/guidance/references/build/fixtures/patterns/import.jsx +102 -0
  86. package/dist/guidance/references/build/fixtures/patterns/members.jsx +106 -0
  87. package/dist/guidance/references/build/fixtures/patterns/onboarding.jsx +70 -0
  88. package/dist/guidance/references/build/fixtures/patterns/screen-layout-decision.jsx +74 -0
  89. package/dist/guidance/references/build/fixtures/patterns/screen-layout-editor.jsx +56 -0
  90. package/dist/guidance/references/build/fixtures/patterns/screen-layout-report.jsx +42 -0
  91. package/dist/guidance/references/build/fixtures/patterns/settings.jsx +73 -0
  92. package/dist/guidance/references/build/fixtures/patterns/sync.jsx +78 -0
  93. package/dist/guidance/references/project/components/src/pattern-async-collection.tsx +57 -0
  94. package/dist/guidance/references/project/components/src/pattern-consequential-action.tsx +77 -0
  95. package/dist/guidance/references/project/components/src/pattern-edit-record.tsx +127 -0
  96. package/dist/guidance/references/project/components/src/pattern-review-submit.tsx +47 -0
  97. package/dist/guidance/references/project/components/src/settings.tsx +79 -0
  98. package/dist/guidance/references/scripts/build-framework-fixture.mjs +20 -0
  99. package/dist/guidance/rules.json +653 -0
  100. package/dist/guidance/visual/Elevation.md +194 -0
  101. package/dist/guidance/visual/Fundamentals.md +83 -0
  102. package/dist/guidance/visual/Research.md +1620 -0
  103. package/dist/index.cjs +19009 -1854
  104. package/dist/index.cjs.map +4 -4
  105. package/dist/index.js +19009 -1854
  106. package/dist/index.js.map +4 -4
  107. package/dist/tokens.css +84 -84
  108. package/dist/tokens.json +301 -128
  109. package/dist/types/adapter-codemirror.d.ts +13 -0
  110. package/dist/types/agent-fleet.d.ts +166 -0
  111. package/dist/types/ai-label.d.ts +30 -0
  112. package/dist/types/app-shell.d.ts +83 -0
  113. package/dist/types/area-chart.d.ts +80 -0
  114. package/dist/types/artifact.d.ts +65 -0
  115. package/dist/types/bar-chart.d.ts +83 -0
  116. package/dist/types/bar-list.d.ts +35 -0
  117. package/dist/types/breadcrumb.d.ts +62 -0
  118. package/dist/types/chart-brush.d.ts +35 -0
  119. package/dist/types/chart-themes.d.ts +229 -0
  120. package/dist/types/chart.d.ts +570 -0
  121. package/dist/types/checklist.d.ts +71 -0
  122. package/dist/types/code-block.d.ts +32 -0
  123. package/dist/types/collections.d.ts +116 -5
  124. package/dist/types/color-picker.d.ts +38 -0
  125. package/dist/types/command.d.ts +6 -3
  126. package/dist/types/comment-thread.d.ts +48 -0
  127. package/dist/types/confirmation.d.ts +60 -0
  128. package/dist/types/context-menu.d.ts +17 -0
  129. package/dist/types/controls.d.ts +134 -22
  130. package/dist/types/conversation.d.ts +130 -0
  131. package/dist/types/copy-button.d.ts +19 -0
  132. package/dist/types/core.d.ts +139 -10
  133. package/dist/types/data-grid.d.ts +76 -0
  134. package/dist/types/date-picker.d.ts +96 -0
  135. package/dist/types/diff-view.d.ts +54 -0
  136. package/dist/types/dot-plot.d.ts +54 -0
  137. package/dist/types/edit-region.d.ts +121 -0
  138. package/dist/types/feature-gate.d.ts +22 -0
  139. package/dist/types/filter-bar.d.ts +65 -0
  140. package/dist/types/formatted-number.d.ts +38 -0
  141. package/dist/types/heatmap.d.ts +64 -0
  142. package/dist/types/in-place.d.ts +24 -0
  143. package/dist/types/index.d.ts +159 -14
  144. package/dist/types/inline-citation.d.ts +35 -0
  145. package/dist/types/inline-edit.d.ts +71 -0
  146. package/dist/types/input-otp.d.ts +37 -0
  147. package/dist/types/item-group.d.ts +104 -0
  148. package/dist/types/line-chart.d.ts +116 -0
  149. package/dist/types/link.d.ts +11 -0
  150. package/dist/types/markdown.d.ts +88 -0
  151. package/dist/types/meter.d.ts +43 -0
  152. package/dist/types/modal-state.d.ts +62 -0
  153. package/dist/types/nav.d.ts +8 -1
  154. package/dist/types/notifications.d.ts +42 -0
  155. package/dist/types/number-input.d.ts +30 -0
  156. package/dist/types/overlays.d.ts +56 -15
  157. package/dist/types/page-header.d.ts +27 -0
  158. package/dist/types/pagination.d.ts +29 -0
  159. package/dist/types/palette.d.ts +64 -0
  160. package/dist/types/password-input.d.ts +22 -0
  161. package/dist/types/pattern-assistant-conversation.d.ts +55 -0
  162. package/dist/types/pattern-async-collection.d.ts +34 -0
  163. package/dist/types/pattern-consequential-action.d.ts +53 -0
  164. package/dist/types/pattern-edit-record.d.ts +72 -0
  165. package/dist/types/pattern-review-submit.d.ts +43 -0
  166. package/dist/types/permission-matrix.d.ts +34 -0
  167. package/dist/types/plan-picker.d.ts +40 -0
  168. package/dist/types/prompt-input.d.ts +106 -0
  169. package/dist/types/property-list.d.ts +67 -0
  170. package/dist/types/question.d.ts +64 -0
  171. package/dist/types/relative-time.d.ts +21 -0
  172. package/dist/types/removal-receipt.d.ts +1180 -0
  173. package/dist/types/resizable.d.ts +48 -0
  174. package/dist/types/roving.d.ts +44 -0
  175. package/dist/types/save-bar.d.ts +38 -0
  176. package/dist/types/saved-views.d.ts +72 -0
  177. package/dist/types/scatter-chart.d.ts +60 -0
  178. package/dist/types/schedule.d.ts +80 -0
  179. package/dist/types/secret-field.d.ts +33 -0
  180. package/dist/types/select.d.ts +44 -2
  181. package/dist/types/separator.d.ts +11 -0
  182. package/dist/types/settings.d.ts +39 -0
  183. package/dist/types/sheet.d.ts +1 -1
  184. package/dist/types/sparkline.d.ts +46 -0
  185. package/dist/types/split-button.d.ts +21 -0
  186. package/dist/types/stat.d.ts +54 -0
  187. package/dist/types/stepper.d.ts +6 -3
  188. package/dist/types/suggestions.d.ts +29 -0
  189. package/dist/types/suspense.d.ts +85 -0
  190. package/dist/types/sync-indicator.d.ts +24 -0
  191. package/dist/types/tag-input.d.ts +65 -0
  192. package/dist/types/terminal.d.ts +33 -0
  193. package/dist/types/thread-list.d.ts +58 -0
  194. package/dist/types/time-input.d.ts +33 -0
  195. package/dist/types/timeline.d.ts +38 -0
  196. package/dist/types/toggle.d.ts +46 -0
  197. package/dist/types/toolbar.d.ts +34 -0
  198. package/dist/types/tour.d.ts +23 -0
  199. package/dist/types/trace.d.ts +65 -0
  200. package/dist/types/tree.d.ts +71 -0
  201. package/dist/types-cjs/adapter-codemirror.d.cts +13 -0
  202. package/dist/types-cjs/agent-fleet.d.cts +166 -0
  203. package/dist/types-cjs/ai-label.d.cts +30 -0
  204. package/dist/types-cjs/app-shell.d.cts +83 -0
  205. package/dist/types-cjs/area-chart.d.cts +80 -0
  206. package/dist/types-cjs/artifact.d.cts +65 -0
  207. package/dist/types-cjs/bar-chart.d.cts +83 -0
  208. package/dist/types-cjs/bar-list.d.cts +35 -0
  209. package/dist/types-cjs/breadcrumb.d.cts +62 -0
  210. package/dist/types-cjs/chart-brush.d.cts +35 -0
  211. package/dist/types-cjs/chart-themes.d.cts +229 -0
  212. package/dist/types-cjs/chart.d.cts +570 -0
  213. package/dist/types-cjs/checklist.d.cts +71 -0
  214. package/dist/types-cjs/code-block.d.cts +32 -0
  215. package/dist/types-cjs/collections.d.cts +246 -0
  216. package/dist/types-cjs/color-picker.d.cts +38 -0
  217. package/dist/types-cjs/command.d.cts +66 -0
  218. package/dist/types-cjs/comment-thread.d.cts +48 -0
  219. package/dist/types-cjs/confirmation.d.cts +60 -0
  220. package/dist/types-cjs/context-menu.d.cts +17 -0
  221. package/dist/types-cjs/controls.d.cts +360 -0
  222. package/dist/types-cjs/conversation.d.cts +130 -0
  223. package/dist/types-cjs/copy-button.d.cts +19 -0
  224. package/dist/types-cjs/core.d.cts +256 -0
  225. package/dist/types-cjs/data-grid.d.cts +76 -0
  226. package/dist/types-cjs/date-picker.d.cts +96 -0
  227. package/dist/types-cjs/diff-view.d.cts +54 -0
  228. package/dist/types-cjs/dot-plot.d.cts +54 -0
  229. package/dist/types-cjs/edit-region.d.cts +121 -0
  230. package/dist/types-cjs/empty.d.cts +24 -0
  231. package/dist/types-cjs/feature-gate.d.cts +22 -0
  232. package/dist/types-cjs/filter-bar.d.cts +65 -0
  233. package/dist/types-cjs/formatted-number.d.cts +38 -0
  234. package/dist/types-cjs/heatmap.d.cts +64 -0
  235. package/dist/types-cjs/in-place.d.cts +24 -0
  236. package/dist/types-cjs/index.d.cts +154 -0
  237. package/dist/types-cjs/inline-citation.d.cts +35 -0
  238. package/dist/types-cjs/inline-edit.d.cts +71 -0
  239. package/dist/types-cjs/input-otp.d.cts +37 -0
  240. package/dist/types-cjs/item-group.d.cts +104 -0
  241. package/dist/types-cjs/line-chart.d.cts +116 -0
  242. package/dist/types-cjs/link.d.cts +11 -0
  243. package/dist/types-cjs/markdown.d.cts +88 -0
  244. package/dist/types-cjs/meter.d.cts +43 -0
  245. package/dist/types-cjs/modal-state.d.cts +62 -0
  246. package/dist/types-cjs/nav.d.cts +25 -0
  247. package/dist/types-cjs/notifications.d.cts +42 -0
  248. package/dist/types-cjs/number-input.d.cts +30 -0
  249. package/dist/types-cjs/overlays.d.cts +170 -0
  250. package/dist/types-cjs/page-header.d.cts +27 -0
  251. package/dist/types-cjs/pagination.d.cts +29 -0
  252. package/dist/types-cjs/palette.d.cts +64 -0
  253. package/dist/types-cjs/password-input.d.cts +22 -0
  254. package/dist/types-cjs/pattern-assistant-conversation.d.cts +55 -0
  255. package/dist/types-cjs/pattern-async-collection.d.cts +34 -0
  256. package/dist/types-cjs/pattern-consequential-action.d.cts +53 -0
  257. package/dist/types-cjs/pattern-edit-record.d.cts +72 -0
  258. package/dist/types-cjs/pattern-review-submit.d.cts +43 -0
  259. package/dist/types-cjs/permission-matrix.d.cts +34 -0
  260. package/dist/types-cjs/pile.d.cts +27 -0
  261. package/dist/types-cjs/plan-picker.d.cts +40 -0
  262. package/dist/types-cjs/prompt-input.d.cts +106 -0
  263. package/dist/types-cjs/property-list.d.cts +67 -0
  264. package/dist/types-cjs/question.d.cts +64 -0
  265. package/dist/types-cjs/relative-time.d.cts +21 -0
  266. package/dist/types-cjs/removal-receipt.d.cts +1180 -0
  267. package/dist/types-cjs/resizable.d.cts +48 -0
  268. package/dist/types-cjs/roving.d.cts +44 -0
  269. package/dist/types-cjs/save-bar.d.cts +38 -0
  270. package/dist/types-cjs/saved-views.d.cts +72 -0
  271. package/dist/types-cjs/scatter-chart.d.cts +60 -0
  272. package/dist/types-cjs/schedule.d.cts +80 -0
  273. package/dist/types-cjs/secret-field.d.cts +33 -0
  274. package/dist/types-cjs/select.d.cts +109 -0
  275. package/dist/types-cjs/separator.d.cts +11 -0
  276. package/dist/types-cjs/settings.d.cts +39 -0
  277. package/dist/types-cjs/sheet.d.cts +27 -0
  278. package/dist/types-cjs/sparkline.d.cts +46 -0
  279. package/dist/types-cjs/split-button.d.cts +21 -0
  280. package/dist/types-cjs/stat.d.cts +54 -0
  281. package/dist/types-cjs/stepper.d.cts +26 -0
  282. package/dist/types-cjs/suggestions.d.cts +29 -0
  283. package/dist/types-cjs/suspense.d.cts +85 -0
  284. package/dist/types-cjs/sync-indicator.d.cts +24 -0
  285. package/dist/types-cjs/tag-input.d.cts +65 -0
  286. package/dist/types-cjs/terminal.d.cts +33 -0
  287. package/dist/types-cjs/thread-list.d.cts +58 -0
  288. package/dist/types-cjs/time-input.d.cts +33 -0
  289. package/dist/types-cjs/timeline.d.cts +38 -0
  290. package/dist/types-cjs/toggle.d.cts +46 -0
  291. package/dist/types-cjs/toolbar.d.cts +34 -0
  292. package/dist/types-cjs/tour.d.cts +23 -0
  293. package/dist/types-cjs/trace.d.cts +65 -0
  294. package/dist/types-cjs/tree.d.cts +71 -0
  295. package/package.json +90 -9
@@ -0,0 +1,63 @@
1
+ // Billing pattern: quotas are Meters (never Progress) that say their state in words; the rest of the cycle is projected
2
+ // (hatched, and said); the estimate says how it's computed; a plan change is reviewed with its proration before it's
3
+ // confirmed, the review heading takes focus, and only a completed change says so; a downgrade lists what will be lost
4
+ // and its button is a danger one; a locked feature stays live with its plan plate; a failed payment is a page banner
5
+ // that asks for action; invoice amounts are formatted currency. Run after a render of every preview (it opens
6
+ // test/p-BillingPattern-daylight.html); exits non-zero on a failure.
7
+ import { chromium } from 'playwright-core';
8
+ const browser = await chromium.launch(process.env.CHROME ? { executablePath: process.env.CHROME } : {});
9
+ const T = new URL('../test/', import.meta.url).pathname;
10
+ let pass = 0, fail = 0;
11
+ const expect = (c, m) => { if (c) pass++; else { fail++; console.log('FAIL', m); } };
12
+ const page = await browser.newPage({ viewport: { width: 960, height: 1200 } });
13
+ await page.goto(`file://${T}p-BillingPattern-daylight.html`);
14
+ await page.waitForTimeout(600);
15
+
16
+ const usage = await page.evaluate(() => { const s = document.querySelector('[aria-labelledby="bl-usage-h"]');
17
+ return { meters: s.querySelectorAll('.alt-meter').length, progress: s.querySelectorAll('.alt-progress, [role="progressbar"]').length, text: s.textContent }; });
18
+ expect(usage.meters === 3 && usage.progress === 0, `quotas are Meters, never Progress (${usage.meters} meters, ${usage.progress} progress)`);
19
+ expect(/8 of 10/.test(usage.text) && /Near limit/.test(usage.text) && /Over limit/.test(usage.text), 'each quota says its reading and state in words');
20
+ expect(/the last two weeks of the cycle are projected/i.test(usage.text) && await page.locator('[aria-labelledby="bl-calls-h"] [data-status="projected"]').count() === 2, 'the rest of the cycle is projected, and said');
21
+ expect(/10 seats × €24, plus €48/.test(usage.text), 'the estimate says how it is computed');
22
+
23
+ await page.getByRole('button', { name: 'Review change' }).click();
24
+ await page.waitForTimeout(200);
25
+ expect(await page.getByText('Choose a plan you’re not on.').count() === 1, 'reviewing without a change says why it can’t');
26
+ await page.getByRole('radio', { name: /Business/ }).click();
27
+ await page.getByRole('button', { name: 'Review change' }).click();
28
+ await page.waitForTimeout(300);
29
+ const review = await page.evaluate(() => ({ focus: document.activeElement.textContent, text: document.querySelector('.bl-proration')?.textContent }));
30
+ expect(review.focus === 'Upgrade to Business?' && /You’ll pay €\d+\.\d\d today for the rest of this cycle, then €480\.00\/month/.test(review.text), `the review states the proration and takes focus (${JSON.stringify(review)})`);
31
+ await page.getByRole('button', { name: 'Upgrade to Business' }).click();
32
+ await page.waitForTimeout(300);
33
+ expect(await page.evaluate(() => [...document.querySelectorAll('[aria-labelledby="bl-plan-h"] [role="status"], [aria-labelledby="bl-plan-h"] .alt-workflow-status')].some((n) => n.textContent === 'Changing plan…')), 'the change is pending while it’s made');
34
+ expect(await page.getByRole('heading', { name: 'You’re on Business' }).count() === 0, 'and nothing claims it’s done yet');
35
+ await page.waitForTimeout(900);
36
+ expect(await page.getByRole('heading', { name: 'You’re on Business' }).count() === 1, 'a completed change says so');
37
+
38
+ await page.getByRole('button', { name: 'Change plan again' }).click();
39
+ await page.waitForTimeout(200);
40
+ await page.getByRole('radio', { name: /Free/ }).click();
41
+ await page.getByRole('button', { name: 'Review change' }).click();
42
+ await page.waitForTimeout(300);
43
+ const down = await page.evaluate(() => ({ head: document.getElementById('bl-plan-h').textContent, losses: [...document.querySelectorAll('.bl-losses li')].map((l) => l.textContent),
44
+ danger: [...document.querySelectorAll('button')].find((b) => b.textContent === 'Downgrade to Free')?.dataset.variant }));
45
+ expect(down.head === 'Downgrade to Free?' && down.losses.length === 3 && /SAML SSO/.test(down.losses[0]), `a downgrade lists what will be lost (${JSON.stringify(down.losses)})`);
46
+ expect(down.danger === 'danger', 'and its button is a danger one');
47
+
48
+ const gate = await page.evaluate(() => { const s = document.querySelector('[aria-labelledby="bl-roles-h"]'), b = [...s.querySelectorAll('button')].find((b) => b.textContent.includes('Create a role'));
49
+ return { live: !!b && !b.disabled, plate: s.textContent.includes('Business') }; });
50
+ expect(gate.live && gate.plate, 'a locked feature stays live with its plan plate');
51
+
52
+ await page.getByRole('switch', { name: 'Simulate a failed payment' }).click();
53
+ await page.waitForTimeout(300);
54
+ const banner = await page.evaluate(() => { const a = document.querySelector('.bl-page > .alt-alert, .bl-page > * .alt-alert');
55
+ return a && { role: a.getAttribute('role') || a.querySelector('[role]')?.getAttribute('role'), page: a.closest('[data-placement="page"], .alt-alert[data-placement="page"]') !== null || a.dataset.placement === 'page', action: [...a.querySelectorAll('button')].map((b) => b.textContent).join() }; });
56
+ expect(banner && banner.page && /Update payment method/.test(banner.action), `a failed payment is a page banner asking for action (${JSON.stringify(banner)})`);
57
+
58
+ const amounts = await page.evaluate(() => [...document.querySelectorAll('[aria-labelledby="bl-inv-h"] tbody td:nth-child(3)')].map((t) => t.textContent));
59
+ expect(amounts.join() === '€264.00,€240.00,€240.00', `invoice amounts are formatted currency (${amounts})`);
60
+
61
+ console.log(`Billing pattern: ${pass} passed, ${fail} failed`);
62
+ if (fail) process.exitCode = 1;
63
+ await browser.close();
@@ -0,0 +1,76 @@
1
+ // Developer settings pattern: a new key is shown once in a dialog that Escape can't close until it's saved; the list
2
+ // shows keys masked, never in full, with when each was last used; rolling says the old key's overlap in words; revoking
3
+ // says what breaks behind a danger button; a webhook retry hovers as a pending write with its attempt, then lands
4
+ // delivered; a test event shows its payload; an integration's OAuth handoff is pending until it's connected, and
5
+ // disconnecting says what stops. Run after a render of every preview (it opens
6
+ // test/p-DeveloperSettingsPattern-daylight.html); exits non-zero on a failure.
7
+ import { chromium } from 'playwright-core';
8
+ const browser = await chromium.launch(process.env.CHROME ? { executablePath: process.env.CHROME } : {});
9
+ const T = new URL('../test/', import.meta.url).pathname;
10
+ let pass = 0, fail = 0;
11
+ const expect = (c, m) => { if (c) pass++; else { fail++; console.log('FAIL', m); } };
12
+ const page = await browser.newPage({ viewport: { width: 960, height: 1000 } });
13
+ await page.goto(`file://${T}p-DeveloperSettingsPattern-daylight.html`);
14
+ await page.waitForTimeout(400);
15
+ const status = () => page.evaluate(() => document.querySelector('.dv-page > [role="status"]').textContent);
16
+
17
+ await page.getByRole('button', { name: 'Create key' }).click();
18
+ await page.waitForTimeout(400);
19
+ await page.getByLabel('Key name').fill('Billing service');
20
+ await page.getByRole('dialog').getByRole('button', { name: 'Create key' }).click();
21
+ await page.waitForTimeout(800);
22
+ const save = page.getByRole('dialog', { name: 'Save your new key' });
23
+ const shown = await save.evaluate((d) => d.textContent + [...d.querySelectorAll('input')].map((i) => i.value).join(' '));
24
+ expect(/You won’t see this key again/.test(shown) && /ak_live_EXAMPLE_7Hq2Xv9LmR3p/.test(shown), 'a new key is shown in full, once, with the warning');
25
+ await page.keyboard.press('Escape');
26
+ await page.waitForTimeout(300);
27
+ expect(await save.count() === 1 && await save.getByRole('button', { name: 'Done' }).isDisabled(), 'the dialog can’t close until the key is saved');
28
+ await save.getByText("I've saved this key").click();
29
+ await page.waitForTimeout(200);
30
+ await save.getByRole('button', { name: 'Done' }).click();
31
+ await page.waitForTimeout(400);
32
+ const list = await page.evaluate(() => document.querySelector('[aria-labelledby="dv-keys-h"] tbody').textContent);
33
+ expect(/Billing service/.test(list) && !/EXAMPLE_7Hq2/.test(list) && /ak_live_••••/.test(list) && /Never used/.test(list), 'the list shows keys masked, with when each was last used');
34
+
35
+ await page.getByRole('button', { name: 'Roll Production key' }).click();
36
+ await page.waitForTimeout(400);
37
+ let d = page.getByRole('dialog', { name: 'Roll the Production key?' });
38
+ expect(/keeps working for 24 hours/.test(await d.textContent()), 'rolling says the old key’s overlap in words');
39
+ await d.getByRole('button', { name: 'Roll key' }).click();
40
+ await page.waitForTimeout(900);
41
+ expect(/rolled\. The old key keeps working for 24 hours\./.test(await status()), 'and says so once rolled');
42
+ await page.getByRole('button', { name: 'Revoke CI pipeline key' }).click();
43
+ await page.waitForTimeout(400);
44
+ d = page.getByRole('dialog', { name: 'Revoke the CI pipeline key?' });
45
+ expect(/Requests that use it fail immediately/.test(await d.textContent()) && await d.getByRole('button', { name: 'Revoke key' }).getAttribute('data-variant') === 'danger', 'revoking says what breaks behind a danger button');
46
+ await d.getByRole('button', { name: 'Revoke key' }).click();
47
+ await page.waitForTimeout(900);
48
+ expect(!/CI pipeline/.test(await page.evaluate(() => document.querySelector('[aria-labelledby="dv-keys-h"] tbody').textContent)) && /revoked/.test(await status()), 'a revoked key leaves the list, and it’s said');
49
+
50
+ await page.getByRole('button', { name: 'Retry' }).click();
51
+ await page.waitForTimeout(400);
52
+ let item = await page.evaluate(() => { const i = [...document.querySelectorAll('.alt-timeline-item')].find((n) => n.textContent.includes('customer.updated')); return { status: i.dataset.status, text: i.textContent }; });
53
+ expect(item.status === 'pending' && /Retrying \(attempt 4 of 5\)/.test(item.text), `a retry hovers as a pending write with its attempt (${item.status})`);
54
+ await page.waitForTimeout(900);
55
+ item = await page.evaluate(() => { const i = [...document.querySelectorAll('.alt-timeline-item')].find((n) => n.textContent.includes('customer.updated')); return { status: i.dataset.status, text: i.textContent }; });
56
+ expect(item.status === 'committed' && /Delivered/.test(item.text), 'then lands, delivered');
57
+ await page.getByRole('button', { name: 'Send test event' }).click();
58
+ await page.waitForTimeout(300);
59
+ expect(/"type": "invoice.paid"/.test(await page.evaluate(() => document.querySelector('.alt-code')?.textContent || '')), 'a test event shows its payload');
60
+
61
+ await page.getByRole('button', { name: 'Connect Calendar sync' }).click();
62
+ await page.waitForTimeout(500);
63
+ const card = () => page.evaluate(() => { const c = [...document.querySelectorAll('.dv-app')].find((n) => n.textContent.includes('Calendar sync')); return { status: c.dataset.status || '', text: c.textContent }; });
64
+ let c = await card();
65
+ expect(c.status === 'pending' && /Waiting for authorization/.test(c.text), 'the OAuth handoff is pending');
66
+ await page.waitForTimeout(1000);
67
+ c = await card();
68
+ expect(c.status !== 'pending' && /Connected/.test(c.text), 'then the integration is connected');
69
+ await page.locator('.dv-app', { hasText: 'Team chat' }).getByRole('button', { name: 'Disconnect' }).click();
70
+ await page.waitForTimeout(400);
71
+ d = page.getByRole('dialog', { name: 'Disconnect Team chat?' });
72
+ expect(/stops receiving updates/.test(await d.textContent()), 'disconnecting says what stops');
73
+
74
+ console.log(`Developer settings pattern: ${pass} passed, ${fail} failed`);
75
+ if (fail) process.exitCode = 1;
76
+ await browser.close();
@@ -0,0 +1,71 @@
1
+ // Schema, referential integrity and reproducible distribution checks (no network).
2
+ import assert from 'node:assert/strict';
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { resolve, relative } from 'node:path';
6
+ import { buildGuidance, inputs, listFiles, root } from '../../scripts/build-guidance.mjs';
7
+ const {base,patterns,refs,version}=inputs();
8
+ const read=p=>JSON.parse(readFileSync(p,'utf8'));
9
+ const schema=read(resolve(base,'pattern.schema.json'));
10
+ function validate(value,s,path='$') {
11
+ if ('const' in s) assert.equal(value,s.const,path);
12
+ if (s.enum) assert.ok(s.enum.includes(value),path);
13
+ if (s.pattern) assert.match(value,new RegExp(s.pattern),path);
14
+ if ('minimum' in s) assert.ok(value>=s.minimum,path);
15
+ assert.equal(Array.isArray(value)?'array':Number.isInteger(value)?'integer':typeof value,s.type,`${path}: expected ${s.type}`);
16
+ if(s.type==='object') {
17
+ for(const k of s.required||[])assert.ok(k in value,`${path}.${k} missing`);
18
+ if(s.additionalProperties===false)for(const k of Object.keys(value))assert.ok(k in s.properties,`${path}.${k} unsupported`);
19
+ for(const [k,v]of Object.entries(value))if(s.properties?.[k])validate(v,s.properties[k],`${path}.${k}`);
20
+ }
21
+ if(s.type==='array') {assert.ok(value.length>=(s.minItems||0));value.forEach((v,i)=>validate(v,s.items,`${path}[${i}]`));}
22
+ if(s.type==='string')assert.ok(value.length>=(s.minLength||0),path);
23
+ }
24
+ const rules=read(resolve(base,'rules.json')).rules,sources=read(resolve(base,'evidence.json')).sources;
25
+ const ruleIds=new Set(rules.map(r=>r.id)),sourceIds=new Set(sources.map(s=>s.id));
26
+ assert.equal(ruleIds.size,rules.length,'unique rule IDs');assert.equal(sourceIds.size,sources.length,'unique source IDs');
27
+ const validDate=value=> typeof value==='string' && /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0,10)===value;
28
+ const authority=['normative-standard','contextual-pattern','heuristic','altitude-policy','experimental-hypothesis'];
29
+ for(const r of rules){
30
+ assert.match(r.id,/^ALT-[A-Z]+-\d{3}$/);assert.ok(authority.includes(r.authority));
31
+ assert.ok(['deterministic','application-integration','contextual-review'].includes(r.diagnosticClass));
32
+ assert.ok(validDate(r.reviewedAt),`${r.id}: valid review date`);assert.ok(r.owner?.trim(),`${r.id}: owner`);
33
+ assert.ok(Array.isArray(r.criteria));
34
+ if(r.authority==='normative-standard')assert.ok(r.criteria.length,`${r.id}: exact normative criterion`);
35
+ for(const url of r.criteria){const u=new URL(url);assert.equal(u.origin,'https://www.w3.org');assert.equal(u.pathname,'/TR/WCAG22/');assert.ok(u.hash.length>1);}
36
+ for(const k of ['applicability','maturity','evidenceStatus','repair'])assert.ok(r[k]);
37
+ assert.ok(Array.isArray(r.exceptions));assert.ok(r.sources.length);assert.ok(r.verification.length);
38
+ for(const id of r.sources)assert.ok(sourceIds.has(id),`${r.id}: unknown source ${id}`);
39
+ for(const p of r.verification)if(!p.startsWith('manual:'))assert.ok(existsSync(resolve(root,p)),`${r.id}: missing ${p}`);
40
+ }
41
+ for(const s of sources){assert.ok(authority.includes(s.authority));for(const k of ['title','url','status','applicability','reviewed'])assert.ok(s[k]);assert.ok(validDate(s.reviewed));
42
+ if(s.url.startsWith('https:'))assert.ok(new URL(s.url).hostname);else {const local=s.url.startsWith('visual/')?resolve(root,'project',s.url.slice(7)):resolve(base,s.url);assert.ok(existsSync(local),`missing local evidence ${s.url}`);}}
43
+ assert.equal(new Set(patterns.map(p=>p.id)).size,patterns.length);
44
+ for(const p of patterns){
45
+ validate(p,schema);
46
+ assert.ok(existsSync(resolve(base,p.document)));
47
+ for(const id of p.rules)assert.ok(ruleIds.has(id),`${p.id}: unknown rule ${id}`);
48
+ for(const id of p.sources)assert.ok(sourceIds.has(id),`${p.id}: unknown source ${id}`);
49
+ const prose=readFileSync(resolve(base,p.document),'utf8');
50
+ for(const term of ['Inputs','States','keyboard','focus','Recovery','verification','evidence'])assert.ok(prose.toLowerCase().includes(term.toLowerCase()),`${p.id}: ${term}`);
51
+ }
52
+ for(const p of refs)assert.ok(existsSync(resolve(root,p)),`missing ${p}`);
53
+ // A damaged schema instance must be rejected, proving the check is active.
54
+ assert.throws(()=>validate({...patterns[0],states:[]},schema));
55
+ assert.throws(()=>validate({...patterns[0],unexpected:true},schema));
56
+ const temp=mkdtempSync(resolve(tmpdir(),'altitude-guidance-'));
57
+ try {
58
+ buildGuidance(temp);
59
+ const dist=resolve(root,'dist/guidance');
60
+ assert.ok(existsSync(dist),'Run npm run build first');
61
+ const expected=listFiles(temp).map(p=>relative(temp,p)).sort(),actual=listFiles(dist).map(p=>relative(dist,p)).sort();
62
+ assert.deepEqual(actual,expected,'distributed file inventory is fresh');
63
+ for(const p of expected)assert.deepEqual(readFileSync(resolve(dist,p)),readFileSync(resolve(temp,p)),`stale generated guidance: ${p}`);
64
+ assert.equal(read(resolve(dist,'index.json')).version,version);
65
+ const pkg=read(resolve(root,'package.json'));
66
+ assert.ok(existsSync(resolve(root,pkg.bin.altitude)),'shipped binary exists');
67
+ assert.equal(pkg.exports['./guidance'],'./dist/guidance/index.json');
68
+ assert.ok(!pkg.scripts.postinstall&&!pkg.scripts.install,'no install-time mutation');
69
+ for(const bundle of ['dist/index.js','dist/index.cjs'])assert.ok(!readFileSync(resolve(root,bundle),'utf8').includes('altitude:start'),'Node tooling is outside browser graph');
70
+ console.log(`guidance: ${patterns.length} patterns, ${rules.length} rules, ${expected.length} distributed files; schema/references/version/freshness pass`);
71
+ } finally {rmSync(temp,{recursive:true,force:true});}
@@ -0,0 +1,69 @@
1
+ // Import pattern: steps are a Stepper; uploading moves to mapping; a column's Select is an opening until it's mapped and
2
+ // required fields still unmapped are listed in words; invalid cells lie in a trench on critical-soft (DataGrid
3
+ // showInvalid) and say their problem; the problem count is said and drops as cells are fixed; "Only rows with problems"
4
+ // narrows the grid; the review says what will be imported and skipped; the import is a cancellable Progress job that
5
+ // ends with a receipt. Also checks DataGrid's showInvalid itself. Run after a render of every preview (it opens
6
+ // test/p-ImportPattern-daylight.html); exits non-zero on a failure.
7
+ import { chromium } from 'playwright-core';
8
+ const browser = await chromium.launch(process.env.CHROME ? { executablePath: process.env.CHROME } : {});
9
+ const T = new URL('../test/', import.meta.url).pathname;
10
+ let pass = 0, fail = 0;
11
+ const expect = (c, m) => { if (c) pass++; else { fail++; console.log('FAIL', m); } };
12
+ const page = await browser.newPage({ viewport: { width: 960, height: 900 } });
13
+ await page.goto(`file://${T}p-ImportPattern-daylight.html`);
14
+ await page.waitForTimeout(400);
15
+ const current = () => page.evaluate(() => document.querySelector('[aria-label="Import steps"] [aria-current="step"]')?.textContent || 'done');
16
+ const status = () => page.evaluate(() => document.querySelector('.im-step [role="status"]')?.textContent);
17
+ const pick = async (col, opt) => { await page.getByRole('button', { name: col }).click(); await page.waitForTimeout(250); await page.getByRole('option', { name: opt }).locator('visible=true').click(); await page.waitForTimeout(300); };
18
+
19
+ expect(/Upload/.test(await current()), 'the import starts at the upload step');
20
+ await page.getByRole('button', { name: 'Use the sample file' }).click();
21
+ await page.waitForTimeout(1300);
22
+ expect(/Map columns/.test(await current()), 'an uploaded file moves to mapping');
23
+ const empty = await page.evaluate(() => [...document.querySelectorAll('.im-map .alt-input')].map((i) => i.hasAttribute('data-filled')));
24
+ expect(empty.every((f) => !f), 'every column starts as an opening, not mapped');
25
+ expect(await status() === 'Map these required fields: Name, Email.' && await page.getByRole('button', { name: 'Continue' }).isDisabled(), 'required fields still unmapped are listed in words');
26
+ await pick('Full name', 'Name (required)');
27
+ await pick('E-mail', 'Email (required)');
28
+ await pick('Company', 'Company');
29
+ const filled = await page.evaluate(() => [...document.querySelectorAll('.im-map .alt-input')].map((i) => i.hasAttribute('data-filled')));
30
+ expect(filled.join() === 'true,true,true,false' && await status() === 'Every required field is mapped.', `a mapped column fills; what's left shows (${filled})`);
31
+ await page.getByRole('button', { name: 'Continue' }).click();
32
+ await page.waitForTimeout(400);
33
+
34
+ const cells = () => page.evaluate(() => [...document.querySelectorAll('.alt-grid-cell[data-invalid]')].map((c) => ({ text: c.textContent, name: c.getAttribute('aria-describedby') && document.getElementById(c.getAttribute('aria-describedby').split(' ')[0])?.textContent,
35
+ bg: getComputedStyle(c).backgroundColor, shadow: getComputedStyle(c).boxShadow, invalid: c.getAttribute('aria-invalid') })));
36
+ let bad = await cells();
37
+ expect(bad.length === 3 && bad.every((c) => c.invalid === 'true' && /inset/.test(c.shadow)), `invalid cells lie in a trench and are marked invalid (${bad.length})`);
38
+ expect(bad.some((c) => c.name === 'This isn’t an email address.') && bad.some((c) => c.name === 'Enter a name.'), 'each invalid cell says its problem');
39
+ expect(await status() === '3 problems left in 3 rows. Rows with problems are skipped.', `the problem count is said (${await status()})`);
40
+ await page.locator('.alt-grid-cell[data-invalid]').first().click();
41
+ await page.keyboard.press('Enter');
42
+ await page.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A');
43
+ await page.keyboard.type('grace@example.com');
44
+ await page.keyboard.press('Enter');
45
+ await page.waitForTimeout(500);
46
+ expect(await status() === '2 problems left in 2 rows. Rows with problems are skipped.' && (await cells()).length === 2, 'fixing a cell lifts it out and the count drops');
47
+ await page.getByRole('switch', { name: 'Only rows with problems' }).click();
48
+ await page.waitForTimeout(300);
49
+ expect(await page.locator('[aria-label="Rows to import"] tbody tr').count() === 2, '"Only rows with problems" narrows the grid');
50
+ await page.getByRole('button', { name: 'Continue' }).click();
51
+ await page.waitForTimeout(400);
52
+
53
+ expect(await page.evaluate(() => document.querySelector('.im-lead').textContent) === '5 contacts will be imported. 3 rows will be skipped: 1 duplicate and 2 with problems.', 'the review says what will be imported and skipped');
54
+ await page.getByRole('button', { name: 'Import 5 contacts' }).click();
55
+ await page.waitForTimeout(450);
56
+ expect(await page.locator('.im-step [role="progressbar"]').count() === 1 && /^Importing \d of 5$/.test(await status()), 'the import is a job with progress in words');
57
+ await page.getByRole('button', { name: 'Cancel import' }).click();
58
+ await page.waitForTimeout(500);
59
+ expect(await page.getByRole('heading', { name: 'Import cancelled' }).count() === 1, 'it can be cancelled, and says what was imported');
60
+ await page.getByRole('button', { name: 'Back to review' }).click();
61
+ await page.waitForTimeout(300);
62
+ await page.getByRole('button', { name: 'Import 5 contacts' }).click();
63
+ await page.waitForTimeout(2400);
64
+ const receipt = await page.evaluate(() => [...document.querySelectorAll('.im-receipt div')].map((d) => d.textContent));
65
+ expect(receipt.join('|') === 'Imported5 contacts|Skipped1 duplicate|Failed2 rows with problems' && await current() === 'done', `the import ends with a receipt (${receipt})`);
66
+
67
+ console.log(`Import pattern: ${pass} passed, ${fail} failed`);
68
+ if (fail) process.exitCode = 1;
69
+ await browser.close();
@@ -0,0 +1,71 @@
1
+ // Members pattern: invalid addresses are refused with the reason; sending invites is a pending write, then each new
2
+ // invitation lands as a settled ground row with an "Invited" plate (never hovering) and the result is said per address;
3
+ // the only owner can't change their own role and the row says why instead of offering the control; a role change hovers
4
+ // while pending, lands as "Role changed", and a failed one drops, says so and restores the role; removing someone names
5
+ // what they lose and is said once done; revoking an invitation is said and focus goes to the heading. Run after a render
6
+ // of every preview (it opens test/p-MembersPattern-daylight.html); exits non-zero on a failure.
7
+ import { chromium } from 'playwright-core';
8
+ const browser = await chromium.launch(process.env.CHROME ? { executablePath: process.env.CHROME } : {});
9
+ const T = new URL('../test/', import.meta.url).pathname;
10
+ let pass = 0, fail = 0;
11
+ const expect = (c, m) => { if (c) pass++; else { fail++; console.log('FAIL', m); } };
12
+ const page = await browser.newPage({ viewport: { width: 960, height: 1000 } });
13
+ await page.goto(`file://${T}p-MembersPattern-daylight.html`);
14
+ await page.waitForTimeout(400);
15
+ const status = () => page.evaluate(() => [...document.querySelectorAll('.mb-page > [role="status"], .mb-page > .alt-workflow-status, .mb-page > * [role="status"]')].map((n) => n.textContent).join('|'));
16
+ const row = (name) => page.evaluate((name) => { const r = [...document.querySelectorAll('tbody tr')].find((r) => r.querySelector('.mb-name').textContent.startsWith(name));
17
+ return r && { status: r.dataset.status || '', settle: r.dataset.settle || '', text: r.textContent, select: !!r.querySelector('[aria-label^="Role for"]'), remove: [...r.querySelectorAll('button')].some((b) => b.textContent === 'Remove'), badge: r.querySelector('.alt-badge')?.textContent }; }, name);
18
+
19
+ const input = page.getByLabel('Email addresses');
20
+ for (const t of ['ana@acme.dev', 'nope', 'theo@acme.dev']) {
21
+ await input.fill(t); await input.press('Enter'); await page.waitForTimeout(150);
22
+ if (t === 'nope') expect(await page.evaluate(() => document.querySelector('.mb-invite .alt-field-error')?.textContent === 'nope isn’t an email address.'), 'an invalid address is refused with the reason');
23
+ }
24
+ const send = page.getByRole('button', { name: 'Send 2 invites' });
25
+ expect(await send.count() === 1, 'the button says how many invites it sends');
26
+ await send.click();
27
+ await page.waitForTimeout(300);
28
+ expect(/Sending invites/.test(await status()), 'sending is said while it happens');
29
+ await page.waitForTimeout(900);
30
+ let r = await row('ana@acme.dev');
31
+ expect(r && r.badge === 'Invited' && r.status === '' && r.settle !== '', `a new invitation lands as a settled row with an Invited plate (${JSON.stringify(r)})`);
32
+ expect(await page.evaluate(() => { const r = [...document.querySelectorAll('tbody tr')].find((r) => r.textContent.includes('ana@acme.dev')); return getComputedStyle(r).animationName !== 'alt-hover'; }), 'an invitation awaiting acceptance never hovers');
33
+ expect(/1 invite sent, 1 address is already in the workspace\./.test(await status()), `the result is said per address (${await status()})`);
34
+
35
+ r = await row('Maya Chen');
36
+ expect(r && !r.select && !r.remove && /You’re the only owner/.test(r.text), 'the only owner gets the reason instead of a role control or Remove');
37
+
38
+ await page.getByRole('button', { name: 'Role for Theo Marsh' }).click();
39
+ await page.getByRole('option', { name: 'Member' }).click();
40
+ await page.waitForTimeout(400);
41
+ r = await row('Theo Marsh');
42
+ expect(r.status === 'pending' && /Changing role…/.test(r.text), `a role change hovers while it saves (${r.status})`);
43
+ await page.waitForTimeout(700);
44
+ r = await row('Theo Marsh');
45
+ expect(r.status === 'committed' && /Role changed/.test(r.text), 'and lands as changed');
46
+ await page.getByRole('switch', { name: 'Make the next role change fail' }).click();
47
+ await page.getByRole('button', { name: 'Role for Ines Duarte' }).click();
48
+ await page.getByRole('option', { name: 'Viewer' }).click();
49
+ await page.waitForTimeout(1100);
50
+ r = await row('Ines Duarte');
51
+ const shown = await page.getByRole('button', { name: 'Role for Ines Duarte' }).textContent();
52
+ expect(r.status === 'failed' && /Couldn’t change the role/.test(r.text) && /Member/.test(shown), `a failed change drops, says so and restores the role (${shown})`);
53
+ await page.getByRole('switch', { name: 'Make the next role change fail' }).click();
54
+
55
+ await page.evaluate(() => [...document.querySelectorAll('tbody tr')].find((r) => r.textContent.includes('Theo Marsh')).querySelector('button:last-of-type').scrollIntoView());
56
+ await page.locator('tbody tr', { hasText: 'Theo Marsh' }).getByRole('button', { name: 'Remove' }).click();
57
+ await page.waitForTimeout(500);
58
+ const dialog = page.getByRole('dialog', { name: 'Remove Theo Marsh?' });
59
+ expect(await dialog.count() === 1 && /loses access to 9 projects/.test(await dialog.textContent()), 'removing someone names what they lose');
60
+ await dialog.getByRole('button', { name: 'Remove Theo' }).click();
61
+ await page.waitForTimeout(1100);
62
+ expect(!(await row('Theo Marsh')) && /Theo Marsh was removed\./.test(await status()), 'and is said once they are removed');
63
+
64
+ await page.locator('tbody tr', { hasText: 'sam@acme.dev' }).getByRole('button', { name: 'Revoke' }).click();
65
+ await page.waitForTimeout(300);
66
+ expect(!(await row('sam@acme.dev')) && /Invitation to sam@acme.dev revoked\./.test(await status()), 'revoking an invitation is said');
67
+ expect(await page.evaluate(() => document.activeElement.id) === 'mb-heading', 'and focus goes to the heading, not the page');
68
+
69
+ console.log(`Members pattern: ${pass} passed, ${fail} failed`);
70
+ if (fail) process.exitCode = 1;
71
+ await browser.close();
@@ -0,0 +1,54 @@
1
+ // Onboarding pattern: setup progress is a Meter, not Progress; tasks can't be checked off by clicking them, only by doing
2
+ // them, and then they're stamped Done; the guide can be skipped before it's finished, hidden after, and found again from
3
+ // Help; an empty region opens with its one action; no tour starts on its own, one runs when asked and doesn't dim the
4
+ // page; "What's new" counts only unseen changes and the count goes once they're seen. Run after a render of every
5
+ // preview (it opens test/p-OnboardingPattern-daylight.html); exits non-zero on a failure.
6
+ import { chromium } from 'playwright-core';
7
+ const browser = await chromium.launch(process.env.CHROME ? { executablePath: process.env.CHROME } : {});
8
+ const T = new URL('../test/', import.meta.url).pathname;
9
+ let pass = 0, fail = 0;
10
+ const expect = (c, m) => { if (c) pass++; else { fail++; console.log('FAIL', m); } };
11
+ const page = await browser.newPage({ viewport: { width: 960, height: 1000 } });
12
+ await page.goto(`file://${T}p-OnboardingPattern-daylight.html`);
13
+ await page.waitForTimeout(500);
14
+ const guide = () => page.evaluate(() => { const g = document.querySelector('.ob-guide'); return g && { meter: g.querySelectorAll('.alt-meter').length, progress: g.querySelectorAll('[role="progressbar"]').length, text: g.querySelector('.alt-meter').textContent,
15
+ done: [...g.querySelectorAll('.ob-task[data-done]')].map((t) => t.querySelector('.ob-title').textContent), checkboxes: g.querySelectorAll('input[type="checkbox"], [role="checkbox"]').length }; });
16
+
17
+ let g = await guide();
18
+ expect(g.meter === 1 && g.progress === 0 && /2 of 4/.test(g.text), `setup progress is a Meter reading "2 of 4" (${g.text})`);
19
+ expect(g.checkboxes === 0 && g.done.join() === 'Create a project,Invite your team', 'tasks can’t be ticked by hand; done ones are stamped');
20
+ expect(await page.locator('[role="dialog"]').count() === 0, 'no tour starts on its own');
21
+ const empty = await page.evaluate(() => { const e = document.querySelector('#ob-connect .alt-empty'); return e && { buttons: e.querySelectorAll('button').length, text: e.textContent }; });
22
+ expect(empty && empty.buttons === 1 && /No data sources yet/.test(empty.text), 'an empty region opens with its one action');
23
+ await page.locator('#ob-connect').getByRole('button', { name: 'Connect a data source' }).click();
24
+ await page.waitForTimeout(300);
25
+ g = await guide();
26
+ expect(g.done.includes('Connect a data source') && /3 of 4/.test(g.text), 'doing the task stamps it and the reading rises');
27
+ expect(await page.locator('.ob-guide').getByRole('button', { name: 'Skip for now' }).count() === 1, 'an unfinished guide can be skipped');
28
+ await page.locator('#ob-billing').getByRole('button', { name: 'Add billing details' }).click();
29
+ await page.waitForTimeout(300);
30
+ await page.locator('.ob-guide').getByRole('button', { name: 'Hide setup guide' }).click();
31
+ await page.waitForTimeout(300);
32
+ expect(await page.locator('.ob-guide').count() === 0 && /Open it again any time from Help/.test(await page.evaluate(() => document.querySelector('.ob-page > [role="status"]')?.textContent || '')), 'a finished guide hides, saying where to find it');
33
+ await page.getByRole('button', { name: 'Help: setup guide' }).click();
34
+ await page.waitForTimeout(300);
35
+ expect(await page.locator('.ob-guide').count() === 1, 'and Help brings it back');
36
+
37
+ await page.getByRole('button', { name: 'Show me what changed in the sidebar' }).click();
38
+ await page.waitForTimeout(600);
39
+ expect(await page.getByRole('dialog', { name: 'Projects are grouped by team' }).count() === 1 && await page.locator('[data-receded]').count() === 0, 'a tour runs when asked, without dimming the page');
40
+ await page.keyboard.press('Escape');
41
+ await page.waitForTimeout(400);
42
+
43
+ const news = page.getByRole('button', { name: /What’s new/ });
44
+ expect(/2/.test(await news.textContent()), 'What’s new counts unseen changes');
45
+ await news.click();
46
+ await page.waitForTimeout(400);
47
+ expect(await page.getByRole('dialog', { name: 'What’s new' }).locator('.alt-timeline-item').count() === 3, 'it lists the changes as a timeline');
48
+ await page.keyboard.press('Escape');
49
+ await page.waitForTimeout(300);
50
+ expect(!(await page.locator('#ob-new .alt-badge').count()), 'once seen, the count goes');
51
+
52
+ console.log(`Onboarding pattern: ${pass} passed, ${fail} failed`);
53
+ if (fail) process.exitCode = 1;
54
+ await browser.close();
@@ -0,0 +1,13 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { sync } from '../../scripts/pattern-previews.mjs';
4
+ const root = fileURLToPath(new URL('../../', import.meta.url));
5
+ for (const config of ['build/fixtures/pattern-tsconfig.json', 'build/fixtures/pattern-examples-tsconfig.json']) {
6
+ const result = spawnSync(process.execPath, ['node_modules/typescript/bin/tsc', '-p', config], {cwd:root, stdio:'inherit'});
7
+ if (result.error) throw result.error;
8
+ if (result.status !== 0) process.exit(result.status || 1);
9
+ }
10
+ // The task patterns' examples are their Storybook previews; the two must not drift.
11
+ const stale = sync(false);
12
+ if (stale.length) { console.error(`Pattern previews differ from build/fixtures/patterns (run node scripts/pattern-previews.mjs): ${stale.join(', ')}`); process.exit(1); }
13
+ console.log('Pattern types: positive/negative contracts and example compositions passed.');