@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,77 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
// Full-screen draft preview: live iframe of the public route with
|
|
4
|
+
// desktop / tablet / mobile widths and a copyable share link that works
|
|
5
|
+
// logged-out (signed token).
|
|
6
|
+
|
|
7
|
+
const DEVICES = [
|
|
8
|
+
{ key: 'desktop', label: 'Desktop', width: '100%' },
|
|
9
|
+
{ key: 'tablet', label: 'Tablet', width: '768px' },
|
|
10
|
+
{ key: 'mobile', label: 'Mobile', width: '390px' },
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
open: boolean;
|
|
15
|
+
url: string; // admin-session preview (relative)
|
|
16
|
+
shareUrl: string; // token preview (absolute-ish), safe to send around
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function PreviewOverlay({ open, url, shareUrl, onClose }: Props) {
|
|
21
|
+
const [device, setDevice] = useState<(typeof DEVICES)[number]>(DEVICES[0]);
|
|
22
|
+
const [copied, setCopied] = useState(false);
|
|
23
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!open) return;
|
|
27
|
+
setRefreshKey((k) => k + 1); // fresh content each time it opens
|
|
28
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
|
29
|
+
window.addEventListener('keydown', onKey);
|
|
30
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
31
|
+
}, [open, onClose]);
|
|
32
|
+
|
|
33
|
+
if (!open) return null;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="pe-preview">
|
|
37
|
+
<div className="pe-preview-bar">
|
|
38
|
+
<button type="button" className="pe-btn ghost sm" onClick={onClose}>‹ Back to editor</button>
|
|
39
|
+
<div className="pe-devices">
|
|
40
|
+
{DEVICES.map((d) => (
|
|
41
|
+
<button
|
|
42
|
+
key={d.key}
|
|
43
|
+
type="button"
|
|
44
|
+
className={device.key === d.key ? 'is-active' : ''}
|
|
45
|
+
onClick={() => setDevice(d)}
|
|
46
|
+
>
|
|
47
|
+
{d.label}
|
|
48
|
+
</button>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
<div className="pe-preview-actions">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="pe-btn ghost sm"
|
|
55
|
+
onClick={async () => {
|
|
56
|
+
await navigator.clipboard.writeText(new URL(shareUrl, location.origin).href);
|
|
57
|
+
setCopied(true);
|
|
58
|
+
setTimeout(() => setCopied(false), 1600);
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{copied ? 'Copied ✓' : 'Copy share link'}
|
|
62
|
+
</button>
|
|
63
|
+
<a className="pe-btn ghost sm" href={url} target="_blank" rel="noopener">Open in tab ↗</a>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="pe-preview-stage">
|
|
67
|
+
<iframe
|
|
68
|
+
key={refreshKey}
|
|
69
|
+
src={url}
|
|
70
|
+
title="Preview"
|
|
71
|
+
style={{ width: device.width }}
|
|
72
|
+
className={device.key !== 'desktop' ? 'is-framed' : ''}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Node } from '@tiptap/core';
|
|
3
|
+
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
|
4
|
+
import type { NodeViewProps } from '@tiptap/react';
|
|
5
|
+
import { actions } from 'astro:actions';
|
|
6
|
+
import { calendarSrcLabel } from '../../../lib/calendar/src';
|
|
7
|
+
|
|
8
|
+
// Calendar card: a *dynamic* block like postListCard. It serializes into
|
|
9
|
+
// bodyHtml as a placeholder div (data-dyn="calendar") that public routes
|
|
10
|
+
// expand at request time via src/lib/calendar/render.ts, so events are always
|
|
11
|
+
// fresh. The editor validates pasted sources through the calendar.preview
|
|
12
|
+
// action (which also normalizes embed URLs / IDs into ICS feed URLs — the
|
|
13
|
+
// stored attr is always the canonical feed URL) and shows the next few real
|
|
14
|
+
// events as a preview. One card renders exactly one view: month, week, or
|
|
15
|
+
// upcoming.
|
|
16
|
+
|
|
17
|
+
// dot colors for multi-calendar cards; index 0 is the site accent. The public
|
|
18
|
+
// side mirrors these as --cal-dot-* custom properties in site.css.
|
|
19
|
+
export const CAL_DOT_COLORS = ['var(--accent)', '#2f9e6e', '#b3541e', '#8250df', '#c2255c', '#0c8599'];
|
|
20
|
+
|
|
21
|
+
export const CalendarCard = Node.create({
|
|
22
|
+
name: 'calendarCard',
|
|
23
|
+
group: 'block',
|
|
24
|
+
atom: true,
|
|
25
|
+
draggable: true,
|
|
26
|
+
|
|
27
|
+
addAttributes() {
|
|
28
|
+
return {
|
|
29
|
+
sources: { default: [] as string[] }, // canonical ICS feed URLs
|
|
30
|
+
view: { default: 'month' }, // month | week | upcoming
|
|
31
|
+
title: { default: '' },
|
|
32
|
+
subtitle: { default: '' },
|
|
33
|
+
limit: { default: 6 }, // upcoming view only
|
|
34
|
+
showOpenLink: { default: true },
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
parseHTML() {
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
tag: 'div[data-dyn="calendar"]',
|
|
42
|
+
getAttrs: (el) => {
|
|
43
|
+
const e = el as HTMLElement;
|
|
44
|
+
let sources: string[] = [];
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(e.getAttribute('data-sources') ?? '[]');
|
|
47
|
+
if (Array.isArray(parsed)) sources = parsed.filter((s): s is string => typeof s === 'string');
|
|
48
|
+
} catch {
|
|
49
|
+
/* malformed attr — start empty */
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
sources,
|
|
53
|
+
view: e.getAttribute('data-view') ?? 'month',
|
|
54
|
+
title: e.getAttribute('data-title') ?? '',
|
|
55
|
+
subtitle: e.getAttribute('data-subtitle') ?? '',
|
|
56
|
+
limit: Number(e.getAttribute('data-limit') ?? 6),
|
|
57
|
+
showOpenLink: e.getAttribute('data-open-link') !== '0',
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
renderHTML({ node }) {
|
|
65
|
+
const a = node.attrs;
|
|
66
|
+
return [
|
|
67
|
+
'div',
|
|
68
|
+
{
|
|
69
|
+
'data-dyn': 'calendar',
|
|
70
|
+
'data-sources': JSON.stringify(a.sources ?? []),
|
|
71
|
+
'data-view': a.view,
|
|
72
|
+
'data-title': a.title,
|
|
73
|
+
'data-subtitle': a.subtitle,
|
|
74
|
+
'data-limit': String(a.limit),
|
|
75
|
+
'data-open-link': a.showOpenLink ? '1' : '0',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
addNodeView() {
|
|
81
|
+
return ReactNodeViewRenderer(CalendarView);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
interface PreviewItem { when: string; title: string; cal: number }
|
|
86
|
+
|
|
87
|
+
function CalendarView({ node, updateAttributes, deleteNode, selected }: NodeViewProps) {
|
|
88
|
+
const a = node.attrs;
|
|
89
|
+
const sources: string[] = a.sources ?? [];
|
|
90
|
+
const [draft, setDraft] = useState('');
|
|
91
|
+
const [busy, setBusy] = useState(false);
|
|
92
|
+
const [error, setError] = useState('');
|
|
93
|
+
const [adding, setAdding] = useState(false);
|
|
94
|
+
const [preview, setPreview] = useState<PreviewItem[] | null>(null);
|
|
95
|
+
|
|
96
|
+
const addSource = async (raw: string) => {
|
|
97
|
+
const value = raw.trim();
|
|
98
|
+
if (!value) return;
|
|
99
|
+
setBusy(true);
|
|
100
|
+
setError('');
|
|
101
|
+
const r = await actions.calendar.preview({ sources: [...sources, value] });
|
|
102
|
+
setBusy(false);
|
|
103
|
+
if (r.error) return setError(r.error.message);
|
|
104
|
+
updateAttributes({ sources: r.data.sources });
|
|
105
|
+
setPreview(r.data.upcoming);
|
|
106
|
+
setDraft('');
|
|
107
|
+
setAdding(false);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// refresh the schematic preview when sources change (e.g. after undo)
|
|
111
|
+
const key = JSON.stringify(sources);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!sources.length) return setPreview(null);
|
|
114
|
+
let alive = true;
|
|
115
|
+
actions.calendar.preview({ sources }).then((r) => {
|
|
116
|
+
if (!alive) return;
|
|
117
|
+
if (r.error) setError(r.error.message);
|
|
118
|
+
else setPreview(r.data.upcoming);
|
|
119
|
+
});
|
|
120
|
+
return () => { alive = false; };
|
|
121
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
|
+
}, [key]);
|
|
123
|
+
|
|
124
|
+
if (!sources.length) {
|
|
125
|
+
return (
|
|
126
|
+
<NodeViewWrapper className={`pe-card pe-calcard is-empty ${selected ? 'is-selected' : ''}`}>
|
|
127
|
+
<form
|
|
128
|
+
className="pe-embed-form"
|
|
129
|
+
contentEditable={false}
|
|
130
|
+
onSubmit={(e) => { e.preventDefault(); void addSource(draft); }}
|
|
131
|
+
>
|
|
132
|
+
<span className="pe-embed-label">Calendar</span>
|
|
133
|
+
<input
|
|
134
|
+
autoFocus
|
|
135
|
+
type="text"
|
|
136
|
+
placeholder="Paste a Google Calendar link, calendar ID, or .ics feed URL…"
|
|
137
|
+
value={draft}
|
|
138
|
+
onChange={(e) => { setDraft(e.target.value); setError(''); }}
|
|
139
|
+
onKeyDown={(e) => { if (e.key === 'Escape') deleteNode(); }}
|
|
140
|
+
/>
|
|
141
|
+
<button type="submit" className="pe-btn primary sm" disabled={busy}>{busy ? 'Checking…' : 'Add calendar'}</button>
|
|
142
|
+
{error && <span className="pe-embed-error">{error}</span>}
|
|
143
|
+
</form>
|
|
144
|
+
</NodeViewWrapper>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<NodeViewWrapper className={`pe-card pe-calcard ${selected ? 'is-selected' : ''}`} data-drag-handle>
|
|
150
|
+
<div className="pe-card-tools" contentEditable={false}>
|
|
151
|
+
<button type="button" title="Remove" onClick={() => deleteNode()}>✕</button>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="pe-calendar-config" contentEditable={false}>
|
|
154
|
+
<span className="pe-postlist-badge">Calendar · dynamic</span>
|
|
155
|
+
<select value={a.view} onChange={(e) => updateAttributes({ view: e.target.value })}>
|
|
156
|
+
<option value="month">Month</option>
|
|
157
|
+
<option value="week">Week</option>
|
|
158
|
+
<option value="upcoming">Upcoming</option>
|
|
159
|
+
</select>
|
|
160
|
+
{a.view === 'upcoming' && (
|
|
161
|
+
<select value={a.limit} onChange={(e) => updateAttributes({ limit: Number(e.target.value) })}>
|
|
162
|
+
{[3, 4, 5, 6, 8, 10, 15, 20].map((n) => <option key={n} value={n}>{n} events</option>)}
|
|
163
|
+
</select>
|
|
164
|
+
)}
|
|
165
|
+
<label>
|
|
166
|
+
<input type="checkbox" checked={a.showOpenLink} onChange={(e) => updateAttributes({ showOpenLink: e.target.checked })} />
|
|
167
|
+
Google link
|
|
168
|
+
</label>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="pe-calendar-config" contentEditable={false}>
|
|
171
|
+
<input
|
|
172
|
+
type="text" className="pe-cal-title"
|
|
173
|
+
placeholder="Title (optional)"
|
|
174
|
+
value={a.title}
|
|
175
|
+
onChange={(e) => updateAttributes({ title: e.target.value })}
|
|
176
|
+
/>
|
|
177
|
+
<input
|
|
178
|
+
type="text" className="pe-cal-title"
|
|
179
|
+
placeholder="Subtitle (optional)"
|
|
180
|
+
value={a.subtitle}
|
|
181
|
+
onChange={(e) => updateAttributes({ subtitle: e.target.value })}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="pe-cal-sources" contentEditable={false}>
|
|
185
|
+
{sources.map((s, i) => (
|
|
186
|
+
<span className="pe-cal-chip" key={s}>
|
|
187
|
+
<i style={{ background: CAL_DOT_COLORS[i % CAL_DOT_COLORS.length] }} />
|
|
188
|
+
{calendarSrcLabel(s)}
|
|
189
|
+
<button
|
|
190
|
+
type="button" title="Remove calendar"
|
|
191
|
+
onClick={() => updateAttributes({ sources: sources.filter((x) => x !== s) })}
|
|
192
|
+
>✕</button>
|
|
193
|
+
</span>
|
|
194
|
+
))}
|
|
195
|
+
{adding ? (
|
|
196
|
+
<form className="pe-cal-add" onSubmit={(e) => { e.preventDefault(); void addSource(draft); }}>
|
|
197
|
+
<input
|
|
198
|
+
autoFocus type="text" placeholder="Another calendar…"
|
|
199
|
+
value={draft}
|
|
200
|
+
onChange={(e) => { setDraft(e.target.value); setError(''); }}
|
|
201
|
+
onKeyDown={(e) => { if (e.key === 'Escape') { setAdding(false); setDraft(''); } }}
|
|
202
|
+
/>
|
|
203
|
+
<button type="submit" className="pe-btn sm" disabled={busy}>{busy ? '…' : 'Add'}</button>
|
|
204
|
+
</form>
|
|
205
|
+
) : (
|
|
206
|
+
sources.length < 6 && <button type="button" className="pe-btn sm" onClick={() => setAdding(true)}>+ Add calendar</button>
|
|
207
|
+
)}
|
|
208
|
+
{error && <span className="pe-embed-error">{error}</span>}
|
|
209
|
+
</div>
|
|
210
|
+
<div className="pe-cal-preview" contentEditable={false}>
|
|
211
|
+
{preview === null ? (
|
|
212
|
+
<p className="pe-postlist-note">Loading events…</p>
|
|
213
|
+
) : preview.length ? (
|
|
214
|
+
<>
|
|
215
|
+
{preview.map((ev, i) => (
|
|
216
|
+
<div className="pe-cal-previewrow" key={i}>
|
|
217
|
+
<i style={{ background: CAL_DOT_COLORS[ev.cal % CAL_DOT_COLORS.length] }} />
|
|
218
|
+
<span className="w">{ev.when}</span>
|
|
219
|
+
<span className="ti">{ev.title}</span>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
<p className="pe-postlist-note">{a.view === 'month' ? 'Month' : a.view === 'week' ? 'Week' : 'Upcoming'} view renders live on the site (see preview)</p>
|
|
223
|
+
</>
|
|
224
|
+
) : (
|
|
225
|
+
<p className="pe-postlist-note">No upcoming events in this calendar — the widget still renders live on the site.</p>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</NodeViewWrapper>
|
|
229
|
+
);
|
|
230
|
+
}
|