@srcroot/ui 0.0.3 → 0.0.5

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.
@@ -5,11 +5,12 @@ import { cn } from "@/lib/utils"
5
5
 
6
6
  // Resizable Context
7
7
  interface ResizablePanelGroupContextValue {
8
+ groupId: string
8
9
  direction: "horizontal" | "vertical"
9
- sizes: number[]
10
- setSizes: React.Dispatch<React.SetStateAction<number[]>>
11
- registerPanel: () => number
12
- getPanelCount: () => number
10
+ registerPanel: (defaultSize: number, minSize: number, maxSize: number) => number
11
+ getSize: (index: number) => number
12
+ getTotalSize: () => number
13
+ onResize: (handleIndex: number, delta: number) => void
13
14
  }
14
15
 
15
16
  const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
@@ -28,38 +29,115 @@ interface ResizablePanelGroupProps extends React.HTMLAttributes<HTMLDivElement>
28
29
  onLayout?: (sizes: number[]) => void
29
30
  }
30
31
 
32
+ interface PanelConfig {
33
+ defaultSize: number
34
+ minSize: number
35
+ maxSize: number
36
+ }
37
+
31
38
  const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
32
39
  ({ className, direction = "horizontal", children, onLayout, ...props }, ref) => {
33
- const [sizes, setSizes] = React.useState<number[]>([])
40
+ const groupId = React.useId()
41
+ const containerRef = React.useRef<HTMLDivElement>(null)
42
+ const [, forceUpdate] = React.useReducer(x => x + 1, 0)
43
+
44
+ // Store panel configs and sizes in refs for stable access
45
+ const panelConfigsRef = React.useRef<PanelConfig[]>([])
46
+ const sizesRef = React.useRef<number[]>([])
34
47
  const panelCountRef = React.useRef(0)
35
48
 
36
- // Reset panel count on each render to handle re-renders properly
37
- React.useLayoutEffect(() => {
38
- panelCountRef.current = 0
39
- })
49
+ // Reset panel count at start of each render
50
+ panelCountRef.current = 0
51
+
52
+ const registerPanel = React.useCallback((defaultSize: number, minSize: number, maxSize: number) => {
53
+ const index = panelCountRef.current++
54
+
55
+ // Only initialize if not already set
56
+ if (sizesRef.current[index] === undefined) {
57
+ sizesRef.current[index] = defaultSize
58
+ panelConfigsRef.current[index] = { defaultSize, minSize, maxSize }
59
+ }
40
60
 
41
- const registerPanel = React.useCallback(() => {
42
- const index = panelCountRef.current
43
- panelCountRef.current += 1
44
61
  return index
45
62
  }, [])
46
63
 
47
- const getPanelCount = React.useCallback(() => {
48
- return panelCountRef.current
64
+ const getSize = React.useCallback((index: number) => {
65
+ return sizesRef.current[index] ?? 50
49
66
  }, [])
50
67
 
51
- // Notify layout changes
52
- React.useEffect(() => {
53
- if (sizes.length > 0) {
54
- onLayout?.(sizes)
68
+ const getTotalSize = React.useCallback(() => {
69
+ return sizesRef.current.reduce((sum, s) => sum + s, 0)
70
+ }, [])
71
+
72
+ const onResize = React.useCallback((handleIndex: number, delta: number) => {
73
+ const container = containerRef.current
74
+ if (!container) return
75
+
76
+ const containerSize = direction === "horizontal"
77
+ ? container.offsetWidth
78
+ : container.offsetHeight
79
+
80
+ if (containerSize === 0) return
81
+
82
+ const deltaPercent = (delta / containerSize) * 100
83
+
84
+ const leftIndex = handleIndex
85
+ const rightIndex = handleIndex + 1
86
+ const sizes = sizesRef.current
87
+ const configs = panelConfigsRef.current
88
+
89
+ if (leftIndex >= sizes.length || rightIndex >= sizes.length) return
90
+
91
+ const leftSize = sizes[leftIndex]
92
+ const rightSize = sizes[rightIndex]
93
+ const totalSize = leftSize + rightSize
94
+
95
+ let newLeftSize = leftSize + deltaPercent
96
+ let newRightSize = rightSize - deltaPercent
97
+
98
+ const leftMin = configs[leftIndex]?.minSize ?? 10
99
+ const leftMax = configs[leftIndex]?.maxSize ?? 90
100
+ const rightMin = configs[rightIndex]?.minSize ?? 10
101
+ const rightMax = configs[rightIndex]?.maxSize ?? 90
102
+
103
+ // Apply constraints
104
+ if (newLeftSize < leftMin) {
105
+ newLeftSize = leftMin
106
+ newRightSize = totalSize - leftMin
107
+ }
108
+ if (newRightSize < rightMin) {
109
+ newRightSize = rightMin
110
+ newLeftSize = totalSize - rightMin
111
+ }
112
+ if (newLeftSize > leftMax) {
113
+ newLeftSize = leftMax
114
+ newRightSize = totalSize - leftMax
55
115
  }
56
- }, [sizes, onLayout])
116
+ if (newRightSize > rightMax) {
117
+ newRightSize = rightMax
118
+ newLeftSize = totalSize - rightMax
119
+ }
120
+
121
+ sizesRef.current[leftIndex] = newLeftSize
122
+ sizesRef.current[rightIndex] = newRightSize
123
+
124
+ // Force re-render to update panel sizes
125
+ forceUpdate()
126
+
127
+ // Notify layout changes
128
+ onLayout?.([...sizesRef.current])
129
+ }, [direction, onLayout])
57
130
 
58
131
  return (
59
- <ResizablePanelGroupContext.Provider value={{ direction, sizes, setSizes, registerPanel, getPanelCount }}>
132
+ <ResizablePanelGroupContext.Provider value={{ groupId, direction, registerPanel, getSize, getTotalSize, onResize }}>
60
133
  <div
61
- ref={ref}
134
+ ref={(node) => {
135
+ containerRef.current = node
136
+ if (typeof ref === "function") ref(node)
137
+ else if (ref) ref.current = node
138
+ }}
62
139
  data-panel-group
140
+ data-panel-group-id={groupId}
63
141
  data-direction={direction}
64
142
  className={cn(
65
143
  "flex h-full w-full",
@@ -85,31 +163,30 @@ interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
85
163
 
86
164
  const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
87
165
  ({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
88
- const { direction, sizes, setSizes, registerPanel } = useResizablePanelGroup()
89
- const [index] = React.useState(() => registerPanel())
90
-
91
- // Initialize size
92
- React.useLayoutEffect(() => {
93
- setSizes(prev => {
94
- const newSizes = [...prev]
95
- if (newSizes[index] === undefined) {
96
- newSizes[index] = defaultSize
97
- }
98
- return newSizes
99
- })
100
- }, [index, defaultSize, setSizes])
166
+ const { groupId, direction, registerPanel, getSize, getTotalSize } = useResizablePanelGroup()
167
+ const indexRef = React.useRef<number>(-1)
168
+
169
+ // Register panel on first render only
170
+ if (indexRef.current === -1) {
171
+ indexRef.current = registerPanel(defaultSize, minSize, maxSize)
172
+ }
101
173
 
102
- const size = sizes[index] ?? defaultSize
174
+ const size = getSize(indexRef.current)
175
+ const totalSize = getTotalSize()
176
+ // Convert to actual percentage of total
177
+ // const actualPercent = totalSize > 0 ? (size / totalSize) * 100 : size
103
178
 
104
179
  return (
105
180
  <div
106
181
  ref={ref}
107
182
  data-panel
108
- data-panel-index={index}
183
+ data-panel-group-id={groupId}
184
+ data-panel-index={indexRef.current}
109
185
  className={cn("overflow-hidden", className)}
110
186
  style={{
111
187
  ...style,
112
- flex: `0 0 ${size}%`,
188
+ // Use flex-grow with the size as the ratio for smoother resizing
189
+ flex: `${size} 1 0`,
113
190
  }}
114
191
  {...props}
115
192
  >
@@ -127,92 +204,78 @@ interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
127
204
 
128
205
  const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
129
206
  ({ className, withHandle = false, ...props }, ref) => {
130
- const { direction, setSizes } = useResizablePanelGroup()
207
+ const { direction, onResize } = useResizablePanelGroup()
131
208
  const [isDragging, setIsDragging] = React.useState(false)
209
+ const handleIndexRef = React.useRef<number>(-1)
210
+ const lastPosRef = React.useRef<number>(0)
132
211
  const handleRef = React.useRef<HTMLDivElement>(null)
133
212
 
134
- const handleMouseDown = (e: React.MouseEvent) => {
135
- e.preventDefault()
136
- setIsDragging(true)
137
-
138
- const startPos = direction === "horizontal" ? e.clientX : e.clientY
213
+ // Determine handle index from DOM position
214
+ const getHandleIndex = React.useCallback(() => {
139
215
  const handle = handleRef.current
140
- if (!handle) return
216
+ if (!handle) return 0
217
+
218
+ const parent = handle.parentElement
219
+ if (!parent) return 0
141
220
 
142
- // Find adjacent panels
143
- const prevPanel = handle.previousElementSibling as HTMLElement
144
- const nextPanel = handle.nextElementSibling as HTMLElement
145
- if (!prevPanel || !nextPanel) return
221
+ let handleCount = 0
222
+ for (const child of Array.from(parent.children)) {
223
+ if (child === handle) break
224
+ if (child.hasAttribute('data-panel-resize-handle')) {
225
+ handleCount++
226
+ }
227
+ }
228
+ return handleCount
229
+ }, [])
146
230
 
147
- const prevIndex = parseInt(prevPanel.dataset.panelIndex || "0")
148
- const nextIndex = parseInt(nextPanel.dataset.panelIndex || "1")
231
+ const handleMouseDown = React.useCallback((e: React.MouseEvent) => {
232
+ e.preventDefault()
233
+ e.stopPropagation()
234
+ setIsDragging(true)
149
235
 
150
- // Get current sizes
151
- const prevRect = prevPanel.getBoundingClientRect()
152
- const nextRect = nextPanel.getBoundingClientRect()
153
- const totalSize = direction === "horizontal"
154
- ? prevRect.width + nextRect.width
155
- : prevRect.height + nextRect.height
236
+ const handleIndex = getHandleIndex()
237
+ const startPos = direction === "horizontal" ? e.clientX : e.clientY
238
+ lastPosRef.current = startPos
156
239
 
157
240
  const handleMouseMove = (moveEvent: MouseEvent) => {
158
241
  const currentPos = direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
159
- const delta = currentPos - startPos
160
- const deltaPercent = (delta / totalSize) * 100
161
-
162
- setSizes(prev => {
163
- const newSizes = [...prev]
164
- const prevSize = prev[prevIndex] ?? 50
165
- const nextSize = prev[nextIndex] ?? 50
166
-
167
- // Calculate new sizes with constraints
168
- let newPrevSize = prevSize + deltaPercent
169
- let newNextSize = nextSize - deltaPercent
170
-
171
- // Apply min/max constraints
172
- if (newPrevSize < 10) {
173
- newPrevSize = 10
174
- newNextSize = prevSize + nextSize - 10
175
- }
176
- if (newNextSize < 10) {
177
- newNextSize = 10
178
- newPrevSize = prevSize + nextSize - 10
179
- }
180
- if (newPrevSize > 90) {
181
- newPrevSize = 90
182
- newNextSize = prevSize + nextSize - 90
183
- }
184
- if (newNextSize > 90) {
185
- newNextSize = 90
186
- newPrevSize = prevSize + nextSize - 90
187
- }
188
-
189
- newSizes[prevIndex] = newPrevSize
190
- newSizes[nextIndex] = newNextSize
191
- return newSizes
192
- })
242
+ const delta = currentPos - lastPosRef.current
243
+ lastPosRef.current = currentPos
244
+
245
+ if (delta !== 0) {
246
+ onResize(handleIndex, delta)
247
+ }
193
248
  }
194
249
 
195
250
  const handleMouseUp = () => {
196
251
  setIsDragging(false)
197
252
  document.removeEventListener("mousemove", handleMouseMove)
198
253
  document.removeEventListener("mouseup", handleMouseUp)
254
+ document.body.style.cursor = ""
255
+ document.body.style.userSelect = ""
199
256
  }
200
257
 
258
+ document.body.style.cursor = direction === "horizontal" ? "col-resize" : "row-resize"
259
+ document.body.style.userSelect = "none"
201
260
  document.addEventListener("mousemove", handleMouseMove)
202
261
  document.addEventListener("mouseup", handleMouseUp)
203
- }
262
+ }, [direction, onResize, getHandleIndex])
204
263
 
205
264
  return (
206
265
  <div
207
- ref={handleRef}
266
+ ref={(node) => {
267
+ handleRef.current = node
268
+ if (typeof ref === "function") ref(node)
269
+ else if (ref) ref.current = node
270
+ }}
208
271
  data-panel-resize-handle
272
+ data-dragging={isDragging}
209
273
  className={cn(
210
- "relative flex items-center justify-center bg-border",
274
+ "relative flex shrink-0 items-center justify-center bg-border",
211
275
  direction === "horizontal"
212
- ? "w-px cursor-col-resize hover:w-1 hover:bg-primary/50"
213
- : "h-px cursor-row-resize hover:h-1 hover:bg-primary/50",
214
- isDragging && (direction === "horizontal" ? "w-1 bg-primary" : "h-1 bg-primary"),
215
- "transition-all",
276
+ ? "w-1 cursor-col-resize hover:bg-primary/50 active:bg-primary"
277
+ : "h-1 cursor-row-resize hover:bg-primary/50 active:bg-primary",
278
+ isDragging && "bg-primary",
216
279
  className
217
280
  )}
218
281
  onMouseDown={handleMouseDown}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @srcroot/ui - Gray Theme
3
+ * True neutral gray (the default)
4
+ */
5
+
6
+ @layer base {
7
+ :root {
8
+ --background: 0 0% 100%;
9
+ --foreground: 224 71.4% 4.1%;
10
+
11
+ --card: 0 0% 100%;
12
+ --card-foreground: 224 71.4% 4.1%;
13
+
14
+ --popover: 0 0% 100%;
15
+ --popover-foreground: 224 71.4% 4.1%;
16
+
17
+ --primary: 220.9 39.3% 11%;
18
+ --primary-foreground: 210 20% 98%;
19
+
20
+ --secondary: 220 14.3% 95.9%;
21
+ --secondary-foreground: 220.9 39.3% 11%;
22
+
23
+ --muted: 220 14.3% 95.9%;
24
+ --muted-foreground: 220 8.9% 46.1%;
25
+
26
+ --accent: 220 14.3% 95.9%;
27
+ --accent-foreground: 220.9 39.3% 11%;
28
+
29
+ --destructive: 0 84.2% 60.2%;
30
+ --destructive-foreground: 210 20% 98%;
31
+
32
+ --success: 142.1 76.2% 36.3%;
33
+ --success-foreground: 210 20% 98%;
34
+
35
+ --warning: 45.4 93.4% 47.5%;
36
+ --warning-foreground: 220.9 39.3% 11%;
37
+
38
+ --info: 201.3 96.3% 32.2%;
39
+ --info-foreground: 210 20% 98%;
40
+
41
+ --border: 220 13% 91%;
42
+ --input: 220 13% 91%;
43
+ --ring: 224 71.4% 4.1%;
44
+
45
+ --radius: 0.5rem;
46
+ }
47
+
48
+ .dark {
49
+ --background: 224 71.4% 4.1%;
50
+ --foreground: 210 20% 98%;
51
+
52
+ --card: 224 71.4% 4.1%;
53
+ --card-foreground: 210 20% 98%;
54
+
55
+ --popover: 224 71.4% 4.1%;
56
+ --popover-foreground: 210 20% 98%;
57
+
58
+ --primary: 210 20% 98%;
59
+ --primary-foreground: 220.9 39.3% 11%;
60
+
61
+ --secondary: 215 27.9% 16.9%;
62
+ --secondary-foreground: 210 20% 98%;
63
+
64
+ --muted: 215 27.9% 16.9%;
65
+ --muted-foreground: 217.9 10.6% 64.9%;
66
+
67
+ --accent: 215 27.9% 16.9%;
68
+ --accent-foreground: 210 20% 98%;
69
+
70
+ --destructive: 0 62.8% 30.6%;
71
+ --destructive-foreground: 210 20% 98%;
72
+
73
+ --success: 142.1 70.6% 45.3%;
74
+ --success-foreground: 220.9 39.3% 11%;
75
+
76
+ --warning: 48 96.5% 53.1%;
77
+ --warning-foreground: 220.9 39.3% 11%;
78
+
79
+ --info: 199.4 95.5% 53.8%;
80
+ --info-foreground: 220.9 39.3% 11%;
81
+
82
+ --border: 215 27.9% 16.9%;
83
+ --input: 215 27.9% 16.9%;
84
+ --ring: 216 12.2% 83.9%;
85
+ }
86
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @srcroot/ui Theme Index
3
+ *
4
+ * Available themes:
5
+ * - neutral.css - Pure gray, no undertones
6
+ * - stone.css - Warm gray with brown undertones
7
+ * - zinc.css - Cool gray with subtle blue undertones
8
+ * - gray.css - True neutral gray
9
+ * - slate.css - Cool gray with strong blue undertones (default)
10
+ *
11
+ * Usage:
12
+ * Copy the theme file contents into your globals.css or import it:
13
+ *
14
+ * @import '@srcroot/ui/themes/slate.css';
15
+ *
16
+ * Or with the base tokens:
17
+ * @import '@srcroot/ui/design-tokens.css';
18
+ * @import '@srcroot/ui/themes/zinc.css';
19
+ */
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @srcroot/ui - Neutral Theme
3
+ * A balanced gray with no strong undertones
4
+ */
5
+
6
+ @layer base {
7
+ :root {
8
+ --background: 0 0% 100%;
9
+ --foreground: 0 0% 3.9%;
10
+
11
+ --card: 0 0% 100%;
12
+ --card-foreground: 0 0% 3.9%;
13
+
14
+ --popover: 0 0% 100%;
15
+ --popover-foreground: 0 0% 3.9%;
16
+
17
+ --primary: 0 0% 9%;
18
+ --primary-foreground: 0 0% 98%;
19
+
20
+ --secondary: 0 0% 96.1%;
21
+ --secondary-foreground: 0 0% 9%;
22
+
23
+ --muted: 0 0% 96.1%;
24
+ --muted-foreground: 0 0% 45.1%;
25
+
26
+ --accent: 0 0% 96.1%;
27
+ --accent-foreground: 0 0% 9%;
28
+
29
+ --destructive: 0 84.2% 60.2%;
30
+ --destructive-foreground: 0 0% 98%;
31
+
32
+ --success: 142.1 76.2% 36.3%;
33
+ --success-foreground: 0 0% 98%;
34
+
35
+ --warning: 45.4 93.4% 47.5%;
36
+ --warning-foreground: 0 0% 9%;
37
+
38
+ --info: 201.3 96.3% 32.2%;
39
+ --info-foreground: 0 0% 98%;
40
+
41
+ --border: 0 0% 89.8%;
42
+ --input: 0 0% 89.8%;
43
+ --ring: 0 0% 3.9%;
44
+
45
+ --radius: 0.5rem;
46
+ }
47
+
48
+ .dark {
49
+ --background: 0 0% 3.9%;
50
+ --foreground: 0 0% 98%;
51
+
52
+ --card: 0 0% 3.9%;
53
+ --card-foreground: 0 0% 98%;
54
+
55
+ --popover: 0 0% 3.9%;
56
+ --popover-foreground: 0 0% 98%;
57
+
58
+ --primary: 0 0% 98%;
59
+ --primary-foreground: 0 0% 9%;
60
+
61
+ --secondary: 0 0% 14.9%;
62
+ --secondary-foreground: 0 0% 98%;
63
+
64
+ --muted: 0 0% 14.9%;
65
+ --muted-foreground: 0 0% 63.9%;
66
+
67
+ --accent: 0 0% 14.9%;
68
+ --accent-foreground: 0 0% 98%;
69
+
70
+ --destructive: 0 62.8% 30.6%;
71
+ --destructive-foreground: 0 0% 98%;
72
+
73
+ --success: 142.1 70.6% 45.3%;
74
+ --success-foreground: 0 0% 9%;
75
+
76
+ --warning: 48 96.5% 53.1%;
77
+ --warning-foreground: 0 0% 9%;
78
+
79
+ --info: 199.4 95.5% 53.8%;
80
+ --info-foreground: 0 0% 9%;
81
+
82
+ --border: 0 0% 14.9%;
83
+ --input: 0 0% 14.9%;
84
+ --ring: 0 0% 83.1%;
85
+ }
86
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @srcroot/ui - Slate Theme
3
+ * Cool gray with strong blue undertones (shadcn default)
4
+ */
5
+
6
+ @layer base {
7
+ :root {
8
+ --background: 0 0% 100%;
9
+ --foreground: 222.2 84% 4.9%;
10
+
11
+ --card: 0 0% 100%;
12
+ --card-foreground: 222.2 84% 4.9%;
13
+
14
+ --popover: 0 0% 100%;
15
+ --popover-foreground: 222.2 84% 4.9%;
16
+
17
+ --primary: 222.2 47.4% 11.2%;
18
+ --primary-foreground: 210 40% 98%;
19
+
20
+ --secondary: 210 40% 96.1%;
21
+ --secondary-foreground: 222.2 47.4% 11.2%;
22
+
23
+ --muted: 210 40% 96.1%;
24
+ --muted-foreground: 215.4 16.3% 46.9%;
25
+
26
+ --accent: 210 40% 96.1%;
27
+ --accent-foreground: 222.2 47.4% 11.2%;
28
+
29
+ --destructive: 0 84.2% 60.2%;
30
+ --destructive-foreground: 210 40% 98%;
31
+
32
+ --success: 142.1 76.2% 36.3%;
33
+ --success-foreground: 210 40% 98%;
34
+
35
+ --warning: 45.4 93.4% 47.5%;
36
+ --warning-foreground: 222.2 47.4% 11.2%;
37
+
38
+ --info: 201.3 96.3% 32.2%;
39
+ --info-foreground: 210 40% 98%;
40
+
41
+ --border: 214.3 31.8% 91.4%;
42
+ --input: 214.3 31.8% 91.4%;
43
+ --ring: 222.2 84% 4.9%;
44
+
45
+ --radius: 0.5rem;
46
+ }
47
+
48
+ .dark {
49
+ --background: 222.2 84% 4.9%;
50
+ --foreground: 210 40% 98%;
51
+
52
+ --card: 222.2 84% 4.9%;
53
+ --card-foreground: 210 40% 98%;
54
+
55
+ --popover: 222.2 84% 4.9%;
56
+ --popover-foreground: 210 40% 98%;
57
+
58
+ --primary: 210 40% 98%;
59
+ --primary-foreground: 222.2 47.4% 11.2%;
60
+
61
+ --secondary: 217.2 32.6% 17.5%;
62
+ --secondary-foreground: 210 40% 98%;
63
+
64
+ --muted: 217.2 32.6% 17.5%;
65
+ --muted-foreground: 215 20.2% 65.1%;
66
+
67
+ --accent: 217.2 32.6% 17.5%;
68
+ --accent-foreground: 210 40% 98%;
69
+
70
+ --destructive: 0 62.8% 30.6%;
71
+ --destructive-foreground: 210 40% 98%;
72
+
73
+ --success: 142.1 70.6% 45.3%;
74
+ --success-foreground: 222.2 47.4% 11.2%;
75
+
76
+ --warning: 48 96.5% 53.1%;
77
+ --warning-foreground: 222.2 47.4% 11.2%;
78
+
79
+ --info: 199.4 95.5% 53.8%;
80
+ --info-foreground: 222.2 47.4% 11.2%;
81
+
82
+ --border: 217.2 32.6% 17.5%;
83
+ --input: 217.2 32.6% 17.5%;
84
+ --ring: 212.7 26.8% 83.9%;
85
+ }
86
+ }