Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,16 @@
information does not exist for a screen reader. -->
<span class="visually-hidden">{{ 'notes.pinnedState' | transloco }}</span>
}
<!-- One line for the badge, the title and the glyphs. It was two, and the first of
them was a whole row spent on a badge and three small marks. -->
<!-- The badge and the glyphs, then the title flowing past them. They were a row of
their own above it. ⚠️ The glyphs come **before** the title and not after: a
mark trailing it wraps onto a second line of its own when the title ends near
the edge, and that second line costs the body one of its own. -->
<span class="card-head">
<!-- No format for a todo list: a "TXT" badge would announce a language nothing
shows. -->
@if (!isChecklist()) {
<app-language-badge [language]="note().language" />
}
<span class="card-title" data-testid="note-card-title">
@if (note().title) {
{{ note().title }}
} @else {
{{ 'notes.untitled' | transloco }}
}
</span>
<span class="card-marks">
@if (hasPlaceholders()) {
<span class="card-fields" data-testid="note-card-fields" aria-hidden="true">⚡</span>
Expand All @@ -54,8 +49,12 @@
{{ 'notes.attachmentsCount' | transloco: { count: note().attachmentCount } }}
</span>
}
@if (note().pinned) {
<span class="card-pin" data-testid="note-card-pin" aria-hidden="true">📌</span>
</span>
<span class="card-title" data-testid="note-card-title">
@if (note().title) {
{{ note().title }}
} @else {
{{ 'notes.untitled' | transloco }}
}
</span>
</span>
Expand Down Expand Up @@ -103,6 +102,14 @@
</span>
</button>

<!-- Driven into the card's corner, over its border. A sibling of the button and not a
child: the head is text now, and a glyph inline with the title wrapped onto a line
of its own whenever the title ended near the edge — costing the body a line. Its
text twin stays inside the card, next to the title it belongs to. -->
@if (note().pinned) {
<span class="card-pin" data-testid="note-card-pin" aria-hidden="true">📌</span>
}

<!-- A button with `aria-pressed` rather than an `<input type=checkbox>`: the parent
card is a button, and a native control would inherit its styles. -->
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,57 +48,58 @@ app-copy-button:focus-within {
box-shadow: 0 0 0 1px var(--amber-dim);
}

// The children are <span>s — the parent <button> allows nothing else — so they need
// their block behaviour handed back explicitly.
// The badge, the title and the marks, together. The badge used to own a row of its own:
// ~17px plus an 8px margin out of the 122px a card has, spent before a character of body.
//
// One line carrying the badge, the title and the marks. The badge used to own a row of
// its own: ~17px plus an 8px margin out of the 122px a card has, spent before a character
// of body. `baseline` and not `center` so the badge and the glyphs sit on the title's
// first line whether it takes one line or two.
// ⚠️ A text block, not a flex row, and that is the whole point of this rule.
//
// As a row the title was a column between the badge and the marks, so it wrapped inside
// 76px of a 224px card — where before the badge had a row of its own it had the full
// width. Merging the rows bought the body two lines and charged the title for them.
//
// Here the badge and the marks are **inline**, so the title flows past them: it starts
// after the badge and its second line runs from the left edge, full width. The clamp
// moves up with it — it is the whole head that is two lines now, which is also the line
// budget the ticket asked for.
.card-head {
display: flex;
align-items: baseline;
gap: 7px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 8px;
// Reserves room for the copy and ⋯ buttons floating over the top-right corner.
padding-right: 50px;
// ⚠️ And for the selection tick on the left, which floats at `left: 8px` while the card's
// own padding starts at 14px. It therefore lands on whatever begins this line — and it is
// not a hover-only concern: `checked` keeps it on with no pointer at all, which is the
// state a multiple selection leaves every card in. Unreserved, it sat squarely on the
font-size: 13px;
line-height: 1.3;
// Reserves room for the copy and ⋯ buttons floating over the top-right corner. Measured
// rather than guessed: on a 224px card they start 6px past where this edge lands.
padding-right: 46px;
// ⚠️ And for the selection tick on the left, which floats over the card's own padding.
// Not a hover-only concern: `checked` keeps it on with no pointer at all, which is the
// state a multiple selection leaves every card in. Unreserved it sat squarely on the
// language badge, and on a todo list, which has no badge, it would sit on the title.
// A permanent gutter rather than one that appears with the tick: the second would shift
// the title sideways under the pointer.
padding-left: 14px;
}

.card-pin {
font-size: 11px;
padding-left: 8px;
}

.card-title {
flex: 1;
// Without it a flex item refuses to shrink below its content, and a long unbroken
// title would push the marks out past the card.
min-width: 0;
font-size: 13px;
font-weight: 600;
color: var(--text-0);
line-height: 1.3;
// ⚠️ A budget, not a nicety. The title had none: a long one took three lines and ate
// the body, so two cards side by side showed different amounts of it for no reason the
// user had chosen.
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

// Inline so the title flows past them rather than around a column of their own, and
// before it so that no glyph can end up alone on a line of its own. The nowrap keeps the
// cluster from being split across the two.
.card-marks {
display: flex;
align-items: baseline;
gap: 6px;
flex-shrink: 0;
white-space: nowrap;
}

app-language-badge,
.card-marks {
margin-right: 5px;
}

.card-marks > * + * {
margin-left: 5px;
}

// `app-code-viewer` brings the colouring only; the typography stays decided here.
Expand Down Expand Up @@ -254,11 +255,26 @@ app-copy-button:focus-within {
color: var(--text-2);
}

// ⚠️ Outside the card, into the grid gap, which is what keeps it clear of the copy and ⋯
// buttons: those sit inside at 8px and 34px from the right edge, and anything drawn in
// that band would be covered the moment the pointer arrived.
.card-pin {
position: absolute;
top: -6px;
right: -4px;
z-index: 2;
font-size: 13px;
pointer-events: none;
}

.card-check {
@include unstyled-control;
position: absolute;
top: 8px;
left: 8px;
// Tight into the corner, so the gutter the head reserves for it can be 8px rather than
// the 14 it needed at `left: 8px` — six pixels of a 224px card, on the line that wanted
// them most.
left: 2px;
z-index: 1;
padding: 2px 4px;
font-size: 12px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,34 @@ describe('NoteCardComponent', () => {
/**
* The badge used to own a row of its own above the title — ~17px plus an 8px margin out
* of the 122px a card has, spent before a character of body. Structural rather than
* visual on purpose: jsdom lays nothing out, so what is asserted is that the three share
* one parent, which is what makes them one line.
* visual on purpose: jsdom lays nothing out, so what is asserted is that they share one
* parent, which is what makes them one block of text.
*/
it('puts the badge, the title and the marks on a single line', async () => {
fixture.componentRef.setInput('note', createNote({ title: 'My note', pinned: true, attachmentCount: 2 }));
it('puts the badge, the marks and the title in one block', async () => {
fixture.componentRef.setInput('note', createNote({ title: 'My note', attachmentCount: 2 }));
await fixture.whenStable();

const head = fixture.nativeElement.querySelector('.card-head');
expect(head.querySelector('app-language-badge')).not.toBeNull();
expect(head.querySelector('[data-testid="note-card-title"]')).not.toBeNull();
expect(head.querySelector('[data-testid="note-card-clip"]')).not.toBeNull();
expect(head.querySelector('[data-testid="note-card-pin"]')).not.toBeNull();
expect(head.querySelector('[data-testid="note-card-title"]')).not.toBeNull();
});

/**
* ⚠️ Outside the head, and outside the card button with it. The head is text now, so a
* glyph inline with the title wrapped onto a line of its own whenever the title ended
* near the edge — and that second line cost the body one of its own.
*/
it('drives the pin into the card corner rather than the line of text', async () => {
fixture.componentRef.setInput('note', createNote({ title: 'My note', pinned: true }));
await fixture.whenStable();

const pin = fixture.nativeElement.querySelector('[data-testid="note-card-pin"]');
expect(pin).not.toBeNull();
expect(pin.closest('.card-head')).toBeNull();
expect(pin.closest('.card-shell')).not.toBeNull();
// The state still reaches a screen reader, from inside the card where it belongs.
expect(fixture.nativeElement.querySelector('.card .visually-hidden').textContent).toBe('Note épinglée');
});

it('falls back to a translated placeholder for an untitled note', async () => {
Expand Down