@furystack/shades 11.0.35 → 12.0.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 (215) hide show
  1. package/CHANGELOG.md +337 -0
  2. package/README.md +99 -13
  3. package/esm/compile-route.spec.d.ts +2 -0
  4. package/esm/compile-route.spec.d.ts.map +1 -0
  5. package/esm/compile-route.spec.js +34 -0
  6. package/esm/compile-route.spec.js.map +1 -0
  7. package/esm/component-factory.spec.js +13 -5
  8. package/esm/component-factory.spec.js.map +1 -1
  9. package/esm/components/index.d.ts +4 -1
  10. package/esm/components/index.d.ts.map +1 -1
  11. package/esm/components/index.js +4 -1
  12. package/esm/components/index.js.map +1 -1
  13. package/esm/components/lazy-load.d.ts +2 -4
  14. package/esm/components/lazy-load.d.ts.map +1 -1
  15. package/esm/components/lazy-load.js +40 -24
  16. package/esm/components/lazy-load.js.map +1 -1
  17. package/esm/components/lazy-load.spec.js +57 -50
  18. package/esm/components/lazy-load.spec.js.map +1 -1
  19. package/esm/components/link-to-route.d.ts +2 -0
  20. package/esm/components/link-to-route.d.ts.map +1 -1
  21. package/esm/components/link-to-route.js +3 -2
  22. package/esm/components/link-to-route.js.map +1 -1
  23. package/esm/components/link-to-route.spec.js +13 -9
  24. package/esm/components/link-to-route.spec.js.map +1 -1
  25. package/esm/components/nested-route-link.d.ts +62 -0
  26. package/esm/components/nested-route-link.d.ts.map +1 -0
  27. package/esm/components/nested-route-link.js +66 -0
  28. package/esm/components/nested-route-link.js.map +1 -0
  29. package/esm/components/nested-route-link.spec.d.ts +2 -0
  30. package/esm/components/nested-route-link.spec.d.ts.map +1 -0
  31. package/esm/components/nested-route-link.spec.js +179 -0
  32. package/esm/components/nested-route-link.spec.js.map +1 -0
  33. package/esm/components/nested-route-types.d.ts +37 -0
  34. package/esm/components/nested-route-types.d.ts.map +1 -0
  35. package/esm/components/nested-route-types.js +2 -0
  36. package/esm/components/nested-route-types.js.map +1 -0
  37. package/esm/components/nested-router.d.ts +103 -0
  38. package/esm/components/nested-router.d.ts.map +1 -0
  39. package/esm/components/nested-router.js +178 -0
  40. package/esm/components/nested-router.js.map +1 -0
  41. package/esm/components/nested-router.spec.d.ts +2 -0
  42. package/esm/components/nested-router.spec.d.ts.map +1 -0
  43. package/esm/components/nested-router.spec.js +659 -0
  44. package/esm/components/nested-router.spec.js.map +1 -0
  45. package/esm/components/route-link.d.ts +4 -0
  46. package/esm/components/route-link.d.ts.map +1 -1
  47. package/esm/components/route-link.js +9 -10
  48. package/esm/components/route-link.js.map +1 -1
  49. package/esm/components/route-link.spec.js +16 -12
  50. package/esm/components/route-link.spec.js.map +1 -1
  51. package/esm/components/router.d.ts +20 -2
  52. package/esm/components/router.d.ts.map +1 -1
  53. package/esm/components/router.js +3 -0
  54. package/esm/components/router.js.map +1 -1
  55. package/esm/components/router.spec.js +75 -74
  56. package/esm/components/router.spec.js.map +1 -1
  57. package/esm/css-generator.d.ts +50 -0
  58. package/esm/css-generator.d.ts.map +1 -0
  59. package/esm/css-generator.js +107 -0
  60. package/esm/css-generator.js.map +1 -0
  61. package/esm/css-generator.spec.d.ts +2 -0
  62. package/esm/css-generator.spec.d.ts.map +1 -0
  63. package/esm/css-generator.spec.js +162 -0
  64. package/esm/css-generator.spec.js.map +1 -0
  65. package/esm/index.d.ts +2 -0
  66. package/esm/index.d.ts.map +1 -1
  67. package/esm/index.js +2 -0
  68. package/esm/index.js.map +1 -1
  69. package/esm/initialize.d.ts +11 -0
  70. package/esm/initialize.d.ts.map +1 -1
  71. package/esm/initialize.js +5 -0
  72. package/esm/initialize.js.map +1 -1
  73. package/esm/jsx.d.ts +83 -2
  74. package/esm/jsx.d.ts.map +1 -1
  75. package/esm/models/children-list.d.ts +5 -1
  76. package/esm/models/children-list.d.ts.map +1 -1
  77. package/esm/models/css-object.d.ts +33 -0
  78. package/esm/models/css-object.d.ts.map +1 -0
  79. package/esm/models/css-object.js +2 -0
  80. package/esm/models/css-object.js.map +1 -0
  81. package/esm/models/index.d.ts +1 -0
  82. package/esm/models/index.d.ts.map +1 -1
  83. package/esm/models/index.js +1 -0
  84. package/esm/models/index.js.map +1 -1
  85. package/esm/models/partial-element.d.ts +12 -2
  86. package/esm/models/partial-element.d.ts.map +1 -1
  87. package/esm/models/render-options.d.ts +89 -3
  88. package/esm/models/render-options.d.ts.map +1 -1
  89. package/esm/models/selection-state.d.ts +4 -0
  90. package/esm/models/selection-state.d.ts.map +1 -1
  91. package/esm/services/location-service.d.ts +11 -0
  92. package/esm/services/location-service.d.ts.map +1 -1
  93. package/esm/services/location-service.js +11 -0
  94. package/esm/services/location-service.js.map +1 -1
  95. package/esm/services/resource-manager.d.ts +24 -0
  96. package/esm/services/resource-manager.d.ts.map +1 -1
  97. package/esm/services/resource-manager.js +30 -0
  98. package/esm/services/resource-manager.js.map +1 -1
  99. package/esm/services/resource-manager.spec.js +93 -0
  100. package/esm/services/resource-manager.spec.js.map +1 -1
  101. package/esm/services/screen-service.d.ts +81 -4
  102. package/esm/services/screen-service.d.ts.map +1 -1
  103. package/esm/services/screen-service.js +75 -4
  104. package/esm/services/screen-service.js.map +1 -1
  105. package/esm/services/screen-service.spec.js +91 -7
  106. package/esm/services/screen-service.spec.js.map +1 -1
  107. package/esm/shade-component.d.ts +17 -4
  108. package/esm/shade-component.d.ts.map +1 -1
  109. package/esm/shade-component.js +67 -5
  110. package/esm/shade-component.js.map +1 -1
  111. package/esm/shade-host-props-ref.integration.spec.d.ts +2 -0
  112. package/esm/shade-host-props-ref.integration.spec.d.ts.map +1 -0
  113. package/esm/shade-host-props-ref.integration.spec.js +381 -0
  114. package/esm/shade-host-props-ref.integration.spec.js.map +1 -0
  115. package/esm/shade-resources.integration.spec.js +208 -39
  116. package/esm/shade-resources.integration.spec.js.map +1 -1
  117. package/esm/shade.d.ts +34 -15
  118. package/esm/shade.d.ts.map +1 -1
  119. package/esm/shade.js +180 -33
  120. package/esm/shade.js.map +1 -1
  121. package/esm/shade.spec.d.ts +2 -0
  122. package/esm/shade.spec.d.ts.map +1 -0
  123. package/esm/shade.spec.js +198 -0
  124. package/esm/shade.spec.js.map +1 -0
  125. package/esm/shades.integration.spec.js +135 -72
  126. package/esm/shades.integration.spec.js.map +1 -1
  127. package/esm/style-manager.d.ts +65 -0
  128. package/esm/style-manager.d.ts.map +1 -0
  129. package/esm/style-manager.js +95 -0
  130. package/esm/style-manager.js.map +1 -0
  131. package/esm/style-manager.spec.d.ts +2 -0
  132. package/esm/style-manager.spec.d.ts.map +1 -0
  133. package/esm/style-manager.spec.js +179 -0
  134. package/esm/style-manager.spec.js.map +1 -0
  135. package/esm/styled-element.spec.d.ts +2 -0
  136. package/esm/styled-element.spec.d.ts.map +1 -0
  137. package/esm/styled-element.spec.js +86 -0
  138. package/esm/styled-element.spec.js.map +1 -0
  139. package/esm/styled-shade.spec.d.ts +2 -0
  140. package/esm/styled-shade.spec.d.ts.map +1 -0
  141. package/esm/styled-shade.spec.js +66 -0
  142. package/esm/styled-shade.spec.js.map +1 -0
  143. package/esm/svg-types.d.ts +389 -0
  144. package/esm/svg-types.d.ts.map +1 -0
  145. package/esm/svg-types.js +9 -0
  146. package/esm/svg-types.js.map +1 -0
  147. package/esm/svg.d.ts +15 -0
  148. package/esm/svg.d.ts.map +1 -0
  149. package/esm/svg.js +76 -0
  150. package/esm/svg.js.map +1 -0
  151. package/esm/svg.spec.d.ts +2 -0
  152. package/esm/svg.spec.d.ts.map +1 -0
  153. package/esm/svg.spec.js +80 -0
  154. package/esm/svg.spec.js.map +1 -0
  155. package/esm/vnode.d.ts +103 -0
  156. package/esm/vnode.d.ts.map +1 -0
  157. package/esm/vnode.integration.spec.d.ts +2 -0
  158. package/esm/vnode.integration.spec.d.ts.map +1 -0
  159. package/esm/vnode.integration.spec.js +494 -0
  160. package/esm/vnode.integration.spec.js.map +1 -0
  161. package/esm/vnode.js +453 -0
  162. package/esm/vnode.js.map +1 -0
  163. package/esm/vnode.spec.d.ts +2 -0
  164. package/esm/vnode.spec.d.ts.map +1 -0
  165. package/esm/vnode.spec.js +473 -0
  166. package/esm/vnode.spec.js.map +1 -0
  167. package/package.json +3 -3
  168. package/src/compile-route.spec.ts +39 -0
  169. package/src/component-factory.spec.tsx +18 -5
  170. package/src/components/index.ts +4 -1
  171. package/src/components/lazy-load.spec.tsx +82 -75
  172. package/src/components/lazy-load.tsx +49 -27
  173. package/src/components/link-to-route.spec.tsx +25 -21
  174. package/src/components/link-to-route.tsx +4 -2
  175. package/src/components/nested-route-link.spec.tsx +303 -0
  176. package/src/components/nested-route-link.tsx +100 -0
  177. package/src/components/nested-route-types.ts +42 -0
  178. package/src/components/nested-router.spec.tsx +817 -0
  179. package/src/components/nested-router.tsx +256 -0
  180. package/src/components/route-link.spec.tsx +22 -18
  181. package/src/components/route-link.tsx +10 -10
  182. package/src/components/router.spec.tsx +109 -108
  183. package/src/components/router.tsx +15 -2
  184. package/src/css-generator.spec.ts +183 -0
  185. package/src/css-generator.ts +117 -0
  186. package/src/index.ts +2 -0
  187. package/src/initialize.ts +12 -0
  188. package/src/jsx.ts +129 -2
  189. package/src/models/children-list.ts +7 -1
  190. package/src/models/css-object.ts +34 -0
  191. package/src/models/index.ts +1 -0
  192. package/src/models/partial-element.ts +13 -2
  193. package/src/models/render-options.ts +90 -3
  194. package/src/models/selection-state.ts +4 -0
  195. package/src/services/location-service.tsx +11 -0
  196. package/src/services/resource-manager.spec.ts +116 -0
  197. package/src/services/resource-manager.ts +30 -0
  198. package/src/services/screen-service.spec.ts +109 -7
  199. package/src/services/screen-service.ts +81 -4
  200. package/src/shade-component.ts +72 -6
  201. package/src/shade-host-props-ref.integration.spec.tsx +460 -0
  202. package/src/shade-resources.integration.spec.tsx +276 -52
  203. package/src/shade.spec.tsx +239 -0
  204. package/src/shade.ts +211 -56
  205. package/src/shades.integration.spec.tsx +154 -80
  206. package/src/style-manager.spec.ts +229 -0
  207. package/src/style-manager.ts +104 -0
  208. package/src/styled-element.spec.tsx +117 -0
  209. package/src/styled-shade.spec.ts +86 -0
  210. package/src/svg-types.ts +437 -0
  211. package/src/svg.spec.ts +89 -0
  212. package/src/svg.ts +78 -0
  213. package/src/vnode.integration.spec.tsx +657 -0
  214. package/src/vnode.spec.ts +579 -0
  215. package/src/vnode.ts +508 -0
