fedipod 0.8.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/LICENSE +21 -0
- package/README.md +123 -0
- package/architecture.svg +89 -0
- package/bin/fedipod.mjs +2303 -0
- package/cli.md +243 -0
- package/groups.md +85 -0
- package/gui.md +99 -0
- package/lib/account.mjs +87 -0
- package/lib/admin.mjs +1606 -0
- package/lib/atproto.mjs +310 -0
- package/lib/bskyfeed.mjs +166 -0
- package/lib/bskygroup.mjs +162 -0
- package/lib/c2s.mjs +305 -0
- package/lib/certs.mjs +265 -0
- package/lib/deliver.mjs +249 -0
- package/lib/directory.mjs +90 -0
- package/lib/embed.mjs +214 -0
- package/lib/export-collections.mjs +161 -0
- package/lib/front-core.mjs +322 -0
- package/lib/gateway-core.mjs +115 -0
- package/lib/guard.mjs +213 -0
- package/lib/home.mjs +161 -0
- package/lib/httpsig.mjs +109 -0
- package/lib/import.mjs +357 -0
- package/lib/intake.mjs +1710 -0
- package/lib/keys.mjs +139 -0
- package/lib/lease.mjs +190 -0
- package/lib/localapi.mjs +87 -0
- package/lib/mastoapi.mjs +1588 -0
- package/lib/migrate.mjs +127 -0
- package/lib/oidc-auth.mjs +53 -0
- package/lib/podrdf.mjs +132 -0
- package/lib/ports.mjs +27 -0
- package/lib/publisher.mjs +1003 -0
- package/lib/remote.mjs +303 -0
- package/lib/safefetch.mjs +235 -0
- package/lib/setup.mjs +262 -0
- package/lib/social.mjs +505 -0
- package/lib/storage.mjs +170 -0
- package/lib/store.mjs +470 -0
- package/lib/streaming.mjs +154 -0
- package/lib/tagfeed.mjs +158 -0
- package/lib/ua.mjs +29 -0
- package/lib/update.mjs +91 -0
- package/lib/webpush.mjs +98 -0
- package/lib/wire.mjs +762 -0
- package/package.json +60 -0
- package/phanpy/dist/404.html +40 -0
- package/phanpy/dist/_headers +2 -0
- package/phanpy/dist/apple-touch-icon.png +0 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js +2 -0
- package/phanpy/dist/assets/ICONS-DIPJeU0C.js.map +1 -0
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js +183 -0
- package/phanpy/dist/assets/ScheduledAtField-D-Abfo4J.js.map +1 -0
- package/phanpy/dist/assets/Temml-CCjoacWt.woff2 +0 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js +2 -0
- package/phanpy/dist/assets/__vite-browser-external-C7iut881.js.map +1 -0
- package/phanpy/dist/assets/boosts-carousel-BiOaNdMT.jpg +0 -0
- package/phanpy/dist/assets/catch-up-O5q5C75Z.png +0 -0
- package/phanpy/dist/assets/chunk-aKtaBQYM.js +1 -0
- package/phanpy/dist/assets/compose-CFEx7wb4.js +2 -0
- package/phanpy/dist/assets/compose-CFEx7wb4.js.map +1 -0
- package/phanpy/dist/assets/compose-ZJN5xSb4.js +35 -0
- package/phanpy/dist/assets/compose-ZJN5xSb4.js.map +1 -0
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js +52 -0
- package/phanpy/dist/assets/debug.module-YFQgwnIr.js.map +1 -0
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js +2 -0
- package/phanpy/dist/assets/dom-p1YcJ5Rw.js.map +1 -0
- package/phanpy/dist/assets/home-mobile-dark@2x-Dc-J8-SM.png +0 -0
- package/phanpy/dist/assets/home-mobile-light@2x-Bd3fod8m.png +0 -0
- package/phanpy/dist/assets/home-tablet-dark@2x-BgcNI8le.png +0 -0
- package/phanpy/dist/assets/home-tablet-light@2x-BW2RV9T6.png +0 -0
- package/phanpy/dist/assets/icons/add-circle-line-WD0mwORD.js +1 -0
- package/phanpy/dist/assets/icons/alert-line-CpFUMf9s.js +1 -0
- package/phanpy/dist/assets/icons/android-2-line-Bzxnrd3S.js +1 -0
- package/phanpy/dist/assets/icons/announcement-line-5nwaJFJm.js +1 -0
- package/phanpy/dist/assets/icons/arrow-down-circle-line-a7Id9-Iv.js +1 -0
- package/phanpy/dist/assets/icons/arrow-down-line-B8uzXzq6.js +1 -0
- package/phanpy/dist/assets/icons/arrow-left-line-CGrGT9C-.js +1 -0
- package/phanpy/dist/assets/icons/arrow-right-line-CE-HERfX.js +1 -0
- package/phanpy/dist/assets/icons/arrow-to-up-line-CvdQjmcy.js +1 -0
- package/phanpy/dist/assets/icons/arrow-up-circle-line-DEparSNF.js +1 -0
- package/phanpy/dist/assets/icons/arrow-up-line-C7yBeugc.js +1 -0
- package/phanpy/dist/assets/icons/arrows-right-line-BtbXmJGP.js +1 -0
- package/phanpy/dist/assets/icons/at-line-CF66Bznk.js +1 -0
- package/phanpy/dist/assets/icons/attachment-line-BY8It-l9.js +1 -0
- package/phanpy/dist/assets/icons/blockquote-line-BS6pU_hm.js +1 -0
- package/phanpy/dist/assets/icons/board-line-C0UWXyA6.js +1 -0
- package/phanpy/dist/assets/icons/bookmark-line-1CtX5JLF.js +1 -0
- package/phanpy/dist/assets/icons/building-5-line-Dd59MzbO.js +1 -0
- package/phanpy/dist/assets/icons/bus-2-line-6_dvA0GG.js +1 -0
- package/phanpy/dist/assets/icons/calendar-day-line-Crpl_fmu.js +1 -0
- package/phanpy/dist/assets/icons/calendar-month-line-vommts2C.js +1 -0
- package/phanpy/dist/assets/icons/calendar-time-add-line-BU_DTlQk.js +1 -0
- package/phanpy/dist/assets/icons/camera-line-Cv69MEei.js +1 -0
- package/phanpy/dist/assets/icons/celebrate-line-B8DvIoi0.js +1 -0
- package/phanpy/dist/assets/icons/chart-bar-line-FK-QOmtF.js +1 -0
- package/phanpy/dist/assets/icons/chart-line-line-BGcJAFWw.js +1 -0
- package/phanpy/dist/assets/icons/chat-3-line-YFK2-hwz.js +1 -0
- package/phanpy/dist/assets/icons/check-circle-line-DsAxFr5-.js +1 -0
- package/phanpy/dist/assets/icons/clipboard-line-BBp364gJ.js +1 -0
- package/phanpy/dist/assets/icons/close-circle-line-CICdiuGz.js +1 -0
- package/phanpy/dist/assets/icons/close-line-DYrFWcjv.js +1 -0
- package/phanpy/dist/assets/icons/cloud-line-tiQAbah8.js +1 -0
- package/phanpy/dist/assets/icons/code-line-fAl8iiV3.js +1 -0
- package/phanpy/dist/assets/icons/comment-2-line-CR3g7WeB.js +1 -0
- package/phanpy/dist/assets/icons/copy-2-line-CLW69yF5.js +1 -0
- package/phanpy/dist/assets/icons/delete-2-line-B8xmbr4n.js +1 -0
- package/phanpy/dist/assets/icons/document-3-line-DBGNhAg2.js +1 -0
- package/phanpy/dist/assets/icons/document-line-C7mb9sb7.js +1 -0
- package/phanpy/dist/assets/icons/down-line-BGqSSThd.js +1 -0
- package/phanpy/dist/assets/icons/edit-4-line-DcgGaEhx.js +1 -0
- package/phanpy/dist/assets/icons/emoji-2-line-D8YgnM-y.js +1 -0
- package/phanpy/dist/assets/icons/exit-line-Dc07BS-x.js +1 -0
- package/phanpy/dist/assets/icons/external-link-line-BvpSkmjO.js +1 -0
- package/phanpy/dist/assets/icons/eye-2-line-CiWXx-f_.js +1 -0
- package/phanpy/dist/assets/icons/eye-close-line-BgTaSJ4P.js +1 -0
- package/phanpy/dist/assets/icons/filter-2-line-B_mR2usy.js +1 -0
- package/phanpy/dist/assets/icons/filter-line-C_nm-3cU.js +1 -0
- package/phanpy/dist/assets/icons/flag-1-line-C2HB05I0.js +1 -0
- package/phanpy/dist/assets/icons/forbid-circle-line-D3LTT9yd.js +1 -0
- package/phanpy/dist/assets/icons/formula-line-DLrt2d6j.js +1 -0
- package/phanpy/dist/assets/icons/grid-line-CgUmEikf.js +1 -0
- package/phanpy/dist/assets/icons/group-line-C5fnX0wU.js +1 -0
- package/phanpy/dist/assets/icons/hand-finger-2-line-BGgxbuL9.js +1 -0
- package/phanpy/dist/assets/icons/hashtag-line-CyTEEXBb.js +1 -0
- package/phanpy/dist/assets/icons/heart-crack-line-BZddbO2C.js +1 -0
- package/phanpy/dist/assets/icons/heart-line-CJ7K47J4.js +1 -0
- package/phanpy/dist/assets/icons/history-2-line-kWWvFzqv.js +1 -0
- package/phanpy/dist/assets/icons/history-line-bGQiwbTH.js +1 -0
- package/phanpy/dist/assets/icons/home-3-line-fRU1zeLG.js +1 -0
- package/phanpy/dist/assets/icons/information-line-iNk6-sOY.js +1 -0
- package/phanpy/dist/assets/icons/keyboard-line-DsCyuQNz.js +1 -0
- package/phanpy/dist/assets/icons/layout-4-line-CZkOX52Z.js +1 -0
- package/phanpy/dist/assets/icons/layout-5-line-B_znt5wO.js +1 -0
- package/phanpy/dist/assets/icons/left-line-eUw0tya4.js +1 -0
- package/phanpy/dist/assets/icons/lightning-line-DaKoDdix.js +1 -0
- package/phanpy/dist/assets/icons/link-2-line-DCqLmMA7.js +1 -0
- package/phanpy/dist/assets/icons/list-check-line-BAxosBh9.js +1 -0
- package/phanpy/dist/assets/icons/lock-line-CPoaXeUW.js +1 -0
- package/phanpy/dist/assets/icons/mail-line-B0P8aa1L.js +1 -0
- package/phanpy/dist/assets/icons/moon-line-CEqNAToP.js +1 -0
- package/phanpy/dist/assets/icons/more-1-fill-Dw2e5xab.js +1 -0
- package/phanpy/dist/assets/icons/more-3-line-BIiVjYTz.js +1 -0
- package/phanpy/dist/assets/icons/notification-line-BZZq2Gtu.js +1 -0
- package/phanpy/dist/assets/icons/pencil-line-CKh8-A1v.js +1 -0
- package/phanpy/dist/assets/icons/photo-album-line-Bnfg2u40.js +1 -0
- package/phanpy/dist/assets/icons/pin-line-DpTolyhs.js +1 -0
- package/phanpy/dist/assets/icons/play-fill-B0-G_8lX.js +1 -0
- package/phanpy/dist/assets/icons/qrcode-2-line-B6SPCaHn.js +1 -0
- package/phanpy/dist/assets/icons/quill-pen-line-B6LUrnCP.js +1 -0
- package/phanpy/dist/assets/icons/quote-left-fill-hQZ1cj_6.js +1 -0
- package/phanpy/dist/assets/icons/radar-line-CKyroTth.js +1 -0
- package/phanpy/dist/assets/icons/react-line-CoIKhhtK.js +1 -0
- package/phanpy/dist/assets/icons/refresh-2-line-D0mTF97S.js +1 -0
- package/phanpy/dist/assets/icons/right-line-B3KWJaqc.js +1 -0
- package/phanpy/dist/assets/icons/rocket-line-4KZl537D.js +1 -0
- package/phanpy/dist/assets/icons/round-fill-C69NTrZ0.js +1 -0
- package/phanpy/dist/assets/icons/round-line-B9k_1uo-.js +1 -0
- package/phanpy/dist/assets/icons/route-line-B28ReHcE.js +1 -0
- package/phanpy/dist/assets/icons/rows-4-line-CrBniagM.js +1 -0
- package/phanpy/dist/assets/icons/scan-line-DSf74YdP.js +1 -0
- package/phanpy/dist/assets/icons/search-2-line-B6geIU1z.js +1 -0
- package/phanpy/dist/assets/icons/settings-3-line-azJwK47w.js +1 -0
- package/phanpy/dist/assets/icons/settings-6-line-C_5zc5YR.js +1 -0
- package/phanpy/dist/assets/icons/share-2-line-D5MxWjrm.js +1 -0
- package/phanpy/dist/assets/icons/share-forward-line-Cp61fZzZ.js +1 -0
- package/phanpy/dist/assets/icons/sparkles-2-line-DYYyXTaW.js +1 -0
- package/phanpy/dist/assets/icons/sparkles-line-C2hY1s6I.js +1 -0
- package/phanpy/dist/assets/icons/time-line-BF3eet1v.js +1 -0
- package/phanpy/dist/assets/icons/transfer-4-line-oisfvTBw.js +1 -0
- package/phanpy/dist/assets/icons/translate-line-BFWol2Ce.js +1 -0
- package/phanpy/dist/assets/icons/unlock-line-X_x5vwv3.js +1 -0
- package/phanpy/dist/assets/icons/upload-3-line-DgzwUJeW.js +1 -0
- package/phanpy/dist/assets/icons/user-4-line-BVSynZCp.js +1 -0
- package/phanpy/dist/assets/icons/user-add-2-line-BlGS5Q8q.js +1 -0
- package/phanpy/dist/assets/icons/user-add-line-BbXdHG4z.js +1 -0
- package/phanpy/dist/assets/icons/user-edit-line-BFAHOV0X.js +1 -0
- package/phanpy/dist/assets/icons/user-follow-line-BXux0wSd.js +1 -0
- package/phanpy/dist/assets/icons/user-star-line-DdYlpHLU.js +1 -0
- package/phanpy/dist/assets/icons/user-warning-line-Cj43j0_E.js +1 -0
- package/phanpy/dist/assets/icons/user-x-line-DIDgNlPH.js +1 -0
- package/phanpy/dist/assets/icons/volume-line-Bs390KQt.js +1 -0
- package/phanpy/dist/assets/icons/volume-mute-line-DzlzIqFZ.js +1 -0
- package/phanpy/dist/assets/icons/walk-line-yNXfB-md.js +1 -0
- package/phanpy/dist/assets/icons/world-2-line-CenkGIwj.js +1 -0
- package/phanpy/dist/assets/icons/zoom-in-line-CaENUphf.js +1 -0
- package/phanpy/dist/assets/icons/zoom-out-line-BDNk0EWo.js +1 -0
- package/phanpy/dist/assets/instances-BNPptnn-.json +333 -0
- package/phanpy/dist/assets/locales/ar-SA-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/ca-ES-DHRW86Tz.js +1 -0
- package/phanpy/dist/assets/locales/cs-CZ-CKoqEuA1.js +1 -0
- package/phanpy/dist/assets/locales/de-DE-ByRPMRFy.js +1 -0
- package/phanpy/dist/assets/locales/eo-UY-CPC7wLCS.js +1 -0
- package/phanpy/dist/assets/locales/es-ES-dJa0Fd9t.js +1 -0
- package/phanpy/dist/assets/locales/eu-ES-C3djlPR2.js +1 -0
- package/phanpy/dist/assets/locales/fa-IR-DTT490Ck.js +1 -0
- package/phanpy/dist/assets/locales/fi-FI-B23MCPuz.js +1 -0
- package/phanpy/dist/assets/locales/fr-FR-BcaQsO5C.js +1 -0
- package/phanpy/dist/assets/locales/gl-ES-BR-CVKBN.js +1 -0
- package/phanpy/dist/assets/locales/he-IL-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/hu-HU-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/it-IT-C1136UFV.js +1 -0
- package/phanpy/dist/assets/locales/ja-JP-CRZmRAhD.js +1 -0
- package/phanpy/dist/assets/locales/kab-Cz8xH_TT.js +1 -0
- package/phanpy/dist/assets/locales/ko-KR-CG9UiY4o.js +1 -0
- package/phanpy/dist/assets/locales/lt-LT-CJcI6sHZ.js +1 -0
- package/phanpy/dist/assets/locales/nb-NO-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/nl-NL-DxN4Yc1t.js +1 -0
- package/phanpy/dist/assets/locales/oc-FR-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/pl-PL-DkwGXf6P.js +1 -0
- package/phanpy/dist/assets/locales/pseudo-LOCALE-CQ4XxkQ0.js +1 -0
- package/phanpy/dist/assets/locales/pt-BR-nGL0HdFS.js +1 -0
- package/phanpy/dist/assets/locales/pt-PT-Cp17aj2z.js +1 -0
- package/phanpy/dist/assets/locales/ru-RU-0Y8cIoaK.js +1 -0
- package/phanpy/dist/assets/locales/th-TH-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/tok-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/tr-TR-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/locales/uk-UA-C5E3dFBf.js +1 -0
- package/phanpy/dist/assets/locales/zh-CN-BHJi7_et.js +1 -0
- package/phanpy/dist/assets/locales/zh-TW-CfheRbCu.js +1 -0
- package/phanpy/dist/assets/main-BdqNbG-a.js +26 -0
- package/phanpy/dist/assets/main-BdqNbG-a.js.map +1 -0
- package/phanpy/dist/assets/mock-home-DGhR__He.js +2 -0
- package/phanpy/dist/assets/mock-home-DGhR__He.js.map +1 -0
- package/phanpy/dist/assets/multi-column-ROck0NVt.jpg +0 -0
- package/phanpy/dist/assets/multi-hashtag-timeline-DH5INVHi.jpg +0 -0
- package/phanpy/dist/assets/nested-comments-thread-hv59kZLc.jpg +0 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js +2 -0
- package/phanpy/dist/assets/polyfill-force-H8sUkLGp.js.map +1 -0
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js +20 -0
- package/phanpy/dist/assets/pwa-viewport-ogKbF51K.js.map +1 -0
- package/phanpy/dist/assets/style-B1iLPTPR.css +2 -0
- package/phanpy/dist/assets/temml-DuyIQMei.js +97 -0
- package/phanpy/dist/assets/temml-DuyIQMei.js.map +1 -0
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js +2 -0
- package/phanpy/dist/assets/tinyld.light.node-DtbBhLYC.js.map +1 -0
- package/phanpy/dist/assets/year-in-posts-B2lywqdx.png +0 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js +3 -0
- package/phanpy/dist/assets/year-in-posts-DJv_star.js.map +1 -0
- package/phanpy/dist/compose/index.html +20 -0
- package/phanpy/dist/favicon.ico +0 -0
- package/phanpy/dist/index.html +73 -0
- package/phanpy/dist/logo-192.png +0 -0
- package/phanpy/dist/logo-512.png +0 -0
- package/phanpy/dist/logo-badge-72.png +0 -0
- package/phanpy/dist/logo-maskable-512.png +0 -0
- package/phanpy/dist/logo-monochrome-512.png +0 -0
- package/phanpy/dist/logo-monochrome-maskable-512.png +0 -0
- package/phanpy/dist/manifest.webmanifest +1 -0
- package/phanpy/dist/og-image-2.jpg +0 -0
- package/phanpy/dist/og-image.png +0 -0
- package/phanpy/dist/robots.txt +2 -0
- package/phanpy/dist/sw.js +25 -0
- package/phanpy/dist/version.json +1 -0
- package/run-agent.mjs +805 -0
- package/scripts/build-dist.mjs +37 -0
- package/scripts/verify-ui.mjs +60 -0
- package/ui/README.md +23 -0
- package/vendor/gate.cjs +132 -0
- package/vendor/idp-grant.cjs +394 -0
- package/vendor/jfetch.cjs +16 -0
- package/web/admin/admin.js +958 -0
- package/web/admin/bar.css +64 -0
- package/web/admin/bar.js +61 -0
- package/web/admin/client/client.js +66 -0
- package/web/admin/client/index.html +42 -0
- package/web/admin/index.html +493 -0
- package/web/admin/setup/index.html +172 -0
- package/web/admin/setup/setup.js +268 -0
- package/web/admin/window.css +55 -0
- package/web/admin/window.js +136 -0
- package/web/front/install.sh +69 -0
- package/web/front/new-account.html +424 -0
- package/web/front/run.html +138 -0
- package/web/front/solid-client-authn.bundle.js +2 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>FediPod — setup</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* 20px on the root, not on body, so every rem below scales with it. */
|
|
9
|
+
:root { color-scheme: light dark; font-size: 112.5%; --heading: #1a4f8a; }
|
|
10
|
+
body { font: 1rem/1.5 system-ui, sans-serif; margin: 0; }
|
|
11
|
+
/* The bar spans the window; the form stays in a centred column under it. */
|
|
12
|
+
/* The same 960 column the bar and the record use, so the three pages line up. */
|
|
13
|
+
#page { max-width: 960px; margin: 0 auto; padding: 1.75rem 1rem 4rem; box-sizing: border-box; }
|
|
14
|
+
h1, h2, h3 { color: var(--heading); }
|
|
15
|
+
h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
|
|
16
|
+
h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
|
|
17
|
+
p { margin: .5rem 0; }
|
|
18
|
+
.sub { color: #666; margin-top: 0; }
|
|
19
|
+
fieldset { border: 1px solid #bbb; border-radius: .4rem; margin: 1rem 0; padding: .75rem 1rem 1rem; }
|
|
20
|
+
legend { padding: 0 .3rem; font-weight: 600; }
|
|
21
|
+
label { display: block; margin: .75rem 0 .2rem; }
|
|
22
|
+
.hint { color: #666; font-size: .9rem; margin: .15rem 0 0; }
|
|
23
|
+
input[type=text], input[type=email], input[type=url], input[type=password], textarea {
|
|
24
|
+
font: inherit; width: 100%; padding: .5rem; box-sizing: border-box;
|
|
25
|
+
border: 1px solid #767676; border-radius: .3rem; background: Field; color: FieldText;
|
|
26
|
+
}
|
|
27
|
+
.choice { display: block; margin: .35rem 0; font-weight: normal; }
|
|
28
|
+
.choice input { margin-right: .4rem; }
|
|
29
|
+
button { font: inherit; padding: .6rem 1.2rem; border-radius: .3rem; border: 1px solid #666;
|
|
30
|
+
background: ButtonFace; color: ButtonText; cursor: pointer; }
|
|
31
|
+
button.primary { font-weight: 600; }
|
|
32
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
33
|
+
.address { font-size: 1.2rem; font-weight: 600; word-break: break-all; }
|
|
34
|
+
.warn { border-left: .25rem solid #c60; padding-left: .75rem; }
|
|
35
|
+
.err { border-left: .25rem solid #b00020; padding-left: .75rem; color: #b00020; }
|
|
36
|
+
/* Announced, not shown: the step list beside it already says this visually. */
|
|
37
|
+
/* In the accessibility tree from load, hidden by being empty: a live region
|
|
38
|
+
added to the tree in the same task as its text does not announce. */
|
|
39
|
+
#form-error:empty, #run-error:empty { display: none; }
|
|
40
|
+
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
|
|
41
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
42
|
+
.steps { list-style: none; padding: 0; }
|
|
43
|
+
.steps li { padding: .3rem 0; }
|
|
44
|
+
.steps .state { display: inline-block; width: 1.6rem; }
|
|
45
|
+
.note { color: #666; font-size: .9rem; word-break: break-all; }
|
|
46
|
+
.origins { font-family: ui-monospace, monospace; font-size: .95rem; word-break: break-all; }
|
|
47
|
+
@media (prefers-color-scheme: dark) { :root { --heading: #7fb3e8; }
|
|
48
|
+
.sub, .hint, .note { color: #aaa; } fieldset { border-color: #555; }
|
|
49
|
+
/* 2.40:1 without this — the record page has always had the override and
|
|
50
|
+
this page, where an error IS the whole message, did not. */
|
|
51
|
+
.err { color: #ff8a8a; border-left-color: #ff8a8a; } }
|
|
52
|
+
</style>
|
|
53
|
+
<link rel="stylesheet" href="../bar.css">
|
|
54
|
+
</head>
|
|
55
|
+
<body>
|
|
56
|
+
|
|
57
|
+
<nav id="bar" aria-label="Site">
|
|
58
|
+
<h1 class="name">FediPod</h1>
|
|
59
|
+
<span id="bar-handle"></span>
|
|
60
|
+
<a id="bar-fediverse" href="../client/" title=" Open the client and read your timeline">visit account</a>
|
|
61
|
+
<a id="bar-manage" href="../" title=" The record: what this actor is, and what you can do to it">manage account</a>
|
|
62
|
+
<a id="bar-add" href="../?new=1" title=" Set up another actor on this machine, with its own pod" aria-current="page">add new account</a>
|
|
63
|
+
</nav>
|
|
64
|
+
<main id="page">
|
|
65
|
+
<p class="sub" id="strap">setting up…</p>
|
|
66
|
+
|
|
67
|
+
<!-- Nothing here is wired up by hand: setup.js reads /setup/state and shows
|
|
68
|
+
exactly one of these. -->
|
|
69
|
+
|
|
70
|
+
<section id="pane-loading">
|
|
71
|
+
<p>Reading this agent's state…</p>
|
|
72
|
+
</section>
|
|
73
|
+
|
|
74
|
+
<section id="pane-done" hidden>
|
|
75
|
+
<h2>Ready</h2>
|
|
76
|
+
<p class="address" id="done-address"></p>
|
|
77
|
+
<p id="done-note"></p>
|
|
78
|
+
<p id="done-links"></p>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section id="pane-configured" hidden>
|
|
82
|
+
<h2>This agent is already set up</h2>
|
|
83
|
+
<p class="address" id="configured-address"></p>
|
|
84
|
+
<p>
|
|
85
|
+
<a href="../">Edit the record</a>
|
|
86
|
+
·
|
|
87
|
+
<a href="../../" id="configured-client">Open the client</a>
|
|
88
|
+
</p>
|
|
89
|
+
<p class="hint">For another actor, use <strong>add new account</strong> above.</p>
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<section id="pane-run" hidden>
|
|
93
|
+
<h2 id="run-title">Setting up</h2>
|
|
94
|
+
<ul class="steps" id="run-steps"></ul>
|
|
95
|
+
<p class="sr-only" id="run-say" role="status"></p>
|
|
96
|
+
<p class="err" id="run-error" role="alert"></p>
|
|
97
|
+
<p><button id="run-again" title=" Try the step that failed again" hidden>Try again</button></p>
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
<section id="pane-form" hidden>
|
|
101
|
+
<form id="form">
|
|
102
|
+
|
|
103
|
+
<fieldset>
|
|
104
|
+
<legend>What kind of Actor?</legend>
|
|
105
|
+
<label class="choice"><input type="radio" name="kind" value="person" checked>
|
|
106
|
+
A <strong>person</strong> — an account you read and post from</label>
|
|
107
|
+
<label class="choice"><input type="radio" name="kind" value="group">
|
|
108
|
+
A <strong>group</strong> of fediverse users you manage</label>
|
|
109
|
+
</fieldset>
|
|
110
|
+
|
|
111
|
+
<fieldset>
|
|
112
|
+
<legend>Actor's Handle</legend>
|
|
113
|
+
<input type="text" id="handle" name="handle" autocomplete="off" autofocus
|
|
114
|
+
placeholder="handle part of fediverse address: @HANDLE@server"
|
|
115
|
+
aria-label="Actor's Handle" aria-describedby="handle-hint">
|
|
116
|
+
<p class="hint" id="handle-hint">The name in your fediverse address. This is permanent. Letters, digits,
|
|
117
|
+
hyphens, and underscores only.</p>
|
|
118
|
+
</fieldset>
|
|
119
|
+
|
|
120
|
+
<fieldset id="fs-pod">
|
|
121
|
+
<legend>Actor's Pod</legend>
|
|
122
|
+
<label class="choice"><input type="radio" name="mode" value="new" checked>
|
|
123
|
+
Create a Solid account and pod for the Actor</label>
|
|
124
|
+
<label class="choice"><input type="radio" name="mode" value="existing">
|
|
125
|
+
Use a pod I already have</label>
|
|
126
|
+
|
|
127
|
+
<label for="issuer">Solid identity provider</label>
|
|
128
|
+
<input type="url" id="issuer" name="issuer" value="https://solidcommunity.net" autocomplete="url">
|
|
129
|
+
|
|
130
|
+
<label for="email">Solid Account email/username</label>
|
|
131
|
+
<input type="email" id="email" name="email" autocomplete="email" placeholder="you@example.org">
|
|
132
|
+
|
|
133
|
+
<div id="row-password">
|
|
134
|
+
<label for="password">Solid Account password</label>
|
|
135
|
+
<input type="password" id="password" name="password" autocomplete="current-password"
|
|
136
|
+
aria-describedby="password-hint">
|
|
137
|
+
<p class="hint" id="password-hint">Password is sent once in return for a credential which is stored locally.
|
|
138
|
+
The password itself is never stored. You may revoke the credential without changing the
|
|
139
|
+
password.</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div id="row-podname">
|
|
143
|
+
<label for="podName">Pod name</label>
|
|
144
|
+
<input type="text" id="podName" name="podName" autocomplete="off"
|
|
145
|
+
aria-describedby="podname-hint">
|
|
146
|
+
<p class="hint" id="podname-hint">The subdomain of your pod address, it will become
|
|
147
|
+
https://YOUR-POD-NAME.your-identity-provider</p>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div id="row-pod" hidden>
|
|
151
|
+
<label for="pod">Pod address</label>
|
|
152
|
+
<input type="url" id="pod" name="pod" autocomplete="url" placeholder="https://you.solidcommunity.net/">
|
|
153
|
+
</div>
|
|
154
|
+
</fieldset>
|
|
155
|
+
|
|
156
|
+
<h2>You will be</h2>
|
|
157
|
+
<p class="address" id="preview">…</p>
|
|
158
|
+
<div id="preview-notes"></div>
|
|
159
|
+
<p>The handle and the pod cannot be changed afterwards. Everything else — display
|
|
160
|
+
name, bio, pictures — you set in the client.</p>
|
|
161
|
+
|
|
162
|
+
<p class="err" id="form-error" role="alert"></p>
|
|
163
|
+
<p><button type="submit" class="primary" id="submit" title=" Create it">Create it</button></p>
|
|
164
|
+
</form>
|
|
165
|
+
</section>
|
|
166
|
+
|
|
167
|
+
</main>
|
|
168
|
+
|
|
169
|
+
<script src="../bar.js"></script>
|
|
170
|
+
<script src="setup.js"></script>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// setup.js — the questions the CLI used to ask, asked here instead. Every
|
|
2
|
+
// request is relative, so the page behaves identically whether it was reached
|
|
3
|
+
// at localhost:<port> or at <handle>.localhost:<port>.
|
|
4
|
+
//
|
|
5
|
+
// External file, not an inline script: the CSP allows 'self' plus the hashes
|
|
6
|
+
// of Phanpy's own inline bootstrap, and nothing else.
|
|
7
|
+
|
|
8
|
+
const $ = (id) => document.getElementById(id);
|
|
9
|
+
// Empty means gone, not a blank line where a sentence used to be.
|
|
10
|
+
const strap = (t) => { const el = $('strap'); el.textContent = t || ''; el.hidden = !t; };
|
|
11
|
+
// By id, not by position: these were `body > section` until a wrapper went
|
|
12
|
+
// round them for layout, and the selector then matched nothing — every pane
|
|
13
|
+
// kept whatever it started as and the page sat on "Reading this agent's state".
|
|
14
|
+
const show = (id) => { for (const s of document.querySelectorAll('section[id^="pane-"]')) s.hidden = s.id !== id; };
|
|
15
|
+
// The door this page is served behind: nothing when the agent runs on this
|
|
16
|
+
// machine, `/app` when the identity is hosted by its own pod server. Read from
|
|
17
|
+
// the page's own address, so one build serves both.
|
|
18
|
+
const BASE = location.pathname.replace(/\/admin\/.*$/u, '');
|
|
19
|
+
const api = async (path, init) => {
|
|
20
|
+
const res = await fetch(BASE + path, init);
|
|
21
|
+
return { status: res.status, json: await res.json().catch(() => null) };
|
|
22
|
+
};
|
|
23
|
+
const postJson = (path, body) => api(path, {
|
|
24
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let state = null;
|
|
28
|
+
|
|
29
|
+
// ---- what the agent needs from us ----
|
|
30
|
+
|
|
31
|
+
async function start() {
|
|
32
|
+
const { json } = await api('/setup/state');
|
|
33
|
+
state = json || {};
|
|
34
|
+
if (state.phase === 'running' || state.phase === 'done' || state.phase === 'error') return watchRun();
|
|
35
|
+
if (state.configured) return paneConfigured();
|
|
36
|
+
return paneForm();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function paneConfigured() {
|
|
40
|
+
strap('already set up');
|
|
41
|
+
$('configured-address').textContent = state.handle ? `@${state.handle}` : '';
|
|
42
|
+
$('configured-client').hidden = state.kind === 'group';
|
|
43
|
+
show('pane-configured');
|
|
44
|
+
// The address needs the pod host, which only /config knows.
|
|
45
|
+
api('/config').then(({ json }) => {
|
|
46
|
+
if (json?.address) $('configured-address').textContent = json.address;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- the form ----
|
|
51
|
+
|
|
52
|
+
function paneForm() {
|
|
53
|
+
// Nothing is asked in a terminal any more — every question is on this page.
|
|
54
|
+
// A fresh run needs no strap at all; a resumed one has something to say.
|
|
55
|
+
strap(state.resumable ? 'finishing a setup that did not complete' : '');
|
|
56
|
+
if (state.handle) $('handle').value = state.handle;
|
|
57
|
+
// A signup page's carry-over: the form opens with its answers already in.
|
|
58
|
+
const fr = state.firstRun;
|
|
59
|
+
if (fr && !state.resumable) {
|
|
60
|
+
if (fr.kind) $('form').elements.kind.value = fr.kind;
|
|
61
|
+
if (fr.handle) $('handle').value = fr.handle;
|
|
62
|
+
if (fr.pod) { $('form').elements.mode.value = 'existing'; $('pod').value = fr.pod; }
|
|
63
|
+
if (fr.issuer) $('issuer').value = fr.issuer;
|
|
64
|
+
if (fr.gatewayHost) strap(`finishing your ${fr.gatewayHost} signup — enter your Solid account details`);
|
|
65
|
+
}
|
|
66
|
+
// Resuming: the account exists and the credential is minted. Asking for a
|
|
67
|
+
// password again would mint a second one and orphan the first, which cannot
|
|
68
|
+
// be recovered.
|
|
69
|
+
if (state.resumable) {
|
|
70
|
+
// Both already settled in the credential this run is resuming from.
|
|
71
|
+
$('fs-pod').hidden = true;
|
|
72
|
+
$('row-password').hidden = true;
|
|
73
|
+
$('submit').textContent = 'Finish setting up';
|
|
74
|
+
} else if (state.passwordSupplied) {
|
|
75
|
+
$('row-password').hidden = true; // AP_PASSWORD is set in the environment
|
|
76
|
+
}
|
|
77
|
+
show('pane-form');
|
|
78
|
+
// The markup's autofocus was set while this pane was still hidden, so it
|
|
79
|
+
// never fired — move focus to the first field now that the pane is showing.
|
|
80
|
+
$('handle').focus();
|
|
81
|
+
for (const el of $('form').elements) {
|
|
82
|
+
el.addEventListener('input', onEdit);
|
|
83
|
+
el.addEventListener('change', onEdit);
|
|
84
|
+
}
|
|
85
|
+
$('form').addEventListener('submit', onSubmit);
|
|
86
|
+
onEdit();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function answers() {
|
|
90
|
+
const f = $('form').elements;
|
|
91
|
+
const kind = f.kind.value;
|
|
92
|
+
const mode = state.resumable ? 'existing' : f.mode.value;
|
|
93
|
+
const a = {
|
|
94
|
+
kind,
|
|
95
|
+
mode,
|
|
96
|
+
handle: f.handle.value.trim(),
|
|
97
|
+
issuer: f.issuer.value.trim(),
|
|
98
|
+
email: f.email.value.trim(),
|
|
99
|
+
};
|
|
100
|
+
if (mode === 'new') a.podName = f.podName.value.trim() || a.handle;
|
|
101
|
+
else a.pod = f.pod.value.trim();
|
|
102
|
+
if (!state.resumable && !state.passwordSupplied) a.password = f.password.value;
|
|
103
|
+
return a;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Every warning the CLI printed before it asked "create pod and fediverse
|
|
107
|
+
// account?", in the same words, as you type.
|
|
108
|
+
const NOTES = {
|
|
109
|
+
'pod-is-a-path': (a) =>
|
|
110
|
+
`This pod is a path on ${host(a.pod)}, not the root of its own host. WebFinger is `
|
|
111
|
+
+ 'answered only at a host root, which this pod cannot write to, so other servers '
|
|
112
|
+
+ 'will not find this address. Posting and reading still work; being discovered does not.',
|
|
113
|
+
};
|
|
114
|
+
const REFUSALS = {
|
|
115
|
+
'group-needs-host-root': () =>
|
|
116
|
+
'A group needs a pod at the root of its own host, or nobody could ever find it. '
|
|
117
|
+
+ 'Give the group a pod of its own, or set this up as a person.',
|
|
118
|
+
};
|
|
119
|
+
const host = (u) => { try { return new URL(u).host; } catch { return u; } };
|
|
120
|
+
|
|
121
|
+
let editTimer = null;
|
|
122
|
+
function onEdit() {
|
|
123
|
+
const mode = state.resumable ? 'existing' : $('form').elements.mode.value;
|
|
124
|
+
$('row-podname').hidden = mode !== 'new';
|
|
125
|
+
$('row-pod').hidden = mode === 'new';
|
|
126
|
+
clearTimeout(editTimer);
|
|
127
|
+
editTimer = setTimeout(preview, 150);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function preview() {
|
|
131
|
+
const a = answers();
|
|
132
|
+
$('preview-notes').textContent = '';
|
|
133
|
+
if (!a.handle || (a.mode === 'existing' && !a.pod)) { $('preview').textContent = '…'; return; }
|
|
134
|
+
if (state.resumable) { $('preview').textContent = `@${a.handle}@…`; return; }
|
|
135
|
+
const { json } = await postJson('/setup/check', a);
|
|
136
|
+
if (!json) return;
|
|
137
|
+
$('preview').textContent = json.address || '…';
|
|
138
|
+
const notes = $('preview-notes');
|
|
139
|
+
for (const w of json.warnings || []) {
|
|
140
|
+
const p = document.createElement('p');
|
|
141
|
+
p.className = 'warn';
|
|
142
|
+
p.textContent = NOTES[w] ? NOTES[w](a) : w;
|
|
143
|
+
notes.appendChild(p);
|
|
144
|
+
}
|
|
145
|
+
if (json.refusal) {
|
|
146
|
+
const p = document.createElement('p');
|
|
147
|
+
p.className = 'err';
|
|
148
|
+
p.textContent = REFUSALS[json.refusal] ? REFUSALS[json.refusal]() : json.refusal;
|
|
149
|
+
notes.appendChild(p);
|
|
150
|
+
}
|
|
151
|
+
$('submit').disabled = !!json.refusal;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function onSubmit(ev) {
|
|
155
|
+
ev.preventDefault();
|
|
156
|
+
$('form-error').textContent = '';
|
|
157
|
+
$('submit').disabled = true;
|
|
158
|
+
const { status, json } = await postJson('/setup', answers());
|
|
159
|
+
if (status !== 202) {
|
|
160
|
+
$('submit').disabled = false;
|
|
161
|
+
$('form-error').textContent = json?.error || `setup refused (HTTP ${status})`;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
watchRun();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---- the run ----
|
|
168
|
+
|
|
169
|
+
const LABELS = {
|
|
170
|
+
account: 'Creating the account and pod',
|
|
171
|
+
credential: 'Minting a credential for this machine',
|
|
172
|
+
bootstrap: 'Provisioning the pod',
|
|
173
|
+
connect: 'Connecting',
|
|
174
|
+
publish: 'Publishing the actor',
|
|
175
|
+
verify: 'Checking the world can see it',
|
|
176
|
+
};
|
|
177
|
+
const MARKS = { waiting: '·', running: '⟳', ok: '✓', skipped: '–', error: '✗' };
|
|
178
|
+
|
|
179
|
+
async function watchRun() {
|
|
180
|
+
show('pane-run');
|
|
181
|
+
strap('setting up');
|
|
182
|
+
const { json: run } = await api('/setup/progress');
|
|
183
|
+
if (!run) return;
|
|
184
|
+
renderRun(run);
|
|
185
|
+
if (run.phase === 'running') return setTimeout(watchRun, 700);
|
|
186
|
+
if (run.phase === 'done') return paneDone(run.result);
|
|
187
|
+
$('run-title').textContent = 'Setup stopped';
|
|
188
|
+
$('run-error').textContent = run.error || 'unknown error';
|
|
189
|
+
$('run-again').hidden = false;
|
|
190
|
+
$('run-again').onclick = async () => {
|
|
191
|
+
// Re-read the state: a run that got as far as the credential resumes from
|
|
192
|
+
// there rather than minting a second one.
|
|
193
|
+
const { json } = await api('/setup/state');
|
|
194
|
+
state = json || state;
|
|
195
|
+
paneForm();
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function renderRun(run) {
|
|
200
|
+
const ul = $('run-steps');
|
|
201
|
+
ul.textContent = '';
|
|
202
|
+
for (const s of run.steps) {
|
|
203
|
+
const li = document.createElement('li');
|
|
204
|
+
const mark = document.createElement('span');
|
|
205
|
+
mark.className = 'state';
|
|
206
|
+
mark.textContent = MARKS[s.state] || '·';
|
|
207
|
+
// The glyph is the whole of what says how a step went, and `⟳` read aloud
|
|
208
|
+
// is not a word. Naming it leaves the visual result exactly as it was.
|
|
209
|
+
mark.setAttribute('aria-label', s.state || 'waiting');
|
|
210
|
+
li.appendChild(mark);
|
|
211
|
+
li.appendChild(document.createTextNode(LABELS[s.key] || s.key));
|
|
212
|
+
if (s.note) {
|
|
213
|
+
const note = document.createElement('div');
|
|
214
|
+
note.className = 'note';
|
|
215
|
+
note.textContent = s.note;
|
|
216
|
+
li.appendChild(note);
|
|
217
|
+
}
|
|
218
|
+
ul.appendChild(li);
|
|
219
|
+
}
|
|
220
|
+
// One polite line rather than a live region over the whole list: the list is
|
|
221
|
+
// rebuilt from scratch on every poll, and announcing all six steps twice a
|
|
222
|
+
// second is not progress, it is noise. Only the step actually moving is said.
|
|
223
|
+
const now = run.steps.find(s => s.state === 'running')
|
|
224
|
+
|| [...run.steps].reverse().find(s => s.state && s.state !== 'waiting');
|
|
225
|
+
const say = now ? `${LABELS[now.key] || now.key}: ${now.state}` : '';
|
|
226
|
+
const line = $('run-say');
|
|
227
|
+
if (line && line.textContent !== say) line.textContent = say;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function paneDone(result) {
|
|
231
|
+
// Straight to the thing you set it up FOR. A group has no client of its own,
|
|
232
|
+
// so it goes to its console instead. Warnings are the exception: an address
|
|
233
|
+
// nobody can resolve, or documents a stranger cannot read, are worth stopping
|
|
234
|
+
// for — the client would just look empty and not say why.
|
|
235
|
+
const stop = !result?.resolvable || result?.unreachable?.length;
|
|
236
|
+
if (!stop) {
|
|
237
|
+
location.href = result?.kind === 'group' ? '/admin/' : '/admin/client/';
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
strap('set up');
|
|
241
|
+
show('pane-done');
|
|
242
|
+
$('done-address').textContent = result?.address || `@${result?.handle} — no resolvable address`;
|
|
243
|
+
const notes = [];
|
|
244
|
+
if (!result?.resolvable) {
|
|
245
|
+
notes.push(`${result?.pod} is not the root of its own host, so this address cannot be `
|
|
246
|
+
+ 'discovered by other servers. Posting and reading still work.');
|
|
247
|
+
}
|
|
248
|
+
if (result?.unreachable?.length) {
|
|
249
|
+
notes.push(`Not readable without credentials yet: ${result.unreachable.join(', ')}. `
|
|
250
|
+
+ 'Other servers cannot fetch this actor until that clears.');
|
|
251
|
+
}
|
|
252
|
+
$('done-note').className = notes.length ? 'warn' : '';
|
|
253
|
+
$('done-note').textContent = notes.join(' ');
|
|
254
|
+
|
|
255
|
+
const links = $('done-links');
|
|
256
|
+
links.textContent = '';
|
|
257
|
+
const add = (href, text) => {
|
|
258
|
+
if (links.hasChildNodes()) links.appendChild(document.createTextNode(' · '));
|
|
259
|
+
const a = document.createElement('a');
|
|
260
|
+
a.href = href;
|
|
261
|
+
a.textContent = text;
|
|
262
|
+
links.appendChild(a);
|
|
263
|
+
};
|
|
264
|
+
if (result?.kind !== 'group') add('/', 'Open the client');
|
|
265
|
+
add('/admin/', 'Edit the record');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
start();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* window.css — the floating panel every page of ours puts a form in.
|
|
2
|
+
Shared like bar.css, so the three surfaces cannot drift apart. */
|
|
3
|
+
|
|
4
|
+
/* Fixed, so it floats over the page rather than pushing it about — which is
|
|
5
|
+
what the accordions did, moving the line you were reading every time one
|
|
6
|
+
opened. Resizing is the browser's own grip: `resize` needs `overflow` set to
|
|
7
|
+
anything but visible, which is also what keeps a long form inside the frame. */
|
|
8
|
+
#win {
|
|
9
|
+
position: fixed; z-index: 40;
|
|
10
|
+
top: 5rem; left: 50%; transform: translateX(-50%);
|
|
11
|
+
width: min(46rem, calc(100vw - 2rem));
|
|
12
|
+
/* Tall enough for what is in it and no taller: a fixed height made a short
|
|
13
|
+
panel roomy and a long one scroll for no reason. `auto` up to a cap, so it
|
|
14
|
+
only ever scrolls when the content genuinely will not fit the screen — and
|
|
15
|
+
the moment you drag the corner, that inline height wins over both. */
|
|
16
|
+
height: auto;
|
|
17
|
+
min-width: 18rem; min-height: 8rem;
|
|
18
|
+
max-width: 100vw; max-height: calc(100vh - 7rem);
|
|
19
|
+
resize: both; overflow: hidden;
|
|
20
|
+
display: flex; flex-direction: column;
|
|
21
|
+
/* Raised, like the bar: it sits ON the page, so it takes the same colour the
|
|
22
|
+
bar does rather than the page's own. The shadow lifts it; the colour says
|
|
23
|
+
which layer it belongs to. */
|
|
24
|
+
background: var(--raised); color: CanvasText;
|
|
25
|
+
border: 1px solid var(--btn-edge); border-radius: .5rem;
|
|
26
|
+
box-shadow: 0 .6rem 2rem #0004;
|
|
27
|
+
}
|
|
28
|
+
/* Dragged by the bar, so the grab area is obvious and a text selection inside
|
|
29
|
+
the form never moves the window. */
|
|
30
|
+
/* Its own title bar keeps a rule: this one IS two surfaces meeting inside a
|
|
31
|
+
single small box, where a tone change alone is too quiet to read as a grab
|
|
32
|
+
handle. Different problem from the page bar, different answer. */
|
|
33
|
+
#win-bar {
|
|
34
|
+
flex: 0 0 auto; display: flex; align-items: baseline; gap: .6rem;
|
|
35
|
+
padding: .45rem .7rem; cursor: move; user-select: none;
|
|
36
|
+
border-bottom: 1px solid var(--btn-edge); border-radius: .5rem .5rem 0 0;
|
|
37
|
+
background: var(--surface);
|
|
38
|
+
}
|
|
39
|
+
#win-title { flex: 1 1 auto; font-weight: 600; color: var(--heading); }
|
|
40
|
+
/* 24x24 is the 2.5.8 minimum; at .1rem of vertical padding this came out
|
|
41
|
+
21.6px tall, which is a small target for the one control that dismisses the
|
|
42
|
+
thing covering your page. Same glyph, same place, bigger hit area. */
|
|
43
|
+
#win-close {
|
|
44
|
+
font: inherit; line-height: 1; cursor: pointer;
|
|
45
|
+
min-width: 24px; min-height: 24px; padding: .2rem .5rem;
|
|
46
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
47
|
+
border: 0; background: none; color: var(--link);
|
|
48
|
+
}
|
|
49
|
+
/* The window scrolls, never the page — same rule the rest of the app follows. */
|
|
50
|
+
#win-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: .2rem 1rem 1rem; }
|
|
51
|
+
#win-body > * { margin-top: .6rem; }
|
|
52
|
+
#win-body pre { overflow-x: auto; background: #0001; padding: .6rem; border-radius: .3rem;
|
|
53
|
+
font-size: .9rem; }
|
|
54
|
+
/* Dragging with the pointer over the frame would otherwise select text inside it. */
|
|
55
|
+
body.dragging { user-select: none; }
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// window.js — the floating panel, shared by every page of ours.
|
|
2
|
+
//
|
|
3
|
+
// It replaces the accordions. Those opened at the foot of the page and pushed
|
|
4
|
+
// whatever you were reading out from under you; worse, only one could be open,
|
|
5
|
+
// so looking at the log meant closing the form you were filling in. A window
|
|
6
|
+
// floats over the page instead, and you can move it off whatever it covers.
|
|
7
|
+
//
|
|
8
|
+
// Resizing is the browser's own grip (`resize: both`), so there is no drag
|
|
9
|
+
// maths here beyond moving it. Nothing is created in script: every panel is
|
|
10
|
+
// declared in the page's markup inside #win-body, and this only shows one.
|
|
11
|
+
|
|
12
|
+
(() => {
|
|
13
|
+
const win = document.getElementById('win');
|
|
14
|
+
if (!win) return;
|
|
15
|
+
const bar = document.getElementById('win-bar');
|
|
16
|
+
const title = document.getElementById('win-title');
|
|
17
|
+
const body = document.getElementById('win-body');
|
|
18
|
+
|
|
19
|
+
// Every direct child of the body is a panel; showing one hides the rest.
|
|
20
|
+
const panels = () => [...body.children];
|
|
21
|
+
let open = null;
|
|
22
|
+
// Where focus was when the window opened, so closing can put it back. Without
|
|
23
|
+
// this, Escape from inside the panel drops focus on <body> and the tab order
|
|
24
|
+
// restarts from the top of the page.
|
|
25
|
+
let returnTo = null;
|
|
26
|
+
|
|
27
|
+
// It IS a dialog, so it says so — and a dialog that opens without taking
|
|
28
|
+
// focus leaves a keyboard user tabbing forward through the rest of the page
|
|
29
|
+
// to find it, and a screen-reader user with no announcement that anything
|
|
30
|
+
// happened at all. It also sits over the control that opened it, so moving
|
|
31
|
+
// focus in is what stops that control being focused-but-obscured.
|
|
32
|
+
win.setAttribute('role', 'dialog');
|
|
33
|
+
win.setAttribute('aria-modal', 'false'); // the page behind stays usable
|
|
34
|
+
win.setAttribute('aria-labelledby', 'win-title');
|
|
35
|
+
|
|
36
|
+
// The first focusable that is actually SHOWING. A plain querySelector matches
|
|
37
|
+
// elements inside hidden sub-blocks (a confirm panel has one warning block per
|
|
38
|
+
// action, all but one hidden), and .focus() on a display:none element is a
|
|
39
|
+
// silent no-op — so a dialog whose first match is hidden would open with focus
|
|
40
|
+
// still outside it. offsetParent is null for anything display:none.
|
|
41
|
+
const focusFirst = () => {
|
|
42
|
+
const panel = panels().find(p => !p.hidden);
|
|
43
|
+
if (!panel) { document.getElementById('win-close')?.focus(); return; }
|
|
44
|
+
const candidates = panel.querySelectorAll(
|
|
45
|
+
'input:not([type=hidden]), select, textarea, button, [href], [tabindex]:not([tabindex="-1"])',
|
|
46
|
+
);
|
|
47
|
+
// Prefer a non-committing control: never open a destructive dialog with a
|
|
48
|
+
// submit/confirm button pre-focused where a stray Enter would fire it.
|
|
49
|
+
let firstVisible = null;
|
|
50
|
+
for (const el of candidates) {
|
|
51
|
+
if (el.offsetParent === null && el !== document.body) continue; // hidden
|
|
52
|
+
if (!firstVisible) firstVisible = el;
|
|
53
|
+
const commits = el.type === 'submit' || /\bconfirm-go\b/.test(el.id);
|
|
54
|
+
if (!commits) { el.focus(); return; }
|
|
55
|
+
}
|
|
56
|
+
(firstVisible || document.getElementById('win-close'))?.focus();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const close = () => {
|
|
60
|
+
win.hidden = true;
|
|
61
|
+
for (const p of panels()) p.hidden = true;
|
|
62
|
+
const was = open;
|
|
63
|
+
open = null;
|
|
64
|
+
// Only if focus is still ours to move: the user may have clicked away.
|
|
65
|
+
const back = returnTo;
|
|
66
|
+
returnTo = null;
|
|
67
|
+
if (back && document.contains(back) && win.contains(document.activeElement)) back.focus();
|
|
68
|
+
else if (back && document.contains(back) && document.activeElement === document.body) back.focus();
|
|
69
|
+
win.dispatchEvent(new CustomEvent('win:closed', { detail: { was } }));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Returns false when it CLOSED an already-open panel, so a button can toggle
|
|
73
|
+
// itself without every caller repeating the check.
|
|
74
|
+
const show = (id, label) => {
|
|
75
|
+
if (open === id && !win.hidden) { close(); return false; }
|
|
76
|
+
const opener = document.activeElement;
|
|
77
|
+
for (const p of panels()) p.hidden = p.id !== id;
|
|
78
|
+
title.textContent = label || '';
|
|
79
|
+
win.hidden = false;
|
|
80
|
+
open = id;
|
|
81
|
+
clampIntoView();
|
|
82
|
+
if (opener && opener !== document.body && !win.contains(opener)) returnTo = opener;
|
|
83
|
+
focusFirst();
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// A window remembers where it was left, so it does not jump back to the
|
|
88
|
+
// middle every time — but only within the page, because a position saved on a
|
|
89
|
+
// wide screen is off-screen on a narrow one and there is nothing to grab.
|
|
90
|
+
let placed = false;
|
|
91
|
+
function clampIntoView() {
|
|
92
|
+
if (!placed) return;
|
|
93
|
+
const r = win.getBoundingClientRect();
|
|
94
|
+
const x = Math.min(Math.max(r.left, 0), Math.max(0, innerWidth - r.width));
|
|
95
|
+
const y = Math.min(Math.max(r.top, 0), Math.max(0, innerHeight - r.height));
|
|
96
|
+
moveTo(x, y);
|
|
97
|
+
}
|
|
98
|
+
function moveTo(x, y) {
|
|
99
|
+
win.style.transform = 'none'; // the centring transform, undone once moved
|
|
100
|
+
win.style.left = `${x}px`;
|
|
101
|
+
win.style.top = `${y}px`;
|
|
102
|
+
placed = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let drag = null;
|
|
106
|
+
bar.addEventListener('pointerdown', (e) => {
|
|
107
|
+
if (e.target.closest('button')) return; // the close button is not a handle
|
|
108
|
+
const r = win.getBoundingClientRect();
|
|
109
|
+
drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };
|
|
110
|
+
bar.setPointerCapture(e.pointerId);
|
|
111
|
+
document.body.classList.add('dragging');
|
|
112
|
+
});
|
|
113
|
+
bar.addEventListener('pointermove', (e) => {
|
|
114
|
+
if (!drag) return;
|
|
115
|
+
const r = win.getBoundingClientRect();
|
|
116
|
+
moveTo(Math.min(Math.max(e.clientX - drag.dx, 0), Math.max(0, innerWidth - r.width)),
|
|
117
|
+
Math.min(Math.max(e.clientY - drag.dy, 0), Math.max(0, innerHeight - r.height)));
|
|
118
|
+
});
|
|
119
|
+
const endDrag = (e) => {
|
|
120
|
+
if (!drag) return;
|
|
121
|
+
drag = null;
|
|
122
|
+
try { bar.releasePointerCapture(e.pointerId); } catch { /* already released */ }
|
|
123
|
+
document.body.classList.remove('dragging');
|
|
124
|
+
};
|
|
125
|
+
bar.addEventListener('pointerup', endDrag);
|
|
126
|
+
bar.addEventListener('pointercancel', endDrag);
|
|
127
|
+
addEventListener('resize', clampIntoView);
|
|
128
|
+
|
|
129
|
+
document.getElementById('win-close').addEventListener('click', close);
|
|
130
|
+
// Escape closes it, which is what every floating thing does and what people
|
|
131
|
+
// try first when a window is covering the thing they wanted to read.
|
|
132
|
+
addEventListener('keydown', (e) => { if (e.key === 'Escape' && !win.hidden) close(); });
|
|
133
|
+
|
|
134
|
+
// The whole API: open a declared panel by id, close, and ask what is open.
|
|
135
|
+
window.solWindow = { show, close, openId: () => (win.hidden ? null : open) };
|
|
136
|
+
})();
|