@hjmds/design-contracts 0.8.2
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 +208 -0
- package/dist/alert-dialog.d.ts +102 -0
- package/dist/alert-dialog.d.ts.map +1 -0
- package/dist/alert-dialog.js +136 -0
- package/dist/alert-dialog.js.map +1 -0
- package/dist/base-recipes.d.ts +184 -0
- package/dist/base-recipes.d.ts.map +1 -0
- package/dist/base-recipes.js +129 -0
- package/dist/base-recipes.js.map +1 -0
- package/dist/behaviors.d.ts +1254 -0
- package/dist/behaviors.d.ts.map +1 -0
- package/dist/behaviors.js +972 -0
- package/dist/behaviors.js.map +1 -0
- package/dist/bottom-navigation-defaults.d.ts +13 -0
- package/dist/bottom-navigation-defaults.d.ts.map +1 -0
- package/dist/bottom-navigation-defaults.js +10 -0
- package/dist/bottom-navigation-defaults.js.map +1 -0
- package/dist/bottom-navigation.d.ts +114 -0
- package/dist/bottom-navigation.d.ts.map +1 -0
- package/dist/bottom-navigation.js +223 -0
- package/dist/bottom-navigation.js.map +1 -0
- package/dist/breadcrumb.d.ts +97 -0
- package/dist/breadcrumb.d.ts.map +1 -0
- package/dist/breadcrumb.js +99 -0
- package/dist/breadcrumb.js.map +1 -0
- package/dist/calendar.d.ts +285 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +297 -0
- package/dist/calendar.js.map +1 -0
- package/dist/card.d.ts +39 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +50 -0
- package/dist/card.js.map +1 -0
- package/dist/carousel.d.ts +180 -0
- package/dist/carousel.d.ts.map +1 -0
- package/dist/carousel.js +172 -0
- package/dist/carousel.js.map +1 -0
- package/dist/catalog.d.ts +7158 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +220 -0
- package/dist/catalog.js.map +1 -0
- package/dist/collection.d.ts +123 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +211 -0
- package/dist/collection.js.map +1 -0
- package/dist/color-references.d.ts +46 -0
- package/dist/color-references.d.ts.map +1 -0
- package/dist/color-references.js +39 -0
- package/dist/color-references.js.map +1 -0
- package/dist/colors.d.ts +56 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +90 -0
- package/dist/colors.js.map +1 -0
- package/dist/command-palette.d.ts +240 -0
- package/dist/command-palette.d.ts.map +1 -0
- package/dist/command-palette.js +97 -0
- package/dist/command-palette.js.map +1 -0
- package/dist/component-contracts.d.ts +137 -0
- package/dist/component-contracts.d.ts.map +1 -0
- package/dist/component-contracts.js +53 -0
- package/dist/component-contracts.js.map +1 -0
- package/dist/component-definitions.d.ts +135 -0
- package/dist/component-definitions.d.ts.map +1 -0
- package/dist/component-definitions.js +142 -0
- package/dist/component-definitions.js.map +1 -0
- package/dist/component-recipes.d.ts +3583 -0
- package/dist/component-recipes.d.ts.map +1 -0
- package/dist/component-recipes.js +1503 -0
- package/dist/component-recipes.js.map +1 -0
- package/dist/component-references.d.ts +416 -0
- package/dist/component-references.d.ts.map +1 -0
- package/dist/component-references.js +140 -0
- package/dist/component-references.js.map +1 -0
- package/dist/content-state.d.ts +104 -0
- package/dist/content-state.d.ts.map +1 -0
- package/dist/content-state.js +116 -0
- package/dist/content-state.js.map +1 -0
- package/dist/counter-badge-recipe.d.ts +80 -0
- package/dist/counter-badge-recipe.d.ts.map +1 -0
- package/dist/counter-badge-recipe.js +44 -0
- package/dist/counter-badge-recipe.js.map +1 -0
- package/dist/counter-badge.d.ts +12 -0
- package/dist/counter-badge.d.ts.map +1 -0
- package/dist/counter-badge.js +21 -0
- package/dist/counter-badge.js.map +1 -0
- package/dist/data-table.d.ts +203 -0
- package/dist/data-table.d.ts.map +1 -0
- package/dist/data-table.js +182 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-picker.d.ts +268 -0
- package/dist/date-picker.d.ts.map +1 -0
- package/dist/date-picker.js +168 -0
- package/dist/date-picker.js.map +1 -0
- package/dist/description-list.d.ts +68 -0
- package/dist/description-list.d.ts.map +1 -0
- package/dist/description-list.js +85 -0
- package/dist/description-list.js.map +1 -0
- package/dist/design-system-provider.d.ts +80 -0
- package/dist/design-system-provider.d.ts.map +1 -0
- package/dist/design-system-provider.js +147 -0
- package/dist/design-system-provider.js.map +1 -0
- package/dist/evidence.d.ts +49 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +133 -0
- package/dist/evidence.js.map +1 -0
- package/dist/file-picker.d.ts +183 -0
- package/dist/file-picker.d.ts.map +1 -0
- package/dist/file-picker.js +224 -0
- package/dist/file-picker.js.map +1 -0
- package/dist/floating-action-button.d.ts +143 -0
- package/dist/floating-action-button.d.ts.map +1 -0
- package/dist/floating-action-button.js +149 -0
- package/dist/floating-action-button.js.map +1 -0
- package/dist/form.d.ts +143 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +206 -0
- package/dist/form.js.map +1 -0
- package/dist/foundations.d.ts +300 -0
- package/dist/foundations.d.ts.map +1 -0
- package/dist/foundations.js +238 -0
- package/dist/foundations.js.map +1 -0
- package/dist/grid.d.ts +75 -0
- package/dist/grid.d.ts.map +1 -0
- package/dist/grid.js +133 -0
- package/dist/grid.js.map +1 -0
- package/dist/icon-button-recipe.d.ts +99 -0
- package/dist/icon-button-recipe.d.ts.map +1 -0
- package/dist/icon-button-recipe.js +53 -0
- package/dist/icon-button-recipe.js.map +1 -0
- package/dist/icon.d.ts +41 -0
- package/dist/icon.d.ts.map +1 -0
- package/dist/icon.js +147 -0
- package/dist/icon.js.map +1 -0
- package/dist/image.d.ts +82 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +100 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +118 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +118 -0
- package/dist/layout.js.map +1 -0
- package/dist/link.d.ts +57 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +142 -0
- package/dist/link.js.map +1 -0
- package/dist/load-more.d.ts +64 -0
- package/dist/load-more.d.ts.map +1 -0
- package/dist/load-more.js +124 -0
- package/dist/load-more.js.map +1 -0
- package/dist/mentions.d.ts +67 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +107 -0
- package/dist/mentions.js.map +1 -0
- package/dist/number-field.d.ts +208 -0
- package/dist/number-field.d.ts.map +1 -0
- package/dist/number-field.js +247 -0
- package/dist/number-field.js.map +1 -0
- package/dist/otp-field.d.ts +152 -0
- package/dist/otp-field.d.ts.map +1 -0
- package/dist/otp-field.js +117 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/pagination.d.ts +181 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +226 -0
- package/dist/pagination.js.map +1 -0
- package/dist/password-field.d.ts +187 -0
- package/dist/password-field.d.ts.map +1 -0
- package/dist/password-field.js +120 -0
- package/dist/password-field.js.map +1 -0
- package/dist/popover.d.ts +152 -0
- package/dist/popover.d.ts.map +1 -0
- package/dist/popover.js +137 -0
- package/dist/popover.js.map +1 -0
- package/dist/progress-recipe.d.ts +43 -0
- package/dist/progress-recipe.d.ts.map +1 -0
- package/dist/progress-recipe.js +16 -0
- package/dist/progress-recipe.js.map +1 -0
- package/dist/recipes.d.ts +31 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +46 -0
- package/dist/recipes.js.map +1 -0
- package/dist/responsive.d.ts +27 -0
- package/dist/responsive.d.ts.map +1 -0
- package/dist/responsive.js +66 -0
- package/dist/responsive.js.map +1 -0
- package/dist/result.d.ts +111 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +97 -0
- package/dist/result.js.map +1 -0
- package/dist/selection-helpers.d.ts +16 -0
- package/dist/selection-helpers.d.ts.map +1 -0
- package/dist/selection-helpers.js +52 -0
- package/dist/selection-helpers.js.map +1 -0
- package/dist/semantic-colors.d.ts +275 -0
- package/dist/semantic-colors.d.ts.map +1 -0
- package/dist/semantic-colors.js +84 -0
- package/dist/semantic-colors.js.map +1 -0
- package/dist/sheet.d.ts +51 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/sheet.js +69 -0
- package/dist/sheet.js.map +1 -0
- package/dist/showcase.d.ts +153 -0
- package/dist/showcase.d.ts.map +1 -0
- package/dist/showcase.js +210 -0
- package/dist/showcase.js.map +1 -0
- package/dist/side-panel.d.ts +199 -0
- package/dist/side-panel.d.ts.map +1 -0
- package/dist/side-panel.js +111 -0
- package/dist/side-panel.js.map +1 -0
- package/dist/slider.d.ts +138 -0
- package/dist/slider.d.ts.map +1 -0
- package/dist/slider.js +150 -0
- package/dist/slider.js.map +1 -0
- package/dist/splitter.d.ts +113 -0
- package/dist/splitter.d.ts.map +1 -0
- package/dist/splitter.js +99 -0
- package/dist/splitter.js.map +1 -0
- package/dist/statistic.d.ts +41 -0
- package/dist/statistic.d.ts.map +1 -0
- package/dist/statistic.js +76 -0
- package/dist/statistic.js.map +1 -0
- package/dist/steps.d.ts +196 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +160 -0
- package/dist/steps.js.map +1 -0
- package/dist/tag.d.ts +128 -0
- package/dist/tag.d.ts.map +1 -0
- package/dist/tag.js +93 -0
- package/dist/tag.js.map +1 -0
- package/dist/timeline.d.ts +147 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +127 -0
- package/dist/timeline.js.map +1 -0
- package/dist/toast.d.ts +164 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +529 -0
- package/dist/toast.js.map +1 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +9 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tooltip.d.ts +44 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.js +88 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/tour.d.ts +218 -0
- package/dist/tour.d.ts.map +1 -0
- package/dist/tour.js +211 -0
- package/dist/tour.js.map +1 -0
- package/dist/transfer-list.d.ts +207 -0
- package/dist/transfer-list.d.ts.map +1 -0
- package/dist/transfer-list.js +193 -0
- package/dist/transfer-list.js.map +1 -0
- package/dist/tree-select.d.ts +78 -0
- package/dist/tree-select.d.ts.map +1 -0
- package/dist/tree-select.js +132 -0
- package/dist/tree-select.js.map +1 -0
- package/dist/tree.d.ts +206 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +223 -0
- package/dist/tree.js.map +1 -0
- package/dist/upload-item.d.ts +173 -0
- package/dist/upload-item.d.ts.map +1 -0
- package/dist/upload-item.js +156 -0
- package/dist/upload-item.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/docs/affix.md +63 -0
- package/docs/anchor.md +59 -0
- package/docs/ant-design-coverage.md +119 -0
- package/docs/app-provider.md +50 -0
- package/docs/app-rn-adoption.md +227 -0
- package/docs/architecture.md +320 -0
- package/docs/authoring-brief.md +87 -0
- package/docs/border-beam.md +66 -0
- package/docs/bottom-navigation.md +127 -0
- package/docs/breadcrumb.md +82 -0
- package/docs/calendar.md +154 -0
- package/docs/carousel.md +130 -0
- package/docs/cascader.md +93 -0
- package/docs/catalog-decision-status.md +306 -0
- package/docs/color-picker.md +72 -0
- package/docs/command-palette.md +116 -0
- package/docs/confirm-popover.md +93 -0
- package/docs/consistency-audit.md +383 -0
- package/docs/consumer-release-gate.md +90 -0
- package/docs/content-state.md +156 -0
- package/docs/context-panel.md +88 -0
- package/docs/cross-platform-core-normalization.md +118 -0
- package/docs/data-table.md +79 -0
- package/docs/date-picker.md +84 -0
- package/docs/description-list.md +75 -0
- package/docs/design-system-provider.md +139 -0
- package/docs/dropdown.md +78 -0
- package/docs/expansion-roadmap.md +285 -0
- package/docs/file-picker.md +59 -0
- package/docs/floating-action-button.md +111 -0
- package/docs/form.md +134 -0
- package/docs/generated/component-maturity.md +103 -0
- package/docs/generated/renderer-evidence.json +5597 -0
- package/docs/generated/renderer-evidence.md +132 -0
- package/docs/generated/showcase-manifest.json +3732 -0
- package/docs/icon.md +22 -0
- package/docs/identity.md +121 -0
- package/docs/image.md +84 -0
- package/docs/implementation-0.5.md +85 -0
- package/docs/layout-primitives.md +107 -0
- package/docs/layout.md +83 -0
- package/docs/library-reference-decisions.md +123 -0
- package/docs/link.md +67 -0
- package/docs/load-more.md +31 -0
- package/docs/mentions.md +82 -0
- package/docs/migration-0.2.md +108 -0
- package/docs/migration-0.3.md +72 -0
- package/docs/migration-0.5.md +82 -0
- package/docs/migration-0.6.md +197 -0
- package/docs/notification.md +54 -0
- package/docs/number-field.md +82 -0
- package/docs/otp-field.md +103 -0
- package/docs/pagination.md +139 -0
- package/docs/password-field.md +98 -0
- package/docs/popover.md +124 -0
- package/docs/promotion-candidates.md +169 -0
- package/docs/qr-code.md +69 -0
- package/docs/rating.md +58 -0
- package/docs/responsive-grid.md +90 -0
- package/docs/result.md +96 -0
- package/docs/showcase.md +97 -0
- package/docs/side-panel.md +71 -0
- package/docs/slider.md +79 -0
- package/docs/splitter.md +55 -0
- package/docs/statistic.md +16 -0
- package/docs/steps.md +115 -0
- package/docs/tag.md +57 -0
- package/docs/time-picker.md +88 -0
- package/docs/timeline.md +146 -0
- package/docs/toast.md +127 -0
- package/docs/tooltip.md +58 -0
- package/docs/tour.md +136 -0
- package/docs/transfer-list.md +94 -0
- package/docs/tree-select.md +101 -0
- package/docs/tree.md +123 -0
- package/docs/upload-item.md +68 -0
- package/docs/utility.md +53 -0
- package/docs/virtual-list.md +70 -0
- package/docs/watermark.md +58 -0
- package/package.json +402 -0
package/dist/form.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { spacing } from "./foundations.js";
|
|
2
|
+
function assertCopy(value, field) {
|
|
3
|
+
if (value.trim().length === 0) {
|
|
4
|
+
throw new TypeError(`Form ${field} must not be empty`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function validateFormSubmitSessionOptions(options) {
|
|
8
|
+
if (typeof options.onSubmit !== "function") {
|
|
9
|
+
throw new TypeError("Form onSubmit must be a function");
|
|
10
|
+
}
|
|
11
|
+
assertCopy(options.fallbackErrorMessage, "fallbackErrorMessage");
|
|
12
|
+
}
|
|
13
|
+
function resolveFailureMessage(options, error) {
|
|
14
|
+
try {
|
|
15
|
+
const resolved = options.resolveErrorMessage?.(error)?.trim();
|
|
16
|
+
return resolved || options.fallbackErrorMessage;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return options.fallbackErrorMessage;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Owns submit re-entrancy and exactly-once settlement. Does not own field
|
|
24
|
+
* values (the caller passes `values` at call time), field-level validation,
|
|
25
|
+
* or when submission is allowed to start (the product decides that from its
|
|
26
|
+
* own dirty/valid state before calling `submit`).
|
|
27
|
+
*/
|
|
28
|
+
export function createFormSubmitSession(options) {
|
|
29
|
+
validateFormSubmitSessionOptions(options);
|
|
30
|
+
let phase = { status: "idle" };
|
|
31
|
+
let disposed = false;
|
|
32
|
+
let interruptCurrentAttempt = null;
|
|
33
|
+
const listeners = new Set();
|
|
34
|
+
const notify = () => {
|
|
35
|
+
for (const listener of listeners)
|
|
36
|
+
listener();
|
|
37
|
+
};
|
|
38
|
+
const transition = (next) => {
|
|
39
|
+
phase = next;
|
|
40
|
+
notify();
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
getSnapshot: () => phase,
|
|
44
|
+
subscribe(listener) {
|
|
45
|
+
listeners.add(listener);
|
|
46
|
+
return () => listeners.delete(listener);
|
|
47
|
+
},
|
|
48
|
+
submit(values) {
|
|
49
|
+
if (disposed)
|
|
50
|
+
throw new Error("Cannot use a disposed Form submit session");
|
|
51
|
+
if (phase.status === "submitting") {
|
|
52
|
+
return Promise.resolve({ outcome: "blocked", reason: "already-submitting" });
|
|
53
|
+
}
|
|
54
|
+
transition({ status: "submitting" });
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
let settled = false;
|
|
57
|
+
const settleOnce = (outcome) => {
|
|
58
|
+
if (settled)
|
|
59
|
+
return;
|
|
60
|
+
settled = true;
|
|
61
|
+
interruptCurrentAttempt = null;
|
|
62
|
+
resolve(outcome);
|
|
63
|
+
};
|
|
64
|
+
interruptCurrentAttempt = settleOnce;
|
|
65
|
+
(async () => {
|
|
66
|
+
try {
|
|
67
|
+
await options.onSubmit(values);
|
|
68
|
+
if (settled)
|
|
69
|
+
return;
|
|
70
|
+
transition({ status: "succeeded" });
|
|
71
|
+
settleOnce({ outcome: "succeeded" });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
const message = resolveFailureMessage(options, error);
|
|
77
|
+
transition({ status: "failed", message });
|
|
78
|
+
settleOnce({ outcome: "failed", message });
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
reset() {
|
|
84
|
+
if (disposed)
|
|
85
|
+
return false;
|
|
86
|
+
if (phase.status === "idle" || phase.status === "submitting")
|
|
87
|
+
return false;
|
|
88
|
+
transition({ status: "idle" });
|
|
89
|
+
return true;
|
|
90
|
+
},
|
|
91
|
+
dispose() {
|
|
92
|
+
if (disposed)
|
|
93
|
+
return false;
|
|
94
|
+
disposed = true;
|
|
95
|
+
interruptCurrentAttempt?.({ outcome: "interrupted" });
|
|
96
|
+
interruptCurrentAttempt = null;
|
|
97
|
+
return true;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Submit-failure focus
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* Form does not register fields or run validation, so it cannot find the
|
|
106
|
+
* first invalid field on its own. The product supplies its own field render
|
|
107
|
+
* order and the ids it currently considers invalid (e.g. React Hook Form's
|
|
108
|
+
* `formState.errors` keys); Form only fixes which one wins the race — first
|
|
109
|
+
* in render order — so every product answers this the same way.
|
|
110
|
+
*/
|
|
111
|
+
export function validateFormFieldOrder(fieldOrder) {
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
fieldOrder.forEach((fieldId, index) => {
|
|
114
|
+
if (fieldId.trim().length === 0) {
|
|
115
|
+
throw new TypeError(`Form field id at index ${index} must not be empty`);
|
|
116
|
+
}
|
|
117
|
+
if (seen.has(fieldId)) {
|
|
118
|
+
throw new TypeError(`Duplicate Form field id: ${fieldId}`);
|
|
119
|
+
}
|
|
120
|
+
seen.add(fieldId);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Web renderers call `.focus()` on the resolved field's control; React Native
|
|
125
|
+
* renderers move accessibility focus to it (e.g. `AccessibilityInfo.sendAccessibilityEvent`
|
|
126
|
+
* or a focus-trap ref). Returns `null` when there are no fields yet or none
|
|
127
|
+
* are invalid, in which case a failed submit has no field to focus and the
|
|
128
|
+
* form-level error slot below owns the announcement instead.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveFirstInvalidFieldFocusTarget(fieldOrder, invalidFieldIds) {
|
|
131
|
+
validateFormFieldOrder(fieldOrder);
|
|
132
|
+
const invalid = invalidFieldIds instanceof Set ? invalidFieldIds : new Set(invalidFieldIds);
|
|
133
|
+
for (const fieldId of fieldOrder) {
|
|
134
|
+
if (invalid.has(fieldId))
|
|
135
|
+
return fieldId;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
export const formDefaults = {
|
|
140
|
+
density: "comfortable",
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Anatomy is fixed, not a configurable prop: fields, then the form-level
|
|
144
|
+
* error, then actions. `field` frame styling (background, border, its own
|
|
145
|
+
* label/hint/error gap) belongs to `fieldRecipe`; this recipe only sets the
|
|
146
|
+
* gap *between* one field and the next. `formError` renders as the existing
|
|
147
|
+
* Notice `tone="danger"` contract — Form fixes only its slot position, it
|
|
148
|
+
* does not redeclare Notice's colors. `actions` has no dedicated layout here;
|
|
149
|
+
* composing Button/Stack in that slot is the product's job, same reasoning
|
|
150
|
+
* the roadmap already applies to Stack.
|
|
151
|
+
*/
|
|
152
|
+
export const formRecipe = {
|
|
153
|
+
slots: ["root", "field", "formError", "actions"],
|
|
154
|
+
defaults: formDefaults,
|
|
155
|
+
density: {
|
|
156
|
+
compact: { fieldGap: spacing.sm },
|
|
157
|
+
comfortable: { fieldGap: spacing.lg },
|
|
158
|
+
},
|
|
159
|
+
formError: {
|
|
160
|
+
position: "beforeActions",
|
|
161
|
+
gap: spacing.sm,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Behavior — source for the shared behaviorRegistry wiring
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
export const formBehaviorDefaults = {
|
|
168
|
+
/** Reuses Toast's own `normal | high` vocabulary: a blocked submission is `high`/assertive. */
|
|
169
|
+
errorAnnouncementPriority: "high",
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Kept beside the Form contract as the source value wired by
|
|
173
|
+
* `behaviorRegistry.form`; this lets the registry reuse the contract without
|
|
174
|
+
* redeclaring Form's inputs, events, states, or scenarios.
|
|
175
|
+
*/
|
|
176
|
+
export const formBehaviorSpec = {
|
|
177
|
+
/*
|
|
178
|
+
값은 제품이 소유하므로 controlled prop이 없다. 대신 renderer가 반드시 배선해야 하는
|
|
179
|
+
것을 `inputs`/`events`로 선언한다 — 공유 불변식이 "controlled + inputs + events가
|
|
180
|
+
하나도 없는 behavior는 renderer 테스트가 구동할 대상이 없다"고 요구하고, 그 지적이
|
|
181
|
+
옳다. 같은 형태(컨트롤러가 상태를 소유하고 renderer가 구동)인 `loadMore`가 이미
|
|
182
|
+
`controlled: []` + `inputs` + `events`로 선언한 선례를 따른다.
|
|
183
|
+
*/
|
|
184
|
+
controlled: [],
|
|
185
|
+
inputs: ["status", "formError", "fieldOrder"],
|
|
186
|
+
events: ["onSubmit"],
|
|
187
|
+
defaults: formBehaviorDefaults,
|
|
188
|
+
stateAxes: {
|
|
189
|
+
availability: ["enabled", "busy"],
|
|
190
|
+
content: ["idle", "loading", "error"],
|
|
191
|
+
},
|
|
192
|
+
web: { roles: ["alert"], keyboard: ["Tab", "Enter"], focus: "native" },
|
|
193
|
+
native: { roles: ["alert"], states: ["busy"], actions: ["submit"] },
|
|
194
|
+
scenarios: [
|
|
195
|
+
"submitting-blocks-every-concurrent-submit-attempt",
|
|
196
|
+
"each-submit-attempt-settles-its-result-exactly-once",
|
|
197
|
+
"failed-submit-returns-to-a-resting-state-that-still-allows-retry",
|
|
198
|
+
"succeeded-submit-remains-resting-and-allows-resubmission",
|
|
199
|
+
"dispose-during-submit-settles-the-pending-attempt-as-interrupted",
|
|
200
|
+
"disposed-session-rejects-further-submit-attempts",
|
|
201
|
+
"form-level-error-never-carries-a-per-field-message",
|
|
202
|
+
"first-invalid-field-in-render-order-receives-focus-on-submit-failure",
|
|
203
|
+
"form-level-error-is-announced-when-no-field-target-exists",
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=form.js.map
|
package/dist/form.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.js","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"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","sourcesContent":["import { spacing } from \"./foundations.js\";\n\n/**\n * Form owns two things only: the vertical rhythm between already-framed\n * Fields, and the repeatable submit session (idle → submitting →\n * succeeded/failed). Field values, registration, dependencies, validateTrigger\n * policy and Form.List-style repeating groups stay product-owned (React Hook\n * Form or equivalent) — see docs/form.md for the full boundary.\n */\n\n// ---------------------------------------------------------------------------\n// Submit session\n// ---------------------------------------------------------------------------\n\nexport type FormSubmitStatus = \"idle\" | \"submitting\" | \"succeeded\" | \"failed\";\n\nexport type FormSubmitPhase =\n | Readonly<{ status: \"idle\" }>\n | Readonly<{ status: \"submitting\" }>\n | Readonly<{ status: \"succeeded\" }>\n | Readonly<{ status: \"failed\"; message: string }>;\n\nexport type FormSubmitOutcome =\n | Readonly<{ outcome: \"succeeded\" }>\n | Readonly<{ outcome: \"failed\"; message: string }>\n | Readonly<{ outcome: \"blocked\"; reason: \"already-submitting\" }>\n | Readonly<{ outcome: \"interrupted\" }>;\n\n/** Rejects to report a submission failure; resolving never implies success. */\nexport type FormSubmitHandler<Values> = (values: Values) => void | Promise<void>;\n\nexport type FormSubmitSessionOptions<Values> = Readonly<{\n onSubmit: FormSubmitHandler<Values>;\n /** Localized fallback shown when `resolveErrorMessage` is absent, throws, or returns empty copy. */\n fallbackErrorMessage: string;\n resolveErrorMessage?: (error: unknown) => string;\n}>;\n\n/**\n * A single submit session is reused across many attempts — unlike\n * AlertDialog's one-shot confirm, a form typically stays mounted and gets\n * resubmitted (edit → retry, or a settings screen saved again later). Each\n * attempt still settles its own returned Promise exactly once, and\n * `submitting` blocks every concurrent attempt the same way AlertDialog's\n * `busy` blocks a second confirm.\n */\nexport type FormSubmitSession<Values> = Readonly<{\n getSnapshot(): FormSubmitPhase;\n subscribe(listener: () => void): () => void;\n submit(values: Values): Promise<FormSubmitOutcome>;\n /** Moves a resting `succeeded`/`failed` phase back to `idle`. No-op while submitting. */\n reset(): boolean;\n dispose(): boolean;\n}>;\n\nfunction assertCopy(value: string, field: string): void {\n if (value.trim().length === 0) {\n throw new TypeError(`Form ${field} must not be empty`);\n }\n}\n\nexport function validateFormSubmitSessionOptions<Values>(\n options: FormSubmitSessionOptions<Values>,\n): void {\n if (typeof options.onSubmit !== \"function\") {\n throw new TypeError(\"Form onSubmit must be a function\");\n }\n assertCopy(options.fallbackErrorMessage, \"fallbackErrorMessage\");\n}\n\nfunction resolveFailureMessage<Values>(\n options: FormSubmitSessionOptions<Values>,\n error: unknown,\n): string {\n try {\n const resolved = options.resolveErrorMessage?.(error)?.trim();\n return resolved || options.fallbackErrorMessage;\n } catch {\n return options.fallbackErrorMessage;\n }\n}\n\n/**\n * Owns submit re-entrancy and exactly-once settlement. Does not own field\n * values (the caller passes `values` at call time), field-level validation,\n * or when submission is allowed to start (the product decides that from its\n * own dirty/valid state before calling `submit`).\n */\nexport function createFormSubmitSession<Values>(\n options: FormSubmitSessionOptions<Values>,\n): FormSubmitSession<Values> {\n validateFormSubmitSessionOptions(options);\n\n let phase: FormSubmitPhase = { status: \"idle\" };\n let disposed = false;\n let interruptCurrentAttempt: ((outcome: FormSubmitOutcome) => void) | null = null;\n const listeners = new Set<() => void>();\n\n const notify = () => {\n for (const listener of listeners) listener();\n };\n const transition = (next: FormSubmitPhase) => {\n phase = next;\n notify();\n };\n\n return {\n getSnapshot: () => phase,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n submit(values) {\n if (disposed) throw new Error(\"Cannot use a disposed Form submit session\");\n if (phase.status === \"submitting\") {\n return Promise.resolve({ outcome: \"blocked\", reason: \"already-submitting\" });\n }\n\n transition({ status: \"submitting\" });\n\n return new Promise<FormSubmitOutcome>((resolve) => {\n let settled = false;\n const settleOnce = (outcome: FormSubmitOutcome) => {\n if (settled) return;\n settled = true;\n interruptCurrentAttempt = null;\n resolve(outcome);\n };\n interruptCurrentAttempt = settleOnce;\n\n (async () => {\n try {\n await options.onSubmit(values);\n if (settled) return;\n transition({ status: \"succeeded\" });\n settleOnce({ outcome: \"succeeded\" });\n } catch (error) {\n if (settled) return;\n const message = resolveFailureMessage(options, error);\n transition({ status: \"failed\", message });\n settleOnce({ outcome: \"failed\", message });\n }\n })();\n });\n },\n reset() {\n if (disposed) return false;\n if (phase.status === \"idle\" || phase.status === \"submitting\") return false;\n transition({ status: \"idle\" });\n return true;\n },\n dispose() {\n if (disposed) return false;\n disposed = true;\n interruptCurrentAttempt?.({ outcome: \"interrupted\" });\n interruptCurrentAttempt = null;\n return true;\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Submit-failure focus\n// ---------------------------------------------------------------------------\n\n/**\n * Form does not register fields or run validation, so it cannot find the\n * first invalid field on its own. The product supplies its own field render\n * order and the ids it currently considers invalid (e.g. React Hook Form's\n * `formState.errors` keys); Form only fixes which one wins the race — first\n * in render order — so every product answers this the same way.\n */\nexport function validateFormFieldOrder(fieldOrder: readonly string[]): void {\n const seen = new Set<string>();\n fieldOrder.forEach((fieldId, index) => {\n if (fieldId.trim().length === 0) {\n throw new TypeError(`Form field id at index ${index} must not be empty`);\n }\n if (seen.has(fieldId)) {\n throw new TypeError(`Duplicate Form field id: ${fieldId}`);\n }\n seen.add(fieldId);\n });\n}\n\n/**\n * Web renderers call `.focus()` on the resolved field's control; React Native\n * renderers move accessibility focus to it (e.g. `AccessibilityInfo.sendAccessibilityEvent`\n * or a focus-trap ref). Returns `null` when there are no fields yet or none\n * are invalid, in which case a failed submit has no field to focus and the\n * form-level error slot below owns the announcement instead.\n */\nexport function resolveFirstInvalidFieldFocusTarget(\n fieldOrder: readonly string[],\n invalidFieldIds: ReadonlySet<string> | readonly string[],\n): string | null {\n validateFormFieldOrder(fieldOrder);\n const invalid =\n invalidFieldIds instanceof Set ? invalidFieldIds : new Set(invalidFieldIds);\n for (const fieldId of fieldOrder) {\n if (invalid.has(fieldId)) return fieldId;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Visual recipe — inter-field rhythm only\n// ---------------------------------------------------------------------------\n\nexport type FormDensity = \"compact\" | \"comfortable\";\n\nexport const formDefaults = {\n density: \"comfortable\",\n} as const satisfies Readonly<{ density: FormDensity }>;\n\n/**\n * Anatomy is fixed, not a configurable prop: fields, then the form-level\n * error, then actions. `field` frame styling (background, border, its own\n * label/hint/error gap) belongs to `fieldRecipe`; this recipe only sets the\n * gap *between* one field and the next. `formError` renders as the existing\n * Notice `tone=\"danger\"` contract — Form fixes only its slot position, it\n * does not redeclare Notice's colors. `actions` has no dedicated layout here;\n * composing Button/Stack in that slot is the product's job, same reasoning\n * the roadmap already applies to Stack.\n */\nexport const formRecipe = {\n slots: [\"root\", \"field\", \"formError\", \"actions\"] as const,\n defaults: formDefaults,\n density: {\n compact: { fieldGap: spacing.sm },\n comfortable: { fieldGap: spacing.lg },\n },\n formError: {\n position: \"beforeActions\",\n gap: spacing.sm,\n },\n} as const satisfies Readonly<{\n slots: readonly [\"root\", \"field\", \"formError\", \"actions\"];\n defaults: Readonly<{ density: FormDensity }>;\n density: Record<FormDensity, Readonly<{ fieldGap: number }>>;\n formError: Readonly<{ position: \"beforeActions\"; gap: number }>;\n}>;\n\n// ---------------------------------------------------------------------------\n// Behavior — source for the shared behaviorRegistry wiring\n// ---------------------------------------------------------------------------\n\nexport const formBehaviorDefaults = {\n /** Reuses Toast's own `normal | high` vocabulary: a blocked submission is `high`/assertive. */\n errorAnnouncementPriority: \"high\",\n} as const satisfies Readonly<{ errorAnnouncementPriority: \"normal\" | \"high\" }>;\n\n/**\n * Kept beside the Form contract as the source value wired by\n * `behaviorRegistry.form`; this lets the registry reuse the contract without\n * redeclaring Form's inputs, events, states, or scenarios.\n */\nexport const formBehaviorSpec = {\n /*\n 값은 제품이 소유하므로 controlled prop이 없다. 대신 renderer가 반드시 배선해야 하는\n 것을 `inputs`/`events`로 선언한다 — 공유 불변식이 \"controlled + inputs + events가\n 하나도 없는 behavior는 renderer 테스트가 구동할 대상이 없다\"고 요구하고, 그 지적이\n 옳다. 같은 형태(컨트롤러가 상태를 소유하고 renderer가 구동)인 `loadMore`가 이미\n `controlled: []` + `inputs` + `events`로 선언한 선례를 따른다.\n */\n controlled: [],\n inputs: [\"status\", \"formError\", \"fieldOrder\"],\n events: [\"onSubmit\"],\n defaults: formBehaviorDefaults,\n stateAxes: {\n availability: [\"enabled\", \"busy\"],\n content: [\"idle\", \"loading\", \"error\"],\n },\n web: { roles: [\"alert\"], keyboard: [\"Tab\", \"Enter\"], focus: \"native\" },\n native: { roles: [\"alert\"], states: [\"busy\"], actions: [\"submit\"] },\n scenarios: [\n \"submitting-blocks-every-concurrent-submit-attempt\",\n \"each-submit-attempt-settles-its-result-exactly-once\",\n \"failed-submit-returns-to-a-resting-state-that-still-allows-retry\",\n \"succeeded-submit-remains-resting-and-allows-resubmission\",\n \"dispose-during-submit-settles-the-pending-attempt-as-interrupted\",\n \"disposed-session-rejects-further-submit-attempts\",\n \"form-level-error-never-carries-a-per-field-message\",\n \"first-invalid-field-in-render-order-receives-focus-on-submit-failure\",\n \"form-level-error-is-announced-when-no-field-target-exists\",\n ],\n} as const;\n"]}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/** Numeric units are consumed as CSS pixels on web and density-independent points on native. */
|
|
2
|
+
export declare const spacing: {
|
|
3
|
+
readonly xxs: 4;
|
|
4
|
+
readonly xs: 8;
|
|
5
|
+
readonly sm: 12;
|
|
6
|
+
readonly md: 16;
|
|
7
|
+
readonly lg: 20;
|
|
8
|
+
readonly xl: 24;
|
|
9
|
+
readonly xxl: 32;
|
|
10
|
+
readonly xxxl: 40;
|
|
11
|
+
};
|
|
12
|
+
export declare const radius: {
|
|
13
|
+
readonly sm: 8;
|
|
14
|
+
readonly md: 12;
|
|
15
|
+
readonly lg: 16;
|
|
16
|
+
readonly xl: 24;
|
|
17
|
+
readonly full: 999;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Ordered fallbacks keep the contract platform-neutral: Web renderers join
|
|
21
|
+
* the list as a CSS stack, while native renderers select the first family
|
|
22
|
+
* available on the device instead of receiving a CSS-only string.
|
|
23
|
+
*/
|
|
24
|
+
export declare const fontFamily: {
|
|
25
|
+
readonly ui: readonly ["Inter", "Pretendard", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "sans-serif"];
|
|
26
|
+
readonly code: readonly ["ui-monospace", "SFMono-Regular", "Consolas", "Liberation Mono", "monospace"];
|
|
27
|
+
};
|
|
28
|
+
export type FontFamily = keyof typeof fontFamily;
|
|
29
|
+
/** String weights translate unchanged to CSS and React Native text styles. */
|
|
30
|
+
export declare const fontWeight: {
|
|
31
|
+
readonly regular: "400";
|
|
32
|
+
readonly medium: "500";
|
|
33
|
+
readonly semibold: "600";
|
|
34
|
+
readonly bold: "700";
|
|
35
|
+
readonly heavy: "800";
|
|
36
|
+
};
|
|
37
|
+
export type FontWeight = keyof typeof fontWeight;
|
|
38
|
+
export type FontWeightValue = (typeof fontWeight)[FontWeight];
|
|
39
|
+
/** Numeric tracking uses CSS pixels on Web and density-independent points on Native. */
|
|
40
|
+
export declare const letterSpacing: {
|
|
41
|
+
readonly tight: -0.2;
|
|
42
|
+
readonly normal: 0;
|
|
43
|
+
readonly wide: 0.2;
|
|
44
|
+
};
|
|
45
|
+
export type LetterSpacing = keyof typeof letterSpacing;
|
|
46
|
+
/** Renderers map the shared intent to `font-variant-numeric` / `fontVariant`. */
|
|
47
|
+
export declare const numeric: {
|
|
48
|
+
readonly proportional: "proportional-nums";
|
|
49
|
+
readonly tabular: "tabular-nums";
|
|
50
|
+
};
|
|
51
|
+
export type NumericVariant = keyof typeof numeric;
|
|
52
|
+
export declare const typography: {
|
|
53
|
+
readonly caption: {
|
|
54
|
+
readonly fontSize: 11;
|
|
55
|
+
readonly lineHeight: 16;
|
|
56
|
+
readonly fontWeight: "400";
|
|
57
|
+
};
|
|
58
|
+
readonly label: {
|
|
59
|
+
readonly fontSize: 12;
|
|
60
|
+
readonly lineHeight: 18;
|
|
61
|
+
readonly fontWeight: "600";
|
|
62
|
+
};
|
|
63
|
+
readonly body: {
|
|
64
|
+
readonly fontSize: 14;
|
|
65
|
+
readonly lineHeight: 20;
|
|
66
|
+
readonly fontWeight: "400";
|
|
67
|
+
};
|
|
68
|
+
readonly bodyLarge: {
|
|
69
|
+
readonly fontSize: 16;
|
|
70
|
+
readonly lineHeight: 24;
|
|
71
|
+
readonly fontWeight: "400";
|
|
72
|
+
};
|
|
73
|
+
readonly title: {
|
|
74
|
+
readonly fontSize: 18;
|
|
75
|
+
readonly lineHeight: 26;
|
|
76
|
+
readonly fontWeight: "700";
|
|
77
|
+
};
|
|
78
|
+
readonly titleLarge: {
|
|
79
|
+
readonly fontSize: 20;
|
|
80
|
+
readonly lineHeight: 28;
|
|
81
|
+
readonly fontWeight: "800";
|
|
82
|
+
};
|
|
83
|
+
readonly heading: {
|
|
84
|
+
readonly fontSize: 24;
|
|
85
|
+
readonly lineHeight: 32;
|
|
86
|
+
readonly fontWeight: "800";
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
export type TextVariant = keyof typeof typography;
|
|
90
|
+
/**
|
|
91
|
+
* Document heading hierarchy without widening the established `TextVariant`
|
|
92
|
+
* union. Levels 3–5 reuse the existing semantic styles; larger levels are
|
|
93
|
+
* additive display roles for dense product pages and documentation.
|
|
94
|
+
*/
|
|
95
|
+
export declare const heading: {
|
|
96
|
+
readonly level1: {
|
|
97
|
+
readonly fontSize: 40;
|
|
98
|
+
readonly lineHeight: 48;
|
|
99
|
+
readonly fontWeight: "800";
|
|
100
|
+
};
|
|
101
|
+
readonly level2: {
|
|
102
|
+
readonly fontSize: 32;
|
|
103
|
+
readonly lineHeight: 40;
|
|
104
|
+
readonly fontWeight: "800";
|
|
105
|
+
};
|
|
106
|
+
readonly level3: {
|
|
107
|
+
readonly fontSize: 24;
|
|
108
|
+
readonly lineHeight: 32;
|
|
109
|
+
readonly fontWeight: "800";
|
|
110
|
+
};
|
|
111
|
+
readonly level4: {
|
|
112
|
+
readonly fontSize: 20;
|
|
113
|
+
readonly lineHeight: 28;
|
|
114
|
+
readonly fontWeight: "800";
|
|
115
|
+
};
|
|
116
|
+
readonly level5: {
|
|
117
|
+
readonly fontSize: 18;
|
|
118
|
+
readonly lineHeight: 26;
|
|
119
|
+
readonly fontWeight: "700";
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
export type HeadingLevel = keyof typeof heading;
|
|
123
|
+
/** Glyphs include icons and avatars; they do not inherit paragraph line-height. */
|
|
124
|
+
export declare const glyph: {
|
|
125
|
+
readonly xs: 14;
|
|
126
|
+
readonly sm: 20;
|
|
127
|
+
readonly md: 24;
|
|
128
|
+
readonly lg: 28;
|
|
129
|
+
readonly xl: 32;
|
|
130
|
+
readonly xxl: 44;
|
|
131
|
+
readonly xxxl: 48;
|
|
132
|
+
};
|
|
133
|
+
export type GlyphSize = keyof typeof glyph;
|
|
134
|
+
export declare const motion: {
|
|
135
|
+
readonly fast: 120;
|
|
136
|
+
readonly normal: 200;
|
|
137
|
+
readonly slow: 320;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Curves are stored as normalized cubic-bezier tuples so CSS and native
|
|
141
|
+
* animation drivers can translate the same motion intent without sharing a
|
|
142
|
+
* renderer dependency.
|
|
143
|
+
*/
|
|
144
|
+
export declare const easing: {
|
|
145
|
+
readonly standard: readonly [0.2, 0, 0, 1];
|
|
146
|
+
readonly enter: readonly [0, 0, 0, 1];
|
|
147
|
+
readonly exit: readonly [0.3, 0, 1, 1];
|
|
148
|
+
readonly emphasized: readonly [0.2, 0, 0, 1];
|
|
149
|
+
};
|
|
150
|
+
export type ReducedMotionBehavior = "instant" | "opacity" | "static";
|
|
151
|
+
/** Shared transition intent; renderers translate easing tuples to their animation API. */
|
|
152
|
+
export declare const motionPreset: {
|
|
153
|
+
readonly micro: {
|
|
154
|
+
readonly duration: 120;
|
|
155
|
+
readonly easing: "standard";
|
|
156
|
+
readonly reducedMotion: "instant";
|
|
157
|
+
};
|
|
158
|
+
readonly enter: {
|
|
159
|
+
readonly duration: 200;
|
|
160
|
+
readonly easing: "enter";
|
|
161
|
+
readonly reducedMotion: "opacity";
|
|
162
|
+
};
|
|
163
|
+
readonly exit: {
|
|
164
|
+
readonly duration: 120;
|
|
165
|
+
readonly easing: "exit";
|
|
166
|
+
readonly reducedMotion: "instant";
|
|
167
|
+
};
|
|
168
|
+
readonly context: {
|
|
169
|
+
readonly duration: 320;
|
|
170
|
+
readonly easing: "emphasized";
|
|
171
|
+
readonly reducedMotion: "opacity";
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
/** Native renderers may use these values directly; web renderers use easing. */
|
|
175
|
+
export declare const spring: {
|
|
176
|
+
readonly responsive: {
|
|
177
|
+
readonly stiffness: 760;
|
|
178
|
+
readonly damping: 52;
|
|
179
|
+
readonly mass: 1;
|
|
180
|
+
};
|
|
181
|
+
readonly expressive: {
|
|
182
|
+
readonly stiffness: 520;
|
|
183
|
+
readonly damping: 38;
|
|
184
|
+
readonly mass: 1;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
/** Reusable state strengths. Components still decide which states are valid. */
|
|
188
|
+
export declare const opacity: {
|
|
189
|
+
readonly disabled: 0.5;
|
|
190
|
+
readonly muted: 0.72;
|
|
191
|
+
readonly pressed: 0.86;
|
|
192
|
+
readonly dragged: 0.64;
|
|
193
|
+
};
|
|
194
|
+
/** Overlaying the content color at these strengths creates predictable states. */
|
|
195
|
+
export declare const stateLayer: {
|
|
196
|
+
readonly hover: 0.06;
|
|
197
|
+
readonly focus: 0.08;
|
|
198
|
+
readonly pressed: 0.1;
|
|
199
|
+
readonly selected: 0.1;
|
|
200
|
+
};
|
|
201
|
+
export declare const stroke: {
|
|
202
|
+
readonly subtle: 1;
|
|
203
|
+
readonly default: 1;
|
|
204
|
+
readonly strong: 2;
|
|
205
|
+
readonly focus: 2;
|
|
206
|
+
};
|
|
207
|
+
export declare const control: {
|
|
208
|
+
readonly minTouchTarget: 44;
|
|
209
|
+
readonly buttonHeight: {
|
|
210
|
+
readonly small: 36;
|
|
211
|
+
readonly medium: 44;
|
|
212
|
+
readonly large: 52;
|
|
213
|
+
};
|
|
214
|
+
readonly buttonHitSlop: {
|
|
215
|
+
readonly small: 4;
|
|
216
|
+
readonly medium: 0;
|
|
217
|
+
readonly large: 0;
|
|
218
|
+
};
|
|
219
|
+
readonly fieldHeight: 44;
|
|
220
|
+
readonly chipHeight: {
|
|
221
|
+
readonly small: 36;
|
|
222
|
+
readonly medium: 44;
|
|
223
|
+
};
|
|
224
|
+
readonly selectionIndicator: 24;
|
|
225
|
+
};
|
|
226
|
+
/** Product renderers may narrow these widths, but should not invent new rhythm. */
|
|
227
|
+
export declare const layout: {
|
|
228
|
+
readonly pagePadding: {
|
|
229
|
+
readonly compact: 16;
|
|
230
|
+
readonly regular: 20;
|
|
231
|
+
readonly spacious: 24;
|
|
232
|
+
};
|
|
233
|
+
readonly sectionGap: 24;
|
|
234
|
+
readonly contentGap: 16;
|
|
235
|
+
readonly rowHeight: {
|
|
236
|
+
readonly singleLine: 56;
|
|
237
|
+
readonly twoLine: 68;
|
|
238
|
+
};
|
|
239
|
+
readonly readingMaxWidth: 720;
|
|
240
|
+
readonly contentMaxWidth: 1200;
|
|
241
|
+
};
|
|
242
|
+
/** Width thresholds use CSS pixels on Web and density-independent points on Native. */
|
|
243
|
+
export declare const breakpoint: {
|
|
244
|
+
readonly compact: 0;
|
|
245
|
+
readonly medium: 600;
|
|
246
|
+
readonly expanded: 960;
|
|
247
|
+
readonly wide: 1280;
|
|
248
|
+
};
|
|
249
|
+
/** Shared stacking intent. Values remain sparse to leave room for app layers. */
|
|
250
|
+
export declare const layer: {
|
|
251
|
+
readonly base: 0;
|
|
252
|
+
readonly sticky: 100;
|
|
253
|
+
readonly dropdown: 400;
|
|
254
|
+
readonly overlay: 800;
|
|
255
|
+
readonly modal: 900;
|
|
256
|
+
readonly tooltip: 950;
|
|
257
|
+
readonly toast: 1000;
|
|
258
|
+
};
|
|
259
|
+
export declare const overlay: {
|
|
260
|
+
readonly scrim: 0.6;
|
|
261
|
+
readonly veil: 0.25;
|
|
262
|
+
};
|
|
263
|
+
export type BackdropReference = Readonly<{
|
|
264
|
+
color: "#000000";
|
|
265
|
+
opacity: number;
|
|
266
|
+
}>;
|
|
267
|
+
/** Fixed backdrop contracts avoid renderer-specific names such as CSS `scrim`. */
|
|
268
|
+
export declare const backdrop: {
|
|
269
|
+
readonly modal: {
|
|
270
|
+
readonly color: "#000000";
|
|
271
|
+
readonly opacity: 0.6;
|
|
272
|
+
};
|
|
273
|
+
readonly veil: {
|
|
274
|
+
readonly color: "#000000";
|
|
275
|
+
readonly opacity: 0.25;
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
export declare const scrim: string;
|
|
279
|
+
/** Renderers translate this contract to box-shadow or native shadow/elevation properties. */
|
|
280
|
+
export declare const shadow: {
|
|
281
|
+
readonly raised: {
|
|
282
|
+
readonly color: "#000000";
|
|
283
|
+
readonly opacity: 0.08;
|
|
284
|
+
readonly radius: 4;
|
|
285
|
+
readonly offsetY: 1;
|
|
286
|
+
};
|
|
287
|
+
readonly floating: {
|
|
288
|
+
readonly color: "#000000";
|
|
289
|
+
readonly opacity: 0.12;
|
|
290
|
+
readonly radius: 12;
|
|
291
|
+
readonly offsetY: 4;
|
|
292
|
+
};
|
|
293
|
+
readonly overlay: {
|
|
294
|
+
readonly color: "#000000";
|
|
295
|
+
readonly opacity: 0.16;
|
|
296
|
+
readonly radius: 24;
|
|
297
|
+
readonly offsetY: 8;
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
//# sourceMappingURL=foundations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"foundations.d.ts","sourceRoot":"","sources":["../src/foundations.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,eAAO,MAAM,OAAO;;;;;;;;;CASV,CAAC;AAEX,eAAO,MAAM,MAAM;;;;;;CAMT,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;CAgBb,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,UAAU,CAAC;AAEjD,8EAA8E;AAC9E,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,UAAU,CAAC;AACjD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,UAAU,CAAC,CAAC;AAE9D,wFAAwF;AACxF,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,aAAa,CAAC;AAEvD,iFAAiF;AACjF,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,OAAO,CAAC;AAElD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAQb,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,UAAU,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;CAMV,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,OAAO,CAAC;AAEhD,mFAAmF;AACnF,eAAO,MAAM,KAAK;;;;;;;;CAQR,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,KAAK,CAAC;AAE3C,eAAO,MAAM,MAAM;;;;CAIT,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAErE,0FAA0F;AAC1F,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;CA4BxB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,MAAM;;;;;;;;;;;CAGT,CAAC;AAEX,gFAAgF;AAChF,eAAO,MAAM,OAAO;;;;;CAKV,CAAC;AAEX,kFAAkF;AAClF,eAAO,MAAM,UAAU;;;;;CAKb,CAAC;AAEX,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AAEX,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;CAkBV,CAAC;AAEX,mFAAmF;AACnF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;CAcT,CAAC;AAEX,uFAAuF;AACvF,eAAO,MAAM,UAAU;;;;;CAKb,CAAC;AAEX,iFAAiF;AACjF,eAAO,MAAM,KAAK;;;;;;;;CAQR,CAAC;AAEX,eAAO,MAAM,OAAO;;;CAGV,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,QAAQ;;;;;;;;;CAG2C,CAAC;AAEjE,eAAO,MAAM,KAAK,QAAoC,CAAC;AAEvD,6FAA6F;AAC7F,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;CAmBT,CAAC"}
|