@hjmds/design-contracts 1.27.1 → 1.28.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 (155) hide show
  1. package/dist/activity-chip.d.ts +1 -1
  2. package/dist/activity-chip.js +1 -1
  3. package/dist/activity-chip.js.map +1 -1
  4. package/dist/auth-screen.d.ts +71 -5
  5. package/dist/auth-screen.d.ts.map +1 -1
  6. package/dist/auth-screen.js +60 -2
  7. package/dist/auth-screen.js.map +1 -1
  8. package/dist/base-recipes.d.ts +9 -1
  9. package/dist/base-recipes.d.ts.map +1 -1
  10. package/dist/base-recipes.js +7 -1
  11. package/dist/base-recipes.js.map +1 -1
  12. package/dist/behaviors.d.ts +1 -1
  13. package/dist/breadcrumb.d.ts +1 -1
  14. package/dist/breadcrumb.js +1 -1
  15. package/dist/breadcrumb.js.map +1 -1
  16. package/dist/calendar.d.ts +1 -1
  17. package/dist/calendar.js +2 -2
  18. package/dist/calendar.js.map +1 -1
  19. package/dist/catalog.d.ts +148 -89
  20. package/dist/catalog.d.ts.map +1 -1
  21. package/dist/component-contracts.d.ts +1 -1
  22. package/dist/component-contracts.js +1 -1
  23. package/dist/component-contracts.js.map +1 -1
  24. package/dist/component-recipes.d.ts +105 -60
  25. package/dist/component-recipes.d.ts.map +1 -1
  26. package/dist/component-recipes.js +138 -69
  27. package/dist/component-recipes.js.map +1 -1
  28. package/dist/conversation-row.d.ts +2 -2
  29. package/dist/conversation-row.d.ts.map +1 -1
  30. package/dist/conversation-row.js +6 -4
  31. package/dist/conversation-row.js.map +1 -1
  32. package/dist/counter-badge-recipe.d.ts +1 -1
  33. package/dist/counter-badge-recipe.js +1 -1
  34. package/dist/counter-badge-recipe.js.map +1 -1
  35. package/dist/data-table.d.ts +4 -4
  36. package/dist/data-table.js +4 -4
  37. package/dist/data-table.js.map +1 -1
  38. package/dist/date-picker.d.ts +1 -1
  39. package/dist/design-profile.d.ts.map +1 -1
  40. package/dist/design-profile.js +19 -16
  41. package/dist/design-profile.js.map +1 -1
  42. package/dist/file-picker.d.ts +1 -1
  43. package/dist/file-picker.js +1 -1
  44. package/dist/file-picker.js.map +1 -1
  45. package/dist/foundations.d.ts +18 -14
  46. package/dist/foundations.d.ts.map +1 -1
  47. package/dist/foundations.js +18 -13
  48. package/dist/foundations.js.map +1 -1
  49. package/dist/heading.d.ts +13 -13
  50. package/dist/heading.js.map +1 -1
  51. package/dist/icon-button-recipe.d.ts +23 -0
  52. package/dist/icon-button-recipe.d.ts.map +1 -1
  53. package/dist/icon-button-recipe.js +20 -1
  54. package/dist/icon-button-recipe.js.map +1 -1
  55. package/dist/number-field.d.ts +1 -1
  56. package/dist/otp-field.d.ts +1 -1
  57. package/dist/password-field.d.ts +1 -1
  58. package/dist/result.d.ts +1 -1
  59. package/dist/result.js +2 -2
  60. package/dist/result.js.map +1 -1
  61. package/dist/screen-patterns.d.ts +12 -1
  62. package/dist/screen-patterns.d.ts.map +1 -1
  63. package/dist/screen-patterns.js +12 -1
  64. package/dist/screen-patterns.js.map +1 -1
  65. package/dist/semantic-colors.d.ts +10 -0
  66. package/dist/semantic-colors.d.ts.map +1 -1
  67. package/dist/semantic-colors.js +6 -0
  68. package/dist/semantic-colors.js.map +1 -1
  69. package/dist/sheet.d.ts +7 -4
  70. package/dist/sheet.d.ts.map +1 -1
  71. package/dist/sheet.js +8 -5
  72. package/dist/sheet.js.map +1 -1
  73. package/dist/side-panel.d.ts +1 -1
  74. package/dist/side-panel.js +2 -2
  75. package/dist/side-panel.js.map +1 -1
  76. package/dist/steps.d.ts +1 -1
  77. package/dist/steps.js +1 -1
  78. package/dist/steps.js.map +1 -1
  79. package/dist/tag.d.ts +1 -1
  80. package/dist/tag.js +1 -1
  81. package/dist/tag.js.map +1 -1
  82. package/dist/timeline.d.ts +1 -1
  83. package/dist/timeline.js +1 -1
  84. package/dist/timeline.js.map +1 -1
  85. package/dist/top.d.ts +4 -4
  86. package/dist/top.d.ts.map +1 -1
  87. package/dist/top.js +3 -1
  88. package/dist/top.js.map +1 -1
  89. package/dist/version.d.ts +1 -1
  90. package/dist/version.js +1 -1
  91. package/dist/version.js.map +1 -1
  92. package/docs/activity-chip.md +5 -1
  93. package/docs/auth-screen.md +2 -0
  94. package/docs/design-profile.md +14 -10
  95. package/docs/generated/component-maturity.md +1 -1
  96. package/docs/generated/renderer-evidence.json +9 -5
  97. package/docs/generated/renderer-evidence.md +1 -1
  98. package/docs/generated/showcase-manifest.json +1 -1
  99. package/docs/heading.md +14 -9
  100. package/docs/sheet.md +2 -0
  101. package/docs/top.md +4 -3
  102. package/docs/usage/README.md +1 -1
  103. package/docs/usage/components/auth-screen-layout.md +26 -9
  104. package/docs/usage/components/bottom-cta.md +6 -2
  105. package/docs/usage/components/bottom-navigation.md +5 -1
  106. package/docs/usage/components/breadcrumb.md +5 -1
  107. package/docs/usage/components/button.md +2 -0
  108. package/docs/usage/components/chat-message.md +6 -2
  109. package/docs/usage/components/checkbox-group.md +5 -1
  110. package/docs/usage/components/collapsible.md +1 -0
  111. package/docs/usage/components/comment-thread-row.md +1 -1
  112. package/docs/usage/components/effect-surface.md +2 -2
  113. package/docs/usage/components/heading.md +14 -12
  114. package/docs/usage/components/icon-button.md +5 -0
  115. package/docs/usage/components/inline-edit.md +1 -1
  116. package/docs/usage/components/list-detail-screen.md +1 -1
  117. package/docs/usage/components/list-row.md +23 -11
  118. package/docs/usage/components/list.md +10 -5
  119. package/docs/usage/components/notice.md +5 -1
  120. package/docs/usage/components/notification-item.md +5 -1
  121. package/docs/usage/components/photo-source-sheet.md +1 -1
  122. package/docs/usage/components/repeatable-field-group.md +1 -1
  123. package/docs/usage/components/saved-items-screen.md +1 -1
  124. package/docs/usage/components/screen-layout.md +10 -6
  125. package/docs/usage/components/search-screen.md +2 -2
  126. package/docs/usage/components/section.md +5 -1
  127. package/docs/usage/components/segmented-control.md +1 -1
  128. package/docs/usage/components/sheet.md +17 -13
  129. package/docs/usage/components/skeleton.md +41 -15
  130. package/docs/usage/components/statistic.md +5 -1
  131. package/docs/usage/components/switch.md +3 -2
  132. package/docs/usage/components/table-column-settings.md +1 -1
  133. package/docs/usage/components/tag.md +5 -1
  134. package/docs/usage/components/text.md +6 -4
  135. package/docs/usage/components/timeline.md +5 -1
  136. package/docs/usage/components/toast.md +10 -1
  137. package/docs/usage/components/top-bar.md +5 -1
  138. package/docs/usage/components/top.md +1 -1
  139. package/docs/usage/components/upload-item.md +1 -1
  140. package/docs/usage/compositions/adaptive-content.md +5 -1
  141. package/docs/usage/compositions/compound-controls.md +2 -1
  142. package/docs/usage/compositions/design-profile-comparison.md +1 -1
  143. package/docs/usage/compositions/family-drawer.md +5 -3
  144. package/docs/usage/compositions/input-sheet.md +1 -1
  145. package/docs/usage/compositions/interaction-flow-apply.md +3 -3
  146. package/docs/usage/compositions/photo-source.md +3 -3
  147. package/docs/usage/compositions/selection-motion.md +5 -1
  148. package/docs/usage/compositions/toast-layout.md +4 -4
  149. package/docs/usage/compositions/visual-foundations.md +5 -4
  150. package/docs/usage/screens/common-login.md +9 -7
  151. package/docs/usage/screens/common-search.md +1 -1
  152. package/docs/usage/screens/landing.md +15 -14
  153. package/docs/usage/screens/notification-settings.md +4 -2
  154. package/docs/usage/tokens/typography.md +21 -13
  155. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { collectionItemContract, fieldFrameContract, floatingSurfaceContract, focusIndicatorContract, formSupportContract, } from "./component-contracts.js";
