@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,215 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { actions } from 'astro:actions';
3
+ import type { Media } from '../../lib/schema';
4
+ import { uploadImage, mediaSrc, mediaSrcset, mediaThumb, unsplashSrcset, DEFAULT_SIZES } from './imaging';
5
+
6
+ // Shared image chooser: used by the editor's image card, the feature-image
7
+ // field, and (later) the widget builder.
8
+
9
+ export interface PickedImage {
10
+ src: string;
11
+ srcset: string;
12
+ sizes: string;
13
+ alt: string;
14
+ width: number;
15
+ height: number;
16
+ caption: string;
17
+ }
18
+
19
+ interface UnsplashPhoto {
20
+ id: string;
21
+ alt: string;
22
+ width: number;
23
+ height: number;
24
+ thumb: string;
25
+ regular: string;
26
+ downloadLocation: string;
27
+ authorName: string;
28
+ authorUrl: string;
29
+ }
30
+
31
+ interface Props {
32
+ open: boolean;
33
+ unsplashEnabled: boolean;
34
+ onClose: () => void;
35
+ onSelect: (img: PickedImage) => void;
36
+ }
37
+
38
+ type Tab = 'upload' | 'library' | 'unsplash';
39
+
40
+ export function ImagePicker({ open, unsplashEnabled, onClose, onSelect }: Props) {
41
+ const [tab, setTab] = useState<Tab>('upload');
42
+ const [busy, setBusy] = useState(false);
43
+ const [error, setError] = useState('');
44
+ const [dragOver, setDragOver] = useState(false);
45
+ const [library, setLibrary] = useState<Media[] | null>(null);
46
+ const [query, setQuery] = useState('');
47
+ const [photos, setPhotos] = useState<UnsplashPhoto[]>([]);
48
+ const [searching, setSearching] = useState(false);
49
+ const fileInput = useRef<HTMLInputElement>(null);
50
+
51
+ useEffect(() => {
52
+ if (!open) return;
53
+ setError('');
54
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
55
+ window.addEventListener('keydown', onKey);
56
+ return () => window.removeEventListener('keydown', onKey);
57
+ }, [open, onClose]);
58
+
59
+ useEffect(() => {
60
+ if (open && tab === 'library' && library === null) {
61
+ actions.media.list({}).then((r) => {
62
+ // this picker chooses images; documents live in the media library page
63
+ if (r.data) setLibrary(r.data.media.filter((m) => m.mime.startsWith('image/')));
64
+ else setError(r.error?.message ?? 'Could not load library.');
65
+ });
66
+ }
67
+ }, [open, tab, library]);
68
+
69
+ if (!open) return null;
70
+
71
+ async function handleFiles(files: FileList | File[]) {
72
+ const file = files[0];
73
+ if (!file || !file.type.startsWith('image/')) return;
74
+ setBusy(true);
75
+ setError('');
76
+ try {
77
+ const media = await uploadImage(file);
78
+ setLibrary(null); // refetch next time
79
+ onSelect({
80
+ src: mediaSrc(media),
81
+ srcset: mediaSrcset(media),
82
+ sizes: media.variants.length ? DEFAULT_SIZES : '',
83
+ alt: media.alt,
84
+ width: media.width,
85
+ height: media.height,
86
+ caption: '',
87
+ });
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : 'Upload failed.');
90
+ } finally {
91
+ setBusy(false);
92
+ }
93
+ }
94
+
95
+ async function search(page = 1) {
96
+ if (!query.trim()) return;
97
+ setSearching(true);
98
+ setError('');
99
+ const r = await actions.unsplash.search({ query: query.trim(), page });
100
+ if (r.data) setPhotos(r.data.photos);
101
+ else setError(r.error?.message ?? 'Search failed.');
102
+ setSearching(false);
103
+ }
104
+
105
+ function pickUnsplash(p: UnsplashPhoto) {
106
+ actions.unsplash.track({ downloadLocation: p.downloadLocation }).catch(() => {});
107
+ onSelect({
108
+ src: p.regular,
109
+ srcset: unsplashSrcset(p.regular),
110
+ sizes: DEFAULT_SIZES,
111
+ alt: p.alt,
112
+ width: p.width,
113
+ height: p.height,
114
+ caption: `Photo by ${p.authorName} / Unsplash`,
115
+ });
116
+ }
117
+
118
+ return (
119
+ <div className="pe-modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
120
+ <div className="pe-modal" role="dialog" aria-label="Choose an image">
121
+ <div className="pe-modal-head">
122
+ <div className="pe-tabs">
123
+ <button type="button" className={tab === 'upload' ? 'is-active' : ''} onClick={() => setTab('upload')}>Upload</button>
124
+ <button type="button" className={tab === 'library' ? 'is-active' : ''} onClick={() => setTab('library')}>Library</button>
125
+ {unsplashEnabled && (
126
+ <button type="button" className={tab === 'unsplash' ? 'is-active' : ''} onClick={() => setTab('unsplash')}>Unsplash</button>
127
+ )}
128
+ </div>
129
+ <button type="button" className="pe-modal-close" onClick={onClose} aria-label="Close">✕</button>
130
+ </div>
131
+
132
+ {error && <div className="pe-modal-error">{error}</div>}
133
+
134
+ {tab === 'upload' && (
135
+ <div
136
+ className={`pe-dropzone ${dragOver ? 'is-over' : ''} ${busy ? 'is-busy' : ''}`}
137
+ onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
138
+ onDragLeave={() => setDragOver(false)}
139
+ onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files); }}
140
+ onClick={() => !busy && fileInput.current?.click()}
141
+ >
142
+ <input
143
+ ref={fileInput}
144
+ type="file"
145
+ accept="image/*"
146
+ hidden
147
+ onChange={(e) => e.target.files && handleFiles(e.target.files)}
148
+ />
149
+ {busy ? (
150
+ <p>Optimizing & uploading…</p>
151
+ ) : (
152
+ <>
153
+ <p><b>Click to upload</b> or drag an image here</p>
154
+ <p className="pe-dropzone-hint">Resized & converted to WebP automatically</p>
155
+ </>
156
+ )}
157
+ </div>
158
+ )}
159
+
160
+ {tab === 'library' && (
161
+ <div className="pe-grid">
162
+ {library === null && <p className="pe-grid-note">Loading…</p>}
163
+ {library?.length === 0 && <p className="pe-grid-note">No uploads yet.</p>}
164
+ {library?.map((m) => (
165
+ <button
166
+ key={m.id}
167
+ type="button"
168
+ className="pe-grid-item"
169
+ title={m.filename}
170
+ onClick={() =>
171
+ onSelect({
172
+ src: mediaSrc(m),
173
+ srcset: mediaSrcset(m),
174
+ sizes: m.variants.length ? DEFAULT_SIZES : '',
175
+ alt: m.alt,
176
+ width: m.width,
177
+ height: m.height,
178
+ caption: '',
179
+ })
180
+ }
181
+ >
182
+ <img src={mediaThumb(m)} alt={m.alt || m.filename} loading="lazy" />
183
+ </button>
184
+ ))}
185
+ </div>
186
+ )}
187
+
188
+ {tab === 'unsplash' && (
189
+ <>
190
+ <form className="pe-unsplash-search" onSubmit={(e) => { e.preventDefault(); search(); }}>
191
+ <input
192
+ autoFocus
193
+ type="search"
194
+ placeholder="Search free high-resolution photos…"
195
+ value={query}
196
+ onChange={(e) => setQuery(e.target.value)}
197
+ />
198
+ <button type="submit" className="pe-btn primary sm" disabled={searching}>
199
+ {searching ? 'Searching…' : 'Search'}
200
+ </button>
201
+ </form>
202
+ <div className="pe-grid">
203
+ {photos.map((p) => (
204
+ <button key={p.id} type="button" className="pe-grid-item" onClick={() => pickUnsplash(p)}>
205
+ <img src={p.thumb} alt={p.alt} loading="lazy" />
206
+ <span className="pe-grid-credit">{p.authorName}</span>
207
+ </button>
208
+ ))}
209
+ </div>
210
+ </>
211
+ )}
212
+ </div>
213
+ </div>
214
+ );
215
+ }