feat(drafts): add local draft backup with restore prompt to EssayForm, SimplePostForm, and PhotoPostForm
This commit is contained in:
parent
c98ba3dcf0
commit
280bdfc06d
4 changed files with 338 additions and 19 deletions
49
src/lib/admin/draftStore.ts
Normal file
49
src/lib/admin/draftStore.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
export type Draft<T = unknown> = { payload: T; ts: number }
|
||||||
|
|
||||||
|
export function makeDraftKey(type: string, id: string | number) {
|
||||||
|
return `admin:draft:${type}:${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveDraft<T>(key: string, payload: T) {
|
||||||
|
try {
|
||||||
|
const entry: Draft<T> = { payload, ts: Date.now() }
|
||||||
|
localStorage.setItem(key, JSON.stringify(entry))
|
||||||
|
} catch {
|
||||||
|
// Ignore quota or serialization errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadDraft<T = unknown>(key: string): Draft<T> | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
if (!raw) return null
|
||||||
|
return JSON.parse(raw) as Draft<T>
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearDraft(key: string) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeAgo(ts: number): string {
|
||||||
|
const diff = Date.now() - ts
|
||||||
|
const sec = Math.floor(diff / 1000)
|
||||||
|
if (sec < 5) return 'just now'
|
||||||
|
if (sec < 60) return `${sec} second${sec !== 1 ? 's' : ''} ago`
|
||||||
|
const min = Math.floor(sec / 60)
|
||||||
|
if (min < 60) return `${min} minute${min !== 1 ? 's' : ''} ago`
|
||||||
|
const hr = Math.floor(min / 60)
|
||||||
|
if (hr < 24) return `${hr} hour${hr !== 1 ? 's' : ''} ago`
|
||||||
|
const day = Math.floor(hr / 24)
|
||||||
|
if (day <= 29) {
|
||||||
|
if (day < 7) return `${day} day${day !== 1 ? 's' : ''} ago`
|
||||||
|
const wk = Math.floor(day / 7)
|
||||||
|
return `${wk} week${wk !== 1 ? 's' : ''} ago`
|
||||||
|
}
|
||||||
|
// Beyond 29 days, show a normal localized date
|
||||||
|
return new Date(ts).toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
import Editor from './Editor.svelte'
|
import Editor from './Editor.svelte'
|
||||||
import Button from './Button.svelte'
|
import Button from './Button.svelte'
|
||||||
import Input from './Input.svelte'
|
import Input from './Input.svelte'
|
||||||
import { toast } from '$lib/stores/toast'
|
import { toast } from '$lib/stores/toast'
|
||||||
|
import { makeDraftKey, saveDraft, loadDraft, clearDraft, timeAgo } from '$lib/admin/draftStore'
|
||||||
import type { JSONContent } from '@tiptap/core'
|
import type { JSONContent } from '@tiptap/core'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -37,7 +38,25 @@
|
||||||
let tagInput = $state('')
|
let tagInput = $state('')
|
||||||
|
|
||||||
// Ref to the editor component
|
// Ref to the editor component
|
||||||
let editorRef: any
|
let editorRef: any
|
||||||
|
|
||||||
|
// Draft backup
|
||||||
|
const draftKey = $derived(makeDraftKey('post', postId ?? 'new'))
|
||||||
|
let showDraftPrompt = $state(false)
|
||||||
|
let draftTimestamp = $state<number | null>(null)
|
||||||
|
let timeTicker = $state(0)
|
||||||
|
const draftTimeText = $derived(() => (draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null))
|
||||||
|
|
||||||
|
function buildPayload() {
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
slug,
|
||||||
|
type: 'essay',
|
||||||
|
status,
|
||||||
|
content,
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tabOptions = [
|
const tabOptions = [
|
||||||
{ value: 'metadata', label: 'Metadata' },
|
{ value: 'metadata', label: 'Metadata' },
|
||||||
|
|
@ -45,14 +64,53 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
// Auto-generate slug from title
|
// Auto-generate slug from title
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (title && !slug) {
|
if (title && !slug) {
|
||||||
slug = title
|
slug = title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
.replace(/^-+|-+$/g, '')
|
.replace(/^-+|-+$/g, '')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Save draft when key fields change
|
||||||
|
$effect(() => {
|
||||||
|
title; slug; status; content; tags
|
||||||
|
saveDraft(draftKey, buildPayload())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show restore prompt if a draft exists
|
||||||
|
$effect(() => {
|
||||||
|
const draft = loadDraft<any>(draftKey)
|
||||||
|
if (draft) {
|
||||||
|
showDraftPrompt = true
|
||||||
|
draftTimestamp = draft.ts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function restoreDraft() {
|
||||||
|
const draft = loadDraft<any>(draftKey)
|
||||||
|
if (!draft) return
|
||||||
|
const p = draft.payload
|
||||||
|
title = p.title ?? title
|
||||||
|
slug = p.slug ?? slug
|
||||||
|
status = p.status ?? status
|
||||||
|
content = p.content ?? content
|
||||||
|
tags = p.tags ?? tags
|
||||||
|
showDraftPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissDraft() {
|
||||||
|
showDraftPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-update draft time text every minute when prompt visible
|
||||||
|
$effect(() => {
|
||||||
|
if (showDraftPrompt) {
|
||||||
|
const id = setInterval(() => (timeTicker = timeTicker + 1), 60000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function addTag() {
|
function addTag() {
|
||||||
if (tagInput && !tags.includes(tagInput)) {
|
if (tagInput && !tags.includes(tagInput)) {
|
||||||
|
|
@ -122,7 +180,8 @@
|
||||||
const savedPost = await response.json()
|
const savedPost = await response.json()
|
||||||
|
|
||||||
toast.dismiss(loadingToastId)
|
toast.dismiss(loadingToastId)
|
||||||
toast.success(`Essay ${mode === 'edit' ? 'saved' : 'created'} successfully!`)
|
toast.success(`Essay ${mode === 'edit' ? 'saved' : 'created'} successfully!`)
|
||||||
|
clearDraft(draftKey)
|
||||||
|
|
||||||
if (mode === 'create') {
|
if (mode === 'create') {
|
||||||
goto(`/admin/posts/${savedPost.id}/edit`)
|
goto(`/admin/posts/${savedPost.id}/edit`)
|
||||||
|
|
@ -237,6 +296,13 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if showDraftPrompt}
|
||||||
|
<div class="draft-prompt">
|
||||||
|
Unsaved draft found{#if draftTimeText} (saved {draftTimeText}){/if}.
|
||||||
|
<button class="link" onclick={restoreDraft}>Restore</button>
|
||||||
|
<button class="link" onclick={dismissDraft}>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -366,6 +432,21 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.draft-prompt {
|
||||||
|
margin-left: $unit-2x;
|
||||||
|
color: $gray-40;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: $gray-20;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: $unit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Custom styles for save/publish buttons to maintain grey color scheme
|
// Custom styles for save/publish buttons to maintain grey color scheme
|
||||||
:global(.save-button.btn-primary) {
|
:global(.save-button.btn-primary) {
|
||||||
background-color: $gray-10;
|
background-color: $gray-10;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
import Input from './Input.svelte'
|
import Input from './Input.svelte'
|
||||||
import ImageUploader from './ImageUploader.svelte'
|
import ImageUploader from './ImageUploader.svelte'
|
||||||
import Editor from './Editor.svelte'
|
import Editor from './Editor.svelte'
|
||||||
import { toast } from '$lib/stores/toast'
|
import { toast } from '$lib/stores/toast'
|
||||||
|
import { makeDraftKey, saveDraft, loadDraft, clearDraft, timeAgo } from '$lib/admin/draftStore'
|
||||||
import type { JSONContent } from '@tiptap/core'
|
import type { JSONContent } from '@tiptap/core'
|
||||||
import type { Media } from '@prisma/client'
|
import type { Media } from '@prisma/client'
|
||||||
|
|
||||||
|
|
@ -34,7 +35,85 @@
|
||||||
let tags = $state(initialData?.tags?.join(', ') || '')
|
let tags = $state(initialData?.tags?.join(', ') || '')
|
||||||
|
|
||||||
// Editor ref
|
// Editor ref
|
||||||
let editorRef: any
|
let editorRef: any
|
||||||
|
|
||||||
|
// Draft backup
|
||||||
|
const draftKey = $derived(makeDraftKey('post', postId ?? 'new'))
|
||||||
|
let showDraftPrompt = $state(false)
|
||||||
|
let draftTimestamp = $state<number | null>(null)
|
||||||
|
let timeTicker = $state(0)
|
||||||
|
const draftTimeText = $derived(() => (draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null))
|
||||||
|
|
||||||
|
function buildPayload() {
|
||||||
|
return {
|
||||||
|
title: title.trim(),
|
||||||
|
slug: createSlug(title),
|
||||||
|
type: 'photo',
|
||||||
|
status,
|
||||||
|
content,
|
||||||
|
featuredImage: featuredImage ? featuredImage.url : null,
|
||||||
|
tags: tags
|
||||||
|
? tags
|
||||||
|
.split(',')
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
title; status; content; featuredImage; tags
|
||||||
|
saveDraft(draftKey, buildPayload())
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const draft = loadDraft<any>(draftKey)
|
||||||
|
if (draft) {
|
||||||
|
showDraftPrompt = true
|
||||||
|
draftTimestamp = draft.ts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function restoreDraft() {
|
||||||
|
const draft = loadDraft<any>(draftKey)
|
||||||
|
if (!draft) return
|
||||||
|
const p = draft.payload
|
||||||
|
title = p.title ?? title
|
||||||
|
status = p.status ?? status
|
||||||
|
content = p.content ?? content
|
||||||
|
tags = Array.isArray(p.tags) ? (p.tags as string[]).join(', ') : tags
|
||||||
|
if (p.featuredImage) {
|
||||||
|
featuredImage = {
|
||||||
|
id: -1,
|
||||||
|
filename: 'photo.jpg',
|
||||||
|
originalName: 'photo.jpg',
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
size: 0,
|
||||||
|
url: p.featuredImage,
|
||||||
|
thumbnailUrl: p.featuredImage,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
|
altText: null,
|
||||||
|
description: null,
|
||||||
|
usedIn: [],
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
showDraftPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissDraft() {
|
||||||
|
showDraftPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-update draft time text every minute when prompt visible
|
||||||
|
$effect(() => {
|
||||||
|
if (showDraftPrompt) {
|
||||||
|
const id = setInterval(() => (timeTicker = timeTicker + 1), 60000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Initialize featured image if editing
|
// Initialize featured image if editing
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -146,10 +225,11 @@
|
||||||
throw new Error(`Failed to ${mode === 'edit' ? 'update' : 'create'} photo post`)
|
throw new Error(`Failed to ${mode === 'edit' ? 'update' : 'create'} photo post`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedPost = await response.json()
|
const savedPost = await response.json()
|
||||||
|
|
||||||
toast.dismiss(loadingToastId)
|
toast.dismiss(loadingToastId)
|
||||||
toast.success(`Photo post ${status === 'published' ? 'published' : 'saved'} successfully!`)
|
toast.success(`Photo post ${status === 'published' ? 'published' : 'saved'} successfully!`)
|
||||||
|
clearDraft(draftKey)
|
||||||
|
|
||||||
// Redirect to posts list or edit page
|
// Redirect to posts list or edit page
|
||||||
if (mode === 'create') {
|
if (mode === 'create') {
|
||||||
|
|
@ -186,6 +266,13 @@
|
||||||
|
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
{#if !isSaving}
|
{#if !isSaving}
|
||||||
|
{#if showDraftPrompt}
|
||||||
|
<div class="draft-prompt">
|
||||||
|
Unsaved draft found{#if draftTimeText} (saved {draftTimeText}){/if}.
|
||||||
|
<button class="link" onclick={restoreDraft}>Restore</button>
|
||||||
|
<button class="link" onclick={dismissDraft}>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Button variant="ghost" onclick={() => goto('/admin/posts')}>Cancel</Button>
|
<Button variant="ghost" onclick={() => goto('/admin/posts')}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -289,6 +376,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.draft-prompt {
|
||||||
|
color: $gray-40;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: $gray-20;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: $unit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-container {
|
.form-container {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
import Editor from './Editor.svelte'
|
import Editor from './Editor.svelte'
|
||||||
import Button from './Button.svelte'
|
import Button from './Button.svelte'
|
||||||
import Input from './Input.svelte'
|
import Input from './Input.svelte'
|
||||||
import { toast } from '$lib/stores/toast'
|
import { toast } from '$lib/stores/toast'
|
||||||
|
import { makeDraftKey, saveDraft, loadDraft, clearDraft, timeAgo } from '$lib/admin/draftStore'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postType: 'post'
|
postType: 'post'
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
mode: 'create' | 'edit'
|
mode: 'create' | 'edit'
|
||||||
}
|
}
|
||||||
|
|
||||||
let { postType, postId, initialData, mode }: Props = $props()
|
let { postType, postId, initialData, mode }: Props = $props()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let isSaving = $state(false)
|
let isSaving = $state(false)
|
||||||
|
|
@ -32,7 +33,7 @@
|
||||||
let linkDescription = $state(initialData?.linkDescription || '')
|
let linkDescription = $state(initialData?.linkDescription || '')
|
||||||
let title = $state(initialData?.title || '')
|
let title = $state(initialData?.title || '')
|
||||||
|
|
||||||
// Character count for posts
|
// Character count for posts
|
||||||
const maxLength = 280
|
const maxLength = 280
|
||||||
const textContent = $derived(() => {
|
const textContent = $derived(() => {
|
||||||
if (!content.content) return ''
|
if (!content.content) return ''
|
||||||
|
|
@ -44,12 +45,77 @@
|
||||||
const isOverLimit = $derived(charCount > maxLength)
|
const isOverLimit = $derived(charCount > maxLength)
|
||||||
|
|
||||||
// Check if form has content
|
// Check if form has content
|
||||||
const hasContent = $derived(() => {
|
const hasContent = $derived(() => {
|
||||||
// For posts, check if either content exists or it's a link with URL
|
// For posts, check if either content exists or it's a link with URL
|
||||||
const hasTextContent = textContent().trim().length > 0
|
const hasTextContent = textContent().trim().length > 0
|
||||||
const hasLinkContent = linkUrl && linkUrl.trim().length > 0
|
const hasLinkContent = linkUrl && linkUrl.trim().length > 0
|
||||||
return hasTextContent || hasLinkContent
|
return hasTextContent || hasLinkContent
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Draft backup
|
||||||
|
const draftKey = $derived(makeDraftKey('post', postId ?? 'new'))
|
||||||
|
let showDraftPrompt = $state(false)
|
||||||
|
let draftTimestamp = $state<number | null>(null)
|
||||||
|
let timeTicker = $state(0)
|
||||||
|
const draftTimeText = $derived(() => (draftTimestamp ? (timeTicker, timeAgo(draftTimestamp)) : null))
|
||||||
|
|
||||||
|
function buildPayload() {
|
||||||
|
const payload: any = {
|
||||||
|
type: 'post',
|
||||||
|
status,
|
||||||
|
content
|
||||||
|
}
|
||||||
|
if (linkUrl && linkUrl.trim()) {
|
||||||
|
payload.title = title || linkUrl
|
||||||
|
payload.link_url = linkUrl
|
||||||
|
payload.linkDescription = linkDescription
|
||||||
|
} else if (title) {
|
||||||
|
payload.title = title
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// Save draft on changes
|
||||||
|
status; content; linkUrl; linkDescription; title
|
||||||
|
saveDraft(draftKey, buildPayload())
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const draft = loadDraft<any>(draftKey)
|
||||||
|
if (draft) {
|
||||||
|
showDraftPrompt = true
|
||||||
|
draftTimestamp = draft.ts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function restoreDraft() {
|
||||||
|
const draft = loadDraft<any>(draftKey)
|
||||||
|
if (!draft) return
|
||||||
|
const p = draft.payload
|
||||||
|
status = p.status ?? status
|
||||||
|
content = p.content ?? content
|
||||||
|
if (p.link_url) {
|
||||||
|
linkUrl = p.link_url
|
||||||
|
linkDescription = p.linkDescription ?? linkDescription
|
||||||
|
title = p.title ?? title
|
||||||
|
} else {
|
||||||
|
title = p.title ?? title
|
||||||
|
}
|
||||||
|
showDraftPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissDraft() {
|
||||||
|
showDraftPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-update draft time text every minute when prompt visible
|
||||||
|
$effect(() => {
|
||||||
|
if (showDraftPrompt) {
|
||||||
|
const id = setInterval(() => (timeTicker = timeTicker + 1), 60000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function handleSave(publishStatus: 'draft' | 'published') {
|
async function handleSave(publishStatus: 'draft' | 'published') {
|
||||||
if (isOverLimit) {
|
if (isOverLimit) {
|
||||||
|
|
@ -105,10 +171,11 @@
|
||||||
throw new Error(`Failed to ${mode === 'edit' ? 'save' : 'create'} post`)
|
throw new Error(`Failed to ${mode === 'edit' ? 'save' : 'create'} post`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedPost = await response.json()
|
const savedPost = await response.json()
|
||||||
|
|
||||||
toast.dismiss(loadingToastId)
|
toast.dismiss(loadingToastId)
|
||||||
toast.success(`Post ${publishStatus === 'published' ? 'published' : 'saved'} successfully!`)
|
toast.success(`Post ${publishStatus === 'published' ? 'published' : 'saved'} successfully!`)
|
||||||
|
clearDraft(draftKey)
|
||||||
|
|
||||||
// Redirect back to posts list after creation
|
// Redirect back to posts list after creation
|
||||||
goto('/admin/posts')
|
goto('/admin/posts')
|
||||||
|
|
@ -145,6 +212,13 @@
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
{#if showDraftPrompt}
|
||||||
|
<div class="draft-prompt">
|
||||||
|
Unsaved draft found{#if draftTimeText} (saved {draftTimeText}){/if}.
|
||||||
|
<button class="link" onclick={restoreDraft}>Restore</button>
|
||||||
|
<button class="link" onclick={dismissDraft}>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<Button variant="secondary" onclick={() => handleSave('draft')} disabled={isSaving}>
|
<Button variant="secondary" onclick={() => handleSave('draft')} disabled={isSaving}>
|
||||||
Save Draft
|
Save Draft
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -355,4 +429,18 @@
|
||||||
color: $gray-60;
|
color: $gray-60;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.draft-prompt {
|
||||||
|
margin-right: $unit-2x;
|
||||||
|
color: $gray-40;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: $gray-20;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: $unit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue