@ncds/ui-admin 1.8.21-alpha.1 → 1.8.21-alpha.10

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 (125) hide show
  1. package/dist/cjs/assets/scripts/notification/FullWidthNotification.js +17 -6
  2. package/dist/cjs/assets/scripts/notification/MessageNotification.js +10 -48
  3. package/dist/cjs/assets/scripts/notification/const/classNames.js +3 -0
  4. package/dist/cjs/assets/scripts/notification/const/index.js +1 -1
  5. package/dist/cjs/assets/scripts/notification/const/sizes.js +4 -2
  6. package/dist/cjs/assets/scripts/notification/messageTemplate.js +106 -0
  7. package/dist/cjs/assets/scripts/notification/utils.js +40 -4
  8. package/dist/cjs/constant/notification.js +21 -0
  9. package/dist/cjs/src/components/forms-and-input/input-base/InputBase.js +30 -18
  10. package/dist/cjs/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +116 -0
  11. package/dist/cjs/src/components/forms-and-input/password-input/PasswordInput.js +3 -3
  12. package/dist/cjs/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.js +59 -0
  13. package/dist/cjs/src/components/index.js +11 -0
  14. package/dist/cjs/src/components/layout/ncua-scope/NcuaScope.js +8 -2
  15. package/dist/cjs/src/components/navigation/pagination/Pagination.js +3 -1
  16. package/dist/cjs/src/components/navigation/pagination/__tests__/Pagination.test.js +74 -0
  17. package/dist/cjs/src/components/overlays/notification/FullWidthNotification.js +3 -1
  18. package/dist/cjs/src/components/overlays/notification/MessageNotification.js +14 -54
  19. package/dist/cjs/src/components/overlays/notification/MessageNotificationParts.js +92 -0
  20. package/dist/cjs/src/components/overlays/notification/__tests__/MessageNotification.test.js +246 -0
  21. package/dist/cjs/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +209 -0
  22. package/dist/cjs/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +259 -0
  23. package/dist/cjs/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.js +70 -0
  24. package/dist/cjs/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +623 -0
  25. package/dist/cjs/src/components/overlays/postcode-search-modal/index.js +16 -0
  26. package/dist/cjs/src/generated/scoped-css.js +1 -1
  27. package/dist/esm/assets/scripts/notification/FullWidthNotification.js +17 -6
  28. package/dist/esm/assets/scripts/notification/MessageNotification.js +12 -50
  29. package/dist/esm/assets/scripts/notification/const/classNames.js +3 -0
  30. package/dist/esm/assets/scripts/notification/const/index.js +3 -2
  31. package/dist/esm/assets/scripts/notification/const/sizes.js +4 -2
  32. package/dist/esm/assets/scripts/notification/messageTemplate.js +100 -0
  33. package/dist/esm/assets/scripts/notification/utils.js +39 -4
  34. package/dist/esm/constant/notification.js +15 -0
  35. package/dist/esm/src/components/forms-and-input/input-base/InputBase.js +30 -18
  36. package/dist/esm/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +116 -0
  37. package/dist/esm/src/components/forms-and-input/password-input/PasswordInput.js +3 -3
  38. package/dist/esm/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.js +56 -0
  39. package/dist/esm/src/components/index.js +1 -0
  40. package/dist/esm/src/components/layout/ncua-scope/NcuaScope.js +8 -2
  41. package/dist/esm/src/components/navigation/pagination/Pagination.js +3 -1
  42. package/dist/esm/src/components/navigation/pagination/__tests__/Pagination.test.js +74 -0
  43. package/dist/esm/src/components/overlays/notification/FullWidthNotification.js +3 -1
  44. package/dist/esm/src/components/overlays/notification/MessageNotification.js +15 -55
  45. package/dist/esm/src/components/overlays/notification/MessageNotificationParts.js +85 -0
  46. package/dist/esm/src/components/overlays/notification/__tests__/MessageNotification.test.js +243 -0
  47. package/dist/esm/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +206 -0
  48. package/dist/esm/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +252 -0
  49. package/dist/esm/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.js +63 -0
  50. package/dist/esm/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +620 -0
  51. package/dist/esm/src/components/overlays/postcode-search-modal/index.js +1 -0
  52. package/dist/esm/src/generated/scoped-css.js +1 -1
  53. package/dist/temp/assets/scripts/notification/FullWidthNotification.d.ts +6 -0
  54. package/dist/temp/assets/scripts/notification/FullWidthNotification.js +14 -5
  55. package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +0 -3
  56. package/dist/temp/assets/scripts/notification/MessageNotification.js +9 -44
  57. package/dist/temp/assets/scripts/notification/const/classNames.d.ts +1 -0
  58. package/dist/temp/assets/scripts/notification/const/classNames.js +3 -0
  59. package/dist/temp/assets/scripts/notification/const/index.d.ts +3 -3
  60. package/dist/temp/assets/scripts/notification/const/index.js +3 -2
  61. package/dist/temp/assets/scripts/notification/const/sizes.d.ts +2 -2
  62. package/dist/temp/assets/scripts/notification/const/sizes.js +4 -2
  63. package/dist/temp/assets/scripts/notification/const/types.d.ts +30 -2
  64. package/dist/temp/assets/scripts/notification/messageTemplate.d.ts +15 -0
  65. package/dist/temp/assets/scripts/notification/messageTemplate.js +79 -0
  66. package/dist/temp/assets/scripts/notification/utils.d.ts +19 -2
  67. package/dist/temp/assets/scripts/notification/utils.js +39 -5
  68. package/dist/temp/constant/notification.d.ts +15 -0
  69. package/dist/temp/constant/notification.js +15 -0
  70. package/dist/temp/src/components/forms-and-input/input-base/InputBase.d.ts +1 -1
  71. package/dist/temp/src/components/forms-and-input/input-base/InputBase.js +26 -14
  72. package/dist/temp/src/components/forms-and-input/input-base/__tests__/InputBase.test.js +79 -0
  73. package/dist/temp/src/components/forms-and-input/password-input/PasswordInput.js +2 -2
  74. package/dist/temp/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.d.ts +1 -0
  75. package/dist/temp/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.js +48 -0
  76. package/dist/temp/src/components/index.d.ts +1 -0
  77. package/dist/temp/src/components/index.js +1 -0
  78. package/dist/temp/src/components/layout/ncua-scope/NcuaScope.js +5 -1
  79. package/dist/temp/src/components/navigation/pagination/Pagination.js +3 -1
  80. package/dist/temp/src/components/navigation/pagination/__tests__/Pagination.test.js +74 -0
  81. package/dist/temp/src/components/overlays/notification/FullWidthNotification.d.ts +6 -0
  82. package/dist/temp/src/components/overlays/notification/FullWidthNotification.js +3 -2
  83. package/dist/temp/src/components/overlays/notification/MessageNotification.d.ts +6 -0
  84. package/dist/temp/src/components/overlays/notification/MessageNotification.js +4 -13
  85. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.d.ts +28 -0
  86. package/dist/temp/src/components/overlays/notification/MessageNotificationParts.js +28 -0
  87. package/dist/temp/src/components/overlays/notification/Notification.d.ts +11 -2
  88. package/dist/temp/src/components/overlays/notification/__tests__/MessageNotification.test.d.ts +1 -0
  89. package/dist/temp/src/components/overlays/notification/__tests__/MessageNotification.test.js +190 -0
  90. package/dist/temp/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.d.ts +1 -0
  91. package/dist/temp/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.js +160 -0
  92. package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.d.ts +45 -0
  93. package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +125 -0
  94. package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.d.ts +16 -0
  95. package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.js +24 -0
  96. package/dist/temp/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.d.ts +1 -0
  97. package/dist/temp/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +503 -0
  98. package/dist/temp/src/components/overlays/postcode-search-modal/index.d.ts +1 -0
  99. package/dist/temp/src/components/overlays/postcode-search-modal/index.js +1 -0
  100. package/dist/temp/src/generated/scoped-css.js +1 -1
  101. package/dist/types/assets/scripts/notification/FullWidthNotification.d.ts +6 -0
  102. package/dist/types/assets/scripts/notification/MessageNotification.d.ts +0 -3
  103. package/dist/types/assets/scripts/notification/const/classNames.d.ts +1 -0
  104. package/dist/types/assets/scripts/notification/const/index.d.ts +3 -3
  105. package/dist/types/assets/scripts/notification/const/sizes.d.ts +2 -2
  106. package/dist/types/assets/scripts/notification/const/types.d.ts +30 -2
  107. package/dist/types/assets/scripts/notification/messageTemplate.d.ts +15 -0
  108. package/dist/types/assets/scripts/notification/utils.d.ts +19 -2
  109. package/dist/types/constant/notification.d.ts +15 -0
  110. package/dist/types/src/components/forms-and-input/input-base/InputBase.d.ts +1 -1
  111. package/dist/types/src/components/forms-and-input/password-input/__tests__/PasswordInput.test.d.ts +1 -0
  112. package/dist/types/src/components/index.d.ts +1 -0
  113. package/dist/types/src/components/overlays/notification/FullWidthNotification.d.ts +6 -0
  114. package/dist/types/src/components/overlays/notification/MessageNotification.d.ts +6 -0
  115. package/dist/types/src/components/overlays/notification/MessageNotificationParts.d.ts +28 -0
  116. package/dist/types/src/components/overlays/notification/Notification.d.ts +11 -2
  117. package/dist/types/src/components/overlays/notification/__tests__/MessageNotification.test.d.ts +1 -0
  118. package/dist/types/src/components/overlays/notification/__tests__/hidePermanentlyLabel.test.d.ts +1 -0
  119. package/dist/types/src/components/overlays/postcode-search-modal/PostcodeSearchModal.d.ts +45 -0
  120. package/dist/types/src/components/overlays/postcode-search-modal/PostcodeSearchModalTip.d.ts +16 -0
  121. package/dist/types/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.d.ts +1 -0
  122. package/dist/types/src/components/overlays/postcode-search-modal/index.d.ts +1 -0
  123. package/dist/ui-admin/assets/styles/style.css +280 -35
  124. package/dist/ui-admin/assets/styles/style.scoped.css +280 -35
  125. package/package.json +1 -1
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.FullWidthNotification = void 0;
7
+ var _notification = require("../../../constant/notification");
7
8
  var _sanitize = require("../utils/sanitize");
