From 7da99c16a2bb194707e296a3b80398b03980d7d6 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 23 Sep 2026 04:03:19 +0000 Subject: [PATCH] feat: add mobile AI pack ZIP upload --- server/templateEngine.test.js | 19 +++++++++++++ src/App.tsx | 46 ++++++++++++++++++++++++++++++- src/refine.css | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/server/templateEngine.test.js b/server/templateEngine.test.js index ff6974a..7ea0e27 100644 --- a/server/templateEngine.test.js +++ b/server/templateEngine.test.js @@ -17,3 +17,22 @@ test('email renderer produces HTML', () => { assert.match(html, /Hello/) assert.match(html, /doctype/i) }) + +test('report renderer applies constrained pack theme styling', () => { + const html = renderReportHtml({ + theme: { + backgroundColor: '#f6f1e8', + accentColor: '#8b5e34', + pageBackgroundImage: 'https://templates.arrowstack.dev/media/token/bg.webp' + }, + blocks: [{ + type: 'heading', + props: { text: 'Styled' }, + style: { backgroundColor: '#fffaf2', padding: 18, borderRadius: 12 } + }] + }, {}) + assert.match(html, /#f6f1e8/) + assert.match(html, /#8b5e34/) + assert.match(html, /bg\.webp/) + assert.match(html, /padding:18px/) +}) diff --git a/src/App.tsx b/src/App.tsx index c205ca3..990c15a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { get, set } from 'idb-keyval' -import { api, apiBlob } from './api' +import { api, apiBlob, apiZip } from './api' import type { ApiKeySummary, Block, @@ -98,6 +98,8 @@ export function App({ user }: Props) { const [newKeyName, setNewKeyName] = useState('') const [createdKey, setCreatedKey] = useState(null) const [copied, setCopied] = useState('') + const [packImporting, setPackImporting] = useState(false) + const packFileInput = useRef(null) const loadedId = useRef(null) const timer = useRef(undefined) @@ -317,6 +319,30 @@ export function App({ user }: Props) { window.setTimeout(() => setCopied(''), 1400) } + async function importTemplatePack(file?: File) { + if (!file) return + if (!file.name.toLowerCase().endsWith('.zip')) { + setError('Template packs must be ZIP files.') + return + } + + setPackImporting(true) + setError('') + try { + const imported = await apiZip<{ template: TemplateRecord; asset_count: number }>( + '/api/template-packs/import', + file + ) + await refreshTemplates() + await openTemplate(imported.template.id) + } catch (err) { + setError(String(err)) + } finally { + setPackImporting(false) + if (packFileInput.current) packFileInput.current.value = '' + } + } + if (screen === 'templates') { return (
@@ -353,6 +379,23 @@ export function App({ user }: Props) { ))} +
+ + Pack format + void importTemplatePack(event.target.files?.[0])} + /> +
{error &&
{error}
} @@ -439,6 +482,7 @@ export function App({ user }: Props) {
{active?.type === 'email' ? '✉ Email' : '▤ Report'} {active?.template_key} + {templateDoc.theme && Pack theme}
{templateDoc.blocks.length} blocks diff --git a/src/refine.css b/src/refine.css index e02cb92..5df913c 100644 --- a/src/refine.css +++ b/src/refine.css @@ -657,3 +657,55 @@ body{ grid-template-columns:repeat(3,minmax(0,1fr)); } } + +.pack-actions{ + display:flex; + align-items:center; + gap:9px; + padding-top:9px; +} +.pack-import{ + border:1px solid #111827; + border-radius:999px; + background:#111827; + color:white; + padding:8px 12px; + font-size:11px; + font-weight:800; +} +.pack-import:disabled{ + opacity:.55; + cursor:progress; +} +.pack-actions a{ + color:#64748b; + font-size:10px; + font-weight:800; + text-decoration:none; +} +.visually-hidden{ + position:absolute!important; + width:1px!important; + height:1px!important; + padding:0!important; + margin:-1px!important; + overflow:hidden!important; + clip:rect(0,0,0,0)!important; + white-space:nowrap!important; + border:0!important; +} +.theme-pill{ + flex:0 0 auto; + border-radius:999px; + background:#f4ecff; + color:#7048a8; + padding:4px 7px; + font-size:9px; + font-weight:800; +} +@media(min-width:760px){ + .pack-actions{ + padding-top:0; + margin-left:auto; + } +}