@furystack/shades-common-components 10.0.35 → 11.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 (295) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/esm/components/animations.spec.d.ts +2 -0
  3. package/esm/components/animations.spec.d.ts.map +1 -0
  4. package/esm/components/animations.spec.js +201 -0
  5. package/esm/components/animations.spec.js.map +1 -0
  6. package/esm/components/app-bar-link.js +21 -20
  7. package/esm/components/app-bar-link.js.map +1 -1
  8. package/esm/components/app-bar-link.spec.d.ts +2 -0
  9. package/esm/components/app-bar-link.spec.d.ts.map +1 -0
  10. package/esm/components/app-bar-link.spec.js +252 -0
  11. package/esm/components/app-bar-link.spec.js.map +1 -0
  12. package/esm/components/app-bar.js +21 -21
  13. package/esm/components/app-bar.js.map +1 -1
  14. package/esm/components/app-bar.spec.d.ts +2 -0
  15. package/esm/components/app-bar.spec.d.ts.map +1 -0
  16. package/esm/components/app-bar.spec.js +117 -0
  17. package/esm/components/app-bar.spec.js.map +1 -0
  18. package/esm/components/avatar.d.ts.map +1 -1
  19. package/esm/components/avatar.js +15 -19
  20. package/esm/components/avatar.js.map +1 -1
  21. package/esm/components/avatar.spec.d.ts +2 -0
  22. package/esm/components/avatar.spec.d.ts.map +1 -0
  23. package/esm/components/avatar.spec.js +114 -0
  24. package/esm/components/avatar.spec.js.map +1 -0
  25. package/esm/components/button.d.ts.map +1 -1
  26. package/esm/components/button.js +145 -156
  27. package/esm/components/button.js.map +1 -1
  28. package/esm/components/button.spec.d.ts +2 -0
  29. package/esm/components/button.spec.d.ts.map +1 -0
  30. package/esm/components/button.spec.js +155 -0
  31. package/esm/components/button.spec.js.map +1 -0
  32. package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
  33. package/esm/components/command-palette/command-palette-input.js +18 -16
  34. package/esm/components/command-palette/command-palette-input.js.map +1 -1
  35. package/esm/components/command-palette/command-palette-input.spec.d.ts +2 -0
  36. package/esm/components/command-palette/command-palette-input.spec.d.ts.map +1 -0
  37. package/esm/components/command-palette/command-palette-input.spec.js +233 -0
  38. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -0
  39. package/esm/components/command-palette/command-palette-manager.spec.d.ts +2 -0
  40. package/esm/components/command-palette/command-palette-manager.spec.d.ts.map +1 -0
  41. package/esm/components/command-palette/command-palette-manager.spec.js +362 -0
  42. package/esm/components/command-palette/command-palette-manager.spec.js.map +1 -0
  43. package/esm/components/command-palette/command-palette-suggestion-list.d.ts.map +1 -1
  44. package/esm/components/command-palette/command-palette-suggestion-list.js +42 -46
  45. package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
  46. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts +2 -0
  47. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts.map +1 -0
  48. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +376 -0
  49. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -0
  50. package/esm/components/command-palette/index.d.ts.map +1 -1
  51. package/esm/components/command-palette/index.js +100 -110
  52. package/esm/components/command-palette/index.js.map +1 -1
  53. package/esm/components/command-palette/index.spec.d.ts +2 -0
  54. package/esm/components/command-palette/index.spec.d.ts.map +1 -0
  55. package/esm/components/command-palette/index.spec.js +509 -0
  56. package/esm/components/command-palette/index.spec.js.map +1 -0
  57. package/esm/components/data-grid/body.js +1 -1
  58. package/esm/components/data-grid/body.js.map +1 -1
  59. package/esm/components/data-grid/body.spec.d.ts +2 -0
  60. package/esm/components/data-grid/body.spec.d.ts.map +1 -0
  61. package/esm/components/data-grid/body.spec.js +228 -0
  62. package/esm/components/data-grid/body.spec.js.map +1 -0
  63. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  64. package/esm/components/data-grid/data-grid-row.js +49 -73
  65. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  66. package/esm/components/data-grid/data-grid-row.spec.d.ts +2 -0
  67. package/esm/components/data-grid/data-grid-row.spec.d.ts.map +1 -0
  68. package/esm/components/data-grid/data-grid-row.spec.js +296 -0
  69. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -0
  70. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  71. package/esm/components/data-grid/data-grid.js +35 -28
  72. package/esm/components/data-grid/data-grid.js.map +1 -1
  73. package/esm/components/data-grid/data-grid.spec.d.ts +2 -0
  74. package/esm/components/data-grid/data-grid.spec.d.ts.map +1 -0
  75. package/esm/components/data-grid/data-grid.spec.js +544 -0
  76. package/esm/components/data-grid/data-grid.spec.js.map +1 -0
  77. package/esm/components/data-grid/footer.js +21 -15
  78. package/esm/components/data-grid/footer.js.map +1 -1
  79. package/esm/components/data-grid/footer.spec.d.ts +2 -0
  80. package/esm/components/data-grid/footer.spec.d.ts.map +1 -0
  81. package/esm/components/data-grid/footer.spec.js +264 -0
  82. package/esm/components/data-grid/footer.spec.js.map +1 -0
  83. package/esm/components/data-grid/header.d.ts.map +1 -1
  84. package/esm/components/data-grid/header.js +55 -33
  85. package/esm/components/data-grid/header.js.map +1 -1
  86. package/esm/components/data-grid/header.spec.d.ts +2 -0
  87. package/esm/components/data-grid/header.spec.d.ts.map +1 -0
  88. package/esm/components/data-grid/header.spec.js +421 -0
  89. package/esm/components/data-grid/header.spec.js.map +1 -0
  90. package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
  91. package/esm/components/data-grid/selection-cell.js +13 -6
  92. package/esm/components/data-grid/selection-cell.js.map +1 -1
  93. package/esm/components/data-grid/selection-cell.spec.d.ts +2 -0
  94. package/esm/components/data-grid/selection-cell.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/selection-cell.spec.js +118 -0
  96. package/esm/components/data-grid/selection-cell.spec.js.map +1 -0
  97. package/esm/components/fab.d.ts.map +1 -1
  98. package/esm/components/fab.js +10 -1
  99. package/esm/components/fab.js.map +1 -1
  100. package/esm/components/fab.spec.d.ts +2 -0
  101. package/esm/components/fab.spec.d.ts.map +1 -0
  102. package/esm/components/fab.spec.js +95 -0
  103. package/esm/components/fab.spec.js.map +1 -0
  104. package/esm/components/form.spec.d.ts +2 -0
  105. package/esm/components/form.spec.d.ts.map +1 -0
  106. package/esm/components/form.spec.js +314 -0
  107. package/esm/components/form.spec.js.map +1 -0
  108. package/esm/components/grid.d.ts.map +1 -1
  109. package/esm/components/grid.js +40 -37
  110. package/esm/components/grid.js.map +1 -1
  111. package/esm/components/grid.spec.d.ts +2 -0
  112. package/esm/components/grid.spec.d.ts.map +1 -0
  113. package/esm/components/grid.spec.js +316 -0
  114. package/esm/components/grid.spec.js.map +1 -0
  115. package/esm/components/inputs/autocomplete.spec.d.ts +2 -0
  116. package/esm/components/inputs/autocomplete.spec.d.ts.map +1 -0
  117. package/esm/components/inputs/autocomplete.spec.js +194 -0
  118. package/esm/components/inputs/autocomplete.spec.js.map +1 -0
  119. package/esm/components/inputs/input.d.ts.map +1 -1
  120. package/esm/components/inputs/input.js +141 -109
  121. package/esm/components/inputs/input.js.map +1 -1
  122. package/esm/components/inputs/input.spec.d.ts +2 -0
  123. package/esm/components/inputs/input.spec.d.ts.map +1 -0
  124. package/esm/components/inputs/input.spec.js +577 -0
  125. package/esm/components/inputs/input.spec.js.map +1 -0
  126. package/esm/components/inputs/text-area.d.ts.map +1 -1
  127. package/esm/components/inputs/text-area.js +54 -58
  128. package/esm/components/inputs/text-area.js.map +1 -1
  129. package/esm/components/inputs/text-area.spec.d.ts +2 -0
  130. package/esm/components/inputs/text-area.spec.d.ts.map +1 -0
  131. package/esm/components/inputs/text-area.spec.js +214 -0
  132. package/esm/components/inputs/text-area.spec.js.map +1 -0
  133. package/esm/components/loader.js +1 -1
  134. package/esm/components/loader.js.map +1 -1
  135. package/esm/components/loader.spec.d.ts +2 -0
  136. package/esm/components/loader.spec.d.ts.map +1 -0
  137. package/esm/components/loader.spec.js +251 -0
  138. package/esm/components/loader.spec.js.map +1 -0
  139. package/esm/components/modal.d.ts.map +1 -1
  140. package/esm/components/modal.js +11 -9
  141. package/esm/components/modal.js.map +1 -1
  142. package/esm/components/modal.spec.d.ts +2 -0
  143. package/esm/components/modal.spec.d.ts.map +1 -0
  144. package/esm/components/modal.spec.js +227 -0
  145. package/esm/components/modal.spec.js.map +1 -0
  146. package/esm/components/noty-list.d.ts.map +1 -1
  147. package/esm/components/noty-list.js +39 -40
  148. package/esm/components/noty-list.js.map +1 -1
  149. package/esm/components/noty-list.spec.d.ts +2 -0
  150. package/esm/components/noty-list.spec.d.ts.map +1 -0
  151. package/esm/components/noty-list.spec.js +486 -0
  152. package/esm/components/noty-list.spec.js.map +1 -0
  153. package/esm/components/paper.d.ts.map +1 -1
  154. package/esm/components/paper.js +15 -12
  155. package/esm/components/paper.js.map +1 -1
  156. package/esm/components/paper.spec.d.ts +2 -0
  157. package/esm/components/paper.spec.d.ts.map +1 -0
  158. package/esm/components/paper.spec.js +63 -0
  159. package/esm/components/paper.spec.js.map +1 -0
  160. package/esm/components/skeleton.js +1 -1
  161. package/esm/components/skeleton.js.map +1 -1
  162. package/esm/components/skeleton.spec.d.ts +2 -0
  163. package/esm/components/skeleton.spec.d.ts.map +1 -0
  164. package/esm/components/skeleton.spec.js +159 -0
  165. package/esm/components/skeleton.spec.js.map +1 -0
  166. package/esm/components/styles.spec.d.ts +2 -0
  167. package/esm/components/styles.spec.d.ts.map +1 -0
  168. package/esm/components/styles.spec.js +56 -0
  169. package/esm/components/styles.spec.js.map +1 -0
  170. package/esm/components/suggest/index.d.ts.map +1 -1
  171. package/esm/components/suggest/index.js +74 -83
  172. package/esm/components/suggest/index.js.map +1 -1
  173. package/esm/components/suggest/index.spec.d.ts +2 -0
  174. package/esm/components/suggest/index.spec.d.ts.map +1 -0
  175. package/esm/components/suggest/index.spec.js +515 -0
  176. package/esm/components/suggest/index.spec.js.map +1 -0
  177. package/esm/components/suggest/suggest-input.d.ts.map +1 -1
  178. package/esm/components/suggest/suggest-input.js +16 -17
  179. package/esm/components/suggest/suggest-input.js.map +1 -1
  180. package/esm/components/suggest/suggest-input.spec.d.ts +2 -0
  181. package/esm/components/suggest/suggest-input.spec.d.ts.map +1 -0
  182. package/esm/components/suggest/suggest-input.spec.js +138 -0
  183. package/esm/components/suggest/suggest-input.spec.js.map +1 -0
  184. package/esm/components/suggest/suggest-manager.spec.d.ts +2 -0
  185. package/esm/components/suggest/suggest-manager.spec.d.ts.map +1 -0
  186. package/esm/components/suggest/suggest-manager.spec.js +308 -0
  187. package/esm/components/suggest/suggest-manager.spec.js.map +1 -0
  188. package/esm/components/suggest/suggestion-list.d.ts.map +1 -1
  189. package/esm/components/suggest/suggestion-list.js +43 -48
  190. package/esm/components/suggest/suggestion-list.js.map +1 -1
  191. package/esm/components/suggest/suggestion-list.spec.d.ts +2 -0
  192. package/esm/components/suggest/suggestion-list.spec.d.ts.map +1 -0
  193. package/esm/components/suggest/suggestion-list.spec.js +252 -0
  194. package/esm/components/suggest/suggestion-list.spec.js.map +1 -0
  195. package/esm/components/tabs.d.ts.map +1 -1
  196. package/esm/components/tabs.js +32 -18
  197. package/esm/components/tabs.js.map +1 -1
  198. package/esm/components/tabs.spec.d.ts +2 -0
  199. package/esm/components/tabs.spec.d.ts.map +1 -0
  200. package/esm/components/tabs.spec.js +187 -0
  201. package/esm/components/tabs.spec.js.map +1 -0
  202. package/esm/components/wizard/index.d.ts.map +1 -1
  203. package/esm/components/wizard/index.js +10 -7
  204. package/esm/components/wizard/index.js.map +1 -1
  205. package/esm/components/wizard/index.spec.d.ts +2 -0
  206. package/esm/components/wizard/index.spec.d.ts.map +1 -0
  207. package/esm/components/wizard/index.spec.js +171 -0
  208. package/esm/components/wizard/index.spec.js.map +1 -0
  209. package/esm/services/collection-service.spec.js +391 -2
  210. package/esm/services/collection-service.spec.js.map +1 -1
  211. package/esm/services/css-variable-theme.d.ts.map +1 -1
  212. package/esm/services/css-variable-theme.js +21 -1
  213. package/esm/services/css-variable-theme.js.map +1 -1
  214. package/esm/services/css-variable-theme.spec.d.ts +2 -0
  215. package/esm/services/css-variable-theme.spec.d.ts.map +1 -0
  216. package/esm/services/css-variable-theme.spec.js +169 -0
  217. package/esm/services/css-variable-theme.spec.js.map +1 -0
  218. package/esm/services/default-palette.d.ts +4 -0
  219. package/esm/services/default-palette.d.ts.map +1 -1
  220. package/esm/services/default-palette.js +22 -0
  221. package/esm/services/default-palette.js.map +1 -1
  222. package/esm/services/theme-provider-service.d.ts +59 -1
  223. package/esm/services/theme-provider-service.d.ts.map +1 -1
  224. package/esm/services/theme-provider-service.js.map +1 -1
  225. package/esm/services/theme-provider-service.spec.d.ts +2 -0
  226. package/esm/services/theme-provider-service.spec.d.ts.map +1 -0
  227. package/esm/services/theme-provider-service.spec.js +166 -0
  228. package/esm/services/theme-provider-service.spec.js.map +1 -0
  229. package/package.json +2 -2
  230. package/src/components/animations.spec.ts +299 -0
  231. package/src/components/app-bar-link.spec.tsx +341 -0
  232. package/src/components/app-bar-link.tsx +21 -21
  233. package/src/components/app-bar.spec.tsx +142 -0
  234. package/src/components/app-bar.tsx +22 -22
  235. package/src/components/avatar.spec.tsx +146 -0
  236. package/src/components/avatar.tsx +17 -20
  237. package/src/components/button.spec.tsx +193 -0
  238. package/src/components/button.tsx +162 -197
  239. package/src/components/command-palette/command-palette-input.spec.tsx +320 -0
  240. package/src/components/command-palette/command-palette-input.tsx +19 -22
  241. package/src/components/command-palette/command-palette-manager.spec.ts +470 -0
  242. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +499 -0
  243. package/src/components/command-palette/command-palette-suggestion-list.tsx +42 -46
  244. package/src/components/command-palette/index.spec.tsx +684 -0
  245. package/src/components/command-palette/index.tsx +107 -136
  246. package/src/components/data-grid/body.spec.tsx +340 -0
  247. package/src/components/data-grid/body.tsx +1 -1
  248. package/src/components/data-grid/data-grid-row.spec.tsx +382 -0
  249. package/src/components/data-grid/data-grid-row.tsx +50 -82
  250. package/src/components/data-grid/data-grid.spec.tsx +939 -0
  251. package/src/components/data-grid/data-grid.tsx +38 -35
  252. package/src/components/data-grid/footer.spec.tsx +344 -0
  253. package/src/components/data-grid/footer.tsx +19 -19
  254. package/src/components/data-grid/header.spec.tsx +563 -0
  255. package/src/components/data-grid/header.tsx +53 -44
  256. package/src/components/data-grid/selection-cell.spec.tsx +150 -0
  257. package/src/components/data-grid/selection-cell.tsx +12 -6
  258. package/src/components/fab.spec.tsx +108 -0
  259. package/src/components/fab.tsx +10 -1
  260. package/src/components/form.spec.tsx +481 -0
  261. package/src/components/grid.spec.tsx +334 -0
  262. package/src/components/grid.tsx +57 -63
  263. package/src/components/inputs/autocomplete.spec.tsx +258 -0
  264. package/src/components/inputs/input.spec.tsx +808 -0
  265. package/src/components/inputs/input.tsx +153 -139
  266. package/src/components/inputs/text-area.spec.tsx +285 -0
  267. package/src/components/inputs/text-area.tsx +53 -79
  268. package/src/components/loader.spec.tsx +346 -0
  269. package/src/components/loader.tsx +1 -1
  270. package/src/components/modal.spec.tsx +304 -0
  271. package/src/components/modal.tsx +11 -9
  272. package/src/components/noty-list.spec.tsx +631 -0
  273. package/src/components/noty-list.tsx +39 -50
  274. package/src/components/paper.spec.tsx +72 -0
  275. package/src/components/paper.tsx +15 -13
  276. package/src/components/skeleton.spec.tsx +219 -0
  277. package/src/components/skeleton.tsx +1 -1
  278. package/src/components/styles.spec.ts +70 -0
  279. package/src/components/suggest/index.spec.tsx +861 -0
  280. package/src/components/suggest/index.tsx +74 -101
  281. package/src/components/suggest/suggest-input.spec.tsx +181 -0
  282. package/src/components/suggest/suggest-input.tsx +16 -24
  283. package/src/components/suggest/suggest-manager.spec.ts +409 -0
  284. package/src/components/suggest/suggestion-list.spec.tsx +334 -0
  285. package/src/components/suggest/suggestion-list.tsx +43 -48
  286. package/src/components/tabs.spec.tsx +236 -0
  287. package/src/components/tabs.tsx +33 -21
  288. package/src/components/wizard/index.spec.tsx +224 -0
  289. package/src/components/wizard/index.tsx +10 -9
  290. package/src/services/collection-service.spec.ts +492 -3
  291. package/src/services/css-variable-theme.spec.ts +204 -0
  292. package/src/services/css-variable-theme.ts +21 -1
  293. package/src/services/default-palette.ts +22 -0
  294. package/src/services/theme-provider-service.spec.ts +195 -0
  295. package/src/services/theme-provider-service.ts +60 -2
