@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.
- package/README.md +839 -0
- package/dist/framework/a11y/components/LiveRegion.d.ts +64 -0
- package/dist/framework/a11y/components/LiveRegion.d.ts.map +1 -0
- package/dist/framework/a11y/components/LiveRegion.js +43 -0
- package/dist/framework/a11y/components/SkipToContent.d.ts +62 -0
- package/dist/framework/a11y/components/SkipToContent.d.ts.map +1 -0
- package/dist/framework/a11y/components/SkipToContent.js +60 -0
- package/dist/framework/a11y/hooks/useFocusManagement.d.ts +60 -0
- package/dist/framework/a11y/hooks/useFocusManagement.d.ts.map +1 -0
- package/dist/framework/a11y/hooks/useFocusManagement.js +71 -0
- package/dist/framework/a11y/hooks/useFocusTrap.d.ts +64 -0
- package/dist/framework/a11y/hooks/useFocusTrap.d.ts.map +1 -0
- package/dist/framework/a11y/hooks/useFocusTrap.js +185 -0
- package/dist/framework/a11y/hooks/useLiveRegion.d.ts +56 -0
- package/dist/framework/a11y/hooks/useLiveRegion.d.ts.map +1 -0
- package/dist/framework/a11y/hooks/useLiveRegion.js +60 -0
- package/dist/framework/a11y/index.d.ts +16 -0
- package/dist/framework/a11y/index.d.ts.map +1 -0
- package/dist/framework/a11y/index.js +11 -0
- package/dist/framework/branding/context.d.ts +52 -0
- package/dist/framework/branding/context.d.ts.map +1 -0
- package/dist/framework/branding/context.js +96 -0
- package/dist/framework/branding/css-vars.d.ts +34 -0
- package/dist/framework/branding/css-vars.d.ts.map +1 -0
- package/dist/framework/branding/css-vars.js +95 -0
- package/dist/framework/branding/tailwind-config.d.ts +38 -0
- package/dist/framework/branding/tailwind-config.d.ts.map +1 -0
- package/dist/framework/branding/tailwind-config.js +66 -0
- package/dist/framework/components/BrandedButton.d.ts +53 -0
- package/dist/framework/components/BrandedButton.d.ts.map +1 -0
- package/dist/framework/components/BrandedButton.js +40 -0
- package/dist/framework/components/BrandedCard.d.ts +52 -0
- package/dist/framework/components/BrandedCard.d.ts.map +1 -0
- package/dist/framework/components/BrandedCard.js +73 -0
- package/dist/framework/components/ErrorBoundary.d.ts +92 -0
- package/dist/framework/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/framework/components/ErrorBoundary.js +121 -0
- package/dist/framework/components/HuaUxLayout.d.ts +29 -0
- package/dist/framework/components/HuaUxLayout.d.ts.map +1 -0
- package/dist/framework/components/HuaUxLayout.js +32 -0
- package/dist/framework/components/HuaUxPage.d.ts +48 -0
- package/dist/framework/components/HuaUxPage.d.ts.map +1 -0
- package/dist/framework/components/HuaUxPage.js +105 -0
- package/dist/framework/components/Providers.d.ts +17 -0
- package/dist/framework/components/Providers.d.ts.map +1 -0
- package/dist/framework/components/Providers.js +72 -0
- package/dist/framework/components/WelcomePage.d.ts +44 -0
- package/dist/framework/components/WelcomePage.d.ts.map +1 -0
- package/dist/framework/components/WelcomePage.js +80 -0
- package/dist/framework/config/index.d.ts +182 -0
- package/dist/framework/config/index.d.ts.map +1 -0
- package/dist/framework/config/index.js +329 -0
- package/dist/framework/config/merge.d.ts +26 -0
- package/dist/framework/config/merge.d.ts.map +1 -0
- package/dist/framework/config/merge.js +160 -0
- package/dist/framework/config/schema.d.ts +25 -0
- package/dist/framework/config/schema.d.ts.map +1 -0
- package/dist/framework/config/schema.js +122 -0
- package/dist/framework/hooks/useMotion.d.ts +45 -0
- package/dist/framework/hooks/useMotion.d.ts.map +1 -0
- package/dist/framework/hooks/useMotion.js +40 -0
- package/dist/framework/index.d.ts +37 -0
- package/dist/framework/index.d.ts.map +1 -0
- package/dist/framework/index.js +42 -0
- package/dist/framework/license/errors.d.ts +15 -0
- package/dist/framework/license/errors.d.ts.map +1 -0
- package/dist/framework/license/errors.js +52 -0
- package/dist/framework/license/index.d.ts +70 -0
- package/dist/framework/license/index.d.ts.map +1 -0
- package/dist/framework/license/index.js +124 -0
- package/dist/framework/license/loader.d.ts +26 -0
- package/dist/framework/license/loader.d.ts.map +1 -0
- package/dist/framework/license/loader.js +137 -0
- package/dist/framework/license/types.d.ts +67 -0
- package/dist/framework/license/types.d.ts.map +1 -0
- package/dist/framework/license/types.js +18 -0
- package/dist/framework/loading/components/SkeletonGroup.d.ts +44 -0
- package/dist/framework/loading/components/SkeletonGroup.d.ts.map +1 -0
- package/dist/framework/loading/components/SkeletonGroup.js +34 -0
- package/dist/framework/loading/components/SuspenseWrapper.d.ts +58 -0
- package/dist/framework/loading/components/SuspenseWrapper.d.ts.map +1 -0
- package/dist/framework/loading/components/SuspenseWrapper.js +40 -0
- package/dist/framework/loading/hoc/withSuspense.d.ts +46 -0
- package/dist/framework/loading/hoc/withSuspense.d.ts.map +1 -0
- package/dist/framework/loading/hoc/withSuspense.js +54 -0
- package/dist/framework/loading/hooks/useDelayedLoading.d.ts +56 -0
- package/dist/framework/loading/hooks/useDelayedLoading.d.ts.map +1 -0
- package/dist/framework/loading/hooks/useDelayedLoading.js +97 -0
- package/dist/framework/loading/hooks/useLoadingState.d.ts +69 -0
- package/dist/framework/loading/hooks/useLoadingState.d.ts.map +1 -0
- package/dist/framework/loading/hooks/useLoadingState.js +59 -0
- package/dist/framework/loading/index.d.ts +16 -0
- package/dist/framework/loading/index.d.ts.map +1 -0
- package/dist/framework/loading/index.js +13 -0
- package/dist/framework/middleware/i18n.d.ts +90 -0
- package/dist/framework/middleware/i18n.d.ts.map +1 -0
- package/dist/framework/middleware/i18n.js +99 -0
- package/dist/framework/plugins/index.d.ts +8 -0
- package/dist/framework/plugins/index.d.ts.map +1 -0
- package/dist/framework/plugins/index.js +6 -0
- package/dist/framework/plugins/registry.d.ts +95 -0
- package/dist/framework/plugins/registry.d.ts.map +1 -0
- package/dist/framework/plugins/registry.js +160 -0
- package/dist/framework/plugins/types.d.ts +97 -0
- package/dist/framework/plugins/types.d.ts.map +1 -0
- package/dist/framework/plugins/types.js +6 -0
- package/dist/framework/seo/geo/examples.d.ts +87 -0
- package/dist/framework/seo/geo/examples.d.ts.map +1 -0
- package/dist/framework/seo/geo/examples.js +295 -0
- package/dist/framework/seo/geo/generateGEOMetadata.d.ts +107 -0
- package/dist/framework/seo/geo/generateGEOMetadata.d.ts.map +1 -0
- package/dist/framework/seo/geo/generateGEOMetadata.js +404 -0
- package/dist/framework/seo/geo/index.d.ts +19 -0
- package/dist/framework/seo/geo/index.d.ts.map +1 -0
- package/dist/framework/seo/geo/index.js +21 -0
- package/dist/framework/seo/geo/presets.d.ts +52 -0
- package/dist/framework/seo/geo/presets.d.ts.map +1 -0
- package/dist/framework/seo/geo/presets.js +47 -0
- package/dist/framework/seo/geo/structuredData.d.ts +187 -0
- package/dist/framework/seo/geo/structuredData.d.ts.map +1 -0
- package/dist/framework/seo/geo/structuredData.js +354 -0
- package/dist/framework/seo/geo/test-utils.d.ts +78 -0
- package/dist/framework/seo/geo/test-utils.d.ts.map +1 -0
- package/dist/framework/seo/geo/test-utils.js +139 -0
- package/dist/framework/seo/geo/types.d.ts +225 -0
- package/dist/framework/seo/geo/types.d.ts.map +1 -0
- package/dist/framework/seo/geo/types.js +51 -0
- package/dist/framework/types/index.d.ts +577 -0
- package/dist/framework/types/index.d.ts.map +1 -0
- package/dist/framework/types/index.js +6 -0
- package/dist/framework/utils/data-fetching.d.ts +45 -0
- package/dist/framework/utils/data-fetching.d.ts.map +1 -0
- package/dist/framework/utils/data-fetching.js +74 -0
- package/dist/framework/utils/file-structure.d.ts +29 -0
- package/dist/framework/utils/file-structure.d.ts.map +1 -0
- package/dist/framework/utils/file-structure.js +72 -0
- package/dist/framework/utils/metadata.d.ts +109 -0
- package/dist/framework/utils/metadata.d.ts.map +1 -0
- package/dist/framework/utils/metadata.js +105 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/presets/index.d.ts +8 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +7 -0
- package/dist/presets/marketing.d.ts +41 -0
- package/dist/presets/marketing.d.ts.map +1 -0
- package/dist/presets/marketing.js +81 -0
- package/dist/presets/product.d.ts +41 -0
- package/dist/presets/product.d.ts.map +1 -0
- package/dist/presets/product.js +74 -0
- package/package.json +91 -0
- package/src/framework/README.md +329 -0
- package/src/framework/__tests__/branding/css-vars.test.ts +147 -0
- package/src/framework/__tests__/components/ErrorBoundary.test.tsx +146 -0
- package/src/framework/__tests__/config/defineConfig.test.ts +138 -0
- package/src/framework/__tests__/hooks/useMotion.test.ts +105 -0
- package/src/framework/__tests__/seo/geo/generateGEOMetadata.test.ts +207 -0
- package/src/framework/__tests__/seo/geo/structuredData.test.ts +262 -0
- package/src/framework/a11y/components/LiveRegion.tsx +89 -0
- package/src/framework/a11y/components/SkipToContent.tsx +103 -0
- package/src/framework/a11y/hooks/useFocusManagement.ts +125 -0
- package/src/framework/a11y/hooks/useFocusTrap.ts +239 -0
- package/src/framework/a11y/hooks/useLiveRegion.ts +95 -0
- package/src/framework/a11y/index.ts +17 -0
- package/src/framework/branding/context.tsx +135 -0
- package/src/framework/branding/css-vars.ts +110 -0
- package/src/framework/branding/tailwind-config.ts +90 -0
- package/src/framework/components/BrandedButton.tsx +94 -0
- package/src/framework/components/BrandedCard.tsx +87 -0
- package/src/framework/components/ErrorBoundary.tsx +215 -0
- package/src/framework/components/HuaUxLayout.tsx +36 -0
- package/src/framework/components/HuaUxPage.tsx +138 -0
- package/src/framework/components/Providers.tsx +98 -0
- package/src/framework/components/WelcomePage.tsx +207 -0
- package/src/framework/config/index.ts +349 -0
- package/src/framework/config/merge.ts +190 -0
- package/src/framework/config/schema.ts +140 -0
- package/src/framework/hooks/useMotion.ts +57 -0
- package/src/framework/index.ts +122 -0
- package/src/framework/license/errors.ts +63 -0
- package/src/framework/license/index.ts +137 -0
- package/src/framework/license/loader.ts +158 -0
- package/src/framework/license/types.ts +95 -0
- package/src/framework/loading/components/SkeletonGroup.tsx +70 -0
- package/src/framework/loading/components/SuspenseWrapper.tsx +88 -0
- package/src/framework/loading/hoc/withSuspense.tsx +96 -0
- package/src/framework/loading/hooks/useDelayedLoading.ts +127 -0
- package/src/framework/loading/hooks/useLoadingState.ts +103 -0
- package/src/framework/loading/index.ts +19 -0
- package/src/framework/middleware/i18n.ts +161 -0
- package/src/framework/middleware/index.ts +7 -0
- package/src/framework/plugins/index.ts +13 -0
- package/src/framework/plugins/registry.ts +186 -0
- package/src/framework/plugins/types.ts +106 -0
- package/src/framework/seo/geo/examples.tsx +415 -0
- package/src/framework/seo/geo/generateGEOMetadata.ts +441 -0
- package/src/framework/seo/geo/index.ts +61 -0
- package/src/framework/seo/geo/presets.ts +58 -0
- package/src/framework/seo/geo/structuredData.ts +422 -0
- package/src/framework/seo/geo/test-utils.ts +179 -0
- package/src/framework/seo/geo/types.ts +315 -0
- package/src/framework/types/index.ts +623 -0
- package/src/framework/utils/data-fetching.ts +95 -0
- package/src/framework/utils/file-structure.ts +88 -0
- package/src/framework/utils/metadata.ts +152 -0
- package/src/index.ts +31 -0
- package/src/presets/index.ts +8 -0
- package/src/presets/marketing.ts +88 -0
- 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
|
+
}
|