@hjmds/design-contracts 1.39.0 → 1.41.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.
Files changed (87) hide show
  1. package/dist/agreement.d.ts +2 -2
  2. package/dist/base-recipes.d.ts +11 -3
  3. package/dist/base-recipes.d.ts.map +1 -1
  4. package/dist/base-recipes.js +9 -4
  5. package/dist/base-recipes.js.map +1 -1
  6. package/dist/behaviors.d.ts +4 -4
  7. package/dist/card.d.ts +1 -1
  8. package/dist/card.js +1 -1
  9. package/dist/card.js.map +1 -1
  10. package/dist/catalog.d.ts +119 -85
  11. package/dist/catalog.d.ts.map +1 -1
  12. package/dist/choice-row.d.ts +26 -0
  13. package/dist/choice-row.d.ts.map +1 -0
  14. package/dist/choice-row.js +24 -0
  15. package/dist/choice-row.js.map +1 -0
  16. package/dist/colors.d.ts +2 -0
  17. package/dist/colors.d.ts.map +1 -1
  18. package/dist/colors.js +3 -1
  19. package/dist/colors.js.map +1 -1
  20. package/dist/command-palette.d.ts +2 -2
  21. package/dist/component-contracts.d.ts +2 -2
  22. package/dist/component-contracts.js +2 -2
  23. package/dist/component-contracts.js.map +1 -1
  24. package/dist/component-recipes.d.ts +76 -64
  25. package/dist/component-recipes.d.ts.map +1 -1
  26. package/dist/component-recipes.js +18 -16
  27. package/dist/component-recipes.js.map +1 -1
  28. package/dist/data-table.d.ts +1 -1
  29. package/dist/floating-action-button.d.ts +14 -2
  30. package/dist/floating-action-button.d.ts.map +1 -1
  31. package/dist/floating-action-button.js +5 -3
  32. package/dist/floating-action-button.js.map +1 -1
  33. package/dist/icon-button-recipe.d.ts +2 -2
  34. package/dist/icon-button-recipe.js +3 -3
  35. package/dist/icon-button-recipe.js.map +1 -1
  36. package/dist/menubar.d.ts +2 -2
  37. package/dist/palette-contrast.d.ts.map +1 -1
  38. package/dist/palette-contrast.js +2 -0
  39. package/dist/palette-contrast.js.map +1 -1
  40. package/dist/semantic-colors.d.ts +18 -1
  41. package/dist/semantic-colors.d.ts.map +1 -1
  42. package/dist/semantic-colors.js +7 -1
  43. package/dist/semantic-colors.js.map +1 -1
  44. package/dist/sidebar.d.ts +2 -2
  45. package/dist/time-picker.d.ts +16 -4
  46. package/dist/time-picker.d.ts.map +1 -1
  47. package/dist/time-picker.js +26 -5
  48. package/dist/time-picker.js.map +1 -1
  49. package/dist/toggle-group.d.ts +3 -3
  50. package/dist/toggle-group.js +3 -3
  51. package/dist/toggle-group.js.map +1 -1
  52. package/dist/transfer-list.d.ts +2 -2
  53. package/dist/tree.d.ts +2 -2
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/dist/version.js.map +1 -1
  57. package/docs/ant-design-coverage.md +6 -1
  58. package/docs/brand-boundary.md +1 -0
  59. package/docs/floating-action-button.md +6 -4
  60. package/docs/generated/component-maturity.md +1 -1
  61. package/docs/generated/renderer-evidence.json +3 -3
  62. package/docs/generated/renderer-evidence.md +1 -1
  63. package/docs/generated/showcase-manifest.json +1 -1
  64. package/docs/optional-adapters.md +1 -1
  65. package/docs/theme-palette.md +60 -0
  66. package/docs/time-picker.md +21 -0
  67. package/docs/toast.md +11 -0
  68. package/docs/usage/FINDING.md +71 -0
  69. package/docs/usage/README.md +5 -3
  70. package/docs/usage/SELECTION.md +8 -2
  71. package/docs/usage/components/bottom-navigation.md +5 -0
  72. package/docs/usage/components/card.md +2 -2
  73. package/docs/usage/components/chart.md +3 -1
  74. package/docs/usage/components/chip.md +1 -1
  75. package/docs/usage/components/choice-row.md +77 -0
  76. package/docs/usage/components/floating-action-button.md +5 -4
  77. package/docs/usage/components/image.md +18 -3
  78. package/docs/usage/components/keyboard-avoidance-context.md +3 -3
  79. package/docs/usage/components/segmented-control.md +1 -1
  80. package/docs/usage/components/sheet.md +1 -0
  81. package/docs/usage/components/surface.md +7 -1
  82. package/docs/usage/components/tabs.md +5 -0
  83. package/docs/usage/components/time-picker.md +20 -2
  84. package/docs/usage/components/toggle-group.md +5 -0
  85. package/docs/usage/compositions/save-feedback.md +43 -21
  86. package/docs/usage/tokens/color.md +17 -3
  87. package/package.json +7 -1