@@ -1,6 +1,7 @@
1
1
  import type { PartialElement } from '@furystack/shades'
2
- import { Shade, attachStyles, createComponent } from '@furystack/shades'
2
+ import { Shade, createComponent } from '@furystack/shades'
3
3
  import { ObservableValue } from '@furystack/utils'
4
+ import { cssVariableTheme } from '../../services/css-variable-theme.js'
4
5
  import type { Palette } from '../../services/theme-provider-service.js'
5
6
  import { ThemeProviderService } from '../../services/theme-provider-service.js'
6
7
  import { FormService } from '../form.js'
@@ -74,76 +75,24 @@ export type TextInputState = {
74
75
  element: JSX.Element<TextInputProps>
75
76
  }
76
77
 
77
- const getLabelStyle = ({
78
+ /**
79
+ * Sets CSS custom properties for dynamic color values.
80
+ * State-based styling (focus, error, disabled) is handled by CSS selectors.
81
+ * Background colors use CSS color-mix() for automatic theme adaptation.
82
+ */
83
+ const setInputColors = ({
84
+ element,
78
85
  themeProvider,
79
86
  props,
80
- state,
81
- validationResult,
82
87
  }: {
88
+ element: HTMLElement
83
89
  themeProvider: ThemeProviderService
84
90
  props: TextInputProps
85
- state: TextInputState
86
- validationResult?: InputValidationResult
87
- }): Partial<CSSStyleDeclaration> => {
88
- const isError = state.validity?.valid === false || validationResult?.isValid === false
89
- const isOutlined = props.variant === 'outlined'
90
- const isContained = props.variant === 'contained'
91
-
92
- return {
93
- display: 'flex',
94
- flexDirection: 'column',
95
- alignItems: 'flex-start',
96
- justifyContent: 'space-between',
97
- fontSize: '11px',
98
- fontWeight: '500',
99
- letterSpacing: '0.01em',
100
- color: props.disabled
101
- ? themeProvider.theme.text.disabled
102
- : isError
103
- ? themeProvider.theme.palette.error.main
104
- : state.focused
105
- ? themeProvider.theme.palette[props.defaultColor || 'primary'].main
106
- : themeProvider.theme.text.secondary,
107
- marginBottom: '1.25em',
108
- padding: '12px 14px',
109
- borderRadius: '8px',
110
- background: isContained
111
- ? themeProvider
112
- .getRgbFromColorString(
113
- isError
114
- ? themeProvider.theme.palette.error.main
115
- : themeProvider.theme.palette[props.defaultColor || 'primary'].main,
116
- )
117
- .update('a', state.focused ? 0.12 : 0.08)
118
- .toString()
119
- : 'transparent',
120
- border:
121
- isOutlined || isContained
122
- ? `2px solid ${
123
- isError
124
- ? themeProvider.theme.palette.error.main
125
- : state.focused
126
- ? themeProvider.theme.palette[props.defaultColor || 'primary'].main
127
- : themeProvider.getRgbFromColorString(themeProvider.theme.text.secondary).update('a', 0.3).toString()
128
- }`
129
- : `2px solid transparent`,
130
- boxShadow:
131
- state.focused && !props.disabled
132
- ? `0 0 0 3px ${themeProvider
133
- .getRgbFromColorString(
134
- isError
135
- ? themeProvider.theme.palette.error.main
136
- : themeProvider.theme.palette[props.defaultColor || 'primary'].main,
137
- )
138
- .update('a', 0.15)
139
- .toString()}`
140
- : 'none',
141
- filter: props.disabled ? 'grayscale(100%)' : 'none',
142
- opacity: props.disabled ? '0.5' : '1',
143
- transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
144
- cursor: props.disabled ? 'not-allowed' : 'text',
145
- ...props.labelProps?.style,
146
- }
91
+ }): void => {
92
+ // Only set the color variables - backgrounds use CSS color-mix()
93
+ const primaryColor = themeProvider.theme.palette[props.defaultColor || 'primary'].main
94
+ element.style.setProperty('--input-primary-color', primaryColor)
95
+ element.style.setProperty('--input-error-color', themeProvider.theme.palette.error.main)
147
96
  }
148
97
 
149
98
  const getDefaultMessagesForValidityState = (state: ValidityState) => {
@@ -180,6 +129,120 @@ const getDefaultMessagesForValidityState = (state: ValidityState) => {
180
129
 
181
130
  export const Input = Shade<TextInputProps>({
182
131
  shadowDomName: 'shade-input',
132
+ css: {
133
+ display: 'block',
134
+ marginBottom: '1.25em',
135
+
136
+ // Base label styles
137
+ '& label': {
138
+ display: 'flex',
139
+ flexDirection: 'column',
140
+ alignItems: 'flex-start',
141
+ justifyContent: 'space-between',
142
+ fontSize: '11px',
143
+ fontWeight: '500',
144
+ letterSpacing: '0.01em',
145
+ padding: '12px 14px',
146
+ borderRadius: '8px',
147
+ transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
148
+ cursor: 'text',
149
+ color: cssVariableTheme.text.secondary,
150
+ background: 'transparent',
151
+ border: '2px solid transparent',
152
+ boxShadow: 'none',
153
+ },
154
+
155
+ // Outlined variant - default border
156
+ '&[data-variant="outlined"] label': {
157
+ borderColor: 'rgba(128, 128, 128, 0.3)',
158
+ },
159
+
160
+ // Contained variant - background using color-mix for theme-aware alpha
161
+ '&[data-variant="contained"] label': {
162
+ borderColor: 'rgba(128, 128, 128, 0.3)',
163
+ background: 'color-mix(in srgb, var(--input-primary-color) 8%, transparent)',
164
+ },
165
+
166
+ // Focus state using :focus-within (color change for all variants)
167
+ '&:focus-within label': {
168
+ color: 'var(--input-primary-color)',
169
+ },
170
+
171
+ // Focus state for outlined/contained variants - add border and shadow
172
+ '&[data-variant="outlined"]:focus-within label, &[data-variant="contained"]:focus-within label': {
173
+ borderColor: 'var(--input-primary-color)',
174
+ boxShadow: '0 0 0 3px rgba(128, 128, 128, 0.15)',
175
+ },
176
+ '&[data-variant="contained"]:focus-within label': {
177
+ background: 'color-mix(in srgb, var(--input-primary-color) 12%, transparent)',
178
+ },
179
+
180
+ // Invalid/error state
181
+ '&[data-invalid] label': {
182
+ color: 'var(--input-error-color)',
183
+ },
184
+ '&[data-invalid][data-variant="outlined"] label, &[data-invalid][data-variant="contained"] label': {
185
+ borderColor: 'var(--input-error-color)',
186
+ },
187
+ '&[data-invalid][data-variant="contained"] label': {
188
+ background: 'color-mix(in srgb, var(--input-error-color) 8%, transparent)',
189
+ },
190
+ '&[data-invalid]:focus-within label': {
191
+ color: 'var(--input-error-color)',
192
+ },
193
+ '&[data-invalid][data-variant="outlined"]:focus-within label, &[data-invalid][data-variant="contained"]:focus-within label':
194
+ {
195
+ borderColor: 'var(--input-error-color)',
196
+ boxShadow: '0 0 0 3px rgba(128, 128, 128, 0.15)',
197
+ },
198
+ '&[data-invalid][data-variant="contained"]:focus-within label': {
199
+ background: 'color-mix(in srgb, var(--input-error-color) 12%, transparent)',
200
+ },
201
+
202
+ // Disabled state
203
+ '&[data-disabled] label': {
204
+ color: cssVariableTheme.text.disabled,
205
+ filter: 'grayscale(100%)',
206
+ opacity: '0.5',
207
+ cursor: 'not-allowed',
208
+ },
209
+ '&[data-disabled]:focus-within label': {
210
+ boxShadow: 'none',
211
+ },
212
+
213
+ '& .input-row': {
214
+ display: 'flex',
215
+ alignItems: 'center',
216
+ width: '100%',
217
+ gap: '8px',
218
+ },
219
+ '& input': {
220
+ color: 'inherit',
221
+ border: 'none',
222
+ backgroundColor: 'transparent',
223
+ outline: 'none',
224
+ fontSize: '13px',
225
+ fontWeight: '400',
226
+ width: '100%',
227
+ textOverflow: 'ellipsis',
228
+ padding: '0',
229
+ marginTop: '8px',
230
+ marginBottom: '2px',
231
+ flexGrow: '1',
232
+ lineHeight: '1.5',
233
+ },
234
+ '& .helperText': {
235
+ fontSize: '11px',
236
+ marginTop: '6px',
237
+ opacity: '0.85',
238
+ lineHeight: '1.4',
239
+ },
240
+ '& .startIcon, & .endIcon': {
241
+ display: 'flex',
242
+ alignItems: 'center',
243
+ fontSize: '16px',
244
+ },
245
+ },
183
246
  constructed: ({ injector, element }) => {
184
247
  if (injector.cachedSingletons.has(FormService)) {
185
248
  const input = element.querySelector('input') as HTMLInputElement
@@ -191,8 +254,22 @@ export const Input = Shade<TextInputProps>({
191
254
  render: ({ props, injector, useObservable, element }) => {
192
255
  const themeProvider = injector.getInstance(ThemeProviderService)
193
256
 
257
+ // Set data attributes for CSS styling
258
+ if (props.variant) {
259
+ element.setAttribute('data-variant', props.variant)
260
+ } else {
261
+ element.removeAttribute('data-variant')
262
+ }
263
+ if (props.disabled) {
264
+ element.setAttribute('data-disabled', '')
265
+ } else {
266
+ element.removeAttribute('data-disabled')
267
+ }
268
+
269
+ // Set dynamic color CSS variables (only needs to happen once per render)
270
+ setInputColors({ element, themeProvider, props })
271
+
194
272
  const updateState = (newState: TextInputState) => {
195
- const label = element.querySelector('label') as HTMLLabelElement
196
273
  const input = element.querySelector('input') as HTMLInputElement
197
274
 
198
275
  newState.value = input?.value || newState.value
@@ -214,14 +291,13 @@ export const Input = Shade<TextInputProps>({
214
291
 
215
292
  const validationResult = props.getValidationResult?.({ state: newState })
216
293
 
294
+ // Set data-invalid attribute for CSS styling
217
295
  if (validationResult?.isValid === false || newState.validity?.valid === false) {
218
- element.setAttribute('data-validation-failed', 'true')
296
+ element.setAttribute('data-invalid', '')
219
297
  } else {
220
- element.removeAttribute('data-validation-failed')
298
+ element.removeAttribute('data-invalid')
221
299
  }
222
300
 
223
- attachStyles(label, { style: getLabelStyle({ themeProvider, props, state: newState, validationResult }) })
224
-
225
301
  const helper = element.querySelector<HTMLSpanElement>('span.helperText')
226
302
  const helperNode =
227
303
  (validationResult?.isValid === false && validationResult?.message) ||
@@ -230,38 +306,16 @@ export const Input = Shade<TextInputProps>({
230
306
  ''
231
307
  if (helper) {
232
308
  helper.replaceChildren(helperNode)
233
- attachStyles(helper, {
234
- style: {
235
- fontSize: '11px',
236
- marginTop: '6px',
237
- opacity: '0.85',
238
- lineHeight: '1.4',
239
- },
240
- })
241
309
  }
242
310
 
243
311
  const startIcon = element.querySelector<HTMLSpanElement>('span.startIcon')
244
312
  if (startIcon) {
245
313
  startIcon.replaceChildren(props.getStartIcon?.({ state: newState, validationResult }) || '')
246
- attachStyles(startIcon, {
247
- style: {
248
- display: 'flex',
249
- alignItems: 'center',
250
- fontSize: '16px',
251
- },
252
- })
253
314
  }
254
315
 
255
316
  const endIcon = element.querySelector<HTMLSpanElement>('span.endIcon')
256
317
  if (endIcon) {
257
318
  endIcon.replaceChildren(props.getEndIcon?.({ state: newState, validationResult }) || '')
258
- attachStyles(endIcon, {
259
- style: {
260
- display: 'flex',
261
- alignItems: 'center',
262
- fontSize: '16px',
263
- },
264
- })
265
319
  }
266
320
 
267
321
  if (injector.cachedSingletons.has(FormService)) {
@@ -282,22 +336,11 @@ export const Input = Shade<TextInputProps>({
282
336
  )
283
337
 
284
338
  return (
285
- <label {...props.labelProps} style={getLabelStyle({ props, state, themeProvider })}>
339
+ <label {...props.labelProps}>
286
340
  {props.labelTitle}
287
341
 
288
- <div
289
- style={{
290
- display: 'flex',
291
- alignItems: 'center',
292
- width: '100%',
293
- gap: '8px',
294
- }}
295
- >
296
- {props.getStartIcon ? (
297
- <span className="startIcon" style={{ display: 'flex', alignItems: 'center', fontSize: '16px' }}>
298
- {props.getStartIcon?.({ state })}
299
- </span>
300
- ) : null}
342
+ <div className="input-row">
343
+ {props.getStartIcon ? <span className="startIcon">{props.getStartIcon?.({ state })}</span> : null}
301
344
  <input
302
345
  oninvalid={(ev) => {
303
346
  ev.preventDefault()
@@ -320,41 +363,12 @@ export const Input = Shade<TextInputProps>({
320
363
  setState({ ...state, focused: false, validity: el.validity })
321
364
  }}
322
365
  {...props}
323
- style={{
324
- color: 'inherit',
325
- border: 'none',
326
- backgroundColor: 'transparent',
327
- outline: 'none',
328
- fontSize: '13px',
329
- fontWeight: '400',
330
- width: '100%',
331
- textOverflow: 'ellipsis',
332
- padding: '0',
333
- marginTop: '8px',
334
- marginBottom: '2px',
335
- flexGrow: '1',
336
- lineHeight: '1.5',
337
- ...props.style,
338
- }}
366
+ style={props.style}
339
367
  value={state.value}
340
368
  />
341
- {props.getEndIcon ? (
342
- <span className="endIcon" style={{ display: 'flex', alignItems: 'center', fontSize: '16px' }}>
343
- {props.getEndIcon({ state })}
344
- </span>
345
- ) : null}
369
+ {props.getEndIcon ? <span className="endIcon">{props.getEndIcon({ state })}</span> : null}
346
370
  </div>
347
- <span
348
- className="helperText"
349
- style={{
350
- fontSize: '11px',
351
- marginTop: '6px',
352
- opacity: '0.85',
353
- lineHeight: '1.4',
354
- }}
355
- >
356
- {props.getHelperText?.({ state })}
357
- </span>
371
+ <span className="helperText">{props.getHelperText?.({ state })}</span>
358
372
  </label>
359
373
  )
360
374
  },
@@ -0,0 +1,285 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
+ import { sleepAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5
+ import { TextArea } from './text-area.js'
6
+
7
+ describe('TextArea', () => {
8
+ beforeEach(() => {
9
+ document.body.innerHTML = '<div id="root"></div>'
10
+ })
11
+
12
+ afterEach(() => {
13
+ document.body.innerHTML = ''
14
+ })
15
+
16
+ it('should render with shadow DOM', async () => {
17
+ const injector = new Injector()
18
+ const rootElement = document.getElementById('root') as HTMLDivElement
19
+
20
+ initializeShadeRoot({
21
+ injector,
22
+ rootElement,
23
+ jsxElement: <TextArea />,
24
+ })
25
+
26
+ await sleepAsync(50)
27
+
28
+ const textArea = document.querySelector('shade-text-area')
29
+ expect(textArea).not.toBeNull()
30
+ })
31
+
32
+ it('should render label with labelTitle', async () => {
33
+ const injector = new Injector()
34
+ const rootElement = document.getElementById('root') as HTMLDivElement
35
+
36
+ initializeShadeRoot({
37
+ injector,
38
+ rootElement,
39
+ jsxElement: <TextArea labelTitle="Test Label" />,
40
+ })
41
+
42
+ await sleepAsync(50)
43
+
44
+ const textArea = document.querySelector('shade-text-area')
45
+ expect(textArea).not.toBeNull()
46
+
47
+ const label = textArea?.querySelector('label')
48
+ expect(label).not.toBeNull()
49
+
50
+ const span = textArea?.querySelector('span')
51
+ expect(span?.textContent).toBe('Test Label')
52
+ })
53
+
54
+ it('should apply labelProps to label element', async () => {
55
+ const injector = new Injector()
56
+ const rootElement = document.getElementById('root') as HTMLDivElement
57
+
58
+ initializeShadeRoot({
59
+ injector,
60
+ rootElement,
61
+ jsxElement: <TextArea labelTitle="Test" labelProps={{ className: 'custom-label-class' }} />,
62
+ })
63
+
64
+ await sleepAsync(50)
65
+
66
+ const textArea = document.querySelector('shade-text-area')
67
+ const label = textArea?.querySelector('label')
68
+
69
+ expect(label?.className).toBe('custom-label-class')
70
+ })
71
+
72
+ it('should set data-variant attribute for outlined variant', async () => {
73
+ const injector = new Injector()
74
+ const rootElement = document.getElementById('root') as HTMLDivElement
75
+
76
+ initializeShadeRoot({
77
+ injector,
78
+ rootElement,
79
+ jsxElement: <TextArea variant="outlined" />,
80
+ })
81
+
82
+ await sleepAsync(50)
83
+
84
+ const textArea = document.querySelector('shade-text-area') as HTMLElement
85
+ expect(textArea).not.toBeNull()
86
+ expect(textArea.getAttribute('data-variant')).toBe('outlined')
87
+ })
88
+
89
+ it('should set data-variant attribute for contained variant', async () => {
90
+ const injector = new Injector()
91
+ const rootElement = document.getElementById('root') as HTMLDivElement
92
+
93
+ initializeShadeRoot({
94
+ injector,
95
+ rootElement,
96
+ jsxElement: <TextArea variant="contained" />,
97
+ })
98
+
99
+ await sleepAsync(50)
100
+
101
+ const textArea = document.querySelector('shade-text-area') as HTMLElement
102
+ expect(textArea).not.toBeNull()
103
+ expect(textArea.getAttribute('data-variant')).toBe('contained')
104
+ })
105
+
106
+ it('should not set data-variant attribute when no variant is provided', async () => {
107
+ const injector = new Injector()
108
+ const rootElement = document.getElementById('root') as HTMLDivElement
109
+
110
+ initializeShadeRoot({
111
+ injector,
112
+ rootElement,
113
+ jsxElement: <TextArea />,
114
+ })
115
+
116
+ await sleepAsync(50)
117
+
118
+ const textArea = document.querySelector('shade-text-area') as HTMLElement
119
+ expect(textArea).not.toBeNull()
120
+ expect(textArea.hasAttribute('data-variant')).toBe(false)
121
+ })
122
+
123
+ it('should set data-disabled attribute when disabled', async () => {
124
+ const injector = new Injector()
125
+ const rootElement = document.getElementById('root') as HTMLDivElement
126
+
127
+ initializeShadeRoot({
128
+ injector,
129
+ rootElement,
130
+ jsxElement: <TextArea disabled />,
131
+ })
132
+
133
+ await sleepAsync(50)
134
+
135
+ const textArea = document.querySelector('shade-text-area') as HTMLElement
136
+ expect(textArea).not.toBeNull()
137
+ expect(textArea.hasAttribute('data-disabled')).toBe(true)
138
+ })
139
+
140
+ it('should not set data-disabled attribute when not disabled', async () => {
141
+ const injector = new Injector()
142
+ const rootElement = document.getElementById('root') as HTMLDivElement
143
+
144
+ initializeShadeRoot({
145
+ injector,
146
+ rootElement,
147
+ jsxElement: <TextArea disabled={false} />,
148
+ })
149
+
150
+ await sleepAsync(50)
151
+
152
+ const textArea = document.querySelector('shade-text-area') as HTMLElement
153
+ expect(textArea).not.toBeNull()
154
+ expect(textArea.hasAttribute('data-disabled')).toBe(false)
155
+ })
156
+
157
+ it('should render value in contentEditable div', async () => {
158
+ const injector = new Injector()
159
+ const rootElement = document.getElementById('root') as HTMLDivElement
160
+
161
+ initializeShadeRoot({
162
+ injector,
163
+ rootElement,
164
+ jsxElement: <TextArea value="Test content" />,
165
+ })
166
+
167
+ await sleepAsync(50)
168
+
169
+ const textArea = document.querySelector('shade-text-area')
170
+ const contentDiv = textArea?.querySelector('.textarea-content')
171
+
172
+ expect(contentDiv).not.toBeNull()
173
+ expect(contentDiv?.textContent).toBe('Test content')
174
+ })
175
+
176
+ it('should have contentEditable true when not readOnly or disabled', async () => {
177
+ const injector = new Injector()
178
+ const rootElement = document.getElementById('root') as HTMLDivElement
179
+
180
+ initializeShadeRoot({
181
+ injector,
182
+ rootElement,
183
+ jsxElement: <TextArea />,
184
+ })
185
+
186
+ await sleepAsync(50)
187
+
188
+ const textArea = document.querySelector('shade-text-area')
189
+ const contentDiv = textArea?.querySelector('.textarea-content') as HTMLElement
190
+
191
+ expect(contentDiv?.contentEditable).toBe('true')
192
+ })
193
+
194
+ it('should have contentEditable inherit when readOnly is true', async () => {
195
+ const injector = new Injector()
196
+ const rootElement = document.getElementById('root') as HTMLDivElement
197
+
198
+ initializeShadeRoot({
199
+ injector,
200
+ rootElement,
201
+ jsxElement: <TextArea readOnly />,
202
+ })
203
+
204
+ await sleepAsync(50)
205
+
206
+ const textArea = document.querySelector('shade-text-area')
207
+ const contentDiv = textArea?.querySelector('.textarea-content') as HTMLElement
208
+
209
+ expect(contentDiv?.contentEditable).toBe('inherit')
210
+ })
211
+
212
+ it('should have contentEditable inherit when disabled is true', async () => {
213
+ const injector = new Injector()
214
+ const rootElement = document.getElementById('root') as HTMLDivElement
215
+
216
+ initializeShadeRoot({
217
+ injector,
218
+ rootElement,
219
+ jsxElement: <TextArea disabled />,
220
+ })
221
+
222
+ await sleepAsync(50)
223
+
224
+ const textArea = document.querySelector('shade-text-area')
225
+ const contentDiv = textArea?.querySelector('.textarea-content') as HTMLElement
226
+
227
+ expect(contentDiv?.contentEditable).toBe('inherit')
228
+ })
229
+
230
+ it('should apply custom style to content div', async () => {
231
+ const injector = new Injector()
232
+ const rootElement = document.getElementById('root') as HTMLDivElement
233
+
234
+ initializeShadeRoot({
235
+ injector,
236
+ rootElement,
237
+ jsxElement: <TextArea style={{ color: 'red' }} />,
238
+ })
239
+
240
+ await sleepAsync(50)
241
+
242
+ const textArea = document.querySelector('shade-text-area')
243
+ const contentDiv = textArea?.querySelector('.textarea-content') as HTMLElement
244
+
245
+ expect(contentDiv?.style.color).toBe('red')
246
+ })
247
+
248
+ it('should apply custom style to label element', async () => {
249
+ const injector = new Injector()
250
+ const rootElement = document.getElementById('root') as HTMLDivElement
251
+
252
+ initializeShadeRoot({
253
+ injector,
254
+ rootElement,
255
+ jsxElement: <TextArea labelProps={{ style: { backgroundColor: 'blue' } }} />,
256
+ })
257
+
258
+ await sleepAsync(50)
259
+
260
+ const textArea = document.querySelector('shade-text-area')
261
+ const label = textArea?.querySelector('label') as HTMLElement
262
+
263
+ expect(label?.style.backgroundColor).toBe('blue')
264
+ })
265
+
266
+ it('should have correct CSS styles applied', async () => {
267
+ const injector = new Injector()
268
+ const rootElement = document.getElementById('root') as HTMLDivElement
269
+
270
+ initializeShadeRoot({
271
+ injector,
272
+ rootElement,
273
+ jsxElement: <TextArea />,
274
+ })
275
+
276
+ await sleepAsync(50)
277
+
278
+ const textArea = document.querySelector('shade-text-area') as HTMLElement
279
+ expect(textArea).not.toBeNull()
280
+
281
+ const computedStyle = window.getComputedStyle(textArea)
282
+ expect(computedStyle.display).toBe('block')
283
+ expect(computedStyle.marginBottom).toBe('1em')
284
+ })
285
+ })