@hjmds/design-contracts 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agreement.d.ts +183 -0
- package/dist/agreement.d.ts.map +1 -0
- package/dist/agreement.js +137 -0
- package/dist/agreement.js.map +1 -0
- package/dist/anchor.d.ts +62 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +43 -0
- package/dist/anchor.js.map +1 -0
- package/dist/asset.d.ts +94 -0
- package/dist/asset.d.ts.map +1 -0
- package/dist/asset.js +60 -0
- package/dist/asset.js.map +1 -0
- package/dist/base-recipes.d.ts +19 -0
- package/dist/base-recipes.d.ts.map +1 -1
- package/dist/base-recipes.js +17 -0
- package/dist/base-recipes.js.map +1 -1
- package/dist/behaviors.d.ts +346 -3
- package/dist/behaviors.d.ts.map +1 -1
- package/dist/behaviors.js +35 -1
- package/dist/behaviors.js.map +1 -1
- package/dist/bottom-info.d.ts +58 -0
- package/dist/bottom-info.d.ts.map +1 -0
- package/dist/bottom-info.js +48 -0
- package/dist/bottom-info.js.map +1 -0
- package/dist/carousel.d.ts +3 -3
- package/dist/carousel.js +1 -1
- package/dist/carousel.js.map +1 -1
- package/dist/catalog.d.ts +1250 -239
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +65 -25
- package/dist/catalog.js.map +1 -1
- package/dist/collapsible.d.ts +56 -0
- package/dist/collapsible.d.ts.map +1 -0
- package/dist/collapsible.js +37 -0
- package/dist/collapsible.js.map +1 -0
- package/dist/component-definitions.d.ts +16 -0
- package/dist/component-definitions.d.ts.map +1 -1
- package/dist/component-definitions.js +16 -0
- package/dist/component-definitions.js.map +1 -1
- package/dist/component-recipes.d.ts +20 -3
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +24 -1
- package/dist/component-recipes.js.map +1 -1
- package/dist/context-menu.d.ts +53 -0
- package/dist/context-menu.d.ts.map +1 -0
- package/dist/context-menu.js +44 -0
- package/dist/context-menu.js.map +1 -0
- package/dist/counter-badge-recipe.d.ts +5 -0
- package/dist/counter-badge-recipe.d.ts.map +1 -1
- package/dist/counter-badge-recipe.js +5 -0
- package/dist/counter-badge-recipe.js.map +1 -1
- package/dist/dataviz.d.ts +76 -0
- package/dist/dataviz.d.ts.map +1 -0
- package/dist/dataviz.js +58 -0
- package/dist/dataviz.js.map +1 -0
- package/dist/date-range.d.ts +56 -0
- package/dist/date-range.d.ts.map +1 -0
- package/dist/date-range.js +79 -0
- package/dist/date-range.js.map +1 -0
- package/dist/design-system-provider.d.ts +24 -0
- package/dist/design-system-provider.d.ts.map +1 -1
- package/dist/design-system-provider.js +21 -0
- package/dist/design-system-provider.js.map +1 -1
- package/dist/floating-action-button.d.ts +1 -1
- package/dist/floating-action-button.d.ts.map +1 -1
- package/dist/floating-action-button.js +3 -1
- package/dist/floating-action-button.js.map +1 -1
- package/dist/formatters.d.ts +37 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +69 -0
- package/dist/formatters.js.map +1 -0
- package/dist/heading.d.ts +82 -0
- package/dist/heading.d.ts.map +1 -0
- package/dist/heading.js +48 -0
- package/dist/heading.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/menubar.d.ts +133 -0
- package/dist/menubar.d.ts.map +1 -0
- package/dist/menubar.js +80 -0
- package/dist/menubar.js.map +1 -0
- package/dist/native-platform.d.ts +88 -0
- package/dist/native-platform.d.ts.map +1 -0
- package/dist/native-platform.js +70 -0
- package/dist/native-platform.js.map +1 -0
- package/dist/popover.d.ts +19 -4
- package/dist/popover.d.ts.map +1 -1
- package/dist/popover.js +3 -0
- package/dist/popover.js.map +1 -1
- package/dist/progress-recipe.d.ts +14 -0
- package/dist/progress-recipe.d.ts.map +1 -1
- package/dist/progress-recipe.js +14 -1
- package/dist/progress-recipe.js.map +1 -1
- package/dist/provider-button.d.ts +140 -0
- package/dist/provider-button.d.ts.map +1 -0
- package/dist/provider-button.js +83 -0
- package/dist/provider-button.js.map +1 -0
- package/dist/recipes.d.ts +15 -1
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js +14 -0
- package/dist/recipes.js.map +1 -1
- package/dist/sheet.d.ts +16 -0
- package/dist/sheet.d.ts.map +1 -1
- package/dist/sheet.js +33 -0
- package/dist/sheet.js.map +1 -1
- package/dist/sidebar.d.ts +168 -0
- package/dist/sidebar.d.ts.map +1 -0
- package/dist/sidebar.js +98 -0
- package/dist/sidebar.js.map +1 -0
- package/dist/skip-nav.d.ts +67 -0
- package/dist/skip-nav.d.ts.map +1 -0
- package/dist/skip-nav.js +47 -0
- package/dist/skip-nav.js.map +1 -0
- package/dist/tags-input.d.ts +157 -0
- package/dist/tags-input.d.ts.map +1 -0
- package/dist/tags-input.js +108 -0
- package/dist/tags-input.js.map +1 -0
- package/dist/text-formats.d.ts +85 -0
- package/dist/text-formats.d.ts.map +1 -0
- package/dist/text-formats.js +43 -0
- package/dist/text-formats.js.map +1 -0
- package/dist/toggle-group.d.ts +111 -0
- package/dist/toggle-group.d.ts.map +1 -0
- package/dist/toggle-group.js +78 -0
- package/dist/toggle-group.js.map +1 -0
- package/dist/top.d.ts +113 -0
- package/dist/top.d.ts.map +1 -0
- package/dist/top.js +72 -0
- package/dist/top.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/agreement.md +33 -0
- package/docs/anchor.md +59 -59
- package/docs/ant-design-coverage.md +5 -3
- package/docs/asset.md +26 -0
- package/docs/bottom-info.md +15 -0
- package/docs/breadcrumb.md +15 -15
- package/docs/button-label.md +20 -0
- package/docs/calendar.md +82 -154
- package/docs/carousel.md +27 -2
- package/docs/cascader.md +17 -0
- package/docs/chart.md +33 -0
- package/docs/clipboard.md +13 -0
- package/docs/collapsible.md +19 -0
- package/docs/command-palette.md +19 -0
- package/docs/confirm-popover.md +11 -1
- package/docs/context-menu.md +23 -0
- package/docs/data-table.md +22 -0
- package/docs/date-range.md +31 -0
- package/docs/density.md +23 -0
- package/docs/expansion-roadmap.md +16 -3
- package/docs/floating-action-button.md +35 -3
- package/docs/formatters.md +18 -0
- package/docs/generated/component-maturity.md +39 -23
- package/docs/generated/renderer-evidence.json +2921 -642
- package/docs/generated/renderer-evidence.md +53 -6
- package/docs/generated/showcase-manifest.json +1087 -64
- package/docs/heading.md +25 -0
- package/docs/identity.md +5 -0
- package/docs/layout.md +1 -1
- package/docs/library-gap-analysis.md +29 -9
- package/docs/list-row.md +14 -0
- package/docs/mentions.md +18 -0
- package/docs/menubar.md +22 -0
- package/docs/native-platform.md +36 -0
- package/docs/overlay-stack.md +24 -0
- package/docs/pagination.md +16 -8
- package/docs/popover.md +42 -13
- package/docs/product-audit-2026-09-15.md +88 -0
- package/docs/progress.md +16 -0
- package/docs/provider-button.md +33 -0
- package/docs/rating.md +19 -0
- package/docs/react-native-completion.md +347 -0
- package/docs/screen-chrome.md +52 -0
- package/docs/side-panel.md +23 -2
- package/docs/sidebar.md +30 -0
- package/docs/skip-nav.md +20 -0
- package/docs/splitter.md +22 -2
- package/docs/stable-promotion.md +51 -0
- package/docs/tags-input.md +25 -0
- package/docs/text-formats.md +15 -0
- package/docs/theming.md +84 -0
- package/docs/time-picker.md +22 -1
- package/docs/toast.md +18 -0
- package/docs/toggle-group.md +21 -0
- package/docs/top.md +32 -0
- package/docs/tour.md +23 -2
- package/docs/transfer-list.md +20 -0
- package/docs/tree-select.md +18 -0
- package/docs/tree.md +21 -0
- package/package.json +120 -6
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { type CheckboxSelectionItem, type CheckboxState } from "./selection-helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* 가입·결제 앞에 서는 "약관 동의" 묶음이다. CheckboxGroup으로 매번 조립하던 것을
|
|
4
|
+
* 계약으로 올리는 이유는 세 가지가 **체크박스 목록이 아니라 법적 절차**이기 때문이다.
|
|
5
|
+
*
|
|
6
|
+
* 1. 필수/선택이 제출 가능 여부를 결정한다 — CheckboxGroup은 그 판정을 갖지 않는다.
|
|
7
|
+
* 2. 전체 동의는 자식의 합이지 별도 항목이 아니다(DataTable 머리글·TreeSelect 부모와
|
|
8
|
+
* 같은 파생 관계). 저장되는 것은 개별 항목뿐이다.
|
|
9
|
+
* 3. 각 항목은 "읽을 수 있어야" 한다 — 상세 보기 경로가 없는 동의는 동의가 아니다.
|
|
10
|
+
* 그래서 `detail`은 renderer가 장식으로 넣는 화살표가 아니라 계약된 행동이다.
|
|
11
|
+
*
|
|
12
|
+
* 문구·링크 주소·법적 유효성은 제품이 소유한다. 이 계약은 판정과 배치만 갖는다.
|
|
13
|
+
*/
|
|
14
|
+
export type AgreementItemDescriptor<Id extends string = string> = CheckboxSelectionItem<Id> & Readonly<{
|
|
15
|
+
/** 필수 항목은 하나라도 비면 제출을 막는다. 기본값은 선택(false). */
|
|
16
|
+
required?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 전문을 여는 경로. 제품이 링크(`href`)로 열지 시트로 열지는 renderer 밖의
|
|
19
|
+
* 결정이라 여기서는 "열 수 있다"는 사실만 계약한다.
|
|
20
|
+
*/
|
|
21
|
+
detail?: Readonly<{
|
|
22
|
+
label: string;
|
|
23
|
+
href?: string;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
export type AgreementDescriptor<Id extends string = string> = Readonly<{
|
|
27
|
+
/** 묶음 전체의 접근성 이름. "약관 동의"처럼 제품이 현지화한다. */
|
|
28
|
+
accessibilityLabel: string;
|
|
29
|
+
/** 전체 동의 행의 라벨. 필수이므로 renderer가 임의 문구를 만들지 않는다. */
|
|
30
|
+
allLabel: string;
|
|
31
|
+
items: readonly AgreementItemDescriptor<Id>[];
|
|
32
|
+
}>;
|
|
33
|
+
export declare function validateAgreementDescriptor<Id extends string>(descriptor: AgreementDescriptor<Id>): void;
|
|
34
|
+
export type AgreementState<Id extends string = string> = Readonly<{
|
|
35
|
+
/** 전체 동의 행이 그릴 tri-state. 개별 항목의 합에서만 나온다. */
|
|
36
|
+
all: CheckboxState;
|
|
37
|
+
/** 필수 항목이 모두 체크됐는가 — 제품의 제출 버튼이 읽는 값. */
|
|
38
|
+
satisfied: boolean;
|
|
39
|
+
/** 아직 체크되지 않은 필수 항목. 제품이 "무엇이 남았는지" 문장을 만든다. */
|
|
40
|
+
missingRequiredIds: readonly Id[];
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* 비활성 항목은 분모에서 뺀다 — 사용자가 바꿀 수 없는 것을 "덜 동의했다"고 셀 수
|
|
44
|
+
* 없기 때문이다(`resolveDataTableSelectAllState`·`resolveTreeCheckedStates`와 같은 규칙).
|
|
45
|
+
* 필수 항목은 비활성일 수 없으므로(위 validate) `satisfied` 판정에는 이 예외가 없다.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveAgreementState<Id extends string>(descriptor: AgreementDescriptor<Id>, checkedIds: ReadonlySet<Id>): AgreementState<Id>;
|
|
48
|
+
/** 개별 항목 토글. 비활성 항목 보호는 공용 helper가 이미 갖고 있다. */
|
|
49
|
+
export declare function toggleAgreementItem<Id extends string>(descriptor: AgreementDescriptor<Id>, checkedIds: ReadonlySet<Id>, id: Id): ReadonlySet<Id>;
|
|
50
|
+
/**
|
|
51
|
+
* 전체 동의는 "모두 체크됨"의 반대로 간다 — mixed에서 누르면 채운다(공용
|
|
52
|
+
* `getCheckboxNextState`의 기본값과 같은 관습). 비활성 항목의 현재 상태는 건드리지
|
|
53
|
+
* 않는다: 그 값은 사용자가 만든 것이 아니라 제품이 고정한 것이다.
|
|
54
|
+
*/
|
|
55
|
+
export declare function toggleAgreementAll<Id extends string>(descriptor: AgreementDescriptor<Id>, checkedIds: ReadonlySet<Id>): ReadonlySet<Id>;
|
|
56
|
+
/** 목록에서 사라진 항목의 동의는 남기지 않는다. */
|
|
57
|
+
export declare function reconcileAgreementSelection<Id extends string>(descriptor: AgreementDescriptor<Id>, checkedIds: ReadonlySet<Id>): ReadonlySet<Id>;
|
|
58
|
+
/**
|
|
59
|
+
* 전체 동의 행은 개별 행보다 시각적으로 무겁다 — 목록의 제목 역할을 하기 때문이다.
|
|
60
|
+
* 나머지는 `collectionItemContract`를 그대로 쓴다(Menu·Tree·ListRow와 같은 행 chrome).
|
|
61
|
+
*/
|
|
62
|
+
export declare const agreementRecipe: {
|
|
63
|
+
readonly slots: readonly ["root", "all", "list", "item", "label", "detail"];
|
|
64
|
+
readonly all: {
|
|
65
|
+
readonly minHeight: 44;
|
|
66
|
+
readonly gap: 12;
|
|
67
|
+
readonly paddingHorizontal: 16;
|
|
68
|
+
readonly paddingVertical: 12;
|
|
69
|
+
readonly background: Readonly<{
|
|
70
|
+
source: "theme";
|
|
71
|
+
key: "surfaceAlt";
|
|
72
|
+
alpha?: number;
|
|
73
|
+
}>;
|
|
74
|
+
readonly radius: "md";
|
|
75
|
+
readonly textVariant: "bodyLarge";
|
|
76
|
+
readonly color: Readonly<{
|
|
77
|
+
source: "theme";
|
|
78
|
+
key: "text";
|
|
79
|
+
alpha?: number;
|
|
80
|
+
}>;
|
|
81
|
+
};
|
|
82
|
+
readonly item: {
|
|
83
|
+
readonly minHeight: 44;
|
|
84
|
+
readonly paddingHorizontal: 12;
|
|
85
|
+
readonly gap: 12;
|
|
86
|
+
readonly radius: "md";
|
|
87
|
+
readonly label: {
|
|
88
|
+
readonly color: Readonly<{
|
|
89
|
+
source: "theme";
|
|
90
|
+
key: "textBody";
|
|
91
|
+
alpha?: number;
|
|
92
|
+
}>;
|
|
93
|
+
readonly textVariant: "body";
|
|
94
|
+
};
|
|
95
|
+
readonly description: {
|
|
96
|
+
readonly color: Readonly<{
|
|
97
|
+
source: "theme";
|
|
98
|
+
key: "textMuted";
|
|
99
|
+
alpha?: number;
|
|
100
|
+
}>;
|
|
101
|
+
readonly textVariant: "label";
|
|
102
|
+
};
|
|
103
|
+
readonly highlightedBackground: Readonly<{
|
|
104
|
+
source: "theme";
|
|
105
|
+
key: "text";
|
|
106
|
+
alpha?: number;
|
|
107
|
+
}>;
|
|
108
|
+
readonly focus: {
|
|
109
|
+
readonly color: Readonly<{
|
|
110
|
+
source: "theme";
|
|
111
|
+
key: "contentBrand";
|
|
112
|
+
alpha?: number;
|
|
113
|
+
}>;
|
|
114
|
+
readonly width: 2;
|
|
115
|
+
readonly offset: 2;
|
|
116
|
+
};
|
|
117
|
+
readonly selectedBackground: Readonly<{
|
|
118
|
+
source: "theme";
|
|
119
|
+
key: "primary";
|
|
120
|
+
alpha?: number;
|
|
121
|
+
}>;
|
|
122
|
+
readonly selectedIndicator: Readonly<{
|
|
123
|
+
source: "theme";
|
|
124
|
+
key: "contentBrand";
|
|
125
|
+
alpha?: number;
|
|
126
|
+
}>;
|
|
127
|
+
readonly danger: Readonly<{
|
|
128
|
+
source: "theme";
|
|
129
|
+
key: "danger";
|
|
130
|
+
alpha?: number;
|
|
131
|
+
}>;
|
|
132
|
+
};
|
|
133
|
+
readonly detail: {
|
|
134
|
+
readonly color: Readonly<{
|
|
135
|
+
source: "theme";
|
|
136
|
+
key: "textMuted";
|
|
137
|
+
alpha?: number;
|
|
138
|
+
}>;
|
|
139
|
+
readonly textVariant: "label";
|
|
140
|
+
readonly minHeight: 44;
|
|
141
|
+
};
|
|
142
|
+
readonly required: {
|
|
143
|
+
readonly color: Readonly<{
|
|
144
|
+
source: "theme";
|
|
145
|
+
key: "contentBrand";
|
|
146
|
+
alpha?: number;
|
|
147
|
+
}>;
|
|
148
|
+
readonly textVariant: "label";
|
|
149
|
+
};
|
|
150
|
+
readonly gap: 8;
|
|
151
|
+
readonly states: {
|
|
152
|
+
readonly focus: {
|
|
153
|
+
readonly color: Readonly<{
|
|
154
|
+
source: "theme";
|
|
155
|
+
key: "contentBrand";
|
|
156
|
+
alpha?: number;
|
|
157
|
+
}>;
|
|
158
|
+
readonly width: 2;
|
|
159
|
+
readonly offset: 2;
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
export declare const agreementBehavior: {
|
|
164
|
+
readonly controlled: readonly ["checkedIds", "defaultCheckedIds", "onCheckedIdsChange"];
|
|
165
|
+
readonly inputs: readonly ["items", "allLabel", "accessibilityLabel"];
|
|
166
|
+
readonly events: readonly ["onDetail"];
|
|
167
|
+
readonly stateAxes: {
|
|
168
|
+
readonly availability: readonly ["enabled", "disabled"];
|
|
169
|
+
readonly value: readonly ["unchecked", "checked", "mixed"];
|
|
170
|
+
};
|
|
171
|
+
readonly web: {
|
|
172
|
+
readonly roles: readonly ["group", "checkbox", "link", "button"];
|
|
173
|
+
readonly keyboard: readonly ["Tab", "Space", "Enter"];
|
|
174
|
+
readonly focus: "native";
|
|
175
|
+
};
|
|
176
|
+
readonly native: {
|
|
177
|
+
readonly roles: readonly ["checkbox", "link", "button"];
|
|
178
|
+
readonly states: readonly ["checked", "disabled"];
|
|
179
|
+
readonly actions: readonly ["toggle", "toggleAll", "openDetail"];
|
|
180
|
+
};
|
|
181
|
+
readonly scenarios: readonly ["all-agree-is-derived-from-the-items-never-stored-as-its-own-value", "checking-all-checks-every-enabled-item-and-leaves-disabled-ones-untouched", "required-items-alone-decide-whether-the-product-may-submit", "a-required-item-can-never-be-disabled-the-descriptor-is-rejected", "every-item-may-open-its-full-text-and-that-control-is-a-separate-tab-stop", "the-detail-control-never-toggles-the-agreement-it-only-opens-the-text", "optional-items-never-block-submission-and-are-announced-as-optional", "removing-an-item-drops-its-consent-instead-of-keeping-an-orphan-id"];
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=agreement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement.d.ts","sourceRoot":"","sources":["../src/agreement.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EACnB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,uBAAuB,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAC5D,qBAAqB,CAAC,EAAE,CAAC,GACvB,QAAQ,CAAC;IACP,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD,CAAC,CAAC;AAEP,MAAM,MAAM,mBAAmB,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IACrE,0CAA0C;IAC1C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,uBAAuB,CAAC,EAAE,CAAC,EAAE,CAAC;CAC/C,CAAC,CAAC;AAQH,wBAAgB,2BAA2B,CAAC,EAAE,SAAS,MAAM,EAC3D,UAAU,EAAE,mBAAmB,CAAC,EAAE,CAAC,GAClC,IAAI,CAyBN;AAED,MAAM,MAAM,cAAc,CAAC,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI,QAAQ,CAAC;IAChE,8CAA8C;IAC9C,GAAG,EAAE,aAAa,CAAC;IACnB,yCAAyC;IACzC,SAAS,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,kBAAkB,EAAE,SAAS,EAAE,EAAE,CAAC;CACnC,CAAC,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,SAAS,MAAM,EACrD,UAAU,EAAE,mBAAmB,CAAC,EAAE,CAAC,EACnC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,GAC1B,cAAc,CAAC,EAAE,CAAC,CAapB;AAED,gDAAgD;AAChD,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,MAAM,EACnD,UAAU,EAAE,mBAAmB,CAAC,EAAE,CAAC,EACnC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,EAC3B,EAAE,EAAE,EAAE,GACL,WAAW,CAAC,EAAE,CAAC,CAGjB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAClD,UAAU,EAAE,mBAAmB,CAAC,EAAE,CAAC,EACnC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,GAC1B,WAAW,CAAC,EAAE,CAAC,CASjB;AAED,gCAAgC;AAChC,wBAAgB,2BAA2B,CAAC,EAAE,SAAS,MAAM,EAC3D,UAAU,EAAE,mBAAmB,CAAC,EAAE,CAAC,EACnC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC,GAC1B,WAAW,CAAC,EAAE,CAAC,CAGjB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsC3B,CAAC;AAEF,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;CA4BO,CAAC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { collectionItemContract, focusIndicatorContract } from "./component-contracts.js";
|
|
2
|
+
import { control, spacing } from "./foundations.js";
|
|
3
|
+
import { semanticColors } from "./semantic-colors.js";
|
|
4
|
+
import { reconcileCheckboxSelection, toggleCheckboxSelection, } from "./selection-helpers.js";
|
|
5
|
+
function assertNonEmpty(value, field) {
|
|
6
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
7
|
+
throw new TypeError(`Agreement ${field} must not be empty`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function validateAgreementDescriptor(descriptor) {
|
|
11
|
+
assertNonEmpty(descriptor.accessibilityLabel, "accessibilityLabel");
|
|
12
|
+
assertNonEmpty(descriptor.allLabel, "allLabel");
|
|
13
|
+
if (!Array.isArray(descriptor.items) || descriptor.items.length === 0) {
|
|
14
|
+
throw new RangeError("Agreement must contain at least one item");
|
|
15
|
+
}
|
|
16
|
+
const ids = new Set();
|
|
17
|
+
for (const item of descriptor.items) {
|
|
18
|
+
assertNonEmpty(item.id, "item id");
|
|
19
|
+
assertNonEmpty(item.label, "item label");
|
|
20
|
+
if (ids.has(item.id))
|
|
21
|
+
throw new TypeError(`Duplicate Agreement item id: ${item.id}`);
|
|
22
|
+
ids.add(item.id);
|
|
23
|
+
if (item.detail !== undefined) {
|
|
24
|
+
assertNonEmpty(item.detail.label, `item ${item.id} detail label`);
|
|
25
|
+
}
|
|
26
|
+
if (item.required === true && item.disabled === true) {
|
|
27
|
+
/*
|
|
28
|
+
비활성 필수 항목은 사용자가 영원히 제출할 수 없는 화면을 만든다. 조용히
|
|
29
|
+
허용하면 "버튼이 왜 안 눌리는지" 알 수 없는 상태가 되므로 여기서 막는다.
|
|
30
|
+
*/
|
|
31
|
+
throw new TypeError(`Agreement item ${item.id} cannot be both required and disabled`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 비활성 항목은 분모에서 뺀다 — 사용자가 바꿀 수 없는 것을 "덜 동의했다"고 셀 수
|
|
37
|
+
* 없기 때문이다(`resolveDataTableSelectAllState`·`resolveTreeCheckedStates`와 같은 규칙).
|
|
38
|
+
* 필수 항목은 비활성일 수 없으므로(위 validate) `satisfied` 판정에는 이 예외가 없다.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveAgreementState(descriptor, checkedIds) {
|
|
41
|
+
validateAgreementDescriptor(descriptor);
|
|
42
|
+
const selectable = descriptor.items.filter((item) => !item.disabled);
|
|
43
|
+
const checkedCount = selectable.filter((item) => checkedIds.has(item.id)).length;
|
|
44
|
+
const all = selectable.length === 0 || checkedCount === 0
|
|
45
|
+
? false
|
|
46
|
+
: checkedCount === selectable.length
|
|
47
|
+
? true
|
|
48
|
+
: "mixed";
|
|
49
|
+
const missingRequiredIds = descriptor.items
|
|
50
|
+
.filter((item) => item.required === true && !checkedIds.has(item.id))
|
|
51
|
+
.map((item) => item.id);
|
|
52
|
+
return { all, satisfied: missingRequiredIds.length === 0, missingRequiredIds };
|
|
53
|
+
}
|
|
54
|
+
/** 개별 항목 토글. 비활성 항목 보호는 공용 helper가 이미 갖고 있다. */
|
|
55
|
+
export function toggleAgreementItem(descriptor, checkedIds, id) {
|
|
56
|
+
validateAgreementDescriptor(descriptor);
|
|
57
|
+
return toggleCheckboxSelection(descriptor.items, checkedIds, id);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 전체 동의는 "모두 체크됨"의 반대로 간다 — mixed에서 누르면 채운다(공용
|
|
61
|
+
* `getCheckboxNextState`의 기본값과 같은 관습). 비활성 항목의 현재 상태는 건드리지
|
|
62
|
+
* 않는다: 그 값은 사용자가 만든 것이 아니라 제품이 고정한 것이다.
|
|
63
|
+
*/
|
|
64
|
+
export function toggleAgreementAll(descriptor, checkedIds) {
|
|
65
|
+
const { all } = resolveAgreementState(descriptor, checkedIds);
|
|
66
|
+
const next = new Set(checkedIds);
|
|
67
|
+
for (const item of descriptor.items) {
|
|
68
|
+
if (item.disabled)
|
|
69
|
+
continue;
|
|
70
|
+
if (all === true)
|
|
71
|
+
next.delete(item.id);
|
|
72
|
+
else
|
|
73
|
+
next.add(item.id);
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
/** 목록에서 사라진 항목의 동의는 남기지 않는다. */
|
|
78
|
+
export function reconcileAgreementSelection(descriptor, checkedIds) {
|
|
79
|
+
validateAgreementDescriptor(descriptor);
|
|
80
|
+
return reconcileCheckboxSelection(descriptor.items, checkedIds);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 전체 동의 행은 개별 행보다 시각적으로 무겁다 — 목록의 제목 역할을 하기 때문이다.
|
|
84
|
+
* 나머지는 `collectionItemContract`를 그대로 쓴다(Menu·Tree·ListRow와 같은 행 chrome).
|
|
85
|
+
*/
|
|
86
|
+
export const agreementRecipe = {
|
|
87
|
+
slots: ["root", "all", "list", "item", "label", "detail"],
|
|
88
|
+
all: {
|
|
89
|
+
minHeight: control.minTouchTarget,
|
|
90
|
+
gap: spacing.sm,
|
|
91
|
+
paddingHorizontal: spacing.md,
|
|
92
|
+
paddingVertical: spacing.sm,
|
|
93
|
+
background: semanticColors.surface.sunken,
|
|
94
|
+
radius: "md",
|
|
95
|
+
textVariant: "bodyLarge",
|
|
96
|
+
color: semanticColors.content.primary,
|
|
97
|
+
},
|
|
98
|
+
item: collectionItemContract,
|
|
99
|
+
detail: {
|
|
100
|
+
color: semanticColors.content.secondary,
|
|
101
|
+
textVariant: "label",
|
|
102
|
+
minHeight: control.minTouchTarget,
|
|
103
|
+
},
|
|
104
|
+
required: { color: semanticColors.content.brand, textVariant: "label" },
|
|
105
|
+
gap: spacing.xs,
|
|
106
|
+
states: { focus: focusIndicatorContract },
|
|
107
|
+
};
|
|
108
|
+
export const agreementBehavior = {
|
|
109
|
+
controlled: ["checkedIds", "defaultCheckedIds", "onCheckedIdsChange"],
|
|
110
|
+
inputs: ["items", "allLabel", "accessibilityLabel"],
|
|
111
|
+
events: ["onDetail"],
|
|
112
|
+
stateAxes: {
|
|
113
|
+
availability: ["enabled", "disabled"],
|
|
114
|
+
value: ["unchecked", "checked", "mixed"],
|
|
115
|
+
},
|
|
116
|
+
web: {
|
|
117
|
+
roles: ["group", "checkbox", "link", "button"],
|
|
118
|
+
keyboard: ["Tab", "Space", "Enter"],
|
|
119
|
+
focus: "native",
|
|
120
|
+
},
|
|
121
|
+
native: {
|
|
122
|
+
roles: ["checkbox", "link", "button"],
|
|
123
|
+
states: ["checked", "disabled"],
|
|
124
|
+
actions: ["toggle", "toggleAll", "openDetail"],
|
|
125
|
+
},
|
|
126
|
+
scenarios: [
|
|
127
|
+
"all-agree-is-derived-from-the-items-never-stored-as-its-own-value",
|
|
128
|
+
"checking-all-checks-every-enabled-item-and-leaves-disabled-ones-untouched",
|
|
129
|
+
"required-items-alone-decide-whether-the-product-may-submit",
|
|
130
|
+
"a-required-item-can-never-be-disabled-the-descriptor-is-rejected",
|
|
131
|
+
"every-item-may-open-its-full-text-and-that-control-is-a-separate-tab-stop",
|
|
132
|
+
"the-detail-control-never-toggles-the-agreement-it-only-opens-the-text",
|
|
133
|
+
"optional-items-never-block-submission-and-are-announced-as-optional",
|
|
134
|
+
"removing-an-item-drops-its-consent-instead-of-keeping-an-orphan-id",
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=agreement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agreement.js","sourceRoot":"","sources":["../src/agreement.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { collectionItemContract, focusIndicatorContract } from \"./component-contracts.js\";\nimport { control, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\nimport {\n reconcileCheckboxSelection,\n toggleCheckboxSelection,\n type CheckboxSelectionItem,\n type CheckboxState,\n} from \"./selection-helpers.js\";\n\n/**\n * 가입·결제 앞에 서는 \"약관 동의\" 묶음이다. CheckboxGroup으로 매번 조립하던 것을\n * 계약으로 올리는 이유는 세 가지가 **체크박스 목록이 아니라 법적 절차**이기 때문이다.\n *\n * 1. 필수/선택이 제출 가능 여부를 결정한다 — CheckboxGroup은 그 판정을 갖지 않는다.\n * 2. 전체 동의는 자식의 합이지 별도 항목이 아니다(DataTable 머리글·TreeSelect 부모와\n * 같은 파생 관계). 저장되는 것은 개별 항목뿐이다.\n * 3. 각 항목은 \"읽을 수 있어야\" 한다 — 상세 보기 경로가 없는 동의는 동의가 아니다.\n * 그래서 `detail`은 renderer가 장식으로 넣는 화살표가 아니라 계약된 행동이다.\n *\n * 문구·링크 주소·법적 유효성은 제품이 소유한다. 이 계약은 판정과 배치만 갖는다.\n */\nexport type AgreementItemDescriptor<Id extends string = string> =\n CheckboxSelectionItem<Id> &\n Readonly<{\n /** 필수 항목은 하나라도 비면 제출을 막는다. 기본값은 선택(false). */\n required?: boolean;\n /**\n * 전문을 여는 경로. 제품이 링크(`href`)로 열지 시트로 열지는 renderer 밖의\n * 결정이라 여기서는 \"열 수 있다\"는 사실만 계약한다.\n */\n detail?: Readonly<{ label: string; href?: string }>;\n }>;\n\nexport type AgreementDescriptor<Id extends string = string> = Readonly<{\n /** 묶음 전체의 접근성 이름. \"약관 동의\"처럼 제품이 현지화한다. */\n accessibilityLabel: string;\n /** 전체 동의 행의 라벨. 필수이므로 renderer가 임의 문구를 만들지 않는다. */\n allLabel: string;\n items: readonly AgreementItemDescriptor<Id>[];\n}>;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`Agreement ${field} must not be empty`);\n }\n}\n\nexport function validateAgreementDescriptor<Id extends string>(\n descriptor: AgreementDescriptor<Id>,\n): void {\n assertNonEmpty(descriptor.accessibilityLabel, \"accessibilityLabel\");\n assertNonEmpty(descriptor.allLabel, \"allLabel\");\n if (!Array.isArray(descriptor.items) || descriptor.items.length === 0) {\n throw new RangeError(\"Agreement must contain at least one item\");\n }\n const ids = new Set<Id>();\n for (const item of descriptor.items) {\n assertNonEmpty(item.id, \"item id\");\n assertNonEmpty(item.label, \"item label\");\n if (ids.has(item.id)) throw new TypeError(`Duplicate Agreement item id: ${item.id}`);\n ids.add(item.id);\n if (item.detail !== undefined) {\n assertNonEmpty(item.detail.label, `item ${item.id} detail label`);\n }\n if (item.required === true && item.disabled === true) {\n /*\n 비활성 필수 항목은 사용자가 영원히 제출할 수 없는 화면을 만든다. 조용히\n 허용하면 \"버튼이 왜 안 눌리는지\" 알 수 없는 상태가 되므로 여기서 막는다.\n */\n throw new TypeError(\n `Agreement item ${item.id} cannot be both required and disabled`,\n );\n }\n }\n}\n\nexport type AgreementState<Id extends string = string> = Readonly<{\n /** 전체 동의 행이 그릴 tri-state. 개별 항목의 합에서만 나온다. */\n all: CheckboxState;\n /** 필수 항목이 모두 체크됐는가 — 제품의 제출 버튼이 읽는 값. */\n satisfied: boolean;\n /** 아직 체크되지 않은 필수 항목. 제품이 \"무엇이 남았는지\" 문장을 만든다. */\n missingRequiredIds: readonly Id[];\n}>;\n\n/**\n * 비활성 항목은 분모에서 뺀다 — 사용자가 바꿀 수 없는 것을 \"덜 동의했다\"고 셀 수\n * 없기 때문이다(`resolveDataTableSelectAllState`·`resolveTreeCheckedStates`와 같은 규칙).\n * 필수 항목은 비활성일 수 없으므로(위 validate) `satisfied` 판정에는 이 예외가 없다.\n */\nexport function resolveAgreementState<Id extends string>(\n descriptor: AgreementDescriptor<Id>,\n checkedIds: ReadonlySet<Id>,\n): AgreementState<Id> {\n validateAgreementDescriptor(descriptor);\n const selectable = descriptor.items.filter((item) => !item.disabled);\n const checkedCount = selectable.filter((item) => checkedIds.has(item.id)).length;\n const all: CheckboxState = selectable.length === 0 || checkedCount === 0\n ? false\n : checkedCount === selectable.length\n ? true\n : \"mixed\";\n const missingRequiredIds = descriptor.items\n .filter((item) => item.required === true && !checkedIds.has(item.id))\n .map((item) => item.id);\n return { all, satisfied: missingRequiredIds.length === 0, missingRequiredIds };\n}\n\n/** 개별 항목 토글. 비활성 항목 보호는 공용 helper가 이미 갖고 있다. */\nexport function toggleAgreementItem<Id extends string>(\n descriptor: AgreementDescriptor<Id>,\n checkedIds: ReadonlySet<Id>,\n id: Id,\n): ReadonlySet<Id> {\n validateAgreementDescriptor(descriptor);\n return toggleCheckboxSelection(descriptor.items, checkedIds, id);\n}\n\n/**\n * 전체 동의는 \"모두 체크됨\"의 반대로 간다 — mixed에서 누르면 채운다(공용\n * `getCheckboxNextState`의 기본값과 같은 관습). 비활성 항목의 현재 상태는 건드리지\n * 않는다: 그 값은 사용자가 만든 것이 아니라 제품이 고정한 것이다.\n */\nexport function toggleAgreementAll<Id extends string>(\n descriptor: AgreementDescriptor<Id>,\n checkedIds: ReadonlySet<Id>,\n): ReadonlySet<Id> {\n const { all } = resolveAgreementState(descriptor, checkedIds);\n const next = new Set(checkedIds);\n for (const item of descriptor.items) {\n if (item.disabled) continue;\n if (all === true) next.delete(item.id);\n else next.add(item.id);\n }\n return next;\n}\n\n/** 목록에서 사라진 항목의 동의는 남기지 않는다. */\nexport function reconcileAgreementSelection<Id extends string>(\n descriptor: AgreementDescriptor<Id>,\n checkedIds: ReadonlySet<Id>,\n): ReadonlySet<Id> {\n validateAgreementDescriptor(descriptor);\n return reconcileCheckboxSelection(descriptor.items, checkedIds);\n}\n\n/**\n * 전체 동의 행은 개별 행보다 시각적으로 무겁다 — 목록의 제목 역할을 하기 때문이다.\n * 나머지는 `collectionItemContract`를 그대로 쓴다(Menu·Tree·ListRow와 같은 행 chrome).\n */\nexport const agreementRecipe = {\n slots: [\"root\", \"all\", \"list\", \"item\", \"label\", \"detail\"] as const,\n all: {\n minHeight: control.minTouchTarget,\n gap: spacing.sm,\n paddingHorizontal: spacing.md,\n paddingVertical: spacing.sm,\n background: semanticColors.surface.sunken,\n radius: \"md\" as const,\n textVariant: \"bodyLarge\" as const,\n color: semanticColors.content.primary,\n },\n item: collectionItemContract,\n detail: {\n color: semanticColors.content.secondary,\n textVariant: \"label\" as const,\n minHeight: control.minTouchTarget,\n },\n required: { color: semanticColors.content.brand, textVariant: \"label\" as const },\n gap: spacing.xs,\n states: { focus: focusIndicatorContract },\n} as const satisfies {\n slots: readonly [\"root\", \"all\", \"list\", \"item\", \"label\", \"detail\"];\n all: {\n minHeight: number;\n gap: number;\n paddingHorizontal: number;\n paddingVertical: number;\n background: ColorReference;\n radius: \"md\";\n textVariant: \"bodyLarge\";\n color: ColorReference;\n };\n item: typeof collectionItemContract;\n detail: { color: ColorReference; textVariant: \"label\"; minHeight: number };\n required: { color: ColorReference; textVariant: \"label\" };\n gap: number;\n states: { focus: typeof focusIndicatorContract };\n};\n\nexport const agreementBehavior = {\n controlled: [\"checkedIds\", \"defaultCheckedIds\", \"onCheckedIdsChange\"],\n inputs: [\"items\", \"allLabel\", \"accessibilityLabel\"],\n events: [\"onDetail\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\"],\n value: [\"unchecked\", \"checked\", \"mixed\"],\n },\n web: {\n roles: [\"group\", \"checkbox\", \"link\", \"button\"],\n keyboard: [\"Tab\", \"Space\", \"Enter\"],\n focus: \"native\",\n },\n native: {\n roles: [\"checkbox\", \"link\", \"button\"],\n states: [\"checked\", \"disabled\"],\n actions: [\"toggle\", \"toggleAll\", \"openDetail\"],\n },\n scenarios: [\n \"all-agree-is-derived-from-the-items-never-stored-as-its-own-value\",\n \"checking-all-checks-every-enabled-item-and-leaves-disabled-ones-untouched\",\n \"required-items-alone-decide-whether-the-product-may-submit\",\n \"a-required-item-can-never-be-disabled-the-descriptor-is-rejected\",\n \"every-item-may-open-its-full-text-and-that-control-is-a-separate-tab-stop\",\n \"the-detail-control-never-toggles-the-agreement-it-only-opens-the-text\",\n \"optional-items-never-block-submission-and-are-announced-as-optional\",\n \"removing-an-item-drops-its-consent-instead-of-keeping-an-orphan-id\",\n ],\n} as const satisfies BehaviorContract;\n"]}
|
package/dist/anchor.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** IDs refer to same-document sections, never arbitrary external destinations. */
|
|
2
|
+
export type AnchorItem = Readonly<{
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function resolveAnchorItems(items: readonly AnchorItem[]): any[];
|
|
7
|
+
/** Positions use one viewport's coordinates. DOM reads and scroll ownership stay in the renderer. */
|
|
8
|
+
export declare function getAnchorCurrentId(positions: readonly Readonly<{
|
|
9
|
+
id: string;
|
|
10
|
+
top: number;
|
|
11
|
+
}>[], offset?: number, atEnd?: boolean): string | undefined;
|
|
12
|
+
export declare const anchorRecipe: {
|
|
13
|
+
readonly slots: readonly ["root", "list", "link", "indicator"];
|
|
14
|
+
readonly gap: 4;
|
|
15
|
+
readonly link: {
|
|
16
|
+
readonly padding: 8;
|
|
17
|
+
readonly color: Readonly<{
|
|
18
|
+
source: "theme";
|
|
19
|
+
key: "textMuted";
|
|
20
|
+
alpha?: number;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
readonly current: {
|
|
24
|
+
readonly color: Readonly<{
|
|
25
|
+
source: "theme";
|
|
26
|
+
key: "contentBrand";
|
|
27
|
+
alpha?: number;
|
|
28
|
+
}>;
|
|
29
|
+
readonly border: Readonly<{
|
|
30
|
+
source: "theme";
|
|
31
|
+
key: "contentBrand";
|
|
32
|
+
alpha?: number;
|
|
33
|
+
}>;
|
|
34
|
+
readonly width: 2;
|
|
35
|
+
};
|
|
36
|
+
readonly focus: {
|
|
37
|
+
readonly color: Readonly<{
|
|
38
|
+
source: "theme";
|
|
39
|
+
key: "contentBrand";
|
|
40
|
+
alpha?: number;
|
|
41
|
+
}>;
|
|
42
|
+
readonly width: 2;
|
|
43
|
+
readonly offset: 2;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export declare const anchorBehavior: {
|
|
47
|
+
readonly controlled: readonly [];
|
|
48
|
+
readonly inputs: readonly ["items", "offset", "container", "historyMode"];
|
|
49
|
+
readonly stateAxes: {};
|
|
50
|
+
readonly web: {
|
|
51
|
+
readonly roles: readonly ["navigation", "list", "listitem", "link"];
|
|
52
|
+
readonly keyboard: readonly ["Tab", "Enter"];
|
|
53
|
+
readonly focus: "native";
|
|
54
|
+
};
|
|
55
|
+
readonly native: {
|
|
56
|
+
readonly roles: readonly [];
|
|
57
|
+
readonly states: readonly [];
|
|
58
|
+
readonly actions: readonly [];
|
|
59
|
+
};
|
|
60
|
+
readonly scenarios: readonly ["scroll-position-selects-one-current-location", "activation-focuses-target-and-preserves-offset", "reduced-motion-jumps-instantly", "browser-history-restores-a-section", "missing-targets-are-not-current"];
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=anchor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../src/anchor.ts"],"names":[],"mappings":"AAKA,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACjE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,SAU9D;AAED,qGAAqG;AACrG,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,EAAE,MAAM,SAAI,EAAE,KAAK,UAAQ,GAAG,MAAM,GAAG,SAAS,CAM7I;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAMf,CAAC;AACX,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;CAKU,CAAC"}
|
package/dist/anchor.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spacing, stroke } from "./foundations.js";
|
|
2
|
+
import { semanticColors } from "./semantic-colors.js";
|
|
3
|
+
import { focusIndicatorContract } from "./component-contracts.js";
|
|
4
|
+
export function resolveAnchorItems(items) {
|
|
5
|
+
if (!Array.isArray(items) || items.length === 0)
|
|
6
|
+
throw new TypeError("Anchor items must not be empty");
|
|
7
|
+
const ids = new Set();
|
|
8
|
+
return items.map((item) => {
|
|
9
|
+
if (!item || typeof item.id !== "string" || !item.id || /\s/u.test(item.id))
|
|
10
|
+
throw new TypeError("Anchor id must be a non-empty HTML id without whitespace");
|
|
11
|
+
if (typeof item.label !== "string" || !item.label.trim())
|
|
12
|
+
throw new TypeError("Anchor label must not be empty");
|
|
13
|
+
if (ids.has(item.id))
|
|
14
|
+
throw new TypeError(`Duplicate Anchor id: ${item.id}`);
|
|
15
|
+
ids.add(item.id);
|
|
16
|
+
return { ...item, href: `#${encodeURIComponent(item.id)}` };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Positions use one viewport's coordinates. DOM reads and scroll ownership stay in the renderer. */
|
|
20
|
+
export function getAnchorCurrentId(positions, offset = 0, atEnd = false) {
|
|
21
|
+
if (!Number.isFinite(offset) || offset < 0)
|
|
22
|
+
throw new RangeError("Anchor offset must be non-negative");
|
|
23
|
+
if (positions.some(({ top }) => !Number.isFinite(top)))
|
|
24
|
+
throw new TypeError("Anchor positions must be finite");
|
|
25
|
+
const ordered = [...positions].sort((a, b) => a.top - b.top);
|
|
26
|
+
if (atEnd)
|
|
27
|
+
return ordered.at(-1)?.id;
|
|
28
|
+
return ordered.filter(({ top }) => top <= offset).at(-1)?.id ?? ordered[0]?.id;
|
|
29
|
+
}
|
|
30
|
+
export const anchorRecipe = {
|
|
31
|
+
slots: ["root", "list", "link", "indicator"],
|
|
32
|
+
gap: spacing.xxs,
|
|
33
|
+
link: { padding: spacing.xs, color: semanticColors.content.secondary },
|
|
34
|
+
current: { color: semanticColors.content.brand, border: semanticColors.border.focus, width: stroke.strong },
|
|
35
|
+
focus: focusIndicatorContract,
|
|
36
|
+
};
|
|
37
|
+
export const anchorBehavior = {
|
|
38
|
+
controlled: [], inputs: ["items", "offset", "container", "historyMode"], stateAxes: {},
|
|
39
|
+
web: { roles: ["navigation", "list", "listitem", "link"], keyboard: ["Tab", "Enter"], focus: "native" },
|
|
40
|
+
native: { roles: [], states: [], actions: [] },
|
|
41
|
+
scenarios: ["scroll-position-selects-one-current-location", "activation-focuses-target-and-preserves-offset", "reduced-motion-jumps-instantly", "browser-history-restores-a-section", "missing-targets-are-not-current"],
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=anchor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor.js","sourceRoot":"","sources":["../src/anchor.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport { spacing, stroke } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\nimport { focusIndicatorContract } from \"./component-contracts.js\";\n\n/** IDs refer to same-document sections, never arbitrary external destinations. */\nexport type AnchorItem = Readonly<{ id: string; label: string }>;\nexport function resolveAnchorItems(items: readonly AnchorItem[]) {\n if (!Array.isArray(items) || items.length === 0) throw new TypeError(\"Anchor items must not be empty\");\n const ids = new Set<string>();\n return items.map((item) => {\n if (!item || typeof item.id !== \"string\" || !item.id || /\\s/u.test(item.id)) throw new TypeError(\"Anchor id must be a non-empty HTML id without whitespace\");\n if (typeof item.label !== \"string\" || !item.label.trim()) throw new TypeError(\"Anchor label must not be empty\");\n if (ids.has(item.id)) throw new TypeError(`Duplicate Anchor id: ${item.id}`);\n ids.add(item.id);\n return { ...item, href: `#${encodeURIComponent(item.id)}` };\n });\n}\n\n/** Positions use one viewport's coordinates. DOM reads and scroll ownership stay in the renderer. */\nexport function getAnchorCurrentId(positions: readonly Readonly<{ id: string; top: number }>[], offset = 0, atEnd = false): string | undefined {\n if (!Number.isFinite(offset) || offset < 0) throw new RangeError(\"Anchor offset must be non-negative\");\n if (positions.some(({ top }) => !Number.isFinite(top))) throw new TypeError(\"Anchor positions must be finite\");\n const ordered = [...positions].sort((a, b) => a.top - b.top);\n if (atEnd) return ordered.at(-1)?.id;\n return ordered.filter(({ top }) => top <= offset).at(-1)?.id ?? ordered[0]?.id;\n}\n\nexport const anchorRecipe = {\n slots: [\"root\", \"list\", \"link\", \"indicator\"] as const,\n gap: spacing.xxs,\n link: { padding: spacing.xs, color: semanticColors.content.secondary },\n current: { color: semanticColors.content.brand, border: semanticColors.border.focus, width: stroke.strong },\n focus: focusIndicatorContract,\n} as const;\nexport const anchorBehavior = {\n controlled: [], inputs: [\"items\", \"offset\", \"container\", \"historyMode\"], stateAxes: {},\n web: { roles: [\"navigation\", \"list\", \"listitem\", \"link\"], keyboard: [\"Tab\", \"Enter\"], focus: \"native\" },\n native: { roles: [], states: [], actions: [] },\n scenarios: [\"scroll-position-selects-one-current-location\", \"activation-focuses-target-and-preserves-offset\", \"reduced-motion-jumps-instantly\", \"browser-history-restores-a-section\", \"missing-targets-are-not-current\"],\n} as const satisfies BehaviorContract;\n"]}
|
package/dist/asset.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 아이콘·이미지·Lottie·비디오를 **같은 액자**에 넣는 자리.
|
|
3
|
+
*
|
|
4
|
+
* Icon·Image·Avatar가 이미 있는데 왜 또 두는가: 세 컴포넌트는 각자 다른 모양 규칙을 갖고
|
|
5
|
+
* 있고(아이콘은 정사각, Avatar는 원, Image는 비율), 화면에서 이들이 **같은 줄에 섞여**
|
|
6
|
+
* 나올 때 크기와 모서리가 어긋난다. Diairy의 `LottieArt`·`AnimalArt`가 그 어긋남을
|
|
7
|
+
* 제품 안에서 다시 맞추고 있었다. Asset은 그 액자 규칙만 갖고, **무엇을 그릴지는 슬롯으로
|
|
8
|
+
* 받는다** — Lottie와 비디오 재생기를 이 패키지가 의존하지 않기 위해서다.
|
|
9
|
+
*
|
|
10
|
+
* 그래서 이 계약에 재생 상태(play/pause/seek)가 없다. 재생기는 제품 것이고, 여기서
|
|
11
|
+
* 정하는 것은 액자·크기·겹침·보조 표식뿐이다.
|
|
12
|
+
*/
|
|
13
|
+
export type AssetMediaKind = "icon" | "image" | "lottie" | "video";
|
|
14
|
+
export type AssetShape = "square" | "rounded" | "circle";
|
|
15
|
+
export type AssetSize = "small" | "medium" | "large" | "xlarge";
|
|
16
|
+
export type AssetDescriptor = Readonly<{
|
|
17
|
+
kind: AssetMediaKind;
|
|
18
|
+
size?: AssetSize;
|
|
19
|
+
shape?: AssetShape;
|
|
20
|
+
/**
|
|
21
|
+
* 장식이면 `true`. 장식이 아닌데 이름이 없으면 거절한다 — 움직이는 그림일수록
|
|
22
|
+
* "무엇을 뜻하는지"가 화면에 없으면 보조기기에서 사라진다.
|
|
23
|
+
*/
|
|
24
|
+
decorative?: boolean;
|
|
25
|
+
accessibilityLabel?: string;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function validateAssetDescriptor(descriptor: AssetDescriptor): void;
|
|
28
|
+
/**
|
|
29
|
+
* 움직이는 자산은 `reducedMotion`에서 **첫 프레임(또는 포스터)로 정지**한다. 숨기지
|
|
30
|
+
* 않는 이유: 그 자리에 그림이 있다는 사실 자체가 화면의 뜻인 경우가 많다(에어리의 동물).
|
|
31
|
+
* 정지 화면을 제품이 주지 못하면 재생을 멈추기만 한다.
|
|
32
|
+
*/
|
|
33
|
+
export declare function shouldAnimateAsset(kind: AssetMediaKind, reducedMotion: boolean): boolean;
|
|
34
|
+
export declare const assetRecipe: {
|
|
35
|
+
readonly slots: readonly ["root", "frame", "media", "accessory", "group"];
|
|
36
|
+
readonly kinds: readonly ["icon", "image", "lottie", "video"];
|
|
37
|
+
readonly defaults: {
|
|
38
|
+
readonly size: "medium";
|
|
39
|
+
readonly shape: "rounded";
|
|
40
|
+
};
|
|
41
|
+
readonly sizes: {
|
|
42
|
+
readonly small: 32;
|
|
43
|
+
readonly medium: 48;
|
|
44
|
+
readonly large: 72;
|
|
45
|
+
readonly xlarge: 120;
|
|
46
|
+
};
|
|
47
|
+
readonly shapes: {
|
|
48
|
+
readonly square: 0;
|
|
49
|
+
readonly rounded: 12;
|
|
50
|
+
readonly circle: 999;
|
|
51
|
+
};
|
|
52
|
+
readonly background: Readonly<{
|
|
53
|
+
source: "theme";
|
|
54
|
+
key: "surfaceAlt";
|
|
55
|
+
alpha?: number;
|
|
56
|
+
}>;
|
|
57
|
+
readonly border: Readonly<{
|
|
58
|
+
source: "theme";
|
|
59
|
+
key: "border";
|
|
60
|
+
alpha?: number;
|
|
61
|
+
}>;
|
|
62
|
+
/** 묶음으로 겹칠 때의 비율. Avatar와 같은 값을 쓴다 — 한 줄에 섞여 나오기 때문이다. */
|
|
63
|
+
readonly overlapRatio: 0.3;
|
|
64
|
+
/** 보조 표식(재생 아이콘·상태 점)은 액자 **바깥 모서리**에 붙어 매체를 가리지 않는다. */
|
|
65
|
+
readonly accessory: {
|
|
66
|
+
readonly offset: 4;
|
|
67
|
+
readonly gap: 4;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export declare const assetBehavior: {
|
|
71
|
+
/** 상태가 없다 — 액자는 값을 갖지 않고 슬롯만 받는다. */
|
|
72
|
+
readonly controlled: readonly [];
|
|
73
|
+
readonly inputs: readonly ["kind", "size", "shape", "decorative", "accessibilityLabel"];
|
|
74
|
+
readonly configuration: {
|
|
75
|
+
readonly kind: readonly ["icon", "image", "lottie", "video"];
|
|
76
|
+
readonly shape: readonly ["square", "rounded", "circle"];
|
|
77
|
+
readonly size: readonly ["small", "medium", "large", "xlarge"];
|
|
78
|
+
};
|
|
79
|
+
readonly stateAxes: {
|
|
80
|
+
readonly availability: readonly ["enabled"];
|
|
81
|
+
};
|
|
82
|
+
readonly web: {
|
|
83
|
+
readonly roles: readonly ["img", "presentation"];
|
|
84
|
+
readonly keyboard: readonly [];
|
|
85
|
+
readonly focus: "none";
|
|
86
|
+
};
|
|
87
|
+
readonly native: {
|
|
88
|
+
readonly roles: readonly ["image"];
|
|
89
|
+
readonly states: readonly [];
|
|
90
|
+
readonly actions: readonly [];
|
|
91
|
+
};
|
|
92
|
+
readonly scenarios: readonly ["one-frame-rule-covers-icon-image-lottie-and-video-so-a-mixed-row-lines-up", "the-package-never-depends-on-a-player-the-media-arrives-as-a-slot", "a-meaningful-asset-without-a-name-is-rejected-instead-of-silently-decorative", "reduced-motion-freezes-the-frame-rather-than-removing-the-asset", "an-accessory-sits-on-the-frames-outer-corner-and-never-covers-the-media"];
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=asset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset.d.ts","sourceRoot":"","sources":["../src/asset.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AACzD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhE,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC,CAAC;AAEH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI,CAazE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAExF;AAED,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAUtB,0DAA0D;;IAE1D,yDAAyD;;;;;CAEjD,CAAC;AAEX,eAAO,MAAM,aAAa;IACxB,qCAAqC;;;;;;;;;;;;;;;;;;;;;;CAkBF,CAAC"}
|
package/dist/asset.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { radius, spacing } from "./foundations.js";
|
|
2
|
+
import { semanticColors } from "./semantic-colors.js";
|
|
3
|
+
export function validateAssetDescriptor(descriptor) {
|
|
4
|
+
if (!assetRecipe.kinds.includes(descriptor.kind)) {
|
|
5
|
+
throw new TypeError(`Unsupported Asset kind: ${String(descriptor.kind)}`);
|
|
6
|
+
}
|
|
7
|
+
const decorative = descriptor.decorative ?? false;
|
|
8
|
+
const label = descriptor.accessibilityLabel?.trim() ?? "";
|
|
9
|
+
if (!decorative && label.length === 0) {
|
|
10
|
+
throw new TypeError("A meaningful Asset must provide accessibilityLabel");
|
|
11
|
+
}
|
|
12
|
+
if (decorative && label.length > 0) {
|
|
13
|
+
// 둘 다 주면 어느 쪽이 참인지 화면이 모른다. 조용히 무시하면 이름을 지운 줄 모른다.
|
|
14
|
+
throw new TypeError("A decorative Asset must not provide accessibilityLabel");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 움직이는 자산은 `reducedMotion`에서 **첫 프레임(또는 포스터)로 정지**한다. 숨기지
|
|
19
|
+
* 않는 이유: 그 자리에 그림이 있다는 사실 자체가 화면의 뜻인 경우가 많다(에어리의 동물).
|
|
20
|
+
* 정지 화면을 제품이 주지 못하면 재생을 멈추기만 한다.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldAnimateAsset(kind, reducedMotion) {
|
|
23
|
+
return (kind === "lottie" || kind === "video") && !reducedMotion;
|
|
24
|
+
}
|
|
25
|
+
export const assetRecipe = {
|
|
26
|
+
slots: ["root", "frame", "media", "accessory", "group"],
|
|
27
|
+
kinds: ["icon", "image", "lottie", "video"],
|
|
28
|
+
defaults: { size: "medium", shape: "rounded" },
|
|
29
|
+
sizes: { small: 32, medium: 48, large: 72, xlarge: 120 },
|
|
30
|
+
// 정사각은 모서리 0이다 — radius 토큰에 0이 없는 이유는 "모서리 없음"이 토큰이 아니라
|
|
31
|
+
// 값의 부재이기 때문이다.
|
|
32
|
+
shapes: { square: 0, rounded: radius.md, circle: radius.full },
|
|
33
|
+
background: semanticColors.surface.sunken,
|
|
34
|
+
border: semanticColors.border.default,
|
|
35
|
+
/** 묶음으로 겹칠 때의 비율. Avatar와 같은 값을 쓴다 — 한 줄에 섞여 나오기 때문이다. */
|
|
36
|
+
overlapRatio: 0.3,
|
|
37
|
+
/** 보조 표식(재생 아이콘·상태 점)은 액자 **바깥 모서리**에 붙어 매체를 가리지 않는다. */
|
|
38
|
+
accessory: { offset: spacing.xxs, gap: spacing.xxs },
|
|
39
|
+
};
|
|
40
|
+
export const assetBehavior = {
|
|
41
|
+
/** 상태가 없다 — 액자는 값을 갖지 않고 슬롯만 받는다. */
|
|
42
|
+
controlled: [],
|
|
43
|
+
inputs: ["kind", "size", "shape", "decorative", "accessibilityLabel"],
|
|
44
|
+
configuration: {
|
|
45
|
+
kind: ["icon", "image", "lottie", "video"],
|
|
46
|
+
shape: ["square", "rounded", "circle"],
|
|
47
|
+
size: ["small", "medium", "large", "xlarge"],
|
|
48
|
+
},
|
|
49
|
+
stateAxes: { availability: ["enabled"] },
|
|
50
|
+
web: { roles: ["img", "presentation"], keyboard: [], focus: "none" },
|
|
51
|
+
native: { roles: ["image"], states: [], actions: [] },
|
|
52
|
+
scenarios: [
|
|
53
|
+
"one-frame-rule-covers-icon-image-lottie-and-video-so-a-mixed-row-lines-up",
|
|
54
|
+
"the-package-never-depends-on-a-player-the-media-arrives-as-a-slot",
|
|
55
|
+
"a-meaningful-asset-without-a-name-is-rejected-instead-of-silently-decorative",
|
|
56
|
+
"reduced-motion-freezes-the-frame-rather-than-removing-the-asset",
|
|
57
|
+
"an-accessory-sits-on-the-frames-outer-corner-and-never-covers-the-media",
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=asset.js.map
|