@hjmds/design-contracts 1.41.2 → 1.43.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/affix.d.ts +18 -0
- package/dist/affix.d.ts.map +1 -1
- package/dist/affix.js +13 -1
- package/dist/affix.js.map +1 -1
- package/dist/alert-dialog.d.ts +32 -0
- package/dist/alert-dialog.d.ts.map +1 -1
- package/dist/alert-dialog.js +52 -0
- package/dist/alert-dialog.js.map +1 -1
- package/dist/behaviors.d.ts +6 -5
- package/dist/behaviors.d.ts.map +1 -1
- package/dist/behaviors.js +1 -1
- package/dist/behaviors.js.map +1 -1
- package/dist/catalog.d.ts +168 -15
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +4 -4
- package/dist/catalog.js.map +1 -1
- package/dist/character-count.d.ts +103 -0
- package/dist/character-count.d.ts.map +1 -0
- package/dist/character-count.js +68 -0
- package/dist/character-count.js.map +1 -0
- package/dist/chart.d.ts +30 -0
- package/dist/chart.d.ts.map +1 -1
- package/dist/chart.js +18 -0
- package/dist/chart.js.map +1 -1
- package/dist/collection-rail.d.ts +13 -1
- package/dist/collection-rail.d.ts.map +1 -1
- package/dist/collection-rail.js +9 -2
- package/dist/collection-rail.js.map +1 -1
- package/dist/communication.d.ts +25 -0
- package/dist/communication.d.ts.map +1 -1
- package/dist/communication.js +3 -0
- package/dist/communication.js.map +1 -1
- package/dist/component-recipes.d.ts +172 -2
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +122 -8
- package/dist/component-recipes.js.map +1 -1
- package/dist/design-system-provider.d.ts +13 -2
- package/dist/design-system-provider.d.ts.map +1 -1
- package/dist/design-system-provider.js +19 -3
- package/dist/design-system-provider.js.map +1 -1
- package/dist/foundations.d.ts +15 -0
- package/dist/foundations.d.ts.map +1 -1
- package/dist/foundations.js +7 -0
- package/dist/foundations.js.map +1 -1
- package/dist/icon-button-recipe.d.ts +33 -0
- package/dist/icon-button-recipe.d.ts.map +1 -1
- package/dist/icon-button-recipe.js +36 -1
- package/dist/icon-button-recipe.js.map +1 -1
- package/dist/image.d.ts +13 -0
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js +17 -0
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/recipes.d.ts +1 -1
- package/dist/recipes.d.ts.map +1 -1
- package/dist/recipes.js +1 -1
- package/dist/recipes.js.map +1 -1
- package/dist/screen-patterns.d.ts +95 -0
- package/dist/screen-patterns.d.ts.map +1 -1
- package/dist/screen-patterns.js +83 -2
- package/dist/screen-patterns.js.map +1 -1
- package/dist/selection-helpers.d.ts +6 -0
- package/dist/selection-helpers.d.ts.map +1 -1
- package/dist/selection-helpers.js +12 -0
- package/dist/selection-helpers.js.map +1 -1
- package/dist/sidebar.d.ts +17 -2
- package/dist/sidebar.d.ts.map +1 -1
- package/dist/sidebar.js +8 -2
- package/dist/sidebar.js.map +1 -1
- package/dist/steps.d.ts +19 -19
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +21 -9
- package/dist/steps.js.map +1 -1
- package/dist/timeline.d.ts +41 -0
- package/dist/timeline.d.ts.map +1 -1
- package/dist/timeline.js +47 -1
- package/dist/timeline.js.map +1 -1
- package/dist/top.d.ts +21 -2
- package/dist/top.d.ts.map +1 -1
- package/dist/top.js +17 -1
- package/dist/top.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/affix.md +7 -0
- package/docs/avatar-fallback.md +19 -0
- package/docs/brand-boundary.md +24 -2
- package/docs/chart.md +7 -0
- package/docs/collection-rail.md +4 -1
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +10 -6
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +12 -5
- package/docs/image.md +5 -0
- package/docs/list-row.md +8 -0
- package/docs/optional-adapters.md +3 -0
- package/docs/overlay-stack.md +27 -0
- package/docs/screen-patterns.md +39 -2
- package/docs/sidebar.md +15 -0
- package/docs/steps.md +23 -19
- package/docs/theme-palette.md +3 -3
- package/docs/theming.md +1 -0
- package/docs/timeline.md +18 -1
- package/docs/top.md +7 -0
- package/docs/usage/README.md +9 -2
- package/docs/usage/components/affix.md +18 -4
- package/docs/usage/components/alert-dialog.md +28 -0
- package/docs/usage/components/avatar.md +30 -9
- package/docs/usage/components/badge.md +20 -5
- package/docs/usage/components/chart.md +19 -1
- package/docs/usage/components/chat-message.md +5 -0
- package/docs/usage/components/chat-room-actions.md +58 -0
- package/docs/usage/components/chat-room-create.md +58 -0
- package/docs/usage/components/chat-room-details.md +58 -0
- package/docs/usage/components/chat-room-invite.md +58 -0
- package/docs/usage/components/chat-room-members.md +58 -0
- package/docs/usage/components/chat-room-profile.md +58 -0
- package/docs/usage/components/checkbox-group.md +24 -1
- package/docs/usage/components/choice-row.md +19 -4
- package/docs/usage/components/composition-layout.md +6 -2
- package/docs/usage/components/counter-badge.md +2 -2
- package/docs/usage/components/design-system-provider.md +2 -1
- package/docs/usage/components/divider.md +12 -1
- package/docs/usage/components/field.md +51 -0
- package/docs/usage/components/icon-button.md +48 -4
- package/docs/usage/components/icon-tile.md +13 -2
- package/docs/usage/components/image.md +25 -0
- package/docs/usage/components/list-row.md +8 -4
- package/docs/usage/components/list.md +1 -1
- package/docs/usage/components/notification-item.md +5 -1
- package/docs/usage/components/permission-screen.md +25 -4
- package/docs/usage/components/screen-layout.md +27 -1
- package/docs/usage/components/segmented-control.md +16 -3
- package/docs/usage/components/select.md +1 -0
- package/docs/usage/components/settings-screen.md +5 -4
- package/docs/usage/components/sheet.md +2 -1
- package/docs/usage/components/sidebar.md +34 -6
- package/docs/usage/components/steps.md +29 -8
- package/docs/usage/components/surface.md +2 -1
- package/docs/usage/components/text-area.md +2 -1
- package/docs/usage/components/timeline.md +25 -4
- package/docs/usage/components/top.md +15 -2
- package/docs/usage/compositions/chat-room.md +110 -0
- package/docs/usage/screens/common-settings.md +3 -2
- package/docs/usage/screens/flow-permission.md +3 -3
- package/docs/usage/tokens/elevation-opacity.md +14 -2
- package/package.json +7 -1
package/dist/top.d.ts
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* 계약을 합치지 않았다 — 합치면 "이 Section이 화면의 h1인가"를 매번 물어야 한다.
|
|
15
15
|
*/
|
|
16
16
|
export type TopSize = "medium" | "large";
|
|
17
|
+
/**
|
|
18
|
+
* `\n` in the title (2026-10-23). `collapse`(기본): 줄바꿈 문자는 공백이 되고 폭에 따라 자연스럽게 감긴다 — Web이 원래
|
|
19
|
+
* 그렇게 그렸고, Native만 `\n`을 그대로 그려 두 플랫폼이 달랐다. `preserve`: 제품이 의도한 줄바꿈을 지킨다(히어로 문구).
|
|
20
|
+
* 어느 쪽이든 제목 이름은 한 문장으로 읽힌다 — 줄은 공백 하나로 이어진다. 포트폴리오 사이트 히어로가 `white-space: pre-line`
|
|
21
|
+
* span을 Heading 안에 넣느라 Top을 채택하지 못했다.
|
|
22
|
+
*/
|
|
23
|
+
export type TopTitleLineBreaks = "collapse" | "preserve";
|
|
17
24
|
export type TopDescriptor = Readonly<{
|
|
18
25
|
title: string;
|
|
19
26
|
/** 제목 위 한 줄. 카테고리·단계처럼 제목을 한정하는 짧은 말. */
|
|
@@ -21,6 +28,8 @@ export type TopDescriptor = Readonly<{
|
|
|
21
28
|
/** 제목 아래 보조 문장. 길면 줄바꿈되며 잘리지 않는다. */
|
|
22
29
|
description?: string;
|
|
23
30
|
size?: TopSize;
|
|
31
|
+
/** 제목 안 `\n`의 처리. 기본 `collapse`. */
|
|
32
|
+
titleLineBreaks?: TopTitleLineBreaks;
|
|
24
33
|
/**
|
|
25
34
|
* 문서 구조상의 제목 단계. 화면당 하나인 Top은 `1`이 기본이지만, 한 라우트가
|
|
26
35
|
* 여러 화면을 담는 경우(시트 안의 화면 등) 제품이 낮출 수 있다.
|
|
@@ -30,8 +39,18 @@ export type TopDescriptor = Readonly<{
|
|
|
30
39
|
export declare const topDefaults: {
|
|
31
40
|
readonly size: "large";
|
|
32
41
|
readonly headingLevel: 1;
|
|
42
|
+
readonly titleLineBreaks: "collapse";
|
|
33
43
|
};
|
|
34
44
|
export declare function validateTopDescriptor(descriptor: TopDescriptor): void;
|
|
45
|
+
export type ResolvedTopTitle = Readonly<{
|
|
46
|
+
/** Text to draw. `preserve` keeps one `\n` between trimmed, non-empty lines; `collapse` has none. */
|
|
47
|
+
text: string;
|
|
48
|
+
/** The heading's spoken name: the same lines joined by one space, so an intended break never cuts the title. */
|
|
49
|
+
accessibleName: string;
|
|
50
|
+
/** Lines as drawn (one entry when collapsed). */
|
|
51
|
+
lines: readonly string[];
|
|
52
|
+
}>;
|
|
53
|
+
export declare function resolveTopTitle(descriptor: TopDescriptor): ResolvedTopTitle;
|
|
35
54
|
/**
|
|
36
55
|
* `large`는 화면의 첫 제목, `medium`은 시트·모달 안의 첫 제목이다. 두 단계뿐인 이유는
|
|
37
56
|
* 세 번째가 필요해지는 자리가 곧 `Section`이기 때문이다 — 크기를 더 늘리는 대신 다른
|
|
@@ -96,7 +115,7 @@ export declare const topRecipe: {
|
|
|
96
115
|
export declare const topBehavior: {
|
|
97
116
|
/** 상태 축이 없다 — 화면이 제목을 바꾸면 그냥 다른 문자열을 넘긴다. */
|
|
98
117
|
readonly controlled: readonly [];
|
|
99
|
-
readonly inputs: readonly ["title", "eyebrow", "description", "size", "headingLevel"];
|
|
118
|
+
readonly inputs: readonly ["title", "eyebrow", "description", "size", "headingLevel", "titleLineBreaks"];
|
|
100
119
|
readonly stateAxes: {};
|
|
101
120
|
readonly web: {
|
|
102
121
|
readonly roles: readonly ["heading"];
|
|
@@ -108,6 +127,6 @@ export declare const topBehavior: {
|
|
|
108
127
|
readonly states: readonly [];
|
|
109
128
|
readonly actions: readonly [];
|
|
110
129
|
};
|
|
111
|
-
readonly scenarios: readonly ["the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text", "top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome", "description-wraps-instead-of-truncating-so-large-text-never-hides-the-question", "a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out", "eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form"];
|
|
130
|
+
readonly scenarios: readonly ["the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text", "top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome", "description-wraps-instead-of-truncating-so-large-text-never-hides-the-question", "preserved-title-line-breaks-change-only-the-drawing-and-the-heading-name-stays-one-sentence", "a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out", "eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form"];
|
|
112
131
|
};
|
|
113
132
|
//# sourceMappingURL=top.d.ts.map
|
package/dist/top.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"top.d.ts","sourceRoot":"","sources":["../src/top.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"top.d.ts","sourceRoot":"","sources":["../src/top.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AACzC;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,UAAU,CAAC;AAEzD,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oCAAoC;IACpC,eAAe,CAAC,EAAE,kBAAkB,CAAC;IACrC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;CAC1B,CAAC,CAAC;AAEH,eAAO,MAAM,WAAW;;;;CAIsF,CAAC;AAQ/G,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,aAAa,GAAG,IAAI,CAyBrE;AAED,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,qGAAqG;IACrG,IAAI,EAAE,MAAM,CAAC;IACb,gHAAgH;IAChH,cAAc,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B,CAAC,CAAC;AAEH,wBAAgB,eAAe,CAAC,UAAU,EAAE,aAAa,GAAG,gBAAgB,CAQ3E;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAoBpB,uDAAuD;;;;;CAexD,CAAC;AAEF,eAAO,MAAM,WAAW;IACtB,6CAA6C;;;;;;;;;;;;;;;CAcV,CAAC"}
|
package/dist/top.js
CHANGED
|
@@ -3,6 +3,7 @@ import { semanticColors } from "./semantic-colors.js";
|
|
|
3
3
|
export const topDefaults = {
|
|
4
4
|
size: "large",
|
|
5
5
|
headingLevel: 1,
|
|
6
|
+
titleLineBreaks: "collapse",
|
|
6
7
|
};
|
|
7
8
|
function assertNonEmpty(value, field) {
|
|
8
9
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
@@ -22,11 +23,25 @@ export function validateTopDescriptor(descriptor) {
|
|
|
22
23
|
if (descriptor.size !== undefined && descriptor.size !== "medium" && descriptor.size !== "large") {
|
|
23
24
|
throw new TypeError(`Unsupported Top size: ${String(descriptor.size)}`);
|
|
24
25
|
}
|
|
26
|
+
if (descriptor.titleLineBreaks !== undefined &&
|
|
27
|
+
descriptor.titleLineBreaks !== "collapse" &&
|
|
28
|
+
descriptor.titleLineBreaks !== "preserve") {
|
|
29
|
+
throw new TypeError(`Unsupported Top titleLineBreaks: ${String(descriptor.titleLineBreaks)}`);
|
|
30
|
+
}
|
|
25
31
|
if (descriptor.headingLevel !== undefined &&
|
|
26
32
|
![1, 2, 3].includes(descriptor.headingLevel)) {
|
|
27
33
|
throw new TypeError(`Unsupported Top headingLevel: ${String(descriptor.headingLevel)}`);
|
|
28
34
|
}
|
|
29
35
|
}
|
|
36
|
+
export function resolveTopTitle(descriptor) {
|
|
37
|
+
validateTopDescriptor(descriptor);
|
|
38
|
+
const lines = descriptor.title.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
39
|
+
const accessibleName = lines.join(" ");
|
|
40
|
+
const preserve = (descriptor.titleLineBreaks ?? topDefaults.titleLineBreaks) === "preserve";
|
|
41
|
+
return preserve
|
|
42
|
+
? { text: lines.join("\n"), accessibleName, lines }
|
|
43
|
+
: { text: accessibleName, accessibleName, lines: [accessibleName] };
|
|
44
|
+
}
|
|
30
45
|
/**
|
|
31
46
|
* `large`는 화면의 첫 제목, `medium`은 시트·모달 안의 첫 제목이다. 두 단계뿐인 이유는
|
|
32
47
|
* 세 번째가 필요해지는 자리가 곧 `Section`이기 때문이다 — 크기를 더 늘리는 대신 다른
|
|
@@ -59,7 +74,7 @@ export const topRecipe = {
|
|
|
59
74
|
export const topBehavior = {
|
|
60
75
|
/** 상태 축이 없다 — 화면이 제목을 바꾸면 그냥 다른 문자열을 넘긴다. */
|
|
61
76
|
controlled: [],
|
|
62
|
-
inputs: ["title", "eyebrow", "description", "size", "headingLevel"],
|
|
77
|
+
inputs: ["title", "eyebrow", "description", "size", "headingLevel", "titleLineBreaks"],
|
|
63
78
|
stateAxes: {},
|
|
64
79
|
web: { roles: ["heading"], keyboard: [], focus: "none" },
|
|
65
80
|
native: { roles: ["header"], states: [], actions: [] },
|
|
@@ -67,6 +82,7 @@ export const topBehavior = {
|
|
|
67
82
|
"the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text",
|
|
68
83
|
"top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome",
|
|
69
84
|
"description-wraps-instead-of-truncating-so-large-text-never-hides-the-question",
|
|
85
|
+
"preserved-title-line-breaks-change-only-the-drawing-and-the-heading-name-stays-one-sentence",
|
|
70
86
|
"a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out",
|
|
71
87
|
"eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form",
|
|
72
88
|
],
|
package/dist/top.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"top.js","sourceRoot":"","sources":["../src/top.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { heading, spacing, typography } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * 화면 본문의 **첫 블록**이다. `TopBar`(상단 내비게이션 바)와 다른 자리이고, 그 차이가\n * 이 계약의 존재 이유다.\n *\n * - `TopBar`는 화면에 고정된 크롬이다 — 뒤로가기, 화면 이름, 액션. 스크롤과 무관하게\n * 붙어 있고 safe area를 다룬다.\n * - `Top`은 본문이다 — 스크롤과 함께 올라가고, 사용자가 \"이 화면이 무엇을 묻는지\"\n * 읽는 문장이 여기 있다. 토스 TDS가 \"Top과 ListRow로 화면 대부분을 만든다\"고\n * 말하는 그 Top이고, 지금까지 BurnTok `AppScreenHeader`·Taground `screen-shell`·\n * Diairy `DetailPanel`이 각자 다시 만들던 블록이다.\n *\n * `Section`과도 다르다. Section은 본문 **중간**의 묶음 제목이라 heading level이 화면\n * 구조에 종속되지만, Top은 화면당 하나뿐인 첫 제목이라 기본이 `h1`이다. 그래서 두\n * 계약을 합치지 않았다 — 합치면 \"이 Section이 화면의 h1인가\"를 매번 물어야 한다.\n */\nexport type TopSize = \"medium\" | \"large\";\n\nexport type TopDescriptor = Readonly<{\n title: string;\n /** 제목 위 한 줄. 카테고리·단계처럼 제목을 한정하는 짧은 말. */\n eyebrow?: string;\n /** 제목 아래 보조 문장. 길면 줄바꿈되며 잘리지 않는다. */\n description?: string;\n size?: TopSize;\n /**\n * 문서 구조상의 제목 단계. 화면당 하나인 Top은 `1`이 기본이지만, 한 라우트가\n * 여러 화면을 담는 경우(시트 안의 화면 등) 제품이 낮출 수 있다.\n */\n headingLevel?: 1 | 2 | 3;\n}>;\n\nexport const topDefaults = {\n size: \"large\",\n headingLevel: 1,\n} as const satisfies Readonly<{ size: TopSize; headingLevel: 1 | 2 | 3 }>;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`Top ${field} must not be empty`);\n }\n}\n\nexport function validateTopDescriptor(descriptor: TopDescriptor): void {\n if (descriptor === null || typeof descriptor !== \"object\") {\n throw new TypeError(\"Top descriptor must be an object\");\n }\n assertNonEmpty(descriptor.title, \"title\");\n if (descriptor.eyebrow !== undefined) assertNonEmpty(descriptor.eyebrow, \"eyebrow\");\n if (descriptor.description !== undefined) {\n assertNonEmpty(descriptor.description, \"description\");\n }\n if (descriptor.size !== undefined && descriptor.size !== \"medium\" && descriptor.size !== \"large\") {\n throw new TypeError(`Unsupported Top size: ${String(descriptor.size)}`);\n }\n if (\n descriptor.headingLevel !== undefined &&\n ![1, 2, 3].includes(descriptor.headingLevel)\n ) {\n throw new TypeError(`Unsupported Top headingLevel: ${String(descriptor.headingLevel)}`);\n }\n}\n\n/**\n * `large`는 화면의 첫 제목, `medium`은 시트·모달 안의 첫 제목이다. 두 단계뿐인 이유는\n * 세 번째가 필요해지는 자리가 곧 `Section`이기 때문이다 — 크기를 더 늘리는 대신 다른\n * 컴포넌트를 쓰라는 신호로 둔다.\n */\nexport const topRecipe = {\n slots: [\"root\", \"eyebrow\", \"title\", \"description\", \"trailing\"] as const,\n defaults: { size: topDefaults.size },\n sizes: {\n medium: { title: typography.titleLarge, paddingTop: spacing.md, paddingBottom: spacing.sm },\n // The screen's first title uses the HJM ceiling, `heading.level3` (24). It was `heading.level2` (32) until the\n // display tier was removed (2026-10-17 user decision, \"글자 너무 커\").\n large: { title: heading.level3, paddingTop: spacing.xl, paddingBottom: spacing.md },\n },\n eyebrow: {\n color: semanticColors.content.brand,\n textVariant: \"label\" as const,\n marginBottom: spacing.xxs,\n },\n title: { color: semanticColors.content.primary },\n description: {\n color: semanticColors.content.body,\n textVariant: \"body\" as const,\n marginTop: spacing.xs,\n },\n /** 제목 줄 오른쪽 보조 행동. 본문이므로 TopBar처럼 아이콘 전용을 강제하지 않는다. */\n trailing: { gap: spacing.xs },\n gap: spacing.xs,\n} as const satisfies {\n slots: readonly [\"root\", \"eyebrow\", \"title\", \"description\", \"trailing\"];\n defaults: { size: TopSize };\n sizes: Readonly<{\n medium: { title: typeof typography.titleLarge; paddingTop: number; paddingBottom: number };\n large: { title: typeof heading.level3; paddingTop: number; paddingBottom: number };\n }>;\n eyebrow: { color: ColorReference; textVariant: \"label\"; marginBottom: number };\n title: { color: ColorReference };\n description: { color: ColorReference; textVariant: \"body\"; marginTop: number };\n trailing: { gap: number };\n gap: number;\n};\n\nexport const topBehavior = {\n /** 상태 축이 없다 — 화면이 제목을 바꾸면 그냥 다른 문자열을 넘긴다. */\n controlled: [],\n inputs: [\"title\", \"eyebrow\", \"description\", \"size\", \"headingLevel\"],\n stateAxes: {},\n web: { roles: [\"heading\"], keyboard: [], focus: \"none\" },\n native: { roles: [\"header\"], states: [], actions: [] },\n scenarios: [\n \"the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text\",\n \"top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome\",\n \"description-wraps-instead-of-truncating-so-large-text-never-hides-the-question\",\n \"a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out\",\n \"eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form\",\n ],\n} as const satisfies BehaviorContract;\n"]}
|
|
1
|
+
{"version":3,"file":"top.js","sourceRoot":"","sources":["../src/top.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { heading, spacing, typography } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * 화면 본문의 **첫 블록**이다. `TopBar`(상단 내비게이션 바)와 다른 자리이고, 그 차이가\n * 이 계약의 존재 이유다.\n *\n * - `TopBar`는 화면에 고정된 크롬이다 — 뒤로가기, 화면 이름, 액션. 스크롤과 무관하게\n * 붙어 있고 safe area를 다룬다.\n * - `Top`은 본문이다 — 스크롤과 함께 올라가고, 사용자가 \"이 화면이 무엇을 묻는지\"\n * 읽는 문장이 여기 있다. 토스 TDS가 \"Top과 ListRow로 화면 대부분을 만든다\"고\n * 말하는 그 Top이고, 지금까지 BurnTok `AppScreenHeader`·Taground `screen-shell`·\n * Diairy `DetailPanel`이 각자 다시 만들던 블록이다.\n *\n * `Section`과도 다르다. Section은 본문 **중간**의 묶음 제목이라 heading level이 화면\n * 구조에 종속되지만, Top은 화면당 하나뿐인 첫 제목이라 기본이 `h1`이다. 그래서 두\n * 계약을 합치지 않았다 — 합치면 \"이 Section이 화면의 h1인가\"를 매번 물어야 한다.\n */\nexport type TopSize = \"medium\" | \"large\";\n/**\n * `\\n` in the title (2026-10-23). `collapse`(기본): 줄바꿈 문자는 공백이 되고 폭에 따라 자연스럽게 감긴다 — Web이 원래\n * 그렇게 그렸고, Native만 `\\n`을 그대로 그려 두 플랫폼이 달랐다. `preserve`: 제품이 의도한 줄바꿈을 지킨다(히어로 문구).\n * 어느 쪽이든 제목 이름은 한 문장으로 읽힌다 — 줄은 공백 하나로 이어진다. 포트폴리오 사이트 히어로가 `white-space: pre-line`\n * span을 Heading 안에 넣느라 Top을 채택하지 못했다.\n */\nexport type TopTitleLineBreaks = \"collapse\" | \"preserve\";\n\nexport type TopDescriptor = Readonly<{\n title: string;\n /** 제목 위 한 줄. 카테고리·단계처럼 제목을 한정하는 짧은 말. */\n eyebrow?: string;\n /** 제목 아래 보조 문장. 길면 줄바꿈되며 잘리지 않는다. */\n description?: string;\n size?: TopSize;\n /** 제목 안 `\\n`의 처리. 기본 `collapse`. */\n titleLineBreaks?: TopTitleLineBreaks;\n /**\n * 문서 구조상의 제목 단계. 화면당 하나인 Top은 `1`이 기본이지만, 한 라우트가\n * 여러 화면을 담는 경우(시트 안의 화면 등) 제품이 낮출 수 있다.\n */\n headingLevel?: 1 | 2 | 3;\n}>;\n\nexport const topDefaults = {\n size: \"large\",\n headingLevel: 1,\n titleLineBreaks: \"collapse\",\n} as const satisfies Readonly<{ size: TopSize; headingLevel: 1 | 2 | 3; titleLineBreaks: TopTitleLineBreaks }>;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`Top ${field} must not be empty`);\n }\n}\n\nexport function validateTopDescriptor(descriptor: TopDescriptor): void {\n if (descriptor === null || typeof descriptor !== \"object\") {\n throw new TypeError(\"Top descriptor must be an object\");\n }\n assertNonEmpty(descriptor.title, \"title\");\n if (descriptor.eyebrow !== undefined) assertNonEmpty(descriptor.eyebrow, \"eyebrow\");\n if (descriptor.description !== undefined) {\n assertNonEmpty(descriptor.description, \"description\");\n }\n if (descriptor.size !== undefined && descriptor.size !== \"medium\" && descriptor.size !== \"large\") {\n throw new TypeError(`Unsupported Top size: ${String(descriptor.size)}`);\n }\n if (\n descriptor.titleLineBreaks !== undefined &&\n descriptor.titleLineBreaks !== \"collapse\" &&\n descriptor.titleLineBreaks !== \"preserve\"\n ) {\n throw new TypeError(`Unsupported Top titleLineBreaks: ${String(descriptor.titleLineBreaks)}`);\n }\n if (\n descriptor.headingLevel !== undefined &&\n ![1, 2, 3].includes(descriptor.headingLevel)\n ) {\n throw new TypeError(`Unsupported Top headingLevel: ${String(descriptor.headingLevel)}`);\n }\n}\n\nexport type ResolvedTopTitle = Readonly<{\n /** Text to draw. `preserve` keeps one `\\n` between trimmed, non-empty lines; `collapse` has none. */\n text: string;\n /** The heading's spoken name: the same lines joined by one space, so an intended break never cuts the title. */\n accessibleName: string;\n /** Lines as drawn (one entry when collapsed). */\n lines: readonly string[];\n}>;\n\nexport function resolveTopTitle(descriptor: TopDescriptor): ResolvedTopTitle {\n validateTopDescriptor(descriptor);\n const lines = descriptor.title.split(/\\r?\\n/).map((line) => line.trim()).filter((line) => line.length > 0);\n const accessibleName = lines.join(\" \");\n const preserve = (descriptor.titleLineBreaks ?? topDefaults.titleLineBreaks) === \"preserve\";\n return preserve\n ? { text: lines.join(\"\\n\"), accessibleName, lines }\n : { text: accessibleName, accessibleName, lines: [accessibleName] };\n}\n\n/**\n * `large`는 화면의 첫 제목, `medium`은 시트·모달 안의 첫 제목이다. 두 단계뿐인 이유는\n * 세 번째가 필요해지는 자리가 곧 `Section`이기 때문이다 — 크기를 더 늘리는 대신 다른\n * 컴포넌트를 쓰라는 신호로 둔다.\n */\nexport const topRecipe = {\n slots: [\"root\", \"eyebrow\", \"title\", \"description\", \"trailing\"] as const,\n defaults: { size: topDefaults.size },\n sizes: {\n medium: { title: typography.titleLarge, paddingTop: spacing.md, paddingBottom: spacing.sm },\n // The screen's first title uses the HJM ceiling, `heading.level3` (24). It was `heading.level2` (32) until the\n // display tier was removed (2026-10-17 user decision, \"글자 너무 커\").\n large: { title: heading.level3, paddingTop: spacing.xl, paddingBottom: spacing.md },\n },\n eyebrow: {\n color: semanticColors.content.brand,\n textVariant: \"label\" as const,\n marginBottom: spacing.xxs,\n },\n title: { color: semanticColors.content.primary },\n description: {\n color: semanticColors.content.body,\n textVariant: \"body\" as const,\n marginTop: spacing.xs,\n },\n /** 제목 줄 오른쪽 보조 행동. 본문이므로 TopBar처럼 아이콘 전용을 강제하지 않는다. */\n trailing: { gap: spacing.xs },\n gap: spacing.xs,\n} as const satisfies {\n slots: readonly [\"root\", \"eyebrow\", \"title\", \"description\", \"trailing\"];\n defaults: { size: TopSize };\n sizes: Readonly<{\n medium: { title: typeof typography.titleLarge; paddingTop: number; paddingBottom: number };\n large: { title: typeof heading.level3; paddingTop: number; paddingBottom: number };\n }>;\n eyebrow: { color: ColorReference; textVariant: \"label\"; marginBottom: number };\n title: { color: ColorReference };\n description: { color: ColorReference; textVariant: \"body\"; marginTop: number };\n trailing: { gap: number };\n gap: number;\n};\n\nexport const topBehavior = {\n /** 상태 축이 없다 — 화면이 제목을 바꾸면 그냥 다른 문자열을 넘긴다. */\n controlled: [],\n inputs: [\"title\", \"eyebrow\", \"description\", \"size\", \"headingLevel\", \"titleLineBreaks\"],\n stateAxes: {},\n web: { roles: [\"heading\"], keyboard: [], focus: \"none\" },\n native: { roles: [\"header\"], states: [], actions: [] },\n scenarios: [\n \"the-title-is-a-real-heading-element-at-the-declared-level-not-styled-text\",\n \"top-is-body-content-that-scrolls-away-while-topbar-is-fixed-chrome\",\n \"description-wraps-instead-of-truncating-so-large-text-never-hides-the-question\",\n \"preserved-title-line-breaks-change-only-the-drawing-and-the-heading-name-stays-one-sentence\",\n \"a-trailing-action-shares-the-title-row-and-drops-below-it-when-space-runs-out\",\n \"eyebrow-never-carries-information-the-title-does-not-repeat-in-some-form\",\n ],\n} as const satisfies BehaviorContract;\n"]}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAiB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"1.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAiB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"1.43.0\" as const;\n"]}
|
package/docs/affix.md
CHANGED
|
@@ -25,3 +25,10 @@ CSS sticky의 기준을 바꾼다. 부모 자체가 콘텐츠와 같은 높이
|
|
|
25
25
|
계약: [affix.ts](../src/affix.ts). UI: [renderer](../../react/src/affix.tsx).
|
|
26
26
|
사용 예제: [Web additions](../../../showcase/web/src/patterns/WebAdditions.stories.tsx).
|
|
27
27
|
검증: [브라우저 스크롤·초점 회귀](../../react/test/web-additions.browser.test.tsx).
|
|
28
|
+
|
|
29
|
+
## 쌓임과 바탕 (2026-10-23, 개발 소스·미게시)
|
|
30
|
+
|
|
31
|
+
`layer`(`sticky` 기본 · `base`)는 공통 `layer` 단계에서 고르고, `fill`(`canvas` 기본 · `none`)은 상자 뒤 바탕이다.
|
|
32
|
+
기본값은 `affixRecipe.defaults`가 소유한다. 이전에는 z-index가 없어 뒤에 오는 position 지정 콘텐츠가 고정된 상자 위로 그려졌다.
|
|
33
|
+
상자는 언제나 sticky 또는 relative라 고정·해제 상태 모두 같은 쌓임을 가진다. 숫자 z-index는 받지 않는다 —
|
|
34
|
+
dropdown·modal 단계보다 위로 올라가면 메뉴와 대화상자를 가리기 때문이다. 근거와 사용법은 [사용 지침](usage/components/affix.md).
|
package/docs/avatar-fallback.md
CHANGED
|
@@ -79,3 +79,22 @@ or exhaustive geometry and screen-reader behavior.
|
|
|
79
79
|
the last word, counted in code points. Web had taken the first two words and Native the first and last, so "Kim Min Jun"
|
|
80
80
|
read "KM" on Web and "KJ" on Native; Native also split surrogate pairs. First+last was kept because it shows the family and
|
|
81
81
|
given names instead of a middle name. Native `initials` still overrides (at most three code points).
|
|
82
|
+
|
|
83
|
+
## Presentation axes (2026-10-23)
|
|
84
|
+
|
|
85
|
+
Burntok's `UserAvatar` (about 70 call sites, 22/28/32/44, transparent circle filled by a generated SVG) and
|
|
86
|
+
Yajalal's `PlayerAvatar` (rounded square with a team badge on the corner) could not adopt Avatar: `name` was
|
|
87
|
+
required, Web had no image host, Web had four sizes, Native threw below 24, and the frame always drew a background
|
|
88
|
+
and a 1px border. The axes now live in `avatarRecipe`:
|
|
89
|
+
|
|
90
|
+
- `size`: named steps `xxsmall` 22 · `xsmall` 28 · `small` 32 · `medium` 40 · `touch` 44 · `large` 48 · `xlarge` 64
|
|
91
|
+
(`resolveAvatarSize`). Native keeps numeric points for existing callers; the floor is the smallest step (22),
|
|
92
|
+
where the label-size initials still fit.
|
|
93
|
+
- `decorative`: hides the whole avatar and makes `name` optional. A meaningful avatar still requires `name`
|
|
94
|
+
(Web) or `name` + `accessibilityLabel` (Native).
|
|
95
|
+
- `frame`: `outlined` (default, unchanged) · `surface` · `none`.
|
|
96
|
+
- `shape`: `circle` · `rounded` on both renderers (Native was circle only).
|
|
97
|
+
- `accessory` + `accessoryPlacement` (`bottom-end` default · `top-end`): a non-interactive corner mark outside the
|
|
98
|
+
clipping frame, on the avatar's box corner (`avatarRecipe.accessory.offset` 0; offsets are outward overhangs as in
|
|
99
|
+
Asset and IconTile). IconTile uses the same placement names.
|
|
100
|
+
- Web `renderImage`: product image host with a generation-guarded `onError`, exclusive with `imageProps`.
|
package/docs/brand-boundary.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 브랜드 경계 — 제품이 바꿀 수 있는 것과 그 경로
|
|
2
2
|
|
|
3
|
-
상태: 규범 · 검토일: 2026-10-
|
|
3
|
+
상태: 규범 · 검토일: 2026-10-23 · 이 문서가 브랜드·테마 주입 규칙의 단일 원본이다.
|
|
4
4
|
[theming.md](./theming.md)는 사용법 예시, [consumer-policy.md](./consumer-policy.md)는 채택 정책,
|
|
5
5
|
[design-system-provider.md](./design-system-provider.md)는 환경 축 설계를 다루며 브랜드 규칙은 여기로 링크한다.
|
|
6
6
|
|
|
@@ -32,6 +32,21 @@ CSS 변수 재정의, 제품 자체 토큰 생성기)였고, 문서 세 개가
|
|
|
32
32
|
|
|
33
33
|
안전 영역 등 환경을 연결하는 `value={resolvedValue}` 중첩 Provider도 현재 모드의 해석된 브랜드 팔레트를 하위 Provider에 전달한다. 명시적으로 새 `brandPalette`를 지정하면 기존처럼 그 범위를 대체한다. 이 구분은 환경 bridge 뒤의 일반 Provider가 브랜드색을 기본값으로 되돌리던 문제를 막는다.
|
|
34
34
|
|
|
35
|
+
### 1.1 하위 범위의 일부 역할만 바꾸기 — `brandPaletteMode` (개발 소스·미게시)
|
|
36
|
+
|
|
37
|
+
중첩 Provider의 `brandPaletteMode`는 하위 `brandPalette`와 상위 팔레트의 관계를 정한다.
|
|
38
|
+
|
|
39
|
+
| 값 | 결과 | 쓰는 곳 |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `replace`(기본) | HJM 기본값(또는 프로필) 위에 하위가 넘긴 역할만 놓는다. 상위 브랜드는 이 범위에서 끊긴다 | 다른 브랜드의 임베드처럼 상위 색을 물려받지 않아야 하는 범위 |
|
|
42
|
+
| `extend` | theme별로 상위 팔레트 위에 하위가 넘긴 역할만 덮는다. 더 깊은 `extend`도 차례로 쌓인다 | 종이·바다처럼 바탕·표면 몇 역할만 바꾸고 제품 브랜드는 유지하는 범위 |
|
|
43
|
+
|
|
44
|
+
`brandPalette` 없이 준 모드는 효과가 없다. `value` 중첩 Provider와는 함께 쓸 수 없다(타입이 막는다).
|
|
45
|
+
기존 `replace` 기본값은 바꾸지 않는다. 기본을 `extend`로 바꾸면 지금 상위 색을 끊으려고 `brandPalette`를 다시 준
|
|
46
|
+
범위가 조용히 상위 색을 물려받게 되기 때문이다. 결정 근거: 2026-10-23 번뚝 웹·앱 ThemeProvider는 상위 덮어쓰기를
|
|
47
|
+
직접 누적했고(`apps/web/src/components/ThemeProvider.tsx` `BrandPaletteScope`), 다에리 앱은 `shoreBrandPaletteWith()`로
|
|
48
|
+
기본 팔레트를 매번 다시 합쳤다. 병합 규칙은 contracts `resolveProviderBrandPalette(inherited, supplied, value, mode)`가 소유한다.
|
|
49
|
+
|
|
35
50
|
## 2. 모든 브랜드 팔레트는 대비 검사를 통과해야 한다
|
|
36
51
|
|
|
37
52
|
`@hjmds/design-contracts/palette-contrast`의 `checkBrandPaletteContrast(brandPalette)`가 Provider와 같은 방식으로
|
|
@@ -98,5 +113,12 @@ HJM: semantic color role과 light/dark 대비, spacing·type scale·radius·moti
|
|
|
98
113
|
키보드·screen-reader 행동, loading·empty·error·disabled·focus 문법.
|
|
99
114
|
|
|
100
115
|
제품: 이름·로고·대표 이미지, 검증한 `designProfile` 데이터와 `brandPalette`로 표현한 브랜드 색, 도메인 상태 → `AccentTone` 매핑, 콘텐츠 밀도와
|
|
101
|
-
정보 우선순위, copy·i18n·내비게이션 구조.
|
|
116
|
+
정보 우선순위, copy·i18n·내비게이션 구조.
|
|
117
|
+
|
|
118
|
+
### 제품 분류 색(Badge `categoryColor`) — 개발 소스, 미게시
|
|
119
|
+
|
|
120
|
+
경기 시리즈·계약 등급처럼 상태(`AccentTone`)도 브랜드도 아닌 **제품 분류**는 Badge의 `categoryColor`(테마별 6자리 hex)로 넣는다.
|
|
121
|
+
분류의 색 정의는 제품이, 틀·글자·간격과 글자색 선택은 HJM이 소유한다. HJM은 `resolveBadgeCategoryColor`(`@hjmds/design-contracts/recipes`)로
|
|
122
|
+
테마마다 채움 위 글자 또는 outline 글자의 대비를 계산하고 4.5:1 미만이면 렌더 전에 던진다. 배지 하나를 `brandPalette` Provider로
|
|
123
|
+
감싸 분류색을 칠하지 않는다 — 그 범위의 primary·contentBrand 등이 함께 바뀌는 브랜드 경로의 오용이다(2026-10-23 야잘알 GameSeriesBadge·FaCards 감사). 제품 고유 gradient는 light/dark·on-color 대비와 reduce-motion을 제품 테스트로
|
|
102
124
|
검증한다([consumer-policy.md §4](./consumer-policy.md#4-제품-정체성-경계)).
|
package/docs/chart.md
CHANGED
|
@@ -204,3 +204,10 @@ reduced motion은 즉시 교체하고, background는 새 표현에 정착하며,
|
|
|
204
204
|
`LineChart.scrubEmphasis="before" | "after"`는 현재 포커스의 x에서 클립을 나누어 선택한
|
|
205
205
|
쪽을 계열 색으로, 나머지를 축 레이블 색으로 표시한다. 새로운 점·수치·추천을 계산하지
|
|
206
206
|
않으며 기존 드래그·키보드·접근성 포커스와 `holdOnRelease`를 그대로 사용한다.
|
|
207
|
+
|
|
208
|
+
## 범례 배치 (2026-10-23, 개발 소스·미게시)
|
|
209
|
+
|
|
210
|
+
`ChartLegend.layout`은 `stack`(계열마다 한 행)과 `inline`(한 줄로 흐르고 감김) 두 값이다. 견본 10·모서리 3, 견본·이름·값 사이
|
|
211
|
+
`spacing.xs`, inline 항목 사이 `spacing.md`·줄 사이 `spacing.xxs`, stack 행 최소 32, 선택 밖 흐림 0.5는 `chartRecipe.legend`가 소유한다.
|
|
212
|
+
선택의창 지도 범례가 Native의 세로 고정 때문에 ChartLegend를 채택하지 못한 것이 계기다(사용 지침 [Chart](usage/components/chart.md)).
|
|
213
|
+
기본값은 기존 동작을 유지한다: Web은 선택 버튼(`onSelect`)이 없으면 `inline`, 있으면 `stack`, Native는 `stack`.
|
package/docs/collection-rail.md
CHANGED
|
@@ -33,7 +33,10 @@ Native row만 RTL에서 역방향으로 배치하며 데이터/React key/읽기
|
|
|
33
33
|
|
|
34
34
|
## 배치와 플랫폼 경계
|
|
35
35
|
|
|
36
|
-
comfortable 최대 폭360은 `layout.readingMaxWidth / 2`, compact240은 `/3
|
|
36
|
+
comfortable 최대 폭360은 `layout.readingMaxWidth / 2`, compact240은 `/3`, chip144는 `/5`다(2026-10-23 추가).
|
|
37
|
+
간격은 `gapSteps`의 spacing 단계로 카드 밀도는 `md`16, chip은 `xs`8이다. 야잘알 홈의 145pt 미니 스코어보드 줄이
|
|
38
|
+
카드 폭(240+)을 강제받아 원시 ScrollView를 쓰던 문제다. 내용 폭 항목(`itemWidth="content"`)은 넣지 않았다 —
|
|
39
|
+
이전/다음·앞쪽 anchor·초점 노출이 모두 한 항목 폭(stride) 단위의 geometry이고, 실제 소비처의 칩도 같은 폭이다. 실제 폭은 viewport와
|
|
37
40
|
`spacing.xl`24 hint를 함께 고려한다. 간격 `spacing.md`16을 유지하므로 viewport358에서 카드334와 다음 카드8이 노출된다.
|
|
38
41
|
뷰포트24 이하에서는 peek보다 내용 폭을 우선한다. 길고 많은 데이터는 VirtualList/제품별 가상화 계약을 사용한다.
|
|
39
42
|
브랜드 색·표면·폰트·카드 경계는 Provider와 Card가 소유한다. 가로 탐색을 테마의 미지원 screen/composition 축에 자동 연결하지 않는다.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Component maturity and evidence manifest
|
|
2
2
|
|
|
3
|
-
> Generated from `@hjmds/design-contracts` 1.
|
|
3
|
+
> Generated from `@hjmds/design-contracts` 1.43.0. Do not edit this file directly; run `pnpm contracts:sync`.
|
|
4
4
|
|
|
5
5
|
This document is the human-readable projection of the same catalog and Showcase manifest consumed by product CI.
|
|
6
6
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"designSystemVersion": "1.
|
|
3
|
+
"designSystemVersion": "1.43.0",
|
|
4
4
|
"source": "packages/design-contracts/docs/generated/showcase-manifest.json",
|
|
5
5
|
"surfaces": {
|
|
6
6
|
"web": {
|
|
7
7
|
"packageName": "@hjmds/react",
|
|
8
|
-
"packageVersion": "1.
|
|
8
|
+
"packageVersion": "1.43.0",
|
|
9
9
|
"activeComponents": 131,
|
|
10
10
|
"claimedComponents": 131,
|
|
11
11
|
"completeScenarioComponents": 130,
|
|
@@ -830,7 +830,8 @@
|
|
|
830
830
|
"claimed": true,
|
|
831
831
|
"subpath": "./actions",
|
|
832
832
|
"exportNames": [
|
|
833
|
-
"IconButton"
|
|
833
|
+
"IconButton",
|
|
834
|
+
"IconButtonLink"
|
|
834
835
|
],
|
|
835
836
|
"proofs": [
|
|
836
837
|
{
|
|
@@ -7062,7 +7063,7 @@
|
|
|
7062
7063
|
},
|
|
7063
7064
|
"native": {
|
|
7064
7065
|
"packageName": "@hjmds/react-native",
|
|
7065
|
-
"packageVersion": "1.
|
|
7066
|
+
"packageVersion": "1.43.0",
|
|
7066
7067
|
"activeComponents": 113,
|
|
7067
7068
|
"claimedComponents": 113,
|
|
7068
7069
|
"completeScenarioComponents": 110,
|
|
@@ -7768,7 +7769,8 @@
|
|
|
7768
7769
|
"claimed": true,
|
|
7769
7770
|
"subpath": "./bottom-cta",
|
|
7770
7771
|
"exportNames": [
|
|
7771
|
-
"IconButton"
|
|
7772
|
+
"IconButton",
|
|
7773
|
+
"IconButtonLink"
|
|
7772
7774
|
],
|
|
7773
7775
|
"proofs": [
|
|
7774
7776
|
{
|
|
@@ -11868,7 +11870,9 @@
|
|
|
11868
11870
|
"claimed": true,
|
|
11869
11871
|
"subpath": "./screens",
|
|
11870
11872
|
"exportNames": [
|
|
11871
|
-
"ScreenLayout"
|
|
11873
|
+
"ScreenLayout",
|
|
11874
|
+
"ScreenHost",
|
|
11875
|
+
"ScreenRefreshControl"
|
|
11872
11876
|
],
|
|
11873
11877
|
"proofs": [
|
|
11874
11878
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Renderer evidence coverage
|
|
2
2
|
|
|
3
|
-
> Generated for HJM 1.
|
|
3
|
+
> Generated for HJM 1.43.0. Do not edit directly; run `pnpm evidence:sync`.
|
|
4
4
|
|
|
5
5
|
This projection joins the canonical surface maturity manifest with first-party renderer claims. Missing scenarios are explicit beta promotion debt; stable surfaces are blocked by CI until none remain.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"designSystemVersion": "1.
|
|
3
|
+
"designSystemVersion": "1.43.0",
|
|
4
4
|
"packageName": "@hjmds/design-contracts",
|
|
5
5
|
"components": [
|
|
6
6
|
{
|
|
@@ -809,7 +809,8 @@
|
|
|
809
809
|
"name": "IconButton",
|
|
810
810
|
"label": "아이콘 버튼",
|
|
811
811
|
"companions": [
|
|
812
|
-
"ClipboardIconButton"
|
|
812
|
+
"ClipboardIconButton",
|
|
813
|
+
"IconButtonLink"
|
|
813
814
|
],
|
|
814
815
|
"extensions": [
|
|
815
816
|
"NotificationBell"
|
|
@@ -4228,7 +4229,8 @@
|
|
|
4228
4229
|
"companions": [],
|
|
4229
4230
|
"extensions": [
|
|
4230
4231
|
"GridReveal",
|
|
4231
|
-
"ImageViewer"
|
|
4232
|
+
"ImageViewer",
|
|
4233
|
+
"ImageViewerTrigger"
|
|
4232
4234
|
],
|
|
4233
4235
|
"category": "data-display",
|
|
4234
4236
|
"platform": "shared",
|
|
@@ -4976,7 +4978,9 @@
|
|
|
4976
4978
|
"storyId": "overlay/alert-dialog",
|
|
4977
4979
|
"name": "AlertDialog",
|
|
4978
4980
|
"label": "확인 대화상자",
|
|
4979
|
-
"companions": [
|
|
4981
|
+
"companions": [
|
|
4982
|
+
"AlertDialogQueueProvider"
|
|
4983
|
+
],
|
|
4980
4984
|
"extensions": [],
|
|
4981
4985
|
"category": "overlay",
|
|
4982
4986
|
"platform": "adaptive",
|
|
@@ -5414,7 +5418,10 @@
|
|
|
5414
5418
|
"storyId": "layout/screen-layout",
|
|
5415
5419
|
"name": "ScreenLayout",
|
|
5416
5420
|
"label": "화면 골격",
|
|
5417
|
-
"companions": [
|
|
5421
|
+
"companions": [
|
|
5422
|
+
"ScreenHost",
|
|
5423
|
+
"ScreenRefreshControl"
|
|
5424
|
+
],
|
|
5418
5425
|
"extensions": [],
|
|
5419
5426
|
"category": "layout",
|
|
5420
5427
|
"platform": "adaptive",
|
package/docs/image.md
CHANGED
|
@@ -77,6 +77,11 @@ Dialog·Sheet가 이미 소유한 문제입니다. 이 조합을 Image 자체에
|
|
|
77
77
|
필요해지면 별도 Web 전용 `ImagePreview` overlay가 기존 `Dialog`/`AnchoredOverlay` 계약을
|
|
78
78
|
조합해서 풀 문제이지, `Image`가 escape hatch로 열 문제가 아닙니다.
|
|
79
79
|
|
|
80
|
+
2026-10-23: 같은 이유로 "누르면 크게 보기"의 트리거도 `Image` prop이 아니라 뷰어 쪽 `ImageViewerTrigger`
|
|
81
|
+
(`/image-viewer`)에 둡니다. 포트폴리오 사이트와 Spint가 사진을 원시 button·Pressable로 감싸 이름·포커스 링·
|
|
82
|
+
커서를 각자 정하던 우회를 대신합니다. 트리거는 button 의미와 `imageViewerTriggerRecipe`(액자 `radius.md`,
|
|
83
|
+
`zoom-in` 커서, 눌림·비활성 투명도)를 소유하고, 뷰어의 `open` 수명주기는 여전히 `ImageViewer`와 제품이 가집니다.
|
|
84
|
+
|
|
80
85
|
## 플랫폼 번역
|
|
81
86
|
|
|
82
87
|
`fit: cover | contain | fill`은 Web `object-fit`과 거의 그대로 대응합니다. RN
|
package/docs/list-row.md
CHANGED
|
@@ -12,3 +12,11 @@
|
|
|
12
12
|
주면 된다.
|
|
13
13
|
- 로딩 행은 상호작용하지 않는다 — 아직 활성화할 것이 없다.
|
|
14
14
|
- `role="status"` + `aria-busy`로 알리고, 문구(`loadingLabel`)는 제품이 현지화한다.
|
|
15
|
+
|
|
16
|
+
## trailing accessory (2026-10-23, 개발 소스·미게시)
|
|
17
|
+
|
|
18
|
+
`trailingAccessory="disclosure"`는 행이 다른 화면으로 이동한다는 의미 축이다. 모양(`chevronEnd`·`glyph.sm`·`content.decorative`·Icon `regular`)과
|
|
19
|
+
간격(trailing 값 뒤 `spacing.xs`)은 `listRowRecipe.accessories`가, 순서(content → trailing → accessory → `trailingAction`)는 두 renderer가 같게 지킨다.
|
|
20
|
+
장식이라 Web은 `aria-hidden`, Native는 보조기기에서 숨기며 행 이름에 더하지 않는다. RTL에서는 읽기 방향 끝을 가리킨다.
|
|
21
|
+
기존 API로는 `trailing`에 Icon을 넣는 방법뿐이었고, 제품마다 크기·색·RTL·값과의 간격이 달라졌다(야잘알 10·유틸버스 24·spint 3곳 감사).
|
|
22
|
+
사용법은 [사용 지침](usage/components/list-row.md)을 따른다.
|
|
@@ -97,6 +97,9 @@ Expo Go cannot verify these linked native modules.
|
|
|
97
97
|
paging without a swipe, and unmounts on close to reset its session. Failed images offer retry.
|
|
98
98
|
Host-provided safe-area insets are required when placing it edge-to-edge. Remote image permission,
|
|
99
99
|
URL lifetime and caching remain product responsibilities; the adapter does not fetch metadata.
|
|
100
|
+
- ImageViewerTrigger (same subpath, 2026-10-23) is the button around product media that opens the viewer:
|
|
101
|
+
localized `label`, image-frame focus ring, zoom cursor and pressed opacity from `imageViewerTriggerRecipe`.
|
|
102
|
+
The product still owns `open`; the trigger adds no gesture code of its own.
|
|
100
103
|
- KeyboardDock translates with the keyboard. `clearance` is an additional nonnegative gap above it;
|
|
101
104
|
the host retains bottom safe-area ownership. Do not combine this with another keyboard avoidance
|
|
102
105
|
wrapper around the same content. KeyboardFormScrollView handles focused form fields.
|
package/docs/overlay-stack.md
CHANGED
|
@@ -22,3 +22,30 @@ openSheet({ … }); // 그 다음에 다음 표면을 연다
|
|
|
22
22
|
|
|
23
23
|
**타이머가 사라진다.** `closed`는 Dialog/Sheet의 `onDismissComplete`에서 resolve되므로
|
|
24
24
|
제품이 0ms 타이머로 정리 시점을 추측할 필요가 없다.
|
|
25
|
+
|
|
26
|
+
## 결과를 돌려주는 확인 대기열 — `AlertDialogQueueProvider` (2026-10-23, 개발 소스·미게시)
|
|
27
|
+
|
|
28
|
+
번뚝 웹 `components/ui/AppDialog.tsx`(321줄)와 앱 `AppDialog.tsx`(293줄)가 AlertDialog 둘레에 같은 것을 각자 만들었다.
|
|
29
|
+
결과 Promise, async confirm(처리 중 유지·실패 시 열린 채 오류), FIFO 대기열, 포커스 복귀, route scope, blocking 우선순위다.
|
|
30
|
+
위의 `useDialog`/`useSheet`는 Dialog·Sheet만 받고 두 번째 열기를 거부하며 RN에는 이 층이 없었다. 확인 질문은 다른 확인이 떠 있을 때
|
|
31
|
+
도착하는 것이 정상이므로 "한 번에 하나"를 거부가 아니라 **순서 대기**로 지키는 별도 공급자를 둔다.
|
|
32
|
+
|
|
33
|
+
| 소유 | 위치 |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| 순서·결과 1회 확정·scope 정리 | contracts `createAlertDialogQueue`(`components/alert-dialog`). `enqueue → Promise<AlertDialogResult>`, `settle`은 맨 앞 항목만, `retainScope`가 다른 scope 항목을 `interrupted`로, `setActive(false)`가 전부와 이후 요청을 `interrupted`로 끝낸다 |
|
|
36
|
+
| 한 대화상자의 확인·처리 중·실패 | 기존 `createAlertDialogSession`(AlertDialog). `request.onConfirm`이 Promise면 처리 중 닫힘을 막고, 거부되면 열린 채 오류를 보이며 Promise는 성공할 때까지 기다린다 |
|
|
37
|
+
| 화면 표시 | Web·Native `AlertDialogQueueProvider` + `useAlertDialogQueue().open(request, options?)` (`/alert-dialog-queue`) |
|
|
38
|
+
|
|
39
|
+
- **결과:** Web은 AlertDialog `onOpenChange(false, reason)`의 사유로(`confirm` → confirmed, 나머지 → cancelled 사유), Native는 native 닫힘 뒤 `onResult`로 확정한다.
|
|
40
|
+
그래서 Native는 앞 대화상자가 사라진 뒤에 다음 대화상자를 띄운다.
|
|
41
|
+
- **scope:** 공급자 `scope`(보통 route)가 바뀌면 다른 scope의 질문은 화면에서 내려가고 `interrupted`가 된다. `open`은 호출 컴포넌트가 렌더된 scope에 묶이므로
|
|
42
|
+
떠난 화면의 늦은 콜백도 다음 화면에 질문을 띄우지 못한다. 공급자가 사라지면 남은 질문과 이후 요청이 모두 `interrupted`다.
|
|
43
|
+
- **포커스(Web):** 질문 묶음이 시작될 때 초점이 있던 요소를 기억해 묶음의 모든 대화상자가 그곳으로 돌려준다. 두 번째 대화상자가 지워진 첫 대화상자의 버튼으로 돌아가지 않는다.
|
|
44
|
+
결과는 renderer 갱신보다 먼저 resolve되므로 `await open()` 바로 뒤에 다시 켠 trigger에 초점이 돌아간다. 대화상자가 떠 있는 동안 배경은 이미 inert이므로
|
|
45
|
+
호출부가 trigger를 따로 disabled로 두지 않는다(번뚝은 disabled trigger 때문에 0ms 뒤 초점 재시도를 붙였다). 작업까지 기다릴 때는 `request.onConfirm`에 둔다.
|
|
46
|
+
- **우선순위(Web):** 공급자 `modalPriority`(기본 0)로 대기열 대화상자의 층을 정한다. 필수 gate는 더 높은 `modalPriority`의 모달로 두면 위에서 입력을 받고,
|
|
47
|
+
대기열 대화상자는 그 아래에서 inert로 기다린다. 제품 DOM 감시로 대기열을 멈추지 않는다. Native AlertDialog에는 우선순위 축이 없어 제공하지 않는다.
|
|
48
|
+
- **StrictMode(Web AlertDialog 수정):** 열린 채 마운트한 AlertDialog는 StrictMode의 모의 unmount가 render 중 만든 세션을 interrupt해 확인·취소가 되지 않았다.
|
|
49
|
+
재마운트 때 interrupt된 세션을 새로 만든다. 번뚝은 이를 피하려고 열기를 한 task 미뤘다.
|
|
50
|
+
- 아이콘(Web `icon`)·크기(`size`)는 요청별 `options`, Native 초점 복귀는 `options.returnFocusRef`로 넘긴다. 문구·행동은 `AlertDialogRequest`가 그대로 소유한다.
|
|
51
|
+
|
package/docs/screen-patterns.md
CHANGED
|
@@ -44,7 +44,9 @@ Flutter 앱에는 React 컴포넌트를 주입할 수 없다. 화면 역할과
|
|
|
44
44
|
저장 방식·낙관적 갱신·실패 복구·탈퇴 확인은 [action-session](action-session.md)과 제품이 소유한다.
|
|
45
45
|
- `NotificationInboxScreen`: 필터는 상태가 바뀌어도 유지하고 본문만 교체한다. `NotificationItem`은
|
|
46
46
|
기존 ListRow에 지역화한 읽음 상태와 시각을 조합한다. href/onClick(Web), onPress(Native)를
|
|
47
|
-
그대로 사용하며 표시·탭·스크롤 자체가 읽음 처리를 실행하지 않는다.
|
|
47
|
+
그대로 사용하며 표시·탭·스크롤 자체가 읽음 처리를 실행하지 않는다. `unreadLabel`을 주면 안 읽음 항목에
|
|
48
|
+
NotificationRow와 같은 점(`notificationRowRecipe.unreadSize`, content brand)을 이름과 함께 그린다(2026-10-23,
|
|
49
|
+
Spint가 `trailing`에 "•" 글자를 두던 우회). Native 누르는 행의 합성 이름에 상태·시각 줄과 안 읽음 이름을 포함한다.
|
|
48
50
|
- `ChatMessage`: 발신/수신 정렬, 작성자, 아바타, 답장 인용, 전송 상태와 시각을 합성한다.
|
|
49
51
|
기존 Asset/VoiceNote 등은 children에 넣고 영수증·반응·재시도는 제품이 넘긴다. 전체 타임라인을
|
|
50
52
|
live region으로 만들지 않으므로 과거 메시지 로딩 때 읽기 순서를 가로채지 않는다.
|
|
@@ -158,7 +160,7 @@ UploadItem, RadioGroup, AlertDialog, TextField와 ScreenLayout의 계약을 그
|
|
|
158
160
|
| ModerationScreen | 사유 선택, 신고 활성 조건, 차단 확인 | 서버 신고/권한, 차단 mutation |
|
|
159
161
|
| MediaSelectionScreen | 썸네일 격자, 업로드 상태, 순서/삭제/재시도, 제품 작업 중 순서·회전·삭제 잠금(`disabled`, 1.32.0) | 실제 picker, 권한, 이미지 URI 수명, 업로드, 잠글 작업의 시작·끝 |
|
|
160
162
|
| SearchScreen | 300ms debounce, 이전 요청 AbortSignal, 검색/필터 슬롯, 기본 입력의 확정 신호(`onSubmit`)·검색 중 표시(`searching`)·label 숨김, 한 줄 가로 스크롤 필터 줄, 두 단계 검색 단계(검색 전·입력 중·결과)와 확정 경로, 최근·추천 검색어·제안 배치, 결과 개수·정렬, 적용 필터 칩, 초안/적용 필터 시트, 0건 원인별 복구, 개수 낭독 | API, 제안·결과·개수 조회, 오류 문구, 필터 정의와 계산, 정렬 값, 최근 검색 저장소, 늦은 응답 무시 |
|
|
161
|
-
| PermissionScreen | prompt/denied/granted/unavailable에 맞는 action | OS 요청과 설정 이동, 앱 복귀 후 실제 권한
|
|
163
|
+
| PermissionScreen | prompt/denied/granted/unavailable에 맞는 action, footer 순서 primary → recheck(denied) → alternative(granted 전) → skip(`resolvePermissionFooter`, 2026-10-23) | OS 요청과 설정 이동, 앱 복귀 후 실제 권한 조회, 대체 경로 실행 |
|
|
162
164
|
| OnboardingScreen | 단계 범위 검증, 이전/다음/건너뛰기 | 선택 데이터와 완료 여부 저장 |
|
|
163
165
|
| CommentThreadScreen | 부모/답글, 펼침, 좋아요/답글, 작성창 | 실제 댓글/권한/전송과 성공 후 초안 정리 |
|
|
164
166
|
|
|
@@ -474,9 +476,44 @@ HJM 1.31.0 채택 뒤에도 제품이 공통 계산을 다시 하던 세 지점
|
|
|
474
476
|
- `IconTile.icon`은 함수도 받는다. 함수는 `IconTileArtwork`(`{ edge }`: `fixed`는 64, `fill`은 측정한 칸 크기)를 받아
|
|
475
477
|
고정 크기 이미지가 스스로 크기를 맞춘다. 측정 전 추정이 필요한 host는 `resolveIconTileFillEdge(cellWidth)`로
|
|
476
478
|
같은 48~68 제한을 쓴다. 유틸버스 런처가 이 제한을 격자 폭에서 다시 계산해 아트워크 크기로 넘겼다.
|
|
479
|
+
- `IconTile` `locked`·`lockedLabel`·`accessory`·`accessoryPlacement`(2026-10-23): 눌러서 이유를 보는 잠금 상태와 틀 모서리
|
|
480
|
+
표식을 HJM이 소유한다. 잠금은 틀·제목만 `iconTileRecipe.locked.opacity`로 흐리고 표식은 그대로 두며, 표식은
|
|
481
|
+
`iconTileRecipe.accessory.offset` 4만큼 바깥으로 나간다(Asset·Avatar와 같은 뜻의 offset, 모서리 이름 `CornerAccessoryPlacement`).
|
|
482
|
+
유틸버스 ToolboxScreen이 바깥 View에 `activation.disabledOpacity`를 주고 배지를 `right: -4, top: -4`에 놓던 우회를 대신한다.
|
|
477
483
|
- `BottomNavigationInsetProvider`의 덮인 높이는 ScreenLayout만 적용한다(patch). 사용 지침이 `scroll="content"` 화면에
|
|
478
484
|
`useBottomNavigationInset()`을 다시 더하라고 안내해, 따르면 마지막 영역 여백과 목록 여백이 두 번 쌓였다. 코드는
|
|
479
485
|
이미 한 곳(마지막 영역)만 적용했으므로 지침을 고치고 두 renderer에 한 번만 적용되는지 검사를 더했다.
|
|
480
486
|
|
|
481
487
|
검증: Web `hjm-1320-screen-insets.browser.test.tsx`(Chromium), Native `hjm-1320-screen-insets.test.tsx`(host 렌더),
|
|
482
488
|
contracts `screen-patterns.test.ts`. 실기기·npm 게시·소비 앱 반영을 뜻하지 않는다.
|
|
489
|
+
|
|
490
|
+
## 2026-10-23 화면 바깥 host — Native `ScreenHost` (개발 소스·미게시)
|
|
491
|
+
|
|
492
|
+
선택의창 `app/_layout.tsx` AppCanvas, Unairplane `ui/index.tsx` AppScreenHost·`FlightLookupScreen`·`BoardingPassScannerScreen`이
|
|
493
|
+
같은 `SafeAreaView edges={["top","left","right"]}` + `colors.bg` 칠하기를 반복했다. ScreenLayout(Native)은 `safeAreaTop`·`safeAreaBottom`
|
|
494
|
+
숫자만 받고 좌우 inset이 없으며, 루트가 읽기 폭(720)에서 멈춰 그 밖과 노치 아래에 창 배경이 드러났기 때문이다.
|
|
495
|
+
여러 화면·navigator 전체를 감싸는 자리라 ScreenLayout prop이 아니라 바깥 host로 둔다.
|
|
496
|
+
|
|
497
|
+
- `ScreenHost`(`@hjmds/react-native/screens`, ScreenLayout companion): 창 전체를 차지하고 `surface="default"`이면 theme `bg`를 칠한다.
|
|
498
|
+
`edges`(기본 `screenHostDefaults.edges` = top·right·left)만큼 HjmNativeProvider `safeAreaInsets`를 padding으로 준다.
|
|
499
|
+
값과 검증은 contracts `resolveScreenHostInsets`(알 수 없는·중복 edge는 TypeError, 음수 inset은 RangeError)가 소유한다.
|
|
500
|
+
- 아래는 기본에서 뺀다. ScreenLayout `safeAreaBottom`이 마지막 영역에 max(16, inset)을 한 번 주므로 host가 아래를 들이면 16 + inset이 된다.
|
|
501
|
+
ScreenLayout·footer가 없는 내용을 감쌀 때만 `bottom`을 넣는다.
|
|
502
|
+
- Web은 만들지 않는다. HjmProvider `host="surface"`가 문서 바탕을 칠하고 각 고정 영역이 `env(safe-area-inset-*)`를 직접 쓰므로
|
|
503
|
+
같은 공백(창 배경 노출·좌우 inset 누락)이 없다.
|
|
504
|
+
|
|
505
|
+
## 2026-10-23 당겨서 새로고침 — Native ScreenLayout `onRefresh` (개발 소스·미게시)
|
|
506
|
+
|
|
507
|
+
spint 홈·알림·셀 세 화면이 `RefreshControl tintColor="transparent" colors={["transparent"]}`로 시스템 스피너를 숨기고
|
|
508
|
+
목록 머리에 FlowerSpinner를 직접 넣었다. ScreenLayout(Native)은 `scrollProps.refreshControl`을 그대로 넘길 뿐 새로고침 상태와
|
|
509
|
+
제품 표시 자리가 없었기 때문이다.
|
|
510
|
+
|
|
511
|
+
- ScreenLayout `onRefresh`·`refreshing`(기본 false)·`refreshLabel`: 화면이 당김 동작과 표시를 소유한다. `scroll="screen"`(그리고 상태 교체 중의
|
|
512
|
+
본문)은 ScreenLayout의 ScrollView에 붙인다. `scroll="content"` 목록은 그 목록 안에서 `ScreenRefreshControl`을 `refreshControl`로 넘기며,
|
|
513
|
+
상태·색·표시는 감싸는 ScreenLayout context에서 읽는다(밖에서 쓰면 TypeError).
|
|
514
|
+
- 표시: 기존 `loadingIndicator` 슬롯을 그대로 쓴다. 있으면 시스템 컨트롤은 제스처와 iOS 상단 공간을 위해 남기되 투명하게 그리고,
|
|
515
|
+
제품 표시를 본문 위쪽 `screenPatternRecipe.refresh.indicatorOffset`(16) 아래 가운데에 `refreshing` 동안 띄운다. 입력을 막지 않으며
|
|
516
|
+
보조기술에는 `refreshLabel` 이름의 busy progressbar로 알린다. 없으면 시스템 스피너가 `refresh.tint`(spinner brand)·`refresh.background`(surface)로 돈다.
|
|
517
|
+
- `pullToDismiss`와 함께 쓰지 않는다(같은 맨 위 아래 당김). `scrollProps.refreshControl`과도 같이 주지 않는다(소유자 하나).
|
|
518
|
+
- Web은 만들지 않는다. 브라우저 기본 당김(overscroll)이 있고 HJM Web에 당김 제스처 계약이 없어 같은 반복이 확인되지 않았다.
|
|
519
|
+
|
package/docs/sidebar.md
CHANGED
|
@@ -43,3 +43,18 @@
|
|
|
43
43
|
2026-10-07 소비 감사에서 Web 항목의 숫자 recipe 모서리가 프로필을 우회했다. 기존 `radius.md` 역할을 가까운 Provider의 CSS 변수로 읽고 변수 없는 독립 사용은 recipe 12를 유지한다. 레일 폭·링크·현재 위치·접기·초점과 Web 전용 범위는 바꾸지 않는다.
|
|
44
44
|
|
|
45
45
|
프로필 연결은 1.15.0 이후 미게시 변경이며, 기본 배포 계약과 실제 Native 기기 검증은 구분한다.
|
|
46
|
+
|
|
47
|
+
## 라우터 링크 어댑터와 폭 단계 (2026-10-23, 개발 소스·미게시)
|
|
48
|
+
|
|
49
|
+
**`renderLink`.** 항목은 순수 `<a href>`였고 `onNavigate`는 알림이라 기본 이동을 막지 못했다. 그래서 Next 같은
|
|
50
|
+
client router를 쓰는 제품은 항목마다 문서 전체를 다시 불러오게 되어 Sidebar를 채택하지 못하고 자체 nav를 유지했다
|
|
51
|
+
(다에리 웹 `WebShell` ProductNav). `BottomNavigation.renderLink`와 같은 계약으로 제품이 항목 링크를 그린다.
|
|
52
|
+
HJM은 `href`·`className`·`aria-current`·접힘 `aria-label`·`onClick`(비활성 차단·bounce·`onNavigate`)·내용을 준비하고
|
|
53
|
+
어댑터는 그 props를 자기 링크에 그대로 펼친다. 비활성 항목과 `destination` 없는 항목은 어댑터를 거치지 않는다 —
|
|
54
|
+
죽은 링크가 router로 넘어가 이동하는 일을 막는다. 단일 앵커 컴포넌트(`Link`·`ButtonLink`)의 `renderAnchor`가 아니라
|
|
55
|
+
탐색 묶음(BottomNavigation·Card·ListRow)의 `renderLink` 이름을 따른다.
|
|
56
|
+
|
|
57
|
+
**`size`.** 펼친 폭은 `regular` 260(기본)과 `compact` 220 두 단계다(`sidebarRecipe.sizes`). 그룹 제목과 긴 라벨이 있는
|
|
58
|
+
관리·문서 목록은 regular, 그룹 없이 짧은 라벨 3~5개를 두는 제품 셸 rail은 compact다. 다에리 rail이 220이었다.
|
|
59
|
+
임의 px를 받지 않는 이유는 폭이 라벨 줄바꿈·배지 위치와 함께 정해지는 recipe 값이기 때문이다. 접힌 폭 72는 두 단계 모두 같다.
|
|
60
|
+
`widths.expanded`(260)는 1.12부터 recipe를 읽던 소비자를 위해 regular 값으로 남긴다.
|