@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.
Files changed (107) hide show
  1. package/cli.mjs +75 -0
  2. package/integration.mjs +286 -0
  3. package/package.json +89 -0
  4. package/src/actions/index.ts +502 -0
  5. package/src/components/DesignManager.tsx +542 -0
  6. package/src/components/LinksManager.tsx +694 -0
  7. package/src/components/Modal.astro +65 -0
  8. package/src/components/NavManager.tsx +168 -0
  9. package/src/components/WidgetRenderer.astro +84 -0
  10. package/src/components/builder/PageBuilder.tsx +575 -0
  11. package/src/components/editor/ImagePicker.tsx +215 -0
  12. package/src/components/editor/PostEditor.tsx +1011 -0
  13. package/src/components/editor/PreviewOverlay.tsx +77 -0
  14. package/src/components/editor/extensions/calendar.tsx +230 -0
  15. package/src/components/editor/extensions/cards.tsx +488 -0
  16. package/src/components/editor/extensions/codeBlock.tsx +70 -0
  17. package/src/components/editor/extensions/infoCards.tsx +270 -0
  18. package/src/components/editor/extensions/mediaCards.tsx +376 -0
  19. package/src/components/editor/extensions/postList.tsx +130 -0
  20. package/src/components/editor/extensions/slash.tsx +250 -0
  21. package/src/components/editor/extensions/tableTyping.ts +63 -0
  22. package/src/components/editor/imaging.ts +146 -0
  23. package/src/components/media/CropModal.tsx +229 -0
  24. package/src/components/media/MediaLibrary.tsx +295 -0
  25. package/src/layouts/Admin.astro +119 -0
  26. package/src/layouts/Site.astro +218 -0
  27. package/src/lib/auth.ts +94 -0
  28. package/src/lib/backup/unzip.ts +38 -0
  29. package/src/lib/backup/zip.ts +105 -0
  30. package/src/lib/blocksToHtml.ts +68 -0
  31. package/src/lib/brand.ts +26 -0
  32. package/src/lib/calendar/ics.ts +402 -0
  33. package/src/lib/calendar/render.ts +275 -0
  34. package/src/lib/calendar/src.ts +68 -0
  35. package/src/lib/content.ts +53 -0
  36. package/src/lib/dates.ts +34 -0
  37. package/src/lib/dynamicBlocks.ts +115 -0
  38. package/src/lib/env.ts +6 -0
  39. package/src/lib/highlightCode.ts +44 -0
  40. package/src/lib/i18n.ts +40 -0
  41. package/src/lib/markdown.ts +7 -0
  42. package/src/lib/media/store.ts +95 -0
  43. package/src/lib/net.ts +51 -0
  44. package/src/lib/preview.ts +28 -0
  45. package/src/lib/repo/d1.ts +44 -0
  46. package/src/lib/repo/index.ts +23 -0
  47. package/src/lib/repo/migrations.ts +231 -0
  48. package/src/lib/repo/search.ts +14 -0
  49. package/src/lib/repo/sql.ts +721 -0
  50. package/src/lib/repo/sqlite.ts +43 -0
  51. package/src/lib/repo/types.ts +132 -0
  52. package/src/lib/schema.ts +286 -0
  53. package/src/lib/seo.ts +37 -0
  54. package/src/lib/theme.ts +206 -0
  55. package/src/lib/widgets.ts +108 -0
  56. package/src/middleware.ts +109 -0
  57. package/src/pages/[...slug].astro +43 -0
  58. package/src/pages/admin/c/[collection]/[id].astro +56 -0
  59. package/src/pages/admin/c/[collection]/index.astro +163 -0
  60. package/src/pages/admin/design.astro +42 -0
  61. package/src/pages/admin/index.astro +145 -0
  62. package/src/pages/admin/links.astro +21 -0
  63. package/src/pages/admin/login.astro +83 -0
  64. package/src/pages/admin/logout.ts +7 -0
  65. package/src/pages/admin/media.astro +13 -0
  66. package/src/pages/admin/navigation.astro +17 -0
  67. package/src/pages/admin/pages/[id].astro +49 -0
  68. package/src/pages/admin/pages/index.astro +100 -0
  69. package/src/pages/admin/posts/[id].astro +6 -0
  70. package/src/pages/admin/posts/index.astro +7 -0
  71. package/src/pages/admin/settings.astro +512 -0
  72. package/src/pages/admin/tags.astro +101 -0
  73. package/src/pages/admin/users.astro +149 -0
  74. package/src/pages/api/backup.ts +63 -0
  75. package/src/pages/api/drafts.ts +106 -0
  76. package/src/pages/api/media/upload.ts +89 -0
  77. package/src/pages/api/restore.ts +102 -0
  78. package/src/pages/author/[slug].astro +66 -0
  79. package/src/pages/blog/[slug].astro +240 -0
  80. package/src/pages/blog/index.astro +51 -0
  81. package/src/pages/blog/tag/[slug].astro +83 -0
  82. package/src/pages/embed/[pageId]/[blockId].ts +45 -0
  83. package/src/pages/go/[id].ts +10 -0
  84. package/src/pages/index.astro +52 -0
  85. package/src/pages/links.astro +110 -0
  86. package/src/pages/llms.txt.ts +47 -0
  87. package/src/pages/media/[...key].ts +18 -0
  88. package/src/pages/preview/[type]/[id].astro +52 -0
  89. package/src/pages/robots.txt.ts +28 -0
  90. package/src/pages/rss.xml.ts +58 -0
  91. package/src/pages/search.astro +83 -0
  92. package/src/pages/sitemap.xml.ts +55 -0
  93. package/src/styles/admin.css +652 -0
  94. package/src/styles/builder.css +121 -0
  95. package/src/styles/cards.css +176 -0
  96. package/src/styles/editor.css +596 -0
  97. package/src/styles/site.css +427 -0
  98. package/src/themes/editorial/templates/home.astro +127 -0
  99. package/src/themes/editorial/templates/post-minimal.astro +50 -0
  100. package/src/themes/editorial/theme.css +108 -0
  101. package/src/themes/editorial/theme.json +20 -0
  102. package/src/themes/field/theme.json +17 -0
  103. package/src/themes/ink/theme.json +17 -0
  104. package/src/themes/night/theme.css +7 -0
  105. package/src/themes/night/theme.json +17 -0
  106. package/src/virtual.d.ts +25 -0
  107. 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
+ }