@pixelated-tech/components 3.3.0 → 3.3.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 (60) hide show
  1. package/README.COMPONENTS.md +232 -2
  2. package/README.md +133 -27
  3. package/dist/components/cms/cloudinary.image.js +1 -0
  4. package/dist/components/cms/wordpress.components.js +1 -0
  5. package/dist/components/general/sidepanel.js +1 -1
  6. package/dist/components/menu/menu-accordion.css +10 -0
  7. package/dist/components/menu/menu-accordion.js +2 -1
  8. package/dist/components/menu/menu-simple.css +0 -7
  9. package/dist/components/sitebuilder/config/CompoundFontSelector.css +25 -0
  10. package/dist/components/sitebuilder/config/CompoundFontSelector.js +41 -0
  11. package/dist/components/sitebuilder/config/ConfigBuilder.css +11 -0
  12. package/dist/components/sitebuilder/config/ConfigBuilder.js +188 -29
  13. package/dist/components/sitebuilder/config/ConfigEngine.js +82 -0
  14. package/dist/components/sitebuilder/config/FontSelector.css +82 -0
  15. package/dist/components/sitebuilder/config/FontSelector.js +115 -0
  16. package/dist/components/sitebuilder/config/google-fonts.js +112 -0
  17. package/dist/components/sitebuilder/form/formbuilder.js +1 -0
  18. package/dist/components/sitebuilder/form/formcomponents.js +28 -4
  19. package/dist/components/structured/markdown.js +1 -0
  20. package/dist/components/structured/recipe.js +1 -0
  21. package/dist/components/structured/timeline.js +1 -0
  22. package/dist/css/pixelated.global.css +0 -35
  23. package/dist/css/pixelated.grid.scss +4 -0
  24. package/dist/css/pixelated.visualdesign.scss +88 -0
  25. package/dist/data/routes.json +32 -1
  26. package/dist/data/visualdesignform.json +244 -0
  27. package/dist/index.js +4 -0
  28. package/dist/index.server.js +3 -3
  29. package/dist/types/components/cms/cloudinary.image.d.ts.map +1 -1
  30. package/dist/types/components/cms/wordpress.components.d.ts.map +1 -1
  31. package/dist/types/components/menu/menu-accordion.d.ts.map +1 -1
  32. package/dist/types/components/sitebuilder/config/CompoundFontSelector.d.ts +23 -0
  33. package/dist/types/components/sitebuilder/config/CompoundFontSelector.d.ts.map +1 -0
  34. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +270 -2
  35. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
  36. package/dist/types/components/sitebuilder/config/ConfigEngine.d.ts +10 -0
  37. package/dist/types/components/sitebuilder/config/ConfigEngine.d.ts.map +1 -0
  38. package/dist/types/components/sitebuilder/config/FontSelector.d.ts +27 -0
  39. package/dist/types/components/sitebuilder/config/FontSelector.d.ts.map +1 -0
  40. package/dist/types/components/sitebuilder/config/google-fonts.d.ts +41 -0
  41. package/dist/types/components/sitebuilder/config/google-fonts.d.ts.map +1 -0
  42. package/dist/types/components/sitebuilder/form/formbuilder.d.ts.map +1 -1
  43. package/dist/types/components/sitebuilder/form/formcomponents.d.ts +3 -1
  44. package/dist/types/components/sitebuilder/form/formcomponents.d.ts.map +1 -1
  45. package/dist/types/components/structured/markdown.d.ts.map +1 -1
  46. package/dist/types/components/structured/recipe.d.ts.map +1 -1
  47. package/dist/types/components/structured/timeline.d.ts.map +1 -1
  48. package/dist/types/index.d.ts +4 -0
  49. package/dist/types/index.server.d.ts +3 -3
  50. package/dist/types/stories/sitebuilder/compoundfontselector.stories.d.ts +51 -0
  51. package/dist/types/stories/sitebuilder/compoundfontselector.stories.d.ts.map +1 -0
  52. package/dist/types/stories/sitebuilder/configbuilder.stories.d.ts +55 -0
  53. package/dist/types/stories/sitebuilder/configbuilder.stories.d.ts.map +1 -1
  54. package/dist/types/tests/compoundfontselector.test.d.ts +2 -0
  55. package/dist/types/tests/compoundfontselector.test.d.ts.map +1 -0
  56. package/dist/types/tests/configengine.test.d.ts +2 -0
  57. package/dist/types/tests/configengine.test.d.ts.map +1 -0
  58. package/dist/types/tests/fontselector.test.d.ts +2 -0
  59. package/dist/types/tests/fontselector.test.d.ts.map +1 -0
  60. package/package.json +11 -9