@@ -1 +1 @@
1
- {"version":3,"file":"time-picker.js","sourceRoot":"","sources":["../src/time-picker.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport { control, spacing, typography } from \"./foundations.js\";\n\nexport type TimePickerPart = \"hour\" | \"minute\";\nexport type TimePickerDraft = Readonly<{ hour: string | null; minute: string | null }>;\nexport type TimePickerLabels = Readonly<{\n label: string; hour: string; minute: string; placeholder: string; confirm: string; cancel: string;\n}>;\nexport type TimePickerOptions = Readonly<{\n value: string | null;\n onValueChange(value: string | null): void;\n labels: TimePickerLabels;\n /** Inline changes update the parent's draft; its enclosing form owns confirmation. */\n presentation?: \"field\" | \"inline\";\n minuteStep?: number;\n isTimeUnavailable?: (value: string) => boolean;\n disabled?: boolean;\n readOnly?: boolean;\n description?: string;\n error?: string;\n}>;\n\n// A single two-column session replaces independently closing Select overlays (2026-10-19).\n// Keep the five 44px selection rows, but use a compact readout: the 40px summary exceeded the shared\n// 24px cap and dominated the mobile departure Sheet (2026-10-20 review; APP_VISUAL_QUALITY_STANDARD §4).\nexport const timePickerRecipe = {\n rowHeight: control.minTouchTarget, visibleRows: 5, gap: spacing.sm, sectionGap: spacing.md,\n radius: \"md\", readoutSize: typography.titleLarge.fontSize, readoutLineHeight: typography.titleLarge.lineHeight, panelWidth: 304,\n defaults: { presentation: \"field\", minuteStep: 1 },\n} as const;\n\nexport function validateTimePicker(options: TimePickerOptions): void {\n if (options.value !== null && !/^([01]\\d|2[0-3]):[0-5]\\d$/.test(options.value)) throw new TypeError(\"TimePicker value must be HH:mm or null\");\n const step = options.minuteStep ?? 1;\n if (!Number.isInteger(step) || step < 1 || step > 60 || 60 % step !== 0) throw new RangeError(\"TimePicker minuteStep must divide 60\");\n for (const label of Object.values(options.labels)) if (!label.trim()) throw new TypeError(\"TimePicker labels must not be empty\");\n if (options.presentation !== undefined && options.presentation !== \"field\" && options.presentation !== \"inline\") throw new TypeError(\"Invalid TimePicker presentation\");\n}\nexport function timePickerDraft(value: string | null): TimePickerDraft {\n return { hour: value?.slice(0, 2) ?? null, minute: value?.slice(3, 5) ?? null };\n}\nexport function resolveTimePickerValue(draft: TimePickerDraft, options: Pick<TimePickerOptions, \"minuteStep\" | \"isTimeUnavailable\">): string | null {\n if (draft.hour === null || draft.minute === null) return null;\n const value = `${draft.hour}:${draft.minute}`;\n if (!/^([01]\\d|2[0-3]):[0-5]\\d$/.test(value) || Number(draft.minute) % (options.minuteStep ?? 1) !== 0 || options.isTimeUnavailable?.(value)) return null;\n return value;\n}\nexport function timePickerItems(part: TimePickerPart, draft: TimePickerDraft, options: Pick<TimePickerOptions, \"minuteStep\" | \"isTimeUnavailable\">) {\n const step = options.minuteStep ?? 1;\n return Array.from({ length: part === \"hour\" ? 24 : 60 / step }, (_, index) => {\n const value = String(part === \"hour\" ? index : index * step).padStart(2, \"0\");\n // An hour remains available if any of its minute slots is valid. Never silently replace the minute.\n const disabled = part === \"hour\"\n ? !Array.from({ length: 60 / step }, (_, i) => `${value}:${String(i * step).padStart(2, \"0\")}`).some(time => !options.isTimeUnavailable?.(time))\n : draft.hour !== null && !!options.isTimeUnavailable?.(`${draft.hour}:${value}`);\n return { value, disabled };\n });\n}\n\nexport const timePickerBehavior = {\n controlled: [\"value\", \"onValueChange\"], inputs: [\"labels\", \"minuteStep\", \"isTimeUnavailable\", \"presentation\"],\n stateAxes: { availability: [\"enabled\", \"disabled\", \"readOnly\"], value: [\"empty\", \"partial\", \"selected\"], interaction: [\"closed\", \"editing\"] },\n web: { roles: [\"button\", \"dialog\", \"listbox\", \"option\"], keyboard: [\"Tab\", \"Enter\", \"Space\", \"Escape\", \"ArrowUp\", \"ArrowDown\", \"Home\", \"End\"], focus: \"roving\", dismiss: [\"action\", \"escape\", \"outside\", \"blur\"] },\n native: { roles: [\"button\", \"dialog\", \"radio\"], states: [\"disabled\", \"expanded\", \"checked\"], actions: [\"activate\", \"dismiss\"], dismiss: [\"action\", \"back\", \"outside\"] },\n scenarios: [\"two-columns-share-one-draft-and-overlay\", \"column-selection-never-dismisses\", \"confirm-commits-a-complete-available-HH:mm\", \"cancel-discards-edits\", \"external-value-reconciles-draft\", \"inline-parent-owns-confirmation\", \"unavailable-options-never-activate\", \"midnight-is-a-valid-value\"],\n} as const satisfies BehaviorContract;\n"]}
1
+ {"version":3,"file":"time-picker.js","sourceRoot":"","sources":["../src/time-picker.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport { control, spacing, stroke, typography } from \"./foundations.js\";\n\nexport type TimePickerPart = \"hour\" | \"minute\";\nexport type TimePickerDraft = Readonly<{ hour: string | null; minute: string | null }>;\nexport type TimePickerLabels = Readonly<{\n label: string; hour: string; minute: string; placeholder: string; confirm: string; cancel: string;\n}>;\nexport type TimePickerOptions = Readonly<{\n value: string | null;\n onValueChange(value: string | null): void;\n labels: TimePickerLabels;\n /** Inline changes update the parent's draft; its enclosing form owns confirmation. */\n presentation?: \"field\" | \"inline\";\n /** Keep tap-list behavior by default; wheel selection shares the same draft and confirmation. */\n selectionStyle?: \"list\" | \"wheel\";\n minuteStep?: number;\n isTimeUnavailable?: (value: string) => boolean;\n disabled?: boolean;\n readOnly?: boolean;\n description?: string;\n error?: string;\n}>;\n\n// A single two-column session replaces independently closing Select overlays (2026-10-19).\n// Keep the five 44px selection rows, but use a compact readout: the 40px summary exceeded the shared\n// 24px cap and dominated the mobile departure Sheet (2026-10-20 review; APP_VISUAL_QUALITY_STANDARD §4).\nexport const timePickerRecipe = {\n rowHeight: control.minTouchTarget, visibleRows: 5, gap: spacing.sm, sectionGap: spacing.md,\n radius: \"md\", readoutSize: typography.titleLarge.fontSize, readoutLineHeight: typography.titleLarge.lineHeight, panelWidth: 304,\n selectionStroke: stroke.default,\n defaults: { presentation: \"field\", selectionStyle: \"list\", minuteStep: 1 },\n} as const;\n\nexport function validateTimePicker(options: TimePickerOptions): void {\n if (options.value !== null && !/^([01]\\d|2[0-3]):[0-5]\\d$/.test(options.value)) throw new TypeError(\"TimePicker value must be HH:mm or null\");\n const step = options.minuteStep ?? 1;\n if (!Number.isInteger(step) || step < 1 || step > 60 || 60 % step !== 0) throw new RangeError(\"TimePicker minuteStep must divide 60\");\n for (const label of Object.values(options.labels)) if (!label.trim()) throw new TypeError(\"TimePicker labels must not be empty\");\n if (options.presentation !== undefined && options.presentation !== \"field\" && options.presentation !== \"inline\") throw new TypeError(\"Invalid TimePicker presentation\");\n if (options.selectionStyle !== undefined && options.selectionStyle !== \"list\" && options.selectionStyle !== \"wheel\") throw new TypeError(\"Invalid TimePicker selectionStyle\");\n}\n\n/** Offset 0 centers the first row because both renderers add half a viewport of end padding. */\nexport function resolveTimePickerWheelIndex(items: readonly Readonly<{ disabled: boolean }>[], offset: number): number | null {\n const position = Math.max(0, offset) / timePickerRecipe.rowHeight;\n let nearest: number | null = null;\n for (let index = 0; index < items.length; index++) {\n if (!items[index]!.disabled && (nearest === null || Math.abs(index - position) < Math.abs(nearest - position))) nearest = index;\n }\n return nearest;\n}\n\n/** Keyboard and native adjustable actions use the same available slots, without wrapping. */\nexport function moveTimePickerWheelIndex(items: readonly Readonly<{ disabled: boolean }>[], from: number, direction: 1 | -1): number | null {\n for (let index = from < 0 ? (direction === 1 ? 0 : items.length - 1) : from + direction; index >= 0 && index < items.length; index += direction) {\n if (!items[index]!.disabled) return index;\n }\n return null;\n}\nexport function timePickerDraft(value: string | null): TimePickerDraft {\n return { hour: value?.slice(0, 2) ?? null, minute: value?.slice(3, 5) ?? null };\n}\nexport function resolveTimePickerValue(draft: TimePickerDraft, options: Pick<TimePickerOptions, \"minuteStep\" | \"isTimeUnavailable\">): string | null {\n if (draft.hour === null || draft.minute === null) return null;\n const value = `${draft.hour}:${draft.minute}`;\n if (!/^([01]\\d|2[0-3]):[0-5]\\d$/.test(value) || Number(draft.minute) % (options.minuteStep ?? 1) !== 0 || options.isTimeUnavailable?.(value)) return null;\n return value;\n}\nexport function timePickerItems(part: TimePickerPart, draft: TimePickerDraft, options: Pick<TimePickerOptions, \"minuteStep\" | \"isTimeUnavailable\">) {\n const step = options.minuteStep ?? 1;\n return Array.from({ length: part === \"hour\" ? 24 : 60 / step }, (_, index) => {\n const value = String(part === \"hour\" ? index : index * step).padStart(2, \"0\");\n // An hour remains available if any of its minute slots is valid. Never silently replace the minute.\n const disabled = part === \"hour\"\n ? !Array.from({ length: 60 / step }, (_, i) => `${value}:${String(i * step).padStart(2, \"0\")}`).some(time => !options.isTimeUnavailable?.(time))\n : draft.hour !== null && !!options.isTimeUnavailable?.(`${draft.hour}:${value}`);\n return { value, disabled };\n });\n}\n\nexport const timePickerBehavior = {\n controlled: [\"value\", \"onValueChange\"], inputs: [\"labels\", \"minuteStep\", \"isTimeUnavailable\", \"presentation\", \"selectionStyle\"],\n stateAxes: { availability: [\"enabled\", \"disabled\", \"readOnly\"], value: [\"empty\", \"partial\", \"selected\"], interaction: [\"closed\", \"editing\"] },\n web: { roles: [\"button\", \"dialog\", \"listbox\", \"option\"], keyboard: [\"Tab\", \"Enter\", \"Space\", \"Escape\", \"ArrowUp\", \"ArrowDown\", \"Home\", \"End\"], focus: \"roving\", dismiss: [\"action\", \"escape\", \"outside\", \"blur\"] },\n native: { roles: [\"button\", \"dialog\", \"radio\", \"adjustable\"], states: [\"disabled\", \"expanded\", \"checked\"], actions: [\"activate\", \"dismiss\", \"increment\", \"decrement\"], dismiss: [\"action\", \"back\", \"outside\"] },\n scenarios: [\"two-columns-share-one-draft-and-overlay\", \"column-selection-never-dismisses\", \"confirm-commits-a-complete-available-HH:mm\", \"cancel-discards-edits\", \"external-value-reconciles-draft\", \"inline-parent-owns-confirmation\", \"unavailable-options-never-activate\", \"midnight-is-a-valid-value\", \"wheel-centers-available-slots-without-initial-commit\", \"wheel-adjustment-preserves-confirm-and-cancel\"],\n} as const satisfies BehaviorContract;\n"]}
@@ -62,17 +62,17 @@ export declare const toggleGroupRecipe: {
62
62
  readonly pressed: {
63
63
  readonly background: Readonly<{
64
64
  source: "theme";
65
- key: "bg";
65
+ key: "surfaceSelected";
66
66
  alpha?: number;
67
67
  }>;
68
68
  readonly color: Readonly<{
69
69
  source: "theme";
70
- key: "contentBrand";
70
+ key: "text";
71
71
  alpha?: number;
72
72
  }>;
73
73
  readonly border: Readonly<{
74
74
  source: "theme";
75
- key: "contentBrand";
75
+ key: "borderControl";
76
76
  alpha?: number;
77
77
  }>;
78
78
  };
@@ -46,9 +46,9 @@ export const toggleGroupRecipe = {
46
46
  border: semanticColors.border.control,
47
47
  },
48
48
  pressed: {
49
- background: semanticColors.canvas,
50
- color: semanticColors.content.brand,
51
- border: semanticColors.border.focus,
49
+ background: semanticColors.selection.background,
50
+ color: semanticColors.selection.content,
51
+ border: semanticColors.selection.border,
52
52
  },
53
53
  states: { focus: focusIndicatorContract },
54
54
  };
