@oscarrf2/goo-ds 0.1.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.
- package/README.md +163 -0
- package/package.json +60 -0
- package/src/components/Button/Button.css +107 -0
- package/src/components/Button/Button.tsx +82 -0
- package/src/components/Button/Button.types.ts +62 -0
- package/src/components/Button/index.ts +3 -0
- package/src/components/Cell/Cell.css +64 -0
- package/src/components/Cell/Cell.tsx +42 -0
- package/src/components/Cell/Cell.types.ts +42 -0
- package/src/components/Cell/index.ts +3 -0
- package/src/components/Codeblock/Codeblock.css +90 -0
- package/src/components/Codeblock/Codeblock.tsx +88 -0
- package/src/components/Codeblock/Codeblock.types.ts +42 -0
- package/src/components/Codeblock/index.ts +3 -0
- package/src/components/CoreText/CoreText.tsx +43 -0
- package/src/components/CoreText/CoreText.types.ts +56 -0
- package/src/components/CoreText/index.ts +2 -0
- package/src/components/Divider/Divider.css +38 -0
- package/src/components/Divider/Divider.tsx +35 -0
- package/src/components/Divider/Divider.types.ts +19 -0
- package/src/components/Divider/index.ts +3 -0
- package/src/components/InputImage/InputImage.css +212 -0
- package/src/components/InputImage/InputImage.tsx +314 -0
- package/src/components/InputImage/InputImage.types.ts +86 -0
- package/src/components/InputImage/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.css +35 -0
- package/src/components/Sidebar/Sidebar.tsx +42 -0
- package/src/components/Sidebar/Sidebar.types.ts +24 -0
- package/src/components/Sidebar/index.ts +3 -0
- package/src/components/SidebarItem/SidebarItem.css +70 -0
- package/src/components/SidebarItem/SidebarItem.tsx +55 -0
- package/src/components/SidebarItem/SidebarItem.types.ts +39 -0
- package/src/components/SidebarItem/index.ts +3 -0
- package/src/components/Skeleton/Skeleton.css +25 -0
- package/src/components/Skeleton/Skeleton.tsx +41 -0
- package/src/components/Skeleton/Skeleton.types.ts +65 -0
- package/src/components/Skeleton/index.ts +5 -0
- package/src/components/Spacer/Spacer.tsx +31 -0
- package/src/components/Spacer/Spacer.types.ts +58 -0
- package/src/components/Spacer/index.ts +3 -0
- package/src/components/TabItem/TabItem.css +67 -0
- package/src/components/TabItem/TabItem.tsx +45 -0
- package/src/components/TabItem/TabItem.types.ts +35 -0
- package/src/components/TabItem/index.ts +3 -0
- package/src/components/Table/Table.css +16 -0
- package/src/components/Table/Table.tsx +39 -0
- package/src/components/Table/Table.types.ts +18 -0
- package/src/components/Table/index.ts +3 -0
- package/src/components/TableRow/TableRow.css +53 -0
- package/src/components/TableRow/TableRow.tsx +53 -0
- package/src/components/TableRow/TableRow.types.ts +41 -0
- package/src/components/TableRow/index.ts +3 -0
- package/src/components/Tabs/Tabs.css +11 -0
- package/src/components/Tabs/Tabs.tsx +37 -0
- package/src/components/Tabs/Tabs.types.ts +18 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/index.ts +15 -0
- package/src/compositions/index.ts +3 -0
- package/src/index.css +68 -0
- package/src/index.ts +4 -0
- package/src/styles/component-tokens.css +270 -0
- package/src/styles/component-tokens.current.css +3 -0
- package/src/styles/fonts.css +11 -0
- package/src/styles/global-tokens.css +257 -0
- package/src/styles/index.css +20 -0
- package/src/styles/number-tokens.css +72 -0
- package/src/styles/semantic-tokens.css +84 -0
- package/src/styles/style-tokens.css +219 -0
- package/src/styles/typography-tokens.css +50 -0
- package/src/styles.css +2 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { InputImageProps } from "./InputImage.types";
|
|
2
|
+
import { Button } from "../Button/Button";
|
|
3
|
+
import { CoreText } from "../CoreText/CoreText";
|
|
4
|
+
import "./InputImage.css";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* InputImage - Image upload input component with drag-and-drop support
|
|
8
|
+
*
|
|
9
|
+
* Provides a file upload interface with multiple visual states including:
|
|
10
|
+
* - Default: Empty state with drag-and-drop zone
|
|
11
|
+
* - Hover: Visual feedback during drag-over
|
|
12
|
+
* - Filled: Shows uploaded image with remove button
|
|
13
|
+
* - Loading: Loading skeleton state
|
|
14
|
+
* - Generating: Blurred preview during processing
|
|
15
|
+
* - Generated: Final result with optional comparison slider
|
|
16
|
+
* - Disabled: Non-interactive state
|
|
17
|
+
*
|
|
18
|
+
* Token-first per Constitution (no hardcoded values).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <InputImage state="Default" onUploadClick={handleUpload} />
|
|
23
|
+
* <InputImage state="Filled" imageSrc="/path/to/image.jpg" onRemoveClick={handleRemove} />
|
|
24
|
+
* <InputImage state="Generated" imageSrc="/path/to/image.jpg" comparisonSlider={true} />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function InputImage({
|
|
28
|
+
state = "Default",
|
|
29
|
+
comparisonSlider = true,
|
|
30
|
+
imageSrc,
|
|
31
|
+
imageAlt = "",
|
|
32
|
+
onUploadClick,
|
|
33
|
+
onRemoveClick,
|
|
34
|
+
onDragEnter,
|
|
35
|
+
onDragLeave,
|
|
36
|
+
onDrop,
|
|
37
|
+
className = "",
|
|
38
|
+
children,
|
|
39
|
+
sliderPosition = 50,
|
|
40
|
+
// onSliderDrag not yet implemented
|
|
41
|
+
}: InputImageProps) {
|
|
42
|
+
// Default state
|
|
43
|
+
if (state === "Default") {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={`gds-input-image gds-input-image-default ${className}`.trim()}
|
|
47
|
+
onDragEnter={onDragEnter}
|
|
48
|
+
onDragLeave={onDragLeave}
|
|
49
|
+
onDrop={onDrop}
|
|
50
|
+
data-component="InputImage"
|
|
51
|
+
data-state={state}
|
|
52
|
+
>
|
|
53
|
+
{children || (
|
|
54
|
+
<>
|
|
55
|
+
<CoreText
|
|
56
|
+
variant="label"
|
|
57
|
+
size="lg"
|
|
58
|
+
weight="medium"
|
|
59
|
+
className="gds-input-image-label"
|
|
60
|
+
>
|
|
61
|
+
Drag and drop
|
|
62
|
+
</CoreText>
|
|
63
|
+
<CoreText
|
|
64
|
+
variant="body"
|
|
65
|
+
size="sm"
|
|
66
|
+
weight="regular"
|
|
67
|
+
className="gds-input-image-separator"
|
|
68
|
+
>
|
|
69
|
+
or
|
|
70
|
+
</CoreText>
|
|
71
|
+
<Button
|
|
72
|
+
variant="primary"
|
|
73
|
+
size="small"
|
|
74
|
+
onClick={onUploadClick}
|
|
75
|
+
className="gds-input-image-button"
|
|
76
|
+
>
|
|
77
|
+
Click to upload
|
|
78
|
+
</Button>
|
|
79
|
+
</>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Hover state
|
|
86
|
+
if (state === "Hover") {
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
className={`gds-input-image gds-input-image-hover ${className}`.trim()}
|
|
90
|
+
onDragEnter={onDragEnter}
|
|
91
|
+
onDragLeave={onDragLeave}
|
|
92
|
+
onDrop={onDrop}
|
|
93
|
+
data-component="InputImage"
|
|
94
|
+
data-state={state}
|
|
95
|
+
>
|
|
96
|
+
{children || (
|
|
97
|
+
<>
|
|
98
|
+
<CoreText
|
|
99
|
+
variant="label"
|
|
100
|
+
size="lg"
|
|
101
|
+
weight="medium"
|
|
102
|
+
className="gds-input-image-label"
|
|
103
|
+
>
|
|
104
|
+
Drag and drop
|
|
105
|
+
</CoreText>
|
|
106
|
+
<CoreText
|
|
107
|
+
variant="body"
|
|
108
|
+
size="sm"
|
|
109
|
+
weight="regular"
|
|
110
|
+
className="gds-input-image-separator"
|
|
111
|
+
>
|
|
112
|
+
or
|
|
113
|
+
</CoreText>
|
|
114
|
+
<Button
|
|
115
|
+
variant="primary"
|
|
116
|
+
size="small"
|
|
117
|
+
onClick={onUploadClick}
|
|
118
|
+
className="gds-input-image-button"
|
|
119
|
+
>
|
|
120
|
+
Click to upload
|
|
121
|
+
</Button>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Filled state
|
|
129
|
+
if (state === "Filled") {
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
className={`gds-input-image gds-input-image-filled ${className}`.trim()}
|
|
133
|
+
data-component="InputImage"
|
|
134
|
+
data-state={state}
|
|
135
|
+
>
|
|
136
|
+
{imageSrc && (
|
|
137
|
+
<img
|
|
138
|
+
src={imageSrc}
|
|
139
|
+
alt={imageAlt}
|
|
140
|
+
className="gds-input-image-preview"
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
<Button
|
|
144
|
+
variant="primary"
|
|
145
|
+
size="small"
|
|
146
|
+
onClick={onRemoveClick}
|
|
147
|
+
className="gds-input-image-remove-button"
|
|
148
|
+
iconLeft={
|
|
149
|
+
<svg
|
|
150
|
+
width="16"
|
|
151
|
+
height="16"
|
|
152
|
+
viewBox="0 0 16 16"
|
|
153
|
+
fill="none"
|
|
154
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
155
|
+
aria-hidden="true"
|
|
156
|
+
>
|
|
157
|
+
<path
|
|
158
|
+
d="M12 4L4 12M4 4L12 12"
|
|
159
|
+
stroke="currentColor"
|
|
160
|
+
strokeWidth="2"
|
|
161
|
+
strokeLinecap="round"
|
|
162
|
+
strokeLinejoin="round"
|
|
163
|
+
/>
|
|
164
|
+
</svg>
|
|
165
|
+
}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Loading state
|
|
172
|
+
if (state === "Loading") {
|
|
173
|
+
return (
|
|
174
|
+
<div
|
|
175
|
+
className={`gds-input-image gds-input-image-loading ${className}`.trim()}
|
|
176
|
+
data-component="InputImage"
|
|
177
|
+
data-state={state}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Generating state
|
|
183
|
+
if (state === "Generating") {
|
|
184
|
+
return (
|
|
185
|
+
<div
|
|
186
|
+
className={`gds-input-image gds-input-image-generating ${className}`.trim()}
|
|
187
|
+
data-component="InputImage"
|
|
188
|
+
data-state={state}
|
|
189
|
+
>
|
|
190
|
+
<div className="gds-input-image-blur-container">
|
|
191
|
+
{imageSrc && (
|
|
192
|
+
<img
|
|
193
|
+
src={imageSrc}
|
|
194
|
+
alt={imageAlt}
|
|
195
|
+
className="gds-input-image-blur-preview"
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Generated state
|
|
204
|
+
if (state === "Generated") {
|
|
205
|
+
return (
|
|
206
|
+
<div
|
|
207
|
+
className={`gds-input-image gds-input-image-generated ${className}`.trim()}
|
|
208
|
+
data-component="InputImage"
|
|
209
|
+
data-state={state}
|
|
210
|
+
>
|
|
211
|
+
{imageSrc && (
|
|
212
|
+
<img
|
|
213
|
+
src={imageSrc}
|
|
214
|
+
alt={imageAlt}
|
|
215
|
+
className="gds-input-image-result"
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
{comparisonSlider && (
|
|
219
|
+
<div
|
|
220
|
+
className="gds-input-image-comparison-slider"
|
|
221
|
+
style={{
|
|
222
|
+
left: `${sliderPosition}%`,
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<div className="gds-input-image-slider-line" />
|
|
226
|
+
<Button
|
|
227
|
+
variant="secondary"
|
|
228
|
+
size="small"
|
|
229
|
+
className="gds-input-image-slider-button"
|
|
230
|
+
iconLeft={
|
|
231
|
+
<svg
|
|
232
|
+
width="16"
|
|
233
|
+
height="16"
|
|
234
|
+
viewBox="0 0 16 16"
|
|
235
|
+
fill="none"
|
|
236
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
237
|
+
aria-hidden="true"
|
|
238
|
+
>
|
|
239
|
+
<path
|
|
240
|
+
d="M6 12L2 8L6 4"
|
|
241
|
+
stroke="currentColor"
|
|
242
|
+
strokeWidth="2"
|
|
243
|
+
strokeLinecap="round"
|
|
244
|
+
strokeLinejoin="round"
|
|
245
|
+
/>
|
|
246
|
+
<path
|
|
247
|
+
d="M10 4L14 8L10 12"
|
|
248
|
+
stroke="currentColor"
|
|
249
|
+
strokeWidth="2"
|
|
250
|
+
strokeLinecap="round"
|
|
251
|
+
strokeLinejoin="round"
|
|
252
|
+
/>
|
|
253
|
+
</svg>
|
|
254
|
+
}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Disabled state
|
|
263
|
+
if (state === "Disabled") {
|
|
264
|
+
return (
|
|
265
|
+
<div
|
|
266
|
+
className={`gds-input-image gds-input-image-disabled ${className}`.trim()}
|
|
267
|
+
data-component="InputImage"
|
|
268
|
+
data-state={state}
|
|
269
|
+
>
|
|
270
|
+
{children || (
|
|
271
|
+
<>
|
|
272
|
+
<CoreText
|
|
273
|
+
variant="label"
|
|
274
|
+
size="lg"
|
|
275
|
+
weight="medium"
|
|
276
|
+
className="gds-input-image-label"
|
|
277
|
+
>
|
|
278
|
+
Drag and drop
|
|
279
|
+
</CoreText>
|
|
280
|
+
<CoreText
|
|
281
|
+
variant="body"
|
|
282
|
+
size="sm"
|
|
283
|
+
weight="regular"
|
|
284
|
+
className="gds-input-image-separator"
|
|
285
|
+
>
|
|
286
|
+
or
|
|
287
|
+
</CoreText>
|
|
288
|
+
<Button
|
|
289
|
+
variant="primary"
|
|
290
|
+
size="small"
|
|
291
|
+
disabled
|
|
292
|
+
className="gds-input-image-button"
|
|
293
|
+
>
|
|
294
|
+
Click to upload
|
|
295
|
+
</Button>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fallback to default
|
|
303
|
+
return (
|
|
304
|
+
<div
|
|
305
|
+
className={`gds-input-image gds-input-image-default ${className}`.trim()}
|
|
306
|
+
data-component="InputImage"
|
|
307
|
+
data-state="Default"
|
|
308
|
+
>
|
|
309
|
+
{children}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export default InputImage;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* InputImage component props
|
|
5
|
+
* Provides image upload input with drag-and-drop, multiple states, and comparison slider
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type InputImageState =
|
|
9
|
+
| "Default"
|
|
10
|
+
| "Hover"
|
|
11
|
+
| "Filled"
|
|
12
|
+
| "Generating"
|
|
13
|
+
| "Generated"
|
|
14
|
+
| "Loading"
|
|
15
|
+
| "Disabled";
|
|
16
|
+
|
|
17
|
+
export interface InputImageProps {
|
|
18
|
+
/**
|
|
19
|
+
* The current state of the input
|
|
20
|
+
* @default "Default"
|
|
21
|
+
*/
|
|
22
|
+
state?: InputImageState;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether to show the comparison slider in Generated state
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
comparisonSlider?: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Position of the comparison slider (0-100 percentage)
|
|
32
|
+
* @default 50
|
|
33
|
+
*/
|
|
34
|
+
sliderPosition?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Callback when slider is being dragged
|
|
38
|
+
*/
|
|
39
|
+
onSliderDrag?: (position: number) => void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The image source URL for Filled, Generating, and Generated states
|
|
43
|
+
*/
|
|
44
|
+
imageSrc?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Alt text for the image
|
|
48
|
+
* @default ""
|
|
49
|
+
*/
|
|
50
|
+
imageAlt?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Callback when the upload button is clicked
|
|
54
|
+
*/
|
|
55
|
+
onUploadClick?: () => void;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Callback when the remove/close button is clicked (in Filled state)
|
|
59
|
+
*/
|
|
60
|
+
onRemoveClick?: () => void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Callback when drag enter occurs
|
|
64
|
+
*/
|
|
65
|
+
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Callback when drag leave occurs
|
|
69
|
+
*/
|
|
70
|
+
onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Callback when drop occurs
|
|
74
|
+
*/
|
|
75
|
+
onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Additional CSS class names
|
|
79
|
+
*/
|
|
80
|
+
className?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Custom children to replace default content (advanced use case)
|
|
84
|
+
*/
|
|
85
|
+
children?: ReactNode;
|
|
86
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar component styles
|
|
3
|
+
* Token-driven sidebar container styling for the goo-ds design system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
.gds-sidebar {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: stretch;
|
|
9
|
+
position: relative;
|
|
10
|
+
background-color: var(--sidebar-background-default);
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Sidebar content area */
|
|
15
|
+
.gds-sidebar-content {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
align-items: flex-start;
|
|
19
|
+
min-width: var(--sidebar-medium-minWidth);
|
|
20
|
+
width: var(--sidebar-medium-minWidth);
|
|
21
|
+
padding-top: var(--sidebar-medium-paddingTop);
|
|
22
|
+
padding-bottom: var(--sidebar-medium-paddingBottom);
|
|
23
|
+
padding-left: var(--sidebar-medium-paddingHorizontal);
|
|
24
|
+
padding-right: var(--sidebar-medium-paddingHorizontal);
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Vertical divider on the right */
|
|
30
|
+
.gds-sidebar-divider {
|
|
31
|
+
width: 1px;
|
|
32
|
+
align-self: stretch;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
background-color: var(--divider-background-default);
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SidebarProps } from "./Sidebar.types";
|
|
2
|
+
import "./Sidebar.css";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sidebar - Sidebar container component for the goo-ds design system
|
|
6
|
+
*
|
|
7
|
+
* Provides consistent sidebar layout with proper spacing and background.
|
|
8
|
+
* Designed to contain SidebarItem components, titles, and other navigation elements.
|
|
9
|
+
* Token-first per Constitution (no hardcoded values).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Sidebar>
|
|
14
|
+
* <CoreText style="headline-xs-bold">Navigation</CoreText>
|
|
15
|
+
* <Spacer size="16" />
|
|
16
|
+
* <SidebarItem icon={<HomeIcon />}>Home</SidebarItem>
|
|
17
|
+
* <SidebarItem icon={<SettingsIcon />}>Settings</SidebarItem>
|
|
18
|
+
* </Sidebar>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function Sidebar({
|
|
22
|
+
children,
|
|
23
|
+
showDivider = true,
|
|
24
|
+
className = "",
|
|
25
|
+
...props
|
|
26
|
+
}: SidebarProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={`gds-sidebar ${className}`.trim()}
|
|
30
|
+
data-component="Sidebar"
|
|
31
|
+
data-show-divider={showDivider}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
<div className="gds-sidebar-content">
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
{showDivider && <div className="gds-sidebar-divider" aria-hidden="true" />}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default Sidebar;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sidebar component props
|
|
5
|
+
* Provides sidebar container using design system tokens
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
/**
|
|
10
|
+
* Sidebar content (typically title, SidebarItem components, etc.)
|
|
11
|
+
*/
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether to show the divider on the right edge
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
showDivider?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Additional CSS class names
|
|
22
|
+
*/
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SidebarItem component styles
|
|
3
|
+
* Token-driven sidebar navigation item styling for the goo-ds design system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
.gds-sidebar-item {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: var(--size-8);
|
|
10
|
+
padding: var(--sidebar-item-medium-paddingVertical) var(--sidebar-item-medium-paddingHorizontal);
|
|
11
|
+
border-radius: var(--sidebar-item-medium-radius);
|
|
12
|
+
font-family: var(--coreText-family-primary);
|
|
13
|
+
font-size: var(--font-size-sm);
|
|
14
|
+
font-weight: var(--font-weight-regular);
|
|
15
|
+
line-height: var(--font-lineHeight-sm);
|
|
16
|
+
color: var(--sidebar-item-text-default);
|
|
17
|
+
transition: all 0.2s ease;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
position: relative;
|
|
20
|
+
width: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Interactive items */
|
|
24
|
+
.gds-sidebar-item-interactive {
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
user-select: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.gds-sidebar-item-interactive:hover:not(.gds-sidebar-item-active) {
|
|
30
|
+
background-color: var(--sidebar-item-background-hover);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.gds-sidebar-item-interactive:active:not(.gds-sidebar-item-active) {
|
|
34
|
+
background-color: var(--sidebar-item-background-pressed);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.gds-sidebar-item-interactive:focus-visible {
|
|
38
|
+
outline: 2px solid var(--color-stroke-focus);
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Active state */
|
|
43
|
+
.gds-sidebar-item-active {
|
|
44
|
+
background-color: var(--sidebar-item-background-active);
|
|
45
|
+
color: var(--sidebar-item-text-active);
|
|
46
|
+
font-weight: var(--font-weight-medium);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Icon styling */
|
|
50
|
+
.gds-sidebar-item-icon {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Text styling */
|
|
58
|
+
.gds-sidebar-item-text {
|
|
59
|
+
flex: 1;
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
text-overflow: ellipsis;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Respect user's motion preferences */
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
.gds-sidebar-item {
|
|
68
|
+
transition: none;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { SidebarItemProps } from "./SidebarItem.types";
|
|
2
|
+
import "./SidebarItem.css";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SidebarItem - Sidebar navigation item component for the goo-ds design system
|
|
6
|
+
*
|
|
7
|
+
* Provides consistent sidebar navigation item with interactive states.
|
|
8
|
+
* Supports icons and active state indication.
|
|
9
|
+
* Token-first per Constitution (no hardcoded values).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <SidebarItem>Dashboard</SidebarItem>
|
|
14
|
+
* <SidebarItem active icon={<HomeIcon />}>Home</SidebarItem>
|
|
15
|
+
* <SidebarItem onClick={() => navigate('/settings')}>Settings</SidebarItem>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function SidebarItem({
|
|
19
|
+
active = false,
|
|
20
|
+
icon,
|
|
21
|
+
children,
|
|
22
|
+
className = "",
|
|
23
|
+
onClick,
|
|
24
|
+
href,
|
|
25
|
+
...props
|
|
26
|
+
}: SidebarItemProps) {
|
|
27
|
+
const isInteractive = Boolean(onClick || href);
|
|
28
|
+
|
|
29
|
+
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
30
|
+
if (onClick) {
|
|
31
|
+
onClick(event);
|
|
32
|
+
}
|
|
33
|
+
if (href && !onClick) {
|
|
34
|
+
window.location.href = href;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className={`gds-sidebar-item ${active ? "gds-sidebar-item-active" : ""} ${isInteractive ? "gds-sidebar-item-interactive" : ""} ${className}`.trim()}
|
|
41
|
+
onClick={isInteractive ? handleClick : undefined}
|
|
42
|
+
role={isInteractive ? "button" : undefined}
|
|
43
|
+
tabIndex={isInteractive ? 0 : undefined}
|
|
44
|
+
aria-current={active ? "page" : undefined}
|
|
45
|
+
data-component="SidebarItem"
|
|
46
|
+
data-active={active}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{icon && <span className="gds-sidebar-item-icon">{icon}</span>}
|
|
50
|
+
<span className="gds-sidebar-item-text">{children}</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default SidebarItem;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SidebarItem component props
|
|
5
|
+
* Provides sidebar navigation items using design system tokens
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SidebarItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
/**
|
|
10
|
+
* Whether the item is currently active/selected
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
13
|
+
active?: boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Icon to display before the text
|
|
17
|
+
*/
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Item content (text or other elements)
|
|
22
|
+
*/
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Additional CSS class names
|
|
27
|
+
*/
|
|
28
|
+
className?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Click handler for navigation
|
|
32
|
+
*/
|
|
33
|
+
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* href for link behavior (optional)
|
|
37
|
+
*/
|
|
38
|
+
href?: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeleton pulse animation
|
|
3
|
+
* Provides a smooth opacity pulse to indicate loading state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
@keyframes skeleton-pulse {
|
|
7
|
+
0%,
|
|
8
|
+
100% {
|
|
9
|
+
opacity: 1;
|
|
10
|
+
}
|
|
11
|
+
50% {
|
|
12
|
+
opacity: 0.5;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.skeleton-pulse {
|
|
17
|
+
animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Respect user's motion preferences */
|
|
21
|
+
@media (prefers-reduced-motion: reduce) {
|
|
22
|
+
.skeleton-pulse {
|
|
23
|
+
animation: none;
|
|
24
|
+
}
|
|
25
|
+
}
|