@@ -150,6 +150,17 @@
150
150
  margin-bottom: 15px;
151
151
  }
152
152
 
153
+ .route-headers {
154
+ display: grid;
155
+ grid-template-columns: 2fr 2fr 2fr 2fr auto;
156
+ gap: 10px;
157
+ align-items: center;
158
+ padding: 10px;
159
+ font-weight: bold;
160
+ color: #333;
161
+ margin-bottom: 10px;
162
+ }
163
+
153
164
  .route-item {
154
165
  display: grid;
155
166
  grid-template-columns: 2fr 2fr 2fr 2fr auto;
@@ -1,9 +1,13 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useState, useEffect } from 'react';
3
4
  import PropTypes from 'prop-types';
4
5
  import { Tab } from '../../general/tab';
6
+ import { Accordion } from '../../general/accordion';
5
7
  import { FormEngine } from '../form/formengine';
6
8
  import siteInfoForm from '../../../data/siteinfo-form.json';
9
+ import visualDesignForm from '../../../data/visualdesignform.json';
10
+ import defaultConfigData from '../../../data/routes.json';
7
11
  import './ConfigBuilder.css';
8
12
  const SiteInfoPropTypes = {
9
13
  name: PropTypes.string.isRequired,
@@ -39,38 +43,162 @@ const RoutePropTypes = {
39
43
  title: PropTypes.string,
40
44
  description: PropTypes.string,
41
45
  };
46
+ const VisualDesignPropTypes = {
47
+ 'primary-color': PropTypes.shape({
48
+ value: PropTypes.string.isRequired,
49
+ type: PropTypes.string.isRequired,
50
+ group: PropTypes.string.isRequired,
51
+ label: PropTypes.string.isRequired
52
+ }).isRequired,
53
+ 'secondary-color': PropTypes.shape({
54
+ value: PropTypes.string.isRequired,
55
+ type: PropTypes.string.isRequired,
56
+ group: PropTypes.string.isRequired,
57
+ label: PropTypes.string.isRequired
58
+ }).isRequired,
59
+ 'accent1-color': PropTypes.shape({
60
+ value: PropTypes.string.isRequired,
61
+ type: PropTypes.string.isRequired,
62
+ group: PropTypes.string.isRequired,
63
+ label: PropTypes.string.isRequired
64
+ }).isRequired,
65
+ 'accent2-color': PropTypes.shape({
66
+ value: PropTypes.string.isRequired,
67
+ type: PropTypes.string.isRequired,
68
+ group: PropTypes.string.isRequired,
69
+ label: PropTypes.string.isRequired
70
+ }).isRequired,
71
+ 'bg-color': PropTypes.shape({
72
+ value: PropTypes.string.isRequired,
73
+ type: PropTypes.string.isRequired,
74
+ group: PropTypes.string.isRequired,
75
+ label: PropTypes.string.isRequired
76
+ }).isRequired,
77
+ 'text-color': PropTypes.shape({
78
+ value: PropTypes.string.isRequired,
79
+ type: PropTypes.string.isRequired,
80
+ group: PropTypes.string.isRequired,
81
+ label: PropTypes.string.isRequired
82
+ }).isRequired,
83
+ 'header-font': PropTypes.shape({
84
+ value: PropTypes.string.isRequired,
85
+ type: PropTypes.string.isRequired,
86
+ group: PropTypes.string.isRequired,
87
+ label: PropTypes.string.isRequired
88
+ }).isRequired,
89
+ 'body-font': PropTypes.shape({
90
+ value: PropTypes.string.isRequired,
91
+ type: PropTypes.string.isRequired,
92
+ group: PropTypes.string.isRequired,
93
+ label: PropTypes.string.isRequired
94
+ }).isRequired,
95
+ 'font-size1-min': PropTypes.shape({
96
+ value: PropTypes.string.isRequired,
97
+ type: PropTypes.string.isRequired,
98
+ group: PropTypes.string.isRequired,
99
+ label: PropTypes.string.isRequired
100
+ }).isRequired,
101
+ 'font-size1-max': PropTypes.shape({
102
+ value: PropTypes.string.isRequired,
103
+ type: PropTypes.string.isRequired,
104
+ group: PropTypes.string.isRequired,
105
+ label: PropTypes.string.isRequired
106
+ }).isRequired,
107
+ 'font-size2-min': PropTypes.shape({
108
+ value: PropTypes.string.isRequired,
109
+ type: PropTypes.string.isRequired,
110
+ group: PropTypes.string.isRequired,
111
+ label: PropTypes.string.isRequired
112
+ }).isRequired,
113
+ 'font-size2-max': PropTypes.shape({
114
+ value: PropTypes.string.isRequired,
115
+ type: PropTypes.string.isRequired,
116
+ group: PropTypes.string.isRequired,
117
+ label: PropTypes.string.isRequired
118
+ }).isRequired,
119
+ 'font-size3-min': PropTypes.shape({
120
+ value: PropTypes.string.isRequired,
121
+ type: PropTypes.string.isRequired,
122
+ group: PropTypes.string.isRequired,
123
+ label: PropTypes.string.isRequired
124
+ }).isRequired,
125
+ 'font-size3-max': PropTypes.shape({
126
+ value: PropTypes.string.isRequired,
127
+ type: PropTypes.string.isRequired,
128
+ group: PropTypes.string.isRequired,
129
+ label: PropTypes.string.isRequired
130
+ }).isRequired,
131
+ 'font-size4-min': PropTypes.shape({
132
+ value: PropTypes.string.isRequired,
133
+ type: PropTypes.string.isRequired,
134
+ group: PropTypes.string.isRequired,
135
+ label: PropTypes.string.isRequired
136
+ }).isRequired,
137
+ 'font-size4-max': PropTypes.shape({
138
+ value: PropTypes.string.isRequired,
139
+ type: PropTypes.string.isRequired,
140
+ group: PropTypes.string.isRequired,
141
+ label: PropTypes.string.isRequired
142
+ }).isRequired,
143
+ 'font-size5-min': PropTypes.shape({
144
+ value: PropTypes.string.isRequired,
145
+ type: PropTypes.string.isRequired,
146
+ group: PropTypes.string.isRequired,
147
+ label: PropTypes.string.isRequired
148
+ }).isRequired,
149
+ 'font-size5-max': PropTypes.shape({
150
+ value: PropTypes.string.isRequired,
151
+ type: PropTypes.string.isRequired,
152
+ group: PropTypes.string.isRequired,
153
+ label: PropTypes.string.isRequired
154
+ }).isRequired,
155
+ 'font-size6-min': PropTypes.shape({
156
+ value: PropTypes.string.isRequired,
157
+ type: PropTypes.string.isRequired,
158
+ group: PropTypes.string.isRequired,
159
+ label: PropTypes.string.isRequired
160
+ }).isRequired,
161
+ 'font-size6-max': PropTypes.shape({
162
+ value: PropTypes.string.isRequired,
163
+ type: PropTypes.string.isRequired,
164
+ group: PropTypes.string.isRequired,
165
+ label: PropTypes.string.isRequired
166
+ }).isRequired,
167
+ 'font-min-screen': PropTypes.shape({
168
+ value: PropTypes.string.isRequired,
169
+ type: PropTypes.string.isRequired,
170
+ group: PropTypes.string.isRequired,
171
+ label: PropTypes.string.isRequired
172
+ }).isRequired,
173
+ 'font-max-screen': PropTypes.shape({
174
+ value: PropTypes.string.isRequired,
175
+ type: PropTypes.string.isRequired,
176
+ group: PropTypes.string.isRequired,
177
+ label: PropTypes.string.isRequired
178
+ }).isRequired,
179
+ };
42
180
  const SiteConfigPropTypes = {
43
181
  siteInfo: PropTypes.shape(SiteInfoPropTypes).isRequired,
44
182
  routes: PropTypes.arrayOf(PropTypes.shape(RoutePropTypes).isRequired).isRequired,
183
+ visualdesign: PropTypes.shape(VisualDesignPropTypes),
45
184
  };
46
185
  const ConfigBuilderPropTypes = {
47
186
  initialConfig: PropTypes.shape(SiteConfigPropTypes),
48
187
  onSave: PropTypes.func,
49
188
  };
50
- export function ConfigBuilder({ initialConfig, onSave }) {
51
- const [config, setConfig] = useState(initialConfig || {
52
- siteInfo: {
53
- name: '',
54
- author: '',
55
- description: '',
56
- url: '',
57
- email: '',
58
- favicon: '/favicon.ico',
59
- favicon_sizes: '64x64 32x32 24x24 16x16',
60
- favicon_type: 'image/x-icon',
61
- theme_color: '#ffffff',
62
- background_color: '#ffffff',
63
- default_locale: 'en',
64
- display: 'standalone',
65
- address: {
66
- streetAddress: '',
67
- addressLocality: '',
68
- addressRegion: '',
69
- postalCode: '',
70
- addressCountry: ''
71
- }
72
- },
73
- routes: []
189
+ export function ConfigBuilder(props) {
190
+ const { initialConfig, onSave } = props;
191
+ const defaultConfig = {
192
+ siteInfo: defaultConfigData.siteInfo,
193
+ routes: [], // Start with empty routes, the JSON structure is different
194
+ visualdesign: defaultConfigData.visualdesign
195
+ };
196
+ const [config, setConfig] = useState({
197
+ ...defaultConfig,
198
+ ...initialConfig,
199
+ siteInfo: { ...defaultConfig.siteInfo, ...initialConfig?.siteInfo },
200
+ routes: initialConfig?.routes || [],
201
+ visualdesign: { ...defaultConfig.visualdesign, ...initialConfig?.visualdesign }
74
202
  });
75
203
  const [socialLinks, setSocialLinks] = useState(initialConfig?.siteInfo?.sameAs || ['']);
76
204
  const [isFormValid, setIsFormValid] = useState(false);
@@ -114,9 +242,10 @@ export function ConfigBuilder({ initialConfig, onSave }) {
114
242
  };
115
243
  useEffect(() => {
116
244
  if (initialConfig) {
117
- setConfig(prev => ({
245
+ setConfig((prev) => ({
118
246
  siteInfo: { ...prev.siteInfo, ...initialConfig.siteInfo },
119
- routes: initialConfig.routes || []
247
+ routes: initialConfig.routes || [],
248
+ visualdesign: initialConfig.visualdesign || prev.visualdesign || {}
120
249
  }));
121
250
  setSocialLinks(initialConfig.siteInfo?.sameAs || ['']);
122
251
  }
@@ -130,7 +259,7 @@ export function ConfigBuilder({ initialConfig, onSave }) {
130
259
  value: config.siteInfo[field.props.name] || '',
131
260
  defaultValue: config.siteInfo[field.props.name] || field.props.defaultValue || '',
132
261
  onChange: (value) => {
133
- setConfig(prev => ({
262
+ setConfig((prev) => ({
134
263
  ...prev,
135
264
  siteInfo: {
136
265
  ...prev.siteInfo,
@@ -141,6 +270,26 @@ export function ConfigBuilder({ initialConfig, onSave }) {
141
270
  }
142
271
  }))
143
272
  };
273
+ // Visual Design form data
274
+ const visualFormData = {
275
+ fields: (visualDesignForm.fields || []).map((field) => ({
276
+ ...field,
277
+ props: {
278
+ ...field.props,
279
+ value: (config.visualdesign && config.visualdesign[field.props.name]) ? (config.visualdesign[field.props.name].value ?? config.visualdesign[field.props.name]) : '',
280
+ defaultValue: (config.visualdesign && config.visualdesign[field.props.name]) ? (config.visualdesign[field.props.name].value ?? config.visualdesign[field.props.name]) : field.props.defaultValue || '',
281
+ onChange: (value) => {
282
+ setConfig((prev) => ({
283
+ ...prev,
284
+ visualdesign: {
285
+ ...(prev.visualdesign || {}),
286
+ [field.props.name]: { value }
287
+ }
288
+ }));
289
+ }
290
+ }
291
+ }))
292
+ };
144
293
  const handleFormSubmit = (event) => {
145
294
  event.preventDefault();
146
295
  const formData = new FormData(event.target);
@@ -214,8 +363,18 @@ export function ConfigBuilder({ initialConfig, onSave }) {
214
363
  {
215
364
  id: 'routes',
216
365
  label: 'Routes',
217
- content: (_jsxs("div", { className: "routes-section", children: [_jsx("div", { className: "routes-list", children: config.routes.map((route, index) => (_jsxs("div", { className: "route-item", children: [_jsx("input", { type: "text", placeholder: "Path", value: route.path, onChange: (e) => updateRoute(index, 'path', e.target.value) }), _jsx("input", { type: "text", placeholder: "Component", value: route.component, onChange: (e) => updateRoute(index, 'component', e.target.value) }), _jsx("input", { type: "text", placeholder: "Title", value: route.title || '', onChange: (e) => updateRoute(index, 'title', e.target.value) }), _jsx("input", { type: "text", placeholder: "Description", value: route.description || '', onChange: (e) => updateRoute(index, 'description', e.target.value) }), _jsx("button", { onClick: () => removeRoute(index), children: "Remove" })] }, index))) }), _jsx("button", { onClick: addRoute, children: "Add Route" })] }))
366
+ content: (_jsxs("div", { className: "routes-section", children: [_jsxs("div", { className: "routes-list", children: [_jsxs("div", { className: "route-headers", children: [_jsx("span", { children: "Path" }), _jsx("span", { children: "Component" }), _jsx("span", { children: "Title" }), _jsx("span", { children: "Description" }), _jsx("span", { children: "Actions" })] }), config.routes.map((route, index) => (_jsxs("div", { className: "route-item", children: [_jsx("input", { type: "text", placeholder: "Path", value: route.path, onChange: (e) => updateRoute(index, 'path', e.target.value) }), _jsx("input", { type: "text", placeholder: "Component", value: route.component, onChange: (e) => updateRoute(index, 'component', e.target.value) }), _jsx("input", { type: "text", placeholder: "Title", value: route.title || '', onChange: (e) => updateRoute(index, 'title', e.target.value) }), _jsx("input", { type: "text", placeholder: "Description", value: route.description || '', onChange: (e) => updateRoute(index, 'description', e.target.value) }), _jsx("button", { onClick: () => removeRoute(index), children: "Remove" })] }, index)))] }), _jsx("button", { onClick: addRoute, children: "Add Route" })] }))
367
+ },
368
+ {
369
+ id: 'visualdesign',
370
+ label: 'Visual Design',
371
+ content: (_jsx("div", { className: "visual-design-section", children: _jsx(FormEngine, { formData: visualFormData, name: "visualdesign", id: "visualdesign" }) }))
372
+ }
373
+ ], orientation: "top" }), _jsx("button", { onClick: handleSave, disabled: !isFormValid, className: isFormValid ? 'save-button-valid' : 'save-button-invalid', children: "Save Config" }), !isFormValid && (_jsx("div", { className: "validation-message", children: "Please fill in all required fields (marked with *) before saving." })), _jsx(Accordion, { items: [
374
+ {
375
+ title: 'Configuration Preview',
376
+ content: _jsx("pre", { children: JSON.stringify(config, null, 2) })
218
377
  }
219
- ], orientation: "top" }), _jsx("button", { onClick: handleSave, disabled: !isFormValid, className: isFormValid ? 'save-button-valid' : 'save-button-invalid', children: "Save Config" }), !isFormValid && (_jsx("div", { className: "validation-message", children: "Please fill in all required fields (marked with *) before saving." })), _jsx("pre", { children: JSON.stringify(config, null, 2) })] }));
378
+ ] })] }));
220
379
  }
221
380
  ConfigBuilder.propTypes = ConfigBuilderPropTypes;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { generateGoogleFontsUrl } from './google-fonts';
3
+ export function VisualDesignStyles({ visualdesign }) {
4
+ const tokens = visualdesign || {};
5
+ const resolveValue = (v) => (v && typeof v === 'object' && 'value' in v) ? v.value : v;
6
+ const varLines = [];
7
+ // Always include base font sizing first (from pixelated.visualdesign.scss)
8
+ varLines.push('--font-size1-min: 2.00rem;', '--font-size1-max: 3.00rem;', '--font-size2-min: 1.35rem;', '--font-size2-max: 1.75rem;', '--font-size3-min: 1.17rem;', '--font-size3-max: 1.35rem;', '--font-size4-min: 1.00rem;', '--font-size4-max: 1.25rem;', '--font-size5-min: 0.83rem;', '--font-size5-max: 1.00rem;', '--font-size6-min: 0.67rem;', '--font-size6-max: 0.85rem;', '--font-min-screen: 375px;', '--font-max-screen: 1440px;');
9
+ for (const [key, val] of Object.entries(tokens)) {
10
+ const value = resolveValue(val);
11
+ if (value === undefined || value === null)
12
+ continue;
13
+ // Handle font family construction from 3-field structure (legacy)
14
+ if (key.endsWith('-primary')) {
15
+ const baseKey = key.replace('-primary', '');
16
+ const primary = value;
17
+ const fallback = resolveValue(tokens[`${baseKey}-fallback`]);
18
+ const generic = resolveValue(tokens[`${baseKey}-generic`]);
19
+ // Build font stack: "Primary Font", Fallback, generic
20
+ const fontStack = [primary, fallback, generic].filter(Boolean).map(f => `"${f}"`).join(', ');
21
+ varLines.push(`--${baseKey}-family: ${fontStack};`);
22
+ }
23
+ else if (key === 'header-font' || key === 'body-font') {
24
+ // Handle compound font values (new format)
25
+ varLines.push(`--${key}: ${value};`);
26
+ }
27
+ else if (!key.endsWith('-fallback') && !key.endsWith('-generic')) {
28
+ // Skip fallback and generic fields as they're handled above
29
+ varLines.push(`--${key}: ${value};`);
30
+ }
31
+ }
32
+ const fontLines = [];
33
+ // Generate clamp calculations for font sizes
34
+ for (let i = 1; i <= 6; i++) {
35
+ varLines.push(`--font-size${i}: clamp(var(--font-size${i}-min), calc(var(--font-size${i}-min) + ((var(--font-size${i}-max) - var(--font-size${i}-min)) * ((100vw - var(--font-min-screen)) / (var(--font-max-screen) - var(--font-min-screen))))), var(--font-size${i}-max));`);
36
+ }
37
+ // Generate h1-h6 font-size rules
38
+ for (let i = 1; i <= 6; i++) {
39
+ fontLines.push(`h${i} { font-size: var(--font-size${i}); }`);
40
+ }
41
+ const css = [
42
+ ':root {',
43
+ ...varLines.map(l => ` ${l}`),
44
+ '}',
45
+ '',
46
+ '/* Base visual design styles */',
47
+ 'body {',
48
+ ' background-color: var(--bg-color) !important;',
49
+ ' color: var(--text-color);',
50
+ ' font-family: var(--body-font);',
51
+ '}',
52
+ 'h1, h2, h3, h4, h5, h6 {',
53
+ ' line-height: 1.1;',
54
+ ' font-family: var(--header-font);',
55
+ '}',
56
+ 'html { font-size: 1.0rem; /* 16px; */ }',
57
+ '',
58
+ ...fontLines
59
+ ].join('\n');
60
+ return _jsx("style", { dangerouslySetInnerHTML: { __html: css } });
61
+ }
62
+ /**
63
+ * Component to handle Google Fonts imports - should be used in the document head
64
+ */
65
+ export function GoogleFontsImports({ visualdesign }) {
66
+ const tokens = visualdesign || {};
67
+ const fonts = [];
68
+ // Extract Google font names from the new 3-field font structure
69
+ for (const [key, val] of Object.entries(tokens)) {
70
+ if (key.endsWith('-primary') && typeof val === 'string' && val.trim()) {
71
+ // Only include fonts that are not web-safe (web-safe fonts don't need Google Fonts import)
72
+ const webSafeFonts = ['Arial', 'Helvetica', 'Times New Roman', 'Times', 'Courier New', 'Courier', 'Georgia', 'Palatino', 'Garamond', 'Bookman', 'Comic Sans MS', 'Trebuchet MS', 'Arial Black', 'Impact', 'Lucida Sans Unicode', 'Lucida Grande', 'MS Sans Serif', 'MS Serif', 'New York', 'System', 'Apple System', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue'];
73
+ if (!webSafeFonts.includes(val.trim())) {
74
+ fonts.push(val.trim());
75
+ }
76
+ }
77
+ }
78
+ const googleFontsUrl = generateGoogleFontsUrl(fonts);
79
+ if (!googleFontsUrl)
80
+ return null;
81
+ return (_jsxs(_Fragment, { children: [_jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }), _jsx("link", { href: googleFontsUrl, rel: "stylesheet" })] }));
82
+ }
@@ -0,0 +1,82 @@
1
+ .font-selector-container {
2
+ margin-bottom: 1rem;
3
+ }
4
+
5
+ .font-selector-label {
6
+ display: block;
7
+ margin-bottom: 0.5rem;
8
+ font-weight: 500;
9
+ }
10
+
11
+ .font-selector-required {
12
+ color: #e53e3e;
13
+ margin-left: 0.25rem;
14
+ }
15
+
16
+ .font-selector-tooltip {
17
+ margin-left: 0.5rem;
18
+ cursor: help;
19
+ opacity: 0.7;
20
+ }
21
+
22
+ .font-selector-input-container {
23
+ position: relative;
24
+ }
25
+
26
+ .font-selector-input {
27
+ width: 100%;
28
+ padding: 0.5rem;
29
+ border: 1px solid #d1d5db;
30
+ border-radius: 0.375rem;
31
+ font-size: 0.875rem;
32
+ }
33
+
34
+ .font-selector-input:focus {
35
+ outline: none;
36
+ border-color: #3b82f6;
37
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
38
+ }
39
+
40
+ .font-selector-dropdown {
41
+ position: absolute;
42
+ top: 100%;
43
+ left: 0;
44
+ right: 0;
45
+ background: white;
46
+ border: 1px solid #d1d5db;
47
+ border-radius: 0.375rem;
48
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
49
+ max-height: 200px;
50
+ overflow-y: auto;
51
+ z-index: 1000;
52
+ }
53
+
54
+ .font-selector-option {
55
+ padding: 0.5rem;
56
+ cursor: pointer;
57
+ border-bottom: 1px solid #f3f4f6;
58
+ }
59
+
60
+ .font-selector-option:hover {
61
+ background-color: #f9fafb;
62
+ }
63
+
64
+ .font-selector-option:last-child {
65
+ border-bottom: none;
66
+ }
67
+
68
+ .font-selector-loading {
69
+ padding: 0.5rem;
70
+ color: #6b7280;
71
+ font-style: italic;
72
+ }
73
+
74
+ .font-selector-font-name {
75
+ font-weight: 500;
76
+ }
77
+
78
+ .font-selector-font-category {
79
+ color: #6b7280;
80
+ font-size: 0.875rem;
81
+ margin-left: 0.5rem;
82
+ }
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { getFontOptions } from './google-fonts';
5
+ import './FontSelector.css';
6
+ const WEBSafe_FONTS = [
7
+ { value: 'Arial', label: 'Arial' },
8
+ { value: 'Bookman', label: 'Bookman' },
9
+ { value: 'Comic Sans MS', label: 'Comic Sans MS' },
10
+ { value: 'Courier New', label: 'Courier New' },
11
+ { value: 'Garamond', label: 'Garamond' },
12
+ { value: 'Geneva', label: 'Geneva' },
13
+ { value: 'Georgia', label: 'Georgia' },
14
+ { value: 'Helvetica', label: 'Helvetica' },
15
+ { value: 'Impact', label: 'Impact' },
16
+ { value: 'Lucida Console', label: 'Lucida Console' },
17
+ { value: 'Monaco', label: 'Monaco' },
18
+ { value: 'Palatino', label: 'Palatino' },
19
+ { value: 'Tahoma', label: 'Tahoma' },
20
+ { value: 'Times New Roman', label: 'Times New Roman' },
21
+ { value: 'Trebuchet MS', label: 'Trebuchet MS' },
22
+ { value: 'Verdana', label: 'Verdana' },
23
+ ];
24
+ const GENERIC_FAMILIES = [
25
+ { value: 'cursive', label: 'cursive' },
26
+ { value: 'fantasy', label: 'fantasy' },
27
+ { value: 'monospace', label: 'monospace' },
28
+ { value: 'sans-serif', label: 'sans-serif' },
29
+ { value: 'serif', label: 'serif' },
30
+ { value: 'system-ui', label: 'system-ui' },
31
+ { value: 'ui-monospace', label: 'ui-monospace' },
32
+ { value: 'ui-rounded', label: 'ui-rounded' },
33
+ { value: 'ui-sans-serif', label: 'ui-sans-serif' },
34
+ { value: 'ui-serif', label: 'ui-serif' },
35
+ ];
36
+ export function FontSelector({ id, name, label, fontType, required = false, placeholder, value = '', onChange }) {
37
+ const [inputValue, setInputValue] = useState(value);
38
+ const [googleFonts, setGoogleFonts] = useState([]);
39
+ const [isLoading, setIsLoading] = useState(false);
40
+ const [showDropdown, setShowDropdown] = useState(false);
41
+ // Load Google Fonts for autocomplete
42
+ useEffect(() => {
43
+ if (fontType === 'google') {
44
+ setIsLoading(true);
45
+ getFontOptions().then(options => {
46
+ setGoogleFonts(options);
47
+ setIsLoading(false);
48
+ }).catch(() => {
49
+ setIsLoading(false);
50
+ });
51
+ }
52
+ }, [fontType]);
53
+ // Get options based on font type
54
+ const options = useMemo(() => {
55
+ switch (fontType) {
56
+ case 'google':
57
+ return googleFonts;
58
+ case 'websafe':
59
+ return WEBSafe_FONTS;
60
+ case 'generic':
61
+ return GENERIC_FAMILIES;
62
+ default:
63
+ return [];
64
+ }
65
+ }, [fontType, googleFonts]);
66
+ // Filter options based on input
67
+ const filteredOptions = useMemo(() => {
68
+ if (!inputValue)
69
+ return options.slice(0, 10); // Show first 10 when no input
70
+ return options.filter(option => option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
71
+ option.value.toLowerCase().includes(inputValue.toLowerCase())).slice(0, 10); // Limit to 10 results
72
+ }, [options, inputValue]);
73
+ const handleInputChange = (e) => {
74
+ const newValue = e.target.value;
75
+ setInputValue(newValue);
76
+ setShowDropdown(true);
77
+ onChange?.(newValue);
78
+ };
79
+ const handleOptionSelect = (option) => {
80
+ setInputValue(option.value);
81
+ setShowDropdown(false);
82
+ onChange?.(option.value);
83
+ };
84
+ const handleFocus = () => {
85
+ setShowDropdown(true);
86
+ };
87
+ const handleBlur = () => {
88
+ // Delay hiding dropdown to allow clicks on options
89
+ setTimeout(() => setShowDropdown(false), 200);
90
+ };
91
+ // Generate tooltip with Google Fonts link for Google fonts
92
+ const getTooltip = () => {
93
+ if (fontType === 'google' && inputValue) {
94
+ const fontName = inputValue.replace(/\s+/g, '+');
95
+ return `👁️ [Preview ${inputValue} on Google Fonts](https://fonts.google.com/specimen/${fontName})`;
96
+ }
97
+ return null;
98
+ };
99
+ return (_jsxs("div", { className: "font-selector-container", children: [_jsxs("label", { htmlFor: id, className: "font-selector-label", children: [label, required && _jsx("span", { className: "font-selector-required", children: "*" }), getTooltip() && (_jsx("span", { className: "font-selector-tooltip", title: getTooltip().replace(/\[([^\]]+)\]\([^)]+\)/, '$1'), children: "\uD83D\uDC41\uFE0F" }))] }), _jsxs("div", { className: "font-selector-input-container", children: [_jsx("input", { type: "text", id: id, name: name, value: inputValue, onChange: handleInputChange, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder, required: required, autoComplete: "off", className: "font-selector-input" }), showDropdown && filteredOptions.length > 0 && (_jsx("div", { className: "font-selector-dropdown", children: isLoading ? (_jsx("div", { className: "font-selector-loading", children: "Loading fonts..." })) : (filteredOptions.map((option) => (_jsxs("div", { className: "font-selector-option", onClick: () => handleOptionSelect(option), onKeyDown: (e) => {
100
+ if (e.key === 'Enter' || e.key === ' ') {
101
+ e.preventDefault();
102
+ handleOptionSelect(option);
103
+ }
104
+ }, tabIndex: 0, role: "option", "aria-selected": inputValue === option.value, children: [_jsx("span", { className: "font-selector-font-name", children: option.label }), option.category && (_jsxs("span", { className: "font-selector-font-category", children: ["(", option.category, ")"] }))] }, option.value)))) }))] })] }));
105
+ }
106
+ FontSelector.propTypes = {
107
+ id: PropTypes.string.isRequired,
108
+ name: PropTypes.string.isRequired,
109
+ label: PropTypes.string.isRequired,
110
+ fontType: PropTypes.oneOf(['google', 'websafe', 'generic']).isRequired,
111
+ required: PropTypes.bool,
112
+ placeholder: PropTypes.string,
113
+ value: PropTypes.string,
114
+ onChange: PropTypes.func,
115
+ };