diff --git a/src/app/shared/blocks-list/blocks-list.css b/src/app/shared/blocks-list/blocks-list.css index 94bceb9..0cc7158 100644 --- a/src/app/shared/blocks-list/blocks-list.css +++ b/src/app/shared/blocks-list/blocks-list.css @@ -1,8 +1,12 @@ -:host { +/* + * Palette lives here rather than in the markup so the blocks and containers lists, which share + * these class names, stay identical apart from their accent. + */ +.blocks-list-root { display: flex; - flex: 1 1 auto; - min-height: 0; + flex-direction: column; height: 100%; + min-height: 0; } .blocks-list-loading { @@ -12,178 +16,138 @@ height: 8rem; } -.blocks-list-root { - display: flex; - flex: 1 1 auto; - flex-direction: column; - width: 100%; - min-width: 0; - height: 100%; - min-height: 0; - overflow-x: hidden; -} - .blocks-list-search { width: 100%; padding: 8px 0; } +.blocks-list-search mat-form-field { + width: 100%; +} + .blocks-list-empty { - display: flex; - flex: 1; - flex-direction: column; padding-top: 2.5rem; - margin-top: 2.5rem; color: #94a3b8; text-align: center; } .blocks-list-items { display: flex; - flex: 1 1 auto; + flex: 1; flex-direction: column; - align-items: stretch; - gap: 8px; - width: 100%; - min-width: 0; + gap: 4px; min-height: 0; - box-sizing: border-box; - overflow-x: hidden; + padding-bottom: 12px; overflow-y: auto; - padding: 8px 0 20px; } +/* Flat by default: a palette you drag from should not compete with the canvas. */ .blocks-list-card { - display: block; - flex-shrink: 0; - width: 100%; - min-width: 0; - overflow: hidden; + padding: 7px 8px; + border: 1px solid #e2e8f0; + border-left: 3px solid #cbd5e1; + border-radius: 6px; + background: #ffffff; + box-shadow: none; + /* The whole card is the drag source, so say so. */ cursor: grab; - border: 1px solid #dbe7f5; - border-radius: 16px; - background: - radial-gradient(circle at top right, rgba(56, 189, 248, 0.12), transparent 34%), - linear-gradient(180deg, #ffffff 0%, #f7fbff 100%); - box-shadow: - 0 12px 26px rgba(15, 23, 42, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.72); - transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease; + transition: background-color 0.12s ease, border-color 0.12s ease; +} + +.blocks-list-card:hover { + border-color: #cbd5e1; + background: #f8fafc; } .blocks-list-card:active { cursor: grabbing; } -.blocks-list-card:hover { - transform: translateY(-2px); - border-color: #7dd3fc; - box-shadow: 0 18px 30px rgba(14, 116, 144, 0.14); +.blocks-list-card--container { + border-left-color: #a78bfa; } -.blocks-list-card-header { - display: flex; - align-items: center; - gap: 12px; - padding: 14px 14px 0; -} - -.blocks-list-card-heading { - display: flex; - flex: 1 1 auto; - min-width: 0; - flex-direction: column; -} - -.blocks-list-card-title-row { +.blocks-list-card-row { display: flex; align-items: center; gap: 6px; min-width: 0; } -.blocks-list-card-avatar { - display: flex; - align-items: center; - justify-content: center; - width: 42px; - height: 42px; - margin: 0; - border-radius: 12px; - color: #075985; - background: linear-gradient(180deg, #e0f2fe 0%, #bae6fd 100%); +.blocks-list-card-kind { + flex: 0 0 auto; + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; + color: #94a3b8; } -.blocks-list-card-avatar .mat-icon { - width: 22px; - height: 22px; - font-size: 22px; +.blocks-list-card-kind--interactive { + color: #0ea5e9; +} + +.blocks-list-card-kind--container { + color: #7c3aed; +} + +.blocks-list-card-meta { + flex: 1 1 auto; + min-width: 0; } .blocks-list-card-title { - flex: 1 1 auto; - min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: 14px; - font-weight: 700; + font-size: 13px; + font-weight: 600; color: #0f172a; } -.blocks-list-card-info { - display: inline-flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - border: 0; - border-radius: 999px; - background: rgba(224, 242, 254, 0.95); - color: #0369a1; - cursor: default; - flex-shrink: 0; -} - -.blocks-list-card-info .mat-icon { - width: 14px; - height: 14px; - font-size: 14px; -} - -.blocks-list-card-subtitle { - color: #475569; - font-size: 12px; -} - -.blocks-list-card-content { - padding: 0 14px 8px; -} - -.blocks-list-card-copy { - margin: 0; - font-size: 12px; - line-height: 1.45; +.blocks-list-card-sub { + font-size: 11px; color: #64748b; } -.blocks-list-card-actions { - padding: 0 14px 14px !important; - margin: 0; +.blocks-list-card-info { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: 999px; + background: transparent; + color: #cbd5e1; + cursor: help; } -.blocks-list-card-chip { - min-height: 28px; - font-size: 12px; - font-weight: 600; - color: #0369a1 !important; - background: rgba(224, 242, 254, 0.95) !important; - border: 1px solid rgba(125, 211, 252, 0.7); +.blocks-list-card:hover .blocks-list-card-info { + color: #64748b; } -.blocks-list-card-chip-interactive { - color: #b45309 !important; - background: rgba(254, 243, 199, 0.98) !important; - border-color: rgba(252, 211, 77, 0.72); +.blocks-list-card-info--open { + background: #e2e8f0; + color: #334155; +} + +.blocks-list-card-info .mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; +} + +/* Expanded in place: these descriptions run to several hundred characters. */ +.blocks-list-card-copy { + margin: 6px 0 2px; + padding-top: 6px; + border-top: 1px dashed #e2e8f0; + font-size: 11.5px; + line-height: 1.45; + color: #475569; + cursor: text; } diff --git a/src/app/shared/blocks-list/blocks-list.html b/src/app/shared/blocks-list/blocks-list.html index 6f73608..c9555eb 100644 --- a/src/app/shared/blocks-list/blocks-list.html +++ b/src/app/shared/blocks-list/blocks-list.html @@ -12,12 +12,6 @@ - - @if (filteredBlocks().length === 0) {
No blocks found @@ -28,38 +22,38 @@ - -
- -
-
-
- {{ block.type }} - + +
+ + +
+
{{ block.type }}
+
+ {{ block.userInteractive ? 'Human step' : 'Automated step' }}
- - {{ block.userInteractive ? 'Interactive' : 'Automatic' }} -
- - -

{{ block.description }}

-
- - - {{ block.userInteractive ? 'Human step' : 'Automated step' }} - - + + +
+ + @if (infoOpenFor() === block.type) { +

{{ block.description }}

+ } }
diff --git a/src/app/shared/blocks-list/blocks-list.spec.ts b/src/app/shared/blocks-list/blocks-list.spec.ts index dffcad1..3ca376b 100644 --- a/src/app/shared/blocks-list/blocks-list.spec.ts +++ b/src/app/shared/blocks-list/blocks-list.spec.ts @@ -6,6 +6,18 @@ import { vi } from 'vitest'; import { BlocksList } from './blocks-list'; +const BLOCK_TYPES = [ + { + type: 'LLMBlock', + family: 'block', + description: 'Runs a single prompt against an LLM and produces a textual response.', + userInteractive: false, + configurationType: null, + configurationClass: null, + schema: null + } +]; + describe('BlocksList', () => { let component: BlocksList; let fixture: ComponentFixture; @@ -19,9 +31,9 @@ describe('BlocksList', () => { provide: BlocksService, useValue: { catalogLoading: signal(false), - blockTypes: signal([]), + blockTypes: signal(BLOCK_TYPES), hasLoadedBlockTypes: vi.fn().mockReturnValue(true), - getAllBlocksTypes: vi.fn().mockResolvedValue(signal([])) + getAllBlocksTypes: vi.fn().mockResolvedValue(signal(BLOCK_TYPES)) } } ] @@ -37,4 +49,33 @@ describe('BlocksList', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('keeps every card draggable, which is the whole point of the palette', () => { + const card = fixture.nativeElement.querySelector('.blocks-list-card'); + expect(card).not.toBeNull(); + expect(card.getAttribute('draggable')).toBe('true'); + }); + + it('hides the description until the info button asks for it', () => { + // Descriptions run to several hundred characters; showing them all turns the palette into a wall + // of text, and a tooltip could not render them readably either. + expect(fixture.nativeElement.querySelector('.blocks-list-card-copy')).toBeNull(); + + fixture.nativeElement.querySelector('.blocks-list-card-info').click(); + fixture.detectChanges(); + + const copy = fixture.nativeElement.querySelector('.blocks-list-card-copy'); + expect(copy.textContent).toContain('Runs a single prompt'); + + fixture.nativeElement.querySelector('.blocks-list-card-info').click(); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.blocks-list-card-copy')).toBeNull(); + }); + + it('shows one description at a time, so the list cannot unfold into a wall of text', () => { + component.toggleInfo('LLMBlock', new Event('click')); + component.toggleInfo('SomethingElse', new Event('click')); + + expect(component.infoOpenFor()).toBe('SomethingElse'); + }); }); diff --git a/src/app/shared/blocks-list/blocks-list.ts b/src/app/shared/blocks-list/blocks-list.ts index 5de3bda..6b36eb1 100644 --- a/src/app/shared/blocks-list/blocks-list.ts +++ b/src/app/shared/blocks-list/blocks-list.ts @@ -3,7 +3,6 @@ import { BlockType } from '@models/flow'; import { BlocksService } from '@services/blocks/blocks'; import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder'; import { MatCardModule } from '@angular/material/card'; -import { MatChipsModule } from '@angular/material/chips'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; @@ -13,7 +12,7 @@ import { BLOCK_TYPE_DRAG_MIME } from './block-drag'; @Component({ selector: 'app-blocks-list', - imports: [MatCardModule, MatChipsModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTooltipModule], + imports: [MatCardModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTooltipModule], templateUrl: './blocks-list.html', styleUrl: './blocks-list.css', changeDetection: ChangeDetectionStrategy.OnPush @@ -25,6 +24,18 @@ export class BlocksList extends ListStateViewHolder { private blocksService = inject(BlocksService); loading: WritableSignal = signal(true); + + /** + * Which card is showing its description, expanded in place. Descriptions run to several hundred + * characters, which a tooltip cannot show readably, and a modal is heavy for a hint you read + * while dragging. + */ + readonly infoOpenFor = signal(null); + + toggleInfo(type: string, event: Event) { + event.stopPropagation(); + this.infoOpenFor.update((current) => (current === type ? null : type)); + } readonly serviceLoading = this.blocksService.catalogLoading; /* diff --git a/src/app/shared/containers-list/containers-list.css b/src/app/shared/containers-list/containers-list.css index 89a8499..0cc7158 100644 --- a/src/app/shared/containers-list/containers-list.css +++ b/src/app/shared/containers-list/containers-list.css @@ -1,8 +1,12 @@ -:host { +/* + * Palette lives here rather than in the markup so the blocks and containers lists, which share + * these class names, stay identical apart from their accent. + */ +.blocks-list-root { display: flex; - flex: 1 1 auto; - min-height: 0; + flex-direction: column; height: 100%; + min-height: 0; } .blocks-list-loading { @@ -12,187 +16,138 @@ height: 8rem; } -.blocks-list-root { - display: flex; - flex: 1 1 auto; - flex-direction: column; - width: 100%; - min-width: 0; - height: 100%; - min-height: 0; - overflow-x: hidden; -} - .blocks-list-search { width: 100%; padding: 8px 0; } +.blocks-list-search mat-form-field { + width: 100%; +} + .blocks-list-empty { - display: flex; - flex: 1; - flex-direction: column; padding-top: 2.5rem; - margin-top: 2.5rem; color: #94a3b8; text-align: center; } .blocks-list-items { display: flex; - flex: 1 1 auto; + flex: 1; flex-direction: column; - align-items: stretch; - gap: 8px; - width: 100%; - min-width: 0; + gap: 4px; min-height: 0; - box-sizing: border-box; - overflow-x: hidden; + padding-bottom: 12px; overflow-y: auto; - padding: 8px 0 20px; } +/* Flat by default: a palette you drag from should not compete with the canvas. */ .blocks-list-card { - display: block; - flex-shrink: 0; - width: 100%; - min-width: 0; - overflow: hidden; + padding: 7px 8px; + border: 1px solid #e2e8f0; + border-left: 3px solid #cbd5e1; + border-radius: 6px; + background: #ffffff; + box-shadow: none; + /* The whole card is the drag source, so say so. */ cursor: grab; - border: 1px solid #dbe7f5; - border-radius: 16px; - background: - radial-gradient(circle at top right, rgba(56, 189, 248, 0.12), transparent 34%), - linear-gradient(180deg, #ffffff 0%, #f7fbff 100%); - box-shadow: - 0 12px 26px rgba(15, 23, 42, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.72); - transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease; + transition: background-color 0.12s ease, border-color 0.12s ease; } -.blocks-list-card--container { - border-color: #bae6fd; - background: - radial-gradient(circle at top right, rgba(14, 165, 233, 0.16), transparent 34%), - linear-gradient(180deg, #ffffff 0%, #f0f9ff 100%); +.blocks-list-card:hover { + border-color: #cbd5e1; + background: #f8fafc; } .blocks-list-card:active { cursor: grabbing; } -.blocks-list-card:hover { - transform: translateY(-2px); - border-color: #38bdf8; - box-shadow: 0 18px 30px rgba(14, 116, 144, 0.14); +.blocks-list-card--container { + border-left-color: #a78bfa; } -.blocks-list-card-header { - display: flex; - align-items: center; - gap: 12px; - padding: 14px 14px 0; -} - -.blocks-list-card-heading { - display: flex; - flex: 1 1 auto; - min-width: 0; - flex-direction: column; -} - -.blocks-list-card-title-row { +.blocks-list-card-row { display: flex; align-items: center; gap: 6px; min-width: 0; } -.blocks-list-card-avatar { - display: flex; - align-items: center; - justify-content: center; - width: 42px; - height: 42px; - margin: 0; - border-radius: 12px; - color: #075985; - background: linear-gradient(180deg, #e0f2fe 0%, #bae6fd 100%); +.blocks-list-card-kind { + flex: 0 0 auto; + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; + color: #94a3b8; } -.blocks-list-card-avatar--container { - color: #155e75; - background: linear-gradient(180deg, #cffafe 0%, #a5f3fc 100%); +.blocks-list-card-kind--interactive { + color: #0ea5e9; } -.blocks-list-card-avatar .mat-icon { - width: 22px; - height: 22px; - font-size: 22px; +.blocks-list-card-kind--container { + color: #7c3aed; +} + +.blocks-list-card-meta { + flex: 1 1 auto; + min-width: 0; } .blocks-list-card-title { - flex: 1 1 auto; - min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: 14px; - font-weight: 700; + font-size: 13px; + font-weight: 600; color: #0f172a; } -.blocks-list-card-info { - display: inline-flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - border: 0; - border-radius: 999px; - background: rgba(224, 242, 254, 0.95); - color: #0369a1; - cursor: default; - flex-shrink: 0; -} - -.blocks-list-card-info .mat-icon { - width: 14px; - height: 14px; - font-size: 14px; -} - -.blocks-list-card-subtitle { - color: #475569; - font-size: 12px; -} - -.blocks-list-card-content { - padding: 0 14px 8px; -} - -.blocks-list-card-copy { - margin: 0; - font-size: 12px; - line-height: 1.45; +.blocks-list-card-sub { + font-size: 11px; color: #64748b; } -.blocks-list-card-actions { - padding: 0 14px 14px !important; - margin: 0; +.blocks-list-card-info { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: 999px; + background: transparent; + color: #cbd5e1; + cursor: help; } -.blocks-list-card-chip { - min-height: 28px; - font-size: 12px; - font-weight: 600; +.blocks-list-card:hover .blocks-list-card-info { + color: #64748b; } -.blocks-list-card-chip-container { - color: #0c4a6e !important; - background: rgba(224, 242, 254, 0.95) !important; - border: 1px solid rgba(56, 189, 248, 0.72); +.blocks-list-card-info--open { + background: #e2e8f0; + color: #334155; +} + +.blocks-list-card-info .mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + line-height: 16px; +} + +/* Expanded in place: these descriptions run to several hundred characters. */ +.blocks-list-card-copy { + margin: 6px 0 2px; + padding-top: 6px; + border-top: 1px dashed #e2e8f0; + font-size: 11.5px; + line-height: 1.45; + color: #475569; + cursor: text; } diff --git a/src/app/shared/containers-list/containers-list.html b/src/app/shared/containers-list/containers-list.html index 164dcdd..babe848 100644 --- a/src/app/shared/containers-list/containers-list.html +++ b/src/app/shared/containers-list/containers-list.html @@ -22,38 +22,33 @@ - -
- + +
+ + +
+
{{ container.type }}
+
Container
-
-
- {{ container.type }} - -
- - Container - -
- - -

{{ container.description }}

-
- - - Container node - - + + +
+ + @if (infoOpenFor() === container.type) { +

{{ container.description }}

+ } }
diff --git a/src/app/shared/containers-list/containers-list.ts b/src/app/shared/containers-list/containers-list.ts index 0c0d460..9d8a17f 100644 --- a/src/app/shared/containers-list/containers-list.ts +++ b/src/app/shared/containers-list/containers-list.ts @@ -4,7 +4,6 @@ import { ContainersService } from '@services/containers/containers'; import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder'; import { FormsModule } from '@angular/forms'; import { MatCardModule } from '@angular/material/card'; -import { MatChipsModule } from '@angular/material/chips'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; @@ -14,7 +13,7 @@ import { BLOCK_TYPE_DRAG_MIME } from '@shared/blocks-list/block-drag'; @Component({ selector: 'app-containers-list', - imports: [FormsModule, MatCardModule, MatChipsModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTooltipModule], + imports: [FormsModule, MatCardModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTooltipModule], templateUrl: './containers-list.html', styleUrl: './containers-list.css', changeDetection: ChangeDetectionStrategy.OnPush @@ -25,6 +24,18 @@ export class ContainersList extends ListStateViewHolder { private containersService = inject(ContainersService); loading: WritableSignal = signal(true); + + /** + * Which card is showing its description, expanded in place. Descriptions run to several hundred + * characters, which a tooltip cannot show readably, and a modal is heavy for a hint you read + * while dragging. + */ + readonly infoOpenFor = signal(null); + + toggleInfo(type: string, event: Event) { + event.stopPropagation(); + this.infoOpenFor.update((current) => (current === type ? null : type)); + } readonly serviceLoading = this.containersService.catalogLoading; containerTypes?: Signal;