@aphrody/m3-showcase 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/Album-20-x0fyhqxj.png +0 -0
- package/dist/Album-txdjxr6p.png +0 -0
- package/dist/Album_Light-bnvn9x43.png +0 -0
- package/dist/Alias-20-jpvzp7sd.png +0 -0
- package/dist/Alias-fbqj79wp.png +0 -0
- package/dist/Alias_Light-hv5b8k7t.png +0 -0
- package/dist/AmazonS3-x58haer2.png +0 -0
- package/dist/AndroidPhotos-cj9z6mnt.png +0 -0
- package/dist/AppleFiles-g4g8431e.png +0 -0
- package/dist/ApplePhotos-f2nesvx1.png +0 -0
- package/dist/Application-ft1as292.png +0 -0
- package/dist/Application_Light-h2p2z3de.png +0 -0
- package/dist/Archive-20-y262mxsm.png +0 -0
- package/dist/Archive-vmmq3f0p.png +0 -0
- package/dist/Archive_Light-fqxdt82g.png +0 -0
- package/dist/Audio-20-345hqxr8.png +0 -0
- package/dist/Audio-3w697yx9.png +0 -0
- package/dist/Audio_Light-4pb6z10h.png +0 -0
- package/dist/BackBlaze-6m4qpkyc.png +0 -0
- package/dist/Ball-d0bcgqha.png +0 -0
- package/dist/Book-20-etd4fzhm.png +0 -0
- package/dist/Book-ekfqxh7a.png +0 -0
- package/dist/BookBlue-17k9z7dy.png +0 -0
- package/dist/Book_Light-0n5wsr76.png +0 -0
- package/dist/Box-rd26c0g6.png +0 -0
- package/dist/CloudSync-wxh5ez5h.png +0 -0
- package/dist/CloudSync_Light-nb09rvv4.png +0 -0
- package/dist/Code-20-ybgq5j59.png +0 -0
- package/dist/Collection-20-7g9nzzcf.png +0 -0
- package/dist/Collection-52f2j9wx.png +0 -0
- package/dist/CollectionSparkle-nj38wazp.png +0 -0
- package/dist/CollectionSparkle_Light-hp1jptvv.png +0 -0
- package/dist/Collection_Light-z7p3knd7.png +0 -0
- package/dist/Compute-ghst3emj.png +0 -0
- package/dist/Config-20-vqd0vqeb.png +0 -0
- package/dist/DAV-mr5nm7zs.png +0 -0
- package/dist/Database-20-0q97pw36.png +0 -0
- package/dist/Database-rcxjw7yv.png +0 -0
- package/dist/Database_Light-1dxfaj96.png +0 -0
- package/dist/DeleteLocation-3hw52syq.png +0 -0
- package/dist/Devices-t2f2k803.png +0 -0
- package/dist/Document-20-0f78jpbp.png +0 -0
- package/dist/Document-kgsdt3nq.png +0 -0
- package/dist/Document_Light-ht9avf71.png +0 -0
- package/dist/Document_doc-wqd2zqp2.png +0 -0
- package/dist/Document_doc_Light-4ppbm2he.png +0 -0
- package/dist/Document_memory-6vymqjra.png +0 -0
- package/dist/Document_pdf-7b1zt469.png +0 -0
- package/dist/Document_pdf_Light-42j6dt8z.png +0 -0
- package/dist/Document_srt-symqv91r.png +0 -0
- package/dist/Document_xls-j5tbzav2.png +0 -0
- package/dist/Document_xls_Light-w6kbrvem.png +0 -0
- package/dist/Document_xmp-7xqx4tm8.png +0 -0
- package/dist/Dotfile-20-w53rabv6.png +0 -0
- package/dist/Drive-7wb1bf5w.png +0 -0
- package/dist/Drive-AmazonS3_Light-z6j2s9xm.png +0 -0
- package/dist/Drive-BackBlaze_Light-tr4wfkey.png +0 -0
- package/dist/Drive-DAV_Light-kse85f46.png +0 -0
- package/dist/Drive-Darker-mss4n9ws.png +0 -0
- package/dist/Drive-Dropbox_Light-48naz6gc.png +0 -0
- package/dist/Drive-GoogleDrive_Light-t4d4180c.png +0 -0
- package/dist/Drive-Mega-z6k8n821.png +0 -0
- package/dist/Drive-Mega_Light-nk0qskfb.png +0 -0
- package/dist/Drive-OneDrive-sx649zqw.png +0 -0
- package/dist/Drive-OneDrive_Light-2n556kpe.png +0 -0
- package/dist/Drive-OpenStack-tcs5rge7.png +0 -0
- package/dist/Drive-OpenStack_Light-05qnc2gy.png +0 -0
- package/dist/Drive-PCloud_Light-d0h9kbwp.png +0 -0
- package/dist/Drive-box_Light-0cb17zc1.png +0 -0
- package/dist/Drive_Light-dcjdmxm9.png +0 -0
- package/dist/Dropbox-q4nkmy9v.png +0 -0
- package/dist/Encrypted-20-m8ax3zxz.png +0 -0
- package/dist/Encrypted-ddrf9wk4.png +0 -0
- package/dist/Encrypted_Light-n1mjh529.png +0 -0
- package/dist/Entity-a4438mse.png +0 -0
- package/dist/Entity_Light-acxm9r2c.png +0 -0
- package/dist/Executable-20-fp7e8r9j.png +0 -0
- package/dist/Executable_Light_old-6xd6fa1z.png +0 -0
- package/dist/Executable_old-cyj158kz.png +0 -0
- package/dist/Face_Light-pjctyrvt.png +0 -0
- package/dist/Folder-20-r5505wvj.png +0 -0
- package/dist/Folder-kc594khj.png +0 -0
- package/dist/Folder-tag-xmp-ff4bgkr4.png +0 -0
- package/dist/FolderGrey-2vy13evc.png +0 -0
- package/dist/FolderGrey_Light-7z31xy56.png +0 -0
- package/dist/FolderNoSpace-d65fr1rz.png +0 -0
- package/dist/FolderNoSpace_Light-r5fc4pfj.png +0 -0
- package/dist/Folder_Light-rrg0jy36.png +0 -0
- package/dist/Font-20-r5n0z7f9.png +0 -0
- package/dist/Game-ts1kdw4e.png +0 -0
- package/dist/Game_Light-4d6ty5nz.png +0 -0
- package/dist/Globe-b379ffpd.png +0 -0
- package/dist/GlobeAlt-2mjbsvpp.png +0 -0
- package/dist/Globe_Light-2r9sdhb0.png +0 -0
- package/dist/GoogleDrive-1swewbzf.png +0 -0
- package/dist/GoogleSansFlex-latin-23tdmmt1.woff2 +0 -0
- package/dist/HDD-e85re2j5.png +0 -0
- package/dist/HDD_Light-pfbhb5wg.png +0 -0
- package/dist/Heart-3nx0yjvz.png +0 -0
- package/dist/Heart_Light-msf9zhq6.png +0 -0
- package/dist/Home-waerfpg1.png +0 -0
- package/dist/Home_Light-mdswjrcg.png +0 -0
- package/dist/Image-20-7vvqrhrb.png +0 -0
- package/dist/Image-fz5c8462.png +0 -0
- package/dist/Image_Light-rp0w5n3r.png +0 -0
- package/dist/Indexed-nxd6ctrt.png +0 -0
- package/dist/Key-20-z5nhfv0c.png +0 -0
- package/dist/Key-4en80vnn.png +0 -0
- package/dist/Key_Light-h3pc5e2c.png +0 -0
- package/dist/Keys-9jknyggw.png +0 -0
- package/dist/Keys_Light-5w8kcsmz.png +0 -0
- package/dist/Laptop-9gwskqny.png +0 -0
- package/dist/Laptop_Light-pwtbqbkp.png +0 -0
- package/dist/Link-20-f7hh1tcv.png +0 -0
- package/dist/Link-w9y1yad0.png +0 -0
- package/dist/Link_Light-4dy9pa5j.png +0 -0
- package/dist/Location-7vx0yx6r.png +0 -0
- package/dist/LocationManaged-w1n71jx5.png +0 -0
- package/dist/LocationReplica-4c6ncswq.png +0 -0
- package/dist/Lock-mnveqyfm.png +0 -0
- package/dist/Lock_Light-xwnbzkjz.png +0 -0
- package/dist/Mega-h4j4t3sb.png +0 -0
- package/dist/Mesh-20-rkky209q.png +0 -0
- package/dist/Mesh-efjcay8j.png +0 -0
- package/dist/Mesh_Light-t9yyzb3y.png +0 -0
- package/dist/MiniSilverBox-g8dfw0cp.png +0 -0
- package/dist/Mobile-Android-wy5kr3bj.png +0 -0
- package/dist/Mobile-jpsv7vp8.png +0 -0
- package/dist/Mobile_Light-2rwnn5bx.png +0 -0
- package/dist/MoveLocation-kft0z94v.png +0 -0
- package/dist/MoveLocation_Light-c23j5x59.png +0 -0
- package/dist/Movie-nxat82kv.png +0 -0
- package/dist/Movie_Light-eh1hmsr5.png +0 -0
- package/dist/NewLocation-2hfv9dwq.png +0 -0
- package/dist/Node-wykht7k0.png +0 -0
- package/dist/Node_Light-maewxhk1.png +0 -0
- package/dist/OneDrive-e4jvssjp.png +0 -0
- package/dist/OpenStack-pqp01jfd.png +0 -0
- package/dist/PC-eyfa7ewm.png +0 -0
- package/dist/PCloud-p0n6g1hq.png +0 -0
- package/dist/Package-20-grg5wkse.png +0 -0
- package/dist/Package-d7ex1fnn.png +0 -0
- package/dist/Package_Light-yj2cvgyw.png +0 -0
- package/dist/SD-h17wvz4z.png +0 -0
- package/dist/SD_Light-y3mmqh6b.png +0 -0
- package/dist/Scrapbook-f92c8p1x.png +0 -0
- package/dist/Scrapbook_Light-50pjr0q7.png +0 -0
- package/dist/Screenshot-20-z4tfe78m.png +0 -0
- package/dist/Screenshot-9nd23fde.png +0 -0
- package/dist/ScreenshotAlt-en1yt2gv.png +0 -0
- package/dist/Screenshot_Light-hnbah84r.png +0 -0
- package/dist/Search-7x8c5z1s.png +0 -0
- package/dist/SearchAlt-dmxzk2jb.png +0 -0
- package/dist/Search_Light-n23xd2ws.png +0 -0
- package/dist/Server-hqsf8851.png +0 -0
- package/dist/Server_Light-3x01jqz4.png +0 -0
- package/dist/SilverBox-0mk6sxrh.png +0 -0
- package/dist/Spacedrop-1-d230z0az.png +0 -0
- package/dist/Spacedrop-r1t67bcq.png +0 -0
- package/dist/Spacedrop_Light-dj5tgtep.png +0 -0
- package/dist/Storage-4x2q4nx6.png +0 -0
- package/dist/Sync-531te7fq.png +0 -0
- package/dist/Sync_Light-754vfxyr.png +0 -0
- package/dist/Tablet-ze6ermb7.png +0 -0
- package/dist/Tablet_Light-32skyk12.png +0 -0
- package/dist/Tags-bfapk266.png +0 -0
- package/dist/Tags_Light-7s00envw.png +0 -0
- package/dist/Terminal-t1yjx33x.png +0 -0
- package/dist/Terminal_Light-4s5azdgr.png +0 -0
- package/dist/Text-20-pt0zjg62.png +0 -0
- package/dist/Text-6fgmf4b2.png +0 -0
- package/dist/TextAlt_Light-w9kbgvm8.png +0 -0
- package/dist/Text_Light-c2k3w31k.png +0 -0
- package/dist/Text_txt-xhvv80ys.png +0 -0
- package/dist/TexturedMesh-ysk831xz.png +0 -0
- package/dist/TexturedMesh_Light-4twy5k6k.png +0 -0
- package/dist/Trash-hzscccvk.png +0 -0
- package/dist/Trash_Light-78nc56tp.png +0 -0
- package/dist/Undefined-pkbecmq4.png +0 -0
- package/dist/Undefined_Light-jxesg2zv.png +0 -0
- package/dist/Unknown-20-07efh8ya.png +0 -0
- package/dist/Video-20-q7yg9p4w.png +0 -0
- package/dist/Video-370fzqyw.png +0 -0
- package/dist/Video_Light-00vhrp0m.png +0 -0
- package/dist/WebPageArchive-20-wn0v4x27.png +0 -0
- package/dist/Widget-20-9t860nnc.png +0 -0
- package/dist/Widget-k1p15mg9.png +0 -0
- package/dist/Widget_Light-5md790ar.png +0 -0
- package/dist/ace-ph0rt669.svg +1 -0
- package/dist/acemanifest-je939qnz.svg +1 -0
- package/dist/adoc-kfbf89h8.svg +1 -0
- package/dist/adonis-pn6tbmnq.svg +1 -0
- package/dist/adonisconfig-5wfnycbk.svg +1 -0
- package/dist/afdesign-5nzbppfa.svg +1 -0
- package/dist/afphoto-p941z6q7.svg +1 -0
- package/dist/afpub-3232tcp9.svg +1 -0
- package/dist/ai-vvwvj03w.svg +1 -0
- package/dist/air-e3h8cfx1.svg +1 -0
- package/dist/angular-fyg22v82.svg +1 -0
- package/dist/angular-kzcwnjdt.svg +1 -0
- package/dist/anim-15kqy432.svg +1 -0
- package/dist/astro-wdcxh4zx.svg +1 -0
- package/dist/astroconfig-73qsx73n.svg +1 -0
- package/dist/atomizer-42xe88w7.svg +1 -0
- package/dist/audio-xy5qb7t1.svg +1 -0
- package/dist/audiomp3-s01feh78.svg +1 -0
- package/dist/audioogg-bcake0v6.svg +1 -0
- package/dist/audiowav-px37pxve.svg +1 -0
- package/dist/audiowv-bq8tbfdv.svg +1 -0
- package/dist/azure-veqzyzqq.svg +1 -0
- package/dist/babel-vxcfp57a.svg +1 -0
- package/dist/ballerina-faxx9p3b.svg +1 -0
- package/dist/ballerinaconfig-tmd09kkg.svg +1 -0
- package/dist/bat-vjr63aqs.svg +1 -0
- package/dist/bazel-4e01p5gp.svg +1 -0
- package/dist/bazelignore-wzgc7h2d.svg +1 -0
- package/dist/bicep-dae0dh59.svg +1 -0
- package/dist/bicepconfig-v64442sp.svg +1 -0
- package/dist/bicepparam-ts4jjq60.svg +1 -0
- package/dist/binary-dz3thnjq.svg +1 -0
- package/dist/biome-gbahcmjy.svg +1 -0
- package/dist/blade-je22xc2d.svg +1 -0
- package/dist/brotli-1m7qfk77.svg +1 -0
- package/dist/browserslist-005php91.svg +1 -0
- package/dist/bruno-h4bh0s2c.svg +1 -0
- package/dist/bsconfig-gmwc0r2r.svg +1 -0
- package/dist/buck-gtasjqw1.svg +1 -0
- package/dist/bun-6y6y7t4s.svg +1 -0
- package/dist/bun-zxmcandz.svg +1 -0
- package/dist/bundler-qhs5a5xb.svg +1 -0
- package/dist/bunlock-74b62xbm.svg +1 -0
- package/dist/c-k2se7t6q.svg +1 -0
- package/dist/c-v9cxtxe1.svg +1 -0
- package/dist/cargo-ma5bwyg5.svg +1 -0
- package/dist/cargolock-ak0tnv5y.svg +1 -0
- package/dist/cert-z62mqtqq.svg +1 -0
- package/dist/cheader-mgw2nf38.svg +1 -0
- package/dist/chunk-08yjrfs5.js +2 -0
- package/dist/chunk-2pmrsdd5.js +2 -0
- package/dist/chunk-3995cv9k.js +1 -0
- package/dist/chunk-58a3eq49.js +2 -0
- package/dist/chunk-616szvb9.js +2 -0
- package/dist/chunk-6638b8v6.js +1 -0
- package/dist/chunk-6ej2n64j.js +1 -0
- package/dist/chunk-7tt0cawd.js +10 -0
- package/dist/chunk-9gxv4c5g.js +1 -0
- package/dist/chunk-9na12mjw.js +1 -0
- package/dist/chunk-b7mpjcry.js +1 -0
- package/dist/chunk-c1g6tshf.js +2 -0
- package/dist/chunk-d50zvrb1.css +1 -0
- package/dist/chunk-d6c35fm0.js +2 -0
- package/dist/chunk-ehpz56xv.js +2 -0
- package/dist/chunk-ewtr6sjm.js +9 -0
- package/dist/chunk-g9rnrhvz.js +2 -0
- package/dist/chunk-gv83r4qx.js +262 -0
- package/dist/chunk-j46wg1ym.js +156 -0
- package/dist/chunk-kapsksxf.js +1 -0
- package/dist/chunk-m98tyb4g.js +6 -0
- package/dist/chunk-nn0prq25.js +2 -0
- package/dist/chunk-nsgjb3bx.js +2 -0
- package/dist/chunk-p8r6wjkz.js +3 -0
- package/dist/chunk-rdnm87fw.js +1 -0
- package/dist/chunk-re36kkra.js +1 -0
- package/dist/chunk-vm9edd26.js +2 -0
- package/dist/chunk-x2hztqk0.css +1 -0
- package/dist/chunk-yyxb5zbq.js +2 -0
- package/dist/civet-5bbs33v3.svg +1 -0
- package/dist/claude-jscxahsf.svg +1 -0
- package/dist/cli-4f5nfe5s.svg +1 -0
- package/dist/clojure-0h7h9znc.svg +1 -0
- package/dist/cmake-q8ryfre0.svg +1 -0
- package/dist/cmake-qwe6x8nx.svg +1 -0
- package/dist/codeworkspace-326020b4.svg +1 -0
- package/dist/coffeescript-ec1bytnk.svg +1 -0
- package/dist/commitlint-93j0tfvp.svg +1 -0
- package/dist/compodoc-36b7gn70.svg +1 -0
- package/dist/composer-vv2tvxkr.svg +1 -0
- package/dist/composerlock-nf0232yh.svg +1 -0
- package/dist/conan-dggdfcr9.svg +1 -0
- package/dist/conf-5756g97p.svg +1 -0
- package/dist/copilot-1phy76q3.svg +1 -0
- package/dist/cpp-4yvddm6j.svg +1 -0
- package/dist/cpp-pbznrtpp.svg +1 -0
- package/dist/crystal-x20aj00e.svg +1 -0
- package/dist/csharp-0p3hstq8.svg +1 -0
- package/dist/csharp-cpmfe8y0.svg +1 -0
- package/dist/cshtml-xb5gtb0r.svg +1 -0
- package/dist/csproj-ftptw15a.svg +1 -0
- package/dist/css-tqps22nw.svg +1 -0
- package/dist/css-ycr1qc5f.svg +1 -0
- package/dist/cssmap-3gnm34f6.svg +1 -0
- package/dist/csv-gg0axfar.svg +1 -0
- package/dist/cucumber-m06dfkm5.svg +1 -0
- package/dist/cursor-jw7n7tpq.svg +1 -0
- package/dist/cypress-xsm2fq1p.svg +1 -0
- package/dist/cypressjs-hapzyfm1.svg +1 -0
- package/dist/cypressts-jey9ahc7.svg +1 -0
- package/dist/d-4x2njcan.svg +1 -0
- package/dist/dartlang-7dwdwv03.svg +1 -0
- package/dist/dartlang-mra7r0f8.svg +1 -0
- package/dist/delphiproject-7snmvsdx.svg +1 -0
- package/dist/diff-hekg2jpm.svg +1 -0
- package/dist/docker-8q58y2kc.svg +1 -0
- package/dist/dockerdebug-8zhgj7bd.svg +1 -0
- package/dist/dockerignore-488safx9.svg +1 -0
- package/dist/drawio-6qm0rhkv.svg +1 -0
- package/dist/drizzle-5bct2tg6.svg +1 -0
- package/dist/dsstore-pmhpmkdr.svg +1 -0
- package/dist/dune-r4xhac56.svg +1 -0
- package/dist/duneproject-3b99jndq.svg +1 -0
- package/dist/edge-0jwced3c.svg +1 -0
- package/dist/editorconfig-hee1zp1h.svg +1 -0
- package/dist/eex-q1bkj66x.svg +1 -0
- package/dist/elixir-48ge02m5.svg +1 -0
- package/dist/elm-97mh5wye.svg +1 -0
- package/dist/env-ysamxkcd.svg +1 -0
- package/dist/eraser-7mejjgrg.svg +1 -0
- package/dist/erb-f28j5x6b.svg +1 -0
- package/dist/erlang-3ebtbtfw.svg +1 -0
- package/dist/esbuild-wptxye5y.svg +1 -0
- package/dist/eslint-mrbexpc5.svg +1 -0
- package/dist/eslintignore-k65wpbsr.svg +1 -0
- package/dist/excalidraw-28avfa1h.svg +1 -0
- package/dist/exs-5w2cfpaf.svg +1 -0
- package/dist/exx-s897xhz3.svg +1 -0
- package/dist/farm-m80hxnfy.svg +1 -0
- package/dist/favicon-djc6sne4.svg +1 -0
- package/dist/figma-krwq889b.svg +1 -0
- package/dist/file-hetj1tjc.svg +1 -0
- package/dist/file_light-jx35pv2m.svg +1 -0
- package/dist/flakelock-2nsqn4gw.svg +1 -0
- package/dist/flutter-277q83en.svg +1 -0
- package/dist/flutterlock-h0nywsvt.svg +1 -0
- package/dist/flutterpackage-cpgnfwtt.svg +1 -0
- package/dist/folder-2xmsfyxp.svg +1 -0
- package/dist/folder_open-wk4ndx1f.svg +1 -0
- package/dist/fonteot-a8xmqj3j.svg +1 -0
- package/dist/fontotf-f01hg174.svg +1 -0
- package/dist/fontttf-pcr3rt21.svg +1 -0
- package/dist/fontwoff-v1gdka62.svg +1 -0
- package/dist/fontwoff2-1w1r47x8.svg +1 -0
- package/dist/freemarker-s9m714ah.svg +1 -0
- package/dist/fsharp-89mjndn4.svg +1 -0
- package/dist/gbl-v20qxsgx.svg +1 -0
- package/dist/git-wc9x7c5e.svg +1 -0
- package/dist/gitlab-bhcvbetz.svg +1 -0
- package/dist/gleam-h2ze8vtz.svg +1 -0
- package/dist/gleamconfig-6sq0xy19.svg +1 -0
- package/dist/go-15qaph9s.svg +1 -0
- package/dist/go-j2dk6r7k.svg +1 -0
- package/dist/go_package-5nc95prx.svg +1 -0
- package/dist/godot-v7nnsyvc.svg +1 -0
- package/dist/gradle-khh2bn3v.svg +1 -0
- package/dist/gradlebat-631878b0.svg +1 -0
- package/dist/gradlekotlin-f8netdbt.svg +1 -0
- package/dist/grain-tztem5b9.svg +1 -0
- package/dist/graphql-egmdpvzr.svg +1 -0
- package/dist/groovy-c0sstajm.svg +1 -0
- package/dist/grunt-56ssa3fr.svg +1 -0
- package/dist/gulp-j7wc27hd.svg +1 -0
- package/dist/h-4xbbejcs.svg +1 -0
- package/dist/haml-6pyt136a.svg +1 -0
- package/dist/handlebars-1v8g0n32.svg +1 -0
- package/dist/hardhat-1k6gym6f.svg +1 -0
- package/dist/hash-xd1ymkyp.svg +1 -0
- package/dist/hashicorp-vty9d6de.svg +1 -0
- package/dist/haskell-ncbxh3wk.svg +1 -0
- package/dist/haxe-j5nqdw5z.svg +1 -0
- package/dist/haxeml-bn7wtpx1.svg +1 -0
- package/dist/hpp-rpxcfk6j.svg +1 -0
- package/dist/htaccess-s4rrm1re.svg +1 -0
- package/dist/html-3vbw2fxn.svg +1 -0
- package/dist/html-v1z6sanr.svg +1 -0
- package/dist/http-9bfh1zqw.svg +1 -0
- package/dist/identifier-6gsrr6st.svg +1 -0
- package/dist/image-pstdg0vj.svg +1 -0
- package/dist/imagegif-any2rrn3.svg +1 -0
- package/dist/imageico-cng69rft.svg +1 -0
- package/dist/imagejpg-rtx9yy9y.svg +1 -0
- package/dist/imagepng-j6f306v1.svg +1 -0
- package/dist/imagewebp-q1mcm8np.svg +1 -0
- package/dist/imba-xxn1q1r2.svg +1 -0
- package/dist/index.html +16 -0
- package/dist/info-qssn7jw5.svg +1 -0
- package/dist/instructions-3068x11a.svg +1 -0
- package/dist/ipynb-qm8fb1p1.svg +1 -0
- package/dist/jar-src0eb7a.svg +1 -0
- package/dist/java-126v951r.svg +1 -0
- package/dist/java-hhh3k9xf.svg +1 -0
- package/dist/jenkins-ap6tzby2.svg +1 -0
- package/dist/jest-xrtp7hh0.svg +1 -0
- package/dist/jinja-v9kggcps.svg +1 -0
- package/dist/js-qsntbmzx.svg +1 -0
- package/dist/js-r0b86hmj.svg +1 -0
- package/dist/jsmap-1ns6yn3z.svg +1 -0
- package/dist/json-1jhmg3gf.svg +1 -0
- package/dist/jsp-7n103tna.svg +1 -0
- package/dist/julia-nba7etbq.svg +1 -0
- package/dist/karma-zcbeze4p.svg +1 -0
- package/dist/keep-tw8xf3gf.svg +1 -0
- package/dist/key-hd2nnhmr.svg +1 -0
- package/dist/knex-87w40q0j.svg +1 -0
- package/dist/knip-kzsz2w4s.svg +1 -0
- package/dist/kotlin-360tzrt1.svg +1 -0
- package/dist/kotlin-hbykz1k8.svg +1 -0
- package/dist/kotlins-4zz4rshm.svg +1 -0
- package/dist/krita-wx5qyrx7.svg +1 -0
- package/dist/latex-jwrpzd2b.svg +1 -0
- package/dist/launch-8wsjpmd3.svg +1 -0
- package/dist/lazarusproject-gma2fvya.svg +1 -0
- package/dist/less-p4zwh19p.svg +1 -0
- package/dist/license-qamq8mp3.svg +1 -0
- package/dist/light_editorconfig-k28g3f4k.svg +1 -0
- package/dist/liquid-wtexakya.svg +1 -0
- package/dist/llvm-cfgd7591.svg +1 -0
- package/dist/lock-wv5q3nef.svg +1 -0
- package/dist/log-ykdkwnpq.svg +1 -0
- package/dist/lua-aynthw24.svg +1 -0
- package/dist/m-va54fte8.svg +1 -0
- package/dist/m3-bun.manifest.json +3785 -0
- package/dist/makefile-kkcgz3sb.svg +1 -0
- package/dist/manifest-ede4t0vc.svg +1 -0
- package/dist/markdown-x7kezf76.svg +1 -0
- package/dist/markdownx-y5mf21hn.svg +1 -0
- package/dist/maven-djc7vb1b.svg +1 -0
- package/dist/mermaid-4bpjjghk.svg +1 -0
- package/dist/mesh-q798mf4t.svg +1 -0
- package/dist/mgcb-cfmfr63b.svg +1 -0
- package/dist/mint-b732cej4.svg +1 -0
- package/dist/mix-41smb4q2.svg +1 -0
- package/dist/mixlock-z0cvwpkq.svg +1 -0
- package/dist/mjml-4kzxtz0b.svg +1 -0
- package/dist/mkdocs-5t1davz8.svg +1 -0
- package/dist/mockoon-5pw3ckhf.svg +1 -0
- package/dist/motoko-yv95035d.svg +1 -0
- package/dist/mov-r3cjb3vr.svg +1 -0
- package/dist/mp4-31hded8m.svg +1 -0
- package/dist/mtl-01f1f06m.svg +1 -0
- package/dist/mustache-1s62ewsg.svg +1 -0
- package/dist/nelua-97199e3p.svg +1 -0
- package/dist/neon-gcp7rsp5.svg +1 -0
- package/dist/nestjs-sme3dmgk.svg +1 -0
- package/dist/nestjscontroller-2ceskrfc.svg +1 -0
- package/dist/nestjsdecorator-52k9dwm2.svg +1 -0
- package/dist/nestjsdto-xf36my7y.svg +1 -0
- package/dist/nestjsentity-q17fyvm1.svg +1 -0
- package/dist/nestjsfilter-qytmtqzj.svg +1 -0
- package/dist/nestjsguard-4pyb34xe.svg +1 -0
- package/dist/nestjsinterceptor-7s690j87.svg +1 -0
- package/dist/nestjsmodule-zeyz5633.svg +1 -0
- package/dist/nestjsrepository-kz26s1jf.svg +1 -0
- package/dist/nestjsresolver-8qdvexrk.svg +1 -0
- package/dist/nestjsservice-kp3b933m.svg +1 -0
- package/dist/nestscheduler-60p4j2tg.svg +1 -0
- package/dist/netlify-bbgdw42s.svg +1 -0
- package/dist/nextconfig-gr1sd69d.svg +1 -0
- package/dist/nextron-sf3ag316.svg +1 -0
- package/dist/nginx-73stz0yt.svg +1 -0
- package/dist/nim-gqnvnkc5.svg +1 -0
- package/dist/nix-t1m4qhc8.svg +1 -0
- package/dist/njk-dfv3nrxr.svg +1 -0
- package/dist/node-w3gp0xxn.svg +1 -0
- package/dist/nodemon-q41mkhvz.svg +1 -0
- package/dist/npm-rt8ga6qx.svg +1 -0
- package/dist/npmlock-8p4sp377.svg +1 -0
- package/dist/nuxt-13qdp31g.svg +1 -0
- package/dist/nvidia-fysz92ep.svg +1 -0
- package/dist/nvim-h3ynkfqg.svg +1 -0
- package/dist/nvm-ser6majj.svg +1 -0
- package/dist/nx-av0cw2sw.svg +1 -0
- package/dist/obj-thakg863.svg +1 -0
- package/dist/ocaml-maevkwa7.svg +1 -0
- package/dist/ocamli-w0gc5jq8.svg +1 -0
- package/dist/ocamll-h2wysvh1.svg +1 -0
- package/dist/ocamly-ppj6pfy3.svg +1 -0
- package/dist/odin-x7qb7b6x.svg +1 -0
- package/dist/opengl-h6pbxg3n.svg +1 -0
- package/dist/oxlint-7qwxtp86.svg +1 -0
- package/dist/panda-9qqa1nn4.svg +1 -0
- package/dist/parcel-gzckrwpy.svg +1 -0
- package/dist/pascal-kj6crs4h.svg +1 -0
- package/dist/pdf-wk39v1nv.svg +1 -0
- package/dist/perl-11dgrnh8.svg +1 -0
- package/dist/perlm-7xrwgd93.svg +1 -0
- package/dist/pfx-ggdb4v6p.svg +1 -0
- package/dist/photoshop-m3gw1xv7.svg +1 -0
- package/dist/php-jgyt96zk.svg +1 -0
- package/dist/php-jx84j7ns.svg +1 -0
- package/dist/plantuml-04ncqrmc.svg +1 -0
- package/dist/playright-dfc8tvme.svg +1 -0
- package/dist/plop-n71aee6w.svg +1 -0
- package/dist/pnpm-jvkts1qb.svg +1 -0
- package/dist/pnpmlock-2kqbdtn7.svg +1 -0
- package/dist/poetry-yjeeyvtr.svg +1 -0
- package/dist/poetrylock-da30xbws.svg +1 -0
- package/dist/postcssconfig-yyvnwr01.svg +1 -0
- package/dist/powershell-hf358xbb.svg +1 -0
- package/dist/powershelldata-nggemhrs.svg +1 -0
- package/dist/powershellmodule-v5wz6sed.svg +1 -0
- package/dist/precommit-wmfmt8tq.svg +1 -0
- package/dist/prettier-nyc1v0qk.svg +1 -0
- package/dist/prettierignore-hx4x759w.svg +1 -0
- package/dist/prisma-te242v0q.svg +1 -0
- package/dist/prolog-twb6h758.svg +1 -0
- package/dist/prompt-eg8c0gqa.svg +1 -0
- package/dist/properties-c39zkae1.svg +1 -0
- package/dist/proto-rsvsx4xb.svg +1 -0
- package/dist/pug-rqxd67km.svg +1 -0
- package/dist/pvk-bc4g1qms.svg +1 -0
- package/dist/py-0g79s39m.svg +1 -0
- package/dist/pyproject-5s59g4en.svg +1 -0
- package/dist/python-ds482k8k.svg +1 -0
- package/dist/qt-y3kxpf67.svg +1 -0
- package/dist/quarkus-xrq4b999.svg +1 -0
- package/dist/quasar-hvna6b7f.svg +1 -0
- package/dist/r-n1bnfab8.svg +1 -0
- package/dist/racket-vx5e6n1d.svg +1 -0
- package/dist/raku-4ydzc5wj.svg +1 -0
- package/dist/razor-cbqpr6vt.svg +1 -0
- package/dist/rb-mfkwkq7e.svg +1 -0
- package/dist/reactjs-qjyhk3rf.svg +1 -0
- package/dist/reactts-p0hf29yr.svg +1 -0
- package/dist/readme-3qcyy95y.svg +1 -0
- package/dist/redis-ya1xyn4v.svg +1 -0
- package/dist/rego-tr1y9cqm.svg +1 -0
- package/dist/remix-61pbjda4.svg +1 -0
- package/dist/rescript-9c3xnepn.svg +1 -0
- package/dist/rescriptinterface-6ne0a2rf.svg +1 -0
- package/dist/restructuredtext-7k21mjx4.svg +1 -0
- package/dist/rjson-3s2edhhr.svg +1 -0
- package/dist/robots-geh7c3pq.svg +1 -0
- package/dist/rollup-0dxydzr9.svg +1 -0
- package/dist/rome-1gxetp8g.svg +1 -0
- package/dist/ron-ft34rzr3.svg +1 -0
- package/dist/root_folder-rhwm7290.svg +1 -0
- package/dist/root_folder_light-ngkg5m4x.svg +1 -0
- package/dist/root_folder_light_open-c6nhg6sy.svg +1 -0
- package/dist/root_folder_open-genv0ps7.svg +1 -0
- package/dist/rs-60bcevwn.svg +1 -0
- package/dist/ruby-7j5zzh38.svg +1 -0
- package/dist/rust-a9528zdg.svg +1 -0
- package/dist/rustfmt-c8evpzd7.svg +1 -0
- package/dist/sails-5ecccxrg.svg +1 -0
- package/dist/salesforce-4d89045j.svg +1 -0
- package/dist/sass-g9qbr3x4.svg +1 -0
- package/dist/scala-8gnn0ekc.svg +1 -0
- package/dist/scss-g9qbr3x4.svg +1 -0
- package/dist/sentinel-g8py3peh.svg +1 -0
- package/dist/sequelize-qvzwnjhv.svg +1 -0
- package/dist/sh-bkp0n5vb.svg +1 -0
- package/dist/shaderlab-gkww43v2.svg +1 -0
- package/dist/shell-kc0a9zjq.svg +1 -0
- package/dist/silq-mczpsqmz.svg +1 -0
- package/dist/slim-8j9jf3zt.svg +1 -0
- package/dist/sln-nketqakm.svg +1 -0
- package/dist/smarty-w1zxys1s.svg +1 -0
- package/dist/sol-adyfjwzz.svg +1 -0
- package/dist/spc-kk30w5m6.svg +1 -0
- package/dist/sql-3c4gcy7a.svg +1 -0
- package/dist/sqlite-cppkxr0a.svg +1 -0
- package/dist/storybook-885ssp75.svg +1 -0
- package/dist/stylelint-z7s6yhc9.svg +1 -0
- package/dist/stylelintignore-xwnh8sx1.svg +1 -0
- package/dist/stylus-qz53ytkd.svg +1 -0
- package/dist/suo-3fvnz3bx.svg +1 -0
- package/dist/svelte-jp3x9gw7.svg +1 -0
- package/dist/svelteconfig-m0n00zjj.svg +1 -0
- package/dist/svg-8dyqf96t.svg +1 -0
- package/dist/swift-8g870v6j.svg +1 -0
- package/dist/symfony-q77kcepw.svg +1 -0
- package/dist/tailwind-3es9cgbd.svg +1 -0
- package/dist/tauri-xnj526kw.svg +1 -0
- package/dist/taze-e3ykqs0k.svg +1 -0
- package/dist/terrafile-gqn1tysh.svg +1 -0
- package/dist/terraform-jy4fbwsr.svg +1 -0
- package/dist/terraformvars-05qje1sw.svg +1 -0
- package/dist/terraformversion-gq2tkwmn.svg +1 -0
- package/dist/testjs-sqgjsb8h.svg +1 -0
- package/dist/testts-19m1sf26.svg +1 -0
- package/dist/tmpl-enggw2gc.svg +1 -0
- package/dist/todo-ccw0mpg2.svg +1 -0
- package/dist/toml-rmx5hba4.svg +1 -0
- package/dist/toolversions-s7fttfyp.svg +1 -0
- package/dist/tox-a4j4yvmb.svg +1 -0
- package/dist/travis-gr3jtpx2.svg +1 -0
- package/dist/tres-qt3kk1t0.svg +1 -0
- package/dist/ts-7amn21ms.svg +1 -0
- package/dist/tscn-g1jthb4m.svg +1 -0
- package/dist/tsconfig-r1s750bq.svg +1 -0
- package/dist/tsx-a3rsjrrz.svg +1 -0
- package/dist/turbo-dcmj75tc.svg +1 -0
- package/dist/twig-3xgrb919.svg +1 -0
- package/dist/txt-y0t8b72p.svg +1 -0
- package/dist/typescript-kd5g4jkg.svg +1 -0
- package/dist/typescriptdef-y43343m6.svg +1 -0
- package/dist/ui-pxvg5zj2.svg +1 -0
- package/dist/unocss-b0f5fthj.svg +1 -0
- package/dist/user-q7tk1k2d.svg +1 -0
- package/dist/v-c25jfygk.svg +1 -0
- package/dist/vanillaextract-3w6vds6m.svg +1 -0
- package/dist/vb-87ckj7t3.svg +1 -0
- package/dist/vercel-hfxjvaj9.svg +1 -0
- package/dist/version-zd7hfmf1.svg +1 -0
- package/dist/vhd-xaw7zbny.svg +1 -0
- package/dist/vhdl-s308rb5a.svg +1 -0
- package/dist/video-z6khth5y.svg +1 -0
- package/dist/vite-5g5xkmpk.svg +1 -0
- package/dist/viteenv-jbmx0tb0.svg +1 -0
- package/dist/vitest-5267dzt2.svg +1 -0
- package/dist/vmod-8jftapdc.svg +1 -0
- package/dist/vscode-8tvcs7ae.svg +1 -0
- package/dist/vue-j5sjdz4b.svg +1 -0
- package/dist/vue-x9h2d0d2.svg +1 -0
- package/dist/vueconfig-726zh8dd.svg +1 -0
- package/dist/wasm-vgmg9kv4.svg +1 -0
- package/dist/webpack-hfwce90m.svg +1 -0
- package/dist/wgsl-68923cz5.svg +1 -0
- package/dist/windi-c9m8en7k.svg +1 -0
- package/dist/wren-7ep5ph5r.svg +1 -0
- package/dist/xmake-mc60ax7m.svg +1 -0
- package/dist/xml-feq1fbt4.svg +1 -0
- package/dist/yaml-n43aw0fx.svg +1 -0
- package/dist/yang-mtcxs2mg.svg +1 -0
- package/dist/yarn-k4k6e35h.svg +1 -0
- package/dist/yarnerror-5jkesx9j.svg +1 -0
- package/dist/yarnignore-ryh8jd5p.svg +1 -0
- package/dist/yarnlock-9zvhbg04.svg +1 -0
- package/dist/yin-f931bzgj.svg +1 -0
- package/dist/zig-nx2e65ns.svg +1 -0
- package/dist/zip-7hqmj0b0.svg +1 -0
- package/m3.config.ts +32 -0
- package/package.json +60 -0
- package/scripts/browser-contract.ts +400 -0
- package/scripts/icons.ts +56 -0
- package/scripts/inventory.ts +86 -0
- package/scripts/shots.ts +154 -0
- package/scripts/tauri.ts +401 -0
- package/src/App.tsx +40 -0
- package/src/adaptive/dpad.ts +223 -0
- package/src/adaptive/window.ts +133 -0
- package/src/api/index.ts +332 -0
- package/src/components/ui.tsx +89 -0
- package/src/env.d.ts +5 -0
- package/src/favicon.svg +1 -0
- package/src/generated/inventory.json +426 -0
- package/src/index.html +16 -0
- package/src/inventory.ts +19 -0
- package/src/main.tsx +6 -0
- package/src/pages/Adaptive.tsx +128 -0
- package/src/pages/Ai.tsx +174 -0
- package/src/pages/Brands.tsx +156 -0
- package/src/pages/Components.tsx +91 -0
- package/src/pages/Design.tsx +163 -0
- package/src/pages/Explorer.tsx +125 -0
- package/src/pages/Forms.tsx +128 -0
- package/src/pages/Icons.tsx +138 -0
- package/src/pages/Motion.tsx +157 -0
- package/src/pages/Next.tsx +107 -0
- package/src/pages/Overview.tsx +105 -0
- package/src/pages/Primitives.tsx +138 -0
- package/src/pages/Reader.tsx +101 -0
- package/src/pages/System.tsx +283 -0
- package/src/pages/Theme.tsx +146 -0
- package/src/pages/index.ts +25 -0
- package/src/router.ts +22 -0
- package/src/shell/Shell.tsx +383 -0
- package/src/shell/nav.ts +198 -0
- package/src/styles.css +61 -0
- package/test/contracts.test.ts +87 -0
- package/test/host-contract.test.ts +183 -0
- package/test/runtime-api.test.ts +138 -0
- package/test-dom/dpad.test.ts +78 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// D-pad navigation for TV: arrow keys move focus to the nearest focusable element in that direction (spatial
|
|
2
|
+
// navigation), Enter activates, Back (Escape, Backspace, XF86Back, Tizen 10009, webOS 461, Android 4) closes the
|
|
3
|
+
// topmost dialog or goes back. `pickNext` is pure; `attachDpad` binds it to a document.
|
|
4
|
+
|
|
5
|
+
export type Direction = "up" | "down" | "left" | "right";
|
|
6
|
+
|
|
7
|
+
export interface Box {
|
|
8
|
+
left: number;
|
|
9
|
+
top: number;
|
|
10
|
+
right: number;
|
|
11
|
+
bottom: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const boxOf = (rect: { left: number; top: number; right: number; bottom: number }): Box => ({
|
|
15
|
+
left: rect.left,
|
|
16
|
+
top: rect.top,
|
|
17
|
+
right: rect.right,
|
|
18
|
+
bottom: rect.bottom,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const centerX = (b: Box) => (b.left + b.right) / 2;
|
|
22
|
+
const centerY = (b: Box) => (b.top + b.bottom) / 2;
|
|
23
|
+
|
|
24
|
+
/** Overlap length of two ranges (0 when they only touch or are apart). */
|
|
25
|
+
const overlap = (a0: number, a1: number, b0: number, b1: number) =>
|
|
26
|
+
Math.max(0, Math.min(a1, b1) - Math.max(a0, b0));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The candidate to move to from `from` in `direction`, or undefined. A candidate must lie in the direction (its
|
|
30
|
+
* centre beyond the origin's centre, its near edge not behind the origin's far edge by more than a tolerance).
|
|
31
|
+
* The score is the gap along the axis plus twice the offset across it, minus credit for overlapping the origin's
|
|
32
|
+
* extent across the axis, so a card straight below beats a nearer one far to the side.
|
|
33
|
+
*/
|
|
34
|
+
export function pickNext<T>(
|
|
35
|
+
from: Box,
|
|
36
|
+
candidates: ReadonlyArray<{ item: T; box: Box }>,
|
|
37
|
+
direction: Direction,
|
|
38
|
+
): T | undefined {
|
|
39
|
+
const horizontal = direction === "left" || direction === "right";
|
|
40
|
+
const sign = direction === "right" || direction === "down" ? 1 : -1;
|
|
41
|
+
let best: { item: T; score: number } | undefined;
|
|
42
|
+
for (const { item, box } of candidates) {
|
|
43
|
+
if (box === from) continue;
|
|
44
|
+
const along = horizontal ? centerX(box) - centerX(from) : centerY(box) - centerY(from);
|
|
45
|
+
if (along * sign <= 1) continue;
|
|
46
|
+
// Near edge of the candidate relative to the far edge of the origin, along the movement axis.
|
|
47
|
+
const gap = horizontal
|
|
48
|
+
? sign > 0
|
|
49
|
+
? box.left - from.right
|
|
50
|
+
: from.left - box.right
|
|
51
|
+
: sign > 0
|
|
52
|
+
? box.top - from.bottom
|
|
53
|
+
: from.top - box.bottom;
|
|
54
|
+
if (gap < -Math.min(horizontal ? from.right - from.left : from.bottom - from.top, 24)) continue;
|
|
55
|
+
const across = horizontal
|
|
56
|
+
? Math.abs(centerY(box) - centerY(from))
|
|
57
|
+
: Math.abs(centerX(box) - centerX(from));
|
|
58
|
+
const shared = horizontal
|
|
59
|
+
? overlap(from.top, from.bottom, box.top, box.bottom)
|
|
60
|
+
: overlap(from.left, from.right, box.left, box.right);
|
|
61
|
+
const score = Math.max(0, gap) + 2 * across - shared;
|
|
62
|
+
if (!best || score < best.score) best = { item, score };
|
|
63
|
+
}
|
|
64
|
+
return best?.item;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Keys that mean "back" on TV platforms. */
|
|
68
|
+
export const BACK_KEYS = new Set(["Escape", "Backspace", "BrowserBack", "GoBack", "XF86Back"]);
|
|
69
|
+
export const BACK_KEY_CODES = new Set([4, 10009, 461]);
|
|
70
|
+
|
|
71
|
+
const FOCUSABLE = [
|
|
72
|
+
"a[href]",
|
|
73
|
+
"button:not([disabled])",
|
|
74
|
+
"input:not([disabled]):not([type=hidden])",
|
|
75
|
+
"select:not([disabled])",
|
|
76
|
+
"textarea:not([disabled])",
|
|
77
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
78
|
+
"[data-dpad]",
|
|
79
|
+
"md-filled-button",
|
|
80
|
+
"md-outlined-button",
|
|
81
|
+
"md-text-button",
|
|
82
|
+
"md-filled-tonal-button",
|
|
83
|
+
"md-elevated-button",
|
|
84
|
+
"md-icon-button",
|
|
85
|
+
"md-filled-icon-button",
|
|
86
|
+
"md-fab",
|
|
87
|
+
"md-chip-set md-filter-chip",
|
|
88
|
+
"md-switch",
|
|
89
|
+
"md-checkbox",
|
|
90
|
+
"md-radio",
|
|
91
|
+
"md-slider",
|
|
92
|
+
"md-primary-tab",
|
|
93
|
+
"md-secondary-tab",
|
|
94
|
+
"md-navigation-tab",
|
|
95
|
+
"md-list-item[type=button]",
|
|
96
|
+
"md-list-item[type=link]",
|
|
97
|
+
].join(",");
|
|
98
|
+
|
|
99
|
+
function disabledByFieldset(element: HTMLElement): boolean {
|
|
100
|
+
for (
|
|
101
|
+
let fieldset = element.closest("fieldset[disabled]");
|
|
102
|
+
fieldset;
|
|
103
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null
|
|
104
|
+
) {
|
|
105
|
+
const legend = [...fieldset.children].find((child) => child.tagName === "LEGEND");
|
|
106
|
+
if (!legend?.contains(element)) return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
/** The visible, enabled focusable elements under `root`. */
|
|
111
|
+
export function focusables(root: ParentNode): HTMLElement[] {
|
|
112
|
+
return [...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter((el) => {
|
|
113
|
+
if (el.closest("[hidden], [inert], [aria-hidden='true'], [aria-disabled='true']")) return false;
|
|
114
|
+
if (
|
|
115
|
+
disabledByFieldset(el) ||
|
|
116
|
+
el.matches(":disabled") ||
|
|
117
|
+
el.hasAttribute("disabled") ||
|
|
118
|
+
el.getAttribute("aria-disabled") === "true"
|
|
119
|
+
)
|
|
120
|
+
return false;
|
|
121
|
+
const rect = el.getBoundingClientRect();
|
|
122
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
123
|
+
const style = getComputedStyle(el);
|
|
124
|
+
return style.visibility !== "hidden" && style.display !== "none";
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The topmost modal: an open dialog or an `aria-modal` container. Focus is confined to it. */
|
|
129
|
+
export function activeModal(doc: Document): HTMLElement | null {
|
|
130
|
+
const modals = [
|
|
131
|
+
...doc.querySelectorAll<HTMLElement>("dialog[open], md-dialog[open], [aria-modal='true']"),
|
|
132
|
+
].filter((element) => {
|
|
133
|
+
if (element.closest("[hidden], [inert], [aria-hidden='true']")) return false;
|
|
134
|
+
const style = getComputedStyle(element);
|
|
135
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
136
|
+
});
|
|
137
|
+
return modals.at(-1) ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface DpadOptions {
|
|
141
|
+
/** Called for a Back key when no modal consumed it. */
|
|
142
|
+
onBack?: () => void;
|
|
143
|
+
/** The document to bind to (default: the global one). */
|
|
144
|
+
doc?: Document;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Bind the keys to `doc`. Returns a disposer. */
|
|
148
|
+
export function attachDpad(options: DpadOptions = {}): () => void {
|
|
149
|
+
const doc = options.doc ?? document;
|
|
150
|
+
const move = (direction: Direction): boolean => {
|
|
151
|
+
const scope: ParentNode = activeModal(doc) ?? doc;
|
|
152
|
+
const items = focusables(scope);
|
|
153
|
+
if (items.length === 0) return false;
|
|
154
|
+
const current = doc.activeElement as HTMLElement | null;
|
|
155
|
+
const origin =
|
|
156
|
+
current && items.includes(current)
|
|
157
|
+
? current
|
|
158
|
+
: (current?.closest<HTMLElement>(FOCUSABLE) ?? null);
|
|
159
|
+
if (!origin) {
|
|
160
|
+
(items.find((el) => el.hasAttribute("data-dpad-start")) ?? items[0])!.focus();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
const from = boxOf(origin.getBoundingClientRect());
|
|
164
|
+
const target = pickNext(
|
|
165
|
+
from,
|
|
166
|
+
items
|
|
167
|
+
.filter((el) => el !== origin)
|
|
168
|
+
.map((el) => ({ item: el, box: boxOf(el.getBoundingClientRect()) })),
|
|
169
|
+
direction,
|
|
170
|
+
);
|
|
171
|
+
if (!target) return false;
|
|
172
|
+
target.focus({ preventScroll: false });
|
|
173
|
+
target.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
178
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
179
|
+
const editing = (event.composedPath()[0] as HTMLElement | undefined)?.closest?.(
|
|
180
|
+
"input:not([type=checkbox]):not([type=radio]):not([type=range]), textarea, [contenteditable='true']",
|
|
181
|
+
);
|
|
182
|
+
const direction = (
|
|
183
|
+
{ ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" } as const
|
|
184
|
+
)[event.key as "ArrowUp"];
|
|
185
|
+
if (direction) {
|
|
186
|
+
// Inside a text field the arrows belong to the caret; leave through the field's edges with Tab or Back.
|
|
187
|
+
if (editing) return;
|
|
188
|
+
if (move(direction)) event.preventDefault();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (event.key === "Enter" && !editing) {
|
|
192
|
+
const target = doc.activeElement as HTMLElement | null;
|
|
193
|
+
// Native buttons and links activate themselves; custom controls and tabindex containers need the click.
|
|
194
|
+
if (
|
|
195
|
+
target &&
|
|
196
|
+
!(target instanceof HTMLButtonElement) &&
|
|
197
|
+
!(target instanceof HTMLAnchorElement) &&
|
|
198
|
+
!(target instanceof HTMLInputElement) &&
|
|
199
|
+
!(target instanceof HTMLSelectElement)
|
|
200
|
+
) {
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
target.click();
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (BACK_KEYS.has(event.key) || BACK_KEY_CODES.has(event.keyCode)) {
|
|
207
|
+
if (editing && event.key === "Backspace") return;
|
|
208
|
+
const modal = activeModal(doc);
|
|
209
|
+
if (modal) {
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
if ("close" in modal && typeof modal.close === "function") modal.close();
|
|
212
|
+
else modal.dispatchEvent(new Event("close", { bubbles: true }));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (options.onBack) {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
options.onBack();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
doc.addEventListener("keydown", onKeyDown);
|
|
222
|
+
return () => doc.removeEventListener("keydown", onKeyDown);
|
|
223
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { windowClassFor, type WindowClassState } from "@aphrody/m3-front/window-class";
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
|
|
5
|
+
export type LayoutMode = "mobile" | "desktop" | "tv";
|
|
6
|
+
export const LAYOUT_MODES: readonly LayoutMode[] = ["mobile", "desktop", "tv"];
|
|
7
|
+
export const TV_USER_AGENT =
|
|
8
|
+
/SMART-TV|SmartTV|Tizen|Web0S|WebOS|NetCast|HbbTV|BRAVIA|AFTM|AFTB|AFTT|AFTS|Android TV|GoogleTV|CrKey|Viera|Roku|AppleTV|PlayStation|Xbox/i;
|
|
9
|
+
export function layoutModeFor(width: WindowClassState["width"], tv: boolean): LayoutMode {
|
|
10
|
+
return tv ? "tv" : width === "compact" ? "mobile" : "desktop";
|
|
11
|
+
}
|
|
12
|
+
export interface LayoutOverride {
|
|
13
|
+
mode: LayoutMode | null;
|
|
14
|
+
}
|
|
15
|
+
export function parseOverride(search: string, stored: string | null): LayoutMode | null {
|
|
16
|
+
const params = new URLSearchParams(search);
|
|
17
|
+
const value = params.get("layout") ?? (params.has("tv") ? "tv" : stored);
|
|
18
|
+
return (LAYOUT_MODES as readonly string[]).includes(value ?? "") ? (value as LayoutMode) : null;
|
|
19
|
+
}
|
|
20
|
+
export interface AdaptiveState extends WindowClassState {
|
|
21
|
+
widthPx: number;
|
|
22
|
+
heightPx: number;
|
|
23
|
+
mode: LayoutMode;
|
|
24
|
+
tv: boolean;
|
|
25
|
+
source: "user-agent" | "override" | "viewport";
|
|
26
|
+
landscape: boolean;
|
|
27
|
+
touch: boolean;
|
|
28
|
+
}
|
|
29
|
+
export const STORAGE_KEY = "m3-showcase:layout";
|
|
30
|
+
export function adaptiveFor(input: {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
userAgent: string;
|
|
34
|
+
override: LayoutMode | null;
|
|
35
|
+
touch: boolean;
|
|
36
|
+
}): AdaptiveState {
|
|
37
|
+
const classes = windowClassFor(input.width, input.height);
|
|
38
|
+
const uaTv = TV_USER_AGENT.test(input.userAgent);
|
|
39
|
+
const mode = input.override ?? layoutModeFor(classes.width, uaTv);
|
|
40
|
+
return {
|
|
41
|
+
...classes,
|
|
42
|
+
widthPx: input.width,
|
|
43
|
+
heightPx: input.height,
|
|
44
|
+
mode,
|
|
45
|
+
tv: mode === "tv",
|
|
46
|
+
source: input.override ? "override" : uaTv ? "user-agent" : "viewport",
|
|
47
|
+
landscape: input.width >= input.height,
|
|
48
|
+
touch: input.touch,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
type Listener = () => void;
|
|
52
|
+
const listeners = new Set<Listener>();
|
|
53
|
+
let snapshot: AdaptiveState | undefined;
|
|
54
|
+
// An explicit choice works even when storage is unavailable; a URL is an initial preference.
|
|
55
|
+
let sessionOverride: LayoutMode | null | undefined;
|
|
56
|
+
const SERVER_SNAPSHOT = adaptiveFor({
|
|
57
|
+
width: 1280,
|
|
58
|
+
height: 800,
|
|
59
|
+
userAgent: "",
|
|
60
|
+
override: null,
|
|
61
|
+
touch: false,
|
|
62
|
+
});
|
|
63
|
+
let pointerQuery: MediaQueryList | undefined;
|
|
64
|
+
function read(): AdaptiveState {
|
|
65
|
+
let stored: string | null = null;
|
|
66
|
+
try {
|
|
67
|
+
stored = localStorage.getItem(STORAGE_KEY);
|
|
68
|
+
} catch {
|
|
69
|
+
/* The session preference still works. */
|
|
70
|
+
}
|
|
71
|
+
const next = adaptiveFor({
|
|
72
|
+
width: window.innerWidth,
|
|
73
|
+
height: window.innerHeight,
|
|
74
|
+
userAgent: navigator.userAgent,
|
|
75
|
+
override:
|
|
76
|
+
sessionOverride === undefined ? parseOverride(location.search, stored) : sessionOverride,
|
|
77
|
+
touch: window.matchMedia?.("(pointer: coarse)").matches ?? false,
|
|
78
|
+
});
|
|
79
|
+
if (snapshot && JSON.stringify(snapshot) === JSON.stringify(next)) return snapshot;
|
|
80
|
+
snapshot = next;
|
|
81
|
+
const data = document.documentElement.dataset;
|
|
82
|
+
data["layout"] = next.mode;
|
|
83
|
+
data["m3Width"] = next.width;
|
|
84
|
+
data["m3Height"] = next.height;
|
|
85
|
+
data["m3Nav"] = next.navigation;
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
function emit(): void {
|
|
89
|
+
read();
|
|
90
|
+
for (const listener of listeners) listener();
|
|
91
|
+
}
|
|
92
|
+
function onStorage(event: StorageEvent): void {
|
|
93
|
+
if (event.key !== STORAGE_KEY && event.key !== null) return;
|
|
94
|
+
sessionOverride = undefined;
|
|
95
|
+
emit();
|
|
96
|
+
}
|
|
97
|
+
/** Set a live preference; persistence is best effort. Clearing returns to the viewport/TV detection. */
|
|
98
|
+
export function setLayoutOverride(mode: LayoutMode | null): void {
|
|
99
|
+
if (mode !== null && !LAYOUT_MODES.includes(mode)) throw new TypeError("Unknown layout mode");
|
|
100
|
+
sessionOverride = mode;
|
|
101
|
+
try {
|
|
102
|
+
if (mode) localStorage.setItem(STORAGE_KEY, mode);
|
|
103
|
+
else localStorage.removeItem(STORAGE_KEY);
|
|
104
|
+
} catch {
|
|
105
|
+
/* Session only. */
|
|
106
|
+
}
|
|
107
|
+
emit();
|
|
108
|
+
}
|
|
109
|
+
function subscribe(listener: Listener): () => void {
|
|
110
|
+
if (listeners.size === 0) {
|
|
111
|
+
window.addEventListener("resize", emit, { passive: true });
|
|
112
|
+
window.addEventListener("orientationchange", emit);
|
|
113
|
+
window.addEventListener("popstate", emit);
|
|
114
|
+
window.addEventListener("storage", onStorage);
|
|
115
|
+
pointerQuery = window.matchMedia?.("(pointer: coarse)");
|
|
116
|
+
pointerQuery?.addEventListener("change", emit);
|
|
117
|
+
}
|
|
118
|
+
listeners.add(listener);
|
|
119
|
+
return () => {
|
|
120
|
+
listeners.delete(listener);
|
|
121
|
+
if (listeners.size === 0) {
|
|
122
|
+
window.removeEventListener("resize", emit);
|
|
123
|
+
window.removeEventListener("orientationchange", emit);
|
|
124
|
+
window.removeEventListener("popstate", emit);
|
|
125
|
+
window.removeEventListener("storage", onStorage);
|
|
126
|
+
pointerQuery?.removeEventListener("change", emit);
|
|
127
|
+
pointerQuery = undefined;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function useAdaptive(): AdaptiveState {
|
|
132
|
+
return useSyncExternalStore(subscribe, read, () => SERVER_SNAPSHOT);
|
|
133
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Local showcase computations. Providers, credentials and OS writes stay with their native owners.
|
|
3
|
+
import { defineM3Api } from "@aphrody/m3-bun/api";
|
|
4
|
+
import { EventEncoder, EventType, type BaseEvent } from "@aphrody/ag-ui";
|
|
5
|
+
import { DesignCompiler } from "@aphrody/m3-design";
|
|
6
|
+
import { generate, TARGETS, type Target, type ThemeInput } from "@aphrody/m3-os-themes";
|
|
7
|
+
import {
|
|
8
|
+
generateM3Metadata,
|
|
9
|
+
generateM3Viewport,
|
|
10
|
+
m3JsonLd,
|
|
11
|
+
m3RobotsTxt,
|
|
12
|
+
m3SchemeColors,
|
|
13
|
+
m3SchemeStylesheet,
|
|
14
|
+
m3SitemapXml,
|
|
15
|
+
withM3,
|
|
16
|
+
} from "@aphrody/m3-next";
|
|
17
|
+
import { renderTemplate, TEMPLATES } from "@aphrody/scaffold";
|
|
18
|
+
import { transformSource, type MigrationKind } from "@aphrody/m3-codemods";
|
|
19
|
+
import lintPlugin from "@aphrody/eslint-plugin-m3";
|
|
20
|
+
import googleSansCode from "@aphrody/m3-fonts/fonts/web/GoogleSansCode-latin.woff2" with { type: "file" };
|
|
21
|
+
import robotoMono from "@aphrody/m3-fonts/fonts/web/RobotoMono-latin.woff2" with { type: "file" };
|
|
22
|
+
import { PACKAGES } from "../inventory.ts";
|
|
23
|
+
|
|
24
|
+
class InputError extends Error {}
|
|
25
|
+
const json = (data: unknown, status = 200) =>
|
|
26
|
+
Response.json(data, {
|
|
27
|
+
status,
|
|
28
|
+
headers: { "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" },
|
|
29
|
+
});
|
|
30
|
+
const guarded =
|
|
31
|
+
(handler: (request: Request) => Response | Promise<Response>) => async (request: Request) => {
|
|
32
|
+
try {
|
|
33
|
+
return await handler(request);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return json(
|
|
36
|
+
{
|
|
37
|
+
error:
|
|
38
|
+
error instanceof InputError
|
|
39
|
+
? error.message
|
|
40
|
+
: "The local computation could not be completed.",
|
|
41
|
+
},
|
|
42
|
+
error instanceof InputError ? 400 : 500,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Bound incoming JSON before parsing; no conversation or user source is persisted. */
|
|
48
|
+
async function readJson(request: Request): Promise<Record<string, unknown>> {
|
|
49
|
+
const reader = request.body?.getReader();
|
|
50
|
+
if (!reader) throw new InputError("A JSON request body is required.");
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
let body = "",
|
|
53
|
+
bytes = 0;
|
|
54
|
+
try {
|
|
55
|
+
// eslint-disable-next-line no-await-in-loop -- The next request-body chunk depends on the previous read.
|
|
56
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
57
|
+
bytes += chunk.value.byteLength;
|
|
58
|
+
if (bytes > 65_536) {
|
|
59
|
+
// eslint-disable-next-line no-await-in-loop -- Stop the sequential request stream before rejecting oversized input.
|
|
60
|
+
await reader.cancel();
|
|
61
|
+
throw new InputError("The request body exceeds 64 KiB.");
|
|
62
|
+
}
|
|
63
|
+
body += decoder.decode(chunk.value, { stream: true });
|
|
64
|
+
}
|
|
65
|
+
body += decoder.decode();
|
|
66
|
+
} finally {
|
|
67
|
+
reader.releaseLock();
|
|
68
|
+
}
|
|
69
|
+
let value: unknown;
|
|
70
|
+
try {
|
|
71
|
+
value = JSON.parse(body);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new InputError("The request body must be valid JSON.");
|
|
74
|
+
}
|
|
75
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
76
|
+
throw new InputError("The request body must be an object.");
|
|
77
|
+
return value as Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
function textInput(value: unknown, name: string, limit = 2000): string {
|
|
80
|
+
if (typeof value !== "string" || !value.trim() || value.length > limit)
|
|
81
|
+
throw new InputError(name + " must be a non-empty string of at most " + limit + " characters.");
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function themeInput(url: URL): ThemeInput {
|
|
85
|
+
const seed = url.searchParams.get("seed") ?? "#6750a4";
|
|
86
|
+
const mode = url.searchParams.get("mode") ?? "auto";
|
|
87
|
+
if (!/^#[a-f\d]{6}$/i.test(seed)) throw new InputError("seed must be #rrggbb.");
|
|
88
|
+
if (!["light", "dark", "auto"].includes(mode))
|
|
89
|
+
throw new InputError("mode must be light, dark or auto.");
|
|
90
|
+
return { name: "Showcase", seed, mode: mode as ThemeInput["mode"] };
|
|
91
|
+
}
|
|
92
|
+
function targetsFor(url: URL): readonly Target[] {
|
|
93
|
+
const target = url.searchParams.get("target") ?? "all";
|
|
94
|
+
if (target === "all") return TARGETS;
|
|
95
|
+
if (!(TARGETS as readonly string[]).includes(target))
|
|
96
|
+
throw new InputError("Unknown OS theme target.");
|
|
97
|
+
return [target as Target];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let toolsPromise: Promise<unknown[]> | undefined;
|
|
101
|
+
async function discoverMcpTools(): Promise<unknown[]> {
|
|
102
|
+
if (!toolsPromise)
|
|
103
|
+
toolsPromise = (async () => {
|
|
104
|
+
const [{ createM3McpServer }, { Client }, { InMemoryTransport }] = await Promise.all([
|
|
105
|
+
import("@aphrody/m3-mcp"),
|
|
106
|
+
import("@modelcontextprotocol/sdk/client/index.js"),
|
|
107
|
+
import("@modelcontextprotocol/sdk/inMemory.js"),
|
|
108
|
+
]);
|
|
109
|
+
const server = createM3McpServer();
|
|
110
|
+
const client = new Client({ name: "m3-showcase", version: "3.3.3" });
|
|
111
|
+
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
|
112
|
+
try {
|
|
113
|
+
await server.connect(serverTransport);
|
|
114
|
+
await client.connect(clientTransport);
|
|
115
|
+
return (await client.listTools()).tools;
|
|
116
|
+
} finally {
|
|
117
|
+
await client.close();
|
|
118
|
+
await server.close();
|
|
119
|
+
}
|
|
120
|
+
})().catch((error: unknown) => {
|
|
121
|
+
toolsPromise = undefined;
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
return toolsPromise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fonts: Readonly<Record<string, string>> = {
|
|
128
|
+
"GoogleSansCode-latin.woff2": googleSansCode,
|
|
129
|
+
"RobotoMono-latin.woff2": robotoMono,
|
|
130
|
+
};
|
|
131
|
+
const fontTags = new Map<string, string>();
|
|
132
|
+
|
|
133
|
+
/** A labelled local AG-UI fixture: real package data, protocol events, no model calls. */
|
|
134
|
+
function fixtureStream(request: Request, prompt: string): Response {
|
|
135
|
+
const encoder = new EventEncoder();
|
|
136
|
+
const runId = "fixture-" + crypto.randomUUID();
|
|
137
|
+
const messageId = runId + "-message";
|
|
138
|
+
const response =
|
|
139
|
+
"Local protocol fixture: your draft contains " +
|
|
140
|
+
prompt.length +
|
|
141
|
+
" characters. This workspace exposes " +
|
|
142
|
+
PACKAGES.length +
|
|
143
|
+
" packages. The transcript below is rendered from AG-UI events.";
|
|
144
|
+
const events: BaseEvent[] = [
|
|
145
|
+
{ type: EventType.RUN_STARTED, threadId: "showcase-fixture", runId },
|
|
146
|
+
{ type: EventType.TOOL_CALL_START, toolCallId: "inventory", toolCallName: "package_inventory" },
|
|
147
|
+
{ type: EventType.TOOL_CALL_ARGS, toolCallId: "inventory", delta: "{}" },
|
|
148
|
+
{ type: EventType.TOOL_CALL_END, toolCallId: "inventory" },
|
|
149
|
+
{
|
|
150
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
151
|
+
toolCallId: "inventory",
|
|
152
|
+
messageId: runId + "-inventory",
|
|
153
|
+
role: "tool",
|
|
154
|
+
content: JSON.stringify({ packages: PACKAGES.length, source: "build inventory" }),
|
|
155
|
+
},
|
|
156
|
+
{ type: EventType.TEXT_MESSAGE_START, messageId, role: "assistant" },
|
|
157
|
+
...response
|
|
158
|
+
.match(/.{1,12}/g)!
|
|
159
|
+
.map((delta) => ({ type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta })),
|
|
160
|
+
{ type: EventType.TEXT_MESSAGE_END, messageId },
|
|
161
|
+
{ type: EventType.RUN_FINISHED, threadId: "showcase-fixture", runId },
|
|
162
|
+
] as BaseEvent[];
|
|
163
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
164
|
+
let abort: (() => void) | undefined;
|
|
165
|
+
const cleanup = () => {
|
|
166
|
+
if (timer) clearInterval(timer);
|
|
167
|
+
if (abort) request.signal.removeEventListener("abort", abort);
|
|
168
|
+
};
|
|
169
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
170
|
+
start(controller) {
|
|
171
|
+
if (request.signal.aborted) {
|
|
172
|
+
controller.error(request.signal.reason);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
let index = 0;
|
|
176
|
+
abort = () => {
|
|
177
|
+
cleanup();
|
|
178
|
+
controller.error(request.signal.reason);
|
|
179
|
+
};
|
|
180
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
181
|
+
timer = setInterval(() => {
|
|
182
|
+
controller.enqueue(new TextEncoder().encode(encoder.encodeSSE(events[index++])));
|
|
183
|
+
if (index === events.length) {
|
|
184
|
+
cleanup();
|
|
185
|
+
controller.close();
|
|
186
|
+
}
|
|
187
|
+
}, 45);
|
|
188
|
+
},
|
|
189
|
+
cancel() {
|
|
190
|
+
cleanup();
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
return new Response(stream, {
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "text/event-stream",
|
|
196
|
+
"Cache-Control": "no-store",
|
|
197
|
+
"X-M3-Fixture": "true",
|
|
198
|
+
"X-Content-Type-Options": "nosniff",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export default defineM3Api({
|
|
204
|
+
"/api/packages": () =>
|
|
205
|
+
Response.json(PACKAGES, { headers: { "Cache-Control": "public, max-age=60" } }),
|
|
206
|
+
"/api/package": (request) => {
|
|
207
|
+
const name = new URL(request.url).searchParams.get("name");
|
|
208
|
+
const found = PACKAGES.find((p) => p.name === name);
|
|
209
|
+
return found ? json(found) : json({ error: "Unknown package " + String(name) }, 404);
|
|
210
|
+
},
|
|
211
|
+
"/api/echo": {
|
|
212
|
+
POST: guarded(async (request) =>
|
|
213
|
+
json({
|
|
214
|
+
received: await readJson(request),
|
|
215
|
+
method: request.method,
|
|
216
|
+
contentType: request.headers.get("content-type"),
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
"/api/m3/fonts/:name": async (request) => {
|
|
221
|
+
const name = request.params.name;
|
|
222
|
+
if (!Object.hasOwn(fonts, name)) return json({ error: "Unknown font." }, 404);
|
|
223
|
+
const file = Bun.file(fonts[name]);
|
|
224
|
+
let tag = fontTags.get(name);
|
|
225
|
+
if (!tag) {
|
|
226
|
+
tag =
|
|
227
|
+
'"' + new Bun.CryptoHasher("sha256").update(await file.arrayBuffer()).digest("hex") + '"';
|
|
228
|
+
fontTags.set(name, tag);
|
|
229
|
+
}
|
|
230
|
+
const headers = {
|
|
231
|
+
"Content-Type": "font/woff2",
|
|
232
|
+
"Cache-Control": "public, max-age=86400",
|
|
233
|
+
ETag: tag,
|
|
234
|
+
"X-Content-Type-Options": "nosniff",
|
|
235
|
+
};
|
|
236
|
+
if (request.headers.get("if-none-match") === tag)
|
|
237
|
+
return new Response(null, { status: 304, headers });
|
|
238
|
+
return new Response(request.method === "HEAD" ? null : file, { headers });
|
|
239
|
+
},
|
|
240
|
+
"/api/ai/stream": {
|
|
241
|
+
POST: guarded(async (request) =>
|
|
242
|
+
fixtureStream(request, textInput((await readJson(request)).prompt, "prompt")),
|
|
243
|
+
),
|
|
244
|
+
},
|
|
245
|
+
"/api/design": {
|
|
246
|
+
POST: guarded(async (request) => {
|
|
247
|
+
const body = await readJson(request);
|
|
248
|
+
const brief = textInput(body.brief, "brief");
|
|
249
|
+
const seed = body.seed === undefined ? undefined : textInput(body.seed, "seed", 7);
|
|
250
|
+
if (seed !== undefined && !/^#[a-f\d]{6}$/i.test(seed))
|
|
251
|
+
throw new InputError("seed must be #rrggbb.");
|
|
252
|
+
return json(new DesignCompiler().compile(brief, seed));
|
|
253
|
+
}),
|
|
254
|
+
},
|
|
255
|
+
"/api/next": guarded((request) => {
|
|
256
|
+
const url = new URL(request.url);
|
|
257
|
+
const theme = themeInput(url);
|
|
258
|
+
const options = { seed: theme.seed };
|
|
259
|
+
return json({
|
|
260
|
+
metadata: generateM3Metadata({
|
|
261
|
+
name: "Aphrody M3 Showcase",
|
|
262
|
+
description: "An adaptive Material 3 component showcase.",
|
|
263
|
+
url: url.origin,
|
|
264
|
+
}),
|
|
265
|
+
viewport: generateM3Viewport(options),
|
|
266
|
+
schemeCss: m3SchemeStylesheet(
|
|
267
|
+
m3SchemeColors(options),
|
|
268
|
+
m3SchemeColors({ ...options, dark: true }),
|
|
269
|
+
),
|
|
270
|
+
config: withM3({}),
|
|
271
|
+
robots: m3RobotsTxt({ origin: url.origin, rules: [{ userAgent: "*", disallow: ["/api/"] }] }),
|
|
272
|
+
sitemap: m3SitemapXml(["/", "/theme", "/components"], url.origin),
|
|
273
|
+
jsonLd: m3JsonLd({
|
|
274
|
+
"@context": "https://schema.org",
|
|
275
|
+
"@type": "SoftwareApplication",
|
|
276
|
+
name: "Aphrody M3 Showcase",
|
|
277
|
+
applicationCategory: "DesignApplication",
|
|
278
|
+
url: url.origin,
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
}),
|
|
282
|
+
"/api/system": guarded(async (request) => {
|
|
283
|
+
const url = new URL(request.url);
|
|
284
|
+
const files = generate(themeInput(url), targetsFor(url));
|
|
285
|
+
const sample = renderTemplate({ template: "web", name: "m3-demo", m3Version: "3.3.3" });
|
|
286
|
+
return json({
|
|
287
|
+
themeFiles: files.map((file) => ({
|
|
288
|
+
target: file.target,
|
|
289
|
+
path: file.path,
|
|
290
|
+
bytes:
|
|
291
|
+
typeof file.content === "string"
|
|
292
|
+
? Buffer.byteLength(file.content)
|
|
293
|
+
: file.content.byteLength,
|
|
294
|
+
binary: typeof file.content !== "string",
|
|
295
|
+
})),
|
|
296
|
+
templates: TEMPLATES,
|
|
297
|
+
scaffold: sample,
|
|
298
|
+
mcpTools: await discoverMcpTools(),
|
|
299
|
+
lintRules: Object.entries(lintPlugin.rules).map(([name, rule]) => ({
|
|
300
|
+
name,
|
|
301
|
+
description: rule.meta.docs?.description ?? "",
|
|
302
|
+
})),
|
|
303
|
+
migration: transformSource(
|
|
304
|
+
'import { Button } from "@mui/material"; export default () => <Button variant="contained">Save</Button>;',
|
|
305
|
+
),
|
|
306
|
+
});
|
|
307
|
+
}),
|
|
308
|
+
"/api/os-theme": guarded((request) => {
|
|
309
|
+
const url = new URL(request.url);
|
|
310
|
+
const path = url.searchParams.get("path");
|
|
311
|
+
const file = generate(themeInput(url), targetsFor(url)).find((item) => item.path === path);
|
|
312
|
+
if (!file) return json({ error: "Unknown generated theme file." }, 404);
|
|
313
|
+
if (typeof file.content !== "string")
|
|
314
|
+
return new Response(file.content as unknown as BodyInit, {
|
|
315
|
+
headers: { "Content-Type": "image/png", "Cache-Control": "no-store" },
|
|
316
|
+
});
|
|
317
|
+
return json({ path: file.path, content: file.content });
|
|
318
|
+
}),
|
|
319
|
+
"/api/migrate": {
|
|
320
|
+
POST: guarded(async (request) => {
|
|
321
|
+
const body = await readJson(request);
|
|
322
|
+
const source = textInput(body.source, "source", 40_000);
|
|
323
|
+
const kind = body.kind ?? "mui";
|
|
324
|
+
if (kind !== "mui" && kind !== "icons") throw new InputError("kind must be mui or icons.");
|
|
325
|
+
try {
|
|
326
|
+
return json(transformSource(source, kind as MigrationKind));
|
|
327
|
+
} catch {
|
|
328
|
+
throw new InputError("The source must be valid TSX.");
|
|
329
|
+
}
|
|
330
|
+
}),
|
|
331
|
+
},
|
|
332
|
+
});
|