@hua-labs/hua-ux 0.1.0-alpha.0.1

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 (210) hide show
  1. package/README.md +839 -0
  2. package/dist/framework/a11y/components/LiveRegion.d.ts +64 -0
  3. package/dist/framework/a11y/components/LiveRegion.d.ts.map +1 -0
  4. package/dist/framework/a11y/components/LiveRegion.js +43 -0
  5. package/dist/framework/a11y/components/SkipToContent.d.ts +62 -0
  6. package/dist/framework/a11y/components/SkipToContent.d.ts.map +1 -0
  7. package/dist/framework/a11y/components/SkipToContent.js +60 -0
  8. package/dist/framework/a11y/hooks/useFocusManagement.d.ts +60 -0
  9. package/dist/framework/a11y/hooks/useFocusManagement.d.ts.map +1 -0
  10. package/dist/framework/a11y/hooks/useFocusManagement.js +71 -0
  11. package/dist/framework/a11y/hooks/useFocusTrap.d.ts +64 -0
  12. package/dist/framework/a11y/hooks/useFocusTrap.d.ts.map +1 -0
  13. package/dist/framework/a11y/hooks/useFocusTrap.js +185 -0
  14. package/dist/framework/a11y/hooks/useLiveRegion.d.ts +56 -0
  15. package/dist/framework/a11y/hooks/useLiveRegion.d.ts.map +1 -0
  16. package/dist/framework/a11y/hooks/useLiveRegion.js +60 -0
  17. package/dist/framework/a11y/index.d.ts +16 -0
  18. package/dist/framework/a11y/index.d.ts.map +1 -0
  19. package/dist/framework/a11y/index.js +11 -0
  20. package/dist/framework/branding/context.d.ts +52 -0
  21. package/dist/framework/branding/context.d.ts.map +1 -0
  22. package/dist/framework/branding/context.js +96 -0
  23. package/dist/framework/branding/css-vars.d.ts +34 -0
  24. package/dist/framework/branding/css-vars.d.ts.map +1 -0
  25. package/dist/framework/branding/css-vars.js +95 -0
  26. package/dist/framework/branding/tailwind-config.d.ts +38 -0
  27. package/dist/framework/branding/tailwind-config.d.ts.map +1 -0
  28. package/dist/framework/branding/tailwind-config.js +66 -0
  29. package/dist/framework/components/BrandedButton.d.ts +53 -0
  30. package/dist/framework/components/BrandedButton.d.ts.map +1 -0
  31. package/dist/framework/components/BrandedButton.js +40 -0
  32. package/dist/framework/components/BrandedCard.d.ts +52 -0
  33. package/dist/framework/components/BrandedCard.d.ts.map +1 -0
  34. package/dist/framework/components/BrandedCard.js +73 -0
  35. package/dist/framework/components/ErrorBoundary.d.ts +92 -0
  36. package/dist/framework/components/ErrorBoundary.d.ts.map +1 -0
  37. package/dist/framework/components/ErrorBoundary.js +121 -0
  38. package/dist/framework/components/HuaUxLayout.d.ts +29 -0
  39. package/dist/framework/components/HuaUxLayout.d.ts.map +1 -0
  40. package/dist/framework/components/HuaUxLayout.js +32 -0
  41. package/dist/framework/components/HuaUxPage.d.ts +48 -0
  42. package/dist/framework/components/HuaUxPage.d.ts.map +1 -0
  43. package/dist/framework/components/HuaUxPage.js +105 -0
  44. package/dist/framework/components/Providers.d.ts +17 -0
  45. package/dist/framework/components/Providers.d.ts.map +1 -0
  46. package/dist/framework/components/Providers.js +72 -0
  47. package/dist/framework/components/WelcomePage.d.ts +44 -0
  48. package/dist/framework/components/WelcomePage.d.ts.map +1 -0
  49. package/dist/framework/components/WelcomePage.js +80 -0
  50. package/dist/framework/config/index.d.ts +182 -0
  51. package/dist/framework/config/index.d.ts.map +1 -0
  52. package/dist/framework/config/index.js +329 -0
  53. package/dist/framework/config/merge.d.ts +26 -0
  54. package/dist/framework/config/merge.d.ts.map +1 -0
  55. package/dist/framework/config/merge.js +160 -0
  56. package/dist/framework/config/schema.d.ts +25 -0
  57. package/dist/framework/config/schema.d.ts.map +1 -0
  58. package/dist/framework/config/schema.js +122 -0
  59. package/dist/framework/hooks/useMotion.d.ts +45 -0
  60. package/dist/framework/hooks/useMotion.d.ts.map +1 -0
  61. package/dist/framework/hooks/useMotion.js +40 -0
  62. package/dist/framework/index.d.ts +37 -0
  63. package/dist/framework/index.d.ts.map +1 -0
  64. package/dist/framework/index.js +42 -0
  65. package/dist/framework/license/errors.d.ts +15 -0
  66. package/dist/framework/license/errors.d.ts.map +1 -0
  67. package/dist/framework/license/errors.js +52 -0
  68. package/dist/framework/license/index.d.ts +70 -0
  69. package/dist/framework/license/index.d.ts.map +1 -0
  70. package/dist/framework/license/index.js +124 -0
  71. package/dist/framework/license/loader.d.ts +26 -0
  72. package/dist/framework/license/loader.d.ts.map +1 -0
  73. package/dist/framework/license/loader.js +137 -0
  74. package/dist/framework/license/types.d.ts +67 -0
  75. package/dist/framework/license/types.d.ts.map +1 -0
  76. package/dist/framework/license/types.js +18 -0
  77. package/dist/framework/loading/components/SkeletonGroup.d.ts +44 -0
  78. package/dist/framework/loading/components/SkeletonGroup.d.ts.map +1 -0
  79. package/dist/framework/loading/components/SkeletonGroup.js +34 -0
  80. package/dist/framework/loading/components/SuspenseWrapper.d.ts +58 -0
  81. package/dist/framework/loading/components/SuspenseWrapper.d.ts.map +1 -0
  82. package/dist/framework/loading/components/SuspenseWrapper.js +40 -0
  83. package/dist/framework/loading/hoc/withSuspense.d.ts +46 -0
  84. package/dist/framework/loading/hoc/withSuspense.d.ts.map +1 -0
  85. package/dist/framework/loading/hoc/withSuspense.js +54 -0
  86. package/dist/framework/loading/hooks/useDelayedLoading.d.ts +56 -0
  87. package/dist/framework/loading/hooks/useDelayedLoading.d.ts.map +1 -0
  88. package/dist/framework/loading/hooks/useDelayedLoading.js +97 -0
  89. package/dist/framework/loading/hooks/useLoadingState.d.ts +69 -0
  90. package/dist/framework/loading/hooks/useLoadingState.d.ts.map +1 -0
  91. package/dist/framework/loading/hooks/useLoadingState.js +59 -0
  92. package/dist/framework/loading/index.d.ts +16 -0
  93. package/dist/framework/loading/index.d.ts.map +1 -0
  94. package/dist/framework/loading/index.js +13 -0
  95. package/dist/framework/middleware/i18n.d.ts +90 -0
  96. package/dist/framework/middleware/i18n.d.ts.map +1 -0
  97. package/dist/framework/middleware/i18n.js +99 -0
  98. package/dist/framework/plugins/index.d.ts +8 -0
  99. package/dist/framework/plugins/index.d.ts.map +1 -0
  100. package/dist/framework/plugins/index.js +6 -0
  101. package/dist/framework/plugins/registry.d.ts +95 -0
  102. package/dist/framework/plugins/registry.d.ts.map +1 -0
  103. package/dist/framework/plugins/registry.js +160 -0
  104. package/dist/framework/plugins/types.d.ts +97 -0
  105. package/dist/framework/plugins/types.d.ts.map +1 -0
  106. package/dist/framework/plugins/types.js +6 -0
  107. package/dist/framework/seo/geo/examples.d.ts +87 -0
  108. package/dist/framework/seo/geo/examples.d.ts.map +1 -0
  109. package/dist/framework/seo/geo/examples.js +295 -0
  110. package/dist/framework/seo/geo/generateGEOMetadata.d.ts +107 -0
  111. package/dist/framework/seo/geo/generateGEOMetadata.d.ts.map +1 -0
  112. package/dist/framework/seo/geo/generateGEOMetadata.js +404 -0
  113. package/dist/framework/seo/geo/index.d.ts +19 -0
  114. package/dist/framework/seo/geo/index.d.ts.map +1 -0
  115. package/dist/framework/seo/geo/index.js +21 -0
  116. package/dist/framework/seo/geo/presets.d.ts +52 -0
  117. package/dist/framework/seo/geo/presets.d.ts.map +1 -0
  118. package/dist/framework/seo/geo/presets.js +47 -0
  119. package/dist/framework/seo/geo/structuredData.d.ts +187 -0
  120. package/dist/framework/seo/geo/structuredData.d.ts.map +1 -0
  121. package/dist/framework/seo/geo/structuredData.js +354 -0
  122. package/dist/framework/seo/geo/test-utils.d.ts +78 -0
  123. package/dist/framework/seo/geo/test-utils.d.ts.map +1 -0
  124. package/dist/framework/seo/geo/test-utils.js +139 -0
  125. package/dist/framework/seo/geo/types.d.ts +225 -0
  126. package/dist/framework/seo/geo/types.d.ts.map +1 -0
  127. package/dist/framework/seo/geo/types.js +51 -0
  128. package/dist/framework/types/index.d.ts +577 -0
  129. package/dist/framework/types/index.d.ts.map +1 -0
  130. package/dist/framework/types/index.js +6 -0
  131. package/dist/framework/utils/data-fetching.d.ts +45 -0
  132. package/dist/framework/utils/data-fetching.d.ts.map +1 -0
  133. package/dist/framework/utils/data-fetching.js +74 -0
  134. package/dist/framework/utils/file-structure.d.ts +29 -0
  135. package/dist/framework/utils/file-structure.d.ts.map +1 -0
  136. package/dist/framework/utils/file-structure.js +72 -0
  137. package/dist/framework/utils/metadata.d.ts +109 -0
  138. package/dist/framework/utils/metadata.d.ts.map +1 -0
  139. package/dist/framework/utils/metadata.js +105 -0
  140. package/dist/index.d.ts +15 -0
  141. package/dist/index.d.ts.map +1 -0
  142. package/dist/index.js +21 -0
  143. package/dist/presets/index.d.ts +8 -0
  144. package/dist/presets/index.d.ts.map +1 -0
  145. package/dist/presets/index.js +7 -0
  146. package/dist/presets/marketing.d.ts +41 -0
  147. package/dist/presets/marketing.d.ts.map +1 -0
  148. package/dist/presets/marketing.js +81 -0
  149. package/dist/presets/product.d.ts +41 -0
  150. package/dist/presets/product.d.ts.map +1 -0
  151. package/dist/presets/product.js +74 -0
  152. package/package.json +91 -0
  153. package/src/framework/README.md +329 -0
  154. package/src/framework/__tests__/branding/css-vars.test.ts +147 -0
  155. package/src/framework/__tests__/components/ErrorBoundary.test.tsx +146 -0
  156. package/src/framework/__tests__/config/defineConfig.test.ts +138 -0
  157. package/src/framework/__tests__/hooks/useMotion.test.ts +105 -0
  158. package/src/framework/__tests__/seo/geo/generateGEOMetadata.test.ts +207 -0
  159. package/src/framework/__tests__/seo/geo/structuredData.test.ts +262 -0
  160. package/src/framework/a11y/components/LiveRegion.tsx +89 -0
  161. package/src/framework/a11y/components/SkipToContent.tsx +103 -0
  162. package/src/framework/a11y/hooks/useFocusManagement.ts +125 -0
  163. package/src/framework/a11y/hooks/useFocusTrap.ts +239 -0
  164. package/src/framework/a11y/hooks/useLiveRegion.ts +95 -0
  165. package/src/framework/a11y/index.ts +17 -0
  166. package/src/framework/branding/context.tsx +135 -0
  167. package/src/framework/branding/css-vars.ts +110 -0
  168. package/src/framework/branding/tailwind-config.ts +90 -0
  169. package/src/framework/components/BrandedButton.tsx +94 -0
  170. package/src/framework/components/BrandedCard.tsx +87 -0
  171. package/src/framework/components/ErrorBoundary.tsx +215 -0
  172. package/src/framework/components/HuaUxLayout.tsx +36 -0
  173. package/src/framework/components/HuaUxPage.tsx +138 -0
  174. package/src/framework/components/Providers.tsx +98 -0
  175. package/src/framework/components/WelcomePage.tsx +207 -0
  176. package/src/framework/config/index.ts +349 -0
  177. package/src/framework/config/merge.ts +190 -0
  178. package/src/framework/config/schema.ts +140 -0
  179. package/src/framework/hooks/useMotion.ts +57 -0
  180. package/src/framework/index.ts +122 -0
  181. package/src/framework/license/errors.ts +63 -0
  182. package/src/framework/license/index.ts +137 -0
  183. package/src/framework/license/loader.ts +158 -0
  184. package/src/framework/license/types.ts +95 -0
  185. package/src/framework/loading/components/SkeletonGroup.tsx +70 -0
  186. package/src/framework/loading/components/SuspenseWrapper.tsx +88 -0
  187. package/src/framework/loading/hoc/withSuspense.tsx +96 -0
  188. package/src/framework/loading/hooks/useDelayedLoading.ts +127 -0
  189. package/src/framework/loading/hooks/useLoadingState.ts +103 -0
  190. package/src/framework/loading/index.ts +19 -0
  191. package/src/framework/middleware/i18n.ts +161 -0
  192. package/src/framework/middleware/index.ts +7 -0
  193. package/src/framework/plugins/index.ts +13 -0
  194. package/src/framework/plugins/registry.ts +186 -0
  195. package/src/framework/plugins/types.ts +106 -0
  196. package/src/framework/seo/geo/examples.tsx +415 -0
  197. package/src/framework/seo/geo/generateGEOMetadata.ts +441 -0
  198. package/src/framework/seo/geo/index.ts +61 -0
  199. package/src/framework/seo/geo/presets.ts +58 -0
  200. package/src/framework/seo/geo/structuredData.ts +422 -0
  201. package/src/framework/seo/geo/test-utils.ts +179 -0
  202. package/src/framework/seo/geo/types.ts +315 -0
  203. package/src/framework/types/index.ts +623 -0
  204. package/src/framework/utils/data-fetching.ts +95 -0
  205. package/src/framework/utils/file-structure.ts +88 -0
  206. package/src/framework/utils/metadata.ts +152 -0
  207. package/src/index.ts +31 -0
  208. package/src/presets/index.ts +8 -0
  209. package/src/presets/marketing.ts +88 -0
  210. package/src/presets/product.ts +81 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - Structured Data Tests
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ generateSoftwareApplicationLD,
8
+ generateFAQPageLD,
9
+ generateTechArticleLD,
10
+ generateHowToLD,
11
+ } from '../../../seo/geo/structuredData';
12
+ import type { GEOConfig } from '../../../seo/geo/types';
13
+
14
+ describe('generateSoftwareApplicationLD', () => {
15
+ const baseConfig: GEOConfig = {
16
+ name: 'Test App',
17
+ description: 'A test application',
18
+ };
19
+
20
+ it('should generate basic SoftwareApplication JSON-LD', () => {
21
+ const result = generateSoftwareApplicationLD(baseConfig);
22
+
23
+ expect(result['@context']).toBe('https://schema.org');
24
+ expect(result['@type']).toBe('SoftwareApplication');
25
+ expect(result.name).toBe('Test App');
26
+ expect(result.description).toBe('A test application');
27
+ });
28
+
29
+ it('should include alternate names', () => {
30
+ const config: GEOConfig = {
31
+ ...baseConfig,
32
+ alternateName: ['test-app', '@test/app'],
33
+ };
34
+
35
+ const result = generateSoftwareApplicationLD(config);
36
+
37
+ expect(result.alternateName).toEqual(['test-app', '@test/app']);
38
+ });
39
+
40
+ it('should include version', () => {
41
+ const config: GEOConfig = {
42
+ ...baseConfig,
43
+ version: '1.0.0',
44
+ };
45
+
46
+ const result = generateSoftwareApplicationLD(config);
47
+
48
+ expect(result.softwareVersion).toBe('1.0.0');
49
+ });
50
+
51
+ it('should include application category', () => {
52
+ const config: GEOConfig = {
53
+ ...baseConfig,
54
+ applicationCategory: 'UX Framework',
55
+ };
56
+
57
+ const result = generateSoftwareApplicationLD(config);
58
+
59
+ expect(result.applicationCategory).toBe('UX Framework');
60
+ });
61
+
62
+ it('should handle multiple application categories', () => {
63
+ const config: GEOConfig = {
64
+ ...baseConfig,
65
+ applicationCategory: ['UX Framework', 'Developer Tool'],
66
+ };
67
+
68
+ const result = generateSoftwareApplicationLD(config);
69
+
70
+ expect(result.applicationCategory).toBe('UX Framework, Developer Tool');
71
+ });
72
+
73
+ it('should include programming language', () => {
74
+ const config: GEOConfig = {
75
+ ...baseConfig,
76
+ programmingLanguage: 'TypeScript',
77
+ };
78
+
79
+ const result = generateSoftwareApplicationLD(config);
80
+
81
+ expect(result.programmingLanguage).toBe('TypeScript');
82
+ });
83
+
84
+ it('should include URLs', () => {
85
+ const config: GEOConfig = {
86
+ ...baseConfig,
87
+ url: 'https://example.com',
88
+ documentationUrl: 'https://example.com/docs',
89
+ codeRepository: 'https://github.com/example/app',
90
+ };
91
+
92
+ const result = generateSoftwareApplicationLD(config);
93
+
94
+ expect(result.url).toBe('https://example.com');
95
+ expect(result.softwareHelp).toBeDefined();
96
+ expect(result.softwareHelp?.['@type']).toBe('CreativeWork');
97
+ expect(result.codeRepository).toBe('https://github.com/example/app');
98
+ });
99
+
100
+ it('should include license', () => {
101
+ const config: GEOConfig = {
102
+ ...baseConfig,
103
+ license: 'MIT',
104
+ };
105
+
106
+ const result = generateSoftwareApplicationLD(config);
107
+
108
+ expect(result.license).toBe('MIT');
109
+ });
110
+
111
+ it('should include author', () => {
112
+ const config: GEOConfig = {
113
+ ...baseConfig,
114
+ author: {
115
+ name: 'Test Author',
116
+ url: 'https://example.com/author',
117
+ },
118
+ };
119
+
120
+ const result = generateSoftwareApplicationLD(config);
121
+
122
+ expect(result.author).toBeDefined();
123
+ expect(result.author?.['@type']).toBe('Organization');
124
+ expect(result.author?.name).toBe('Test Author');
125
+ expect(result.author?.url).toBe('https://example.com/author');
126
+ });
127
+
128
+ it('should include features', () => {
129
+ const config: GEOConfig = {
130
+ ...baseConfig,
131
+ features: ['i18n', 'Motion', 'Accessibility'],
132
+ };
133
+
134
+ const result = generateSoftwareApplicationLD(config);
135
+
136
+ expect(result.featureList).toBe('i18n, Motion, Accessibility');
137
+ });
138
+
139
+ it('should include keywords', () => {
140
+ const config: GEOConfig = {
141
+ ...baseConfig,
142
+ keywords: ['react', 'nextjs'],
143
+ };
144
+
145
+ const result = generateSoftwareApplicationLD(config);
146
+
147
+ expect(result.keywords).toBe('react, nextjs');
148
+ });
149
+ });
150
+
151
+ describe('generateFAQPageLD', () => {
152
+ it('should generate FAQPage JSON-LD', () => {
153
+ const faqs = [
154
+ { question: 'What is it?', answer: 'It is a test app' },
155
+ { question: 'How to use?', answer: 'Just use it' },
156
+ ];
157
+
158
+ const result = generateFAQPageLD(faqs);
159
+
160
+ expect(result['@context']).toBe('https://schema.org');
161
+ expect(result['@type']).toBe('FAQPage');
162
+ expect(result.mainEntity).toBeDefined();
163
+ expect(result.mainEntity.length).toBe(2);
164
+ });
165
+
166
+ it('should format FAQ items correctly', () => {
167
+ const faqs = [
168
+ { question: 'What is it?', answer: 'It is a test app' },
169
+ ];
170
+
171
+ const result = generateFAQPageLD(faqs);
172
+
173
+ const question = result.mainEntity[0];
174
+ expect(question['@type']).toBe('Question');
175
+ expect(question.name).toBe('What is it?');
176
+ expect(question.acceptedAnswer['@type']).toBe('Answer');
177
+ expect(question.acceptedAnswer.text).toBe('It is a test app');
178
+ });
179
+ });
180
+
181
+ describe('generateTechArticleLD', () => {
182
+ it('should generate TechArticle JSON-LD', () => {
183
+ const article = {
184
+ headline: 'Test Article',
185
+ description: 'A test article',
186
+ datePublished: '2025-12-29',
187
+ author: { name: 'Test Author' },
188
+ };
189
+
190
+ const result = generateTechArticleLD(article);
191
+
192
+ expect(result['@context']).toBe('https://schema.org');
193
+ expect(result['@type']).toBe('TechArticle');
194
+ expect(result.headline).toBe('Test Article');
195
+ expect(result.description).toBe('A test article');
196
+ expect(result.datePublished).toBe('2025-12-29');
197
+ });
198
+
199
+ it('should include optional fields', () => {
200
+ const article = {
201
+ headline: 'Test Article',
202
+ dateModified: '2025-12-30',
203
+ image: 'https://example.com/image.jpg',
204
+ };
205
+
206
+ const result = generateTechArticleLD(article);
207
+
208
+ expect(result.dateModified).toBe('2025-12-30');
209
+ expect(result.image).toBe('https://example.com/image.jpg');
210
+ });
211
+ });
212
+
213
+ describe('generateHowToLD', () => {
214
+ it('should generate HowTo JSON-LD', () => {
215
+ const howTo = {
216
+ name: 'How to test',
217
+ description: 'A test guide',
218
+ steps: [
219
+ { name: 'Step 1', text: 'Do this' },
220
+ { name: 'Step 2', text: 'Do that' },
221
+ ],
222
+ };
223
+
224
+ const result = generateHowToLD(howTo);
225
+
226
+ expect(result['@context']).toBe('https://schema.org');
227
+ expect(result['@type']).toBe('HowTo');
228
+ expect(result.name).toBe('How to test');
229
+ expect(result.description).toBe('A test guide');
230
+ expect(result.step).toBeDefined();
231
+ expect(result.step.length).toBe(2);
232
+ });
233
+
234
+ it('should format steps correctly', () => {
235
+ const howTo = {
236
+ name: 'How to test',
237
+ steps: [
238
+ { name: 'Step 1', text: 'Do this' },
239
+ ],
240
+ };
241
+
242
+ const result = generateHowToLD(howTo);
243
+
244
+ const step = result.step[0];
245
+ expect(step['@type']).toBe('HowToStep');
246
+ expect(step.position).toBe(1);
247
+ expect(step.name).toBe('Step 1');
248
+ expect(step.text).toBe('Do this');
249
+ });
250
+
251
+ it('should include totalTime when provided', () => {
252
+ const howTo = {
253
+ name: 'How to test',
254
+ steps: [{ name: 'Step 1', text: 'Do this' }],
255
+ totalTime: 'PT10M',
256
+ };
257
+
258
+ const result = generateHowToLD(howTo);
259
+
260
+ expect(result.totalTime).toBe('PT10M');
261
+ });
262
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - LiveRegion
3
+ *
4
+ * 스크린 리더 사용자에게 동적 상태 변화를 알리는 컴포넌트
5
+ * Component that announces dynamic state changes to screen reader users
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+
12
+ /**
13
+ * LiveRegion 컴포넌트 props
14
+ */
15
+ export interface LiveRegionProps {
16
+ /**
17
+ * 알림할 메시지
18
+ * Message to announce
19
+ */
20
+ message?: string;
21
+
22
+ /**
23
+ * Live region의 politeness 레벨
24
+ * Politeness level of the live region
25
+ *
26
+ * - 'polite': 현재 작업이 완료된 후 알림 (기본값)
27
+ * - 'assertive': 즉시 알림
28
+ * - 'off': 알림 비활성화
29
+ *
30
+ * @default "polite"
31
+ */
32
+ politeness?: 'polite' | 'assertive' | 'off';
33
+
34
+ /**
35
+ * 추가 CSS 클래스
36
+ * Additional CSS classes
37
+ */
38
+ className?: string;
39
+ }
40
+
41
+ /**
42
+ * LiveRegion 컴포넌트
43
+ *
44
+ * 스크린 리더 사용자에게 동적 상태 변화를 알립니다.
45
+ * Announces dynamic state changes to screen reader users.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * function MyForm() {
50
+ * const [message, setMessage] = useState('');
51
+ *
52
+ * const handleSubmit = async () => {
53
+ * setMessage('저장 중...');
54
+ * await saveData();
55
+ * setMessage('저장되었습니다!');
56
+ * };
57
+ *
58
+ * return (
59
+ * <div>
60
+ * <form onSubmit={handleSubmit}>Form fields</form>
61
+ * <LiveRegion message={message} />
62
+ * </div>
63
+ * );
64
+ * }
65
+ * ```
66
+ *
67
+ * @param props - LiveRegion props
68
+ * @returns LiveRegion 컴포넌트
69
+ */
70
+ export function LiveRegion({
71
+ message,
72
+ politeness = 'polite',
73
+ className,
74
+ }: LiveRegionProps): React.JSX.Element {
75
+ // aria-live는 자동으로 업데이트를 감지하므로
76
+ // 메시지 변경만으로도 알림이 발생합니다.
77
+ // 별도의 useEffect는 필요하지 않습니다.
78
+
79
+ return (
80
+ <div
81
+ role="status"
82
+ aria-live={politeness}
83
+ aria-atomic="true"
84
+ className={className || 'sr-only'}
85
+ >
86
+ {message}
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - SkipToContent
3
+ *
4
+ * 키보드 사용자를 위한 "콘텐츠로 건너뛰기" 링크
5
+ * "Skip to content" link for keyboard users
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+
12
+ /**
13
+ * SkipToContent 컴포넌트 props
14
+ */
15
+ export interface SkipToContentProps {
16
+ /**
17
+ * 메인 콘텐츠의 ID
18
+ * ID of the main content element
19
+ *
20
+ * @default "main-content"
21
+ */
22
+ targetId?: string;
23
+
24
+ /**
25
+ * 링크 텍스트
26
+ * Link text
27
+ *
28
+ * @default "Skip to content"
29
+ */
30
+ children?: React.ReactNode;
31
+
32
+ /**
33
+ * 추가 CSS 클래스
34
+ * Additional CSS classes
35
+ */
36
+ className?: string;
37
+ }
38
+
39
+ /**
40
+ * SkipToContent 컴포넌트
41
+ *
42
+ * 키보드 사용자가 네비게이션을 건너뛰고 메인 콘텐츠로 바로 이동할 수 있도록 합니다.
43
+ * Allows keyboard users to skip navigation and go directly to main content.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * // app/layout.tsx
48
+ * import { SkipToContent } from '@hua-labs/hua-ux/framework';
49
+ *
50
+ * export default function RootLayout({ children }) {
51
+ * return (
52
+ * <html>
53
+ * <body>
54
+ * <SkipToContent />
55
+ * <nav>Navigation</nav>
56
+ * <main id="main-content" tabIndex={-1}>
57
+ * {children}
58
+ * </main>
59
+ * </body>
60
+ * </html>
61
+ * );
62
+ * }
63
+ * ```
64
+ *
65
+ * @param props - SkipToContent props
66
+ * @returns SkipToContent 컴포넌트
67
+ */
68
+ export function SkipToContent({
69
+ targetId = 'main-content',
70
+ children = 'Skip to content',
71
+ className,
72
+ }: SkipToContentProps): React.JSX.Element {
73
+ const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
74
+ event.preventDefault();
75
+
76
+ try {
77
+ const target = document.getElementById(targetId);
78
+ if (target) {
79
+ target.focus();
80
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
81
+ } else if (process.env.NODE_ENV === 'development') {
82
+ console.warn(`[SkipToContent] Target element with id "${targetId}" not found`);
83
+ }
84
+ } catch (error) {
85
+ // focus() 또는 scrollIntoView() 실패 시 조용히 처리
86
+ if (process.env.NODE_ENV === 'development') {
87
+ console.warn('[SkipToContent] Failed to focus or scroll to target:', error);
88
+ }
89
+ }
90
+ };
91
+
92
+ const defaultClassName = 'sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white focus:rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2';
93
+
94
+ return (
95
+ <a
96
+ href={`#${targetId}`}
97
+ onClick={handleClick}
98
+ className={className || defaultClassName}
99
+ >
100
+ {children}
101
+ </a>
102
+ );
103
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @hua-labs/hua-ux/framework - useFocusManagement
3
+ *
4
+ * 페이지 전환 시 자동으로 메인 콘텐츠에 포커스를 이동시키는 hook
5
+ * Automatically focuses main content on page transitions
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { useEffect, useRef, useMemo, type RefObject } from 'react';
11
+
12
+ /**
13
+ * Focus Management 옵션
14
+ */
15
+ export interface FocusManagementOptions {
16
+ /**
17
+ * 마운트 시 자동으로 포커스할지 여부
18
+ * Whether to automatically focus on mount
19
+ *
20
+ * @default true
21
+ */
22
+ autoFocus?: boolean;
23
+
24
+ /**
25
+ * 포커스할 요소의 선택자 (기본값: 요소 자체)
26
+ * Selector for element to focus (default: element itself)
27
+ */
28
+ focusSelector?: string;
29
+
30
+ /**
31
+ * 포커스 시 스크롤할지 여부
32
+ * Whether to scroll when focusing
33
+ *
34
+ * @default false
35
+ */
36
+ scrollIntoView?: boolean;
37
+
38
+ /**
39
+ * 스크롤 옵션 (scrollIntoView가 true일 때)
40
+ * Scroll options (when scrollIntoView is true)
41
+ */
42
+ scrollOptions?: ScrollIntoViewOptions;
43
+ }
44
+
45
+ /**
46
+ * Focus Management Hook
47
+ *
48
+ * 페이지 전환 시 자동으로 메인 콘텐츠에 포커스를 이동시킵니다.
49
+ * Automatically focuses main content on page transitions.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * function MyPage() {
54
+ * const mainRef = useFocusManagement({ autoFocus: true });
55
+ *
56
+ * return (
57
+ * <main ref={mainRef} tabIndex={-1}>
58
+ * <h1>Page Title</h1>
59
+ * </main>
60
+ * );
61
+ * }
62
+ * ```
63
+ *
64
+ * @param options - Focus Management 옵션
65
+ * @returns 요소에 연결할 ref
66
+ */
67
+ export function useFocusManagement<T extends HTMLElement = HTMLElement>(
68
+ options: FocusManagementOptions = {}
69
+ ): RefObject<T | null> {
70
+ const {
71
+ autoFocus = true,
72
+ focusSelector,
73
+ scrollIntoView = false,
74
+ scrollOptions: userScrollOptions,
75
+ } = options;
76
+
77
+ const ref = useRef<T>(null);
78
+
79
+ // scrollOptions 메모이제이션 (객체가 매번 새로 생성되는 것을 방지)
80
+ const scrollOptions = useMemo<ScrollIntoViewOptions>(
81
+ () => userScrollOptions ?? { behavior: 'smooth' as const, block: 'start' as const },
82
+ [userScrollOptions]
83
+ );
84
+
85
+ useEffect(() => {
86
+ if (!autoFocus || !ref.current) {
87
+ return;
88
+ }
89
+
90
+ // 포커스할 요소 찾기
91
+ let elementToFocus: HTMLElement | null = ref.current;
92
+
93
+ if (focusSelector) {
94
+ const found = ref.current.querySelector<HTMLElement>(focusSelector);
95
+ if (found) {
96
+ elementToFocus = found;
97
+ }
98
+ }
99
+
100
+ // 포커스 이동
101
+ if (elementToFocus) {
102
+ // 약간의 지연을 두어 렌더링 완료 후 포커스
103
+ const timeoutId = setTimeout(() => {
104
+ try {
105
+ elementToFocus?.focus();
106
+
107
+ if (scrollIntoView && elementToFocus && scrollOptions) {
108
+ elementToFocus.scrollIntoView(scrollOptions);
109
+ }
110
+ } catch (error) {
111
+ // focus() 실패 시 조용히 처리
112
+ if (process.env.NODE_ENV === 'development') {
113
+ console.warn('[useFocusManagement] Failed to focus element:', error);
114
+ }
115
+ }
116
+ }, 0);
117
+
118
+ return () => {
119
+ clearTimeout(timeoutId);
120
+ };
121
+ }
122
+ }, [autoFocus, focusSelector, scrollIntoView, scrollOptions]);
123
+
124
+ return ref;
125
+ }