8
9
  var _const = require("./const");
9
10
  var _utils = require("./utils");
@@ -30,7 +31,8 @@ class FullWidthNotification {
30
31
  actions,
31
32
  onClose,
32
33
  supportTextLink,
33
- onHidePermanently
34
+ onHidePermanently,
35
+ hidePermanentlyLabel
34
36
  } = this.options;
35
37
  const wrapper = (0, _utils.createWrapperElement)(_const.CLASS_NAMES.FULL_WIDTH.BASE, color, className);
36
38
  const iconHtml = _const.FULL_WIDTH_ICON_MAP[color](_const.FULL_WIDTH_SIZES.ICON);
@@ -41,7 +43,8 @@ class FullWidthNotification {
41
43
  actions,
42
44
  onClose,
43
45
  supportTextLink,
44
- onHidePermanently
46
+ onHidePermanently,
47
+ hidePermanentlyLabel
45
48
  }));
46
49
  return wrapper;
47
50
  }
@@ -53,7 +56,8 @@ class FullWidthNotification {
53
56
  actions,
54
57
  onClose,
55
58
  supportTextLink,
56
- onHidePermanently
59
+ onHidePermanently,
60
+ hidePermanentlyLabel
57
61
  } = params;
58
62
  return `
59
63
  <div class="${_const.CLASS_NAMES.FULL_WIDTH.CONTAINER}">
@@ -67,18 +71,25 @@ class FullWidthNotification {
67
71
  </div>
68
72
  <div class="${_const.CLASS_NAMES.FULL_WIDTH.ACTIONS_CONTAINER}">
69
73
  ${actions && actions.length > 0 ? (0, _utils.renderActions)(actions, _const.CLASS_NAMES.FULL_WIDTH.ACTIONS) : ''}
70
- ${this.renderHidePermanentlyButton(onHidePermanently)}
74
+ ${this.renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
71
75
  ${this.renderCloseButton(onClose)}
72
76
  </div>
73
77
  </div>
74
78
  </div>
75
79
  `;
