@tcn/ui 0.0.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +38 -3
  2. package/dist/actions/button/base_button/base_button.d.ts.map +1 -1
  3. package/dist/actions/button/base_button/base_button.js +17 -12
  4. package/dist/actions/button/base_button/base_button.js.map +1 -1
  5. package/dist/actions/button/button/button.d.ts.map +1 -1
  6. package/dist/actions/button/button/button.js +7 -7
  7. package/dist/actions/button/button/button.js.map +1 -1
  8. package/dist/actions/button/slim_button/slim_button.js +2 -2
  9. package/dist/actions/button/slim_button/slim_button.js.map +1 -1
  10. package/dist/button.css +1 -1
  11. package/dist/draggable.css +1 -0
  12. package/dist/feedback/progress/progress_bar.js +1 -1
  13. package/dist/footer.css +1 -1
  14. package/dist/form/field/common/field_description.js +1 -1
  15. package/dist/form/field/common/field_error.js +4 -3
  16. package/dist/form/field/common/field_error.js.map +1 -1
  17. package/dist/form/field/common/field_label.js +1 -1
  18. package/dist/inputs/date_picker/date_picker_date.js +1 -1
  19. package/dist/inputs/date_picker/date_picker_day.js +1 -1
  20. package/dist/inputs/date_picker/date_picker_time_selector.js +1 -1
  21. package/dist/inputs/date_picker/date_picker_year_selector.js +1 -1
  22. package/dist/inputs/input/input.d.ts +2 -2
  23. package/dist/inputs/input/input.d.ts.map +1 -1
  24. package/dist/inputs/input/input.js.map +1 -1
  25. package/dist/inputs/options/option.d.ts +1 -0
  26. package/dist/inputs/options/option.d.ts.map +1 -1
  27. package/dist/inputs/options/option.js.map +1 -1
  28. package/dist/inputs/phone_number_input/phone_number_input.d.ts +8 -1
  29. package/dist/inputs/phone_number_input/phone_number_input.d.ts.map +1 -1
  30. package/dist/inputs/phone_number_input/phone_number_input.js +187 -137
  31. package/dist/inputs/phone_number_input/phone_number_input.js.map +1 -1
  32. package/dist/inputs/suggestions/suggestion_item.d.ts +1 -1
  33. package/dist/inputs/suggestions/suggestion_item.d.ts.map +1 -1
  34. package/dist/inputs/suggestions/suggestion_item.js +23 -18
  35. package/dist/inputs/suggestions/suggestion_item.js.map +1 -1
  36. package/dist/inputs/suggestions/suggestion_list.d.ts +1 -1
  37. package/dist/inputs/suggestions/suggestion_list.d.ts.map +1 -1
  38. package/dist/inputs/suggestions/suggestion_list.js +106 -96
  39. package/dist/inputs/suggestions/suggestion_list.js.map +1 -1
  40. package/dist/inputs/textarea/textarea.d.ts +2 -2
  41. package/dist/inputs/textarea/textarea.d.ts.map +1 -1
  42. package/dist/inputs/textarea/textarea.js.map +1 -1
  43. package/dist/layouts/footer/footer.js +5 -5
  44. package/dist/layouts/footer/footer.js.map +1 -1
  45. package/dist/layouts/header/header.d.ts.map +1 -1
  46. package/dist/layouts/header/header.js.map +1 -1
  47. package/dist/layouts/index.d.ts +3 -2
  48. package/dist/layouts/index.d.ts.map +1 -1
  49. package/dist/layouts/index.js +26 -24
  50. package/dist/layouts/index.js.map +1 -1
  51. package/dist/layouts/list/item.d.ts +1 -0
  52. package/dist/layouts/list/item.d.ts.map +1 -1
  53. package/dist/layouts/list/item.js +17 -6
  54. package/dist/layouts/list/item.js.map +1 -1
  55. package/dist/layouts/list/list.js +10 -10
  56. package/dist/layouts/list/list.js.map +1 -1
  57. package/dist/overlay/context_menu/context_menu.js +4 -4
  58. package/dist/overlay/frame/frame.d.ts +11 -0
  59. package/dist/overlay/frame/frame.d.ts.map +1 -0
  60. package/dist/overlay/frame/frame.js +18 -0
  61. package/dist/overlay/frame/frame.js.map +1 -0
  62. package/dist/overlay/index.d.ts +1 -0
  63. package/dist/overlay/index.d.ts.map +1 -1
  64. package/dist/overlay/index.js +5 -3
  65. package/dist/overlay/index.js.map +1 -1
  66. package/dist/overlay/popper/popper.js +12 -12
  67. package/dist/{portal-qqIp4SIl.js → overlay/portal/portal.js} +3 -3
  68. package/dist/overlay/portal/portal.js.map +1 -0
  69. package/dist/overlay/portal/portal_platform.js +3 -4
  70. package/dist/overlay/portal/portal_platform.js.map +1 -1
  71. package/dist/phone_number_input.css +1 -1
  72. package/dist/slim_button.css +1 -1
  73. package/dist/stacks/box/box.d.ts +1 -1
  74. package/dist/stacks/box/box.d.ts.map +1 -1
  75. package/dist/stacks/box/box.js.map +1 -1
  76. package/dist/surfaces/card/card.d.ts.map +1 -1
  77. package/dist/surfaces/card/card.js +7 -16
  78. package/dist/surfaces/card/card.js.map +1 -1
  79. package/dist/surfaces/confirm/confirm.js +4 -4
  80. package/dist/surfaces/index.d.ts +2 -2
  81. package/dist/surfaces/index.d.ts.map +1 -1
  82. package/dist/surfaces/index.js +22 -22
  83. package/dist/surfaces/modal/modal.d.ts +3 -2
  84. package/dist/surfaces/modal/modal.d.ts.map +1 -1
  85. package/dist/surfaces/modal/modal.js +14 -13
  86. package/dist/surfaces/modal/modal.js.map +1 -1
  87. package/dist/surfaces/window/window.d.ts +3 -2
  88. package/dist/surfaces/window/window.d.ts.map +1 -1
  89. package/dist/surfaces/window/window.js +17 -7
  90. package/dist/surfaces/window/window.js.map +1 -1
  91. package/dist/themes/index.js +6 -141
  92. package/dist/themes/index.js.map +1 -1
  93. package/dist/themes/stylesheets/reset.js +140 -0
  94. package/dist/themes/stylesheets/reset.js.map +1 -0
  95. package/dist/themes/themes/ergo/ergo_theme.js +664 -0
  96. package/dist/themes/themes/ergo/ergo_theme.js.map +1 -0
  97. package/dist/tokens/bubble/bubble.js +17 -16
  98. package/dist/tokens/bubble/bubble.js.map +1 -1
  99. package/dist/tokens/chip/chip.js +9 -8
  100. package/dist/tokens/chip/chip.js.map +1 -1
  101. package/dist/typography/title/title.d.ts +2 -1
  102. package/dist/typography/title/title.d.ts.map +1 -1
  103. package/dist/typography/title/title.js +24 -23
  104. package/dist/typography/title/title.js.map +1 -1
  105. package/dist/utils/dnd/context.d.ts +4 -0
  106. package/dist/utils/dnd/context.d.ts.map +1 -0
  107. package/dist/utils/dnd/context.js +20 -0
  108. package/dist/utils/dnd/context.js.map +1 -0
  109. package/dist/utils/dnd/draggable/draggable.d.ts +7 -0
  110. package/dist/utils/dnd/draggable/draggable.d.ts.map +1 -0
  111. package/dist/utils/dnd/draggable/draggable.js +27 -0
  112. package/dist/utils/dnd/draggable/draggable.js.map +1 -0
  113. package/dist/utils/dnd/handle.d.ts +6 -0
  114. package/dist/utils/dnd/handle.d.ts.map +1 -0
  115. package/dist/utils/dnd/handle.js +22 -0
  116. package/dist/utils/dnd/handle.js.map +1 -0
  117. package/dist/utils/dnd/hooks/use_drag_container.d.ts +7 -0
  118. package/dist/utils/dnd/hooks/use_drag_container.d.ts.map +1 -0
  119. package/dist/utils/dnd/hooks/use_drag_container.js +30 -0
  120. package/dist/utils/dnd/hooks/use_drag_container.js.map +1 -0
  121. package/dist/utils/{hooks → dnd/hooks}/use_draggable.d.ts +3 -3
  122. package/dist/utils/dnd/hooks/use_draggable.d.ts.map +1 -0
  123. package/dist/utils/dnd/hooks/use_draggable.js +41 -0
  124. package/dist/utils/dnd/hooks/use_draggable.js.map +1 -0
  125. package/dist/utils/dnd/types.d.ts +10 -0
  126. package/dist/utils/dnd/types.d.ts.map +1 -0
  127. package/dist/utils/dnd/types.js +2 -0
  128. package/dist/utils/dnd/types.js.map +1 -0
  129. package/dist/utils/index.d.ts +1 -1
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/index.js +1 -1
  132. package/package.json +9 -3
  133. package/src/actions/button/base_button/base_button.tsx +7 -2
  134. package/src/actions/button/button/button.module.css +0 -78
  135. package/src/actions/button/button/button.tsx +2 -4
  136. package/src/actions/button/slim_button/slim_button.module.css +0 -26
  137. package/src/actions/button/slim_button/slim_button.tsx +1 -1
  138. package/src/inputs/input/input.tsx +3 -2
  139. package/src/inputs/options/option.tsx +1 -0
  140. package/src/inputs/phone_number_input/phone_number_input.module.css +12 -0
  141. package/src/inputs/phone_number_input/phone_number_input.stories.tsx +8 -0
  142. package/src/inputs/phone_number_input/phone_number_input.tsx +107 -21
  143. package/src/inputs/suggestions/suggestion_item.tsx +12 -2
  144. package/src/inputs/suggestions/suggestion_list.tsx +22 -3
  145. package/src/inputs/textarea/textarea.tsx +2 -2
  146. package/src/layouts/footer/footer.module.css +0 -1
  147. package/src/layouts/footer/footer.tsx +1 -1
  148. package/src/layouts/header/header.tsx +0 -1
  149. package/src/layouts/index.ts +3 -2
  150. package/src/layouts/list/item.tsx +10 -2
  151. package/src/layouts/list/list.tsx +2 -2
  152. package/src/overlay/frame/frame.stories.tsx +40 -0
  153. package/src/overlay/frame/frame.tsx +34 -0
  154. package/src/overlay/frame/frame_stories.module.css +14 -0
  155. package/src/overlay/index.ts +1 -0
  156. package/src/stacks/box/box.tsx +8 -2
  157. package/src/surfaces/card/card.tsx +2 -8
  158. package/src/surfaces/index.ts +2 -2
  159. package/src/surfaces/modal/__stories__/modal.stories.tsx +19 -27
  160. package/src/surfaces/modal/modal.tsx +13 -10
  161. package/src/surfaces/panel/__stories__/panel.stories.tsx +13 -12
  162. package/src/surfaces/window/window.stories.tsx +37 -4
  163. package/src/surfaces/window/window.tsx +14 -6
  164. package/src/themes/themes/ergo/__stories__/components/material_picker/sb_inverted_materials.module.css +34 -0
  165. package/src/themes/themes/ergo/__stories__/components/material_picker/sb_material_picker.tsx +52 -0
  166. package/src/themes/themes/ergo/__stories__/components/tone_picker/sb_card.module.css +5 -0
  167. package/src/themes/themes/ergo/__stories__/components/tone_picker/sb_tone_card.tsx +40 -0
  168. package/src/themes/themes/ergo/__stories__/components/tone_picker/sb_tone_picker.tsx +83 -0
  169. package/src/themes/themes/ergo/__stories__/components/tone_picker/types.ts +7 -0
  170. package/src/themes/themes/ergo/__stories__/material.stories.tsx +154 -0
  171. package/src/themes/themes/ergo/__stories__/sb_materials.module.css +110 -0
  172. package/src/themes/themes/ergo/__stories__/utils.ts +92 -0
  173. package/src/themes/themes/ergo/ergo_theme.css +358 -26
  174. package/src/typography/title/title.tsx +23 -19
  175. package/src/utils/dnd/__stories__/draggable.stories.tsx +48 -0
  176. package/src/utils/dnd/__stories__/draggable_stories.module.css +21 -0
  177. package/src/utils/{__stories__ → dnd/__stories__}/use_draggable.stories.tsx +15 -10
  178. package/src/utils/dnd/context.ts +24 -0
  179. package/src/utils/dnd/draggable/draggable.module.css +8 -0
  180. package/src/utils/dnd/draggable/draggable.tsx +42 -0
  181. package/src/utils/dnd/handle.tsx +32 -0
  182. package/src/utils/dnd/hooks/use_drag_container.ts +42 -0
  183. package/src/utils/{hooks → dnd/hooks}/use_draggable.ts +23 -17
  184. package/src/utils/dnd/types.ts +6 -0
  185. package/src/utils/index.ts +1 -1
  186. package/tsconfig.json +0 -3
  187. package/dist/card.css +0 -1
  188. package/dist/portal-qqIp4SIl.js.map +0 -1
  189. package/dist/themes/stylesheets/reset.css +0 -1
  190. package/dist/themes/themes/ergo/ergo_theme.css +0 -1
  191. package/dist/themes/themes/windows_98/windows_98.css +0 -1
  192. package/dist/title.module-B16de2jd.js +0 -5
  193. package/dist/title.module-B16de2jd.js.map +0 -1
  194. package/dist/utils/hooks/use_draggable.d.ts.map +0 -1
  195. package/dist/utils/hooks/use_draggable.js +0 -30
  196. package/dist/utils/hooks/use_draggable.js.map +0 -1
  197. package/src/surfaces/card/card.module.css +0 -5
  198. /package/dist/{overlay/portal/portal.css → portal_platform.css} +0 -0
