From 47532766ab665dc6142aba1ff6fa72336bdbf858 Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 05:04:17 +0500 Subject: [PATCH 01/11] feat(site): a landing page for Hyperpoint A static page in site/, designed with elayadesign's landing-page-design and design-to-motion skills. The hero shows the real menu bar menu, four cards act out the reconnect, the hyper key, the speed curve and Add Rule, and the proof is the owner's config. Includes a 404, a privacy page, an og.png drawn by og.py, and a launch config to preview it. Co-Authored-By: Claude Opus 5.5 --- .claude/launch.json | 11 + site/404.html | 53 ++ site/README.md | 78 +++ site/apple-touch-icon.png | Bin 0 -> 4794 bytes site/assets/hyperpoint-app-icon.svg | 1 + .../hyperpoint-horizontal-color-on-dark.svg | 1 + site/favicon.ico | Bin 0 -> 2070 bytes site/favicon.svg | 1 + site/icon-192.png | Bin 0 -> 5162 bytes site/icon-512.png | Bin 0 -> 14749 bytes site/index.html | 570 ++++++++++++++++ site/main.js | 307 +++++++++ site/maskable-512.png | Bin 0 -> 12406 bytes site/og.png | Bin 0 -> 65614 bytes site/og.py | 82 +++ site/privacy.html | 55 ++ site/site.webmanifest | 24 + site/styles.css | 639 ++++++++++++++++++ 18 files changed, 1822 insertions(+) create mode 100644 .claude/launch.json create mode 100644 site/404.html create mode 100644 site/README.md create mode 100644 site/apple-touch-icon.png create mode 100644 site/assets/hyperpoint-app-icon.svg create mode 100644 site/assets/hyperpoint-horizontal-color-on-dark.svg create mode 100644 site/favicon.ico create mode 100644 site/favicon.svg create mode 100644 site/icon-192.png create mode 100644 site/icon-512.png create mode 100644 site/index.html create mode 100644 site/main.js create mode 100644 site/maskable-512.png create mode 100644 site/og.png create mode 100644 site/og.py create mode 100644 site/privacy.html create mode 100644 site/site.webmanifest create mode 100644 site/styles.css diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..acd57e9 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "site", + "runtimeExecutable": "python3", + "runtimeArgs": ["-m", "http.server", "4319", "--directory", "site"], + "port": 4319 + } + ] +} diff --git a/site/404.html b/site/404.html new file mode 100644 index 0000000..7a0b3a8 --- /dev/null +++ b/site/404.html @@ -0,0 +1,53 @@ + + + + + +Page not found · Hyperpoint + + + + + + + + + + + + + + +
+
+
+
+ +

404

+

This page is not here

+

The link may be old, or the address has a typo. Everything about Hyperpoint is on the home page.

+ Back to Hyperpoint +
+
+
+
+
+
+ + © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech and MX Master are trademarks of their owners. +
+
+ + diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..9fb64d7 --- /dev/null +++ b/site/README.md @@ -0,0 +1,78 @@ +# Hyperpoint website + +A static landing page: plain HTML, CSS and JavaScript with no build step. It was designed with the +[`landing-page-design`](https://github.com/elayadesign/ai-design-skills/tree/main/skills/landing-page-design) and +[`design-to-motion`](https://github.com/elayadesign/ai-design-skills/tree/main/skills/design-to-motion) skills. + +```bash +python3 -m http.server 4319 --directory site +``` + +Serve `site/` from the root of a domain. `404.html` and `privacy.html` use root paths (`/styles.css`), so they work +at any depth. Once the site has a domain, make `og:image` in `index.html` an absolute URL. The download buttons link +to the latest GitHub release, which only people with access to the repository can open while it is private. + +| File | What it is | +|---|---| +| `index.html` | The page. The icon sprite (Phosphor, MIT), the config excerpt and the FAQ schema are written inline | +| `styles.css` | Tokens and every section's styles | +| `main.js` | Island nav, scroll reveals, card intros and stories, the tagline reveal | +| `404.html`, `privacy.html` | The branded 404 and the privacy page | +| `og.png` | Social preview, drawn by `og.py` from the logo kit and Geist | +| `favicon.*`, `icon-*.png`, `site.webmanifest`, `assets/` | Copied from `design/logo/` | + +## The brief + +**One offer, one audience, one action.** Hyperpoint for Mac users who set up a mouse and Caps Lock with BetterMouse +and Karabiner, often with an MX Master 3 that forgets its buttons on reconnect. The one action is *Download for Mac*. + +**Layout: A, classic hero plus sections.** The product reads at a glance from its menu bar menu, so the hero shows +that menu with real device rows and the reconnect note. + +**Hero.** *Your mouse setup, still there after every reconnect.* The subheadline says what it is and does, the CTA +downloads, and the proof line says it is notarized, its version and what it runs on. + +**Order.** Hero, problem to solution (before and after), tagline reveal, four benefit cards with stories, three +smaller features, how it works in three steps, proof (the owner's real config and four facts), FAQ (10 questions with +FAQ schema), final CTA with risk reversal, footer. + +**Benefits.** Settings that survive sleep. A hyper key on Caps Lock. A wheel that glides. Change a button by pressing +it. + +**Steps.** Install and allow two permissions. Set up each device. Press a button to add a rule. + +**Proof without testimonials.** There are no users to quote yet, and invented quotes would be fake. The proof is the +config file of the Mac Hyperpoint was built on, plus facts taken from the code: notarized releases, no network code, +an invalid file rejected whole, key mappings cleared on quit. + +**Risk reversal.** A fresh install changes nothing until a device is set up, and quitting puts the keys back. + +**SEO.** Index it: the offer is evergreen. Title, description, Open Graph, a `SoftwareApplication` schema and the FAQ +as `FAQPage`. + +## The visual system + +- Dark only, on purpose. Caps Green is made for dark grounds, and on this page green always means *on* or *applied*, + like the key's lit LED in the logo. +- Geist for everything, Geist Mono only for chords, numbers and the config. Sizes stay on Tailwind's type scale, + spacing on the skill's 2 to 96 px steps, radii on Tailwind's, with nested radii derived from their parent. +- No background gradients. The one gradient is the hero heading's white to grey text. +- Motion uses the skills' curves: `cubic-bezier(0.32, 0.72, 0, 1)` for reveals and the nav, the design-to-motion + set for card intros and stories. `prefers-reduced-motion` shows every section at rest, with no stories. + +## What moves + +| Section | Motion | +|---|---| +| Hero menu | The desk, menu bar and menu stay; their rows load in order | +| Cards | Rise in when 30% visible, then each visual tells its story on a loop, paused off screen | +| Settings card | The mouse disconnects, its checks clear, it reconnects, each setting is written back, the time updates | +| Hyper key card | Caps Lock goes down, the four modifiers light, T sends ⌃⌥⇧⌘T, everything lets go | +| Wheel card | A dot rides the real speed curve from a slow turn to a flick and back while the readout counts | +| Rules card | Add Rule listens, a press lands as a new rule on top, the others step down | +| Tagline | Each word lights up in reading order as the section scrolls | +| Text sections | Fade up from blur as they enter the viewport | + +The card and curve data come from the app: the menu strings from `StatusMenu.swift` and `MenuPresentation.swift`, the +rules from the MX Master 3 preset, and the curve and measured wheel bands from `SpeedCurve.swift` and +`CurveGraph.swift`. Update the page when those change. diff --git a/site/apple-touch-icon.png b/site/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d7ee7dc1438e41dfd1e1505ca6fa7570a1f995f5 GIT binary patch literal 4794 zcmchb!Scab||RU*6f6@4EJ%0KWKX4;9TABy(fnsiTj=y zM2JJfN`}KH$A|-a55+0Pal_HMw}zAZKlq@hND@cdu*O_oMWrS!t)!@EV_@L-REf5_ zdUe`o6U6yG(@Y+OGSwRuxaJBq3p~wSZtKhvA9J0o_V@SaptN^C3XhK`Q?s-ZTwo;& z47~N&x}yNqct?!!P_wtR_}PweiHR;f5dasXA)=!Bk_4I(UfW741AQ5?XlzY(R_X)1 zfZ*VtAt52Dsqi9QRW0A)@WTvtR&#++lP-sunNtNe`J|-L^OF-0MT&QquiN^DOpN;NhOMX6Oidwk3`NF;WEynBf zgX{HV8V0f+%UUNVnT1xbp;@Fcl=18Xp=?}i>?$Sml!fbNx@>TeqTi^m~TV^fp1 z;21RUYWR3-?2swL4Gi^&#DwPN$}N4InVa+R@={fa^l^9J{{1`P&cw$@`B9d{_wUhZ zf?ga8L+uDzF0QX_p1j6{osQ{zWG*h%C!X~7`;t;^APzAIP(((c)w19QTc*NR2DhOgXS)1`LA z4l{P%oeG+nxs-oP5DG-2#XX=|*XJyDJDy>F86VJCn4TX08>bgutexY_%K|Ci$FilW z14}xFsgzy+My#*b!;~TP;RUbua2tyQ#@Ei?y1D zhO)|Uqi?6$TMq}5OThdg*lBT>6&PxkB@jIoKm$sD(dj&@;Hjf-Y7OU zHXtx??HA_l+hQa#fu4_o%ZBXv^Y*JN2P!Hm4R``e6VmWkzh&)gHTjPgk&qjkc-=C7 zUS3TN4L+f2>)FXP)eLqvHfdq^r|=3Y?z@tzsy9Wt4Jg!GYin9sTKY$ipirnQlp|2Q z)}WG8eYYT=?Zbx;pVem29~07rnJjcS2S!GW8otvc54QOnn3yb;?E7VAW=3Ipn55R? z;^Gb;y28H&TSJ}4a4u(En>zKSTH1D}$2CSw67H3?wf1K@!M9gl z_V$MEN0&Llck{q08ZJ5r#edn+AtU7-IWjT=tlFN9;;nW3TmDqK zZrfx?VkPpdw2a)JK2O_6N~+t zlf%T^k)csSm3KWCT~?+V7)YAXkG9q`^z&G3G+6oo0fQr>qn8>SxB6Lxx&vKBUn(nK z23Yv{aoRKY=OH#DV`4DnBt)UQx_?z4C;MKXy;Fb3d50SJ zAH9d(nyV)7vKE$<+}`hqxEXSgW)>IqoT@UFGBtryLhg3vsSsPQm!k{JK7xbv!e z)NRhU;D0iCzqB7}Vkl@E2dAg0D`-gA*F`gK-#CLVaCSFO43w@t<$gKh;1AEj@JK#;C@t7t4iE-qeM|DD=A$}3-%st z^xzA_YbYp;2V1ngcHqEbfVi^mbeb78oJ-X=%-7Ei9M#Or5_ zEw`PUKWYJlKH2DHeqJK<;^xuw=g%wQ*38A9Q%ud8$QF4IIS2>{>gwuV>w=DAM5f;h zDxdMwR@5D}W@ig+2_Au@-C!U!u9%2biBbqz+ASQu2PdqL}3LxVXNapP#pE zPyo@}e}B*V8xxcBkeFCeQL%>MZd>1MePe@)f--Ns=&NE}S*dQ>XB7}zXvV$M=!L<~ zn3#vC3!~b)y7H>3E`KvrX{i$b3gnR6L~w58-RbsBRdI1~U|^tt0CKJ>1&iS8wyfpj z`@Xxk2R8Kb^lWacwouy&ce6WeV52^oX;zg!1Y>4rU-HDpL`8k^9Iw`iRVk{o>W}XX zn2%G;1r_R+ft37zkKfaPr>3Uv%p!}{f0$C0jxkCzp99g$!$U*^uAL)ud~&ks`U430 zmbK&kwj(5u7nzut4iBY*Z)#utGJir8s-dB=y;D&uSXpKIl!j={?ZJD|(i{(`rAM)P zfHI||rtU3YUoR|_7Z!>H1<8o8_!$@(sTG0Wgi9*=W}^!VQohCxC5OD$SjT7Uk~8!%}ORp>tlJR@DhfPZ#%bupTlSg@*$ z6qAAO#{L?UJ?H38Yia^+IzdHX@5RNff@tH%67tH;+Wd9+@dHH5MIWkx@x~uE`@B%$i=zZL|UtS0~uH??ZYubtw6^<^9l!Oj%d}Ji@ z<;%iR-)0kM6GoY)0~+ENR_vQQ_d7aPyuBL%Blls^iu}c^V=AnXGc{6+Nu26r%s17o zt$gB5dIs+d`BUY(=6aN?qSo>YngmRmbvzDeX=AdoQYTt&e?zAu(E|f^ z$8U-KiDM~1tDU~JGxfIrzFb;KGYdhXDm>H|7nR*Usy0?uxV<67IV@J@=5kSvM4>;N z8s)lV{Z9}m@*fKeM0jqswaMBJe?xnMrMP%NgfW=>fEWE+{mimK?voJSgnm*I66zRy z8JU%^H}bi`bH@-K(8q)^hDzm?=6btCKOG(OpgSRDju<*)fE8tAy(ic_@=eJ}NtfFZ z33fuwav{*^=JA(!mbJOLxvtBt4Qz>E)~+1XTn;gD^gxSBo&+_&jiZC}BsM^|4Cd|S zMd3qZV?<4iS!j?K5J(@h;Ry-V1V61HAM}x=d6ubJC(KGD>ADJf{rO&^5E*SuI3hSD zFE1}PHU^1AUXaBI5n~4KZV5S)VCA^oIP+^dWiaip(ilh7=4Ptw-Qn0%{nowt<2H{i z!BsK-j#5}T5Jq!z<3wcd6cl;VKfz%zhh`&sX6DhcPGa-`(pc};_4uDZ8HD8fB{Q@N zK0ZE1My*OoR~K%NXKG7v@u?T1m54D|_`{XmzeDL3H zLT;@97TRaG4ObHr6KiXZ;MmWpsTM}dk(izWWRh2>gw|gr6Vj_SwfaAiFM3CEe1N`;8=c0Pw&}axi&4pOONZM%J(A=l`?(nJ`lC(Sk(TE?$a8ly!7{>vwl=&X;6L#ryCD`z9&RWUYQWkZFA*R{b753d6R-uBH+EE1$%bEsb7W;DC50gn zdPF*2es+e{XqH5ibBeJ6zb@L_&W=Z1T!<6DtgH-UU20*7El?f_3fi&L^R3KYd(X=A z?AcZCgK32>d!U72VJ*_Qj^#BdRBSAXw#ttxEX2k4qH%khLGxu7-QJB+2&)QLa`l@% zKG3(|`FtIoJ}n35m2csH$bD|wQ$Bq%Cq3wieM0-tj*|>I!U)2vo!OTlSZ5A8GPSh64cy1X+- z7<&+VdUggVvTBk2#s*D#qnQMwmfZV)28K@r%PjeVucojfwxg7gItIH=Xy58#^Xx2( zfPwkRrlt!5AvJqu0_e@fjX?-2?N1maWnp14Q+z7Fk9KlRqMw60nnFws1E@)Lb%Fsu z5s8WR$x~;mPnV!8!~~6-IR@aAXU~jSBkOA~BQ-ivlGFHW>BA*CL2Q%{^7GkFPfrKW zgRZ@wUusR@tX+hMKcwsHRf1luR~~kW9+E1>BzMPxBQN7eC{~ z9~>D$-@XDbO{6eQLZFPJ?Fg^FzP|OgyK@g`UQNw(1V}gB*@g`GRYxfk4GnsHn(CUG|0S$% zLMA}KFbL1k$Y^$U1y(Ssou^~cZd8gXv!s+vlVBwR(q$gOjz$Z`)m_bL!542uTY1Qc zLV+xQHyperpoint diff --git a/site/assets/hyperpoint-horizontal-color-on-dark.svg b/site/assets/hyperpoint-horizontal-color-on-dark.svg new file mode 100644 index 0000000..ccbd4a8 --- /dev/null +++ b/site/assets/hyperpoint-horizontal-color-on-dark.svg @@ -0,0 +1 @@ +Hyperpoint diff --git a/site/favicon.ico b/site/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..45d7f5a9b73061399a346eb5e0f93a10e381b536 GIT binary patch literal 2070 zcmaKsdpy(oAIHCpx#jGVJ88L0k#NebTq46Pq=T5-CQ>5PPPQ1ShD{r}uZRxeQKwug zmxQr4k`B5!NVF|cnp`ILa@INj{2q_rd7Q`R@%X$Ruh0Aac>nQyyxt!GkODve6beWb z0YpjxfE54$2*f}Bwln|~OWw@Q|M4$D0N^460Q4hXZpw;Uijr1kw7bhuiAp8_PABA^#8cH(k*{K;<9vC}K1#l-Bmn(FZnt^MK>vBT`CMQ#0I*S%kS z`UD$v6JKY0ymH8?X3@4w%WFvT3Gv3k*lFz!v6+`YdZ5%kW# z6wR<2!OB^rgH-b7r_1~Mqu;kEnccb4ym#Ex!&H7*Gf6;ka7mGjsLe9F8=-Cw~ekiFnhoh}b9os+ys-yX)6hR8T~)-+)}v3!i$DO{&EvrsKZM`Ipz@)gEj%q)tqW zML+#n5!cZ3c>uoS=BvvVrihhR%?qx_Dtc%8hYL*tORqjYw7dEK)r`5m#%nU(gi zx;^@W9`3T%U3j14Bv}`)zt*kTT$&T%pEau&>~flV)w+PE8jgJP%f%)=7jKRQWjE|a z=4}++D?%og1_c_1t6LNOCk!>H-n*tC8c?Tp&372u0r8-kAzDs{j@Zr&Gvxl!oT`bU z>)yJQa@>QOdQFzwUoThjEcbPkOc)bkl>gy(#)>89V6U=AxLbOEYzh2Xb$zb6(hP^- zaXcL5zRQ)Lv7odFdQUvd82qYhw@c6SO9ygG7IeRcvL<^dJQjt66REC#GzT)WrZR!w zz^-Tt9bPM67!5??@YYZBhypw{Onb6GA>b&KqMVe3r~{d~EGBcPEyeO1sTHikIrMUd z-RaKCXXR&m&6Gg(@ZQL#v2?i5Rn*<=a@aIMP&(k9PI5bjAbun4un>0Z{(-QoT^TSfsgtT zsCbrE-4^ebeyU2 zF2=!n>z#Bxx_r6%9fjzqNS`YnR~fg|e@8vsiPdOhI~5xkm!oVR&~8a<>boPOPx|;C ziD3DAeHs=%uN<9mm~xmZIk3Fid13F{DN_69-YMHnu_NSz*7RjJ{T8hxf_xqs+N)*g zF<{8*%*kSPeGVAY!P%$|p~oxxhP2!5tLiknEu}_1YtfjSs_S$6yfkg% ziwpUr^9m1z=HpK)K_}#j;rJxpnYZc3!BUQpFchvz3BIZuMBoG?34t>FxROjC3ij$# zC%ks3rMg1vHcfI;cbplw*g2=y^F+1zj^x{mM#9>sxX;F=K^MF0n-Dp*@z!cZ26Kdn zRI{{Y{VG@t*IJe?;^_yl6dl9JJ$D$^NV2j3O9{C8l*OG+3E_$$zgbOujQMTnq$sqg zddogFi$vS4?I9I5S6LlZEt4ka9M3fhTMvI+ZQdGYaH&any#-{iX)#iuz$Pz^yd~kB zS*fL1gZ6J@xz{=>uJK=y6)>NF`jyBP4UNbU7k%_rRK66vnBjf@!#k4mJ%OyN0oqlE+&0wZGaf;x z<}ZK<;~2A-mOV}(=O!&+VW@Kyx~!b z>RMYY2S*hRvqV@HMS)UzBGajFR@gN z&ke4%1GS3D3G2jPY!drErh!6K7t5UAw>Jt$V1hG?4`|)6x8|f3r{&c+u(z74CU$Mu}=)m)gfyh_HtBy?4u0j6{UW?yT>+VXD!!* zzzP@iQ$B&nQ34%Wp(1I+kbjS3Yv(>!*msy3%S*8{6L~SC<=@?be1Am1SiF>pDbR&y zIP9dfotiL-+;+pjm+v)a&nYVGS`w>_QOnXAzJd2ds~Lp_GS6pzmooi1b{+ZUOVIyr F^k3rrus{F+ literal 0 HcmV?d00001 diff --git a/site/favicon.svg b/site/favicon.svg new file mode 100644 index 0000000..6ada008 --- /dev/null +++ b/site/favicon.svg @@ -0,0 +1 @@ +Hyperpoint \ No newline at end of file diff --git a/site/icon-192.png b/site/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..7f409679bbb2a3f8668249585118d5f5bfd03c26 GIT binary patch literal 5162 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 literal 0 HcmV?d00001 diff --git a/site/icon-512.png b/site/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..383a87a3ace7a9ce3f3b857aa351af0772b3e9ed GIT binary patch literal 14749 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 ZDOP-~N8UH&8k{w9S6)@_i>#sV{{u*BZAbtB literal 0 HcmV?d00001 diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..ae82632 --- /dev/null +++ b/site/index.html @@ -0,0 +1,570 @@ + + + + + +Hyperpoint · Mouse buttons, smooth scrolling and a hyper key for macOS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

