@hypen-space/web 0.2.12 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/src/dom/applicators/effects.js +38 -2
  2. package/dist/src/dom/applicators/effects.js.map +3 -3
  3. package/dist/src/dom/applicators/events.js +280 -397
  4. package/dist/src/dom/applicators/events.js.map +5 -4
  5. package/dist/src/dom/applicators/font.js +94 -5
  6. package/dist/src/dom/applicators/font.js.map +3 -3
  7. package/dist/src/dom/applicators/index.js +590 -425
  8. package/dist/src/dom/applicators/index.js.map +10 -9
  9. package/dist/src/dom/applicators/layout.js +33 -5
  10. package/dist/src/dom/applicators/layout.js.map +3 -3
  11. package/dist/src/dom/applicators/size.js +81 -16
  12. package/dist/src/dom/applicators/size.js.map +3 -3
  13. package/dist/src/dom/components/hypenapp.js +296 -0
  14. package/dist/src/dom/components/hypenapp.js.map +10 -0
  15. package/dist/src/dom/components/index.js +263 -1
  16. package/dist/src/dom/components/index.js.map +5 -4
  17. package/dist/src/dom/element-data.js +140 -0
  18. package/dist/src/dom/element-data.js.map +10 -0
  19. package/dist/src/dom/index.js +857 -430
  20. package/dist/src/dom/index.js.map +13 -11
  21. package/dist/src/dom/renderer.js +857 -430
  22. package/dist/src/dom/renderer.js.map +13 -11
  23. package/dist/src/hypen.js +857 -430
  24. package/dist/src/hypen.js.map +13 -11
  25. package/dist/src/index.js +862 -430
  26. package/dist/src/index.js.map +15 -12
  27. package/package.json +3 -3
  28. package/src/canvas/QUICKSTART.md +2 -4
  29. package/src/dom/applicators/effects.ts +45 -1
  30. package/src/dom/applicators/events.ts +348 -537
  31. package/src/dom/applicators/font.ts +127 -2
  32. package/src/dom/applicators/index.ts +117 -7
  33. package/src/dom/applicators/layout.ts +40 -4
  34. package/src/dom/applicators/size.ts +101 -16
  35. package/src/dom/components/hypenapp.ts +348 -0
  36. package/src/dom/components/index.ts +2 -0
  37. package/src/dom/element-data.ts +234 -0
  38. package/src/dom/renderer.ts +8 -5
  39. package/src/index.ts +3 -0
@@ -1,9 +1,78 @@
1
1
  /**
2
- * Font Applicators
2
+ * Font Applicators with Google Fonts support
3
3
  */
4
4
 
5
5
  import type { ApplicatorHandler } from "./index.js";
6
6
 
