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.
- package/README.md +46 -8
- package/dist/adapters/codemirror.cjs +88 -0
- package/dist/adapters/codemirror.cjs.map +7 -0
- package/dist/adapters/codemirror.js +67 -0
- package/dist/adapters/codemirror.js.map +7 -0
- package/dist/adapters/react-flow.css +102 -0
- package/dist/adapters/tiptap.css +71 -0
- package/dist/altitude.css +3165 -246
- package/dist/altitude.iife.js +33 -5
- package/dist/altitude.no-fonts.css +3165 -246
- package/dist/chart-themes.cjs +195 -0
- package/dist/chart-themes.cjs.map +7 -0
- package/dist/chart-themes.js +165 -0
- package/dist/chart-themes.js.map +7 -0
- package/dist/cli/altitude.mjs +134 -0
- package/dist/guidance/README.md +32 -0
- package/dist/guidance/agent-setup.md +22 -0
- package/dist/guidance/app-skeleton.md +17 -0
- package/dist/guidance/evidence.json +95 -0
- package/dist/guidance/index.json +493 -0
- package/dist/guidance/llms.txt +20 -0
- package/dist/guidance/manifest.json +92 -0
- package/dist/guidance/pattern.schema.json +181 -0
- package/dist/guidance/patterns/agent-action.json +77 -0
- package/dist/guidance/patterns/agent-action.md +39 -0
- package/dist/guidance/patterns/assistant-conversation.json +77 -0
- package/dist/guidance/patterns/assistant-conversation.md +50 -0
- package/dist/guidance/patterns/async-collection.json +71 -0
- package/dist/guidance/patterns/async-collection.md +29 -0
- package/dist/guidance/patterns/audit-log.json +75 -0
- package/dist/guidance/patterns/audit-log.md +27 -0
- package/dist/guidance/patterns/auth.json +86 -0
- package/dist/guidance/patterns/auth.md +29 -0
- package/dist/guidance/patterns/billing.json +80 -0
- package/dist/guidance/patterns/billing.md +29 -0
- package/dist/guidance/patterns/consequential-action.json +76 -0
- package/dist/guidance/patterns/consequential-action.md +27 -0
- package/dist/guidance/patterns/developer-settings.json +79 -0
- package/dist/guidance/patterns/developer-settings.md +29 -0
- package/dist/guidance/patterns/edit-record.json +74 -0
- package/dist/guidance/patterns/edit-record.md +51 -0
- package/dist/guidance/patterns/import.json +77 -0
- package/dist/guidance/patterns/import.md +31 -0
- package/dist/guidance/patterns/members.json +77 -0
- package/dist/guidance/patterns/members.md +27 -0
- package/dist/guidance/patterns/onboarding.json +78 -0
- package/dist/guidance/patterns/onboarding.md +31 -0
- package/dist/guidance/patterns/review-submit.json +72 -0
- package/dist/guidance/patterns/review-submit.md +27 -0
- package/dist/guidance/patterns/screen-layout.json +79 -0
- package/dist/guidance/patterns/screen-layout.md +40 -0
- package/dist/guidance/patterns/settings.json +78 -0
- package/dist/guidance/patterns/settings.md +32 -0
- package/dist/guidance/patterns/sync.json +77 -0
- package/dist/guidance/patterns/sync.md +31 -0
- package/dist/guidance/principles.md +23 -0
- package/dist/guidance/references/build/checks/agent-action-pattern.mjs +57 -0
- package/dist/guidance/references/build/checks/agent-setup.mjs +78 -0
- package/dist/guidance/references/build/checks/assistant-conversation-pattern.mjs +49 -0
- package/dist/guidance/references/build/checks/audit-log-pattern.mjs +63 -0
- package/dist/guidance/references/build/checks/auth-pattern.mjs +67 -0
- package/dist/guidance/references/build/checks/billing-pattern.mjs +63 -0
- package/dist/guidance/references/build/checks/developer-settings-pattern.mjs +76 -0
- package/dist/guidance/references/build/checks/guidance.mjs +71 -0
- package/dist/guidance/references/build/checks/import-pattern.mjs +69 -0
- package/dist/guidance/references/build/checks/members-pattern.mjs +71 -0
- package/dist/guidance/references/build/checks/onboarding-pattern.mjs +54 -0
- package/dist/guidance/references/build/checks/pattern-types.mjs +13 -0
- package/dist/guidance/references/build/checks/pattern-workflows.mjs +226 -0
- package/dist/guidance/references/build/checks/primary-scope.mjs +49 -0
- package/dist/guidance/references/build/checks/render-tells.mjs +72 -0
- package/dist/guidance/references/build/checks/screen-layout-pattern.mjs +111 -0
- package/dist/guidance/references/build/checks/semantics.mjs +72 -0
- package/dist/guidance/references/build/checks/settings-pattern.mjs +73 -0
- package/dist/guidance/references/build/checks/sync-pattern.mjs +62 -0
- package/dist/guidance/references/build/checks/tells-lint.mjs +164 -0
- package/dist/guidance/references/build/fixtures/pattern-workflows.tsx +83 -0
- package/dist/guidance/references/build/fixtures/patterns/agent-action.jsx +60 -0
- package/dist/guidance/references/build/fixtures/patterns/app-skeleton.jsx +94 -0
- package/dist/guidance/references/build/fixtures/patterns/assistant-conversation.jsx +79 -0
- package/dist/guidance/references/build/fixtures/patterns/audit-log.jsx +81 -0
- package/dist/guidance/references/build/fixtures/patterns/auth.jsx +150 -0
- package/dist/guidance/references/build/fixtures/patterns/billing.jsx +96 -0
- package/dist/guidance/references/build/fixtures/patterns/developer-settings.jsx +95 -0
- package/dist/guidance/references/build/fixtures/patterns/import.jsx +102 -0
- package/dist/guidance/references/build/fixtures/patterns/members.jsx +106 -0
- package/dist/guidance/references/build/fixtures/patterns/onboarding.jsx +70 -0
- package/dist/guidance/references/build/fixtures/patterns/screen-layout-decision.jsx +74 -0
- package/dist/guidance/references/build/fixtures/patterns/screen-layout-editor.jsx +56 -0
- package/dist/guidance/references/build/fixtures/patterns/screen-layout-report.jsx +42 -0
- package/dist/guidance/references/build/fixtures/patterns/settings.jsx +73 -0
- package/dist/guidance/references/build/fixtures/patterns/sync.jsx +78 -0
- package/dist/guidance/references/project/components/src/pattern-async-collection.tsx +57 -0
- package/dist/guidance/references/project/components/src/pattern-consequential-action.tsx +77 -0
- package/dist/guidance/references/project/components/src/pattern-edit-record.tsx +127 -0
- package/dist/guidance/references/project/components/src/pattern-review-submit.tsx +47 -0
- package/dist/guidance/references/project/components/src/settings.tsx +79 -0
- package/dist/guidance/references/scripts/build-framework-fixture.mjs +20 -0
- package/dist/guidance/rules.json +653 -0
- package/dist/guidance/visual/Elevation.md +194 -0
- package/dist/guidance/visual/Fundamentals.md +83 -0
- package/dist/guidance/visual/Research.md +1620 -0
- package/dist/index.cjs +19009 -1854
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +19009 -1854
- package/dist/index.js.map +4 -4
- package/dist/tokens.css +84 -84
- package/dist/tokens.json +301 -128
- package/dist/types/adapter-codemirror.d.ts +13 -0
- package/dist/types/agent-fleet.d.ts +166 -0
- package/dist/types/ai-label.d.ts +30 -0
- package/dist/types/app-shell.d.ts +83 -0
- package/dist/types/area-chart.d.ts +80 -0
- package/dist/types/artifact.d.ts +65 -0
- package/dist/types/bar-chart.d.ts +83 -0
- package/dist/types/bar-list.d.ts +35 -0
- package/dist/types/breadcrumb.d.ts +62 -0
- package/dist/types/chart-brush.d.ts +35 -0
- package/dist/types/chart-themes.d.ts +229 -0
- package/dist/types/chart.d.ts +570 -0
- package/dist/types/checklist.d.ts +71 -0
- package/dist/types/code-block.d.ts +32 -0
- package/dist/types/collections.d.ts +116 -5
- package/dist/types/color-picker.d.ts +38 -0
- package/dist/types/command.d.ts +6 -3
- package/dist/types/comment-thread.d.ts +48 -0
- package/dist/types/confirmation.d.ts +60 -0
- package/dist/types/context-menu.d.ts +17 -0
- package/dist/types/controls.d.ts +134 -22
- package/dist/types/conversation.d.ts +130 -0
- package/dist/types/copy-button.d.ts +19 -0
- package/dist/types/core.d.ts +139 -10
- package/dist/types/data-grid.d.ts +76 -0
- package/dist/types/date-picker.d.ts +96 -0
- package/dist/types/diff-view.d.ts +54 -0
- package/dist/types/dot-plot.d.ts +54 -0
- package/dist/types/edit-region.d.ts +121 -0
- package/dist/types/feature-gate.d.ts +22 -0
- package/dist/types/filter-bar.d.ts +65 -0
- package/dist/types/formatted-number.d.ts +38 -0
- package/dist/types/heatmap.d.ts +64 -0
- package/dist/types/in-place.d.ts +24 -0
- package/dist/types/index.d.ts +159 -14
- package/dist/types/inline-citation.d.ts +35 -0
- package/dist/types/inline-edit.d.ts +71 -0
- package/dist/types/input-otp.d.ts +37 -0
- package/dist/types/item-group.d.ts +104 -0
- package/dist/types/line-chart.d.ts +116 -0
- package/dist/types/link.d.ts +11 -0
- package/dist/types/markdown.d.ts +88 -0
- package/dist/types/meter.d.ts +43 -0
- package/dist/types/modal-state.d.ts +62 -0
- package/dist/types/nav.d.ts +8 -1
- package/dist/types/notifications.d.ts +42 -0
- package/dist/types/number-input.d.ts +30 -0
- package/dist/types/overlays.d.ts +56 -15
- package/dist/types/page-header.d.ts +27 -0
- package/dist/types/pagination.d.ts +29 -0
- package/dist/types/palette.d.ts +64 -0
- package/dist/types/password-input.d.ts +22 -0
- package/dist/types/pattern-assistant-conversation.d.ts +55 -0
- package/dist/types/pattern-async-collection.d.ts +34 -0
- package/dist/types/pattern-consequential-action.d.ts +53 -0
- package/dist/types/pattern-edit-record.d.ts +72 -0
- package/dist/types/pattern-review-submit.d.ts +43 -0
- package/dist/types/permission-matrix.d.ts +34 -0
- package/dist/types/plan-picker.d.ts +40 -0
- package/dist/types/prompt-input.d.ts +106 -0
- package/dist/types/property-list.d.ts +67 -0
- package/dist/types/question.d.ts +64 -0
- package/dist/types/relative-time.d.ts +21 -0
- package/dist/types/removal-receipt.d.ts +1180 -0
- package/dist/types/resizable.d.ts +48 -0
- package/dist/types/roving.d.ts +44 -0
- package/dist/types/save-bar.d.ts +38 -0
- package/dist/types/saved-views.d.ts +72 -0
- package/dist/types/scatter-chart.d.ts +60 -0
- package/dist/types/schedule.d.ts +80 -0
- package/dist/types/secret-field.d.ts +33 -0
- package/dist/types/select.d.ts +44 -2
- package/dist/types/separator.d.ts +11 -0
- package/dist/types/settings.d.ts +39 -0
- package/dist/types/sheet.d.ts +1 -1
- package/dist/types/sparkline.d.ts +46 -0
- package/dist/types/split-button.d.ts +21 -0
- package/dist/types/stat.d.ts +54 -0
- package/dist/types/stepper.d.ts +6 -3
- package/dist/types/suggestions.d.ts +29 -0
- package/dist/types/suspense.d.ts +85 -0
- package/dist/types/sync-indicator.d.ts +24 -0
- package/dist/types/tag-input.d.ts +65 -0
- package/dist/types/terminal.d.ts +33 -0
- package/dist/types/thread-list.d.ts +58 -0
- package/dist/types/time-input.d.ts +33 -0
- package/dist/types/timeline.d.ts +38 -0
- package/dist/types/toggle.d.ts +46 -0
- package/dist/types/toolbar.d.ts +34 -0
- package/dist/types/tour.d.ts +23 -0
- package/dist/types/trace.d.ts +65 -0
- package/dist/types/tree.d.ts +71 -0
- package/dist/types-cjs/adapter-codemirror.d.cts +13 -0
- package/dist/types-cjs/agent-fleet.d.cts +166 -0
- package/dist/types-cjs/ai-label.d.cts +30 -0
- package/dist/types-cjs/app-shell.d.cts +83 -0
- package/dist/types-cjs/area-chart.d.cts +80 -0
- package/dist/types-cjs/artifact.d.cts +65 -0
- package/dist/types-cjs/bar-chart.d.cts +83 -0
- package/dist/types-cjs/bar-list.d.cts +35 -0
- package/dist/types-cjs/breadcrumb.d.cts +62 -0
- package/dist/types-cjs/chart-brush.d.cts +35 -0
- package/dist/types-cjs/chart-themes.d.cts +229 -0
- package/dist/types-cjs/chart.d.cts +570 -0
- package/dist/types-cjs/checklist.d.cts +71 -0
- package/dist/types-cjs/code-block.d.cts +32 -0
- package/dist/types-cjs/collections.d.cts +246 -0
- package/dist/types-cjs/color-picker.d.cts +38 -0
- package/dist/types-cjs/command.d.cts +66 -0
- package/dist/types-cjs/comment-thread.d.cts +48 -0
- package/dist/types-cjs/confirmation.d.cts +60 -0
- package/dist/types-cjs/context-menu.d.cts +17 -0
- package/dist/types-cjs/controls.d.cts +360 -0
- package/dist/types-cjs/conversation.d.cts +130 -0
- package/dist/types-cjs/copy-button.d.cts +19 -0
- package/dist/types-cjs/core.d.cts +256 -0
- package/dist/types-cjs/data-grid.d.cts +76 -0
- package/dist/types-cjs/date-picker.d.cts +96 -0
- package/dist/types-cjs/diff-view.d.cts +54 -0
- package/dist/types-cjs/dot-plot.d.cts +54 -0
- package/dist/types-cjs/edit-region.d.cts +121 -0
- package/dist/types-cjs/empty.d.cts +24 -0
- package/dist/types-cjs/feature-gate.d.cts +22 -0
- package/dist/types-cjs/filter-bar.d.cts +65 -0
- package/dist/types-cjs/formatted-number.d.cts +38 -0
- package/dist/types-cjs/heatmap.d.cts +64 -0
- package/dist/types-cjs/in-place.d.cts +24 -0
- package/dist/types-cjs/index.d.cts +154 -0
- package/dist/types-cjs/inline-citation.d.cts +35 -0
- package/dist/types-cjs/inline-edit.d.cts +71 -0
- package/dist/types-cjs/input-otp.d.cts +37 -0
- package/dist/types-cjs/item-group.d.cts +104 -0
- package/dist/types-cjs/line-chart.d.cts +116 -0
- package/dist/types-cjs/link.d.cts +11 -0
- package/dist/types-cjs/markdown.d.cts +88 -0
- package/dist/types-cjs/meter.d.cts +43 -0
- package/dist/types-cjs/modal-state.d.cts +62 -0
- package/dist/types-cjs/nav.d.cts +25 -0
- package/dist/types-cjs/notifications.d.cts +42 -0
- package/dist/types-cjs/number-input.d.cts +30 -0
- package/dist/types-cjs/overlays.d.cts +170 -0
- package/dist/types-cjs/page-header.d.cts +27 -0
- package/dist/types-cjs/pagination.d.cts +29 -0
- package/dist/types-cjs/palette.d.cts +64 -0
- package/dist/types-cjs/password-input.d.cts +22 -0
- package/dist/types-cjs/pattern-assistant-conversation.d.cts +55 -0
- package/dist/types-cjs/pattern-async-collection.d.cts +34 -0
- package/dist/types-cjs/pattern-consequential-action.d.cts +53 -0
- package/dist/types-cjs/pattern-edit-record.d.cts +72 -0
- package/dist/types-cjs/pattern-review-submit.d.cts +43 -0
- package/dist/types-cjs/permission-matrix.d.cts +34 -0
- package/dist/types-cjs/pile.d.cts +27 -0
- package/dist/types-cjs/plan-picker.d.cts +40 -0
- package/dist/types-cjs/prompt-input.d.cts +106 -0
- package/dist/types-cjs/property-list.d.cts +67 -0
- package/dist/types-cjs/question.d.cts +64 -0
- package/dist/types-cjs/relative-time.d.cts +21 -0
- package/dist/types-cjs/removal-receipt.d.cts +1180 -0
- package/dist/types-cjs/resizable.d.cts +48 -0
- package/dist/types-cjs/roving.d.cts +44 -0
- package/dist/types-cjs/save-bar.d.cts +38 -0
- package/dist/types-cjs/saved-views.d.cts +72 -0
- package/dist/types-cjs/scatter-chart.d.cts +60 -0
- package/dist/types-cjs/schedule.d.cts +80 -0
- package/dist/types-cjs/secret-field.d.cts +33 -0
- package/dist/types-cjs/select.d.cts +109 -0
- package/dist/types-cjs/separator.d.cts +11 -0
- package/dist/types-cjs/settings.d.cts +39 -0
- package/dist/types-cjs/sheet.d.cts +27 -0
- package/dist/types-cjs/sparkline.d.cts +46 -0
- package/dist/types-cjs/split-button.d.cts +21 -0
- package/dist/types-cjs/stat.d.cts +54 -0
- package/dist/types-cjs/stepper.d.cts +26 -0
- package/dist/types-cjs/suggestions.d.cts +29 -0
- package/dist/types-cjs/suspense.d.cts +85 -0
- package/dist/types-cjs/sync-indicator.d.cts +24 -0
- package/dist/types-cjs/tag-input.d.cts +65 -0
- package/dist/types-cjs/terminal.d.cts +33 -0
- package/dist/types-cjs/thread-list.d.cts +58 -0
- package/dist/types-cjs/time-input.d.cts +33 -0
- package/dist/types-cjs/timeline.d.cts +38 -0
- package/dist/types-cjs/toggle.d.cts +46 -0
- package/dist/types-cjs/toolbar.d.cts +34 -0
- package/dist/types-cjs/tour.d.cts +23 -0
- package/dist/types-cjs/trace.d.cts +65 -0
- package/dist/types-cjs/tree.d.cts +71 -0
- package/package.json +90 -9
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "settings",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"title": "Settings",
|
|
6
|
+
"intents": [
|
|
7
|
+
"settings",
|
|
8
|
+
"preferences",
|
|
9
|
+
"workspace settings",
|
|
10
|
+
"account settings",
|
|
11
|
+
"danger zone"
|
|
12
|
+
],
|
|
13
|
+
"purpose": "Change how a workspace or account behaves, deciding per section whether changes apply now or save together.",
|
|
14
|
+
"useWhen": [
|
|
15
|
+
"A page of settings mixes standalone preferences, records saved together and destructive actions."
|
|
16
|
+
],
|
|
17
|
+
"avoidWhen": [
|
|
18
|
+
"A single record form with no standalone preferences; use the edit-record contract alone."
|
|
19
|
+
],
|
|
20
|
+
"alternatives": [
|
|
21
|
+
"Use the edit-record contract for one record's form, and the consequential-action contract for a single destructive action outside settings."
|
|
22
|
+
],
|
|
23
|
+
"requiredInputs": [
|
|
24
|
+
"current settings and who may change each section",
|
|
25
|
+
"a save adapter per record saved together",
|
|
26
|
+
"a write adapter per apply-now preference",
|
|
27
|
+
"consequence and execute adapter per destructive action"
|
|
28
|
+
],
|
|
29
|
+
"states": [
|
|
30
|
+
"loading",
|
|
31
|
+
"ready",
|
|
32
|
+
"apply-pending",
|
|
33
|
+
"apply-committed",
|
|
34
|
+
"apply-failed",
|
|
35
|
+
"draft-dirty",
|
|
36
|
+
"draft-submitting",
|
|
37
|
+
"draft-failed",
|
|
38
|
+
"read-only",
|
|
39
|
+
"concurrent-change"
|
|
40
|
+
],
|
|
41
|
+
"transitions": [
|
|
42
|
+
"ready → apply-pending when an apply-now control changes",
|
|
43
|
+
"apply-pending → apply-committed on acknowledgement, or apply-failed with the control restored",
|
|
44
|
+
"ready → draft-dirty when a field saved together changes",
|
|
45
|
+
"draft-dirty → draft-submitting on save, or ready on discard",
|
|
46
|
+
"draft-submitting → ready on success, or draft-failed with the draft kept"
|
|
47
|
+
],
|
|
48
|
+
"rules": [
|
|
49
|
+
"ALT-SETTINGS-001",
|
|
50
|
+
"ALT-EDIT-001",
|
|
51
|
+
"ALT-EDIT-002",
|
|
52
|
+
"ALT-ACTION-001",
|
|
53
|
+
"ALT-STATUS-001",
|
|
54
|
+
"ALT-VISUAL-003"
|
|
55
|
+
],
|
|
56
|
+
"sources": [
|
|
57
|
+
"nng-toggle",
|
|
58
|
+
"govuk-validation",
|
|
59
|
+
"altitude-elevation",
|
|
60
|
+
"task-review"
|
|
61
|
+
],
|
|
62
|
+
"authority": "contextual-pattern",
|
|
63
|
+
"applicability": "Settings pages that combine standalone preferences, records saved together and destructive actions.",
|
|
64
|
+
"exceptions": [
|
|
65
|
+
"Require only states supported by the application; a page with only one mode needs only that mode's states."
|
|
66
|
+
],
|
|
67
|
+
"maturity": "provisional",
|
|
68
|
+
"evidenceStatus": "engineering-fixtures; human-effectiveness-unmeasured",
|
|
69
|
+
"document": "patterns/settings.md",
|
|
70
|
+
"implementation": "project/components/src/settings.tsx",
|
|
71
|
+
"examples": [
|
|
72
|
+
"build/fixtures/patterns/settings.jsx"
|
|
73
|
+
],
|
|
74
|
+
"verification": [
|
|
75
|
+
"build/checks/settings-pattern.mjs",
|
|
76
|
+
"build/checks/pattern-types.mjs"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Settings
|
|
2
|
+
|
|
3
|
+
Help someone review and change how a workspace, account or product behaves: sections of settings, some applied the moment they change and some saved together, with destructive actions kept apart. It composes [Edit record](edit-record.md) for fields saved together and [Consequential action](consequential-action.md) for the danger zone; this contract says which applies where. Use a route per settings area; don't put settings in a dialog.
|
|
4
|
+
|
|
5
|
+
## Inputs and ownership
|
|
6
|
+
|
|
7
|
+
Supply the current settings, which person may change which section, a save adapter for each record saved together, a write adapter for each apply-now preference, and the consequence and execute adapter for each destructive action. The application owns authorization (read-only sections must also be rejected by the server), persistence, concurrency between two people editing the same settings, and navigation guards. A switch that appears saved is not saved until its write is acknowledged.
|
|
8
|
+
|
|
9
|
+
## States and transitions
|
|
10
|
+
|
|
11
|
+
Decide each section's mode before building it, and never mix the two inside one section:
|
|
12
|
+
|
|
13
|
+
- **Apply now**: a preference that stands alone (a notification, a theme, a toggle for one feature). `Switch`, `RadioGroup` or `Select` changes it immediately and the row reports the write through `SettingsRow status`: pending hovers (after the grace period, so fast writes never flicker), committed lands with "Saved", failed drops into a trench with the reason, and the control returns to its last saved value.
|
|
14
|
+
- **Save together**: fields that form one record (workspace name and URL, a billing address). They join one draft, follow the edit-record states (editing, invalid, submitting, saved, failed) and are saved or discarded once with `SaveBar`, which appears when the draft has changed. Navigation away with unsaved changes asks before discarding.
|
|
15
|
+
|
|
16
|
+
Other applicable states: loading (each section's card stays up with its body as an imprint), read-only (`SettingsSection readOnlyReason` disables the rows and says why in words, for example "Only workspace owners can change billing details"), save failure (the draft is kept and the bar says what failed), and a concurrent change by someone else (show the newer value and who changed it; don't overwrite it silently).
|
|
17
|
+
|
|
18
|
+
## Layout, keyboard, focus and announcements
|
|
19
|
+
|
|
20
|
+
Lay out sections as `SettingsSection` cards, each with a heading that names its region, navigated by in-page links, `Tabs` or a sidebar for long pages. Each setting is a `SettingsRow`: label and description on the start side, the control on the end side, stacked on narrow screens; the row's label names the control and its description describes it. Put destructive actions last, in a section titled "Danger zone" (`tone="critical"`): the same height as every other section, a critical rim, and each action named by its outcome ("Delete workspace") with its consequence in words. Delete, transfer and similar actions follow the consequential-action contract: a typed confirmation in a stationary `Dialog`, or a `HoldButton`, and success reported only after the service confirms it.
|
|
21
|
+
|
|
22
|
+
Tab follows the visual order; every control is reachable and named. An invalid save moves focus to the error summary; an apply-now write announces its result politely beside the control without moving focus. Long labels and values wrap; at 320px rows stack. Verify forced colors (the critical rim stays visible), reduced motion (a pending row holds still, raised), zoom and RTL.
|
|
23
|
+
|
|
24
|
+
## Recovery and working implementation
|
|
25
|
+
|
|
26
|
+
A failed apply-now write puts the control back and says why; trying again is the same change. A failed save keeps the draft; retry resubmits it. A failed destructive action says nothing was removed and offers to try again. The working example in `build/fixtures/patterns/settings.jsx` (shipped as `references/build/fixtures/patterns/settings.jsx`) composes general fields saved together, apply-now notification switches, a read-only billing section and a typed-confirmation delete, with a demo service that can fail. It is also the Storybook preview "Patterns/SettingsPattern". [Metadata](settings.json) names the runtime and verification sources.
|
|
27
|
+
|
|
28
|
+
## Rules, verification and evidence
|
|
29
|
+
|
|
30
|
+
`ALT-SETTINGS-001` covers the mode decision and the danger zone; `ALT-EDIT-001` and `ALT-EDIT-002` cover the draft saved together; `ALT-ACTION-001` covers destructive actions; `ALT-STATUS-001` covers announcements; `ALT-VISUAL-003` covers what must not rise. `build/checks/settings-pattern.mjs` checks the example in a browser: named regions, labelled rows, the apply-now write's three states, the read-only reason, the danger zone's height and rim, the SaveBar draft and the typed confirmation. See [rules](../rules.json) for source authority and applicability. Whether people understand which settings apply immediately still needs evaluation with representative users.
|
|
31
|
+
|
|
32
|
+
See [running examples and verification](../principles.md#run-the-examples-and-verification) for the installed example command, repository test command, and the limits of shipped test-reference snapshots.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "sync",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"title": "Sync",
|
|
6
|
+
"intents": [
|
|
7
|
+
"autosave",
|
|
8
|
+
"offline",
|
|
9
|
+
"sync",
|
|
10
|
+
"conflict",
|
|
11
|
+
"session timeout",
|
|
12
|
+
"session expiry",
|
|
13
|
+
"local-first",
|
|
14
|
+
"optimistic"
|
|
15
|
+
],
|
|
16
|
+
"purpose": "Say truthfully what has reached the server, through offline queues, conflicts and expiring sessions.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"Work is saved continuously or optimistically, may go offline, or may be edited by two people at once."
|
|
19
|
+
],
|
|
20
|
+
"avoidWhen": [
|
|
21
|
+
"Forms saved once with an explicit button; use the edit-record contract."
|
|
22
|
+
],
|
|
23
|
+
"alternatives": [
|
|
24
|
+
"Use the edit-record contract for explicit saves, and SyncIndicator alone for a single autosaving document."
|
|
25
|
+
],
|
|
26
|
+
"requiredInputs": [
|
|
27
|
+
"each change's write and acknowledgement",
|
|
28
|
+
"the connection state",
|
|
29
|
+
"the queue of unacknowledged changes",
|
|
30
|
+
"conflict detection with both versions and the other author",
|
|
31
|
+
"session expiry time and a way to extend it"
|
|
32
|
+
],
|
|
33
|
+
"states": [
|
|
34
|
+
"saved",
|
|
35
|
+
"pending",
|
|
36
|
+
"failed",
|
|
37
|
+
"offline-queued",
|
|
38
|
+
"replaying",
|
|
39
|
+
"conflict",
|
|
40
|
+
"resolved",
|
|
41
|
+
"session-expiring",
|
|
42
|
+
"session-expired"
|
|
43
|
+
],
|
|
44
|
+
"transitions": [
|
|
45
|
+
"saved → pending on a change → saved when acknowledged, or failed with Retry",
|
|
46
|
+
"pending → offline-queued when the connection drops",
|
|
47
|
+
"offline-queued → replaying → saved when it returns",
|
|
48
|
+
"pending → conflict when another version arrived → resolved by keep mine, theirs or both",
|
|
49
|
+
"session-expiring → saved on Stay signed in, or session-expired keeping the draft"
|
|
50
|
+
],
|
|
51
|
+
"rules": [
|
|
52
|
+
"ALT-SYNC-001",
|
|
53
|
+
"ALT-EDIT-001",
|
|
54
|
+
"ALT-STATUS-001"
|
|
55
|
+
],
|
|
56
|
+
"sources": [
|
|
57
|
+
"altitude-elevation",
|
|
58
|
+
"task-review",
|
|
59
|
+
"wcag22"
|
|
60
|
+
],
|
|
61
|
+
"authority": "contextual-pattern",
|
|
62
|
+
"applicability": "Autosaving, optimistic or local-first work, and sessions that expire.",
|
|
63
|
+
"exceptions": [
|
|
64
|
+
"Require only states supported by the application; online-only apps skip the offline states but still never claim unacknowledged work is saved."
|
|
65
|
+
],
|
|
66
|
+
"maturity": "provisional",
|
|
67
|
+
"evidenceStatus": "engineering-fixtures; human-effectiveness-unmeasured",
|
|
68
|
+
"document": "patterns/sync.md",
|
|
69
|
+
"implementation": "build/fixtures/patterns/sync.jsx",
|
|
70
|
+
"examples": [
|
|
71
|
+
"build/fixtures/patterns/sync.jsx"
|
|
72
|
+
],
|
|
73
|
+
"verification": [
|
|
74
|
+
"build/checks/sync-pattern.mjs",
|
|
75
|
+
"build/checks/pattern-types.mjs"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Sync
|
|
2
|
+
|
|
3
|
+
Help someone trust what has reached the server: autosaving editors, optimistic lists and local-first apps that survive going offline, resolve the rare edit conflict without dropping anyone's work, and keep a draft through an expired session. It extends the persistence rules of [Edit record](edit-record.md) to work saved continuously.
|
|
4
|
+
|
|
5
|
+
## Inputs and ownership
|
|
6
|
+
|
|
7
|
+
Supply each change's write and its acknowledgement, the connection state, the queue of changes not yet acknowledged, conflict detection with both versions and who made the other one, and the session's expiry time with a way to extend it. The application owns the merge strategy (CRDT, operational transform, last write wins or a three-way merge), the durability of the local queue across reloads, idempotent replays, and re-authentication. A timer or a local write doesn't prove persistence; only the server's acknowledgement does.
|
|
8
|
+
|
|
9
|
+
## States and transitions
|
|
10
|
+
|
|
11
|
+
**Per item**: a change is `status="pending"` from the moment it's made until the server acknowledges it (it hovers, after the grace period so fast writes never flicker), then committed (it lands), or failed (it drops into a trench with Retry). **Globally**, `SyncIndicator` ties the items together: it counts the queue, says "Saved" only when the queue is empty and acknowledged (and announces "Saved" at most every few seconds), and offers Retry when sending fails.
|
|
12
|
+
|
|
13
|
+
**Offline**: queued changes stay pending and say "Waiting to sync"; the indicator says "Offline" with the count; nothing claims to be saved. Coming back online replays the queue in order; each item lands as it's acknowledged.
|
|
14
|
+
|
|
15
|
+
**Conflicts**: resolve automatically wherever the merge strategy can. For the rest, show both versions in place, as `EditRegion` does: the conflict asks for action (raised, level 1, "Changed by Jane Park while you were editing"); the other person's version wears their `presence-*` colour and name, because it's their hold on the record. Offer "Keep mine", "Keep theirs" and, where meaningful, "Keep both". Never drop an edit silently: say where the other version went ("Yours is in the history"). The indicator counts unresolved conflicts and takes you to them.
|
|
16
|
+
|
|
17
|
+
**Session expiry**: before the session ends, a stationary `Dialog` that Escape doesn't dismiss says the time left in words ("Your session ends in 2 minutes") with "Stay signed in" as primary. Any draft is kept locally across re-authentication, and the dialog says so. After expiry, signing in again returns to the same draft.
|
|
18
|
+
|
|
19
|
+
## Content, keyboard, focus and announcements
|
|
20
|
+
|
|
21
|
+
Going offline and conflicts are announced immediately; "Saved" is debounced. The indicator's plate is a button when there's something to show (the queue, the conflicts). Resolving a conflict moves focus to the resolved field's heading. Pending items keep their content readable; nothing is disabled just because it's waiting. Verify forced colors (pending items keep a dotted border, failed ones dashed), reduced motion (pending holds still, raised), zoom and RTL.
|
|
22
|
+
|
|
23
|
+
## Recovery and working implementation
|
|
24
|
+
|
|
25
|
+
A failed write offers Retry and keeps the change queued. A failed replay after reconnecting leaves the remaining items pending and says how many are waiting. The working example in `build/fixtures/patterns/sync.jsx` (shipped as `references/build/fixtures/patterns/sync.jsx`) composes pending items with a demo server, an offline queue, a conflict with keep mine, keep theirs or keep both, and an expiring session. It is also the Storybook preview "Patterns/SyncPattern". [Metadata](sync.json) names the runtime and verification sources.
|
|
26
|
+
|
|
27
|
+
## Rules, verification and evidence
|
|
28
|
+
|
|
29
|
+
`ALT-SYNC-001` covers pending until acknowledged, offline queues and conflicts that never drop work; `ALT-EDIT-001` covers truthful persistence; `ALT-STATUS-001` covers announcements. WCAG 2.2's timing-adjustable criterion applies to session expiry. `build/checks/sync-pattern.mjs` checks the example in a browser. See [rules](../rules.json). Whether people understand conflict choices still needs evaluation with representative users.
|
|
30
|
+
|
|
31
|
+
See [running examples and verification](../principles.md#run-the-examples-and-verification) for the installed example command, repository test command, and the limits of shipped test-reference snapshots.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Scope and ownership
|
|
2
|
+
|
|
3
|
+
Altitude supplies native/control semantics, keyboard and focus contracts, operation state, and reusable compositions. Applications supply authorization, validation, durable persistence, identity, service concurrency, routing/history, and the truth of content. People validate whether a task and its language fit their context.
|
|
4
|
+
|
|
5
|
+
Keep the ground as the record. Unsaved input is local editing; pending starts when a write is dispatched. Show committed only after the service confirms the promised outcome. Pair elevation with text, state, icons or borders. Follow the existing Elevation guide, shipped as `visual/Elevation.md` in package guidance; this framework adds no tier.
|
|
6
|
+
|
|
7
|
+
Rule authority is explicit: normative standards retain their applicability and exceptions; informative implementation patterns and heuristics are contextual; Altitude visual policies govern this library; experimental hypotheses are not established human-performance findings. A rule's diagnostic class is separate from its authority. Deterministic violations can fail checks; application integration obligations need service fixtures; contextual findings need product review.
|
|
8
|
+
|
|
9
|
+
The machine-readable sources are `patterns/*.json`, `rules.json`, `evidence.json`, and their schemas. Pattern Markdown explains those contracts. Runtime sources and executable fixtures implement them; tests establish only the behavior they exercise. Package builds copy prose and generate indexes, versions, hashes, and reference snapshots. Do not hand-edit `dist/guidance`.
|
|
10
|
+
|
|
11
|
+
For each supported journey test narrow viewports (320 CSS px with touch; see `project/Mobile.md` and `build/checks/phone.mjs`), long translated content, RTL, zoom, reduced motion and forced colors. Keep controls and status reachable without relying on hover, color or depth. Test with keyboard and relevant assistive technology; record browser/AT versions and remaining gaps. A passing scan is not a conformance claim.
|
|
12
|
+
|
|
13
|
+
## Run the examples and verification
|
|
14
|
+
|
|
15
|
+
The package includes runnable example compositions with public imports. In a consumer project with React, React DOM and Altitude installed, add the example bundler as a development dependency (`npm install --save-dev esbuild`), then run:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
node node_modules/altitude-design/dist/guidance/references/scripts/build-framework-fixture.mjs --out work/altitude-examples
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Open the generated `work/altitude-examples/index.html` in a browser. The helper aliases React, React DOM and Altitude to the consuming project's installed copies, avoiding duplicate providers or React instances. Its in-memory service demonstrates delays, rejection, real mutation and recovery; it does not supply production persistence. The same helper in the repository is `node scripts/build-framework-fixture.mjs --out work/altitude-examples`, after `npm run build`.
|
|
22
|
+
|
|
23
|
+
Files under `references/build/checks/` are version-matched evidence snapshots for inspection, not standalone installed test commands: they depend on repository tooling and fixtures. Maintainers run `node build/checks/pattern-workflows.mjs` in the repository after installing root and build dependencies, with a supported Playwright browser (or the `CHROME` executable environment variable). Consumers should adapt these scenarios into their own test runner using public exports and their actual service/navigation adapters; exercise delayed rejection, retry, retained data, keyboard and the real persistence outcome. `altitude doctor` checks installation and setup only. It does not run those application integration checks.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Agent action pattern: steps map to one reading each (planned, running, awaiting approval, done, failed); a reversible
|
|
2
|
+
// step runs without asking and offers Undo; sending email waits for approval as a Proposed card (raised, never hovering),
|
|
3
|
+
// hovers as pending only once approved, and lands with what happened; deleting for good asks in a stationary dialog; a
|
|
4
|
+
// failed step says nothing changed and retries; the changed record carries an AI label naming the approver. Run after a
|
|
5
|
+
// render of every preview (it opens test/p-AgentActionPattern-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: 1100 } });
|
|
12
|
+
await page.goto(`file://${T}p-AgentActionPattern-daylight.html`);
|
|
13
|
+
await page.waitForTimeout(300);
|
|
14
|
+
const steps = () => page.evaluate(() => [...document.querySelectorAll('.alt-trace-item')].map((s) => s.dataset.trace));
|
|
15
|
+
const confirm = (n) => page.evaluate((n) => { const c = document.querySelectorAll('.alt-confirm')[n]; return c && { state: c.dataset.state, status: c.dataset.status || '', anim: getComputedStyle(c).animationName, text: c.textContent }; }, n);
|
|
16
|
+
|
|
17
|
+
expect((await steps()).join() === 'running,planned,planned,planned', `planned steps wait while the first runs (${await steps()})`);
|
|
18
|
+
await page.waitForTimeout(2000);
|
|
19
|
+
expect((await steps()).join() === 'done,done,awaiting-approval,planned', `a reversible step runs without asking (${await steps()})`);
|
|
20
|
+
expect(/4 leads archived\. You can undo this for 30 days\./.test(await page.evaluate(() => document.querySelector('.ag-receipt').textContent)), 'and says what it did, with Undo');
|
|
21
|
+
let c = await confirm(0);
|
|
22
|
+
expect(c.state === 'requested' && c.status === '' && c.anim !== 'alt-hover' && /Theo Marsh, Ines Duarte and Sam Okafor/.test(c.text), `sending email waits as a Proposed card, never hovering (${JSON.stringify({ ...c, text: '' })})`);
|
|
23
|
+
await page.getByRole('button', { name: 'Send 3 emails' }).click();
|
|
24
|
+
await page.waitForTimeout(400);
|
|
25
|
+
c = await confirm(0);
|
|
26
|
+
expect(c.state === 'running' && c.status === 'pending', `approved, it hovers as pending (${c.state}/${c.status})`);
|
|
27
|
+
await page.waitForTimeout(900);
|
|
28
|
+
c = await confirm(0);
|
|
29
|
+
expect(c.state === 'done' && /3 emails sent/.test(c.text), 'then lands with what happened');
|
|
30
|
+
|
|
31
|
+
await page.getByRole('button', { name: 'Delete list' }).click();
|
|
32
|
+
await page.waitForTimeout(500);
|
|
33
|
+
const dialog = page.getByRole('dialog', { name: 'Delete the “Summer import” list' });
|
|
34
|
+
expect(await dialog.count() === 1 && await page.locator('[data-receded]').count() === 0, 'deleting for good asks in a stationary dialog');
|
|
35
|
+
await dialog.getByRole('button', { name: 'Delete list' }).click();
|
|
36
|
+
await page.waitForTimeout(1300);
|
|
37
|
+
expect((await steps()).join() === 'done,done,done,done', `every step lands (${await steps()})`);
|
|
38
|
+
|
|
39
|
+
const record = await page.evaluate(() => { const r = document.querySelector('.ag-record'); return r && { label: r.querySelector('.alt-ailabel')?.textContent, text: r.textContent }; });
|
|
40
|
+
expect(record && record.label === 'Drafted with AI' && /approved by Maya Chen/.test(record.text), 'the changed record carries provenance and its approver');
|
|
41
|
+
|
|
42
|
+
await page.getByRole('button', { name: 'Undo archive' }).click();
|
|
43
|
+
await page.waitForTimeout(200);
|
|
44
|
+
expect(/Archive undone/.test(await page.evaluate(() => document.querySelector('.ag-receipt').textContent)), 'Undo reverses the reversible step');
|
|
45
|
+
|
|
46
|
+
await page.getByRole('switch', { name: 'Make the next step fail' }).click();
|
|
47
|
+
await page.getByRole('button', { name: 'Run again' }).click();
|
|
48
|
+
await page.waitForTimeout(1100);
|
|
49
|
+
const failed = await page.evaluate(() => { const s = document.querySelector('.alt-trace-item'); return { state: s.dataset.trace, text: s.textContent }; });
|
|
50
|
+
expect(failed.state === 'failed' && /Nothing was changed/.test(failed.text), 'a failed step sinks and says nothing changed');
|
|
51
|
+
await page.getByRole('button', { name: /Retry/ }).first().click();
|
|
52
|
+
await page.waitForTimeout(1900);
|
|
53
|
+
expect((await steps()).slice(0, 2).join() === 'done,done', 'and Retry runs it again');
|
|
54
|
+
|
|
55
|
+
console.log(`Agent action pattern: ${pass} passed, ${fail} failed`);
|
|
56
|
+
if (fail) process.exitCode = 1;
|
|
57
|
+
await browser.close();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Exercises the actual packed-and-installed Node CLI and its two instruction adapters.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync, rmSync, symlinkSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { root } from '../../scripts/build-guidance.mjs';
|
|
8
|
+
// npm 11 and earlier print an array from `pack --json`; npm 12 keys the result by package name.
|
|
9
|
+
const firstPacked=result=>Array.isArray(result)?result[0]:Object.values(result)[0];
|
|
10
|
+
const dir=mkdtempSync(resolve(tmpdir(),'altitude-agent-setup-'));
|
|
11
|
+
try {
|
|
12
|
+
const pack=firstPacked(JSON.parse(execFileSync('npm',['pack','--json','--pack-destination',dir],{cwd:root,encoding:'utf8'})));
|
|
13
|
+
writeFileSync(resolve(dir,'package.json'),'{"name":"altitude-setup-consumer","private":true,"version":"1.0.0"}\n');
|
|
14
|
+
execFileSync('npm',['install','--ignore-scripts','--legacy-peer-deps','--no-audit','--no-fund',resolve(dir,pack.filename)],{cwd:dir,stdio:'pipe'});
|
|
15
|
+
const cli=resolve(dir,'node_modules/altitude-design/dist/cli/altitude.mjs');
|
|
16
|
+
assert.ok(existsSync(cli));
|
|
17
|
+
function run(args,ok=true){const r=spawnSync(process.execPath,[cli,...args],{cwd:dir,encoding:'utf8'});assert.equal(r.status,ok?0:1,`${args}: ${r.stdout}\n${r.stderr}`);return JSON.parse(r.stdout);}
|
|
18
|
+
const version=JSON.parse(readFileSync(resolve(root,'package.json'),'utf8')).version;
|
|
19
|
+
for(const [client,file]of [['codex','AGENTS.md'],['claude','CLAUDE.md']]) {
|
|
20
|
+
const p=resolve(dir,file),original='# Existing instructions\r\nKeep this exact text without trailing newline';
|
|
21
|
+
writeFileSync(p,original);
|
|
22
|
+
const preview=run(['init','--client',client]);assert.equal(preview.mode,'preview');assert.equal(readFileSync(p,'utf8'),original);
|
|
23
|
+
assert.ok(preview.changes[0].after.includes(`Altitude framework (${version})`));
|
|
24
|
+
run(['init','--client',client,'--apply']);const installed=readFileSync(p,'utf8');assert.ok(installed.startsWith(original));
|
|
25
|
+
const again=run(['init','--client',client,'--apply']);assert.equal(again.changes[0].action,'unchanged');assert.equal(readFileSync(p,'utf8'),installed);
|
|
26
|
+
assert.ok(run(['doctor','--json']).ok);
|
|
27
|
+
// Later user additions survive removal byte-for-byte.
|
|
28
|
+
writeFileSync(p,installed+'\nLater project instructions.');
|
|
29
|
+
run(['init','--client',client,'--remove']);assert.ok(readFileSync(p,'utf8').includes('altitude:start'));
|
|
30
|
+
run(['init','--client',client,'--remove','--apply']);assert.equal(readFileSync(p,'utf8'),original+'\nLater project instructions.');
|
|
31
|
+
rmSync(p);run(['init','--client',client,'--apply']);run(['init','--client',client,'--remove','--apply']);assert.ok(!existsSync(p),'remove newly created file');
|
|
32
|
+
run(['init','--client',client,'--apply']);writeFileSync(p,readFileSync(p,'utf8').replace('For UI tasks','My changed content'));
|
|
33
|
+
run(['init','--client',client,'--apply'],false);assert.equal(run(['doctor','--json'],false).ok,false);rmSync(p);
|
|
34
|
+
}
|
|
35
|
+
const discovery=run(['patterns','edit settings','--json']);assert.equal(discovery.patterns[0].id,'edit-record');assert.equal(discovery.version,version);
|
|
36
|
+
assert.equal(run(['patterns','unmatched-gibberish','--json']).patterns.length,0);
|
|
37
|
+
assert.equal(run(['explain','ALT-EDIT-001','--json']).diagnosticClass,'application-integration');
|
|
38
|
+
run(['explain','unknown'],false);
|
|
39
|
+
const report=run(['doctor','--json']);assert.ok(report.findings.some(f=>f.kind==='contextual-review'&&f.status==='unverified'));assert.ok(report.findings.some(f=>f.kind==='application-integration'));
|
|
40
|
+
// Reject conflicting markers and symlinks without overwriting user-owned instructions.
|
|
41
|
+
writeFileSync(resolve(dir,'AGENTS.md'),'<!-- altitude:start -->');run(['init','--client','all','--apply'],false);assert.ok(!existsSync(resolve(dir,'CLAUDE.md')));rmSync(resolve(dir,'AGENTS.md'));
|
|
42
|
+
writeFileSync(resolve(dir,'outside.md'),'Keep');symlinkSync(resolve(dir,'outside.md'),resolve(dir,'AGENTS.md'));run(['init','--client','codex','--apply'],false);assert.equal(readFileSync(resolve(dir,'outside.md'),'utf8'),'Keep');rmSync(resolve(dir,'AGENTS.md'));
|
|
43
|
+
// Dangling links must not create their destination through writeFileSync.
|
|
44
|
+
symlinkSync(resolve(dir,'missing-target.md'),resolve(dir,'AGENTS.md'));run(['init','--client','codex','--apply'],false);assert.ok(!existsSync(resolve(dir,'missing-target.md')));rmSync(resolve(dir,'AGENTS.md'));
|
|
45
|
+
// Codex gives nonempty overrides precedence over AGENTS.md in the same directory.
|
|
46
|
+
writeFileSync(resolve(dir,'AGENTS.md'),'Ordinary instructions');writeFileSync(resolve(dir,'AGENTS.override.md'),'Active override');
|
|
47
|
+
const overridden=run(['init','--client','codex','--apply']);assert.ok(overridden.changes[0].path.endsWith('AGENTS.override.md'));
|
|
48
|
+
assert.equal(readFileSync(resolve(dir,'AGENTS.md'),'utf8'),'Ordinary instructions');assert.ok(run(['doctor','--json']).ok);
|
|
49
|
+
run(['init','--client','codex','--remove','--apply']);assert.equal(readFileSync(resolve(dir,'AGENTS.override.md'),'utf8'),'Active override');
|
|
50
|
+
rmSync(resolve(dir,'AGENTS.md'));rmSync(resolve(dir,'AGENTS.override.md'));
|
|
51
|
+
// Removal finds owned blocks even after the client switches instruction targets.
|
|
52
|
+
const ordinaryPath=resolve(dir,'AGENTS.md'),overridePath=resolve(dir,'AGENTS.override.md');
|
|
53
|
+
const ordinary='Original ordinary instructions\r\nNo trailing newline',override='Later override instructions';
|
|
54
|
+
writeFileSync(ordinaryPath,ordinary);run(['init','--client','codex','--apply']);
|
|
55
|
+
writeFileSync(overridePath,override);
|
|
56
|
+
const hiddenPreview=run(['init','--client','codex','--remove']);
|
|
57
|
+
assert.ok(hiddenPreview.changes.some(change=>change.path.endsWith('/AGENTS.md')&&change.action==='update'));
|
|
58
|
+
assert.ok(readFileSync(ordinaryPath,'utf8').includes('altitude:start'),'preview keeps inactive owned block');
|
|
59
|
+
run(['init','--client','codex','--remove','--apply']);
|
|
60
|
+
assert.equal(readFileSync(ordinaryPath,'utf8'),ordinary);assert.equal(readFileSync(overridePath,'utf8'),override);
|
|
61
|
+
// A managed block may exist in both files after setup follows a newly added override.
|
|
62
|
+
rmSync(overridePath);run(['init','--client','codex','--apply']);writeFileSync(overridePath,override);run(['init','--client','codex','--apply']);
|
|
63
|
+
const bothPreview=run(['init','--client','codex','--remove']);
|
|
64
|
+
assert.equal(bothPreview.changes.filter(change=>change.action==='update').length,2);
|
|
65
|
+
const intactOrdinary=readFileSync(ordinaryPath,'utf8'),intactOverride=readFileSync(overridePath,'utf8');
|
|
66
|
+
writeFileSync(ordinaryPath,intactOrdinary.replace('For UI tasks','User edited managed block'));
|
|
67
|
+
run(['init','--client','codex','--remove','--apply'],false);
|
|
68
|
+
assert.equal(readFileSync(overridePath,'utf8'),intactOverride,'invalid inactive block prevents partial removal');
|
|
69
|
+
writeFileSync(ordinaryPath,intactOrdinary);run(['init','--client','codex','--remove','--apply']);
|
|
70
|
+
assert.equal(readFileSync(ordinaryPath,'utf8'),ordinary);assert.equal(readFileSync(overridePath,'utf8'),override);
|
|
71
|
+
assert.ok(run(['init','--client','codex','--remove','--apply']).changes.every(change=>change.action==='unchanged'));
|
|
72
|
+
rmSync(ordinaryPath);rmSync(overridePath);
|
|
73
|
+
// Version mismatch and changed source detection are deterministic failures.
|
|
74
|
+
const indexPath=resolve(dir,'node_modules/altitude-design/dist/guidance/index.json'),originalIndex=readFileSync(indexPath,'utf8');
|
|
75
|
+
const index=JSON.parse(originalIndex);index.version='0.0.0';writeFileSync(indexPath,JSON.stringify(index));assert.ok(run(['doctor','--json'],false).findings.some(f=>f.observed.includes('0.0.0')));writeFileSync(indexPath,originalIndex);
|
|
76
|
+
const entry=resolve(dir,'node_modules/altitude-design/dist/guidance/README.md');writeFileSync(entry,'stale');assert.ok(run(['doctor','--json'],false).findings.some(f=>f.observed.includes('README.md')));
|
|
77
|
+
console.log('agent setup: packed install, Codex + Claude preview/apply/idempotence/removal/preservation/discovery/diagnostics pass');
|
|
78
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Assistant conversation pattern (#285): in its preview, a send shows the person's message and streams the answer on
|
|
2
|
+
// a dashed rule; a double Enter sends once; Stop keeps the partial answer as "Stopped" with Retry; offline and rate
|
|
3
|
+
// limited failures keep the message in the composer and say what happened in words; context full offers a new
|
|
4
|
+
// conversation; reloading mid-answer shows it "Interrupted" with Retry, never complete; the context Meter speaks in
|
|
5
|
+
// words. Run after a render of every preview (it opens test/p-AssistantConversationPattern-daylight.html); exits
|
|
6
|
+
// 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
|
+
const errors = []; page.on('pageerror', (e) => errors.push(e.message));
|
|
14
|
+
await page.goto(`file://${T}p-AssistantConversationPattern-daylight.html`); await page.waitForTimeout(400);
|
|
15
|
+
const box = page.locator('.alt-prompt textarea');
|
|
16
|
+
const read = () => page.evaluate(() => ({ user: [...document.querySelectorAll('.alt-msg[data-from="user"]')].map((m) => m.textContent), answer: document.querySelector('.alt-msg[data-from="assistant"]'), alert: document.querySelector('.alt-alert')?.textContent ?? null, draft: document.querySelector('.alt-prompt textarea').value }))
|
|
17
|
+
.then((r) => r);
|
|
18
|
+
const answer = () => page.evaluate(() => { const m = document.querySelector('.alt-msg[data-from="assistant"]'); return m ? { stream: m.dataset.stream, text: m.querySelector('.alt-msg-content')?.textContent ?? '', caption: m.querySelector('.alt-msg-caption')?.textContent ?? '', retry: !!m.querySelector('button') } : null; });
|
|
19
|
+
await box.fill('What are the open risks?'); await box.press('Enter'); await box.press('Enter'); await page.waitForTimeout(600);
|
|
20
|
+
let a = await answer();
|
|
21
|
+
expect((await read()).user.length === 1 && a?.stream === 'streaming', `a send streams the answer, and a double Enter sends once (${JSON.stringify(a)})`);
|
|
22
|
+
await page.click('.alt-prompt button[aria-label*="Stop" i]'); await page.waitForTimeout(150);
|
|
23
|
+
a = await answer();
|
|
24
|
+
expect(a.stream === 'stopped' && a.text.length > 0 && /Stopped/.test(a.caption) && a.retry, `Stop keeps the partial answer as "Stopped" with Retry (${JSON.stringify(a)})`);
|
|
25
|
+
await page.click('.alt-msg[data-from="assistant"] button:has-text("Retry")'); await page.waitForTimeout(3200);
|
|
26
|
+
a = await answer();
|
|
27
|
+
expect(a.stream === 'complete' && a.text.includes('decision from legal by Friday'), `Retry completes the answer (${a.stream})`);
|
|
28
|
+
// Offline: the message stays in the composer, said in words.
|
|
29
|
+
await page.click('[role="radio"]:has-text("Offline"), button:has-text("Offline")'); await page.waitForTimeout(100);
|
|
30
|
+
await box.fill('Summarise it'); await box.press('Enter'); await page.waitForTimeout(700);
|
|
31
|
+
let r = await read();
|
|
32
|
+
expect(/We can't reach the assistant/.test(r.alert ?? '') && r.draft === 'Summarise it', `offline keeps the message and says so (${JSON.stringify({ alert: r.alert, draft: r.draft })})`);
|
|
33
|
+
await page.click('button:has-text("Rate limited")'); await page.waitForTimeout(100);
|
|
34
|
+
await box.press('Enter'); await page.waitForTimeout(700);
|
|
35
|
+
r = await read();
|
|
36
|
+
expect(/You've reached this hour's limit\. Try again at/.test(r.alert ?? '') && r.draft === 'Summarise it', `a rate limit says when (${r.alert})`);
|
|
37
|
+
await page.click('button:has-text("Context full")'); await page.waitForTimeout(100);
|
|
38
|
+
await box.press('Enter'); await page.waitForTimeout(700);
|
|
39
|
+
r = await read();
|
|
40
|
+
expect(/too long to continue/.test(r.alert ?? '') && await page.locator('.alt-alert button:has-text("Start a new conversation")').count() === 1, 'context full offers a new conversation');
|
|
41
|
+
const meter = await page.evaluate(() => document.querySelector('.ac-context [role="meter"], .ac-context[role="meter"]')?.getAttribute('aria-valuetext') ?? document.querySelector('.ac-context')?.textContent);
|
|
42
|
+
expect(/% of context used/.test(meter ?? ''), `the context meter speaks in words (${meter})`);
|
|
43
|
+
await page.click('button:has-text("Reload mid-answer")'); await page.waitForTimeout(300);
|
|
44
|
+
a = await answer();
|
|
45
|
+
expect(a.stream === 'error' && /Interrupted/.test(await page.evaluate(() => document.querySelector('.ac-note')?.textContent ?? '')) && a.retry, `after a reload, an unfinished answer is interrupted with Retry (${JSON.stringify(a)})`);
|
|
46
|
+
expect(!errors.length, `no page errors (${errors.join('; ')})`);
|
|
47
|
+
console.log(`Assistant conversation pattern: ${pass} passed, ${fail} failed`);
|
|
48
|
+
if (fail) process.exitCode = 1;
|
|
49
|
+
await browser.close();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Audit log pattern: entries are the record, flush rows that read as one sentence and never lift; an entry expands to
|
|
2
|
+
// show what changed; the server pages the log, holding the rows while the next page loads; filtering narrows it and
|
|
3
|
+
// returns to the first page; exporting runs as a task (Progress with its percentage), then a toast offers the download
|
|
4
|
+
// and the export is logged; a record's activity is a Timeline whose times have their exact value. Run after a render of
|
|
5
|
+
// every preview (it opens test/p-AuditLogPattern-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-AuditLogPattern-daylight.html`);
|
|
13
|
+
await page.waitForTimeout(500);
|
|
14
|
+
const table = page.getByRole('table', { name: 'Audit log events' });
|
|
15
|
+
const events = () => table.locator('tbody tr:not(.alt-dt-detail-row) td:nth-child(2)').allTextContents();
|
|
16
|
+
|
|
17
|
+
let first = await events();
|
|
18
|
+
expect(first.length === 5 && /^Maya Chen changed Ines Duarte’s role from Member to Admin$/.test(first[0]), `each entry reads as one sentence (${first[0]})`);
|
|
19
|
+
await table.locator('tbody tr').nth(1).hover();
|
|
20
|
+
await page.waitForTimeout(250);
|
|
21
|
+
const flush = await table.locator('tbody tr').nth(1).evaluate((r) => ({ t: getComputedStyle(r).transform, button: r.querySelector('[role="button"], a') !== null }));
|
|
22
|
+
expect(flush.t === 'none' && !flush.button, `entries are flush and don’t lift (${JSON.stringify(flush)})`);
|
|
23
|
+
|
|
24
|
+
await table.getByRole('button', { name: /Maya Chen changed Ines Duarte’s role/ }).first().click();
|
|
25
|
+
await page.waitForTimeout(400);
|
|
26
|
+
const diff = await page.evaluate(() => document.querySelector('.alt-diff')?.textContent || '');
|
|
27
|
+
expect(/Member/.test(diff) && /Admin/.test(diff), 'an entry expands to show what changed');
|
|
28
|
+
|
|
29
|
+
await page.getByRole('button', { name: 'Next page' }).click();
|
|
30
|
+
await page.waitForTimeout(150);
|
|
31
|
+
expect(await page.evaluate(() => !!document.querySelector('[aria-busy="true"], [data-loading]')), 'the next page loads with the rows held');
|
|
32
|
+
await page.waitForTimeout(500);
|
|
33
|
+
const second = await events();
|
|
34
|
+
expect(second.length === 5 && second[0] !== first[0], 'the server pages the log');
|
|
35
|
+
|
|
36
|
+
await page.getByRole('button', { name: 'Export CSV' }).click();
|
|
37
|
+
await page.waitForTimeout(600);
|
|
38
|
+
const job = await page.evaluate(() => ({ bar: !!document.querySelector('.al-export [role="progressbar"]'), words: document.querySelector('.al-export [role="status"]')?.textContent }));
|
|
39
|
+
expect(job.bar && /^Preparing export, \d+%$/.test(job.words || ''), `exporting is a task with its percentage (${JSON.stringify(job)})`);
|
|
40
|
+
await page.waitForTimeout(1400);
|
|
41
|
+
expect(await page.getByRole('button', { name: 'Download' }).count() === 1, 'a toast offers the download');
|
|
42
|
+
await page.getByRole('button', { name: 'Page 1' }).click().catch(() => {});
|
|
43
|
+
await page.waitForTimeout(600);
|
|
44
|
+
expect((await events())[0] === 'Maya Chen exported 22 audit log events as CSV', 'and the export is itself logged');
|
|
45
|
+
|
|
46
|
+
await page.getByRole('button', { name: 'Next page' }).click();
|
|
47
|
+
await page.waitForTimeout(600);
|
|
48
|
+
expect(await page.locator('[aria-current="page"]').first().textContent() === '2', 'on page 2 before filtering');
|
|
49
|
+
await page.getByRole('button', { name: 'Add filter' }).click();
|
|
50
|
+
await page.getByRole('option', { name: 'Actor' }).click();
|
|
51
|
+
await page.getByRole('dialog', { name: 'Actor is' }).waitFor();
|
|
52
|
+
await page.getByRole('option', { name: 'Theo Marsh' }).click();
|
|
53
|
+
await page.waitForTimeout(300);
|
|
54
|
+
const theo = await events();
|
|
55
|
+
expect(theo.length > 0 && theo.every((e) => e.startsWith('Theo Marsh')), `filtering narrows the log (${theo.length} rows)`);
|
|
56
|
+
expect(await page.locator('[aria-current="page"]').first().textContent() === '1', 'and returns to the first page');
|
|
57
|
+
|
|
58
|
+
const times = await page.evaluate(() => [...document.querySelectorAll('.al-record time')].map((t) => t.getAttribute('datetime')));
|
|
59
|
+
expect(times.length === 3 && times.every(Boolean), 'record activity is a Timeline whose times carry their exact value');
|
|
60
|
+
|
|
61
|
+
console.log(`Audit log pattern: ${pass} passed, ${fail} failed`);
|
|
62
|
+
if (fail) process.exitCode = 1;
|
|
63
|
+
await browser.close();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Auth pattern: the form is on a raised Card, never a dialog; every field has its autocomplete token; an empty submit
|
|
2
|
+
// focuses the error summary; a wrong password never says whether the account exists, clears only the password and keeps
|
|
3
|
+
// the email; repeated failures are rate limited with the wait in words; an emailed link says "if that address has an
|
|
4
|
+
// account", counts down before a resend in words, and keeps the email when you go back; two-factor isn't submitted for
|
|
5
|
+
// you, a wrong code says so, and the right one signs in; sign-up shows the password rules and asks for a new password.
|
|
6
|
+
// Run after a render of every preview (it opens test/p-AuthPattern-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: 800 } });
|
|
13
|
+
await page.goto(`file://${T}p-AuthPattern-daylight.html`);
|
|
14
|
+
await page.waitForTimeout(400);
|
|
15
|
+
const focused = () => page.evaluate(() => document.activeElement.textContent || document.activeElement.className);
|
|
16
|
+
const text = () => page.evaluate(() => document.querySelector('.au-card').textContent);
|
|
17
|
+
const signIn = async (email, password) => {
|
|
18
|
+
await page.fill('#au-email', email); await page.fill('#au-password', password);
|
|
19
|
+
await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await page.waitForTimeout(1000);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
expect(await page.locator('[role="dialog"]').count() === 0 && await page.locator('.au-card.alt-card').count() === 1, 'sign-in is on a raised card, not a dialog');
|
|
23
|
+
expect(await page.getAttribute('#au-email', 'autocomplete') === 'username' && await page.getAttribute('#au-password', 'autocomplete') === 'current-password', 'fields carry autocomplete tokens');
|
|
24
|
+
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
|
25
|
+
await page.waitForTimeout(300);
|
|
26
|
+
expect(await page.evaluate(() => !!document.activeElement.closest('.alt-error-summary')), 'an empty submit focuses the error summary');
|
|
27
|
+
await signIn('maya@acme.dev', 'wrong');
|
|
28
|
+
let t = await text();
|
|
29
|
+
expect(/That email and password don’t match/.test(t) && !/no account|not found|doesn’t exist/i.test(t), 'a wrong password never says whether the account exists');
|
|
30
|
+
expect(await page.inputValue('#au-email') === 'maya@acme.dev' && await page.inputValue('#au-password') === '', 'it keeps the email and clears only the password');
|
|
31
|
+
await signIn('maya@acme.dev', 'altitude');
|
|
32
|
+
expect(await focused() === 'Enter your code', 'the right password moves to two-factor, focusing its heading');
|
|
33
|
+
expect(await page.getAttribute('#au-code', 'autocomplete') === 'one-time-code', 'the code field asks for a one-time code');
|
|
34
|
+
const verify = page.getByRole('button', { name: 'Verify', exact: true });
|
|
35
|
+
await page.locator('#au-code').pressSequentially('000000');
|
|
36
|
+
await page.waitForTimeout(300);
|
|
37
|
+
expect(await page.getByRole('heading', { name: 'You’re signed in' }).count() === 0 && !(await verify.isDisabled()), 'a complete code isn’t submitted for you');
|
|
38
|
+
await verify.click();
|
|
39
|
+
await page.waitForTimeout(1000);
|
|
40
|
+
expect(/That code didn’t work/.test(await text()), 'a wrong code says so');
|
|
41
|
+
await page.locator('#au-code').pressSequentially('123456');
|
|
42
|
+
await verify.click();
|
|
43
|
+
await page.waitForTimeout(1000);
|
|
44
|
+
expect(await page.getByRole('heading', { name: 'You’re signed in' }).count() === 1, 'the right code signs in');
|
|
45
|
+
await page.getByRole('button', { name: 'Sign out' }).click();
|
|
46
|
+
await page.waitForTimeout(200);
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < 3; i++) await signIn('maya@acme.dev', 'nope');
|
|
49
|
+
await signIn('maya@acme.dev', 'nope');
|
|
50
|
+
expect(/Too many attempts\. Wait 5 minutes/.test(await text()), 'repeated failures are rate limited, with the wait in words');
|
|
51
|
+
|
|
52
|
+
await page.getByRole('button', { name: 'Email me a sign-in link' }).click();
|
|
53
|
+
await page.waitForTimeout(1000);
|
|
54
|
+
t = await text();
|
|
55
|
+
expect(await focused() === 'Check your email' && /If maya@acme\.dev has an Acme account, we’ve sent a sign-in link/.test(t), 'the link screen never says whether the account exists');
|
|
56
|
+
expect(/You can resend the email in (30|29) seconds\./.test(t), 'a resend waits, with the time in words');
|
|
57
|
+
await page.getByRole('button', { name: 'Use a different email address' }).click();
|
|
58
|
+
await page.waitForTimeout(200);
|
|
59
|
+
expect(await page.inputValue('#au-email') === 'maya@acme.dev', 'going back keeps the email');
|
|
60
|
+
|
|
61
|
+
await page.getByRole('button', { name: 'Create an account' }).click();
|
|
62
|
+
await page.waitForTimeout(200);
|
|
63
|
+
expect(await page.getAttribute('#au-password', 'autocomplete') === 'new-password' && /At least 12 characters/.test(await text()), 'sign-up asks for a new password and shows its rules');
|
|
64
|
+
|
|
65
|
+
console.log(`Auth pattern: ${pass} passed, ${fail} failed`);
|
|
66
|
+
if (fail) process.exitCode = 1;
|
|
67
|
+
await browser.close();
|