@duro-app/ui 0.24.0 → 0.26.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 (96) hide show
  1. package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -1
  2. package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -1
  3. package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -1
  4. package/dist/components/Combobox/Combobox.d.ts +44 -0
  5. package/dist/components/Combobox/Combobox.d.ts.map +1 -0
  6. package/dist/components/Combobox/Combobox.meta.d.ts +3 -0
  7. package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -0
  8. package/dist/components/Combobox/Combobox.stories.d.ts +9 -0
  9. package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -0
  10. package/dist/components/Combobox/ComboboxContext.d.ts +19 -0
  11. package/dist/components/Combobox/ComboboxContext.d.ts.map +1 -0
  12. package/dist/components/Combobox/styles.css.d.ts +112 -0
  13. package/dist/components/Combobox/styles.css.d.ts.map +1 -0
  14. package/dist/components/Combobox/useComboboxRoot.d.ts +15 -0
  15. package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -0
  16. package/dist/components/Field/Field.d.ts +4 -0
  17. package/dist/components/Field/Field.d.ts.map +1 -1
  18. package/dist/components/Field/Field.stories.d.ts +3 -0
  19. package/dist/components/Field/Field.stories.d.ts.map +1 -1
  20. package/dist/components/Field/FieldContext.d.ts +5 -0
  21. package/dist/components/Field/FieldContext.d.ts.map +1 -1
  22. package/dist/components/Field/styles.css.d.ts +26 -0
  23. package/dist/components/Field/styles.css.d.ts.map +1 -1
  24. package/dist/components/Form/Form.d.ts +7 -2
  25. package/dist/components/Form/Form.d.ts.map +1 -1
  26. package/dist/components/Form/Form.stories.d.ts +6 -0
  27. package/dist/components/Form/Form.stories.d.ts.map +1 -1
  28. package/dist/components/Form/FormContext.d.ts +9 -2
  29. package/dist/components/Form/FormContext.d.ts.map +1 -1
  30. package/dist/components/Input/Input.d.ts.map +1 -1
  31. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  32. package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -1
  33. package/dist/components/Table/SortIndicator.d.ts.map +1 -1
  34. package/dist/components/Table/Table.d.ts.map +1 -1
  35. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/components/Table/useDataTable.d.ts.map +1 -1
  37. package/dist/components/Tag/Tag.d.ts +18 -0
  38. package/dist/components/Tag/Tag.d.ts.map +1 -0
  39. package/dist/components/Tag/Tag.meta.d.ts +3 -0
  40. package/dist/components/Tag/Tag.meta.d.ts.map +1 -0
  41. package/dist/components/Tag/Tag.stories.d.ts +11 -0
  42. package/dist/components/Tag/Tag.stories.d.ts.map +1 -0
  43. package/dist/components/Tag/styles.css.d.ts +80 -0
  44. package/dist/components/Tag/styles.css.d.ts.map +1 -0
  45. package/dist/components/TagGroup/TagGroup.d.ts +30 -0
  46. package/dist/components/TagGroup/TagGroup.d.ts.map +1 -0
  47. package/dist/components/TagGroup/TagGroup.meta.d.ts +3 -0
  48. package/dist/components/TagGroup/TagGroup.meta.d.ts.map +1 -0
  49. package/dist/components/TagGroup/TagGroup.stories.d.ts +11 -0
  50. package/dist/components/TagGroup/TagGroup.stories.d.ts.map +1 -0
  51. package/dist/components/TagGroup/TagGroupContext.d.ts +17 -0
  52. package/dist/components/TagGroup/TagGroupContext.d.ts.map +1 -0
  53. package/dist/components/TagGroup/styles.css.d.ts +75 -0
  54. package/dist/components/TagGroup/styles.css.d.ts.map +1 -0
  55. package/dist/components/Textarea/Textarea.d.ts.map +1 -1
  56. package/dist/index.css +1 -1
  57. package/dist/index.d.ts +4 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +3256 -2318
  60. package/dist/index.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/components/ButtonGroup/ButtonGroup.meta.ts +10 -2
  63. package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +4 -1
  64. package/src/components/CheckboxGroup/CheckboxGroup.tsx +2 -9
  65. package/src/components/Combobox/Combobox.meta.ts +41 -0
  66. package/src/components/Combobox/Combobox.stories.tsx +157 -0
  67. package/src/components/Combobox/Combobox.tsx +235 -0
  68. package/src/components/Combobox/ComboboxContext.ts +26 -0
  69. package/src/components/Combobox/styles.css.ts +119 -0
  70. package/src/components/Combobox/useComboboxRoot.ts +235 -0
  71. package/src/components/Field/Field.stories.tsx +62 -0
  72. package/src/components/Field/Field.tsx +94 -9
  73. package/src/components/Field/FieldContext.ts +5 -0
  74. package/src/components/Field/styles.css.ts +26 -0
  75. package/src/components/Form/Form.stories.tsx +275 -0
  76. package/src/components/Form/Form.tsx +69 -7
  77. package/src/components/Form/FormContext.ts +10 -1
  78. package/src/components/Input/Input.tsx +2 -1
  79. package/src/components/RadioGroup/RadioGroup.meta.ts +4 -1
  80. package/src/components/RadioGroup/RadioGroup.tsx +1 -4
  81. package/src/components/Table/SortIndicator.tsx +1 -4
  82. package/src/components/Table/Table.stories.tsx +190 -84
  83. package/src/components/Table/Table.tsx +10 -5
  84. package/src/components/Table/useDataTable.ts +34 -48
  85. package/src/components/Tag/Tag.meta.ts +22 -0
  86. package/src/components/Tag/Tag.stories.tsx +92 -0
  87. package/src/components/Tag/Tag.tsx +139 -0
  88. package/src/components/Tag/styles.css.ts +99 -0
  89. package/src/components/TagGroup/TagGroup.meta.ts +27 -0
  90. package/src/components/TagGroup/TagGroup.stories.tsx +193 -0
  91. package/src/components/TagGroup/TagGroup.tsx +302 -0
  92. package/src/components/TagGroup/TagGroupContext.ts +22 -0
  93. package/src/components/TagGroup/styles.css.ts +97 -0
  94. package/src/components/Textarea/Textarea.tsx +2 -1
  95. package/src/index.ts +4 -0
  96. package/src/strict.css +1 -1