7
+ // Track loaded Google Fonts to avoid duplicate link tags
8
+ const loadedGoogleFonts = new Set<string>();
9
+
10
+ // System font keywords that shouldn't be loaded from Google Fonts
11
+ const systemFontKeywords = new Set([
12
+ "default", "system", "system-ui", "inherit", "initial", "unset",
13
+ "serif", "sans-serif", "monospace", "cursive", "fantasy",
14
+ "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Arial", "Helvetica",
15
+ "Times New Roman", "Georgia", "Courier New", "Verdana", "Tahoma",
16
+ ]);
17
+
18
+ /**
19
+ * Check if a font name is a system font or generic CSS font.
20
+ */
21
+ function isSystemFont(fontName: string): boolean {
22
+ const normalized = fontName.toLowerCase().trim();
23
+ return systemFontKeywords.has(normalized) ||
24
+ normalized.startsWith("-") ||
25
+ normalized.startsWith("ui-");
26
+ }
27
+
28
+ /**
29
+ * Load a Google Font by injecting a link tag.
30
+ * @param fontName - The Google Font name (e.g., "Roboto", "Open Sans")
31
+ */
32
+ function loadGoogleFont(fontName: string): void {
33
+ const normalized = fontName.trim();
34
+
35
+ // Skip if already loaded or is a system font
36
+ if (loadedGoogleFonts.has(normalized) || isSystemFont(normalized)) {
37
+ return;
38
+ }
39
+
40
+ // Mark as loading to avoid duplicates
41
+ loadedGoogleFonts.add(normalized);
42
+
43
+ // Create link element for Google Fonts
44
+ const link = document.createElement("link");
45
+ link.rel = "stylesheet";
46
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(normalized)}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
47
+
48
+ // Add to document head
49
+ document.head.appendChild(link);
50
+ }
51
+
52
+ /**
53
+ * Parse a fontFamily value and load any Google Fonts.
54
+ * Returns a CSS-safe font-family string.
55
+ */
56
+ function processFontFamily(value: string): string {
57
+ // Split by comma to handle font stacks
58
+ const fonts = value.split(",").map(f => f.trim().replace(/["']/g, ""));
59
+
60
+ for (const font of fonts) {
61
+ if (!isSystemFont(font)) {
62
+ loadGoogleFont(font);
63
+ }
64
+ }
65
+
66
+ // Return original value with proper quoting for CSS
67
+ return fonts.map(f => {
68
+ // Quote font names that contain spaces or special characters
69
+ if (f.includes(" ") && !f.startsWith('"') && !f.startsWith("'")) {
70
+ return `"${f}"`;
71
+ }
72
+ return f;
73
+ }).join(", ");
74
+ }
75
+
7
76
  export const fontHandlers: Record<string, ApplicatorHandler> = {
8
77
  fontSize: (el, value) => {
9
78
  el.style.fontSize = typeof value === "number" ? `${value}px` : String(value);
@@ -14,7 +83,9 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
14
83
  },
15
84
 
16
85
  fontFamily: (el, value) => {
17
- el.style.fontFamily = String(value);
86
+ const fontValue = String(value);
87
+ // Process font family and load Google Fonts as needed
88
+ el.style.fontFamily = processFontFamily(fontValue);
18
89
  },
19
90
 
20
91
  textAlign: (el, value) => {
@@ -24,4 +95,58 @@ export const fontHandlers: Record<string, ApplicatorHandler> = {
24
95
  lineHeight: (el, value) => {
25
96
  el.style.lineHeight = String(value);
26
97
  },
98
+
99
+ fontStyle: (el, value) => {
100
+ // Support: normal, italic, oblique
101
+ el.style.fontStyle = String(value);
102
+ },
103
+
104
+ textTransform: (el, value) => {
105
+ // Support: none, capitalize, uppercase, lowercase
106
+ el.style.textTransform = String(value);
107
+ },
108
+ };
109
+
110
+ // Export the font loading utility for manual use
111
+ export const GoogleFonts = {
112
+ /**
113
+ * Preload a Google Font before it's used.
114
+ */
115
+ preload: loadGoogleFont,
116
+
117
+ /**
118
+ * Check if a font has been loaded.
119
+ */
120
+ isLoaded: (fontName: string) => loadedGoogleFonts.has(fontName.trim()),
121
+
122
+ /**
123
+ * Get list of loaded fonts.
124
+ */
125
+ getLoadedFonts: () => Array.from(loadedGoogleFonts),
126
+
127
+ /**
128
+ * Popular Google Fonts for reference.
129
+ */
130
+ popular: [
131
+ "Roboto",
132
+ "Open Sans",
133
+ "Lato",
134
+ "Montserrat",
135
+ "Poppins",
136
+ "Inter",
137
+ "Nunito",
138
+ "Playfair Display",
139
+ "Merriweather",
140
+ "Source Code Pro",
141
+ "Fira Code",
142
+ "JetBrains Mono",
143
+ "Raleway",
144
+ "Ubuntu",
145
+ "Oswald",
146
+ "Quicksand",
147
+ "Work Sans",
148
+ "Rubik",
149
+ "Karla",
150
+ "DM Sans",
151
+ ],
27
152
  };
@@ -6,6 +6,47 @@
6
6
 
7
7
  export type ApplicatorHandler = (element: HTMLElement, value: any) => void;
8
8
 
9
+ /**
10
+ * Tailwind breakpoint values for responsive variants
11
+ */
12
+ const BREAKPOINTS: Record<string, string> = {
13
+ sm: '640px',
14
+ md: '768px',
15
+ lg: '1024px',
16
+ xl: '1280px',
17
+ '2xl': '1536px',
18
+ };
19
+
20
+ /**
21
+ * Singleton stylesheet for variant CSS rules
22
+ */
23
+ let variantStyleSheet: CSSStyleSheet | null = null;
24
+
25
+ /**
26
+ * Track inserted rules to avoid duplicates
27
+ */
28
+ const insertedRules = new Set<string>();
29
+
30
+ /**
31
+ * Get or create the variant stylesheet
32
+ */
33
+ function getVariantStyleSheet(): CSSStyleSheet {
34
+ if (!variantStyleSheet) {
35
+ const style = document.createElement('style');
36
+ style.id = 'hypen-variants';
37
+ document.head.appendChild(style);
38
+ variantStyleSheet = style.sheet as CSSStyleSheet;
39
+ }
40
+ return variantStyleSheet;
41
+ }
42
+
43
+ /**
44
+ * Generate a simple hash for deduplication
45
+ */
46
+ function hashValue(value: any): string {
47
+ return String(value).replace(/[^a-zA-Z0-9]/g, '').slice(0, 8);
48
+ }
49
+
9
50
  export class ApplicatorRegistry {
10
51
  private handlers: Map<string, ApplicatorHandler> = new Map();
11
52
  private elementState: WeakMap<HTMLElement, Map<string, any>> = new WeakMap();
@@ -245,18 +286,87 @@ export class ApplicatorRegistry {
245
286
  }
246
287
 
247
288
  /**
248
- * Set a CSS property with automatic unit handling
289
+ * Set a CSS property with automatic unit handling and variant support
249
290
  */
250
291
  private setStyleProperty(element: HTMLElement, name: string, value: any): void {
251
- // Convert camelCase to kebab-case
252
- const cssName = name.replace(/([A-Z])/g, "-$1").toLowerCase();
292
+ const atIndex = name.indexOf('@');
293
+ const colonIndex = name.indexOf(':');
294
+
295
+ // Responsive variant: padding@md, width@lg, etc.
296
+ if (atIndex !== -1) {
297
+ const prop = name.slice(0, atIndex);
298
+ const breakpoint = name.slice(atIndex + 1);
299
+ const minWidth = BREAKPOINTS[breakpoint];
300
+
301
+ if (minWidth) {
302
+ const cssName = this.toKebabCase(prop);
303
+ const cssValue = this.formatCssValue(cssName, value);
304
+ const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${breakpoint}-${hashValue(value)}`;
305
+ const ruleKey = `${className}:${cssValue}`;
306
+
307
+ // Avoid duplicate rule insertion
308
+ if (!insertedRules.has(ruleKey)) {
309
+ const sheet = getVariantStyleSheet();
310
+ sheet.insertRule(
311
+ `@media (min-width: ${minWidth}) { .${className} { ${cssName}: ${cssValue}; } }`,
312
+ sheet.cssRules.length
313
+ );
314
+ insertedRules.add(ruleKey);
315
+ }
253
316
 
254
- // Add units for numeric values on size properties
317
+ element.classList.add(className);
318
+ }
319
+ return;
320
+ }
321
+
322
+ // State variant: background-color:hover, border-color:focus, etc.
323
+ if (colonIndex !== -1) {
324
+ const prop = name.slice(0, colonIndex);
325
+ const state = name.slice(colonIndex + 1);
326
+
327
+ // Only handle known CSS pseudo-states
328
+ const validStates = ['hover', 'focus', 'active', 'disabled', 'focus-visible', 'focus-within'];
329
+ if (validStates.includes(state)) {
330
+ const cssName = this.toKebabCase(prop);
331
+ const cssValue = this.formatCssValue(cssName, value);
332
+ const className = `hypen-${cssName.replace(/[^a-zA-Z0-9-]/g, '')}-${state}-${hashValue(value)}`;
333
+ const ruleKey = `${className}:${cssValue}`;
334
+
335
+ // Avoid duplicate rule insertion
336
+ if (!insertedRules.has(ruleKey)) {
337
+ const sheet = getVariantStyleSheet();
338
+ sheet.insertRule(
339
+ `.${className}:${state} { ${cssName}: ${cssValue}; }`,
340
+ sheet.cssRules.length
341
+ );
342
+ insertedRules.add(ruleKey);
343
+ }
344
+
345
+ element.classList.add(className);
346
+ }
347
+ return;
348
+ }
349
+
350
+ // Normal property: convert camelCase to kebab-case and apply
351
+ const cssName = this.toKebabCase(name);
352
+ element.style.setProperty(cssName, this.formatCssValue(cssName, value));
353
+ }
354
+
355
+ /**
356
+ * Convert camelCase to kebab-case
357
+ */
358
+ private toKebabCase(name: string): string {
359
+ return name.replace(/([A-Z])/g, "-$1").toLowerCase();
360
+ }
361
+
362
+ /**
363
+ * Format a CSS value with automatic unit handling
364
+ */
365
+ private formatCssValue(cssName: string, value: any): string {
255
366
  if (typeof value === "number" && this.needsUnit(cssName)) {
256
- element.style.setProperty(cssName, `${value}px`);
257
- } else {
258
- element.style.setProperty(cssName, String(value));
367
+ return `${value}px`;
259
368
  }
369
+ return String(value);
260
370
  }
261
371
 
262
372
  /**
@@ -4,10 +4,46 @@
4
4
 
5
5
  import type { ApplicatorHandler } from "./index.js";
6
6
 
7
+ /**
8
+ * Maps Hypen alignment values to CSS flexbox values.
9
+ * Ensures consistent cross-platform API (Android/iOS/Web).
10
+ */
11
+ function mapAlignmentValue(value: string): string {
12
+ const v = String(value).toLowerCase();
13
+ switch (v) {
14
+ // Positional values -> CSS equivalents
15
+ case "top":
16
+ case "start":
17
+ case "leading":
18
+ case "left":
19
+ return "flex-start";
20
+ case "bottom":
21
+ case "end":
22
+ case "trailing":
23
+ case "right":
24
+ return "flex-end";
25
+ case "center":
26
+ return "center";
27
+ // Spacing values -> CSS equivalents
28
+ case "spacebetween":
29
+ case "space-between":
30
+ return "space-between";
31
+ case "spacearound":
32
+ case "space-around":
33
+ return "space-around";
34
+ case "spaceevenly":
35
+ case "space-evenly":
36
+ return "space-evenly";
37
+ // Pass through CSS values as-is
38
+ default:
39
+ return v;
40
+ }
41
+ }
42
+
7
43
  export const layoutHandlers: Record<string, ApplicatorHandler> = {
8
44
  // Unified alignment API - works for both Column and Row
9
45
  verticalAlignment: (el, value) => {
10
- const val = String(value);
46
+ const val = mapAlignmentValue(String(value));
11
47
  // Check flex-direction to determine which CSS property to set
12
48
  const flexDirection = getComputedStyle(el).flexDirection;
13
49
  if (flexDirection === "column" || flexDirection === "column-reverse") {
@@ -20,7 +56,7 @@ export const layoutHandlers: Record<string, ApplicatorHandler> = {
20
56
  },
21
57
 
22
58
  horizontalAlignment: (el, value) => {
23
- const val = String(value);
59
+ const val = mapAlignmentValue(String(value));
24
60
  // Check flex-direction to determine which CSS property to set
25
61
  const flexDirection = getComputedStyle(el).flexDirection;
26
62
  if (flexDirection === "column" || flexDirection === "column-reverse") {
@@ -34,11 +70,11 @@ export const layoutHandlers: Record<string, ApplicatorHandler> = {
34
70
 
35
71
  // Legacy aliases (kept for backward compatibility)
36
72
  horizontalAlign: (el, value) => {
37
- el.style.justifyContent = String(value);
73
+ el.style.justifyContent = mapAlignmentValue(String(value));
38
74
  },
39
75
 
40
76
  verticalAlign: (el, value) => {
41
- el.style.alignItems = String(value);
77
+ el.style.alignItems = mapAlignmentValue(String(value));
42
78
  },
43
79
 
44
80
  gap: (el, value) => {
@@ -1,52 +1,137 @@
1
1
  /**
2
2
  * Size Applicators
3
+ *
4
+ * Cross-platform sizing value support:
5
+ * - Numbers: treated as px (platform default)
6
+ * - "100px": absolute pixels (1px = 1px everywhere)
7
+ * - "100dp" / "100pt": density-independent (1dp ≈ 1pt, scaled by device)
8
+ * - "50%": percentage of parent
9
+ * - "50vw" / "50vh": viewport width/height
10
+ * - "fill" / "100%": fill available space
11
+ * - "wrap" / "auto": fit content
3
12
  */
4
13
 
5
14
  import type { ApplicatorHandler } from "./index.js";
6
15
 
16
+ /**
17
+ * Parse a size value and return CSS-compatible string.
18
+ * Ensures cross-platform compatibility with Android/iOS.
19
+ */
20
+ function parseSizeValue(value: any): string | null {
21
+ if (value === null || value === undefined) return null;
22
+
23
+ // Numbers default to px
24
+ if (typeof value === "number") {
25
+ return `${value}px`;
26
+ }
27
+
28
+ const str = String(value).trim().toLowerCase();
29
+
30
+ // Keywords
31
+ switch (str) {
32
+ case "fill":
33
+ case "match_parent":
34
+ return "100%";
35
+ case "wrap":
36
+ case "wrap_content":
37
+ case "auto":
38
+ return "auto";
39
+ case "infinity":
40
+ case "inf":
41
+ case "max":
42
+ return "100%";
43
+ }
44
+
45
+ // Parse value with unit
46
+ const match = str.match(/^(-?[\d.]+)\s*(px|dp|pt|%|vw|vh|vmin|vmax|em|rem)?$/);
47
+ if (!match) {
48
+ // Pass through other CSS values as-is (e.g., "calc(...)", "fit-content")
49
+ return str;
50
+ }
51
+
52
+ const num = parseFloat(match[1]);
53
+ const unit = match[2] || "px";
54
+
55
+ switch (unit) {
56
+ case "px":
57
+ // Absolute pixels - use as-is
58
+ return `${num}px`;
59
+ case "dp":
60
+ case "pt":
61
+ // Density-independent points
62
+ // On web, 1dp/1pt ≈ 1px at standard density (96dpi)
63
+ // CSS already handles this via px, so we just use px
64
+ // For true density independence, we'd need to query devicePixelRatio
65
+ // but CSS px is already defined as 1/96th of an inch
66
+ return `${num}px`;
67
+ case "%":
68
+ return `${num}%`;
69
+ case "vw":
70
+ return `${num}vw`;
71
+ case "vh":
72
+ return `${num}vh`;
73
+ case "vmin":
74
+ return `${num}vmin`;
75
+ case "vmax":
76
+ return `${num}vmax`;
77
+ case "em":
78
+ return `${num}em`;
79
+ case "rem":
80
+ return `${num}rem`;
81
+ default:
82
+ return `${num}px`;
83
+ }
84
+ }
85
+
7
86
  export const sizeHandlers: Record<string, ApplicatorHandler> = {
8
87
  width: (el, value) => {
9
- el.style.width = typeof value === "number" ? `${value}px` : String(value);
88
+ const size = parseSizeValue(value);
89
+ if (size) el.style.width = size;
10
90
  },
11
91
 
12
92
  height: (el, value) => {
13
- el.style.height = typeof value === "number" ? `${value}px` : String(value);
93
+ const size = parseSizeValue(value);
94
+ if (size) el.style.height = size;
14
95
  },
15
96
 
16
97
  minWidth: (el, value) => {
17
- el.style.minWidth = typeof value === "number" ? `${value}px` : String(value);
98
+ const size = parseSizeValue(value);
99
+ if (size) el.style.minWidth = size;
18
100
  },
19
101
 
20
102
  minHeight: (el, value) => {
21
- el.style.minHeight = typeof value === "number" ? `${value}px` : String(value);
103
+ const size = parseSizeValue(value);
104
+ if (size) el.style.minHeight = size;
22
105
  },
23
106
 
24
107
  maxWidth: (el, value) => {
25
- el.style.maxWidth = typeof value === "number" ? `${value}px` : String(value);
108
+ const size = parseSizeValue(value);
109
+ if (size) el.style.maxWidth = size;
26
110
  },
27
111
 
28
112
  maxHeight: (el, value) => {
29
- el.style.maxHeight = typeof value === "number" ? `${value}px` : String(value);
113
+ const size = parseSizeValue(value);
114
+ if (size) el.style.maxHeight = size;
30
115
  },
31
116
 
32
117
  // Combined size applicator - sets both width and height
33
118
  size: (el, value) => {
34
- if (typeof value === "number") {
35
- el.style.width = `${value}px`;
36
- el.style.height = `${value}px`;
37
- } else if (typeof value === "object" && value !== null) {
119
+ if (typeof value === "object" && value !== null) {
38
120
  const obj = value as Record<string, any>;
39
121
  if (obj.width !== undefined) {
40
- el.style.width = typeof obj.width === "number" ? `${obj.width}px` : String(obj.width);
122
+ const w = parseSizeValue(obj.width);
123
+ if (w) el.style.width = w;
41
124
  }
42
125
  if (obj.height !== undefined) {
43
- el.style.height = typeof obj.height === "number" ? `${obj.height}px` : String(obj.height);
126
+ const h = parseSizeValue(obj.height);
127
+ if (h) el.style.height = h;
44
128
  }
45
129
  } else {
46
- // Single value for both
47
- const size = String(value);
48
- el.style.width = size;
49
- el.style.height = size;
130
+ const size = parseSizeValue(value);
131
+ if (size) {
132
+ el.style.width = size;
133
+ el.style.height = size;
134
+ }
50
135
  }
51
136
  },
52
137