@hjmds/design-contracts 2.1.0 → 2.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/layout.d.ts +10 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +11 -1
- package/dist/layout.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/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/layout.md +3 -1
- package/docs/usage/components/layout.md +4 -3
- package/docs/usage/components/sidebar.md +2 -0
- package/package.json +1 -1
package/dist/layout.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export type LayoutDescriptor = Readonly<{
|
|
|
23
23
|
sidebar?: LayoutSidebarDescriptor;
|
|
24
24
|
/** Web-only: required whenever a header or sidebar precedes `main` (WCAG 2.4.1 bypass blocks). */
|
|
25
25
|
skipLinkLabel?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Web-only explicit opt-out: `false` renders no skip link and relies on the shell's landmarks (`nav`, `main`) for
|
|
28
|
+
* bypass (WCAG technique ARIA11). For products that removed the link by a recorded product decision (Diairy,
|
|
29
|
+
* 2026-10-02); the default still requires `skipLinkLabel`. Cannot be combined with `skipLinkLabel`.
|
|
30
|
+
*/
|
|
31
|
+
skipLink?: false;
|
|
26
32
|
}>;
|
|
27
33
|
/**
|
|
28
34
|
* Cross-platform structure validation. This deliberately does not require a
|
|
@@ -53,6 +59,10 @@ export declare const layoutRecipe: {
|
|
|
53
59
|
readonly maxWidth: 1200;
|
|
54
60
|
readonly paddingHorizontal: 20;
|
|
55
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated Layout no longer applies this width (2.2): the persistent column takes its content's width, and
|
|
64
|
+
* Sidebar owns its steps (`sidebarRecipe.sizes`). Kept for 2.x compatibility of the exported recipe shape.
|
|
65
|
+
*/
|
|
56
66
|
readonly sidebar: {
|
|
57
67
|
readonly width: 280;
|
|
58
68
|
};
|
package/dist/layout.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAMA;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,eAAe,CAAC;AAC/D,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,SAAS,CAAC;AAEzD,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,0GAA0G;IAC1G,IAAI,EAAE,iBAAiB,CAAC;IACxB,gIAAgI;IAChI,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAClC,kGAAkG;IAClG,aAAa,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAMA;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,eAAe,CAAC;AAC/D,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,SAAS,CAAC;AAEzD,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,0GAA0G;IAC1G,IAAI,EAAE,iBAAiB,CAAC;IACxB,gIAAgI;IAChI,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAClC,kGAAkG;IAClG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,CAAC,CAAC;AAQH;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAcxE;AAED,+EAA+E;AAC/E,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAc9E;AAED,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,YAAY,GACZ,eAAe,GACf,MAAM,GACN,aAAa,CAAC;AAElB;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,gBAAgB,GAC3B,SAAS,kBAAkB,EAAE,CAQ/B;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY;;;;;;;IAOvB;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyBJ,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;CAuBU,CAAC"}
|
package/dist/layout.js
CHANGED
|
@@ -29,7 +29,13 @@ export function validateLayoutRegions(descriptor) {
|
|
|
29
29
|
export function validateLayoutWebDescriptor(descriptor) {
|
|
30
30
|
validateLayoutRegions(descriptor);
|
|
31
31
|
const needsSkipLink = descriptor.hasHeader === true || descriptor.sidebar !== undefined;
|
|
32
|
-
if (
|
|
32
|
+
if (descriptor.skipLink === false && descriptor.skipLinkLabel !== undefined) {
|
|
33
|
+
throw new TypeError("Layout skipLink={false} cannot be combined with skipLinkLabel");
|
|
34
|
+
}
|
|
35
|
+
if (descriptor.skipLink !== undefined && descriptor.skipLink !== false) {
|
|
36
|
+
throw new TypeError("Layout skipLink only accepts false");
|
|
37
|
+
}
|
|
38
|
+
if (needsSkipLink && descriptor.skipLinkLabel === undefined && descriptor.skipLink !== false) {
|
|
33
39
|
throw new TypeError("Layout requires skipLinkLabel when a header or sidebar region precedes main");
|
|
34
40
|
}
|
|
35
41
|
}
|
|
@@ -65,6 +71,10 @@ export const layoutRecipe = {
|
|
|
65
71
|
maxWidth: layoutFoundation.contentMaxWidth,
|
|
66
72
|
paddingHorizontal: layoutFoundation.pagePadding.regular,
|
|
67
73
|
},
|
|
74
|
+
/**
|
|
75
|
+
* @deprecated Layout no longer applies this width (2.2): the persistent column takes its content's width, and
|
|
76
|
+
* Sidebar owns its steps (`sidebarRecipe.sizes`). Kept for 2.x compatibility of the exported recipe shape.
|
|
77
|
+
*/
|
|
68
78
|
sidebar: {
|
|
69
79
|
width: 280,
|
|
70
80
|
},
|
package/dist/layout.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ColorReference } from \"./color-references.js\";\nimport type { BehaviorContract } from \"./behaviors.js\";\nimport { focusIndicatorContract } from \"./component-contracts.js\";\nimport { layout as layoutFoundation, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * `Layout` (alias `AppShell`) does NOT own header or footer chrome — that is\n * already `TopBar` (native, beta) and `BottomNavigation` (adaptive, beta).\n * What is left after subtracting those: ordered regions, a sidebar that is\n * either always-visible chrome or a dismissible overlay, and Web-only\n * landmark composition (one `main` plus a required skip link whenever\n * repeated navigation precedes it). The overlay case reuses `SidePanel`\n * unchanged — see docs/layout.md for why this module does not redeclare\n * open/dismiss state.\n */\nexport type LayoutSidebarRole = \"navigation\" | \"complementary\";\nexport type LayoutSidebarMode = \"persistent\" | \"overlay\";\n\nexport type LayoutSidebarDescriptor = Readonly<{\n /** Primary site navigation vs. supplementary content (filters, related items) — independent of `mode`. */\n role: LayoutSidebarRole;\n /** `persistent`: always-visible landmark. `overlay`: a `SidePanel` — Layout does not re-contract its open/dismiss lifecycle. */\n mode: LayoutSidebarMode;\n label: string;\n}>;\n\nexport type LayoutDescriptor = Readonly<{\n hasHeader?: boolean;\n hasFooter?: boolean;\n sidebar?: LayoutSidebarDescriptor;\n /** Web-only: required whenever a header or sidebar precedes `main` (WCAG 2.4.1 bypass blocks). */\n skipLinkLabel?: string;\n}>;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`Layout ${field} must not be empty`);\n }\n}\n\n/**\n * Cross-platform structure validation. This deliberately does not require a\n * skip link: Native has no page-landmark or bypass-link equivalent.\n */\nexport function validateLayoutRegions(descriptor: LayoutDescriptor): void {\n if (descriptor.sidebar !== undefined) {\n const { role, mode, label } = descriptor.sidebar;\n if (role !== \"navigation\" && role !== \"complementary\") {\n throw new TypeError(`Unsupported Layout sidebar role: ${String(role)}`);\n }\n if (mode !== \"persistent\" && mode !== \"overlay\") {\n throw new TypeError(`Unsupported Layout sidebar mode: ${String(mode)}`);\n }\n assertNonEmpty(label, \"sidebar.label\");\n }\n if (descriptor.skipLinkLabel !== undefined) {\n assertNonEmpty(descriptor.skipLinkLabel, \"skipLinkLabel\");\n }\n}\n\n/** Web landmark validation, including the WCAG 2.4.1 bypass-link invariant. */\nexport function validateLayoutWebDescriptor(descriptor: LayoutDescriptor): void {\n validateLayoutRegions(descriptor);\n const needsSkipLink = descriptor.hasHeader === true || descriptor.sidebar !== undefined;\n if (needsSkipLink && descriptor.skipLinkLabel === undefined) {\n throw new TypeError(\n \"Layout requires skipLinkLabel when a header or sidebar region precedes main\",\n );\n }\n}\n\nexport type LayoutLandmarkRole =\n | \"banner\"\n | \"navigation\"\n | \"complementary\"\n | \"main\"\n | \"contentinfo\";\n\n/**\n * Web-only translation: real landmark elements exist there. Native has no\n * landmark-role equivalent at all (`accessibilityRole` covers headings and\n * controls, not page regions) — RN renderers instead rely on visual/DOM\n * order plus `accessibilityViewIsModal` for the overlay sidebar case. That\n * asymmetry is documented, not papered over with a fake mapping.\n */\nexport function resolveLayoutWebLandmarks(\n descriptor: LayoutDescriptor,\n): readonly LayoutLandmarkRole[] {\n validateLayoutWebDescriptor(descriptor);\n const roles: LayoutLandmarkRole[] = [];\n if (descriptor.hasHeader) roles.push(\"banner\");\n if (descriptor.sidebar) roles.push(descriptor.sidebar.role);\n roles.push(\"main\");\n if (descriptor.hasFooter) roles.push(\"contentinfo\");\n return roles;\n}\n\n/**\n * Skip link is hidden until it matters — visible only on keyboard focus.\n * A skip link visible at rest fights identity.md's \"조용한 화면 위에 중요한\n * 순간만 선명하게\": for a mouse/touch user it is noise every screen, for a\n * keyboard user it is the first landmark on the page.\n */\nexport const layoutRecipe = {\n slots: [\"root\", \"skipLink\", \"header\", \"sidebar\", \"main\", \"footer\"] as const,\n defaults: {},\n main: {\n maxWidth: layoutFoundation.contentMaxWidth,\n paddingHorizontal: layoutFoundation.pagePadding.regular,\n },\n sidebar: {\n width: 280,\n },\n skipLink: {\n visibility: \"focus-only\",\n background: semanticColors.action.brand.background,\n color: semanticColors.action.brand.content,\n paddingHorizontal: spacing.md,\n paddingVertical: spacing.sm,\n },\n states: { focus: focusIndicatorContract },\n} as const satisfies {\n slots: readonly [\"root\", \"skipLink\", \"header\", \"sidebar\", \"main\", \"footer\"];\n defaults: Record<string, never>;\n main: { maxWidth: number; paddingHorizontal: number };\n sidebar: { width: number };\n skipLink: {\n visibility: \"focus-only\";\n background: ColorReference;\n color: ColorReference;\n paddingHorizontal: number;\n paddingVertical: number;\n };\n states: { focus: typeof focusIndicatorContract };\n};\n\n/**\n * `controlled: []` on purpose: an overlay sidebar's open/dismiss lifecycle\n * is a `SidePanel` (`src/side-panel.ts`) composed as-is. Redeclaring\n * `open`/`defaultOpen`/`onOpenChange`/`dismissPolicy` here would be the same\n * mistake DataTable avoided with Pagination/LoadMore — two places owning\n * one piece of state that will drift the first time either one changes.\n */\nexport const layoutBehavior = {\n controlled: [],\n inputs: [\"hasHeader\", \"hasFooter\", \"sidebar\", \"skipLinkLabel\"],\n configuration: {\n \"sidebar.mode\": [\"persistent\", \"overlay\"],\n \"sidebar.role\": [\"navigation\", \"complementary\"],\n },\n stateAxes: {},\n web: {\n roles: [\"banner\", \"navigation\", \"complementary\", \"main\", \"contentinfo\"],\n keyboard: [\"Tab\"],\n focus: \"native\",\n },\n native: { roles: [], states: [], actions: [] },\n scenarios: [\n \"exactly-one-main-landmark-exists-per-layout\",\n \"web-skip-link-is-required-whenever-a-header-or-sidebar-precedes-main\",\n \"sidebar-role-navigation-or-complementary-is-chosen-independent-of-persistent-or-overlay-mode\",\n \"overlay-sidebar-reuses-sidepanel-open-state-and-dismiss-policy-unchanged\",\n \"header-and-footer-content-is-not-owned-here-compose-topbar-and-bottomnavigation\",\n \"skip-link-is-visually-hidden-until-keyboard-focus-reaches-it\",\n \"native-has-no-landmark-role-equivalent-translation-relies-on-order-and-accessibilityviewismodal\",\n ],\n} as const satisfies BehaviorContract;\n"]}
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ColorReference } from \"./color-references.js\";\nimport type { BehaviorContract } from \"./behaviors.js\";\nimport { focusIndicatorContract } from \"./component-contracts.js\";\nimport { layout as layoutFoundation, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\n/**\n * `Layout` (alias `AppShell`) does NOT own header or footer chrome — that is\n * already `TopBar` (native, beta) and `BottomNavigation` (adaptive, beta).\n * What is left after subtracting those: ordered regions, a sidebar that is\n * either always-visible chrome or a dismissible overlay, and Web-only\n * landmark composition (one `main` plus a required skip link whenever\n * repeated navigation precedes it). The overlay case reuses `SidePanel`\n * unchanged — see docs/layout.md for why this module does not redeclare\n * open/dismiss state.\n */\nexport type LayoutSidebarRole = \"navigation\" | \"complementary\";\nexport type LayoutSidebarMode = \"persistent\" | \"overlay\";\n\nexport type LayoutSidebarDescriptor = Readonly<{\n /** Primary site navigation vs. supplementary content (filters, related items) — independent of `mode`. */\n role: LayoutSidebarRole;\n /** `persistent`: always-visible landmark. `overlay`: a `SidePanel` — Layout does not re-contract its open/dismiss lifecycle. */\n mode: LayoutSidebarMode;\n label: string;\n}>;\n\nexport type LayoutDescriptor = Readonly<{\n hasHeader?: boolean;\n hasFooter?: boolean;\n sidebar?: LayoutSidebarDescriptor;\n /** Web-only: required whenever a header or sidebar precedes `main` (WCAG 2.4.1 bypass blocks). */\n skipLinkLabel?: string;\n /**\n * Web-only explicit opt-out: `false` renders no skip link and relies on the shell's landmarks (`nav`, `main`) for\n * bypass (WCAG technique ARIA11). For products that removed the link by a recorded product decision (Diairy,\n * 2026-10-02); the default still requires `skipLinkLabel`. Cannot be combined with `skipLinkLabel`.\n */\n skipLink?: false;\n}>;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`Layout ${field} must not be empty`);\n }\n}\n\n/**\n * Cross-platform structure validation. This deliberately does not require a\n * skip link: Native has no page-landmark or bypass-link equivalent.\n */\nexport function validateLayoutRegions(descriptor: LayoutDescriptor): void {\n if (descriptor.sidebar !== undefined) {\n const { role, mode, label } = descriptor.sidebar;\n if (role !== \"navigation\" && role !== \"complementary\") {\n throw new TypeError(`Unsupported Layout sidebar role: ${String(role)}`);\n }\n if (mode !== \"persistent\" && mode !== \"overlay\") {\n throw new TypeError(`Unsupported Layout sidebar mode: ${String(mode)}`);\n }\n assertNonEmpty(label, \"sidebar.label\");\n }\n if (descriptor.skipLinkLabel !== undefined) {\n assertNonEmpty(descriptor.skipLinkLabel, \"skipLinkLabel\");\n }\n}\n\n/** Web landmark validation, including the WCAG 2.4.1 bypass-link invariant. */\nexport function validateLayoutWebDescriptor(descriptor: LayoutDescriptor): void {\n validateLayoutRegions(descriptor);\n const needsSkipLink = descriptor.hasHeader === true || descriptor.sidebar !== undefined;\n if (descriptor.skipLink === false && descriptor.skipLinkLabel !== undefined) {\n throw new TypeError(\"Layout skipLink={false} cannot be combined with skipLinkLabel\");\n }\n if (descriptor.skipLink !== undefined && descriptor.skipLink !== false) {\n throw new TypeError(\"Layout skipLink only accepts false\");\n }\n if (needsSkipLink && descriptor.skipLinkLabel === undefined && descriptor.skipLink !== false) {\n throw new TypeError(\n \"Layout requires skipLinkLabel when a header or sidebar region precedes main\",\n );\n }\n}\n\nexport type LayoutLandmarkRole =\n | \"banner\"\n | \"navigation\"\n | \"complementary\"\n | \"main\"\n | \"contentinfo\";\n\n/**\n * Web-only translation: real landmark elements exist there. Native has no\n * landmark-role equivalent at all (`accessibilityRole` covers headings and\n * controls, not page regions) — RN renderers instead rely on visual/DOM\n * order plus `accessibilityViewIsModal` for the overlay sidebar case. That\n * asymmetry is documented, not papered over with a fake mapping.\n */\nexport function resolveLayoutWebLandmarks(\n descriptor: LayoutDescriptor,\n): readonly LayoutLandmarkRole[] {\n validateLayoutWebDescriptor(descriptor);\n const roles: LayoutLandmarkRole[] = [];\n if (descriptor.hasHeader) roles.push(\"banner\");\n if (descriptor.sidebar) roles.push(descriptor.sidebar.role);\n roles.push(\"main\");\n if (descriptor.hasFooter) roles.push(\"contentinfo\");\n return roles;\n}\n\n/**\n * Skip link is hidden until it matters — visible only on keyboard focus.\n * A skip link visible at rest fights identity.md's \"조용한 화면 위에 중요한\n * 순간만 선명하게\": for a mouse/touch user it is noise every screen, for a\n * keyboard user it is the first landmark on the page.\n */\nexport const layoutRecipe = {\n slots: [\"root\", \"skipLink\", \"header\", \"sidebar\", \"main\", \"footer\"] as const,\n defaults: {},\n main: {\n maxWidth: layoutFoundation.contentMaxWidth,\n paddingHorizontal: layoutFoundation.pagePadding.regular,\n },\n /**\n * @deprecated Layout no longer applies this width (2.2): the persistent column takes its content's width, and\n * Sidebar owns its steps (`sidebarRecipe.sizes`). Kept for 2.x compatibility of the exported recipe shape.\n */\n sidebar: {\n width: 280,\n },\n skipLink: {\n visibility: \"focus-only\",\n background: semanticColors.action.brand.background,\n color: semanticColors.action.brand.content,\n paddingHorizontal: spacing.md,\n paddingVertical: spacing.sm,\n },\n states: { focus: focusIndicatorContract },\n} as const satisfies {\n slots: readonly [\"root\", \"skipLink\", \"header\", \"sidebar\", \"main\", \"footer\"];\n defaults: Record<string, never>;\n main: { maxWidth: number; paddingHorizontal: number };\n sidebar: { width: number };\n skipLink: {\n visibility: \"focus-only\";\n background: ColorReference;\n color: ColorReference;\n paddingHorizontal: number;\n paddingVertical: number;\n };\n states: { focus: typeof focusIndicatorContract };\n};\n\n/**\n * `controlled: []` on purpose: an overlay sidebar's open/dismiss lifecycle\n * is a `SidePanel` (`src/side-panel.ts`) composed as-is. Redeclaring\n * `open`/`defaultOpen`/`onOpenChange`/`dismissPolicy` here would be the same\n * mistake DataTable avoided with Pagination/LoadMore — two places owning\n * one piece of state that will drift the first time either one changes.\n */\nexport const layoutBehavior = {\n controlled: [],\n inputs: [\"hasHeader\", \"hasFooter\", \"sidebar\", \"skipLinkLabel\"],\n configuration: {\n \"sidebar.mode\": [\"persistent\", \"overlay\"],\n \"sidebar.role\": [\"navigation\", \"complementary\"],\n },\n stateAxes: {},\n web: {\n roles: [\"banner\", \"navigation\", \"complementary\", \"main\", \"contentinfo\"],\n keyboard: [\"Tab\"],\n focus: \"native\",\n },\n native: { roles: [], states: [], actions: [] },\n scenarios: [\n \"exactly-one-main-landmark-exists-per-layout\",\n \"web-skip-link-is-required-whenever-a-header-or-sidebar-precedes-main\",\n \"sidebar-role-navigation-or-complementary-is-chosen-independent-of-persistent-or-overlay-mode\",\n \"overlay-sidebar-reuses-sidepanel-open-state-and-dismiss-policy-unchanged\",\n \"header-and-footer-content-is-not-owned-here-compose-topbar-and-bottomnavigation\",\n \"skip-link-is-visually-hidden-until-keyboard-focus-reaches-it\",\n \"native-has-no-landmark-role-equivalent-translation-relies-on-order-and-accessibilityviewismodal\",\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,OAAgB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"2.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAgB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"2.2.0\" as const;\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Component maturity and evidence manifest
|
|
2
2
|
|
|
3
|
-
> Generated from `@hjmds/design-contracts` 2.
|
|
3
|
+
> Generated from `@hjmds/design-contracts` 2.2.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": "2.
|
|
3
|
+
"designSystemVersion": "2.2.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": "2.
|
|
8
|
+
"packageVersion": "2.2.0",
|
|
9
9
|
"activeComponents": 132,
|
|
10
10
|
"claimedComponents": 132,
|
|
11
11
|
"completeScenarioComponents": 131,
|
|
@@ -7114,7 +7114,7 @@
|
|
|
7114
7114
|
},
|
|
7115
7115
|
"native": {
|
|
7116
7116
|
"packageName": "@hjmds/react-native",
|
|
7117
|
-
"packageVersion": "2.
|
|
7117
|
+
"packageVersion": "2.2.0",
|
|
7118
7118
|
"activeComponents": 115,
|
|
7119
7119
|
"claimedComponents": 115,
|
|
7120
7120
|
"completeScenarioComponents": 112,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Renderer evidence coverage
|
|
2
2
|
|
|
3
|
-
> Generated for HJM 2.
|
|
3
|
+
> Generated for HJM 2.2.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
|
|
package/docs/layout.md
CHANGED
|
@@ -20,7 +20,9 @@ stable)이다. `Layout`이 그 콘텐츠나 상태를 다시 계약하면 두
|
|
|
20
20
|
랜드마크는 **정확히 하나**여야 하고, 헤더나 사이드바처럼 반복되는 내비게이션이
|
|
21
21
|
`main` 앞에 있으면 WCAG 2.4.1(Bypass Blocks)에 따라 skip link가 있어야 한다.
|
|
22
22
|
Web 전용 `validateLayoutWebDescriptor`가 이 규칙을 강제한다 — `hasHeader`나
|
|
23
|
-
`sidebar`가 있는데 `skipLinkLabel`이 없으면 던진다.
|
|
23
|
+
`sidebar`가 있는데 `skipLinkLabel`이 없으면 던진다. 예외는 명시적 `skipLink: false`뿐이다 — skip link 없이
|
|
24
|
+
랜드마크(`nav`·`main`)로 건너뛰기를 제공한다(WCAG 기법 ARIA11). 제품이 기록된 결정으로 링크를 없앤 경우를 위한
|
|
25
|
+
것이다(다에리 2026-10-02 사용자 결정, 2026-10-30 HJM Layout 채택 때 추가). `skipLinkLabel`과 함께 쓰면 던진다. 공통
|
|
24
26
|
`validateLayoutRegions`는 region/sidebar 구조만 검사하므로, bypass-link 개념이
|
|
25
27
|
없는 Native에 Web 요구를 강제하지 않는다. 기존 `validateLayoutDescriptor` 호환 alias는 2.0에서 제거했다.
|
|
26
28
|
- **플랫폼 번역**: 성립하지만 비대칭적으로 성립한다. Web은 실제 랜드마크
|
|
@@ -65,18 +65,19 @@ import { Layout } from "@hjmds/react-native/primitives";
|
|
|
65
65
|
| `sidebar.renderOverlay` | Web `(sidebarLandmark: ReactElement) => ReactNode` · Native `(sidebar: ReactNode) => ReactNode` | — | `mode: "overlay"`에서만, 그리고 필수다. 받은 landmark(사이드바 영역)를 SidePanel 등 안에 넣어 돌려준다. 열림·닫힘은 그 SidePanel이 소유한다 |
|
|
66
66
|
| `sidebar` 모양 | `{ role, mode, label, children, renderOverlay? }` + Web `landmarkProps`·`landmarkRef`, Native `containerProps` | — | `label`은 현지화 문자열 |
|
|
67
67
|
| `skipLinkLabel`(Web) | 현지화 문자열 | — | Web skip link는 키보드 포커스가 닿을 때만 보인다 |
|
|
68
|
+
| `skipLink`(Web) | `false` | — | skip link를 만들지 않고 랜드마크(`nav`·`main`)로 건너뛰기를 제공한다(WCAG ARIA11). 제품이 기록된 결정으로 링크를 없앤 경우에만 쓴다(다에리 2026-10-02 결정, 2026-10-30 추가). `skipLinkLabel`과 함께 쓰면 `TypeError` |
|
|
68
69
|
| `mainId`(Web) | 공백 없는 문자열 | 자동 생성 | 비거나 공백이 있으면 `TypeError` |
|
|
69
70
|
| `layoutStyle`(Web) | 배치 key만 | — | 루트 배치. Native는 `layoutStyle`이 없고 `style`(layout primitive, 1.13 deprecated 제외 대상)만 받는다 |
|
|
70
71
|
|
|
71
72
|
- 이벤트 콜백은 없다. 사이드바 열림 상태는 `renderOverlay` 안의 SidePanel `open`·`onOpenChange`가 소유한다.
|
|
72
73
|
|
|
73
|
-
- Web persistent 사이드바 폭은
|
|
74
|
+
- Web persistent 사이드바 폭은 안에 넣은 Sidebar의 폭 단계를 따른다. main 최대 폭·좌우 여백은 foundations의 `contentMaxWidth`·`pagePadding.regular`.
|
|
74
75
|
|
|
75
76
|
## 배치
|
|
76
77
|
|
|
77
78
|
| 항목 | 값 | 근거 |
|
|
78
79
|
| --- | --- | --- |
|
|
79
|
-
| 크기 | Web: persistent 사이드바
|
|
80
|
+
| 크기 | Web: persistent 사이드바 칸은 내용 폭을 따른다 — [Sidebar](sidebar.md)의 폭 단계(260·220·접힘 72)가 정한다(2026-10-30, 예전 280 고정은 Sidebar 옆에 빈 띠를 남겼다. `layoutRecipe.sidebar.width`는 2.x 호환으로만 남는다). main 최대 폭 1200(`layout.contentMaxWidth`). Native: 루트·main은 `flex: 1`이고 여백·폭 제한은 없다 | `design-contracts/src/layout.ts`(`layoutRecipe`), `react/src/layout.tsx`, `react-native/src/primitives.tsx`(Layout) |
|
|
80
81
|
| 간격 | Web: main 좌우 여백 `spacing.lg` 20(`layout.pagePadding.regular`). skip link는 화면 왼쪽 위에서 `spacing.sm` 12 떨어진다 | `design-contracts/src/foundations.ts`(`layout`), `react/src/styles.css`(`.hjm-layout__skip-link`) |
|
|
81
82
|
| 순서·정렬 | Web: 루트는 grid다. header·footer는 전체 폭, persistent 사이드바는 왼쪽(RTL에서는 오른쪽) 칸, main은 나머지 칸에서 가운데 정렬된다. Native: header → (persistent 사이드바) → main → footer 순서로 세로로 쌓인다 | `react/src/styles.css`(`.hjm-layout`), `react-native/src/primitives.tsx`(Layout) |
|
|
82
83
|
| 고정·스크롤 | 스크롤은 main 안의 내용이 소유한다. header·footer는 main 바깥이라 스크롤되지 않는다. Web skip link는 화면에 고정되고 키보드 포커스 때만 내려온다. overlay 사이드바는 그리드 칸을 차지하지 않고 `renderOverlay`의 SidePanel이 위를 덮는다 | `react/src/styles.css`(`.hjm-layout`), `react/src/layout.tsx` |
|
|
@@ -97,7 +98,7 @@ Web (persistent sidebar) Native
|
|
|
97
98
|
|
|
98
99
|
## 꼭 지킬 것
|
|
99
100
|
|
|
100
|
-
- Web에서 `header`나 `sidebar`가 있으면 `skipLinkLabel`(현지화)이 필수다. 없으면 `TypeError`.
|
|
101
|
+
- Web에서 `header`나 `sidebar`가 있으면 `skipLinkLabel`(현지화)이 필수다. 없으면 `TypeError`. 명시적 `skipLink={false}`만 예외다.
|
|
101
102
|
- 화면마다 Layout을 다시 세우지 않는다. `main` landmark는 하나여야 한다.
|
|
102
103
|
- 어느 화면 폭에서 persistent↔overlay를 바꿀지는 제품이 정한다. Layout은 전환 규칙을 갖지 않는다.
|
|
103
104
|
- 사이드바 내용·헤더 내용·푸터 내용은 각 컴포넌트가 소유한다. Layout에 상태를 다시 만들지 않는다.
|
|
@@ -94,6 +94,8 @@ Native: 없음. 폰은 BottomNavigation, 태블릿 split view는 navigator가
|
|
|
94
94
|
| `renderLink` | `(props: SidebarLinkRenderProps) => ReactElement` | 일반 `<a>` | client router 어댑터. props(`href`·`className`·`aria-current`·접힘 `aria-label`·`onClick`·`children` 등)를 모두 자기 링크에 펼친다. 비활성·`destination` 없는 항목은 어댑터를 거치지 않는다. `BottomNavigation.renderLink`와 같은 계약. 개발 소스·미게시 |
|
|
95
95
|
| `renderIcon` | `(item) => ReactNode` | — | 접힌 레일에 필수 |
|
|
96
96
|
| `renderBadge` | `(count: number, item) => ReactNode` | 숫자 배지 | — |
|
|
97
|
+
| `header` | `ReactNode` | — | 그룹 위 제품 칸(브랜드 표식·주 행동). 2026-10-30 다에리·번뚝 데스크탑 셸이 HJM 밖에 따로 그리던 자리 |
|
|
98
|
+
| `footer` | `ReactNode` | — | 맨 아래에 붙는 제품 칸(설정·언어). 그룹이 짧아도 바닥에 선다(위 경계선 1px) |
|
|
97
99
|
| `className` · `layoutStyle` | 문자열 · 배치 전용 style 객체 | — | 루트 배치만. 폭·색은 recipe 소유 |
|
|
98
100
|
|
|
99
101
|
## 배치
|