Compare commits
7 Commits
b720759c89
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f89c2fa776 | ||
|
|
c21619c53a | ||
|
|
b34b5851cd | ||
|
|
9581dc88aa | ||
|
|
40b0af6b01 | ||
|
|
2592b19c8f | ||
|
|
99edff828a |
@@ -38,6 +38,10 @@ Tailwind v4 uses the modern CSS-based config in `src/styles/app.css`:
|
|||||||
- DaisyUI configured via `@plugin 'daisyui' { themes: retro --default, coffee --prefersdark; }`
|
- DaisyUI configured via `@plugin 'daisyui' { themes: retro --default, coffee --prefersdark; }`
|
||||||
- `@tailwindcss/typography` for prose styling
|
- `@tailwindcss/typography` for prose styling
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
Use [Tabler icons](https://tabler.io/icons) for every icon. Paste the outline SVG inline and keep `stroke="currentColor"` so it inherits color from its parent (icons in the note header render in `--color-accent`).
|
||||||
|
|
||||||
### Directory Structure
|
### Directory Structure
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -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()"
|
||||||
@@ -90,7 +90,25 @@ const copy = async (url: string, label: string) => {
|
|||||||
class="share-option"
|
class="share-option"
|
||||||
@click="copy(snapshotUrl(), 'Snapshot')"
|
@click="copy(snapshotUrl(), 'Snapshot')"
|
||||||
>
|
>
|
||||||
<span class="share-option-icon">📌</span>
|
<span class="share-option-icon">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="icon icon-tabler icon-tabler-pin"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M15 4.5l-4 4l-4 1.5l-1.5 1.5l7 7l1.5 -1.5l1.5 -4l4 -4" />
|
||||||
|
<path d="M9 15l-4.5 4.5" />
|
||||||
|
<path d="M14.5 4l5.5 5.5" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
<span class="share-option-text">
|
<span class="share-option-text">
|
||||||
<span class="share-option-title">This exact version</span>
|
<span class="share-option-title">This exact version</span>
|
||||||
<span class="share-option-desc">
|
<span class="share-option-desc">
|
||||||
@@ -105,20 +123,31 @@ const copy = async (url: string, label: string) => {
|
|||||||
class="share-option"
|
class="share-option"
|
||||||
@click="copy(livingUrl(), 'Always-latest')"
|
@click="copy(livingUrl(), 'Always-latest')"
|
||||||
>
|
>
|
||||||
<span class="share-option-icon">🔄</span>
|
<span class="share-option-icon">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="icon icon-tabler icon-tabler-repeat"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3" />
|
||||||
|
<path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
<span class="share-option-text">
|
<span class="share-option-text">
|
||||||
<span class="share-option-title">Always latest</span>
|
<span class="share-option-title">Always latest</span>
|
||||||
<span class="share-option-desc">
|
<span class="share-option-desc">
|
||||||
Follows these notes — the reader gets the current version.
|
Follows these notes, so the reader always sees the current version.
|
||||||
</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>
|
||||||
@@ -147,8 +176,8 @@ const copy = async (url: string, label: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.share-option-icon {
|
.share-option-icon {
|
||||||
font-size: 1.4rem;
|
flex-shrink: 0;
|
||||||
line-height: 1.6rem;
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-option-text {
|
.share-option-text {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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' }"
|
||||||
|
|||||||
Reference in New Issue
Block a user