Compare commits

5 Commits

Author SHA1 Message Date
Julien Calixte
f89c2fa776 style: just changed the view latest button
All checks were successful
CI / verify (push) Successful in 2m7s
2026-07-12 12:36:54 +02:00
Julien Calixte
c21619c53a feat(notes): drop the redundant Close button from share popup
All checks were successful
CI / verify (push) Successful in 1m6s
The backdrop already dismisses the dialog on outside click.
2026-07-11 12:57:44 +02:00
Julien Calixte
b34b5851cd fix(notes): color the share trigger icon with accent
Scoped styles in HeaderNote don't cross into the ShareNote child
component, so text-base-content painted the icon white/black instead
of matching the other header icons.
2026-07-11 12:57:41 +02:00
Julien Calixte
9581dc88aa fix(notes): don't save empty content over a note that hasn't loaded
All checks were successful
CI / verify (push) Successful in 1m9s
Entering and exiting edit mode while a note was still loading (or after a
failed fetch) triggered a save of the empty placeholder, clobbering the
real file on GitHub with an empty commit. Gate the dirty check, performSave,
and the edit button on a loaded baseline.
2026-07-11 12:51:27 +02:00
Julien Calixte
40b0af6b01 style(welcome): tighten landing copy and reduce em-dash use
All checks were successful
CI / verify (push) Successful in 1m7s
Rework the lede parenthetical and thin the calm/quiet/pool
metaphor where it doubled up on itself, to soften AI-writing tells.
2026-07-11 12:21:15 +02:00
3 changed files with 32 additions and 36 deletions

View File

