@acms-subir/core 0.1.0 → 0.3.0
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/CHANGELOG.md +106 -0
- package/cli/common.mjs +35 -0
- package/cli/legacy.mjs +317 -0
- package/cli/main.mjs +66 -0
- package/cli/palette.mjs +4 -0
- package/cli/preview.mjs +50 -0
- package/cli/remote.mjs +55 -0
- package/cli/themes.mjs +127 -0
- package/cli.mjs +2 -315
- package/deploy/Caddyfile +8 -0
- package/deploy/Dockerfile +34 -0
- package/deploy/compose.https.yml +21 -0
- package/deploy/compose.yml +46 -0
- package/docs/theme-authoring.md +103 -0
- package/email-runner.mjs +32 -0
- package/integration.mjs +193 -2
- package/ops-check.mjs +27 -0
- package/package.json +35 -12
- package/src/actions/index.ts +744 -38
- package/src/components/AiCleaner.tsx +291 -0
- package/src/components/AiSettings.tsx +575 -0
- package/src/components/CommandPalette.tsx +200 -0
- package/src/components/ContentList.astro +246 -0
- package/src/components/DesignAccess.tsx +182 -0
- package/src/components/DesignManager.tsx +455 -47
- package/src/components/EmailSettings.tsx +521 -0
- package/src/components/EventPage.astro +178 -0
- package/src/components/LegalPage.astro +27 -0
- package/src/components/LinksManager.tsx +216 -97
- package/src/components/NavManager.tsx +83 -38
- package/src/components/ThemeScript.astro +50 -0
- package/src/components/ThemeSections.astro +57 -0
- package/src/components/ThemeSectionsEditor.tsx +513 -0
- package/src/components/booking/BookingFlow.astro +150 -0
- package/src/components/booking/HoursEditor.astro +63 -0
- package/src/components/builder/PageBuilder.tsx +10 -2
- package/src/components/editor/EventPanel.tsx +401 -0
- package/src/components/editor/FormatDock.tsx +81 -0
- package/src/components/editor/LivePane.tsx +122 -0
- package/src/components/editor/MobileToolbar.tsx +93 -0
- package/src/components/editor/PostEditor.tsx +826 -271
- package/src/components/editor/PreviewOverlay.tsx +242 -43
- package/src/components/editor/extensions/calendar.tsx +51 -7
- package/src/components/editor/extensions/cards.tsx +17 -1
- package/src/components/editor/extensions/codeBlock.tsx +1 -1
- package/src/components/editor/extensions/events.tsx +153 -0
- package/src/components/editor/extensions/infoCards.tsx +16 -1
- package/src/components/editor/extensions/mediaCards.tsx +36 -2
- package/src/components/editor/extensions/slash.tsx +77 -9
- package/src/components/editor/extensions/toc.tsx +89 -0
- package/src/components/editor/extensions/toggle.tsx +70 -0
- package/src/components/site/Pager.astro +24 -0
- package/src/components/site/PostCard.astro +64 -0
- package/src/components/site/TagLabel.astro +11 -0
- package/src/layouts/Admin.astro +289 -49
- package/src/layouts/Site.astro +331 -57
- package/src/lib/agent-api.ts +13 -0
- package/src/lib/agent-discovery.ts +40 -0
- package/src/lib/agent.ts +114 -0
- package/src/lib/ai-config.ts +58 -0
- package/src/lib/ai-schema.ts +22 -0
- package/src/lib/ai.ts +90 -0
- package/src/lib/analytics.ts +121 -0
- package/src/lib/api-keys.ts +65 -0
- package/src/lib/api.ts +52 -0
- package/src/lib/auth.ts +25 -5
- package/src/lib/backup/content.ts +36 -0
- package/src/lib/backup/unzip.ts +12 -1
- package/src/lib/backup/zip.ts +1 -1
- package/src/lib/booking/agent.ts +108 -0
- package/src/lib/booking/availability.ts +221 -0
- package/src/lib/booking/emails.ts +115 -0
- package/src/lib/booking/forms.ts +77 -0
- package/src/lib/booking/service.ts +277 -0
- package/src/lib/booking/view.ts +54 -0
- package/src/lib/calendar/agent-schema.ts +48 -0
- package/src/lib/calendar/agent.ts +136 -0
- package/src/lib/calendar/api.ts +81 -0
- package/src/lib/calendar/cards.ts +102 -0
- package/src/lib/calendar/config.ts +52 -0
- package/src/lib/calendar/data.ts +130 -0
- package/src/lib/calendar/events.ts +261 -0
- package/src/lib/calendar/feeds.ts +43 -0
- package/src/lib/calendar/hooks.ts +38 -0
- package/src/lib/calendar/http.ts +30 -0
- package/src/lib/calendar/ics-write.ts +120 -0
- package/src/lib/calendar/ics.ts +15 -5
- package/src/lib/calendar/import.ts +203 -0
- package/src/lib/calendar/render.ts +120 -37
- package/src/lib/calendar/rrule.ts +149 -0
- package/src/lib/calendar/seo.ts +48 -0
- package/src/lib/design-service.ts +58 -0
- package/src/lib/designDiff.ts +145 -0
- package/src/lib/designPreview.ts +112 -0
- package/src/lib/documents/cleanup.ts +31 -0
- package/src/lib/documents/extract.ts +154 -0
- package/src/lib/documents/vendor.d.ts +4 -0
- package/src/lib/drafts.ts +131 -0
- package/src/lib/dynamicBlocks.ts +79 -7
- package/src/lib/email/accounts.ts +55 -0
- package/src/lib/email/config.ts +86 -0
- package/src/lib/email/migration.ts +24 -0
- package/src/lib/email/providers.ts +89 -0
- package/src/lib/email/service.ts +78 -0
- package/src/lib/email/store.ts +167 -0
- package/src/lib/email/templates.ts +18 -0
- package/src/lib/email/types.ts +32 -0
- package/src/lib/email/webhooks.ts +87 -0
- package/src/lib/email.ts +15 -0
- package/src/lib/env.ts +1 -1
- package/src/lib/legal/index.ts +25 -0
- package/src/lib/mobile/bookings.ts +73 -0
- package/src/lib/mobile/calendar.ts +85 -0
- package/src/lib/mobile/content.ts +85 -0
- package/src/lib/mobile/http.ts +63 -0
- package/src/lib/mobile/route.ts +20 -0
- package/src/lib/ops.ts +8 -0
- package/src/lib/preview.ts +21 -1
- package/src/lib/repo/migrations.ts +254 -0
- package/src/lib/repo/sql.ts +956 -46
- package/src/lib/repo/types.ts +140 -3
- package/src/lib/scheduler.ts +25 -0
- package/src/lib/schema.ts +362 -0
- package/src/lib/search-i18n.ts +3 -0
- package/src/lib/socialIcons.ts +32 -0
- package/src/lib/tagColor.ts +11 -0
- package/src/lib/theme.ts +59 -40
- package/src/lib/themeTokens.ts +174 -0
- package/src/lib/umami.ts +108 -0
- package/src/lib/webhooks.ts +139 -0
- package/src/middleware.ts +36 -3
- package/src/pages/404.astro +23 -0
- package/src/pages/[...slug].astro +10 -5
- package/src/pages/account/[action].astro +53 -0
- package/src/pages/admin/ai.astro +12 -0
- package/src/pages/admin/c/[collection]/[id].astro +14 -4
- package/src/pages/admin/c/[collection]/index.astro +127 -91
- package/src/pages/admin/calendar/bookings.astro +400 -0
- package/src/pages/admin/calendar/calendars.astro +265 -0
- package/src/pages/admin/calendar/events/[id].astro +56 -0
- package/src/pages/admin/calendar/events/index.astro +408 -0
- package/src/pages/admin/calendar/index.ts +4 -0
- package/src/pages/admin/calendar/integrations.astro +221 -0
- package/src/pages/admin/calendar/resources/[id].astro +180 -0
- package/src/pages/admin/calendar/resources/index.astro +106 -0
- package/src/pages/admin/calendar/resources/link.astro +196 -0
- package/src/pages/admin/design.astro +16 -4
- package/src/pages/admin/email.astro +13 -0
- package/src/pages/admin/index.astro +237 -87
- package/src/pages/admin/links.astro +3 -3
- package/src/pages/admin/login.astro +14 -10
- package/src/pages/admin/navigation.astro +8 -2
- package/src/pages/admin/pages/[id].astro +14 -3
- package/src/pages/admin/pages/index.astro +41 -47
- package/src/pages/admin/settings.astro +475 -276
- package/src/pages/admin/tags.astro +4 -4
- package/src/pages/admin/users.astro +38 -28
- package/src/pages/api/agent/docs.ts +33 -0
- package/src/pages/api/agent/index.ts +21 -0
- package/src/pages/api/ai/clean.ts +13 -0
- package/src/pages/api/ai/draft.ts +14 -0
- package/src/pages/api/ai/settings.ts +53 -0
- package/src/pages/api/ai/test.ts +12 -0
- package/src/pages/api/backup.ts +8 -21
- package/src/pages/api/calendar/availability.ts +35 -0
- package/src/pages/api/calendar/bookings/[id].ts +49 -0
- package/src/pages/api/calendar/bookings/index.ts +73 -0
- package/src/pages/api/calendar/calendars.ts +35 -0
- package/src/pages/api/calendar/docs.ts +64 -0
- package/src/pages/api/calendar/events/[id].ts +63 -0
- package/src/pages/api/calendar/events/index.ts +112 -0
- package/src/pages/api/calendar/resources.ts +34 -0
- package/src/pages/api/design/capabilities.ts +10 -0
- package/src/pages/api/design/index.ts +58 -0
- package/src/pages/api/design/keys.ts +24 -0
- package/src/pages/api/design/media.ts +32 -0
- package/src/pages/api/email/run.ts +11 -0
- package/src/pages/api/email/settings.ts +61 -0
- package/src/pages/api/email/webhooks/[provider].ts +4 -0
- package/src/pages/api/mcp.ts +59 -0
- package/src/pages/api/media/upload.ts +2 -2
- package/src/pages/api/ops/status.ts +39 -0
- package/src/pages/api/restore.ts +27 -37
- package/src/pages/api/search.ts +18 -4
- package/src/pages/api/v1/availability.ts +7 -0
- package/src/pages/api/v1/bookings.ts +17 -0
- package/src/pages/api/v1/calendars.ts +7 -0
- package/src/pages/api/v1/content/[id].ts +7 -0
- package/src/pages/api/v1/content/index.ts +7 -0
- package/src/pages/api/v1/events.ts +7 -0
- package/src/pages/api/v1/resources.ts +7 -0
- package/src/pages/author/[slug].astro +43 -35
- package/src/pages/blog/[slug].astro +140 -66
- package/src/pages/blog/index.astro +95 -30
- package/src/pages/blog/tag/[slug].astro +45 -40
- package/src/pages/book/[resource]/[link].astro +70 -0
- package/src/pages/book/[resource]/index.astro +63 -0
- package/src/pages/book/[resource].ics.ts +38 -0
- package/src/pages/book/index.astro +47 -0
- package/src/pages/book/manage/[token].astro +108 -0
- package/src/pages/calendar/[slug].ics.ts +27 -0
- package/src/pages/calendar/feed.ics.ts +15 -0
- package/src/pages/calendar/index.astro +191 -0
- package/src/pages/embed/book/[resource]/[link].astro +33 -0
- package/src/pages/embed/calendar/[slug].astro +43 -0
- package/src/pages/events/[slug].astro +74 -0
- package/src/pages/events/[slug].ics.ts +25 -0
- package/src/pages/index.astro +9 -3
- package/src/pages/legal/privacy.astro +4 -0
- package/src/pages/legal/terms.astro +4 -0
- package/src/pages/links.astro +33 -7
- package/src/pages/llms.txt.ts +4 -0
- package/src/pages/preview/[type]/[id].astro +34 -8
- package/src/pages/search.astro +40 -20
- package/src/pages/sitemap.xml.ts +16 -1
- package/src/styles/admin.css +928 -105
- package/src/styles/cards.css +101 -60
- package/src/styles/editor.css +532 -102
- package/src/styles/email-settings.css +117 -0
- package/src/styles/sections.css +12 -0
- package/src/styles/site.css +938 -278
- package/src/styles/theme-editor.css +135 -0
- package/src/theme-sdk/contract.d.mts +619 -0
- package/src/theme-sdk/contract.mjs +80 -0
- package/src/theme-sdk/index.ts +15 -0
- package/src/theme-sdk/tokens.d.mts +148 -0
- package/src/theme-sdk/tokens.mjs +52 -0
- package/src/themes/editorial/templates/home.astro +4 -4
- package/src/themes/editorial/templates/post-minimal.astro +2 -2
- package/src/themes/editorial/theme.css +22 -37
- package/src/themes/editorial/theme.json +23 -3
- package/src/themes/field/theme.json +26 -9
- package/src/themes/ink/theme.json +30 -12
- package/src/themes/night/theme.css +4 -7
- package/src/themes/night/theme.json +27 -10
- package/src/virtual.d.ts +14 -0
- package/src/worker.ts +5 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
// Side-by-side live preview. Reloads on every autosave (tick), keeps the
|
|
4
|
+
// reader's scroll position across reloads, briefly highlights the first
|
|
5
|
+
// block whose text changed, and follows the writer's caret: `focusText` is
|
|
6
|
+
// the text of the block being edited, matched against the rendered page.
|
|
7
|
+
// The iframe is same-origin (/preview/…), so reading its DOM is allowed.
|
|
8
|
+
|
|
9
|
+
const BLOCKS = 'main :is(p, h1, h2, h3, h4, li, blockquote, pre, figcaption, td)';
|
|
10
|
+
const DEVICES = [
|
|
11
|
+
{ key: 'desktop', label: 'Desktop width', d: 'M3 4h18v12H3zM8 20h8M12 16v4' },
|
|
12
|
+
{ key: 'phone', label: 'Phone width', d: 'M7 2h10v20H7zM11 18h2' },
|
|
13
|
+
] as const;
|
|
14
|
+
const FLASH_BG = 'rgb(238 93 32 / 12%)';
|
|
15
|
+
|
|
16
|
+
const norm = (s: string | null | undefined) => (s ?? '').replace(/\s+/g, ' ').trim();
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
src: string;
|
|
20
|
+
// public path shown in the header, e.g. /posts/my-slug
|
|
21
|
+
path: string;
|
|
22
|
+
tick: number;
|
|
23
|
+
focusText: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function LivePane({ src, path, tick, focusText }: Props) {
|
|
27
|
+
const [device, setDevice] = useState<'desktop' | 'phone'>(() => {
|
|
28
|
+
try { return localStorage.getItem('pe-split-device') === 'phone' ? 'phone' : 'desktop'; } catch { return 'desktop'; }
|
|
29
|
+
});
|
|
30
|
+
const [updatedAt, setUpdatedAt] = useState(Date.now());
|
|
31
|
+
const [, force] = useState(0);
|
|
32
|
+
const frame = useRef<HTMLIFrameElement>(null);
|
|
33
|
+
const prevTexts = useRef<string[] | null>(null);
|
|
34
|
+
const scrollY = useRef(0);
|
|
35
|
+
|
|
36
|
+
// relative "updated" label ticks every 10s
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const t = setInterval(() => force((n) => n + 1), 10_000);
|
|
39
|
+
return () => clearInterval(t);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const blocks = () => {
|
|
43
|
+
let doc: Document | null = null;
|
|
44
|
+
try { doc = frame.current?.contentDocument ?? null; } catch { doc = null; }
|
|
45
|
+
return doc ? [...doc.querySelectorAll<HTMLElement>(BLOCKS)] : [];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const flash = (el: HTMLElement) => {
|
|
49
|
+
el.style.transition = 'background-color 1.6s ease, box-shadow 1.6s ease';
|
|
50
|
+
el.style.backgroundColor = FLASH_BG;
|
|
51
|
+
el.style.boxShadow = `0 0 0 5px ${FLASH_BG}`;
|
|
52
|
+
el.style.borderRadius = '4px';
|
|
53
|
+
setTimeout(() => { el.style.backgroundColor = 'transparent'; el.style.boxShadow = '0 0 0 5px transparent'; }, 900);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const reveal = (el: HTMLElement) => {
|
|
57
|
+
const win = frame.current?.contentWindow;
|
|
58
|
+
if (!win) return;
|
|
59
|
+
const r = el.getBoundingClientRect();
|
|
60
|
+
if (r.top < 0 || r.bottom > win.innerHeight) el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// follow the caret: scroll the block being edited into view
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const want = norm(focusText);
|
|
66
|
+
if (want.length < 3) return;
|
|
67
|
+
const head = want.slice(0, 40);
|
|
68
|
+
const el = blocks().find((b) => {
|
|
69
|
+
const t = norm(b.textContent);
|
|
70
|
+
return t.length >= 3 && (t === want || t.startsWith(head) || want.startsWith(t.slice(0, 40)));
|
|
71
|
+
});
|
|
72
|
+
if (el) reveal(el);
|
|
73
|
+
}, [focusText]);
|
|
74
|
+
|
|
75
|
+
const onLoad = () => {
|
|
76
|
+
setUpdatedAt(Date.now());
|
|
77
|
+
const all = blocks();
|
|
78
|
+
if (!frame.current?.contentWindow) return;
|
|
79
|
+
const texts = all.map((b) => b.textContent?.trim() ?? '');
|
|
80
|
+
const prev = prevTexts.current;
|
|
81
|
+
prevTexts.current = texts;
|
|
82
|
+
const win = frame.current.contentWindow;
|
|
83
|
+
// the iframe is recreated on every autosave: restore where the reader was
|
|
84
|
+
win.scrollTo(0, scrollY.current);
|
|
85
|
+
win.addEventListener('scroll', () => { scrollY.current = win.scrollY; }, { passive: true });
|
|
86
|
+
if (!prev) return;
|
|
87
|
+
const idx = texts.findIndex((t, i) => t !== prev[i]);
|
|
88
|
+
if (idx < 0) return;
|
|
89
|
+
flash(all[idx]);
|
|
90
|
+
reveal(all[idx]);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const pick = (d: 'desktop' | 'phone') => {
|
|
94
|
+
setDevice(d);
|
|
95
|
+
try { localStorage.setItem('pe-split-device', d); } catch { /* private mode */ }
|
|
96
|
+
};
|
|
97
|
+
const secs = Math.round((Date.now() - updatedAt) / 1000);
|
|
98
|
+
const ago = secs < 10 ? 'just now' : secs < 60 ? `${Math.floor(secs / 10) * 10}s ago` : `${Math.floor(secs / 60)}m ago`;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<aside className="pe-splitpane" aria-label="Live preview">
|
|
102
|
+
<div className="pe-split-bar">
|
|
103
|
+
<span className="pe-live" title={`Updated ${ago}`}><i />Live</span>
|
|
104
|
+
<span className="pe-live-path">{path}</span>
|
|
105
|
+
<small className="pe-live-ago">{ago}</small>
|
|
106
|
+
<div className="pe-seg" role="group" aria-label="Preview width">
|
|
107
|
+
{DEVICES.map((d) => (
|
|
108
|
+
<button key={d.key} type="button" aria-pressed={device === d.key} title={d.label} aria-label={d.label} onClick={() => pick(d.key)}>
|
|
109
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d={d.d} /></svg>
|
|
110
|
+
</button>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
<a className="pe-btn ghost sm pe-icon-btn" href={src} target="_blank" rel="noopener" title="Open preview in a new tab" aria-label="Open preview in a new tab">
|
|
114
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5" /></svg>
|
|
115
|
+
</a>
|
|
116
|
+
</div>
|
|
117
|
+
<div className={`pe-split-stage ${device}`}>
|
|
118
|
+
<iframe ref={frame} key={tick} src={src} title="Live preview" onLoad={onLoad} />
|
|
119
|
+
</div>
|
|
120
|
+
</aside>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { Editor } from '@tiptap/react';
|
|
3
|
+
|
|
4
|
+
// Phone formatting bar, pinned just above the on-screen keyboard. The layout
|
|
5
|
+
// viewport doesn't shrink when the keyboard opens (iOS), so the bar follows
|
|
6
|
+
// visualViewport instead. Only shown on touch devices while the body has
|
|
7
|
+
// focus; desktop keeps the selection bubble menu.
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
editor: Editor;
|
|
11
|
+
onLink: () => void;
|
|
12
|
+
onImage: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const I = {
|
|
16
|
+
plus: 'M12 5v14M5 12h14',
|
|
17
|
+
h: 'M5 5v14M15 5v14M5 12h10M19 10v9',
|
|
18
|
+
list: 'M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01',
|
|
19
|
+
quote: 'M7 7h4v4c0 3-2 5-4 6M14 7h4v4c0 3-2 5-4 6',
|
|
20
|
+
link: 'M10 14a4 4 0 0 0 6 0l3-3a4 4 0 0 0-6-6l-1.5 1.5M14 10a4 4 0 0 0-6 0l-3 3a4 4 0 0 0 6 6l1.5-1.5',
|
|
21
|
+
image: 'M3 5h18v14H3zM3 15l5-5 4 4 3-3 6 6',
|
|
22
|
+
undo: 'M9 14 4 9l5-5M4 9h11a5 5 0 0 1 0 10h-3',
|
|
23
|
+
down: 'M6 9l6 6 6-6',
|
|
24
|
+
};
|
|
25
|
+
const Svg = ({ d }: { d: string }) => (
|
|
26
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d={d} /></svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export function MobileToolbar({ editor, onLink, onImage }: Props) {
|
|
30
|
+
const [touch] = useState(() => typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches);
|
|
31
|
+
const [focused, setFocused] = useState(false);
|
|
32
|
+
const [bottom, setBottom] = useState(0);
|
|
33
|
+
const [, rerender] = useState(0);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!touch) return;
|
|
37
|
+
const onFocus = () => setFocused(true);
|
|
38
|
+
const onBlur = () => setTimeout(() => setFocused(editor.isFocused), 120);
|
|
39
|
+
const onTx = () => rerender((n) => n + 1);
|
|
40
|
+
editor.on('focus', onFocus);
|
|
41
|
+
editor.on('blur', onBlur);
|
|
42
|
+
editor.on('transaction', onTx);
|
|
43
|
+
const vv = window.visualViewport;
|
|
44
|
+
const place = () => {
|
|
45
|
+
if (!vv) return;
|
|
46
|
+
setBottom(Math.max(0, window.innerHeight - (vv.height + vv.offsetTop)));
|
|
47
|
+
};
|
|
48
|
+
vv?.addEventListener('resize', place);
|
|
49
|
+
vv?.addEventListener('scroll', place);
|
|
50
|
+
place();
|
|
51
|
+
return () => {
|
|
52
|
+
editor.off('focus', onFocus);
|
|
53
|
+
editor.off('blur', onBlur);
|
|
54
|
+
editor.off('transaction', onTx);
|
|
55
|
+
vv?.removeEventListener('resize', place);
|
|
56
|
+
vv?.removeEventListener('scroll', place);
|
|
57
|
+
};
|
|
58
|
+
}, [editor, touch]);
|
|
59
|
+
|
|
60
|
+
if (!touch || !focused) return null;
|
|
61
|
+
|
|
62
|
+
// keep focus in the editor: act on mousedown/touch before blur fires
|
|
63
|
+
const btn = (key: string, label: string, active: boolean, run: () => void, child: React.ReactNode) => (
|
|
64
|
+
<button
|
|
65
|
+
key={key}
|
|
66
|
+
type="button"
|
|
67
|
+
aria-label={label}
|
|
68
|
+
aria-pressed={active}
|
|
69
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
70
|
+
onClick={run}
|
|
71
|
+
>
|
|
72
|
+
{child}
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
75
|
+
const c = () => editor.chain().focus();
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="pe-mtoolbar" style={{ bottom }} role="toolbar" aria-label="Formatting">
|
|
79
|
+
{btn('insert', 'Insert block', false, () => c().insertContent('/').run(), <Svg d={I.plus} />)}
|
|
80
|
+
<span className="pe-mtoolbar-sep" />
|
|
81
|
+
{btn('bold', 'Bold', editor.isActive('bold'), () => c().toggleBold().run(), <b>B</b>)}
|
|
82
|
+
{btn('italic', 'Italic', editor.isActive('italic'), () => c().toggleItalic().run(), <i>I</i>)}
|
|
83
|
+
{btn('h2', 'Heading', editor.isActive('heading', { level: 2 }), () => c().toggleHeading({ level: 2 }).run(), <Svg d={I.h} />)}
|
|
84
|
+
{btn('list', 'Bulleted list', editor.isActive('bulletList'), () => c().toggleBulletList().run(), <Svg d={I.list} />)}
|
|
85
|
+
{btn('quote', 'Quote', editor.isActive('blockquote'), () => c().toggleBlockquote().run(), <Svg d={I.quote} />)}
|
|
86
|
+
{btn('link', 'Link', editor.isActive('link'), onLink, <Svg d={I.link} />)}
|
|
87
|
+
{btn('image', 'Image', false, onImage, <Svg d={I.image} />)}
|
|
88
|
+
{btn('undo', 'Undo', false, () => c().undo().run(), <Svg d={I.undo} />)}
|
|
89
|
+
<span className="pe-mtoolbar-sep" />
|
|
90
|
+
{btn('done', 'Hide keyboard', false, () => editor.commands.blur(), <Svg d={I.down} />)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|