@jackuait/blok 0.21.0 → 0.22.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.
Files changed (35) hide show
  1. package/dist/blok.cjs +1 -1
  2. package/dist/blok.iife.js +4 -4
  3. package/dist/blok.mjs +2 -2
  4. package/dist/blok.umd.js +41 -41
  5. package/dist/chunks/{blok-Cz71HTuP.cjs → blok-Blnd71Ww.cjs} +5 -5
  6. package/dist/chunks/{blok-x8D2aON2.mjs → blok-S6c68iVJ.mjs} +2570 -2269
  7. package/dist/chunks/{constants-BK6kLyZB.cjs → constants-BIqDMhRj.cjs} +1 -1
  8. package/dist/chunks/{constants-CLzXh7Tb.mjs → constants-BvxJIgSQ.mjs} +1 -1
  9. package/dist/chunks/{tools-Bw8_CkhE.mjs → tools-B89FUeNz.mjs} +451 -416
  10. package/dist/chunks/{tools-BKjP4TXy.cjs → tools-DTjC0C5o.cjs} +3 -3
  11. package/dist/full.cjs +1 -1
  12. package/dist/full.mjs +3 -3
  13. package/dist/react.cjs +1 -1
  14. package/dist/react.mjs +1 -1
  15. package/dist/tools.cjs +1 -1
  16. package/dist/tools.mjs +2 -2
  17. package/package.json +1 -1
  18. package/src/components/modules/paste/index.ts +33 -6
  19. package/src/components/modules/paste/next-space-blocks.ts +520 -0
  20. package/src/components/modules/paste/notion-blocks-v3.ts +28 -4
  21. package/src/components/utils/media-empty-state.ts +80 -47
  22. package/src/tools/audio/empty-state.ts +4 -0
  23. package/src/tools/audio/index.ts +4 -0
  24. package/src/tools/file/empty-state.ts +4 -0
  25. package/src/tools/file/index.ts +2 -0
  26. package/src/tools/image/empty-state.ts +4 -0
  27. package/src/tools/image/index.ts +5 -0
  28. package/src/tools/video/empty-state.ts +4 -0
  29. package/src/tools/video/index.ts +4 -0
  30. package/types/tools/audio.d.ts +6 -0
  31. package/types/tools/file.d.ts +6 -0
  32. package/types/tools/image.d.ts +6 -0
  33. package/types/tools/media-source.d.ts +12 -0
  34. package/types/tools/video.d.ts +6 -0
  35. package/types/tools-entry.d.ts +1 -0
@@ -20,7 +20,7 @@
20
20
  * correctly-nested block tree.
21
21
  */
22
22
 
23
- import { colorVarName } from '../../shared/color-presets';
23
+ import { COLOR_PRESETS, colorVarName } from '../../shared/color-presets';
24
24
  import { DEFAULT_EMOJI } from '../../../tools/callout/constants';
25
25
  import { DEFAULT_LANGUAGE, LANGUAGES } from '../../../tools/code/constants';
26
26
  import { matchEmbedService } from '../../../tools/link/registry';
@@ -151,6 +151,17 @@ export function parseNotionBlocksV3(json: string): NotionParsedBlock[] | null {
151
151
 
152
152
  result.push(block);
153
153
 
154
+ // Blok's callout keeps its body in CHILD blocks, so the callout's own inline
155
+ // title line becomes a child paragraph (emitted before its `content`
156
+ // children), matching what a native callout copy carries.
157
+ if (value.type === 'callout') {
158
+ const calloutTitle = richText(value.properties?.title, byId);
159
+
160
+ if (calloutTitle.length > 0) {
161
+ result.push({ id: `${id}:callout-body`, tool: 'paragraph', data: { text: calloutTitle }, parentId: id });
162
+ }
163
+ }
164
+
154
165
  children.forEach((childId) => walk(childId, id));
155
166
  };
156
167
 
@@ -328,7 +339,10 @@ function mapValue(value: NotionValue, byId: Map<string, NotionValue>): Mapped |
328
339
  const emoji = typeof format.page_icon === 'string' && format.page_icon.length > 0 ? format.page_icon : DEFAULT_EMOJI;
