diff --git a/.claude/skills/docs/SKILL.md b/.claude/skills/docs/SKILL.md
index 82814c50..92a88e8f 100644
--- a/.claude/skills/docs/SKILL.md
+++ b/.claude/skills/docs/SKILL.md
@@ -104,6 +104,7 @@ const player = createPlayer({ src: 'video.mp4' });
**Rules:**
+- **Sentence case for headings** — capitalize only the first word and proper nouns (e.g., "Choose your JS framework", not "Choose Your JS Framework")
- Active voice, second person ("you")
- Short sentences
- No filler ("In order to", "basically", "simply")
diff --git a/packages/__tech-preview__/react/package.json b/packages/__tech-preview__/react/package.json
index f877c0f5..7d808d2d 100644
--- a/packages/__tech-preview__/react/package.json
+++ b/packages/__tech-preview__/react/package.json
@@ -47,7 +47,7 @@
"build:watch": "tsdown --watch ./src --ignore-watch ./src/icons/generated-icons --silent",
"dev": "pnpm run build:watch",
"format:icons": "echo noop. Need to replace based on eslint vs. prettier decisions!",
- "generate:icons": "svgr --config-file svgr.config.js --out-dir src/icons/generated-icons ../icons/assets && pnpm format:icons",
+ "generate:icons": "svgr --config-file svgr.config.js --no-prettier --out-dir src/icons/generated-icons ../icons/assets",
"test": "echo \"No tests yet\"",
"clean": "rm -rf dist src/icons/generated-icons"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a7a0b708..04982f3e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -330,6 +330,12 @@ importers:
'@base-ui-components/react':
specifier: 1.0.0-beta.4
version: 1.0.0-beta.4(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@mux/mux-node':
+ specifier: ^12.8.1
+ version: 12.8.1
+ '@mux/mux-uploader-react':
+ specifier: ^1.4.1
+ version: 1.4.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@nanostores/react':
specifier: ^1.0.0
version: 1.0.0(nanostores@1.1.0)(react@18.3.1)
@@ -357,6 +363,12 @@ importers:
github-slugger:
specifier: ^2.0.0
version: 2.0.0
+ iron-session:
+ specifier: ^8.0.4
+ version: 8.0.4
+ jose:
+ specifier: ^6.1.3
+ version: 6.1.3
just-debounce-it:
specifier: ^3.2.0
version: 3.2.0
@@ -1374,6 +1386,28 @@ packages:
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
+ '@mux/mux-node@12.8.1':
+ resolution: {integrity: sha512-ey2eKn7iwVrjRVJfB/yF9HPDVpEl+fZV00ydx0kc9/BMs+wjBi0KgU0HMqUjTgEoyMUTlHmJ3U3BfDvreyg5iQ==}
+
+ '@mux/mux-uploader-react@1.4.1':
+ resolution: {integrity: sha512-D5tXivE8k+DJSbUq+8Wp4G9IsyocJQOZdZPOrpoxor2Alcwo/fJ/+gHkNNPsltx6yv1eQ0O5FWTCKSiIOkxy5w==}
+ peerDependencies:
+ '@types/react': ^17.0.0 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0
+ '@types/react-dom': '*'
+ react: ^17.0.2 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0
+ react-dom: ^17.0.2 || ^17.0.2-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@mux/mux-uploader@1.4.1':
+ resolution: {integrity: sha512-7ZOrXuv6iODhAcGkj0OI8FINGOF4EKXm4bB4vnQhqgwjP4BsKZr5qFvg78e8oHR3CEA7fNZobk/x4Qtnr4Fnhg==}
+
+ '@mux/upchunk@3.5.0':
+ resolution: {integrity: sha512-D+TtvlujlZQjh5I+vFzJ31h5E1uVpEaLdR8M3BNaCFbVLnFMZs8J/L/fYSUyVGnyHT/yDtPHn/IHKdo3G6oSjA==}
+
'@nanostores/react@1.0.0':
resolution: {integrity: sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A==}
engines: {node: ^20.0.0 || >=22.0.0}
@@ -2522,9 +2556,15 @@ packages:
'@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
+ '@types/node-fetch@2.6.13':
+ resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
+
'@types/node@17.0.45':
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
+ '@types/node@18.19.130':
+ resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
+
'@types/node@20.19.30':
resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==}
@@ -2761,6 +2801,10 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
+ agentkeepalive@4.6.0:
+ resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
+ engines: {node: '>= 8.0.0'}
+
ajv-errors@3.0.0:
resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==}
peerDependencies:
@@ -2861,6 +2905,9 @@ packages:
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
@@ -2961,6 +3008,10 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
callsite@1.0.0:
resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==}
@@ -3089,6 +3140,10 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -3158,6 +3213,10 @@ packages:
cookie-es@1.2.2:
resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==}
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
@@ -3317,6 +3376,10 @@ packages:
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -3412,6 +3475,9 @@ packages:
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+ dom-walk@0.1.2:
+ resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
+
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
@@ -3450,6 +3516,10 @@ packages:
oxc-resolver:
optional: true
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -3509,9 +3579,25 @@ packages:
error-ex@1.3.4:
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -3590,6 +3676,10 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
+ event-target-shim@6.0.2:
+ resolution: {integrity: sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==}
+ engines: {node: '>=10.13.0'}
+
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@@ -3708,6 +3798,17 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
+ form-data-encoder@1.7.2:
+ resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
+
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
+ formdata-node@4.4.1:
+ resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
+ engines: {node: '>= 12.20'}
+
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
@@ -3742,6 +3843,10 @@ packages:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
get-port-please@3.2.0:
resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==}
@@ -3749,6 +3854,10 @@ packages:
resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==}
engines: {node: '>=16'}
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
get-stream@5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
@@ -3785,11 +3894,18 @@ packages:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
engines: {node: '>=18'}
+ global@4.4.0:
+ resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
+
gonzales-pe@4.3.0:
resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==}
engines: {node: '>=0.6.0'}
hasBin: true
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -3804,6 +3920,14 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -3890,6 +4014,9 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
+ humanize-ms@1.2.1:
+ resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -3952,6 +4079,9 @@ packages:
resolution: {integrity: sha512-7Xnt54Dco7uYkfdAw0r2vCly3z0rSaVhEXMzPvl3FndsTVm5p26j+PO+gyinkYmcsEUvX2Rh7OGK7KzYWRu6BA==}
hasBin: true
+ iron-session@8.0.4:
+ resolution: {integrity: sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==}
+
iron-webcrypto@1.2.1:
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
@@ -3992,6 +4122,9 @@ packages:
resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==}
engines: {node: '>=18'}
+ is-function@1.0.2:
+ resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==}
+
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@@ -4095,6 +4228,12 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
+ jose@4.15.9:
+ resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
+
+ jose@6.1.3:
+ resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
+
jpeg-js@0.4.4:
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
@@ -4415,6 +4554,10 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
mdast-util-definitions@6.0.0:
resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
@@ -4599,10 +4742,18 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
@@ -4615,6 +4766,9 @@ packages:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
+ min-document@2.19.2:
+ resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==}
+
min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
@@ -4759,6 +4913,10 @@ packages:
nwsapi@2.2.23:
resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
@@ -4855,6 +5013,9 @@ packages:
resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==}
engines: {node: '>=14'}
+ parse-headers@2.0.6:
+ resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==}
+
parse-imports@2.2.1:
resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==}
engines: {node: '>= 18'}
@@ -5020,6 +5181,9 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
@@ -5060,6 +5224,9 @@ packages:
peerDependencies:
react: ^19.2.3
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -5763,6 +5930,9 @@ packages:
uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+ undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -6064,6 +6234,10 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
+ web-streams-polyfill@4.0.0-beta.3:
+ resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
+ engines: {node: '>= 14'}
+
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -6163,6 +6337,9 @@ packages:
utf-8-validate:
optional: true
+ xhr@2.6.0:
+ resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==}
+
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
@@ -7171,6 +7348,38 @@ snapshots:
'@mixmark-io/domino@2.2.0': {}
+ '@mux/mux-node@12.8.1':
+ dependencies:
+ '@types/node': 18.19.130
+ '@types/node-fetch': 2.6.13
+ abort-controller: 3.0.0
+ agentkeepalive: 4.6.0
+ form-data-encoder: 1.7.2
+ formdata-node: 4.4.1
+ jose: 4.15.9
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
+ '@mux/mux-uploader-react@1.4.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@mux/mux-uploader': 1.4.1
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+ '@mux/mux-uploader@1.4.1':
+ dependencies:
+ '@mux/upchunk': 3.5.0
+
+ '@mux/upchunk@3.5.0':
+ dependencies:
+ event-target-shim: 6.0.2
+ xhr: 2.6.0
+
'@nanostores/react@1.0.0(nanostores@1.1.0)(react@18.3.1)':
dependencies:
nanostores: 1.1.0
@@ -8544,8 +8753,17 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/node-fetch@2.6.13':
+ dependencies:
+ '@types/node': 22.19.3
+ form-data: 4.0.5
+
'@types/node@17.0.45': {}
+ '@types/node@18.19.130':
+ dependencies:
+ undici-types: 5.26.5
+
'@types/node@20.19.30':
dependencies:
undici-types: 6.21.0
@@ -8882,6 +9100,10 @@ snapshots:
agent-base@7.1.4: {}
+ agentkeepalive@4.6.0:
+ dependencies:
+ humanize-ms: 1.2.1
+
ajv-errors@3.0.0(ajv@8.17.1):
dependencies:
ajv: 8.17.1
@@ -9080,6 +9302,8 @@ snapshots:
async@3.2.6: {}
+ asynckit@0.4.0: {}
+
axobject-query@4.1.0: {}
b4a@1.7.3: {}
@@ -9169,6 +9393,11 @@ snapshots:
cac@6.7.14: {}
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
callsite@1.0.0: {}
callsites@3.1.0: {}
@@ -9284,6 +9513,10 @@ snapshots:
colorette@2.0.20: {}
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
comma-separated-tokens@2.0.3: {}
commander@10.0.1: {}
@@ -9340,6 +9573,8 @@ snapshots:
cookie-es@1.2.2: {}
+ cookie@0.7.2: {}
+
cookie@1.1.1: {}
copy-file@11.1.0:
@@ -9483,6 +9718,8 @@ snapshots:
defu@6.1.4: {}
+ delayed-stream@1.0.0: {}
+
dequal@2.0.3: {}
destr@2.0.5: {}
@@ -9577,6 +9814,8 @@ snapshots:
domhandler: 5.0.3
entities: 4.5.0
+ dom-walk@0.1.2: {}
+
domelementtype@2.3.0: {}
domhandler@5.0.3:
@@ -9608,6 +9847,12 @@ snapshots:
dts-resolver@2.1.3: {}
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
@@ -9651,8 +9896,23 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
es-module-lexer@1.7.0: {}
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
esast-util-from-estree@2.0.0:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -9784,6 +10044,8 @@ snapshots:
event-target-shim@5.0.1: {}
+ event-target-shim@6.0.2: {}
+
eventemitter3@5.0.1: {}
events-universal@1.0.1:
@@ -9914,6 +10176,21 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
+ form-data-encoder@1.7.2: {}
+
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ formdata-node@4.4.1:
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 4.0.0-beta.3
+
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
@@ -9938,10 +10215,28 @@ snapshots:
get-east-asian-width@1.4.0: {}
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
get-port-please@3.2.0: {}
get-port@7.1.0: {}
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
get-stream@5.2.0:
dependencies:
pump: 3.0.3
@@ -9985,10 +10280,17 @@ snapshots:
dependencies:
ini: 4.1.1
+ global@4.4.0:
+ dependencies:
+ min-document: 2.19.2
+ process: 0.11.10
+
gonzales-pe@4.3.0:
dependencies:
minimist: 1.2.8
+ gopd@1.2.0: {}
+
graceful-fs@4.2.11: {}
h3@1.15.4:
@@ -10011,6 +10313,12 @@ snapshots:
has-flag@4.0.0: {}
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -10188,6 +10496,10 @@ snapshots:
human-signals@5.0.0: {}
+ humanize-ms@1.2.1:
+ dependencies:
+ ms: 2.1.3
+
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@@ -10277,6 +10589,12 @@ snapshots:
- ioredis
- uploadthing
+ iron-session@8.0.4:
+ dependencies:
+ cookie: 0.7.2
+ iron-webcrypto: 1.2.1
+ uncrypto: 0.1.3
+
iron-webcrypto@1.2.1: {}
is-alphabetical@2.0.1: {}
@@ -10308,6 +10626,8 @@ snapshots:
dependencies:
get-east-asian-width: 1.4.0
+ is-function@1.0.2: {}
+
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
@@ -10389,6 +10709,10 @@ snapshots:
jiti@2.6.1: {}
+ jose@4.15.9: {}
+
+ jose@6.1.3: {}
+
jpeg-js@0.4.4: {}
js-image-generator@1.0.4:
@@ -10724,6 +11048,8 @@ snapshots:
markdown-table@3.0.4: {}
+ math-intrinsics@1.1.0: {}
+
mdast-util-definitions@6.0.0:
dependencies:
'@types/mdast': 4.0.4
@@ -11178,8 +11504,14 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
+ mime-db@1.52.0: {}
+
mime-db@1.54.0: {}
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
@@ -11188,6 +11520,10 @@ snapshots:
mimic-function@5.0.1: {}
+ min-document@2.19.2:
+ dependencies:
+ dom-walk: 0.1.2
+
min-indent@1.0.1: {}
minimatch@5.1.6:
@@ -11301,6 +11637,8 @@ snapshots:
nwsapi@2.2.23: {}
+ object-assign@4.1.1: {}
+
obug@2.1.1: {}
ofetch@1.5.1:
@@ -11402,6 +11740,8 @@ snapshots:
parse-gitignore@2.0.0: {}
+ parse-headers@2.0.6: {}
+
parse-imports@2.2.1:
dependencies:
es-module-lexer: 1.7.0
@@ -11562,6 +11902,12 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
property-information@7.1.0: {}
proxy-from-env@1.1.0: {}
@@ -11596,6 +11942,8 @@ snapshots:
react: 19.2.3
scheduler: 0.27.0
+ react-is@16.13.1: {}
+
react-is@17.0.2: {}
react-refresh@0.17.0: {}
@@ -12418,6 +12766,8 @@ snapshots:
uncrypto@0.1.3: {}
+ undici-types@5.26.5: {}
+
undici-types@6.21.0: {}
unicode-properties@1.4.1:
@@ -12724,6 +13074,8 @@ snapshots:
web-streams-polyfill@3.3.3: {}
+ web-streams-polyfill@4.0.0-beta.3: {}
+
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}
@@ -12819,6 +13171,13 @@ snapshots:
ws@8.18.3: {}
+ xhr@2.6.0:
+ dependencies:
+ global: 4.4.0
+ is-function: 1.0.2
+ parse-headers: 2.0.6
+ xtend: 4.0.2
+
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
diff --git a/site/.gitignore b/site/.gitignore
index acfa5d93..3b51c803 100644
--- a/site/.gitignore
+++ b/site/.gitignore
@@ -28,3 +28,8 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
+.vercel
+.env*.local
+
+# Local Netlify folder
+.netlify
diff --git a/site/CLAUDE.md b/site/CLAUDE.md
index 61ff329d..0d4b73b8 100644
--- a/site/CLAUDE.md
+++ b/site/CLAUDE.md
@@ -104,6 +104,36 @@ import clsx from 'clsx';
```
+### Avoiding Arbitrary Values
+
+**Prefer token-based utilities or inline styles over arbitrary values:**
+
+```astro
+
+
Content
+
+
+Content
+
+
+Content
+```
+
+**For responsive/dark mode with non-token values, use CSS custom properties:**
+
+```astro
+
+Content
+
+
+Content
+```
+
+This limits the classnames Tailwind must generate.
+
## Project Structure
```
diff --git a/site/env.d.ts b/site/env.d.ts
new file mode 100644
index 00000000..3ee6a312
--- /dev/null
+++ b/site/env.d.ts
@@ -0,0 +1,12 @@
+///
+
+declare namespace App {
+ interface Locals {
+ user?: {
+ name: string;
+ email: string;
+ };
+
+ accessToken?: string;
+ }
+}
diff --git a/site/package.json b/site/package.json
index 79c3e119..66b35e48 100644
--- a/site/package.json
+++ b/site/package.json
@@ -18,6 +18,8 @@
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.6.0",
"@base-ui-components/react": "1.0.0-beta.4",
+ "@mux/mux-node": "^12.8.1",
+ "@mux/mux-uploader-react": "^1.4.1",
"@nanostores/react": "^1.0.0",
"@pagefind/default-ui": "^1.4.0",
"@sentry/astro": "^10.32.1",
@@ -27,6 +29,8 @@
"astro": "^5.14.4",
"clsx": "^2.1.1",
"github-slugger": "^2.0.0",
+ "iron-session": "^8.0.4",
+ "jose": "^6.1.3",
"just-debounce-it": "^3.2.0",
"just-throttle": "^4.2.0",
"lucide-react": "^0.546.0",
diff --git a/site/src/actions/auth.ts b/site/src/actions/auth.ts
new file mode 100644
index 00000000..b2b0df64
--- /dev/null
+++ b/site/src/actions/auth.ts
@@ -0,0 +1,69 @@
+import { ActionError, defineAction } from 'astro:actions';
+import { z } from 'astro:schema';
+import { SESSION_COOKIE_NAME } from '@/utils/auth';
+
+const { OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_URL, PROD } = import.meta.env;
+
+export const auth = {
+ /**
+ * Initiates the OAuth 2.0 login flow
+ *
+ * Generates a CSRF-protected state parameter and returns the authorization URL
+ * for the client to redirect to. The state is stored in a short-lived cookie
+ * and verified in the callback endpoint.
+ *
+ * @returns {authorizationUrl} - The OAuth authorization URL to redirect to
+ */
+ initiateLogin: defineAction({
+ handler: async (_input, ctx) => {
+ // Validate required OAuth configuration
+ if (!OAUTH_CLIENT_ID || !OAUTH_REDIRECT_URI || !OAUTH_URL) {
+ throw new ActionError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'OAuth configuration missing',
+ });
+ }
+
+ // Generate cryptographically secure random state for CSRF protection
+ const state = crypto.randomUUID();
+
+ // Build OAuth 2.0 authorization URL with required parameters
+ const params = new URLSearchParams({
+ client_id: OAUTH_CLIENT_ID,
+ redirect_uri: OAUTH_REDIRECT_URI,
+ response_type: 'code',
+ scope: 'openid profile email offline_access',
+ state,
+ });
+
+ // Store state in a short-lived cookie for verification in the callback
+ ctx.cookies.set('state', state, {
+ httpOnly: true,
+ secure: PROD,
+ sameSite: 'lax',
+ maxAge: 600, // Expires after 10 minutes
+ path: '/',
+ });
+
+ return { authorizationUrl: `${OAUTH_URL}/oauth2/authorize?${params.toString()}` };
+ },
+ }),
+
+ /**
+ * Logs out the current user by clearing the session cookie
+ *
+ * Accepts form submissions to prevent CSRF attacks on logout endpoints.
+ *
+ * @returns {success: true} - Indicates successful logout
+ */
+ logout: defineAction({
+ accept: 'form',
+ input: z.object({}),
+ handler: async (_input, ctx) => {
+ // Clear the encrypted session cookie
+ ctx.cookies.delete(SESSION_COOKIE_NAME, { path: '/' });
+
+ return { success: true };
+ },
+ }),
+};
diff --git a/site/src/actions/index.ts b/site/src/actions/index.ts
new file mode 100644
index 00000000..f43c741a
--- /dev/null
+++ b/site/src/actions/index.ts
@@ -0,0 +1,7 @@
+import { auth } from './auth';
+import { mux } from './mux';
+
+export const server = {
+ auth,
+ mux,
+};
diff --git a/site/src/actions/mux.ts b/site/src/actions/mux.ts
new file mode 100644
index 00000000..413fb4ab
--- /dev/null
+++ b/site/src/actions/mux.ts
@@ -0,0 +1,175 @@
+import { ActionError, defineAction } from 'astro:actions';
+import { z } from 'astro:schema';
+import Mux from '@mux/mux-node';
+
+/**
+ * Creates an authenticated Mux API client
+ *
+ * Uses the access token from the user's session (provided by middleware)
+ * to authenticate requests to the Mux API.
+ *
+ * @param token - OAuth access token from user session
+ * @throws {Error} If token is missing or invalid
+ */
+function getMuxClient(token: string | undefined) {
+ if (!token) {
+ throw new ActionError({ code: 'UNAUTHORIZED' });
+ }
+
+ return new Mux({
+ authorizationToken: token,
+ baseURL: import.meta.env.MUX_API_URL ?? 'https://api.mux.com',
+ });
+}
+
+export const mux = {
+ /**
+ * List video assets with pagination
+ *
+ * Fetches a paginated list of video assets from Mux.
+ * Requires an authenticated session with valid access token.
+ *
+ * @param limit - Number of assets per page (default: 25)
+ * @param page - Page number to fetch (default: 1)
+ * @returns Array of video asset objects
+ */
+ listAssets: defineAction({
+ input: z.object({
+ limit: z.number().optional().default(25),
+ page: z.number().optional().default(1),
+ }),
+ handler: async (input, ctx) => {
+ const muxClient = getMuxClient(ctx.locals.accessToken);
+
+ try {
+ const assets = await muxClient.video.assets.list({
+ limit: input.limit,
+ page: input.page,
+ });
+
+ return assets.data;
+ } catch (error) {
+ throw new ActionError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: error instanceof Error ? error.message : 'Failed to fetch assets',
+ });
+ }
+ },
+ }),
+
+ /**
+ * Retrieve a single video asset by ID
+ *
+ * Fetches detailed information about a specific video asset.
+ * Requires an authenticated session with valid access token.
+ *
+ * @param id - The unique Mux asset ID
+ * @returns Video asset object with full details
+ */
+ getAsset: defineAction({
+ input: z.object({
+ id: z.string(),
+ }),
+ handler: async (input, ctx) => {
+ const muxClient = getMuxClient(ctx.locals.accessToken);
+
+ try {
+ const asset = await muxClient.video.assets.retrieve(input.id);
+
+ return asset;
+ } catch (error) {
+ throw new ActionError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: error instanceof Error ? error.message : 'Failed to fetch asset',
+ });
+ }
+ },
+ }),
+
+ /**
+ * Create a direct upload URL for client-side uploads.
+ * Returns the signed upload URL and upload ID for tracking.
+ */
+ createDirectUpload: defineAction({
+ input: z.object({
+ corsOrigin: z.string().optional(),
+ }),
+ handler: async (input, ctx) => {
+ const muxClient = getMuxClient(ctx.locals.accessToken);
+
+ try {
+ const upload = await muxClient.video.uploads.create({
+ cors_origin: input.corsOrigin || '*',
+ new_asset_settings: {
+ playback_policy: ['public'],
+ video_quality: 'basic',
+ },
+ });
+
+ return {
+ uploadUrl: upload.url,
+ uploadId: upload.id,
+ };
+ } catch (error) {
+ throw new ActionError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: error instanceof Error ? error.message : 'Failed to create upload',
+ });
+ }
+ },
+ }),
+
+ /**
+ * Poll upload status to get asset_id once processing begins.
+ * Status: 'waiting' | 'asset_created' | 'errored' | 'cancelled' | 'timed_out'
+ */
+ getUploadStatus: defineAction({
+ input: z.object({
+ uploadId: z.string(),
+ }),
+ handler: async (input, ctx) => {
+ const muxClient = getMuxClient(ctx.locals.accessToken);
+
+ try {
+ const upload = await muxClient.video.uploads.retrieve(input.uploadId);
+
+ return {
+ status: upload.status,
+ assetId: upload.asset_id,
+ };
+ } catch (error) {
+ throw new ActionError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: error instanceof Error ? error.message : 'Failed to get upload status',
+ });
+ }
+ },
+ }),
+
+ /**
+ * Poll asset status to get playback_id once ready.
+ * Status: 'preparing' | 'ready' | 'errored'
+ */
+ getAssetStatus: defineAction({
+ input: z.object({
+ assetId: z.string(),
+ }),
+ handler: async (input, ctx) => {
+ const muxClient = getMuxClient(ctx.locals.accessToken);
+
+ try {
+ const asset = await muxClient.video.assets.retrieve(input.assetId);
+
+ return {
+ status: asset.status,
+ playbackId: asset.playback_ids?.[0]?.id,
+ };
+ } catch (error) {
+ throw new ActionError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: error instanceof Error ? error.message : 'Failed to get asset status',
+ });
+ }
+ },
+ }),
+};
diff --git a/site/src/components/Code/ClientCode.tsx b/site/src/components/Code/ClientCode.tsx
index 2c2ff27c..eb749888 100644
--- a/site/src/components/Code/ClientCode.tsx
+++ b/site/src/components/Code/ClientCode.tsx
@@ -1,3 +1,4 @@
+import bash from 'shiki/langs/bash.mjs';
import css from 'shiki/langs/css.mjs';
import html from 'shiki/langs/html.mjs';
import javascript from 'shiki/langs/javascript.mjs';
@@ -7,7 +8,7 @@ import type { SharedProps } from './Shared';
import Shared from './Shared';
const clientHighlighter = await createHighlighter({
- langs: [html, tsx, css, javascript],
+ langs: [bash, html, tsx, css, javascript],
});
/**
diff --git a/site/src/components/ImageRadioGroup.tsx b/site/src/components/ImageRadioGroup.tsx
new file mode 100644
index 00000000..9c030782
--- /dev/null
+++ b/site/src/components/ImageRadioGroup.tsx
@@ -0,0 +1,88 @@
+import { Radio } from '@base-ui-components/react/radio';
+import { RadioGroup } from '@base-ui-components/react/radio-group';
+import clsx from 'clsx';
+import type { ReactNode } from 'react';
+import { twMerge } from 'tailwind-merge';
+
+export interface ImageRadioOption {
+ value: T;
+ label: string;
+ image: ReactNode;
+ disabled?: boolean;
+}
+
+export interface ImageRadioGroupProps {
+ value: T;
+ onChange: (value: T) => void;
+ options: ImageRadioOption[];
+ 'aria-label': string;
+ className?: string;
+ size?: 'sm' | 'md';
+ labelPosition?: 'block' | 'inline';
+}
+
+export default function ImageRadioGroup({
+ value,
+ onChange,
+ options,
+ 'aria-label': ariaLabel,
+ className,
+ size = 'md',
+ labelPosition = 'block',
+}: ImageRadioGroupProps) {
+ const boxSize = size === 'sm' ? 8 : 20;
+ return (
+ onChange(newValue as T)}
+ aria-label={ariaLabel}
+ className={twMerge(clsx('grid auto-rows-min', size === 'sm' && 'gap-2', size === 'md' && 'gap-4'), className)}
+ style={{
+ gridTemplateColumns:
+ labelPosition === 'block'
+ ? `repeat(auto-fill, minmax(calc(var(--spacing) * ${boxSize}), 1fr))`
+ : 'repeat(auto-fill, minmax(calc(var(--spacing) * 40), 1fr))',
+ }}
+ >
+ {options.map((option) => {
+ const isSelected = value === option.value;
+ const isDisabled = option.disabled;
+
+ return (
+
+
+ {option.label}
+
+ );
+ })}
+
+ );
+}
diff --git a/site/src/components/Select.tsx b/site/src/components/Select.tsx
index 4423f925..9418a4b5 100644
--- a/site/src/components/Select.tsx
+++ b/site/src/components/Select.tsx
@@ -39,6 +39,7 @@ export function Select({
data-testid={dataTestId}
>
+
diff --git a/site/src/components/Tabs.tsx b/site/src/components/Tabs.tsx
index 4605978c..20c94500 100644
--- a/site/src/components/Tabs.tsx
+++ b/site/src/components/Tabs.tsx
@@ -7,13 +7,21 @@
* namely, that separate islands can't share context, and that,
* depending on rendering context, Astro may render the component tree
* bottom-up or top-down
+ *
+ * IMPORTANT: Use `client:idle` instead of `client:visible` when hydrating
+ * these components. TabsPanel elements start with `hidden` attribute,
+ * which prevents them from triggering Intersection Observer visibility,
+ * causing `client:visible` to never hydrate non-initial panels.
*/
import clsx from 'clsx';
+
import { Check, Copy } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { twMerge } from 'tailwind-merge';
+
import useIsHydrated from '@/utils/useIsHydrated';
+
import CopyButton from './CopyButton';
interface TabsRootProps {
diff --git a/site/src/components/docs/Selectors.tsx b/site/src/components/docs/Selectors.tsx
index 20872e5b..5dabeef0 100644
--- a/site/src/components/docs/Selectors.tsx
+++ b/site/src/components/docs/Selectors.tsx
@@ -2,9 +2,17 @@ import { useStore } from '@nanostores/react';
import { Select } from '@/components/Select';
import { currentStyle as styleStore } from '@/stores/preferences';
import type { AnySupportedStyle, SupportedFramework } from '@/types/docs';
-import { FRAMEWORK_STYLES, isValidFramework, isValidStyleForFramework, SUPPORTED_FRAMEWORKS } from '@/types/docs';
+import {
+ FRAMEWORK_LABELS,
+ FRAMEWORK_STYLES,
+ isValidFramework,
+ isValidStyleForFramework,
+ STYLE_LABELS,
+ SUPPORTED_FRAMEWORKS,
+} from '@/types/docs';
import { setStylePreferenceClient, updateStyleAttribute } from '@/utils/docs/preferences';
import { resolveFrameworkChange } from '@/utils/docs/routing';
+import useIsHydrated from '@/utils/useIsHydrated';
interface SelectorProps {
currentFramework: SupportedFramework;
@@ -12,10 +20,9 @@ interface SelectorProps {
}
export function Selectors({ currentFramework, currentSlug }: SelectorProps) {
- // Read style from nanostore (StyleInit + PreferenceUpdater guarantee a valid value)
- // Guard against React hydrating before style is initialized
const currentStyle = useStore(styleStore);
- if (!currentStyle) return null;
+ const isHydrated = useIsHydrated();
+ const hydrationSafeCurrentStyle = isHydrated ? currentStyle : null;
const handleFrameworkChange = (newFramework: SupportedFramework | null) => {
if (newFramework === null) return;
@@ -52,12 +59,12 @@ export function Selectors({ currentFramework, currentSlug }: SelectorProps) {
const frameworkOptions = SUPPORTED_FRAMEWORKS.map((fw) => ({
value: fw,
- label: fw,
+ label: FRAMEWORK_LABELS[fw],
}));
const styleOptions = availableStyles.map((st) => ({
value: st,
- label: st,
+ label: STYLE_LABELS[st],
}));
return (
@@ -77,7 +84,7 @@ export function Selectors({ currentFramework, currentSlug }: SelectorProps) {
/>
Style