76
80
  }
77
- renderHidePermanentlyButton(onHidePermanently) {
81
+ /**
82
+ * 다시보지 않기 버튼.
83
+ *
84
+ * `label`은 이 함수의 반환값이 `wrapper.innerHTML`에 들어가기 전에 `sanitizeHtml`로
85
+ * 처리되므로 별도 이스케이프하지 않는다 — `renderSupportingText`와 같은 정책이다.
86
+ */
87
+ renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
78
88
  if (!onHidePermanently) return '';
89
+ const label = hidePermanentlyLabel ?? _notification.HIDE_PERMANENTLY_LABEL.fullWidth;
79
90
  return `
80
91
  <button type="button" class="ncua-notification__action-button ncua-notification__action-button--text ncua-full-width-notification__link" data-hide-permanently="true">
81
- 다시보지 않기
92
+ ${label}
82
93
  </button>
83
94
  `;
84
95
  }
@@ -7,6 +7,7 @@ exports.MessageNotification = void 0;
7
7
  var _featuredIcon = require("../featuredIcon");
8
8
  var _sanitize = require("../utils/sanitize");
9
9
  var _const = require("./const");
10
+ var _messageTemplate = require("./messageTemplate");
10
11
  var _utils = require("./utils");
11
12
  class MessageNotification {
12
13
  constructor(options) {
@@ -30,7 +31,8 @@ class MessageNotification {
30
31
  className,
31
32
  actions,
32
33
  onClose,
33
- onHidePermanently
34
+ onHidePermanently,
35
+ hidePermanentlyLabel
34
36
  } = this.options;
35
37
  // message 타입은 neutral, error, warning, success 4가지 색상만 지원
36
38
  let actualColor = color;
@@ -47,16 +49,20 @@ class MessageNotification {
47
49
  svgString: iconSvg,
48
50
  theme: 'light-circle',
49
51
  color: actualColor,
50
- size: _const.MESSAGE_SIZES.FEATURED_ICON
52
+ size: _const.MESSAGE_SIZES.FEATURED_ICON,
53
+ // React 구현과 동일하게 `__icon`(flex-shrink: 0)을 부여한다.
54
+ // 누락 시 긴 타이틀에서 아이콘이 찌그러진다.
55
+ className: _const.CLASS_NAMES.MESSAGE.ICON
51
56
  });
52
57
  featuredIconElement = this.featuredIcon.getElement();
53
58
  }
