@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/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
+ }
@@ -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
+ }