329
340
  const { textColor, backgroundColor } = parseBlockColor(format.block_color);
330
341
 
331
- return { tool: 'callout', data: { emoji, text, textColor, backgroundColor } };
342
+ // No inline `text`: Blok's callout stores its body in CHILD blocks, so the
343
+ // callout's title line is emitted as a child paragraph by `walk` (and a
344
+ // stray `data.text` would be silently discarded by the callout tool).
345
+ return { tool: 'callout', data: { emoji, textColor, backgroundColor } };
332
346
  }
333
347
  case 'page':
334
348
  // A `page` block inside content is a SUB-PAGE reference: its body lives on
@@ -862,11 +876,21 @@ function buildColourStyle(colourFlags: unknown[]): string {
862
876
  return parts.join('; ');
863
877
  }
864
878
 
865
- /** Map a Notion `block_color` to callout `{ textColor, backgroundColor }` names. */
879
+ /** The set of colour preset names Blok recognises (its callout/marker palette). */
880
+ const BLOK_COLOR_NAMES = new Set(COLOR_PRESETS.map((preset) => preset.name));
881
+
882
+ /**
883
+ * Map a Notion `block_color` to callout `{ textColor, backgroundColor }` names.
884
+ * Names outside Blok's preset palette collapse to `null`: the callout tool
885
+ * builds `var(--blok-color-<name>-…)` from the name verbatim, so an unrecognised
886
+ * name yields an undefined custom property that silently drops the colour AND
887
+ * the callout's border. Notion's palette already aligns with Blok's, so this is
888
+ * a safety clamp rather than a remap.
889
+ */
866
890
  function parseBlockColor(token: unknown): { textColor: string | null; backgroundColor: string | null } {
867
891
  const colour = parseNotionColour(token);
868
892
 
869
- if (colour === null) {
893
+ if (colour === null || !BLOK_COLOR_NAMES.has(colour.name)) {
870
894
  return { textColor: null, backgroundColor: null };
871
895
  }
872
896
 
@@ -1,3 +1,4 @@
1
+ import type { MediaSource } from '../../../types/tools/media-source';
1
2
  import {
2
3
  IconArrowDownLine,
3
4
  IconArrowUp,
@@ -48,6 +49,15 @@ export interface MediaEmptyStateOptions {
48
49
  labels: MediaEmptyStateLabels;
49
50
  onFile(file: File): void;
50
51
  onUrl(url: string): void;
52
+ /**
53
+ * Which insert sources to expose:
54
+ * - `'both'` (default): show the Upload and Link tabs.
55
+ * - `'upload'`: file upload only — the Link tab and URL bar are hidden.
56
+ * - `'url'`: link/embed only — the Upload dropzone, file picker, and
57
+ * drag/paste-to-upload affordances are hidden.
58
+ * With a single source the tablist is omitted entirely.
59
+ */
60
+ sources?: MediaSource;
51
61
  /**
52
62
  * How the panel transitions when switching Upload/Link tabs:
53
63
  * - `'reflow'` (default): ease the panel height between the two layouts and
@@ -64,6 +74,9 @@ export interface MediaEmptyStateOptions {
64
74
 
65
75
  export type MediaEmptyStateSwap = 'reflow' | 'slide' | 'none';
66
76
 
77
+ /** Re-exported from the public type so the renderer and tools share one source. */
78
+ export type { MediaSource };
79
+
67
80
  const MIME_LABELS: Record<string, string> = {
68
81
  'image/jpeg': 'JPG',
69
82
  'image/jpg': 'JPG',
@@ -213,6 +226,10 @@ export function renderMediaEmptyState(opts: MediaEmptyStateOptions): MediaEmptyS
213
226
 
214
227
  const swapMode: MediaEmptyStateSwap = opts.swap ?? 'reflow';
215
228
 
229
+ const source: MediaSource = opts.sources ?? 'both';
230
+ const showUpload = source !== 'url';
231
+ const showEmbed = source !== 'upload';
232
+
216
233
  const root = document.createElement('div') as unknown as MediaEmptyStateElement;
217
234
  root.className = 'blok-media-empty';
218
235
  root.setAttribute('data-blok-media-empty-state', '');
@@ -233,17 +250,27 @@ export function renderMediaEmptyState(opts: MediaEmptyStateOptions): MediaEmptyS
233
250
  panel.setAttribute('data-panel-host', '');
234
251
  panel.setAttribute('role', 'tabpanel');
235
252
 
253
+ const initialKind: SourceKind = showUpload ? 'upload' : 'embed';
254
+
236
255
  const tabs = document.createElement('div');
237
256
  tabs.className = 'blok-media-empty__tabs';
238
257
  tabs.setAttribute('role', 'tablist');
239
258
  tabs.setAttribute('aria-label', labels.sourceAria);
240
- const uploadTab = makeTab('upload', labels.upload, panel.id, true);
241
- const embedTab = makeTab('embed', labels.embed, panel.id, false);
242
- tabs.append(uploadTab, embedTab);
243
- panel.setAttribute('aria-labelledby', uploadTab.id);
244
-
245
- const tabList = [uploadTab, embedTab];
246
- const tabKinds: SourceKind[] = ['upload', 'embed'];
259
+ const tabList: HTMLButtonElement[] = [];
260
+ const tabKinds: SourceKind[] = [];
261
+ if (showUpload) {
262
+ const uploadTab = makeTab('upload', labels.upload, panel.id, true);
263
+ tabs.append(uploadTab);
264
+ tabList.push(uploadTab);
265
+ tabKinds.push('upload');
266
+ }
267
+ if (showEmbed) {
268
+ const embedTab = makeTab('embed', labels.embed, panel.id, !showUpload);
269
+ tabs.append(embedTab);
270
+ tabList.push(embedTab);
271
+ tabKinds.push('embed');
272
+ }
273
+ panel.setAttribute('aria-labelledby', tabList[0].id);
247
274
 
248
275
  const dropOverlay = document.createElement('div');
249
276
  dropOverlay.className = 'blok-media-empty__drop-overlay';
@@ -456,53 +483,59 @@ export function renderMediaEmptyState(opts: MediaEmptyStateOptions): MediaEmptyS
456
483
  }
457
484
  });
458
485
 
459
- card.addEventListener('paste', (ev) => {
460
- if (card.getAttribute('data-active-tab') !== 'upload') return;
461
- const clip = ev.clipboardData;
462
- if (!clip) return;
463
- for (const item of Array.from(clip.items)) {
464
- if (item.kind !== 'file') continue;
465
- const file = item.getAsFile();
466
- if (file && (!types.length || matchesMime(file.type, types))) {
467
- ev.preventDefault();
468
- opts.onFile(file);
469
- return;
486
+ // File-based affordances (paste/drag/drop) only apply when upload is enabled.
487
+ if (showUpload) {
488
+ card.addEventListener('paste', (ev) => {
489
+ if (card.getAttribute('data-active-tab') !== 'upload') return;
490
+ const clip = ev.clipboardData;
491
+ if (!clip) return;
492
+ for (const item of Array.from(clip.items)) {
493
+ if (item.kind !== 'file') continue;
494
+ const file = item.getAsFile();
495
+ if (file && (!types.length || matchesMime(file.type, types))) {
496
+ ev.preventDefault();
497
+ opts.onFile(file);
498
+ return;
499
+ }
470
500
  }
471
- }
472
- });
501
+ });
473
502
 
474
- const drag = { depth: 0 };
475
- card.addEventListener('dragenter', (ev) => {
476
- if (!ev.dataTransfer?.types.includes('Files')) return;
477
- ev.preventDefault();
478
- drag.depth += 1;
479
- card.classList.add('is-dragover');
480
- });
481
- card.addEventListener('dragover', (ev) => {
482
- const dt = ev.dataTransfer;
483
- if (!dt?.types.includes('Files')) return;
484
- ev.preventDefault();
485
- dt.dropEffect = 'copy';
486
- });
487
- card.addEventListener('dragleave', () => {
488
- drag.depth = Math.max(0, drag.depth - 1);
489
- if (drag.depth === 0) card.classList.remove('is-dragover');
490
- });
491
- card.addEventListener('drop', (ev) => {
492
- ev.preventDefault();
493
- drag.depth = 0;
494
- card.classList.remove('is-dragover');
495
- const file = ev.dataTransfer?.files?.[0];
496
- if (file) opts.onFile(file);
497
- });
503
+ const drag = { depth: 0 };
504
+ card.addEventListener('dragenter', (ev) => {
505
+ if (!ev.dataTransfer?.types.includes('Files')) return;
506
+ ev.preventDefault();
507
+ drag.depth += 1;
508
+ card.classList.add('is-dragover');
509
+ });
510
+ card.addEventListener('dragover', (ev) => {
511
+ const dt = ev.dataTransfer;
512
+ if (!dt?.types.includes('Files')) return;
513
+ ev.preventDefault();
514
+ dt.dropEffect = 'copy';
515
+ });
516
+ card.addEventListener('dragleave', () => {
517
+ drag.depth = Math.max(0, drag.depth - 1);
518
+ if (drag.depth === 0) card.classList.remove('is-dragover');
519
+ });
520
+ card.addEventListener('drop', (ev) => {
521
+ ev.preventDefault();
522
+ drag.depth = 0;
523
+ card.classList.remove('is-dragover');
524
+ const file = ev.dataTransfer?.files?.[0];
525
+ if (file) opts.onFile(file);
526
+ });
527
+ }
498
528
 
499
529
  const header = document.createElement('div');
500
530
  header.className = 'blok-media-empty__header';
501
- header.append(label, tabs);
531
+ // A single source needs no tablist — show just the caption.
532
+ if (tabList.length > 1) header.append(label, tabs);
533
+ else header.append(label);
502
534
 
503
- card.append(header, panel, dropOverlay);
535
+ card.append(header, panel);
536
+ if (showUpload) card.append(dropOverlay);
504
537
  root.append(card, error);
505
- activate('upload');
538
+ activate(initialKind);
506
539
 
507
540
  root.setError = (message: string | null): void => {
508
541
  if (!message) {
@@ -3,6 +3,7 @@ import { tr } from './i18n';
3
3
  import {
4
4
  renderMediaEmptyState,
5
5
  type MediaEmptyStateElement,
6
+ type MediaSource,
6
7
  } from '../../components/utils/media-empty-state';
7
8
  import type { I18nInstance } from '../../components/utils/tools';
8
9
 
@@ -13,6 +14,8 @@ export interface EmptyStateOptions {
13
14
  acceptTypes?: string[];
14
15
  /** Max file size in bytes — surfaced in the formats hint when set. */
15
16
  maxSize?: number;
17
+ /** Which insert sources to expose. Default `'both'`. */
18
+ sources?: MediaSource;
16
19
  i18n?: I18nInstance;
17
20
  }
18
21
 
@@ -23,6 +26,7 @@ export function renderEmptyState(opts: EmptyStateOptions): EmptyStateElement {
23
26
  return renderMediaEmptyState({
24
27
  acceptTypes: opts.acceptTypes ?? [...DEFAULT_MIME_TYPES],
25
28
  maxSize: opts.maxSize,
29
+ sources: opts.sources,
26
30
  onFile: opts.onFile,
27
31
  onUrl: opts.onUrl,
28
32
  labels: {
@@ -118,11 +118,14 @@ export class AudioTool implements BlockTool {
118
118
  }
119
119
 
120
120
  public onPaste(event: PasteEvent): void {
121
+ const sources = this.config.sources;
121
122
  if (event.type === 'pattern') {
123
+ if (sources === 'upload') return;
122
124
  this.applyResult({ url: (event as PatternPasteEvent).detail.data });
123
125
  return;
124
126
  }
125
127
  if (event.type === 'file') {
128
+ if (sources === 'url') return;
126
129
  this.startUpload((event as FilePasteEvent).detail.file);
127
130
  }
128
131
  }
@@ -364,6 +367,7 @@ export class AudioTool implements BlockTool {
364
367
  onUrl: (url) => this.startUrl(url),
365
368
  acceptTypes: this.config.types,
366
369
  maxSize: pickDisplayMaxSize(this.config.maxSize),
370
+ sources: this.config.sources,
367
371
  i18n: this.api.i18n,
368
372
  });
369
373
  this.root.appendChild(el);
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  renderMediaEmptyState,
3
3
  type MediaEmptyStateElement,
4
+ type MediaSource,
4
5
  } from '../../components/utils/media-empty-state';
5
6
  import type { I18nInstance } from '../../components/utils/tools';
6
7
 
@@ -11,6 +12,8 @@ export interface EmptyStateOptions {
11
12
  acceptTypes: string[];
12
13
  /** Max file size in bytes — surfaced in the formats hint when set. */
13
14
  maxSize?: number;
15
+ /** Which insert sources to expose. Default `'both'`. */
16
+ sources?: MediaSource;
14
17
  i18n: I18nInstance;
15
18
  onFile(file: File): void;
16
19
  onUrl(url: string): void;
@@ -21,6 +24,7 @@ export function renderEmptyState(opts: EmptyStateOptions): EmptyStateElement {
21
24
  return renderMediaEmptyState({
22
25
  acceptTypes: opts.acceptTypes,
23
26
  maxSize: opts.maxSize,
27
+ sources: opts.sources,
24
28
  onFile: opts.onFile,
25
29
  onUrl: opts.onUrl,
26
30
  labels: {
@@ -98,6 +98,7 @@ export class FileTool implements BlockTool {
98
98
 
99
99
  public onPaste(event: PasteEvent): void {
100
100
  if (event.type === 'file') {
101
+ if (this.config.sources === 'url') return;
101
102
  this.startUpload((event as FilePasteEvent).detail.file);
102
103
  }
103
104
  }
@@ -346,6 +347,7 @@ export class FileTool implements BlockTool {
346
347
  private buildEmpty(): EmptyStateElement {
347
348
  return renderEmptyState({
348
349
  acceptTypes: this.config.types ?? [],
350
+ sources: this.config.sources,
349
351
  i18n: this.api.i18n,
350
352
  onFile: (file) => this.startUpload(file),
351
353
  onUrl: (url) => this.startUrl(url),
@@ -3,6 +3,7 @@ import { tr } from './i18n';
3
3
  import {
4
4
  renderMediaEmptyState,
5
5
  type MediaEmptyStateElement,
6
+ type MediaSource,
6
7
  } from '../../components/utils/media-empty-state';
7
8
  import type { I18nInstance } from '../../components/utils/tools';
8
9
 
@@ -13,6 +14,8 @@ export interface EmptyStateOptions {
13
14
  acceptTypes?: string[];
14
15
  /** Max file size in bytes — surfaced in the formats hint when set. */
15
16
  maxSize?: number;
17
+ /** Which insert sources to expose. Default `'both'`. */
18
+ sources?: MediaSource;
16
19
  uploadLabel?: string;
17
20
  embedLabel?: string;
18
21
  embedPlaceholder?: string;
@@ -27,6 +30,7 @@ export function renderEmptyState(opts: EmptyStateOptions): EmptyStateElement {
27
30
  return renderMediaEmptyState({
28
31
  acceptTypes: opts.acceptTypes ?? [...DEFAULT_MIME_TYPES],
29
32
  maxSize: opts.maxSize,
33
+ sources: opts.sources,
30
34
  onFile: opts.onFile,
31
35
  onUrl: opts.onUrl,
32
36
  labels: {
@@ -156,7 +156,9 @@ export class ImageTool implements BlockTool {
156
156
  }
157
157
 
158
158
  public onPaste(event: PasteEvent): void {
159
+ const sources = this.config.sources;
159
160
  if (event.type === 'pattern') {
161
+ if (sources === 'upload') return;
160
162
  const detail = (event as PatternPasteEvent).detail;
161
163
  if (this.shouldConvertGifUrl(detail.data)) {
162
164
  this.startUrl(detail.data);
@@ -166,6 +168,7 @@ export class ImageTool implements BlockTool {
166
168
  return;
167
169
  }
168
170
  if (event.type === 'tag') {
171
+ if (sources === 'upload') return;
169
172
  const node = (event as HTMLPasteEvent).detail.data;
170
173
  const src = node?.getAttribute?.('src') ?? '';
171
174
  if (!src) return;
@@ -173,6 +176,7 @@ export class ImageTool implements BlockTool {
173
176
  return;
174
177
  }
175
178
  if (event.type === 'file') {
179
+ if (sources === 'url') return;
176
180
  const detail = (event as FilePasteEvent).detail;
177
181
  this.startUpload(detail.file);
178
182
  }
@@ -687,6 +691,7 @@ export class ImageTool implements BlockTool {
687
691
  onUrl: (url) => this.startUrl(url),
688
692
  acceptTypes: this.config.types,
689
693
  maxSize: pickDisplayMaxSize(this.config.maxSize),
694
+ sources: this.config.sources,
690
695
  i18n: this.api.i18n,
691
696
  });
692
697
  this.emptyStateEl = el;
@@ -3,6 +3,7 @@ import { tr } from './i18n';
3
3
  import {
4
4
  renderMediaEmptyState,
5
5
  type MediaEmptyStateElement,
6
+ type MediaSource,
6
7
  } from '../../components/utils/media-empty-state';
7
8
  import type { I18nInstance } from '../../components/utils/tools';
8
9
 
@@ -13,6 +14,8 @@ export interface EmptyStateOptions {
13
14
  acceptTypes?: string[];
14
15
  /** Max file size in bytes — surfaced in the formats hint when set. */
15
16
  maxSize?: number;
17
+ /** Which insert sources to expose. Default `'both'`. */
18
+ sources?: MediaSource;
16
19
  i18n?: I18nInstance;
17
20
  }
18
21
 
@@ -23,6 +26,7 @@ export function renderEmptyState(opts: EmptyStateOptions): EmptyStateElement {
23
26
  return renderMediaEmptyState({
24
27
  acceptTypes: opts.acceptTypes ?? [...DEFAULT_MIME_TYPES],
25
28
  maxSize: opts.maxSize,
29
+ sources: opts.sources,
26
30
  onFile: opts.onFile,
27
31
  onUrl: opts.onUrl,
28
32
  labels: {
@@ -112,11 +112,14 @@ export class VideoTool implements BlockTool {
112
112
  }
113
113
 
114
114
  public onPaste(event: PasteEvent): void {
115
+ const sources = this.config.sources;
115
116
  if (event.type === 'pattern') {
117
+ if (sources === 'upload') return;
116
118
  this.applyResult({ url: (event as PatternPasteEvent).detail.data });
117
119
  return;
118
120
  }
119
121
  if (event.type === 'file') {
122
+ if (sources === 'url') return;
120
123
  this.startUpload((event as FilePasteEvent).detail.file);
121
124
  }
122
125
  }
@@ -322,6 +325,7 @@ export class VideoTool implements BlockTool {
322
325
  onUrl: (url) => this.startUrl(url),
323
326
  acceptTypes: this.config.types,
324
327
  maxSize: pickDisplayMaxSize(this.config.maxSize),
328
+ sources: this.config.sources,
325
329
  i18n: this.api.i18n,
326
330
  });
327
331
  this.root.appendChild(el);
@@ -1,5 +1,6 @@
1
1
  import { BlockToolData } from './block-tool-data';
2
2
  import { MaxSizeConfig } from './max-size';
3
+ import { MediaSource } from './media-source';
3
4
 
4
5
  export type AudioAlignment = 'left' | 'center' | 'right';
5
6
 
@@ -36,5 +37,10 @@ export interface AudioConfig {
36
37
  */
37
38
  types?: string[];
38
39
  maxSize?: MaxSizeConfig;
40
+ /**
41
+ * Restrict how audio may be added. Default `'both'` (Upload + Link).
42
+ * Use `'upload'` for file-only or `'url'` for link-only. See {@link MediaSource}.
43
+ */
44
+ sources?: MediaSource;
39
45
  captionPlaceholder?: string;
40
46
  }
@@ -1,5 +1,6 @@
1
1
  import { BlockToolData } from './block-tool-data';
2
2
  import { MaxSizeConfig } from './max-size';
3
+ import { MediaSource } from './media-source';
3
4
 
4
5
  /** Persisted data shape for the File block tool. */
5
6
  export interface FileData extends BlockToolData {
@@ -69,6 +70,11 @@ export interface FileConfig {
69
70
  * unlimited. See {@link MaxSizeConfig}.
70
71
  */
71
72
  maxSize?: MaxSizeConfig;
73
+ /**
74
+ * Restrict how a file may be added. Default `'both'` (Upload + Link).
75
+ * Use `'upload'` for file-only or `'url'` for link-only. See {@link MediaSource}.
76
+ */
77
+ sources?: MediaSource;
72
78
  /** Caption placeholder. */
73
79
  captionPlaceholder?: string;
74
80
  }
@@ -1,5 +1,6 @@
1
1
  import { BlockToolData } from './block-tool-data';
2
2
  import { MaxSizeConfig } from './max-size';
3
+ import { MediaSource } from './media-source';
3
4
 
4
5
  /** Horizontal alignment of the image within its container. */
5
6
  export type ImageAlignment = 'left' | 'center' | 'right';
@@ -88,6 +89,11 @@ export interface ImageConfig {
88
89
  * type with `'*'` as the fallback. Default 30 MiB. See {@link MaxSizeConfig}.
89
90
  */
90
91
  maxSize?: MaxSizeConfig;
92
+ /**
93
+ * Restrict how an image may be added. Default `'both'` (Upload + Link).
94
+ * Use `'upload'` for file-only or `'url'` for link-only. See {@link MediaSource}.
95
+ */
96
+ sources?: MediaSource;
91
97
  /**
92
98
  * Auto-convert animated GIFs to a looping WebM video block on insert.
93
99
  * Default true. Set false to keep GIFs as image blocks.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Which insert sources a media tool (image, video, audio, file) exposes in its
3
+ * empty state.
4
+ *
5
+ * - `'both'` (default): offer both file Upload and Link (URL/embed) tabs.
6
+ * - `'upload'`: file upload only — the Link tab and URL bar are hidden.
7
+ * - `'url'`: link/embed only — the Upload dropzone, file picker, and
8
+ * drag/paste-to-upload affordances are hidden.
9
+ *
10
+ * With a single source the tab switcher is omitted entirely.
11
+ */
12
+ export type MediaSource = 'upload' | 'url' | 'both';
@@ -1,5 +1,6 @@
1
1
  import { BlockToolData } from './block-tool-data';
2
2
  import { MaxSizeConfig } from './max-size';
3
+ import { MediaSource } from './media-source';
3
4
 
4
5
  /** Horizontal alignment of the video within its container. */
5
6
  export type VideoAlignment = 'left' | 'center' | 'right';
@@ -68,6 +69,11 @@ export interface VideoConfig {
68
69
  * type with `'*'` as the fallback. Default 100 MiB. See {@link MaxSizeConfig}.
69
70
  */
70
71
  maxSize?: MaxSizeConfig;
72
+ /**
73
+ * Restrict how a video may be added. Default `'both'` (Upload + Link).
74
+ * Use `'upload'` for file-only or `'url'` for link-only. See {@link MediaSource}.
75
+ */
76
+ sources?: MediaSource;
71
77
  /** Caption placeholder. Default "Write a caption…" */
72
78
  captionPlaceholder?: string;
73
79
  /** Ambient glow intensity behind every player. Default 'minimal'. */
@@ -61,6 +61,7 @@ export { ImageData, ImageConfig, ImageUploader, ImageAlignment, ImageSize, Image
61
61
  export { FileData, FileConfig, FileUploader, FileUploadContext, FileUploadResult } from './tools/file';
62
62
  export { AudioData, AudioConfig, AudioUploader, AudioUploadContext, AudioAlignment } from './tools/audio';
63
63
  export { VideoData, VideoConfig, VideoUploader, VideoUploadContext, VideoAlignment, VideoGlow } from './tools/video';
64
+ export { MediaSource } from './tools/media-source';
64
65
  export { ColumnListData } from './tools/column-list';
65
66
  export { ColumnData } from './tools/column';
66
67