```
refactor: improve block UI consistency and comment icon styling - Wrapped heading blocks in rounded pill containers matching paragraph block styling - Unified comment icon design across all block types (full-width and columns) with consistent SVG speech bubble - Reduced block wrapper vertical padding (py-1 → py-0.5) and inline toolbar padding for more compact layout - Added right padding to block content (pr-8) to prevent comment icon overlap with colored backgrounds - Excluded paragraph,
This commit is contained in:
parent
ba86bd4b91
commit
8b2510e9cc
@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, Output, EventEmitter, inject, HostListener, ElementRef, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
|
import { Component, Input, Output, EventEmitter, inject, HostListener, ElementRef, OnChanges, SimpleChanges, ViewChild, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Block, BlockType } from '../../core/models/block.model';
|
import { Block, BlockType } from '../../core/models/block.model';
|
||||||
import { DocumentService } from '../../services/document.service';
|
import { DocumentService } from '../../services/document.service';
|
||||||
@ -818,7 +818,7 @@ export interface MenuAction {
|
|||||||
@keyframes fadeIn { from { opacity:0; transform: scale(.97);} to { opacity:1; transform: scale(1);} }
|
@keyframes fadeIn { from { opacity:0; transform: scale(.97);} to { opacity:1; transform: scale(1);} }
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class BlockContextMenuComponent implements OnChanges {
|
export class BlockContextMenuComponent implements OnChanges, OnDestroy {
|
||||||
@Input() block!: Block;
|
@Input() block!: Block;
|
||||||
@Input() visible = false;
|
@Input() visible = false;
|
||||||
@Input() position = { x: 0, y: 0 };
|
@Input() position = { x: 0, y: 0 };
|
||||||
@ -838,15 +838,28 @@ export class BlockContextMenuComponent implements OnChanges {
|
|||||||
top = -9999;
|
top = -9999;
|
||||||
opacity = 0;
|
opacity = 0;
|
||||||
|
|
||||||
|
private appendedToBody = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
try {
|
try {
|
||||||
const el = this.elementRef.nativeElement as HTMLElement;
|
const el = this.elementRef.nativeElement as HTMLElement;
|
||||||
if (el && el.parentElement !== document.body) {
|
if (el && el.parentElement !== document.body) {
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
|
this.appendedToBody = true;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
try {
|
||||||
|
const el = this.elementRef.nativeElement as HTMLElement;
|
||||||
|
if (el && el.parentElement === document.body && this.appendedToBody) {
|
||||||
|
document.body.removeChild(el);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
this.closeSubmenu();
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('document:click', ['$event'])
|
@HostListener('document:click', ['$event'])
|
||||||
onDocumentClick(event: MouseEvent): void {
|
onDocumentClick(event: MouseEvent): void {
|
||||||
const root = this.menuRef?.nativeElement;
|
const root = this.menuRef?.nativeElement;
|
||||||
@ -1217,6 +1230,11 @@ export class BlockContextMenuComponent implements OnChanges {
|
|||||||
// Emit action for parent to handle (including ratios/alignment payload)
|
// Emit action for parent to handle (including ratios/alignment payload)
|
||||||
this.action.emit({ type, payload });
|
this.action.emit({ type, payload });
|
||||||
}
|
}
|
||||||
|
// Locally hide the menu to ensure it disappears immediately, even if the host is destroyed right after
|
||||||
|
this.visible = false;
|
||||||
|
this.opacity = 0;
|
||||||
|
this.left = -9999;
|
||||||
|
this.top = -9999;
|
||||||
this.close.emit();
|
this.close.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ import { CollapsibleBlockComponent } from './blocks/collapsible-block.component'
|
|||||||
[attr.data-block-index]="index"
|
[attr.data-block-index]="index"
|
||||||
[class.active]="isActive()"
|
[class.active]="isActive()"
|
||||||
[class.locked]="block.meta?.locked"
|
[class.locked]="block.meta?.locked"
|
||||||
[style.background-color]="(block.type === 'list-item' || block.type === 'file') ? null : block.meta?.bgColor"
|
[style.background-color]="(block.type === 'list-item' || block.type === 'file' || block.type === 'paragraph' || block.type === 'list' || block.type === 'heading') ? null : block.meta?.bgColor"
|
||||||
[ngStyle]="blockStyles()"
|
[ngStyle]="blockStyles()"
|
||||||
(click)="onBlockClick($event)"
|
(click)="onBlockClick($event)"
|
||||||
>
|
>
|
||||||
@ -101,8 +101,8 @@ import { CollapsibleBlockComponent } from './blocks/collapsible-block.component'
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Block content -->
|
<!-- Block content (extra right padding so the comment icon sits on the dark background, not on top of the pill) -->
|
||||||
<div class="block-content" [class.locked]="block.meta?.locked">
|
<div class="block-content pr-8" [class.locked]="block.meta?.locked">
|
||||||
@switch (block.type) {
|
@switch (block.type) {
|
||||||
@case ('paragraph') {
|
@case ('paragraph') {
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@ -191,16 +191,38 @@ import { CollapsibleBlockComponent } from './blocks/collapsible-block.component'
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="block.type !== 'table' && block.type !== 'columns'">
|
<ng-container *ngIf="block.type !== 'table' && block.type !== 'columns'">
|
||||||
<!-- Filled white speech bubble with count (count in black) -->
|
<!-- Filled comment icon with count -->
|
||||||
<button *ngIf="totalComments() > 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 flex items-center justify-center z-20"
|
<button *ngIf="totalComments() > 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 flex items-center justify-center z-20"
|
||||||
title="View comments" (click)="openComments()">
|
title="View comments" (click)="openComments()">
|
||||||
<svg viewBox="0 0 24 24" class="w-7 h-7"><path fill="#e5e7eb" d="M12 4c4.97 0 9 2.96 9 6.6S16.97 17.2 12 17.2c-.75 0-1.49-.07-2.2-.22L6 20l1.57-3.14C6.13 16 3 13.86 3 10.6 3 6.96 7.03 4 12 4z"/></svg>
|
<svg viewBox="0 0 64 64" class="w-5 h-5 text-gray-100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M32 8C19.85 8 10 17.85 10 30c0 4.2 1.2 8.3 3.3 11.7L10 56l13.1-4.4C25.9 53.2 28.9 54 32 54c12.15 0 22-9.85 22-22S44.15 8 32 8Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="24" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="32" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="40" cy="30" r="3" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
<span class="absolute text-[11px] font-semibold text-black">{{ totalComments() }}</span>
|
<span class="absolute text-[11px] font-semibold text-black">{{ totalComments() }}</span>
|
||||||
</button>
|
</button>
|
||||||
<!-- Outline bubble (no comments) -->
|
<!-- Outline comment icon when there are no comments -->
|
||||||
<button *ngIf="totalComments() === 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-20"
|
<button *ngIf="totalComments() === 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-20"
|
||||||
title="Add a comment" (click)="openComments()">
|
title="Add a comment" (click)="openComments()">
|
||||||
<svg viewBox="0 0 24 24" class="w-7 h-7"><path fill="none" stroke="#e5e7eb" stroke-width="1.5" d="M12 4c4.97 0 9 2.96 9 6.6S16.97 17.2 12 17.2c-.75 0-1.49-.07-2.2-.22L6 20l1.57-3.14C6.13 16 3 13.86 3 10.6 3 6.96 7.03 4 12 4z"/></svg>
|
<svg viewBox="0 0 64 64" class="w-5 h-5 text-gray-100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M32 8C19.85 8 10 17.85 10 30c0 4.2 1.2 8.3 3.3 11.7L10 56l13.1-4.4C25.9 53.2 28.9 54 32 54c12.15 0 22-9.85 22-22S44.15 8 32 8Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="24" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="32" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="40" cy="30" r="3" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@ -216,7 +238,7 @@ import { CollapsibleBlockComponent } from './blocks/collapsible-block.component'
|
|||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [`
|
||||||
.block-wrapper {
|
.block-wrapper {
|
||||||
@apply relative py-1 px-3 rounded-md transition-all;
|
@apply relative py-0.5 px-3 rounded-md transition-all;
|
||||||
/* No fixed min-height; let content define height */
|
/* No fixed min-height; let content define height */
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -691,6 +713,7 @@ export class BlockHostComponent implements OnDestroy {
|
|||||||
this.documentService.duplicateBlock(this.block.id);
|
this.documentService.duplicateBlock(this.block.id);
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
this.closeMenu();
|
||||||
this.documentService.deleteBlock(this.block.id);
|
this.documentService.deleteBlock(this.block.id);
|
||||||
break;
|
break;
|
||||||
case 'lock':
|
case 'lock':
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export interface InlineToolbarAction {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
template: `
|
template: `
|
||||||
<div class="group/block relative flex items-center justify-between gap-2 bg-[var(--block-bg,var(--editor-bg))] rounded-2xl px-4 py-1 shadow-sm z-[10]">
|
<div class="group/block relative flex items-center justify-between gap-2 bg-[var(--block-bg,var(--editor-bg))] rounded-sm px-4 py-0.5 shadow-sm z-[10]">
|
||||||
<!-- Drag handle (visible on hover) -->
|
<!-- Drag handle (visible on hover) -->
|
||||||
@if (showDragHandle) {
|
@if (showDragHandle) {
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { Component, Input, Output, EventEmitter, inject, ViewChild, signal, effect, ElementRef, HostListener, AfterViewInit } from '@angular/core';
|
import { Component, Input, Output, EventEmitter, inject, ViewChild, signal, effect, ElementRef, HostListener, AfterViewInit, OnDestroy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Block, ColumnsProps, ColumnItem } from '../../../core/models/block.model';
|
import { Block, ColumnsProps, ColumnItem } from '../../../core/models/block.model';
|
||||||
import { DragDropService } from '../../../services/drag-drop.service';
|
import { DragDropService } from '../../../services/drag-drop.service';
|
||||||
import { CommentService } from '../../../services/comment.service';
|
|
||||||
import { DocumentService } from '../../../services/document.service';
|
import { DocumentService } from '../../../services/document.service';
|
||||||
import { SelectionService } from '../../../services/selection.service';
|
import { SelectionService } from '../../../services/selection.service';
|
||||||
|
import { CommentStoreService } from '../../../services/comment-store.service';
|
||||||
|
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||||
|
import { ComponentPortal } from '@angular/cdk/portal';
|
||||||
|
|
||||||
// Import ALL block components for full support
|
// Import ALL block components for full support
|
||||||
import { ParagraphBlockComponent } from './paragraph-block.component';
|
import { ParagraphBlockComponent } from './paragraph-block.component';
|
||||||
@ -27,7 +29,7 @@ import { KanbanBlockComponent } from './kanban-block.component';
|
|||||||
import { EmbedBlockComponent } from './embed-block.component';
|
import { EmbedBlockComponent } from './embed-block.component';
|
||||||
import { OutlineBlockComponent } from './outline-block.component';
|
import { OutlineBlockComponent } from './outline-block.component';
|
||||||
import { ListBlockComponent } from './list-block.component';
|
import { ListBlockComponent } from './list-block.component';
|
||||||
import { CommentsPanelComponent } from '../../comments/comments-panel.component';
|
import { BlockCommentComposerComponent } from '../../comment/block-comment-composer.component';
|
||||||
import { BlockContextMenuComponent } from '../block-context-menu.component';
|
import { BlockContextMenuComponent } from '../block-context-menu.component';
|
||||||
import { DragDropFilesDirective } from '../../../../blocks/file/directives/drag-drop-files.directive';
|
import { DragDropFilesDirective } from '../../../../blocks/file/directives/drag-drop-files.directive';
|
||||||
import { PaletteItem } from '../../../core/constants/palette-items';
|
import { PaletteItem } from '../../../core/constants/palette-items';
|
||||||
@ -57,7 +59,6 @@ import { PaletteItem } from '../../../core/constants/palette-items';
|
|||||||
EmbedBlockComponent,
|
EmbedBlockComponent,
|
||||||
OutlineBlockComponent,
|
OutlineBlockComponent,
|
||||||
ListBlockComponent,
|
ListBlockComponent,
|
||||||
CommentsPanelComponent,
|
|
||||||
BlockContextMenuComponent,
|
BlockContextMenuComponent,
|
||||||
DragDropFilesDirective
|
DragDropFilesDirective
|
||||||
],
|
],
|
||||||
@ -80,12 +81,12 @@ import { PaletteItem } from '../../../core/constants/palette-items';
|
|||||||
<!-- Menu button (3 dots) - Outside left, centered vertically -->
|
<!-- Menu button (3 dots) - Outside left, centered vertically -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="menu-handle absolute -left-9 top-1/2 -translate-y-1/2 w-7 h-7 flex items-center justify-center opacity-0 group-hover/block:opacity-100 transition-opacity bg-gray-700 hover:bg-gray-600 rounded-md z-10"
|
class="menu-handle opacity-0 group-hover/block:opacity-100 transition-opacity absolute -left-5 top-1/2 -translate-y-1/2 w-5 h-5 flex items-center justify-center rounded-full bg-surface2/80 hover:bg-surface2 dark:bg-gray-700/80 dark:hover:bg-gray-700 shadow-sm z-10"
|
||||||
title="Drag to move or click for menu"
|
title="Drag to move or click for menu"
|
||||||
(click)="openMenu(block, $event)"
|
(click)="openMenu(block, $event)"
|
||||||
(mousedown)="onDragStart(block, colIndex, blockIndex, $event)"
|
(mousedown)="onDragStart(block, colIndex, blockIndex, $event)"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 text-gray-300" viewBox="0 0 16 16" fill="currentColor">
|
<svg class="w-2 h-2 text-gray-300" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<circle cx="3" cy="8" r="1.5"/>
|
<circle cx="3" cy="8" r="1.5"/>
|
||||||
<circle cx="8" cy="8" r="1.5"/>
|
<circle cx="8" cy="8" r="1.5"/>
|
||||||
<circle cx="13" cy="8" r="1.5"/>
|
<circle cx="13" cy="8" r="1.5"/>
|
||||||
@ -100,16 +101,40 @@ import { PaletteItem } from '../../../core/constants/palette-items';
|
|||||||
[style.background-color]="getBlockBgColor(block)"
|
[style.background-color]="getBlockBgColor(block)"
|
||||||
[ngStyle]="getBlockStyles(block)"
|
[ngStyle]="getBlockStyles(block)"
|
||||||
>
|
>
|
||||||
<!-- Comment icon inside the block, aligned to the right -->
|
<!-- Comment icon inside the block, aligned to the right (same size/position as full-width blocks) -->
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<button *ngIf="getBlockCommentCount(block.id) > 0" class="absolute top-1/2 -translate-y-1/2 right-1 w-7 h-7 flex items-center justify-center z-20"
|
<!-- Filled comment icon with count -->
|
||||||
|
<button *ngIf="getBlockCommentCount(block.id) > 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 flex items-center justify-center z-20"
|
||||||
title="View comments" (click)="openComments(block.id)">
|
title="View comments" (click)="openComments(block.id)">
|
||||||
<svg viewBox="0 0 24 24" class="w-6 h-6"><path fill="#e5e7eb" d="M12 4c4.97 0 9 2.96 9 6.6S16.97 17.2 12 17.2c-.75 0-1.49-.07-2.2-.22L6 20l1.57-3.14C6.13 16 3 13.86 3 10.6 3 6.96 7.03 4 12 4z"/></svg>
|
<svg viewBox="0 0 64 64" class="w-5 h-5 text-gray-100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M32 8C19.85 8 10 17.85 10 30c0 4.2 1.2 8.3 3.3 11.7L10 56l13.1-4.4C25.9 53.2 28.9 54 32 54c12.15 0 22-9.85 22-22S44.15 8 32 8Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="24" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="32" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="40" cy="30" r="3" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
<span class="absolute text-[11px] font-semibold text-black">{{ getBlockCommentCount(block.id) }}</span>
|
<span class="absolute text-[11px] font-semibold text-black">{{ getBlockCommentCount(block.id) }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="getBlockCommentCount(block.id) === 0" class="absolute top-1/2 -translate-y-1/2 right-1 w-7 h-7 opacity-0 group-hover/block:opacity-100 transition-opacity flex items-center justify-center z-20"
|
<!-- Outline comment icon when there are no comments -->
|
||||||
|
<button *ngIf="getBlockCommentCount(block.id) === 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 opacity-0 group-hover/block:opacity-100 transition-opacity flex items-center justify-center z-20"
|
||||||
title="Add a comment" (click)="openComments(block.id)">
|
title="Add a comment" (click)="openComments(block.id)">
|
||||||
<svg viewBox="0 0 24 24" class="w-6 h-6"><path fill="none" stroke="#e5e7eb" stroke-width="1.5" d="M12 4c4.97 0 9 2.96 9 6.6S16.97 17.2 12 17.2c-.75 0-1.49-.07-2.2-.22L6 20l1.57-3.14C6.13 16 3 13.86 3 10.6 3 6.96 7.03 4 12 4z"/></svg>
|
<svg viewBox="0 0 64 64" class="w-5 h-5 text-gray-100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M32 8C19.85 8 10 17.85 10 30c0 4.2 1.2 8.3 3.3 11.7L10 56l13.1-4.4C25.9 53.2 28.9 54 32 54c12.15 0 22-9.85 22-22S44.15 8 32 8Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="24" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="32" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="40" cy="30" r="3" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@switch (block.type) {
|
@switch (block.type) {
|
||||||
@ -134,7 +159,11 @@ import { PaletteItem } from '../../../core/constants/palette-items';
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@case ('list-item') {
|
@case ('list-item') {
|
||||||
<app-list-item-block [block]="block" (update)="onBlockUpdate($event, block.id)" />
|
<app-list-item-block
|
||||||
|
[block]="block"
|
||||||
|
(update)="onBlockUpdate($event, block.id)"
|
||||||
|
(createBelow)="onListItemCreateBelow(block.id, colIndex, blockIndex)"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
@case ('code') {
|
@case ('code') {
|
||||||
<app-code-block [block]="block" (update)="onBlockUpdate($event, block.id)" />
|
<app-code-block [block]="block" (update)="onBlockUpdate($event, block.id)" />
|
||||||
@ -218,9 +247,6 @@ import { PaletteItem } from '../../../core/constants/palette-items';
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comments Panel -->
|
|
||||||
<app-comments-panel #commentsPanel />
|
|
||||||
|
|
||||||
<!-- Block Context Menu -->
|
<!-- Block Context Menu -->
|
||||||
<app-block-context-menu
|
<app-block-context-menu
|
||||||
[block]="selectedBlock() || createDummyBlock()"
|
[block]="selectedBlock() || createDummyBlock()"
|
||||||
@ -258,15 +284,15 @@ import { PaletteItem } from '../../../core/constants/palette-items';
|
|||||||
}
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class ColumnsBlockComponent implements AfterViewInit {
|
export class ColumnsBlockComponent implements AfterViewInit, OnDestroy {
|
||||||
private readonly dragDrop = inject(DragDropService);
|
private readonly dragDrop = inject(DragDropService);
|
||||||
private readonly commentService = inject(CommentService);
|
private readonly commentsStore = inject(CommentStoreService);
|
||||||
private readonly documentService = inject(DocumentService);
|
private readonly documentService = inject(DocumentService);
|
||||||
private readonly selectionService = inject(SelectionService);
|
private readonly selectionService = inject(SelectionService);
|
||||||
|
private readonly overlay = inject(Overlay);
|
||||||
|
|
||||||
@Input({ required: true }) block!: Block<ColumnsProps>;
|
@Input({ required: true }) block!: Block<ColumnsProps>;
|
||||||
@Output() update = new EventEmitter<ColumnsProps>();
|
@Output() update = new EventEmitter<ColumnsProps>();
|
||||||
@ViewChild('commentsPanel') commentsPanel?: CommentsPanelComponent;
|
|
||||||
@ViewChild('columnsContainer', { static: true }) columnsContainerRef!: ElementRef<HTMLElement>;
|
@ViewChild('columnsContainer', { static: true }) columnsContainerRef!: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
// Menu state
|
// Menu state
|
||||||
@ -283,12 +309,20 @@ export class ColumnsBlockComponent implements AfterViewInit {
|
|||||||
private resizeState: { active: boolean; index: number; startX: number; containerWidth: number; leftStart: number; rightStart: number } | null = null;
|
private resizeState: { active: boolean; index: number; startX: number; containerWidth: number; leftStart: number; rightStart: number } | null = null;
|
||||||
resizerPositions = signal<number[]>([]);
|
resizerPositions = signal<number[]>([]);
|
||||||
|
|
||||||
|
// Comments popover state
|
||||||
|
private commentRef?: OverlayRef;
|
||||||
|
private commentSub: { unsubscribe(): void } | null = null;
|
||||||
|
|
||||||
get props(): ColumnsProps {
|
get props(): ColumnsProps {
|
||||||
return this.block.props;
|
return this.block.props;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockCommentCount(blockId: string): number {
|
getBlockCommentCount(blockId: string): number {
|
||||||
return this.commentService.getCommentCount(blockId);
|
try {
|
||||||
|
return this.commentsStore.count(blockId);
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onConvertRequested(item: PaletteItem, blockId: string): void {
|
onConvertRequested(item: PaletteItem, blockId: string): void {
|
||||||
@ -315,7 +349,40 @@ export class ColumnsBlockComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openComments(blockId: string): void {
|
openComments(blockId: string): void {
|
||||||
this.commentsPanel?.open(blockId);
|
this.closeComments();
|
||||||
|
const container = this.columnsContainerRef?.nativeElement;
|
||||||
|
const anchor = (container?.querySelector(`[data-block-id="${blockId}"]`) as HTMLElement) || container;
|
||||||
|
if (!anchor) return;
|
||||||
|
|
||||||
|
const pos = this.overlay.position().flexibleConnectedTo(anchor).withPositions([
|
||||||
|
{ originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 8 },
|
||||||
|
{ originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -8 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.commentRef = this.overlay.create({
|
||||||
|
hasBackdrop: true,
|
||||||
|
backdropClass: 'cdk-overlay-transparent-backdrop',
|
||||||
|
positionStrategy: pos,
|
||||||
|
panelClass: 'nimbus-menu-panel'
|
||||||
|
});
|
||||||
|
|
||||||
|
const portal = new ComponentPortal(BlockCommentComposerComponent);
|
||||||
|
const ref = this.commentRef.attach(portal);
|
||||||
|
ref.instance.blockId = blockId;
|
||||||
|
this.commentSub = ref.instance.close.subscribe(() => this.closeComments());
|
||||||
|
this.commentRef.backdropClick().subscribe(() => this.closeComments());
|
||||||
|
this.commentRef.keydownEvents().subscribe((e) => { if ((e as KeyboardEvent).key === 'Escape') this.closeComments(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeComments(): void {
|
||||||
|
if (this.commentSub) {
|
||||||
|
try { this.commentSub.unsubscribe(); } catch {}
|
||||||
|
this.commentSub = null;
|
||||||
|
}
|
||||||
|
if (this.commentRef) {
|
||||||
|
this.commentRef.dispose();
|
||||||
|
this.commentRef = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlockMetaChange(metaChanges: any, blockId: string): void {
|
onBlockMetaChange(metaChanges: any, blockId: string): void {
|
||||||
@ -362,6 +429,54 @@ export class ColumnsBlockComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onListItemCreateBelow(blockId: string, columnIndex: number, blockIndex: number): void {
|
||||||
|
// Insert a new list-item block directly after the given list-item within the same column
|
||||||
|
const updatedColumns = this.props.columns.map((column, colIdx) => {
|
||||||
|
if (colIdx !== columnIndex) return column;
|
||||||
|
|
||||||
|
const blocks = [...column.blocks];
|
||||||
|
const existing = blocks[blockIndex];
|
||||||
|
if (!existing || existing.type !== 'list-item') {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: any = existing.props || {};
|
||||||
|
const newProps: any = {
|
||||||
|
kind: props.kind,
|
||||||
|
text: '',
|
||||||
|
checked: props.kind === 'check' ? false : undefined,
|
||||||
|
indent: props.indent || 0,
|
||||||
|
align: props.align
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.kind === 'numbered' && typeof props.number === 'number') {
|
||||||
|
newProps.number = props.number + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBlock = this.documentService.createBlock('list-item' as any, newProps);
|
||||||
|
const newBlocks = [...blocks];
|
||||||
|
newBlocks.splice(blockIndex + 1, 0, newBlock);
|
||||||
|
|
||||||
|
return { ...column, blocks: newBlocks };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.update.emit({ columns: updatedColumns });
|
||||||
|
|
||||||
|
// Focus the new list-item input once it is rendered
|
||||||
|
const newId = updatedColumns[columnIndex]?.blocks[blockIndex + 1]?.id;
|
||||||
|
if (!newId) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = document.querySelector(
|
||||||
|
`[data-block-id="${newId}"] input[type="text"]`
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
const len = input.value.length;
|
||||||
|
input.setSelectionRange(len, len);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
onBlockDelete(blockId: string): void {
|
onBlockDelete(blockId: string): void {
|
||||||
// Delete a specific block from columns
|
// Delete a specific block from columns
|
||||||
@ -648,7 +763,9 @@ export class ColumnsBlockComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getBlockBgColor(block: Block): string | undefined {
|
getBlockBgColor(block: Block): string | undefined {
|
||||||
if (block.type === 'paragraph') {
|
// Paragraph, heading and list(-item) blocks in columns should not have a full-width
|
||||||
|
// background; their inner editable/input pill handles the colored capsule.
|
||||||
|
if (block.type === 'paragraph' || block.type === 'heading' || block.type === 'list' || block.type === 'list-item') {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const bgColor = (block.meta as any)?.bgColor;
|
const bgColor = (block.meta as any)?.bgColor;
|
||||||
@ -680,6 +797,10 @@ export class ColumnsBlockComponent implements AfterViewInit {
|
|||||||
setTimeout(() => this.computeResizerPositions(), 0);
|
setTimeout(() => this.computeResizerPositions(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.closeComments();
|
||||||
|
}
|
||||||
|
|
||||||
@HostListener('window:resize')
|
@HostListener('window:resize')
|
||||||
onWindowResize(): void {
|
onWindowResize(): void {
|
||||||
this.computeResizerPositions();
|
this.computeResizerPositions();
|
||||||
|
|||||||
@ -9,38 +9,46 @@ import { DocumentService } from '../../../services/document.service';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule],
|
||||||
template: `
|
template: `
|
||||||
@switch (props.level) {
|
<div class="w-full" [style.--block-bg]="getBlockBgColor()">
|
||||||
@case (1) {
|
@switch (props.level) {
|
||||||
<h1
|
@case (1) {
|
||||||
contenteditable="true"
|
<div class="group/block relative flex items-center justify-between gap-2 bg-[var(--block-bg,var(--editor-bg))] rounded-sm px-4 py-0.5 shadow-sm">
|
||||||
class="text-xl font-bold focus:outline-none px-2 py-0.5 rounded-md m-0 w-full bg-transparent"
|
<h1
|
||||||
#editable
|
contenteditable="true"
|
||||||
(input)="onInput($event)"
|
class="text-xl font-bold focus:outline-none m-0 bg-transparent"
|
||||||
(keydown)="onKeyDown($event)"
|
#editable
|
||||||
placeholder="Heading 1"
|
(input)="onInput($event)"
|
||||||
></h1>
|
(keydown)="onKeyDown($event)"
|
||||||
|
placeholder="Heading 1"
|
||||||
|
></h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@case (2) {
|
||||||
|
<div class="group/block relative flex items-center justify-between gap-2 bg-[var(--block-bg,var(--editor-bg))] rounded-sm px-4 py-0.5 shadow-sm">
|
||||||
|
<h2
|
||||||
|
contenteditable="true"
|
||||||
|
class="text-lg font-semibold focus:outline-none m-0 bg-transparent"
|
||||||
|
#editable
|
||||||
|
(input)="onInput($event)"
|
||||||
|
(keydown)="onKeyDown($event)"
|
||||||
|
placeholder="Heading 2"
|
||||||
|
></h2>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@case (3) {
|
||||||
|
<div class="group/block relative flex items-center justify-between gap-2 bg-[var(--block-bg,var(--editor-bg))] rounded-sm px-4 py-0.5 shadow-sm">
|
||||||
|
<h3
|
||||||
|
contenteditable="true"
|
||||||
|
class="text-base font-semibold focus:outline-none m-0 bg-transparent"
|
||||||
|
#editable
|
||||||
|
(input)="onInput($event)"
|
||||||
|
(keydown)="onKeyDown($event)"
|
||||||
|
placeholder="Heading 3"
|
||||||
|
></h3>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@case (2) {
|
</div>
|
||||||
<h2
|
|
||||||
contenteditable="true"
|
|
||||||
class="text-lg font-semibold focus:outline-none px-2 py-0.5 rounded-md m-0 w-full bg-transparent"
|
|
||||||
#editable
|
|
||||||
(input)="onInput($event)"
|
|
||||||
(keydown)="onKeyDown($event)"
|
|
||||||
placeholder="Heading 2"
|
|
||||||
></h2>
|
|
||||||
}
|
|
||||||
@case (3) {
|
|
||||||
<h3
|
|
||||||
contenteditable="true"
|
|
||||||
class="text-base font-semibold focus:outline-none px-2 py-0.5 rounded-md m-0 w-full bg-transparent"
|
|
||||||
#editable
|
|
||||||
(input)="onInput($event)"
|
|
||||||
(keydown)="onKeyDown($event)"
|
|
||||||
placeholder="Heading 3"
|
|
||||||
></h3>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [`
|
||||||
[contenteditable]:empty:before {
|
[contenteditable]:empty:before {
|
||||||
@ -67,6 +75,12 @@ export class HeadingBlockComponent implements AfterViewInit {
|
|||||||
return this.block.props;
|
return this.block.props;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBlockBgColor(): string | undefined {
|
||||||
|
const meta: any = this.block?.meta || {};
|
||||||
|
const bgColor = meta.bgColor;
|
||||||
|
return bgColor && bgColor !== 'transparent' ? bgColor : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
if (this.editable?.nativeElement) {
|
if (this.editable?.nativeElement) {
|
||||||
this.editable.nativeElement.textContent = this.props.text || '';
|
this.editable.nativeElement.textContent = this.props.text || '';
|
||||||
|
|||||||
@ -13,18 +13,18 @@ import { SelectionService } from '../../../services/selection.service';
|
|||||||
template: `
|
template: `
|
||||||
<div class="w-full space-y-2">
|
<div class="w-full space-y-2">
|
||||||
@for (it of items(); track it.id; let i = $index) {
|
@for (it of items(); track it.id; let i = $index) {
|
||||||
<div class="flex items-center gap-3 cursor-text" (click)="onItemClick(i)">
|
<div class="flex items-center gap-2 cursor-text" (click)="onItemClick(i)">
|
||||||
<!-- Marker (bullet, checkbox, or number) -->
|
<!-- Marker (bullet, checkbox, or number) -->
|
||||||
<div class="flex-shrink-0 flex items-center justify-center" style="width: 24px; min-width: 24px;">
|
<div class="flex-shrink-0 flex items-center justify-center" style="width: 22px; min-width: 22px;">
|
||||||
@if (kind() === 'bullet') {
|
@if (kind() === 'bullet') {
|
||||||
<div class="rounded-full bg-slate-200" style="width: 8px; height: 8px;"></div>
|
<div class="rounded-full bg-slate-200" style="width: 6px; height: 6px;"></div>
|
||||||
} @else if (kind() === 'check') {
|
} @else if (kind() === 'check') {
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="cursor-pointer flex items-center justify-center focus:outline-none"
|
class="cursor-pointer flex items-center justify-center focus:outline-none"
|
||||||
(click)="onToggleCheck(it.id, $event)"
|
(click)="onToggleCheck(it.id, $event)"
|
||||||
(keydown)="onCheckboxKeyDown(i, $event)"
|
(keydown)="onCheckboxKeyDown(i, $event)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center bg-transparent" style="width: 20px; height: 20px; border: 2px solid rgba(148, 163, 184, 0.8); border-radius: 2px;">
|
<div class="flex items-center justify-center bg-transparent" style="width: 18px; height: 18px; border: 2px solid rgba(148, 163, 184, 0.8); border-radius: 3px;">
|
||||||
@if (it.checked) {
|
@if (it.checked) {
|
||||||
<svg viewBox="0 0 24 24" class="w-4 h-4 text-slate-900">
|
<svg viewBox="0 0 24 24" class="w-4 h-4 text-slate-900">
|
||||||
<path fill="currentColor" d="M9.5 17.25L4.75 12.5L6.16 11.09L9.5 14.43L17.84 6.09L19.25 7.5L9.5 17.25Z" />
|
<path fill="currentColor" d="M9.5 17.25L4.75 12.5L6.16 11.09L9.5 14.43L17.84 6.09L19.25 7.5L9.5 17.25Z" />
|
||||||
@ -33,13 +33,13 @@ import { SelectionService } from '../../../services/selection.service';
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="text-slate-200" style="font-size: 16px; font-weight: 500;">{{ i + 1 }}.</span>
|
<span class="text-slate-200" style="font-size: 14px; font-weight: 500;">{{ i + 1 }}.</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input pill - inherits block color or uses transparent background -->
|
<!-- Input pill - inherits block color or uses transparent background -->
|
||||||
<input #inp type="text"
|
<input #inp type="text"
|
||||||
class="flex-1 rounded-xl px-5 py-2.5 text-xl leading-7 focus:outline-none cursor-text border-none"
|
class="flex-1 rounded-sm px-4 py-0.5 text-sm leading-tight focus:outline-none cursor-text border-none shadow-sm"
|
||||||
[class.text-slate-900]="hasBlockColor() && !(kind() === 'check' && it.checked)"
|
[class.text-slate-900]="hasBlockColor() && !(kind() === 'check' && it.checked)"
|
||||||
[class.text-slate-100]="!hasBlockColor() && !(kind() === 'check' && it.checked)"
|
[class.text-slate-100]="!hasBlockColor() && !(kind() === 'check' && it.checked)"
|
||||||
[class.text-slate-500]="kind() === 'check' && it.checked"
|
[class.text-slate-500]="kind() === 'check' && it.checked"
|
||||||
|
|||||||
@ -10,11 +10,11 @@ import { DocumentService } from '../../../services/document.service';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule],
|
||||||
template: `
|
template: `
|
||||||
<div class="flex items-center gap-3 cursor-text w-full" (click)="onContainerClick($event)" [style.padding-left.px]="getIndentPadding()">
|
<div class="flex items-center gap-2 cursor-text w-full" (click)="onContainerClick($event)" [style.padding-left.px]="getIndentPadding()">
|
||||||
<!-- Marker (bullet, checkbox, or number) -->
|
<!-- Marker (bullet, checkbox, or number) -->
|
||||||
<div class="flex-shrink-0 flex items-center justify-center" style="width: 24px; min-width: 24px;">
|
<div class="flex-shrink-0 flex items-center justify-center" style="width: 22px; min-width: 22px;">
|
||||||
@if (props.kind === 'bullet') {
|
@if (props.kind === 'bullet') {
|
||||||
<span class="text-slate-200" style="font-size: 18px; line-height: 1;">{{ getBulletSymbol() }}</span>
|
<span class="text-slate-200" style="font-size: 14px; line-height: 1;">{{ getBulletSymbol() }}</span>
|
||||||
} @else if (props.kind === 'check') {
|
} @else if (props.kind === 'check') {
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="cursor-pointer flex items-center justify-center focus:outline-none"
|
class="cursor-pointer flex items-center justify-center focus:outline-none"
|
||||||
@ -26,7 +26,7 @@ import { DocumentService } from '../../../services/document.service';
|
|||||||
class="flex items-center justify-center"
|
class="flex items-center justify-center"
|
||||||
[class.bg-slate-100]="props.checked"
|
[class.bg-slate-100]="props.checked"
|
||||||
[class.text-slate-900]="props.checked"
|
[class.text-slate-900]="props.checked"
|
||||||
style="width: 20px; height: 20px; border: 2px solid rgba(148, 163, 184, 0.8); border-radius: 2px;">
|
style="width: 18px; height: 18px; border: 2px solid rgba(148, 163, 184, 0.8); border-radius: 3px;">
|
||||||
@if (props.checked) {
|
@if (props.checked) {
|
||||||
<svg viewBox="0 0 24 24" class="w-4 h-4">
|
<svg viewBox="0 0 24 24" class="w-4 h-4">
|
||||||
<path fill="currentColor" d="M9.5 17.25L4.75 12.5L6.16 11.09L9.5 14.43L17.84 6.09L19.25 7.5L9.5 17.25Z" />
|
<path fill="currentColor" d="M9.5 17.25L4.75 12.5L6.16 11.09L9.5 14.43L17.84 6.09L19.25 7.5L9.5 17.25Z" />
|
||||||
@ -35,13 +35,13 @@ import { DocumentService } from '../../../services/document.service';
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="text-slate-200" style="font-size: 16px; font-weight: 500;">{{ props.number || 1 }}.</span>
|
<span class="text-slate-200" style="font-size: 14px; font-weight: 500;">{{ props.number || 1 }}.</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input text - inherits block color or uses transparent background -->
|
<!-- Input text - inherits block color or uses transparent background -->
|
||||||
<input #inp type="text"
|
<input #inp type="text"
|
||||||
class="flex-1 rounded-xl px-5 py-2.5 text-xl leading-7 cursor-text border-none shadow-none"
|
class="flex-1 rounded-sm px-4 py-0.5 text-sm leading-tight cursor-text border-none shadow-sm"
|
||||||
[class.text-slate-900]="hasBlockColor() && !(props.kind === 'check' && props.checked)"
|
[class.text-slate-900]="hasBlockColor() && !(props.kind === 'check' && props.checked)"
|
||||||
[class.text-slate-100]="!hasBlockColor() && !(props.kind === 'check' && props.checked)"
|
[class.text-slate-100]="!hasBlockColor() && !(props.kind === 'check' && props.checked)"
|
||||||
[class.text-slate-500]="props.kind === 'check' && props.checked"
|
[class.text-slate-500]="props.kind === 'check' && props.checked"
|
||||||
@ -72,6 +72,9 @@ import { DocumentService } from '../../../services/document.service';
|
|||||||
export class ListItemBlockComponent implements OnInit, AfterViewInit {
|
export class ListItemBlockComponent implements OnInit, AfterViewInit {
|
||||||
@Input({ required: true }) block!: Block<ListItemProps>;
|
@Input({ required: true }) block!: Block<ListItemProps>;
|
||||||
@Output() update = new EventEmitter<ListItemProps>();
|
@Output() update = new EventEmitter<ListItemProps>();
|
||||||
|
// Optional: when provided (e.g. in ColumnsBlockComponent), delegate creation of the next item
|
||||||
|
// to the parent container instead of inserting a top-level block via DocumentService.
|
||||||
|
@Output() createBelow = new EventEmitter<void>();
|
||||||
|
|
||||||
@ViewChild('inp') input!: ElementRef<HTMLInputElement>;
|
@ViewChild('inp') input!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
@ -162,12 +165,16 @@ export class ListItemBlockComponent implements OnInit, AfterViewInit {
|
|||||||
if (ev.key === 'Enter' && !ev.shiftKey) {
|
if (ev.key === 'Enter' && !ev.shiftKey) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
// Get the current block index
|
// If parent container listens to createBelow (e.g. columns), delegate creation to it
|
||||||
|
if (this.createBelow.observers.length > 0) {
|
||||||
|
this.createBelow.emit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: legacy behavior for top-level list-item blocks
|
||||||
const blocks = this.documentService.blocks();
|
const blocks = this.documentService.blocks();
|
||||||
const currentIndex = blocks.findIndex(b => b.id === this.block.id);
|
const currentIndex = blocks.findIndex(b => b.id === this.block.id);
|
||||||
|
|
||||||
if (currentIndex !== -1) {
|
if (currentIndex !== -1) {
|
||||||
// Create new list item with same kind and indent
|
|
||||||
let newProps: ListItemProps = {
|
let newProps: ListItemProps = {
|
||||||
kind: this.props.kind,
|
kind: this.props.kind,
|
||||||
text: '',
|
text: '',
|
||||||
@ -175,12 +182,11 @@ export class ListItemBlockComponent implements OnInit, AfterViewInit {
|
|||||||
indent: this.props.indent || 0,
|
indent: this.props.indent || 0,
|
||||||
align: this.props.align
|
align: this.props.align
|
||||||
};
|
};
|
||||||
|
|
||||||
// For numbered lists, increment the number
|
|
||||||
if (this.props.kind === 'numbered' && this.props.number) {
|
if (this.props.kind === 'numbered' && this.props.number) {
|
||||||
newProps.number = this.props.number + 1;
|
newProps.number = this.props.number + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBlock = this.documentService.createBlock('list-item' as any, newProps);
|
const newBlock = this.documentService.createBlock('list-item' as any, newProps);
|
||||||
this.documentService.insertBlock(this.block.id, newBlock);
|
this.documentService.insertBlock(this.block.id, newBlock);
|
||||||
this.selection.setActive(newBlock.id);
|
this.selection.setActive(newBlock.id);
|
||||||
|
|||||||
@ -11,53 +11,57 @@ import { CommentActionMenuComponent } from './comment-action-menu.component';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, OverlayModule, PortalModule],
|
imports: [CommonModule, FormsModule, OverlayModule, PortalModule],
|
||||||
template: `
|
template: `
|
||||||
<div class="bg-[#333333] border border-neutral-600 rounded-xl shadow-xl w-[420px] max-w-[95vw]">
|
<div class="panel bg-surface-elevated shadow-elevated ring-1 ring-app w-[420px] max-w-[95vw]">
|
||||||
<div class="flex items-center justify-between px-3 py-2 border-b border-neutral-700">
|
<div class="flex items-center justify-between px-3 py-2 border-b border-border bg-surface">
|
||||||
<div class="text-gray-200 font-medium">Comments</div>
|
<div class="text-sm font-semibold text-text-main">Comments</div>
|
||||||
<button class="text-gray-300" (click)="close.emit()">
|
<button class="text-muted hover:text-text-main transition-colors" (click)="close.emit()">
|
||||||
<svg viewBox="0 0 24 24" class="w-5 h-5"><path fill="currentColor" d="M18 6L6 18M6 6l12 12"/></svg>
|
<svg viewBox="0 0 24 24" class="w-5 h-5"><path fill="currentColor" d="M18 6L6 18M6 6l12 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative z-20 max-h-[300px] overflow-auto px-3 py-2 space-y-3">
|
<div class="relative z-20 max-h-[300px] overflow-auto px-3 py-2 space-y-3 bg-surface">
|
||||||
<div *ngFor="let c of comments()" class="space-y-1 relative">
|
<div *ngFor="let c of comments()" class="space-y-1 relative">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-8 h-8 rounded-full overflow-hidden bg-neutral-600"></div>
|
<div class="w-8 h-8 rounded-full overflow-hidden bg-neutral-500 flex items-center justify-center text-xs font-semibold text-white">
|
||||||
|
{{ (c.author || 'U').slice(0, 2) }}
|
||||||
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-xs">
|
||||||
<div class="text-gray-200 font-semibold">{{ c.author || 'User' }}</div>
|
<div class="font-semibold text-text-main">{{ c.author || 'User' }}</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-gray-400">{{ c.createdAt | date:'shortTime' }}</div>
|
<div class="text-muted">{{ c.createdAt | date:'shortTime' }}</div>
|
||||||
<button class="text-gray-400" (click)="openCommentMenu($event, c)">
|
<button class="text-muted hover:text-text-main transition-colors" (click)="openCommentMenu($event, c)">
|
||||||
<svg viewBox="0 0 24 24" class="w-5 h-5"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
<svg viewBox="0 0 24 24" class="w-5 h-5"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="editingId !== c.id; else editTpl">
|
<ng-container *ngIf="editingId !== c.id; else editTpl">
|
||||||
<div class="text-gray-200 whitespace-pre-wrap">{{ c.text }}</div>
|
<div class="mt-1 text-sm text-text-main whitespace-pre-wrap">{{ c.text }}</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #editTpl>
|
<ng-template #editTpl>
|
||||||
<div class="space-y-2">
|
<div class="mt-1 space-y-2">
|
||||||
<input class="nimbus-input w-full" [(ngModel)]="editText" autofocus />
|
<input class="nimbus-input w-full" [(ngModel)]="editText" autofocus />
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button class="px-3 py-1 rounded bg-neutral-700" (click)="cancelEdit()">Cancel</button>
|
<button class="btn btn-secondary btn-sm px-3 py-1" (click)="cancelEdit()">Cancel</button>
|
||||||
<button class="px-3 py-1 rounded bg-sky-600 text-white" (click)="saveEdit(c.id)">Save</button>
|
<button class="btn btn-primary btn-sm px-3 py-1" (click)="saveEdit(c.id)">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<!-- Reply preview inside item (optional) could go here -->
|
<!-- Reply preview inside item (optional) could go here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-px bg-neutral-700"></div>
|
<div class="h-px bg-border/70"></div>
|
||||||
<!-- Action menu now rendered via CDK Overlay -->
|
<!-- Action menu now rendered via CDK Overlay -->
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!comments().length" class="text-sm text-gray-400">No comments yet.</div>
|
<div *ngIf="!comments().length" class="text-sm text-muted">No comments yet.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative z-10 px-3 py-2 border-t border-neutral-700" (click)="menuForId = null">
|
<div class="relative z-10 px-3 py-2 border-t border-border bg-surface" (click)="menuForId = null">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="w-8 h-8 rounded-full overflow-hidden bg-neutral-600"></div>
|
<div class="w-8 h-8 rounded-full overflow-hidden bg-neutral-500 flex items-center justify-center text-xs font-semibold text-white">
|
||||||
|
{{ 'You' | slice:0:2 }}
|
||||||
|
</div>
|
||||||
<input type="text" class="flex-1 nimbus-input" placeholder="Add a comment" [(ngModel)]="text" (keydown.enter)="send()" />
|
<input type="text" class="flex-1 nimbus-input" placeholder="Add a comment" [(ngModel)]="text" (keydown.enter)="send()" />
|
||||||
<button class="px-2 py-1 rounded-md bg-sky-500 text-white disabled:opacity-50" [disabled]="!text" (click)="send()">
|
<button class="btn btn-primary btn-icon px-2 py-1 min-h-0 h-8 w-8 rounded-md disabled:opacity-50" [disabled]="!text" (click)="send()">
|
||||||
<svg class="w-5 h-5" viewBox="0 0 24 24"><path fill="currentColor" d="M3 13l17-9-7 18-2-7z"/></svg>
|
<svg class="w-4 h-4" viewBox="0 0 24 24"><path fill="currentColor" d="M3 13l17-9-7 18-2-7z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,16 +12,16 @@ export interface CommentMenuItem {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
template: `
|
template: `
|
||||||
<div class="w-36 bg-neutral-800 border border-neutral-700 rounded-lg py-1 shadow-xl">
|
<div class="w-40 panel bg-surface-elevated shadow-elevated border border-border py-1">
|
||||||
<button class="w-full text-left px-3 py-1.5 hover:bg-neutral-700 flex items-center gap-2" (click)="reply.emit(context)">
|
<button class="w-full text-left px-3 py-1.5 hover:bg-surface2 transition-colors flex items-center gap-2 text-sm text-text-main" (click)="reply.emit(context)">
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24"><path fill="currentColor" d="M10 19l-7-7 7-7v14z"/></svg>
|
<svg class="w-4 h-4" viewBox="0 0 24 24"><path fill="currentColor" d="M10 19l-7-7 7-7v14z"/></svg>
|
||||||
<span>Reply</span>
|
<span>Reply</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="w-full text-left px-3 py-1.5 hover:bg-neutral-700 flex items-center gap-2" (click)="edit.emit(context)">
|
<button class="w-full text-left px-3 py-1.5 hover:bg-surface2 transition-colors flex items-center gap-2 text-sm text-text-main" (click)="edit.emit(context)">
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25z"/></svg>
|
<svg class="w-4 h-4" viewBox="0 0 24 24"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25z"/></svg>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="w-full text-left px-3 py-1.5 hover:bg-neutral-700 flex items-center gap-2 text-red-300" (click)="remove.emit(context)">
|
<button class="w-full text-left px-3 py-1.5 hover:bg-surface2 transition-colors flex items-center gap-2 text-sm" (click)="remove.emit(context)" style="color: var(--danger)">
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24"><path fill="currentColor" d="M6 7h12l-1 13H7L6 7zm3-3h6l1 2H8l1-2z"/></svg>
|
<svg class="w-4 h-4" viewBox="0 0 24 24"><path fill="currentColor" d="M6 7h12l-1 13H7L6 7zm3-3h6l1 2H8l1-2z"/></svg>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -148,15 +148,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table-level comments bubble (speech bubble style to match other blocks) -->
|
<!-- Table-level comments bubble (uses same comment icon as other blocks) -->
|
||||||
<button *ngIf="totalComments() > 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 flex items-center justify-center"
|
<button *ngIf="totalComments() > 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 flex items-center justify-center"
|
||||||
title="View comments" (click)="openFirstComment()">
|
title="View comments" (click)="openFirstComment()">
|
||||||
<svg viewBox="0 0 24 24" class="w-7 h-7"><path fill="#e5e7eb" d="M12 4c4.97 0 9 2.96 9 6.6S16.97 17.2 12 17.2c-.75 0-1.49-.07-2.2-.22L6 20l1.57-3.14C6.13 16 3 13.86 3 10.6 3 6.96 7.03 4 12 4z"/></svg>
|
<svg viewBox="0 0 64 64" class="w-5 h-5 text-gray-100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M32 8C19.85 8 10 17.85 10 30c0 4.2 1.2 8.3 3.3 11.7L10 56l13.1-4.4C25.9 53.2 28.9 54 32 54c12.15 0 22-9.85 22-22S44.15 8 32 8Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="24" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="32" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="40" cy="30" r="3" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
<span class="absolute text-[11px] font-semibold text-black">{{ totalComments() }}</span>
|
<span class="absolute text-[11px] font-semibold text-black">{{ totalComments() }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="totalComments() === 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
<button *ngIf="totalComments() === 0" class="absolute top-1/2 -translate-y-1/2 right-2 w-8 h-8 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
||||||
title="Add a comment" (click)="openBlockComment()">
|
title="Add a comment" (click)="openBlockComment()">
|
||||||
<svg viewBox="0 0 24 24" class="w-7 h-7"><path fill="none" stroke="#e5e7eb" stroke-width="1.5" d="M12 4c4.97 0 9 2.96 9 6.6S16.97 17.2 12 17.2c-.75 0-1.49-.07-2.2-.22L6 20l1.57-3.14C6.13 16 3 13.86 3 10.6 3 6.96 7.03 4 12 4z"/></svg>
|
<svg viewBox="0 0 64 64" class="w-5 h-5 text-gray-100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M32 8C19.85 8 10 17.85 10 30c0 4.2 1.2 8.3 3.3 11.7L10 56l13.1-4.4C25.9 53.2 28.9 54 32 54c12.15 0 22-9.85 22-22S44.15 8 32 8Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="24" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="32" cy="30" r="3" fill="currentColor" />
|
||||||
|
<circle cx="40" cy="30" r="3" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Quick-add controls: positioned outside the table -->
|
<!-- Quick-add controls: positioned outside the table -->
|
||||||
|
|||||||
2
vault/.obsidian/bookmarks.json
vendored
2
vault/.obsidian/bookmarks.json
vendored
@ -10,7 +10,7 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "tata/Les Compléments Alimentaires Un Guide Général.md",
|
"path": "tata/Les Compléments Alimentaires Un Guide Général.md",
|
||||||
"title": "Les Compléments Alimentaires Un Guide Général.md",
|
"title": "Les Compléments Alimentaires Un Guide Général.md",
|
||||||
"ctime": 1763156633684
|
"ctime": 1763267370357
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -3,7 +3,6 @@ titre: "Nouvelle note 1"
|
|||||||
auteur: "Bruno Charest"
|
auteur: "Bruno Charest"
|
||||||
creation_date: "2025-10-24T03:30:58.977Z"
|
creation_date: "2025-10-24T03:30:58.977Z"
|
||||||
modification_date: "2025-11-03T22:06:07-04:00"
|
modification_date: "2025-11-03T22:06:07-04:00"
|
||||||
tags: [""]
|
|
||||||
status: "en-cours"
|
status: "en-cours"
|
||||||
publish: false
|
publish: false
|
||||||
favoris: true
|
favoris: true
|
||||||
@ -13,6 +12,11 @@ archive: true
|
|||||||
draft: false
|
draft: false
|
||||||
private: false
|
private: false
|
||||||
description: "Une expédition militaire et scientifique découvre la cité mythique d'Atlantis dans la galaxie de Pégase et affronte les Wraiths."
|
description: "Une expédition militaire et scientifique découvre la cité mythique d'Atlantis dans la galaxie de Pégase et affronte les Wraiths."
|
||||||
|
tags:
|
||||||
|
- titi
|
||||||
|
- test
|
||||||
|
- tag4
|
||||||
|
- configuration
|
||||||
---
|
---
|
||||||
*Stargate Atlantis* est une série de science-fiction dérivée de la populaire *Stargate SG-1*. Elle suit les aventures d'une expédition internationale, composée de scientifiques et de militaires, qui voyage à travers la porte des étoiles vers la lointaine galaxie de Pégase. Leur destination est la cité mythique d'Atlantis, une métropole volante abandonnée construite par une race ancienne et technologiquement supérieure connue sous le nom d'Anciens.
|
*Stargate Atlantis* est une série de science-fiction dérivée de la populaire *Stargate SG-1*. Elle suit les aventures d'une expédition internationale, composée de scientifiques et de militaires, qui voyage à travers la porte des étoiles vers la lointaine galaxie de Pégase. Leur destination est la cité mythique d'Atlantis, une métropole volante abandonnée construite par une race ancienne et technologiquement supérieure connue sous le nom d'Anciens.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 805 KiB |
@ -10,338 +10,59 @@ documentModelFormat: "block-model-v1"
|
|||||||
"title": "Page Tests",
|
"title": "Page Tests",
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{
|
{
|
||||||
"id": "block_1763234865120_um14zlycy",
|
"id": "block_1763307699824_r3elleo33",
|
||||||
"type": "heading",
|
"type": "heading",
|
||||||
"props": {
|
"props": {
|
||||||
"level": 1,
|
"level": 1,
|
||||||
"text": "H1"
|
"text": "asdassda"
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"createdAt": "2025-11-15T19:27:45.120Z",
|
"createdAt": "2025-11-16T15:41:39.824Z",
|
||||||
"updatedAt": "2025-11-15T20:06:49.723Z",
|
"updatedAt": "2025-11-16T15:41:42.459Z"
|
||||||
"align": "center",
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "block_1763240860187_hdklpobdf",
|
"id": "block_1763308160356_nfhdtf1p1",
|
||||||
"type": "line",
|
"type": "kanban",
|
||||||
"props": {
|
|
||||||
"style": "solid"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T21:07:40.187Z",
|
|
||||||
"updatedAt": "2025-11-15T21:36:29.164Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763234657433_5malb56fe",
|
|
||||||
"type": "list-item",
|
|
||||||
"props": {
|
|
||||||
"kind": "check",
|
|
||||||
"text": "checkbox",
|
|
||||||
"checked": false,
|
|
||||||
"indent": 0,
|
|
||||||
"align": "left"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T19:24:17.433Z",
|
|
||||||
"updatedAt": "2025-11-15T20:13:56.404Z",
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763237180130_m82opx9yx",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": "paragraphe"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:06:20.130Z",
|
|
||||||
"updatedAt": "2025-11-15T20:13:50.464Z",
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763234665227_e09ql6sb4",
|
|
||||||
"type": "list-item",
|
|
||||||
"props": {
|
|
||||||
"kind": "bullet",
|
|
||||||
"text": "bullet",
|
|
||||||
"indent": 0,
|
|
||||||
"align": "left"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T19:24:25.227Z",
|
|
||||||
"updatedAt": "2025-11-15T20:07:27.797Z",
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763237840896_fuw5hvm9t",
|
|
||||||
"type": "columns",
|
|
||||||
"props": {
|
"props": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"id": "or66s9hqb",
|
"id": "block_1763308177122_doyf2zh37",
|
||||||
"blocks": [
|
"title": "To Do",
|
||||||
|
"cards": [
|
||||||
{
|
{
|
||||||
"id": "block_1763237836743_a06ez4lux",
|
"id": "item_1763308197023_pbeezlint",
|
||||||
"type": "paragraph",
|
"title": "New Card 2",
|
||||||
"props": {
|
"description": ""
|
||||||
"text": "paragraphe"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:17:16.743Z",
|
|
||||||
"updatedAt": "2025-11-15T20:17:16.743Z",
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "n90fsx72s",
|
"id": "item_1763308195207_1skel85f7",
|
||||||
"type": "paragraph",
|
"title": "New Card 1",
|
||||||
"props": {
|
"description": ""
|
||||||
"text": "paragraphe"
|
|
||||||
},
|
|
||||||
"children": [],
|
|
||||||
"meta": {
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tkk3ir62w",
|
"id": "item_1763308197933_aj34wtfd9",
|
||||||
"type": "paragraph",
|
"title": "New Card 3",
|
||||||
"props": {
|
"description": ""
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"children": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ljyumvc5w",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"children": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "rae60bsx3",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"children": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "xa3p8sybb",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"children": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "qln2xcscy",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"children": []
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"width": 50
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "s16nsirzh",
|
"id": "item_1763308190239_dmw2vomdm",
|
||||||
"blocks": [
|
"title": "done",
|
||||||
{
|
"cards": []
|
||||||
"id": "block_1763237223906_8fazui2p9",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": "paragraphe"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:07:03.906Z",
|
|
||||||
"updatedAt": "2025-11-15T20:07:24.349Z",
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"width": 50
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"createdAt": "2025-11-15T20:17:20.896Z",
|
"createdAt": "2025-11-16T15:49:20.356Z",
|
||||||
"updatedAt": "2025-11-15T22:13:34.707Z"
|
"updatedAt": "2025-11-16T15:50:10.618Z"
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763237641170_alnjnsj8c",
|
|
||||||
"type": "list-item",
|
|
||||||
"props": {
|
|
||||||
"kind": "numbered",
|
|
||||||
"text": "number list 1",
|
|
||||||
"indent": 0,
|
|
||||||
"align": "left",
|
|
||||||
"number": 1
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:14:01.171Z",
|
|
||||||
"updatedAt": "2025-11-15T20:14:14.163Z",
|
|
||||||
"bgColor": "#dc2626"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238667046_3xvnp49yo",
|
|
||||||
"type": "heading",
|
|
||||||
"props": {
|
|
||||||
"level": 1,
|
|
||||||
"text": "H2"
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:07.046Z",
|
|
||||||
"updatedAt": "2025-11-15T21:07:39.675Z",
|
|
||||||
"bgColor": "#525252"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238706236_v3kaqyax7",
|
|
||||||
"type": "heading",
|
|
||||||
"props": {
|
|
||||||
"level": 1,
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:46.236Z",
|
|
||||||
"updatedAt": "2025-11-15T21:55:37.233Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238706552_9zpoiafug",
|
|
||||||
"type": "table",
|
|
||||||
"props": {
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"id": "block_1763248205146_pnw8cifrx",
|
|
||||||
"cells": [
|
|
||||||
{
|
|
||||||
"id": "block_1763248205146_p9wc7se4g",
|
|
||||||
"text": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"header": false
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:46.552Z",
|
|
||||||
"updatedAt": "2025-11-15T23:10:05.176Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238706720_x5fdjzcm1",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:46.720Z",
|
|
||||||
"updatedAt": "2025-11-15T20:32:24.875Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238706867_cbgv0yetb",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:46.867Z",
|
|
||||||
"updatedAt": "2025-11-15T20:31:46.867Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238707162_bh255zvaf",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:47.162Z",
|
|
||||||
"updatedAt": "2025-11-15T20:31:47.162Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763248197366_ynjr796za",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T23:09:57.366Z",
|
|
||||||
"updatedAt": "2025-11-15T23:09:57.366Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763248197834_sb477zyix",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T23:09:57.834Z",
|
|
||||||
"updatedAt": "2025-11-15T23:09:57.834Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763248198129_38lauudq6",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T23:09:58.130Z",
|
|
||||||
"updatedAt": "2025-11-15T23:09:58.130Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763248199064_xs3run960",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T23:09:59.064Z",
|
|
||||||
"updatedAt": "2025-11-15T23:09:59.064Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238707334_uupqjbbia",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:47.334Z",
|
|
||||||
"updatedAt": "2025-11-15T20:31:47.334Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "block_1763238707506_lj73550jb",
|
|
||||||
"type": "paragraph",
|
|
||||||
"props": {
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"createdAt": "2025-11-15T20:31:47.506Z",
|
|
||||||
"updatedAt": "2025-11-15T20:31:47.506Z"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"meta": {
|
"meta": {
|
||||||
"createdAt": "2025-11-14T19:38:33.471Z",
|
"createdAt": "2025-11-14T19:38:33.471Z",
|
||||||
"updatedAt": "2025-11-15T23:10:05.176Z"
|
"updatedAt": "2025-11-16T15:50:10.618Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user