@@ -1 +1 @@
1
- {"version":3,"file":"toggle-group.js","sourceRoot":"","sources":["../src/toggle-group.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { focusIndicatorContract } from \"./component-contracts.js\";\nimport { control, radius, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\nimport {\n reconcileCheckboxSelection,\n toggleCheckboxSelection,\n type CheckboxSelectionItem,\n} from \"./selection-helpers.js\";\n\n/**\n * 여러 개를 동시에 켜고 끄는 버튼 묶음이다 — 굵게/기울임/밑줄, 필터 칩 여러 개처럼.\n *\n * `SegmentedControl`과 경계가 분명하다. Segmented는 **하나를 고른다**(어떤 화면을 볼지),\n * ToggleGroup은 **여러 개를 켠다**(무엇을 적용할지). 그래서 Segmented에는 \"선택 없음\"이\n * 없고 ToggleGroup에는 있다. 둘을 한 컴포넌트의 `mode` 축으로 합치면 그 차이가 사라지고,\n * \"아무것도 선택되지 않은 Segmented\"라는 표현 불가능한 상태가 타입에 생긴다.\n *\n * `CheckboxGroup`과도 다르다. CheckboxGroup은 목록 안의 동의/선택이라 각 항목이 자기\n * 줄과 설명을 갖는다. ToggleGroup은 도구 모음이라 한 줄에 붙어 있고 라벨이 짧다.\n * 판정(무엇을 켤 수 있고 비활성은 어떻게 되는가)은 같은 helper를 공유한다.\n */\nexport type ToggleGroupItemDescriptor<Id extends string = string> =\n CheckboxSelectionItem<Id>;\n\nexport type ToggleGroupDescriptor<Id extends string = string> = Readonly<{\n /** 묶음 전체의 접근성 이름. 개별 버튼 이름으로 대체할 수 없다. */\n accessibilityLabel: string;\n items: readonly ToggleGroupItemDescriptor<Id>[];\n}>;\n\nexport type ToggleGroupSize = \"small\" | \"medium\";\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`ToggleGroup ${field} must not be empty`);\n }\n}\n\nexport function validateToggleGroupDescriptor<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n): void {\n assertNonEmpty(descriptor.accessibilityLabel, \"accessibilityLabel\");\n if (!Array.isArray(descriptor.items) || descriptor.items.length === 0) {\n throw new RangeError(\"ToggleGroup 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 ToggleGroup item id: ${item.id}`);\n ids.add(item.id);\n }\n}\n\n/** 하나를 켜고 끈다. 비활성 보호는 공용 helper가 이미 갖고 있다. */\nexport function toggleGroupSelection<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n pressedIds: ReadonlySet<Id>,\n id: Id,\n): ReadonlySet<Id> {\n validateToggleGroupDescriptor(descriptor);\n return toggleCheckboxSelection(descriptor.items, pressedIds, id);\n}\n\n/** 목록에서 사라진 항목의 눌림 상태는 남기지 않는다. */\nexport function reconcileToggleGroupSelection<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n pressedIds: ReadonlySet<Id>,\n): ReadonlySet<Id> {\n validateToggleGroupDescriptor(descriptor);\n return reconcileCheckboxSelection(descriptor.items, pressedIds);\n}\n\nexport const toggleGroupRecipe = {\n slots: [\"root\", \"item\", \"label\"] as const,\n defaults: { size: \"medium\" as ToggleGroupSize },\n sizes: {\n small: { minHeight: control.minTouchTarget, paddingHorizontal: spacing.sm, textVariant: \"label\" as const },\n medium: { minHeight: 44, paddingHorizontal: spacing.md, textVariant: \"body\" as const },\n },\n radius: radius.md,\n gap: spacing.xxs,\n idle: {\n background: semanticColors.canvas,\n color: semanticColors.content.body,\n border: semanticColors.border.control,\n },\n pressed: {\n background: semanticColors.canvas,\n color: semanticColors.content.brand,\n border: semanticColors.border.focus,\n },\n states: { focus: focusIndicatorContract },\n} as const satisfies {\n slots: readonly [\"root\", \"item\", \"label\"];\n defaults: { size: ToggleGroupSize };\n sizes: Record<ToggleGroupSize, { minHeight: number; paddingHorizontal: number; textVariant: \"label\" | \"body\" }>;\n radius: number;\n gap: number;\n idle: { background: ColorReference; color: ColorReference; border: ColorReference };\n pressed: { background: ColorReference; color: ColorReference; border: ColorReference };\n states: { focus: typeof focusIndicatorContract };\n};\n\nexport const toggleGroupBehavior = {\n controlled: [\"pressedIds\", \"defaultPressedIds\", \"onPressedIdsChange\"],\n inputs: [\"items\", \"accessibilityLabel\", \"size\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\"],\n value: [\"pressed\", \"unpressed\"],\n },\n web: {\n roles: [\"group\", \"button\"],\n keyboard: [\"Tab\", \"Enter\", \"Space\"],\n /** Toolbar-style roving focus is not used: each toggle is its own tab stop, like a row of checkboxes. */\n focus: \"native\",\n },\n native: { roles: [\"button\"], states: [\"selected\", \"disabled\"], actions: [\"toggle\"] },\n scenarios: [\n \"several-items-can-be-pressed-at-once-and-none-is-a-valid-state\",\n \"pressed-state-rides-on-aria-pressed-not-on-color-alone\",\n \"a-disabled-item-never-toggles-and-never-enters-the-pressed-set\",\n \"removing-an-item-drops-its-pressed-state-instead-of-keeping-an-orphan-id\",\n \"single-choice-belongs-to-segmented-control-this-contract-never-adds-a-single-mode\",\n \"the-group-carries-its-own-accessible-name-separate-from-each-buttons-name\",\n ],\n} as const satisfies BehaviorContract;\n"]}
1
+ {"version":3,"file":"toggle-group.js","sourceRoot":"","sources":["../src/toggle-group.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorContract } from \"./behaviors.js\";\nimport type { ColorReference } from \"./color-references.js\";\nimport { focusIndicatorContract } from \"./component-contracts.js\";\nimport { control, radius, spacing } from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\nimport {\n reconcileCheckboxSelection,\n toggleCheckboxSelection,\n type CheckboxSelectionItem,\n} from \"./selection-helpers.js\";\n\n/**\n * 여러 개를 동시에 켜고 끄는 버튼 묶음이다 — 굵게/기울임/밑줄, 필터 칩 여러 개처럼.\n *\n * `SegmentedControl`과 경계가 분명하다. Segmented는 **하나를 고른다**(어떤 화면을 볼지),\n * ToggleGroup은 **여러 개를 켠다**(무엇을 적용할지). 그래서 Segmented에는 \"선택 없음\"이\n * 없고 ToggleGroup에는 있다. 둘을 한 컴포넌트의 `mode` 축으로 합치면 그 차이가 사라지고,\n * \"아무것도 선택되지 않은 Segmented\"라는 표현 불가능한 상태가 타입에 생긴다.\n *\n * `CheckboxGroup`과도 다르다. CheckboxGroup은 목록 안의 동의/선택이라 각 항목이 자기\n * 줄과 설명을 갖는다. ToggleGroup은 도구 모음이라 한 줄에 붙어 있고 라벨이 짧다.\n * 판정(무엇을 켤 수 있고 비활성은 어떻게 되는가)은 같은 helper를 공유한다.\n */\nexport type ToggleGroupItemDescriptor<Id extends string = string> =\n CheckboxSelectionItem<Id>;\n\nexport type ToggleGroupDescriptor<Id extends string = string> = Readonly<{\n /** 묶음 전체의 접근성 이름. 개별 버튼 이름으로 대체할 수 없다. */\n accessibilityLabel: string;\n items: readonly ToggleGroupItemDescriptor<Id>[];\n}>;\n\nexport type ToggleGroupSize = \"small\" | \"medium\";\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`ToggleGroup ${field} must not be empty`);\n }\n}\n\nexport function validateToggleGroupDescriptor<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n): void {\n assertNonEmpty(descriptor.accessibilityLabel, \"accessibilityLabel\");\n if (!Array.isArray(descriptor.items) || descriptor.items.length === 0) {\n throw new RangeError(\"ToggleGroup 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 ToggleGroup item id: ${item.id}`);\n ids.add(item.id);\n }\n}\n\n/** 하나를 켜고 끈다. 비활성 보호는 공용 helper가 이미 갖고 있다. */\nexport function toggleGroupSelection<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n pressedIds: ReadonlySet<Id>,\n id: Id,\n): ReadonlySet<Id> {\n validateToggleGroupDescriptor(descriptor);\n return toggleCheckboxSelection(descriptor.items, pressedIds, id);\n}\n\n/** 목록에서 사라진 항목의 눌림 상태는 남기지 않는다. */\nexport function reconcileToggleGroupSelection<Id extends string>(\n descriptor: ToggleGroupDescriptor<Id>,\n pressedIds: ReadonlySet<Id>,\n): ReadonlySet<Id> {\n validateToggleGroupDescriptor(descriptor);\n return reconcileCheckboxSelection(descriptor.items, pressedIds);\n}\n\nexport const toggleGroupRecipe = {\n slots: [\"root\", \"item\", \"label\"] as const,\n defaults: { size: \"medium\" as ToggleGroupSize },\n sizes: {\n small: { minHeight: control.minTouchTarget, paddingHorizontal: spacing.sm, textVariant: \"label\" as const },\n medium: { minHeight: 44, paddingHorizontal: spacing.md, textVariant: \"body\" as const },\n },\n radius: radius.md,\n gap: spacing.xxs,\n idle: {\n background: semanticColors.canvas,\n color: semanticColors.content.body,\n border: semanticColors.border.control,\n },\n pressed: {\n background: semanticColors.selection.background,\n color: semanticColors.selection.content,\n border: semanticColors.selection.border,\n },\n states: { focus: focusIndicatorContract },\n} as const satisfies {\n slots: readonly [\"root\", \"item\", \"label\"];\n defaults: { size: ToggleGroupSize };\n sizes: Record<ToggleGroupSize, { minHeight: number; paddingHorizontal: number; textVariant: \"label\" | \"body\" }>;\n radius: number;\n gap: number;\n idle: { background: ColorReference; color: ColorReference; border: ColorReference };\n pressed: { background: ColorReference; color: ColorReference; border: ColorReference };\n states: { focus: typeof focusIndicatorContract };\n};\n\nexport const toggleGroupBehavior = {\n controlled: [\"pressedIds\", \"defaultPressedIds\", \"onPressedIdsChange\"],\n inputs: [\"items\", \"accessibilityLabel\", \"size\"],\n stateAxes: {\n availability: [\"enabled\", \"disabled\"],\n value: [\"pressed\", \"unpressed\"],\n },\n web: {\n roles: [\"group\", \"button\"],\n keyboard: [\"Tab\", \"Enter\", \"Space\"],\n /** Toolbar-style roving focus is not used: each toggle is its own tab stop, like a row of checkboxes. */\n focus: \"native\",\n },\n native: { roles: [\"button\"], states: [\"selected\", \"disabled\"], actions: [\"toggle\"] },\n scenarios: [\n \"several-items-can-be-pressed-at-once-and-none-is-a-valid-state\",\n \"pressed-state-rides-on-aria-pressed-not-on-color-alone\",\n \"a-disabled-item-never-toggles-and-never-enters-the-pressed-set\",\n \"removing-an-item-drops-its-pressed-state-instead-of-keeping-an-orphan-id\",\n \"single-choice-belongs-to-segmented-control-this-contract-never-adds-a-single-mode\",\n \"the-group-carries-its-own-accessible-name-separate-from-each-buttons-name\",\n ],\n} as const satisfies BehaviorContract;\n"]}
@@ -138,12 +138,12 @@ export declare const transferListRecipe: {
138
138
  };
