@remix-run/ui 0.0.0 → 0.1.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 (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +920 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +32 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +28 -0
  124. package/dist/runtime/navigation.js +107 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1262 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +195 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +46 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1011 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +124 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1218 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +397 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +148 -0
  247. package/src/runtime/reconcile.ts +1757 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +269 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +51 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1347 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/utils.ts +45 -0
  262. package/src/test.ts +2 -0
  263. package/src/theme/contract.ts +183 -0
  264. package/src/theme/glyph-contract.ts +57 -0
  265. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  266. package/src/theme/presets/rmx-01/index.ts +121 -0
  267. package/src/theme/runtime.ts +116 -0
  268. package/src/theme/theme.ts +15 -0
  269. package/src/utils/flash-attribute.ts +11 -0
  270. package/src/utils/scroll-lock.ts +97 -0
  271. package/src/utils/wait-for-css-transition.ts +66 -0
  272. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,82 @@
1
+ import { createMixin, renderMixinElement } from '../runtime/mixins/mixin.ts'
2
+ import type { MixinDescriptor } from '../runtime/mixins/mixin.ts'
3
+ import type { ElementProps } from '../runtime/jsx.ts'
4
+ import { invariant } from '../runtime/invariant.ts'
5
+ import { processStyleClass } from '../style/index.ts'
6
+ import type { CSSProps } from '../style/style.ts'
7
+
8
+ type StyleEntry = { selector: string; css: string }
9
+ type StyleCache = Map<string, StyleEntry>
10
+ type StyleManagerLike = {
11
+ insert(className: string, rule: string): void
12
+ remove(className: string): void
13
+ getGeneration?(): number
14
+ }
15
+
16
+ export type CSSMixinDescriptor = MixinDescriptor<Element, [styles: CSSProps], ElementProps>
17
+
18
+ const clientStyleCache: StyleCache = new Map()
19
+
20
+ /**
21
+ * Applies generated class names for CSS object styles.
22
+ */
23
+ export const css = createMixin<Element, [styles: CSSProps], ElementProps>((handle) => {
24
+ let activeSelector = ''
25
+ let activeGeneration = -1
26
+ let currentStyles: CSSProps = {}
27
+
28
+ handle.addEventListener('remove', () => {
29
+ if (!activeSelector) return
30
+ let runtime = handle.frame.$runtime as {
31
+ styleCache?: StyleCache
32
+ styleManager?: StyleManagerLike
33
+ }
34
+ invariant(runtime, 'css mixin requires frame runtime')
35
+ let styleTarget = resolveStyleTarget(runtime)
36
+ styleTarget.styleManager?.remove(activeSelector)
37
+ activeSelector = ''
38
+ activeGeneration = -1
39
+ })
40
+
41
+ return (styles, props) => {
42
+ currentStyles = styles
43
+ let runtime = handle.frame.$runtime as {
44
+ styleCache?: StyleCache
45
+ styleManager?: StyleManagerLike
46
+ }
47
+ invariant(runtime, 'css mixin requires frame runtime')
48
+ let styleTarget = resolveStyleTarget(runtime)
49
+ let { selector, css: cssText } = processStyleClass(currentStyles, styleTarget.styleCache)
50
+ let styleGeneration = styleTarget.styleManager?.getGeneration?.() ?? 0
51
+
52
+ if (styleTarget.styleManager) {
53
+ if (activeSelector && activeSelector !== selector) {
54
+ styleTarget.styleManager.remove(activeSelector)
55
+ }
56
+ if (selector && (activeSelector !== selector || activeGeneration !== styleGeneration)) {
57
+ styleTarget.styleManager.insert(selector, cssText)
58
+ }
59
+ activeSelector = selector
60
+ activeGeneration = selector ? styleGeneration : -1
61
+ }
62
+
63
+ if (!selector) {
64
+ return handle.element
65
+ }
66
+
67
+ return renderMixinElement(handle.element, {
68
+ ...(props ?? {}),
69
+ className: props?.className ? `${props.className} ${selector}` : selector,
70
+ })
71
+ }
72
+ })
73
+
74
+ function resolveStyleTarget(runtime: {
75
+ styleCache?: StyleCache
76
+ styleManager?: StyleManagerLike
77
+ }): { styleCache: StyleCache; styleManager?: StyleManagerLike } {
78
+ return {
79
+ styleCache: runtime.styleCache ?? clientStyleCache,
80
+ styleManager: runtime.styleManager,
81
+ }
82
+ }
@@ -0,0 +1,10 @@
1
+ import { createStyleManager } from './stylesheet.ts'
2
+
3
+ export type {
4
+ CSSProps as EnhancedStyleProperties,
5
+ DOMStyleProperties as StyleProperties,
6
+ } from './style.ts'
7
+ export { processStyleClass, normalizeCssValue } from './style.ts'
8
+ export { createStyleManager }
9
+
10
+ export type StyleManager = ReturnType<typeof createStyleManager>
@@ -0,0 +1,312 @@
1
+ export type DOMStyleProperties = {
2
+ [key in keyof Omit<
3
+ CSSStyleDeclaration,
4
+ 'item' | 'setProperty' | 'removeProperty' | 'getPropertyValue' | 'getPropertyPriority'
5
+ >]?: string | number | null | undefined
6
+ }
7
+ export type AllStyleProperties = {
8
+ [key: string]: string | number | null | undefined
9
+ }
10
+ export interface StyleProps extends AllStyleProperties, DOMStyleProperties {
11
+ cssText?: string | null
12
+ }
13
+
14
+ // allow nesting
15
+ export interface CSSProps extends DOMStyleProperties {
16
+ [key: string]: CSSProps | string | number | null | undefined
17
+ }
18
+
19
+ type StyleObject = Record<string, unknown>
20
+
21
+ // Convert camelCase CSS properties to kebab-case
22
+ function camelToKebab(str: string): string {
23
+ return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
24
+ }
25
+
26
+ // Properties that should remain unitless (numeric values without px)
27
+ const NUMERIC_CSS_PROPS = new Set([
28
+ 'aspect-ratio',
29
+ 'z-index',
30
+ 'opacity',
31
+ 'flex-grow',
32
+ 'flex-shrink',
33
+ 'flex-order',
34
+ 'grid-area',
35
+ 'grid-row',
36
+ 'grid-column',
37
+ 'font-weight',
38
+ 'line-height',
39
+ 'order',
40
+ 'orphans',
41
+ 'widows',
42
+ 'zoom',
43
+ 'columns',
44
+ 'column-count',
45
+ ])
46
+
47
+ // Normalize numeric CSS values: append 'px' for properties that need units
48
+ // In Standards Mode, browsers drop numeric values without units when multiple properties
49
+ // are present in insertRule(). We must normalize them ourselves.
50
+ export function normalizeCssValue(key: string, value: unknown): string {
51
+ if (value == null) return String(value)
52
+ if (typeof value === 'number' && value !== 0) {
53
+ let cssKey = camelToKebab(key)
54
+ if (!NUMERIC_CSS_PROPS.has(cssKey) && !cssKey.startsWith('--')) {
55
+ return `${value}px`
56
+ }
57
+ }
58
+ return String(value)
59
+ }
60
+
61
+ // Check if a style property is a nested selector or media query
62
+ function isComplexSelector(key: string): boolean {
63
+ return (
64
+ key.startsWith('&') ||
65
+ key.startsWith('@') ||
66
+ key.startsWith(':') ||
67
+ key.startsWith('[') ||
68
+ key.startsWith('.')
69
+ )
70
+ }
71
+
72
+ // Detect @keyframes (including vendor-prefixed variants)
73
+ function isKeyframesAtRule(key: string): boolean {
74
+ if (!key.startsWith('@')) return false
75
+ let lower = key.toLowerCase()
76
+ return (
77
+ lower.startsWith('@keyframes') ||
78
+ lower.startsWith('@-webkit-keyframes') ||
79
+ lower.startsWith('@-moz-keyframes') ||
80
+ lower.startsWith('@-o-keyframes')
81
+ )
82
+ }
83
+
84
+ // Generate a hash for style objects to create unique class names
85
+ function hashStyle(obj: any): string {
86
+ // Sort keys to ensure consistent hashing, but include values in the string
87
+ let sortedEntries = Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))
88
+ let str = JSON.stringify(sortedEntries)
89
+ let hash = 0
90
+ for (let i = 0; i < str.length; i++) {
91
+ let char = str.charCodeAt(i)
92
+ hash = (hash << 5) - hash + char
93
+ hash = hash & hash // Convert to 32-bit integer
94
+ }
95
+ return Math.abs(hash).toString(36)
96
+ }
97
+
98
+ // Convert style object to CSS text
99
+ function styleToCss(styles: StyleObject, selector: string = ''): string {
100
+ let baseDeclarations: string[] = []
101
+ let nestedBlocks: string[] = []
102
+ let atRules: string[] = []
103
+ let preludeAtRules: string[] = []
104
+
105
+ for (let [key, value] of Object.entries(styles)) {
106
+ if (isComplexSelector(key)) {
107
+ if (key.startsWith('@')) {
108
+ // Allow at-rules to be conditionally disabled.
109
+ // e.g. { '@media (min-width: 600px)': condition ? undefined : { ... } }
110
+ let record = toRecord(value)
111
+ if (!record) continue
112
+
113
+ // Some at-rules (e.g., @media) scope declarations to the selector.
114
+ // Others (e.g., @function) must NOT include the selector in their body.
115
+ if (key.startsWith('@function')) {
116
+ let body = atRuleBodyToCss(record)
117
+ if (body.trim().length > 0) {
118
+ preludeAtRules.push(`${key} {\n${indent(body, 2)}\n}`)
119
+ } else {
120
+ preludeAtRules.push(`${key} {\n}`)
121
+ }
122
+ } else if (isKeyframesAtRule(key)) {
123
+ // Keyframes definitions must not be wrapped with the element selector.
124
+ // Emit them before the class rule so animations can be referenced.
125
+ let body = keyframesBodyToCss(record)
126
+ if (body.trim().length > 0) {
127
+ preludeAtRules.push(`${key} {\n${indent(body, 2)}\n}`)
128
+ } else {
129
+ preludeAtRules.push(`${key} {\n}`)
130
+ }
131
+ } else {
132
+ // Default: keep at-rules nested with the element selector
133
+ let inner = styleToCss(record, selector)
134
+ if (inner.trim().length > 0) {
135
+ atRules.push(`${key} {\n${indent(inner, 2)}\n}`)
136
+ } else {
137
+ // Empty at-rule body with selector block
138
+ atRules.push(`${key} {\n ${selector} {\n }\n}`)
139
+ }
140
+ }
141
+ continue
142
+ }
143
+
144
+ // For nested selectors, keep them wholesale inside the base block
145
+ // Allow nested selectors to be conditionally disabled.
146
+ // e.g. { '&:hover': condition ? undefined : { ... } }
147
+ let record = toRecord(value)
148
+ if (!record) continue
149
+
150
+ let nestedContent = ''
151
+ for (let [prop, propValue] of Object.entries(record)) {
152
+ if (propValue != null) {
153
+ let normalizedValue = normalizeCssValue(prop, propValue)
154
+ nestedContent += ` ${camelToKebab(prop)}: ${normalizedValue};\n`
155
+ }
156
+ }
157
+ if (nestedContent) {
158
+ // Preserve key verbatim (e.g., '&[aria-selected], &[rmx-focus]')
159
+ nestedBlocks.push(` ${key} {\n${nestedContent} }`)
160
+ }
161
+ } else {
162
+ // Base declaration
163
+ if (value != null) {
164
+ let normalizedValue = normalizeCssValue(key, value)
165
+ baseDeclarations.push(` ${camelToKebab(key)}: ${normalizedValue};`)
166
+ }
167
+ }
168
+ }
169
+
170
+ let css = ''
171
+ if (preludeAtRules.length > 0) {
172
+ css += preludeAtRules.join('\n')
173
+ }
174
+ if (selector && (baseDeclarations.length > 0 || nestedBlocks.length > 0)) {
175
+ css += (css ? '\n' : '') + `${selector} {\n`
176
+ if (baseDeclarations.length > 0) {
177
+ css += baseDeclarations.join('\n') + '\n'
178
+ }
179
+ if (nestedBlocks.length > 0) {
180
+ css += nestedBlocks.join('\n') + '\n'
181
+ }
182
+ css += '}'
183
+ }
184
+
185
+ if (atRules.length > 0) {
186
+ css += (css ? '\n' : '') + atRules.join('\n')
187
+ }
188
+
189
+ return css
190
+ }
191
+
192
+ function indent(text: string, spaces: number): string {
193
+ let pad = ' '.repeat(spaces)
194
+ return text
195
+ .split('\n')
196
+ .map((line) => (line.length ? pad + line : line))
197
+ .join('\n')
198
+ }
199
+
200
+ // Narrow unknown values to plain record objects
201
+ function isRecord(value: unknown): value is Record<string, unknown> {
202
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
203
+ }
204
+
205
+ function toRecord(value: unknown): Record<string, unknown> | null {
206
+ return isRecord(value) ? value : null
207
+ }
208
+
209
+ // Build the body of a @keyframes rule (without wrapping selector)
210
+ function keyframesBodyToCss(frames: StyleObject): string {
211
+ let blocks: string[] = []
212
+
213
+ for (let [frameSelector, frameValue] of Object.entries(frames)) {
214
+ if (!isRecord(frameValue)) {
215
+ // Skip non-object frame definitions
216
+ continue
217
+ }
218
+
219
+ let declarations: string[] = []
220
+ for (let [prop, propValue] of Object.entries(frameValue)) {
221
+ if (propValue == null) continue
222
+ // Ignore nested selectors/at-rules inside keyframe steps
223
+ if (isComplexSelector(prop)) continue
224
+ let normalizedValue = normalizeCssValue(prop, propValue)
225
+ declarations.push(` ${camelToKebab(prop)}: ${normalizedValue};`)
226
+ }
227
+
228
+ if (declarations.length > 0) {
229
+ blocks.push(`${frameSelector} {\n${declarations.join('\n')}\n}`)
230
+ } else {
231
+ blocks.push(`${frameSelector} {\n}`)
232
+ }
233
+ }
234
+
235
+ return blocks.join('\n')
236
+ }
237
+
238
+ // Build the body for at-rules that should not include a selector wrapper (e.g., @function)
239
+ function atRuleBodyToCss(styles: StyleObject): string {
240
+ let declarations: string[] = []
241
+ let nested: string[] = []
242
+
243
+ for (let [key, value] of Object.entries(styles)) {
244
+ if (isComplexSelector(key)) {
245
+ if (key.startsWith('@')) {
246
+ // Nested at-rules inside definition blocks; render their bodies recursively without selectors
247
+ let record = toRecord(value)
248
+ if (!record) continue
249
+ let inner = atRuleBodyToCss(record)
250
+ if (inner.trim().length > 0) {
251
+ nested.push(`${key} {\n${indent(inner, 2)}\n}`)
252
+ } else {
253
+ nested.push(`${key} {\n}`)
254
+ }
255
+ } else {
256
+ // Ignore nested selectors (&, :, ., [) inside definition-style at-rules
257
+ // They are not meaningful within e.g. @function bodies
258
+ continue
259
+ }
260
+ } else {
261
+ if (value != null) {
262
+ let normalizedValue = normalizeCssValue(key, value)
263
+ declarations.push(` ${camelToKebab(key)}: ${normalizedValue};`)
264
+ }
265
+ }
266
+ }
267
+
268
+ let body = ''
269
+ if (declarations.length > 0) {
270
+ body += declarations.join('\n')
271
+ }
272
+ if (nested.length > 0) {
273
+ body += (body ? '\n' : '') + nested.join('\n')
274
+ }
275
+ return body
276
+ }
277
+
278
+ export function processStyleClass(
279
+ styleObj: CSSProps,
280
+ styleCache: Map<string, { selector: string; css: string }>,
281
+ ): {
282
+ selector: string
283
+ css: string
284
+ } {
285
+ // Check if the object is empty
286
+ if (Object.keys(styleObj).length === 0) {
287
+ return { selector: '', css: '' }
288
+ }
289
+
290
+ // Generate a hash for the style object
291
+ let hash = hashStyle(styleObj)
292
+ let selector = `rmxc-${hash}`
293
+
294
+ // Check cache first
295
+ let cached = styleCache.get(hash)
296
+ if (cached) {
297
+ return cached
298
+ }
299
+
300
+ let css = styleToCss(styleObj, `.${selector}`)
301
+ let result = { selector, css }
302
+
303
+ // Store in cache
304
+ styleCache.set(hash, result)
305
+
306
+ return result
307
+ }
308
+
309
+ // Clear style cache (useful for testing)
310
+ export function clearStyleCache(styleCache: Map<string, { selector: string; css: string }>): void {
311
+ styleCache.clear()
312
+ }
@@ -0,0 +1,209 @@
1
+ type RuleEntry = { count: number; index: number }
2
+ type ServerStyleSource = ParentNode | Iterable<Node>
3
+
4
+ const SERVER_STYLE_SELECTOR = 'style[data-rmx]'
5
+ const DEFAULT_STYLE_LAYER = 'rmx'
6
+
7
+ function getStyleLayerName(className: string, layer: string = DEFAULT_STYLE_LAYER): string {
8
+ return `${layer}.${className}`
9
+ }
10
+
11
+ function compareNodesInDocumentOrder(a: Node, b: Node): number {
12
+ if (a === b) return 0
13
+
14
+ let position = a.compareDocumentPosition(b)
15
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
16
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
17
+ return 0
18
+ }
19
+
20
+ function isParentNode(value: ServerStyleSource): value is ParentNode {
21
+ return 'querySelectorAll' in value
22
+ }
23
+
24
+ function collectServerStyleTagsFromNode(node: Node, into: Set<HTMLStyleElement>): void {
25
+ if (isHtmlStyleElement(node) && node.matches(SERVER_STYLE_SELECTOR)) {
26
+ into.add(node)
27
+ return
28
+ }
29
+
30
+ if (
31
+ !(node instanceof Element) &&
32
+ !(node instanceof Document) &&
33
+ !(node instanceof DocumentFragment)
34
+ ) {
35
+ return
36
+ }
37
+
38
+ let nested = node.querySelectorAll?.(SERVER_STYLE_SELECTOR) ?? []
39
+ for (let i = 0; i < nested.length; i++) {
40
+ let el = nested[i]
41
+ if (isHtmlStyleElement(el)) {
42
+ into.add(el)
43
+ }
44
+ }
45
+ }
46
+
47
+ function collectServerStyleTags(source: ServerStyleSource): HTMLStyleElement[] {
48
+ let styles = new Set<HTMLStyleElement>()
49
+
50
+ if (isParentNode(source)) {
51
+ collectServerStyleTagsFromNode(source as unknown as Node, styles)
52
+ } else {
53
+ for (let node of source) {
54
+ collectServerStyleTagsFromNode(node, styles)
55
+ }
56
+ }
57
+
58
+ return Array.from(styles).sort(compareNodesInDocumentOrder)
59
+ }
60
+
61
+ function isHtmlStyleElement(node: unknown): node is HTMLStyleElement {
62
+ return typeof node === 'object' && node !== null && node instanceof HTMLStyleElement
63
+ }
64
+
65
+ function getStyleSelector(styleEl: HTMLStyleElement): string | null {
66
+ let selector = styleEl.getAttribute('data-rmx')?.trim()
67
+ return selector ? selector : null
68
+ }
69
+
70
+ export function createStyleManager(layer: string = 'rmx') {
71
+ let stylesheet: CSSStyleSheet | null = null
72
+ let generation = 0
73
+
74
+ // Track usage count and rule index per className
75
+ // Using an object to track both count and index together
76
+ let ruleMap = new Map<string, RuleEntry>()
77
+
78
+ function getStylesheet(): CSSStyleSheet {
79
+ if (!stylesheet) {
80
+ stylesheet = new CSSStyleSheet()
81
+ document.adoptedStyleSheets.push(stylesheet)
82
+ }
83
+ return stylesheet
84
+ }
85
+
86
+ function removeStylesheet() {
87
+ if (!stylesheet) return
88
+ document.adoptedStyleSheets = Array.from(document.adoptedStyleSheets).filter(
89
+ (s) => s !== stylesheet,
90
+ )
91
+ stylesheet = null
92
+ }
93
+
94
+ function clearStylesheet() {
95
+ if (!stylesheet) return
96
+ for (let i = stylesheet.cssRules.length - 1; i >= 0; i--) {
97
+ stylesheet.deleteRule(i)
98
+ }
99
+ }
100
+
101
+ function adoptServerStyleTag(styleEl: HTMLStyleElement) {
102
+ let selector = getStyleSelector(styleEl)
103
+ if (!selector) return
104
+
105
+ if (ruleMap.has(selector)) {
106
+ styleEl.remove()
107
+ return
108
+ }
109
+
110
+ let cssText = styleEl.textContent?.trim() ?? ''
111
+ if (cssText.length === 0) {
112
+ styleEl.remove()
113
+ return
114
+ }
115
+
116
+ try {
117
+ let sheet = getStylesheet()
118
+ let index = sheet.cssRules.length
119
+ sheet.insertRule(cssText, index)
120
+ ruleMap.set(selector, { count: 1, index })
121
+ styleEl.remove()
122
+ } catch {
123
+ // If adoption fails, keep the <style> tag in the DOM so styles still apply.
124
+ }
125
+ }
126
+
127
+ function has(className: string) {
128
+ let entry = ruleMap.get(className)
129
+ return entry !== undefined && entry.count > 0
130
+ }
131
+
132
+ function getGeneration() {
133
+ return generation
134
+ }
135
+
136
+ function insert(className: string, rule: string) {
137
+ let entry = ruleMap.get(className)
138
+
139
+ if (entry) {
140
+ // Already exists, just increment count
141
+ entry.count++
142
+ return
143
+ }
144
+
145
+ // New rule - insert and track
146
+ let sheet = getStylesheet()
147
+ let index = sheet.cssRules.length
148
+ // This may throw for invalid CSS. If it does, we intentionally let it
149
+ // bubble so the rule is not tracked unless insertion actually succeeds.
150
+ sheet.insertRule(`@layer ${getStyleLayerName(className, layer)} { ${rule} }`, index)
151
+ ruleMap.set(className, { count: 1, index })
152
+ }
153
+
154
+ function remove(className: string) {
155
+ let entry = ruleMap.get(className)
156
+
157
+ if (!entry) return
158
+
159
+ // Decrement count
160
+ entry.count--
161
+
162
+ if (entry.count > 0) {
163
+ // Still in use, keep the rule
164
+ return
165
+ }
166
+
167
+ // Count reached zero, remove the rule
168
+ let indexToDelete = entry.index
169
+
170
+ // Remove from tracking
171
+ ruleMap.delete(className)
172
+
173
+ // TODO: just search and remove, stop re-indexing
174
+ if (!stylesheet) return
175
+ stylesheet.deleteRule(indexToDelete)
176
+
177
+ // Update indices for all rules that came after the deleted one
178
+ // They all shift down by 1
179
+ for (let [, data] of ruleMap.entries()) {
180
+ if (data.index > indexToDelete) {
181
+ data.index--
182
+ }
183
+ }
184
+ }
185
+
186
+ function reset() {
187
+ clearStylesheet()
188
+ ruleMap.clear()
189
+ removeStylesheet()
190
+ generation++
191
+ }
192
+
193
+ function adoptServerStyles(source: ServerStyleSource) {
194
+ let styles = collectServerStyleTags(source)
195
+
196
+ for (let styleEl of styles) {
197
+ adoptServerStyleTag(styleEl)
198
+ }
199
+ }
200
+
201
+ function dispose() {
202
+ removeStylesheet()
203
+ // Clear internal state
204
+ ruleMap.clear()
205
+ generation++
206
+ }
207
+
208
+ return { insert, remove, has, getGeneration, reset, adoptServerStyles, dispose }
209
+ }
@@ -0,0 +1,45 @@
1
+ export type Assert<T extends true> = T
2
+
3
+ export type Equal<X, Y> =
4
+ (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
5
+
6
+ export async function drain(stream: ReadableStream<Uint8Array>): Promise<string> {
7
+ let reader = stream.getReader()
8
+ let decoder = new TextDecoder()
9
+ let html = ''
10
+
11
+ while (true) {
12
+ let { done, value } = await reader.read()
13
+ if (done) break
14
+ html += decoder.decode(value)
15
+ }
16
+
17
+ return html
18
+ }
19
+
20
+ export function readChunks(stream: ReadableStream<Uint8Array>): AsyncGenerator<string, void, void> {
21
+ let reader = stream.getReader()
22
+ let decoder = new TextDecoder()
23
+
24
+ return (async function* () {
25
+ while (true) {
26
+ let { done, value } = await reader.read()
27
+ if (done) break
28
+ yield decoder.decode(value)
29
+ }
30
+ })()
31
+ }
32
+
33
+ export function withResolvers<T = unknown>(): [
34
+ Promise<T>,
35
+ (value: T) => void,
36
+ (error: unknown) => void,
37
+ ] {
38
+ let resolve!: (value: T) => void
39
+ let reject!: (error: unknown) => void
40
+ let promise = new Promise<T>((res, rej) => {
41
+ resolve = res
42
+ reject = rej
43
+ })
44
+ return [promise, resolve, reject]
45
+ }
package/src/test.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { render } from './runtime/render.ts'
2
+ export type { RenderOptions, RenderResult } from './runtime/render.ts'