@aicut/core 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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/index.cjs +3344 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +640 -0
- package/dist/index.d.ts +640 -0
- package/dist/index.js +3331 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/styles/theme.css +414 -0
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aicut/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic core for the AiCut video editor — data model, editor instance, playback engine, vanilla DOM renderer.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"./styles/*.css"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs"
|
|
18
|
+
},
|
|
19
|
+
"./styles.css": "./styles/theme.css"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"styles",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.3.5",
|
|
28
|
+
"typescript": "^5.7.2"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/styles/theme.css
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/* AiCut default theme + structural styles.
|
|
2
|
+
*
|
|
3
|
+
* Theme variables intentionally share names with iqvise's globals.css
|
|
4
|
+
* (--color-brand, --color-secondary, --color-card, --color-surface,
|
|
5
|
+
* --color-dark, --color-muted, etc.). Two consequences:
|
|
6
|
+
* 1. Hosting pages that already define those variables (e.g. via
|
|
7
|
+
* Tailwind v4 @theme, a global :root block, or a `.dark` class
|
|
8
|
+
* toggle) get the editor styled in their palette for free.
|
|
9
|
+
* 2. We use `var(--color-foo, <fallback>)` everywhere so the
|
|
10
|
+
* library still works standalone — the fallback IS the default
|
|
11
|
+
* brand palette.
|
|
12
|
+
*
|
|
13
|
+
* Editor-chrome-specific variables (`--aicut-controls-*`) keep the
|
|
14
|
+
* `aicut-` prefix because they have no analogue in the host theme.
|
|
15
|
+
* They derive from `--color-*` by default so they auto-follow
|
|
16
|
+
* light/dark when the host flips its palette. */
|
|
17
|
+
|
|
18
|
+
.aicut-root {
|
|
19
|
+
/* Brand tokens — same names as iqvise's globals.css. */
|
|
20
|
+
--color-brand: var(--color-brand, #ff3386);
|
|
21
|
+
--color-secondary: var(--color-secondary, #9a31f4);
|
|
22
|
+
--color-surface: var(--color-surface, #f8f7fa);
|
|
23
|
+
--color-dark: var(--color-dark, #1a1a1a);
|
|
24
|
+
--color-muted: var(--color-muted, #999999);
|
|
25
|
+
--color-card: var(--color-card, #ffffff);
|
|
26
|
+
--color-success: var(--color-success, #00d95e);
|
|
27
|
+
--color-warning: var(--color-warning, #faa700);
|
|
28
|
+
--color-info: var(--color-info, #1077ff);
|
|
29
|
+
--color-error: var(--color-error, #ff0909);
|
|
30
|
+
|
|
31
|
+
/* Editor chrome — derived from the brand palette. Defaults pick a
|
|
32
|
+
pro-NLE charcoal so the editor reads as a tool regardless of the
|
|
33
|
+
surrounding page; hosts that explicitly want it to follow their
|
|
34
|
+
light-mode card colour can pass a `theme` prop or set the
|
|
35
|
+
`--aicut-controls-*` vars themselves. */
|
|
36
|
+
--aicut-controls-bg: var(--aicut-controls-bg, #1f1f22);
|
|
37
|
+
--aicut-controls-border: var(--aicut-controls-border, rgba(255, 255, 255, 0.08));
|
|
38
|
+
--aicut-controls-text: var(--aicut-controls-text, rgba(255, 255, 255, 0.85));
|
|
39
|
+
--aicut-controls-hover: var(--aicut-controls-hover, rgba(255, 255, 255, 0.08));
|
|
40
|
+
--aicut-controls-active: var(--aicut-controls-active, rgba(255, 255, 255, 0.12));
|
|
41
|
+
|
|
42
|
+
--aicut-radius-sm: var(--aicut-radius-sm, 8px);
|
|
43
|
+
--aicut-radius-md: var(--aicut-radius-md, 12px);
|
|
44
|
+
--aicut-radius-lg: var(--aicut-radius-lg, 16px);
|
|
45
|
+
|
|
46
|
+
--aicut-clip-bg: linear-gradient(
|
|
47
|
+
180deg,
|
|
48
|
+
color-mix(in srgb, var(--color-brand) 80%, transparent),
|
|
49
|
+
color-mix(in srgb, var(--color-secondary) 70%, transparent)
|
|
50
|
+
);
|
|
51
|
+
--aicut-track-bg: color-mix(in srgb, var(--aicut-controls-text) 6%, transparent);
|
|
52
|
+
--aicut-playhead-color: var(--color-brand);
|
|
53
|
+
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-rows: 1fr auto auto;
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 100%;
|
|
58
|
+
min-height: 480px;
|
|
59
|
+
background: var(--aicut-controls-bg);
|
|
60
|
+
color: var(--aicut-controls-text);
|
|
61
|
+
font-family:
|
|
62
|
+
system-ui,
|
|
63
|
+
-apple-system,
|
|
64
|
+
"Segoe UI",
|
|
65
|
+
Roboto,
|
|
66
|
+
"PingFang SC",
|
|
67
|
+
"Microsoft YaHei",
|
|
68
|
+
sans-serif;
|
|
69
|
+
outline: none;
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.aicut-root *,
|
|
75
|
+
.aicut-root *::before,
|
|
76
|
+
.aicut-root *::after {
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ===== Preview ===== */
|
|
81
|
+
|
|
82
|
+
.aicut-preview-host {
|
|
83
|
+
position: relative;
|
|
84
|
+
/* Letterbox color around the video. Defaults to black (the "film
|
|
85
|
+
editor" convention — videos look cleanest on neutral dark), but
|
|
86
|
+
hosts can override via `theme.previewBg` to follow their UI
|
|
87
|
+
surface in light themes. */
|
|
88
|
+
background: var(--aicut-preview-bg, #000);
|
|
89
|
+
border-radius: 0;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
min-height: 220px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.aicut-fullscreen-exit {
|
|
95
|
+
position: absolute;
|
|
96
|
+
top: 16px;
|
|
97
|
+
right: 16px;
|
|
98
|
+
display: none;
|
|
99
|
+
padding: 6px 12px;
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
color: #fff;
|
|
102
|
+
background: rgba(0, 0, 0, 0.55);
|
|
103
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
backdrop-filter: blur(8px);
|
|
107
|
+
z-index: 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.aicut-fullscreen-exit:hover {
|
|
111
|
+
background: rgba(0, 0, 0, 0.7);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ===== In-tab fullscreen ===== */
|
|
115
|
+
|
|
116
|
+
.aicut-root.aicut-fullscreen {
|
|
117
|
+
/* The root itself stays in flow so its parent layout doesn't jump;
|
|
118
|
+
we lift just the preview to viewport-cover. */
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.aicut-root.aicut-fullscreen .aicut-preview-host {
|
|
122
|
+
position: fixed;
|
|
123
|
+
inset: 0;
|
|
124
|
+
z-index: 9999;
|
|
125
|
+
min-height: 0;
|
|
126
|
+
background: #000;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.aicut-root.aicut-fullscreen .aicut-fullscreen-exit {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ===== Toolbar ===== */
|
|
134
|
+
|
|
135
|
+
.aicut-toolbar {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 12px;
|
|
139
|
+
height: 44px;
|
|
140
|
+
padding: 0 12px;
|
|
141
|
+
border-top: 1px solid var(--aicut-controls-border);
|
|
142
|
+
border-bottom: 1px solid var(--aicut-controls-border);
|
|
143
|
+
background: var(--aicut-controls-bg);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.aicut-toolbar-left,
|
|
147
|
+
.aicut-toolbar-right {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: 8px;
|
|
151
|
+
flex: 1 1 0;
|
|
152
|
+
min-width: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.aicut-toolbar-right {
|
|
156
|
+
justify-content: flex-end;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.aicut-toolbar-center {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 10px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Host-supplied bookend slots — empty until populated. The library
|
|
166
|
+
paints nothing into either; we only reserve the layout box and add
|
|
167
|
+
a subtle separator once the host puts something in. */
|
|
168
|
+
.aicut-toolbar-extras {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
gap: 8px;
|
|
172
|
+
flex: 0 0 auto;
|
|
173
|
+
min-width: 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.aicut-toolbar-extras-left:not(:empty) {
|
|
177
|
+
padding-right: 8px;
|
|
178
|
+
margin-right: 4px;
|
|
179
|
+
border-right: 1px solid var(--aicut-controls-border);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.aicut-toolbar-extras-right:not(:empty) {
|
|
183
|
+
padding-left: 8px;
|
|
184
|
+
margin-left: 4px;
|
|
185
|
+
border-left: 1px solid var(--aicut-controls-border);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.aicut-icon-btn {
|
|
189
|
+
display: inline-flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
justify-content: center;
|
|
192
|
+
width: 32px;
|
|
193
|
+
height: 32px;
|
|
194
|
+
padding: 0;
|
|
195
|
+
border-radius: 8px;
|
|
196
|
+
border: none;
|
|
197
|
+
background: transparent;
|
|
198
|
+
color: var(--aicut-controls-text);
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
transition: background-color 120ms ease, color 120ms ease;
|
|
201
|
+
/* Without this, the flex parent will horizontally squash the
|
|
202
|
+
icon-btns when the row gets crowded (e.g. both extras slots
|
|
203
|
+
populated). Keeping shrink at 0 means the zoom slider takes
|
|
204
|
+
the pressure instead, which is the right call — it has
|
|
205
|
+
legitimate flex range; icons are atomic. */
|
|
206
|
+
flex-shrink: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.aicut-icon-btn:hover:not(:disabled) {
|
|
210
|
+
background: var(--aicut-controls-hover);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.aicut-icon-btn:disabled {
|
|
214
|
+
opacity: 0.35;
|
|
215
|
+
cursor: not-allowed;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.aicut-icon-btn.aicut-toggle-on {
|
|
219
|
+
background: var(--aicut-controls-active);
|
|
220
|
+
color: var(--color-brand);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Toggle-on hover keeps the brand-state visible — without this, the
|
|
224
|
+
generic icon-btn :hover rule above overrides toggle-on's background
|
|
225
|
+
back to controls-hover, which reads as the toggle *dimming* on
|
|
226
|
+
hover (opposite direction from the non-toggled trim/zoom buttons,
|
|
227
|
+
which lighten on hover). Brand-tinted bg makes both feel
|
|
228
|
+
consistently "lights up on hover". */
|
|
229
|
+
.aicut-icon-btn.aicut-toggle-on:hover:not(:disabled) {
|
|
230
|
+
background: color-mix(in srgb, var(--color-brand) 18%, transparent);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.aicut-play-btn {
|
|
234
|
+
display: inline-flex;
|
|
235
|
+
align-items: center;
|
|
236
|
+
justify-content: center;
|
|
237
|
+
width: 32px;
|
|
238
|
+
height: 32px;
|
|
239
|
+
border-radius: 999px;
|
|
240
|
+
border: none;
|
|
241
|
+
background: var(--aicut-controls-active);
|
|
242
|
+
color: var(--aicut-controls-text);
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
transition: background-color 120ms ease;
|
|
245
|
+
flex-shrink: 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* The play button wraps its SVG in a <span> (so play↔pause swaps can
|
|
249
|
+
re-set innerHTML without losing the click target identity). A bare
|
|
250
|
+
span is inline-flow → the SVG inside aligns to font baseline,
|
|
251
|
+
which leaves a couple of px of font-descender space below the
|
|
252
|
+
icon and shifts it visually up-and-right inside the circle. Make
|
|
253
|
+
the span its own flex centring box so the SVG sits at geometric
|
|
254
|
+
center of the button regardless of inherited font metrics. */
|
|
255
|
+
.aicut-play-btn > span {
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
line-height: 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Hover ONLY in the paused state. Restricting by attribute means
|
|
263
|
+
the playing state has no competing background-on-hover rule at
|
|
264
|
+
equal specificity — eliminates any cascade-order surprises that
|
|
265
|
+
could leak `controls-hover` (near-white in light themes) over the
|
|
266
|
+
white pause icon. */
|
|
267
|
+
.aicut-play-btn:not([data-state="playing"]):hover {
|
|
268
|
+
background: var(--aicut-controls-hover);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.aicut-play-btn[data-state="playing"] {
|
|
272
|
+
background: var(--color-brand, #ff3386);
|
|
273
|
+
color: #fff;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.aicut-play-btn[data-state="playing"]:hover {
|
|
277
|
+
background: color-mix(in srgb, var(--color-brand, #ff3386) 88%, black);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.aicut-time-current,
|
|
281
|
+
.aicut-time-total {
|
|
282
|
+
font-size: 13px;
|
|
283
|
+
font-variant-numeric: tabular-nums;
|
|
284
|
+
min-width: 44px;
|
|
285
|
+
text-align: center;
|
|
286
|
+
color: var(--aicut-controls-text);
|
|
287
|
+
flex-shrink: 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.aicut-time-total {
|
|
291
|
+
color: color-mix(in srgb, var(--aicut-controls-text) 60%, transparent);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Slider track. Two solid stops in one gradient — brand color for
|
|
295
|
+
the filled portion, a dedicated `--aicut-track-unfilled` for the
|
|
296
|
+
rest. The unfilled tone derives from `--aicut-controls-text` (the
|
|
297
|
+
chrome text color, which is light-on-dark or dark-on-light), so
|
|
298
|
+
the rail auto-contrasts against any host theme without a brittle
|
|
299
|
+
"is the theme light?" attribute selector. Hosts can still override
|
|
300
|
+
`--aicut-track-unfilled` directly for full control. */
|
|
301
|
+
.aicut-zoom-slider {
|
|
302
|
+
--aicut-track-unfilled: color-mix(
|
|
303
|
+
in srgb,
|
|
304
|
+
var(--aicut-controls-text) 22%,
|
|
305
|
+
transparent
|
|
306
|
+
);
|
|
307
|
+
appearance: none;
|
|
308
|
+
-webkit-appearance: none;
|
|
309
|
+
width: 140px;
|
|
310
|
+
/* Allow the slider to absorb horizontal pressure when extras
|
|
311
|
+
populate the toolbar — it's the only element here with
|
|
312
|
+
legitimate flex-stretch semantics. Hard floor at 60px keeps
|
|
313
|
+
the thumb grabbable. */
|
|
314
|
+
min-width: 60px;
|
|
315
|
+
flex-shrink: 1;
|
|
316
|
+
height: 6px;
|
|
317
|
+
border-radius: 999px;
|
|
318
|
+
background: linear-gradient(
|
|
319
|
+
to right,
|
|
320
|
+
var(--color-brand) 0 var(--aicut-zoom-fill, 50%),
|
|
321
|
+
var(--aicut-track-unfilled) var(--aicut-zoom-fill, 50%) 100%
|
|
322
|
+
);
|
|
323
|
+
box-shadow: inset 0 0 0 1px
|
|
324
|
+
color-mix(in srgb, var(--aicut-controls-text) 10%, transparent);
|
|
325
|
+
border: none;
|
|
326
|
+
cursor: pointer;
|
|
327
|
+
outline: none;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* Hide the chromium default track since we paint via background. */
|
|
331
|
+
.aicut-zoom-slider::-webkit-slider-runnable-track {
|
|
332
|
+
background: transparent;
|
|
333
|
+
height: 6px;
|
|
334
|
+
border-radius: 999px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.aicut-zoom-slider::-moz-range-track {
|
|
338
|
+
background: transparent;
|
|
339
|
+
height: 6px;
|
|
340
|
+
border-radius: 999px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.aicut-zoom-slider::-webkit-slider-thumb {
|
|
344
|
+
appearance: none;
|
|
345
|
+
-webkit-appearance: none;
|
|
346
|
+
width: 14px;
|
|
347
|
+
height: 14px;
|
|
348
|
+
margin-top: -4px;
|
|
349
|
+
border-radius: 50%;
|
|
350
|
+
background: #fff;
|
|
351
|
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
352
|
+
cursor: grab;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.aicut-zoom-slider::-moz-range-thumb {
|
|
356
|
+
width: 14px;
|
|
357
|
+
height: 14px;
|
|
358
|
+
border: none;
|
|
359
|
+
border-radius: 50%;
|
|
360
|
+
background: #fff;
|
|
361
|
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ===== Timeline ===== */
|
|
365
|
+
|
|
366
|
+
/* The timeline is now a single <canvas> — this rule is the host
|
|
367
|
+
element that the canvas sizes itself to. Ruler, tracks, clips,
|
|
368
|
+
thumbnails, playhead, headers, and snap guide are ALL painted on
|
|
369
|
+
that canvas (see packages/core/src/timeline/). */
|
|
370
|
+
.aicut-timeline {
|
|
371
|
+
position: relative;
|
|
372
|
+
flex: 0 0 auto;
|
|
373
|
+
background: var(--aicut-controls-bg);
|
|
374
|
+
height: 240px;
|
|
375
|
+
min-height: 200px;
|
|
376
|
+
overflow: hidden;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.aicut-timeline-canvas canvas {
|
|
380
|
+
display: block;
|
|
381
|
+
width: 100%;
|
|
382
|
+
height: 100%;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* ===== Timeline toolbar (opt-in slot for host-supplied controls) ===== */
|
|
386
|
+
|
|
387
|
+
.aicut-timeline-toolbar {
|
|
388
|
+
flex: 0 0 36px;
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
justify-content: space-between;
|
|
392
|
+
padding: 0 12px;
|
|
393
|
+
gap: 12px;
|
|
394
|
+
background: var(--aicut-controls-bg);
|
|
395
|
+
border-bottom: 1px solid var(--aicut-controls-border);
|
|
396
|
+
color: var(--aicut-controls-text);
|
|
397
|
+
font-size: 13px;
|
|
398
|
+
/* Sit above the canvas in stacking so popovers/dropdowns spawned
|
|
399
|
+
from buttons can render over the timeline area instead of behind it. */
|
|
400
|
+
position: relative;
|
|
401
|
+
z-index: 1;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.aicut-timeline-toolbar-left,
|
|
405
|
+
.aicut-timeline-toolbar-right {
|
|
406
|
+
display: flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
gap: 8px;
|
|
409
|
+
min-width: 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.aicut-timeline-toolbar-right {
|
|
413
|
+
margin-left: auto;
|
|
414
|
+
}
|