Compare commits
31 Commits
d31c774ace
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2f2a3114b | ||
|
|
2f71566083 | ||
|
|
80ae544a28 | ||
|
|
bfd981de13 | ||
|
|
453332513a | ||
|
|
abc0113c8e | ||
|
|
52deb5feb4 | ||
|
|
9e07204430 | ||
|
|
cd60429145 | ||
|
|
aad07184fd | ||
|
|
76829afba2 | ||
|
|
05f59a568d | ||
|
|
559bfccd08 | ||
|
|
f8ae4351d6 | ||
|
|
30f200df30 | ||
|
|
58568e2245 | ||
|
|
fd7d06ce69 | ||
|
|
5a9c0a3704 | ||
|
|
e425be5c96 | ||
|
|
84803c45dd | ||
|
|
a526a9f6af | ||
|
|
08e01d8484 | ||
|
|
c88340d5f1 | ||
|
|
550b3cf019 | ||
|
|
2f05b93f51 | ||
|
|
cc266eac7c | ||
|
|
be006f08b4 | ||
|
|
55ee3bddeb | ||
|
|
1f324208d2 | ||
|
|
002cf9a4b1 | ||
|
|
efe9c01e63 |
@@ -1,11 +1,11 @@
|
|||||||
# ---- Stage 1: deps (only invalidated when lockfile changes) ----
|
# ---- Stage 1: deps (only invalidated when lockfile changes) ----
|
||||||
FROM node:22-alpine AS deps
|
FROM node:22-alpine AS deps
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ RUN pnpm install --frozen-lockfile
|
|||||||
# ---- Stage 2: build (invalidated on any source change) ----
|
# ---- Stage 2: build (invalidated on any source change) ----
|
||||||
FROM node:22-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
40
_scripts/build-monochrome-icon.ts
Normal file
40
_scripts/build-monochrome-icon.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import path from "path"
|
||||||
|
import sharp from "sharp"
|
||||||
|
|
||||||
|
// PWA spec: `purpose: "monochrome"` icons are *masks*. The user agent ignores
|
||||||
|
// RGB and uses only the alpha channel as the silhouette, then paints it with
|
||||||
|
// the platform theme color. So the source PNG must be RGBA with the silhouette
|
||||||
|
// in alpha, NOT a black-on-white RGB image.
|
||||||
|
|
||||||
|
const SRC = path.resolve(__dirname, "../public/favicon.png")
|
||||||
|
const OUT = path.resolve(__dirname, "../public/monochromeicon.png")
|
||||||
|
const SIZE = 1024
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { data, info } = await sharp(SRC)
|
||||||
|
.resize(SIZE, SIZE, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
|
||||||
|
if (info.channels !== 4) throw new Error(`expected RGBA, got ${info.channels} channels`)
|
||||||
|
|
||||||
|
const out = Buffer.alloc(data.length)
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
out[i] = 0
|
||||||
|
out[i + 1] = 0
|
||||||
|
out[i + 2] = 0
|
||||||
|
out[i + 3] = data[i + 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
await sharp(out, { raw: { width: SIZE, height: SIZE, channels: 4 } })
|
||||||
|
.png({ compressionLevel: 9 })
|
||||||
|
.toFile(OUT)
|
||||||
|
|
||||||
|
console.log(`Wrote ${OUT} (${SIZE}x${SIZE} RGBA)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="emerald">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "remanso",
|
"name": "remanso",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "pnpm@11.0.9",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
allowBuilds:
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
core-js: true
|
core-js: true
|
||||||
|
esbuild: true
|
||||||
|
fsevents: true
|
||||||
|
sharp: true
|
||||||
|
vue-demi: true
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB |
BIN
public/monochromeicon.png
Normal file
BIN
public/monochromeicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
@@ -5,6 +5,7 @@ import HeaderNote from "@/components/HeaderNote.vue"
|
|||||||
import SignInGithub from "@/components/SignInGithub.vue"
|
import SignInGithub from "@/components/SignInGithub.vue"
|
||||||
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
import StackedNote from "@/components/StackedNote.vue"
|
import StackedNote from "@/components/StackedNote.vue"
|
||||||
|
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||||
import { useLinks } from "@/hooks/useLinks.hook"
|
import { useLinks } from "@/hooks/useLinks.hook"
|
||||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||||
import { useNoteView } from "@/hooks/useNoteView.hook"
|
import { useNoteView } from "@/hooks/useNoteView.hook"
|
||||||
@@ -44,6 +45,7 @@ const { listenToClick } = useLinks("note-display")
|
|||||||
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||||
|
|
||||||
const { titles } = useNoteView()
|
const { titles } = useNoteView()
|
||||||
|
const { isLogged } = useGitHubLogin()
|
||||||
useResizeContainer("note-container", stackedNotes)
|
useResizeContainer("note-container", stackedNotes)
|
||||||
|
|
||||||
const renderedContent = computed(() =>
|
const renderedContent = computed(() =>
|
||||||
@@ -104,8 +106,13 @@ onUnmounted(() => {
|
|||||||
<slot />
|
<slot />
|
||||||
<skeleton-loader v-if="isLoading" />
|
<skeleton-loader v-if="isLoading" />
|
||||||
<div v-else-if="withContent && !hasContent" class="repo-not-found">
|
<div v-else-if="withContent && !hasContent" class="repo-not-found">
|
||||||
<p>This repository is not accessible.</p>
|
<template v-if="isLogged">
|
||||||
<sign-in-github />
|
<p>This repository is not accessible.</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p>This repository is private. Sign in to view it.</p>
|
||||||
|
<sign-in-github />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-else-if="withContent && hasContent"
|
v-else-if="withContent && hasContent"
|
||||||
@@ -259,7 +266,7 @@ $header-height: 40px;
|
|||||||
|
|
||||||
.note {
|
.note {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100dvh;
|
height: 100svh;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ onMounted(() => {
|
|||||||
started editing. If you save now, their changes will be overwritten.
|
started editing. If you save now, their changes will be overwritten.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="modal-action flex-col gap-2 sm:flex-row sm:justify-end">
|
<div class="modal-action flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost"
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ defineEmits<{ (e: "click"): void }>()
|
|||||||
const formatTime = (d: Date) =>
|
const formatTime = (d: Date) =>
|
||||||
d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
||||||
|
|
||||||
const minutesAgo = (d: Date) =>
|
|
||||||
Math.max(1, Math.round((Date.now() - d.getTime()) / 60000))
|
|
||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
switch (props.status) {
|
switch (props.status) {
|
||||||
case "verified":
|
case "verified":
|
||||||
@@ -24,10 +21,6 @@ const label = computed(() => {
|
|||||||
return "Checking…"
|
return "Checking…"
|
||||||
case "outdated":
|
case "outdated":
|
||||||
return "Outdated"
|
return "Outdated"
|
||||||
case "stale-known":
|
|
||||||
return props.lastCheckedAt
|
|
||||||
? `Checked ${minutesAgo(props.lastCheckedAt)}m ago`
|
|
||||||
: "Not checked"
|
|
||||||
case "offline":
|
case "offline":
|
||||||
return "Can’t reach GitHub"
|
return "Can’t reach GitHub"
|
||||||
case "unknown":
|
case "unknown":
|
||||||
@@ -44,8 +37,6 @@ const tooltip = computed(() => {
|
|||||||
: "Click to re-check."
|
: "Click to re-check."
|
||||||
case "outdated":
|
case "outdated":
|
||||||
return "GitHub has a newer version. Click to pull latest."
|
return "GitHub has a newer version. Click to pull latest."
|
||||||
case "stale-known":
|
|
||||||
return "Click to verify against GitHub."
|
|
||||||
case "offline":
|
case "offline":
|
||||||
return "Could not reach GitHub. Click to retry."
|
return "Could not reach GitHub. Click to retry."
|
||||||
case "checking":
|
case "checking":
|
||||||
@@ -88,14 +79,9 @@ const isBusy = computed(() => props.status === "checking")
|
|||||||
<path d="M15 19l2 2l4 -4" />
|
<path d="M15 19l2 2l4 -4" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg
|
<svg
|
||||||
v-else-if="status === 'unknown' || status === 'stale-known'"
|
v-else-if="status === 'unknown'"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler"
|
class="icon icon-tabler icon-tabler-cloud-question"
|
||||||
:class="
|
|
||||||
status === 'stale-known'
|
|
||||||
? 'icon-tabler-clock'
|
|
||||||
: 'icon-tabler-cloud-question'
|
|
||||||
"
|
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -105,19 +91,13 @@ const isBusy = computed(() => props.status === "checking")
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<template v-if="status === 'stale-known'">
|
<path
|
||||||
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
d="M14.5 18.004h-7.843c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.393 -1.762 1.794 -3.2 3.675 -3.773c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99"
|
||||||
<path d="M12 7v5l3 3" />
|
/>
|
||||||
</template>
|
<path d="M19 22v.01" />
|
||||||
<template v-else>
|
<path
|
||||||
<path
|
d="M19 19a2.003 2.003 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"
|
||||||
d="M14.5 18.004h-7.843c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.393 -1.762 1.794 -3.2 3.675 -3.773c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99"
|
/>
|
||||||
/>
|
|
||||||
<path d="M19 22v.01" />
|
|
||||||
<path
|
|
||||||
d="M19 19a2.003 2.003 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</svg>
|
</svg>
|
||||||
<svg
|
<svg
|
||||||
v-else-if="status === 'outdated'"
|
v-else-if="status === 'outdated'"
|
||||||
@@ -171,7 +151,6 @@ const isBusy = computed(() => props.status === "checking")
|
|||||||
/>
|
/>
|
||||||
<path d="M3 3l18 18" />
|
<path d="M3 3l18 18" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="freshness-label">{{ label }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -213,7 +192,6 @@ const isBusy = computed(() => props.status === "checking")
|
|||||||
}
|
}
|
||||||
|
|
||||||
.state-unknown,
|
.state-unknown,
|
||||||
.state-stale-known,
|
|
||||||
.state-checking {
|
.state-checking {
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
|||||||
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
|
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
|
||||||
import { getFileLanguage, isMarkdownPath } from "@/utils/fileLanguage"
|
import { getFileLanguage, isMarkdownPath } from "@/utils/fileLanguage"
|
||||||
import { filenameToNoteTitle } from "@/utils/noteTitle"
|
import { filenameToNoteTitle } from "@/utils/noteTitle"
|
||||||
|
import { errorMessage } from "@/utils/notif"
|
||||||
|
|
||||||
const LinkedNotes = defineAsyncComponent(
|
const LinkedNotes = defineAsyncComponent(
|
||||||
() => import("@/components/LinkedNotes.vue")
|
() => import("@/components/LinkedNotes.vue")
|
||||||
@@ -239,6 +240,30 @@ const onConflictOverwrite = async () => {
|
|||||||
const onConflictCancel = () => {
|
const onConflictCancel = () => {
|
||||||
if (mode.value === "read") toggleMode()
|
if (mode.value === "read") toggleMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onBadgeClick = async () => {
|
||||||
|
try {
|
||||||
|
if (freshnessStatus.value !== "outdated") {
|
||||||
|
await checkFreshness()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUnsavedEdits = rawContent.value !== initialRawContent.value
|
||||||
|
if (hasUnsavedEdits) {
|
||||||
|
conflictOpen.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRaw = await pullLatest()
|
||||||
|
if (newRaw !== null) {
|
||||||
|
rawContent.value = newRaw
|
||||||
|
initialRawContent.value = newRaw
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("freshness badge click failed", error)
|
||||||
|
errorMessage("❌ Couldn't pull latest from GitHub")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -250,27 +275,13 @@ const onConflictCancel = () => {
|
|||||||
[`note-${sha}`]: true
|
[`note-${sha}`]: true
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<a
|
<div class="title-stacked-note breadcrumbs text-sm" :class="titleClassName">
|
||||||
class="title-stacked-note-link"
|
|
||||||
@click.prevent="scrollToFocusedNote({ noteId: props.sha })"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="title-stacked-note breadcrumbs text-sm"
|
|
||||||
:class="titleClassName"
|
|
||||||
>
|
|
||||||
<ul>
|
|
||||||
<li v-for="(part, i) in breadcrumbs" :key="i">
|
|
||||||
{{ part }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<section class="text-content">
|
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<note-freshness-badge
|
<note-freshness-badge
|
||||||
:status="freshnessStatus"
|
:status="freshnessStatus"
|
||||||
:last-checked-at="lastCheckedAt"
|
:last-checked-at="lastCheckedAt"
|
||||||
@click="checkFreshness"
|
@click="onBadgeClick"
|
||||||
|
class="action"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="isMarkdown"
|
v-if="isMarkdown"
|
||||||
@@ -279,50 +290,62 @@ const onConflictCancel = () => {
|
|||||||
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
||||||
@click="toggleMode"
|
@click="toggleMode"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="mode === 'read'"
|
v-if="mode === 'read'"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler icon-tabler-edit"
|
class="icon icon-tabler icon-tabler-edit"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path
|
<path
|
||||||
d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"
|
d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"
|
d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"
|
||||||
/>
|
/>
|
||||||
<path d="M16 5l3 3" />
|
<path d="M16 5l3 3" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg
|
<svg
|
||||||
v-else
|
v-else
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler icon-tabler-device-floppy"
|
class="icon icon-tabler icon-tabler-device-floppy"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path
|
<path
|
||||||
d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"
|
d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"
|
||||||
/>
|
/>
|
||||||
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
<path d="M14 4l0 4l-6 0l0 -4" />
|
<path d="M14 4l0 4l-6 0l0 -4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<a
|
||||||
|
class="title-stacked-note-link"
|
||||||
|
@click.prevent="scrollToFocusedNote({ noteId: props.sha })"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(part, i) in breadcrumbs" :key="i">
|
||||||
|
{{ part }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<section class="text-content">
|
||||||
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
||||||
<edit-note v-model="rawContent" />
|
<edit-note v-model="rawContent" />
|
||||||
</div>
|
</div>
|
||||||
@@ -370,7 +393,6 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
background-color: var(--color-base-100);
|
background-color: var(--color-base-100);
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
li {
|
li {
|
||||||
@@ -396,12 +418,15 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin: 0.2rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
@@ -410,7 +435,7 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.stacked-note {
|
.stacked-note {
|
||||||
padding: 0 0.75rem 1rem;
|
padding: 0 0.75rem 1rem;
|
||||||
height: 100dvh;
|
height: 100svh;
|
||||||
|
|
||||||
section {
|
section {
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
@@ -433,7 +458,7 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title-stacked-note {
|
.title-stacked-note {
|
||||||
padding: 0 1rem;
|
padding: 0;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
@@ -441,6 +466,12 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
a {
|
a {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
.action {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
background-color: var(--color-base-100);
|
background-color: var(--color-base-100);
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
li {
|
li {
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ export const useGitHubContent = ({
|
|||||||
const octokit = await getOctokit()
|
const octokit = await getOctokit()
|
||||||
const response = await octokit.request(
|
const response = await octokit.request(
|
||||||
"GET /repos/{owner}/{repo}/contents/{path}",
|
"GET /repos/{owner}/{repo}/contents/{path}",
|
||||||
{ owner: user, repo, path }
|
{
|
||||||
|
owner: user,
|
||||||
|
repo,
|
||||||
|
path,
|
||||||
|
headers: { "X-GitHub-Api-Version": "2022-11-28" }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
const data = response?.data
|
const data = response?.data
|
||||||
if (Array.isArray(data) || !data) return null
|
if (Array.isArray(data) || !data) return null
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, Ref, ref } from "vue"
|
import { Ref, ref } from "vue"
|
||||||
|
|
||||||
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
||||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||||
@@ -10,12 +10,9 @@ export type FreshnessStatus =
|
|||||||
| "unknown"
|
| "unknown"
|
||||||
| "checking"
|
| "checking"
|
||||||
| "verified"
|
| "verified"
|
||||||
| "stale-known"
|
|
||||||
| "outdated"
|
| "outdated"
|
||||||
| "offline"
|
| "offline"
|
||||||
|
|
||||||
const STALE_AFTER_MS = 2 * 60 * 1000
|
|
||||||
|
|
||||||
export const useNoteFreshness = ({
|
export const useNoteFreshness = ({
|
||||||
user,
|
user,
|
||||||
repo,
|
repo,
|
||||||
@@ -32,54 +29,45 @@ export const useNoteFreshness = ({
|
|||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
const { fetchLatestSha } = useGitHubContent({ user, repo })
|
const { fetchLatestSha } = useGitHubContent({ user, repo })
|
||||||
|
|
||||||
const rawStatus = ref<FreshnessStatus>("unknown")
|
const status = ref<FreshnessStatus>("unknown")
|
||||||
const lastCheckedAt = ref<Date | null>(null)
|
const lastCheckedAt = ref<Date | null>(null)
|
||||||
const latestSha = ref<string | null>(null)
|
const latestSha = ref<string | null>(null)
|
||||||
const tick = ref(0)
|
|
||||||
let staleTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
const status = computed<FreshnessStatus>(() => {
|
|
||||||
void tick.value
|
|
||||||
if (rawStatus.value !== "verified") return rawStatus.value
|
|
||||||
if (!lastCheckedAt.value) return rawStatus.value
|
|
||||||
const age = Date.now() - lastCheckedAt.value.getTime()
|
|
||||||
return age > STALE_AFTER_MS ? "stale-known" : "verified"
|
|
||||||
})
|
|
||||||
|
|
||||||
const armStaleTimer = () => {
|
|
||||||
if (staleTimer) clearTimeout(staleTimer)
|
|
||||||
staleTimer = setTimeout(() => {
|
|
||||||
tick.value++
|
|
||||||
}, STALE_AFTER_MS + 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedSha = async () => (await getEditedSha()) ?? sha.value
|
const expectedSha = async () => (await getEditedSha()) ?? sha.value
|
||||||
|
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
if (!path.value) return
|
if (!path.value) return
|
||||||
rawStatus.value = "checking"
|
status.value = "checking"
|
||||||
const remoteSha = await fetchLatestSha(path.value)
|
const remoteSha = await fetchLatestSha(path.value)
|
||||||
if (remoteSha === null) {
|
if (remoteSha === null) {
|
||||||
rawStatus.value = "offline"
|
status.value = "offline"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
latestSha.value = remoteSha
|
latestSha.value = remoteSha
|
||||||
lastCheckedAt.value = new Date()
|
lastCheckedAt.value = new Date()
|
||||||
const local = await expectedSha()
|
const local = await expectedSha()
|
||||||
rawStatus.value = remoteSha === local ? "verified" : "outdated"
|
status.value = remoteSha === local ? "verified" : "outdated"
|
||||||
armStaleTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pullLatest = async (): Promise<string | null> => {
|
const pullLatest = async (): Promise<string | null> => {
|
||||||
if (!path.value) return null
|
if (!path.value) return null
|
||||||
|
const usedCachedSha = latestSha.value !== null
|
||||||
const remoteSha = latestSha.value ?? (await fetchLatestSha(path.value))
|
const remoteSha = latestSha.value ?? (await fetchLatestSha(path.value))
|
||||||
if (!remoteSha) {
|
if (!remoteSha) {
|
||||||
rawStatus.value = "offline"
|
console.warn("pullLatest: could not resolve remote sha", { path: path.value })
|
||||||
|
status.value = "offline"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const fileContent = await queryFileContent(user, repo, remoteSha)
|
const fileContent = await queryFileContent(user, repo, remoteSha)
|
||||||
if (!fileContent) {
|
if (!fileContent) {
|
||||||
rawStatus.value = "offline"
|
console.warn("pullLatest: failed to fetch blob content", {
|
||||||
|
path: path.value,
|
||||||
|
remoteSha,
|
||||||
|
usedCachedSha
|
||||||
|
})
|
||||||
|
// Cached SHA may be stale — clear so the next click re-resolves it.
|
||||||
|
if (usedCachedSha) latestSha.value = null
|
||||||
|
status.value = "offline"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
||||||
@@ -90,8 +78,7 @@ export const useNoteFreshness = ({
|
|||||||
store.addFile({ path: path.value, sha: remoteSha })
|
store.addFile({ path: path.value, sha: remoteSha })
|
||||||
latestSha.value = remoteSha
|
latestSha.value = remoteSha
|
||||||
lastCheckedAt.value = new Date()
|
lastCheckedAt.value = new Date()
|
||||||
rawStatus.value = "verified"
|
status.value = "verified"
|
||||||
armStaleTimer()
|
|
||||||
const { getRawContent } = markdownBuilder(sha.value)
|
const { getRawContent } = markdownBuilder(sha.value)
|
||||||
return getRawContent(fileContent)
|
return getRawContent(fileContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,22 @@ export const useOverlay = (listen = true) => {
|
|||||||
}, 80)
|
}, 80)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollToElement = (element: HTMLElement, anchorTop?: number) => {
|
||||||
|
const mainApp = document.getElementById("main-app")
|
||||||
|
if (mainApp && anchorTop !== undefined) {
|
||||||
|
mainApp.scrollTop = anchorTop
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
element.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
isMobile,
|
isMobile,
|
||||||
scrollToNote
|
scrollToNote,
|
||||||
|
scrollToElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const useResizeContainer = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
container.style.height = `${(stackedNotes.value.length + 1) * 100}dvh`
|
container.style.height = `${(stackedNotes.value.length + 1) * 100}svh`
|
||||||
} else {
|
} else {
|
||||||
container.style.minWidth = `${
|
container.style.minWidth = `${
|
||||||
getNoteWidth() * (stackedNotes.value.length + 1)
|
getNoteWidth() * (stackedNotes.value.length + 1)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
})
|
})
|
||||||
const { height } = useWindowSize()
|
const { height } = useWindowSize()
|
||||||
|
|
||||||
const { scrollToNote, isMobile } = useOverlay(false)
|
const { scrollToNote, scrollToElement, isMobile } = useOverlay(false)
|
||||||
|
|
||||||
const scrollToHashInNote = (
|
const scrollToHashInNote = (
|
||||||
cleanSha: string,
|
cleanSha: string,
|
||||||
@@ -50,6 +50,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
const scrollToNoteElement = (
|
const scrollToNoteElement = (
|
||||||
cleanNoteId: string,
|
cleanNoteId: string,
|
||||||
index: number,
|
index: number,
|
||||||
|
anchorTop?: number,
|
||||||
attempts = 30
|
attempts = 30
|
||||||
) => {
|
) => {
|
||||||
const element = document.querySelector(
|
const element = document.querySelector(
|
||||||
@@ -57,7 +58,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
) as HTMLElement | null
|
) as HTMLElement | null
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
scrollToNote((index + 1) * element.clientHeight)
|
scrollToElement(element, anchorTop)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
scrollToNoteElement(cleanNoteId, index, attempts - 1)
|
scrollToNoteElement(cleanNoteId, index, anchorTop, attempts - 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,20 +77,22 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
notes?: string[]
|
notes?: string[]
|
||||||
hash?: string
|
hash?: string
|
||||||
smoothHash?: boolean
|
smoothHash?: boolean
|
||||||
|
anchorTop?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToFocusedNote = ({
|
const scrollToFocusedNote = ({
|
||||||
noteId = null,
|
noteId = null,
|
||||||
notes = stackedNotes.value,
|
notes = stackedNotes.value,
|
||||||
hash,
|
hash,
|
||||||
smoothHash = false
|
smoothHash = false,
|
||||||
|
anchorTop
|
||||||
}: ScrollToFocusedNoteOptions = {}) => {
|
}: ScrollToFocusedNoteOptions = {}) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
|
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
|
||||||
|
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
scrollToNoteElement(noteId.replaceAll(":", "-"), index)
|
scrollToNoteElement(noteId.replaceAll(":", "-"), index, anchorTop)
|
||||||
} else {
|
} else {
|
||||||
scrollToNote(0)
|
scrollToNote(0)
|
||||||
}
|
}
|
||||||
@@ -114,11 +117,15 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
selector?: string,
|
selector?: string,
|
||||||
hash?: string
|
hash?: string
|
||||||
) => {
|
) => {
|
||||||
|
const anchorTop =
|
||||||
|
document.getElementById("main-app")?.scrollTop ?? undefined
|
||||||
|
|
||||||
if (stackedNotes.value.includes(sha)) {
|
if (stackedNotes.value.includes(sha)) {
|
||||||
scrollToFocusedNote({
|
scrollToFocusedNote({
|
||||||
noteId: selector ?? sha,
|
noteId: selector ?? sha,
|
||||||
hash,
|
hash,
|
||||||
smoothHash: true
|
smoothHash: true,
|
||||||
|
anchorTop
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -139,7 +146,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
stackedNotes.value = newStackedNotes
|
stackedNotes.value = newStackedNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToFocusedNote({ noteId: selector ?? sha, hash })
|
scrollToFocusedNote({ noteId: selector ?? sha, hash, anchorTop })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -121,20 +121,23 @@ export const queryFileContent = async (
|
|||||||
repo: string,
|
repo: string,
|
||||||
sha: string
|
sha: string
|
||||||
) => {
|
) => {
|
||||||
const octokit = await getOctokit()
|
|
||||||
|
|
||||||
if (!user || !repo) {
|
if (!user || !repo) {
|
||||||
null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await octokit.request(
|
try {
|
||||||
"GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
|
const octokit = await getOctokit()
|
||||||
{
|
const file = await octokit.request(
|
||||||
owner: user,
|
"GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
|
||||||
repo: repo,
|
{
|
||||||
file_sha: sha
|
owner: user,
|
||||||
}
|
repo: repo,
|
||||||
)
|
file_sha: sha
|
||||||
|
}
|
||||||
return file?.data.content ?? null
|
)
|
||||||
|
return file?.data.content ?? null
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("queryFileContent failed", { user, repo, sha, error })
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { toRaw } from "vue"
|
|
||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
|
import { toRaw } from "vue"
|
||||||
|
|
||||||
import { data, generateId } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
@@ -166,8 +166,10 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
|
|
||||||
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
|
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
|
||||||
if (requestId !== this._requestId) return
|
if (requestId !== this._requestId) return
|
||||||
this.readme = cachedReadme
|
if (cachedReadme) this.readme = cachedReadme
|
||||||
this.readme = await getMainReadme(user, repo)
|
const fetched = await getMainReadme(user, repo)
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
|
this.readme = fetched
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addFile(file: RepoFile) {
|
addFile(file: RepoFile) {
|
||||||
|
|||||||
@@ -95,8 +95,9 @@ export const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(() => {
|
router.beforeEach((to, from) => {
|
||||||
if (!("startViewTransition" in document)) return
|
if (!("startViewTransition" in document)) return
|
||||||
|
if (to.path === from.path) return
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
;(
|
;(
|
||||||
document as Document & {
|
document as Document & {
|
||||||
|
|||||||
@@ -13,7 +13,11 @@
|
|||||||
--light-link: lighten(#445fb9, 45%);
|
--light-link: lighten(#445fb9, 45%);
|
||||||
--background-color: #ffffff;
|
--background-color: #ffffff;
|
||||||
--note-width: 500px;
|
--note-width: 500px;
|
||||||
--note-canvas-bg: color-mix(in oklch, var(--color-base-100) 60%, var(--color-base-200));
|
--note-canvas-bg: color-mix(
|
||||||
|
in oklch,
|
||||||
|
var(--color-base-100) 60%,
|
||||||
|
var(--color-base-200)
|
||||||
|
);
|
||||||
--note-sheet-shadow: 1px 0 8px rgb(0 0 0 / 6%);
|
--note-sheet-shadow: 1px 0 8px rgb(0 0 0 / 6%);
|
||||||
--color-contrast-content: var(--color-success);
|
--color-contrast-content: var(--color-success);
|
||||||
--notyf-margin: 0.5rem;
|
--notyf-margin: 0.5rem;
|
||||||
@@ -27,8 +31,8 @@
|
|||||||
|
|
||||||
@plugin 'daisyui' {
|
@plugin 'daisyui' {
|
||||||
themes:
|
themes:
|
||||||
emerald --default,
|
light --default,
|
||||||
forest --prefersdark;
|
dracula --prefersdark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@config '../../tailwind.config.js';
|
@config '../../tailwind.config.js';
|
||||||
@@ -91,18 +95,36 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.title-stacked-note-link {
|
.title-stacked-note {
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
overflow: visible;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.title-stacked-note-link {
|
||||||
|
display: block;
|
||||||
|
overflow: visible;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-stacked-note ul,
|
||||||
|
.title-stacked-note li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.notyf__toast {
|
.notyf__toast {
|
||||||
border-radius: revert-layer;
|
border-radius: revert-layer;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
// Update these values to change the light and dark themes
|
// Update these values to change the light and dark themes
|
||||||
|
|
||||||
export const themeConfig = {
|
export const themeConfig = {
|
||||||
light: "emerald",
|
light: 'light',
|
||||||
dark: "forest"
|
dark: 'dracula'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default defineConfig(({ command }) => {
|
|||||||
"pwa-512x512.png",
|
"pwa-512x512.png",
|
||||||
"masked-icon.png",
|
"masked-icon.png",
|
||||||
"maskable-icon-512x512.png",
|
"maskable-icon-512x512.png",
|
||||||
"monochrome-icon.png",
|
"monochromeicon.png",
|
||||||
"assets/*.svg"
|
"assets/*.svg"
|
||||||
],
|
],
|
||||||
manifest: {
|
manifest: {
|
||||||
@@ -54,7 +54,7 @@ export default defineConfig(({ command }) => {
|
|||||||
purpose: "maskable"
|
purpose: "maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "monochrome-icon.png",
|
src: "monochromeicon.png",
|
||||||
sizes: "1024x1024",
|
sizes: "1024x1024",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "monochrome"
|
purpose: "monochrome"
|
||||||
|
|||||||
Reference in New Issue
Block a user