@@ -6,90 +6,12 @@
6
6
  --button-color-disabled: var(--status-disabled, #cccccc);
7
7
 
8
8
  min-height: var(--button-height);
9
- padding: 4px 8px;
10
- border-radius: 4px;
11
9
  width: auto;
12
- border: 1px solid transparent;
13
- outline: none;
14
10
  cursor: pointer;
15
- background: transparent;
16
11
  font-size: calc(14px * var(--scalar, 1));
17
12
  line-height: calc(16px * var(--scalar, 1));
18
13
  }
19
14
 
20
- .button:focus-visible {
21
- outline: 3px solid color-mix(in srgb, var(--button-color) 40%, transparent);
22
- }
23
-
24
- /* Primary */
25
- .button[data-hierarchy="primary"] {
26
- background: var(--button-color);
27
- color: #ffffff;
28
- }
29
-
30
- .button[data-hierarchy="primary"]:hover {
31
- background: var(--button-color-hover);
32
- color: #ffffff;
33
- }
34
-
35
- .button[data-hierarchy="primary"]:active {
36
- background: var(--button-color-active);
37
- box-shadow: none;
38
- }
39
-
40
- /* Secondary */
41
- .button[data-hierarchy="secondary"] {
42
- color: rgb(57, 85, 120);
43
- border-color: rgb(57, 85, 120);
44
- }
45
-
46
- .button[data-hierarchy="secondary"]:hover {
47
- background: color-mix(in srgb, rgb(57, 85, 120) 10%, rgb(255, 255, 255, 0.5));
48
- }
49
-
50
- .button[data-hierarchy="secondary"]:active {
51
- background: color-mix(in srgb, rgb(57, 85, 120) 25%, rgb(255, 255, 255, 0.5));
52
- }
53
-
54
- /* Tertiary */
55
- .button[data-hierarchy="tertiary"] {
56
- border-color: transparent;
57
- color: rgb(57, 85, 120);
58
- }
59
-
60
- .button[data-hierarchy="tertiary"]:hover {
61
- text-decoration: underline;
62
- text-decoration-color: rgb(57, 85, 120);
63
- text-decoration-thickness: 1px;
64
- text-underline-offset: 2px;
65
- }
66
-
67
- .button[data-hierarchy="tertiary"]:active {
68
- opacity: 0.5;
69
- }
70
-
71
- /* Disabled */
72
- .button[data-is-disabled="true"] {
73
- pointer-events: none;
74
- background: var(--button-color-disabled);
75
- color: #ffffff;
76
- border-color: var(--button-color-disabled);
77
- }
78
-
79
- /* Disabled Secondary */
80
- .button[data-hierarchy="secondary"][data-is-disabled="true"] {
81
- background: transparent;
82
- color: var(--button-color-disabled);
83
- border-color: var(--button-color-disabled);
84
- }
85
-
86
- /* Disabled Tertiary */
87
- .button[data-hierarchy="tertiary"][data-is-disabled="true"] {
88
- background: transparent;
89
- color: var(--button-color-disabled);
90
- border-color: transparent;
91
- }
92
-
93
15
  /* Sizes */
