@studiocms/ui 0.0.1 → 0.3.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 +28 -544
- package/package.json +11 -6
- package/src/components/Button.astro +303 -269
- package/src/components/Card.astro +37 -13
- package/src/components/Center.astro +2 -2
- package/src/components/Checkbox.astro +99 -29
- package/src/components/Divider.astro +15 -8
- package/src/components/Dropdown/Dropdown.astro +102 -41
- package/src/components/Dropdown/dropdown.ts +111 -23
- package/src/components/Footer.astro +137 -0
- package/src/components/Input.astro +42 -14
- package/src/components/Modal/Modal.astro +84 -30
- package/src/components/Modal/modal.ts +43 -9
- package/src/components/RadioGroup.astro +153 -29
- package/src/components/Row.astro +16 -7
- package/src/components/SearchSelect.astro +278 -222
- package/src/components/Select.astro +260 -127
- package/src/components/Sidebar/Double.astro +12 -12
- package/src/components/Sidebar/Single.astro +6 -6
- package/src/components/Sidebar/helpers.ts +53 -7
- package/src/components/Tabs/TabItem.astro +47 -0
- package/src/components/Tabs/Tabs.astro +376 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Textarea.astro +56 -15
- package/src/components/ThemeToggle.astro +14 -8
- package/src/components/Toast/Toaster.astro +171 -31
- package/src/components/Toggle.astro +89 -21
- package/src/components/User.astro +34 -15
- package/src/components/index.ts +24 -22
- package/src/components.ts +2 -0
- package/src/css/colors.css +65 -65
- package/src/css/resets.css +0 -1
- package/src/integration.ts +18 -0
- package/src/layouts/RootLayout.astro +1 -2
- package/src/types/index.ts +1 -1
- package/src/utils/ThemeHelper.ts +135 -117
- package/src/utils/create-resolver.ts +30 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
class ModalHelper {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
private element: HTMLDialogElement;
|
|
3
|
+
private cancelButton: HTMLButtonElement | undefined;
|
|
4
|
+
private confirmButton: HTMLButtonElement | undefined;
|
|
5
5
|
|
|
6
6
|
private isForm = false;
|
|
7
7
|
private modalForm: HTMLFormElement;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* A helper to manage modals.
|
|
11
|
+
* @param id The ID of the modal.
|
|
12
|
+
* @param triggerID The ID of the element that should trigger the modal.
|
|
13
|
+
*/
|
|
9
14
|
constructor(id: string, triggerID?: string) {
|
|
10
15
|
const element = document.getElementById(id) as HTMLDialogElement;
|
|
11
16
|
|
|
@@ -32,27 +37,36 @@ class ModalHelper {
|
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A helper function which adds event listeners to the modal buttons to close the modal when clicked.
|
|
42
|
+
* @param id The ID of the modal.
|
|
43
|
+
* @param dismissable Whether the modal is dismissable.
|
|
44
|
+
*/
|
|
35
45
|
private addButtonListeners = (id: string, dismissable: boolean) => {
|
|
36
|
-
if (
|
|
46
|
+
if (
|
|
47
|
+
dismissable ||
|
|
48
|
+
(!this.element.dataset.hasCancelButton && !this.element.dataset.hasActionButton)
|
|
49
|
+
) {
|
|
37
50
|
const xMarkButton = document.getElementById(`${id}-btn-x`) as HTMLButtonElement;
|
|
38
51
|
xMarkButton.addEventListener('click', this.hide);
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
if (!this.element.dataset.
|
|
54
|
+
if (!!this.element.dataset.hasCancelButton && !this.element.dataset.hasActionButton) return;
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (usedButtons.includes('cancel')) {
|
|
56
|
+
if (this.element.dataset.hasCancelButton) {
|
|
46
57
|
this.cancelButton = document.getElementById(`${id}-btn-cancel`) as HTMLButtonElement;
|
|
47
58
|
this.cancelButton.addEventListener('click', this.hide);
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
if (
|
|
61
|
+
if (this.element.dataset.hasActionButton) {
|
|
51
62
|
this.confirmButton = document.getElementById(`${id}-btn-confirm`) as HTMLButtonElement;
|
|
52
63
|
this.confirmButton.addEventListener('click', this.hide);
|
|
53
64
|
}
|
|
54
65
|
};
|
|
55
66
|
|
|
67
|
+
/**
|
|
68
|
+
* A helper function to close the modal when the user clicks outside of it.
|
|
69
|
+
*/
|
|
56
70
|
private addDismissiveClickListener = () => {
|
|
57
71
|
this.element.addEventListener('click', (e: MouseEvent) => {
|
|
58
72
|
if (!e.target) return;
|
|
@@ -68,14 +82,25 @@ class ModalHelper {
|
|
|
68
82
|
});
|
|
69
83
|
};
|
|
70
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A function to show the modal.
|
|
87
|
+
*/
|
|
71
88
|
public show = () => {
|
|
72
89
|
this.element.showModal();
|
|
90
|
+
this.element.focus();
|
|
73
91
|
};
|
|
74
92
|
|
|
93
|
+
/**
|
|
94
|
+
* A function to hide the modal.
|
|
95
|
+
*/
|
|
75
96
|
public hide = () => {
|
|
76
97
|
this.element.close();
|
|
77
98
|
};
|
|
78
99
|
|
|
100
|
+
/**
|
|
101
|
+
* A function to add another trigger to show the modal with.
|
|
102
|
+
* @param elementID The ID of the element that should trigger the modal when clicked.
|
|
103
|
+
*/
|
|
79
104
|
public bindTrigger = (elementID: string) => {
|
|
80
105
|
const element = document.getElementById(elementID);
|
|
81
106
|
|
|
@@ -86,6 +111,10 @@ class ModalHelper {
|
|
|
86
111
|
element.addEventListener('click', this.show);
|
|
87
112
|
};
|
|
88
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Registers a callback for the cancel button.
|
|
116
|
+
* @param func The callback function.
|
|
117
|
+
*/
|
|
89
118
|
public registerCancelCallback = (func: () => void) => {
|
|
90
119
|
if (!this.cancelButton) {
|
|
91
120
|
throw new Error('Unable to register cancel callback without a cancel button.');
|
|
@@ -99,6 +128,11 @@ class ModalHelper {
|
|
|
99
128
|
});
|
|
100
129
|
};
|
|
101
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Registers a callback for the confirm button.
|
|
133
|
+
* @param func The callback function. If the modal is a form, the function will be called with
|
|
134
|
+
* the form data as the first argument.
|
|
135
|
+
*/
|
|
102
136
|
public registerConfirmCallback = (func: (data?: FormData | undefined) => void) => {
|
|
103
137
|
if (!this.confirmButton) {
|
|
104
138
|
throw new Error('Unable to register cancel callback without a confirmation button.');
|
|
@@ -2,39 +2,81 @@
|
|
|
2
2
|
import type { StudioCMSColorway } from '../utils/colors';
|
|
3
3
|
import { generateID } from '../utils/generateID';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The props for the RadioGroup component.
|
|
7
|
+
*/
|
|
5
8
|
interface Option {
|
|
9
|
+
/**
|
|
10
|
+
* The label of the option.
|
|
11
|
+
*/
|
|
6
12
|
label: string;
|
|
13
|
+
/**
|
|
14
|
+
* The value of the option.
|
|
15
|
+
*/
|
|
7
16
|
value: string;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the option is disabled.
|
|
19
|
+
*/
|
|
8
20
|
disabled?: boolean;
|
|
9
|
-
}
|
|
21
|
+
}
|
|
10
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The props for the RadioGroup component.
|
|
25
|
+
*/
|
|
11
26
|
interface Props {
|
|
27
|
+
/**
|
|
28
|
+
* The label of the radio group.
|
|
29
|
+
*/
|
|
12
30
|
label: string;
|
|
31
|
+
/**
|
|
32
|
+
* The color of the radio group. Defaults to `default`.
|
|
33
|
+
*/
|
|
13
34
|
color?: StudioCMSColorway;
|
|
35
|
+
/**
|
|
36
|
+
* The default value of the radio group. Needs to be one of the values in the options.
|
|
37
|
+
*/
|
|
14
38
|
defaultValue?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The options to display in the radio group.
|
|
41
|
+
*/
|
|
15
42
|
options: Option[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether the radio group is disabled. Defaults to `false`.
|
|
45
|
+
*/
|
|
16
46
|
disabled?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The name of the radio group.
|
|
49
|
+
*/
|
|
17
50
|
name?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the radio group is required. Defaults to `false`.
|
|
53
|
+
*/
|
|
18
54
|
isRequired?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Whether the radio group is horizontal. Defaults to `false`.
|
|
57
|
+
*/
|
|
19
58
|
horizontal?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Additional classes to apply to the radio group.
|
|
61
|
+
*/
|
|
20
62
|
class?: string;
|
|
21
|
-
}
|
|
63
|
+
}
|
|
22
64
|
|
|
23
65
|
const {
|
|
24
66
|
label,
|
|
25
|
-
color,
|
|
67
|
+
color = 'default',
|
|
26
68
|
defaultValue,
|
|
27
69
|
options,
|
|
28
|
-
disabled,
|
|
70
|
+
disabled = false,
|
|
71
|
+
isRequired = false,
|
|
72
|
+
horizontal = false,
|
|
29
73
|
name = generateID('radio'),
|
|
30
|
-
isRequired,
|
|
31
|
-
horizontal,
|
|
32
74
|
class: className,
|
|
33
75
|
} = Astro.props;
|
|
34
76
|
---
|
|
35
77
|
|
|
36
78
|
<div
|
|
37
|
-
class="radio-container"
|
|
79
|
+
class="sui-radio-container"
|
|
38
80
|
class:list={[
|
|
39
81
|
disabled && "disabled",
|
|
40
82
|
horizontal && "horizontal",
|
|
@@ -45,18 +87,24 @@ const {
|
|
|
45
87
|
<span>
|
|
46
88
|
{label} <span class="req-star">{isRequired && "*"}</span>
|
|
47
89
|
</span>
|
|
48
|
-
<div class="radio-inputs">
|
|
49
|
-
{options.map(({ label, value, disabled: individuallyDisabled }) => (
|
|
90
|
+
<div class="sui-radio-inputs" role="radiogroup">
|
|
91
|
+
{options.map(({ label, value, disabled: individuallyDisabled }, i) => (
|
|
50
92
|
<label
|
|
51
93
|
for={value}
|
|
52
|
-
class="radio-label"
|
|
94
|
+
class="sui-radio-label"
|
|
53
95
|
class:list={[ individuallyDisabled && "disabled" ]}
|
|
54
96
|
>
|
|
55
|
-
<div class="radio-box-container">
|
|
56
|
-
<div
|
|
97
|
+
<div class="sui-radio-box-container">
|
|
98
|
+
<div
|
|
99
|
+
class="sui-radio-box"
|
|
100
|
+
role="radio"
|
|
101
|
+
tabindex={i === 0 ? 0 : -1}
|
|
102
|
+
aria-checked={value === defaultValue}
|
|
103
|
+
aria-label={label}
|
|
104
|
+
/>
|
|
57
105
|
</div>
|
|
58
106
|
<input
|
|
59
|
-
class="radio-toggle"
|
|
107
|
+
class="sui-radio-toggle"
|
|
60
108
|
type="radio"
|
|
61
109
|
value={value}
|
|
62
110
|
id={value}
|
|
@@ -70,19 +118,91 @@ const {
|
|
|
70
118
|
))}
|
|
71
119
|
</div>
|
|
72
120
|
</div>
|
|
121
|
+
<script>
|
|
122
|
+
const elements = document.querySelectorAll<HTMLDivElement>('.sui-radio-container');
|
|
123
|
+
|
|
124
|
+
for (const element of elements) {
|
|
125
|
+
if (element.dataset.initialized) continue;
|
|
126
|
+
|
|
127
|
+
element.dataset.initialized = 'true';
|
|
128
|
+
|
|
129
|
+
const radioBoxes = element.querySelectorAll<HTMLDivElement>('.sui-radio-box');
|
|
130
|
+
|
|
131
|
+
let i = 0;
|
|
132
|
+
|
|
133
|
+
for (const radioBox of radioBoxes) {
|
|
134
|
+
radioBox.addEventListener('keydown', (e) => {
|
|
135
|
+
if (e.key === 'Enter' || e.key === " ") {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
|
|
138
|
+
const input = (e.target as HTMLDivElement).parentElement!.parentElement!.querySelector<HTMLInputElement>('.sui-radio-toggle')!;
|
|
139
|
+
|
|
140
|
+
if (input.disabled) return;
|
|
141
|
+
|
|
142
|
+
input.checked = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
|
|
148
|
+
let nextRadioBox: HTMLDivElement | undefined;
|
|
149
|
+
|
|
150
|
+
radioBoxes.forEach((box, index) => {
|
|
151
|
+
if (box === radioBox) nextRadioBox = radioBoxes[index + 1];
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!nextRadioBox) return;
|
|
155
|
+
|
|
156
|
+
radioBox.tabIndex = -1;
|
|
157
|
+
nextRadioBox.tabIndex = 0;
|
|
158
|
+
nextRadioBox.focus();
|
|
159
|
+
nextRadioBox.click();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
|
|
165
|
+
let previousRadioBox: HTMLDivElement | undefined;
|
|
166
|
+
|
|
167
|
+
radioBoxes.forEach((box, index) => {
|
|
168
|
+
if (box === radioBox) previousRadioBox = radioBoxes[index - 1];
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!previousRadioBox) return;
|
|
172
|
+
|
|
173
|
+
radioBox.tabIndex = -1;
|
|
174
|
+
previousRadioBox.tabIndex = 0;
|
|
175
|
+
previousRadioBox.focus();
|
|
176
|
+
previousRadioBox.click();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
element.addEventListener('keydown', (e) => {
|
|
183
|
+
if (e.key !== 'Enter') return;
|
|
184
|
+
|
|
185
|
+
const checkbox = element.querySelector<HTMLInputElement>('.sui-checkbox');
|
|
186
|
+
|
|
187
|
+
if (!checkbox) return;
|
|
188
|
+
|
|
189
|
+
checkbox.click();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
</script>
|
|
73
193
|
<style>
|
|
74
|
-
.radio-container {
|
|
194
|
+
.sui-radio-container {
|
|
75
195
|
display: flex;
|
|
76
196
|
flex-direction: column;
|
|
77
197
|
gap: .5rem;
|
|
78
198
|
}
|
|
79
199
|
|
|
80
|
-
.radio-container.disabled {
|
|
200
|
+
.sui-radio-container.disabled {
|
|
81
201
|
opacity: 0.5;
|
|
82
202
|
color: hsl(var(--text-muted));
|
|
83
203
|
}
|
|
84
204
|
|
|
85
|
-
.radio-label.disabled {
|
|
205
|
+
.sui-radio-label.disabled {
|
|
86
206
|
opacity: 0.5;
|
|
87
207
|
color: hsl(var(--text-muted));
|
|
88
208
|
pointer-events: none;
|
|
@@ -93,17 +213,17 @@ const {
|
|
|
93
213
|
font-weight: 700;
|
|
94
214
|
}
|
|
95
215
|
|
|
96
|
-
.radio-inputs {
|
|
216
|
+
.sui-radio-inputs {
|
|
97
217
|
display: flex;
|
|
98
218
|
flex-direction: column;
|
|
99
219
|
gap: .75rem;
|
|
100
220
|
}
|
|
101
221
|
|
|
102
|
-
.radio-container.horizontal .radio-inputs {
|
|
222
|
+
.sui-radio-container.horizontal .sui-radio-inputs {
|
|
103
223
|
flex-direction: row;
|
|
104
224
|
}
|
|
105
225
|
|
|
106
|
-
.radio-label {
|
|
226
|
+
.sui-radio-label {
|
|
107
227
|
display: flex;
|
|
108
228
|
flex-direction: row;
|
|
109
229
|
gap: .5rem;
|
|
@@ -111,41 +231,41 @@ const {
|
|
|
111
231
|
align-items: center;
|
|
112
232
|
}
|
|
113
233
|
|
|
114
|
-
.radio-label:hover .radio-box {
|
|
234
|
+
.sui-radio-label:hover .sui-radio-box {
|
|
115
235
|
outline-color: hsl(var(--default-hover));
|
|
116
236
|
}
|
|
117
237
|
|
|
118
|
-
.radio-container:not(.disabled) .radio-label:active .radio-box {
|
|
238
|
+
.sui-radio-container:not(.disabled) .sui-radio-label:active .sui-radio-box {
|
|
119
239
|
outline-color: hsl(var(--default-active));
|
|
120
240
|
scale: 0.9;
|
|
121
241
|
}
|
|
122
242
|
|
|
123
|
-
.radio-label:has(.radio-toggle:checked) .radio-box {
|
|
243
|
+
.sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
|
|
124
244
|
background-color: hsl(var(--text-normal));
|
|
125
245
|
outline-color: hsl(var(--text-normal));
|
|
126
246
|
}
|
|
127
247
|
|
|
128
|
-
.radio-container.primary .radio-label:has(.radio-toggle:checked) .radio-box {
|
|
248
|
+
.sui-radio-container.primary .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
|
|
129
249
|
background-color: hsl(var(--primary-base));
|
|
130
250
|
outline-color: hsl(var(--primary-base));
|
|
131
251
|
}
|
|
132
252
|
|
|
133
|
-
.radio-container.success .radio-label:has(.radio-toggle:checked) .radio-box {
|
|
253
|
+
.sui-radio-container.success .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
|
|
134
254
|
background-color: hsl(var(--success-base));
|
|
135
255
|
outline-color: hsl(var(--success-base));
|
|
136
256
|
}
|
|
137
257
|
|
|
138
|
-
.radio-container.warning .radio-label:has(.radio-toggle:checked) .radio-box {
|
|
258
|
+
.sui-radio-container.warning .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
|
|
139
259
|
background-color: hsl(var(--warning-base));
|
|
140
260
|
outline-color: hsl(var(--warning-base));
|
|
141
261
|
}
|
|
142
262
|
|
|
143
|
-
.radio-container.danger .radio-label:has(.radio-toggle:checked) .radio-box {
|
|
263
|
+
.sui-radio-container.danger .sui-radio-label:has(.sui-radio-toggle:checked) .sui-radio-box {
|
|
144
264
|
background-color: hsl(var(--danger-base));
|
|
145
265
|
outline-color: hsl(var(--danger-base));
|
|
146
266
|
}
|
|
147
267
|
|
|
148
|
-
.radio-box-container {
|
|
268
|
+
.sui-radio-box-container {
|
|
149
269
|
width: 20px;
|
|
150
270
|
height: 20px;
|
|
151
271
|
display: flex;
|
|
@@ -155,7 +275,7 @@ const {
|
|
|
155
275
|
cursor: pointer;
|
|
156
276
|
}
|
|
157
277
|
|
|
158
|
-
.radio-box {
|
|
278
|
+
.sui-radio-box {
|
|
159
279
|
width: 12px;
|
|
160
280
|
height: 12px;
|
|
161
281
|
border-radius: 20px;
|
|
@@ -164,7 +284,11 @@ const {
|
|
|
164
284
|
transition: all .15s ease;
|
|
165
285
|
}
|
|
166
286
|
|
|
167
|
-
.radio-
|
|
287
|
+
.sui-radio-box:focus-visible {
|
|
288
|
+
outline-color: hsl(var(--text-normal)) !important;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.sui-radio-toggle {
|
|
168
292
|
width: 0;
|
|
169
293
|
height: 0;
|
|
170
294
|
visibility: hidden;
|
package/src/components/Row.astro
CHANGED
|
@@ -1,38 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The props for the row component.
|
|
6
|
+
*/
|
|
4
7
|
interface Props extends HTMLAttributes<'div'> {
|
|
8
|
+
/**
|
|
9
|
+
* Whether the row should be aligned to the center. Defaults to `false`.
|
|
10
|
+
*/
|
|
5
11
|
alignCenter?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The size of the gap between the children. Defaults to `md`.
|
|
14
|
+
*/
|
|
6
15
|
gapSize?: 'sm' | 'md' | 'lg';
|
|
7
|
-
}
|
|
16
|
+
}
|
|
8
17
|
|
|
9
18
|
const { alignCenter, gapSize = 'md', ...props } = Astro.props;
|
|
10
19
|
---
|
|
11
20
|
|
|
12
|
-
<div class="row" class:list={[alignCenter && "align", gapSize]} {...props}>
|
|
21
|
+
<div class="sui-row" class:list={[alignCenter && "align", gapSize]} {...props}>
|
|
13
22
|
<slot />
|
|
14
23
|
</div>
|
|
15
24
|
<style>
|
|
16
|
-
.row {
|
|
25
|
+
.sui-row {
|
|
17
26
|
display: flex;
|
|
18
27
|
flex-direction: row;
|
|
19
28
|
position: relative;
|
|
20
29
|
flex-wrap: wrap;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
.row.align {
|
|
32
|
+
.sui-row.align {
|
|
24
33
|
align-items: center;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
.row.sm {
|
|
36
|
+
.sui-row.sm {
|
|
28
37
|
gap: .5rem;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
.row.md {
|
|
40
|
+
.sui-row.md {
|
|
32
41
|
gap: 1rem;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
.row.lg {
|
|
44
|
+
.sui-row.lg {
|
|
36
45
|
gap: 2rem;
|
|
37
46
|
}
|
|
38
47
|
</style>
|