Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aad7c13b0 |
@@ -48,7 +48,7 @@ func (s *Server) handleListNotes(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.internal(w, r, "search notes", err)
|
s.internal(w, r, "search notes", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"notes": s.readable(r, found)})
|
writeJSON(w, http.StatusOK, map[string]any{"notes": briefly(s.readable(r, found))})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func (s *Server) handleListNotes(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.internal(w, r, "notes in project", err)
|
s.internal(w, r, "notes in project", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"notes": found})
|
writeJSON(w, http.StatusOK, map[string]any{"notes": briefly(found)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,41 @@ func (s *Server) handleListNotes(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.internal(w, r, "list notes", err)
|
s.internal(w, r, "list notes", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"notes": found})
|
writeJSON(w, http.StatusOK, map[string]any{"notes": briefly(found)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// briefly replaces each note's body with the first of it.
|
||||||
|
//
|
||||||
|
// The list shows a title and about ninety characters. It was being sent whole
|
||||||
|
// notes to do that: with twelve hundred of them the response was 845 KB, of which
|
||||||
|
// ninety-eight per cent was body text nobody would read on that screen — fetched,
|
||||||
|
// parsed and held in memory on every visit.
|
||||||
|
//
|
||||||
|
// Body is emptied rather than shortened, and that is the important half. A field
|
||||||
|
// called `body` that holds a *piece* of the body is a trap: the editor would open
|
||||||
|
// it, the debounce would save it, and the note would be cut down to its own
|
||||||
|
// preview by nothing more than being looked at. Empty is a shape the client can
|
||||||
|
// check, and it does — see the notes page, which fetches the whole note when it
|
||||||
|
// opens one.
|
||||||
|
//
|
||||||
|
// Four hundred characters rather than ninety: the client strips Markdown before
|
||||||
|
// it shows anything, and a heading marker and a bold word are characters that
|
||||||
|
// vanish. Cheap insurance against a preview that ends up shorter than the line it
|
||||||
|
// is meant to fill.
|
||||||
|
const previewChars = 400
|
||||||
|
|
||||||
|
func briefly(notes []store.Note) []store.Note {
|
||||||
|
out := make([]store.Note, 0, len(notes))
|
||||||
|
for _, n := range notes {
|
||||||
|
body := []rune(n.Body)
|
||||||
|
if len(body) > previewChars {
|
||||||
|
body = body[:previewChars]
|
||||||
|
}
|
||||||
|
n.Preview = string(body)
|
||||||
|
n.Body = ""
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// readable drops what this person may not see.
|
// readable drops what this person may not see.
|
||||||
|
|||||||
@@ -328,12 +328,19 @@ func TestAnImportedNoteKeepsItsPictures(t *testing.T) {
|
|||||||
t.Fatalf("imported %d notes and %d files, want 2 and 1", out.Created, out.Files)
|
t.Fatalf("imported %d notes and %d files, want 2 and 1", out.Created, out.Files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The list carries a preview, not the whole note — see briefly(). The body is
|
||||||
|
// fetched by id, which is what the interface does when it opens one.
|
||||||
_, listed := ts.do(t, "GET", "/api/v1/notes?q=kvittering", nil)
|
_, listed := ts.do(t, "GET", "/api/v1/notes?q=kvittering", nil)
|
||||||
notes := listed["notes"].([]any)
|
notes := listed["notes"].([]any)
|
||||||
if len(notes) != 1 {
|
if len(notes) != 1 {
|
||||||
t.Fatalf("found %d notes", len(notes))
|
t.Fatalf("found %d notes", len(notes))
|
||||||
}
|
}
|
||||||
body := notes[0].(map[string]any)["body"].(string)
|
noteID := notes[0].(map[string]any)["id"].(string)
|
||||||
|
if got := notes[0].(map[string]any)["body"]; got != nil && got != "" {
|
||||||
|
t.Errorf("the list carried a body: %v — an editor handed that would save the truncation", got)
|
||||||
|
}
|
||||||
|
_, whole := ts.do(t, "GET", "/api/v1/notes/"+noteID, nil)
|
||||||
|
body := whole["body"].(string)
|
||||||
|
|
||||||
// The link points at the file where it now lives, not at a path inside an
|
// The link points at the file where it now lives, not at a path inside an
|
||||||
// archive nobody kept.
|
// archive nobody kept.
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ type Note struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||||
|
|
||||||
|
// Preview is the first of the body, for a list that shows one line of it.
|
||||||
|
//
|
||||||
|
// It exists so a list of twelve hundred notes does not have to carry twelve
|
||||||
|
// hundred whole notes: the list rendered 845 KB of JSON to show ninety
|
||||||
|
// characters of each. When it is set, Body is deliberately empty — see
|
||||||
|
// handleListNotes. That is not tidiness but a guard: an editor handed a
|
||||||
|
// truncated body would save the truncation, and the note would be cut down to
|
||||||
|
// its own preview.
|
||||||
|
Preview string `json:"preview,omitempty"`
|
||||||
|
|
||||||
// Links is what the body points at, read out of the text on the way in. Never
|
// Links is what the body points at, read out of the text on the way in. Never
|
||||||
// set by a caller: it is derived, and a caller that set it would be describing
|
// set by a caller: it is derived, and a caller that set it would be describing
|
||||||
// a note whose text says something else.
|
// a note whose text says something else.
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export const da = {
|
|||||||
'beacon.publishHint': 'Uden login, mellemlagret et minut. Viser installationer set de seneste 30 dage — ikke et løbende total, som kun ville stige.',
|
'beacon.publishHint': 'Uden login, mellemlagret et minut. Viser installationer set de seneste 30 dage — ikke et løbende total, som kun ville stige.',
|
||||||
'notes.newHere': 'Ny note',
|
'notes.newHere': 'Ny note',
|
||||||
'notes.noneInProject': 'Ingen noter endnu. En note, der nævner #projektet, dukker også op her.',
|
'notes.noneInProject': 'Ingen noter endnu. En note, der nævner #projektet, dukker også op her.',
|
||||||
|
'notes.clearSearch': 'Ryd søgningen',
|
||||||
|
'notes.found': '{count} noter',
|
||||||
'notes.strike': 'Gennemstreget',
|
'notes.strike': 'Gennemstreget',
|
||||||
'notes.showSource': 'Vis som kode',
|
'notes.showSource': 'Vis som kode',
|
||||||
'notes.source': 'Notens kildetekst',
|
'notes.source': 'Notens kildetekst',
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export const en = {
|
|||||||
'beacon.publishHint': 'No login, cached for a minute. Reports installations seen in the last 30 days — not a running total, which would only ever climb.',
|
'beacon.publishHint': 'No login, cached for a minute. Reports installations seen in the last 30 days — not a running total, which would only ever climb.',
|
||||||
'notes.newHere': 'New note',
|
'notes.newHere': 'New note',
|
||||||
'notes.noneInProject': 'No notes yet. A note that mentions #theproject turns up here too.',
|
'notes.noneInProject': 'No notes yet. A note that mentions #theproject turns up here too.',
|
||||||
|
'notes.clearSearch': 'Clear the search',
|
||||||
|
'notes.found': '{count} notes',
|
||||||
'notes.strike': 'Strikethrough',
|
'notes.strike': 'Strikethrough',
|
||||||
'notes.showSource': 'Show as code',
|
'notes.showSource': 'Show as code',
|
||||||
'notes.source': 'Note source',
|
'notes.source': 'Note source',
|
||||||
|
|||||||
@@ -72,7 +72,33 @@
|
|||||||
return new Date(b[field]).getTime() - new Date(a[field]).getTime();
|
return new Date(b[field]).getTime() - new Date(a[field]).getTime();
|
||||||
});
|
});
|
||||||
let selected = $state(null);
|
let selected = $state(null);
|
||||||
|
// Hvilken række der er valgt. Se open(): editoren venter på hele noten.
|
||||||
|
let selectedId = $state(null);
|
||||||
let query = $state('');
|
let query = $state('');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Det, der står i feltet, og det, der er søgt efter.
|
||||||
|
*
|
||||||
|
* To ting, fordi de ikke er den samme: `draftQuery` opdateres ved hvert
|
||||||
|
* tastetryk,
|
||||||
|
* så feltet føles som et felt, og `query` halter en kvart sekund bagefter, fordi
|
||||||
|
* det er den, der koster en forespørgsel. Uden det sendte "kaffe" fem søgninger
|
||||||
|
* gennem tolv hundrede noter, hvoraf de fire var forældede, inden de kom
|
||||||
|
* tilbage — og svarene kunne lande i den forkerte rækkefølge.
|
||||||
|
*/
|
||||||
|
let draftQuery = $state('');
|
||||||
|
let typing = $state(false);
|
||||||
|
let searchTimer;
|
||||||
|
|
||||||
|
function search(next) {
|
||||||
|
draftQuery = next;
|
||||||
|
typing = next.trim() !== '' && next !== query;
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
query = draftQuery;
|
||||||
|
typing = false;
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
let status = $state('loading');
|
let status = $state('loading');
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
|
|
||||||
@@ -89,7 +115,7 @@
|
|||||||
// opens it rather than dropping the person on a list to find it again.
|
// opens it rather than dropping the person on a list to find it again.
|
||||||
let asked = $derived($page.url.searchParams.get('note'));
|
let asked = $derived($page.url.searchParams.get('note'));
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!asked || selected?.id === asked) return;
|
if (!asked || selectedId === asked) return;
|
||||||
const found = notes.find((n) => n.id === asked);
|
const found = notes.find((n) => n.id === asked);
|
||||||
if (found) open(found);
|
if (found) open(found);
|
||||||
else api.note(asked).then(open).catch(() => {});
|
else api.note(asked).then(open).catch(() => {});
|
||||||
@@ -99,17 +125,57 @@
|
|||||||
try {
|
try {
|
||||||
notes = (await api.notes(q ? { q } : {})).notes ?? [];
|
notes = (await api.notes(q ? { q } : {})).notes ?? [];
|
||||||
status = 'ready';
|
status = 'ready';
|
||||||
if (selected && !notes.some((n) => n.id === selected.id)) open(notes[0] ?? null);
|
if (selectedId && !notes.some((n) => n.id === selectedId)) open(notes[0] ?? null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status = 'failed';
|
status = 'failed';
|
||||||
app.toast(humanMessage(e));
|
app.toast(humanMessage(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function open(note) {
|
/**
|
||||||
|
* Åbner en note — og henter den hel først, hvis listen kun har uddraget.
|
||||||
|
*
|
||||||
|
* Listen bærer `preview` og en tom `body`, så en liste på tolv hundrede noter
|
||||||
|
* ikke er tolv hundrede hele noter. Editoren må aldrig få det uddrag: den ville
|
||||||
|
* gemme det efter sin pause, og noten ville være skåret ned til sit eget uddrag
|
||||||
|
* af ingenting andet end at være blevet kigget på.
|
||||||
|
*
|
||||||
|
* Derfor hentes den hele her. Rækken vises med det samme, så det ikke føles som
|
||||||
|
* at vente; teksten kommer, når den er der.
|
||||||
|
*/
|
||||||
|
async function open(note) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
selected = note;
|
if (!note) {
|
||||||
draft = note?.body ?? '';
|
selectedId = null;
|
||||||
|
selected = null;
|
||||||
|
draft = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rækken fremhæves med det samme; editoren får først noten, når den er hel.
|
||||||
|
//
|
||||||
|
// De to er skilt ad, fordi editoren indlæser sin tekst, når notens *id*
|
||||||
|
// skifter — og hvis den fik uddraget først og hele noten bagefter, ville
|
||||||
|
// id'et være det samme begge gange, så den anden aldrig blev vist. Man ville
|
||||||
|
// sidde med en tom note, der lige havde haft en titel.
|
||||||
|
selectedId = note.id;
|
||||||
|
if (note.body) {
|
||||||
|
selected = note;
|
||||||
|
draft = note.body;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selected = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const full = await api.note(note.id);
|
||||||
|
// Kun hvis den stadig er den, der er valgt: skifter man note, mens
|
||||||
|
// hentningen er undervejs, må den forrige ikke lande oven i den nye.
|
||||||
|
if (selectedId !== note.id) return;
|
||||||
|
selected = full;
|
||||||
|
draft = full.body ?? '';
|
||||||
|
} catch (e) {
|
||||||
|
app.toast(humanMessage(e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,7 +276,10 @@
|
|||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
const saved = await api.updateNote(id, { body });
|
const saved = await api.updateNote(id, { body });
|
||||||
notes = notes.map((n) => (n.id === id ? saved : n));
|
// Svaret er hele noten. Rækken viser `preview`, så den skal med — ellers
|
||||||
|
// står linjen tom, indtil siden hentes igen.
|
||||||
|
const forList = { ...saved, preview: saved.body ?? '' };
|
||||||
|
notes = notes.map((n) => (n.id === id ? forList : n));
|
||||||
// Only if it is still the one on screen: switching notes mid-save must not
|
// Only if it is still the one on screen: switching notes mid-save must not
|
||||||
// drag the previous one's title back onto this one.
|
// drag the previous one's title back onto this one.
|
||||||
if (selected?.id === id) selected = saved;
|
if (selected?.id === id) selected = saved;
|
||||||
@@ -315,13 +384,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<!-- Samme form som søgefeltet i toppen: samme flade, samme kant, samme
|
||||||
class="search"
|
radius. Det var en pille med et forstørrelsesglas tegnet ind som et
|
||||||
type="search"
|
baggrundsbillede med farven skrevet i hånden — så det var det ene sted i
|
||||||
bind:value={query}
|
programmet, der ikke fulgte temaet, og det kunne ses. -->
|
||||||
placeholder={t('notes.search')}
|
<div class="searchbox" class:busy={typing}>
|
||||||
aria-label={t('notes.search')}
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
/>
|
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2" />
|
||||||
|
<path d="M20 20l-3.5-3.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={draftQuery}
|
||||||
|
oninput={(e) => search(e.currentTarget.value)}
|
||||||
|
placeholder={t('notes.search')}
|
||||||
|
aria-label={t('notes.search')}
|
||||||
|
/>
|
||||||
|
{#if draftQuery}
|
||||||
|
<button class="clear" onclick={() => search('')} aria-label={t('notes.clearSearch')}>×</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if query && status === 'ready'}
|
||||||
|
<p class="found">{t('notes.found', { count: notes.length })}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if status === 'loading'}
|
{#if status === 'loading'}
|
||||||
<p class="hint">{t('history.loading')}</p>
|
<p class="hint">{t('history.loading')}</p>
|
||||||
@@ -334,7 +419,7 @@
|
|||||||
<div class="rowline">
|
<div class="rowline">
|
||||||
<button
|
<button
|
||||||
class="row"
|
class="row"
|
||||||
class:on={selected?.id === note.id}
|
class:on={selectedId === note.id}
|
||||||
onclick={() => open(note)}
|
onclick={() => open(note)}
|
||||||
draggable="true"
|
draggable="true"
|
||||||
ondragstart={(e) => onDragStart(e, note)}
|
ondragstart={(e) => onDragStart(e, note)}
|
||||||
@@ -351,7 +436,7 @@
|
|||||||
stod som "i går", og den dato, man sorterede efter, var
|
stod som "i går", og den dato, man sorterede efter, var
|
||||||
ingen steder at se. -->
|
ingen steder at se. -->
|
||||||
<span class="when" title={stamp(note)}>{when(shownDate(note))}</span>
|
<span class="when" title={stamp(note)}>{when(shownDate(note))}</span>
|
||||||
<span class="preview">{plain(note.body)}</span>
|
<span class="preview">{plain(note.preview ?? note.body)}</span>
|
||||||
</span>
|
</span>
|
||||||
{#if note.project_id}
|
{#if note.project_id}
|
||||||
<span class="filed">{projectName(note.project_id)}</span>
|
<span class="filed">{projectName(note.project_id)}</span>
|
||||||
@@ -495,21 +580,74 @@
|
|||||||
— og skifter til det almindelige, så snart feltet får fokus: ikonet forsvandt
|
— og skifter til det almindelige, så snart feltet får fokus: ikonet forsvandt
|
||||||
og formen blev firkantet midt i et klik. Med appearance: none er der kun én
|
og formen blev firkantet midt i et klik. Med appearance: none er der kun én
|
||||||
udgave, og den ser ens ud hele tiden. */
|
udgave, og den ser ens ud hele tiden. */
|
||||||
.search {
|
/* Formet som søgefeltet i toppen — samme flade, kant og radius — så de to
|
||||||
width: 100%;
|
søgefelter på skærmen ligner hinanden i stedet for at være to beslutninger.
|
||||||
appearance: none;
|
Ikonet er en rigtig SVG med `currentColor`, ikke et baggrundsbillede med en
|
||||||
-webkit-appearance: none;
|
farve skrevet ind: den gamle var #8b918d uanset tema. */
|
||||||
padding-left: 30px;
|
.searchbox {
|
||||||
border-radius: 999px;
|
display: flex;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b918d' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.5-3.5'/%3E%3C/svg%3E");
|
align-items: center;
|
||||||
background-repeat: no-repeat;
|
gap: var(--s2);
|
||||||
background-position: 9px center;
|
padding: var(--s2) var(--s3);
|
||||||
background-size: 15px 15px;
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--ink-faint);
|
||||||
|
transition: border-color var(--fast) var(--ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Safaris egen ryd-knap ville sidde oven i vores egen form. */
|
.searchbox:focus-within {
|
||||||
.search::-webkit-search-decoration,
|
border-color: var(--line-strong);
|
||||||
.search::-webkit-search-cancel-button {
|
}
|
||||||
|
|
||||||
|
/* Mens det tastede endnu ikke er søgt efter. Et hint om at der kommer noget,
|
||||||
|
frem for en liste, der ser forkert ud i en kvart sekund. */
|
||||||
|
.searchbox.busy {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbox input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
flex: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear:hover {
|
||||||
|
background: var(--surface-sunken);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.found {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--ink-faint);
|
||||||
|
padding: 0 var(--s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safaris egen ryd-knap ville sidde oven i vores egen. */
|
||||||
|
.searchbox input::-webkit-search-decoration,
|
||||||
|
.searchbox input::-webkit-search-cancel-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,6 +659,25 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rækker uden for skærmen koster ingenting.
|
||||||
|
*
|
||||||
|
* Listen tegner alle noterne. Med ti var det ligegyldigt; med tolv hundrede tog
|
||||||
|
* første tegning to sekunder, og browseren gik i knæ — for hver eneste række
|
||||||
|
* blev der lavet layout og malet, også de tusind, ingen kan se.
|
||||||
|
*
|
||||||
|
* `content-visibility: auto` lader browseren springe netop det over for det,
|
||||||
|
* der er uden for billedet, og tage det, når man ruller derhen. Ingen JavaScript,
|
||||||
|
* ingen rullematematik, ingen liste, der kan komme ud af trit med sig selv —
|
||||||
|
* hvilket er hele grunden til at prøve det her før en vinduesvisning.
|
||||||
|
*
|
||||||
|
* `contain-intrinsic-size` er gættet på, hvor høj en uset række er. Uden det
|
||||||
|
* ville rullebjælken tro, at listen er tom, og hoppe, mens man ruller. Tallet er
|
||||||
|
* målt på en rigtig række: titel plus én linje uddrag. */
|
||||||
|
li {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 54px;
|
||||||
|
}
|
||||||
|
|
||||||
.rowline {
|
.rowline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -868,7 +868,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="/noter?note={note.id}">
|
<a href="/noter?note={note.id}">
|
||||||
<strong>{note.title || t('notes.untitled')}</strong>
|
<strong>{note.title || t('notes.untitled')}</strong>
|
||||||
<span>{note.body.slice(0, 90)}</span>
|
<!-- `preview` når listen bærer det, `body` når noten kom
|
||||||
|
fra et sted, der sender den hel. -->
|
||||||
|
<span>{(note.preview ?? note.body ?? '').slice(0, 90)}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user