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,102 @@
|
|
|
1
|
+
// Import pattern (project/framework/patterns/import.md): upload a CSV, map its columns, fix the rows that have problems,
|
|
2
|
+
// review, then import as a job with a receipt. A column's Select fills when it's mapped, so what's left shows; invalid
|
|
3
|
+
// cells lie in a trench on critical-soft (DataGrid showInvalid); the problem count is said as it changes; the import
|
|
4
|
+
// can be cancelled and ends with what was imported, skipped and why.
|
|
5
|
+
// The lines between the preview markers are its "ImportPattern" preview (scripts/pattern-previews.mjs copies them);
|
|
6
|
+
// build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if the two differ.
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import * as A from 'altitude-design';
|
|
9
|
+
// preview:start
|
|
10
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
const STEPS = [{ title: 'Upload' }, { title: 'Map columns' }, { title: 'Fix problems' }, { title: 'Review' }, { title: 'Import' }];
|
|
12
|
+
const SOURCE = [{ key: 'c0', header: 'Full name', sample: 'Ada Lovelace' }, { key: 'c1', header: 'E-mail', sample: 'ada@example.com' }, { key: 'c2', header: 'Company', sample: 'Analytical Ltd' }, { key: 'c3', header: 'Notes', sample: 'Met at the conference' }];
|
|
13
|
+
const TARGETS = [{ value: 'name', label: 'Name (required)' }, { value: 'email', label: 'Email (required)' }, { value: 'company', label: 'Company' }, { value: 'skip', label: 'Don’t import' }];
|
|
14
|
+
const FILE = [
|
|
15
|
+
['Ada Lovelace', 'ada@example.com', 'Analytical Ltd', ''], ['Grace Hopper', 'grace@example', 'Navy Labs', 'Follow up'], ['Alan Turing', 'alan@example.com', 'Bletchley', ''],
|
|
16
|
+
['', 'nameless@example.com', 'Unknown', ''], ['Katherine Johnson', 'katherine@example.com', 'NASA', ''], ['Ada Lovelace', 'ada@example.com', 'Analytical Ltd', 'Duplicate'],
|
|
17
|
+
['Hedy Lamarr', 'hedy@example.com', 'MGM', ''], ['Edsger Dijkstra', 'edsger@@example.com', 'TU Eindhoven', ''],
|
|
18
|
+
].map((r, i) => ({ id: 'r' + i, line: i + 2, c0: r[0], c1: r[1], c2: r[2], c3: r[3] }));
|
|
19
|
+
const problemsIn = (row, map) => {
|
|
20
|
+
const col = (t) => Object.keys(map).find((k) => map[k] === t);
|
|
21
|
+
const out = [];
|
|
22
|
+
const n = col('name'), e = col('email');
|
|
23
|
+
if (n && !String(row[n]).trim()) out.push({ key: n, message: 'Enter a name.' });
|
|
24
|
+
if (e && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(row[e]))) out.push({ key: e, message: 'This isn’t an email address.' });
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
function App() {
|
|
28
|
+
const [step, setStep] = React.useState(0);
|
|
29
|
+
const [queue, setQueue] = React.useState(/** @type {A.UploadItem[]} */ ([]));
|
|
30
|
+
const [map, setMap] = React.useState(/** @type {Record<string, string>} */ ({}));
|
|
31
|
+
const [rows, setRows] = React.useState(FILE);
|
|
32
|
+
const [onlyProblems, setOnlyProblems] = React.useState(false);
|
|
33
|
+
const [progress, setProgress] = React.useState(0);
|
|
34
|
+
const [cancelled, setCancelled] = React.useState(false);
|
|
35
|
+
const stop = React.useRef(false);
|
|
36
|
+
const heading = React.useRef(/** @type {HTMLHeadingElement | null} */ (null));
|
|
37
|
+
const go = (n) => { setStep(n); requestAnimationFrame(() => heading.current?.focus()); };
|
|
38
|
+
const upload = async (name) => {
|
|
39
|
+
setQueue([{ id: 'f', name, state: 'uploading', progress: 40 }]); await wait(300);
|
|
40
|
+
setQueue([{ id: 'f', name, state: 'uploading', progress: 100 }]); await wait(300);
|
|
41
|
+
setQueue([{ id: 'f', name, state: 'done' }]); await wait(300); go(1);
|
|
42
|
+
};
|
|
43
|
+
const missing = ['name', 'email'].filter((t) => !Object.values(map).includes(t)).map((t) => (t === 'name' ? 'Name' : 'Email'));
|
|
44
|
+
const mapped = SOURCE.filter((s) => map[s.key] && map[s.key] !== 'skip');
|
|
45
|
+
const bad = rows.filter((r) => problemsIn(r, map).length);
|
|
46
|
+
const problemCount = rows.reduce((n, r) => n + problemsIn(r, map).length, 0);
|
|
47
|
+
const emailCol = Object.keys(map).find((k) => map[k] === 'email');
|
|
48
|
+
const seen = new Set(), dupes = new Set();
|
|
49
|
+
rows.forEach((r) => { if (problemsIn(r, map).length || !emailCol) return; const e = String(r[emailCol]); if (seen.has(e)) dupes.add(r.id); seen.add(e); });
|
|
50
|
+
const importing = rows.filter((r) => !problemsIn(r, map).length && !dupes.has(r.id));
|
|
51
|
+
const run = async () => {
|
|
52
|
+
go(4); stop.current = false; setCancelled(false);
|
|
53
|
+
for (let i = 1; i <= importing.length; i++) { await wait(350); if (stop.current) { setCancelled(true); return; } setProgress(i); }
|
|
54
|
+
go(5);
|
|
55
|
+
};
|
|
56
|
+
const H = (text) => <h3 ref={heading} tabIndex={-1} className="im-sub">{text}</h3>;
|
|
57
|
+
return (
|
|
58
|
+
<main className="im-page">
|
|
59
|
+
<h2 className="im-h">Import contacts</h2>
|
|
60
|
+
<A.Stepper steps={STEPS} current={step} aria-label="Import steps" onStepChange={step < 4 ? (i) => go(i) : undefined} />
|
|
61
|
+
{step === 0 && <section className="im-step">{H('Upload a CSV file')}
|
|
62
|
+
<A.DropZone accept=".csv,text/csv" multiple={false} onFiles={(f) => f[0] && upload(f[0].name)} queue={queue}>
|
|
63
|
+
<p className="im-note">Drop a .csv file here or choose one. The first row should name the columns.</p>
|
|
64
|
+
</A.DropZone>
|
|
65
|
+
<div><A.Button onClick={() => upload('contacts.csv')}>Use the sample file</A.Button></div>
|
|
66
|
+
</section>}
|
|
67
|
+
{step === 1 && <section className="im-step">{H('Map columns to fields')}
|
|
68
|
+
<p className="im-note">Choose the field each column of contacts.csv goes into.</p>
|
|
69
|
+
<div className="im-map">{SOURCE.map((s) => <A.Field key={s.key} label={s.header} hint={`For example, “${s.sample}”`}>
|
|
70
|
+
<A.Select value={map[s.key] || ''} placeholder="Not mapped" onValueChange={(v) => setMap((m) => ({ ...m, [s.key]: v }))} items={TARGETS} />
|
|
71
|
+
</A.Field>)}</div>
|
|
72
|
+
<p className="im-note" role="status">{missing.length ? `Map these required fields: ${missing.join(', ')}.` : 'Every required field is mapped.'}</p>
|
|
73
|
+
<div><A.Button variant="primary" disabled={missing.length > 0} onClick={() => go(2)}>Continue</A.Button></div>
|
|
74
|
+
</section>}
|
|
75
|
+
{step === 2 && <section className="im-step">{H('Fix problems')}
|
|
76
|
+
<p className="im-note" role="status">{problemCount ? `${problemCount} ${problemCount === 1 ? 'problem' : 'problems'} left in ${bad.length} ${bad.length === 1 ? 'row' : 'rows'}. Rows with problems are skipped.` : 'No problems left.'}</p>
|
|
77
|
+
<A.Switch label="Only rows with problems" checked={onlyProblems} onCheckedChange={setOnlyProblems} />
|
|
78
|
+
<A.DataGrid aria-label="Rows to import" showInvalid rows={onlyProblems ? bad : rows} getRowId={(r) => r.id} getRowLabel={(r) => `row ${r.line}`}
|
|
79
|
+
columns={[{ key: 'line', header: 'Row', align: 'end', width: 64 }, ...mapped.map((s) => ({ key: s.key, header: TARGETS.find((t) => t.value === map[s.key])?.label.replace(' (required)', '') || s.header, editable: true,
|
|
80
|
+
validate: (v, row) => problemsIn({ ...row, [s.key]: v }, map).find((p) => p.key === s.key)?.message }))]}
|
|
81
|
+
onCellSave={({ rowId, key, draft }) => { setRows((all) => all.map((r) => (r.id === rowId ? { ...r, [key]: draft } : r))); return { ok: true }; }} />
|
|
82
|
+
<div className="im-actions"><A.Button onClick={() => go(1)}>Back to mapping</A.Button><A.Button variant="primary" onClick={() => go(3)}>Continue</A.Button></div>
|
|
83
|
+
</section>}
|
|
84
|
+
{step === 3 && <section className="im-step">{H('Review the import')}
|
|
85
|
+
<p className="im-lead">{importing.length} contacts will be imported. {dupes.size + bad.length} rows will be skipped: {dupes.size} {dupes.size === 1 ? 'duplicate' : 'duplicates'} and {bad.length} with problems.</p>
|
|
86
|
+
<div className="im-actions"><A.Button onClick={() => go(2)}>Fix more rows</A.Button><A.Button variant="primary" onClick={run}>Import {importing.length} contacts</A.Button></div>
|
|
87
|
+
</section>}
|
|
88
|
+
{step === 4 && <section className="im-step">{H(cancelled ? 'Import cancelled' : 'Importing contacts')}
|
|
89
|
+
{cancelled ? <><p className="im-lead">{progress} of {importing.length} contacts were imported before you cancelled; the rest weren’t. Importing again skips the ones already imported.</p><div><A.Button onClick={() => { setProgress(0); go(3); }}>Back to review</A.Button></div></> : <>
|
|
90
|
+
<A.Progress value={progress} max={importing.length} aria-label="Importing contacts" />
|
|
91
|
+
<p className="im-note" role="status">Importing {progress} of {importing.length}</p>
|
|
92
|
+
<div><A.Button onClick={() => { stop.current = true; }}>Cancel import</A.Button></div></>}
|
|
93
|
+
</section>}
|
|
94
|
+
{step === 5 && <section className="im-step">{H('Import finished')}
|
|
95
|
+
<dl className="im-receipt"><div><dt>Imported</dt><dd>{importing.length} contacts</dd></div><div><dt>Skipped</dt><dd>{dupes.size} {dupes.size === 1 ? 'duplicate' : 'duplicates'}</dd></div><div><dt>Failed</dt><dd>{bad.length} {bad.length === 1 ? 'row' : 'rows'} with problems</dd></div></dl>
|
|
96
|
+
<div className="im-actions"><A.Button>Download skipped rows (CSV)</A.Button><A.Button variant="ghost" onClick={() => { setQueue([]); setMap({}); setRows(FILE); setProgress(0); go(0); }}>Import another file</A.Button></div>
|
|
97
|
+
</section>}
|
|
98
|
+
</main>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
// preview:end
|
|
102
|
+
export default App;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Members pattern (project/framework/patterns/members.md): invite people by email with a role, see pending invitations,
|
|
2
|
+
// change someone's role, remove someone. An invitation awaiting acceptance is settled (ground, an "Invited" plate);
|
|
3
|
+
// only sending it hovers. Role changes apply now, per row. The only owner can't demote themselves, and the reason is
|
|
4
|
+
// said where the control would be. Removing someone names what they lose.
|
|
5
|
+
// The lines between the preview markers are its "MembersPattern" preview (scripts/pattern-previews.mjs copies them);
|
|
6
|
+
// build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if the two differ.
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import * as A from 'altitude-design';
|
|
9
|
+
// preview:start
|
|
10
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
const ROLES = [
|
|
12
|
+
{ value: 'admin', label: 'Admin', description: 'Manages members, billing and every project.' },
|
|
13
|
+
{ value: 'member', label: 'Member', description: 'Creates and edits projects they can see.' },
|
|
14
|
+
{ value: 'viewer', label: 'Viewer', description: 'Sees projects and leaves comments. Can’t edit.' },
|
|
15
|
+
];
|
|
16
|
+
const roleName = (v) => (v === 'owner' ? 'Owner' : ROLES.find((r) => r.value === v)?.label);
|
|
17
|
+
function MemberRow({ person, me, owners, fail, onRole, onRemove, onResend, onRevoke }) {
|
|
18
|
+
const [write, setWrite] = React.useState(/** @type {A.DepthProps['status']} */ (undefined));
|
|
19
|
+
const row = React.useRef(null);
|
|
20
|
+
const remove = A.useConsequentialAction({
|
|
21
|
+
target: person.name, consequence: `${person.name} loses access to ${person.projects} projects and every shared dashboard. Their work stays in the workspace.`,
|
|
22
|
+
requiresConfirmation: true, focusFallback: () => document.getElementById('mb-heading'),
|
|
23
|
+
execute: async () => { await wait(700); onRemove(person); return { ok: true, value: person.id }; },
|
|
24
|
+
messages: { pending: `Removing ${person.name}`, succeeded: `${person.name} was removed` },
|
|
25
|
+
});
|
|
26
|
+
const { style, ...depth } = A.useDepth({ status: write, settle: person.invitedAt ? true : undefined });
|
|
27
|
+
const changeRole = async (role) => {
|
|
28
|
+
const before = person.role; onRole(person.id, role); setWrite('pending');
|
|
29
|
+
await wait(800);
|
|
30
|
+
if (fail.current) { onRole(person.id, before); setWrite('failed'); } else setWrite('committed');
|
|
31
|
+
};
|
|
32
|
+
const onlyOwner = person.role === 'owner' && owners === 1;
|
|
33
|
+
return (
|
|
34
|
+
<A.TableRow ref={row} rowId={person.id} {...depth} style={style}>
|
|
35
|
+
<A.TableCell><div className="mb-who"><A.Avatar name={person.name} size={28} /><div><div className="mb-name">{person.name}{person.id === me && ' (you)'}</div><div className="mb-email">{person.email}</div></div></div></A.TableCell>
|
|
36
|
+
<A.TableCell>
|
|
37
|
+
{person.invited ? <span className="mb-invited"><A.Badge>Invited</A.Badge> {roleName(person.role)} · sent {person.sent}</span>
|
|
38
|
+
: onlyOwner ? <span className="mb-reason">Owner. You’re the only owner, so your role can’t change. Transfer ownership first.</span>
|
|
39
|
+
: <div className="mb-role"><A.Select aria-label={`Role for ${person.name}`} value={person.role} onValueChange={changeRole} items={ROLES.map(({ value, label }) => ({ value, label }))} />
|
|
40
|
+
<span className="mb-write" role="status">{write === 'pending' ? 'Changing role…' : write === 'failed' ? 'Couldn’t change the role. Try again.' : write === 'committed' ? 'Role changed' : ''}</span></div>}
|
|
41
|
+
</A.TableCell>
|
|
42
|
+
<A.TableCell align="end">
|
|
43
|
+
{person.invited ? <div className="mb-actions"><A.Button size="sm" variant="ghost" onClick={() => onResend(person)}>Resend</A.Button><A.Button size="sm" variant="ghost" onClick={() => onRevoke(person)}>Revoke</A.Button></div>
|
|
44
|
+
: onlyOwner || person.id === me ? null
|
|
45
|
+
: <A.Button size="sm" variant="ghost" onClick={remove.request}>Remove</A.Button>}
|
|
46
|
+
<A.Dialog open={['confirming', 'pending', 'failed'].includes(remove.phase)} background="stationary" size="sm" dismissible={remove.phase !== 'pending'}
|
|
47
|
+
onOpenChange={(o) => { if (!o) remove.cancel(); }} title={`Remove ${person.name}?`} description={remove.consequence}
|
|
48
|
+
footer={<><A.Button disabled={remove.phase === 'pending'} onClick={remove.cancel}>Keep {person.name.split(' ')[0]}</A.Button><A.Button variant="danger" disabled={remove.phase === 'pending'} onClick={remove.confirm}>{remove.phase === 'pending' ? 'Removing' : `Remove ${person.name.split(' ')[0]}`}</A.Button></>} />
|
|
49
|
+
</A.TableCell>
|
|
50
|
+
</A.TableRow>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function App() {
|
|
54
|
+
const me = 'u1';
|
|
55
|
+
const [people, setPeople] = React.useState([
|
|
56
|
+
{ id: 'u1', name: 'Maya Chen', email: 'maya@acme.dev', role: 'owner', projects: 14 },
|
|
57
|
+
{ id: 'u2', name: 'Theo Marsh', email: 'theo@acme.dev', role: 'admin', projects: 9 },
|
|
58
|
+
{ id: 'u3', name: 'Ines Duarte', email: 'ines@acme.dev', role: 'member', projects: 4 },
|
|
59
|
+
{ id: 'i1', name: 'sam@acme.dev', email: 'Invitation', role: 'member', invited: true, sent: 'yesterday', projects: 0 },
|
|
60
|
+
]);
|
|
61
|
+
const [emails, setEmails] = React.useState(/** @type {string[]} */ ([]));
|
|
62
|
+
const [role, setRole] = React.useState('member');
|
|
63
|
+
const [sending, setSending] = React.useState(false);
|
|
64
|
+
const [status, setStatus] = React.useState('');
|
|
65
|
+
const [failNext, setFailNext] = React.useState(false);
|
|
66
|
+
const fail = React.useRef(false); fail.current = failNext;
|
|
67
|
+
const owners = people.filter((p) => p.role === 'owner').length;
|
|
68
|
+
const send = async () => {
|
|
69
|
+
setSending(true); setStatus('Sending invites…');
|
|
70
|
+
await wait(900);
|
|
71
|
+
const known = new Set(people.map((p) => p.name).concat(people.map((p) => p.email)));
|
|
72
|
+
const fresh = emails.filter((e) => !known.has(e)), already = emails.length - fresh.length;
|
|
73
|
+
const at = Date.now();
|
|
74
|
+
setPeople((list) => list.concat(fresh.map((e, i) => ({ id: 'i' + at + i, name: e, email: 'Invitation', role, invited: true, sent: 'just now', invitedAt: at, projects: 0 }))));
|
|
75
|
+
setEmails([]); setSending(false);
|
|
76
|
+
setStatus([fresh.length && `${fresh.length} ${fresh.length === 1 ? 'invite' : 'invites'} sent`, already && `${already} ${already === 1 ? 'address is' : 'addresses are'} already in the workspace`].filter(Boolean).join(', ') + '.');
|
|
77
|
+
};
|
|
78
|
+
return (
|
|
79
|
+
<main className="mb-page">
|
|
80
|
+
<h2 id="mb-heading" tabIndex={-1} className="mb-h">Members</h2>
|
|
81
|
+
<A.Card className="mb-invite" aria-labelledby="mb-invite-h">
|
|
82
|
+
<h3 id="mb-invite-h" className="mb-sub">Invite people</h3>
|
|
83
|
+
<A.Field label="Email addresses" hint="Separate addresses with commas, or paste a list.">
|
|
84
|
+
<A.TagInput value={emails} onValueChange={setEmails} noun={['address', 'addresses']} placeholder="name@acme.dev" disabled={sending}
|
|
85
|
+
normalize={(t) => t.trim().toLowerCase()} validate={(t) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t) ? undefined : `${t} isn’t an email address.`)} />
|
|
86
|
+
</A.Field>
|
|
87
|
+
<div className="mb-roles"><span id="mb-role-label" className="mb-legend">Role</span>
|
|
88
|
+
<A.RadioGroup value={role} onValueChange={setRole} aria-labelledby="mb-role-label">{ROLES.map((r) => <A.Radio key={r.value} value={r.value} label={r.label} description={r.description} />)}</A.RadioGroup>
|
|
89
|
+
</div>
|
|
90
|
+
<div><A.Button variant="primary" loading={sending} disabled={!emails.length} onClick={send}>{sending ? 'Sending invites' : emails.length > 1 ? `Send ${emails.length} invites` : 'Send invite'}</A.Button></div>
|
|
91
|
+
</A.Card>
|
|
92
|
+
<A.Switch label="Make the next role change fail" checked={failNext} onCheckedChange={setFailNext} />
|
|
93
|
+
<A.Table aria-label="Members and invitations">
|
|
94
|
+
<A.TableHeader><A.TableRow><A.TableHead>Person</A.TableHead><A.TableHead>Role</A.TableHead><A.TableHead align="end"><span className="mb-vh">Actions</span></A.TableHead></A.TableRow></A.TableHeader>
|
|
95
|
+
<A.TableBody>{people.map((p) => <MemberRow key={p.id} person={p} me={me} owners={owners} fail={fail}
|
|
96
|
+
onRole={(id, r) => setPeople((l) => l.map((x) => (x.id === id ? { ...x, role: r } : x)))}
|
|
97
|
+
onRemove={(x) => { setPeople((l) => l.filter((y) => y.id !== x.id)); setStatus(`${x.name} was removed.`); }}
|
|
98
|
+
onResend={(x) => setStatus(`Invite resent to ${x.name}.`)}
|
|
99
|
+
onRevoke={(x) => { setPeople((l) => l.filter((y) => y.id !== x.id)); setStatus(`Invitation to ${x.name} revoked.`); document.getElementById('mb-heading')?.focus(); }} />)}</A.TableBody>
|
|
100
|
+
</A.Table>
|
|
101
|
+
<A.WorkflowStatus>{status}</A.WorkflowStatus>
|
|
102
|
+
</main>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
// preview:end
|
|
106
|
+
export default App;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Onboarding pattern (project/framework/patterns/onboarding.md): a setup checklist whose tasks are stamped done by the
|
|
2
|
+
// app when they actually happen (never by clicking the item), with progress as a Meter; empty regions open with their
|
|
3
|
+
// one action instead of a tour; a short tour only when someone asks for it; "What's new" with a count only for unseen
|
|
4
|
+
// items. Every surface can be dismissed and found again from Help.
|
|
5
|
+
// The lines between the preview markers are its "OnboardingPattern" preview (scripts/pattern-previews.mjs copies them);
|
|
6
|
+
// build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if the two differ.
|
|
7
|
+
import * as React from 'react';
|
|
8
|
+
import * as A from 'altitude-design';
|
|
9
|
+
// preview:start
|
|
10
|
+
const TASKS = [
|
|
11
|
+
{ id: 'project', title: 'Create a project', about: 'Somewhere to keep your work.' },
|
|
12
|
+
{ id: 'invite', title: 'Invite your team', about: 'Work goes faster together.' },
|
|
13
|
+
{ id: 'connect', title: 'Connect a data source', about: 'Bring in what you already have.', action: 'Connect a data source' },
|
|
14
|
+
{ id: 'billing', title: 'Add billing details', about: 'Needed before your trial ends on 14 November.', action: 'Add billing details' },
|
|
15
|
+
];
|
|
16
|
+
function App() {
|
|
17
|
+
const [done, setDone] = React.useState(/** @type {Record<string, boolean>} */ ({ project: true, invite: true }));
|
|
18
|
+
const [guide, setGuide] = React.useState(true);
|
|
19
|
+
const [sources, setSources] = React.useState(/** @type {string[]} */ ([]));
|
|
20
|
+
const [tour, setTour] = React.useState(false);
|
|
21
|
+
const [seen, setSeen] = React.useState(false);
|
|
22
|
+
const count = TASKS.filter((t) => done[t.id]).length;
|
|
23
|
+
const happen = (id) => setDone((d) => ({ ...d, [id]: true }));
|
|
24
|
+
return (
|
|
25
|
+
<main className="ob-page">
|
|
26
|
+
<header className="ob-head"><h2 className="ob-h">Acme workspace</h2>
|
|
27
|
+
<div className="ob-tools">
|
|
28
|
+
<A.Popover aria-label="What’s new" align="end" onOpenChange={(o) => { if (o) setSeen(true); }}
|
|
29
|
+
trigger={<A.Button size="sm" variant="ghost" id="ob-new">What’s new{!seen && <A.Badge variant="count" aria-label="2 unseen">2</A.Badge>}</A.Button>}>
|
|
30
|
+
<div className="ob-news"><A.Timeline aria-label="Recent changes" groupBy="none" locale="en-IE">
|
|
31
|
+
<A.TimelineItem key="n1" time="2026-10-28T09:00:00Z" isNew={!seen}>Saved views: keep a filter and come back to it</A.TimelineItem>
|
|
32
|
+
<A.TimelineItem key="n2" time="2026-10-21T09:00:00Z" isNew={!seen}>The sidebar now groups projects by team</A.TimelineItem>
|
|
33
|
+
<A.TimelineItem key="n3" time="2026-10-07T09:00:00Z">Export any table as CSV</A.TimelineItem>
|
|
34
|
+
</A.Timeline></div>
|
|
35
|
+
</A.Popover>
|
|
36
|
+
<A.Button size="sm" variant="ghost" id="ob-help" onClick={() => setGuide(true)}>Help: setup guide</A.Button>
|
|
37
|
+
</div>
|
|
38
|
+
</header>
|
|
39
|
+
{guide && <A.Card className="ob-guide" aria-labelledby="ob-guide-h">
|
|
40
|
+
<div className="ob-row"><h3 id="ob-guide-h" className="ob-sub">Set up your workspace</h3>
|
|
41
|
+
<A.Button size="sm" variant="ghost" onClick={() => setGuide(false)}>{count === TASKS.length ? 'Hide setup guide' : 'Skip for now'}</A.Button></div>
|
|
42
|
+
<A.Meter label="Setup" value={count} max={TASKS.length} format={(v) => `${v}`} />
|
|
43
|
+
<p className="ob-note">Tasks are checked off when you do them, wherever you do them.</p>
|
|
44
|
+
<ol className="ob-tasks">{TASKS.map((t) => <li key={t.id} className="ob-task" data-done={done[t.id] ? '' : undefined}>
|
|
45
|
+
<div><div className="ob-title">{t.title}</div><div className="ob-note">{t.about}</div></div>
|
|
46
|
+
{done[t.id] ? <A.Badge tone="positive">Done</A.Badge> : t.action ? <A.Button size="sm" onClick={() => document.getElementById('ob-' + t.id)?.scrollIntoView({ block: 'center' })}>{t.action}</A.Button> : null}
|
|
47
|
+
</li>)}</ol>
|
|
48
|
+
</A.Card>}
|
|
49
|
+
{!guide && <p className="ob-note" role="status">Setup guide hidden. Open it again any time from Help.</p>}
|
|
50
|
+
<section className="ob-section" aria-labelledby="ob-sources-h" id="ob-connect">
|
|
51
|
+
<h3 id="ob-sources-h" className="ob-sub">Data sources</h3>
|
|
52
|
+
{sources.length ? <ul className="ob-list">{sources.map((s) => <li key={s}>{s}</li>)}</ul>
|
|
53
|
+
: <A.EmptyState size="sm" title="No data sources yet" description="Connect a database or a spreadsheet and its tables appear here."
|
|
54
|
+
action={<A.Button variant="primary" onClick={() => { setSources(['Sales database']); happen('connect'); }}>Connect a data source</A.Button>} />}
|
|
55
|
+
</section>
|
|
56
|
+
<section className="ob-section" aria-labelledby="ob-billing-h" id="ob-billing">
|
|
57
|
+
<h3 id="ob-billing-h" className="ob-sub">Billing</h3>
|
|
58
|
+
{done.billing ? <p className="ob-note">Card ending 4242 on file.</p> : <div><A.Button onClick={() => happen('billing')}>Add billing details</A.Button></div>}
|
|
59
|
+
</section>
|
|
60
|
+
<nav aria-label="Projects" className="ob-nav" id="ob-sidebar"><span className="ob-note">Design team</span><A.Link href="#/p/1">Website refresh</A.Link><A.Link href="#/p/2">Brand kit</A.Link></nav>
|
|
61
|
+
<div><A.Button size="sm" variant="ghost" onClick={() => setTour(true)}>Show me what changed in the sidebar</A.Button></div>
|
|
62
|
+
<A.Tour open={tour} onOpenChange={setTour} steps={[
|
|
63
|
+
{ target: '#ob-sidebar', title: 'Projects are grouped by team', body: 'Each team’s projects sit under its name. Drag a project to move it.' },
|
|
64
|
+
{ target: '#ob-new', title: 'Find changes here', body: 'What’s new lists every change, so you can come back to this later.' },
|
|
65
|
+
]} />
|
|
66
|
+
</main>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
// preview:end
|
|
70
|
+
export default App;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Screen layout pattern (project/framework/patterns/screen-layout.md), the decision frame.
|
|
2
|
+
// Task: a billing lead decides whether to refund a customer's October invoice.
|
|
3
|
+
// Lead: the choice and its consequence. Primary action: Approve refund. Next: the next request in the queue.
|
|
4
|
+
// The question is the title; the consequence in words and the two choices sit straight under it, before the evidence.
|
|
5
|
+
// Declining asks for its reason in place; Cancel returns focus to Decline. Once decided, the outcome replaces the choice
|
|
6
|
+
// and takes focus. The lines between the preview markers are its "ScreenLayoutDecision" preview
|
|
7
|
+
// (scripts/pattern-previews.mjs copies them); build/checks/pattern-types.mjs type-checks this file against the public
|
|
8
|
+
// declarations and fails if the two differ.
|
|
9
|
+
// preview-name: ScreenLayoutDecision
|
|
10
|
+
import * as React from 'react';
|
|
11
|
+
import * as A from 'altitude-design';
|
|
12
|
+
// preview:start
|
|
13
|
+
function App() {
|
|
14
|
+
const [phase, setPhase] = React.useState(/** @type {'deciding' | 'declining' | 'sending' | 'approved' | 'declined'} */ ('deciding'));
|
|
15
|
+
const [reason, setReason] = React.useState('');
|
|
16
|
+
const [reasonError, setReasonError] = React.useState('');
|
|
17
|
+
const declineRef = React.useRef(/** @type {HTMLButtonElement | null} */ (null));
|
|
18
|
+
const outcome = React.useRef(/** @type {HTMLDivElement | null} */ (null));
|
|
19
|
+
const [sending, setSending] = React.useState(/** @type {'approved' | 'declined' | null} */ (null));
|
|
20
|
+
const decide = (result) => {
|
|
21
|
+
if (result === 'declined' && !reason.trim()) { setReasonError('Say why, so Support can tell Northwind.'); document.getElementById('ld-reason')?.focus(); return; }
|
|
22
|
+
setSending(result); setPhase('sending');
|
|
23
|
+
window.setTimeout(() => { setPhase(result); requestAnimationFrame(() => outcome.current?.focus()); }, 600);
|
|
24
|
+
};
|
|
25
|
+
const cancel = () => { setPhase('deciding'); setReasonError(''); requestAnimationFrame(() => declineRef.current?.focus()); };
|
|
26
|
+
const done = phase === 'approved' || phase === 'declined';
|
|
27
|
+
return (
|
|
28
|
+
<main className="ld-page">
|
|
29
|
+
<A.PageHeader breadcrumb={<A.Breadcrumb items={[{ label: 'Refund requests', href: '#/refunds' }, { label: 'R-1042' }]} />}>
|
|
30
|
+
<A.PageHeaderTitle>Refund €420 to Northwind?</A.PageHeaderTitle>
|
|
31
|
+
<A.PageHeaderDescription>Asked by Jon Park in Support, today at 09:12.</A.PageHeaderDescription>
|
|
32
|
+
</A.PageHeader>
|
|
33
|
+
{done ? (
|
|
34
|
+
<div className="ld-choice" role="status" tabIndex={-1} ref={outcome}>
|
|
35
|
+
<p className="ld-consequence">{phase === 'approved' ? 'Refund approved. €420 goes back to the card ending 4242 in 5–10 days, and the October invoice is marked refunded.' : 'Refund declined. Jon has your reason to send to Northwind.'}</p>
|
|
36
|
+
<div><A.Link href="#/refunds/R-1043">Next request: Halcyon, €96 (2 more waiting)</A.Link></div>
|
|
37
|
+
</div>
|
|
38
|
+
) : (
|
|
39
|
+
<section className="ld-choice" aria-labelledby="ld-choice-h">
|
|
40
|
+
<h2 id="ld-choice-h" className="ld-sr">Your decision</h2>
|
|
41
|
+
<p className="ld-consequence">Approving sends €420 back to Northwind’s card ending 4242 in 5–10 days and marks their October invoice refunded. A refund can’t be taken back once it’s sent.</p>
|
|
42
|
+
{phase === 'declining' || (phase === 'sending' && sending === 'declined') ? <>
|
|
43
|
+
<A.Field label="Why decline?" hint="Jon sends this to Northwind." error={reasonError || undefined}>
|
|
44
|
+
<A.Textarea id="ld-reason" rows={3} value={reason} onChange={(e) => { setReason(e.target.value); setReasonError(''); }}
|
|
45
|
+
onKeyDown={(e) => { if (e.key === 'Escape') cancel(); }} />
|
|
46
|
+
</A.Field>
|
|
47
|
+
<div className="ld-actions">
|
|
48
|
+
<A.Button variant="primary" loading={phase === 'sending'} onClick={() => decide('declined')}>Send decline</A.Button>
|
|
49
|
+
<A.Button variant="ghost" onClick={cancel} disabled={phase === 'sending'}>Cancel</A.Button>
|
|
50
|
+
</div>
|
|
51
|
+
</> : (
|
|
52
|
+
<div className="ld-actions">
|
|
53
|
+
<A.Button variant="primary" loading={phase === 'sending'} onClick={() => decide('approved')}>Approve refund</A.Button>
|
|
54
|
+
<A.Button ref={declineRef} onClick={() => { setPhase('declining'); requestAnimationFrame(() => document.getElementById('ld-reason')?.focus()); }} disabled={phase === 'sending'}>Decline</A.Button>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</section>
|
|
58
|
+
)}
|
|
59
|
+
<section className="ld-evidence" aria-labelledby="ld-why-h">
|
|
60
|
+
<h2 id="ld-why-h" className="ld-sub">Why they’re asking</h2>
|
|
61
|
+
<blockquote className="ld-quote">“We moved to annual billing on 2 October, but the monthly invoice still went out. We were charged twice for October.”</blockquote>
|
|
62
|
+
<A.PropertyList layout="inline" aria-label="The invoice">
|
|
63
|
+
<A.PropertyListItem label="Invoice" value="INV-2026-1017, 1 October" />
|
|
64
|
+
<A.PropertyListItem label="Amount" value="€420.00" />
|
|
65
|
+
<A.PropertyListItem label="Annual plan" value="Started 2 October, paid €4,536" />
|
|
66
|
+
<A.PropertyListItem label="Customer since" value="March 2023" />
|
|
67
|
+
<A.PropertyListItem label="Refunds before" value="None" />
|
|
68
|
+
</A.PropertyList>
|
|
69
|
+
</section>
|
|
70
|
+
</main>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
// preview:end
|
|
74
|
+
export default App;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Screen layout pattern (project/framework/patterns/screen-layout.md), the focused editor frame.
|
|
2
|
+
// Task: a product manager writes this week's release note and publishes it.
|
|
3
|
+
// Lead: the note being written. Primary action: Publish. Next: the published note's place on the changelog.
|
|
4
|
+
// No navigation sidebar, a breadcrumb back; the draft fills the column with its details beside it, narrower and quieter;
|
|
5
|
+
// saving is a status in the header, not a second button. The lines between the preview markers are its
|
|
6
|
+
// "ScreenLayoutEditor" preview (scripts/pattern-previews.mjs copies them); build/checks/pattern-types.mjs type-checks
|
|
7
|
+
// this file against the public declarations and fails if the two differ.
|
|
8
|
+
// preview-name: ScreenLayoutEditor
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import * as A from 'altitude-design';
|
|
11
|
+
// preview:start
|
|
12
|
+
const AUDIENCES = [{ value: 'everyone', label: 'Everyone' }, { value: 'admins', label: 'Workspace admins' }];
|
|
13
|
+
function App() {
|
|
14
|
+
const [title, setTitle] = React.useState('Saved views, and a faster sidebar');
|
|
15
|
+
const [body, setBody] = React.useState('You can now save a filtered view of any table and come back to it from the sidebar.\n\nThe sidebar also opens twice as fast in large workspaces.');
|
|
16
|
+
const [audience, setAudience] = React.useState('everyone');
|
|
17
|
+
const [sync, setSync] = React.useState(/** @type {A.SyncState} */ ('saved'));
|
|
18
|
+
const [titleError, setTitleError] = React.useState('');
|
|
19
|
+
const [phase, setPhase] = React.useState(/** @type {'draft' | 'publishing' | 'published'} */ ('draft'));
|
|
20
|
+
const timer = React.useRef(0);
|
|
21
|
+
const outcome = React.useRef(/** @type {HTMLParagraphElement | null} */ (null));
|
|
22
|
+
const edit = (set) => (e) => { set(e.target.value); setSync('saving'); clearTimeout(timer.current); timer.current = window.setTimeout(() => setSync('saved'), 700); };
|
|
23
|
+
const publish = () => {
|
|
24
|
+
if (!title.trim()) { setTitleError('Give the note a title.'); document.getElementById('le-title')?.focus(); return; }
|
|
25
|
+
setPhase('publishing');
|
|
26
|
+
window.setTimeout(() => { setPhase('published'); requestAnimationFrame(() => outcome.current?.focus()); }, 600);
|
|
27
|
+
};
|
|
28
|
+
return (
|
|
29
|
+
<main className="le-page">
|
|
30
|
+
<A.PageHeader breadcrumb={<A.Breadcrumb items={[{ label: 'Release notes', href: '#/notes' }, { label: 'Week 44' }]} />}
|
|
31
|
+
actions={<A.SyncIndicator state={sync} />}
|
|
32
|
+
primary={phase === 'published' ? undefined : <A.Button variant="primary" loading={phase === 'publishing'} onClick={publish}>Publish</A.Button>}>
|
|
33
|
+
<A.PageHeaderTitle>Week 44 release note</A.PageHeaderTitle>
|
|
34
|
+
</A.PageHeader>
|
|
35
|
+
{phase === 'published' && <p className="le-outcome" role="status" tabIndex={-1} ref={outcome}>Published. Week 44 is at the top of the changelog and in everyone’s What’s new. <A.Link href="#/changelog">View the changelog</A.Link></p>}
|
|
36
|
+
<div className="le-body">
|
|
37
|
+
<section className="le-draft" aria-label="Draft">
|
|
38
|
+
<A.Field label="Title" error={titleError || undefined}>
|
|
39
|
+
<A.Input id="le-title" value={title} readOnly={phase !== 'draft'} onChange={(e) => { setTitleError(''); edit(setTitle)(e); }} />
|
|
40
|
+
</A.Field>
|
|
41
|
+
<A.Field label="Note">
|
|
42
|
+
<A.Textarea className="le-text" rows={14} value={body} readOnly={phase !== 'draft'} onChange={edit(setBody)} />
|
|
43
|
+
</A.Field>
|
|
44
|
+
</section>
|
|
45
|
+
<aside className="le-side" aria-labelledby="le-side-h">
|
|
46
|
+
<h2 id="le-side-h" className="le-sub">Details</h2>
|
|
47
|
+
<A.Field label="Who sees it"><A.Select items={AUDIENCES} value={audience} onValueChange={(v) => setAudience(String(v))} disabled={phase !== 'draft'} /></A.Field>
|
|
48
|
+
<p className="le-note">Publishing adds it to the changelog and to What’s new for {audience === 'everyone' ? 'everyone' : 'workspace admins'}.</p>
|
|
49
|
+
<p className="le-note">Last week: “Export any table as CSV”.</p>
|
|
50
|
+
</aside>
|
|
51
|
+
</div>
|
|
52
|
+
</main>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
// preview:end
|
|
56
|
+
export default App;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Screen layout pattern (project/framework/patterns/screen-layout.md), the report frame.
|
|
2
|
+
// Task: the growth team reads how October's signups went after the pricing change.
|
|
3
|
+
// Lead: a chart whose takeaway is the heading. Primary action: none (reading is the task). Next: the cohort report.
|
|
4
|
+
// The takeaway is written from the data shown; the secondary numbers follow, smaller and grouped, never a row of equal
|
|
5
|
+
// cards above the chart. The lines between the preview markers are its "ScreenLayoutReport" preview
|
|
6
|
+
// (scripts/pattern-previews.mjs copies them); build/checks/pattern-types.mjs type-checks this file against the public
|
|
7
|
+
// declarations and fails if the two differ.
|
|
8
|
+
// preview-name: ScreenLayoutReport
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import * as A from 'altitude-design';
|
|
11
|
+
// preview:start
|
|
12
|
+
// Daily signups for October 2026: about 40 a day, then about 80 a day from the pricing change on the 14th.
|
|
13
|
+
const SIGNUPS = Array.from({ length: 31 }, (_, i) => ({ day: `2026-10-${String(i + 1).padStart(2, '0')}`, signups: (i < 13 ? 41 : 82) + Math.round(4 * Math.sin(i * 2.3)) - (new Date(Date.UTC(2026, 9, i + 1)).getUTCDay() % 6 === 0 ? 6 : 0) }));
|
|
14
|
+
const total = SIGNUPS.reduce((n, d) => n + d.signups, 0);
|
|
15
|
+
const before = SIGNUPS.slice(0, 13), after = SIGNUPS.slice(13);
|
|
16
|
+
const mean = (rows) => Math.round(rows.reduce((n, d) => n + d.signups, 0) / rows.length);
|
|
17
|
+
function App() {
|
|
18
|
+
return (
|
|
19
|
+
<main className="lr-page">
|
|
20
|
+
<A.PageHeader breadcrumb={<A.Breadcrumb items={[{ label: 'Reports', href: '#/reports' }, { label: 'Signups' }]} />}
|
|
21
|
+
actions={<A.Button variant="secondary">Export CSV</A.Button>}>
|
|
22
|
+
<A.PageHeaderTitle>Signups, October 2026</A.PageHeaderTitle>
|
|
23
|
+
</A.PageHeader>
|
|
24
|
+
<A.ChartContainer className="lr-lead" title="Daily signups" takeaway={`Signups doubled after the pricing change on 14 October, from ${mean(before)} a day to ${mean(after)}`}
|
|
25
|
+
config={{ signups: { label: 'Signups' } }} aspect={2.6} zero
|
|
26
|
+
summary={`${total.toLocaleString('en-IE')} signups in October. From 1 to 13 October about ${mean(before)} a day; from 14 October, when the free plan gained a third project, about ${mean(after)} a day.`}>
|
|
27
|
+
<A.LineChart data={SIGNUPS} x="day" />
|
|
28
|
+
</A.ChartContainer>
|
|
29
|
+
<section className="lr-more" aria-labelledby="lr-more-h">
|
|
30
|
+
<h2 id="lr-more-h" className="lr-sub">Around it</h2>
|
|
31
|
+
<A.StatGroup aria-label="October in numbers">
|
|
32
|
+
<A.Stat label="Signups in October" value={total} change={{ value: 0.41, vs: 'September' }} good="up" locale="en-IE" />
|
|
33
|
+
<A.Stat label="Became paying within 7 days" value="6.1%" help="5.8% in September" />
|
|
34
|
+
<A.Stat label="Cancelled in their first week" value={38} change={{ value: -0.12, vs: 'September' }} good="down" locale="en-IE" />
|
|
35
|
+
</A.StatGroup>
|
|
36
|
+
<p className="lr-note">Paid conversion held while signups doubled, so the new free-plan signups aren’t lower-intent so far. <A.Link href="#/reports/cohorts">See the October cohort</A.Link></p>
|
|
37
|
+
</section>
|
|
38
|
+
</main>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
// preview:end
|
|
42
|
+
export default App;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Settings pattern (project/framework/patterns/settings.md): a workspace settings page. General fields are saved
|
|
2
|
+
// together as one draft with a SaveBar; notification switches apply immediately, each reporting its own write; billing
|
|
3
|
+
// is read-only for this person, with the reason in words; the danger zone deletes through a typed confirmation.
|
|
4
|
+
// The lines between the preview markers are its "SettingsPattern" preview (scripts/pattern-previews.mjs copies them);
|
|
5
|
+
// build/checks/pattern-types.mjs type-checks this file against the public declarations and fails if the two differ.
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import * as A from 'altitude-design';
|
|
8
|
+
// preview:start
|
|
9
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
function App() {
|
|
11
|
+
const [failNext, setFailNext] = React.useState(false);
|
|
12
|
+
const fail = React.useRef(false); fail.current = failNext;
|
|
13
|
+
const [saved, setSaved] = React.useState({ name: 'Acme', url: 'acme' });
|
|
14
|
+
const general = A.useEditRecord({
|
|
15
|
+
initialValue: saved, validationMode: 'submit',
|
|
16
|
+
validate: (d) => ({ ...(d.name.trim() ? {} : { name: 'Enter a workspace name.' }), ...(/^[a-z0-9-]+$/.test(d.url) ? {} : { url: 'Use lowercase letters, numbers and hyphens.' }) }),
|
|
17
|
+
save: async (draft) => { await wait(800); if (fail.current) return { ok: false, error: 'We couldn’t reach the server. Your changes are still here.' }; setSaved(draft); return { ok: true, value: draft }; },
|
|
18
|
+
});
|
|
19
|
+
const [prefs, setPrefs] = React.useState({ digest: true, mentions: true });
|
|
20
|
+
const [writes, setWrites] = React.useState({ digest: undefined, mentions: undefined });
|
|
21
|
+
const apply = async (key, value) => {
|
|
22
|
+
setPrefs((p) => ({ ...p, [key]: value })); setWrites((w) => ({ ...w, [key]: 'pending' }));
|
|
23
|
+
await wait(900);
|
|
24
|
+
if (fail.current) { setPrefs((p) => ({ ...p, [key]: !value })); setWrites((w) => ({ ...w, [key]: 'failed' })); }
|
|
25
|
+
else setWrites((w) => ({ ...w, [key]: 'committed' }));
|
|
26
|
+
};
|
|
27
|
+
const [deleted, setDeleted] = React.useState(false);
|
|
28
|
+
const [typed, setTyped] = React.useState('');
|
|
29
|
+
const heading = React.useRef(null);
|
|
30
|
+
const remove = A.useConsequentialAction({
|
|
31
|
+
target: 'Acme workspace', consequence: 'Delete Acme, its 12 projects and 1,204 files for all 8 members. This can’t be undone.',
|
|
32
|
+
requiresConfirmation: true, focusFallback: () => heading.current,
|
|
33
|
+
execute: async () => { await wait(900); if (fail.current) return { ok: false, error: 'We couldn’t delete the workspace. Nothing was removed. Try again.' }; setDeleted(true); return { ok: true, value: 'acme' }; },
|
|
34
|
+
messages: { pending: 'Deleting Acme workspace', succeeded: 'Acme workspace deleted' },
|
|
35
|
+
});
|
|
36
|
+
const confirming = ['confirming', 'pending', 'failed'].includes(remove.phase);
|
|
37
|
+
const field = (key, label, hint) => <A.Field label={label} hint={hint} error={general.errors[key]}><A.Input id={'ws-' + key} value={general.draft[key]} onChange={(e) => general.setField(key, e.target.value)} /></A.Field>;
|
|
38
|
+
if (deleted) return <main className="st-page"><h2 ref={heading} tabIndex={-1} className="st-h">Workspace deleted</h2><p>Acme and everything in it were deleted.</p><A.WorkflowStatus>{remove.status}</A.WorkflowStatus></main>;
|
|
39
|
+
return (
|
|
40
|
+
<main className="st-page">
|
|
41
|
+
<h2 ref={heading} tabIndex={-1} className="st-h">Workspace settings</h2>
|
|
42
|
+
<nav aria-label="Settings sections" className="st-nav"><a href="#st-general">General</a><a href="#st-notify">Notifications</a><a href="#st-billing">Billing</a><a href="#st-danger">Danger zone</a></nav>
|
|
43
|
+
<A.Switch label="Make the next change fail" checked={failNext} onCheckedChange={setFailNext} />
|
|
44
|
+
<A.SettingsSection id="st-general" title="General" description="Saved together: change what you need, then save once.">
|
|
45
|
+
<form className="st-form" noValidate onSubmit={(e) => { e.preventDefault(); void general.submit(); }}>
|
|
46
|
+
<A.ErrorSummary errors={Object.entries(general.errors).map(([k, message]) => ({ id: 'ws-' + k, label: k === 'name' ? 'Workspace name' : 'Workspace URL', message }))} focusKey={general.submitCount} />
|
|
47
|
+
{field('name', 'Workspace name', 'Shown to everyone in the workspace.')}
|
|
48
|
+
{field('url', 'Workspace URL', 'altitude.app/' + general.draft.url)}
|
|
49
|
+
</form>
|
|
50
|
+
</A.SettingsSection>
|
|
51
|
+
<A.SettingsSection id="st-notify" title="Notifications" description="Each change applies as soon as you make it.">
|
|
52
|
+
<A.SettingsRow label="Weekly digest" description="A summary of activity every Monday." status={writes.digest}><A.Switch checked={prefs.digest} onCheckedChange={(v) => apply('digest', v)} /></A.SettingsRow>
|
|
53
|
+
<A.SettingsRow label="Mentions" description="Email me when someone mentions me." status={writes.mentions}><A.Switch checked={prefs.mentions} onCheckedChange={(v) => apply('mentions', v)} /></A.SettingsRow>
|
|
54
|
+
</A.SettingsSection>
|
|
55
|
+
<A.SettingsSection id="st-billing" title="Billing" readOnlyReason="Only workspace owners can change billing details. Ask Maya Chen.">
|
|
56
|
+
<A.SettingsRow label="Billing email" description="Invoices and receipts go here."><A.Input defaultValue="finance@acme.dev" /></A.SettingsRow>
|
|
57
|
+
</A.SettingsSection>
|
|
58
|
+
<A.SettingsSection id="st-danger" title="Danger zone" tone="critical">
|
|
59
|
+
<A.SettingsRow label="Delete workspace" description={remove.consequence}><A.Button variant="danger" onClick={remove.request}>Delete workspace</A.Button></A.SettingsRow>
|
|
60
|
+
</A.SettingsSection>
|
|
61
|
+
<A.WorkflowStatus>{general.status || remove.status}</A.WorkflowStatus>
|
|
62
|
+
<A.SaveBar record={general} />
|
|
63
|
+
<A.Dialog open={confirming} background="stationary" dismissible={remove.phase !== 'pending'} onOpenChange={(o) => { if (!o) { remove.cancel(); setTyped(''); } }} returnFocus={() => heading.current}
|
|
64
|
+
title="Delete Acme workspace?" description={remove.consequence}
|
|
65
|
+
footer={<><A.Button disabled={remove.phase === 'pending'} onClick={() => { remove.cancel(); setTyped(''); }}>Keep workspace</A.Button><A.Button variant="danger" disabled={typed !== 'Acme' || remove.phase === 'pending'} onClick={remove.phase === 'failed' ? remove.retry : remove.confirm}>{remove.phase === 'pending' ? 'Deleting workspace' : remove.phase === 'failed' ? 'Try deleting again' : 'Delete workspace'}</A.Button></>}>
|
|
66
|
+
<A.Field label="Type Acme to confirm"><A.Input value={typed} onChange={(e) => setTyped(e.target.value)} autoComplete="off" /></A.Field>
|
|
67
|
+
{remove.error && <p role="alert">{remove.error}</p>}
|
|
68
|
+
</A.Dialog>
|
|
69
|
+
</main>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
// preview:end
|
|
73
|
+
export default App;
|