@@ -55,7 +55,7 @@ const copy = async (url: string, label: string) => {
<template> <template>
<button <button
class="btn btn-ghost btn-circle text-base-content" class="btn btn-ghost btn-circle text-accent"
title="Share this view" title="Share this view"
aria-label="Share this view" aria-label="Share this view"
onclick="share_modal.showModal()" onclick="share_modal.showModal()"
@@ -148,12 +148,6 @@ const copy = async (url: string, label: string) => {
</span> </span>
</span> </span>
</button> </button>
<div class="modal-action">
<form method="dialog">
<button class="btn btn-ghost">Close</button>
</form>
</div>
</div> </div>
<form method="dialog" class="modal-backdrop"> <form method="dialog" class="modal-backdrop">
<button>close</button> <button>close</button>

View File

@@ -1,11 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import { computed, defineAsyncComponent, onMounted, ref, watch } from "vue"
computed,
defineAsyncComponent,
onMounted,
ref,
watch
} from "vue"
import { useEditionMode } from "@/hooks/useEditionMode" import { useEditionMode } from "@/hooks/useEditionMode"
import { useFile } from "@/hooks/useFile.hook" import { useFile } from "@/hooks/useFile.hook"
@@ -188,6 +182,19 @@ const loadNote = async () => {
onMounted(loadNote) onMounted(loadNote)
// A note only has a trustworthy baseline once it has finished loading. While
// it is still loading (or after a failed load) rawContent holds the empty
// placeholder and initialRawContent is null, so a naive
// `rawContent !== initialRawContent` check reads as a huge edit — and saving
// that would clobber the real file on GitHub with an empty commit. Gate every
// "is this dirty?" decision on a loaded baseline.
const isDirty = computed(
() =>
loadStatus.value === "ready" &&
initialRawContent.value !== null &&
rawContent.value !== initialRawContent.value
)
watch( watch(
path, path,
(p) => { (p) => {
@@ -204,8 +211,7 @@ useMarkdownPostRender(content, () => `.note-${sha.value}`, {
macroplan: true, macroplan: true,
mermaid: () => rawContent.value.includes("```mermaid"), mermaid: () => rawContent.value.includes("```mermaid"),
shikiji: () => isMarkdown.value && rawContent.value.includes("```"), shikiji: () => isMarkdown.value && rawContent.value.includes("```"),
images: () => images: () => (/\!\[.*?\]\(.*?\)/.test(rawContent.value) ? props.sha : null),
/\!\[.*?\]\(.*?\)/.test(rawContent.value) ? props.sha : null,
triggers: [mode] triggers: [mode]
}) })
@@ -215,6 +221,13 @@ const performSave = async (overrideSha?: string) => {
return return
} }
// Defence in depth: never push content we didn't successfully load, or we'd
// overwrite the file on GitHub with the empty placeholder.
if (loadStatus.value !== "ready" || initialRawContent.value === null) {
console.warn("refusing to save a note that hasn't finished loading")
return
}
const editedSha = overrideSha ?? (await getEditedSha()) ?? sha.value const editedSha = overrideSha ?? (await getEditedSha()) ?? sha.value
const { sha: newSha, conflict } = await updateFile({ const { sha: newSha, conflict } = await updateFile({
content: rawContent.value, content: rawContent.value,
@@ -287,8 +300,7 @@ watch(mode, async (newMode) => {
return return
} }
const hasUserFinishedToEdit = const hasUserFinishedToEdit = newMode === "read" && isDirty.value
newMode === "read" && rawContent.value !== initialRawContent.value
if (!hasUserFinishedToEdit) { if (!hasUserFinishedToEdit) {
return return
@@ -336,7 +348,7 @@ const onBadgeClick = async () => {
return return
} }
const hasUnsavedEdits = rawContent.value !== initialRawContent.value const hasUnsavedEdits = isDirty.value
if (hasUnsavedEdits) { if (hasUnsavedEdits) {
await handleConflict() await handleConflict()
return return
@@ -377,7 +389,7 @@ const onBadgeClick = async () => {
class="action" class="action"
/> />
<button <button
v-if="isMarkdown && canPush" v-if="isMarkdown && canPush && loadStatus === 'ready'"
class="action button is-text is-light" class="action button is-text is-light"
:class="{ 'is-link': mode === 'edit' }" :class="{ 'is-link': mode === 'edit' }"
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''" :style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
@@ -489,7 +501,7 @@ const onBadgeClick = async () => {
<span>You're viewing an older shared version.</span> <span>You're viewing an older shared version.</span>
<button <button
type="button" type="button"
class="snapshot-banner-action" class="btn bt-sm btn-soft btn-info"
@click="viewLatest" @click="viewLatest"
> >
View latest View latest
@@ -588,16 +600,6 @@ $border-color: rgba(18, 19, 58, 0.2);
font-size: 0.85em; font-size: 0.85em;
} }
.snapshot-banner-action {
flex-shrink: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--color-primary);
cursor: pointer;
text-decoration: underline;
}
.action { .action {
margin: 0; margin: 0;

View File

@@ -381,10 +381,10 @@ onMounted(() => {
comes to rest. comes to rest.
</h1> </h1>
<p class="lede"> <p class="lede">
Point it at any markdown a GitHub repo of your own, or public Point it at any markdown: a GitHub repo of your own, or public
notes from the open ATProto network and Remanso turns it into a notes from the open ATProto network. Remanso turns it into a
calm, stackable notebook with backlinks where your thinking stackable notebook with backlinks where your thinking finally
finally runs clear. runs clear.
</p> </p>
<div class="hero-ed-paths"> <div class="hero-ed-paths">
<!-- CTA 01: GitHub repo --> <!-- CTA 01: GitHub repo -->
@@ -472,7 +472,7 @@ onMounted(() => {
</div> </div>
</div> </div>
<div v-else-if="publicNotesLoadFailed" class="network-empty"> <div v-else-if="publicNotesLoadFailed" class="network-empty">
The pool is quiet right now. Nothing has drifted in yet.
</div> </div>
<router-link <router-link
:to="{ name: 'PublicNoteListView' }" :to="{ name: 'PublicNoteListView' }"