Quieten the blocks and containers palettes, description on demand
Each card carried an avatar, a title, a subtitle, the full description as body copy and a chip that repeated the subtitle - five elements to say what is really a name and a kind. In a narrow sidebar that reads as a wall of text you have to scroll past to find the block you want to drag. A card is now one line plus a kind icon, with the description behind the info button. It expands in place rather than in a tooltip or a modal: these descriptions run to several hundred characters, which a tooltip cannot render readably, and a modal is heavy for a hint you read while reaching for a drag. Only one description is open at a time, so the list cannot unfold into the same wall of text by another route. Dragging is untouched and now says so: the whole card shows a grab cursor and a delayed tooltip naming the gesture, and a test pins the draggable attribute so a future restyle cannot quietly break the palette's only real job. The chip removal also drops MatChipsModule from both components. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6d1efc4f69
commit
5476324d8c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,6 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="flex py-2 text-sm w-full min-w-0 items-center text-center justify-center h-18 overflow-x-hidden">
|
||||
<app-ordering [orderFields]="orderFields" [orderView]="orderView" (orderChanged)="onOrderChanged($event)"></app-ordering>
|
||||
</div>
|
||||
-->
|
||||
|
||||
@if (filteredBlocks().length === 0) {
|
||||
<div class="blocks-list-empty">
|
||||
No blocks found
|
||||
|
|
@ -28,38 +22,38 @@
|
|||
<mat-card
|
||||
class="blocks-list-card"
|
||||
draggable="true"
|
||||
[matTooltip]="'Drag ' + block.type + ' onto the canvas'"
|
||||
matTooltipShowDelay="600"
|
||||
(dragstart)="onDragStart($event, block)">
|
||||
<mat-card-header class="blocks-list-card-header">
|
||||
<div matCardAvatar class="blocks-list-card-avatar">
|
||||
<mat-icon [fontIcon]="block.userInteractive ? 'touch_app' : 'auto_awesome'"></mat-icon>
|
||||
</div>
|
||||
<div class="blocks-list-card-heading">
|
||||
<div class="blocks-list-card-title-row">
|
||||
<mat-card-title class="blocks-list-card-title">{{ block.type }}</mat-card-title>
|
||||
<button
|
||||
type="button"
|
||||
class="blocks-list-card-info"
|
||||
matTooltip="{{ block.description }}"
|
||||
aria-label="Show block description"
|
||||
title=""
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="$event.stopPropagation()">
|
||||
<mat-icon fontIcon="info"></mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="blocks-list-card-row">
|
||||
<mat-icon
|
||||
class="blocks-list-card-kind"
|
||||
[class.blocks-list-card-kind--interactive]="block.userInteractive"
|
||||
[fontIcon]="block.userInteractive ? 'touch_app' : 'auto_awesome'"></mat-icon>
|
||||
|
||||
<div class="blocks-list-card-meta">
|
||||
<div class="blocks-list-card-title">{{ block.type }}</div>
|
||||
<div class="blocks-list-card-sub">
|
||||
{{ block.userInteractive ? 'Human step' : 'Automated step' }}
|
||||
</div>
|
||||
<mat-card-subtitle class="blocks-list-card-subtitle">
|
||||
{{ block.userInteractive ? 'Interactive' : 'Automatic' }}
|
||||
</mat-card-subtitle>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="blocks-list-card-content">
|
||||
<p class="blocks-list-card-copy">{{ block.description }}</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions class="blocks-list-card-actions" align="end">
|
||||
<mat-chip class="blocks-list-card-chip" [class.blocks-list-card-chip-interactive]="block.userInteractive">
|
||||
{{ block.userInteractive ? 'Human step' : 'Automated step' }}
|
||||
</mat-chip>
|
||||
</mat-card-actions>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="blocks-list-card-info"
|
||||
[class.blocks-list-card-info--open]="infoOpenFor() === block.type"
|
||||
[attr.aria-expanded]="infoOpenFor() === block.type"
|
||||
[attr.aria-label]="'What ' + block.type + ' does'"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="toggleInfo(block.type, $event)">
|
||||
<mat-icon fontIcon="info_outline"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (infoOpenFor() === block.type) {
|
||||
<p class="blocks-list-card-copy">{{ block.description }}</p>
|
||||
}
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<BlocksList>;
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<BlockType> {
|
|||
private blocksService = inject(BlocksService);
|
||||
|
||||
loading: WritableSignal<boolean> = 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<string | null>(null);
|
||||
|
||||
toggleInfo(type: string, event: Event) {
|
||||
event.stopPropagation();
|
||||
this.infoOpenFor.update((current) => (current === type ? null : type));
|
||||
}
|
||||
readonly serviceLoading = this.blocksService.catalogLoading;
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,38 +22,33 @@
|
|||
<mat-card
|
||||
class="blocks-list-card blocks-list-card--container"
|
||||
draggable="true"
|
||||
[matTooltip]="'Drag ' + container.type + ' onto the canvas'"
|
||||
matTooltipShowDelay="600"
|
||||
(dragstart)="onDragStart($event, container)">
|
||||
<mat-card-header class="blocks-list-card-header">
|
||||
<div matCardAvatar class="blocks-list-card-avatar blocks-list-card-avatar--container">
|
||||
<mat-icon fontIcon="inventory_2"></mat-icon>
|
||||
|
||||
<div class="blocks-list-card-row">
|
||||
<mat-icon class="blocks-list-card-kind blocks-list-card-kind--container" fontIcon="inventory_2"></mat-icon>
|
||||
|
||||
<div class="blocks-list-card-meta">
|
||||
<div class="blocks-list-card-title">{{ container.type }}</div>
|
||||
<div class="blocks-list-card-sub">Container</div>
|
||||
</div>
|
||||
<div class="blocks-list-card-heading">
|
||||
<div class="blocks-list-card-title-row">
|
||||
<mat-card-title class="blocks-list-card-title">{{ container.type }}</mat-card-title>
|
||||
<button
|
||||
type="button"
|
||||
class="blocks-list-card-info"
|
||||
matTooltip="{{ container.description }}"
|
||||
aria-label="Show container description"
|
||||
title=""
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="$event.stopPropagation()">
|
||||
<mat-icon fontIcon="info"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-card-subtitle class="blocks-list-card-subtitle">
|
||||
Container
|
||||
</mat-card-subtitle>
|
||||
</div>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="blocks-list-card-content">
|
||||
<p class="blocks-list-card-copy">{{ container.description }}</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions class="blocks-list-card-actions" align="end">
|
||||
<mat-chip class="blocks-list-card-chip blocks-list-card-chip-container">
|
||||
Container node
|
||||
</mat-chip>
|
||||
</mat-card-actions>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="blocks-list-card-info"
|
||||
[class.blocks-list-card-info--open]="infoOpenFor() === container.type"
|
||||
[attr.aria-expanded]="infoOpenFor() === container.type"
|
||||
[attr.aria-label]="'What ' + container.type + ' does'"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="toggleInfo(container.type, $event)">
|
||||
<mat-icon fontIcon="info_outline"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (infoOpenFor() === container.type) {
|
||||
<p class="blocks-list-card-copy">{{ container.description }}</p>
|
||||
}
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<BlockType> {
|
|||
private containersService = inject(ContainersService);
|
||||
|
||||
loading: WritableSignal<boolean> = 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<string | null>(null);
|
||||
|
||||
toggleInfo(type: string, event: Event) {
|
||||
event.stopPropagation();
|
||||
this.infoOpenFor.update((current) => (current === type ? null : type));
|
||||
}
|
||||
readonly serviceLoading = this.containersService.catalogLoading;
|
||||
containerTypes?: Signal<BlockType[]>;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue