@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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { fieldFrameContract, focusIndicatorContract, formSupportContract, } from "./component-contracts.js";
|
|
2
|
+
import { control, opacity, spacing } from "./foundations.js";
|
|
3
|
+
import { semanticColors } from "./semantic-colors.js";
|
|
4
|
+
export const numericRangeDefaults = {
|
|
5
|
+
step: 1,
|
|
6
|
+
};
|
|
7
|
+
export function assertFiniteNumber(value, field) {
|
|
8
|
+
if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
|
|
9
|
+
throw new TypeError(`${field} must be a finite number`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function validateNumericRangeConfig(config) {
|
|
13
|
+
assertFiniteNumber(config.min, "min");
|
|
14
|
+
assertFiniteNumber(config.max, "max");
|
|
15
|
+
if (config.min >= config.max) {
|
|
16
|
+
throw new RangeError("min must be less than max");
|
|
17
|
+
}
|
|
18
|
+
const step = config.step ?? numericRangeDefaults.step;
|
|
19
|
+
assertFiniteNumber(step, "step");
|
|
20
|
+
if (step <= 0) {
|
|
21
|
+
throw new RangeError("step must be greater than 0");
|
|
22
|
+
}
|
|
23
|
+
if (step > config.max - config.min) {
|
|
24
|
+
throw new RangeError("step must not exceed the min–max span");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Number of decimal digits a finite numeric boundary carries. */
|
|
28
|
+
function decimalPrecision(value) {
|
|
29
|
+
const [coefficient = "", exponentText] = value.toString().toLowerCase().split("e");
|
|
30
|
+
const dotIndex = coefficient.indexOf(".");
|
|
31
|
+
const fractionDigits = dotIndex === -1 ? 0 : coefficient.length - dotIndex - 1;
|
|
32
|
+
const exponent = exponentText === undefined ? 0 : Number(exponentText);
|
|
33
|
+
return Math.max(0, fractionDigits - exponent);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A step grid is anchored at `min`, so its exact decimal precision comes from
|
|
37
|
+
* both the origin and the increment. Looking only at `step` turns the valid
|
|
38
|
+
* sequence 0.05, 0.15, 0.25… into 0.1, 0.2, 0.3… after the first operation.
|
|
39
|
+
*/
|
|
40
|
+
function roundToStepGridPrecision(value, min, step) {
|
|
41
|
+
const decimals = Math.max(decimalPrecision(min), decimalPrecision(step));
|
|
42
|
+
if (decimals === 0)
|
|
43
|
+
return Math.round(value);
|
|
44
|
+
const shift = (number, places) => {
|
|
45
|
+
const [coefficient = "0", exponentText] = number.toString().toLowerCase().split("e");
|
|
46
|
+
const exponent = exponentText === undefined ? 0 : Number(exponentText);
|
|
47
|
+
return Number(`${coefficient}e${exponent + places}`);
|
|
48
|
+
};
|
|
49
|
+
return shift(Math.round(shift(value, decimals)), -decimals);
|
|
50
|
+
}
|
|
51
|
+
export function clampToRange(value, config) {
|
|
52
|
+
validateNumericRangeConfig(config);
|
|
53
|
+
assertFiniteNumber(value, "value");
|
|
54
|
+
return Math.min(config.max, Math.max(config.min, value));
|
|
55
|
+
}
|
|
56
|
+
/** Snaps to the nearest step from `min`, then clamps into [min, max]. */
|
|
57
|
+
export function snapToStep(value, config) {
|
|
58
|
+
validateNumericRangeConfig(config);
|
|
59
|
+
assertFiniteNumber(value, "value");
|
|
60
|
+
const step = config.step ?? numericRangeDefaults.step;
|
|
61
|
+
const steps = Math.round((value - config.min) / step);
|
|
62
|
+
const snapped = roundToStepGridPrecision(config.min + steps * step, config.min, step);
|
|
63
|
+
return clampToRange(snapped, config);
|
|
64
|
+
}
|
|
65
|
+
/** Move to the next valid grid boundary in the requested direction. */
|
|
66
|
+
export function stepNumericValue(value, config, direction) {
|
|
67
|
+
const snapped = snapToStep(value, config);
|
|
68
|
+
if (direction === "increment" && snapped > value)
|
|
69
|
+
return snapped;
|
|
70
|
+
if (direction === "decrement" && snapped < value)
|
|
71
|
+
return snapped;
|
|
72
|
+
const step = config.step ?? numericRangeDefaults.step;
|
|
73
|
+
const delta = direction === "increment" ? step : -step;
|
|
74
|
+
return snapToStep(value + delta, config);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parses the renderer's editable text without guessing at locale, currency,
|
|
78
|
+
* or unit formatting. Empty text is a first-class `null` value; `undefined`
|
|
79
|
+
* means the draft is not a complete finite decimal number yet.
|
|
80
|
+
*
|
|
81
|
+
* Exponent notation is accepted because `String(number)` may produce it for
|
|
82
|
+
* very small or large finite values. Renderers keep incomplete drafts such as
|
|
83
|
+
* `-` or `1e` locally and only commit once this parser succeeds.
|
|
84
|
+
*/
|
|
85
|
+
export function parseNumberFieldInput(input) {
|
|
86
|
+
const normalized = input.trim();
|
|
87
|
+
if (normalized.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const value = Number(normalized);
|
|
93
|
+
return Number.isFinite(value) ? value : undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Turns an editable draft into a model value at blur/submit boundaries.
|
|
97
|
+
* Direct input is clamped and snapped with the same range math used by the
|
|
98
|
+
* steppers and Slider. Invalid/incomplete text remains `undefined`, allowing a
|
|
99
|
+
* renderer to restore the last committed value without inventing an error.
|
|
100
|
+
*/
|
|
101
|
+
export function commitNumberFieldInput(input, config) {
|
|
102
|
+
validateNumericRangeConfig(config);
|
|
103
|
+
const parsed = parseNumberFieldInput(input);
|
|
104
|
+
if (parsed === undefined || parsed === null)
|
|
105
|
+
return parsed;
|
|
106
|
+
return snapToStep(parsed, config);
|
|
107
|
+
}
|
|
108
|
+
export function validateNumberFieldDescriptor(descriptor) {
|
|
109
|
+
validateNumericRangeConfig(descriptor);
|
|
110
|
+
if (descriptor.value !== null) {
|
|
111
|
+
assertFiniteNumber(descriptor.value, "value");
|
|
112
|
+
if (descriptor.value < descriptor.min || descriptor.value > descriptor.max) {
|
|
113
|
+
throw new RangeError("NumberField value must be within min and max");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function resolveNumberFieldDescriptor(descriptor) {
|
|
118
|
+
validateNumberFieldDescriptor(descriptor);
|
|
119
|
+
return { ...descriptor, step: descriptor.step ?? numericRangeDefaults.step };
|
|
120
|
+
}
|
|
121
|
+
/** Empty has no boundary yet, so neither stepper direction is disabled. */
|
|
122
|
+
export function resolveNumberFieldStepperState(descriptor) {
|
|
123
|
+
validateNumberFieldDescriptor(descriptor);
|
|
124
|
+
if (descriptor.value === null) {
|
|
125
|
+
return { incrementDisabled: false, decrementDisabled: false };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
incrementDisabled: descriptor.value >= descriptor.max,
|
|
129
|
+
decrementDisabled: descriptor.value <= descriptor.min,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Resolve stepper availability from the visible draft, falling back safely. */
|
|
133
|
+
export function resolveNumberFieldInputStepperState(input, descriptor) {
|
|
134
|
+
validateNumberFieldDescriptor(descriptor);
|
|
135
|
+
const parsed = parseNumberFieldInput(input);
|
|
136
|
+
if (parsed === undefined)
|
|
137
|
+
return resolveNumberFieldStepperState(descriptor);
|
|
138
|
+
if (parsed === null) {
|
|
139
|
+
return resolveNumberFieldStepperState({ ...descriptor, value: null });
|
|
140
|
+
}
|
|
141
|
+
return resolveNumberFieldStepperState({
|
|
142
|
+
...descriptor,
|
|
143
|
+
value: clampToRange(parsed, descriptor),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Stepping from empty lands on the boundary you are moving toward. */
|
|
147
|
+
export function stepNumberFieldValue(descriptor, direction) {
|
|
148
|
+
validateNumberFieldDescriptor(descriptor);
|
|
149
|
+
if (descriptor.value === null) {
|
|
150
|
+
return direction === "increment" ? descriptor.min : descriptor.max;
|
|
151
|
+
}
|
|
152
|
+
return stepNumericValue(descriptor.value, descriptor, direction);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolves a step action from the editable draft without first snapping and
|
|
156
|
+
* then stepping a second time. Off-grid drafts move to the next valid boundary
|
|
157
|
+
* in the requested direction; invalid drafts fall back to the committed model.
|
|
158
|
+
*/
|
|
159
|
+
export function stepNumberFieldInput(input, descriptor, direction) {
|
|
160
|
+
validateNumberFieldDescriptor(descriptor);
|
|
161
|
+
const parsed = parseNumberFieldInput(input);
|
|
162
|
+
if (parsed === undefined)
|
|
163
|
+
return stepNumberFieldValue(descriptor, direction);
|
|
164
|
+
if (parsed === null) {
|
|
165
|
+
return stepNumberFieldValue({ ...descriptor, value: null }, direction);
|
|
166
|
+
}
|
|
167
|
+
return stepNumericValue(parsed, descriptor, direction);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Reuses the Field frame (`fieldFrameContract`) and form support copy
|
|
171
|
+
* (`formSupportContract`) verbatim — a second field frame would drift from
|
|
172
|
+
* Field the first time either one's border or height changes.
|
|
173
|
+
*/
|
|
174
|
+
export const numberFieldRecipe = {
|
|
175
|
+
slots: [
|
|
176
|
+
"root",
|
|
177
|
+
"frame",
|
|
178
|
+
"input",
|
|
179
|
+
"decrement",
|
|
180
|
+
"increment",
|
|
181
|
+
"description",
|
|
182
|
+
"error",
|
|
183
|
+
],
|
|
184
|
+
defaults: { size: "medium" },
|
|
185
|
+
frame: fieldFrameContract,
|
|
186
|
+
support: formSupportContract,
|
|
187
|
+
sizes: {
|
|
188
|
+
medium: {
|
|
189
|
+
minHeight: fieldFrameContract.minHeight,
|
|
190
|
+
paddingHorizontal: fieldFrameContract.paddingHorizontal,
|
|
191
|
+
textVariant: "body",
|
|
192
|
+
stepperDiameter: control.minTouchTarget,
|
|
193
|
+
},
|
|
194
|
+
large: {
|
|
195
|
+
minHeight: control.buttonHeight.large,
|
|
196
|
+
paddingHorizontal: spacing.lg,
|
|
197
|
+
textVariant: "bodyLarge",
|
|
198
|
+
stepperDiameter: control.minTouchTarget,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
value: {
|
|
202
|
+
color: semanticColors.content.body,
|
|
203
|
+
numericVariant: "tabular",
|
|
204
|
+
},
|
|
205
|
+
stepper: {
|
|
206
|
+
color: semanticColors.content.secondary,
|
|
207
|
+
minTarget: control.minTouchTarget,
|
|
208
|
+
},
|
|
209
|
+
states: {
|
|
210
|
+
focus: focusIndicatorContract,
|
|
211
|
+
invalidBorder: semanticColors.border.danger,
|
|
212
|
+
disabledOpacity: opacity.disabled,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Held-repeat on the stepper buttons is deliberately not part of this
|
|
217
|
+
* contract: no product using NumberField has measured demand for press-and-
|
|
218
|
+
* hold, and adding it now would mean guessing at a repeat-rate/acceleration
|
|
219
|
+
* curve nobody has validated. A single activation per press only.
|
|
220
|
+
*/
|
|
221
|
+
export const numberFieldBehavior = {
|
|
222
|
+
controlled: ["value", "defaultValue", "onValueChange"],
|
|
223
|
+
stateAxes: {
|
|
224
|
+
availability: ["enabled", "disabled", "readOnly"],
|
|
225
|
+
value: ["empty", "filled"],
|
|
226
|
+
validation: ["valid", "invalid"],
|
|
227
|
+
},
|
|
228
|
+
web: {
|
|
229
|
+
roles: ["spinbutton"],
|
|
230
|
+
keyboard: ["Tab", "ArrowUp", "ArrowDown"],
|
|
231
|
+
focus: "native",
|
|
232
|
+
},
|
|
233
|
+
native: {
|
|
234
|
+
roles: ["text", "button"],
|
|
235
|
+
states: ["disabled"],
|
|
236
|
+
actions: ["focus", "setText", "increment", "decrement"],
|
|
237
|
+
},
|
|
238
|
+
scenarios: [
|
|
239
|
+
"value-and-range-are-independent-controlled-inputs",
|
|
240
|
+
"stepper-buttons-disable-at-min-and-max",
|
|
241
|
+
"empty-disables-neither-stepper-direction",
|
|
242
|
+
"keyboard-arrow-up-down-step-like-the-stepper-buttons",
|
|
243
|
+
"invalid-is-a-numberfield-only-axis-independent-of-range",
|
|
244
|
+
"held-repeat-is-not-part-of-the-contract",
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=number-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"number-field.js","sourceRoot":"","sources":["../src/number-field.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ColorReference } from \"./color-references.js\";\nimport type { BehaviorContract } from \"./behaviors.js\";\nimport {\n fieldFrameContract,\n focusIndicatorContract,\n formSupportContract,\n} from \"./component-contracts.js\";\nimport { control, opacity, spacing, type TextVariant } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * Shared by NumberField and Slider: both solve \"pick a number within a range.\"\n * Keeping min/max/step/value judgment in one place keeps the two components\n * from drifting into two different meanings of the same numeric domain.\n */\nexport type NumericRangeConfig = Readonly<{\n min: number;\n max: number;\n step?: number;\n}>;\n\nexport const numericRangeDefaults = {\n step: 1,\n} as const satisfies Readonly<{ step: number }>;\n\nexport function assertFiniteNumber(value: number, field: string): void {\n if (typeof value !== \"number\" || Number.isNaN(value) || !Number.isFinite(value)) {\n throw new TypeError(`${field} must be a finite number`);\n }\n}\n\nexport function validateNumericRangeConfig(config: NumericRangeConfig): void {\n assertFiniteNumber(config.min, \"min\");\n assertFiniteNumber(config.max, \"max\");\n if (config.min >= config.max) {\n throw new RangeError(\"min must be less than max\");\n }\n const step = config.step ?? numericRangeDefaults.step;\n assertFiniteNumber(step, \"step\");\n if (step <= 0) {\n throw new RangeError(\"step must be greater than 0\");\n }\n if (step > config.max - config.min) {\n throw new RangeError(\"step must not exceed the min–max span\");\n }\n}\n\n/** Number of decimal digits a finite numeric boundary carries. */\nfunction decimalPrecision(value: number): number {\n const [coefficient = \"\", exponentText] = value.toString().toLowerCase().split(\"e\");\n const dotIndex = coefficient.indexOf(\".\");\n const fractionDigits = dotIndex === -1 ? 0 : coefficient.length - dotIndex - 1;\n const exponent = exponentText === undefined ? 0 : Number(exponentText);\n return Math.max(0, fractionDigits - exponent);\n}\n\n/**\n * A step grid is anchored at `min`, so its exact decimal precision comes from\n * both the origin and the increment. Looking only at `step` turns the valid\n * sequence 0.05, 0.15, 0.25… into 0.1, 0.2, 0.3… after the first operation.\n */\nfunction roundToStepGridPrecision(value: number, min: number, step: number): number {\n const decimals = Math.max(decimalPrecision(min), decimalPrecision(step));\n if (decimals === 0) return Math.round(value);\n const shift = (number: number, places: number): number => {\n const [coefficient = \"0\", exponentText] = number.toString().toLowerCase().split(\"e\");\n const exponent = exponentText === undefined ? 0 : Number(exponentText);\n return Number(`${coefficient}e${exponent + places}`);\n };\n return shift(Math.round(shift(value, decimals)), -decimals);\n}\n\nexport function clampToRange(value: number, config: NumericRangeConfig): number {\n validateNumericRangeConfig(config);\n assertFiniteNumber(value, \"value\");\n return Math.min(config.max, Math.max(config.min, value));\n}\n\n/** Snaps to the nearest step from `min`, then clamps into [min, max]. */\nexport function snapToStep(value: number, config: NumericRangeConfig): number {\n validateNumericRangeConfig(config);\n assertFiniteNumber(value, \"value\");\n const step = config.step ?? numericRangeDefaults.step;\n const steps = Math.round((value - config.min) / step);\n const snapped = roundToStepGridPrecision(config.min + steps * step, config.min, step);\n return clampToRange(snapped, config);\n}\n\n/** Move to the next valid grid boundary in the requested direction. */\nexport function stepNumericValue(\n value: number,\n config: NumericRangeConfig,\n direction: \"increment\" | \"decrement\",\n): number {\n const snapped = snapToStep(value, config);\n if (direction === \"increment\" && snapped > value) return snapped;\n if (direction === \"decrement\" && snapped < value) return snapped;\n const step = config.step ?? numericRangeDefaults.step;\n const delta = direction === \"increment\" ? step : -step;\n return snapToStep(value + delta, config);\n}\n\n/**\n * `null` means no value has been entered yet — distinct from any in-range\n * number, including `min`. Products decide whether empty is allowed to submit.\n */\nexport type NumberFieldValue = number | null;\n\n/**\n * Parses the renderer's editable text without guessing at locale, currency,\n * or unit formatting. Empty text is a first-class `null` value; `undefined`\n * means the draft is not a complete finite decimal number yet.\n *\n * Exponent notation is accepted because `String(number)` may produce it for\n * very small or large finite values. Renderers keep incomplete drafts such as\n * `-` or `1e` locally and only commit once this parser succeeds.\n */\nexport function parseNumberFieldInput(input: string): NumberFieldValue | undefined {\n const normalized = input.trim();\n if (normalized.length === 0) return null;\n if (!/^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/.test(normalized)) {\n return undefined;\n }\n const value = Number(normalized);\n return Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Turns an editable draft into a model value at blur/submit boundaries.\n * Direct input is clamped and snapped with the same range math used by the\n * steppers and Slider. Invalid/incomplete text remains `undefined`, allowing a\n * renderer to restore the last committed value without inventing an error.\n */\nexport function commitNumberFieldInput(\n input: string,\n config: NumericRangeConfig,\n): NumberFieldValue | undefined {\n validateNumericRangeConfig(config);\n const parsed = parseNumberFieldInput(input);\n if (parsed === undefined || parsed === null) return parsed;\n return snapToStep(parsed, config);\n}\n\nexport type NumberFieldDescriptor = Readonly<{\n value: NumberFieldValue;\n min: number;\n max: number;\n step?: number;\n}>;\n\nexport type ResolvedNumberFieldDescriptor = Omit<NumberFieldDescriptor, \"step\"> &\n Readonly<{ step: number }>;\n\nexport function validateNumberFieldDescriptor(descriptor: NumberFieldDescriptor): void {\n validateNumericRangeConfig(descriptor);\n if (descriptor.value !== null) {\n assertFiniteNumber(descriptor.value, \"value\");\n if (descriptor.value < descriptor.min || descriptor.value > descriptor.max) {\n throw new RangeError(\"NumberField value must be within min and max\");\n }\n }\n}\n\nexport function resolveNumberFieldDescriptor(\n descriptor: NumberFieldDescriptor,\n): ResolvedNumberFieldDescriptor {\n validateNumberFieldDescriptor(descriptor);\n return { ...descriptor, step: descriptor.step ?? numericRangeDefaults.step };\n}\n\nexport type NumberFieldStepperState = Readonly<{\n incrementDisabled: boolean;\n decrementDisabled: boolean;\n}>;\n\n/** Empty has no boundary yet, so neither stepper direction is disabled. */\nexport function resolveNumberFieldStepperState(\n descriptor: NumberFieldDescriptor,\n): NumberFieldStepperState {\n validateNumberFieldDescriptor(descriptor);\n if (descriptor.value === null) {\n return { incrementDisabled: false, decrementDisabled: false };\n }\n return {\n incrementDisabled: descriptor.value >= descriptor.max,\n decrementDisabled: descriptor.value <= descriptor.min,\n };\n}\n\n/** Resolve stepper availability from the visible draft, falling back safely. */\nexport function resolveNumberFieldInputStepperState(\n input: string,\n descriptor: NumberFieldDescriptor,\n): NumberFieldStepperState {\n validateNumberFieldDescriptor(descriptor);\n const parsed = parseNumberFieldInput(input);\n if (parsed === undefined) return resolveNumberFieldStepperState(descriptor);\n if (parsed === null) {\n return resolveNumberFieldStepperState({ ...descriptor, value: null });\n }\n return resolveNumberFieldStepperState({\n ...descriptor,\n value: clampToRange(parsed, descriptor),\n });\n}\n\n/** Stepping from empty lands on the boundary you are moving toward. */\nexport function stepNumberFieldValue(\n descriptor: NumberFieldDescriptor,\n direction: \"increment\" | \"decrement\",\n): number {\n validateNumberFieldDescriptor(descriptor);\n if (descriptor.value === null) {\n return direction === \"increment\" ? descriptor.min : descriptor.max;\n }\n return stepNumericValue(descriptor.value, descriptor, direction);\n}\n\n/**\n * Resolves a step action from the editable draft without first snapping and\n * then stepping a second time. Off-grid drafts move to the next valid boundary\n * in the requested direction; invalid drafts fall back to the committed model.\n */\nexport function stepNumberFieldInput(\n input: string,\n descriptor: NumberFieldDescriptor,\n direction: \"increment\" | \"decrement\",\n): number {\n validateNumberFieldDescriptor(descriptor);\n const parsed = parseNumberFieldInput(input);\n if (parsed === undefined) return stepNumberFieldValue(descriptor, direction);\n if (parsed === null) {\n return stepNumberFieldValue({ ...descriptor, value: null }, direction);\n }\n return stepNumericValue(parsed, descriptor, direction);\n}\n\nexport type NumberFieldSize = \"medium\" | \"large\";\n\n/**\n * Reuses the Field frame (`fieldFrameContract`) and form support copy\n * (`formSupportContract`) verbatim — a second field frame would drift from\n * Field the first time either one's border or height changes.\n */\nexport const numberFieldRecipe = {\n slots: [\n \"root\",\n \"frame\",\n \"input\",\n \"decrement\",\n \"increment\",\n \"description\",\n \"error\",\n ] as const,\n defaults: { size: \"medium\" },\n frame: fieldFrameContract,\n support: formSupportContract,\n sizes: {\n medium: {\n minHeight: fieldFrameContract.minHeight,\n paddingHorizontal: fieldFrameContract.paddingHorizontal,\n textVariant: \"body\",\n stepperDiameter: control.minTouchTarget,\n },\n large: {\n minHeight: control.buttonHeight.large,\n paddingHorizontal: spacing.lg,\n textVariant: \"bodyLarge\",\n stepperDiameter: control.minTouchTarget,\n },\n },\n value: {\n color: semanticColors.content.body,\n numericVariant: \"tabular\",\n },\n stepper: {\n color: semanticColors.content.secondary,\n minTarget: control.minTouchTarget,\n },\n states: {\n focus: focusIndicatorContract,\n invalidBorder: semanticColors.border.danger,\n disabledOpacity: opacity.disabled,\n },\n} as const satisfies {\n slots: readonly [\n \"root\",\n \"frame\",\n \"input\",\n \"decrement\",\n \"increment\",\n \"description\",\n \"error\",\n ];\n defaults: { size: NumberFieldSize };\n frame: typeof fieldFrameContract;\n support: typeof formSupportContract;\n sizes: Record<\n NumberFieldSize,\n {\n minHeight: number;\n paddingHorizontal: number;\n textVariant: TextVariant;\n stepperDiameter: number;\n }\n >;\n value: { color: ColorReference; numericVariant: \"tabular\" };\n stepper: { color: ColorReference; minTarget: number };\n states: {\n focus: typeof focusIndicatorContract;\n invalidBorder: ColorReference;\n disabledOpacity: number;\n };\n};\n\n/**\n * Held-repeat on the stepper buttons is deliberately not part of this\n * contract: no product using NumberField has measured demand for press-and-\n * hold, and adding it now would mean guessing at a repeat-rate/acceleration\n * curve nobody has validated. A single activation per press only.\n */\nexport const numberFieldBehavior = {\n controlled: [\"value\", \"defaultValue\", \"onValueChange\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\", \"readOnly\"],\n value: [\"empty\", \"filled\"],\n validation: [\"valid\", \"invalid\"],\n },\n web: {\n roles: [\"spinbutton\"],\n keyboard: [\"Tab\", \"ArrowUp\", \"ArrowDown\"],\n focus: \"native\",\n },\n native: {\n roles: [\"text\", \"button\"],\n states: [\"disabled\"],\n actions: [\"focus\", \"setText\", \"increment\", \"decrement\"],\n },\n scenarios: [\n \"value-and-range-are-independent-controlled-inputs\",\n \"stepper-buttons-disable-at-min-and-max\",\n \"empty-disables-neither-stepper-direction\",\n \"keyboard-arrow-up-down-step-like-the-stepper-buttons\",\n \"invalid-is-a-numberfield-only-axis-independent-of-range\",\n \"held-repeat-is-not-part-of-the-contract\",\n ],\n} as const satisfies BehaviorContract;\n"]}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric-only, matching the domain the brief describes (인증번호). An
|
|
3
|
+
* alphanumeric OTP is out of scope until a real product needs one — adding
|
|
4
|
+
* a configurable character set now would mean guessing at a pattern nobody
|
|
5
|
+
* has validated. See docs/otp-field.md.
|
|
6
|
+
*/
|
|
7
|
+
export type OtpFieldDescriptor = Readonly<{
|
|
8
|
+
/** Number of visible slots. At least two — a single slot is not the multi-slot problem this component solves (Steps applies the same floor to its own step count for the same reason). */
|
|
9
|
+
length: number;
|
|
10
|
+
/** The one logical value, always dense — index i is filled iff i < value.length. Never sparse; see docs/otp-field.md. */
|
|
11
|
+
value: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function validateOtpFieldDescriptor(descriptor: OtpFieldDescriptor): void;
|
|
14
|
+
/** One entry per slot: the digit at that position, or "" for an unfilled slot. Always dense — no gaps. */
|
|
15
|
+
export declare function getOtpFieldSlotValues(descriptor: OtpFieldDescriptor): readonly string[];
|
|
16
|
+
export type ResolvedOtpFieldDescriptor = OtpFieldDescriptor & Readonly<{
|
|
17
|
+
slots: readonly string[];
|
|
18
|
+
complete: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function resolveOtpFieldDescriptor(descriptor: OtpFieldDescriptor): ResolvedOtpFieldDescriptor;
|
|
21
|
+
/**
|
|
22
|
+
* The single hard accessibility judgment this contract makes: OtpField is
|
|
23
|
+
* ONE focusable, one accessible-name-and-value text control, not N separate
|
|
24
|
+
* focusable slots. N separate real inputs is the well-documented
|
|
25
|
+
* accessibility anti-pattern the brief names directly — a screen reader
|
|
26
|
+
* announcing "slot 3 of 6" tells the user nothing about what they are
|
|
27
|
+
* entering. The N visible boxes are a decorative rendering of one control's
|
|
28
|
+
* characters (`getOtpFieldSlotValues`), the same way a styled `<input>` with
|
|
29
|
+
* letter-spacing can look segmented while remaining one field. See
|
|
30
|
+
* docs/otp-field.md.
|
|
31
|
+
*
|
|
32
|
+
* Because there is one real control, typing, backspacing, and pasting
|
|
33
|
+
* (anywhere, including mid-string) are the platform's own native text
|
|
34
|
+
* editing — insertion, deletion, and paste-over-a-selection are not
|
|
35
|
+
* reimplemented here. The only judgment HJM owns is what happens to the
|
|
36
|
+
* resulting raw string: keep digits only, never exceed `length`. This is
|
|
37
|
+
* intentionally the one function this module needs for every edit path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveOtpFieldValue(length: number, rawText: string): string;
|
|
40
|
+
export type OtpFieldSize = "medium" | "large";
|
|
41
|
+
/**
|
|
42
|
+
* The `slot` boxes reuse Field's own border/focus/invalid colors
|
|
43
|
+
* (`fieldFrameContract`) instead of inventing a new palette — each is a
|
|
44
|
+
* small Field-frame look-alike, not a new visual language.
|
|
45
|
+
*/
|
|
46
|
+
export declare const otpFieldRecipe: {
|
|
47
|
+
readonly slots: readonly ["root", "input", "slot", "description", "error"];
|
|
48
|
+
readonly defaults: {
|
|
49
|
+
readonly size: "medium";
|
|
50
|
+
};
|
|
51
|
+
readonly support: {
|
|
52
|
+
readonly label: {
|
|
53
|
+
readonly color: Readonly<{
|
|
54
|
+
source: "theme";
|
|
55
|
+
key: "textBody";
|
|
56
|
+
alpha?: number;
|
|
57
|
+
}>;
|
|
58
|
+
readonly textVariant: "body";
|
|
59
|
+
readonly fontWeight: "600";
|
|
60
|
+
};
|
|
61
|
+
readonly hint: {
|
|
62
|
+
readonly color: Readonly<{
|
|
63
|
+
source: "theme";
|
|
64
|
+
key: "textMuted";
|
|
65
|
+
alpha?: number;
|
|
66
|
+
}>;
|
|
67
|
+
readonly textVariant: "label";
|
|
68
|
+
};
|
|
69
|
+
readonly error: {
|
|
70
|
+
readonly color: Readonly<{
|
|
71
|
+
source: "theme";
|
|
72
|
+
key: "danger";
|
|
73
|
+
alpha?: number;
|
|
74
|
+
}>;
|
|
75
|
+
readonly textVariant: "label";
|
|
76
|
+
};
|
|
77
|
+
readonly gap: 8;
|
|
78
|
+
};
|
|
79
|
+
readonly sizes: {
|
|
80
|
+
readonly medium: {
|
|
81
|
+
readonly slotSize: 44;
|
|
82
|
+
readonly gap: 8;
|
|
83
|
+
readonly textVariant: "title";
|
|
84
|
+
};
|
|
85
|
+
readonly large: {
|
|
86
|
+
readonly slotSize: 52;
|
|
87
|
+
readonly gap: 12;
|
|
88
|
+
readonly textVariant: "titleLarge";
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
readonly slot: {
|
|
92
|
+
readonly border: Readonly<{
|
|
93
|
+
source: "theme";
|
|
94
|
+
key: "textMuted";
|
|
95
|
+
alpha?: number;
|
|
96
|
+
}>;
|
|
97
|
+
readonly focusBorder: Readonly<{
|
|
98
|
+
source: "theme";
|
|
99
|
+
key: "contentBrand";
|
|
100
|
+
alpha?: number;
|
|
101
|
+
}>;
|
|
102
|
+
readonly invalidBorder: Readonly<{
|
|
103
|
+
source: "theme";
|
|
104
|
+
key: "danger";
|
|
105
|
+
alpha?: number;
|
|
106
|
+
}>;
|
|
107
|
+
readonly filledBorder: Readonly<{
|
|
108
|
+
source: "theme";
|
|
109
|
+
key: "contentBrand";
|
|
110
|
+
alpha?: number;
|
|
111
|
+
}>;
|
|
112
|
+
readonly radius: "md";
|
|
113
|
+
readonly borderWidth: 1;
|
|
114
|
+
readonly content: Readonly<{
|
|
115
|
+
source: "theme";
|
|
116
|
+
key: "text";
|
|
117
|
+
alpha?: number;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
readonly states: {
|
|
121
|
+
readonly focus: {
|
|
122
|
+
readonly color: Readonly<{
|
|
123
|
+
source: "theme";
|
|
124
|
+
key: "contentBrand";
|
|
125
|
+
alpha?: number;
|
|
126
|
+
}>;
|
|
127
|
+
readonly width: 2;
|
|
128
|
+
readonly offset: 2;
|
|
129
|
+
};
|
|
130
|
+
readonly disabledOpacity: 0.5;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
export declare const otpFieldBehavior: {
|
|
134
|
+
readonly controlled: readonly ["value", "defaultValue", "onValueChange"];
|
|
135
|
+
readonly stateAxes: {
|
|
136
|
+
readonly availability: readonly ["enabled", "disabled", "readOnly", "busy"];
|
|
137
|
+
readonly value: readonly ["empty", "filled"];
|
|
138
|
+
readonly validation: readonly ["valid", "invalid"];
|
|
139
|
+
};
|
|
140
|
+
readonly web: {
|
|
141
|
+
readonly roles: readonly ["textbox"];
|
|
142
|
+
readonly keyboard: readonly ["Tab", "ArrowLeft", "ArrowRight", "Home", "End"];
|
|
143
|
+
readonly focus: "native";
|
|
144
|
+
};
|
|
145
|
+
readonly native: {
|
|
146
|
+
readonly roles: readonly ["text"];
|
|
147
|
+
readonly states: readonly ["disabled", "busy"];
|
|
148
|
+
readonly actions: readonly ["focus", "setText"];
|
|
149
|
+
};
|
|
150
|
+
readonly scenarios: readonly ["one-accessible-name-and-value-for-the-whole-field-never-per-slot-announcement", "one-tab-stop-native-text-editing-owns-typing-backspacing-and-paste", "paste-anywhere-in-the-value-is-sanitized-to-digits-and-clamped-to-length", "non-digit-characters-are-stripped-not-rejected-outright", "resolveOtpFieldValue-truncates-typed-or-pasted-overflow-instead-of-throwing", "a-too-long-committed-descriptor-still-throws-as-malformed-state", "alphanumeric-otp-is-out-of-scope-until-a-real-product-need-exists"];
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=otp-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp-field.d.ts","sourceRoot":"","sources":["../src/otp-field.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,0LAA0L;IAC1L,MAAM,EAAE,MAAM,CAAC;IACf,yHAAyH;IACzH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAU/E;AAED,0GAA0G;AAC1G,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,kBAAkB,GAC7B,SAAS,MAAM,EAAE,CAGnB;AAED,MAAM,MAAM,0BAA0B,GAAG,kBAAkB,GACzD,QAAQ,CAAC;IACP,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC,CAAC;AAEL,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,kBAAkB,GAC7B,0BAA0B,CAO5B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ5E;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkD1B,CAAC;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;CA0BQ,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { fieldFrameContract, focusIndicatorContract, formSupportContract, } from "./component-contracts.js";
|
|
2
|
+
import { control, opacity, spacing } from "./foundations.js";
|
|
3
|
+
import { semanticColors } from "./semantic-colors.js";
|
|
4
|
+
export function validateOtpFieldDescriptor(descriptor) {
|
|
5
|
+
if (!Number.isInteger(descriptor.length) || descriptor.length < 2) {
|
|
6
|
+
throw new RangeError("OtpField length must be an integer of at least two slots");
|
|
7
|
+
}
|
|
8
|
+
if (typeof descriptor.value !== "string" || !/^\d*$/.test(descriptor.value)) {
|
|
9
|
+
throw new TypeError("OtpField value must be a string of digits only");
|
|
10
|
+
}
|
|
11
|
+
if (descriptor.value.length > descriptor.length) {
|
|
12
|
+
throw new RangeError("OtpField value must not be longer than length");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** One entry per slot: the digit at that position, or "" for an unfilled slot. Always dense — no gaps. */
|
|
16
|
+
export function getOtpFieldSlotValues(descriptor) {
|
|
17
|
+
validateOtpFieldDescriptor(descriptor);
|
|
18
|
+
return Array.from({ length: descriptor.length }, (_, index) => descriptor.value[index] ?? "");
|
|
19
|
+
}
|
|
20
|
+
export function resolveOtpFieldDescriptor(descriptor) {
|
|
21
|
+
validateOtpFieldDescriptor(descriptor);
|
|
22
|
+
return {
|
|
23
|
+
...descriptor,
|
|
24
|
+
slots: getOtpFieldSlotValues(descriptor),
|
|
25
|
+
complete: descriptor.value.length === descriptor.length,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The single hard accessibility judgment this contract makes: OtpField is
|
|
30
|
+
* ONE focusable, one accessible-name-and-value text control, not N separate
|
|
31
|
+
* focusable slots. N separate real inputs is the well-documented
|
|
32
|
+
* accessibility anti-pattern the brief names directly — a screen reader
|
|
33
|
+
* announcing "slot 3 of 6" tells the user nothing about what they are
|
|
34
|
+
* entering. The N visible boxes are a decorative rendering of one control's
|
|
35
|
+
* characters (`getOtpFieldSlotValues`), the same way a styled `<input>` with
|
|
36
|
+
* letter-spacing can look segmented while remaining one field. See
|
|
37
|
+
* docs/otp-field.md.
|
|
38
|
+
*
|
|
39
|
+
* Because there is one real control, typing, backspacing, and pasting
|
|
40
|
+
* (anywhere, including mid-string) are the platform's own native text
|
|
41
|
+
* editing — insertion, deletion, and paste-over-a-selection are not
|
|
42
|
+
* reimplemented here. The only judgment HJM owns is what happens to the
|
|
43
|
+
* resulting raw string: keep digits only, never exceed `length`. This is
|
|
44
|
+
* intentionally the one function this module needs for every edit path.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveOtpFieldValue(length, rawText) {
|
|
47
|
+
if (!Number.isInteger(length) || length < 2) {
|
|
48
|
+
throw new RangeError("OtpField length must be an integer of at least two slots");
|
|
49
|
+
}
|
|
50
|
+
if (typeof rawText !== "string") {
|
|
51
|
+
throw new TypeError("OtpField rawText must be a string");
|
|
52
|
+
}
|
|
53
|
+
return rawText.replace(/\D/g, "").slice(0, length);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The `slot` boxes reuse Field's own border/focus/invalid colors
|
|
57
|
+
* (`fieldFrameContract`) instead of inventing a new palette — each is a
|
|
58
|
+
* small Field-frame look-alike, not a new visual language.
|
|
59
|
+
*/
|
|
60
|
+
export const otpFieldRecipe = {
|
|
61
|
+
slots: ["root", "input", "slot", "description", "error"],
|
|
62
|
+
defaults: { size: "medium" },
|
|
63
|
+
support: formSupportContract,
|
|
64
|
+
sizes: {
|
|
65
|
+
medium: {
|
|
66
|
+
slotSize: control.minTouchTarget,
|
|
67
|
+
gap: spacing.xs,
|
|
68
|
+
textVariant: "title",
|
|
69
|
+
},
|
|
70
|
+
large: {
|
|
71
|
+
slotSize: control.buttonHeight.large,
|
|
72
|
+
gap: spacing.sm,
|
|
73
|
+
textVariant: "titleLarge",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
slot: {
|
|
77
|
+
border: fieldFrameContract.border,
|
|
78
|
+
focusBorder: fieldFrameContract.focusBorder,
|
|
79
|
+
invalidBorder: fieldFrameContract.invalidBorder,
|
|
80
|
+
filledBorder: semanticColors.content.brand,
|
|
81
|
+
radius: fieldFrameContract.radius,
|
|
82
|
+
borderWidth: fieldFrameContract.borderWidth,
|
|
83
|
+
content: semanticColors.content.primary,
|
|
84
|
+
},
|
|
85
|
+
states: {
|
|
86
|
+
focus: focusIndicatorContract,
|
|
87
|
+
disabledOpacity: opacity.disabled,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export const otpFieldBehavior = {
|
|
91
|
+
controlled: ["value", "defaultValue", "onValueChange"],
|
|
92
|
+
stateAxes: {
|
|
93
|
+
availability: ["enabled", "disabled", "readOnly", "busy"],
|
|
94
|
+
value: ["empty", "filled"],
|
|
95
|
+
validation: ["valid", "invalid"],
|
|
96
|
+
},
|
|
97
|
+
web: {
|
|
98
|
+
roles: ["textbox"],
|
|
99
|
+
keyboard: ["Tab", "ArrowLeft", "ArrowRight", "Home", "End"],
|
|
100
|
+
focus: "native",
|
|
101
|
+
},
|
|
102
|
+
native: {
|
|
103
|
+
roles: ["text"],
|
|
104
|
+
states: ["disabled", "busy"],
|
|
105
|
+
actions: ["focus", "setText"],
|
|
106
|
+
},
|
|
107
|
+
scenarios: [
|
|
108
|
+
"one-accessible-name-and-value-for-the-whole-field-never-per-slot-announcement",
|
|
109
|
+
"one-tab-stop-native-text-editing-owns-typing-backspacing-and-paste",
|
|
110
|
+
"paste-anywhere-in-the-value-is-sanitized-to-digits-and-clamped-to-length",
|
|
111
|
+
"non-digit-characters-are-stripped-not-rejected-outright",
|
|
112
|
+
"resolveOtpFieldValue-truncates-typed-or-pasted-overflow-instead-of-throwing",
|
|
113
|
+
"a-too-long-committed-descriptor-still-throws-as-malformed-state",
|
|
114
|
+
"alphanumeric-otp-is-out-of-scope-until-a-real-product-need-exists",
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=otp-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp-field.js","sourceRoot":"","sources":["../src/otp-field.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport {\n fieldFrameContract,\n focusIndicatorContract,\n formSupportContract,\n} from \"./component-contracts.js\";\nimport { control, opacity, spacing, type TextVariant } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * Numeric-only, matching the domain the brief describes (인증번호). An\n * alphanumeric OTP is out of scope until a real product needs one — adding\n * a configurable character set now would mean guessing at a pattern nobody\n * has validated. See docs/otp-field.md.\n */\nexport type OtpFieldDescriptor = Readonly<{\n /** Number of visible slots. At least two — a single slot is not the multi-slot problem this component solves (Steps applies the same floor to its own step count for the same reason). */\n length: number;\n /** The one logical value, always dense — index i is filled iff i < value.length. Never sparse; see docs/otp-field.md. */\n value: string;\n}>;\n\nexport function validateOtpFieldDescriptor(descriptor: OtpFieldDescriptor): void {\n if (!Number.isInteger(descriptor.length) || descriptor.length < 2) {\n throw new RangeError(\"OtpField length must be an integer of at least two slots\");\n }\n if (typeof descriptor.value !== \"string\" || !/^\\d*$/.test(descriptor.value)) {\n throw new TypeError(\"OtpField value must be a string of digits only\");\n }\n if (descriptor.value.length > descriptor.length) {\n throw new RangeError(\"OtpField value must not be longer than length\");\n }\n}\n\n/** One entry per slot: the digit at that position, or \"\" for an unfilled slot. Always dense — no gaps. */\nexport function getOtpFieldSlotValues(\n descriptor: OtpFieldDescriptor,\n): readonly string[] {\n validateOtpFieldDescriptor(descriptor);\n return Array.from({ length: descriptor.length }, (_, index) => descriptor.value[index] ?? \"\");\n}\n\nexport type ResolvedOtpFieldDescriptor = OtpFieldDescriptor &\n Readonly<{\n slots: readonly string[];\n complete: boolean;\n }>;\n\nexport function resolveOtpFieldDescriptor(\n descriptor: OtpFieldDescriptor,\n): ResolvedOtpFieldDescriptor {\n validateOtpFieldDescriptor(descriptor);\n return {\n ...descriptor,\n slots: getOtpFieldSlotValues(descriptor),\n complete: descriptor.value.length === descriptor.length,\n };\n}\n\n/**\n * The single hard accessibility judgment this contract makes: OtpField is\n * ONE focusable, one accessible-name-and-value text control, not N separate\n * focusable slots. N separate real inputs is the well-documented\n * accessibility anti-pattern the brief names directly — a screen reader\n * announcing \"slot 3 of 6\" tells the user nothing about what they are\n * entering. The N visible boxes are a decorative rendering of one control's\n * characters (`getOtpFieldSlotValues`), the same way a styled `<input>` with\n * letter-spacing can look segmented while remaining one field. See\n * docs/otp-field.md.\n *\n * Because there is one real control, typing, backspacing, and pasting\n * (anywhere, including mid-string) are the platform's own native text\n * editing — insertion, deletion, and paste-over-a-selection are not\n * reimplemented here. The only judgment HJM owns is what happens to the\n * resulting raw string: keep digits only, never exceed `length`. This is\n * intentionally the one function this module needs for every edit path.\n */\nexport function resolveOtpFieldValue(length: number, rawText: string): string {\n if (!Number.isInteger(length) || length < 2) {\n throw new RangeError(\"OtpField length must be an integer of at least two slots\");\n }\n if (typeof rawText !== \"string\") {\n throw new TypeError(\"OtpField rawText must be a string\");\n }\n return rawText.replace(/\\D/g, \"\").slice(0, length);\n}\n\nexport type OtpFieldSize = \"medium\" | \"large\";\n\n/**\n * The `slot` boxes reuse Field's own border/focus/invalid colors\n * (`fieldFrameContract`) instead of inventing a new palette — each is a\n * small Field-frame look-alike, not a new visual language.\n */\nexport const otpFieldRecipe = {\n slots: [\"root\", \"input\", \"slot\", \"description\", \"error\"] as const,\n defaults: { size: \"medium\" },\n support: formSupportContract,\n sizes: {\n medium: {\n slotSize: control.minTouchTarget,\n gap: spacing.xs,\n textVariant: \"title\",\n },\n large: {\n slotSize: control.buttonHeight.large,\n gap: spacing.sm,\n textVariant: \"titleLarge\",\n },\n },\n slot: {\n border: fieldFrameContract.border,\n focusBorder: fieldFrameContract.focusBorder,\n invalidBorder: fieldFrameContract.invalidBorder,\n filledBorder: semanticColors.content.brand,\n radius: fieldFrameContract.radius,\n borderWidth: fieldFrameContract.borderWidth,\n content: semanticColors.content.primary,\n },\n states: {\n focus: focusIndicatorContract,\n disabledOpacity: opacity.disabled,\n },\n} as const satisfies {\n slots: readonly string[];\n defaults: { size: OtpFieldSize };\n support: typeof formSupportContract;\n sizes: Record<\n OtpFieldSize,\n { slotSize: number; gap: number; textVariant: TextVariant }\n >;\n slot: {\n border: ColorReference;\n focusBorder: ColorReference;\n invalidBorder: ColorReference;\n filledBorder: ColorReference;\n radius: typeof fieldFrameContract.radius;\n borderWidth: number;\n content: ColorReference;\n };\n states: {\n focus: typeof focusIndicatorContract;\n disabledOpacity: number;\n };\n};\n\nexport const otpFieldBehavior = {\n controlled: [\"value\", \"defaultValue\", \"onValueChange\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\", \"readOnly\", \"busy\"],\n value: [\"empty\", \"filled\"],\n validation: [\"valid\", \"invalid\"],\n },\n web: {\n roles: [\"textbox\"],\n keyboard: [\"Tab\", \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\"],\n focus: \"native\",\n },\n native: {\n roles: [\"text\"],\n states: [\"disabled\", \"busy\"],\n actions: [\"focus\", \"setText\"],\n },\n scenarios: [\n \"one-accessible-name-and-value-for-the-whole-field-never-per-slot-announcement\",\n \"one-tab-stop-native-text-editing-owns-typing-backspacing-and-paste\",\n \"paste-anywhere-in-the-value-is-sanitized-to-digits-and-clamped-to-length\",\n \"non-digit-characters-are-stripped-not-rejected-outright\",\n \"resolveOtpFieldValue-truncates-typed-or-pasted-overflow-instead-of-throwing\",\n \"a-too-long-committed-descriptor-still-throws-as-malformed-state\",\n \"alphanumeric-otp-is-out-of-scope-until-a-real-product-need-exists\",\n ],\n} as const satisfies BehaviorContract;\n"]}
|