54
- wrapper.innerHTML = (0, _sanitize.sanitizeHtml)(this.buildTemplate({
59
+ wrapper.innerHTML = (0, _sanitize.sanitizeHtml)((0, _messageTemplate.buildMessageTemplate)({
55
60
  title,
56
61
  supportingText,
57
62
  actions,
58
63
  onClose,
59
- onHidePermanently
64
+ onHidePermanently,
65
+ hidePermanentlyLabel
60
66
  }));
61
67
  // FeaturedIcon을 content-wrapper에 추가
62
68
  if (featuredIconElement) {
@@ -67,50 +73,6 @@ class MessageNotification {
67
73
  }
68
74
  return wrapper;
69
75
  }
70
- buildTemplate(params) {
71
- const {
72
- title,
73
- supportingText,
74
- actions,
75
- onClose,
76
- onHidePermanently
77
- } = params;
78
- return `
79
- <div class="${_const.CLASS_NAMES.MESSAGE.CONTAINER}">
80
- <div class="${_const.CLASS_NAMES.MESSAGE.CONTENT}">
81
- <div class="${_const.CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}">
82
- <div class="${_const.CLASS_NAMES.MESSAGE.TEXT_CONTAINER}">
83
- <span class="${_const.CLASS_NAMES.MESSAGE.TITLE}">${title}</span>
84
- ${(0, _utils.renderSupportingText)(supportingText, _const.CLASS_NAMES.MESSAGE.SUPPORTING_TEXT)}
85
- </div>
86
- </div>
87
- <div class="${_const.CLASS_NAMES.MESSAGE.ACTIONS_CONTAINER}">
88
- ${actions && actions.length > 0 ? (0, _utils.renderActions)(actions, _const.CLASS_NAMES.MESSAGE.ACTIONS) : ''}
89
- </div>
90
- <div class="${_const.CLASS_NAMES.MESSAGE.ACTIONS_CONTAINER}">
91
- ${this.renderHidePermanentlyButton(onHidePermanently)}
92
- ${this.renderCloseButton(onClose)}
93
- </div>
94
- </div>
95
- </div>
96
- `;
97
- }
98
- renderHidePermanentlyButton(onHidePermanently) {
99
- if (!onHidePermanently) return '';
100
- return `
101
- <button type="button" class="${_const.CLASS_NAMES.COMMON.ACTION_BUTTON} ${_const.CLASS_NAMES.COMMON.ACTION_BUTTON}--text ${_const.CLASS_NAMES.MESSAGE.HIDE_LINK}" data-hide-permanently="true">
102
- 다시 보지 않기
103
- </button>
104
- `;
105
- }
106
- renderCloseButton(onClose) {
107
- if (!onClose) return '';
108
- return `
109
- <button type="button" class="${_const.CLASS_NAMES.MESSAGE.CLOSE_BUTTON}" aria-label="알림 닫기">
110
- ${_const.SVG_ICONS['x-close'](_const.MESSAGE_SIZES.CLOSE_BUTTON).replace('stroke="currentColor"', `stroke="#2F2F30"`)}
111
- </button>
112
- `;
113
- }
114
76
  bindEvents() {
115
77
  (0, _utils.bindNotificationEvents)(this.element, this.options.actions, this.options.onClose, () => this.remove());
116
78
  // 다시보지 않기 버튼 이벤트 바인딩
@@ -41,6 +41,9 @@ const CLASS_NAMES = exports.CLASS_NAMES = {
41
41
  SUPPORTING_TEXT: 'ncua-message-notification__supporting-text',
42
42
  ACTIONS_CONTAINER: 'ncua-message-notification__actions-container',
43
43
  ACTIONS: 'ncua-message-notification__actions',
44
+ // 다시 보지 않기 + 닫기 그룹 전용 래퍼. ACTIONS_CONTAINER를 재사용하면
45
+ // 해당 클래스의 gap(12px)이 그룹 내부에도 적용되어 8px 규격이 깨진다.
46
+ FOOTER_CONTAINER: 'ncua-message-notification__footer-container',
44
47
  HIDE_LINK: 'ncua-message-notification__hide-link',
45
48
  CLOSE_BUTTON: 'ncua-message-notification__close-button'
46
49
  },
@@ -81,7 +81,7 @@ Object.defineProperty(exports, "getSizes", {
81
81
  return _sizes.getSizes;
82
82
  }
83
83
  });
84
- var _icons = require("./icons");
85
84
  var _classNames = require("./classNames");
85
+ var _icons = require("./icons");
86
86
  var _sizes = require("./sizes");
87
87
  var _types = require("./types");
@@ -32,9 +32,11 @@ const FULL_WIDTH_SIZES = exports.FULL_WIDTH_SIZES = {
32
32
  CLOSE_BUTTON: '20'
33
33
  };
34
34
  // Message 알림 고정 사이즈
35
+ // FEATURED_ICON(원 크기)과 ICON_PIXEL(내부 SVG 크기)은 항상 짝을 맞춰야 한다.
36
+ // 'md' = 40px 원 / 20px 아이콘 (FeaturedIcon iconSizeMap 기준)
35
37
  const MESSAGE_SIZES = exports.MESSAGE_SIZES = {
36
- FEATURED_ICON: 'lg',
37
- ICON_PIXEL: '24',
38
+ FEATURED_ICON: 'md',
39
+ ICON_PIXEL: '20',
38
40
  CLOSE_BUTTON: '20'
39
41
  };
40
42
  // 사이즈 유틸리티 함수들
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.buildMessageTemplate = buildMessageTemplate;
7
+ var _notification = require("../../../constant/notification");
8
+ var _const = require("./const");
9
+ var _utils = require("./utils");
10
+ /**
11
+ * message 타입 액션 버튼 사이즈. TO-BE 규격 28px이며 React `<Button size="xs">`와 같다.
12
+ * floating / full-width는 이 값을 쓰지 않고 `renderActions`의 기본값('sm')을 유지한다.
13
+ */
14
+ const ACTION_BUTTON_SIZE = 'xs';
15
+ /**
16
+ * Message Notification의 마크업을 만든다.
17
+ * React 구현(`src/components/overlays/notification/MessageNotification.tsx`)과
18
+ * 동일한 클래스 트리를 산출해야 한다 — SCSS 한 벌을 두 구현이 공유한다.
19
+ */
20
+ function buildMessageTemplate(params) {
21
+ const {
22
+ title,
23
+ supportingText,
24
+ actions,
25
+ onClose,
26
+ onHidePermanently,
27
+ hidePermanentlyLabel
28
+ } = params;
29
+ return `
30
+ <div class="${_const.CLASS_NAMES.MESSAGE.CONTAINER}">
31
+ <div class="${_const.CLASS_NAMES.MESSAGE.CONTENT}">
32
+ <div class="${_const.CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}">
33
+ <div class="${_const.CLASS_NAMES.MESSAGE.TEXT_CONTAINER}">
34
+ <span class="${_const.CLASS_NAMES.MESSAGE.TITLE}">${title}</span>
35
+ ${(0, _utils.renderSupportingText)(supportingText, _const.CLASS_NAMES.MESSAGE.SUPPORTING_TEXT)}
36
+ </div>
37
+ </div>
38
+ ${renderActionsContainer({
39
+ actions,
40
+ onClose,
41
+ onHidePermanently,
42
+ hidePermanentlyLabel
43
+ })}
44
+ </div>
45
+ </div>
46
+ `;
47
+ }
48
+ /**
49
+ * 액션 버튼 그룹과 닫기 그룹을 함께 감싸는 우측 영역.
50
+ * 이 컨테이너의 gap(12px)이 두 그룹 사이 간격을 만든다.
51
+ * 표시할 요소가 하나도 없으면 빈 컨테이너를 남기지 않는다.
52
+ */
53
+ function renderActionsContainer(params) {
54
+ const {
55
+ actions,
56
+ onClose,
57
+ onHidePermanently,
58
+ hidePermanentlyLabel
59
+ } = params;
60
+ const actionsHtml = (0, _utils.renderActions)(actions ?? [], _const.CLASS_NAMES.MESSAGE.ACTIONS, ACTION_BUTTON_SIZE);
61
+ const footerHtml = renderFooterContainer({
62
+ onClose,
63
+ onHidePermanently,
64
+ hidePermanentlyLabel
65
+ });
66
+ if (!actionsHtml && !footerHtml) return '';
67
+ return `<div class="${_const.CLASS_NAMES.MESSAGE.ACTIONS_CONTAINER}">${actionsHtml}${footerHtml}</div>`;
68
+ }
69
+ /** 다시 보지 않기 + 닫기 그룹. 전용 래퍼를 쓰는 이유는 `CLASS_NAMES.MESSAGE.FOOTER_CONTAINER` 참조. */
70
+ function renderFooterContainer(params) {
71
+ const {
72
+ onClose,
73
+ onHidePermanently,
74
+ hidePermanentlyLabel
75
+ } = params;
76
+ if (!onHidePermanently && !onClose) return '';
77
+ return `
78
+ <div class="${_const.CLASS_NAMES.MESSAGE.FOOTER_CONTAINER}">
79
+ ${renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
80
+ ${renderCloseButton(onClose)}
81
+ </div>
82
+ `;
83
+ }
84
+ /**
85
+ * 다시 보지 않기 버튼.
86
+ *
87
+ * `label`은 이 함수의 반환값이 `wrapper.innerHTML`에 들어가기 전에 `sanitizeHtml`로
88
+ * 처리되므로 별도 이스케이프하지 않는다 — `renderSupportingText`와 같은 정책이다.
89
+ */
90
+ function renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
91
+ if (!onHidePermanently) return '';
92
+ const label = hidePermanentlyLabel ?? _notification.HIDE_PERMANENTLY_LABEL.message;
93
+ return `
94
+ <button type="button" class="${_const.CLASS_NAMES.COMMON.ACTION_BUTTON} ${_const.CLASS_NAMES.MESSAGE.HIDE_LINK}" data-hide-permanently="true">
95
+ ${label}
96
+ </button>
97
+ `;
98
+ }
99
+ function renderCloseButton(onClose) {
100
+ if (!onClose) return '';
101
+ return `
102
+ <button type="button" class="${_const.CLASS_NAMES.MESSAGE.CLOSE_BUTTON}" aria-label="알림 닫기">
103
+ ${_const.SVG_ICONS['x-close'](_const.MESSAGE_SIZES.CLOSE_BUTTON).replace('stroke="currentColor"', 'stroke="#2F2F30"')}
104
+ </button>
105
+ `;
106
+ }
@@ -9,6 +9,7 @@ exports.createWrapperElement = createWrapperElement;
9
9
  exports.isMobile = void 0;
10
10
  exports.renderActions = renderActions;
11
11
  exports.renderSupportingText = renderSupportingText;
12
+ exports.resolveActionHierarchy = resolveActionHierarchy;
12
13
  exports.setupAutoClose = setupAutoClose;
13
14
  var _breakpoint = require("../../../src/constant/breakpoint");
14
15
  var _const = require("./const");
@@ -57,16 +58,51 @@ function renderSupportingText(supportingText, className, supportTextLink) {
57
58
  // unsafe URL이거나 link가 없으면 링크 없이 텍스트만 렌더링
58
59
  return `<span class="${className}">${supportingText}</span>`;
59
60
  }
61
+ /** 액션 버튼 색상을 지정하지 않았을 때의 기본값. React `MessageNotificationParts`와 같다. */
62
+ const DEFAULT_ACTION_HIERARCHY = 'text';
63
+ /**
64
+ * 액션 버튼의 색상 수식자를 현행 어휘로 정규화한다.
65
+ *
66
+ * 구 어휘(`'link'` / `'link-gray'`)는 Link 컴포넌트의 색상 이름을 빌려 쓴 것이라
67
+ * `ncua-btn--link`에 대응하는 CSS가 존재하지 않았다. 기존 CDN 사용처를 깨지 않기 위해
68
+ * 타입에서 제거하는 대신 이 지점에서 실제 Button 테마로 매핑한다.
69
+ *
70
+ * 렌더(`renderActions`)와 이벤트 바인딩(`bindNotificationEvents`)이 이 함수를 함께 써야
71
+ * `data-action` 키가 어긋나지 않는다.
72
+ */
73
+ function resolveActionHierarchy(hierarchy) {
74
+ switch (hierarchy) {
75
+ case undefined:
76
+ return DEFAULT_ACTION_HIERARCHY;
77
+ case 'link':
78
+ return 'text';
79
+ case 'link-gray':
80
+ return 'text-gray';
81
+ default:
82
+ return hierarchy;
83
+ }
84
+ }
85
+ /** 액션 버튼의 클릭 라우팅 키. 렌더와 바인딩이 같은 규칙으로 만들어야 한다. */
86
+ function buildActionKey(action) {
87
+ return `${action.label}-${resolveActionHierarchy(action.hierarchy)}`;
88
+ }
89
+ /**
90
+ * 액션 버튼 그룹 HTML을 만든다.
91
+ *
92
+ * `size` 기본값 'sm'은 floating / full-width 타입의 기존 렌더링을 그대로 보존하기 위한 것이다.
93
+ * message 타입만 TO-BE 규격(28px)에 맞춰 'xs'를 넘긴다 — React `<Button size="xs">`와 동일.
94
+ */
60
95
  function renderActions(actions, wrapperClass) {
96
+ let size = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'sm';
61
97
  // 액션이 없으면 빈 문자열 반환
62
98
  if (!actions || actions.length === 0) {
63
99
  return '';
64
100
  }
65
101
  const buttonsHtml = actions.map(action => {
66
102
  const buttonHtml = `
67
- <button
68
- class="ncua-btn ncua-btn--sm ncua-btn--${action.hierarchy || 'text'}"
69
- data-action="${action.label}-${action.hierarchy}"
103
+ <button
104
+ class="ncua-btn ncua-btn--${size} ncua-btn--${resolveActionHierarchy(action.hierarchy)}"
105
+ data-action="${buildActionKey(action)}"
70
106
  >
71
107
  ${action.label}
72
108
  </button>`;
@@ -92,7 +128,7 @@ function bindNotificationEvents(element, actions, onClose, onRemove) {
92
128
  if (actionData) {
93
129
  let matchedAction = null;
94
130
  for (const action of actions) {
95
- const expectedDataAction = `${action.label}-${action.hierarchy || 'link'}`;
131
+ const expectedDataAction = buildActionKey(action);
96
132
  if (actionData === expectedDataAction) {
97
133
  matchedAction = action;
98
134
  break;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.HIDE_PERMANENTLY_LABEL = void 0;
7
+ /**
8
+ * 다시보지 않기 버튼의 타입별 기본 문구.
9
+ *
10
+ * `hidePermanentlyLabel`을 지정하지 않았을 때 쓰인다.
11
+ * React(`src/components/overlays/notification/`)와 Vanilla(`assets/scripts/notification/`)
12
+ * 두 구현이 이 상수를 함께 참조한다 — 문구가 하드코딩으로 흩어져 서로 어긋나던 문제(#460)를
13
+ * 막기 위한 단일 출처다.
14
+ *
15
+ * `message`와 `fullWidth`의 띄어쓰기가 다른 것은 오타가 아니라 현행 유지다.
16
+ * 표기 통일은 별도 결정 사항이며, 결정되면 이 파일 한 곳만 고치면 된다.
17
+ */
18
+ const HIDE_PERMANENTLY_LABEL = exports.HIDE_PERMANENTLY_LABEL = {
19
+ message: '다시 보지 않기',
20
+ fullWidth: '다시보지 않기'
21
+ };
@@ -39,27 +39,28 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
39
39
  ...props
40
40
  } = _ref;
41
41
  const inputRef = (0, _react.useRef)(null);
42
- const [textCount, setTextCount] = (0, _react.useState)(0);
43
- const [hasValue, setHasValue] = (0, _react.useState)(false);
44
- // 유무는 DOM에서 읽는다 — props.value 판정하면 uncontrolled Input에서 항상 빈 값으로 보인다.
45
- // biome-ignore lint/correctness/useExhaustiveDependencies: controlled 값이 프로그래밍적으로 바뀔 때는 input 이벤트가 나지 않아 props.value로 재동기화해야 한다
42
+ // controlled 인풋은 props.value 길이를 알 수 있으므로 상태를 두지 않는다.
43
+ // 입력마다 setState 하면 조합 중(한글 IME)에 리렌더가 끼어들어 조합이 끊긴다.
44
+ const isControlled = props.value !== undefined;
45
+ const [uncontrolledLength, setUncontrolledLength] = (0, _react.useState)(0);
46
+ const valueLength = isControlled ? String(props.value ?? '').length : uncontrolledLength;
47
+ const hasValue = valueLength > 0;
48
+ const textCount = valueLength;
49
+ // uncontrolled 인풋만 DOM 값을 읽어 따라간다 — props.value 가 없어 길이를 알 방법이 이것뿐이다.
46
50
  (0, _react.useEffect)(() => {
51
+ if (isControlled) return;
47
52
  const input = inputRef.current;
48
53
  if (!input) return;
49
54
  const syncFromInput = () => {
50
- const {
51
- length
52
- } = input.value;
53
- setHasValue(length > 0);
54
- setTextCount(length);
55
+ setUncontrolledLength(input.value.length);
55
56
  };
56
57
  syncFromInput();
57
58
  input.addEventListener('input', syncFromInput);
58
59
  return () => {
59
60
  input.removeEventListener('input', syncFromInput);
60
61
  };
61
- }, [props.value]);
62
- const renderOutsideSlot = slot => {
62
+ }, [isControlled]);
63
+ const renderOutsideSlot = (slot, position) => {
63
64
  if (slot.type === 'custom' && slot.placement !== 'outside') {
64
65
  return null;
65
66
  }
@@ -72,10 +73,15 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
72
73
  children: slot.label
73
74
  });
74
75
  case 'text':
75
- return (0, _jsxRuntime.jsx)("span", {
76
- className: (0, _classnames.default)('ncua-input__leading-text', `ncua-input__leading-text--${size}`),
77
- children: slot.text
78
- });
76
+ {
77
+ // 클래스명은 리터럴로 쓴다 — MCP 의 BEM 추출기가 `ncua-input__${position}-text` 형태의
78
+ // 템플릿 리터럴에서는 `ncua-input__` 조각만 읽어 완성된 클래스를 잡지 못한다.
79
+ const textClass = position === 'leading' ? 'ncua-input__leading-text' : 'ncua-input__trailing-text';
80
+ return (0, _jsxRuntime.jsx)("span", {
81
+ className: (0, _classnames.default)(textClass, `${textClass}--${size}`),
82
+ children: slot.text
83
+ });
84
+ }
79
85
  case 'custom':
80
86
  return slot.children;
81
87
  default:
@@ -90,7 +96,12 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
90
96
  return (0, _jsxRuntime.jsx)("div", {
91
97
  className: "ncua-input__icon-wrap",
92
98
  children: (0, _jsxRuntime.jsx)(SlotIcon, {
93
- className: (0, _classnames.default)(`ncua-input__${position}-icon`, slot.className),
99
+ className: (0, _classnames.default)(`ncua-input__${position}-icon`,
100
+ // color 를 지정하지 않았을 때만 상태별 색(Placeholder/Filled/Focused/Disabled)을 CSS 에 맡긴다.
101
+ // 지정한 경우에는 이 클래스를 붙이지 않아 지정 색이 그대로 유지된다.
102
+ {
103
+ 'ncua-input__slot-icon': !slot.color
104
+ }, slot.className),
94
105
  color: slot.color ? _color.COLOR[slot.color] : undefined,
95
106
  width: slot.size ?? iconSvgSize[size],
96
107
  height: slot.size ?? iconSvgSize[size]
@@ -162,13 +173,14 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
162
173
  'full-width': fullWidth,
163
174
  'ncua-input__trailing-button': trailingElement?.type === 'button',
164
175
  'ncua-input__leading-text-wrap': leadingElement?.type === 'text',
176
+ 'ncua-input__trailing-text-wrap': trailingElement?.type === 'text',
165
177
  'has-value': hasValue
166
178
  }, className),
167
179
  children: [renderLabel(), (0, _jsxRuntime.jsxs)("div", {
168
180
  className: "ncua-input__content-wrap",
169
181
  children: [(0, _jsxRuntime.jsxs)("div", {
170
182
  className: "ncua-input__content",
171
- children: [leadingElement && renderOutsideSlot(leadingElement), (0, _jsxRuntime.jsxs)("div", {
183
+ children: [leadingElement && renderOutsideSlot(leadingElement, 'leading'), (0, _jsxRuntime.jsxs)("div", {
172
184
  className: (0, _classnames.default)('ncua-input__field', `ncua-input__field--${size}`),
173
185
  children: [leadingElement && renderInsideSlot(leadingElement, 'left'), (0, _jsxRuntime.jsx)("input", {
174
186
  ref: node => {
@@ -184,7 +196,7 @@ const InputBase = exports.InputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref,
184
196
  maxLength: maxLength,
185
197
  ...props
186
198
  }), renderClearButton(), renderStatusIcon(), trailingElement && renderInsideSlot(trailingElement, 'right')]
187
- }), trailingElement && renderOutsideSlot(trailingElement)]
199
+ }), trailingElement && renderOutsideSlot(trailingElement, 'trailing')]
188
200
  }), showTextCount && maxLength && (0, _jsxRuntime.jsxs)("div", {
189
201
  className: "ncua-input__field-text-count",
190
202
  children: [(0, _jsxRuntime.jsx)("output", {
@@ -5,6 +5,7 @@ var _react = require("react");
5
5
  var _client = require("react-dom/client");
6
6
  var _testUtils = require("react-dom/test-utils");
7
7
  var _vitest = require("vitest");
8
+ var _color = require("../../../../../constant/color");
8
9
  var _InputBase = require("../InputBase");
9
10
  // @vitest-environment jsdom
10
11
 
@@ -133,6 +134,55 @@ function mountInputBase() {
133
134
  view.unmount();
134
135
  });
135
136
  });
137
+ (0, _vitest.describe)('InputBase — 슬롯 아이콘 상태 색상', () => {
138
+ // 상태별 색은 CSS 가 결정한다. jsdom 에서는 스타일시트를 로드하지 않으므로
139
+ // "상태 규칙을 타는 클래스가 붙었는지" 와 "지정 색이 인라인으로 남았는지" 로 검증한다.
140
+ const leftIconOf = container => container.querySelector('.ncua-input__left-icon');
141
+ (0, _vitest.it)('color 를 지정하지 않으면 상태 색상 클래스가 붙는다', () => {
142
+ // Given/When: color 없이 슬롯 아이콘을 렌더하면
143
+ const view = mountInputBase({
144
+ leadingElement: {
145
+ type: 'icon',
146
+ icon: _uiAdminIcon.SearchLg
147
+ }
148
+ });
149
+ // Then: 상태 규칙을 타는 클래스가 붙고, 인라인 색은 지정되지 않는다
150
+ const icon = leftIconOf(view.container);
151
+ (0, _vitest.expect)(icon?.classList.contains('ncua-input__slot-icon')).toBe(true);
152
+ (0, _vitest.expect)(icon?.hasAttribute('color')).toBe(false);
153
+ view.unmount();
154
+ });
155
+ (0, _vitest.it)('color 를 지정하면 상태 색상 클래스가 붙지 않고 지정 색이 유지된다', () => {
156
+ // Given/When: color 를 지정해 렌더하면
157
+ const view = mountInputBase({
158
+ leadingElement: {
159
+ type: 'icon',
160
+ icon: _uiAdminIcon.SearchLg,
161
+ color: 'blue500'
162
+ }
163
+ });
164
+ // Then: 상태 규칙에서 제외되고 지정한 색이 그대로 남는다
165
+ const icon = leftIconOf(view.container);
166
+ (0, _vitest.expect)(icon?.classList.contains('ncua-input__slot-icon')).toBe(false);
167
+ (0, _vitest.expect)(icon?.getAttribute('color')).toBe(_color.COLOR.blue500);
168
+ view.unmount();
169
+ });
170
+ (0, _vitest.it)('slot.className 을 함께 주어도 상태 색상 클래스가 유지된다', () => {
171
+ // Given/When: className 을 추가로 넘기면
172
+ const view = mountInputBase({
173
+ leadingElement: {
174
+ type: 'icon',
175
+ icon: _uiAdminIcon.SearchLg,
176
+ className: 'my-icon'
177
+ }
178
+ });
179
+ // Then: 두 클래스가 모두 붙는다
180
+ const icon = leftIconOf(view.container);
181
+ (0, _vitest.expect)(icon?.classList.contains('ncua-input__slot-icon')).toBe(true);
182
+ (0, _vitest.expect)(icon?.classList.contains('my-icon')).toBe(true);
183
+ view.unmount();
184
+ });
185
+ });
136
186
  (0, _vitest.describe)('InputBase — has-value 상태 클래스', () => {
137
187
  const typeInto = (container, text) => {
138
188
  const input = container.querySelector('input');
@@ -181,4 +231,70 @@ function mountInputBase() {
181
231
  (0, _vitest.expect)(view.container.querySelector('.ncua-input')?.classList.contains('has-value')).toBe(true);
182
232
  view.unmount();
183
233
  });
234
+ });
235
+ (0, _vitest.describe)('InputBase — 텍스트 슬롯', () => {
236
+ (0, _vitest.it)('leadingElement 텍스트는 leading 클래스와 wrap 클래스가 붙는다', () => {
237
+ const {
238
+ container,
239
+ unmount
240
+ } = mountInputBase({
241
+ leadingElement: {
242
+ type: 'text',
243
+ text: 'https://'
244
+ }
245
+ });
246
+ const text = container.querySelector('.ncua-input__leading-text');
247
+ (0, _vitest.expect)(text?.textContent).toBe('https://');
248
+ (0, _vitest.expect)(container.querySelector('.ncua-input__leading-text-wrap')).not.toBeNull();
249
+ unmount();
250
+ });
251
+ (0, _vitest.it)('trailingElement 텍스트는 trailing 클래스와 wrap 클래스가 붙는다', () => {
252
+ const {
253
+ container,
254
+ unmount
255
+ } = mountInputBase({
256
+ trailingElement: {
257
+ type: 'text',
258
+ text: 'px'
259
+ }
260
+ });
261
+ const text = container.querySelector('.ncua-input__trailing-text');
262
+ (0, _vitest.expect)(text?.textContent).toBe('px');
263
+ (0, _vitest.expect)(container.querySelector('.ncua-input__trailing-text-wrap')).not.toBeNull();
264
+ // leading 클래스가 섞이면 모서리·테두리 방향이 반대가 된다
265
+ (0, _vitest.expect)(container.querySelector('.ncua-input__leading-text')).toBeNull();
266
+ unmount();
267
+ });
268
+ (0, _vitest.it)('trailingElement 텍스트는 입력 필드 뒤에 온다', () => {
269
+ const {
270
+ container,
271
+ unmount
272
+ } = mountInputBase({
273
+ trailingElement: {
274
+ type: 'text',
275
+ text: 'px'
276
+ }
277
+ });
278
+ const content = container.querySelector('.ncua-input__content');
279
+ const children = Array.from(content?.children ?? []);
280
+ const fieldIndex = children.findIndex(el => el.classList.contains('ncua-input__field'));
281
+ const textIndex = children.findIndex(el => el.classList.contains('ncua-input__trailing-text'));
282
+ (0, _vitest.expect)(fieldIndex).toBeGreaterThanOrEqual(0);
283
+ (0, _vitest.expect)(textIndex).toBeGreaterThan(fieldIndex);
284
+ unmount();
285
+ });
286
+ (0, _vitest.it)('사이즈 modifier 가 함께 붙는다', () => {
287
+ const {
288
+ container,
289
+ unmount
290
+ } = mountInputBase({
291
+ size: 'sm',
292
+ trailingElement: {
293
+ type: 'text',
294
+ text: 'px'
295
+ }
296
+ });
297
+ (0, _vitest.expect)(container.querySelector('.ncua-input__trailing-text--sm')).not.toBeNull();
298
+ unmount();
299
+ });
184
300
  });
@@ -33,9 +33,9 @@ const PasswordInput = exports.PasswordInput = /*#__PURE__*/(0, _react.forwardRef
33
33
  type: isVisible ? 'text' : 'password',
34
34
  leadingElement: {
35
35
  type: 'icon',
36
- icon: _uiAdminIcon.Lock01,
37
- // 색은 CSS가 결정한다 여기서 props.value로 판정하면 uncontrolled Input에서 영원히 빈 값 취급된다
38
- className: 'ncua-input__password-lock-icon'
36
+ icon: _uiAdminIcon.Lock01
37
+ // color 넘기지 않아 슬롯 아이콘 공통 규칙(상태별 색)을 그대로 따른다.
38
+ // 여기서 props.value 로 판정하면 uncontrolled Input 에서 영원히 빈 값 취급된다.
39
39
  },
40
40
  trailingElement: {
41
41
  type: 'custom',