Your mouse setup,
still there after
every reconnect

+

Hyperpoint is a macOS menu bar app that remaps mouse buttons, makes the scroll wheel glide and turns Caps Lock into a hyper key. Every setting goes back on the moment a device wakes or reconnects.

+ + Download for Mac + +

+ Signed and notarized by Apple + + Version 0.1.1 + + macOS 27 on Apple silicon +

+
+ + +
+
+ + +
+
+
+

Why it exists

+

Two apps for one desk, and a mouse that forgets

+
+
+
+

Before

+
    +
  • BetterMouse for the buttons and the wheel, Karabiner for Caps Lock. Two apps, two settings, two sets of permissions.
  • +
  • The MX Master 3 drops its button setup when it sleeps or reconnects, which can happen many times a day.
  • +
  • A typo in a settings file can quietly turn a rule off, and nothing tells you.
  • +
+
+
+

With Hyperpoint

+
    +
  • One menu bar app and one file, ~/.config/hyperpoint/config.json, for every mouse and keyboard you use.
  • +
  • On every connect Hyperpoint writes the whole setup back to the device, and the menu shows when it did.
  • +
  • A file with a mistake is rejected whole, with the path of the error, and the last good settings keep running.
  • +
+
+
+
+
+ + +
+
+

Press the button
you want to change.
Hyperpoint makes the rule
and keeps it through
every sleep and reconnect.

+
+
+ + +
+
+
+

What it does

+

Buttons, Caps Lock and the wheel, in one app

+

The mouse and Caps Lock setup you had in BetterMouse and Karabiner, made by pressing buttons instead of looking up key codes.

+
+ +
+
+
+
+
+ + MX Master 3 + Connected +
+
    +
  • Gesture buttonMission Control
  • +
  • Top button⇧⌘4
  • +
  • Thumb wheelReversed
  • +
  • Smooth scrollingOn
  • +
  • Right buttonHold to scroll
  • +
+
+

Reconnected at . Every setting written back.

+
+
+
+

Settings that survive sleep

+

When a mouse wakes or reconnects, Hyperpoint writes every setting back: the gesture and top buttons, the thumb wheel, smooth scrolling and the hyper key. The menu shows when it last did.

+
+
+ +
+
+
+
caps lock
+ holds +
+ ⌃⌥⇧⌘ +
+
Tsends⌃⌥⇧⌘T
+
+
+
+

A hyper key on Caps Lock

+

Hold Caps Lock and every key you press gets Control, Option, Shift and Command, ready for app shortcuts. Pick the keyboards it works on, or switch it off everywhere from the menu bar.

+
+
+ +
+
+
+ + + + + + slow + medium + fast + flick + 10 + 100 + 1k + 10k + 1 + 2 + 5 + 10 + 20 + 50 + 100 + + + + + + + + + +
15 steps/s: 900 pt/sWheel steps against points scrolled, log scales
+
+
+
+

A wheel that glides

+

Each wheel step becomes an eased pulse. How far it goes depends on how fast you spin, on a speed curve you drag into shape. The shaded bands are an MX wheel's real speeds, measured.

+
+
+ +
+
+
+
+

Rules

MX Master 3
+ Add Rule +
+
+
ButtonWhenDo
+ +
+
    +
  1. Gesture buttonClickMission Control
  2. +
  3. Top buttonClickShortcut ⇧⌘4
  4. +
  5. Thumb wheelScrollHorizontal scroll, reversed
  6. +
+
+
+
+

Change a button by pressing it

+

Click Add Rule and press the button, key or thumb wheel. Hyperpoint makes the rule for that control, then you pick what it does: Mission Control, App Exposé, a Space, any shortcut or a held modifier.

+
+
+
+ +
    +
  • + +

    Hold to scroll

    +

    Hold a button and move the mouse to scroll, with momentum. The MX Master 3 preset puts it on the right button, and a quick press still clicks.

    +
  • +
  • + +

    Keys from any mouse

    +

    Keys a mouse sends, like Escape and Return on a 2.4G mouse, can become modifiers or shortcuts. A modifier is mapped below apps, so it holds in password fields too.

    +
  • +
  • + +

    Pause from the menu bar

    +

    See each device's state, apply settings again, pause everything or open the config file, all from one menu.

    +
  • +
+
+
+ + +
+
+
+

How it works

+

From download to done in three steps

+
+
    +
  1. + +

    Install and allow two permissions

    +

    Drag Hyperpoint into Applications and open it. Turn on Accessibility and Input Monitoring in System Settings. Until you do, the menu says Needs attention and nothing changes.

    +
  2. +
  3. + +

    Set up each device

    +

    Every keyboard and mouse you plug in shows up under Devices. Start it from a preset for its model, copy another device's settings or start empty. Click Ignore and Hyperpoint leaves it alone.

    +
  4. +
  5. + +

    Press a button to add a rule

    +

    Click Add Rule, press the button on your mouse and choose what it does. From then on, Hyperpoint keeps it in place through every sleep, wake and reconnect.

    +
  6. +
+
+
+ + +
+
+
+
+

Proof

+

One Mac, four devices, one file

+

This is the config from the Mac Hyperpoint was built on. Two mice and two keyboards, each set up from a preset, replacing a BetterMouse and Karabiner setup. The settings window edits this file and nothing else, so every setting can be read, diffed and backed up.

+
+
    +
  • Notarized by AppleReleases are signed with Developer ID and notarized, so the download opens without a warning.
  • +
  • No network codeThe app reads your devices and one file. It never connects to the internet.
  • +
  • Mistakes are caughtAn invalid file is rejected whole with the path of the error. The last good settings keep running.
  • +
  • Quit, and it is goneQuitting clears the key mappings Hyperpoint made, so Caps Lock types as Caps Lock again.
  • +
+
+
+
~/.config/hyperpoint/config.jsonExcerpt, scrolling settings left out
+
{
+  "version": 1,
+  "hyper": { "key": "capsLock", "onTap": "nothing" },
+  "devices": [
+    {
+      "match": { "vendor": "0x046D", "product": "0xB023" },
+      "name": "MX Master 3",
+      "rules": [
+        { "control": { "hidpp": "0xC3" }, "when": "click", "do": "missionControl" },
+        { "control": { "hidpp": "0xC3" }, "when": "holdMoveDown", "do": "appExpose" },
+        { "control": { "hidpp": "0xC4" }, "when": "click", "do": { "shortcut": "shift+cmd+4" } },
+        { "control": "thumbWheel", "when": "scroll", "do": "reverse" },
+        { "control": { "button": 1 }, "when": "holdMove", "do": "scroll" }
+      ]
+    },
+    {
+      "match": { "vendor": "0x1EA7", "product": "0x0002" },
+      "name": "2.4G Mouse",
+      "kind": "mouse",
+      "rules": [
+        { "control": { "key": "escape" }, "when": "hold", "do": { "modifier": "rightOption" } },
+        { "control": { "key": "return" }, "when": "click", "do": { "shortcut": "opt+shift+s" } }
+      ]
+    },
+    { "match": { "builtIn": true }, "name": "Built-in Keyboard", "hyper": true },
+    { "match": { "vendor": "0x004C", "product": "0x0321" }, "name": "Magic Keyboard", "hyper": true }
+  ],
+  "ignored": []
+}
+
+
+
+ + +
+
+
+

FAQ

+

Questions before you download

+

The short answers. The README on GitHub has the long ones.

+
+
+
+ Which Macs does Hyperpoint run on? +

macOS 27 on Apple silicon. Download the DMG from the latest release, open it and drag Hyperpoint into Applications.

+
+
+ Does it work with mice other than the MX Master 3? +

Yes. Every keyboard and mouse Hyperpoint sees shows up under Devices. Logitech mice that speak HID++ get their gesture and top buttons and the thumb wheel. Other mice get their plain buttons and any keys they send. Trackpads stay as macOS has them.

+
+
+ My MX Master 3 forgets its buttons after sleep. Does this fix it? +

That is the problem Hyperpoint was built for. On every wake and reconnect it writes the full setup back to the mouse, and the menu shows when the mouse reconnected and that its settings were applied again.

+
+
+ Do I still need BetterMouse or Karabiner? +

Not for what Hyperpoint covers: button rules and gestures, smooth scrolling, hold to scroll, the hyper key and remapping keys a mouse sends. Karabiner can also rewrite any key on any keyboard with complex rules. If you rely on those, keep it for them.

+
+
+ Why does it need Accessibility and Input Monitoring? +

Accessibility lets the hyper key and smooth scrolling change keys and scrolls as they happen. Input Monitoring lets Hyperpoint set up the buttons on Logitech mice. Until both are on, the menu says Needs attention and nothing is applied.

+
+
+ Does Hyperpoint collect any data? +

No. The app has no network code at all. Your settings live in one file on your Mac, and the log of what it did stays on your Mac too.

+
+
+ Where are my settings stored? +

In ~/.config/hyperpoint/config.json. The settings window edits that file and nothing else, so you can edit it by hand too. A file with an error is rejected whole, with the path of the mistake, and the last good settings keep running.

+
+
+ What happens when I quit? +

Hyperpoint clears the key mappings it made, so Caps Lock and your mouse keys go back to how macOS had them. To stop it for a while without quitting, choose Pause Hyperpoint from the menu.

+
+
+ How do I uninstall it? +

Choose Quit Hyperpoint from the menu and drag the app to the Trash. To remove your settings as well, delete the ~/.config/hyperpoint folder.

+
+
+ macOS says the app cannot be opened. What now? +

Version 0.1.0 came out before releases were notarized. Download 0.1.1 or later, or allow 0.1.0 under System Settings › Privacy & Security › Open Anyway.

+
+
+
+
+ + +
+
+
+
+

Set it up once.
Keep it through
every reconnect.

+

A fresh install changes nothing until you set up a device, and quitting puts your keys back. Try it on the mouse you already have.

+ + Download for Mac + +

+ Signed and notarized by Apple + + Version 0.1.1 + + macOS 27 on Apple silicon +

