feat: add mobile folder navigation and template moves
This commit is contained in:
parent
385f5a819a
commit
a11b6e51c8
241
src/App.tsx
241
src/App.tsx
@ -10,7 +10,8 @@ import type {
|
|||||||
TemplateSummary,
|
TemplateSummary,
|
||||||
TemplateType,
|
TemplateType,
|
||||||
ThemeSummary,
|
ThemeSummary,
|
||||||
EmailRenderResult
|
EmailRenderResult,
|
||||||
|
FolderSummary
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
type Screen = 'templates' | 'editor' | 'preview' | 'data' | 'use'
|
type Screen = 'templates' | 'editor' | 'preview' | 'data' | 'use'
|
||||||
@ -84,6 +85,11 @@ function formatDate(value?: string | null) {
|
|||||||
export function App({ user }: Props) {
|
export function App({ user }: Props) {
|
||||||
const [screen, setScreen] = useState<Screen>('templates')
|
const [screen, setScreen] = useState<Screen>('templates')
|
||||||
const [templates, setTemplates] = useState<TemplateSummary[]>([])
|
const [templates, setTemplates] = useState<TemplateSummary[]>([])
|
||||||
|
const [folders, setFolders] = useState<FolderSummary[]>([])
|
||||||
|
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null)
|
||||||
|
const [createFolderOpen, setCreateFolderOpen] = useState(false)
|
||||||
|
const [createFolderName, setCreateFolderName] = useState('')
|
||||||
|
const [moveTemplate, setMoveTemplate] = useState<TemplateSummary | null>(null)
|
||||||
const [active, setActive] = useState<TemplateRecord | null>(null)
|
const [active, setActive] = useState<TemplateRecord | null>(null)
|
||||||
const [templateDoc, setTemplateDoc] = useState<TemplateDocument>({ blocks: [] })
|
const [templateDoc, setTemplateDoc] = useState<TemplateDocument>({ blocks: [] })
|
||||||
const [sampleData, setSampleData] = useState<Record<string, unknown>>({})
|
const [sampleData, setSampleData] = useState<Record<string, unknown>>({})
|
||||||
@ -120,11 +126,48 @@ export function App({ user }: Props) {
|
|||||||
const visibleTemplates = useMemo(() => {
|
const visibleTemplates = useMemo(() => {
|
||||||
const needle = search.trim().toLowerCase()
|
const needle = search.trim().toLowerCase()
|
||||||
return templates.filter(template => {
|
return templates.filter(template => {
|
||||||
|
if ((template.folder_id || null) !== currentFolderId) return false
|
||||||
if (filter !== 'all' && template.type !== filter) return false
|
if (filter !== 'all' && template.type !== filter) return false
|
||||||
if (!needle) return true
|
if (!needle) return true
|
||||||
return `${template.name} ${template.template_key}`.toLowerCase().includes(needle)
|
return `${template.name} ${template.template_key}`.toLowerCase().includes(needle)
|
||||||
})
|
})
|
||||||
}, [templates, filter, search])
|
}, [templates, filter, search, currentFolderId])
|
||||||
|
|
||||||
|
const visibleFolders = useMemo(
|
||||||
|
() => folders
|
||||||
|
.filter(folder => (folder.parent_id || null) === currentFolderId)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
[folders, currentFolderId]
|
||||||
|
)
|
||||||
|
|
||||||
|
const folderOptions = useMemo(() => {
|
||||||
|
const options: Array<FolderSummary & { depth: number }> = []
|
||||||
|
const walk = (parentId: string | null, depth: number) => {
|
||||||
|
folders
|
||||||
|
.filter(folder => (folder.parent_id || null) === parentId)
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.forEach(folder => {
|
||||||
|
options.push({ ...folder, depth })
|
||||||
|
walk(folder.id, depth + 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
walk(null, 0)
|
||||||
|
return options
|
||||||
|
}, [folders])
|
||||||
|
|
||||||
|
const breadcrumbs = useMemo(() => {
|
||||||
|
const result: FolderSummary[] = []
|
||||||
|
let id = currentFolderId
|
||||||
|
const seen = new Set<string>()
|
||||||
|
while (id && !seen.has(id)) {
|
||||||
|
seen.add(id)
|
||||||
|
const folder = folders.find(item => item.id === id)
|
||||||
|
if (!folder) break
|
||||||
|
result.unshift(folder)
|
||||||
|
id = folder.parent_id || null
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [folders, currentFolderId])
|
||||||
|
|
||||||
const counts = useMemo(() => ({
|
const counts = useMemo(() => ({
|
||||||
total: templates.length,
|
total: templates.length,
|
||||||
@ -136,6 +179,14 @@ export function App({ user }: Props) {
|
|||||||
setTemplates(await api<TemplateSummary[]>('/api/templates'))
|
setTemplates(await api<TemplateSummary[]>('/api/templates'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshFolders() {
|
||||||
|
setFolders(await api<FolderSummary[]>('/api/folders'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshLibrary() {
|
||||||
|
await Promise.all([refreshTemplates(), refreshFolders()])
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshApiKeys() {
|
async function refreshApiKeys() {
|
||||||
setApiKeys(await api<ApiKeySummary[]>('/api/api-keys'))
|
setApiKeys(await api<ApiKeySummary[]>('/api/api-keys'))
|
||||||
}
|
}
|
||||||
@ -145,7 +196,7 @@ export function App({ user }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refreshTemplates().catch(err => setError(String(err)))
|
void refreshLibrary().catch(err => setError(String(err)))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function saveNow() {
|
async function saveNow() {
|
||||||
@ -203,14 +254,42 @@ export function App({ user }: Props) {
|
|||||||
if (!createName.trim()) return
|
if (!createName.trim()) return
|
||||||
const created = await api<TemplateRecord>('/api/templates', {
|
const created = await api<TemplateRecord>('/api/templates', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name: createName.trim(), type: createType })
|
body: JSON.stringify({ name: createName.trim(), type: createType, folderId: currentFolderId })
|
||||||
})
|
})
|
||||||
setCreateOpen(false)
|
setCreateOpen(false)
|
||||||
setCreateName('')
|
setCreateName('')
|
||||||
await refreshTemplates()
|
await refreshLibrary()
|
||||||
await openTemplate(created.id)
|
await openTemplate(created.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createFolder() {
|
||||||
|
if (!createFolderName.trim()) return
|
||||||
|
try {
|
||||||
|
await api<FolderSummary>('/api/folders', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name: createFolderName.trim(), parentId: currentFolderId })
|
||||||
|
})
|
||||||
|
setCreateFolderName('')
|
||||||
|
setCreateFolderOpen(false)
|
||||||
|
await refreshFolders()
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveTemplateTo(template: TemplateSummary, folderId: string | null) {
|
||||||
|
try {
|
||||||
|
await api<TemplateSummary>(`/api/templates/${template.id}/folder`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ folderId })
|
||||||
|
})
|
||||||
|
setMoveTemplate(null)
|
||||||
|
await refreshTemplates()
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addBlock(type: BlockType) {
|
function addBlock(type: BlockType) {
|
||||||
const block: Block = { id: crypto.randomUUID(), type, props: structuredClone(defaults[type]) }
|
const block: Block = { id: crypto.randomUUID(), type, props: structuredClone(defaults[type]) }
|
||||||
setTemplateDoc(current => ({ ...current, blocks: [...current.blocks, block] }))
|
setTemplateDoc(current => ({ ...current, blocks: [...current.blocks, block] }))
|
||||||
@ -449,7 +528,13 @@ export function App({ user }: Props) {
|
|||||||
'/api/template-packs/import',
|
'/api/template-packs/import',
|
||||||
file
|
file
|
||||||
)
|
)
|
||||||
await refreshTemplates()
|
if (currentFolderId) {
|
||||||
|
await api<TemplateSummary>(`/api/templates/${imported.template.id}/folder`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ folderId: currentFolderId })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await refreshLibrary()
|
||||||
await openTemplate(imported.template.id)
|
await openTemplate(imported.template.id)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(String(err))
|
setError(String(err))
|
||||||
@ -514,26 +599,77 @@ export function App({ user }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="folder-browser">
|
||||||
|
<div className="folder-toolbar">
|
||||||
|
<nav className="folder-breadcrumbs" aria-label="Template folders">
|
||||||
|
<button className={!currentFolderId ? 'current' : ''} onClick={() => setCurrentFolderId(null)}>Home</button>
|
||||||
|
{breadcrumbs.map(folder => (
|
||||||
|
<span key={folder.id}>
|
||||||
|
<b>›</b>
|
||||||
|
<button
|
||||||
|
className={currentFolderId === folder.id ? 'current' : ''}
|
||||||
|
onClick={() => setCurrentFolderId(folder.id)}
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<button className="new-folder-button" onClick={() => setCreateFolderOpen(true)}>+ Folder</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!visibleFolders.length && (
|
||||||
|
<div className="folder-grid">
|
||||||
|
{visibleFolders.map(folder => {
|
||||||
|
const childFolders = folders.filter(item => (item.parent_id || null) === folder.id).length
|
||||||
|
return (
|
||||||
|
<button className="folder-card" key={folder.id} onClick={() => setCurrentFolderId(folder.id)}>
|
||||||
|
<span className="folder-icon">▰</span>
|
||||||
|
<span>
|
||||||
|
<strong>{folder.name}</strong>
|
||||||
|
<small>
|
||||||
|
{folder.template_count} template{folder.template_count === 1 ? '' : 's'}
|
||||||
|
{childFolders ? ` · ${childFolders} folder${childFolders === 1 ? '' : 's'}` : ''}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<span className="chevron">›</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
{error && <div className="error-banner">{error}</div>}
|
{error && <div className="error-banner">{error}</div>}
|
||||||
|
|
||||||
<section className="template-list">
|
<section className="template-list">
|
||||||
{visibleTemplates.map(template => (
|
{visibleTemplates.map(template => (
|
||||||
<button key={template.id} className="template-card refined" onClick={() => void openTemplate(template.id)}>
|
<article key={template.id} className="template-card refined folder-aware">
|
||||||
<span className={`template-icon ${template.type}`}>{template.type === 'email' ? '✉' : '▤'}</span>
|
<button className="template-card-open" onClick={() => void openTemplate(template.id)}>
|
||||||
<span className="template-card-body">
|
<span className={`template-icon ${template.type}`}>{template.type === 'email' ? '✉' : '▤'}</span>
|
||||||
<span className="template-card-topline">
|
<span className="template-card-body">
|
||||||
<strong>{template.name}</strong>
|
<span className="template-card-topline">
|
||||||
<span className={`status-dot ${template.published_version ? 'published' : 'draft'}`} />
|
<strong>{template.name}</strong>
|
||||||
|
<span className={`status-dot ${template.published_version ? 'published' : 'draft'}`} />
|
||||||
|
</span>
|
||||||
|
<span className="template-key">{template.template_key}</span>
|
||||||
|
<span className="muted">
|
||||||
|
{template.published_version ? `Published v${template.published_version}` : 'Draft only'} · {new Date(template.updated_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="template-key">{template.template_key}</span>
|
<span className="chevron">›</span>
|
||||||
<span className="muted">
|
</button>
|
||||||
{template.published_version ? `Published v${template.published_version}` : 'Draft only'} · {new Date(template.updated_at).toLocaleDateString()}
|
<button
|
||||||
</span>
|
className="template-move-button"
|
||||||
</span>
|
onClick={() => setMoveTemplate(template)}
|
||||||
<span className="chevron">›</span>
|
aria-label={`Move ${template.name}`}
|
||||||
</button>
|
title="Move to folder"
|
||||||
|
>
|
||||||
|
↗
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
))}
|
))}
|
||||||
{!visibleTemplates.length && <div className="empty">No templates match this view.</div>}
|
{!visibleTemplates.length && !visibleFolders.length && <div className="empty">This folder is empty.</div>}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer className="account-row">
|
<footer className="account-row">
|
||||||
@ -549,7 +685,7 @@ export function App({ user }: Props) {
|
|||||||
<span className="sheet-icon">+</span>
|
<span className="sheet-icon">+</span>
|
||||||
<div>
|
<div>
|
||||||
<h2>New template</h2>
|
<h2>New template</h2>
|
||||||
<p>Choose the output, then build it from blocks.</p>
|
<p>Choose the output, then build it from blocks.{currentFolderId ? ` Saving in ${breadcrumbs.at(-1)?.name || 'this folder'}.` : ''}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
@ -564,6 +700,67 @@ export function App({ user }: Props) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{createFolderOpen && (
|
||||||
|
<div className="sheet-backdrop" onClick={() => setCreateFolderOpen(false)}>
|
||||||
|
<section className="sheet compact" onClick={event => event.stopPropagation()}>
|
||||||
|
<div className="grabber" />
|
||||||
|
<div className="sheet-heading">
|
||||||
|
<span className="sheet-icon">▰</span>
|
||||||
|
<div>
|
||||||
|
<h2>New folder</h2>
|
||||||
|
<p>{currentFolderId ? `Inside ${breadcrumbs.at(-1)?.name || 'current folder'}` : 'At the library root'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={createFolderName}
|
||||||
|
onChange={event => setCreateFolderName(event.target.value)}
|
||||||
|
placeholder="Clients"
|
||||||
|
onKeyDown={event => {
|
||||||
|
if (event.key === 'Enter') void createFolder()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button className="button primary full large" onClick={() => void createFolder()}>Create folder</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{moveTemplate && (
|
||||||
|
<div className="sheet-backdrop" onClick={() => setMoveTemplate(null)}>
|
||||||
|
<section className="sheet compact move-sheet" onClick={event => event.stopPropagation()}>
|
||||||
|
<div className="grabber" />
|
||||||
|
<div className="sheet-heading">
|
||||||
|
<span className="sheet-icon">↗</span>
|
||||||
|
<div>
|
||||||
|
<h2>Move template</h2>
|
||||||
|
<p>{moveTemplate.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="folder-picker">
|
||||||
|
<button
|
||||||
|
className={!moveTemplate.folder_id ? 'active' : ''}
|
||||||
|
onClick={() => void moveTemplateTo(moveTemplate, null)}
|
||||||
|
>
|
||||||
|
<span>⌂</span><strong>Home</strong>
|
||||||
|
</button>
|
||||||
|
{folderOptions.map(folder => (
|
||||||
|
<button
|
||||||
|
key={folder.id}
|
||||||
|
className={moveTemplate.folder_id === folder.id ? 'active' : ''}
|
||||||
|
style={{ paddingLeft: `${12 + folder.depth * 16}px` }}
|
||||||
|
onClick={() => void moveTemplateTo(moveTemplate, folder.id)}
|
||||||
|
>
|
||||||
|
<span>▰</span><strong>{folder.name}</strong>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -575,7 +772,7 @@ export function App({ user }: Props) {
|
|||||||
return (
|
return (
|
||||||
<main className="app editor-shell">
|
<main className="app editor-shell">
|
||||||
<header className="topbar editor-topbar">
|
<header className="topbar editor-topbar">
|
||||||
<button className="circle" onClick={() => { setScreen('templates'); void refreshTemplates() }}>←</button>
|
<button className="circle" onClick={() => { setScreen('templates'); void refreshLibrary() }}>←</button>
|
||||||
<div className="title-stack">
|
<div className="title-stack">
|
||||||
<strong>{active?.name}</strong>
|
<strong>{active?.name}</strong>
|
||||||
<span className="template-meta">
|
<span className="template-meta">
|
||||||
|
|||||||
162
src/refine.css
162
src/refine.css
@ -1197,3 +1197,165 @@ body{
|
|||||||
line-height:1.5;
|
line-height:1.5;
|
||||||
}
|
}
|
||||||
.integration-note code{color:#42516a}
|
.integration-note code{color:#42516a}
|
||||||
|
|
||||||
|
|
||||||
|
.folder-browser{
|
||||||
|
padding:2px 14px 8px;
|
||||||
|
}
|
||||||
|
.folder-toolbar{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap:10px;
|
||||||
|
margin-bottom:8px;
|
||||||
|
}
|
||||||
|
.folder-breadcrumbs{
|
||||||
|
min-width:0;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:4px;
|
||||||
|
overflow-x:auto;
|
||||||
|
scrollbar-width:none;
|
||||||
|
}
|
||||||
|
.folder-breadcrumbs::-webkit-scrollbar{display:none}
|
||||||
|
.folder-breadcrumbs>span{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:4px;
|
||||||
|
flex:0 0 auto;
|
||||||
|
}
|
||||||
|
.folder-breadcrumbs b{
|
||||||
|
color:#a3adba;
|
||||||
|
font-size:12px;
|
||||||
|
}
|
||||||
|
.folder-breadcrumbs button{
|
||||||
|
flex:0 0 auto;
|
||||||
|
border:0;
|
||||||
|
padding:5px 4px;
|
||||||
|
background:transparent;
|
||||||
|
color:#7b8797;
|
||||||
|
font-size:10px;
|
||||||
|
font-weight:800;
|
||||||
|
}
|
||||||
|
.folder-breadcrumbs button.current{
|
||||||
|
color:#172033;
|
||||||
|
}
|
||||||
|
.new-folder-button{
|
||||||
|
flex:0 0 auto;
|
||||||
|
border:1px solid #dce3ea;
|
||||||
|
border-radius:9px;
|
||||||
|
padding:7px 9px;
|
||||||
|
background:white;
|
||||||
|
color:#42516a;
|
||||||
|
font-size:9px;
|
||||||
|
font-weight:800;
|
||||||
|
}
|
||||||
|
.folder-grid{
|
||||||
|
display:grid;
|
||||||
|
gap:7px;
|
||||||
|
margin-bottom:8px;
|
||||||
|
}
|
||||||
|
.folder-card{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:38px minmax(0,1fr) 18px;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
width:100%;
|
||||||
|
padding:10px 12px;
|
||||||
|
border:1px solid #dfe5ec;
|
||||||
|
border-radius:13px;
|
||||||
|
background:rgba(255,255,255,.86);
|
||||||
|
color:#263449;
|
||||||
|
text-align:left;
|
||||||
|
box-shadow:0 2px 8px rgba(15,23,42,.02);
|
||||||
|
}
|
||||||
|
.folder-card>span:nth-child(2){
|
||||||
|
min-width:0;
|
||||||
|
display:grid;
|
||||||
|
gap:2px;
|
||||||
|
}
|
||||||
|
.folder-card strong{
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
white-space:nowrap;
|
||||||
|
font-size:12px;
|
||||||
|
}
|
||||||
|
.folder-card small{
|
||||||
|
color:#8a95a4;
|
||||||
|
font-size:9px;
|
||||||
|
}
|
||||||
|
.folder-icon{
|
||||||
|
width:38px;
|
||||||
|
height:34px;
|
||||||
|
display:grid;
|
||||||
|
place-items:center;
|
||||||
|
border-radius:9px;
|
||||||
|
background:#fff4db;
|
||||||
|
color:#9a6b13;
|
||||||
|
font-size:17px;
|
||||||
|
}
|
||||||
|
.template-card.refined.folder-aware{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:minmax(0,1fr) 34px;
|
||||||
|
gap:4px;
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
.template-card-open{
|
||||||
|
min-width:0;
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:42px minmax(0,1fr) 18px;
|
||||||
|
align-items:center;
|
||||||
|
gap:12px;
|
||||||
|
padding:8px;
|
||||||
|
border:0;
|
||||||
|
background:transparent;
|
||||||
|
color:inherit;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
.template-move-button{
|
||||||
|
align-self:center;
|
||||||
|
width:32px;
|
||||||
|
height:42px;
|
||||||
|
border:0;
|
||||||
|
border-left:1px solid #edf1f5;
|
||||||
|
background:transparent;
|
||||||
|
color:#8c98a8;
|
||||||
|
font-size:16px;
|
||||||
|
font-weight:800;
|
||||||
|
}
|
||||||
|
.folder-picker{
|
||||||
|
display:grid;
|
||||||
|
max-height:52vh;
|
||||||
|
overflow:auto;
|
||||||
|
border:1px solid #e1e6ed;
|
||||||
|
border-radius:12px;
|
||||||
|
}
|
||||||
|
.folder-picker button{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns:24px minmax(0,1fr);
|
||||||
|
align-items:center;
|
||||||
|
gap:7px;
|
||||||
|
width:100%;
|
||||||
|
padding:10px 12px;
|
||||||
|
border:0;
|
||||||
|
border-bottom:1px solid #edf1f5;
|
||||||
|
background:#fff;
|
||||||
|
color:#42516a;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
.folder-picker button:last-child{border-bottom:0}
|
||||||
|
.folder-picker button.active{
|
||||||
|
background:#eef4ff;
|
||||||
|
color:#244f8d;
|
||||||
|
}
|
||||||
|
.folder-picker strong{
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
white-space:nowrap;
|
||||||
|
font-size:11px;
|
||||||
|
}
|
||||||
|
@media(min-width:760px){
|
||||||
|
.folder-grid{
|
||||||
|
grid-template-columns:repeat(3,minmax(0,1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
src/types.ts
13
src/types.ts
@ -42,12 +42,13 @@ export type TemplateRecord = {
|
|||||||
draft_document: TemplateDocument
|
draft_document: TemplateDocument
|
||||||
sample_data: Record<string, unknown>
|
sample_data: Record<string, unknown>
|
||||||
published_version?: number | null
|
published_version?: number | null
|
||||||
|
folder_id?: string | null
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TemplateSummary = Pick<
|
export type TemplateSummary = Pick<
|
||||||
TemplateRecord,
|
TemplateRecord,
|
||||||
'id' | 'template_key' | 'name' | 'type' | 'status' | 'published_version' | 'updated_at'
|
'id' | 'template_key' | 'name' | 'type' | 'status' | 'published_version' | 'folder_id' | 'updated_at'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type ApiKeySummary = {
|
export type ApiKeySummary = {
|
||||||
@ -67,3 +68,13 @@ export type ThemeSummary = {
|
|||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type FolderSummary = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
parent_id?: string | null
|
||||||
|
template_count: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user