@@ -0,0 +1,183 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { camelToKebab, generateCSS, generateCSSRule, isSelectorKey, propertiesToCSSString } from './css-generator.js'
3
+ import type { CSSProperties } from './models/css-object.js'
4
+
5
+ describe('css-generator', () => {
6
+ describe('camelToKebab', () => {
7
+ it('should convert camelCase to kebab-case', () => {
8
+ expect(camelToKebab('backgroundColor')).toBe('background-color')
9
+ expect(camelToKebab('fontSize')).toBe('font-size')
10
+ expect(camelToKebab('borderTopLeftRadius')).toBe('border-top-left-radius')
11
+ })
12
+
13
+ it('should handle single word properties', () => {
14
+ expect(camelToKebab('color')).toBe('color')
15
+ expect(camelToKebab('margin')).toBe('margin')
16
+ })
17
+
18
+ it('should handle empty string', () => {
19
+ expect(camelToKebab('')).toBe('')
20
+ })
21
+ })
22
+
23
+ describe('isSelectorKey', () => {
24
+ it('should return true for selector keys starting with &', () => {
25
+ expect(isSelectorKey('&:hover')).toBe(true)
26
+ expect(isSelectorKey('&:active')).toBe(true)
27
+ expect(isSelectorKey('& .className')).toBe(true)
28
+ expect(isSelectorKey('& > div')).toBe(true)
29
+ })
30
+
31
+ it('should return false for regular CSS property keys', () => {
32
+ expect(isSelectorKey('color')).toBe(false)
33
+ expect(isSelectorKey('backgroundColor')).toBe(false)
34
+ expect(isSelectorKey('fontSize')).toBe(false)
35
+ })
36
+ })
37
+
38
+ describe('propertiesToCSSString', () => {
39
+ it('should convert CSS properties object to CSS string', () => {
40
+ const result = propertiesToCSSString({
41
+ color: 'red',
42
+ backgroundColor: 'blue',
43
+ })
44
+ expect(result).toBe('color: red; background-color: blue')
45
+ })
46
+
47
+ it('should skip undefined and null values', () => {
48
+ const result = propertiesToCSSString({
49
+ color: 'red',
50
+ backgroundColor: undefined,
51
+ })
52
+ expect(result).toBe('color: red')
53
+ })
54
+
55
+ it('should skip empty string values', () => {
56
+ const result = propertiesToCSSString({
57
+ color: 'red',
58
+ backgroundColor: '',
59
+ })
60
+ expect(result).toBe('color: red')
61
+ })
62
+
63
+ it('should return empty string for empty object', () => {
64
+ const result = propertiesToCSSString({})
65
+ expect(result).toBe('')
66
+ })
67
+
68
+ it('should ignore selector keys', () => {
69
+ // Type assertion needed to test mixed object with selectors
70
+ const mixedObject = {
71
+ color: 'red',
72
+ '&:hover': { color: 'blue' },
73
+ }
74
+ const result = propertiesToCSSString(mixedObject as unknown as CSSProperties)
75
+ expect(result).toBe('color: red')
76
+ })
77
+
78
+ it('should filter out non-string values', () => {
79
+ // Type assertion to test edge case with non-string values
80
+ const mixedObject = {
81
+ color: 'red',
82
+ opacity: 0.5, // number - should be filtered
83
+ display: 'flex',
84
+ hidden: true, // boolean - should be filtered
85
+ }
86
+ const result = propertiesToCSSString(mixedObject as unknown as CSSProperties)
87
+ expect(result).toBe('color: red; display: flex')
88
+ })
89
+ })
90
+
91
+ describe('generateCSSRule', () => {
92
+ it('should generate a complete CSS rule', () => {
93
+ const result = generateCSSRule('my-component', {
94
+ color: 'red',
95
+ padding: '10px',
96
+ })
97
+ expect(result).toBe('my-component { color: red; padding: 10px; }')
98
+ })
99
+
100
+ it('should return empty string for empty properties', () => {
101
+ const result = generateCSSRule('my-component', {})
102
+ expect(result).toBe('')
103
+ })
104
+ })
105
+
106
+ describe('generateCSS', () => {
107
+ it('should generate CSS for base properties only', () => {
108
+ const result = generateCSS('my-component', {
109
+ color: 'red',
110
+ padding: '10px',
111
+ })
112
+ expect(result).toBe('my-component { color: red; padding: 10px; }')
113
+ })
114
+
115
+ it('should generate CSS with pseudo-selectors', () => {
116
+ const result = generateCSS('my-component', {
117
+ color: 'red',
118
+ '&:hover': { color: 'blue' },
119
+ })
120
+ expect(result).toContain('my-component { color: red; }')
121
+ expect(result).toContain('my-component:hover { color: blue; }')
122
+ })
123
+
124
+ it('should generate CSS with nested class selectors', () => {
125
+ const result = generateCSS('my-component', {
126
+ padding: '10px',
127
+ '& .inner': { fontWeight: 'bold' },
128
+ })
129
+ expect(result).toContain('my-component { padding: 10px; }')
130
+ expect(result).toContain('my-component .inner { font-weight: bold; }')
131
+ })
132
+
133
+ it('should generate CSS with child selectors', () => {
134
+ const result = generateCSS('my-component', {
135
+ display: 'flex',
136
+ '& > div': { margin: '5px' },
137
+ })
138
+ expect(result).toContain('my-component { display: flex; }')
139
+ expect(result).toContain('my-component > div { margin: 5px; }')
140
+ })
141
+
142
+ it('should handle multiple pseudo-selectors', () => {
143
+ const result = generateCSS('my-button', {
144
+ backgroundColor: 'blue',
145
+ '&:hover': { backgroundColor: 'darkblue' },
146
+ '&:active': { backgroundColor: 'navy' },
147
+ '&:disabled': { opacity: '0.5' },
148
+ })
149
+ expect(result).toContain('my-button { background-color: blue; }')
150
+ expect(result).toContain('my-button:hover { background-color: darkblue; }')
151
+ expect(result).toContain('my-button:active { background-color: navy; }')
152
+ expect(result).toContain('my-button:disabled { opacity: 0.5; }')
153
+ })
154
+
155
+ it('should handle empty css object', () => {
156
+ const result = generateCSS('my-component', {})
157
+ expect(result).toBe('')
158
+ })
159
+
160
+ it('should handle css object with only selectors', () => {
161
+ const result = generateCSS('my-component', {
162
+ '&:hover': { color: 'blue' },
163
+ })
164
+ expect(result).toBe('my-component:hover { color: blue; }')
165
+ })
166
+
167
+ it('should skip selector keys with non-object values', () => {
168
+ // Type assertion to test edge case with invalid selector values
169
+ const cssObject = {
170
+ color: 'red',
171
+ '&:hover': 'invalid', // string instead of object - should be skipped
172
+ '&:active': null, // null - should be skipped
173
+ '&:focus': { backgroundColor: 'blue' }, // valid - should be included
174
+ }
175
+ const result = generateCSS('my-component', cssObject as unknown as Parameters<typeof generateCSS>[1])
176
+ expect(result).toContain('my-component { color: red; }')
177
+ expect(result).toContain('my-component:focus { background-color: blue; }')
178
+ expect(result).not.toContain('invalid')
179
+ expect(result).not.toContain(':hover')
180
+ expect(result).not.toContain(':active')
181
+ })
182
+ })
183
+ })
@@ -0,0 +1,117 @@
1
+ import type { CSSObject, CSSProperties } from './models/css-object.js'
2
+
3
+ /**
4
+ * Converts a camelCase string to kebab-case
5
+ * @param str - The camelCase string to convert
6
+ * @returns The kebab-case string
7
+ * @example
8
+ * camelToKebab('backgroundColor') // 'background-color'
9
+ * camelToKebab('fontSize') // 'font-size'
10
+ */
11
+ export const camelToKebab = (str: string): string => {
12
+ return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
13
+ }
14
+
15
+ /**
16
+ * Checks if a key is a selector key (starts with '&')
17
+ * @param key - The key to check
18
+ * @returns True if the key is a selector key
19
+ */
20
+ export const isSelectorKey = (key: string): boolean => {
21
+ return key.startsWith('&')
22
+ }
23
+
24
+ /**
25
+ * Converts CSS properties to a CSS declaration string
26
+ * @param properties - The CSS properties object
27
+ * @returns A CSS declaration string (e.g., "color: red; padding: 10px;")
28
+ */
29
+ export const propertiesToCSSString = (properties: CSSProperties): string => {
30
+ const declarations: string[] = []
31
+
32
+ for (const key in properties) {
33
+ if (Object.prototype.hasOwnProperty.call(properties, key) && !isSelectorKey(key)) {
34
+ const value = properties[key as keyof CSSProperties]
35
+ if (value !== undefined && value !== null && value !== '' && typeof value === 'string') {
36
+ declarations.push(`${camelToKebab(key)}: ${value}`)
37
+ }
38
+ }
39
+ }
40
+
41
+ return declarations.join('; ')
42
+ }
43
+
44
+ /**
45
+ * Generates a CSS rule string from a selector and properties
46
+ * @param selector - The CSS selector
47
+ * @param properties - The CSS properties object
48
+ * @returns A complete CSS rule string (e.g., "selector { color: red; }")
49
+ */
50
+ export const generateCSSRule = (selector: string, properties: CSSProperties): string => {
51
+ const cssString = propertiesToCSSString(properties)
52
+ if (!cssString) {
53
+ return ''
54
+ }
55
+ return `${selector} { ${cssString}; }`
56
+ }
57
+
58
+ /**
59
+ * Generates complete CSS from a CSSObject for a given component selector
60
+ * @param selector - The base selector (typically the shadowDomName)
61
+ * @param cssObject - The CSSObject containing styles and nested selectors
62
+ * @returns A complete CSS string with all rules
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * generateCSS('my-component', {
67
+ * color: 'red',
68
+ * '&:hover': { color: 'blue' },
69
+ * '& .inner': { fontWeight: 'bold' }
70
+ * })
71
+ * // Returns:
72
+ * // "my-component { color: red; }
73
+ * // my-component:hover { color: blue; }
74
+ * // my-component .inner { font-weight: bold; }"
75
+ * ```
76
+ */
77
+ export const generateCSS = (selector: string, cssObject: CSSObject): string => {
78
+ const rules: string[] = []
79
+
80
+ // Extract base properties (non-selector keys)
81
+ const baseProperties: CSSProperties = {}
82
+ const selectorRules: Array<{ selectorKey: string; properties: CSSProperties }> = []
83
+
84
+ for (const key in cssObject) {
85
+ if (Object.prototype.hasOwnProperty.call(cssObject, key)) {
86
+ if (isSelectorKey(key)) {
87
+ const properties = cssObject[key as keyof CSSObject]
88
+ if (properties && typeof properties === 'object') {
89
+ selectorRules.push({ selectorKey: key, properties: properties as CSSProperties })
90
+ }
91
+ } else {
92
+ const value = cssObject[key as keyof CSSObject]
93
+ if (typeof value !== 'object') {
94
+ ;(baseProperties as Record<string, unknown>)[key] = value
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ // Generate base rule
101
+ const baseRule = generateCSSRule(selector, baseProperties)
102
+ if (baseRule) {
103
+ rules.push(baseRule)
104
+ }
105
+
106
+ // Generate selector rules
107
+ for (const { selectorKey, properties } of selectorRules) {
108
+ // Replace '&' with the base selector
109
+ const fullSelector = selectorKey.replace(/&/g, selector)
110
+ const rule = generateCSSRule(fullSelector, properties)
111
+ if (rule) {
112
+ rules.push(rule)
113
+ }
114
+ }
115
+
116
+ return rules.join('\n')
117
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  export * from './compile-route.js'
2
2
  export * from './components/index.js'
3
+ export * from './css-generator.js'
3
4
  export * from './initialize.js'
4
5
  export * from './models/index.js'
5
6
  export * from './services/index.js'
6
7
  export * from './shade-component.js'
7
8
  export * from './shade.js'
9
+ export * from './style-manager.js'
8
10
  export * from './styled-element.js'
9
11
  export * from './styled-shade.js'
10
12
  import './jsx.js'
package/src/initialize.ts CHANGED
@@ -1,10 +1,22 @@
1
1
  import type { Injector } from '@furystack/inject'
2
2
 
3
+ /**
4
+ * Options for bootstrapping a Shades application.
5
+ */
3
6
  export interface InitializeOptions {
7
+ /** The DOM element that will host the application */
4
8
  rootElement: HTMLElement
9
+ /** The root JSX element to render */
5
10
  jsxElement: JSX.Element
11
+ /** The root injector instance for dependency injection */
6
12
  injector: Injector
7
13
  }
14
+
15
+ /**
16
+ * Bootstraps a Shades application by attaching the root JSX element to a DOM node
17
+ * and wiring up the dependency injection context.
18
+ * @param options The initialization options
19
+ */
8
20
  export const initializeShadeRoot = (options: InitializeOptions) => {
9
21
  options.jsxElement.injector = options.injector
10
22
  options.rootElement.appendChild(options.jsxElement)
package/src/jsx.ts CHANGED
@@ -1,6 +1,48 @@
1
1
  import type { Injector } from '@furystack/inject'
2
2
  import type { ChildrenList, PartialElement } from './models/index.js'
3
3
  import type { ResourceManager } from './services/resource-manager.js'
4
+ import type {
5
+ SvgAnimateAttributes,
6
+ SvgAnimateMotionAttributes,
7
+ SvgAnimateTransformAttributes,
8
+ SvgCircleAttributes,
9
+ SvgClipPathAttributes,
10
+ SvgCoreAttributes,
11
+ SvgDefsAttributes,
12
+ SvgDescAttributes,
13
+ SvgEllipseAttributes,
14
+ SvgFeBlendAttributes,
15
+ SvgFeColorMatrixAttributes,
16
+ SvgFeCompositeAttributes,
17
+ SvgFeFloodAttributes,
18
+ SvgFeGaussianBlurAttributes,
19
+ SvgFeMergeAttributes,
20
+ SvgFeMergeNodeAttributes,
21
+ SvgFeOffsetAttributes,
22
+ SvgFilterAttributes,
23
+ SvgForeignObjectAttributes,
24
+ SvgGAttributes,
25
+ SvgImageAttributes,
26
+ SvgLinearGradientAttributes,
27
+ SvgLineAttributes,
28
+ SvgMarkerAttributes,
29
+ SvgMaskAttributes,
30
+ SvgPathAttributes,
31
+ SvgPatternAttributes,
32
+ SvgPolygonAttributes,
33
+ SvgPolylineAttributes,
34
+ SvgRadialGradientAttributes,
35
+ SvgRectAttributes,
36
+ SvgSetAttributes,
37
+ SvgStopAttributes,
38
+ SvgSvgAttributes,
39
+ SvgSymbolAttributes,
40
+ SvgTextAttributes,
41
+ SvgTextPathAttributes,
42
+ SvgTitleAttributes,
43
+ SvgTspanAttributes,
44
+ SvgUseAttributes,
45
+ } from './svg-types.js'
4
46
 
5
47
  declare global {
6
48
  // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -10,7 +52,6 @@ declare global {
10
52
  props: TProps
11
53
  updateComponent: () => void
12
54
  shadeChildren?: ChildrenList
13
- callConstructed: () => void
14
55
  resourceManager: ResourceManager
15
56
  getRenderCount(): number
16
57
  }
@@ -423,7 +464,7 @@ declare global {
423
464
  * The <svg> tag defines a container for SVG graphics.
424
465
  * SVG has several methods for drawing paths, boxes, circles, text, and graphic images.
425
466
  */
426
- svg: PartialElement<SVGElement>
467
+ svg: SvgSvgAttributes
427
468
  /**
428
469
  * The <table> tag defines an HTML table.
429
470
  * An HTML table consists of the <table> element and one or more <tr>, <th>, and <td> elements.
@@ -519,6 +560,92 @@ declare global {
519
560
  * Tip: When a word is too long, or you are afraid that the browser will break your lines at the wrong place, you can use the <wbr> element to add word break opportunities.
520
561
  */
521
562
  wbr: PartialElement<HTMLElement>
563
+
564
+ // -------------------------------------------------------------------
565
+ // SVG Elements
566
+ // -------------------------------------------------------------------
567
+
568
+ /** The `<g>` element groups SVG elements together. */
569
+ g: SvgGAttributes
570
+ /** The `<defs>` element stores graphical objects for later reuse. */
571
+ defs: SvgDefsAttributes
572
+ /** The `<symbol>` element defines a reusable graphical template. */
573
+ symbol: SvgSymbolAttributes
574
+ /** The `<use>` element references another element for rendering. */
575
+ use: SvgUseAttributes
576
+ /** The `<path>` element defines a shape via SVG path commands. */
577
+ path: SvgPathAttributes
578
+ /** The `<rect>` element draws a rectangle. */
579
+ rect: SvgRectAttributes
580
+ /** The `<circle>` element draws a circle. */
581
+ circle: SvgCircleAttributes
582
+ /** The `<ellipse>` element draws an ellipse. */
583
+ ellipse: SvgEllipseAttributes
584
+ /** The `<line>` element draws a straight line between two points. */
585
+ line: SvgLineAttributes
586
+ /** The `<polyline>` element draws connected straight line segments. */
587
+ polyline: SvgPolylineAttributes
588
+ /** The `<polygon>` element draws a closed shape of connected line segments. */
589
+ polygon: SvgPolygonAttributes
590
+ /** The `<text>` element renders text in SVG. */
591
+ text: SvgTextAttributes
592
+ /** The `<tspan>` element defines a subtext within a `<text>` element. */
593
+ tspan: SvgTspanAttributes
594
+ /** The `<textPath>` element renders text along a path. */
595
+ textPath: SvgTextPathAttributes
596
+ /** The `<clipPath>` element defines a clipping region. */
597
+ clipPath: SvgClipPathAttributes
598
+ /** The `<mask>` element defines an alpha mask for compositing. */
599
+ mask: SvgMaskAttributes
600
+ /** The `<linearGradient>` element defines a linear color gradient. */
601
+ linearGradient: SvgLinearGradientAttributes
602
+ /** The `<radialGradient>` element defines a radial color gradient. */
603
+ radialGradient: SvgRadialGradientAttributes
604
+ /** The `<stop>` element defines a color stop in a gradient. */
605
+ stop: SvgStopAttributes
606
+ /** The `<pattern>` element defines a repeating graphic pattern. */
607
+ pattern: SvgPatternAttributes
608
+ /** The `<marker>` element defines a graphic for drawing on edges of shapes. */
609
+ marker: SvgMarkerAttributes
610
+ /** The `<filter>` element defines a set of filter operations. */
611
+ filter: SvgFilterAttributes
612
+ /** The `<feGaussianBlur>` filter primitive blurs the input image. */
613
+ feGaussianBlur: SvgFeGaussianBlurAttributes
614
+ /** The `<feBlend>` filter primitive composites two inputs. */
615
+ feBlend: SvgFeBlendAttributes
616
+ /** The `<feColorMatrix>` filter primitive applies a matrix color transform. */
617
+ feColorMatrix: SvgFeColorMatrixAttributes
618
+ /** The `<feOffset>` filter primitive offsets the input image. */
619
+ feOffset: SvgFeOffsetAttributes
620
+ /** The `<feFlood>` filter primitive fills with a solid color. */
621
+ feFlood: SvgFeFloodAttributes
622
+ /** The `<feMerge>` filter primitive composites multiple inputs. */
623
+ feMerge: SvgFeMergeAttributes
624
+ /** The `<feMergeNode>` element defines an input for `<feMerge>`. */
625
+ feMergeNode: SvgFeMergeNodeAttributes
626
+ /** The `<feComposite>` filter primitive combines images using Porter-Duff operations. */
627
+ feComposite: SvgFeCompositeAttributes
628
+ /** The `<image>` element (SVG) embeds a raster image. */
629
+ image: SvgImageAttributes
630
+ /** The `<foreignObject>` element embeds external XML (e.g. HTML) in SVG. */
631
+ foreignObject: SvgForeignObjectAttributes
632
+ /** The `<animate>` element animates an attribute over time. */
633
+ animate: SvgAnimateAttributes
634
+ /** The `<animateMotion>` element animates an element along a path. */
635
+ animateMotion: SvgAnimateMotionAttributes
636
+ /** The `<animateTransform>` element animates a transformation attribute. */
637
+ animateTransform: SvgAnimateTransformAttributes
638
+ /** The `<set>` element sets an attribute to a value for a duration. */
639
+ set: SvgSetAttributes
640
+ /** The `<title>` element (SVG) provides an accessible title. */
641
+ title: SvgTitleAttributes
642
+ /** The `<desc>` element provides an accessible description. */
643
+ desc: SvgDescAttributes
644
+ /**
645
+ * Catch-all for SVG filter primitives and other SVG elements
646
+ * not explicitly listed above.
647
+ */
648
+ [key: `fe${string}`]: SvgCoreAttributes
522
649
  }
523
650
  }
524
651
  }
@@ -1 +1,7 @@
1
- export type ChildrenList = Array<string | HTMLElement | JSX.Element | string[] | HTMLElement[] | JSX.Element[]>
1
+ /**
2
+ * The type for children passed to Shade components and intrinsic JSX elements.
3
+ * Supports strings, HTML/SVG elements, JSX elements, and nested arrays of these.
4
+ */
5
+ export type ChildrenList = Array<
6
+ string | HTMLElement | SVGElement | JSX.Element | string[] | HTMLElement[] | SVGElement[] | JSX.Element[]
7
+ >
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Base CSS properties - subset of CSSStyleDeclaration
3
+ */
4
+ export type CSSProperties = Partial<CSSStyleDeclaration>
5
+
6
+ /**
7
+ * Selector key pattern for pseudo-classes and nested selectors
8
+ * Examples: '&:hover', '&:active', '& .className', '& > div'
9
+ */
10
+ export type SelectorKey = `&${string}`
11
+
12
+ /**
13
+ * CSS object supporting nested selectors for component-level styling.
14
+ *
15
+ * Use this type for the `css` property in Shade components to define
16
+ * styles that are injected as a stylesheet during component registration.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const styles: CSSObject = {
21
+ * padding: '16px',
22
+ * backgroundColor: 'white',
23
+ * '&:hover': {
24
+ * backgroundColor: '#f0f0f0'
25
+ * },
26
+ * '& .title': {
27
+ * fontWeight: 'bold'
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export type CSSObject = CSSProperties & {
33
+ [K in SelectorKey]?: CSSProperties
34
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './children-list.js'
2
+ export * from './css-object.js'
2
3
  export * from './partial-element.js'
3
4
  export * from './render-options.js'
4
5
  export * from './selection-state.js'
@@ -1,5 +1,16 @@
1
- export type PartialElement<T> = T extends { style?: CSSStyleDeclaration }
1
+ import type { RefObject } from './render-options.js'
2
+
3
+ /**
4
+ * Makes all properties of an HTML element type optional, with `style` narrowed
5
+ * to `Partial<CSSStyleDeclaration>` and a `ref` prop for capturing DOM references.
6
+ * Used as the props type for intrinsic JSX elements and component host element overrides.
7
+ * @typeParam T - The base HTML element type
8
+ */
9
+ export type PartialElement<T> = (T extends { style?: CSSStyleDeclaration }
2
10
  ? Omit<Partial<T>, 'style'> & {
3
11
  style?: Partial<CSSStyleDeclaration>
4
12
  }
5
- : Partial<T>
13
+ : Partial<T>) & {
14
+ /** Ref object to capture a reference to the underlying DOM element. */
15
+ ref?: RefObject<Element>
16
+ }
@@ -3,12 +3,81 @@ import type { ObservableValue, ValueObserverOptions } from '@furystack/utils'
3
3
  import type { ChildrenList } from './children-list.js'
4
4
  import type { PartialElement } from './partial-element.js'
5
5
 
6
+ /**
7
+ * A reference object returned by `useRef`.
8
+ * `current` is set to the DOM element when it is mounted, and `null` when unmounted.
9
+ * The `readonly` modifier ensures covariance so that `RefObject<HTMLInputElement>`
10
+ * is assignable to `RefObject<Element>`.
11
+ */
12
+ export type RefObject<T extends Element = HTMLElement> = {
13
+ readonly current: T | null
14
+ }
15
+
16
+ /**
17
+ * Options provided to a Shade component's `render` function.
18
+ * Contains the current props, injector, children, and hooks for managing state, side effects, and host element attributes.
19
+ * @typeParam TProps - The component's props type
20
+ * @typeParam TElementBase - The base HTML element type (defaults to HTMLElement)
21
+ */
6
22
  export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement> = {
7
23
  readonly props: TProps & PartialElement<TElementBase>
8
24
  renderCount: number
9
25
  injector: Injector
10
26
  children?: ChildrenList
11
- element: JSX.Element<TProps>
27
+ /**
28
+ * Declaratively sets attributes and styles on the host custom element.
29
+ * Can be called multiple times per render; each call merges into the previous values.
30
+ *
31
+ * CSS custom properties (e.g. `--my-color`) are applied via `setProperty`.
32
+ * The `style` property accepts both standard camelCase properties and CSS custom properties.
33
+ *
34
+ * **Best practice:** Use `useHostProps` for data attributes, ARIA attributes, CSS variables,
35
+ * and event handlers on the host element instead of imperative DOM manipulation.
36
+ *
37
+ * @param hostProps An object of attribute key-value pairs, optionally including a `style` record
38
+ *
39
+ * **Note:** Object and function values are assigned as properties on the host element
40
+ * (not as attributes). This means you can set event handlers (e.g. `onclick`) and
41
+ * even class properties like `injector` via `useHostProps`.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * useHostProps({
46
+ * 'data-variant': props.variant,
47
+ * role: 'progressbar',
48
+ * 'aria-valuenow': String(value),
49
+ * style: {
50
+ * '--btn-color-main': colors.main,
51
+ * display: 'flex',
52
+ * },
53
+ * })
54
+ * ```
55
+ */
56
+ useHostProps: (hostProps: Record<string, unknown> & { style?: Record<string, string> }) => void
57
+
58
+ /**
59
+ * Creates a mutable ref object that can be attached to intrinsic JSX elements via the `ref` prop.
60
+ * The ref's `current` property will be set to the DOM element after mount and `null` on unmount.
61
+ *
62
+ * Refs are cached by key, so calling `useRef` with the same key returns the same object across renders.
63
+ *
64
+ * **Best practice:** Prefer declarative JSX and `useHostProps` when possible.
65
+ * Use refs sparingly for imperative needs like focus management or measuring elements.
66
+ *
67
+ * @param key A unique key for caching the ref object
68
+ * @returns A ref object with a `current` property
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const inputRef = useRef<HTMLInputElement>('input')
73
+ * // In JSX:
74
+ * <input ref={inputRef} />
75
+ * // Later:
76
+ * inputRef.current?.focus()
77
+ * ```
78
+ */
79
+ useRef: <T extends Element = HTMLElement>(key: string) => RefObject<T>
80
+
12
81
  /**
13
82
  * Creates and disposes a resource after the component has been detached from the DOM
14
83
  * @param key The key for caching the disposable resource
@@ -18,11 +87,29 @@ export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement
18
87
  useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T) => T
19
88
 
20
89
  /**
21
- * Creates a state object from an existing observable value
90
+ * Creates a state object from an existing observable value.
91
+ *
92
+ * **Important:** By default, this will trigger a full component re-render when the observable value changes.
93
+ * To prevent re-renders (e.g., for manual DOM updates or animations), provide a custom `onChange` callback.
94
+ *
22
95
  * @param key The key for caching the observable value
23
96
  * @param observable The observable value to observe
24
- * @param options Optional options for the observer including onChange callback
97
+ * @param options Optional options for the observer
98
+ * @param options.onChange Custom callback when value changes. If not provided, the component will re-render on each change.
25
99
  * @returns tuple with the current value and a setter function
100
+ *
101
+ * @example
102
+ * // Default behavior: re-renders component on change
103
+ * const [count] = useObservable('count', countObservable)
104
+ *
105
+ * @example
106
+ * // Custom onChange: no re-render, update host element via useHostProps
107
+ * useHostProps({ 'data-active': count > 0 ? '' : undefined })
108
+ * const [count] = useObservable('count', countObservable, {
109
+ * onChange: () => {
110
+ * // Triggers a re-render so useHostProps above picks up the new value
111
+ * }
112
+ * })
26
113
  */
27
114
  useObservable: <T>(
28
115
  key: string,