tyrell-react 1.0.0-RC6

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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +410 -0
  3. package/dist/components/TyButton.d.ts +50 -0
  4. package/dist/components/TyButton.d.ts.map +1 -0
  5. package/dist/components/TyButton.js +68 -0
  6. package/dist/components/TyButton.js.map +1 -0
  7. package/dist/components/TyCalendar.d.ts +63 -0
  8. package/dist/components/TyCalendar.d.ts.map +1 -0
  9. package/dist/components/TyCalendar.js +127 -0
  10. package/dist/components/TyCalendar.js.map +1 -0
  11. package/dist/components/TyCalendarMonth.d.ts +32 -0
  12. package/dist/components/TyCalendarMonth.d.ts.map +1 -0
  13. package/dist/components/TyCalendarMonth.js +54 -0
  14. package/dist/components/TyCalendarMonth.js.map +1 -0
  15. package/dist/components/TyCalendarNavigation.d.ts +21 -0
  16. package/dist/components/TyCalendarNavigation.d.ts.map +1 -0
  17. package/dist/components/TyCalendarNavigation.js +50 -0
  18. package/dist/components/TyCalendarNavigation.js.map +1 -0
  19. package/dist/components/TyCheckbox.d.ts +39 -0
  20. package/dist/components/TyCheckbox.d.ts.map +1 -0
  21. package/dist/components/TyCheckbox.js +83 -0
  22. package/dist/components/TyCheckbox.js.map +1 -0
  23. package/dist/components/TyCopy.d.ts +21 -0
  24. package/dist/components/TyCopy.d.ts.map +1 -0
  25. package/dist/components/TyCopy.js +42 -0
  26. package/dist/components/TyCopy.js.map +1 -0
  27. package/dist/components/TyDatePicker.d.ts +45 -0
  28. package/dist/components/TyDatePicker.d.ts.map +1 -0
  29. package/dist/components/TyDatePicker.js +119 -0
  30. package/dist/components/TyDatePicker.js.map +1 -0
  31. package/dist/components/TyDropdown.d.ts +56 -0
  32. package/dist/components/TyDropdown.d.ts.map +1 -0
  33. package/dist/components/TyDropdown.js +110 -0
  34. package/dist/components/TyDropdown.js.map +1 -0
  35. package/dist/components/TyIcon.d.ts +17 -0
  36. package/dist/components/TyIcon.d.ts.map +1 -0
  37. package/dist/components/TyIcon.js +41 -0
  38. package/dist/components/TyIcon.js.map +1 -0
  39. package/dist/components/TyInput.d.ts +65 -0
  40. package/dist/components/TyInput.d.ts.map +1 -0
  41. package/dist/components/TyInput.js +127 -0
  42. package/dist/components/TyInput.js.map +1 -0
  43. package/dist/components/TyModal.d.ts +29 -0
  44. package/dist/components/TyModal.d.ts.map +1 -0
  45. package/dist/components/TyModal.js +74 -0
  46. package/dist/components/TyModal.js.map +1 -0
  47. package/dist/components/TyMultiselect.d.ts +51 -0
  48. package/dist/components/TyMultiselect.d.ts.map +1 -0
  49. package/dist/components/TyMultiselect.js +107 -0
  50. package/dist/components/TyMultiselect.js.map +1 -0
  51. package/dist/components/TyOption.d.ts +10 -0
  52. package/dist/components/TyOption.d.ts.map +1 -0
  53. package/dist/components/TyOption.js +25 -0
  54. package/dist/components/TyOption.js.map +1 -0
  55. package/dist/components/TyPopup.d.ts +24 -0
  56. package/dist/components/TyPopup.d.ts.map +1 -0
  57. package/dist/components/TyPopup.js +61 -0
  58. package/dist/components/TyPopup.js.map +1 -0
  59. package/dist/components/TyRadio.d.ts +20 -0
  60. package/dist/components/TyRadio.d.ts.map +1 -0
  61. package/dist/components/TyRadio.js +46 -0
  62. package/dist/components/TyRadio.js.map +1 -0
  63. package/dist/components/TyRadioGroup.d.ts +40 -0
  64. package/dist/components/TyRadioGroup.d.ts.map +1 -0
  65. package/dist/components/TyRadioGroup.js +58 -0
  66. package/dist/components/TyRadioGroup.js.map +1 -0
  67. package/dist/components/TyResizeObserver.d.ts +11 -0
  68. package/dist/components/TyResizeObserver.d.ts.map +1 -0
  69. package/dist/components/TyResizeObserver.js +28 -0
  70. package/dist/components/TyResizeObserver.js.map +1 -0
  71. package/dist/components/TyScrollContainer.d.ts +25 -0
  72. package/dist/components/TyScrollContainer.d.ts.map +1 -0
  73. package/dist/components/TyScrollContainer.js +43 -0
  74. package/dist/components/TyScrollContainer.js.map +1 -0
  75. package/dist/components/TyStep.d.ts +17 -0
  76. package/dist/components/TyStep.d.ts.map +1 -0
  77. package/dist/components/TyStep.js +35 -0
  78. package/dist/components/TyStep.js.map +1 -0
  79. package/dist/components/TySwitch.d.ts +35 -0
  80. package/dist/components/TySwitch.d.ts.map +1 -0
  81. package/dist/components/TySwitch.js +69 -0
  82. package/dist/components/TySwitch.js.map +1 -0
  83. package/dist/components/TyTab.d.ts +13 -0
  84. package/dist/components/TyTab.d.ts.map +1 -0
  85. package/dist/components/TyTab.js +32 -0
  86. package/dist/components/TyTab.js.map +1 -0
  87. package/dist/components/TyTabs.d.ts +23 -0
  88. package/dist/components/TyTabs.d.ts.map +1 -0
  89. package/dist/components/TyTabs.js +48 -0
  90. package/dist/components/TyTabs.js.map +1 -0
  91. package/dist/components/TyTag.d.ts +22 -0
  92. package/dist/components/TyTag.d.ts.map +1 -0
  93. package/dist/components/TyTag.js +45 -0
  94. package/dist/components/TyTag.js.map +1 -0
  95. package/dist/components/TyTextarea.d.ts +37 -0
  96. package/dist/components/TyTextarea.d.ts.map +1 -0
  97. package/dist/components/TyTextarea.js +113 -0
  98. package/dist/components/TyTextarea.js.map +1 -0
  99. package/dist/components/TyTooltip.d.ts +17 -0
  100. package/dist/components/TyTooltip.d.ts.map +1 -0
  101. package/dist/components/TyTooltip.js +40 -0
  102. package/dist/components/TyTooltip.js.map +1 -0
  103. package/dist/components/TyWizard.d.ts +26 -0
  104. package/dist/components/TyWizard.d.ts.map +1 -0
  105. package/dist/components/TyWizard.js +50 -0
  106. package/dist/components/TyWizard.js.map +1 -0
  107. package/dist/components/index.d.ts +105 -0
  108. package/dist/components/index.d.ts.map +1 -0
  109. package/dist/components/index.js +112 -0
  110. package/dist/components/index.js.map +1 -0
  111. package/dist/utils/react-version.d.ts +2 -0
  112. package/dist/utils/react-version.d.ts.map +1 -0
  113. package/dist/utils/react-version.js +8 -0
  114. package/dist/utils/react-version.js.map +1 -0
  115. package/package.json +46 -0
  116. package/src/components/EventConventionTest.tsx +155 -0
  117. package/src/components/TyButton.tsx +145 -0
  118. package/src/components/TyCalendar.tsx +248 -0
  119. package/src/components/TyCalendarMonth.tsx +108 -0
  120. package/src/components/TyCalendarNavigation.tsx +91 -0
  121. package/src/components/TyCheckbox.tsx +152 -0
  122. package/src/components/TyCopy.tsx +78 -0
  123. package/src/components/TyDatePicker.tsx +220 -0
  124. package/src/components/TyDropdown.tsx +225 -0
  125. package/src/components/TyIcon.tsx +72 -0
  126. package/src/components/TyInput.tsx +232 -0
  127. package/src/components/TyModal.tsx +142 -0
  128. package/src/components/TyMultiselect.tsx +203 -0
  129. package/src/components/TyOption.tsx +42 -0
  130. package/src/components/TyPopup.tsx +111 -0
  131. package/src/components/TyRadio.tsx +70 -0
  132. package/src/components/TyRadioGroup.tsx +121 -0
  133. package/src/components/TyResizeObserver.tsx +54 -0
  134. package/src/components/TyScrollContainer.tsx +87 -0
  135. package/src/components/TyStep.tsx +71 -0
  136. package/src/components/TySwitch.tsx +122 -0
  137. package/src/components/TyTab.tsx +63 -0
  138. package/src/components/TyTabs.tsx +93 -0
  139. package/src/components/TyTag.tsx +79 -0
  140. package/src/components/TyTextarea.tsx +177 -0
  141. package/src/components/TyTooltip.tsx +83 -0
  142. package/src/components/TyWizard.tsx +99 -0
  143. package/src/components/index.ts +251 -0
  144. package/src/utils/react-version.ts +8 -0