139
139
  readonly selectedBackground: Readonly<{
140
140
  source: "theme";
141
- key: "bg";
141
+ key: "surfaceSelected";
142
142
  alpha?: number;
143
143
  }>;
144
144
  readonly selectedIndicator: Readonly<{
145
145
  source: "theme";
146
- key: "contentBrand";
146
+ key: "text";
147
147
  alpha?: number;
148
148
  }>;
149
149
  readonly danger: Readonly<{
package/dist/tree.d.ts CHANGED
@@ -175,12 +175,12 @@ export declare const treeRecipe: {
175
175
  };
176
176
  readonly selectedBackground: Readonly<{
177
177
  source: "theme";
178
- key: "bg";
178
+ key: "surfaceSelected";
179
179
  alpha?: number;
180
180
  }>;
181
181
  readonly selectedIndicator: Readonly<{
182
182
  source: "theme";
183
- key: "contentBrand";
183
+ key: "text";
184
184
  alpha?: number;
185
185
  }>;
186
186
  readonly danger: Readonly<{
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Package release shown by documentation surfaces. Kept in sync by a test. */
2
- export declare const designSystemVersion: "1.39.0";
2
+ export declare const designSystemVersion: "1.41.0";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Package release shown by documentation surfaces. Kept in sync by a test. */
2
- export const designSystemVersion = "1.39.0";
2
+ export const designSystemVersion = "1.41.0";
3
3
  //# sourceMappingURL=version.js.map
@@ -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.39.0\" as const;\n"]}
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.41.0\" as const;\n"]}
@@ -52,11 +52,16 @@ message와 notification을 가르는 축(정보 층, 지속 시간, 동시 개
52
52
 
53
53
  | 종류 | 뜻 | 예 | 재검토 신호 |
54
54
  |---|---|---|---|
55
- | **흡수됨** | 그 문제를 이미 다른 컴포넌트가 완결한다 | `Notification`→Toast, `Dropdown`→Menu, `ContextPanel`→SidePanel/Sheet, `Flex`→Stack, `TimePicker`→Select 조합, `Rating`→Slider/Statistic | 흡수한 쪽이 못 푸는 요구가 나올 때 |
55
+ | **흡수됨** | 그 문제를 이미 다른 컴포넌트가 완결한다 | `Notification`→Toast, `Dropdown`→Menu, `ContextPanel`→SidePanel/Sheet, `Flex`→Stack | 흡수한 쪽이 못 푸는 요구가 나올 때 |
56
56
  | **검증할 화면이 없음** | 계약 자체는 유효하나 이를 확인할 제품 화면이 없다 | `Anchor` | 그 화면이 실제로 생길 때 |
57
57
  | **거절됨** | 계약도 유효하고 화면이 생겨도 만들지 않는다 | `BorderBeam`(장식), `AppProvider`(런타임뿐), `Utility`(실체 없음) | 정체성이나 아키텍처 경계가 바뀔 때만 |
58
58
  | **흡수 대기** | 흡수 판정은 끝났으나 **선결 축이 아직 없다** | `Cascader`(TreeSelect에 `valueMode`/`commitAt`가 추가돼야 성립) | 그 축이 실제로 추가될 때 |
59
59
 
60
+ 흡수 판정이 실제로 뒤집힌 예도 있습니다. 표에서 빼고 현재 계약을 따릅니다(2026-10-23 문서 대조에서 표가 옛 판정으로 남아 있던 것을 정정).
61
+
62
+ - `TimePicker`: 2026-10-19 선택의창의 시트 안 시·분 선택 요구로 독립 canonical 컴포넌트가 됐습니다([time-picker.md](time-picker.md)). crosswalk는 `direct`입니다.
63
+ - `Rating`: 2026-10-06 레퍼런스 적용 요청으로 정수 선택·미평가·소수 평균 표시를 구분하는 supplemental `Rating`(`/rating`)이 추가됐습니다([reference-controls.md](reference-controls.md#rating)). supplemental은 catalog `ComponentId`가 아니므로 antd `Rate`의 crosswalk target은 계속 `Slider`/`Statistic`입니다.
64
+
60
65
  둘을 섞으면 안 됩니다. **흡수됨**은 설계 판단이라 제품이 늘어나도 웬만해선 뒤집히지 않지만,
61
66
  **검증할 화면이 없음**은 사실 관찰이라 **커밋 하나로 낡습니다**. 실제로 그런 일이 있었습니다
62
67
  (`docs/expansion-roadmap.md`의 「vertical slice가 없는 계약」).
@@ -45,6 +45,7 @@ theme별 병합 결과를 검사하고, theme별 기준 미달 쌍을 돌려준
45
45
  | --- | --- | --- |
46
46
  | `text`·`textBody`·`textMuted`·`textSub` on `bg`·`surface` | 4.5 | WCAG 1.4.3 본문. `textSub`는 `Text tone="subtle"`의 실제 글자색이다 |
47
47
  | `contentBrand`·`danger` on `bg`·`surface` | 4.5 | 링크·현재 위치·오류 문구는 글자로 읽힌다 |
48
+ | `text`·`textBody`·`textMuted`·`textSub`·`contentBrand`·`danger` on `surfaceSelected` | 4.5 | 일반 선택 면 위 글자. 선택색 분리와 적용 확대에 따른 실제 소비 쌍(2026-10-23) |
48
49
  | `onPrimary` on `primary`, `onDanger` on `dangerFill` | 4.5 | 채운 버튼의 라벨 |
49
50
  | `onStatusFill` on `neutralFill`·`strongFill`·고정 `accentFill.<tone>` | 4.5 | 채운 Badge의 실제 라벨·바탕 쌍 |
50
51
  | `primary`·`borderControl` vs `bg`·`surface` | 3 | WCAG 1.4.11. 채운 컨트롤과 쉬고 있는 컨트롤 윤곽선은 형태로 식별돼야 한다 |
@@ -23,19 +23,21 @@
23
23
  뜨는 경우). 두 계약을 하나로 합치면 "화면의 결론"과 "콘텐츠 위 생성 행동"이라는
24
24
  서로 다른 두 의미를 한 recipe가 떠안게 되므로 분리를 유지한다.
25
25
 
26
+ 2026-10-23 번뜩 버튼 축소 요청: `size="medium"`은 기존 Button/IconButton 티어의 높이 44, 아이콘 `md`, 본문 라벨을 사용한다. 기본 `large`(52)는 유지한다. 두 renderer와 clearance 계산에 같은 크기를 전달하며 제품별 스타일 보정을 두지 않는다. 개발 소스 변경이며 미게시다.
27
+
26
28
  ## 일반화한 계약
27
29
 
28
30
  ### Button/IconButton recipe를 재사용한다 — 새 recipe를 얻지 못한다
29
31
 
30
32
  `floatingActionButtonRecipe`는 새 톤·크기·그림자를 선언하지 않는다.
31
33
 
32
- - `circle`은 `iconButtonRecipe.sizes.large`(diameter 52) 그대로.
34
+ - 기본 `circle`은 `iconButtonRecipe.sizes.large`(diameter 52), `sizes`는 기존 `medium`(44)·`large`(52) 티어를 재사용한다.
33
35
  - `tone`은 `iconButtonRecipe.tones.primary` 그대로.
34
36
  - `shape`는 `iconButtonRecipe.shapes.circle`(`radius.full`) 그대로.
35
37
  - `expandedLabel`은 `buttonRecipe.sizes.large`의 `textVariant`/`paddingHorizontal` 그대로.
36
38
 
37
- 두 모드(collapsed/expanded) 모두 `large` 티어를 공유하므로 모양이 원 ↔ 알약으로
38
- 바뀌는 동안 높이(터치 타깃)는 52로 고정된다 — 라벨이 나타나거나 사라질 때 버튼
39
+ 두 모드(collapsed/expanded) 모두 선택한 티어(기본 `large`)를 공유하므로 모양이 원 ↔ 알약으로
40
+ 바뀌는 동안 높이(터치 타깃)는 기본 52, `medium`에서는 44로 고정된다 — 라벨이 나타나거나 사라질 때 버튼
39
41
  전체가 세로로 들썩이지 않는다. `docs/dropdown.md`가 "Menu와 거의 같은 anatomy를
40
42
  다시 선언하지 않는다"고 판정한 것과 같은 이유로, 이 모듈도 `Button`의 recipe를
41
43
  포크하지 않는다.
@@ -58,7 +60,7 @@ LoadMore의 `viewport` 신호, Popover의 `AnchoredOverlay`와 같은 경계를
58
60
 
59
61
  FAB는 콘텐츠 위에 겹쳐 뜨므로, 스크롤 가능한 콘텐츠는 항상 FAB의 발자국만큼 하단
60
62
  여백을 예약해야 한다. 이 계산을 Web/RN 렌더러가 각자 다시 하면 어긋날 수 있으므로
61
- `resolveFloatingActionButtonContentClearance(safeAreaBottomInset)`이 한 곳에서
63
+ `resolveFloatingActionButtonContentClearance(safeAreaBottomInset, size?)`이 한 곳에서
62
64
  `diameter + margin × 2 + safeAreaBottomInset`을 계산해 돌려준다. safe-area
63
65
  inset은 BottomNavigation·Sheet·Toast와 같은 **additive** 모드다(base 여백에
64
66
  더하지, `max()`로 대체하지 않는다).
@@ -1,6 +1,6 @@
1
1
  # Component maturity and evidence manifest
2
2
 
3
- > Generated from `@hjmds/design-contracts` 1.39.0. Do not edit this file directly; run `pnpm contracts:sync`.
3
+ > Generated from `@hjmds/design-contracts` 1.41.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.39.0",
3
+ "designSystemVersion": "1.41.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.39.0",
8
+ "packageVersion": "1.41.0",
9
9
  "activeComponents": 131,
10
10
  "claimedComponents": 131,
11
11
  "completeScenarioComponents": 130,
@@ -7062,7 +7062,7 @@
7062
7062
  },
7063
7063
  "native": {
7064
7064
  "packageName": "@hjmds/react-native",
7065
- "packageVersion": "1.39.0",
7065
+ "packageVersion": "1.41.0",
7066
7066
  "activeComponents": 113,
7067
7067
  "claimedComponents": 113,
7068
7068
  "completeScenarioComponents": 110,
@@ -1,6 +1,6 @@
1
1
  # Renderer evidence coverage
2
2
 
3
- > Generated for HJM 1.39.0. Do not edit directly; run `pnpm evidence:sync`.
3
+ > Generated for HJM 1.41.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.39.0",
3
+ "designSystemVersion": "1.41.0",
4
4
  "packageName": "@hjmds/design-contracts",
5
5
  "components": [
6
6
  {
@@ -23,7 +23,7 @@ The Native Modal accepts supportedOrientations in fullScreen presentation. Optio
23
23
  safe-area insets protect controls and feedback in landscape while the gallery remains full width.
24
24
  Products must supply updated insets and permit rotation in their manifests; OS rotation lock still applies.
25
25
  The additive Native-only props keep this optional subpath and its existing peers; the base Image
26
- renderers retain their own host APIs. There is no Web ImageViewer counterpart.
26
+ renderers retain their own host APIs. The development Web `@hjmds/react/image-viewer` counterpart uses the shared modal focus engine and DOM pointer gestures without optional peers. Its `renderImage(item)` host mounts only the selected photo; Native host/inspection props remain platform-specific.
27
27
 
28
28
  ## Current evidence — 2026-09-30
29
29
 
@@ -102,3 +102,63 @@ Ant의 원시 색상이나 낮은 테두리 대비까지 복제하지 않고, HJ
102
102
  이전 제품 표면색을 HJM의 밝은 테마 기본값으로 채택한다. `surface`는 `#ffffff`,
103
103
  `surfaceAlt`는 `#ebeef2`다. `bg`는 기존 흰색을 유지하며 `raised` 카드도 기존처럼 `bg`를 쓴다.
104
104
  다크 테마와 브랜드·상태 강조색은 이번 결정의 변경 대상이 아니다. 웹·Native가 같은 `THEMES`를 소비한다.
105
+
106
+ ## 기본 선택 배경을 브랜드에서 분리한다 (2026-10-23)
107
+
108
+ 휠의 하늘색과 카테고리 선택색을 비교한 뒤 사용자가 기본 테마의 뉴트럴 안을 승인했다.
109
+ `surfaceSelected`는 light `#f0f1f3`, dark `#30343b`이고 `semanticColors.selection`이 배경·글자·테두리를 묶는다.
110
+ TimePicker, Chip(그룹의 chips 표현 포함)과 기본 neutral SegmentedControl이 같은 선택 배경을 쓴다.
111
+ 글자는 기존 `text`, Chip 테두리는 `borderControl`을 쓰며 체크·선택 테두리·포커스 표시를 보존한다.
112
+ 이 결정은 위 2026-09-29 기록의 해당 선택 배경을 대체하며 입력 필드 배경은 바꾸지 않는다.
113
+ `surfaceAccent`는 브랜드 보조 배경, `primary`는 주요 동작으로 유지한다.
114
+ 명시적인 `selectionTone="brand"`와 solid 탭은 기존 브랜드 강조를 유지한다.
115
+ 제품은 `brandPalette`의 `surfaceSelected`로 선택 배경을 지정할 수 있다. 기존 팔레트 대비 검사에 `text`/`surfaceSelected` 4.5:1 쌍을 포함한다.
116
+
117
+ ## 선택 상태 색상 기준 (2026-10-23)
118
+
119
+ 기본 테마의 선택 토큰이 일부 컴포넌트에만 연결된 공백을 사용자 검토에서 확인했다.
120
+ 색 이름이나 HTML `aria-selected`만으로 일괄 치환하지 않고, 제품에서 유지하는 값인지
121
+ 현재 키보드/포인터 조작 위치인지에 따라 다음처럼 적용한다. 각 API의 기존 지원 플랫폼에 적용한다.
122
+
123
+ | 의미 | 색상 기준 | 해당 공개 표면 |
124
+ | --- | --- | --- |
125
+ | 지속되는 일반 선택 면 | `selection.background`/`selection.content`, 경계가 필요하면 `selection.border` | Chip·칩 그룹, neutral SegmentedControl, TimePicker, filled Tabs, capsule BottomNavigation, ReactionPicker, ToggleGroup, selected Button·IconButton, 선택 Card·ListRow·Select·Combobox·Menu·Tree·TransferList·DataTable 행 |
126
+ | 현재 조작 위치 | 기존 focus/hover/pressed 역할. 확정 선택과 합치지 않음 | CommandPalette·Mentions·TagsInput 제안의 active descendant, Select·Combobox의 active 항목, 포커스 링 |
127
+ | 명시적인 진한 브랜드 면 | `primary`/`onPrimary` | `Tabs indicator="solid"`, `SegmentedControl selectionTone="brand"`, `BottomNavigation selection="tile"`, prominent destination |
128
+ | 작은 값 표시·현재 위치 표시 | 기존 전용 체크·점·밑줄·테두리 계약 | Checkbox·Radio의 glyph, Switch on, Calendar 날짜 원, underline/text Tabs, 일반 BottomNavigation 아이콘·글자, Pagination·Steps·Anchor·Carousel 위치 표시, 읽기 전용 반응 요약 |
129
+ | 브랜드·장식·업무 강조 | 기존 브랜드/상태 역할 | ChatMessage outgoing, 폴더 그림, liquid toast, 표의 `highlighted` 업무 강조, 브랜드 Surface/Badge, 오류·성공 등 상태 |
130
+ | 미디어 위 선택 증거 | 읽을 수 있는 전용 링·스크림 유지 | ImageChoice·MediaSelection. 회색 면으로 사진을 덮지 않음 |
131
+
132
+ ### 적용 규칙
133
+
134
+ - 선택 토큰은 현재 theme과 product palette로 해석한다. 제품별 직접 색상 분기는 추가하지 않는다.
135
+ - 정적 면과 이동하는 선택 배경은 같은 역할을 쓴다. reduced motion은 이동만 바꾸며 색상 의미를 바꾸지 않는다.
136
+ - 선택 값을 표현하는 체크·굵기·radio/pressed/selected 의미와 비활성·읽기 전용 동작은 보존한다.
137
+ - `Surface tone="selected"`는 선택 Card의 공통 면이다. `accent`는 브랜드 면으로 유지한다.
138
+ - 일반 선택을 브랜드로 보이게 하려고 primary를 바꾸거나, 포커스 위치를 확정 선택값처럼 칠하지 않는다.
139
+ - 선택 면에서 본문·보조·브랜드·위험 글자의 대비는 기존 palette 검사에서 각각 4.5:1을 확인한다.
140
+ `textWeak`의 비활성/장식 용도는 기존 경계를 따른다.
141
+
142
+ 선택 Card로 적용 범위를 넓히면서 light `textSub`가 새 회색 위에서 4.46:1인 것을 확인했다.
143
+ 선택 면 `#f0f1f3`을 유지하고 `textSub`를 `#65707d`에서 `#646f7c`로 한 단계 조정해 4.52:1로 보완했다.
144
+ 위 2026-09-26 값은 과거 기록이며 현재 값의 근거는 이 절이다.
145
+
146
+ ### 확인 경계
147
+
148
+ 이 기준은 HJM 개발 소스의 의미·renderer 연결 기준이다. 새 패키지 게시, 소비 앱의 설치 버전 채택,
149
+ 각 제품 테마의 실기기 화면 확인을 대신하지 않는다. 브라우저의 light/dark·제품 선택색 override와
150
+ Native host 검사를 수행한 범위를 기록하며 실기기 결과는 따로 구분한다.
151
+
152
+ 2026-10-23 개발 확인: Web/Native 선택색 회귀(정적·이동 면, 제품 override, 명시적 solid 강조,
153
+ Combobox의 확정 선택과 활성 후보 분리), 관련 선택 동작, palette 대비 검사에서 총 123건 통과했다.
154
+ 기본 neutral을 포함한 11개 preset의 light/dark 22개 팔레트에서도 새 선택 면의 글자 대비 쌍을 확인했다.
155
+ Web의 `core-normalization.ssr.test.tsx` 중 기존 small Button/IconButton hit-slop CSS 문자열 검사
156
+ 2건은 변경 전 HEAD에서도 불일치하며, 이번 선택색과 무관한 실패로 남겼다. 변경한 Card/선택 상태
157
+ 검사는 통과했다. 세 패키지 소스 빌드는 통과했으며 Native 실기기 확인·패키지 게시·소비 앱 채택은 미실행이다.
158
+
159
+ 같은 날 후속 수정에서 남겨 둔 실패를 해결했다. Web의 오래된 hit-slop CSS 문자열 기대는
160
+ SSR 검사에서 제거하고 기존 브라우저 검사에서 Button/IconButton의 기본 확장·명시적 0·실제 포인터
161
+ hit-test와 클릭 전달을 확인한다. Native 모션 검사는 기본 스크롤 레일의 필수 layout 측정을
162
+ 허용하되 선택 항목 노출·애니메이션 미설치·모션 미실행을 검증하도록 바로잡았다.
163
+ Web core SSR 15건, 브라우저 대상 동작 1건, Native 모션 3건 모두 통과했다.
164
+ 런타임 구현 변경이나 검사 skip 추가 없이 테스트의 오래된 구현 가정을 수정한 결과다.
@@ -8,15 +8,36 @@
8
8
  - 계약과 recipe: `src/time-picker.ts`. Web/RN: `@hjmds/react/time-picker`, `@hjmds/react-native/time-picker`.
9
9
  - `presentation="field"`(기본): 필드 한 개 → Web Popover / Native Sheet 한 개. 두 열 선택은 초안이며 확인 버튼으로만 `onValueChange`한다. 닫기·Escape·바깥 누름·시트 dismiss는 확정하지 않는다.
10
10
  - `presentation="inline"`: 같은 두 열을 기존 폼·시트 안에 표시한다. 최종 확인은 바깥 폼이 소유한다. 각 선택에서 완전하고 사용 가능한 값은 `HH:mm`, 미완성·선택 불가 초안은 `null`로 전달한다. 부모가 null을 반영해도 사용 중인 부분 선택은 유지한다.
11
+ - 2026-10-23 휠 피커 도입 요청: `selectionStyle="list"`(기본)는 기존 탭 목록, `"wheel"`은 관성 스크롤과 중앙 스냅이다. 별도의 WheelPicker를 만들지 않고 같은 초안·확정 계약을 확장했다. 추가 Native peer는 없다. 이 옵션은 개발 소스의 미게시 변경이다.
12
+ - 휠의 양 끝에 두 행씩 여백을 두어 첫 값·마지막 값도 중앙에 놓인다. 가장 가까운 사용 가능 행으로 스냅하고, 같은 거리이면 앞 행을 선택한다. 초기 배치·외부 값 갱신·프로그램 정렬은 선택 이벤트가 아니다. 빈 값은 첫 행을 보여도 선택하지 않으며, 기존 off-grid·불가한 조합을 자동으로 고치지 않는다.
13
+ - 사용자 스크롤이 지나는 사용 가능 값은 초안에 반영한다. field 확인은 스크롤 중 비활성이고 멈춘 뒤에 가능하다. inline의 각 값 변경은 바깥 폼의 초안이며 최종 확정은 바깥 폼이 소유한다. 시 변경으로 선택한 분이 불가능해져도 분을 임의로 바꾸지 않는다.
11
14
  - 값은 `HH:mm | null`, 날짜·시간대·서머타임·현재 시각 계산은 제품 소유. 자정 `00:00`도 유효하다.
12
15
  - `minuteStep` 기본 1. 60의 양의 정수 약수만 허용한다. 기존 값이 간격과 맞지 않아도 자동 반올림하지 않는다.
13
16
  - `isTimeUnavailable(time)`은 제품이 제공한다. 시는 사용 가능한 분이 하나라도 있으면 선택할 수 있다. 선택한 분을 몰래 바꾸지 않으며, 그 조합이 불가하면 확인할 수 없다.
14
17
  - 외부 `value` 변경은 초안에 반영한다. 비활성·읽기 전용으로 전환하면 열린 선택기를 닫고 확정하지 않는다.
15
18
  - Web: 열마다 roving focus, 방향키·Home·End가 비활성 항목을 건너뛰며 선택한다. Enter/Space는 버튼 기본 동작. Popover가 초점·Escape·바깥/blur dismiss·복귀를 소유한다.
16
19
  - Native: 각 값은 이름·checked/disabled 상태를 가진 radio이며 host 활성화를 사용한다. Sheet가 back·dismiss·safe area·복귀를 소유한다. 실제 VoiceOver/TalkBack 기기 검증과 host action 검사는 구분한다.
20
+ - 휠의 Web 표현은 기존 listbox·option·roving keyboard를 유지하고 CSS scroll snap과 [scrollend](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event)로 멈춤을 판정한다. scrollend 지원 브라우저가 필요하며 오래된 브라우저 대상은 기존 list를 선택한다. Native는 [ScrollView의 snapToOffsets](https://reactnative.dev/docs/scrollview#snaptooffsets)와 drag/momentum 이벤트를 사용한다. 열마다 adjustable 역할과 increment/decrement로 불가한 행을 건너뛴다. 내부 radio들은 휠에서 보조기술에 중복 노출하지 않는다.
21
+ - 동작 줄이기는 Provider 설정을 따른다. 값 정렬을 즉시 수행하며 Native 감속률은 0으로 설정한다. 스크롤 선택과 확정·취소 의미는 유지한다.
17
22
 
18
23
  기존 Collection/Select는 단일 선택으로 닫히는 계약이다. TimePicker는 그 wrapper가 아니라 두 부분을 한 번에 확정하는 세션이다. 목록 값과 공통 Popover/Sheet/Button을 재사용하며, 날짜 엔진이나 새 native peer를 추가하지 않는다.
19
24
 
20
25
  디자인 모티브: [Ant Design](https://ant.design/components/time-picker/)의 통합 열·확정,
21
26
  [Material 3](https://m3.material.io/components/time-pickers/)의 큰 시각 표시.
22
27
  색·모서리·간격은 HJM recipe와 소비 제품의 semantic palette를 따른다.
28
+
29
+ ## 2026-10-23 개발 확인 범위
30
+
31
+ 공통 계약 4개, Web 브라우저 13개, Native host 10개, 검색 연결 브라우저 1개가 통과했다.
32
+ 양 renderer의 소스 빌드와 Web·Native Showcase 타입 확인도 통과했다. Web 실화면에서 분 휠을 굴려
33
+ `14:30 → 14:55` 변경과 중앙 정렬을 확인했다. 전체 renderer 테스트 타입 검사에는 기존
34
+ `packages/react/test/selection-motion.browser.test.tsx`의 미사용 `page` import 오류가 남아 있으며 변경 범위 밖이다.
35
+
36
+ Native 실제 제스처·VoiceOver/TalkBack 확인은 **미완료**다. 기존 iPhone 17 Pro·iOS 26.5에 설치된
37
+ Showcase 바이너리는 RN 0.86.2이고 소스는 0.88.0이어서 호환 개발 빌드를 시도했다.
38
+ 메뉴 헤더 import와 이미지 로더 프로토콜은 기존 pnpm 패치에서 바로잡았지만,
39
+ `react-native-ios-utilities@5.2.0/ios/Sources/ObjcHelpers/UIView+RNIPaperHelpers.mm:34`가 RN 0.88에서
40
+ 제거된 `RCTSurfacePresenterBridgeAdapter.bridge`를 참조하여 빌드가 중단됐다.
41
+ [어댑터 이관 조건](../../../docs/interaction-adapters.md#sdk-58-native-showcase-build-blocker-2026-10-23)을 해결한 뒤
42
+ 현재 소스로 개발 바이너리를 빌드·설치하고 휠 제스처·취소·확정·보조기술 액션을 재개해야 한다.
43
+ 호환성 미확인을 휠의 기기 검증 완료나 패키지 게시로 해석하지 않는다.
package/docs/toast.md CHANGED
@@ -199,6 +199,17 @@ Web renderer:
199
199
 
200
200
  Native renderer:
201
201
 
202
+ - 2026-10-22 리포트의 플랫폼별 완료 알림 명세를 기존 renderer와 대조했습니다.
203
+ Android는 `accessibilityLiveRegion`을 사용하며 `normal`은 `polite`, `high`는
204
+ `assertive`입니다. 이 속성은 Android 전용이므로 iOS 전달의 근거로 쓰지 않습니다
205
+ ([RN Accessibility](https://reactnative.dev/docs/accessibility#accessibilityliveregion-android)).
206
+ - iOS는 `announceForAccessibilityWithOptions`로 `normal`에 `queue: true`, `high`에
207
+ `queue: false`를 전달합니다. RN의 기본 알림은 기존 발화를 중단하므로 비차단 완료에는
208
+ 대기열을 명시합니다([RN AccessibilityInfo](https://reactnative.dev/docs/accessibilityinfo#announceforaccessibilitywithoptions)).
209
+ 제품은 Toast와 같은 문구를 별도 announcement로 다시 보내지 않습니다. 패널 없는 짧은 결과는
210
+ 기존 [StatusText](usage/components/text.md#statustext)를 사용합니다.
211
+ - 시각 표시·보조기술 전달·기존 포커스 유지는 각각 확인합니다. 위 내용은 현재 소스의 매핑이며
212
+ 실제 발화 순서·연속 갱신의 중복 여부·VoiceOver/TalkBack 검증 완료를 뜻하지 않습니다.
202
213
  - safe-area inset을 recipe inset에 additive로 적용하고 visual root 전체를 하나의 접근성
203
214
  요소로 병합하지 않습니다. announcement node와 action/close를 각각 독립 접근성 노드로
204
215
  유지하고, 비상호작용 copy만 필요할 때 묶습니다.
@@ -0,0 +1,71 @@
1
+ # 쉬운 말로 찾고 비교하기
2
+
3
+ 2026-10-23 [앱 UI 메뉴판](https://giting.kr/skills/app-menu)의 별칭 → 명칭 → 요청 예시 접근을 참고했다.
4
+ 일상 표현으로 기존 HJM API에 도달하도록 안내하며, 외부 사전의 플랫폼 설명을 HJM의 지원 계약으로 사용하지 않는다.
5
+
6
+ [Storybook 컴포넌트 찾기](https://storybook.jmstudioapps.com/?path=/story/components-overview--explorer)에서
7
+ 쉬운 말·한글 이름·API 이름을 검색한다. 현재 checkout의 개발 변경이며 공개 사이트 반영은 별도다.
8
+ 결과 카드에서 선택 기준·요청 예시를 읽고 실행 예제와 사용 지침으로 이동한다.
9
+ 웹 Storybook은 작성 도구이며 Web 예제로 Native 동작을 검증했다고 판단하지 않는다.
10
+
11
+ ## 이름이 떠오르지 않을 때
12
+
13
+ | 찾는 말 | 먼저 볼 API·지침 | 실행 예제 |
14
+ | --- | --- | --- |
15
+ | 아래에서 올라오는 판 | [Sheet](components/sheet.md) | [시트](https://storybook.jmstudioapps.com/?path=/story/components-overlays--sheet) |
16
+ | 맨 아래 붙은 버튼 | [BottomCTA](components/bottom-cta.md) | [하단 행동](https://storybook.jmstudioapps.com/?path=/story/components-actions--bottom-cta) |
17
+ | 목록 한 줄, 오른쪽 현재 값 | [ListRow](components/list-row.md) | [목록 행](https://storybook.jmstudioapps.com/?path=/story/components-data-display--list-row) |
18
+ | 밀면 나오는 삭제 버튼 | [SwipeActions](components/swipe-actions.md) | [끌기·밀기](https://storybook.jmstudioapps.com/?path=/story/experimental-interaction-adapters--default) |
19
+ | 꾹 눌러 순서 바꾸기 | [SortableCollection](components/sortable-collection.md) | [끌기·밀기](https://storybook.jmstudioapps.com/?path=/story/experimental-interaction-adapters--default) |
20
+ | 끝까지 내리면 더 나옴 | [LoadMore](components/load-more.md) | [추가 로딩](https://storybook.jmstudioapps.com/?path=/story/components-navigation--load-more) |
21
+ | 키보드가 입력칸을 가려 | [KeyboardFormScrollView](components/keyboard-form-scroll-view.md)·[입력 시트](compositions/input-sheet.md) | [입력 시트](https://storybook.jmstudioapps.com/?path=/story/patterns-input-sheet--default) |
22
+ | 갤러리·카메라 선택 | [PhotoSourceSheet](components/photo-source-sheet.md) | [사진 출처](https://storybook.jmstudioapps.com/?path=/story/components-overlays--photo-source-sheet) |
23
+
24
+ 검색 결과에서 List·Form·Sheet가 나오면 카드에 안내된 선택형 API도 확인한다.
25
+ 예를 들어 List는 표시 계약이고, SwipeActions·SortableCollection은 별도의 공개 진입점이다.
26
+ 검색어를 공개 export 이름이나 canonical API 별칭으로 추가하지 않는다.
27
+
28
+ ## 비슷한 UI 중 고르기
29
+
30
+ | 필요한 일 | 선택 | 비교할 경계 |
31
+ | --- | --- | --- |
32
+ | 앱의 주요 구역 이동 | [BottomNavigation](components/bottom-navigation.md) | 한 화면 안 짧은 보기 전환은 SegmentedControl, 관련 패널 탐색은 Tabs |
33
+ | 실행할 행동 고르기 | [Menu](components/menu.md) | 값 입력은 Select, 삭제 직전 확인은 AlertDialog |
34
+ | 보조 선택·입력 작업 | [Sheet](components/sheet.md) | 짧은 집중 작업은 Dialog. “모달”이라는 말만으로 풀스크린을 결정하지 않음 |
35
+ | 끌어서 높이 바꾸는 시트 | [선택형 Native GestureSheet](compositions/optional-adapters.md) | 기본 Sheet의 지원과 optional peer 설치·provider 조건을 구분 |
36
+ | 켜짐·꺼짐 설정 | [Switch](components/switch.md) | 여러 항목 동의는 Checkbox·Agreement, 하나의 선택지는 RadioGroup |
37
+ | 수량 늘리고 줄이기 | [NumberField](components/number-field.md) | Steps는 진행 단계 표시이며 수량 입력이 아님 |
38
+ | 처음 데이터가 없음 | [EmptyState](components/empty-state.md) | 로딩·오류·검색 결과 없음과 실제 상태를 구분 |
39
+ | 설정 화면 구성 | [SettingsScreen](screens/common-settings.md) | 제품에서 그룹·행 배치를 다시 조립하기 전에 화면 API부터 선택 |
40
+
41
+ 웹·앱의 표현은 각 API 지침의 플랫폼 차이를 읽는다. 모든 Native Select를 시트로 바꾸거나,
42
+ 모든 모달을 풀스크린으로 만드는 규칙은 두지 않는다. 화면 이동·딥링크·스플래시 초기화·권한·서버 저장은
43
+ [제품 소유 경계](SELECTION.md#소유권)다. HJM에는 공통 표시와 상호작용을 연결한다.
44
+
45
+ ## 요청을 구현으로 연결하기
46
+
47
+ `화면 목적 + 필요한 구조 + 실제 상태 + 제품 콜백`으로 요청한다. 배치·지원 prop은 연결한 지침이 정본이다.
48
+
49
+ | 요청 예시 | 수정 위치 → 공개 API → 확인 범위 |
50
+ | --- | --- |
51
+ | “설정을 계정·알림·정보 그룹으로 나누고 행 오른쪽에 현재 값을 보여줘” | 제품 설정 데이터 → SettingsScreen·ListRow → 그룹 순서·값·행동 연결 |
52
+ | “댓글 더보기에서 신고·차단을 고르게 해줘” | 제품의 허용 행동 → Menu의 anchored 표현 → 대상·닫힘·후속 행동. [메뉴 지침](components/menu.md) |
53
+ | “사진 출처를 고른 뒤 카메라를 열어줘” | 제품 권한·picker adapter → PhotoSourceSheet → 닫힘 뒤 실행·권한 거부. [사진 출처 지침](components/photo-source-sheet.md) |
54
+ | “저장 실패하면 입력을 유지하고 키보드가 마지막 필드를 가리지 않게 해줘” | 제품 초안·요청 상태 → Form과 입력 host → 실패 후 초안·필드 도달·저장 행동. [입력 시트](compositions/input-sheet.md) |
55
+
56
+ 공통 배치가 부족하면 제품별 보정 여백을 더하지 않고 [선택 기준](SELECTION.md)에 따라 HJM 소유 구현을 보완한다.
57
+ 필요한 검사만 고르며 이 안내는 새 자동 검사나 승인 절차를 추가하지 않는다.
58
+
59
+ ## 휠 피커 검토 결과
60
+
61
+ 2026-10-23 구현: [TimePicker](components/time-picker.md)는 Web·Native 모두 `selectionStyle="wheel"`로
62
+ 관성 스크롤과 중앙 스냅을 제공한다. 기본 `list`는 기존 탭 목록이다. 휠 옵션은 개발 소스의 미게시 변경이며,
63
+ 검색에서 “휠 피커”를 입력하면 기존 TimePicker와 실행 예제로 연결한다.
64
+
65
+ 선택한 설계는 **시각 선택에는 기존 TimePicker를 확장하는 방식**이다. 일반 WheelPicker를 먼저 별도로 만들면
66
+ 초안·확정·취소·사용 불가 시각 계약이 중복된다. 기존 `HH:mm | null` 값·`minuteStep`·선택 불가 판정·명시적 확정을 유지하고,
67
+ 선택 표현 옵션으로 스냅·관성·중앙 선택 표시를 추가했다. 일반 항목 선택 요구가 실제로 생기면
68
+ 공유할 내부 선택 엔진의 범위를 판단한다.
69
+
70
+ 검증은 스크롤 초안과 최종 확정, 불가능한 값 건너뛰기, 취소 복원, 보조기술 액션, 동작 줄이기와
71
+ 플랫폼별 실제 동작으로 나눈다. host 검사·브라우저 검사·Native 기기 확인의 결과는 각각 구분한다.
@@ -5,8 +5,9 @@
5
5
  단계는 Storybook과 같은 `토큰 → 컴포넌트 → 구성 → 화면`이다. 화면을 만들 때는 화면 지침의 영역 구조와
6
6
  버튼 위치에서 시작해 구성·컴포넌트 지침으로 내려가고, 값은 토큰 지침에서 고른다.
7
7
  [선택 기준](SELECTION.md)으로 그대로 사용·공개 슬롯 확장·공통 API 보완·제품 고유 기능 연결을 고른다. 설치 버전의 지침을 본다: `node_modules/@hjmds/design-contracts/docs/usage/`.
8
+ 이름을 모르면 [쉬운 말로 찾고 비교하기](FINDING.md)에서 일상 표현과 선택 기준·요청 예시로 시작한다.
8
9
  제공 기준은 확인한 기본 API 버전이며 최초 출시 버전이 아니다. 구성·화면은 각 API 지침의 제공 버전을 확인한다.
9
- API 안정성은 1.37.2의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
10
+ API 안정성은 1.40.0의 생성 manifest에서 읽은 정규(canonical) API 기준이다. 동반·선택형 확장은 개별 계약을 확인한다.
10
11
  Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품 반영을 의미하지 않는다.
11
12
  `분류`는 Storybook 제목 `<배포|실험>/<단계>/<분류>/<항목>`의 셋째 마디이고, 토큰·구성·화면은 Storybook 메뉴와 같은 순서다.
12
13
 
@@ -63,6 +64,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
63
64
  | [Checkbox](components/checkbox.md) | 입력 | 독립된 예/아니오 하나를 고르는 항목에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
64
65
  | [CheckboxGroup](components/checkbox-group.md) | 입력 | 한 질문에 대한 여러 선택지 중 0개 이상을 고르게 할 때 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
65
66
  | [Chip](components/chip.md) | 입력 | 누를 수 있는 작은 pill이다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
67
+ | [ChoiceRow](components/choice-row.md) | 입력 | 설정 화면에서 언어·화면 테마·기본 정렬처럼 **여러 값 중 하나를 고르는 설정**에 쓴다. | 1.41.0 | Web · Native | 개별 API 계약 참조 | 배포 |
66
68
  | [CodeBlock](components/code-block.md) | 데이터 표시 | 코드·명령·설정 조각을 읽기 전용으로 보여 주고 사용자가 선택·복사하게 할 때 쓴다. | 1.12.1 | Web · Native | 개별 API 계약 참조 | 배포 |
67
69
  | [CohortRetentionTable](components/cohort-retention-table.md) | 구성/정보 표시 | 가입 시점(코호트)별로 기간마다 남은 인원 비율을 직사각형 표와 heat 색으로 비교할 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
68
70
  | [Collapsible](components/collapsible.md) | 데이터 표시 | 이웃 없이 혼자 접었다 펴는 한 덩어리에 쓴다. | 1.12.1 | Web · Native | Web stable / Native stable | 배포 |
@@ -108,7 +110,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
108
110
  | [ImageComparison](components/image-comparison.md) | 데이터 표시 | 같은 좌표와 비율의 두 이미지를 겹쳐 변화량을 비교할 때 쓴다. | 1.14.0 | Web · Native | 개별 API 계약 참조 | 배포 |
109
111
  | [ImportFieldMapping](components/import-field-mapping.md) | 구성/입력과 작성 | CSV 등 원본 열을 제품의 대상 필드에 연결하고, 검증을 통과한 연결만 가져오기로 넘길 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
110
112
  | [InlineEdit](components/inline-edit.md) | 구성/입력과 작성 | 목록·상세의 제목처럼 이미 확정된 짧은 값 하나를 그 자리에서 명시적으로 편집·저장·취소할 때 쓴다. | 1.21.0 | Web · Native | 개별 API 계약 참조 | 배포 |
111
- | [KeyboardAvoidanceContext](components/keyboard-avoidance-context.md) | — | 제품이 직접 만든 키보드 대응 영역 안에서 HJM `ScreenLayout`·`Sheet`가 여백을 두 번 주지 않게 할 때 쓴다. | 미게시(1.26.1 이후) | Native | 개별 API 계약 참조 | 실험 |
113
+ | [KeyboardAvoidanceContext](components/keyboard-avoidance-context.md) | — | 제품이 직접 만든 키보드 대응 영역 안에서 HJM `ScreenLayout`·`Sheet`가 여백을 두 번 주지 않게 할 때 쓴다. | 1.27.0 | Native | 개별 API 계약 참조 | 배포 |
112
114
  | [KeyboardAvoiding](components/keyboard-avoiding.md) | — | 추가 native peer 없이 하단 행동(BottomCTA, 채팅 입력창)이 소프트웨어 키보드에 가려지지 않게 할 때 쓴다. | 1.12.1 | Native | 개별 API 계약 참조 | 배포 |
113
115
  | [KeyboardDock](components/keyboard-dock.md) | 구성/직접 조작과 모션 | `react-native-keyboard-controller`를 설치한 앱에서 화면 하단에 고정된 행동(BottomCTA, 채팅 입력창)이 키보드와 함께 위아래로 움직이게 할 때 쓴다(Native 전용, 별도 보조 기능. API 성숙도는 실험적 어댑터). 내부는 `KeyboardStickyView`이며, 여백을 바꾸는 대신 키보드 움직임을 따라 translate 한다. | 1.12.1 | Native | 개별 API 계약 참조 | 배포 |
114
116
  | [KeyboardFormScrollView](components/keyboard-form-scroll-view.md) | 구성/직접 조작과 모션 | 입력 필드가 여러 개인 세로 스크롤 폼(가입, 프로필 수정, 주소 입력)에서 포커스된 필드가 키보드에 가려지지 않게 스크롤해 줄 때 쓴다(Native 전용, 별도 보조 기능. API 성숙도는 실험적 어댑터). 내부는 `react-native-keyboard-controller`의 `KeyboardAwareScrollView`이고, `keyboardShouldPersistTaps="handled"`로 고정돼 키보드가 열린 채 버튼을 눌러도 탭이 전달된다. | 1.12.1 | Native | 개별 API 계약 참조 | 배포 |
@@ -262,7 +264,7 @@ Storybook 배포/실험은 예제 분류이며 npm 게시·기기 검증·제품
262
264
  | [보관과 실행 취소](compositions/action-recovery-undo.md) | 피드백과 복구 | 보관·숨기기·목록에서 빼기처럼 제품이 역연산을 제공하는 작업 뒤에, 같은 자리에서 실행 취소를 주고 그 복구 요청이 성공해야 화면을 되돌릴 때 쓴다. | 1.12.1 | Web · Native | 구성 API별 | 배포 |
263
265
  | [선택과 오류 복구](compositions/upload-recovery.md) | 피드백과 복구 | 파일 선택과 전송 상태의 취소·재시도를 연결할 때 쓴다. | 1.14.0 | Web · Native | 구성 API별 | 배포 |
264
266
  | [스트림 응답과 중지](compositions/streaming-answer.md) | 피드백과 복구 | 실제 provider가 보내는 응답 delta를 표시하고 중지·재생성·오류를 같은 영역에서 제공할 때 쓴다. | 1.21.0 | Web · Native | 구성 API별 | 배포 |
265
- | [저장 결과](compositions/save-feedback.md) | 피드백과 복구 | 실제 영속 저장 성공 뒤 짧게 결과를 알린다. | 1.37.1 | Web · Native | 구성 API별 | 실험 |
267
+ | [저장 결과](compositions/save-feedback.md) | 피드백과 복구 | 실제 영속 저장 성공 뒤 짧게 결과를 알린다. | 1.37.1 | Web · Native | 구성 API별 | 배포 |
266
268
  | [저장과 재시도](compositions/action-recovery-save.md) | 피드백과 복구 | 입력한 내용을 서버에 저장하는 폼 한 덩어리에서 저장 중 중복 실행을 막고, 실패하면 입력을 지우지 않은 채 제출했던 값 그대로 다시 보낼 때 쓴다. | 1.12.1 | Web · Native | 구성 API별 | 배포 |
267
269
  | [전송 진행과 취소](compositions/transfer-task.md) | 피드백과 복구 | 실제 다운로드·업로드·작업의 시작/대기/진행·취소·실패 복구를 한 구성으로 제공할 때 쓴다. | 1.21.0 | Web · Native | 구성 API별 | 배포 |
268
270
  | [중단해도 남는 현재 상태](compositions/expo-interactions.md) | 피드백과 복구 | 버튼으로 상태를 빠르게 바꾸거나 전환 도중 내용을 닫아도 현재 상태가 바로 보이고 남아야 하는 영역에 쓴다. | 1.12.1 | Native | 구성 API별 | 배포 |
@@ -30,6 +30,12 @@
30
30
 
31
31
  ## 선택 기준
32
32
 
33
+ 선택 상태의 색상은 [기본 테마 선택 상태 기준](../theme-palette.md#선택-상태-색상-기준-2026-10-23)을 따른다.
34
+ 지속되는 선택, 현재 조작 위치, 명시적 브랜드 강조를 구분하고 공통 recipe에서 연결한다.
35
+
36
+ 이름을 모르면 [쉬운 말로 찾고 비교하기](FINDING.md)에서 일상 표현·유사 UI 차이·요청 예시로 시작한다.
37
+ Storybook의 기존 **컴포넌트 찾기**에서도 같은 쉬운 말로 검색해 실행 예제를 연다.
38
+
33
39
  | 선택 | 조건 | 구현 경로 |
34
40
  | --- | --- | --- |
35
41
  | 그대로 사용 | 제공하는 화면·구성이 제품의 목적에 맞는다 | HJM 공개 API에 데이터·문구·콜백 연결 |
@@ -71,8 +77,8 @@ HJM의 기존 API를 확장하거나 하위 구성을 공개한다. 도메인
71
77
 
72
78
  | 의도 | 쓸 것 | 쓰지 않을 것 |
73
79
  | --- | --- | --- |
74
- | 하나 고르기(주제·정렬·탭) | `Chip`(`selectionMode="single"`), `SegmentedControl`, `RadioGroup`, `ToggleGroup` | `selected`를 준 `Button` 나열 |
75
- | 여러 개 고르기 | `Chip`(`selectionMode="multiple"`), `CheckboxGroup` | 버튼 토글 |
80
+ | 하나 고르기(주제·정렬·탭) | `Chip`(`selectionMode="single"`), `SegmentedControl`, `RadioGroup` | `selected`를 준 `Button` 나열, `ToggleGroup`(단일 모드가 없는 다중 토글 계약) |
81
+ | 여러 개 고르기 | `Chip`(`selectionMode="multiple"`), `CheckboxGroup`, 짧은 서식·필터 토글 묶음은 `ToggleGroup` | 버튼 토글 |
76
82
  | 켜고 끄기 설정 | `Switch`(제목·설명 포함), 설정 목록은 `ListRow` + `Switch` | "○○ 켜짐/꺼짐" 글자 버튼 |
77
83
  | 주 행동 | 화면 지침의 "버튼과 행동 위치"대로 하나의 primary, 하단 고정이면 `BottomCTA`/`footer` | 같은 표면에 경쟁하는 primary 여럿, 폭이 제각각인 버튼 줄 |
78
84
  | 보조 행동 | 주 행동 아래 같은 폭 secondary, 또는 헤더 `actions` | 본문 중간에 흩어진 버튼, 나가기를 보조 행동으로 두기 |
@@ -274,3 +274,8 @@ floating 그림자는 그리지 않는다. 옆면이 높이를 표현하므로
274
274
  개발 브라우저에서 Native renderer를 실행할 때도 현재 목적지의 선택 상태와 비활성 상태를 전달한다.
275
275
  Tab은 탐색 막대에 한 번 진입하고, 방향키·Home·End는 포커스만 옮긴다. Enter·Space로 목적지를
276
276
  활성화하며 실제 경로 선택은 계속 제품 navigator가 공급한다.
277
+
278
+ ### 기본 선택 면
279
+
280
+ `presentation: "capsule"`의 선택 항목은 `selectionCapsule`의 neutral 선택 배경과 글자를 쓴다.
281
+ 기본 아이콘·글자 탐색의 위치 강조와 명시적 `selection: "tile"`의 브랜드 면은 기존 계약을 따른다.