94
16
  .button[data-size="sm"] {
95
17
  --button-height: calc(22px * var(--scalar, 1));
@@ -12,13 +12,11 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function
12
12
  return (
13
13
  <BaseButton
14
14
  ref={ref}
15
- className={clsx(styles.button, 'button', 'tcn-button', className)}
15
+ className={clsx(styles.button, 'tcn-button', className)}
16
16
  {...props}
17
17
  >
18
18
  {typeof children === 'string' ? (
19
- <span className={clsx(styles['button-text'], 'button-text', 'tcn-button-text')}>
20
- {children}
21
- </span>
19
+ <span className={clsx(styles['button-text'], 'tcn-button-text')}>{children}</span>
22
20
  ) : (
23
21
  children
24
22
  )}
@@ -5,29 +5,3 @@
5
5
  .slim-button[data-is-disabled="true"] {
6
6
  pointer-events: none;
7
7
  }
8
-
9
- .slim-button[data-size="sm"] {
10
- padding: 1px;
11
- }
12
-
13
- .slim-button[data-size="md"] {
14
- padding: 2px;
15
- }
16
-
17
- .slim-button[data-size="lg"] {
18
- padding: 3px;
19
- }
20
-
21
- .slim-button[data-hierarchy="tertiary"][data-is-disabled="true"] {
22
- background-color: transparent;
23
- border: none;
24
- color: var(--button-color-disabled);
25
- min-height: auto;
26
- pointer-events: none;
27
- }
28
-
29
- .slim-button[data-hierarchy="secondary"][data-is-disabled="true"] {
30
- background-color: transparent;
31
- border: 1px solid var(--button-color-disabled);
32
- color: var(--button-color-disabled);
33
- }
@@ -9,7 +9,7 @@ export const SlimButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
9
9
  return (
10
10
  <Button
11
11
  ref={ref}
12
- className={clsx(styles['slim-button'], 'slim-button', className)}
12
+ className={clsx(styles['slim-button'], 'tcn-slim-button', className)}
13
13
  {...props}
14
14
  >
15
15
  {children}
@@ -1,9 +1,10 @@
1
1
  import { clsx } from 'clsx';
2
2
  import React from 'react';
3
- import { HTMLAttributes } from 'react';
3
+ import { InputHTMLAttributes } from 'react';
4
4
  import styles from './input.module.css';
5
5
 
6
- export interface InputProps extends Omit<HTMLAttributes<HTMLInputElement>, 'onChange'> {
6
+ export interface InputProps
7
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
7
8
  type?: React.HTMLInputTypeAttribute;
8
9
  width?: string;
9
10
  height?: string;
@@ -9,6 +9,7 @@ export interface OptionProps {
9
9
  keywords?: string[];
10
10
  selected?: boolean;
11
11
  disabled?: boolean;
12
+ obfuscate?: boolean;
12
13
  }
13
14
 
14
15
  export function Option({ children }: OptionProps) {
@@ -48,3 +48,15 @@
48
48
  max-height: calc(26px * var(--scalar, 1)) !important;
49
49
  max-width: calc(26px * var(--scalar, 1)) !important;
50
50
  }
51
+
52
+ .phone-number-input-obfuscated {
53
+ user-select: none;
54
+ -webkit-user-select: none;
55
+ cursor: default;
56
+ width: 100%;
57
+ height: 100%;
58
+ }
59
+
60
+ .phone-number-input-obfuscated::selection {
61
+ background: transparent;
62
+ }
@@ -90,6 +90,14 @@ export function PhoneNumberInput() {
90
90
  >
91
91
  Bob Johnson - +1 (435) 586-5955
92
92
  </Option>
93
+ <Option
94
+ value="+14355865956"
95
+ label="Obfuscated Number"
96
+ keywords={['obfuscated', 'number']}
97
+ obfuscate={true}
98
+ >
99
+ Obfuscated Number
100
+ </Option>
93
101
  </Base>
94
102
  </td>
95
103
  </tr>
@@ -21,6 +21,16 @@ import { NotebookIcon } from '@tcn/icons/notebook_icon.js';
21
21
  import { Option, OptionProps } from '../options/option.js';
22
22
  import { SuggestionList } from '../suggestions/suggestion_list.js';
23
23
  import { stripNonNumericAfterCountryCode } from './utils.js';
24
+ import { useForkRef } from '../../utils/index.js';
25
+
26
+ const OBFUSCATED_CHARACTER = '•';
27
+
28
+ function createObfuscatedMasks(masks: { mask: string; placeholder?: string }[]) {
29
+ return masks.map(m => ({
30
+ ...m,
31
+ placeholder: m.mask.replace(/[9a*]/g, OBFUSCATED_CHARACTER),
32
+ }));
33
+ }
24
34
 
25
35
  const countryList = countriesPhoneInformation.map(i => ({
26
36
  name: i.name,
@@ -108,8 +118,15 @@ function getCountryCodeFromValue(
108
118
  export interface PhoneNumberInputProps
109
119
  extends Omit<HStackProps, 'onChange' | 'children'> {
110
120
  value?: string;
121
+ name?: string;
122
+ autoComplete?: string;
111
123
  defaultCountry?: string;
112
- onChange?: (value: string) => void;
124
+ /**
125
+ * Callback fired when the phone number value changes.
126
+ * @param value - The phone number value with country prefix
127
+ * @param obfuscate - Whether the selected phone number is obfuscated (e.g., from a phone book entry marked as obfuscated)
128
+ */
129
+ onChange?: (value: string, obfuscate: boolean) => void;
113
130
  countrySelectRef?: React.Ref<HTMLButtonElement>;
114
131
  phoneNumberInputRef?: React.Ref<HTMLInputElement>;
115
132
  disabled?: boolean;
@@ -120,6 +137,8 @@ export interface PhoneNumberInputProps
120
137
  export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
121
138
  {
122
139
  value = '',
140
+ name,
141
+ autoComplete,
123
142
  defaultCountry = 'US',
124
143
  onChange,
125
144
  countrySelectRef: countryRef,
@@ -161,6 +180,10 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
161
180
  const [currentMasks, setCurrentMasks] = useState([
162
181
  ...countriesPhoneInformation[0].masks,
163
182
  ]);
183
+ const [obfuscateValue, setObfuscateValue] = useState(false);
184
+ const [shouldFocusAfterClear, setShouldFocusAfterClear] = useState(false);
185
+ const internalInputRef = useRef<HTMLInputElement>(null);
186
+ const forkedInputRef = useForkRef(numberRef, internalInputRef);
164
187
 
165
188
  const countryOptions = useMemo(() => {
166
189
  return createCountryOptions(allowedCountryCodes);
@@ -183,7 +206,7 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
183
206
 
184
207
  const value = `${countryInformation.prefix}${stripNonNumericAfterCountryCode(phoneNumber)}`;
185
208
  lastOutputValueRef.current = value;
186
- onChange && onChange(value);
209
+ onChange && onChange(value, false);
187
210
  }
188
211
 
189
212
  useLayoutEffect(() => {
@@ -202,9 +225,14 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
202
225
  const lineNumber = stripNonNumericAfterCountryCode(newPhoneNumber);
203
226
  const outputValue = countryPrefix + lineNumber;
204
227
 
228
+ // Clear obfuscated state when user types manually
229
+ if (obfuscateValue) {
230
+ setObfuscateValue(false);
231
+ }
232
+
205
233
  lastOutputValueRef.current = outputValue;
206
234
  phoneNumber !== newPhoneNumber && setPhoneNumber(newPhoneNumber);
207
- onChange && onChange(outputValue);
235
+ onChange && onChange(outputValue, false);
208
236
  }
209
237
 
210
238
  function togglePhoneBook(e: React.MouseEvent<HTMLButtonElement>) {
@@ -219,9 +247,15 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
219
247
  setPhoneBookElement(null);
220
248
  }
221
249
 
222
- function handlePhoneBookOptionSelect(value: string) {
250
+ function handlePhoneBookOptionSelect(
251
+ value: string,
252
+ _label: string | undefined,
253
+ _isSuggestion: boolean,
254
+ obfuscate: boolean
255
+ ) {
223
256
  // Update the phone number with the selected value
224
- updatePhoneNumber(value);
257
+ setObfuscateValue(obfuscate);
258
+ updatePhoneNumber(value, obfuscate);
225
259
  closePhoneBook();
226
260
  }
227
261
 
@@ -244,8 +278,22 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
244
278
  return value;
245
279
  }
246
280
 
281
+ function handleObfuscatedInputChange(newValue: string) {
282
+ // When user types on a obfuscated input, clear the obfuscated state and start fresh
283
+ // The newValue will be the digits the user typed (mask filters to valid input)
284
+ setShouldFocusAfterClear(true);
285
+ setObfuscateValue(false);
286
+ setPhoneNumber(newValue);
287
+
288
+ const countryPrefix = countryCodeMap.get(countryCode)?.prefix;
289
+ const lineNumber = stripNonNumericAfterCountryCode(newValue);
290
+ const outputValue = countryPrefix + lineNumber;
291
+ lastOutputValueRef.current = outputValue;
292
+ onChange && onChange(outputValue, false);
293
+ }
294
+
247
295
  const updatePhoneNumber = useCallback(
248
- (value: string) => {
296
+ (value: string, obfuscate = false) => {
249
297
  const oldValue = lastOutputValueRef.current;
250
298
  const countryInformation = getCountryCodeFromValue(
251
299
  value,
@@ -259,7 +307,7 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
259
307
 
260
308
  if (oldValue !== value) {
261
309
  setPhoneNumber(phoneNumber);
262
- onChange && onChange(value);
310
+ onChange && onChange(value, obfuscate);
263
311
  }
264
312
  },
265
313
  [defaultCountry, selectedCountry, onChange]
@@ -269,6 +317,14 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
269
317
  updatePhoneNumber(value);
270
318
  }, [value, updatePhoneNumber]);
271
319
 
320
+ // Focus the input after transitioning from obfuscated to normal mode
321
+ useLayoutEffect(() => {
322
+ if (shouldFocusAfterClear && !obfuscateValue && internalInputRef.current) {
323
+ internalInputRef.current.focus();
324
+ setShouldFocusAfterClear(false);
325
+ }
326
+ }, [shouldFocusAfterClear, obfuscateValue]);
327
+
272
328
  return (
273
329
  <HStack
274
330
  ref={ref}
@@ -280,25 +336,55 @@ export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
280
336
  className={clsx(styles['phone-number-input-select'], 'phone-number-input-select')}
281
337
  ref={countryRef}
282
338
  width="auto"
283
- value={countryCode}
339
+ value={obfuscateValue ? '' : countryCode}
284
340
  onChange={changeCountry}
285
- disabled={disabled}
286
- data-is-disabled={disabled}
341
+ disabled={disabled || obfuscateValue}
342
+ data-is-disabled={disabled || obfuscateValue}
343
+ data-is-obfuscated={obfuscateValue}
344
+ placeholder={obfuscateValue ? '––' : undefined}
287
345
  >
288
346
  {countryOptions}
289
347
  </Select>
290
348
  <HStack width="flex" className={clsx(styles['phone-number-input-container'])}>
291
- <MaskInput
292
- ref={numberRef}
293
- value={phoneNumber}
294
- mask={currentMasks}
295
- onChange={transformValue}
296
- disabled={disabled}
297
- data-is-disabled={disabled}
298
- data-has-phone-book={showPhoneBook}
299
- className={clsx(styles['phone-number-input'], 'phone-number-input')}
300
- preparePasteValue={preparePasteValue}
301
- />
349
+ {obfuscateValue ? (
350
+ <MaskInput
351
+ key="obfuscated"
352
+ name={name}
353
+ autoComplete={autoComplete}
354
+ ref={forkedInputRef}
355
+ value=""
356
+ mask={createObfuscatedMasks(currentMasks)}
357
+ onChange={handleObfuscatedInputChange}
358
+ disabled={disabled}
359
+ data-is-disabled={disabled}
360
+ data-has-phone-book={showPhoneBook}
361
+ data-is-obfuscated={true}
362
+ className={clsx(
363
+ styles['phone-number-input'],
364
+ styles['phone-number-input-obfuscated'],
365
+ 'phone-number-input'
366
+ )}
367
+ preparePasteValue={() => ''}
368
+ prepareCopyValue={() => ''}
369
+ prepareCutValue={() => ''}
370
+ />
371
+ ) : (
372
+ <MaskInput
373
+ key="normal"
374
+ name={name}
375
+ autoComplete={autoComplete}
376
+ ref={forkedInputRef}
377
+ value={phoneNumber}
378
+ mask={currentMasks}
379
+ onChange={transformValue}
380
+ disabled={disabled}
381
+ data-is-disabled={disabled}
382
+ data-has-phone-book={showPhoneBook}
383
+ data-is-obfuscated={false}
384
+ className={clsx(styles['phone-number-input'], 'phone-number-input')}
385
+ preparePasteValue={preparePasteValue}
386
+ />
387
+ )}
302
388
  </HStack>
303
389
  {showPhoneBook && (
304
390
  <>
@@ -6,7 +6,12 @@ export interface SuggestionItemProps {
6
6
  isFocused: boolean;
7
7
  isSelected: boolean;
8
8
  option: React.ReactElement;
9
- onClick?: (value: string, label: string, isSuggestion: boolean) => void;
9
+ onClick?: (
10
+ value: string,
11
+ label: string,
12
+ isSuggestion: boolean,
13
+ obfuscate: boolean
14
+ ) => void;
10
15
  }
11
16
 
12
17
  export function SuggestionItem({
@@ -43,7 +48,12 @@ export function SuggestionItem({
43
48
  disabled={isDisabled}
44
49
  onClick={() => {
45
50
  if (!isDisabled && onClick) {
46
- onClick(option.props.value, option.props.label, true);
51
+ onClick(
52
+ option.props.value,
53
+ option.props.label,
54
+ true,
55
+ option.props.obfuscate ?? false
56
+ );
47
57
  }
48
58
  }}
49
59
  >
@@ -25,7 +25,8 @@ export interface SuggestionListProps
25
25
  onOptionSelect?: (
26
26
  value: string,
27
27
  label: string | undefined,
28
- isSuggestion: boolean
28
+ isSuggestion: boolean,
29
+ obfuscate: boolean
29
30
  ) => void;
30
31
  noSuggestionMessage?: React.ReactNode;
31
32
  trimCustomInput?: boolean;
@@ -112,7 +113,13 @@ export function SuggestionList({
112
113
  const optionValue = option?.props.value || value;
113
114
 
114
115
  requestAnimationFrame(() => {
115
- onOptionSelect && onOptionSelect(optionValue, label, isSuggestion);
116
+ onOptionSelect &&
117
+ onOptionSelect(
118
+ optionValue,
119
+ label,
120
+ isSuggestion,
121
+ option?.props.obfuscate ?? false
122
+ );
116
123
  });
117
124
 
118
125
  break;
@@ -126,7 +133,13 @@ export function SuggestionList({
126
133
  }
127
134
 
128
135
  requestAnimationFrame(() => {
129
- onOptionSelect && onOptionSelect(optionProps.value, optionProps.label, true);
136
+ onOptionSelect &&
137
+ onOptionSelect(
138
+ optionProps.value,
139
+ optionProps.label,
140
+ true,
141
+ optionProps.obfuscate ?? false
142
+ );
130
143
  });
131
144
  break;
132
145
  }
@@ -260,6 +273,12 @@ export function SuggestionList({
260
273
  const keywords = props.keywords?.map(k => k.toLocaleLowerCase()) || [];
261
274
  const optionValue = String(props.value).toLocaleLowerCase();
262
275
  const searchValue = value.toLocaleLowerCase();
276
+ const obfuscate = props.obfuscate ?? false;
277
+
278
+ // Obfuscated options can only be searched by label or keywords, not by value
279
+ if (obfuscate) {
280
+ return label.includes(searchValue) || keywords.some(k => k.includes(searchValue));
281
+ }
263
282
 
264
283
  return (
265
284
  label.includes(searchValue) ||
@@ -1,10 +1,10 @@
1
1
  import { clsx } from 'clsx';
2
2
  import React from 'react';
3
- import { HTMLAttributes } from 'react';
3
+ import { TextareaHTMLAttributes } from 'react';
4
4
  import styles from './textarea.module.css';
5
5
 
6
6
  export interface TextareaProps
7
- extends Omit<HTMLAttributes<HTMLTextAreaElement>, 'onChange'> {
7
+ extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> {
8
8
  value?: string;
9
9
  width?: string;
10
10
  height?: string;
@@ -1,7 +1,6 @@
1
1
  .footer {
2
2
  width: 100%;
3
3
  min-height: 0;
4
- border-top: 1px solid rgb(221, 221, 221);
5
4
  padding: var(--padding) var(--padding-large);
6
5
  gap: var(--gap);
7
6
  }
@@ -18,7 +18,7 @@ export const Footer = React.forwardRef<HTMLElement, FooterProps>(function Footer
18
18
  <HStack
19
19
  ref={ref}
20
20
  as="footer"
21
- className={clsx(styles.footer, className, 'footer', 'tcn-footer')}
21
+ className={clsx(styles.footer, className, 'tcn-footer')}
22
22
  data-hierarchy={hierarchy}
23
23
  data-size={size}
24
24
  {...props}
@@ -4,7 +4,6 @@ import { HStack, type HStackProps } from '../../stacks/h_stack.js';
4
4
  import type { Hierarchy, Size } from '../../utils/index.js';
5
5
  import styles from './header.module.css';
6
6
 
7
- // UtilityBar
8
7
  export interface HeaderProps extends Omit<HStackProps, 'as'> {
9
8
  hierarchy?: Hierarchy;
10
9
  size?: Size;
@@ -1,3 +1,5 @@
1
+ export * from './body/h_body.js';
2
+ export * from './body/v_body.js';
1
3
  export * from './column/column.js';
2
4
  export * from './divider/divider.js';
3
5
  export * from './footer/footer.js';
@@ -8,5 +10,4 @@ export * from './list/list.js';
8
10
  export * from './list/section_header.js';
9
11
  export * from './sidebar_end/sidebar_end.js';
10
12
  export * from './sidebar_start/sidebar_start.js';
11
- export * from './body/h_body.js';
12
- export * from './body/v_body.js';
13
+ export * from './utility_bar/utility_bar.js';
@@ -4,14 +4,22 @@ import { clsx } from 'clsx';
4
4
 
5
5
  export interface ItemProps extends Omit<HStackProps, 'as' | 'children'> {
6
6
  children?: React.ReactNode;
7
+ selected?: boolean;
7
8
  }
8
9
 
9
10
  export const Item = React.forwardRef<HTMLDivElement, ItemProps>(function Item(
10
- { children, className, ...props }: ItemProps,
11
+ { children, className, selected = false, ...props }: ItemProps,
11
12
  ref
12
13
  ) {
13
14
  return (
14
- <HStack ref={ref} as="li" className={clsx('item', className)} {...props}>
15
+ <HStack
16
+ ref={ref}
17
+ as="li"
18
+ data-hierarchy={selected ? 'primary' : 'tertiary'}
19
+ data-is-selected={selected}
20
+ className={clsx('tcn-item', 'tcn-interactive', className)}
21
+ {...props}
22
+ >
15
23
  {children}
16
24
  </HStack>
17
25
  );
@@ -6,7 +6,7 @@ export interface ListProps extends Omit<VStackProps, 'as'> {
6
6
  isOrdered?: boolean;
7
7
  }
8
8
 
9
- export const List = React.forwardRef<HTMLDivElement, ListProps>(function Item(
9
+ export const List = React.forwardRef<HTMLDivElement, ListProps>(function List(
10
10
  { children, className, isOrdered = false, ...props }: ListProps,
11
11
  ref
12
12
  ) {
@@ -14,7 +14,7 @@ export const List = React.forwardRef<HTMLDivElement, ListProps>(function Item(
14
14
  <VStack
15
15
  ref={ref}
16
16
  as={isOrdered ? 'ol' : 'ul'}
17
- className={clsx('list', className)}
17
+ className={clsx('tcn-list', className)}
18
18
  {...props}
19
19
  >
20
20
  {children}
@@ -0,0 +1,40 @@
1
+ import { ZStack } from '../../stacks/z_stack.js';
2
+ import { Frame, type FrameOwnProps } from './frame.js';
3
+ import { DragHandle } from '../../utils/dnd/handle.js';
4
+ import { Title } from '../../typography/title/title.js';
5
+ import { BodyText } from '../../typography/index.js';
6
+ import { Header } from '../../layouts/index.js';
7
+ import styles from './frame_stories.module.css';
8
+ export default {
9
+ title: 'Overlay/Floating/Frame',
10
+ component: Frame,
11
+ tags: ['autodocs'],
12
+
13
+ args: {
14
+ isOpen: true,
15
+ draggable: true,
16
+ veil: false,
17
+ },
18
+ };
19
+
20
+ export const FrameStory = (args: Omit<FrameOwnProps, 'children'>) => {
21
+ return (
22
+ <ZStack height="100%" width="100%" minHeight="600px">
23
+ <Frame
24
+ width="300px"
25
+ height="300px"
26
+ className={styles['sb-frame-container']}
27
+ {...args}
28
+ >
29
+ <Header className={styles['sb-frame-header']}>
30
+ <Title> This is a frame</Title>
31
+ </Header>
32
+ <DragHandle>
33
+ <Header className={styles['sb-frame-header']}>
34
+ <BodyText> You can drag here.</BodyText>
35
+ </Header>
36
+ </DragHandle>
37
+ </Frame>
38
+ </ZStack>
39
+ );
40
+ };
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { ZStack, type ZStackProps } from '../../stacks/index.js';
3
+ import { Portal } from '../portal/portal.js';
4
+ import { Draggable } from '../../utils/dnd/draggable/draggable.js';
5
+
6
+ export interface FrameOwnProps {
7
+ isOpen?: boolean;
8
+ children?: React.ReactNode;
9
+ draggable?: boolean;
10
+ veil?: boolean;
11
+ }
12
+
13
+ export type FrameProps = ZStackProps & FrameOwnProps;
14
+
15
+ export const Frame = React.forwardRef<HTMLDialogElement, FrameProps>(function Frame(
16
+ { children, isOpen = false, draggable = true, veil = false, ...rest }: FrameProps,
17
+ ref
18
+ ) {
19
+ if (!isOpen) {
20
+ return null;
21
+ }
22
+
23
+ return (
24
+ <Portal>
25
+ <ZStack width="100%" height="100%" data-is-veil={veil} className="tcn-frame">
26
+ <Draggable draggable={draggable}>
27
+ <ZStack ref={ref} {...rest}>
28
+ {children}
29
+ </ZStack>
30
+ </Draggable>
31
+ </ZStack>
32
+ </Portal>
33
+ );
34
+ });