+
+ The Hyperpoint app icon: the Caps Lock arrow tipped over into a pointer, above a green bar +
+
+
+ +
+ + + + + + diff --git a/site/main.js b/site/main.js new file mode 100644 index 0000000..6e12b31 --- /dev/null +++ b/site/main.js @@ -0,0 +1,307 @@ +// Motion for the landing page. Everything here is progressive: without this file every section is +// visible and still. Reveals and intros use IntersectionObserver; the card stories share one +// requestAnimationFrame clock that stops whenever the cards are off screen. +(() => { + const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; + const root = document.documentElement; + window.hpReady = true; + + /* Island nav and its sheet ------------------------------------------------ */ + + const burger = document.querySelector(".burger"); + const sheet = document.getElementById("sheet"); + sheet.inert = true; + + function setSheet(open) { + sheet.classList.toggle("open", open); + sheet.inert = !open; + sheet.setAttribute("aria-hidden", String(!open)); + burger.setAttribute("aria-expanded", String(open)); + burger.setAttribute("aria-label", open ? "Close menu" : "Open menu"); + document.body.classList.toggle("menu-open", open); + if (open) sheet.querySelector("a").focus({ preventScroll: true }); + } + burger.addEventListener("click", () => setSheet(!sheet.classList.contains("open"))); + sheet.addEventListener("click", (e) => { if (e.target.closest("a")) setSheet(false); }); + addEventListener("keydown", (e) => { + if (e.key === "Escape" && sheet.classList.contains("open")) { setSheet(false); burger.focus(); } + }); + + // The section in view is the current one in both menus. + const navLinks = [...document.querySelectorAll('.nav-links a, .sheet a[href^="#"]')]; + const current = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + const id = "#" + entry.target.id; + for (const a of navLinks) a.setAttribute("aria-current", String(a.getAttribute("href") === id)); + } + }, { rootMargin: "-45% 0px -50% 0px" }); + for (const id of ["top", "why", "features", "how", "proof", "faq"]) { + const el = document.getElementById(id); + if (el) current.observe(el); + } + + if (reduced) return; + + /* Scroll reveal ------------------------------------------------------------ */ + + const reveal = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + entry.target.classList.add("is-in"); + reveal.unobserve(entry.target); + } + }, { threshold: 0.15 }); + document.querySelectorAll(".reveal").forEach((el) => reveal.observe(el)); + + /* Intros: the hero menu loads its rows, the cards rise in -------------------- */ + + const desk = document.querySelector(".desk"); + desk.querySelectorAll(".mi").forEach((el, i) => { + const inMenu = el.closest(".menu"); + el.style.setProperty("--d", (inMenu ? 260 + i * 45 : i * 60) + "ms"); + }); + + const bento = document.querySelector(".bento"); + const cards = [...bento.querySelectorAll(".card")]; + cards.forEach((card, i) => card.style.setProperty("--d", i * 85 + "ms")); + + const intro = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + entry.target.classList.add("in"); + intro.unobserve(entry.target); + if (entry.target === bento) startStories(); + } + }, { threshold: 0.3 }); + intro.observe(desk); + intro.observe(bento); + + /* Story clock ---------------------------------------------------------------- */ + + const stories = []; + let bentoVisible = false; + let running = false; + let last = 0; + + // A story is a loop of cues, [time, fn], plus an optional frame(t) for continuous motion. + // Every loop starts and ends on the design as it sits in the HTML. + function story(el, duration, cues, frame) { + return { el, duration, cues: cues.sort((a, b) => a[0] - b[0]), frame, t: 0, i: 0, start: 0 }; + } + + function tick(now) { + const dt = Math.min(0.1, (now - last) / 1000); + last = now; + for (const s of stories) { + s.start -= dt; + if (s.start > 0) continue; + s.t += dt; + while (s.i < s.cues.length && s.t >= s.cues[s.i][0]) s.cues[s.i++][1](); + if (s.frame) s.frame(s.t); + if (s.t >= s.duration) { s.t -= s.duration; s.i = 0; } + } + if (running) requestAnimationFrame(tick); + } + + function setRunning(on) { + if (on === running) return; + running = on; + if (on) { last = performance.now(); requestAnimationFrame(tick); } + } + + new IntersectionObserver(([entry]) => { + bentoVisible = entry.isIntersecting; + setRunning(bentoVisible && stories.length > 0 && !document.hidden); + }).observe(bento); + document.addEventListener("visibilitychange", () => setRunning(bentoVisible && stories.length > 0 && !document.hidden)); + + function startStories() { + // Offset so attention travels around the grid in reading order. + const offsets = { reconnect: 1.4, hyper: 2.3, curve: 3.2, rules: 4.1 }; + for (const card of cards) { + const s = makers[card.dataset.story](card); + s.start = offsets[card.dataset.story]; + stories.push(s); + } + setRunning(bentoVisible && !document.hidden); + } + + // Fades text out, swaps it, fades it back in, as two cues. + function swap(cues, t, el, text) { + cues.push([t, () => el.classList.add("swap")], [t + 0.2, () => { el.textContent = text; el.classList.remove("swap"); }]); + } + + const makers = { + // The mouse sleeps, loses its setup, reconnects, and every setting is written back. + reconnect(card) { + const state = card.querySelector(".state"); + const text = card.querySelector(".state-text"); + const checks = [...card.querySelectorAll(".chk")]; + const time = card.querySelector(".foot time"); + const times = ["11:07", "13:26", "16:52", "9:41"]; + let n = 0; + const cues = []; + cues.push([1.0, () => { state.dataset.s = "off"; state.classList.remove("ring"); }]); + swap(cues, 0.9, text, "Not connected"); + checks.forEach((c, i) => cues.push([1.3 + i * 0.06, () => c.classList.add("off")])); + cues.push([2.5, () => { state.dataset.s = "busy"; }]); + swap(cues, 2.4, text, "Applying…"); + checks.forEach((c, i) => cues.push([3.0 + i * 0.16, () => c.classList.remove("off")])); + cues.push([4.0, () => { state.dataset.s = "on"; state.classList.add("ring"); }]); + swap(cues, 3.9, text, "Connected"); + cues.push([4.1, () => time.classList.add("swap")]); + cues.push([4.3, () => { time.textContent = times[n++ % times.length]; time.classList.remove("swap"); }]); + return story(card, 8, cues); + }, + + // Caps Lock goes down, the four modifiers light, T sends the full chord, everything lets go. + hyper(card) { + const caps = card.querySelector(".caps"); + const mods = [...card.querySelectorAll(".mod")]; + const t = card.querySelector(".keycap.t"); + const chip = card.querySelector(".chip"); + const cues = [ + [0.5, () => caps.classList.add("down", "lit")], + [1.6, () => t.classList.add("down")], + [1.7, () => chip.classList.add("on")], + [2.5, () => t.classList.remove("down")], + [3.3, () => caps.classList.remove("down", "lit")], + [3.6, () => chip.classList.remove("on")], + ]; + mods.forEach((m, i) => { + cues.push([0.7 + i * 0.07, () => m.classList.add("on")]); + cues.push([3.35 + i * 0.05, () => m.classList.remove("on")]); + }); + return story(card, 5, cues); + }, + + // A dot rides the speed curve from a slow turn to a flick and back while the readout counts. + curve(card) { + const rider = card.querySelector(".rider"); + const scrub = rider.querySelector(".scrub"); + const knob = rider.querySelector(".knob"); + const halo = rider.querySelector(".halo"); + const out = card.querySelector("output"); + const rest = out.textContent; + const pts = [[1, 40], [5, 250], [15, 900], [40, 2800], [120, 9000]]; + const X = (r) => 36 + (Math.log(r) / Math.log(150)) * 312; + const Y = (v) => 168 - (Math.log(v / 10) / Math.log(3000)) * 144; + const speed = (r) => { + for (let i = 1; i < pts.length; i++) { + const [r0, v0] = pts[i - 1], [r1, v1] = pts[i]; + if (r <= r1) return v0 * Math.pow(v1 / v0, Math.log(r / r0) / Math.log(r1 / r0)); + } + return pts[pts.length - 1][1]; + }; + const sig = (v) => { const u = Math.pow(10, Math.floor(Math.log10(v)) - 2); return Math.round(v / u) * u; }; + const rate = (r) => (r < 10 ? r.toFixed(1).replace(/\.0$/, "") : Math.round(r).toString()); + const L = Math.log; + const keys = [[0, L(15)], [0.6, L(15)], [2.4, L(120)], [2.9, L(120)], [4.9, L(1)], [5.2, L(1)], [6.3, L(15)]]; + const io = (k) => (k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2); + const track = (t) => { + if (t <= keys[0][0]) return keys[0][1]; + for (let i = 1; i < keys.length; i++) { + const [t1, v1] = keys[i], [t0, v0] = keys[i - 1]; + if (t <= t1) return v0 + (v1 - v0) * io((t - t0) / (t1 - t0)); + } + return keys[keys.length - 1][1]; + }; + const draw = (r) => { + const x = X(r), y = Y(speed(r)); + scrub.setAttribute("x1", x); scrub.setAttribute("x2", x); + knob.setAttribute("cx", x); knob.setAttribute("cy", y); + halo.setAttribute("cx", x); halo.setAttribute("cy", y); + }; + draw(15); + const cues = [ + [0.1, () => rider.classList.add("on")], + [6.5, () => { rider.classList.remove("on"); out.textContent = rest; }], + ]; + return story(card, 7.4, cues, (t) => { + if (t > 6.5) return; + const r = Math.exp(track(t)); + draw(r); + out.textContent = `${rate(r)} steps/s: ${sig(speed(r)).toLocaleString("en-US")} pt/s`; + }); + }, + + // Add Rule listens, a press lands as a new rule on top, the others step down, the last leaves. + rules(card) { + const btn = card.querySelector(".fake-btn"); + const slot = card.querySelector(".rp-slot"); + const list = card.querySelector(".rp-list"); + const queue = [ + ["Gesture button", "Hold, move left", "Space to the left"], + ["Right button", "Hold and move", "Scroll"], + ["Gesture button", "Hold, move down", "App Exposé"], + ["Gesture button", "Hold, move right", "Space to the right"], + ]; + const row = (cells) => { + const li = document.createElement("li"); + for (const c of cells) li.append(Object.assign(document.createElement("span"), { textContent: c })); + return li; + }; + let fresh = null, leaving = null; + const cues = [ + [0.4, () => btn.classList.add("down")], + [0.6, () => { btn.classList.remove("down"); btn.classList.add("listening"); slot.classList.add("listen"); }], + [2.0, () => { + slot.classList.remove("listen"); + btn.classList.remove("listening"); + const rows = [...list.children]; + leaving = rows[rows.length - 1]; + queue.push([...leaving.children].map((s) => s.textContent)); + rows.forEach((li, i) => li.style.setProperty("--slot", i + 1)); + leaving.classList.add("leave"); + fresh = row(queue.shift()); + fresh.style.setProperty("--slot", 0); + fresh.classList.add("enter"); + list.prepend(fresh); + }], + [2.06, () => { fresh.classList.remove("enter"); fresh.classList.add("fresh"); }], + [2.8, () => leaving.remove()], + [4.4, () => fresh.classList.remove("fresh")], + ]; + return story(card, 6, cues); + }, + }; + + /* Tagline: each word lights up in reading order as the section scrolls past ------- */ + + const tagline = document.querySelector("[data-words]"); + const words = []; + for (const node of [...tagline.childNodes]) { + const green = node.nodeType === 1 && node.classList.contains("g"); + if (node.nodeType === 1 && !green) continue; //
+ const parts = node.textContent.split(/(\s+)/); + const frag = document.createDocumentFragment(); + for (const part of parts) { + if (!part) continue; + if (/^\s+$/.test(part)) { frag.append(part); continue; } + const w = document.createElement("span"); + w.className = green ? "w g" : "w"; + w.textContent = part; + words.push(w); + frag.append(w); + } + node.replaceWith(frag); + } + const section = tagline.closest(".tagline"); + section.classList.add("armed"); + + let queued = false; + function light() { + queued = false; + const r = tagline.getBoundingClientRect(); + const vh = innerHeight; + const progress = (vh * 0.85 - r.top) / (vh * 0.35 + r.height); + const lit = Math.round(Math.min(1, Math.max(0, progress)) * words.length); + words.forEach((w, i) => w.classList.toggle("lit", i < lit)); + } + const onScroll = () => { if (!queued) { queued = true; requestAnimationFrame(light); } }; + new IntersectionObserver(([entry]) => { + if (entry.isIntersecting) { addEventListener("scroll", onScroll, { passive: true }); light(); } + else removeEventListener("scroll", onScroll); + }).observe(section); +})(); diff --git a/site/maskable-512.png b/site/maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..12c04a3beed6d083ffd074d1995122da6827e5f9 GIT binary patch literal 12406 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 QeJgW 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%0Av6H*efA8Xkw!1GOF&N}jC~jdGe>)cM}~*d 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 literal 0 HcmV?d00001 diff --git a/site/og.py b/site/og.py new file mode 100644 index 0000000..fbc7a2c --- /dev/null +++ b/site/og.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Draws site/og.png, the 1200 x 630 social preview, from the logo kit and Geist. + +The text is converted to outlines so rsvg-convert needs no installed fonts. Needs fonttools +(pip3 install --user fonttools), rsvg-convert (brew install librsvg) and the Geist variable font: + + python3 site/og.py ~/Downloads/Geist[wght].ttf +""" + +import pathlib +import re +import subprocess +import sys + +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.pens.transformPen import TransformPen +from fontTools.ttLib import TTFont + +HERE = pathlib.Path(__file__).resolve().parent +LOGO = HERE.parent / "design/logo/svg" +W, H = 1200, 630 + + +class Text: + def __init__(self, path, weight): + self.font = TTFont(path) + self.glyphs = self.font.getGlyphSet(location={"wght": weight}) + self.cmap = self.font.getBestCmap() + self.em = self.font["head"].unitsPerEm + + def path(self, text, x, y, size, tracking=0.0): + """SVG path data for `text` with its baseline starting at (x, y).""" + scale = size / self.em + pen = SVGPathPen(self.glyphs) + cursor = 0.0 + for char in text: + name = self.cmap[ord(char)] + glyph = self.glyphs[name] + glyph.draw(TransformPen(pen, (scale, 0, 0, -scale, x + cursor, y))) + cursor += glyph.width * scale + tracking * size + return pen.getCommands(), cursor + + +def inner(svg): + """The drawing inside an SVG file, without its and .""" + body = re.search(r"]*>(.*)", svg, re.S).group(1) + return re.sub(r".*?", "", body) + + +def main(): + if len(sys.argv) != 2: + sys.exit(__doc__) + bold = Text(sys.argv[1], 600) + regular = Text(sys.argv[1], 400) + + heading = "" + widest = 0.0 + for i, line in enumerate(["Your mouse setup,", "still there after", "every reconnect"]): + d, width = bold.path(line, 80, 268 + i * 80, 72, tracking=-0.03) + heading += d + widest = max(widest, width) + sub, _ = regular.path("Mouse buttons, smooth scrolling and a hyper key for macOS", 80, 548, 28) + + lockup = inner((LOGO / "hyperpoint-horizontal-color-on-dark.svg").read_text()) + icon = inner((LOGO / "hyperpoint-app-icon.svg").read_text()) + + svg = f""" + + + +{lockup} + + +{icon} +""" + out = HERE / "og.png" + subprocess.run(["rsvg-convert", "-w", str(W), "-h", str(H), "-o", str(out)], input=svg.encode(), check=True) + print(f"wrote {out}") + + +if __name__ == "__main__": + main() diff --git a/site/privacy.html b/site/privacy.html new file mode 100644 index 0000000..e2c5ce8 --- /dev/null +++ b/site/privacy.html @@ -0,0 +1,55 @@ + + + + + +Privacy · Hyperpoint + + + + + + + + + + + + + +
+
+
+

Privacy

+

Last updated 26 October 2026.

+

The app

+

Hyperpoint has no network code. It does not collect, send or sell anything. It reads the keyboards and mice connected to your Mac and one settings file, and it writes only to your devices and that file.

+

Your settings live in ~/.config/hyperpoint/config.json. The record of what Hyperpoint did, like a device connecting or settings being applied again, goes to the macOS log on your Mac and nowhere else.

+

macOS asks you to grant Accessibility and Input Monitoring. Hyperpoint uses them to change keys, scrolls and mouse buttons as they happen. It does not record what you type.

+

This website

+

The site sets no cookies and runs no analytics. Its fonts load from Google Fonts, which sees your IP address when your browser asks for them. Downloads and release notes are served by GitHub, under GitHub's privacy statement.

+

Questions

+

Open an issue in the Hyperpoint repository.

+
+
+
+
+
+ + © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech and MX Master are trademarks of their owners. +
+
+ + diff --git a/site/site.webmanifest b/site/site.webmanifest new file mode 100644 index 0000000..6e23649 --- /dev/null +++ b/site/site.webmanifest @@ -0,0 +1,24 @@ +{ + "name": "Hyperpoint", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#18181D", + "background_color": "#18181D", + "display": "standalone" +} \ No newline at end of file diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..af9f4fb --- /dev/null +++ b/site/styles.css @@ -0,0 +1,639 @@ +/* Layout: one 1120px column under a floating island nav. Dark only, on purpose: the brand's Caps Green is + made for dark grounds, and green on this page always means "on" or "applied", like the key's lit LED. */ +:root { + --bg: #181818; + --surface: #1f1f1f; + --raised: #272727; + --line: #313131; + --fg: #ffffff; + --fg-2: #b4b4b4; + --muted: #9b9b9b; + --faint: #6b6b6b; + --accent: #52e07a; + --accent-soft: rgba(82, 224, 122, 0.14); + --ink: #18181d; + --glass: rgba(31, 31, 31, 0.72); + --glass-line: rgba(255, 255, 255, 0.08); + + --font: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + --mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace; + + /* landing-page-design B7 */ + --fluid: cubic-bezier(0.32, 0.72, 0, 1); + /* design-to-motion B6 */ + --e: cubic-bezier(0.16, 1, 0.3, 1); + --e2: cubic-bezier(0.33, 1, 0.68, 1); + --io: cubic-bezier(0.65, 0, 0.35, 1); + + --gutter: 16px; + --section: 64px; + color-scheme: dark; +} + +@media (min-width: 768px) { + :root { --gutter: 32px; --section: 96px; } +} + +*, *::before, *::after { box-sizing: border-box; } + +html { scroll-behavior: smooth; scroll-padding-top: 96px; -webkit-text-size-adjust: 100%; } +@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } + +body { + margin: 0; + background: var(--bg); + color: var(--fg-2); + font-family: var(--font); + font-size: 16px; + line-height: 24px; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +body.menu-open { overflow: hidden; } + +h1, h2, h3, p, ul, ol, figure, pre { margin: 0; } +ul, ol { padding: 0; list-style: none; } +h1, h2, h3 { color: var(--fg); font-weight: 600; text-wrap: balance; } +p, li, dd { text-wrap: pretty; } +img, svg { display: block; max-width: 100%; } +a { color: inherit; } +code, kbd, pre { font-family: var(--mono); } + +:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } + +.sr-only { + position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; + overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; +} + +.skip { + position: fixed; z-index: 60; + top: calc(8px + env(safe-area-inset-top, 0px)); + left: max(16px, env(safe-area-inset-left, 0px)); + padding: 8px 12px; border-radius: 9999px; + background: var(--fg); color: var(--ink); + font-size: 14px; line-height: 20px; font-weight: 600; text-decoration: none; + transform: translateY(-64px); + transition: transform 700ms var(--fluid); +} +.skip:focus { transform: none; } + +.wrap { + width: 100%; + max-width: calc(1120px + 2 * var(--gutter)); + margin-inline: auto; + padding-left: max(var(--gutter), env(safe-area-inset-left, 0px)); + padding-right: max(var(--gutter), env(safe-area-inset-right, 0px)); +} + +.section { padding-block: var(--section); } + +.icon { width: 20px; height: 20px; flex: none; fill: currentColor; } +.icon-sm { width: 16px; height: 16px; } + +/* Buttons ---------------------------------------------------------------- */ + +.btn { + display: inline-flex; align-items: center; gap: 8px; + padding: 8px 12px; border-radius: 9999px; border: 0; + font: inherit; font-size: 16px; line-height: 24px; font-weight: 600; + text-decoration: none; cursor: pointer; + transition: transform 700ms var(--fluid), background-color 700ms var(--fluid), color 700ms var(--fluid); +} +.btn:active { transform: scale(0.98); } +.btn-primary { background: var(--fg); color: var(--ink); } +.btn-primary:hover { background: #e4e4e4; transform: translateY(-1px); } +.btn-primary:active { transform: scale(0.98); } +.btn-sm { font-size: 14px; line-height: 20px; } +.btn-lg { padding: 12px 24px; } + +/* Island nav ------------------------------------------------------------- */ + +.nav { + position: fixed; z-index: 50; + top: calc(24px + env(safe-area-inset-top, 0px)); + left: 50%; transform: translateX(-50%); + width: max-content; max-width: calc(100% - 32px); + display: flex; align-items: center; gap: 4px; + padding: 8px 8px 8px 16px; + border-radius: 9999px; + background: var(--glass); + border: 1px solid var(--glass-line); + -webkit-backdrop-filter: blur(24px) saturate(140%); + backdrop-filter: blur(24px) saturate(140%); +} +.nav-logo { display: flex; align-items: center; margin-right: 16px; border-radius: 4px; } +.nav-logo img { height: 20px; width: auto; } +.nav-links { display: none; gap: 4px; } +.nav-links a { + padding: 8px 12px; border-radius: 9999px; + font-size: 14px; line-height: 20px; font-weight: 500; color: var(--muted); text-decoration: none; + transition: color 700ms var(--fluid), background-color 700ms var(--fluid); +} +.nav-links a:hover { color: var(--fg); background: rgba(255, 255, 255, 0.06); } +.nav-links a[aria-current="true"] { color: var(--fg); background: rgba(255, 255, 255, 0.1); } +.nav .btn { margin-left: 8px; display: none; } + +.burger { + position: relative; width: 40px; height: 40px; margin-left: 8px; + border: 0; border-radius: 9999px; background: rgba(255, 255, 255, 0.08); color: var(--fg); cursor: pointer; + transition: background-color 700ms var(--fluid), transform 700ms var(--fluid); +} +.burger:active { transform: scale(0.98); } +.burger span { + position: absolute; left: 12px; width: 16px; height: 2px; border-radius: 2px; background: currentColor; + transition: transform 700ms var(--fluid), top 700ms var(--fluid); +} +.burger span:nth-child(1) { top: 15px; } +.burger span:nth-child(2) { top: 23px; } +.burger[aria-expanded="true"] span:nth-child(1) { top: 19px; transform: rotate(45deg); } +.burger[aria-expanded="true"] span:nth-child(2) { top: 19px; transform: rotate(-45deg); } + +@media (min-width: 768px) { + .nav-links { display: flex; } + .nav .btn { display: inline-flex; } + .burger { display: none; } +} + +.sheet { + position: fixed; inset: 0; z-index: 40; + display: flex; flex-direction: column; justify-content: center; gap: 32px; + padding: calc(96px + env(safe-area-inset-top, 0px)) max(32px, env(safe-area-inset-right, 0px)) + calc(48px + env(safe-area-inset-bottom, 0px)) max(32px, env(safe-area-inset-left, 0px)); + background: rgba(0, 0, 0, 0.8); + -webkit-backdrop-filter: blur(64px); + backdrop-filter: blur(64px); + opacity: 0; visibility: hidden; + transition: opacity 700ms var(--fluid), visibility 0s linear 700ms; +} +.sheet.open { opacity: 1; visibility: visible; transition: opacity 700ms var(--fluid), visibility 0s; } +.sheet ul { display: flex; flex-direction: column; gap: 8px; } +.sheet a:not(.btn) { + display: block; padding-block: 8px; + font-size: 36px; line-height: 40px; font-weight: 600; color: var(--fg); text-decoration: none; +} +.sheet a[aria-current="true"] { color: var(--accent); } +.sheet .stagger { transform: translateY(48px); opacity: 0; transition: transform 700ms var(--fluid), opacity 700ms var(--fluid); } +.sheet.open .stagger { transform: none; opacity: 1; } +.sheet.open .stagger:nth-child(1) { transition-delay: 100ms; } +.sheet.open .stagger:nth-child(2) { transition-delay: 150ms; } +.sheet.open .stagger:nth-child(3) { transition-delay: 200ms; } +.sheet.open .stagger:nth-child(4) { transition-delay: 250ms; } +.sheet.open .sheet-cta { transition-delay: 300ms; } +.sheet .btn { align-self: flex-start; } + +/* Scroll reveal (B7): only once JS has said motion is fine ------------------ */ + +.motion .reveal { + opacity: 0; transform: translateY(64px); filter: blur(12px); + transition: opacity 900ms var(--fluid), transform 900ms var(--fluid), filter 900ms var(--fluid); + transition-delay: var(--rd, 0ms); +} +.motion .reveal.is-in { opacity: 1; transform: none; filter: blur(0); } + +/* Section headings -------------------------------------------------------- */ + +.eyebrow { font-size: 14px; line-height: 20px; font-weight: 500; color: var(--muted); } +.h2 { font-size: 30px; line-height: 36px; letter-spacing: -0.02em; } +.lead { font-size: 18px; line-height: 28px; color: var(--fg-2); max-width: 600px; } +.head { display: flex; flex-direction: column; gap: 16px; max-width: 680px; margin-bottom: 48px; } +.head .eyebrow { margin-bottom: -4px; } + +@media (min-width: 768px) { + .h2 { font-size: 36px; line-height: 40px; } +} + +/* Hero --------------------------------------------------------------------- */ + +main { padding-top: calc(80px + env(safe-area-inset-top, 0px)); } +.hero { padding-block: var(--section); } +.hero-grid { display: grid; gap: 48px; align-items: center; } +.hero-copy { display: flex; flex-direction: column; align-items: flex-start; gap: 24px; min-width: 0; } +.hero h1 { + max-width: 680px; + font-size: 36px; line-height: 40px; letter-spacing: -0.03em; + background: linear-gradient(90deg, #ffffff, #9b9b9b); + -webkit-background-clip: text; background-clip: text; color: transparent; +} +.hero .sub { max-width: 680px; font-size: 18px; line-height: 28px; color: var(--fg-2); } +.hero .btn { margin-top: 8px; } + +.proof-line { + display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; + font-size: 14px; line-height: 20px; color: var(--muted); +} +.proof-line span { display: inline-flex; align-items: center; gap: 8px; } +.proof-line .icon { color: var(--accent); } +.proof-line .dotsep { display: none; width: 4px; height: 4px; border-radius: 9999px; background: var(--line); } +@media (min-width: 560px) { .proof-line .dotsep { display: block; } } + +@media (min-width: 768px) { + .hero h1 { font-size: 48px; line-height: 48px; } +} +@media (min-width: 1200px) { + .hero-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 456px); gap: 64px; } + .hero h1 { font-size: 60px; line-height: 60px; } +} + +/* Hero mockup: a Mac menu bar with Hyperpoint's menu open ------------------ */ + +.desk { + position: relative; min-width: 0; + border-radius: 24px; background: var(--surface); border: 1px solid var(--line); + overflow: hidden; +} +.menubar { + display: flex; align-items: center; justify-content: space-between; gap: 16px; + height: 32px; padding: 0 12px; background: var(--raised); + font-size: 14px; line-height: 20px; color: var(--fg); + white-space: nowrap; +} +.menubar-apps { display: flex; gap: 16px; overflow: hidden; } +.menubar-apps b { font-weight: 600; } +.menubar-apps span:nth-child(n + 4) { display: none; } +.menubar-right { display: flex; align-items: center; gap: 12px; } +.status-item { + display: grid; place-items: center; width: 32px; height: 24px; border-radius: 6px; + background: rgba(255, 255, 255, 0.16); +} +.status-item svg { width: 18px; height: 18px; fill: var(--fg); } +@media (min-width: 480px) { .menubar-apps span:nth-child(n + 4) { display: inline; } } +@media (min-width: 1200px) { .menubar-apps span:nth-child(n + 5) { display: none; } } + +.desk-body { display: flex; justify-content: flex-end; padding: 4px 8px 32px; } +.menu { + width: 100%; max-width: 312px; padding: 4px; + border-radius: 12px; background: rgba(39, 39, 39, 0.96); border: 1px solid var(--line); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.45); + font-size: 14px; line-height: 20px; color: var(--fg); +} +.m-row { + display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; align-items: center; gap: 4px 8px; + padding: 2px 12px 2px 8px; border-radius: 8px; +} +.m-row .r { color: var(--muted); } +.m-row .k { color: var(--muted); font-family: var(--font); } +.m-row.title .l { font-weight: 600; } +.m-row.hl { background: rgba(255, 255, 255, 0.1); } +.m-check { color: var(--fg); font-size: 12px; line-height: 16px; } +.m-head { padding: 4px 12px 0 32px; font-size: 12px; line-height: 16px; font-weight: 600; color: var(--muted); } +.m-note { padding: 2px 12px 4px 32px; font-size: 12px; line-height: 16px; color: var(--muted); } +.m-sep { height: 1px; margin: 4px 12px; background: var(--line); transform-origin: left center; } + +/* Hero intro (design-to-motion A5): the desk, bar and menu stay; their contents load. */ +.motion .desk .mi { + transition: opacity 0.75s var(--e2), transform 1.05s var(--e), filter 0.75s var(--e2); + transition-delay: var(--d, 0ms); +} +.motion .desk:not(.in) .mi { opacity: 0; transform: translateY(10px); filter: blur(4px); } +.motion .desk:not(.in) .mi.pop { transform: scale(0.85); } +.motion .desk:not(.in) .m-sep.mi { transform: scaleX(0); filter: none; opacity: 1; } + +/* Tagline reveal (B11) ----------------------------------------------------- */ + +.tagline p { + max-width: 680px; + font-size: 36px; line-height: 40px; font-weight: 600; letter-spacing: -0.02em; color: var(--fg); +} +.tagline .w { transition: color 700ms var(--fluid); } +.tagline.armed .w { color: rgba(255, 255, 255, 0.3); } +.tagline.armed .w.lit { color: var(--fg); } +.tagline .w.lit.g { color: var(--accent); } +@media (min-width: 768px) { .tagline p { font-size: 48px; line-height: 48px; } } + +/* Problem to solution ----------------------------------------------------- */ + +.versus { display: grid; gap: 16px; } +.vs-col { + display: flex; flex-direction: column; gap: 24px; + padding: 32px 24px; border-radius: 24px; border: 1px solid var(--line); +} +.vs-col.after { background: var(--surface); } +.vs-col h3 { display: flex; align-items: center; gap: 12px; font-size: 18px; line-height: 28px; } +.vs-col ul { display: flex; flex-direction: column; gap: 16px; } +.vs-col li { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; } +.vs-col li .icon { margin-top: 2px; } +.vs-col.before li .icon { color: var(--faint); } +.vs-col.after li .icon { color: var(--accent); } +.vs-col code { font-size: 14px; color: var(--fg); overflow-wrap: anywhere; } +.led { width: 16px; height: 4px; border-radius: 2px; background: var(--line); } +.after .led { background: var(--accent); } +@media (min-width: 768px) { + .versus { grid-template-columns: 1fr 1fr; } + .vs-col { padding: 40px 32px; } +} + +/* Bento --------------------------------------------------------------------- */ + +.bento { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; } +.card { + display: flex; flex-direction: column; min-width: 0; + padding: 8px; border-radius: 24px; background: var(--surface); border: 1px solid var(--line); +} +.vis { + position: relative; display: grid; place-items: center; min-height: 288px; padding: 24px 16px; + border-radius: 16px; background: var(--bg); border: 1px solid var(--raised); overflow: hidden; + isolation: isolate; +} +.copy { display: flex; flex-direction: column; gap: 8px; padding: 24px 16px 16px; } +.copy h3 { font-size: 20px; line-height: 28px; letter-spacing: -0.01em; } +.copy p { max-width: 520px; } + +@media (min-width: 900px) { + .bento { grid-template-columns: repeat(12, minmax(0, 1fr)); } + .vis { min-height: 320px; } + .span-7 { grid-column: span 7; } + .span-5 { grid-column: span 5; } +} + +/* Card intro (design-to-motion B2, `card` and `up` kinds) */ +.motion .bento .card { + transition: transform 0.9s var(--e), opacity 0.6s var(--e2), filter 0.6s var(--e2); + transition-delay: var(--d, 0ms); +} +.motion .bento:not(.in) .card { opacity: 0; transform: translateY(24px) scale(0.985); filter: blur(6px); } +.motion .bento .up { + transition: transform 1.05s var(--e), opacity 0.75s var(--e2), filter 0.75s var(--e2); + transition-delay: calc(var(--d, 0ms) + 150ms + var(--i, 0) * 60ms); +} +.motion .bento:not(.in) .up { opacity: 0; transform: translateY(10px); filter: blur(4px); } + +.panel { + width: 100%; max-width: 400px; padding: 16px; + border-radius: 12px; background: var(--surface); border: 1px solid var(--line); +} + +/* Card: settings come back */ +.dev-head { display: flex; align-items: center; gap: 8px; color: var(--fg); } +.dev-head .name { font-weight: 600; flex: 1; min-width: 0; } +.state { display: inline-flex; align-items: center; gap: 8px; font-size: 14px; line-height: 20px; color: var(--muted); } +.state .dot { position: relative; width: 8px; height: 8px; border-radius: 9999px; background: var(--accent); transition: background-color 400ms var(--e2); } +.state .dot::after { + content: ""; position: absolute; inset: 0; border-radius: inherit; background: var(--accent); + opacity: 0; transform: scale(1); +} +.state.ring .dot::after { animation: ring 2s var(--e2) 1; } +.state[data-s="off"] .dot { background: var(--faint); } +.state[data-s="busy"] .dot { background: var(--fg); animation: breathe 1.2s var(--io) infinite; } +.state-text { transition: opacity 300ms var(--e2), filter 300ms var(--e2); } +.state-text.swap { opacity: 0; filter: blur(4px); } +@keyframes ring { 0% { opacity: 0.55; transform: scale(1); } 100% { opacity: 0; transform: scale(2.6); } } +@keyframes breathe { 50% { opacity: 0.35; } } + +.applied { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; font-size: 14px; line-height: 20px; } +.applied li { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto; align-items: center; gap: 8px; } +.applied .chk { color: var(--accent); transition: color 300ms var(--e2), transform 500ms var(--e); } +.applied .chk.off { color: var(--line); transform: scale(0.85); } +.applied .lbl { color: var(--fg-2); white-space: nowrap; } +.applied .det { color: var(--faint); font-size: 12px; line-height: 16px; } +.rule { height: 1px; margin: 16px 0 12px; background: var(--line); } +.foot { font-size: 12px; line-height: 16px; color: var(--muted); } +.foot time { color: var(--fg); transition: opacity 400ms var(--e2), filter 400ms var(--e2); } +.foot time.swap { opacity: 0; filter: blur(4px); } + +/* Card: hyper key */ +.hyper { display: flex; flex-direction: column; align-items: center; gap: 16px; } +.keycap { + position: relative; display: grid; place-items: center; + width: 48px; height: 48px; border-radius: 8px; + background: var(--raised); border: 1px solid var(--line); color: var(--fg-2); + box-shadow: 0 3px 0 #0e0e0e; + font-size: 18px; line-height: 28px; + transition: transform 220ms var(--e), box-shadow 220ms var(--e), color 300ms var(--e2), border-color 300ms var(--e2), background-color 300ms var(--e2); +} +.keycap.down { transform: translateY(3px); box-shadow: 0 0 0 #0e0e0e; } +.keycap.on { color: var(--fg); border-color: rgba(82, 224, 122, 0.55); background: #2b332d; } +.keycap.caps { width: 112px; height: 64px; place-items: end start; padding: 8px 12px; } +.keycap.caps .cap-led { + position: absolute; top: 8px; left: 12px; width: 16px; height: 4px; border-radius: 2px; background: var(--line); + transition: background-color 300ms var(--e2), box-shadow 300ms var(--e2); +} +.keycap.caps.lit .cap-led { background: var(--accent); box-shadow: 0 0 12px rgba(82, 224, 122, 0.6); } +.keycap.caps .cap-text { font-size: 12px; line-height: 16px; color: var(--muted); } +.mods { display: flex; gap: 8px; } +.holds { font-size: 12px; line-height: 16px; color: var(--faint); } +.chord { display: flex; align-items: center; gap: 12px; font-size: 14px; line-height: 20px; color: var(--muted); } +.keycap.t { width: 32px; height: 32px; font-size: 14px; line-height: 20px; box-shadow: 0 2px 0 #0e0e0e; } +.keycap.t.down { transform: translateY(2px); box-shadow: 0 0 0 #0e0e0e; } +.chip { + padding: 4px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--surface); + font-family: var(--mono); font-size: 14px; line-height: 20px; color: var(--fg-2); + transition: color 300ms var(--e2), border-color 300ms var(--e2), background-color 300ms var(--e2); +} +.chip.on { color: var(--accent); border-color: rgba(82, 224, 122, 0.55); background: var(--accent-soft); } + +/* Card: speed curve */ +.curve { width: 100%; display: flex; flex-direction: column; gap: 8px; } +.curve svg { width: 100%; height: auto; overflow: visible; } +.curve .band { fill: rgba(255, 255, 255, 0.04); } +.curve .band-l { fill: var(--faint); font-size: 12px; font-family: var(--font); } +.curve .grid { stroke: var(--raised); stroke-width: 1; } +.curve .tick { fill: var(--faint); font-size: 12px; font-family: var(--mono); } +.curve .area { fill: var(--accent-soft); } +.curve .line { fill: none; stroke: var(--fg); stroke-width: 2; stroke-linejoin: round; } +.curve .pt { fill: var(--bg); stroke: var(--fg); stroke-width: 2; } +.curve .scrub { stroke: var(--accent); stroke-width: 1; stroke-dasharray: 3 3; } +.curve .knob { fill: var(--accent); } +.curve .halo { fill: rgba(82, 224, 122, 0.25); } +.curve .rider { opacity: 0; transition: opacity 600ms var(--e2); } +.curve .rider.on { opacity: 1; } +.readout { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; font-size: 14px; line-height: 20px; } +.readout output { font-family: var(--mono); color: var(--fg); font-variant-numeric: tabular-nums; white-space: nowrap; } +.readout span { color: var(--faint); font-size: 12px; line-height: 16px; } + +/* Card: add a rule by pressing */ +.rules-panel { max-width: 520px; } +.rp-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.rp-head h4 { margin: 0; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--fg); } +.rp-head small { display: block; font-size: 12px; line-height: 16px; color: var(--faint); } +.fake-btn { + display: inline-flex; align-items: center; gap: 4px; padding: 4px 12px; border-radius: 6px; + background: var(--raised); border: 1px solid var(--line); color: var(--fg); + font-size: 14px; line-height: 20px; font-weight: 600; + transition: transform 220ms var(--e), background-color 300ms var(--e2), border-color 300ms var(--e2); +} +.fake-btn.down { transform: scale(0.96); background: var(--line); } +.fake-btn.listening { border-color: rgba(82, 224, 122, 0.55); } +.rp-slot { position: relative; height: 32px; margin-top: 12px; } +.rp-cols, .rp-listen { + position: absolute; inset: 0; display: grid; align-items: center; + transition: opacity 400ms var(--e2), transform 600ms var(--e), filter 400ms var(--e2); +} +.rp-cols { grid-template-columns: minmax(0, 1fr) auto; gap: 8px; padding: 0 8px; font-size: 12px; line-height: 16px; color: var(--faint); } +.rp-cols span:nth-child(2) { display: none; } +.rp-listen { + grid-template-columns: 8px minmax(0, 1fr); gap: 8px; padding: 0 12px; border-radius: 6px; + background: var(--accent-soft); color: var(--fg); font-size: 12px; line-height: 16px; + opacity: 0; transform: translateY(6px); filter: blur(4px); +} +.rp-listen .dot { width: 8px; height: 8px; border-radius: 9999px; background: var(--accent); animation: breathe 1.2s var(--io) infinite; } +.rp-slot.listen .rp-cols { opacity: 0; transform: translateY(-6px); filter: blur(4px); } +.rp-slot.listen .rp-listen { opacity: 1; transform: none; filter: blur(0); } +.rp-list { --row: 48px; --step: 52px; position: relative; height: calc(3 * var(--step)); margin-top: 4px; overflow: hidden; } +.rp-list li { + position: absolute; left: 0; right: 0; top: 0; height: var(--row); + display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "c d" "w d"; + align-items: center; column-gap: 8px; padding: 0 12px; + border-radius: 6px; background: var(--raised); border: 1px solid transparent; + font-size: 12px; line-height: 16px; color: var(--fg-2); + transform: translateY(calc(var(--slot) * var(--step))); + transition: transform 700ms var(--fluid), opacity 600ms var(--e2), filter 600ms var(--e2), border-color 900ms var(--e2), background-color 900ms var(--e2); +} +.rp-list li span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rp-list li span:nth-child(1) { grid-area: c; color: var(--fg); } +.rp-list li span:nth-child(2) { grid-area: w; color: var(--faint); } +.rp-list li span:nth-child(3) { grid-area: d; text-align: right; white-space: normal; } +.rp-list li.fresh { border-color: rgba(82, 224, 122, 0.55); background: #26302a; } +.rp-list li.enter { opacity: 0; filter: blur(6px); transform: translateY(-12px); } +@media (min-width: 560px) { + .rp-cols, .rp-list li { grid-template-columns: 1fr 1fr 1.2fr; grid-template-areas: "c w d"; } + .rp-cols span:nth-child(2) { display: inline; } + .rp-list { --row: 40px; --step: 44px; } + .rp-list li { padding: 0 8px; } + .rp-list li span:nth-child(2) { color: inherit; } + .rp-list li span:nth-child(3) { text-align: left; white-space: nowrap; } +} +.rp-list li.leave { opacity: 0; filter: blur(4px); } +@media (min-width: 560px) { + .rp-list li, .rp-cols { font-size: 14px; line-height: 20px; } + .rp-cols { font-size: 12px; line-height: 16px; } + .rp-listen { font-size: 14px; line-height: 20px; } +} + +/* Smaller features ---------------------------------------------------------- */ + +.trio { display: grid; gap: 32px; margin-top: 64px; } +.trio li { display: flex; flex-direction: column; gap: 8px; } +.trio .icon { width: 24px; height: 24px; color: var(--fg); margin-bottom: 8px; } +.trio h3 { font-size: 18px; line-height: 28px; } +@media (min-width: 768px) { .trio { grid-template-columns: repeat(3, minmax(0, 1fr)); } } + +/* How it works --------------------------------------------------------------- */ + +.steps { display: grid; gap: 16px; counter-reset: step; } +.step { + display: flex; flex-direction: column; gap: 12px; + padding: 32px 24px; border-radius: 24px; background: var(--surface); border: 1px solid var(--line); +} +.step-n { + display: grid; place-items: center; width: 32px; height: 32px; margin-bottom: 12px; + border-radius: 9999px; border: 1px solid var(--line); + font-family: var(--mono); font-size: 14px; line-height: 20px; color: var(--fg); +} +.step h3 { font-size: 20px; line-height: 28px; } +.step strong { color: var(--fg); font-weight: 500; } +@media (min-width: 900px) { .steps { grid-template-columns: repeat(3, minmax(0, 1fr)); } } + +/* Proof ---------------------------------------------------------------------- */ + +.proof-grid { display: grid; gap: 48px; margin-bottom: 48px; } +.proof-grid .head { margin-bottom: 0; } +.facts { display: grid; gap: 24px; align-content: start; } +.facts li { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; } +.facts .icon { margin-top: 2px; color: var(--accent); } +.facts b { display: block; color: var(--fg); font-weight: 600; } +.code { + min-width: 0; border-radius: 24px; background: var(--surface); border: 1px solid var(--line); overflow: hidden; +} +.code-bar { + display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 24px; + background: var(--raised); font-size: 12px; line-height: 16px; color: var(--muted); +} +.code-bar code { color: var(--fg-2); } +.code pre { + padding: 24px; overflow-x: auto; font-size: 12px; line-height: 16px; color: var(--fg-2); + tab-size: 2; +} +.code .k { color: var(--fg); } +.code .s { color: var(--accent); } +.code .n { color: var(--fg-2); } +.code .p { color: var(--faint); } +@media (min-width: 560px) { .code pre { font-size: 14px; line-height: 20px; } } +@media (min-width: 600px) { .facts { grid-template-columns: 1fr 1fr; gap: 32px 24px; } } +@media (min-width: 1024px) { + .proof-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: 64px; align-items: end; } +} + +/* FAQ ------------------------------------------------------------------------ */ + +.faq-grid { display: grid; gap: 32px; } +.faq-grid .head { margin-bottom: 0; } +.faq-list { display: flex; flex-direction: column; gap: 8px; min-width: 0; } +.faq-list details { + border-radius: 16px; background: var(--surface); border: 1px solid var(--line); + transition: background-color 700ms var(--fluid), border-color 700ms var(--fluid); + interpolate-size: allow-keywords; +} +.faq-list details:hover { border-color: #3d3d3d; } +.faq-list details[open] { background: var(--raised); } +.faq-list summary { + display: flex; align-items: center; justify-content: space-between; gap: 16px; + padding: 16px 24px; cursor: pointer; list-style: none; border-radius: 16px; + font-size: 16px; line-height: 24px; font-weight: 600; color: var(--fg); +} +.faq-list summary::-webkit-details-marker { display: none; } +.faq-list summary .icon { transition: transform 700ms var(--fluid); color: var(--muted); } +.faq-list details[open] summary .icon { transform: rotate(45deg); } +.faq-list details::details-content { + block-size: 0; overflow: hidden; + transition: block-size 700ms var(--fluid), content-visibility 700ms allow-discrete; +} +.faq-list details[open]::details-content { block-size: auto; } +.faq-list .a { padding: 0 24px 24px; max-width: 640px; } +.faq-list .a code { font-size: 14px; color: var(--fg); overflow-wrap: anywhere; } +@media (min-width: 1024px) { + .faq-grid { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: 64px; align-items: start; } + .faq-grid .head { position: sticky; top: 96px; } +} + +/* Final CTA ------------------------------------------------------------------ */ + +.cta { + display: grid; gap: 40px; align-items: center; + padding: 48px 24px; border-radius: 24px; background: var(--surface); border: 1px solid var(--line); +} +.cta-copy { display: flex; flex-direction: column; align-items: flex-start; gap: 24px; min-width: 0; } +.cta h2 { font-size: 36px; line-height: 40px; letter-spacing: -0.03em; max-width: 680px; } +.cta .lead { max-width: 560px; } +.cta-icon { width: 96px; height: 96px; } +@media (min-width: 768px) { + .cta { grid-template-columns: minmax(0, 1fr) auto; padding: 64px 48px; } + .cta h2 { font-size: 48px; line-height: 48px; } + .cta-icon { width: 160px; height: 160px; } +} + +/* Footer --------------------------------------------------------------------- */ + +.footer { padding-block: 48px calc(48px + env(safe-area-inset-bottom, 0px)); } +.footer-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 24px; } +.footer img { height: 24px; width: auto; } +.footer nav ul { display: flex; flex-wrap: wrap; gap: 8px 24px; } +.footer nav a { + font-size: 14px; line-height: 20px; color: var(--muted); text-decoration: none; border-radius: 4px; + transition: color 700ms var(--fluid); +} +.footer nav a:hover { color: var(--fg); } +.footer small { display: block; margin-top: 24px; font-size: 12px; line-height: 16px; color: var(--faint); } + +/* Plain pages (privacy, 404) --------------------------------------------------- */ + +.page { padding-block: var(--section); } +.prose { display: flex; flex-direction: column; gap: 16px; max-width: 640px; } +.prose h1 { font-size: 36px; line-height: 40px; letter-spacing: -0.02em; } +.prose h2 { margin-top: 24px; font-size: 20px; line-height: 28px; } +.prose code { font-size: 14px; color: var(--fg); overflow-wrap: anywhere; } +.prose a { color: var(--fg); text-underline-offset: 3px; } +.lost { display: flex; flex-direction: column; align-items: flex-start; gap: 24px; max-width: 560px; } +.lost img { width: 96px; height: 96px; } +.lost .code-n { font-family: var(--mono); font-size: 14px; line-height: 20px; color: var(--accent); } +.lost h1 { font-size: 48px; line-height: 48px; letter-spacing: -0.03em; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation: none !important; } +} + +/* Plain pages carry a smaller nav and rise in on load with CSS alone. */ +.nav-simple .btn { display: inline-flex; } +.rise { animation: rise 900ms var(--fluid) both; animation-delay: var(--rd, 0ms); } +@keyframes rise { from { opacity: 0; transform: translateY(64px); filter: blur(12px); } } From 67f313b062c8ae24db63c8feff42fcef9af249d5 Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 13:19:11 +0500 Subject: [PATCH 02/11] feat(site): say how small Hyperpoint is The v0.1.1 DMG is 1.2 MB and the app inside it 3.3 MB. The size replaces the version in both proof lines, gets a proof fact and an FAQ answer, and the proof grid gains a sixth fact, the missing Dock icon, to stay even. Co-Authored-By: Claude Opus 5.5 --- site/README.md | 3 ++- site/index.html | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/site/README.md b/site/README.md index 9fb64d7..6ec6f39 100644 --- a/site/README.md +++ b/site/README.md @@ -75,4 +75,5 @@ as `FAQPage`. The card and curve data come from the app: the menu strings from `StatusMenu.swift` and `MenuPresentation.swift`, the rules from the MX Master 3 preset, and the curve and measured wheel bands from `SpeedCurve.swift` and -`CurveGraph.swift`. Update the page when those change. +`CurveGraph.swift`. Update the page when those change. The sizes (1.2 MB download, 3.3 MB installed) are measured from +the v0.1.1 DMG and the app inside it, so check them on each release. diff --git a/site/index.html b/site/index.html index ae82632..01fe03d 100644 --- a/site/index.html +++ b/site/index.html @@ -33,6 +33,7 @@ "operatingSystem": "macOS 27", "applicationCategory": "UtilitiesApplication", "softwareVersion": "0.1.1", + "fileSize": "1.2 MB", "downloadUrl": "https://github.com/progremir/hyperpoint/releases/latest", "description": "A macOS menu bar app that remaps mouse buttons, makes the scroll wheel glide and turns Caps Lock into a hyper key, and puts every setting back when a device reconnects." } @@ -50,6 +51,14 @@ "text": "macOS 27 on Apple silicon. Download the DMG from the latest release, open it and drag Hyperpoint into Applications." } }, + { + "@type": "Question", + "name": "How big is it?", + "acceptedAnswer": { + "@type": "Answer", + "text": "The download is 1.2 MB, and the app takes 3.3 MB once installed. It is one native Apple silicon binary with no bundled frameworks or runtime." + } + }, { "@type": "Question", "name": "Does it work with mice other than the MX Master 3?", @@ -174,7 +183,7 @@

Your mouse setup,
still there after
every reconnect

Signed and notarized by Apple - Version 0.1.1 + 1.2 MB download macOS 27 on Apple silicon

@@ -433,6 +442,8 @@

One Mac, four devices, one file

  • No network codeThe app reads your devices and one file. It never connects to the internet.
  • Mistakes are caughtAn invalid file is rejected whole with the path of the error. The last good settings keep running.
  • Quit, and it is goneQuitting clears the key mappings Hyperpoint made, so Caps Lock types as Caps Lock again.
  • +
  • 1.2 MB to downloadOne native Apple silicon app, 3.3 MB once installed, with no bundled frameworks or runtime.
  • +
  • Out of your DockHyperpoint lives in the menu bar. It has no Dock icon and opens a window only when you ask for Settings.
  • @@ -483,6 +494,10 @@

    Questions before you download

    Which Macs does Hyperpoint run on?

    macOS 27 on Apple silicon. Download the DMG from the latest release, open it and drag Hyperpoint into Applications.

    +
    + How big is it? +

    The download is 1.2 MB, and the app takes 3.3 MB once installed. It is one native Apple silicon binary with no bundled frameworks or runtime.

    +
    Does it work with mice other than the MX Master 3?

    Yes. Every keyboard and mouse Hyperpoint sees shows up under Devices. Logitech mice that speak HID++ get their gesture and top buttons and the thumb wheel. Other mice get their plain buttons and any keys they send. Trackpads stay as macOS has them.

    @@ -536,7 +551,7 @@

    Set it up once.
    Keep it through
    every reconnect.

    Signed and notarized by Apple - Version 0.1.1 + 1.2 MB download macOS 27 on Apple silicon

    From fd161bb42a37a25f1f76d292a9315c90c7945231 Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 13:20:54 +0500 Subject: [PATCH 03/11] fix(site): a bigger nav logo, and Karabiner did the hyper key From comments on the preview: the nav lockup goes from 20 to 28 px tall, and the Before column names the hyper key rather than Caps Lock. Co-Authored-By: Claude Opus 5.5 --- site/404.html | 2 +- site/index.html | 4 ++-- site/privacy.html | 2 +- site/styles.css | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/site/404.html b/site/404.html index 7a0b3a8..d528d02 100644 --- a/site/404.html +++ b/site/404.html @@ -18,7 +18,7 @@
    diff --git a/site/index.html b/site/index.html index 01fe03d..fa79bbc 100644 --- a/site/index.html +++ b/site/index.html @@ -141,7 +141,7 @@ - © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech and MX Master are trademarks of their owners. + © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech, MX Master, Razer and Naga are trademarks of their owners. diff --git a/site/index.html b/site/index.html index fa79bbc..43bf00f 100644 --- a/site/index.html +++ b/site/index.html @@ -390,7 +390,7 @@

    Hold to scroll

  • Keys from any mouse

    -

    Keys a mouse sends, like Escape and Return on a 2.4G mouse, can become modifiers or shortcuts. A modifier is mapped below apps, so it holds in password fields too.

    +

    Keys a mouse sends, like the number keys a Razer Naga's side buttons type, can become modifiers or shortcuts. A modifier is mapped below apps, so it holds in password fields too.

  • @@ -576,7 +576,7 @@

    Set it up once.
    Keep it through
    every reconnect.

    - © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech and MX Master are trademarks of their owners. + © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech, MX Master, Razer and Naga are trademarks of their owners. diff --git a/site/privacy.html b/site/privacy.html index 7f3b6f4..29e9982 100644 --- a/site/privacy.html +++ b/site/privacy.html @@ -48,7 +48,7 @@

    Questions

    - © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech and MX Master are trademarks of their owners. + © 2026 Hyperpoint. BetterMouse, Karabiner, Logitech, MX Master, Razer and Naga are trademarks of their owners. From 7726b2ca641f3a170ade2f6efe2bca50e3aeee1b Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 13:23:59 +0500 Subject: [PATCH 05/11] fix(site): the proof reads top to bottom, and agents can edit the config From comments on the preview: the heading sat beside a tall stack of facts, so the section is now heading, config, then the facts three across. The notarized fact (still in both proof lines) gives way to one saying the config is plain JSON an agent can edit, which the app applies on save. Co-Authored-By: Claude Opus 5.5 --- site/index.html | 28 +++++++++++++--------------- site/styles.css | 11 ++++------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/site/index.html b/site/index.html index 43bf00f..2a14365 100644 --- a/site/index.html +++ b/site/index.html @@ -429,22 +429,12 @@

    Press a button to add a rule

    -
    +
    -
    -
    -

    Proof

    -

    One Mac, four devices, one file

    -

    This is the config from the Mac Hyperpoint was built on. Two mice and two keyboards, each set up from a preset, replacing a BetterMouse and Karabiner setup. The settings window edits this file and nothing else, so every setting can be read, diffed and backed up.

    -
    -
      -
    • Notarized by AppleReleases are signed with Developer ID and notarized, so the download opens without a warning.
    • -
    • No network codeThe app reads your devices and one file. It never connects to the internet.
    • -
    • Mistakes are caughtAn invalid file is rejected whole with the path of the error. The last good settings keep running.
    • -
    • Quit, and it is goneQuitting clears the key mappings Hyperpoint made, so Caps Lock types as Caps Lock again.
    • -
    • 1.2 MB to downloadOne native Apple silicon app, 3.3 MB once installed, with no bundled frameworks or runtime.
    • -
    • Out of your DockHyperpoint lives in the menu bar. It has no Dock icon and opens a window only when you ask for Settings.
    • -
    +
    +

    Proof

    +

    One Mac, four devices, one file

    +

    This is the config from the Mac Hyperpoint was built on. Two mice and two keyboards, each set up from a preset, replacing a BetterMouse and Karabiner setup. The settings window edits this file and nothing else, so every setting can be read, diffed and backed up.

    ~/.config/hyperpoint/config.jsonExcerpt, scrolling settings left out
    @@ -478,6 +468,14 @@

    One Mac, four devices, one file

    "ignored": [] }
    +
      +
    • Agents can edit itIt is plain JSON, so you or a coding agent like Claude Code can change any rule. Hyperpoint applies the file the moment it is saved.
    • +
    • No network codeThe app reads your devices and one file. It never connects to the internet.
    • +
    • Mistakes are caughtAn invalid file is rejected whole with the path of the error. The last good settings keep running.
    • +
    • Quit, and it is goneQuitting clears the key mappings Hyperpoint made, so Caps Lock types as Caps Lock again.
    • +
    • 1.2 MB to downloadOne native Apple silicon app, 3.3 MB once installed, with no bundled frameworks or runtime.
    • +
    • Out of your DockHyperpoint lives in the menu bar. It has no Dock icon and opens a window only when you ask for Settings.
    • +
    diff --git a/site/styles.css b/site/styles.css index 68713de..81db633 100644 --- a/site/styles.css +++ b/site/styles.css @@ -527,9 +527,8 @@ main { padding-top: calc(80px + env(safe-area-inset-top, 0px)); } /* Proof ---------------------------------------------------------------------- */ -.proof-grid { display: grid; gap: 48px; margin-bottom: 48px; } -.proof-grid .head { margin-bottom: 0; } -.facts { display: grid; gap: 24px; align-content: start; } +.proof .code { margin-bottom: 48px; } +.facts { display: grid; gap: 32px 24px; } .facts li { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; } .facts .icon { margin-top: 2px; color: var(--accent); } .facts b { display: block; color: var(--fg); font-weight: 600; } @@ -550,10 +549,8 @@ main { padding-top: calc(80px + env(safe-area-inset-top, 0px)); } .code .n { color: var(--fg-2); } .code .p { color: var(--faint); } @media (min-width: 560px) { .code pre { font-size: 14px; line-height: 20px; } } -@media (min-width: 600px) { .facts { grid-template-columns: 1fr 1fr; gap: 32px 24px; } } -@media (min-width: 1024px) { - .proof-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: 64px; align-items: end; } -} +@media (min-width: 600px) { .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (min-width: 1024px) { .facts { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px 32px; } } /* FAQ ------------------------------------------------------------------------ */ From 65676fcd86019525f1be34baa4e2c1d32d118804 Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 15:02:34 +0500 Subject: [PATCH 06/11] feat(skills): a skill that manages the Hyperpoint config skills/hyperpoint-config teaches an agent the config format and every rule ConfigCodec enforces, and how to find a device, back up, check, save and confirm from the app's log that the change took. Its check_config.py rejects a file at the same path the app would, and scripts/check-config-skill.py holds it to the parser's 60 invalid-file tests. The README says how to install it; the landing page links it from the agents fact and a new FAQ answer. Co-Authored-By: Claude Opus 5.5 --- README.md | 19 +- scripts/check-config-skill.py | 70 +++ site/README.md | 2 +- site/index.html | 14 +- site/styles.css | 2 + skills/hyperpoint-config/SKILL.md | 199 +++++++ .../hyperpoint-config/scripts/check_config.py | 500 ++++++++++++++++++ 7 files changed, 803 insertions(+), 3 deletions(-) create mode 100755 scripts/check-config-skill.py create mode 100644 skills/hyperpoint-config/SKILL.md create mode 100755 skills/hyperpoint-config/scripts/check_config.py diff --git a/README.md b/README.md index 7b74a91..0dcec79 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,31 @@ Everything lives in `~/.config/hyperpoint/config.json`, created with no devices edits this file and nothing else. An invalid file is rejected whole with the path of the error, and the last good config keeps running. The format and the reasoning behind it are in [docs/architecture.md](docs/architecture.md). +### Manage it with an agent + +[skills/hyperpoint-config](skills/hyperpoint-config/SKILL.md) is a skill for Claude Code and other agents that read +`SKILL.md` files. It knows the format and every rule the app enforces, checks an edit with its own checker before +saving it, and reads the app's log to confirm the change took. Install it once: + +```bash +git clone --depth 1 https://github.com/progremir/hyperpoint.git /tmp/hyperpoint +mkdir -p ~/.claude/skills && cp -r /tmp/hyperpoint/skills/hyperpoint-config ~/.claude/skills/ +``` + +Then ask in plain words, such as "make the gesture button on my MX Master open App Exposé". The checker works on its +own too: `python3 skills/hyperpoint-config/scripts/check_config.py` prints the first problem in your config with its +path, or the devices it sets up. + ## Development ```bash swift test scripts/check-layers.sh +scripts/check-config-skill.py ``` -`check-layers.sh` enforces the module layering described in the architecture doc. To try a change on the real devices, +`check-layers.sh` enforces the module layering described in the architecture doc. `check-config-skill.py` runs the +skill's checker against the parser's own test files, so a change to `ConfigCodec` that the checker misses fails there. To try a change on the real devices, use the [verify-hyperpoint](.claude/skills/verify-hyperpoint/SKILL.md) skill: it replaces the running app with the build under test and reads device state back as proof. diff --git a/scripts/check-config-skill.py b/scripts/check-config-skill.py new file mode 100755 index 0000000..b7001aa --- /dev/null +++ b/scripts/check-config-skill.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Keeps the config skill's checker in step with the app's parser. + +The documented example in docs/architecture.md must pass skills/hyperpoint-config/scripts/check_config.py, and every +invalid file in Tests/HyperpointKitTests/ConfigCodecTests.swift must be rejected at the path the Swift test expects. +Run it from the repository root after changing ConfigCodec. +""" + +import importlib.util +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +spec = importlib.util.spec_from_file_location("check_config", ROOT / "skills/hyperpoint-config/scripts/check_config.py") +checker = importlib.util.module_from_spec(spec) +spec.loader.exec_module(checker) + + +def documented_example(): + text = (ROOT / "docs/architecture.md").read_text() + start = text.index("```json\n") + len("```json\n") + return text[start:text.index("\n```", start)] + + +def swift_string(literal): + return re.sub(r"\\(.)", lambda m: {"n": "\n", "t": "\t"}.get(m.group(1), m.group(1)), literal) + + +def mutations(): + """(find, replace, path) for each Mutation in the Swift test, joining literals split with +.""" + text = (ROOT / "Tests/HyperpointKitTests/ConfigCodecTests.swift").read_text() + block = text[text.index("static let mutations"):text.index("@Test(arguments: mutations)")] + for match in re.finditer(r"Mutation\((.*?)\)(?=,\s*(?:Mutation|\]))", block, re.S): + parts = re.split(r"\b(find|replace|path):", match.group(1)) + fields = { + label: "".join(swift_string(s) for s in re.findall(r'"((?:[^"\\]|\\.)*)"', segment)) + for label, segment in zip(parts[1::2], parts[2::2]) + } + yield fields["find"], fields["replace"], fields["path"] + + +def main(): + example = documented_example() + failures = [] + for name, text in [("documented example", example), + ("chord aliases", example.replace('"shift+cmd+4"', '"Command+Shift+4"'))]: + try: + checker.check(text) + except checker.Problem as problem: + failures.append(f"{name} is rejected: {problem}") + cases = list(mutations()) + for find, replace, path in cases: + if find not in example: + failures.append(f"mutation for {path!r} does not apply to the documented example") + continue + try: + checker.check(example.replace(find, replace)) + failures.append(f"accepted, but the app rejects it at {path!r}: {replace[:60]}") + except checker.Problem as problem: + if problem.path != path: + failures.append(f"rejected at {problem.path!r}, the app at {path!r} ({problem.message})") + for failure in failures: + print(failure) + print(f"{len(cases)} invalid files and 2 valid ones checked, {len(failures)} disagreements") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/site/README.md b/site/README.md index 6ec6f39..c0ffcb9 100644 --- a/site/README.md +++ b/site/README.md @@ -33,7 +33,7 @@ that menu with real device rows and the reconnect note. downloads, and the proof line says it is notarized, its version and what it runs on. **Order.** Hero, problem to solution (before and after), tagline reveal, four benefit cards with stories, three -smaller features, how it works in three steps, proof (the owner's real config and four facts), FAQ (10 questions with +smaller features, how it works in three steps, proof (the owner's real config and six facts), FAQ (12 questions with FAQ schema), final CTA with risk reversal, footer. **Benefits.** Settings that survive sleep. A hyper key on Caps Lock. A wheel that glides. Change a button by pressing diff --git a/site/index.html b/site/index.html index 2a14365..0c3f578 100644 --- a/site/index.html +++ b/site/index.html @@ -83,6 +83,14 @@ "text": "Not for what Hyperpoint covers: button rules and gestures, smooth scrolling, hold to scroll, the hyper key and remapping keys a mouse sends. Karabiner can also rewrite any key on any keyboard with complex rules. If you rely on those, keep it for them." } }, + { + "@type": "Question", + "name": "Can an AI agent change my settings?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Yes. Install the Hyperpoint config skill in Claude Code, then ask for what you want, like \"make the gesture button open App Exposé\". It knows every rule the app enforces, checks the edit before saving it and reads the app's log to confirm it took." + } + }, { "@type": "Question", "name": "Why does it need Accessibility and Input Monitoring?", @@ -469,7 +477,7 @@

    One Mac, four devices, one file

    }
    • -
    • Agents can edit itIt is plain JSON, so you or a coding agent like Claude Code can change any rule. Hyperpoint applies the file the moment it is saved.
    • +
    • Agents can edit itIt is plain JSON, and Hyperpoint applies it the moment it is saved. Install the config skill and ask Claude Code for a change in plain words.
    • No network codeThe app reads your devices and one file. It never connects to the internet.
    • Mistakes are caughtAn invalid file is rejected whole with the path of the error. The last good settings keep running.
    • Quit, and it is goneQuitting clears the key mappings Hyperpoint made, so Caps Lock types as Caps Lock again.
    • @@ -508,6 +516,10 @@

      Questions before you download

      Do I still need BetterMouse or Karabiner?

      Not for what Hyperpoint covers: button rules and gestures, smooth scrolling, hold to scroll, the hyper key and remapping keys a mouse sends. Karabiner can also rewrite any key on any keyboard with complex rules. If you rely on those, keep it for them.

      +
      + Can an AI agent change my settings? +

      Yes. Install the Hyperpoint config skill in Claude Code, then ask for what you want, like "make the gesture button open App Exposé". It knows every rule the app enforces, checks the edit before saving it and reads the app's log to confirm it took.

      +
      Why does it need Accessibility and Input Monitoring?

      Accessibility lets the hyper key and smooth scrolling change keys and scrolls as they happen. Input Monitoring lets Hyperpoint set up the buttons on Logitech mice. Until both are on, the menu says Needs attention and nothing is applied.

      diff --git a/site/styles.css b/site/styles.css index 81db633..721b6bc 100644 --- a/site/styles.css +++ b/site/styles.css @@ -532,6 +532,8 @@ main { padding-top: calc(80px + env(safe-area-inset-top, 0px)); } .facts li { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: 12px; } .facts .icon { margin-top: 2px; color: var(--accent); } .facts b { display: block; color: var(--fg); font-weight: 600; } +.facts a, .faq-list .a a { color: var(--fg); text-underline-offset: 3px; transition: color 700ms var(--fluid); } +.facts a:hover, .faq-list .a a:hover { color: var(--accent); } .code { min-width: 0; border-radius: 24px; background: var(--surface); border: 1px solid var(--line); overflow: hidden; } diff --git a/skills/hyperpoint-config/SKILL.md b/skills/hyperpoint-config/SKILL.md new file mode 100644 index 0000000..3f8486b --- /dev/null +++ b/skills/hyperpoint-config/SKILL.md @@ -0,0 +1,199 @@ +--- +name: hyperpoint-config +description: Read, explain and change the Hyperpoint config file (~/.config/hyperpoint/config.json), which sets up mouse buttons, Logitech gestures, smooth scrolling, hold to scroll and the Caps Lock hyper key on macOS. Use whenever the user wants to remap a mouse button or a key a mouse sends, add or change a Hyperpoint rule or shortcut, tune scrolling, set up or ignore a mouse or keyboard, turn the hyper key on or off, or asks what their Hyperpoint setup does, even if they never name the file. +--- + +# Hyperpoint config + +Hyperpoint is a macOS menu bar app. Everything it does comes from one JSON file: + +``` +~/.config/hyperpoint/config.json +``` + +The app watches that file and applies it the moment it is saved, to every connected device. Its settings window +edits the same file and nothing else. A file with any mistake is rejected whole: the app keeps running the last good +settings and its menu shows "Config file has an error" with the path of the mistake, such as +`devices[1].rules[0].when`. So a bad edit never breaks the user's mouse, but it also changes nothing until it is fixed. + +## Workflow + +1. **Read the current file** and say in plain words what it sets up before changing anything. + + ```bash + cat ~/.config/hyperpoint/config.json + ``` + +2. **Back it up.** + + ```bash + cp ~/.config/hyperpoint/config.json ~/.config/hyperpoint/config.json.bak + ``` + +3. **Make the smallest edit that does what the user asked.** Keep every other device, rule and setting as it is. + Write the new version to a temporary file first, for example by loading the JSON, changing it and dumping it with + two-space indentation. + +4. **Check it with the bundled checker** before it replaces the real file. It applies the app's own rules and + prints the first problem with the same path the app would show. + + ```bash + python3 /scripts/check_config.py /tmp/hyperpoint-config.json + ``` + + Fix what it reports and check again. When it prints `ok`, move the file into place: + + ```bash + mv /tmp/hyperpoint-config.json ~/.config/hyperpoint/config.json + ``` + +5. **Confirm the app took it.** Use `/usr/bin/log`, because zsh has a `log` builtin that prints nothing here. + + ```bash + /usr/bin/log show --last 2m --style compact --predicate 'subsystem == "dev.emir.hyperpoint"' \ + | grep -E 'state=|reasons=configChanged' + ``` + + A `state=` line with `notices=[Config file has an error]` means the app rejected the file: restore the backup or + fix it. Otherwise the app applied it. Devices whose keys it maps (keyboards with the hyper key, mice with key + rules) log `apply ... reasons=configChanged ... applied`, and a Logitech mouse whose diverted controls changed logs + `hidpp ... reasons=configChanged ... applied`. Changing only what a button does logs nothing, and the menu logs a + `state=` line only when what it shows changes, so a quiet log after a clean check is normal. If + `pgrep -x Hyperpoint` prints nothing, the app is not running and reads the file at its next launch. + +6. **Tell the user what changed** in terms of buttons and keys ("the gesture button now opens Mission Control"), + and what to press to try it. + +## Finding a device + +Each profile names its device by vendor and product id, written as 4-digit hex strings, or as +`{ "builtIn": true }` for the Mac's own keyboard. + +Hyperpoint logs every device it sees, with its ids, and `profile=none` for one that has no profile yet: + +```bash +/usr/bin/log show --last 7d --style compact \ + --predicate 'subsystem == "dev.emir.hyperpoint" AND category == "devices"' | grep -E ' (present|connect) ' +``` + +Without the log, `hidutil list` shows every HID device with its `VendorID` and `ProductID` (`0x46d` becomes +`"0x046D"`). Trackpads, including the built-in one, cannot be configured. + +For a Logitech mouse, the `discovered` lines in the same log list its HID++ controls, for example +`controls=[0x50,0x51,0x52+divert+rawXY,...,0xC3+divert+rawXY] thumbWheel=true`. Only a control marked `+divert` can +have a rule, and only one marked `+rawXY` can take hold-and-move rules. Known ids: `0x50` left, `0x51` right, `0x52` +middle, `0x53` back, `0x56` forward, `0xC3` gesture button, `0xC4` top button (MX Master 3). When the user cannot +say which control a button is, have them open Settings › Buttons, click Add Rule and press it: the app writes a rule +for exactly that control, which you can then edit. + +## The file + +```json +{ + "version": 1, + "hyper": { "enabled": true, "key": "capsLock", "onTap": "nothing" }, + "devices": [ + { + "match": { "vendor": "0x046D", "product": "0xB023" }, + "name": "MX Master 3", + "rules": [ + { "control": { "hidpp": "0xC3" }, "when": "click", "do": "missionControl" }, + { "control": { "hidpp": "0xC3" }, "when": "holdMoveLeft", "do": "spaceLeft" }, + { "control": { "hidpp": "0xC4" }, "when": "click", "do": { "shortcut": "shift+cmd+4" } }, + { "control": "thumbWheel", "when": "scroll", "do": "reverse" }, + { "control": { "button": 1 }, "when": "holdMove", "do": "scroll" } + ], + "gestures": { "fire": "pastDistance", "distance": 120 }, + "scrolling": { + "smooth": { "duration": 4, "brake": 2, "easeIn": 60, "curve": [ + { "stepsPerSecond": 1, "pointsPerSecond": 40 }, + { "stepsPerSecond": 120, "pointsPerSecond": 9000 } + ] }, + "holdToScroll": { "speed": 1, "direction": "grab", "momentum": true } + } + }, + { "match": { "vendor": "0x1EA7", "product": "0x0002" }, "name": "2.4G Mouse", "kind": "mouse", "rules": [ + { "control": { "key": "escape" }, "when": "hold", "do": { "modifier": "rightOption" } }, + { "control": { "key": "return" }, "when": "click", "do": { "shortcut": "opt+shift+s" } } + ] }, + { "match": { "builtIn": true }, "name": "Built-in Keyboard", "hyper": true } + ], + "ignored": [] +} +``` + +**Top level.** `version` is always `1`. `devices` holds one profile per device. `ignored` lists matches for devices +the user chose to leave alone; a device cannot be in both. + +**`hyper`** applies to every keyboard whose profile has `"hyper": true`. `key` is the key that becomes the hyper key +(⌃⌥⇧⌘ while held), almost always `"capsLock"`. `onTap` can only be `"nothing"` for now. `enabled: false` turns the +hyper key off on every keyboard while each profile keeps its own `hyper` for later. + +**A profile** needs `match` and `name`. Everything else is optional, and a missing section leaves that part of the +device as macOS has it. + +| Key | Value | +|---|---| +| `kind` | `"mouse"` or `"keyboard"`. Only a keyboard can host the hyper key. Leave it out and the device's interfaces decide, which makes a mouse that sends keys look like a keyboard | +| `rules` | List of rules, below | +| `gestures` | `fire`: `"pastDistance"` (fires once the hand moves `distance` sensor counts) or `"onRelease"` (fires when the button is let go). `distance`: 20 to 2000, default 120 | +| `scrolling` | Below | +| `hyper` | `true` makes the hyper key work on this keyboard | + +### Rules + +A rule is `{ "control": …, "when": …, "do": … }`. Only these combinations work; the app rejects any other. + +| Control | `when` | `do` | +|---|---|---| +| `{ "hidpp": "0xC3" }`, a Logitech HID++ control | `click`, or `holdMoveUp` / `Down` / `Left` / `Right` | a system action or a shortcut | +| | `holdMove` (any direction) | `"scroll"` | +| `"thumbWheel"` | `scroll` | `"reverse"` (reverses horizontal scrolling) | +| `{ "key": "escape" }`, a key a mouse sends | `click` | a shortcut | +| | `hold` | `{ "modifier": … }` | +| `{ "button": 1 }`, a plain mouse button | `click` | a system action or a shortcut | +| | `holdMove` | `"scroll"` | + +- **Plain buttons** are numbered as macOS numbers them: 1 right, 2 middle, 3 back, 4 forward, up to 31. The left + button, 0, cannot have rules. +- **System actions:** `"missionControl"`, `"appExpose"`, `"spaceLeft"`, `"spaceRight"`. +- **Shortcuts:** `{ "shortcut": "shift+cmd+4" }`. Modifiers, in any order and any case: `ctrl`/`control`, + `opt`/`option`/`alt`, `shift`, `cmd`/`command`. The last part is the key. +- **Held modifiers:** `leftControl`, `leftShift`, `leftOption`, `leftCommand`, `rightControl`, `rightShift`, + `rightOption`, `rightCommand`. They are mapped at the HID layer, so they work in password fields too. +- **Key names:** `a` to `z`, `0` to `9`, `return`, `escape`, `delete`, `tab`, `space`, `minus`, `equal`, + `leftBracket`, `rightBracket`, `backslash`, `semicolon`, `quote`, `grave`, `comma`, `period`, `slash`, `capsLock`, + `f1` to `f12`, `home`, `pageUp`, `forwardDelete`, `end`, `pageDown`, `left`, `right`, `up`, `down`. +- **One rule per control and `when`.** A control with `holdMove` cannot also have `holdMoveUp`-style swipes, since + both read the same movement. A key held as a modifier has no click left to give, so it cannot also have a `click` + rule. +- With a `holdMove` scroll rule, a quick press of a plain button still clicks as usual. On a HID++ control a quick + press does nothing unless the control also has a `click` rule. + +### Scrolling + +| Key | Value | +|---|---| +| `smooth` | Smooth scrolling for the main wheel. `duration` 1 to 20 (tenths of a second each step glides), `brake` 0.5 to 10 (higher stops harder first), `easeIn` 0 to 300 ms (default 60), and `curve` | +| `smooth.curve` | How far the page moves for how fast the wheel spins: 2 or more points of `stepsPerSecond` (1 to 150, strictly rising) and `pointsPerSecond` (10 to 30000, never falling), joined on log scales. To scroll faster overall, multiply every `pointsPerSecond` by the same factor. The default is 1→40, 5→250, 15→900, 40→2800, 120→9000 | +| `holdToScroll` | How a `holdMove` scroll rule scrolls. `speed` 0.25 to 4 (1 follows the hand), `direction` `"grab"` (the page moves with the hand) or `"wheel"` (moving down scrolls down), `momentum` true or false | +| `hiResWheel`, `clickThrough` | Accepted and saved, but they do nothing yet. Do not offer them as a fix | + +## Recipes + +- **New mouse with no profile:** add `{ "match": …, "name": "…", "rules": [] }` and the rules the user wants. For a + mouse that sends keys from a keyboard interface, add `"kind": "mouse"`. +- **Hyper key on another keyboard:** add `"hyper": true` to its profile, or a new profile + `{ "match": …, "name": "…", "hyper": true }`. +- **Turn the hyper key off everywhere:** set `"hyper": { …, "enabled": false }`. +- **Hold a button to scroll:** a `holdMove` → `"scroll"` rule on it, plus `scrolling.holdToScroll` if the user wants + a different speed or direction. +- **Stop listing a device:** move its match into `ignored` and remove its profile. + +## Rules of thumb + +- Never add keys the app does not know or set a key to `null`: both make the app reject the whole file. Leave an + optional key out instead. +- Keep `version` at `1` and keep the user's other devices and rules. +- When a request is ambiguous (which button, which keyboard), ask before editing. +- If the checker and the app ever disagree, the app is right: read the error from its menu or Settings › General. diff --git a/skills/hyperpoint-config/scripts/check_config.py b/skills/hyperpoint-config/scripts/check_config.py new file mode 100755 index 0000000..c72024b --- /dev/null +++ b/skills/hyperpoint-config/scripts/check_config.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +"""Checks a Hyperpoint config file the way the app's parser does, before the app sees it. + + python3 check_config.py [path] # default ~/.config/hyperpoint/config.json + +Prints the devices it found, or the first problem with its path (such as devices[1].rules[0].when), which is the +path the app's menu would show. Exits 0 when the app would accept the file, 1 when it would reject it, 2 when the +file cannot be read. It mirrors Sources/HyperpointKit/Model/ConfigCodec.swift; the app stays the judge. +""" + +import json +import os +import sys + +DEFAULT_PATH = os.path.expanduser("~/.config/hyperpoint/config.json") + +KEYS = ( + "a b c d e f g h i j k l m n o p q r s t u v w x y z 1 2 3 4 5 6 7 8 9 0 return escape delete tab space minus " + "equal leftBracket rightBracket backslash semicolon quote grave comma period slash capsLock f1 f2 f3 f4 f5 f6 f7 " + "f8 f9 f10 f11 f12 home pageUp forwardDelete end pageDown right left down up" +).split() +KEY_NAMES = {name.lower() for name in KEYS} +MODIFIER_NAMES = {"ctrl", "control", "opt", "option", "alt", "shift", "cmd", "command"} +HELD_MODIFIERS = { + "leftControl", "leftShift", "leftOption", "leftCommand", + "rightControl", "rightShift", "rightOption", "rightCommand", +} +SYSTEM_ACTIONS = {"missionControl", "appExpose", "spaceLeft", "spaceRight"} +DIRECTIONS = ("Up", "Down", "Left", "Right") +TRIGGERS = {"click", "hold", "holdMove", "scroll"} | {"holdMove" + d for d in DIRECTIONS} + +# Which triggers each kind of control takes, and which actions each trigger may do. "swipe" is hold and move one +# way, "holdMove" hold and move any way. +RULE_TABLE = { + "hidpp": {"click": {"system", "shortcut"}, "swipe": {"system", "shortcut"}, "holdMove": {"scroll"}}, + "thumbWheel": {"scroll": {"reverse"}}, + "key": {"click": {"shortcut"}, "hold": {"modifier"}}, + "button": {"click": {"system", "shortcut"}, "holdMove": {"scroll"}}, +} + + +class Problem(Exception): + def __init__(self, path, message): + super().__init__(message) + self.path = path + self.message = message + + def __str__(self): + return f"{self.path}: {self.message}" if self.path else self.message + + +def join(path, key): + if isinstance(key, int): + return f"{path}[{key}]" + return f"{path}.{key}" if path else key + + +def g(number): + """A number as Swift's %g writes it.""" + return f"{number:g}" + + +# Decoding, in the order the app's decoder reads fields, so the first problem is the one the app reports. + +def obj(value, path): + if not isinstance(value, dict): + raise Problem(path, "expected an object") + return value + + +def expect(value, path, kind): + checks = { + "a string": lambda v: isinstance(v, str), + "true or false": lambda v: isinstance(v, bool), + "a number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool), + "a list": lambda v: isinstance(v, list), + } + if not checks[kind](value): + raise Problem(path, f"expected {kind}") + return value + + +def whole(value, path): + expect(value, path, "a number") + if value != int(value): + raise Problem(path, f"Parsed JSON number <{value}> does not fit in Int.") + return int(value) + + +def required(section, key, path): + if key not in section: + raise Problem(join(path, key), "missing key") + if section[key] is None: + raise Problem(join(path, key), "expected a value") + return section[key] + + +def optional(section, key): + return section.get(key) + + +def one_of(value, path, allowed, what): + expect(value, path, "a string") + if value not in allowed: + raise Problem(path, f'"{value}" is not a valid {what}. Use one of: {", ".join(sorted(allowed))}') + return value + + +def hex_id(value, path): + expect(value, path, "a string") + digits = value[2:] + if value[:2] not in ("0x", "0X") or not digits or any(c not in "0123456789abcdefABCDEF" for c in digits) \ + or int(digits, 16) > 0xFFFF: + raise Problem(path, f'"{value}" is not a hex id such as "0x046D"') + return int(digits, 16) + + +def key_name(value, path): + expect(value, path, "a string") + if value.lower() not in KEY_NAMES: + raise Problem(path, f'unknown key "{value}"') + return value.lower() + + +def decode_match(value, path): + obj(value, path) + if value.get("builtIn") is not None: + built_in = expect(value["builtIn"], join(path, "builtIn"), "true or false") + if not built_in or "vendor" in value or "product" in value: + raise Problem(join(path, "builtIn"), 'write {"builtIn": true} alone, or a vendor and product instead') + return ("builtIn",) + vendor = hex_id(required(value, "vendor", path), join(path, "vendor")) + product = hex_id(required(value, "product", path), join(path, "product")) + return ("device", vendor, product) + + +def decode_control(value, path): + if isinstance(value, str): + if value != "thumbWheel": + raise Problem(path, f'unknown control "{value}"') + return ("thumbWheel",) + obj(value, path) + present = [key for key in ("hidpp", "key", "button") if key in value] + if len(present) != 1: + raise Problem(path, 'a control is "thumbWheel" or one of {"hidpp"}, {"key"}, {"button"}') + kind = present[0] + inner = required(value, kind, path) + if kind == "hidpp": + return ("hidpp", hex_id(inner, join(path, "hidpp"))) + if kind == "key": + return ("key", key_name(inner, join(path, "key"))) + return ("button", whole(inner, join(path, "button"))) + + +def decode_trigger(value, path): + expect(value, path, "a string") + if value not in TRIGGERS: + raise Problem(path, f'unknown trigger "{value}"') + return value + + +def decode_chord(value, path): + expect(value, path, "a string") + tokens = value.split("+") + if tokens[-1].lower() not in KEY_NAMES: + raise Problem(path, f'"{value}" does not end in a key name such as "s" or "4"') + for token in tokens[:-1]: + if token.lower() not in MODIFIER_NAMES: + raise Problem(path, f'unknown modifier "{token}" in "{value}"') + return value.lower() + + +def decode_action(value, path): + if isinstance(value, str): + if value in ("reverse", "scroll"): + return (value,) + if value in SYSTEM_ACTIONS: + return ("system", value) + raise Problem(path, f'unknown action "{value}"') + obj(value, path) + present = [key for key in ("shortcut", "modifier") if key in value] + if len(present) != 1: + raise Problem(path, 'an action is a name such as "missionControl", or {"shortcut"} or {"modifier"}') + kind = present[0] + inner = required(value, kind, path) + if kind == "shortcut": + return ("shortcut", decode_chord(inner, join(path, "shortcut"))) + return ("modifier", one_of(inner, join(path, "modifier"), HELD_MODIFIERS, "modifier")) + + +def decode_rule(value, path): + obj(value, path) + return { + "control": decode_control(required(value, "control", path), join(path, "control")), + "when": decode_trigger(required(value, "when", path), join(path, "when")), + "do": decode_action(required(value, "do", path), join(path, "do")), + } + + +def decode_curve(value, path): + expect(value, path, "a list") + points = [] + for index, point in enumerate(value): + point_path = join(path, index) + obj(point, point_path) + points.append(( + expect(required(point, "stepsPerSecond", point_path), join(point_path, "stepsPerSecond"), "a number"), + expect(required(point, "pointsPerSecond", point_path), join(point_path, "pointsPerSecond"), "a number"), + )) + if len(points) < 2: + raise Problem(path, "needs at least 2 points") + for index, (rate, speed) in enumerate(points): + point_path = join(path, index) + if not 1 <= rate <= 150: + raise Problem(join(point_path, "stepsPerSecond"), "must be between 1 and 150") + if not 10 <= speed <= 30_000: + raise Problem(join(point_path, "pointsPerSecond"), "must be between 10 and 30000") + if index > 0 and rate <= points[index - 1][0]: + raise Problem(join(point_path, "stepsPerSecond"), "must be more than the point before") + if index > 0 and speed < points[index - 1][1]: + raise Problem(join(point_path, "pointsPerSecond"), "must be at least the point before") + return points + + +def decode_smooth(value, path): + obj(value, path) + smooth = { + "duration": expect(required(value, "duration", path), join(path, "duration"), "a number"), + "brake": expect(required(value, "brake", path), join(path, "brake"), "a number"), + "easeIn": 60, + } + if optional(value, "easeIn") is not None: + smooth["easeIn"] = expect(value["easeIn"], join(path, "easeIn"), "a number") + if optional(value, "curve") is not None: + decode_curve(value["curve"], join(path, "curve")) + else: + # Files from before the speed curve: these keys convert to one. + acceleration = optional(value, "acceleration") + if acceleration is not None and not isinstance(acceleration, bool): + acceleration_path = join(path, "acceleration") + obj(acceleration, acceleration_path) + for key in ("from", "to", "max"): + expect(required(acceleration, key, acceleration_path), join(acceleration_path, key), "a number") + for key in ("speed", "startBoost"): + if optional(value, key) is not None: + expect(value[key], join(path, key), "a number") + return smooth + + +def decode_scrolling(value, path): + obj(value, path) + scrolling = {} + if optional(value, "smooth") is not None: + scrolling["smooth"] = decode_smooth(value["smooth"], join(path, "smooth")) + if optional(value, "hiResWheel") is not None: + expect(value["hiResWheel"], join(path, "hiResWheel"), "true or false") + if optional(value, "holdToScroll") is not None: + hold_path = join(path, "holdToScroll") + hold = obj(value["holdToScroll"], hold_path) + scrolling["holdToScroll"] = { + "speed": expect(required(hold, "speed", hold_path), join(hold_path, "speed"), "a number"), + } + one_of(required(hold, "direction", hold_path), join(hold_path, "direction"), {"grab", "wheel"}, "direction") + expect(required(hold, "momentum", hold_path), join(hold_path, "momentum"), "true or false") + if optional(value, "clickThrough") is not None: + click_path = join(path, "clickThrough") + click = obj(value["clickThrough"], click_path) + for key in ("left", "right"): + expect(required(click, key, click_path), join(click_path, key), "true or false") + return scrolling + + +def decode_profile(value, path): + obj(value, path) + profile = { + "match": decode_match(required(value, "match", path), join(path, "match")), + "name": expect(required(value, "name", path), join(path, "name"), "a string"), + "kind": None, + "rules": [], + "gestures": None, + "scrolling": {}, + "hyper": False, + } + if optional(value, "kind") is not None: + profile["kind"] = one_of(value["kind"], join(path, "kind"), {"mouse", "keyboard"}, "kind") + if optional(value, "rules") is not None: + rules_path = join(path, "rules") + expect(value["rules"], rules_path, "a list") + profile["rules"] = [decode_rule(rule, join(rules_path, i)) for i, rule in enumerate(value["rules"])] + if optional(value, "gestures") is not None: + gestures_path = join(path, "gestures") + gestures = obj(value["gestures"], gestures_path) + one_of(required(gestures, "fire", gestures_path), join(gestures_path, "fire"), {"pastDistance", "onRelease"}, + "fire") + profile["gestures"] = { + "distance": whole(required(gestures, "distance", gestures_path), join(gestures_path, "distance")), + } + if optional(value, "scrolling") is not None: + profile["scrolling"] = decode_scrolling(value["scrolling"], join(path, "scrolling")) + if optional(value, "hyper") is not None: + profile["hyper"] = expect(value["hyper"], join(path, "hyper"), "true or false") + # Files from before hold to scroll: right-button pan becomes a hold-and-move scroll rule on the right button. + pan = value["scrolling"].get("rightButtonPan") if isinstance(value.get("scrolling"), dict) else None + if pan is not None: + pan_path = join(join(path, "scrolling"), "rightButtonPan") + expect(required(obj(pan, pan_path), "momentum", pan_path), join(pan_path, "momentum"), "true or false") + rule = {"control": ("button", 1), "when": "holdMove", "do": ("scroll",)} + if not any(r["control"] == rule["control"] and r["when"] == rule["when"] for r in profile["rules"]): + profile["rules"].append(rule) + return profile + + +def decode(tree): + obj(tree, "") + config = {"version": whole(required(tree, "version", ""), "version")} + hyper = obj(required(tree, "hyper", ""), "hyper") + if optional(hyper, "enabled") is not None: + expect(hyper["enabled"], "hyper.enabled", "true or false") + key_name(required(hyper, "key", "hyper"), "hyper.key") + one_of(required(hyper, "onTap", "hyper"), "hyper.onTap", {"nothing"}, "onTap") + expect(required(tree, "devices", ""), "devices", "a list") + config["devices"] = [decode_profile(p, join("devices", i)) for i, p in enumerate(tree["devices"])] + expect(required(tree, "ignored", ""), "ignored", "a list") + config["ignored"] = [decode_match(m, join("ignored", i)) for i, m in enumerate(tree["ignored"])] + return config + + +# Unknown keys and nulls, checked like the app: every key in sorted order, depth first. + +def schema(tree): + """The keys the app knows at each place in this file, shaped like the file.""" + def match(m): + return {"builtIn": True} if m.get("builtIn") is not None else {"vendor": 0, "product": 0} + + def control(c): + return {k: 0 for k in ("hidpp", "key", "button") if k in c} if isinstance(c, dict) else c + + def action(a): + return {k: 0 for k in ("shortcut", "modifier") if k in a} if isinstance(a, dict) else a + + def scrolling(s): + known = { + "smooth": {"duration": 0, "brake": 0, "easeIn": 0, "curve": [ + {"stepsPerSecond": 0, "pointsPerSecond": 0} for _ in (s.get("smooth") or {}).get("curve") or [] + ]}, + "hiResWheel": 0, + "holdToScroll": {"speed": 0, "direction": 0, "momentum": 0}, + "clickThrough": {"left": 0, "right": 0}, + } + if "curve" not in (s.get("smooth") or {}): + known["smooth"].update({"speed": 0, "startBoost": 0, "acceleration": {"from": 0, "to": 0, "max": 0}}) + known["rightButtonPan"] = {"momentum": 0} + return known + + devices = [] + for profile in tree["devices"]: + devices.append({ + "match": match(profile["match"]), + "name": 0, + "kind": 0, + "rules": [{"control": control(r["control"]), "when": 0, "do": action(r["do"])} + for r in profile.get("rules") or []], + "gestures": {"fire": 0, "distance": 0}, + "scrolling": scrolling(profile.get("scrolling") or {}), + "hyper": 0, + }) + return { + "version": 0, + "hyper": {"enabled": 0, "key": 0, "onTap": 0}, + "devices": devices, + "ignored": [match(m) for m in tree["ignored"]], + } + + +def unknown_key(user, known, path): + if isinstance(user, dict) and isinstance(known, dict): + for key in sorted(user): + child = join(path, key) + if user[key] is None: + raise Problem(child, "null is not allowed. Leave the key out instead") + if key not in known: + raise Problem(child, "unknown key") + unknown_key(user[key], known[key], child) + elif isinstance(user, list) and isinstance(known, list): + for index, (item, known_item) in enumerate(zip(user, known)): + unknown_key(item, known_item, join(path, index)) + + +# The checks that need the whole file. + +def trigger_kind(trigger): + if trigger == "holdMove": + return "holdMove" + return "swipe" if trigger.startswith("holdMove") else trigger + + +def action_kind(action): + return action[0] + + +def conflict(trigger, other): + pair = {trigger_kind(trigger), trigger_kind(other)} + if pair == {"holdMove", "swipe"}: + return "scrolling and swipes both use the movement" + if pair == {"click", "hold"}: + return "a key held as a modifier becomes that modifier, so it has no click of its own" + return None + + +def validate(config): + if config["version"] != 1: + raise Problem("version", f"version {config['version']} is not supported") + for index, profile in enumerate(config["devices"]): + path = join("devices", index) + earlier = [p["match"] for p in config["devices"][:index]] + if profile["match"] in earlier: + raise Problem(join(path, "match"), f"the same device as devices[{earlier.index(profile['match'])}]") + if profile["match"] in config["ignored"]: + raise Problem(join(path, "match"), "this device is also in ignored") + gestures = profile["gestures"] + if gestures and not 20 <= gestures["distance"] <= 2000: + raise Problem(join(path, "gestures.distance"), "must be between 20 and 2000") + hold = profile["scrolling"].get("holdToScroll") + if hold and not 0.25 <= hold["speed"] <= 4: + raise Problem(join(path, "scrolling.holdToScroll.speed"), "must be between 0.25 and 4") + smooth = profile["scrolling"].get("smooth") + if smooth: + for key, low, high in (("duration", 1, 20), ("brake", 0.5, 10), ("easeIn", 0, 300)): + if not low <= smooth[key] <= high: + raise Problem(join(path, f"scrolling.smooth.{key}"), f"must be between {g(low)} and {g(high)}") + rules = profile["rules"] + for rule_index, rule in enumerate(rules): + rule_path = join(join(path, "rules"), rule_index) + control, trigger = rule["control"], rule["when"] + if control[0] == "button" and not 1 <= control[1] <= 31: + raise Problem(join(rule_path, "control.button"), + "must be between 1 and 31. 1 is the right button, and the left button, 0, cannot have rules") + actions = RULE_TABLE[control[0]].get(trigger_kind(trigger)) + if actions is None: + raise Problem(join(rule_path, "when"), f'"{trigger}" does not work with this control') + if action_kind(rule["do"]) not in actions: + raise Problem(join(rule_path, "do"), f'this action does not work with "{trigger}" on this control') + for earlier_index, earlier_rule in enumerate(rules[:rule_index]): + if earlier_rule["control"] == control and earlier_rule["when"] == trigger: + raise Problem(rule_path, f"the same control and trigger as rules[{earlier_index}]") + for earlier_index, earlier_rule in enumerate(rules[:rule_index]): + reason = conflict(trigger, earlier_rule["when"]) if earlier_rule["control"] == control else None + if reason: + raise Problem(join(rule_path, "when"), + f'"{trigger}" cannot share a control with rules[{earlier_index}], because {reason}') + + +def check(text): + """The config the app would load from `text`, or the Problem it would reject it with.""" + def no_constants(name): + raise ValueError(f"{name} is not a number JSON allows") + + try: + tree = json.loads(text, parse_constant=no_constants) + except ValueError as error: + raise Problem("", f"not valid JSON. {error}") + config = decode(tree) + unknown_key(tree, schema(tree), "") + validate(config) + return config + + +def describe(match): + return "built-in keyboard" if match[0] == "builtIn" else f"vendor 0x{match[1]:04X}, product 0x{match[2]:04X}" + + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PATH + try: + with open(path, encoding="utf-8") as file: + text = file.read() + except OSError as error: + print(f"cannot read {path}: {error.strerror}", file=sys.stderr) + return 2 + try: + config = check(text) + except Problem as problem: + print(f"rejected: {problem}") + return 1 + print(f"ok: {path}") + for profile in config["devices"]: + parts = [describe(profile["match"])] + if profile["kind"]: + parts.append(profile["kind"]) + parts.append(f"{len(profile['rules'])} rules") + if profile["hyper"]: + parts.append("hyper key") + print(f" {profile['name']} ({', '.join(parts)})") + for match in config["ignored"]: + print(f" ignored: {describe(match)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 1537b11a5f8afd0fbe9d39abefd5b73a1a1f2295 Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 15:11:20 +0500 Subject: [PATCH 07/11] feat(site): deploy to hyperpoint.progremir.dev on Cloudflare The site is now a static-assets Worker, hyperpoint-site, set up with the cf CLI. The served files move to site/public, which wrangler.config.ts names as the assets, so the README and og.py stay out of the upload. cloudflare.config.ts holds the account, the custom domain and 404-page handling. The page gets a canonical URL and an absolute og:image. .github/workflows/site.yml dry-runs the build on pull requests that touch site/ and deploys on pushes to main, which needs a CLOUDFLARE_API_TOKEN repository secret. Co-Authored-By: Claude Opus 5.5 --- .claude/launch.json | 2 +- .github/workflows/site.yml | 42 + .gitignore | 1 + site/.gitignore | 7 + site/README.md | 28 +- site/cloudflare.config.ts | 13 + site/og.py | 4 +- site/package-lock.json | 1653 +++++++++++++++++ site/package.json | 13 + site/{ => public}/404.html | 0 site/{ => public}/apple-touch-icon.png | Bin .../assets/hyperpoint-app-icon.svg | 0 .../hyperpoint-horizontal-color-on-dark.svg | 0 site/{ => public}/favicon.ico | Bin site/{ => public}/favicon.svg | 0 site/{ => public}/icon-192.png | Bin site/{ => public}/icon-512.png | Bin site/{ => public}/index.html | 6 +- site/{ => public}/main.js | 0 site/{ => public}/maskable-512.png | Bin site/{ => public}/og.png | Bin site/{ => public}/privacy.html | 0 site/{ => public}/site.webmanifest | 0 site/{ => public}/styles.css | 0 site/wrangler.config.ts | 5 + 25 files changed, 1759 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/site.yml create mode 100644 site/.gitignore create mode 100644 site/cloudflare.config.ts create mode 100644 site/package-lock.json create mode 100644 site/package.json rename site/{ => public}/404.html (100%) rename site/{ => public}/apple-touch-icon.png (100%) rename site/{ => public}/assets/hyperpoint-app-icon.svg (100%) rename site/{ => public}/assets/hyperpoint-horizontal-color-on-dark.svg (100%) rename site/{ => public}/favicon.ico (100%) rename site/{ => public}/favicon.svg (100%) rename site/{ => public}/icon-192.png (100%) rename site/{ => public}/icon-512.png (100%) rename site/{ => public}/index.html (99%) rename site/{ => public}/main.js (100%) rename site/{ => public}/maskable-512.png (100%) rename site/{ => public}/og.png (100%) rename site/{ => public}/privacy.html (100%) rename site/{ => public}/site.webmanifest (100%) rename site/{ => public}/styles.css (100%) create mode 100644 site/wrangler.config.ts diff --git a/.claude/launch.json b/.claude/launch.json index acd57e9..dce6f2a 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,7 +4,7 @@ { "name": "site", "runtimeExecutable": "python3", - "runtimeArgs": ["-m", "http.server", "4319", "--directory", "site"], + "runtimeArgs": ["-m", "http.server", "4319", "--directory", "site/public"], "port": 4319 } ] diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml new file mode 100644 index 0000000..b6478cb --- /dev/null +++ b/.github/workflows/site.yml @@ -0,0 +1,42 @@ +name: Site + +# Checks the site's build on pull requests and deploys it to hyperpoint.progremir.dev on pushes to main. +on: + pull_request: + paths: ["site/**", ".github/workflows/site.yml"] + push: + branches: [main] + paths: ["site/**", ".github/workflows/site.yml"] + +concurrency: + group: site-${{ github.ref }} + cancel-in-progress: false + +jobs: + site: + runs-on: ubuntu-latest + defaults: + run: + working-directory: site + env: + CF_SEND_TELEMETRY: "false" + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: site/package-lock.json + - run: npm ci + - run: npx cf build + - run: npx cf deploy --prebuilt --dry-run + - name: Deploy + if: github.event_name == 'push' + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + run: | + if [ -z "$CLOUDFLARE_API_TOKEN" ]; then + echo "::error::Add a CLOUDFLARE_API_TOKEN repository secret that can deploy Workers and their custom domains." + exit 1 + fi + npx cf deploy --prebuilt --message "${GITHUB_SHA::7}" diff --git a/.gitignore b/.gitignore index aa22564..c28c80e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ xcuserdata/ .verify-evidence/ .verify-bin/ dist/ +.cloudflare/ diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..7bb1da6 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.cloudflare/ +.wrangler/ +.dev.vars* +!.dev.vars.example +.env* +!.env.example diff --git a/site/README.md b/site/README.md index c0ffcb9..6463b74 100644 --- a/site/README.md +++ b/site/README.md @@ -4,22 +4,30 @@ A static landing page: plain HTML, CSS and JavaScript with no build step. It was [`landing-page-design`](https://github.com/elayadesign/ai-design-skills/tree/main/skills/landing-page-design) and [`design-to-motion`](https://github.com/elayadesign/ai-design-skills/tree/main/skills/design-to-motion) skills. +Live at https://hyperpoint.progremir.dev, served by the `hyperpoint-site` Worker as static assets on Cloudflare. + ```bash -python3 -m http.server 4319 --directory site +python3 -m http.server 4319 --directory site/public ``` -Serve `site/` from the root of a domain. `404.html` and `privacy.html` use root paths (`/styles.css`), so they work -at any depth. Once the site has a domain, make `og:image` in `index.html` an absolute URL. The download buttons link -to the latest GitHub release, which only people with access to the repository can open while it is private. +**Deploying.** `.github/workflows/site.yml` checks the build on pull requests that touch `site/` and deploys it on +pushes to `main`, which needs a `CLOUDFLARE_API_TOKEN` repository secret that can deploy Workers and their custom +domains. To deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in `site/`. +`cloudflare.config.ts` holds the Worker's name, account, domain and 404 handling, and `wrangler.config.ts` names +`public/` as the assets, so only that folder is uploaded. + +`404.html` and `privacy.html` use root paths (`/styles.css`), so they work at any depth. The download buttons link to +the latest GitHub release, which only people with access to the repository can open while it is private. | File | What it is | |---|---| -| `index.html` | The page. The icon sprite (Phosphor, MIT), the config excerpt and the FAQ schema are written inline | -| `styles.css` | Tokens and every section's styles | -| `main.js` | Island nav, scroll reveals, card intros and stories, the tagline reveal | -| `404.html`, `privacy.html` | The branded 404 and the privacy page | -| `og.png` | Social preview, drawn by `og.py` from the logo kit and Geist | -| `favicon.*`, `icon-*.png`, `site.webmanifest`, `assets/` | Copied from `design/logo/` | +| `public/index.html` | The page. The icon sprite (Phosphor, MIT), the config excerpt and the FAQ schema are written inline | +| `public/styles.css` | Tokens and every section's styles | +| `public/main.js` | Island nav, scroll reveals, card intros and stories, the tagline reveal | +| `public/404.html`, `public/privacy.html` | The branded 404 and the privacy page | +| `public/og.png` | Social preview, drawn by `og.py` from the logo kit and Geist | +| `public/favicon.*`, `icon-*.png`, `site.webmanifest`, `assets/` | Copied from `design/logo/` | +| `cloudflare.config.ts`, `wrangler.config.ts`, `package.json` | The Cloudflare project; `cf` and Wrangler are pinned in `package-lock.json` | ## The brief diff --git a/site/cloudflare.config.ts b/site/cloudflare.config.ts new file mode 100644 index 0000000..63a3400 --- /dev/null +++ b/site/cloudflare.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "cf/config"; + +// The site is static: wrangler.config.ts names public/ as the assets, and there is no Worker code. +export default defineConfig({ + accountId: "dd1171d285218486ca1fb1fcec2a759c", + worker: { + name: "hyperpoint-site", + compatibilityDate: "2026-10-19", + domains: ["hyperpoint.progremir.dev"], + assets: { htmlHandling: "auto-trailing-slash", notFoundHandling: "404-page" }, + observability: { enabled: true }, + }, +}); diff --git a/site/og.py b/site/og.py index fbc7a2c..73b15dc 100644 --- a/site/og.py +++ b/site/og.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Draws site/og.png, the 1200 x 630 social preview, from the logo kit and Geist. +"""Draws site/public/og.png, the 1200 x 630 social preview, from the logo kit and Geist. The text is converted to outlines so rsvg-convert needs no installed fonts. Needs fonttools (pip3 install --user fonttools), rsvg-convert (brew install librsvg) and the Geist variable font: @@ -73,7 +73,7 @@ def main(): {icon} """ - out = HERE / "og.png" + out = HERE / "public/og.png" subprocess.run(["rsvg-convert", "-w", str(W), "-h", str(H), "-o", str(out)], input=svg.encode(), check=True) print(f"wrote {out}") diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000..bb11c5a --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,1653 @@ +{ + "name": "hyperpoint-site", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hyperpoint-site", + "devDependencies": { + "cf": "^1.0.0-beta.21", + "wrangler": "^4.152.0" + } + }, + "node_modules/@cloudflare/build-output-utils": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@cloudflare/build-output-utils/-/build-output-utils-0.9.4.tgz", + "integrity": "sha512-5rVdn81HEP2Uugs/wSfXu5XbadaZQkgKZl1xwkKb3kgatSOpTH2vszBXWTRh4IlE1vHnc3LwHcnc2O4TcXVbuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/config": "0.28.0" + } + }, + "node_modules/@cloudflare/config": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@cloudflare/config/-/config-0.28.0.tgz", + "integrity": "sha512-2SaZn5rzNSxAR5VQYIlMm8+zHj2EGfLbaN+abYiPXDI42IFRCHSENpDxjAQpLj2NZnUVszHY4h6T1XljwHk5eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "4.6.5" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/runtime-types": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@cloudflare/runtime-types/-/runtime-types-0.1.15.tgz", + "integrity": "sha512-E8WCMGir4IRp/ksqodMWGpxHJvgtfgW9lbVM5mNAdWrnonMNOtGocyxVt6hG8/RGW9sjFWR7F7Cp3SCF1Cz6zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "miniflare": "5.20261019.2-beta", + "workerd": "1.20261019.1" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.2.tgz", + "integrity": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20261019.1.tgz", + "integrity": "sha512-5ZILbAta/fBQvhmAYeCE7dKMMYIr7N9wJEX50fEGLKSIwGtvT0HzSfHNIK/O7cBQ4gHQ2WfIiPClvwcye6E0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20261019.1.tgz", + "integrity": "sha512-cww2Ab6lVMxpTiEEjKmPI58p5EMo0vYTZSH+3WkqXJdQgc3ES66GuL247+U2B2YWajhhu2tqj7jySrXneU749g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20261019.1.tgz", + "integrity": "sha512-4++VRi9vkQROYqrpZYOixIJuVDmujkwrT4kszsxNFlvcmsX6Mr2GumvxIN3gIixS3vJUh3Umdtkthqj5X0S3Xw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20261019.1.tgz", + "integrity": "sha512-75PVtJOwfjScQ53sbLtQXI7D8yP1c21t+WEUPaYbqkzaz4ZqfR5wAd+zuWs8G5Q7kOj2SQIvEfY5Sn8Y+UVrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20261019.1.tgz", + "integrity": "sha512-z49fvKTETMCnG6wnMcQwxx9CpryFceiKz04FRjq26EkhCyzRNJ6c3UXhyZz3r5f1CXnnBZc5DcU8ZouPkUJCqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cf": { + "version": "1.0.0-beta.21", + "resolved": "https://registry.npmjs.org/cf/-/cf-1.0.0-beta.21.tgz", + "integrity": "sha512-rJqfa8zW7pN0m/fmc11xT3GzcXrqwhtZ9pizO3LZyZJjoOPR+h/us/+ajK0tXPhL+jHqaW/39ODQ481GDUyziQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/build-output-utils": "0.9.4", + "@cloudflare/config": "0.28.0", + "@cloudflare/runtime-types": "0.1.15", + "blake3-wasm": "2.1.5", + "miniflare": "5.20261019.2-beta" + }, + "bin": { + "cf": "bin/cf", + "cloudflare": "bin/cf" + }, + "engines": { + "node": "^22.23.0 || ^24.22.0 || ^26.12.0" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "5.20261019.2-beta", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20261019.2-beta.tgz", + "integrity": "sha512-QNgCA/axizZ7kv7RV3nwbRoz+w4watBlwIsyXjwgYHw4eSw5BrR1F2152s3Kz+Bzy/YxjKFHrZl3LzBRbPgYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.5", + "undici": "7.29.1", + "workerd": "1.20261019.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20261019.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20261019.1.tgz", + "integrity": "sha512-UUYRUq/7DmeCl11YriNbcVbjUZVLtt8uuCXFu+nz77hGFf8d4DEVXoCYtnA+MlBatE4+wxoj47DVqzK3xGBAiw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20261019.1", + "@cloudflare/workerd-darwin-arm64": "1.20261019.1", + "@cloudflare/workerd-linux-64": "1.20261019.1", + "@cloudflare/workerd-linux-arm64": "1.20261019.1", + "@cloudflare/workerd-windows-64": "1.20261019.1" + } + }, + "node_modules/wrangler": { + "version": "4.152.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.152.0.tgz", + "integrity": "sha512-lmXTF1sCDEqBaEMpreGczvg/x3AIh1d/qxc4BN7utQ4jEDwdA4YG0L6kr3ev0TtVLHRb6QLySNAOAunwn+dHeA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.2", + "miniflare": "5.20261019.2-beta", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20261019.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20261019.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..8443ec0 --- /dev/null +++ b/site/package.json @@ -0,0 +1,13 @@ +{ + "name": "hyperpoint-site", + "private": true, + "type": "module", + "scripts": { + "dev": "cf dev", + "deploy": "cf deploy" + }, + "devDependencies": { + "cf": "^1.0.0-beta.21", + "wrangler": "^4.152.0" + } +} diff --git a/site/404.html b/site/public/404.html similarity index 100% rename from site/404.html rename to site/public/404.html diff --git a/site/apple-touch-icon.png b/site/public/apple-touch-icon.png similarity index 100% rename from site/apple-touch-icon.png rename to site/public/apple-touch-icon.png diff --git a/site/assets/hyperpoint-app-icon.svg b/site/public/assets/hyperpoint-app-icon.svg similarity index 100% rename from site/assets/hyperpoint-app-icon.svg rename to site/public/assets/hyperpoint-app-icon.svg diff --git a/site/assets/hyperpoint-horizontal-color-on-dark.svg b/site/public/assets/hyperpoint-horizontal-color-on-dark.svg similarity index 100% rename from site/assets/hyperpoint-horizontal-color-on-dark.svg rename to site/public/assets/hyperpoint-horizontal-color-on-dark.svg diff --git a/site/favicon.ico b/site/public/favicon.ico similarity index 100% rename from site/favicon.ico rename to site/public/favicon.ico diff --git a/site/favicon.svg b/site/public/favicon.svg similarity index 100% rename from site/favicon.svg rename to site/public/favicon.svg diff --git a/site/icon-192.png b/site/public/icon-192.png similarity index 100% rename from site/icon-192.png rename to site/public/icon-192.png diff --git a/site/icon-512.png b/site/public/icon-512.png similarity index 100% rename from site/icon-512.png rename to site/public/icon-512.png diff --git a/site/index.html b/site/public/index.html similarity index 99% rename from site/index.html rename to site/public/index.html index 0c3f578..33c73e4 100644 --- a/site/index.html +++ b/site/public/index.html @@ -5,12 +5,13 @@ Hyperpoint · Mouse buttons, smooth scrolling and a hyper key for macOS + + - - + @@ -30,6 +31,7 @@ "@context": "https://schema.org", "@type": "SoftwareApplication", "name": "Hyperpoint", + "url": "https://hyperpoint.progremir.dev/", "operatingSystem": "macOS 27", "applicationCategory": "UtilitiesApplication", "softwareVersion": "0.1.1", diff --git a/site/main.js b/site/public/main.js similarity index 100% rename from site/main.js rename to site/public/main.js diff --git a/site/maskable-512.png b/site/public/maskable-512.png similarity index 100% rename from site/maskable-512.png rename to site/public/maskable-512.png diff --git a/site/og.png b/site/public/og.png similarity index 100% rename from site/og.png rename to site/public/og.png diff --git a/site/privacy.html b/site/public/privacy.html similarity index 100% rename from site/privacy.html rename to site/public/privacy.html diff --git a/site/site.webmanifest b/site/public/site.webmanifest similarity index 100% rename from site/site.webmanifest rename to site/public/site.webmanifest diff --git a/site/styles.css b/site/public/styles.css similarity index 100% rename from site/styles.css rename to site/public/styles.css diff --git a/site/wrangler.config.ts b/site/wrangler.config.ts new file mode 100644 index 0000000..2b75134 --- /dev/null +++ b/site/wrangler.config.ts @@ -0,0 +1,5 @@ +import { defineWranglerConfig } from "wrangler/experimental-config"; + +export default defineWranglerConfig({ + assetsDirectory: "public", +}); From fb010b9fbccaad080e7c1b939ceaedd6da6726de Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 15:21:25 +0500 Subject: [PATCH 08/11] ci(site): deploys move to Cloudflare's Workers Builds GitHub Actions cannot run jobs on this account right now, and Workers Builds needs no API token in the repository. The workflow loses its deploy step and its push trigger and only checks that pull requests build. site/.node-version pins Node 24 for both, which cf needs at 22.18 or later. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/site.yml | 22 +++------------------- site/.node-version | 1 + site/README.md | 7 ++++--- 3 files changed, 8 insertions(+), 22 deletions(-) create mode 100644 site/.node-version diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index b6478cb..249ed72 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -1,16 +1,10 @@ name: Site -# Checks the site's build on pull requests and deploys it to hyperpoint.progremir.dev on pushes to main. +# Checks that the site still builds on pull requests that touch it. Deploys to hyperpoint.progremir.dev run in +# Cloudflare's Workers Builds on pushes to main, not here. on: pull_request: paths: ["site/**", ".github/workflows/site.yml"] - push: - branches: [main] - paths: ["site/**", ".github/workflows/site.yml"] - -concurrency: - group: site-${{ github.ref }} - cancel-in-progress: false jobs: site: @@ -24,19 +18,9 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: - node-version: 24 + node-version-file: site/.node-version cache: npm cache-dependency-path: site/package-lock.json - run: npm ci - run: npx cf build - run: npx cf deploy --prebuilt --dry-run - - name: Deploy - if: github.event_name == 'push' - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - run: | - if [ -z "$CLOUDFLARE_API_TOKEN" ]; then - echo "::error::Add a CLOUDFLARE_API_TOKEN repository secret that can deploy Workers and their custom domains." - exit 1 - fi - npx cf deploy --prebuilt --message "${GITHUB_SHA::7}" diff --git a/site/.node-version b/site/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/site/.node-version @@ -0,0 +1 @@ +24 diff --git a/site/README.md b/site/README.md index 6463b74..f83496e 100644 --- a/site/README.md +++ b/site/README.md @@ -10,9 +10,10 @@ Live at https://hyperpoint.progremir.dev, served by the `hyperpoint-site` Worker python3 -m http.server 4319 --directory site/public ``` -**Deploying.** `.github/workflows/site.yml` checks the build on pull requests that touch `site/` and deploys it on -pushes to `main`, which needs a `CLOUDFLARE_API_TOKEN` repository secret that can deploy Workers and their custom -domains. To deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in `site/`. +**Deploying.** Cloudflare's Workers Builds deploys `site/` to the `hyperpoint-site` Worker on every push to `main` +that touches it. It runs `npx cf deploy` in `site/` on Node 24 (`.node-version`), with its own API token, so the +repository holds no Cloudflare secret. `.github/workflows/site.yml` only checks that pull requests still build. To +deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in `site/`. `cloudflare.config.ts` holds the Worker's name, account, domain and 404 handling, and `wrangler.config.ts` names `public/` as the assets, so only that folder is uploaded. From feb35349de006443a0710db8e82f14bc5f5f7879 Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 15:36:04 +0500 Subject: [PATCH 09/11] ci(site): previews on workers.dev, and no GitHub workflow Workers Builds will build pushes to main as production and every other branch as a Worker Preview, so the Worker turns on preview URLs, which workers.dev serves with X-Robots-Tag: noindex. Production stays on hyperpoint.progremir.dev alone with workersDev off. The site check workflow goes; Cloudflare's builds cover it. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/site.yml | 26 -------------------------- site/README.md | 12 +++++++----- site/cloudflare.config.ts | 3 +++ 3 files changed, 10 insertions(+), 31 deletions(-) delete mode 100644 .github/workflows/site.yml diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml deleted file mode 100644 index 249ed72..0000000 --- a/.github/workflows/site.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Site - -# Checks that the site still builds on pull requests that touch it. Deploys to hyperpoint.progremir.dev run in -# Cloudflare's Workers Builds on pushes to main, not here. -on: - pull_request: - paths: ["site/**", ".github/workflows/site.yml"] - -jobs: - site: - runs-on: ubuntu-latest - defaults: - run: - working-directory: site - env: - CF_SEND_TELEMETRY: "false" - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version-file: site/.node-version - cache: npm - cache-dependency-path: site/package-lock.json - - run: npm ci - - run: npx cf build - - run: npx cf deploy --prebuilt --dry-run diff --git a/site/README.md b/site/README.md index f83496e..770eed7 100644 --- a/site/README.md +++ b/site/README.md @@ -10,11 +10,13 @@ Live at https://hyperpoint.progremir.dev, served by the `hyperpoint-site` Worker python3 -m http.server 4319 --directory site/public ``` -**Deploying.** Cloudflare's Workers Builds deploys `site/` to the `hyperpoint-site` Worker on every push to `main` -that touches it. It runs `npx cf deploy` in `site/` on Node 24 (`.node-version`), with its own API token, so the -repository holds no Cloudflare secret. `.github/workflows/site.yml` only checks that pull requests still build. To -deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in `site/`. -`cloudflare.config.ts` holds the Worker's name, account, domain and 404 handling, and `wrangler.config.ts` names +**Deploying.** Cloudflare's Workers Builds builds `site/` on every push that touches it. A push to `main` runs +`npx cf deploy` and updates the live site. A push to any other branch runs `npx cf previews deploy`, which publishes +that branch at `https://-hyperpoint-site.progremir.workers.dev` (public, but sent with `X-Robots-Tag: +noindex`). Both run in `site/` on Node 24 (`.node-version`) with Cloudflare's own build token, so the repository +holds no Cloudflare secret. To deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in +`site/`, or `npx cf previews deploy` for a preview of the current branch. +`cloudflare.config.ts` holds the Worker's name, account, domain, preview URLs and 404 handling, and `wrangler.config.ts` names `public/` as the assets, so only that folder is uploaded. `404.html` and `privacy.html` use root paths (`/styles.css`), so they work at any depth. The download buttons link to diff --git a/site/cloudflare.config.ts b/site/cloudflare.config.ts index 63a3400..8d2c9d8 100644 --- a/site/cloudflare.config.ts +++ b/site/cloudflare.config.ts @@ -7,6 +7,9 @@ export default defineConfig({ name: "hyperpoint-site", compatibilityDate: "2026-10-19", domains: ["hyperpoint.progremir.dev"], + // Production is served only on the domain. Previews get noindexed workers.dev URLs. + workersDev: false, + previewUrls: true, assets: { htmlHandling: "auto-trailing-slash", notFoundHandling: "404-page" }, observability: { enabled: true }, }, From 61a79c64e127cfe4d7696e2d2b948993918e2ab3 Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 15:43:31 +0500 Subject: [PATCH 10/11] docs(site): where the Workers Builds connection lives Co-Authored-By: Claude Opus 5.5 --- site/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/README.md b/site/README.md index 770eed7..19fd57c 100644 --- a/site/README.md +++ b/site/README.md @@ -14,7 +14,8 @@ python3 -m http.server 4319 --directory site/public `npx cf deploy` and updates the live site. A push to any other branch runs `npx cf previews deploy`, which publishes that branch at `https://-hyperpoint-site.progremir.workers.dev` (public, but sent with `X-Robots-Tag: noindex`). Both run in `site/` on Node 24 (`.node-version`) with Cloudflare's own build token, so the repository -holds no Cloudflare secret. To deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in +holds no Cloudflare secret. The connection lives on the Worker in the Cloudflare dashboard, under Settings › +Builds, and Cloudflare posts each branch's preview link on its pull request. To deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in `site/`, or `npx cf previews deploy` for a preview of the current branch. `cloudflare.config.ts` holds the Worker's name, account, domain, preview URLs and 404 handling, and `wrangler.config.ts` names `public/` as the assets, so only that folder is uploaded. From b03fdc426282d2ef9b744969ff3e17b95e8925ce Mon Sep 17 00:00:00 2001 From: Emir Amanbekov Date: Mon, 26 Oct 2026 15:50:42 +0500 Subject: [PATCH 11/11] chore(site): the Worker is hyperpoint, set up from the dashboard The project is now created in Cloudflare's dashboard as "hyperpoint", which Workers Builds requires to match the config's Worker name. The README drops the hand-deploy steps: Git builds do all deploys. Co-Authored-By: Claude Opus 5.5 --- site/README.md | 7 +++---- site/cloudflare.config.ts | 2 +- site/package-lock.json | 4 ++-- site/package.json | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/site/README.md b/site/README.md index 19fd57c..12d629b 100644 --- a/site/README.md +++ b/site/README.md @@ -4,7 +4,7 @@ A static landing page: plain HTML, CSS and JavaScript with no build step. It was [`landing-page-design`](https://github.com/elayadesign/ai-design-skills/tree/main/skills/landing-page-design) and [`design-to-motion`](https://github.com/elayadesign/ai-design-skills/tree/main/skills/design-to-motion) skills. -Live at https://hyperpoint.progremir.dev, served by the `hyperpoint-site` Worker as static assets on Cloudflare. +Live at https://hyperpoint.progremir.dev, served by the `hyperpoint` Worker as static assets on Cloudflare. ```bash python3 -m http.server 4319 --directory site/public @@ -12,11 +12,10 @@ python3 -m http.server 4319 --directory site/public **Deploying.** Cloudflare's Workers Builds builds `site/` on every push that touches it. A push to `main` runs `npx cf deploy` and updates the live site. A push to any other branch runs `npx cf previews deploy`, which publishes -that branch at `https://-hyperpoint-site.progremir.workers.dev` (public, but sent with `X-Robots-Tag: +that branch at `https://-hyperpoint.progremir.workers.dev` (public, but sent with `X-Robots-Tag: noindex`). Both run in `site/` on Node 24 (`.node-version`) with Cloudflare's own build token, so the repository holds no Cloudflare secret. The connection lives on the Worker in the Cloudflare dashboard, under Settings › -Builds, and Cloudflare posts each branch's preview link on its pull request. To deploy by hand, sign in with `cf auth login` and run `npm ci && npm run deploy` in -`site/`, or `npx cf previews deploy` for a preview of the current branch. +Builds, and Cloudflare posts each branch's preview link on its pull request. `cloudflare.config.ts` holds the Worker's name, account, domain, preview URLs and 404 handling, and `wrangler.config.ts` names `public/` as the assets, so only that folder is uploaded. diff --git a/site/cloudflare.config.ts b/site/cloudflare.config.ts index 8d2c9d8..c3e9fa1 100644 --- a/site/cloudflare.config.ts +++ b/site/cloudflare.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from "cf/config"; export default defineConfig({ accountId: "dd1171d285218486ca1fb1fcec2a759c", worker: { - name: "hyperpoint-site", + name: "hyperpoint", compatibilityDate: "2026-10-19", domains: ["hyperpoint.progremir.dev"], // Production is served only on the domain. Previews get noindexed workers.dev URLs. diff --git a/site/package-lock.json b/site/package-lock.json index bb11c5a..1162f8e 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1,10 +1,10 @@ { - "name": "hyperpoint-site", + "name": "hyperpoint", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hyperpoint-site", + "name": "hyperpoint", "devDependencies": { "cf": "^1.0.0-beta.21", "wrangler": "^4.152.0" diff --git a/site/package.json b/site/package.json index 8443ec0..2d88d70 100644 --- a/site/package.json +++ b/site/package.json @@ -1,5 +1,5 @@ { - "name": "hyperpoint-site", + "name": "hyperpoint", "private": true, "type": "module", "scripts": {