2
- import { backdrop, breakpoint, control, glyph, layer, largeTextThreshold, layout, fontWeight, motion, motionPreset, opacity, radius, spacing, stroke, } from "./foundations.js";
2
+ import { backdrop, breakpoint, control, glyph, layer, largeTextThreshold, layout, fontWeight, motion, motionPreset, opacity, radius, spacing, stroke, typography, } from "./foundations.js";
3
3
  import { semanticColors } from "./semantic-colors.js";
4
4
  import { tooltipDescriptorDefaults } from "./tooltip.js";
5
5
  import { bottomNavigationRecipeDefaults } from "./bottom-navigation-defaults.js";
6
- export { iconButtonRecipe, resolveIconButtonPresentation, } from "./icon-button-recipe.js";
6
+ export { iconButtonEdges, iconButtonRecipe, resolveIconButtonEdgeOverhang, resolveIconButtonPresentation, } from "./icon-button-recipe.js";
7
7
  export { counterBadgeDefaults, counterBadgeRecipe, formatCounterBadgeCount, } from "./counter-badge-recipe.js";
8
8
  export { progressRecipe, } from "./progress-recipe.js";
9
9
  export const textRecipe = {
@@ -19,14 +19,18 @@ export const textRecipe = {
19
19
  danger: semanticColors.content.danger,
20
20
  inverse: semanticColors.content.inverse,
21
21
  },
22
+ // 2026-10-17 user decisions: the strongest inline emphasis matches the title weight. strong=700 made unread
23
+ // notification titles and labels heavier than every title after titles moved to 600 ("왜 폰트가 진해"), so
24
+ // strong became 600; the second round ("굵기 좀더 줄여") moved titles 600→500 and display headings 700→600,
25
+ // so strong follows the title role (500) and equals `medium`. Semibold 600 is the key-number/selected step.
22
26
  emphasis: {
23
27
  regular: fontWeight.regular,
24
- medium: fontWeight.semibold,
25
- strong: fontWeight.bold,
28
+ medium: fontWeight.medium,
29
+ strong: typography.title.fontWeight,
26
30
  },
27
31
  /**
28
32
  * Variants whose omitted `emphasis` renders the variant's typography weight even without a design profile.
29
- * Label's 500 has no emphasis step (`medium` is 600), and the shared `regular` default drew form labels at
33
+ * Label's 500 equals the `medium` emphasis step, but the shared `regular` default drew form labels at
30
34
  * body weight (2026-10-17 user decision: label 500 on Web and Native). Other variants keep `regular`.
31
35
  */
32
36
  variantWeight: ["label"],
@@ -86,7 +90,7 @@ export const linkRecipe = {
86
90
  variants: {
87
91
  inline: {
88
92
  textVariant: "body",
89
- fontWeight: fontWeight.semibold,
93
+ fontWeight: fontWeight.medium,
90
94
  underline: "always",
91
95
  minHeight: null,
92
96
  minWidth: null,
@@ -94,7 +98,7 @@ export const linkRecipe = {
94
98
  /** 그 자체가 동작이므로 두 축 모두 최소 터치 타깃. 높이만 잡으면 짧은 라벨이 좁아진다. */
95
99
  standalone: {
96
100
  textVariant: "body",
97
- fontWeight: fontWeight.bold,
101
+ fontWeight: fontWeight.medium,
98
102
  underline: "hover",
99
103
  minHeight: control.minTouchTarget,
100
104
  minWidth: control.minTouchTarget,
@@ -218,7 +222,7 @@ export const badgeRecipe = {
218
222
  },
219
223
  radius: "full",
220
224
  borderWidth: stroke.default,
221
- fontWeight: fontWeight.bold,
225
+ fontWeight: fontWeight.medium,
222
226
  };
223
227
  /** Search is a field specialization with stable affordance and clear targets. */
224
228
  export const searchFieldRecipe = {
@@ -320,7 +324,8 @@ export const chipRecipe = {
320
324
  },
321
325
  radius: "full",
322
326
  borderWidth: stroke.default,
323
- label: { fontWeight: fontWeight.semibold, selectedFontWeight: fontWeight.bold },
327
+ // Selected keeps one step above idle (500 → 600) so the state still carries a weight cue on the lighter scale.
328
+ label: { fontWeight: fontWeight.medium, selectedFontWeight: fontWeight.semibold },
324
329
  selectionIndicator: {
325
330
  color: semanticColors.content.brand,
326
331
  glyph: "xs",
@@ -338,16 +343,36 @@ export const dividerRecipe = {
338
343
  both: spacing.md,
339
344
  },
340
345
  };
346
+ /**
347
+ * How far a ListRow surface (pressed, hover, selected, focus ring) reaches past the row text on each inline side.
348
+ * 2026-10-17 user decision: the row text sits on its container's content edge (the 16 screen gutter on a default
349
+ * screen) and only the surface bleeds outward by this amount. Rows used to put it inside as padding, so row text
350
+ * started at 16 + 8 = 24 while section titles and result headers started at 16. Containers that clip (grouped List,
351
+ * VirtualList) keep exactly this much inline room so the bleed is never cut.
352
+ */
353
+ const listRowSurfaceBleed = spacing.xs;
354
+ /** ListRow leading frame and the gap before its text column; the indented separator reuses both. */
355
+ const listRowLeadingSize = 40;
356
+ const listRowGap = spacing.sm;
341
357
  export const listRecipe = {
342
358
  slots: ["root", "item", "separator"],
343
359
  defaults: { separator: "indented" },
360
+ /**
361
+ * Separators are measured from the list's content edge, where the row text sits, and each one belongs to the row
362
+ * above it (iOS cell separator). "full" spans the row surface (content edge ± bleed). "indented" follows that row's
363
+ * text column: it starts at the text — `leadingInsetStart` (leading 40 + gap 12) when the row has a leading
364
+ * element, `insetStart` 0 when it has none — and ends on the row's trailing content edge (0).
365
+ * 2026-10-17 user report: a fixed 52 start cut the line off midway under rows without leading (text at 16, line
366
+ * from 68), and the −8 end ran past the trailing content.
367
+ */
344
368
  separators: {
345
369
  none: null,
346
- full: { insetStart: 0, insetEnd: 0 },
347
- // Starts where the row text starts: row padding (xs, every ListRow density) + leading 40 + gap sm. Without the row
348
- // padding the line began 8px before the text (2026-10-17 review E5).
349
- indented: { insetStart: spacing.xs + 40 + spacing.sm, insetEnd: 0 },
370
+ full: { insetStart: -listRowSurfaceBleed, leadingInsetStart: -listRowSurfaceBleed, insetEnd: -listRowSurfaceBleed },
371
+ indented: { insetStart: 0, leadingInsetStart: listRowLeadingSize + listRowGap, insetEnd: 0 },
350
372
  },
373
+ // A grouped list paints its own rounded surface and clips to it, so it leaves the row bleed as inline padding:
374
+ // rows keep their full surface inside the radius and the text stays 8 inside the group edge as before.
375
+ grouped: { paddingInline: listRowSurfaceBleed },
351
376
  background: null,
352
377
  };
353
378
  export const listRowRecipe = {
@@ -359,13 +384,13 @@ export const listRowRecipe = {
359
384
  compact: {
360
385
  oneLineMinHeight: control.minTouchTarget,
361
386
  twoLineMinHeight: 60,
362
- paddingHorizontal: spacing.xs,
387
+ paddingHorizontal: listRowSurfaceBleed,
363
388
  paddingVertical: spacing.xxs,
364
389
  },
365
390
  comfortable: {
366
391
  oneLineMinHeight: layout.rowHeight.singleLine,
367
392
  twoLineMinHeight: layout.rowHeight.twoLine,
368
- paddingHorizontal: spacing.xs,
393
+ paddingHorizontal: listRowSurfaceBleed,
369
394
  paddingVertical: spacing.xs,
370
395
  },
371
396
  /*
@@ -377,27 +402,33 @@ export const listRowRecipe = {
377
402
  relaxed: {
378
403
  oneLineMinHeight: layout.rowHeight.singleLine + spacing.xs,
379
404
  twoLineMinHeight: layout.rowHeight.twoLine + spacing.xs,
380
- paddingHorizontal: spacing.xs,
405
+ paddingHorizontal: listRowSurfaceBleed,
381
406
  paddingVertical: spacing.sm,
382
407
  },
383
408
  spacious: {
384
409
  oneLineMinHeight: layout.rowHeight.singleLine + spacing.md,
385
410
  twoLineMinHeight: layout.rowHeight.twoLine + spacing.md,
386
- paddingHorizontal: spacing.xs,
411
+ paddingHorizontal: listRowSurfaceBleed,
387
412
  paddingVertical: spacing.md,
388
413
  },
389
414
  },
390
415
  // Loading uses the same row/line boxes as content; only the copy width is illustrative.
391
416
  loading: { titleWidthPercent: 60, descriptionWidthPercent: 40, trailingWidth: spacing.xxl },
392
- gap: spacing.sm,
393
- leadingSize: 40,
417
+ /**
418
+ * Inline placement (2026-10-17 user decision, see `listRowSurfaceBleed`): the content box sits on the container's
419
+ * content edge and the surface bleeds outward by `paddingHorizontal` — renderers draw the row with that padding and
420
+ * the same negative inline margin, so pressed/selected fills keep 8 of room beyond the text.
421
+ */
422
+ surfaceBleed: listRowSurfaceBleed,
423
+ gap: listRowGap,
424
+ leadingSize: listRowLeadingSize,
394
425
  /**
395
426
  * Frame geometry for the leading slot. The size was already declared here but
396
427
  * no renderer bound it, so every consumer re-derived the avatar box (radius,
397
428
  * clipping, centering) in product styles.
398
429
  */
399
430
  leadingShapes: { square: null, circle: "full" },
400
- title: { color: semanticColors.content.body, textVariant: "bodyLarge", fontWeight: fontWeight.bold },
431
+ title: { color: semanticColors.content.body, textVariant: "bodyLarge", fontWeight: typography.title.fontWeight },
401
432
  description: { color: semanticColors.content.secondary, textVariant: "body" },
402
433
  trailing: {
403
434
  textColor: semanticColors.content.secondary,
@@ -420,7 +451,7 @@ export const accordionRecipe = {
420
451
  },
421
452
  paddingHorizontal: spacing.xs,
422
453
  gap: spacing.sm,
423
- title: { color: semanticColors.content.body, textVariant: "bodyLarge", fontWeight: fontWeight.bold },
454
+ title: { color: semanticColors.content.body, textVariant: "bodyLarge", fontWeight: typography.title.fontWeight },
424
455
  indicator: { color: semanticColors.content.secondary, glyph: "sm" },
425
456
  panel: {
426
457
  color: semanticColors.content.body,
@@ -560,7 +591,7 @@ export const selectRecipe = {
560
591
  paddingVertical: spacing.xs,
561
592
  },
562
593
  optionLeading: { color: semanticColors.content.secondary, glyph: "sm" },
563
- optionLabel: { fontWeight: fontWeight.semibold, selectedFontWeight: fontWeight.bold },
594
+ optionLabel: { fontWeight: fontWeight.medium, selectedFontWeight: fontWeight.semibold },
564
595
  selectionIndicator: { color: semanticColors.border.focus, glyph: "sm" },
565
596
  stateMessage: {
566
597
  color: semanticColors.content.secondary,
@@ -631,6 +662,8 @@ export const comboboxRecipe = {
631
662
  states: selectRecipe.states,
632
663
  transition: selectRecipe.transition,
633
664
  };
665
+ /** Block-side inset of the connected track: `container.padding` + `container.borderWidth`. */
666
+ const segmentedTrackInset = spacing.xxs + stroke.subtle;
634
667
  export const segmentedControlRecipe = {
635
668
  // Text navigation reuses radio selection, without cloning a product-only tab engine.
636
669
  text: { gap: spacing.md, indicatorWidth: 16, indicatorHeight: 2, indicatorRadius: radius.full,
@@ -667,16 +700,25 @@ export const segmentedControlRecipe = {
667
700
  padding: spacing.xxs,
668
701
  gap: spacing.xxs,
669
702
  },
703
+ /**
704
+ * `height` is the connected control's outer height (track border + padding included) and matches the Button /
705
+ * IconButton / Chip height of the same size step, so a segmented control and a chip in one row line up.
706
+ * `minHeight` is the option inside the track (height − 2 × (`container.padding` + `container.borderWidth`)) and `hitSlop` extends each
707
+ * option vertically to the 44 touch target. Before 2026-10-17 `minHeight` was the option itself, so the track
708
+ * drew 46/54 next to 36/44 chips and buttons (DiscoveryGallery sort row, user request "높이 맞춰").
709
+ */
670
710
  sizes: {
671
711
  small: {
672
- minHeight: 36,
673
- hitSlop: 4,
712
+ height: control.buttonHeight.small,
713
+ minHeight: control.buttonHeight.small - 2 * segmentedTrackInset,
714
+ hitSlop: (control.minTouchTarget - (control.buttonHeight.small - 2 * segmentedTrackInset)) / 2,
674
715
  paddingHorizontal: spacing.sm,
675
716
  textVariant: "label",
676
717
  },
677
718
  medium: {
678
- minHeight: control.minTouchTarget,
679
- hitSlop: 0,
719
+ height: control.buttonHeight.medium,
720
+ minHeight: control.buttonHeight.medium - 2 * segmentedTrackInset,
721
+ hitSlop: (control.minTouchTarget - (control.buttonHeight.medium - 2 * segmentedTrackInset)) / 2,
680
722
  paddingHorizontal: spacing.md,
681
723
  textVariant: "body",
682
724
  },
@@ -685,7 +727,7 @@ export const segmentedControlRecipe = {
685
727
  radius: "md",
686
728
  gap: spacing.xxs,
687
729
  idleContent: semanticColors.content.secondary,
688
- fontWeight: fontWeight.semibold,
730
+ fontWeight: fontWeight.medium,
689
731
  selectedBackground: semanticColors.canvas,
690
732
  selectedContent: semanticColors.content.brand,
691
733
  /**
@@ -695,7 +737,7 @@ export const segmentedControlRecipe = {
695
737
  */
696
738
  selectedBorder: semanticColors.border.focus,
697
739
  selectedBorderWidth: stroke.strong,
698
- selectedFontWeight: fontWeight.bold,
740
+ selectedFontWeight: fontWeight.semibold,
699
741
  focusRing: semanticColors.border.focus,
700
742
  pressedOpacity: opacity.pressed,
701
743
  disabledOpacity: opacity.disabled,
@@ -756,11 +798,16 @@ export const switchRecipe = {
756
798
  pressedOpacity: opacity.pressed,
757
799
  },
758
800
  rowMinHeight: control.minTouchTarget,
759
- // A switch with a description is a two-line row like any other list row,
760
- // and a product author found that letting content decide the height gave
761
- // neighbouring rows different heights — this pins it to the same value as
762
- // `listRowRecipe.density.comfortable.twoLineMinHeight`.
763
- rowTwoLineMinHeight: 68,
801
+ // A described switch is a two-line row: the copy (body 20 + gap 4 + caption 16 = 40) with the
802
+ // comfortable list-row vertical padding (8) on both sides. The padding is drawn by the row itself
803
+ // so it survives larger text, and the minimum keeps neighbouring rows equal (a product author found
804
+ // content-only height gave them different heights). It used to borrow ListRow's 68, which is sized
805
+ // for a bodyLarge title + body description; on the smaller switch copy that left 14 above and below,
806
+ // and stacked rows read as widely spaced (2026-10-17 user report, notification settings: 84 pitch
807
+ // for 40 of copy, together with a Stack gap the rows must not have).
808
+ rowPaddingVertical: listRowRecipe.density.comfortable.paddingVertical,
809
+ rowTwoLineMinHeight: typography.body.lineHeight + spacing.xxs + typography.caption.lineHeight
810
+ + 2 * listRowRecipe.density.comfortable.paddingVertical,
764
811
  };
765
812
  export const selectionControlRecipe = {
766
813
  slots: [
@@ -836,8 +883,8 @@ export const selectionControlRecipe = {
836
883
  },
837
884
  label: {
838
885
  color: semanticColors.content.body,
839
- fontWeight: fontWeight.semibold,
840
- checkedFontWeight: fontWeight.bold,
886
+ fontWeight: fontWeight.medium,
887
+ checkedFontWeight: fontWeight.semibold,
841
888
  },
842
889
  leading: { size: "md", color: semanticColors.content.secondary },
843
890
  description: { color: semanticColors.content.secondary },
@@ -1001,8 +1048,8 @@ export const bottomNavigationRecipe = {
1001
1048
  },
1002
1049
  },
1003
1050
  label: {
1004
- fontWeight: fontWeight.semibold,
1005
- selectedFontWeight: fontWeight.bold,
1051
+ fontWeight: fontWeight.medium,
1052
+ selectedFontWeight: fontWeight.semibold,
1006
1053
  textAlign: "center",
1007
1054
  wrap: true,
1008
1055
  fixedLines: null,
@@ -1073,7 +1120,7 @@ export const tabsRecipe = {
1073
1120
  },
1074
1121
  gap: spacing.xs,
1075
1122
  icon: { glyph: "xs" },
1076
- label: { fontWeight: fontWeight.semibold, selectedFontWeight: fontWeight.bold },
1123
+ label: { fontWeight: fontWeight.medium, selectedFontWeight: fontWeight.semibold },
1077
1124
  layouts: {
1078
1125
  content: { fitted: false },
1079
1126
  fitted: { fitted: true },
@@ -1112,21 +1159,28 @@ export const noticeRecipe = {
1112
1159
  contentGap: spacing.xxs,
1113
1160
  iconSize: "sm",
1114
1161
  borderWidth: stroke.default,
1115
- title: { textVariant: "body", fontWeight: fontWeight.bold },
1162
+ title: { textVariant: "body", fontWeight: typography.title.fontWeight },
1116
1163
  description: { color: semanticColors.content.body, textVariant: "body" },
1117
1164
  };
1118
1165
  export const skeletonRecipe = {
1119
- slots: ["root"],
1166
+ slots: ["root", "line"],
1120
1167
  // animated는 0.9.12까지 false였다. 정지한 블록은 로딩이 아니라 깨진 화면으로
1121
1168
  // 읽힌다. 전환 근거와 소비 측 migration은
1122
1169
  // .changeset/skeleton-pulse-by-default.md에 있다.
1123
- defaults: { shape: "block", animated: true },
1124
- background: semanticColors.canvas,
1170
+ defaults: { shape: "block", animated: true, lines: 1 },
1171
+ // 2026-10-17 사용자 요청: canvas 채움 + 1px 테두리는 흰 배경에서 거의 보이지 않아 로딩 모양을 알 수 없었다.
1172
+ // 2026-09-29 "넓은 회색 면은 canvas" 방향의 예외로, 자리표시는 채운 면이어야 모양이 읽힌다(skeleton.md 근거).
1173
+ background: semanticColors.surface.placeholder,
1174
+ // `aspectRatio`가 있으면 높이 대신 폭에 비례한다(media). `button`은 medium 버튼 높이·모서리와 같다.
1125
1175
  shapes: {
1126
- block: { radius: "md", defaultHeight: spacing.xxl },
1127
- text: { radius: "sm", defaultHeight: spacing.md },
1128
- circle: { radius: "full", defaultHeight: glyph.xxl },
1129
- },
1176
+ block: { radius: "md", defaultHeight: spacing.xxl, aspectRatio: null },
1177
+ text: { radius: "sm", defaultHeight: spacing.md, aspectRatio: null },
1178
+ circle: { radius: "full", defaultHeight: glyph.xxl, aspectRatio: null },
1179
+ media: { radius: "md", defaultHeight: null, aspectRatio: 16 / 9 },
1180
+ button: { radius: "md", defaultHeight: control.buttonHeight.medium, aspectRatio: null },
1181
+ },
1182
+ // `lines` > 1 (text 전용): 줄 사이 간격과 짧게 끝나는 마지막 줄. 16 막대 + 8 간격 = 본문 큰 글자 줄 높이 24.
1183
+ lines: { gap: spacing.xs, lastLineWidthPercent: 60 },
1130
1184
  animation: {
1131
1185
  duration: motion.slow * 4,
1132
1186
  easing: "standard",
@@ -1145,7 +1199,7 @@ export const emptyStateRecipe = {
1145
1199
  paddingHorizontal: spacing.xl,
1146
1200
  gap: spacing.xs,
1147
1201
  icon: { size: "lg", color: semanticColors.content.decorative },
1148
- title: { textVariant: "body", color: semanticColors.content.primary, fontWeight: fontWeight.semibold },
1202
+ title: { textVariant: "body", color: semanticColors.content.primary, fontWeight: typography.title.fontWeight },
1149
1203
  description: { textVariant: "label", color: semanticColors.content.secondary },
1150
1204
  };
1151
1205
  export const spinnerRecipe = {
@@ -1197,7 +1251,7 @@ export const loadMoreRecipe = {
1197
1251
  radius: "md",
1198
1252
  color: semanticColors.content.brand,
1199
1253
  textVariant: "label",
1200
- fontWeight: fontWeight.bold,
1254
+ fontWeight: fontWeight.medium,
1201
1255
  },
1202
1256
  spinner: { size: "small", tone: "brand" },
1203
1257
  states: {
@@ -1219,7 +1273,7 @@ export const avatarRecipe = {
1219
1273
  background: semanticColors.canvas,
1220
1274
  content: semanticColors.content.secondary,
1221
1275
  border: semanticColors.border.default,
1222
- /** Initials are bold; their size follows the avatar (Web `.hjm-avatar[data-size]` steps). */
1276
+ /** Initials use the strong emphasis step; their size follows the avatar (Web `.hjm-avatar[data-size]` steps). */
1223
1277
  initialsWeight: "strong",
1224
1278
  };
1225
1279
  /**
@@ -1277,18 +1331,20 @@ export const statisticRecipe = {
1277
1331
  },
1278
1332
  label: {
1279
1333
  color: semanticColors.content.secondary,
1280
- fontWeight: fontWeight.semibold,
1334
+ fontWeight: fontWeight.medium,
1281
1335
  },
1282
1336
  value: {
1283
1337
  color: semanticColors.content.primary,
1284
- fontWeight: fontWeight.heavy,
1338
+ // The key number keeps the semibold emphasis step (600, 2026-10-17 lighter scale) at the `heading` size.
1339
+ // It used to borrow the display heading weight, which was removed with the display tier the same day.
1340
+ fontWeight: fontWeight.semibold,
1285
1341
  numericVariant: "tabular",
1286
1342
  maxLines: null,
1287
1343
  },
1288
1344
  affix: {
1289
1345
  color: semanticColors.content.body,
1290
1346
  textVariant: "body",
1291
- fontWeight: fontWeight.semibold,
1347
+ fontWeight: fontWeight.medium,
1292
1348
  },
1293
1349
  hint: {
1294
1350
  color: semanticColors.content.secondary,
@@ -1296,7 +1352,7 @@ export const statisticRecipe = {
1296
1352
  },
1297
1353
  trend: {
1298
1354
  textVariant: "caption",
1299
- fontWeight: fontWeight.bold,
1355
+ fontWeight: fontWeight.medium,
1300
1356
  gap: spacing.xxs,
1301
1357
  marks: {
1302
1358
  up: "trendUp",
@@ -1361,12 +1417,8 @@ export const sheetRecipe = {
1361
1417
  showWhen: "swipe-dismiss-enabled",
1362
1418
  },
1363
1419
  header: { minHeight: control.minTouchTarget, gap: spacing.sm },
1364
- title: {
1365
- color: semanticColors.content.primary,
1366
- textVariant: "title",
1367
- fontWeight: fontWeight.bold,
1368
- gap: spacing.xxs,
1369
- },
1420
+ // No visual `title` region: sheets render the title only as the accessible name (2026-10-17 user decision).
1421
+ // The "title" slot stays because the title is still the dialog's name.
1370
1422
  body: {
1371
1423
  color: semanticColors.content.body,
1372
1424
  textVariant: "body",
@@ -1450,7 +1502,7 @@ export const alertDialogRecipe = {
1450
1502
  title: {
1451
1503
  color: semanticColors.content.primary,
1452
1504
  textVariant: "title",
1453
- fontWeight: fontWeight.bold,
1505
+ fontWeight: typography.title.fontWeight,
1454
1506
  },
1455
1507
  description: {
1456
1508
  color: semanticColors.content.secondary,
@@ -1467,6 +1519,12 @@ export const alertDialogRecipe = {
1467
1519
  },
1468
1520
  transition: dialogRecipe.transition,
1469
1521
  };
1522
+ // First-line anchor (2026-10-17 user: "기본 토스트 좌우랑 글자 라인이 안맞는데"). The badge box is the one
1523
+ // line every leading column, first text line and close glyph centre on; the close keeps its 44 touch target but
1524
+ // overhangs so its glyph, not its hit area, sets the visible inset. Derived here so both renderers read numbers.
1525
+ const toastBadgeDiameter = 32;
1526
+ const toastCloseDiameter = control.minTouchTarget;
1527
+ const toastCloseGlyph = "xs";
1470
1528
  export const toastRecipe = {
1471
1529
  slots: [
1472
1530
  "viewport",
@@ -1542,13 +1600,20 @@ export const toastRecipe = {
1542
1600
  radius: "xl",
1543
1601
  shadow: floatingSurfaceContract.shadow,
1544
1602
  minHeight: layout.rowHeight.singleLine,
1545
- padding: spacing.md,
1603
+ // block sm + anchor 32 + sm = the 56 single-line row; inline md on both edges so the badge's start inset
1604
+ // equals the close glyph's end inset (Web had sm/xs/sm/md, Native md on all sides — 2026-10-17 measurement).
1605
+ padding: { block: spacing.sm, inline: spacing.md },
1546
1606
  gap: spacing.sm,
1547
1607
  maxWidth: 420,
1548
1608
  },
1609
+ // First text line (title, else description) is centred on this box: content insets its first line by
1610
+ // (blockSize − first-line line-height) / 2. Multi-line copy grows downward; the lead and close stay beside
1611
+ // the first line instead of centring on the whole block. Product media (44) start-aligns to the same top and
1612
+ // is not the anchor: centring the title on the media pushed the description below the avatar.
1613
+ anchor: { blockSize: toastBadgeDiameter },
1549
1614
  // Retired in 1.10.0 (width 0): the icon badge carries tone. Kept so the slot contract stays additive.
1550
1615
  toneMark: { width: 0, radius: "full" },
1551
- icon: { glyph: "sm", badgeDiameter: 32, badgeRadius: "full" },
1616
+ icon: { glyph: "sm", badgeDiameter: toastBadgeDiameter, badgeRadius: "full" },
1552
1617
  // A product media (mascot, avatar) replaces the tone badge in the same leading column. 44 keeps a
1553
1618
  // character legible; it is decorative, so the tone still lives in `data-tone` and the copy.
1554
1619
  media: { size: 44 },
@@ -1556,7 +1621,7 @@ export const toastRecipe = {
1556
1621
  title: {
1557
1622
  color: semanticColors.content.primary,
1558
1623
  textVariant: "body",
1559
- fontWeight: fontWeight.bold,
1624
+ fontWeight: typography.title.fontWeight,
1560
1625
  },
1561
1626
  // Secondary copy so the title leads; body-on-body read as one grey block.
1562
1627
  description: {
@@ -1564,7 +1629,7 @@ export const toastRecipe = {
1564
1629
  textVariant: "body",
1565
1630
  },
1566
1631
  // Step dots of a running task: done and current are brand, upcoming is the divider colour; the current
1567
- // label is bold. Labels sit beside each dot so the row reads without colour alone (WCAG 1.4.1).
1632
+ // label is emphasized. Labels sit beside each dot so the row reads without colour alone (WCAG 1.4.1).
1568
1633
  progress: {
1569
1634
  gap: spacing.sm,
1570
1635
  dotSize: 6,
@@ -1584,14 +1649,18 @@ export const toastRecipe = {
1584
1649
  radius: "full",
1585
1650
  align: "start",
1586
1651
  textVariant: "body",
1587
- fontWeight: fontWeight.bold,
1652
+ fontWeight: fontWeight.medium,
1588
1653
  minHeight: control.minTouchTarget,
1589
1654
  paddingHorizontal: spacing.md,
1590
1655
  },
1591
1656
  close: {
1592
1657
  color: semanticColors.content.secondary,
1593
- diameter: control.minTouchTarget,
1594
- glyph: "xs",
1658
+ diameter: toastCloseDiameter,
1659
+ glyph: toastCloseGlyph,
1660
+ // Negative block margin on both sides: the 44 target centres on the anchor line without stretching the row.
1661
+ blockOverhang: (toastCloseDiameter - toastBadgeDiameter) / 2,
1662
+ // Negative inline-end margin: the glyph box ends at the md padding edge, mirroring the badge's start inset.
1663
+ inlineOverhang: (toastCloseDiameter - glyph[toastCloseGlyph]) / 2,
1595
1664
  },
1596
1665
  states: {
1597
1666
  pressedOpacity: opacity.pressed,
@@ -1649,7 +1718,7 @@ export const topBarRecipe = {
1649
1718
  paddingHorizontal: spacing.md,
1650
1719
  gap: spacing.xs,
1651
1720
  largeTextThreshold,
1652
- title: { textVariant: "bodyLarge", color: semanticColors.content.primary, fontWeight: fontWeight.bold },
1721
+ title: { textVariant: "bodyLarge", color: semanticColors.content.primary, fontWeight: typography.title.fontWeight },
1653
1722
  titleAction: {
1654
1723
  minHeight: control.minTouchTarget,
1655
1724
  minWidth: control.minTouchTarget,
@@ -1710,7 +1779,7 @@ export const sectionRecipe = {
1710
1779
  gap: spacing.xs,
1711
1780
  headerGap: spacing.sm,
1712
1781
  copyGap: spacing.xxs,
1713
- title: { textVariant: "title", color: semanticColors.content.body, fontWeight: fontWeight.bold },
1782
+ title: { textVariant: "title", color: semanticColors.content.body, fontWeight: typography.title.fontWeight },
1714
1783
  description: { textVariant: "caption", color: semanticColors.content.secondary },
1715
1784
  };
1716
1785
  //# sourceMappingURL=component-recipes.js.map