@jackuait/blok 0.18.0 → 0.19.1
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/dist/blok.cjs +1 -1
- package/dist/blok.iife.js +4 -4
- package/dist/blok.mjs +2 -2
- package/dist/blok.umd.js +3 -3
- package/dist/chunks/{blok-BoAuTqli.mjs → blok-C52qeiPg.mjs} +31 -8
- package/dist/chunks/{blok-B7IRrvgX.cjs → blok-gFn3FI95.cjs} +3 -3
- package/dist/chunks/{constants-BWoyU4yv.cjs → constants-DmKwEMyC.cjs} +1 -1
- package/dist/chunks/{constants-BO6zLfL2.mjs → constants-DzQOOvgv.mjs} +2 -1
- package/dist/chunks/{tools-DXagFf6I.cjs → tools-BcF1rHBg.cjs} +2 -2
- package/dist/chunks/{tools-DRwLS-6t.mjs → tools-CelSzJoA.mjs} +384 -285
- package/dist/full.cjs +1 -1
- package/dist/full.mjs +3 -3
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/blok.ts +50 -4
- package/src/components/constants/data-attributes.ts +2 -0
- package/src/components/modules/ui.ts +30 -0
- package/src/styles/audio.css +26 -3
- package/src/styles/main.css +11 -0
- package/src/tools/audio/index.ts +76 -21
- package/src/tools/audio/liveliness.ts +136 -0
- package/src/tools/audio/ui.ts +6 -1
- package/src/tools/audio/waveform.ts +130 -24
- package/types/api/index.d.ts +1 -0
- package/types/api/width.d.ts +31 -0
- package/types/data-attributes.d.ts +1 -0
- package/types/index.d.ts +32 -1
package/dist/full.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/blok-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/blok-gFn3FI95.cjs`),t=require(`./chunks/constants-DmKwEMyC.cjs`),n=require(`./chunks/objectSpread2-iNkE6aJh.cjs`),r=require(`./chunks/tools-BcF1rHBg.cjs`);var i={paragraph:{class:r.k,inlineToolbar:!0,config:{preserveBlank:!0}},header:{class:r.O,inlineToolbar:!0},list:{class:r.D,inlineToolbar:!0}},a=n.t(n.t({},i),{},{bold:{class:r.l},italic:{class:r.c},marker:{class:r.o},underline:{class:r.a},strikethrough:{class:r.i},link:{class:r.s}});exports.Blok=e.t,exports.Bold=r.l,exports.DATA_ATTR=t.Pi,exports.Header=r.O,exports.Italic=r.c,exports.Link=r.s,exports.List=r.D,exports.Marker=r.o,exports.Paragraph=r.k,exports.Strikethrough=r.i,exports.Toggle=r.T,exports.Underline=r.a,exports.allTools=a,exports.defaultBlockTools=r.t,exports.defaultInlineTools=r.n,exports.defaultTools=i,exports.version=e.n;
|
package/dist/full.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as e, t } from "./chunks/blok-
|
|
2
|
-
import { Pi as n } from "./chunks/constants-
|
|
1
|
+
import { n as e, t } from "./chunks/blok-C52qeiPg.mjs";
|
|
2
|
+
import { Pi as n } from "./chunks/constants-DzQOOvgv.mjs";
|
|
3
3
|
import { t as r } from "./chunks/objectSpread2-jlBcdYeb.mjs";
|
|
4
|
-
import { D as i, O as a, T as o, a as s, c, i as l, k as u, l as d, n as f, o as p, s as m, t as h } from "./chunks/tools-
|
|
4
|
+
import { D as i, O as a, T as o, a as s, c, i as l, k as u, l as d, n as f, o as p, s as m, t as h } from "./chunks/tools-CelSzJoA.mjs";
|
|
5
5
|
//#region src/full.ts
|
|
6
6
|
var g = {
|
|
7
7
|
paragraph: {
|
package/dist/react.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require(`./chunks/chunk-BL1yB9zm.cjs`);const e=require(`./chunks/blok-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require(`./chunks/chunk-BL1yB9zm.cjs`);const e=require(`./chunks/blok-gFn3FI95.cjs`),t=require(`./chunks/objectSpread2-iNkE6aJh.cjs`);let n=require(`react`),r=require(`react/jsx-runtime`);var i=new WeakMap;function a(e,t){i.set(e,t)}function o(e){return i.get(e)}function s(e){i.delete(e)}function c(r,i){let[o,c]=(0,n.useState)(null),u=(0,n.useRef)(r);u.current=r;let d=(0,n.useRef)({editor:null,holder:null,destroyTimeout:null,isDestroyed:!1,depsToken:null}),f=(0,n.useMemo)(()=>({}),i==null?[]:i);(0,n.useEffect)(()=>{if(typeof window>`u`)return;let n=d.current;if(n.destroyTimeout!==null&&(clearTimeout(n.destroyTimeout),n.destroyTimeout=null),n.editor!==null&&!n.isDestroyed&&n.depsToken===f)return c(n.editor),()=>{l(n,c)};if(n.editor!==null&&!n.isDestroyed){s(n.editor);try{n.editor.destroy()}catch(e){}n.editor=null,n.holder=null,n.isDestroyed=!0,c(null)}let r=document.createElement(`div`);n.holder=r,n.isDestroyed=!1,n.depsToken=f;let i=u.current,o=new e.t(t.t(t.t({},i),{},{holder:r,onReady:()=>{var e,t;(e=(t=u.current).onReady)==null||e.call(t)},onChange:(...e)=>{var t,n;(t=(n=u.current).onChange)==null||t.call(n,...e)}}));return n.editor=o,a(o,r),o.isReady.then(()=>{n.editor===o&&!n.isDestroyed&&c(o)}).catch(()=>{if(n.editor===o&&!n.isDestroyed){s(o);try{o.destroy()}catch(e){}n.editor=null,n.holder=null,n.isDestroyed=!0,c(null)}}),()=>{l(n,c)}},[f]);let{readOnly:p}=r;(0,n.useEffect)(()=>{o!==null&&o.readOnly.set(p==null?!1:p)},[o,p]);let{autofocus:m}=r;return(0,n.useEffect)(()=>{o===null||!m||o.focus()},[o,m]),o}function l(e,t){e.destroyTimeout=setTimeout(()=>{if(e.editor!==null){s(e.editor);try{e.editor.destroy()}catch(e){}e.editor=null,e.holder=null,e.isDestroyed=!0,e.destroyTimeout=null,t(null)}},0)}var u=[`editor`],d=(0,n.forwardRef)(function(i,a){let{editor:s}=i,c=e.i(i,u),l=(0,n.useRef)(null);return(0,n.useEffect)(()=>{if(s===null||l.current===null)return;let e=o(s);if(e!==void 0)return l.current.appendChild(e),()=>{e.remove()}},[s]),(0,r.jsx)(`div`,t.t({ref:e=>{if(l.current=e,typeof a==`function`)a(e);else if(a!=null){let t=a;t.current=e}}},c))});exports.BlokContent=d,exports.useBlok=c;
|
package/dist/react.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as e, t } from "./chunks/blok-
|
|
1
|
+
import { i as e, t } from "./chunks/blok-C52qeiPg.mjs";
|
|
2
2
|
import { t as n } from "./chunks/objectSpread2-jlBcdYeb.mjs";
|
|
3
3
|
import { forwardRef as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
|
|
4
4
|
import { jsx as c } from "react/jsx-runtime";
|
package/dist/tools.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/constants-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./chunks/constants-DmKwEMyC.cjs`),t=require(`./chunks/tools-BcF1rHBg.cjs`);exports.Audio=t.g,exports.Bold=t.l,exports.Bookmark=t.u,exports.Callout=t.w,exports.Code=t.y,exports.Column=t.p,exports.ColumnList=t.m,exports.Columns=t.f,exports.Convert=e.m,exports.Database=t.C,exports.DatabaseRow=t.S,exports.Divider=t.x,exports.Embed=t.d,exports.File=t._,exports.Header=t.O,exports.Image=t.v,exports.InlineCode=t.r,exports.Italic=t.c,exports.Link=t.s,exports.List=t.D,exports.Marker=t.o,exports.Paragraph=t.k,exports.Quote=t.b,exports.Strikethrough=t.i,exports.Table=t.E,exports.Toggle=t.T,exports.Underline=t.a,exports.Video=t.h,exports.defaultBlockTools=t.t,exports.defaultInlineTools=t.n;
|
package/dist/tools.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { m as e } from "./chunks/constants-
|
|
2
|
-
import { C as t, D as n, E as r, O as i, S as a, T as o, _ as s, a as c, b as l, c as u, d, f, g as p, h as m, i as h, k as g, l as _, m as v, n as y, o as b, p as x, r as S, s as C, t as w, u as T, v as E, w as D, x as O, y as k } from "./chunks/tools-
|
|
1
|
+
import { m as e } from "./chunks/constants-DzQOOvgv.mjs";
|
|
2
|
+
import { C as t, D as n, E as r, O as i, S as a, T as o, _ as s, a as c, b as l, c as u, d, f, g as p, h as m, i as h, k as g, l as _, m as v, n as y, o as b, p as x, r as S, s as C, t as w, u as T, v as E, w as D, x as O, y as k } from "./chunks/tools-CelSzJoA.mjs";
|
|
3
3
|
export { p as Audio, _ as Bold, T as Bookmark, D as Callout, k as Code, x as Column, v as ColumnList, f as Columns, e as Convert, t as Database, a as DatabaseRow, O as Divider, d as Embed, s as File, i as Header, E as Image, S as InlineCode, u as Italic, C as Link, n as List, b as Marker, g as Paragraph, l as Quote, h as Strikethrough, r as Table, o as Toggle, c as Underline, m as Video, w as defaultBlockTools, y as defaultInlineTools };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackuait/blok",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
|
|
5
5
|
"module": "dist/blok.mjs",
|
|
6
6
|
"main": "./dist/blok.cjs",
|
package/src/blok.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import '@babel/register';
|
|
7
7
|
|
|
8
|
-
import type { BlokConfig, API } from '../types';
|
|
8
|
+
import type { BlokConfig, API, EditorWidth } from '../types';
|
|
9
9
|
|
|
10
10
|
import { DATA_ATTR } from './components/constants/data-attributes';
|
|
11
11
|
import { Core } from './components/core';
|
|
@@ -36,9 +36,11 @@ class Blok {
|
|
|
36
36
|
private readonly initialConfiguration: BlokConfig|string|undefined;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Promise that resolves when core modules are ready and UI is rendered on the page
|
|
39
|
+
* Promise that resolves when core modules are ready and UI is rendered on the page.
|
|
40
|
+
* Resolves with the fully-initialized Blok instance, so awaiting it narrows a
|
|
41
|
+
* pre-ready reference (see {@link PendingBlok}) to the complete API surface.
|
|
40
42
|
*/
|
|
41
|
-
public isReady: Promise<
|
|
43
|
+
public isReady: Promise<Blok>;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Stores destroy method implementation.
|
|
@@ -141,6 +143,38 @@ class Blok {
|
|
|
141
143
|
},
|
|
142
144
|
};
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Width API — expose it on the instance immediately (mirrors the theme API),
|
|
148
|
+
* so host apps can call instance.width.set('full') before isReady without a
|
|
149
|
+
* silent no-op. The chosen mode is buffered and replayed once UI is ready.
|
|
150
|
+
*/
|
|
151
|
+
const widthBuffer = { pendingMode: null as EditorWidth | null };
|
|
152
|
+
|
|
153
|
+
const getUIModule = (): BlokModules['UI'] | undefined =>
|
|
154
|
+
(blok.moduleInstances as Partial<BlokModules>).UI;
|
|
155
|
+
|
|
156
|
+
const readWidthMode = (): EditorWidth => {
|
|
157
|
+
const ui = getUIModule();
|
|
158
|
+
|
|
159
|
+
return ui !== undefined ? ui.getWidthMode() : (widthBuffer.pendingMode ?? 'narrow');
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const applyWidthMode = (mode: EditorWidth): void => {
|
|
163
|
+
widthBuffer.pendingMode = mode;
|
|
164
|
+
|
|
165
|
+
const ui = getUIModule();
|
|
166
|
+
|
|
167
|
+
if (ui !== undefined) {
|
|
168
|
+
ui.setWidthMode(mode);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
(this as Record<string, unknown>).width = {
|
|
173
|
+
get: (): EditorWidth => readWidthMode(),
|
|
174
|
+
set: (mode: EditorWidth): void => applyWidthMode(mode),
|
|
175
|
+
toggle: (): void => applyWidthMode(readWidthMode() === 'full' ? 'narrow' : 'full'),
|
|
176
|
+
};
|
|
177
|
+
|
|
144
178
|
/**
|
|
145
179
|
* We need to export isReady promise in the constructor
|
|
146
180
|
* as it can be used before other API methods are exported
|
|
@@ -186,7 +220,7 @@ class Blok {
|
|
|
186
220
|
|
|
187
221
|
Object.setPrototypeOf(this, null);
|
|
188
222
|
|
|
189
|
-
return;
|
|
223
|
+
return this;
|
|
190
224
|
}
|
|
191
225
|
|
|
192
226
|
this.exportAPI(blok);
|
|
@@ -203,6 +237,16 @@ class Blok {
|
|
|
203
237
|
themeBuffer.pendingMode = null;
|
|
204
238
|
}
|
|
205
239
|
|
|
240
|
+
// Apply any width mode buffered before isReady resolved.
|
|
241
|
+
if (widthBuffer.pendingMode !== null) {
|
|
242
|
+
const ui = (blok.moduleInstances as Partial<BlokModules>).UI;
|
|
243
|
+
|
|
244
|
+
if (ui !== undefined) {
|
|
245
|
+
ui.setWidthMode(widthBuffer.pendingMode);
|
|
246
|
+
}
|
|
247
|
+
widthBuffer.pendingMode = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
206
250
|
// Scroll to the block referenced by the URL hash, if present.
|
|
207
251
|
// isReady resolves only after all blocks are in the DOM (requestIdleCallback fence in Renderer),
|
|
208
252
|
// so no extra polling is needed even on slow connections.
|
|
@@ -234,6 +278,8 @@ class Blok {
|
|
|
234
278
|
* @todo pass API as an argument. It will allow to use Blok's API when blok is ready
|
|
235
279
|
*/
|
|
236
280
|
onReady();
|
|
281
|
+
|
|
282
|
+
return this;
|
|
237
283
|
});
|
|
238
284
|
}
|
|
239
285
|
|
|
@@ -63,6 +63,8 @@ export const DATA_ATTR = {
|
|
|
63
63
|
contentAlign: 'data-blok-content-align',
|
|
64
64
|
/** Right-to-left mode */
|
|
65
65
|
rtl: 'data-blok-rtl',
|
|
66
|
+
/** Editor content width mode (present with value "full" for wide mode; absent = narrow) */
|
|
67
|
+
width: 'data-blok-width',
|
|
66
68
|
|
|
67
69
|
// ============================================
|
|
68
70
|
// Drag and Drop
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Module UI
|
|
4
4
|
* @type {UI}
|
|
5
5
|
*/
|
|
6
|
+
import type { EditorWidth } from '../../../types/api/width';
|
|
6
7
|
import styles from '../../styles/main.css?inline';
|
|
7
8
|
import { Module } from '../__module';
|
|
8
9
|
import {
|
|
@@ -347,6 +348,35 @@ export class UI extends Module<UINodes> {
|
|
|
347
348
|
this.nodes.wrapper.setAttribute(DATA_ATTR.empty, BlockManager.isBlokEmpty ? 'true' : 'false');
|
|
348
349
|
}
|
|
349
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Current editor content width mode. Defaults to 'narrow'.
|
|
353
|
+
*/
|
|
354
|
+
private widthMode: EditorWidth = 'narrow';
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Returns the current editor content width mode.
|
|
358
|
+
*/
|
|
359
|
+
public getWidthMode(): EditorWidth {
|
|
360
|
+
return this.widthMode;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Sets the editor content width mode by writing the width data attribute on
|
|
365
|
+
* the editor wrapper. 'narrow' (the default) leaves the attribute absent so
|
|
366
|
+
* the content keeps its `--max-width-content` constraint; 'full' adds it so
|
|
367
|
+
* the dedicated CSS rule removes the constraint.
|
|
368
|
+
* @param mode - the width mode to apply
|
|
369
|
+
*/
|
|
370
|
+
public setWidthMode(mode: EditorWidth): void {
|
|
371
|
+
this.widthMode = mode;
|
|
372
|
+
|
|
373
|
+
if (mode === 'full') {
|
|
374
|
+
this.nodes.wrapper.setAttribute(DATA_ATTR.width, 'full');
|
|
375
|
+
} else {
|
|
376
|
+
this.nodes.wrapper.removeAttribute(DATA_ATTR.width);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
350
380
|
/**
|
|
351
381
|
* Check if one of Toolbar is opened
|
|
352
382
|
* Used to prevent global keydowns (for example, Enter) conflicts with Enter-on-toolbar
|
package/src/styles/audio.css
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
--blok-audio-play-size: 36px;
|
|
26
26
|
--blok-audio-bar: var(--blok-border-subtle);
|
|
27
27
|
--blok-audio-bar-played: var(--blok-text-primary);
|
|
28
|
+
/* Comet-head tint — played colour nudged toward the editor accent. Subtle by
|
|
29
|
+
design; the waveform canvas blends bars toward it only at the playhead. */
|
|
30
|
+
--blok-audio-bar-head: color-mix(in srgb, var(--blok-audio-bar-played) 68%, var(--blok-color-accent) 32%);
|
|
28
31
|
--blok-audio-seek-pct: 0%;
|
|
29
32
|
--blok-audio-vol-pct: 100%;
|
|
30
33
|
--blok-audio-speed-pct: 50%;
|
|
@@ -382,20 +385,38 @@
|
|
|
382
385
|
color: var(--blok-text-primary);
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
/* ----- caption row (full-width footer) --------------------
|
|
388
|
+
/* ----- caption row (full-width footer) --------------------
|
|
389
|
+
The row is a single-track grid wrapper so it can collapse to zero height
|
|
390
|
+
(grid-template-rows 1fr → 0fr) when the caption is toggled, while the inner
|
|
391
|
+
element holds the actual padding/border that fade out with it. */
|
|
386
392
|
[data-blok-tool="audio"] .blok-audio-caption-row {
|
|
387
393
|
grid-column: 1 / -1;
|
|
388
394
|
grid-row: 2;
|
|
395
|
+
display: grid;
|
|
396
|
+
grid-template-rows: 1fr;
|
|
397
|
+
transition:
|
|
398
|
+
grid-template-rows var(--blok-duration-slow) var(--blok-ease-popover),
|
|
399
|
+
opacity var(--blok-duration-slow) var(--blok-ease-popover);
|
|
400
|
+
}
|
|
401
|
+
[data-blok-tool="audio"] .blok-audio-caption-row.is-collapsed {
|
|
402
|
+
grid-template-rows: 0fr;
|
|
403
|
+
opacity: 0;
|
|
404
|
+
}
|
|
405
|
+
/* overflow:hidden + min-height:0 lets the grid track collapse fully to 0; the
|
|
406
|
+
padding/border live on the caption child below so they get clipped too. */
|
|
407
|
+
[data-blok-tool="audio"] .blok-audio-caption-row__inner {
|
|
408
|
+
min-height: 0;
|
|
409
|
+
overflow: hidden;
|
|
389
410
|
display: flex;
|
|
390
411
|
align-items: flex-start;
|
|
391
412
|
gap: var(--blok-space-2);
|
|
392
|
-
padding-top: var(--blok-space-2-5);
|
|
393
|
-
border-top: 1px solid var(--blok-border-subtle);
|
|
394
413
|
}
|
|
395
414
|
[data-blok-tool="audio"] .blok-audio-caption {
|
|
396
415
|
flex: 1 1 auto;
|
|
397
416
|
min-width: 0;
|
|
398
417
|
display: block;
|
|
418
|
+
padding-top: var(--blok-space-2-5);
|
|
419
|
+
border-top: 1px solid var(--blok-border-subtle);
|
|
399
420
|
font-size: var(--blok-font-size-ui-label);
|
|
400
421
|
color: var(--blok-text-secondary);
|
|
401
422
|
line-height: var(--blok-line-height-body);
|
|
@@ -525,4 +546,6 @@
|
|
|
525
546
|
}
|
|
526
547
|
/* The badge still appears while playing — it just holds steady. */
|
|
527
548
|
[data-blok-tool="audio"] .blok-audio-eq { transition: none; }
|
|
549
|
+
/* Caption appears/disappears instantly (JS also takes the instant path). */
|
|
550
|
+
[data-blok-tool="audio"] .blok-audio-caption-row { transition: none; }
|
|
528
551
|
}
|
package/src/styles/main.css
CHANGED
|
@@ -273,6 +273,17 @@
|
|
|
273
273
|
margin-left: auto;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Content width mode
|
|
278
|
+
* 'full' (data-blok-width="full" on the editor wrapper) removes the
|
|
279
|
+
* --max-width-content constraint so content fills its container. The default
|
|
280
|
+
* 'narrow' mode leaves the attribute absent and keeps the max-w-blok-content
|
|
281
|
+
* Tailwind utility intact. Unlayered selector overrides the @layer utilities rule.
|
|
282
|
+
*/
|
|
283
|
+
[data-blok-width="full"] [data-blok-element-content] {
|
|
284
|
+
max-width: none;
|
|
285
|
+
}
|
|
286
|
+
|
|
276
287
|
/**
|
|
277
288
|
* Multi-block drag visual feedback
|
|
278
289
|
* Makes selected blocks semi-transparent during multi-block drag operation
|
package/src/tools/audio/index.ts
CHANGED
|
@@ -52,6 +52,8 @@ export class AudioTool implements BlockTool {
|
|
|
52
52
|
private controlsHandle: ControlsHandle | null = null;
|
|
53
53
|
private waveformHandle: WaveformHandle | null = null;
|
|
54
54
|
private destroyed = false;
|
|
55
|
+
/** Pending fallback timer for the caption collapse-out animation. */
|
|
56
|
+
private captionExitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
55
57
|
|
|
56
58
|
constructor(options: BlockToolConstructorOptions<AudioData, AudioConfig>) {
|
|
57
59
|
this.api = options.api;
|
|
@@ -199,6 +201,7 @@ export class AudioTool implements BlockTool {
|
|
|
199
201
|
|
|
200
202
|
public removed(): void {
|
|
201
203
|
this.destroyed = true;
|
|
204
|
+
if (this.captionExitTimer) { clearTimeout(this.captionExitTimer); this.captionExitTimer = null; }
|
|
202
205
|
this.controlsHandle?.destroy();
|
|
203
206
|
this.controlsHandle = null;
|
|
204
207
|
this.waveformHandle?.destroy();
|
|
@@ -299,6 +302,7 @@ export class AudioTool implements BlockTool {
|
|
|
299
302
|
|
|
300
303
|
private renderState(): void {
|
|
301
304
|
if (!this.root) return;
|
|
305
|
+
if (this.captionExitTimer) { clearTimeout(this.captionExitTimer); this.captionExitTimer = null; }
|
|
302
306
|
this.controlsHandle?.destroy();
|
|
303
307
|
this.controlsHandle = null;
|
|
304
308
|
this.waveformHandle?.destroy();
|
|
@@ -393,14 +397,9 @@ export class AudioTool implements BlockTool {
|
|
|
393
397
|
body.appendChild(this.controlsHandle.element);
|
|
394
398
|
|
|
395
399
|
// Caption row — outside body, below the card
|
|
396
|
-
const placeholder = this.config.captionPlaceholder ?? DEFAULT_CAPTION_PLACEHOLDER;
|
|
397
400
|
const captionVisible = this.data.captionVisible !== false;
|
|
398
|
-
if (captionVisible
|
|
399
|
-
figure.appendChild(
|
|
400
|
-
value: this.data.caption ?? '',
|
|
401
|
-
placeholder,
|
|
402
|
-
editable: !this.readOnly,
|
|
403
|
-
}));
|
|
401
|
+
if (captionVisible) {
|
|
402
|
+
figure.appendChild(this.createCaptionRow());
|
|
404
403
|
}
|
|
405
404
|
|
|
406
405
|
// Wire title/artist blur handlers
|
|
@@ -419,21 +418,32 @@ export class AudioTool implements BlockTool {
|
|
|
419
418
|
}
|
|
420
419
|
});
|
|
421
420
|
|
|
422
|
-
// Wire caption blur handler
|
|
423
|
-
const captionEl = figure.querySelector<HTMLElement>('[data-role="audio-caption"]');
|
|
424
|
-
if (captionEl) {
|
|
425
|
-
captionEl.addEventListener('blur', () => {
|
|
426
|
-
const next = captionEl.textContent ?? '';
|
|
427
|
-
if (next !== this.data.caption) {
|
|
428
|
-
this.data.caption = next || undefined;
|
|
429
|
-
this.block.dispatchChange();
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
421
|
this.root.appendChild(figure);
|
|
435
422
|
}
|
|
436
423
|
|
|
424
|
+
/** Build a caption row (with its blur handler wired) ready to mount. */
|
|
425
|
+
private createCaptionRow(): HTMLElement {
|
|
426
|
+
const placeholder = this.config.captionPlaceholder ?? DEFAULT_CAPTION_PLACEHOLDER;
|
|
427
|
+
const row = renderCaptionRow({
|
|
428
|
+
value: this.data.caption ?? '',
|
|
429
|
+
placeholder,
|
|
430
|
+
editable: !this.readOnly,
|
|
431
|
+
});
|
|
432
|
+
const captionEl = row.querySelector<HTMLElement>('[data-role="audio-caption"]');
|
|
433
|
+
captionEl?.addEventListener('blur', () => {
|
|
434
|
+
const next = captionEl.textContent ?? '';
|
|
435
|
+
if (next !== this.data.caption) {
|
|
436
|
+
this.data.caption = next || undefined;
|
|
437
|
+
this.block.dispatchChange();
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return row;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private prefersReducedMotion(): boolean {
|
|
444
|
+
return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
445
|
+
}
|
|
446
|
+
|
|
437
447
|
private setAlignment(next: AudioAlignment): void {
|
|
438
448
|
if (this.data.alignment === next) return;
|
|
439
449
|
this.data.alignment = next;
|
|
@@ -442,9 +452,54 @@ export class AudioTool implements BlockTool {
|
|
|
442
452
|
}
|
|
443
453
|
|
|
444
454
|
private toggleCaption(): void {
|
|
445
|
-
|
|
455
|
+
const enabling = this.data.captionVisible === false;
|
|
456
|
+
this.data.captionVisible = enabling;
|
|
457
|
+
// Removing the caption clears its text too, so re-enabling starts fresh
|
|
458
|
+
// rather than resurrecting the old text.
|
|
459
|
+
if (!enabling) this.data.caption = undefined;
|
|
446
460
|
this.block.dispatchChange();
|
|
447
|
-
|
|
461
|
+
|
|
462
|
+
const figure = this.root?.querySelector<HTMLElement>('[data-role="audio-figure"]');
|
|
463
|
+
// No live card to animate (or motion is off) — fall back to an instant
|
|
464
|
+
// re-render so the caption snaps in/out.
|
|
465
|
+
if (!figure || this.prefersReducedMotion()) {
|
|
466
|
+
this.renderState();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
this.syncRootAttributes();
|
|
470
|
+
if (enabling) this.animateCaptionIn(figure);
|
|
471
|
+
else this.animateCaptionOut(figure);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private animateCaptionIn(figure: HTMLElement): void {
|
|
475
|
+
const existing = figure.querySelector<HTMLElement>('[data-role="audio-caption-row"]');
|
|
476
|
+
if (existing) {
|
|
477
|
+
// A collapse-out was mid-flight — cancel it and re-open in place.
|
|
478
|
+
if (this.captionExitTimer) { clearTimeout(this.captionExitTimer); this.captionExitTimer = null; }
|
|
479
|
+
existing.classList.remove('is-collapsed');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const row = this.createCaptionRow();
|
|
483
|
+
row.classList.add('is-collapsed');
|
|
484
|
+
figure.appendChild(row);
|
|
485
|
+
// Force a reflow so the collapsed state is committed, then expand into view.
|
|
486
|
+
void row.offsetHeight;
|
|
487
|
+
row.classList.remove('is-collapsed');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private animateCaptionOut(figure: HTMLElement): void {
|
|
491
|
+
const row = figure.querySelector<HTMLElement>('[data-role="audio-caption-row"]');
|
|
492
|
+
if (!row) return;
|
|
493
|
+
row.classList.add('is-collapsed');
|
|
494
|
+
const finish = (): void => {
|
|
495
|
+
if (this.captionExitTimer) { clearTimeout(this.captionExitTimer); this.captionExitTimer = null; }
|
|
496
|
+
row.remove();
|
|
497
|
+
};
|
|
498
|
+
row.addEventListener('transitionend', (e) => {
|
|
499
|
+
if (e.propertyName === 'grid-template-rows') finish();
|
|
500
|
+
}, { once: true });
|
|
501
|
+
// Fallback in case transitionend never fires (display:none, interrupted, etc.).
|
|
502
|
+
this.captionExitTimer = setTimeout(finish, 360);
|
|
448
503
|
}
|
|
449
504
|
|
|
450
505
|
private toggleLoop(): void {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waveform "now playing" liveliness math.
|
|
3
|
+
*
|
|
4
|
+
* The cached peaks are static — there's no real-time FFT — so the sense of a
|
|
5
|
+
* live visualizer is composed from two layers:
|
|
6
|
+
*
|
|
7
|
+
* 1. A focal **comet** at the playhead: a bright head spike, a tight leading
|
|
8
|
+
* edge, and a longer decaying wake trailing into the already-played bars.
|
|
9
|
+
* It travels left→right as playback advances.
|
|
10
|
+
* 2. A global **ambient** ripple that flows across the entire field so no bar
|
|
11
|
+
* sits frozen. It's biased stronger on the played side (a shimmering wake
|
|
12
|
+
* of music) and faint on the not-yet-played preview ahead.
|
|
13
|
+
*
|
|
14
|
+
* All functions are pure so the behaviour is unit-testable; the canvas in
|
|
15
|
+
* waveform.ts just samples them per bar, per frame.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Bars ahead of the playhead (not yet played) that still react. */
|
|
19
|
+
export const REACH_AHEAD = 4;
|
|
20
|
+
/** Bars behind the playhead (already played) that keep a decaying wake. */
|
|
21
|
+
export const REACH_BEHIND = 9;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Asymmetric "comet" falloff around the playhead. `distance` is the bar index
|
|
25
|
+
* minus the fractional playhead index: negative = behind (already-played wake),
|
|
26
|
+
* positive = ahead (not yet played). The wake reaches further than the leading
|
|
27
|
+
* edge so the energy reads as travelling with momentum. 1 at the playhead,
|
|
28
|
+
* easing to 0 at the respective reach.
|
|
29
|
+
*/
|
|
30
|
+
export function cometEnvelope(distance: number, reachAhead = REACH_AHEAD, reachBehind = REACH_BEHIND): number {
|
|
31
|
+
const reach = distance < 0 ? reachBehind : reachAhead;
|
|
32
|
+
const d = Math.abs(distance);
|
|
33
|
+
if (d >= reach) return 0;
|
|
34
|
+
const t = 1 - d / reach;
|
|
35
|
+
return t * t;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tight symmetric spike centred on the playhead so the bar under it stands
|
|
40
|
+
* clearly proud of its neighbours — the bright head of the comet. Falls off
|
|
41
|
+
* faster (cubed) than the comet body.
|
|
42
|
+
*/
|
|
43
|
+
export function headFocus(distance: number, reach = 3): number {
|
|
44
|
+
const d = Math.abs(distance);
|
|
45
|
+
if (d >= reach) return 0;
|
|
46
|
+
const t = 1 - d / reach;
|
|
47
|
+
return t * t * t;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Deterministic per-bar wobble in [-1, 1]. Two out-of-phase sines keyed to the
|
|
52
|
+
* bar index so neighbouring bars rise and fall independently — the cluster
|
|
53
|
+
* reads as a dancing equalizer rather than one uniform swell.
|
|
54
|
+
*/
|
|
55
|
+
export function barWobble(index: number, timeSeconds: number): number {
|
|
56
|
+
const a = Math.sin(timeSeconds * 8.5 + index * 0.7);
|
|
57
|
+
const b = Math.sin(timeSeconds * 5.3 + index * 1.9);
|
|
58
|
+
return a * 0.6 + b * 0.4;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Slow travelling undulation across the whole bar field, in [-1, 1]. Two waves
|
|
63
|
+
* propagate in opposite directions (the `-index`/`+index` phase terms) so the
|
|
64
|
+
* field gently churns rather than sliding uniformly. Kept low-frequency so the
|
|
65
|
+
* motion is graceful, not busy.
|
|
66
|
+
*/
|
|
67
|
+
export function ambientWave(index: number, timeSeconds: number): number {
|
|
68
|
+
const a = Math.sin(timeSeconds * 2.1 - index * 0.35);
|
|
69
|
+
const b = Math.sin(timeSeconds * 1.3 + index * 0.21);
|
|
70
|
+
return a * 0.6 + b * 0.4;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Ambient swing applied to every bar (the faint preview-side breath). */
|
|
74
|
+
const AMBIENT_BASE = 0.035;
|
|
75
|
+
/** Extra ambient swing layered onto the played side (the shimmering wake). */
|
|
76
|
+
const AMBIENT_PLAYED = 0.06;
|
|
77
|
+
/** Bars ahead of the playhead over which the played-side shimmer fades out. */
|
|
78
|
+
const AMBIENT_AHEAD_FADE = 26;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ease-out cubic for the head-colour entrance ramp: 0→1 with most of the travel
|
|
82
|
+
* up front, so the colour blooms in quickly then settles. Clamped to [0, 1].
|
|
83
|
+
*/
|
|
84
|
+
export function entranceEase(progress: number): number {
|
|
85
|
+
const t = Math.min(1, Math.max(0, progress));
|
|
86
|
+
return 1 - (1 - t) ** 3;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 0..1 tint strength for a bar's comet-head colour overlay. Peaks at the
|
|
91
|
+
* playhead (via {@link headFocus}) and scales with both the live `energy` (so it
|
|
92
|
+
* fades out on the pause settle) and the `entrance` ramp (so it animates in when
|
|
93
|
+
* playback starts).
|
|
94
|
+
*/
|
|
95
|
+
export function headColorBlend(opts: { distance: number; energy: number; entrance: number }): number {
|
|
96
|
+
return headFocus(opts.distance) * opts.energy * opts.entrance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface LiveAmplitudeOptions {
|
|
100
|
+
basePeak: number;
|
|
101
|
+
index: number;
|
|
102
|
+
/** Fractional bar position of the playhead (played * peaks.length). */
|
|
103
|
+
playheadIndex: number;
|
|
104
|
+
timeSeconds: number;
|
|
105
|
+
reduced: boolean;
|
|
106
|
+
/** 0..1 multiplier on the live boost — 1 while playing, ramps to 0 on the pause settle. */
|
|
107
|
+
energy?: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Final 0..1 bar amplitude, blending the static peak with two live layers: the
|
|
112
|
+
* playhead-localized comet (body swell + bright head spike + bidirectional
|
|
113
|
+
* dance) and a global ambient ripple biased toward the played side. The
|
|
114
|
+
* combined boost is scaled by `energy` (for the pause settle) and zeroed under
|
|
115
|
+
* `reduced` (prefers-reduced-motion).
|
|
116
|
+
*/
|
|
117
|
+
export function liveAmplitude(opts: LiveAmplitudeOptions): number {
|
|
118
|
+
const { basePeak, index, playheadIndex, timeSeconds, reduced, energy = 1 } = opts;
|
|
119
|
+
if (reduced || energy <= 0) return basePeak;
|
|
120
|
+
|
|
121
|
+
const distance = index - playheadIndex;
|
|
122
|
+
|
|
123
|
+
// Layer 1 — focal comet near the playhead.
|
|
124
|
+
const env = cometEnvelope(distance);
|
|
125
|
+
const head = headFocus(distance);
|
|
126
|
+
const comet = env * 0.15 + head * 0.2 + env * 0.28 * barWobble(index, timeSeconds);
|
|
127
|
+
|
|
128
|
+
// Layer 2 — global ambient ripple. Full strength behind the playhead (the
|
|
129
|
+
// played wake), fading across AMBIENT_AHEAD_FADE bars into the preview ahead.
|
|
130
|
+
const playedBias = distance <= 0 ? 1 : Math.max(0, 1 - distance / AMBIENT_AHEAD_FADE);
|
|
131
|
+
const ambient = ambientWave(index, timeSeconds) * (AMBIENT_BASE + AMBIENT_PLAYED * playedBias);
|
|
132
|
+
|
|
133
|
+
const amp = basePeak + (comet + ambient) * energy;
|
|
134
|
+
|
|
135
|
+
return Math.min(1, Math.max(0, amp));
|
|
136
|
+
}
|
package/src/tools/audio/ui.ts
CHANGED
|
@@ -97,8 +97,13 @@ export function renderCaptionRow(opts: CaptionRowOptions): HTMLElement {
|
|
|
97
97
|
const row = document.createElement('div');
|
|
98
98
|
row.className = 'blok-audio-caption-row';
|
|
99
99
|
row.setAttribute('data-role', 'audio-caption-row');
|
|
100
|
+
// Inner wrapper carries the visible layout (padding/border) so the row itself
|
|
101
|
+
// can collapse cleanly via grid-template-rows when toggled.
|
|
102
|
+
const inner = document.createElement('div');
|
|
103
|
+
inner.className = 'blok-audio-caption-row__inner';
|
|
100
104
|
const cap = editableLine('audio-caption', opts.value, opts.editable, opts.placeholder);
|
|
101
105
|
cap.className = 'blok-audio-caption';
|
|
102
|
-
|
|
106
|
+
inner.appendChild(cap);
|
|
107
|
+
row.appendChild(inner);
|
|
103
108
|
return row;
|
|
104
109
|
}
|