@@ -0,0 +1,302 @@
1
+ import {
2
+ type ReactNode,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react'
11
+ import {html} from 'react-strict-dom'
12
+ import {useControllableValue} from '../../hooks/useControllableValue'
13
+ import {useFieldContext} from '../Field/FieldContext'
14
+ import {TagGroupContext, type TagGroupContextValue} from './TagGroupContext'
15
+ import {styles} from './styles.css'
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Local context accessor (non-throwing, returns null)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function useTagGroupCtx() {
22
+ return useContext(TagGroupContext)
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // TagGroup.Root
27
+ // ---------------------------------------------------------------------------
28
+
29
+ interface RootProps {
30
+ value?: string[]
31
+ defaultValue?: string[]
32
+ onValueChange?: (values: string[]) => void
33
+ /** Validate a value before adding. Return `true` if valid, or an error string. */
34
+ onValidate?: (value: string) => true | string
35
+ disabled?: boolean
36
+ /** Form field name — renders hidden inputs for each tag value */
37
+ name?: string
38
+ 'aria-label'?: string
39
+ children: ReactNode
40
+ }
41
+
42
+ function Root({
43
+ value: controlledValue,
44
+ defaultValue = [],
45
+ onValueChange,
46
+ onValidate,
47
+ disabled: disabledProp = false,
48
+ name,
49
+ 'aria-label': ariaLabel,
50
+ children,
51
+ }: RootProps) {
52
+ const gridId = useId()
53
+ const fieldCtx = useFieldContext()
54
+ const [values, setValues] = useControllableValue(controlledValue, defaultValue, onValueChange)
55
+ const [focusedIndex, setFocusedIndex] = useState(-1)
56
+ const [editable, setEditable] = useState(false)
57
+ const [announcement, setAnnouncement] = useState('')
58
+ const inputRef = useRef<HTMLInputElement>(null)
59
+
60
+ const disabled = disabledProp || (fieldCtx?.disabled ?? false)
61
+ const isInvalid = fieldCtx?.invalid ?? false
62
+
63
+ const addTag = useCallback(
64
+ (value: string) => {
65
+ const trimmed = value.trim()
66
+ if (!trimmed) return
67
+ if (values.includes(trimmed)) return
68
+ if (onValidate) {
69
+ const result = onValidate(trimmed)
70
+ if (result !== true) return
71
+ }
72
+ setValues([...values, trimmed])
73
+ setAnnouncement(`Added ${trimmed}`)
74
+ },
75
+ [values, setValues, onValidate],
76
+ )
77
+
78
+ const removeTag = useCallback(
79
+ (value: string) => {
80
+ const idx = values.indexOf(value)
81
+ if (idx === -1) return
82
+ const next = values.filter((v) => v !== value)
83
+ setValues(next)
84
+ setAnnouncement(`Removed ${value}`)
85
+
86
+ // Focus management: move to next tag, or previous, or input
87
+ if (next.length === 0) {
88
+ inputRef.current?.focus()
89
+ setFocusedIndex(-1)
90
+ } else if (idx >= next.length) {
91
+ setFocusedIndex(next.length - 1)
92
+ } else {
93
+ setFocusedIndex(idx)
94
+ }
95
+ },
96
+ [values, setValues],
97
+ )
98
+
99
+ // Focus the tag at focusedIndex when it changes
100
+ useEffect(() => {
101
+ if (focusedIndex >= 0 && focusedIndex < values.length) {
102
+ const container = document.getElementById(gridId)
103
+ if (!container) return
104
+ const rows = container.querySelectorAll('[role="row"]')
105
+ const target = rows[focusedIndex] as HTMLElement | undefined
106
+ target?.focus()
107
+ }
108
+ }, [focusedIndex, gridId, values.length])
109
+
110
+ const ctx = useMemo<TagGroupContextValue>(
111
+ () => ({
112
+ values,
113
+ addTag,
114
+ removeTag,
115
+ editable,
116
+ setEditable,
117
+ disabled,
118
+ validate: onValidate,
119
+ focusedIndex,
120
+ setFocusedIndex,
121
+ inputRef,
122
+ gridId,
123
+ name,
124
+ }),
125
+ [values, addTag, removeTag, editable, disabled, onValidate, focusedIndex, gridId, name],
126
+ )
127
+
128
+ return (
129
+ <TagGroupContext.Provider value={ctx}>
130
+ <html.div style={styles.root} aria-label={ariaLabel}>
131
+ <html.div
132
+ style={[
133
+ styles.container,
134
+ !editable && styles.containerStatic,
135
+ isInvalid && styles.containerError,
136
+ disabled && styles.containerDisabled,
137
+ ]}
138
+ aria-describedby={
139
+ fieldCtx
140
+ ? `${fieldCtx.descriptionId} ${fieldCtx.invalid ? fieldCtx.errorId : ''}`.trim()
141
+ : undefined
142
+ }
143
+ aria-invalid={isInvalid || undefined}
144
+ >
145
+ {children}
146
+ </html.div>
147
+
148
+ {/* Hidden inputs for form submission */}
149
+ {name && values.map((v) => <html.input key={v} type="hidden" name={name} value={v} />)}
150
+
151
+ {/* Live region for screen reader announcements */}
152
+ <html.div role="status" aria-live="polite" aria-atomic style={styles.liveRegion}>
153
+ {announcement}
154
+ </html.div>
155
+ </html.div>
156
+ </TagGroupContext.Provider>
157
+ )
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // TagGroup.List
162
+ // ---------------------------------------------------------------------------
163
+
164
+ interface ListProps {
165
+ children: ReactNode
166
+ 'aria-label'?: string
167
+ }
168
+
169
+ function List({children, 'aria-label': ariaLabel}: ListProps) {
170
+ const ctx = useTagGroupCtx()
171
+
172
+ return (
173
+ <html.div role={'grid' as any} aria-label={ariaLabel} id={ctx?.gridId} style={styles.list}>
174
+ {children}
175
+ </html.div>
176
+ )
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // TagGroup.Input
181
+ // ---------------------------------------------------------------------------
182
+
183
+ interface InputProps {
184
+ placeholder?: string
185
+ }
186
+
187
+ function Input({placeholder}: InputProps) {
188
+ const ctx = useTagGroupCtx()
189
+ const [inputValue, setInputValue] = useState('')
190
+ const localRef = useRef<HTMLInputElement>(null)
191
+
192
+ // Sync local ref with context inputRef
193
+ useEffect(() => {
194
+ if (ctx?.inputRef) {
195
+ ;(ctx.inputRef as React.MutableRefObject<HTMLInputElement | null>).current = localRef.current
196
+ }
197
+ })
198
+
199
+ // Register as editable on mount
200
+ useEffect(() => {
201
+ ctx?.setEditable(true)
202
+ return () => ctx?.setEditable(false)
203
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
204
+
205
+ const commitValueRef = useRef<(raw: string) => boolean>(() => false)
206
+ commitValueRef.current = (raw: string) => {
207
+ const trimmed = raw.trim()
208
+ if (!trimmed || !ctx) return false
209
+ ctx.addTag(trimmed)
210
+ setInputValue('')
211
+ return true
212
+ }
213
+
214
+ // Native keydown for full KeyboardEvent access (preventDefault)
215
+ useEffect(() => {
216
+ const el = localRef.current
217
+ if (!el) return
218
+
219
+ function handleKeyDown(e: KeyboardEvent) {
220
+ if (!ctx) return
221
+ const target = e.target as HTMLInputElement
222
+
223
+ if (e.key === 'Enter' || e.key === ',' || e.key === ';') {
224
+ e.preventDefault()
225
+ commitValueRef.current(target.value)
226
+ return
227
+ }
228
+
229
+ if (e.key === 'Tab' && target.value.trim()) {
230
+ e.preventDefault()
231
+ commitValueRef.current(target.value)
232
+ return
233
+ }
234
+
235
+ if (e.key === 'Backspace' && !target.value && ctx.values.length > 0) {
236
+ e.preventDefault()
237
+ ctx.removeTag(ctx.values[ctx.values.length - 1])
238
+ return
239
+ }
240
+
241
+ if (e.key === 'ArrowLeft' && (target.selectionStart ?? 0) === 0 && ctx.values.length > 0) {
242
+ e.preventDefault()
243
+ ctx.setFocusedIndex(ctx.values.length - 1)
244
+ return
245
+ }
246
+
247
+ if (e.key === 'Escape') {
248
+ setInputValue('')
249
+ }
250
+ }
251
+
252
+ function handlePaste(e: ClipboardEvent) {
253
+ if (!ctx) return
254
+ const text = e.clipboardData?.getData('text') ?? ''
255
+ const items = text
256
+ .split(/[\n,;]+/)
257
+ .map((s) => s.trim())
258
+ .filter(Boolean)
259
+ if (items.length > 1) {
260
+ e.preventDefault()
261
+ for (const item of items) {
262
+ ctx.addTag(item)
263
+ }
264
+ }
265
+ }
266
+
267
+ function handleFocus() {
268
+ ctx?.setFocusedIndex(-1)
269
+ }
270
+
271
+ el.addEventListener('keydown', handleKeyDown)
272
+ el.addEventListener('paste', handlePaste)
273
+ el.addEventListener('focus', handleFocus)
274
+ return () => {
275
+ el.removeEventListener('keydown', handleKeyDown)
276
+ el.removeEventListener('paste', handlePaste)
277
+ el.removeEventListener('focus', handleFocus)
278
+ }
279
+ }, [ctx])
280
+
281
+ return (
282
+ <html.input
283
+ ref={localRef as React.Ref<HTMLInputElement>}
284
+ type="text"
285
+ value={inputValue}
286
+ onChange={(e: any) => setInputValue(e.target.value)}
287
+ placeholder={!ctx?.values.length ? placeholder : undefined}
288
+ disabled={ctx?.disabled}
289
+ style={styles.input}
290
+ />
291
+ )
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Namespace export
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export const TagGroup = {
299
+ Root,
300
+ List,
301
+ Input,
302
+ }
@@ -0,0 +1,22 @@
1
+ import {createContext, useContext} from 'react'
2
+
3
+ export interface TagGroupContextValue {
4
+ values: string[]
5
+ addTag: (value: string) => void
6
+ removeTag: (value: string) => void
7
+ editable: boolean
8
+ setEditable: (v: boolean) => void
9
+ disabled: boolean
10
+ validate?: (value: string) => true | string
11
+ focusedIndex: number
12
+ setFocusedIndex: (index: number) => void
13
+ inputRef: React.RefObject<HTMLInputElement | null>
14
+ gridId: string
15
+ name?: string
16
+ }
17
+
18
+ export const TagGroupContext = createContext<TagGroupContextValue | null>(null)
19
+
20
+ export function useTagGroup(): TagGroupContextValue | null {
21
+ return useContext(TagGroupContext)
22
+ }
@@ -0,0 +1,97 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+
6
+ export const styles = css.create({
7
+ root: {
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ },
11
+ container: {
12
+ display: 'flex',
13
+ flexWrap: 'wrap',
14
+ alignItems: 'center',
15
+ gap: spacing.xs,
16
+ paddingTop: spacing.xs,
17
+ paddingBottom: spacing.xs,
18
+ paddingLeft: spacing.sm,
19
+ paddingRight: spacing.sm,
20
+ borderWidth: 1,
21
+ borderStyle: 'solid',
22
+ borderColor: {
23
+ default: colors.border,
24
+ ':focus-within': colors.accent,
25
+ },
26
+ borderRadius: radii.sm,
27
+ backgroundColor: colors.bg,
28
+ minHeight: 40,
29
+ transitionProperty: 'border-color',
30
+ transitionDuration: '150ms',
31
+ transitionTimingFunction: 'ease',
32
+ outlineWidth: {
33
+ default: 0,
34
+ ':focus-within': 2,
35
+ },
36
+ outlineStyle: {
37
+ default: 'none',
38
+ ':focus-within': 'solid',
39
+ },
40
+ outlineColor: {
41
+ default: 'transparent',
42
+ ':focus-within': colors.accent,
43
+ },
44
+ outlineOffset: {
45
+ default: 0,
46
+ ':focus-within': 1,
47
+ },
48
+ },
49
+ containerError: {
50
+ borderColor: {
51
+ default: colors.error,
52
+ ':focus-within': colors.error,
53
+ },
54
+ },
55
+ containerDisabled: {
56
+ opacity: 0.5,
57
+ cursor: 'not-allowed',
58
+ },
59
+ // Static display (no Input) — no input-field border
60
+ containerStatic: {
61
+ borderWidth: 0,
62
+ paddingTop: 0,
63
+ paddingBottom: 0,
64
+ paddingLeft: 0,
65
+ paddingRight: 0,
66
+ minHeight: 0,
67
+ backgroundColor: 'transparent',
68
+ outlineWidth: 0,
69
+ },
70
+ list: {
71
+ display: 'flex',
72
+ flexWrap: 'wrap',
73
+ gap: spacing.xs,
74
+ alignItems: 'center',
75
+ },
76
+ input: {
77
+ flex: 1,
78
+ minWidth: 120,
79
+ borderWidth: 0,
80
+ backgroundColor: 'transparent',
81
+ fontFamily: typography.fontFamily,
82
+ fontSize: typography.fontSizeSm,
83
+ lineHeight: typography.lineHeight,
84
+ color: colors.text,
85
+ outlineWidth: 0,
86
+ paddingTop: spacing.xs,
87
+ paddingBottom: spacing.xs,
88
+ paddingLeft: spacing.xs,
89
+ paddingRight: spacing.xs,
90
+ },
91
+ liveRegion: {
92
+ position: 'absolute',
93
+ width: 1,
94
+ height: 1,
95
+ overflow: 'hidden',
96
+ },
97
+ })
@@ -41,6 +41,7 @@ export function Textarea({
41
41
  const effectiveOnChange = onChange ?? fieldCtx?.onChange
42
42
  const effectiveOnBlur = onBlur ?? fieldCtx?.onBlur
43
43
  const effectiveRef = ref ?? fieldCtx?.ref
44
+ const effectiveDisabled = disabled ?? ctx?.disabled
44
45
  const effectiveVariant = ctx?.invalid ? 'error' : variant
45
46
 
46
47
  return (
@@ -52,7 +53,7 @@ export function Textarea({
52
53
  rows={rows}
53
54
  value={effectiveValue}
54
55
  defaultValue={defaultValue}
55
- disabled={disabled}
56
+ disabled={effectiveDisabled}
56
57
  aria-describedby={
57
58
  ctx ? `${ctx.descriptionId} ${ctx.invalid ? ctx.errorId : ''}`.trim() : undefined
58
59
  }
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export {Heading, type HeadingVariant} from './components/Heading/Heading'
21
21
  export {List, type ListSelectionMode} from './components/List/List'
22
22
  export {Field} from './components/Field/Field'
23
23
  export {Form, type FormProps} from './components/Form/Form'
24
+ export type {LabelPosition, NecessityIndicator} from './components/Form/FormContext'
24
25
  export {Fieldset, type FieldsetGap} from './components/Fieldset/Fieldset'
25
26
  export {Input, type InputVariant} from './components/Input/Input'
26
27
  export {InputGroup} from './components/InputGroup/InputGroup'
@@ -33,6 +34,7 @@ export {Menu} from './components/Menu/Menu'
33
34
  export {Panel} from './components/Panel/Panel'
34
35
  export {RadioGroup} from './components/RadioGroup/RadioGroup'
35
36
  export {ScrollArea} from './components/ScrollArea/ScrollArea'
37
+ export {Combobox} from './components/Combobox/Combobox'
36
38
  export {Select} from './components/Select/Select'
37
39
  export {SideNav} from './components/SideNav/SideNav'
38
40
  export {Spinner, type SpinnerSize} from './components/Spinner/Spinner'
@@ -41,6 +43,8 @@ export {
41
43
  type StatusIconName,
42
44
  type StatusIconVariant,
43
45
  } from './components/StatusIcon/StatusIcon'
46
+ export {Tag, type TagVariant, type TagSize} from './components/Tag/Tag'
47
+ export {TagGroup} from './components/TagGroup/TagGroup'
44
48
  export {Table, type TableVariant, type TableSize} from './components/Table/Table'
45
49
  // TanStack Table: consumers install @tanstack/react-table directly and use
46
50
  // Table.Pagination, Table.SortIndicator, Table.ColumnFilter for styled integration
package/src/strict.css CHANGED
@@ -2,6 +2,6 @@
2
2
  @react-strict-dom;
3
3
 
4
4
  /* Table: reveal sort indicator on header cell hover */
5
- [role="columnheader"]:hover [data-sort-indicator] {
5
+ [role='columnheader']:hover [data-sort-indicator] {
6
6
  opacity: 1 !important;
7
7
  }