@jackuait/blok 0.18.0 → 0.19.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/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-B7IRrvgX.cjs → blok-BosfCfPX.cjs} +3 -3
- package/dist/chunks/{blok-BoAuTqli.mjs → blok-CXB-exXU.mjs} +29 -5
- package/dist/chunks/{constants-BO6zLfL2.mjs → constants-BmCvHbkL.mjs} +2 -1
- package/dist/chunks/{constants-BWoyU4yv.cjs → constants-ChsAFZ8F.cjs} +1 -1
- package/dist/chunks/{tools-DRwLS-6t.mjs → tools-BdoYHyV0.mjs} +321 -253
- package/dist/chunks/{tools-DXagFf6I.cjs → tools-DmEzyrn3.cjs} +2 -2
- 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 +43 -1
- package/src/components/constants/data-attributes.ts +2 -0
- package/src/components/modules/ui.ts +30 -0
- package/src/styles/audio.css +3 -0
- package/src/styles/main.css +11 -0
- package/src/tools/audio/liveliness.ts +136 -0
- 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 +5 -0
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-BosfCfPX.cjs`),t=require(`./chunks/constants-ChsAFZ8F.cjs`),n=require(`./chunks/objectSpread2-iNkE6aJh.cjs`),r=require(`./chunks/tools-DmEzyrn3.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-CXB-exXU.mjs";
|
|
2
|
+
import { Pi as n } from "./chunks/constants-BmCvHbkL.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-BdoYHyV0.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-BosfCfPX.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-CXB-exXU.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-ChsAFZ8F.cjs`),t=require(`./chunks/tools-DmEzyrn3.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-BmCvHbkL.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-BdoYHyV0.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.0",
|
|
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';
|
|
@@ -141,6 +141,38 @@ class Blok {
|
|
|
141
141
|
},
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Width API — expose it on the instance immediately (mirrors the theme API),
|
|
146
|
+
* so host apps can call instance.width.set('full') before isReady without a
|
|
147
|
+
* silent no-op. The chosen mode is buffered and replayed once UI is ready.
|
|
148
|
+
*/
|
|
149
|
+
const widthBuffer = { pendingMode: null as EditorWidth | null };
|
|
150
|
+
|
|
151
|
+
const getUIModule = (): BlokModules['UI'] | undefined =>
|
|
152
|
+
(blok.moduleInstances as Partial<BlokModules>).UI;
|
|
153
|
+
|
|
154
|
+
const readWidthMode = (): EditorWidth => {
|
|
155
|
+
const ui = getUIModule();
|
|
156
|
+
|
|
157
|
+
return ui !== undefined ? ui.getWidthMode() : (widthBuffer.pendingMode ?? 'narrow');
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const applyWidthMode = (mode: EditorWidth): void => {
|
|
161
|
+
widthBuffer.pendingMode = mode;
|
|
162
|
+
|
|
163
|
+
const ui = getUIModule();
|
|
164
|
+
|
|
165
|
+
if (ui !== undefined) {
|
|
166
|
+
ui.setWidthMode(mode);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
(this as Record<string, unknown>).width = {
|
|
171
|
+
get: (): EditorWidth => readWidthMode(),
|
|
172
|
+
set: (mode: EditorWidth): void => applyWidthMode(mode),
|
|
173
|
+
toggle: (): void => applyWidthMode(readWidthMode() === 'full' ? 'narrow' : 'full'),
|
|
174
|
+
};
|
|
175
|
+
|
|
144
176
|
/**
|
|
145
177
|
* We need to export isReady promise in the constructor
|
|
146
178
|
* as it can be used before other API methods are exported
|
|
@@ -203,6 +235,16 @@ class Blok {
|
|
|
203
235
|
themeBuffer.pendingMode = null;
|
|
204
236
|
}
|
|
205
237
|
|
|
238
|
+
// Apply any width mode buffered before isReady resolved.
|
|
239
|
+
if (widthBuffer.pendingMode !== null) {
|
|
240
|
+
const ui = (blok.moduleInstances as Partial<BlokModules>).UI;
|
|
241
|
+
|
|
242
|
+
if (ui !== undefined) {
|
|
243
|
+
ui.setWidthMode(widthBuffer.pendingMode);
|
|
244
|
+
}
|
|
245
|
+
widthBuffer.pendingMode = null;
|
|
246
|
+
}
|
|
247
|
+
|
|
206
248
|
// Scroll to the block referenced by the URL hash, if present.
|
|
207
249
|
// isReady resolves only after all blocks are in the DOM (requestIdleCallback fence in Renderer),
|
|
208
250
|
// so no extra polling is needed even on slow connections.
|
|
@@ -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%;
|
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
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WAVEFORM_BUCKETS } from './constants';
|
|
2
|
+
import { liveAmplitude, headColorBlend, entranceEase } from './liveliness';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Reduce raw mono samples to `buckets` peak values normalized to 0..1.
|
|
@@ -95,16 +96,51 @@ export function attachWaveform(opts: {
|
|
|
95
96
|
canvas.className = 'blok-audio-waveform__canvas';
|
|
96
97
|
mount.appendChild(canvas);
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
+
const prefersReducedMotion = (): boolean =>
|
|
100
|
+
globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
101
|
+
|
|
102
|
+
// Animation state. `playing` is true between play and pause/ended and pins the
|
|
103
|
+
// boost energy at full. On pause we keep the loop alive briefly with `settling`
|
|
104
|
+
// so the bars glide back to their resting peaks (energy ramps 1→0) instead of
|
|
105
|
+
// snapping. `energy` reflects the current boost multiplier.
|
|
106
|
+
const SETTLE_MS = 420;
|
|
107
|
+
// The comet-head colour blooms in over ENTRANCE_MS each time playback starts.
|
|
108
|
+
const ENTRANCE_MS = 650;
|
|
109
|
+
const anim = { playing: false, settling: false, settleStart: 0, entranceStart: 0, rafId: 0 };
|
|
110
|
+
|
|
111
|
+
const energyAt = (now: number): number => {
|
|
112
|
+
if (anim.playing) return 1;
|
|
113
|
+
if (!anim.settling) return 0;
|
|
114
|
+
return Math.min(1, Math.max(0, 1 - (now - anim.settleStart) / SETTLE_MS));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const entranceAt = (now: number): number => entranceEase((now - anim.entranceStart) / ENTRANCE_MS);
|
|
118
|
+
|
|
119
|
+
const draw = (now: number): void => {
|
|
99
120
|
const rect = canvas.getBoundingClientRect();
|
|
100
121
|
if (rect.width <= 0 || rect.height <= 0) return;
|
|
101
122
|
const dpr = globalThis.devicePixelRatio || 1;
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
const targetW = Math.round(rect.width * dpr);
|
|
124
|
+
const targetH = Math.round(rect.height * dpr);
|
|
125
|
+
// Resizing the backing store clears it; only pay that cost when the box
|
|
126
|
+
// actually changed (every frame of the loop would otherwise reallocate).
|
|
127
|
+
if (canvas.width !== targetW || canvas.height !== targetH) {
|
|
128
|
+
canvas.width = targetW;
|
|
129
|
+
canvas.height = targetH;
|
|
130
|
+
}
|
|
104
131
|
const ctx = canvas.getContext('2d');
|
|
105
132
|
if (!ctx) return;
|
|
106
|
-
ctx.
|
|
133
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
134
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
135
|
+
|
|
107
136
|
const played = media.duration ? media.currentTime / media.duration : 0;
|
|
137
|
+
const playheadIndex = played * peaks.length;
|
|
138
|
+
const reduced = prefersReducedMotion();
|
|
139
|
+
const timeSeconds = now / 1000;
|
|
140
|
+
const energy = reduced ? 0 : energyAt(now);
|
|
141
|
+
const live = energy > 0;
|
|
142
|
+
const entrance = live ? entranceAt(now) : 0;
|
|
143
|
+
|
|
108
144
|
const slot = rect.width / peaks.length;
|
|
109
145
|
const gap = Math.min(2, slot * 0.34);
|
|
110
146
|
const barW = Math.max(1, slot - gap);
|
|
@@ -112,11 +148,9 @@ export function attachWaveform(opts: {
|
|
|
112
148
|
const styles = getComputedStyle(canvas);
|
|
113
149
|
const playedColor = styles.getPropertyValue('--blok-audio-bar-played').trim() || '#222';
|
|
114
150
|
const baseColor = styles.getPropertyValue('--blok-audio-bar').trim() || '#ccc';
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const y = (rect.height - h) / 2;
|
|
119
|
-
ctx.fillStyle = i / peaks.length < played ? playedColor : baseColor;
|
|
151
|
+
const headColor = styles.getPropertyValue('--blok-audio-bar-head').trim() || playedColor;
|
|
152
|
+
|
|
153
|
+
const paintBar = (x: number, y: number, h: number): void => {
|
|
120
154
|
if (typeof ctx.roundRect === 'function') {
|
|
121
155
|
ctx.beginPath();
|
|
122
156
|
ctx.roundRect(x, y, barW, h, radius);
|
|
@@ -124,26 +158,91 @@ export function attachWaveform(opts: {
|
|
|
124
158
|
} else {
|
|
125
159
|
ctx.fillRect(x, y, barW, h);
|
|
126
160
|
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
peaks.forEach((peak, i) => {
|
|
164
|
+
const amp = live
|
|
165
|
+
? liveAmplitude({ basePeak: peak, index: i, playheadIndex, timeSeconds, reduced, energy })
|
|
166
|
+
: peak;
|
|
167
|
+
const h = Math.max(2, amp * rect.height * 0.92);
|
|
168
|
+
const x = i * slot;
|
|
169
|
+
const y = (rect.height - h) / 2;
|
|
170
|
+
ctx.fillStyle = i < playheadIndex ? playedColor : baseColor;
|
|
171
|
+
paintBar(x, y, h);
|
|
172
|
+
|
|
173
|
+
// Comet-head colour shift — overlay the head tint, its alpha ramping in
|
|
174
|
+
// with the entrance and fading out on the settle (energy).
|
|
175
|
+
if (live) {
|
|
176
|
+
const blend = headColorBlend({ distance: i - playheadIndex, energy, entrance });
|
|
177
|
+
if (blend > 0.001) {
|
|
178
|
+
ctx.save();
|
|
179
|
+
ctx.globalAlpha = Math.min(1, blend);
|
|
180
|
+
ctx.fillStyle = headColor;
|
|
181
|
+
paintBar(x, y, h);
|
|
182
|
+
ctx.restore();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
127
185
|
});
|
|
128
186
|
};
|
|
129
187
|
|
|
130
|
-
const
|
|
188
|
+
const setSeekVar = (): void => {
|
|
189
|
+
mount.style.setProperty(
|
|
190
|
+
'--blok-audio-seek-pct',
|
|
191
|
+
String(media.duration ? (media.currentTime / media.duration) * 100 : 0),
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const loop = (now: number): void => {
|
|
196
|
+
draw(now);
|
|
197
|
+
// Keep looping while playing, or while the post-pause settle still has energy.
|
|
198
|
+
const keepGoing = anim.playing || (anim.settling && energyAt(now) > 0);
|
|
199
|
+
if (keepGoing) {
|
|
200
|
+
anim.rafId = requestAnimationFrame(loop);
|
|
201
|
+
} else {
|
|
202
|
+
anim.settling = false;
|
|
203
|
+
anim.rafId = 0;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const onPlay = (): void => {
|
|
208
|
+
setSeekVar();
|
|
209
|
+
anim.playing = true;
|
|
210
|
+
anim.settling = false;
|
|
211
|
+
// Restart the head-colour entrance so it blooms in on every play.
|
|
212
|
+
anim.entranceStart = globalThis.performance?.now?.() ?? 0;
|
|
213
|
+
// No continuous loop under reduced motion — timeupdate alone advances the
|
|
214
|
+
// played boundary, with no dancing bars.
|
|
215
|
+
if (!anim.rafId && !prefersReducedMotion()) anim.rafId = requestAnimationFrame(loop);
|
|
216
|
+
};
|
|
217
|
+
const onStop = (): void => {
|
|
218
|
+
// Hand off to the settle ramp so the bars glide down rather than snap. Under
|
|
219
|
+
// reduced motion there's no loop/boost to settle, so just stop.
|
|
220
|
+
if (anim.playing && !prefersReducedMotion()) {
|
|
221
|
+
anim.playing = false;
|
|
222
|
+
anim.settling = true;
|
|
223
|
+
anim.settleStart = globalThis.performance?.now?.() ?? 0;
|
|
224
|
+
if (!anim.rafId) anim.rafId = requestAnimationFrame(loop);
|
|
225
|
+
} else {
|
|
226
|
+
anim.playing = false;
|
|
227
|
+
anim.settling = false;
|
|
228
|
+
if (anim.rafId) { cancelAnimationFrame(anim.rafId); anim.rafId = 0; }
|
|
229
|
+
draw(0);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Keeps the played boundary + seek var current while paused-seeking or when
|
|
234
|
+
// the continuous loop is off (reduced motion). The loop owns drawing while it
|
|
235
|
+
// runs, so skip the redundant paint then.
|
|
131
236
|
const onTime = (): void => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
rafState.id = 0;
|
|
135
|
-
mount.style.setProperty(
|
|
136
|
-
'--blok-audio-seek-pct',
|
|
137
|
-
String(media.duration ? (media.currentTime / media.duration) * 100 : 0),
|
|
138
|
-
);
|
|
139
|
-
draw();
|
|
140
|
-
});
|
|
237
|
+
setSeekVar();
|
|
238
|
+
if (!anim.rafId) draw(0);
|
|
141
239
|
};
|
|
142
240
|
|
|
143
241
|
const seek = (clientX: number): void => {
|
|
144
242
|
if (!media.duration) return;
|
|
145
243
|
media.currentTime = ratioFromPointer(clientX, canvas.getBoundingClientRect()) * media.duration;
|
|
146
|
-
|
|
244
|
+
setSeekVar();
|
|
245
|
+
if (!anim.rafId) draw(0);
|
|
147
246
|
};
|
|
148
247
|
const drag = { active: false };
|
|
149
248
|
const onDown = (e: PointerEvent): void => { drag.active = true; seek(e.clientX); };
|
|
@@ -153,18 +252,25 @@ export function attachWaveform(opts: {
|
|
|
153
252
|
canvas.addEventListener('pointerdown', onDown);
|
|
154
253
|
globalThis.addEventListener('pointermove', onMove);
|
|
155
254
|
globalThis.addEventListener('pointerup', onUp);
|
|
255
|
+
media.addEventListener('play', onPlay);
|
|
256
|
+
media.addEventListener('pause', onStop);
|
|
257
|
+
media.addEventListener('ended', onStop);
|
|
156
258
|
media.addEventListener('timeupdate', onTime);
|
|
157
|
-
media.addEventListener('loadedmetadata',
|
|
158
|
-
draw();
|
|
259
|
+
media.addEventListener('loadedmetadata', onTime);
|
|
260
|
+
draw(0);
|
|
159
261
|
|
|
160
262
|
return {
|
|
161
263
|
destroy(): void {
|
|
162
|
-
if (
|
|
264
|
+
if (anim.rafId) cancelAnimationFrame(anim.rafId);
|
|
265
|
+
anim.playing = false;
|
|
163
266
|
canvas.removeEventListener('pointerdown', onDown);
|
|
164
267
|
globalThis.removeEventListener('pointermove', onMove);
|
|
165
268
|
globalThis.removeEventListener('pointerup', onUp);
|
|
269
|
+
media.removeEventListener('play', onPlay);
|
|
270
|
+
media.removeEventListener('pause', onStop);
|
|
271
|
+
media.removeEventListener('ended', onStop);
|
|
166
272
|
media.removeEventListener('timeupdate', onTime);
|
|
167
|
-
media.removeEventListener('loadedmetadata',
|
|
273
|
+
media.removeEventListener('loadedmetadata', onTime);
|
|
168
274
|
canvas.remove();
|
|
169
275
|
},
|
|
170
276
|
};
|
package/types/api/index.d.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor content width mode.
|
|
3
|
+
*
|
|
4
|
+
* - `'narrow'` — content is constrained to the default `--max-width-content` (the default).
|
|
5
|
+
* - `'full'` — content max-width constraint is removed so it fills its container.
|
|
6
|
+
*/
|
|
7
|
+
export type EditorWidth = 'full' | 'narrow';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Describes Blok's width API for controlling the editor content width mode.
|
|
11
|
+
*/
|
|
12
|
+
export interface Width {
|
|
13
|
+
/**
|
|
14
|
+
* Returns the current editor content width mode.
|
|
15
|
+
*
|
|
16
|
+
* @returns {EditorWidth} the active mode (defaults to `'narrow'`)
|
|
17
|
+
*/
|
|
18
|
+
get(): EditorWidth;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sets the editor content width mode.
|
|
22
|
+
*
|
|
23
|
+
* @param {EditorWidth} value - the mode to apply
|
|
24
|
+
*/
|
|
25
|
+
set(value: EditorWidth): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Toggles the editor content width mode between `'narrow'` and `'full'`.
|
|
29
|
+
*/
|
|
30
|
+
toggle(): void;
|
|
31
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
Theme,
|
|
34
34
|
ThemeMode,
|
|
35
35
|
ResolvedTheme,
|
|
36
|
+
Width,
|
|
36
37
|
} from './api';
|
|
37
38
|
|
|
38
39
|
import { OutputData } from './data-formats';
|
|
@@ -129,6 +130,8 @@ export {
|
|
|
129
130
|
Theme,
|
|
130
131
|
ThemeMode,
|
|
131
132
|
ResolvedTheme,
|
|
133
|
+
Width,
|
|
134
|
+
EditorWidth,
|
|
132
135
|
} from './api';
|
|
133
136
|
export {
|
|
134
137
|
BlockMutationType,
|
|
@@ -212,6 +215,7 @@ export class Blok {
|
|
|
212
215
|
|
|
213
216
|
public blocks: Blocks;
|
|
214
217
|
public caret: Caret;
|
|
218
|
+
public history: History;
|
|
215
219
|
public sanitizer: Sanitizer;
|
|
216
220
|
public saver: Saver;
|
|
217
221
|
public selection: Selection;
|
|
@@ -221,6 +225,7 @@ export class Blok {
|
|
|
221
225
|
public tooltip: Tooltip;
|
|
222
226
|
public readOnly: ReadOnly;
|
|
223
227
|
public theme: Theme;
|
|
228
|
+
public width: Width;
|
|
224
229
|
constructor(configuration?: BlokConfig|string);
|
|
225
230
|
|
|
226
231
|
/**
|