@@ -0,0 +1,72 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ // Type definitions for Ty Icon component
4
+ export interface TyIconProps extends React.HTMLAttributes<HTMLElement> {
5
+ /** Icon name from the icon registry (e.g., 'home', 'star', 'settings') */
6
+ name: string;
7
+
8
+ /** Icon size - relative (em-based) or absolute (pixel-based) */
9
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '12' | '14' | '16' | '18' | '20' | '24' | '32' | '48';
10
+
11
+ /** Enable spinning animation */
12
+ spin?: boolean;
13
+
14
+ /** Enable pulse animation */
15
+ pulse?: boolean;
16
+
17
+ /** Animation tempo/speed */
18
+ tempo?: 'slow' | 'fast';
19
+
20
+ /** Additional CSS classes */
21
+ className?: string;
22
+ }
23
+
24
+ // React wrapper for ty-icon web component
25
+ export const TyIcon = React.forwardRef<HTMLElement, TyIconProps>(
26
+ ({ name, size, spin, pulse, tempo, className, ...props }, ref) => {
27
+ const elementRef = useRef<HTMLElement>(null);
28
+
29
+ // Handle ref forwarding
30
+ useEffect(() => {
31
+ if (ref && elementRef.current) {
32
+ if (typeof ref === 'function') {
33
+ ref(elementRef.current);
34
+ } else {
35
+ ref.current = elementRef.current;
36
+ }
37
+ }
38
+ }, [ref]);
39
+
40
+ // Convert React props to web component attributes
41
+ const webComponentProps: Record<string, any> = {
42
+ ...props,
43
+ name,
44
+ ref: elementRef,
45
+ };
46
+
47
+ // Add optional attributes only if they have values
48
+ if (size) {
49
+ webComponentProps.size = size;
50
+ }
51
+
52
+ if (spin) {
53
+ webComponentProps.spin = ''; // Boolean attributes as empty string
54
+ }
55
+
56
+ if (pulse) {
57
+ webComponentProps.pulse = ''; // Boolean attributes as empty string
58
+ }
59
+
60
+ if (tempo) {
61
+ webComponentProps.tempo = tempo;
62
+ }
63
+
64
+ if (className) {
65
+ webComponentProps.class = className; // HTML attribute is 'class', not 'className'
66
+ }
67
+
68
+ return React.createElement('ty-icon', webComponentProps);
69
+ }
70
+ );
71
+
72
+ TyIcon.displayName = 'TyIcon';
@@ -0,0 +1,232 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+
4
+ // Event detail structure for ty-input events
5
+ export interface TyInputEventDetail {
6
+ value: any; // shadow value (processed/parsed)
7
+ formattedValue: string; // user-visible formatted value
8
+ rawValue: string; // raw input value
9
+ originalEvent: Event; // original DOM event
10
+ }
11
+
12
+ export interface TyInputCSSProperties extends React.CSSProperties {
13
+ '--input-bg'?: string;
14
+ '--input-color'?: string;
15
+ '--input-border'?: string;
16
+ '--input-border-hover'?: string;
17
+ '--input-border-focus'?: string;
18
+ '--input-shadow-focus'?: string;
19
+ '--input-placeholder'?: string;
20
+ '--input-disabled-bg'?: string;
21
+ '--input-disabled-border'?: string;
22
+ '--input-disabled-color'?: string;
23
+ }
24
+
25
+ // Type definitions for Ty Input component
26
+ export interface TyInputProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onFocus' | 'onBlur' | 'style'> {
27
+ style?: TyInputCSSProperties;
28
+ /** Input type */
29
+ type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'
30
+ | 'currency' | 'percent' | 'compact';
31
+
32
+ /** Semantic styling variant */
33
+ flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
34
+
35
+ /** Input size */
36
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
37
+
38
+ /** Input value */
39
+ value?: string;
40
+
41
+ /** Placeholder text */
42
+ placeholder?: string;
43
+
44
+ /** Input label */
45
+ label?: string;
46
+
47
+ /** Error message */
48
+ error?: string;
49
+
50
+ /** Disable the input */
51
+ disabled?: boolean;
52
+
53
+ /** Required field */
54
+ required?: boolean;
55
+
56
+ /** Form field name for form submission */
57
+ name?: string;
58
+
59
+ /** Checked state for checkbox inputs */
60
+ checked?: boolean;
61
+
62
+ // Numeric formatting props
63
+ currency?: string;
64
+ locale?: string;
65
+ precision?: string | number;
66
+
67
+ /** Debounce in milliseconds (0-5000) */
68
+ debounce?: number;
69
+
70
+ // React event handlers - override with our custom types
71
+ /**
72
+ * Fires on every keystroke (React convention)
73
+ * Maps to native 'input' event from ty-input
74
+ */
75
+ onChange?: (event: CustomEvent<TyInputEventDetail>) => void;
76
+
77
+ /**
78
+ * Fires on blur if value changed (native DOM behavior)
79
+ * Maps to native 'change' event from ty-input
80
+ */
81
+ onChangeCommit?: (event: CustomEvent<TyInputEventDetail>) => void;
82
+
83
+ /** Standard focus event */
84
+ onFocus?: (event: FocusEvent) => void;
85
+
86
+ /** Standard blur event */
87
+ onBlur?: (event: FocusEvent) => void;
88
+ }
89
+
90
+ // One-time global warning flags so we don't spam the console.
91
+ let _warnedOnInputProp = false;
92
+
93
+ // React wrapper for ty-input web component
94
+ export const TyInput = React.forwardRef<HTMLElement, TyInputProps>(
95
+ ({ onChange, onChangeCommit, onFocus, onBlur, disabled, name, checked, debounce, ...props }, ref) => {
96
+ const elementRef = useRef<HTMLElement>(null);
97
+
98
+ // Catch the most common mistake: passing `onInput` (React's prop) instead
99
+ // of `onChange` (our wrapper's prop). React's synthetic-event system
100
+ // strips event.detail, so the user's handler crashes on `e.detail.value`.
101
+ // Forward to onChange and log a one-shot warning.
102
+ const onInputProp = (props as any).onInput as ((e: any) => void) | undefined;
103
+ if (onInputProp && !onChange) {
104
+ if (!_warnedOnInputProp) {
105
+ _warnedOnInputProp = true;
106
+ console.warn(
107
+ '[tyrell-react] <TyInput> received `onInput`. ' +
108
+ 'React strips event.detail; use `onChange` instead — it receives the raw CustomEvent. ' +
109
+ 'Forwarding for now, but please rename the prop.'
110
+ );
111
+ }
112
+ onChange = onInputProp;
113
+ }
114
+ // Either way, drop onInput from spread so React doesn't double-wire it.
115
+ delete (props as any).onInput;
116
+
117
+ // Map onChange to input event (React convention)
118
+ const handleInput = useCallback((event: CustomEvent<TyInputEventDetail>) => {
119
+ if (onChange) {
120
+ onChange(event);
121
+ }
122
+ }, [onChange]);
123
+
124
+ // Map onChangeCommit to change event (blur behavior)
125
+ const handleChangeCommit = useCallback((event: CustomEvent<TyInputEventDetail>) => {
126
+ if (onChangeCommit) {
127
+ onChangeCommit(event);
128
+ }
129
+ }, [onChangeCommit]);
130
+
131
+ const handleFocus = useCallback((event: FocusEvent) => {
132
+ if (onFocus) {
133
+ onFocus(event);
134
+ }
135
+ }, [onFocus]);
136
+
137
+ const handleBlur = useCallback((event: FocusEvent) => {
138
+ if (onBlur) {
139
+ onBlur(event);
140
+ }
141
+ }, [onBlur]);
142
+
143
+ useEffect(() => {
144
+ const element = elementRef.current;
145
+ if (!element) return;
146
+
147
+ // Listen for custom input/change events from ty-input
148
+ // Map onChange → input event (React convention)
149
+ if (onChange) {
150
+ element.addEventListener('input', handleInput as EventListener);
151
+ }
152
+
153
+ // Map onChangeCommit → change event (blur behavior)
154
+ if (onChangeCommit) {
155
+ element.addEventListener('change', handleChangeCommit as EventListener);
156
+ }
157
+
158
+ // Listen for standard focus/blur events
159
+ if (onFocus) {
160
+ element.addEventListener('focus', handleFocus as EventListener);
161
+ }
162
+
163
+ if (onBlur) {
164
+ element.addEventListener('blur', handleBlur as EventListener);
165
+ }
166
+
167
+ return () => {
168
+ if (onChange) {
169
+ element.removeEventListener('input', handleInput as EventListener);
170
+ }
171
+ if (onChangeCommit) {
172
+ element.removeEventListener('change', handleChangeCommit as EventListener);
173
+ }
174
+ if (onFocus) {
175
+ element.removeEventListener('focus', handleFocus as EventListener);
176
+ }
177
+ if (onBlur) {
178
+ element.removeEventListener('blur', handleBlur as EventListener);
179
+ }
180
+ };
181
+ }, [handleInput, handleChangeCommit, handleFocus, handleBlur, onChange, onChangeCommit, onFocus, onBlur]);
182
+
183
+ // Handle ref forwarding
184
+ useEffect(() => {
185
+ if (ref && elementRef.current) {
186
+ if (typeof ref === 'function') {
187
+ ref(elementRef.current);
188
+ } else {
189
+ ref.current = elementRef.current;
190
+ }
191
+ }
192
+ }, [ref]);
193
+
194
+ // Imperatively sync `value` to the underlying element's property whenever
195
+ // the React prop changes. React 18's prop-to-property bridging for custom
196
+ // elements is unreliable for empty strings, so we set the property directly
197
+ // to guarantee resets (`value=""`) clear the visible content. React 19+
198
+ // handles this natively, so the effect short-circuits there.
199
+ useEffect(() => {
200
+ if (!needsPropertyBridge) return;
201
+ const element = elementRef.current as any;
202
+ if (!element) return;
203
+ const next = (props as any).value ?? '';
204
+ if (element.value !== next) {
205
+ element.value = next;
206
+ }
207
+ }, [(props as any).value]);
208
+
209
+ // Convert React props to web component attributes
210
+ const webComponentProps: Record<string, any> = {
211
+ ...props,
212
+ ref: elementRef,
213
+ };
214
+
215
+ // Add conditional attributes
216
+ if (disabled) webComponentProps.disabled = '';
217
+ if (checked) webComponentProps.checked = '';
218
+
219
+ // Add string attributes
220
+ if (name) webComponentProps.name = name;
221
+
222
+ // Add debounce attribute
223
+ if (debounce !== undefined) webComponentProps.debounce = debounce;
224
+
225
+ return React.createElement(
226
+ 'ty-input',
227
+ webComponentProps
228
+ );
229
+ }
230
+ );
231
+
232
+ TyInput.displayName = 'TyInput';
@@ -0,0 +1,142 @@
1
+ import React, { useEffect, useRef, useImperativeHandle } from 'react';
2
+
3
+ // Event detail structure for modal events
4
+ export interface TyModalEventDetail {
5
+ reason?: 'programmatic' | 'native' | 'backdrop' | 'escape' | 'close-button';
6
+ returnValue?: string;
7
+ }
8
+
9
+ // Type definitions for Ty Modal component
10
+ export interface TyModalProps extends React.HTMLAttributes<HTMLElement> {
11
+ /** Controls modal visibility */
12
+ open?: boolean;
13
+
14
+ /** Show backdrop behind modal (default: true) */
15
+ backdrop?: boolean;
16
+
17
+ /** Allow closing modal by clicking backdrop (default: true) */
18
+ closeOnOutsideClick?: boolean;
19
+
20
+ /** Allow closing modal with Escape key (default: true) */
21
+ closeOnEscape?: boolean;
22
+
23
+ /** Require confirmation before closing when there are unsaved changes */
24
+ protected?: boolean;
25
+
26
+ /** React event handlers */
27
+ onOpen?: (event: CustomEvent<TyModalEventDetail>) => void;
28
+ onClose?: (event: CustomEvent<TyModalEventDetail>) => void;
29
+
30
+ /** Modal content */
31
+ children?: React.ReactNode;
32
+ }
33
+
34
+ // Ref interface for imperative methods
35
+ export interface TyModalRef {
36
+ show: () => void;
37
+ hide: () => void;
38
+ element: HTMLElement | null;
39
+ }
40
+
41
+ // React wrapper for ty-modal web component
42
+ export const TyModal = React.forwardRef<TyModalRef, TyModalProps>(
43
+ ({
44
+ open,
45
+ backdrop,
46
+ closeOnOutsideClick,
47
+ closeOnEscape,
48
+ protected: isProtected,
49
+ onOpen,
50
+ onClose,
51
+ children,
52
+ ...props
53
+ }, ref) => {
54
+ const elementRef = useRef<HTMLElement>(null);
55
+
56
+ // Expose imperative methods through ref
57
+ useImperativeHandle(ref, () => ({
58
+ show: () => {
59
+ if (elementRef.current && typeof (elementRef.current as any).show === 'function') {
60
+ (elementRef.current as any).show();
61
+ }
62
+ },
63
+ hide: () => {
64
+ if (elementRef.current && typeof (elementRef.current as any).hide === 'function') {
65
+ (elementRef.current as any).hide();
66
+ }
67
+ },
68
+ element: elementRef.current,
69
+ }), []);
70
+
71
+ // Handle modal events
72
+ useEffect(() => {
73
+ const element = elementRef.current;
74
+ if (!element) return;
75
+
76
+ const handleOpen = (event: CustomEvent<TyModalEventDetail>) => {
77
+ if (onOpen) {
78
+ onOpen(event);
79
+ }
80
+ };
81
+
82
+ const handleClose = (event: CustomEvent<TyModalEventDetail>) => {
83
+ if (onClose) {
84
+ onClose(event);
85
+ }
86
+ };
87
+
88
+ // Listen for custom modal events
89
+ if (onOpen) {
90
+ element.addEventListener('open', handleOpen as EventListener);
91
+ }
92
+
93
+ if (onClose) {
94
+ element.addEventListener('close', handleClose as EventListener);
95
+ }
96
+
97
+ return () => {
98
+ if (onOpen) {
99
+ element.removeEventListener('open', handleOpen as EventListener);
100
+ }
101
+ if (onClose) {
102
+ element.removeEventListener('close', handleClose as EventListener);
103
+ }
104
+ };
105
+ }, [onOpen, onClose]);
106
+
107
+ // Convert React props to web component attributes
108
+ const webComponentProps: Record<string, any> = {
109
+ ...props,
110
+ ref: elementRef,
111
+ };
112
+
113
+ // Add boolean attributes using correct HTML attribute names
114
+ if (open) {
115
+ webComponentProps.open = ''; // Boolean attributes as empty string
116
+ }
117
+
118
+ if (backdrop === false) { // Only set if explicitly false (default is true)
119
+ webComponentProps.backdrop = 'false';
120
+ }
121
+
122
+ if (closeOnOutsideClick === false) { // Only set if explicitly false (default is true)
123
+ webComponentProps['close-on-outside-click'] = 'false';
124
+ }
125
+
126
+ if (closeOnEscape === false) { // Only set if explicitly false (default is true)
127
+ webComponentProps['close-on-escape'] = 'false';
128
+ }
129
+
130
+ if (isProtected) {
131
+ webComponentProps.protected = ''; // Boolean attributes as empty string
132
+ }
133
+
134
+ return React.createElement(
135
+ 'ty-modal',
136
+ webComponentProps,
137
+ children
138
+ );
139
+ }
140
+ );
141
+
142
+ TyModal.displayName = 'TyModal';
@@ -0,0 +1,203 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { needsPropertyBridge } from '../utils/react-version';
3
+
4
+ // Type definitions for Ty Multiselect component
5
+ export interface TyMultiselectEventDetail {
6
+ /** Array of currently selected values */
7
+ values: string[];
8
+ /** Action that triggered the change: "add" | "remove" */
9
+ action: 'add' | 'remove';
10
+ /** The specific item that was added or removed */
11
+ item: string;
12
+ }
13
+
14
+ export interface TyMultiselectProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'style'> {
15
+ style?: import('./TyInput').TyInputCSSProperties;
16
+ /** Current selected values as comma-separated string or array */
17
+ value?: string | string[];
18
+
19
+ /** Placeholder text when no items are selected */
20
+ placeholder?: string;
21
+
22
+ /** Disable the multiselect component */
23
+ disabled?: boolean;
24
+
25
+ /** Make the multiselect read-only */
26
+ readonly?: boolean;
27
+
28
+ /** Semantic styling variant */
29
+ flavor?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'neutral';
30
+
31
+ /** Label text for the multiselect */
32
+ label?: string;
33
+
34
+ /** Mark the field as required */
35
+ required?: boolean;
36
+
37
+ /**
38
+ * Switch to external (remote) search mode. Default is `false` — the
39
+ * multiselect filters its own children client-side. Set to `true` when you
40
+ * want to handle filtering yourself (e.g. server-side): the component will
41
+ * dispatch `search` events on each keystroke, and you must update the children
42
+ * in response. The `onSearch` prop receives those events.
43
+ */
44
+ externalSearch?: boolean;
45
+
46
+ /** Debounce in milliseconds (0-5000) */
47
+ debounce?: number;
48
+
49
+ /** Mobile section label for selected items */
50
+ selectedLabel?: string;
51
+
52
+ /** Form field name for form submission */
53
+ name?: string;
54
+
55
+ /** Callback when selection changes */
56
+ onChange?: (event: CustomEvent<TyMultiselectEventDetail>) => void;
57
+
58
+ /** Callback fired on each search input change (debounced by `debounce`). Use for external/server-side filtering. */
59
+ onSearch?: (event: CustomEvent<{ query: string; element: HTMLElement }>) => void;
60
+
61
+ /** Children should be TyTag components, not TyOption */
62
+ children?: React.ReactNode;
63
+ }
64
+
65
+ // React wrapper for ty-multiselect web component
66
+ export const TyMultiselect = React.forwardRef<HTMLElement, TyMultiselectProps>(
67
+ ({
68
+ value,
69
+ placeholder,
70
+ disabled,
71
+ readonly,
72
+ flavor,
73
+ label,
74
+ required,
75
+ externalSearch,
76
+ debounce,
77
+ selectedLabel,
78
+ name,
79
+ onChange,
80
+ onSearch,
81
+ children,
82
+ ...props
83
+ }, ref) => {
84
+ const elementRef = useRef<HTMLElement>(null);
85
+
86
+ // Handle ref forwarding
87
+ useEffect(() => {
88
+ if (ref && elementRef.current) {
89
+ if (typeof ref === 'function') {
90
+ ref(elementRef.current);
91
+ } else {
92
+ ref.current = elementRef.current;
93
+ }
94
+ }
95
+ }, [ref]);
96
+
97
+ // Imperatively sync `value` to the underlying property so resets
98
+ // (`value=""` or null) reliably clear the visible selection.
99
+ // React 18 workaround; React 19+ handles this natively.
100
+ useEffect(() => {
101
+ if (!needsPropertyBridge) return;
102
+ const element = elementRef.current as any;
103
+ if (!element) return;
104
+ const next = (props as any).value ?? '';
105
+ if (element.value !== next) {
106
+ element.value = next;
107
+ }
108
+ }, [(props as any).value]);
109
+
110
+ // Handle change events
111
+ const handleChange = useCallback((event: Event) => {
112
+ const customEvent = event as CustomEvent<TyMultiselectEventDetail>;
113
+ if (onChange) {
114
+ onChange(customEvent);
115
+ }
116
+ }, [onChange]);
117
+
118
+ const handleSearch = useCallback((event: Event) => {
119
+ const customEvent = event as CustomEvent<{ query: string; element: HTMLElement }>;
120
+ if (onSearch) {
121
+ onSearch(customEvent);
122
+ }
123
+ }, [onSearch]);
124
+
125
+ // Set up event listeners
126
+ useEffect(() => {
127
+ const element = elementRef.current;
128
+ if (!element) return;
129
+
130
+ if (onChange) element.addEventListener('change', handleChange);
131
+ if (onSearch) element.addEventListener('search', handleSearch);
132
+
133
+ return () => {
134
+ if (onChange) element.removeEventListener('change', handleChange);
135
+ if (onSearch) element.removeEventListener('search', handleSearch);
136
+ };
137
+ }, [handleChange, handleSearch, onChange, onSearch]);
138
+
139
+ // Convert React props to web component attributes
140
+ const webComponentProps: Record<string, any> = {
141
+ ...props,
142
+ ref: elementRef,
143
+ };
144
+
145
+ // Handle value conversion (array to comma-separated string)
146
+ if (value !== undefined) {
147
+ const valueString = Array.isArray(value) ? value.join(',') : value;
148
+ webComponentProps.value = valueString;
149
+ }
150
+
151
+ // Add optional attributes only if they have values
152
+ if (placeholder) {
153
+ webComponentProps.placeholder = placeholder;
154
+ }
155
+
156
+ if (disabled) {
157
+ webComponentProps.disabled = ''; // Boolean attributes as empty string
158
+ }
159
+
160
+ if (readonly) {
161
+ webComponentProps.readonly = ''; // Boolean attributes as empty string
162
+ }
163
+
164
+ if (flavor) {
165
+ webComponentProps.flavor = flavor;
166
+ }
167
+
168
+ if (label) {
169
+ webComponentProps.label = label;
170
+ }
171
+
172
+ if (required) {
173
+ webComponentProps.required = ''; // Boolean attributes as empty string
174
+ }
175
+
176
+ if (name) {
177
+ webComponentProps.name = name;
178
+ }
179
+
180
+ // External (remote) search mode: parent owns filtering, multiselect dispatches search events
181
+ if (externalSearch) {
182
+ webComponentProps['external-search'] = '';
183
+ }
184
+
185
+ // Add debounce attribute
186
+ if (debounce !== undefined) {
187
+ webComponentProps.debounce = debounce;
188
+ }
189
+
190
+ // Add selectedLabel attribute
191
+ if (selectedLabel) {
192
+ webComponentProps['selected-label'] = selectedLabel;
193
+ }
194
+
195
+ return React.createElement(
196
+ 'ty-multiselect',
197
+ webComponentProps,
198
+ children
199
+ );
200
+ }
201
+ );
202
+
203
+ TyMultiselect.displayName = 'TyMultiselect';
@@ -0,0 +1,42 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ // Type definitions for Ty Option component
4
+ export interface TyOptionProps extends React.HTMLAttributes<HTMLElement> {
5
+ value?: string;
6
+ disabled?: boolean;
7
+ selected?: boolean;
8
+ hidden?: boolean;
9
+ children?: React.ReactNode;
10
+ }
11
+
12
+ // React wrapper for ty-option web component
13
+ export const TyOption = React.forwardRef<HTMLElement, TyOptionProps>(
14
+ ({ children, disabled, selected, hidden, ...props }, ref) => {
15
+ const elementRef = useRef<HTMLElement>(null);
16
+
17
+ // Handle ref forwarding
18
+ useEffect(() => {
19
+ if (ref && elementRef.current) {
20
+ if (typeof ref === 'function') {
21
+ ref(elementRef.current);
22
+ } else {
23
+ ref.current = elementRef.current;
24
+ }
25
+ }
26
+ }, [ref]);
27
+
28
+ return React.createElement(
29
+ 'ty-option',
30
+ {
31
+ ...props,
32
+ ...(disabled && { disabled: "" }),
33
+ ...(selected && { selected: "" }),
34
+ ...(hidden && { hidden: "" }),
35
+ ref: elementRef,
36
+ },
37
+ children
38
+ );
39
+ }
40
+ );
41
+
42
+ TyOption.displayName = 'TyOption';