@acms-subir/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +75 -0
- package/integration.mjs +286 -0
- package/package.json +89 -0
- package/src/actions/index.ts +502 -0
- package/src/components/DesignManager.tsx +542 -0
- package/src/components/LinksManager.tsx +694 -0
- package/src/components/Modal.astro +65 -0
- package/src/components/NavManager.tsx +168 -0
- package/src/components/WidgetRenderer.astro +84 -0
- package/src/components/builder/PageBuilder.tsx +575 -0
- package/src/components/editor/ImagePicker.tsx +215 -0
- package/src/components/editor/PostEditor.tsx +1011 -0
- package/src/components/editor/PreviewOverlay.tsx +77 -0
- package/src/components/editor/extensions/calendar.tsx +230 -0
- package/src/components/editor/extensions/cards.tsx +488 -0
- package/src/components/editor/extensions/codeBlock.tsx +70 -0
- package/src/components/editor/extensions/infoCards.tsx +270 -0
- package/src/components/editor/extensions/mediaCards.tsx +376 -0
- package/src/components/editor/extensions/postList.tsx +130 -0
- package/src/components/editor/extensions/slash.tsx +250 -0
- package/src/components/editor/extensions/tableTyping.ts +63 -0
- package/src/components/editor/imaging.ts +146 -0
- package/src/components/media/CropModal.tsx +229 -0
- package/src/components/media/MediaLibrary.tsx +295 -0
- package/src/layouts/Admin.astro +119 -0
- package/src/layouts/Site.astro +218 -0
- package/src/lib/auth.ts +94 -0
- package/src/lib/backup/unzip.ts +38 -0
- package/src/lib/backup/zip.ts +105 -0
- package/src/lib/blocksToHtml.ts +68 -0
- package/src/lib/brand.ts +26 -0
- package/src/lib/calendar/ics.ts +402 -0
- package/src/lib/calendar/render.ts +275 -0
- package/src/lib/calendar/src.ts +68 -0
- package/src/lib/content.ts +53 -0
- package/src/lib/dates.ts +34 -0
- package/src/lib/dynamicBlocks.ts +115 -0
- package/src/lib/env.ts +6 -0
- package/src/lib/highlightCode.ts +44 -0
- package/src/lib/i18n.ts +40 -0
- package/src/lib/markdown.ts +7 -0
- package/src/lib/media/store.ts +95 -0
- package/src/lib/net.ts +51 -0
- package/src/lib/preview.ts +28 -0
- package/src/lib/repo/d1.ts +44 -0
- package/src/lib/repo/index.ts +23 -0
- package/src/lib/repo/migrations.ts +231 -0
- package/src/lib/repo/search.ts +14 -0
- package/src/lib/repo/sql.ts +721 -0
- package/src/lib/repo/sqlite.ts +43 -0
- package/src/lib/repo/types.ts +132 -0
- package/src/lib/schema.ts +286 -0
- package/src/lib/seo.ts +37 -0
- package/src/lib/theme.ts +206 -0
- package/src/lib/widgets.ts +108 -0
- package/src/middleware.ts +109 -0
- package/src/pages/[...slug].astro +43 -0
- package/src/pages/admin/c/[collection]/[id].astro +56 -0
- package/src/pages/admin/c/[collection]/index.astro +163 -0
- package/src/pages/admin/design.astro +42 -0
- package/src/pages/admin/index.astro +145 -0
- package/src/pages/admin/links.astro +21 -0
- package/src/pages/admin/login.astro +83 -0
- package/src/pages/admin/logout.ts +7 -0
- package/src/pages/admin/media.astro +13 -0
- package/src/pages/admin/navigation.astro +17 -0
- package/src/pages/admin/pages/[id].astro +49 -0
- package/src/pages/admin/pages/index.astro +100 -0
- package/src/pages/admin/posts/[id].astro +6 -0
- package/src/pages/admin/posts/index.astro +7 -0
- package/src/pages/admin/settings.astro +512 -0
- package/src/pages/admin/tags.astro +101 -0
- package/src/pages/admin/users.astro +149 -0
- package/src/pages/api/backup.ts +63 -0
- package/src/pages/api/drafts.ts +106 -0
- package/src/pages/api/media/upload.ts +89 -0
- package/src/pages/api/restore.ts +102 -0
- package/src/pages/author/[slug].astro +66 -0
- package/src/pages/blog/[slug].astro +240 -0
- package/src/pages/blog/index.astro +51 -0
- package/src/pages/blog/tag/[slug].astro +83 -0
- package/src/pages/embed/[pageId]/[blockId].ts +45 -0
- package/src/pages/go/[id].ts +10 -0
- package/src/pages/index.astro +52 -0
- package/src/pages/links.astro +110 -0
- package/src/pages/llms.txt.ts +47 -0
- package/src/pages/media/[...key].ts +18 -0
- package/src/pages/preview/[type]/[id].astro +52 -0
- package/src/pages/robots.txt.ts +28 -0
- package/src/pages/rss.xml.ts +58 -0
- package/src/pages/search.astro +83 -0
- package/src/pages/sitemap.xml.ts +55 -0
- package/src/styles/admin.css +652 -0
- package/src/styles/builder.css +121 -0
- package/src/styles/cards.css +176 -0
- package/src/styles/editor.css +596 -0
- package/src/styles/site.css +427 -0
- package/src/themes/editorial/templates/home.astro +127 -0
- package/src/themes/editorial/templates/post-minimal.astro +50 -0
- package/src/themes/editorial/theme.css +108 -0
- package/src/themes/editorial/theme.json +20 -0
- package/src/themes/field/theme.json +17 -0
- package/src/themes/ink/theme.json +17 -0
- package/src/themes/night/theme.css +7 -0
- package/src/themes/night/theme.json +17 -0
- package/src/virtual.d.ts +25 -0
- package/src/worker.ts +28 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Kit modal on the native <dialog>. Server-rendered `open` (e.g. ?new=1) shows
|
|
3
|
+
// the form without JS; the script upgrades it to a real modal with backdrop.
|
|
4
|
+
interface Props {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
}
|
|
9
|
+
const { id, title, open = false } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<dialog class="modal" id={id} open={open}>
|
|
13
|
+
<div class="modal-head">
|
|
14
|
+
<span>{title}</span>
|
|
15
|
+
<button class="modal-x" type="button" data-close aria-label="Close">✕</button>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="modal-body"><slot /></div>
|
|
18
|
+
</dialog>
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
const slugifyLite = (s: string) =>
|
|
22
|
+
s
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.normalize('NFKD')
|
|
25
|
+
.replace(/[̀-ͯ]/g, '')
|
|
26
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '')
|
|
28
|
+
.slice(0, 80);
|
|
29
|
+
|
|
30
|
+
for (const d of document.querySelectorAll<HTMLDialogElement>('dialog.modal[open]')) {
|
|
31
|
+
d.close();
|
|
32
|
+
d.showModal();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
document.addEventListener('click', (e) => {
|
|
36
|
+
const t = e.target as HTMLElement;
|
|
37
|
+
const opener = t.closest<HTMLElement>('[data-modal]');
|
|
38
|
+
if (opener) {
|
|
39
|
+
const dlg = document.getElementById(opener.dataset.modal!);
|
|
40
|
+
if (dlg instanceof HTMLDialogElement) {
|
|
41
|
+
dlg.showModal();
|
|
42
|
+
dlg.querySelector<HTMLInputElement>('input:not([type=hidden])')?.focus();
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (t.closest('[data-close]')) {
|
|
47
|
+
t.closest('dialog')?.close();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// click on the backdrop (the dialog element itself, outside its content)
|
|
51
|
+
if (t instanceof HTMLDialogElement && t.classList.contains('modal')) t.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
document.addEventListener('input', (e) => {
|
|
55
|
+
const el = e.target as HTMLInputElement;
|
|
56
|
+
if (el.dataset.slugDest !== undefined) {
|
|
57
|
+
el.dataset.touched = '1';
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (el.dataset.slugFor) {
|
|
61
|
+
const dest = document.getElementById(el.dataset.slugFor);
|
|
62
|
+
if (dest instanceof HTMLInputElement && !dest.dataset.touched) dest.value = slugifyLite(el.value);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { DndContext, PointerSensor, KeyboardSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';
|
|
3
|
+
import type { DragEndEvent } from '@dnd-kit/core';
|
|
4
|
+
import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
5
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
6
|
+
import { actions } from 'astro:actions';
|
|
7
|
+
import type { NavItem, NavArea } from '../lib/schema';
|
|
8
|
+
|
|
9
|
+
// Navigation manager: three drag-sortable areas. Header replaces the old
|
|
10
|
+
// auto-list of published pages (which stays as a fallback while the header
|
|
11
|
+
// area is empty); footer renders above the footer text; social feeds the
|
|
12
|
+
// footer social row and share UIs.
|
|
13
|
+
|
|
14
|
+
type SaveState = 'saved' | 'saving' | 'error';
|
|
15
|
+
|
|
16
|
+
const AREAS: { key: NavArea; title: string; hint: string }[] = [
|
|
17
|
+
{ key: 'header', title: 'Header', hint: 'Top navigation. While empty, the site falls back to auto-listing published pages.' },
|
|
18
|
+
{ key: 'footer', title: 'Footer', hint: 'Link row above the footer text.' },
|
|
19
|
+
{ key: 'social', title: 'Social', hint: 'Footer social row — also used for share links on posts.' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function Row({
|
|
23
|
+
item, onEdit, onArea, onDelete,
|
|
24
|
+
}: {
|
|
25
|
+
item: NavItem;
|
|
26
|
+
onEdit: (patch: Partial<Pick<NavItem, 'label' | 'url'>>) => void;
|
|
27
|
+
onArea: (area: NavArea) => void;
|
|
28
|
+
onDelete: () => void;
|
|
29
|
+
}) {
|
|
30
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id });
|
|
31
|
+
const external = /^https?:\/\//.test(item.url);
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
ref={setNodeRef}
|
|
35
|
+
className="row nm-row"
|
|
36
|
+
style={{ transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.6 : 1 }}
|
|
37
|
+
>
|
|
38
|
+
<button className="lm-handle" type="button" aria-label="Reorder" {...attributes} {...listeners}>⋮⋮</button>
|
|
39
|
+
<input
|
|
40
|
+
className="bare nm-label"
|
|
41
|
+
value={item.label}
|
|
42
|
+
placeholder="Label"
|
|
43
|
+
onChange={(e) => onEdit({ label: e.target.value })}
|
|
44
|
+
/>
|
|
45
|
+
<input
|
|
46
|
+
className="bare nm-url"
|
|
47
|
+
value={item.url}
|
|
48
|
+
placeholder="/path or https://…"
|
|
49
|
+
onChange={(e) => onEdit({ url: e.target.value })}
|
|
50
|
+
/>
|
|
51
|
+
{external && <span className="nm-ext" title="External link">↗</span>}
|
|
52
|
+
<select
|
|
53
|
+
className="lm-select nm-area reveal"
|
|
54
|
+
value={item.area}
|
|
55
|
+
onChange={(e) => onArea(e.target.value as NavArea)}
|
|
56
|
+
>
|
|
57
|
+
{AREAS.map((a) => <option key={a.key} value={a.key}>{a.title}</option>)}
|
|
58
|
+
</select>
|
|
59
|
+
<button className="btn sm ghost reveal" type="button" onClick={onDelete}>Delete</button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default function NavManager({ items: initial }: { items: NavItem[] }) {
|
|
65
|
+
const [items, setItems] = useState<NavItem[]>(initial);
|
|
66
|
+
const [saveState, setSaveState] = useState<SaveState>('saved');
|
|
67
|
+
const pending = useRef(0);
|
|
68
|
+
const timers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
|
69
|
+
|
|
70
|
+
const sensors = useSensors(
|
|
71
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
72
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
async function run(fn: () => Promise<{ error?: unknown }>) {
|
|
76
|
+
pending.current += 1;
|
|
77
|
+
setSaveState('saving');
|
|
78
|
+
const r = await fn();
|
|
79
|
+
pending.current -= 1;
|
|
80
|
+
if (r.error) setSaveState('error');
|
|
81
|
+
else if (pending.current === 0) setSaveState('saved');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function debounced(key: string, fn: () => void, ms = 600) {
|
|
85
|
+
const prev = timers.current.get(key);
|
|
86
|
+
if (prev) clearTimeout(prev);
|
|
87
|
+
timers.current.set(key, setTimeout(() => { timers.current.delete(key); fn(); }, ms));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function edit(id: string, patch: Partial<Pick<NavItem, 'label' | 'url'>>) {
|
|
91
|
+
setItems((ls) => ls.map((n) => (n.id === id ? { ...n, ...patch } : n)));
|
|
92
|
+
debounced(id, () => void run(() => actions.nav.update({ id, ...patch })));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function setArea(id: string, area: NavArea) {
|
|
96
|
+
setItems((ls) => ls.map((n) => (n.id === id ? { ...n, area } : n)));
|
|
97
|
+
void run(() => actions.nav.update({ id, area }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function add(area: NavArea) {
|
|
101
|
+
await run(async () => {
|
|
102
|
+
const r = await actions.nav.create({ label: 'New link', url: '/', area });
|
|
103
|
+
if (!r.error) setItems((ls) => [...ls, r.data.item]);
|
|
104
|
+
return r;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function remove(id: string) {
|
|
109
|
+
if (!window.confirm('Delete this navigation item?')) return;
|
|
110
|
+
setItems((ls) => ls.filter((n) => n.id !== id));
|
|
111
|
+
void run(() => actions.nav.delete({ id }));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onDragEnd(area: NavArea, e: DragEndEvent) {
|
|
115
|
+
const { active, over } = e;
|
|
116
|
+
if (!over || active.id === over.id) return;
|
|
117
|
+
setItems((ls) => {
|
|
118
|
+
const group = ls.filter((n) => n.area === area);
|
|
119
|
+
const rest = ls.filter((n) => n.area !== area);
|
|
120
|
+
const from = group.findIndex((n) => n.id === active.id);
|
|
121
|
+
const to = group.findIndex((n) => n.id === over.id);
|
|
122
|
+
const moved = arrayMove(group, from, to);
|
|
123
|
+
const next = [...rest, ...moved];
|
|
124
|
+
// persist the full display order: header, footer, social, each in order
|
|
125
|
+
const ordered = AREAS.flatMap((a) => next.filter((n) => n.area === a.key)).map((n) => n.id);
|
|
126
|
+
void run(() => actions.nav.reorder({ ids: ordered }));
|
|
127
|
+
return next;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="nm">
|
|
133
|
+
<div className="nm-head">
|
|
134
|
+
<span className={`lm-savestate ${saveState}`}>
|
|
135
|
+
{saveState === 'saving' ? 'Saving…' : saveState === 'error' ? 'Save failed' : 'Saved'}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
{AREAS.map((a) => {
|
|
139
|
+
const group = items.filter((n) => n.area === a.key);
|
|
140
|
+
return (
|
|
141
|
+
<div className="card rowlist nm-group" key={a.key}>
|
|
142
|
+
<div className="nm-group-head">
|
|
143
|
+
<div>
|
|
144
|
+
<p className="card-title">{a.title}</p>
|
|
145
|
+
<p className="hint">{a.hint}</p>
|
|
146
|
+
</div>
|
|
147
|
+
<button className="btn sm" type="button" onClick={() => void add(a.key)}>+ Add item</button>
|
|
148
|
+
</div>
|
|
149
|
+
{group.length === 0 && <p className="nm-empty">No items yet.</p>}
|
|
150
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={(e) => onDragEnd(a.key, e)}>
|
|
151
|
+
<SortableContext items={group.map((n) => n.id)} strategy={verticalListSortingStrategy}>
|
|
152
|
+
{group.map((n) => (
|
|
153
|
+
<Row
|
|
154
|
+
key={n.id}
|
|
155
|
+
item={n}
|
|
156
|
+
onEdit={(patch) => edit(n.id, patch)}
|
|
157
|
+
onArea={(area) => setArea(n.id, area)}
|
|
158
|
+
onDelete={() => remove(n.id)}
|
|
159
|
+
/>
|
|
160
|
+
))}
|
|
161
|
+
</SortableContext>
|
|
162
|
+
</DndContext>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { parseConfig, type WidgetInstance, type WidgetType } from '../lib/schema';
|
|
3
|
+
import { renderMarkdown } from '../lib/markdown';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
pageId: string;
|
|
7
|
+
blocks: WidgetInstance[];
|
|
8
|
+
}
|
|
9
|
+
const { pageId, blocks } = Astro.props;
|
|
10
|
+
|
|
11
|
+
// Parse each block's config through its zod schema so bad/missing fields
|
|
12
|
+
// degrade to defaults instead of crashing the public page.
|
|
13
|
+
const items = blocks.flatMap((b) => {
|
|
14
|
+
try {
|
|
15
|
+
return [{ block: b, config: parseConfig(b.type as WidgetType, b.config) as Record<string, any> }];
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{items.map(({ block, config }) => {
|
|
23
|
+
switch (block.type) {
|
|
24
|
+
case 'hero':
|
|
25
|
+
return (
|
|
26
|
+
<section class={`w-hero ${config.align}`}>
|
|
27
|
+
<h1>{config.heading}</h1>
|
|
28
|
+
{config.sub && <p class="sub">{config.sub}</p>}
|
|
29
|
+
{config.ctaLabel && config.ctaHref && (
|
|
30
|
+
<a class="w-btn primary" href={config.ctaHref}>{config.ctaLabel}</a>
|
|
31
|
+
)}
|
|
32
|
+
</section>
|
|
33
|
+
);
|
|
34
|
+
case 'text':
|
|
35
|
+
return <section class="w-text" set:html={renderMarkdown(config.markdown)} />;
|
|
36
|
+
case 'image':
|
|
37
|
+
return config.src ? (
|
|
38
|
+
<figure class="w-image">
|
|
39
|
+
<img src={config.src} alt={config.alt} loading="lazy" />
|
|
40
|
+
{config.caption && <figcaption>{config.caption}</figcaption>}
|
|
41
|
+
</figure>
|
|
42
|
+
) : null;
|
|
43
|
+
case 'button':
|
|
44
|
+
return config.label ? (
|
|
45
|
+
<p><a class={`w-btn ${config.variant}`} href={config.href}>{config.label}</a></p>
|
|
46
|
+
) : null;
|
|
47
|
+
case 'spacer':
|
|
48
|
+
return <div class={`w-spacer ${config.size}`} />;
|
|
49
|
+
case 'embed':
|
|
50
|
+
return config.url ? (
|
|
51
|
+
<div class="w-embed">
|
|
52
|
+
<iframe src={config.url} height={config.height} loading="lazy" allowfullscreen />
|
|
53
|
+
</div>
|
|
54
|
+
) : null;
|
|
55
|
+
case 'customHtml':
|
|
56
|
+
return (
|
|
57
|
+
<div class="w-custom">
|
|
58
|
+
<iframe
|
|
59
|
+
src={`/embed/${pageId}/${block.id}`}
|
|
60
|
+
height={config.height}
|
|
61
|
+
sandbox="allow-scripts"
|
|
62
|
+
loading="lazy"
|
|
63
|
+
data-embed-id={block.id}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
default:
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
})}
|
|
71
|
+
|
|
72
|
+
<script>
|
|
73
|
+
// Auto-height for sandboxed custom widgets: the embed document posts its
|
|
74
|
+
// height; match it to the iframe with the same block id.
|
|
75
|
+
window.addEventListener('message', (e) => {
|
|
76
|
+
const d = e.data;
|
|
77
|
+
if (!d || typeof d !== 'object' || !d.acmsEmbedId || typeof d.height !== 'number') return;
|
|
78
|
+
document.querySelectorAll<HTMLIFrameElement>('iframe[data-embed-id]').forEach((f) => {
|
|
79
|
+
if (f.dataset.embedId === d.acmsEmbedId && f.contentWindow === e.source) {
|
|
80
|
+
f.height = String(Math.min(Math.max(d.height, 40), 4000));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
</script>
|