diff --git a/CLAUDE.md b/CLAUDE.md index 47d818b2ea..b91fff3ea5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,24 @@ All documentation lives in `src/content/docs/{lang}/` as `.mdx` files with YAML ### Path Alias -`~/*` maps to `./src/*` (configured in tsconfig.json). +**Always use the `@`-prefixed path aliases for local imports** — never relative paths (`../../`) and never the legacy `~/*` alias for new code. Aliases are configured in `tsconfig.json`: + +| Alias | Maps to | +|-------|---------| +| `@models/*` | `src/models/*` | +| `@components/*` | `src/components/*` | +| `@layouts/*` | `src/layouts/*` | +| `@styles/*` | `src/styles/*` | +| `@data/*` | `src/data/*` | +| `@util/*` | `src/util/*` | +| `@includes/*` | `src/content/_includes/*` | +| `@root/*` | `src/*` (catch-all for paths without a dedicated alias, e.g. `@root/consts`, `@root/pages/...`) | + +Prefer the most specific alias whenever one matches the target folder; fall back to `@root/*` only for `src/` paths no other alias covers. + +Example: `import { foo } from '@util/fetch-utils';` (not `~/util/fetch-utils` or `../../util/fetch-utils`). + +`~/*` (maps to `./src/*`) still exists for legacy code, but always prefer an `@` alias. SCSS `@use`/`@import` use **relative paths** (e.g., `@use '../../styles/_variables.scss' as *;`); inside `src/styles/` use sibling imports like `@use 'variables' as *;`. ### Starlight Customization @@ -259,6 +276,7 @@ Use the `release` skill for the full checklist. Key files: - Tabs for indentation in code files; spaces for JSON, Markdown, MDX, YAML, TOML - Prettier with `prettier-plugin-astro`, printWidth 100, single quotes, trailing commas - ESLint flat config with TypeScript and Astro plugins +- **No Figma references in comments.** Don't write "Figma", "Figma node 1234:5678", or any tool-specific node IDs in source comments — they're meaningless to anyone without access to the Figma file and rot fast. Refer to the visual spec as "the design" (or "per the design", "matches the design") and describe what's actually being implemented (sizes, colors, behaviors) so the comment stands on its own. ## CI Checks diff --git a/astro.config.ts b/astro.config.ts index dce6bd84f2..96fd7ed375 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -41,7 +41,6 @@ export default defineConfig({ vite: { resolve: { alias: { - '~': fileURLToPath(new URL('./src', import.meta.url)), '@starlight/icons': fileURLToPath( new URL('./node_modules/@astrojs/starlight/components-internals/Icons.ts', import.meta.url) ), @@ -53,23 +52,6 @@ export default defineConfig({ ), }, }, - css: { - preprocessorOptions: { - scss: { - importers: [ - { - findFileUrl(url: string) { - if (!url.startsWith('~/')) return null; - return new URL( - url.slice(2), - new URL('./src/', import.meta.url) - ); - }, - }, - ], - }, - }, - }, optimizeDeps: { include: ['photoswipe', 'photoswipe/lightbox'], }, diff --git a/package.json b/package.json index 07eb5faf52..334c5368a5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@types/hast": "^3.0.4", "@types/node": "^24.12.2", "@typescript-eslint/parser": "^8.59.1", - "astro": "^6.1.9", + "astro": "^6.3.8", "astro-eslint-parser": "^1.4.0", "astro-icon": "^1.1.5", "dedent-js": "^1.0.1", @@ -61,11 +61,13 @@ "vite-plugin-svgo": "^2.0.0" }, "dependencies": { - "@astrojs/check": "^0.9.8", + "@astrojs/check": "^0.9.9", + "@astrojs/markdown-remark": "^7.1.2", "@astrojs/rss": "4.0.18", - "@astrojs/sitemap": "^3.7.2", - "@astrojs/starlight": "^0.38.4", + "@astrojs/sitemap": "^3.7.3", + "@astrojs/starlight": "^0.39.2", "@expressive-code/plugin-collapsible-sections": "^0.41.7", + "@fontsource/material-icons": "^5.2.7", "@lunariajs/core": "^0.1.1", "canvas-confetti": "^1.9.4", "photoswipe": "^5.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e27531c07..4333c4537b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,20 +9,26 @@ importers: .: dependencies: '@astrojs/check': - specifier: ^0.9.8 - version: 0.9.8(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2) + specifier: ^0.9.9 + version: 0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2) + '@astrojs/markdown-remark': + specifier: ^7.1.2 + version: 7.1.2 '@astrojs/rss': specifier: 4.0.18 version: 4.0.18 '@astrojs/sitemap': - specifier: ^3.7.2 - version: 3.7.2 + specifier: ^3.7.3 + version: 3.7.3 '@astrojs/starlight': - specifier: ^0.38.4 - version: 0.38.4(astro@6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3)) + specifier: ^0.39.2 + version: 0.39.2(astro@6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3))(typescript@5.6.2) '@expressive-code/plugin-collapsible-sections': specifier: ^0.41.7 version: 0.41.7 + '@fontsource/material-icons': + specifier: ^5.2.7 + version: 5.2.7 '@lunariajs/core': specifier: ^0.1.1 version: 0.1.1 @@ -73,8 +79,8 @@ importers: specifier: ^8.59.1 version: 8.59.1(eslint@9.39.4(jiti@2.3.3))(typescript@5.6.2) astro: - specifier: ^6.1.9 - version: 6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3) + specifier: ^6.3.8 + version: 6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3) astro-eslint-parser: specifier: ^1.4.0 version: 1.4.0 @@ -165,11 +171,11 @@ packages: '@antfu/utils@8.1.1': resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} - '@astrojs/check@0.9.8': - resolution: {integrity: sha512-LDng8446QLS5ToKjRHd3bgUdirvemVVExV7nRyJfW2wV36xuv7vDxwy5NWN9zqeSEDgg0Tv84sP+T3yEq+Zlkw==} + '@astrojs/check@0.9.9': + resolution: {integrity: sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==} hasBin: true peerDependencies: - typescript: ^5.0.0 + typescript: ^5.0.0 || ^6.0.0 '@astrojs/compiler@2.13.1': resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} @@ -177,11 +183,17 @@ packages: '@astrojs/compiler@3.0.1': resolution: {integrity: sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==} + '@astrojs/compiler@4.0.0': + resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==} + '@astrojs/internal-helpers@0.9.0': resolution: {integrity: sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg==} - '@astrojs/language-server@2.16.6': - resolution: {integrity: sha512-N990lu+HSFiG57owR0XBkr02BYMgiLCshLf+4QG4v6jjSWkBeQGnzqi+E1L08xFPPJ7eEeXnxPXGLaVv5pa4Ug==} + '@astrojs/internal-helpers@0.9.1': + resolution: {integrity: sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==} + + '@astrojs/language-server@2.16.10': + resolution: {integrity: sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w==} hasBin: true peerDependencies: prettier: ^3.0.0 @@ -195,6 +207,9 @@ packages: '@astrojs/markdown-remark@7.1.1': resolution: {integrity: sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA==} + '@astrojs/markdown-remark@7.1.2': + resolution: {integrity: sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==} + '@astrojs/mdx@5.0.4': resolution: {integrity: sha512-tSbuuYueNODiFAFaME7pjHY5lOLoxBYJi1cKd6scw9+a4ZO7C7UGdafEoVAQvOV2eO8a6RaHSAJYGVPL1w8BPA==} engines: {node: '>=22.12.0'} @@ -205,23 +220,27 @@ packages: resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==} engines: {node: '>=22.12.0'} + '@astrojs/prism@4.0.2': + resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==} + engines: {node: '>=22.12.0'} + '@astrojs/rss@4.0.18': resolution: {integrity: sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg==} - '@astrojs/sitemap@3.7.2': - resolution: {integrity: sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA==} + '@astrojs/sitemap@3.7.3': + resolution: {integrity: sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==} - '@astrojs/starlight@0.38.4': - resolution: {integrity: sha512-TGFIr2aVC+gcZCPQzJOO4ZnA/yL3jRnsUDcKlVdEhxhxaOQnWr9lZ9MRScg9zU6uh3HVeZAmmjkLCdTlHdcaZA==} + '@astrojs/starlight@0.39.2': + resolution: {integrity: sha512-vlw+bwnjtf5buCTUtLU7JfV6D3knslxqnspr6LKs6hfRuFZiyr5hT44F7GyDqR9FKANUqFxnIzWM81F1k/kOUA==} peerDependencies: astro: ^6.0.0 - '@astrojs/telemetry@3.3.1': - resolution: {integrity: sha512-7fcIxXS9J4ls5tr8b3ww9rbAIz2+HrhNJYZdkAhhB4za/I5IZ/60g+Bs8q7zwG0tOIZfNB4JWhVJ1Qkl/OrNCw==} + '@astrojs/telemetry@3.3.2': + resolution: {integrity: sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - '@astrojs/yaml2ts@0.2.3': - resolution: {integrity: sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg==} + '@astrojs/yaml2ts@0.2.4': + resolution: {integrity: sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==} '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} @@ -236,10 +255,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -482,22 +497,28 @@ packages: '@expressive-code/core@0.41.7': resolution: {integrity: sha512-ck92uZYZ9Wba2zxkiZLsZGi9N54pMSAVdrI9uW3Oo9AtLglD5RmrdTwbYPCT2S/jC36JGB2i+pnQtBm/Ib2+dg==} + '@expressive-code/core@0.42.0': + resolution: {integrity: sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw==} + '@expressive-code/plugin-collapsible-sections@0.41.7': resolution: {integrity: sha512-uh74qWhAW6FEoNdlQAcHCcGBfuhslLvbWL5Fqmi+db/9mZI/I2G1Sr8NfApTEzD+jiIB/GmdPHV9kbjebkn0+g==} - '@expressive-code/plugin-frames@0.41.7': - resolution: {integrity: sha512-diKtxjQw/979cTglRFaMCY/sR6hWF0kSMg8jsKLXaZBSfGS0I/Hoe7Qds3vVEgeoW+GHHQzMcwvgx/MOIXhrTA==} + '@expressive-code/plugin-frames@0.42.0': + resolution: {integrity: sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA==} - '@expressive-code/plugin-shiki@0.41.7': - resolution: {integrity: sha512-DL605bLrUOgqTdZ0Ot5MlTaWzppRkzzqzeGEu7ODnHF39IkEBbFdsC7pbl3LbUQ1DFtnfx6rD54k/cdofbW6KQ==} + '@expressive-code/plugin-shiki@0.42.0': + resolution: {integrity: sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g==} - '@expressive-code/plugin-text-markers@0.41.7': - resolution: {integrity: sha512-Ewpwuc5t6eFdZmWlFyeuy3e1PTQC0jFvw2Q+2bpcWXbOZhPLsT7+h8lsSIJxb5mS7wZko7cKyQ2RLYDyK6Fpmw==} + '@expressive-code/plugin-text-markers@0.42.0': + resolution: {integrity: sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ==} '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@fontsource/material-icons@5.2.7': + resolution: {integrity: sha512-crPmK0L34lPGmS5GSGLasKpRGQzl95SxMsLM+QhBHPgR9uxSsyI5CUTb0cgoMpjtR+Bf1bC9QOe6pavoybbBwg==} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1082,30 +1103,18 @@ packages: cpu: [x64] os: [win32] - '@shikijs/core@3.23.0': - resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} - '@shikijs/core@4.0.2': resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} engines: {node: '>=20'} - '@shikijs/engine-javascript@3.23.0': - resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} - '@shikijs/engine-javascript@4.0.2': resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@3.23.0': - resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} - '@shikijs/engine-oniguruma@4.0.2': resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} engines: {node: '>=20'} - '@shikijs/langs@3.23.0': - resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} - '@shikijs/langs@4.0.2': resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} engines: {node: '>=20'} @@ -1114,16 +1123,10 @@ packages: resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} engines: {node: '>=20'} - '@shikijs/themes@3.23.0': - resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} - '@shikijs/themes@4.0.2': resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} engines: {node: '>=20'} - '@shikijs/types@3.23.0': - resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} - '@shikijs/types@4.0.2': resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} engines: {node: '>=20'} @@ -1339,16 +1342,16 @@ packages: resolution: {integrity: sha512-+QDcgc7e+au6EZ0YjMmRRjNoQo5bDMlaR45aWDoFsuxQTCM9qmCHRoiKJPELgckJ8Wmr7vcfpa9eCDHBFh6G4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - astro-expressive-code@0.41.7: - resolution: {integrity: sha512-hUpogGc6DdAd+I7pPXsctyYPRBJDK7Q7d06s4cyP0Vz3OcbziP3FNzN0jZci1BpCvLn9675DvS7B9ctKKX64JQ==} + astro-expressive-code@0.42.0: + resolution: {integrity: sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag==} peerDependencies: astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta astro-icon@1.1.5: resolution: {integrity: sha512-CJYS5nWOw9jz4RpGWmzNQY7D0y2ZZacH7atL2K9DeJXJVaz7/5WrxeyIxO8KASk1jCM96Q4LjRx/F3R+InjJrw==} - astro@6.1.9: - resolution: {integrity: sha512-NsAHzMzpznB281g2aM5qnBt2QjfH6ttKiZ3hSZw52If8JJ+62kbnBKbyKhR2glQcJLl7Jfe4GSl0DihFZ36rRQ==} + astro@6.3.8: + resolution: {integrity: sha512-xH2UA8Z17IS+JaqSlSkBor7jO6gd7zXTLdmu06nKpfpDDJFbi/7KZEy3NDmWxmier+6XrCZ9Z4aitO8jhC9oiA==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -1601,9 +1604,6 @@ packages: resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==} hasBin: true - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -1771,8 +1771,8 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - expressive-code@0.41.7: - resolution: {integrity: sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA==} + expressive-code@0.42.0: + resolution: {integrity: sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g==} exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -1882,6 +1882,10 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} + get-tsconfig@5.0.0-beta.4: + resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==} + engines: {node: '>=20.20.0'} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -1994,8 +1998,13 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - i18next@23.16.8: - resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} + i18next@26.3.0: + resolution: {integrity: sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==} + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} @@ -2232,8 +2241,8 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - micromark-extension-directive@3.0.2: - resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + micromark-extension-directive@4.0.0: + resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==} micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} @@ -2610,8 +2619,8 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - rehype-expressive-code@0.41.7: - resolution: {integrity: sha512-25f8ZMSF1d9CMscX7Cft0TSQIqdwjce2gDOvQ+d/w0FovsMwrSt3ODP4P3Z7wO1jsIJ4eYyaDRnIR/27bd/EMQ==} + rehype-expressive-code@0.42.0: + resolution: {integrity: sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA==} rehype-format@5.0.1: resolution: {integrity: sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==} @@ -2634,8 +2643,8 @@ packages: rehype@13.0.2: resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} - remark-directive@3.0.1: - resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + remark-directive@4.0.0: + resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==} remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -2674,6 +2683,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} @@ -2740,9 +2752,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@3.23.0: - resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} - shiki@4.0.2: resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} engines: {node: '>=20'} @@ -2864,16 +2873,6 @@ packages: peerDependencies: typescript: '>=4.8.4' - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3305,9 +3304,9 @@ snapshots: '@antfu/utils@8.1.1': {} - '@astrojs/check@0.9.8(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2)': + '@astrojs/check@0.9.9(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2)': dependencies: - '@astrojs/language-server': 2.16.6(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2) + '@astrojs/language-server': 2.16.10(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2) chokidar: 4.0.3 kleur: 4.1.5 typescript: 5.6.2 @@ -3320,14 +3319,20 @@ snapshots: '@astrojs/compiler@3.0.1': {} + '@astrojs/compiler@4.0.0': {} + '@astrojs/internal-helpers@0.9.0': dependencies: picomatch: 4.0.4 - '@astrojs/language-server@2.16.6(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2)': + '@astrojs/internal-helpers@0.9.1': + dependencies: + picomatch: 4.0.4 + + '@astrojs/language-server@2.16.10(prettier-plugin-astro@0.14.1)(prettier@3.8.3)(typescript@5.6.2)': dependencies: '@astrojs/compiler': 2.13.1 - '@astrojs/yaml2ts': 0.2.3 + '@astrojs/yaml2ts': 0.2.4 '@jridgewell/sourcemap-codec': 1.5.5 '@volar/kit': 2.4.28(typescript@5.6.2) '@volar/language-core': 2.4.28 @@ -3376,12 +3381,38 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@5.0.4(astro@6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3))': + '@astrojs/markdown-remark@7.1.2': + dependencies: + '@astrojs/internal-helpers': 0.9.1 + '@astrojs/prism': 4.0.2 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + js-yaml: 4.1.1 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + retext-smartypants: 6.2.0 + shiki: 4.0.2 + smol-toml: 1.6.1 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/mdx@5.0.4(astro@6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3))': dependencies: '@astrojs/markdown-remark': 7.1.1 '@mdx-js/mdx': 3.1.1 acorn: 8.16.0 - astro: 6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3) + astro: 6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3) es-module-lexer: 2.1.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -3399,35 +3430,39 @@ snapshots: dependencies: prismjs: 1.30.0 + '@astrojs/prism@4.0.2': + dependencies: + prismjs: 1.30.0 + '@astrojs/rss@4.0.18': dependencies: fast-xml-parser: 5.7.2 piccolore: 0.1.3 zod: 4.3.6 - '@astrojs/sitemap@3.7.2': + '@astrojs/sitemap@3.7.3': dependencies: sitemap: 9.0.1 stream-replace-string: 2.0.0 zod: 4.3.6 - '@astrojs/starlight@0.38.4(astro@6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3))': + '@astrojs/starlight@0.39.2(astro@6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3))(typescript@5.6.2)': dependencies: - '@astrojs/markdown-remark': 7.1.1 - '@astrojs/mdx': 5.0.4(astro@6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3)) - '@astrojs/sitemap': 3.7.2 + '@astrojs/markdown-remark': 7.1.2 + '@astrojs/mdx': 5.0.4(astro@6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3)) + '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3) - astro-expressive-code: 0.41.7(astro@6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3)) + astro: 6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3) + astro-expressive-code: 0.42.0(astro@6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 hast-util-to-string: 3.0.1 hastscript: 9.0.1 - i18next: 23.16.8 + i18next: 26.3.0(typescript@5.6.2) js-yaml: 4.1.1 klona: 2.0.6 magic-string: 0.30.21 @@ -3437,24 +3472,24 @@ snapshots: pagefind: 1.5.2 rehype: 13.0.2 rehype-format: 5.0.1 - remark-directive: 3.0.1 + remark-directive: 4.0.0 ultrahtml: 1.6.0 unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color + - typescript - '@astrojs/telemetry@3.3.1': + '@astrojs/telemetry@3.3.2': dependencies: ci-info: 4.4.0 - dlv: 1.1.3 dset: 3.1.4 is-docker: 4.0.0 is-wsl: 3.1.1 which-pm-runs: 1.1.0 - '@astrojs/yaml2ts@0.2.3': + '@astrojs/yaml2ts@0.2.4': dependencies: yaml: 2.8.3 @@ -3466,8 +3501,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/runtime@7.29.2': {} - '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -3660,25 +3693,39 @@ snapshots: unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 + '@expressive-code/core@0.42.0': + dependencies: + '@ctrl/tinycolor': 4.2.0 + hast-util-select: 6.0.4 + hast-util-to-html: 9.0.5 + hast-util-to-text: 4.0.2 + hastscript: 9.0.1 + postcss: 8.5.12 + postcss-nested: 6.2.0(postcss@8.5.12) + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + '@expressive-code/plugin-collapsible-sections@0.41.7': dependencies: '@expressive-code/core': 0.41.7 - '@expressive-code/plugin-frames@0.41.7': + '@expressive-code/plugin-frames@0.42.0': dependencies: - '@expressive-code/core': 0.41.7 + '@expressive-code/core': 0.42.0 - '@expressive-code/plugin-shiki@0.41.7': + '@expressive-code/plugin-shiki@0.42.0': dependencies: - '@expressive-code/core': 0.41.7 - shiki: 3.23.0 + '@expressive-code/core': 0.42.0 + shiki: 4.0.2 - '@expressive-code/plugin-text-markers@0.41.7': + '@expressive-code/plugin-text-markers@0.42.0': dependencies: - '@expressive-code/core': 0.41.7 + '@expressive-code/core': 0.42.0 '@fastify/busboy@2.1.1': {} + '@fontsource/material-icons@5.2.7': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -4123,13 +4170,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true - '@shikijs/core@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - '@shikijs/core@4.0.2': dependencies: '@shikijs/primitive': 4.0.2 @@ -4138,32 +4178,17 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - '@shikijs/engine-javascript@4.0.2': dependencies: '@shikijs/types': 4.0.2 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@4.0.2': dependencies: '@shikijs/types': 4.0.2 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/langs@4.0.2': dependencies: '@shikijs/types': 4.0.2 @@ -4174,19 +4199,10 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/themes@3.23.0': - dependencies: - '@shikijs/types': 3.23.0 - '@shikijs/themes@4.0.2': dependencies: '@shikijs/types': 4.0.2 - '@shikijs/types@3.23.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/types@4.0.2': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -4461,10 +4477,10 @@ snapshots: transitivePeerDependencies: - supports-color - astro-expressive-code@0.41.7(astro@6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3)): + astro-expressive-code@0.42.0(astro@6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3)): dependencies: - astro: 6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3) - rehype-expressive-code: 0.41.7 + astro: 6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3) + rehype-expressive-code: 0.42.0 astro-icon@1.1.5: dependencies: @@ -4474,12 +4490,12 @@ snapshots: transitivePeerDependencies: - supports-color - astro@6.1.9(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(typescript@5.6.2)(yaml@2.8.3): + astro@6.3.8(@types/node@24.12.2)(jiti@2.3.3)(rollup@4.60.2)(sass@1.99.0)(yaml@2.8.3): dependencies: - '@astrojs/compiler': 3.0.1 - '@astrojs/internal-helpers': 0.9.0 - '@astrojs/markdown-remark': 7.1.1 - '@astrojs/telemetry': 3.3.1 + '@astrojs/compiler': 4.0.0 + '@astrojs/internal-helpers': 0.9.1 + '@astrojs/markdown-remark': 7.1.2 + '@astrojs/telemetry': 3.3.2 '@capsizecss/unpack': 4.0.0 '@clack/prompts': 1.2.0 '@oslojs/encoding': 1.1.0 @@ -4497,10 +4513,12 @@ snapshots: esbuild: 0.27.7 flattie: 1.1.1 fontace: 0.4.1 + get-tsconfig: 5.0.0-beta.4 github-slugger: 2.0.0 html-escaper: 3.0.3 http-cache-semantics: 4.2.0 js-yaml: 4.1.1 + jsonc-parser: 3.3.1 magic-string: 0.30.21 magicast: 0.5.2 mrmime: 2.0.1 @@ -4519,7 +4537,6 @@ snapshots: tinyclip: 0.1.12 tinyexec: 1.1.1 tinyglobby: 0.2.16 - tsconfck: 3.1.6(typescript@5.6.2) ultrahtml: 1.6.0 unifont: 0.7.4 unist-util-visit: 5.1.0 @@ -4563,7 +4580,6 @@ snapshots: - supports-color - terser - tsx - - typescript - uploadthing - yaml @@ -4784,8 +4800,6 @@ snapshots: direction@2.0.1: {} - dlv@1.1.3: {} - dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -5015,12 +5029,12 @@ snapshots: eventemitter3@5.0.4: {} - expressive-code@0.41.7: + expressive-code@0.42.0: dependencies: - '@expressive-code/core': 0.41.7 - '@expressive-code/plugin-frames': 0.41.7 - '@expressive-code/plugin-shiki': 0.41.7 - '@expressive-code/plugin-text-markers': 0.41.7 + '@expressive-code/core': 0.42.0 + '@expressive-code/plugin-frames': 0.42.0 + '@expressive-code/plugin-shiki': 0.42.0 + '@expressive-code/plugin-text-markers': 0.42.0 exsolve@1.0.8: {} @@ -5128,6 +5142,10 @@ snapshots: dependencies: pump: 3.0.4 + get-tsconfig@5.0.0-beta.4: + dependencies: + resolve-pkg-maps: 1.0.0 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -5368,9 +5386,9 @@ snapshots: http-cache-semantics@4.2.0: {} - i18next@23.16.8: - dependencies: - '@babel/runtime': 7.29.2 + i18next@26.3.0(typescript@5.6.2): + optionalDependencies: + typescript: 5.6.2 iconv-lite@0.6.3: dependencies: @@ -5711,7 +5729,7 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-directive@3.0.2: + micromark-extension-directive@4.0.0: dependencies: devlop: 1.1.0 micromark-factory-space: 2.0.1 @@ -6255,9 +6273,9 @@ snapshots: dependencies: regex-utilities: 2.3.0 - rehype-expressive-code@0.41.7: + rehype-expressive-code@0.42.0: dependencies: - expressive-code: 0.41.7 + expressive-code: 0.42.0 rehype-format@5.0.1: dependencies: @@ -6305,11 +6323,11 @@ snapshots: rehype-stringify: 10.0.1 unified: 11.0.5 - remark-directive@3.0.1: + remark-directive@4.0.0: dependencies: '@types/mdast': 4.0.4 mdast-util-directive: 3.1.0 - micromark-extension-directive: 3.0.2 + micromark-extension-directive: 4.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color @@ -6372,6 +6390,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + retext-latin@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -6509,17 +6529,6 @@ snapshots: shebang-regex@3.0.0: {} - shiki@3.23.0: - dependencies: - '@shikijs/core': 3.23.0 - '@shikijs/engine-javascript': 3.23.0 - '@shikijs/engine-oniguruma': 3.23.0 - '@shikijs/langs': 3.23.0 - '@shikijs/themes': 3.23.0 - '@shikijs/types': 3.23.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - shiki@4.0.2: dependencies: '@shikijs/core': 4.0.2 @@ -6662,10 +6671,6 @@ snapshots: dependencies: typescript: 5.6.2 - tsconfck@3.1.6(typescript@5.6.2): - optionalDependencies: - typescript: 5.6.2 - tslib@2.8.1: optional: true diff --git a/public/icons/iot-hub-search.svg b/public/icons/iot-hub-search.svg new file mode 100644 index 0000000000..fd5c12b4af --- /dev/null +++ b/public/icons/iot-hub-search.svg @@ -0,0 +1 @@ + diff --git a/src/assets/iot-hub/category-alarm-rules-img.png b/src/assets/iot-hub/category-alarm-rules-img.png new file mode 100644 index 0000000000..ab374fadef Binary files /dev/null and b/src/assets/iot-hub/category-alarm-rules-img.png differ diff --git a/src/assets/iot-hub/category-calculated-fields-img.png b/src/assets/iot-hub/category-calculated-fields-img.png new file mode 100644 index 0000000000..acdcd322e8 Binary files /dev/null and b/src/assets/iot-hub/category-calculated-fields-img.png differ diff --git a/src/assets/iot-hub/category-device-library-img.png b/src/assets/iot-hub/category-device-library-img.png new file mode 100644 index 0000000000..9e7817b70a Binary files /dev/null and b/src/assets/iot-hub/category-device-library-img.png differ diff --git a/src/assets/iot-hub/category-rule-chains-img.png b/src/assets/iot-hub/category-rule-chains-img.png new file mode 100644 index 0000000000..bd136772b3 Binary files /dev/null and b/src/assets/iot-hub/category-rule-chains-img.png differ diff --git a/src/assets/iot-hub/category-solution-templates-img.png b/src/assets/iot-hub/category-solution-templates-img.png new file mode 100644 index 0000000000..3c649f92ef Binary files /dev/null and b/src/assets/iot-hub/category-solution-templates-img.png differ diff --git a/src/assets/iot-hub/category-widgets-img.png b/src/assets/iot-hub/category-widgets-img.png new file mode 100644 index 0000000000..0808b123cc Binary files /dev/null and b/src/assets/iot-hub/category-widgets-img.png differ diff --git a/src/assets/iot-hub/hero-alarm-rule-cluster.svg b/src/assets/iot-hub/hero-alarm-rule-cluster.svg new file mode 100644 index 0000000000..545e7db89b --- /dev/null +++ b/src/assets/iot-hub/hero-alarm-rule-cluster.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/hero-calculated-field-cluster.svg b/src/assets/iot-hub/hero-calculated-field-cluster.svg new file mode 100644 index 0000000000..a5206aa4ea --- /dev/null +++ b/src/assets/iot-hub/hero-calculated-field-cluster.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/hero-device-cluster.svg b/src/assets/iot-hub/hero-device-cluster.svg new file mode 100644 index 0000000000..7e75c744a7 --- /dev/null +++ b/src/assets/iot-hub/hero-device-cluster.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/hero-rule-chain-cluster.svg b/src/assets/iot-hub/hero-rule-chain-cluster.svg new file mode 100644 index 0000000000..ee3a4d39de --- /dev/null +++ b/src/assets/iot-hub/hero-rule-chain-cluster.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/hero-solution-template-cluster.svg b/src/assets/iot-hub/hero-solution-template-cluster.svg new file mode 100644 index 0000000000..2afd1f2ab8 --- /dev/null +++ b/src/assets/iot-hub/hero-solution-template-cluster.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/hero-widget-cluster.svg b/src/assets/iot-hub/hero-widget-cluster.svg new file mode 100644 index 0000000000..0df6ad4336 --- /dev/null +++ b/src/assets/iot-hub/hero-widget-cluster.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/items-page-alarm-rules-hero.svg b/src/assets/iot-hub/items-page-alarm-rules-hero.svg new file mode 100644 index 0000000000..74ba7569b4 --- /dev/null +++ b/src/assets/iot-hub/items-page-alarm-rules-hero.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/items-page-calculated-fields-hero.svg b/src/assets/iot-hub/items-page-calculated-fields-hero.svg new file mode 100644 index 0000000000..916c521ea0 --- /dev/null +++ b/src/assets/iot-hub/items-page-calculated-fields-hero.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/items-page-devices-hero.svg b/src/assets/iot-hub/items-page-devices-hero.svg new file mode 100644 index 0000000000..d951dd8744 --- /dev/null +++ b/src/assets/iot-hub/items-page-devices-hero.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/items-page-rule-chains-hero.svg b/src/assets/iot-hub/items-page-rule-chains-hero.svg new file mode 100644 index 0000000000..05cb14b0e2 --- /dev/null +++ b/src/assets/iot-hub/items-page-rule-chains-hero.svg @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/items-page-solution-templates-hero.png b/src/assets/iot-hub/items-page-solution-templates-hero.png new file mode 100644 index 0000000000..690c23f552 Binary files /dev/null and b/src/assets/iot-hub/items-page-solution-templates-hero.png differ diff --git a/src/assets/iot-hub/items-page-widgets-hero.svg b/src/assets/iot-hub/items-page-widgets-hero.svg new file mode 100644 index 0000000000..84c4b37a2f --- /dev/null +++ b/src/assets/iot-hub/items-page-widgets-hero.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/search-fetch-error.svg b/src/assets/iot-hub/search-fetch-error.svg new file mode 100644 index 0000000000..2c591b268a --- /dev/null +++ b/src/assets/iot-hub/search-fetch-error.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/iot-hub/search-no-results.svg b/src/assets/iot-hub/search-no-results.svg new file mode 100644 index 0000000000..cadc8c1ec0 --- /dev/null +++ b/src/assets/iot-hub/search-no-results.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Blog/BlogCard.astro b/src/components/Blog/BlogCard.astro index e1c585297b..fa1ef1a577 100644 --- a/src/components/Blog/BlogCard.astro +++ b/src/components/Blog/BlogCard.astro @@ -47,7 +47,7 @@ const author = getAuthor(post.data.author); diff --git a/src/components/IotHub/CategorySection.astro b/src/components/IotHub/CategorySection.astro new file mode 100644 index 0000000000..50e1f96c7d --- /dev/null +++ b/src/components/IotHub/CategorySection.astro @@ -0,0 +1,122 @@ +--- +import { getCardVariant, type ListingView } from '@models/iot-hub'; +import ListingCard from './ListingCard.astro'; +import ListingGrid from './ListingGrid.astro'; + +interface Props { + categorySlug: string; + categoryLabel: string; + items: ListingView[]; + home?: boolean; + /** Forwarded to ListingCard — false hides the creator row. */ + showCreator?: boolean; +} + +const { categorySlug, categoryLabel, items, home = false, showCreator = true } = Astro.props; +const indexHref = `/iot-hub/${categorySlug}/`; +const variant = getCardVariant(items[0]?.itemType ?? ''); +--- + +
+ + +

+ + +

+
+ { + items.length > 0 ? ( + + {items.map((item) => ( + + ))} + + ) : ( +

No items available yet.

+ ) + } +
+ + diff --git a/src/components/IotHub/CategorySectionTemplate.astro b/src/components/IotHub/CategorySectionTemplate.astro new file mode 100644 index 0000000000..34685f193c --- /dev/null +++ b/src/components/IotHub/CategorySectionTemplate.astro @@ -0,0 +1,41 @@ +--- +// Pattern C clone source for runtime category-section rendering. The +// search page (and any future dynamic listing page) emits one of these +// once; JS clones it per group, fills the header href + label, and +// appends bound cards to the inner grid. +// +// Styles for `.iot-hub-section*` and `.iot-hub-grid*` live in +// CategorySection.astro and ListingGrid.astro respectively, both with +// `is:global` so the cloned DOM picks them up. +--- + + diff --git a/src/components/IotHub/CategoryTile.astro b/src/components/IotHub/CategoryTile.astro new file mode 100644 index 0000000000..8f7d6343d3 --- /dev/null +++ b/src/components/IotHub/CategoryTile.astro @@ -0,0 +1,173 @@ +--- +import type { ImageMetadata } from 'astro'; +import { Image } from 'astro:assets'; + +import deviceImg from '@root/assets/iot-hub/category-device-library-img.png'; +import solutionImg from '@root/assets/iot-hub/category-solution-templates-img.png'; +import widgetImg from '@root/assets/iot-hub/category-widgets-img.png'; +import calcFieldImg from '@root/assets/iot-hub/category-calculated-fields-img.png'; +import alarmImg from '@root/assets/iot-hub/category-alarm-rules-img.png'; +import ruleChainImg from '@root/assets/iot-hub/category-rule-chains-img.png'; + +interface Props { + slug: string; + label: string; + color: string; + colorDark: string; + itemType: string; +} + +const { slug, label, color, colorDark, itemType } = Astro.props; + +const imageByType: Record = { + DEVICE: deviceImg, + SOLUTION_TEMPLATE: solutionImg, + WIDGET: widgetImg, + CALCULATED_FIELD: calcFieldImg, + ALARM_RULE: alarmImg, + RULE_CHAIN: ruleChainImg, +}; + +const image = imageByType[itemType] ?? deviceImg; +const href = `/iot-hub/${slug}/`; +--- + + + + {label} + + +
+ +
+
+ + diff --git a/src/components/IotHub/CategoryTilesGrid.astro b/src/components/IotHub/CategoryTilesGrid.astro new file mode 100644 index 0000000000..0b4f27b0c9 --- /dev/null +++ b/src/components/IotHub/CategoryTilesGrid.astro @@ -0,0 +1,70 @@ +--- +import { IOT_HUB_CATEGORIES } from '@models/iot-hub'; +import CategoryTile from './CategoryTile.astro'; +--- + +
+
+ { + IOT_HUB_CATEGORIES.map((cat) => ( + + )) + } +
+
+ + diff --git a/src/components/IotHub/CreatorCTA.astro b/src/components/IotHub/CreatorCTA.astro new file mode 100644 index 0000000000..6a6e9b3a43 --- /dev/null +++ b/src/components/IotHub/CreatorCTA.astro @@ -0,0 +1,124 @@ +--- +import { IOT_HUB_API_URL } from '@models/iot-hub'; +import IotHubChevron from './IotHubChevron.astro'; + +const submitUrl = `${IOT_HUB_API_URL}/signup`; +--- + +
+
+
+
+

Become a Creator

+

+ Submit your templates to the ThingsBoard IoT Hub to get featured and showcase your + solutions to our global community. +

+
+ + Contribute + + +
+
+
+ + diff --git a/src/components/IotHub/CreatorHero.astro b/src/components/IotHub/CreatorHero.astro new file mode 100644 index 0000000000..2da4530817 --- /dev/null +++ b/src/components/IotHub/CreatorHero.astro @@ -0,0 +1,234 @@ +--- +import { Icon } from 'astro-icon/components'; +import { getInitials, getWebsiteLabel, IOT_HUB_STRINGS } from '@models/iot-hub'; +import { normalizeSiteUrl } from '@util/site-links'; +import IotHubBreadcrumbs from './IotHubBreadcrumbs.astro'; +import '@fontsource/material-icons'; + +const T = IOT_HUB_STRINGS.creatorPage; + +interface Props { + name: string; + avatarUrl: string | null; + verified: boolean; + bio?: string | null; + website?: string | null; + email?: string | null; + github?: string | null; + linkedin?: string | null; + twitter?: string | null; +} + +const { name, avatarUrl, verified, bio, website, email, github, linkedin, twitter } = + Astro.props; + +const initials = getInitials(name) || '?'; +--- + +
+ +
+ +
+
+

{name}

+ {verified && ( + + verified + + )} +
+ {bio &&

{bio}

} +
+ {website && ( + + + {getWebsiteLabel(website)} + + )} + {email && ( + + + + )} + {github && ( + + + + )} + {linkedin && ( + + + + )} + {twitter && ( + + + + )} +
+
+
+
+ + diff --git a/src/components/IotHub/DetailGallery.astro b/src/components/IotHub/DetailGallery.astro new file mode 100644 index 0000000000..d9df3f27ea --- /dev/null +++ b/src/components/IotHub/DetailGallery.astro @@ -0,0 +1,407 @@ +--- +import { IOT_HUB_API_URL } from '@models/iot-hub'; + +// Interactive image carousel for solution-template screenshots. Drop-in +// for the detail hero's preview slot when a listing has multiple +// screenshots — single-screenshot listings get a still image rendered by +// DetailHero directly. Scoped Astro markup + a small is:inline script for +// prev/next + dot navigation and gentle auto-advance; no carousel library. + +interface Props { + screenshots: Array<{ id: string }>; + listingName: string; +} + +const { screenshots, listingName } = Astro.props; +const resourceUrl = (id: string) => `${IOT_HUB_API_URL}/api/resources/${id}`; +--- + +{ + screenshots.length > 0 && ( + + ) +} + + + + diff --git a/src/components/IotHub/DetailHero.astro b/src/components/IotHub/DetailHero.astro new file mode 100644 index 0000000000..abbe6e2ce4 --- /dev/null +++ b/src/components/IotHub/DetailHero.astro @@ -0,0 +1,552 @@ +--- +import { + IOT_HUB_CATEGORIES, + formatInstalls, + getCardVariant, + getPlaceholderIcon, + getTbVersionLabel, + resolveImage, + type ListingDetail, +} from '@models/iot-hub'; +import InstallButton from './InstallButton.astro'; +import IotHubBreadcrumbs from './IotHubBreadcrumbs.astro'; +import IotHubIcon from './IotHubIcon.astro'; +import DetailGallery from './DetailGallery.astro'; +import IotHubMarkdown from '@components/IotHub/IotHubMarkdown.astro'; + +interface Props { + detail: ListingDetail; + categorySlug: string; +} + +const { detail, categorySlug } = Astro.props; + +// Use `tileLabel` (same source as CategoryHero's breadcrumb) so the parent +// crumb on the detail page matches what users see one level up — e.g. +// "Device Library" rather than the API's plural "Devices". +const categoryLabel = + IOT_HUB_CATEGORIES.find((c) => c.slug === categorySlug)?.tileLabel ?? + categorySlug; +const isCompact = getCardVariant(detail.itemType) === 'small'; + +// SOLUTION_TEMPLATE listings ship multiple screenshots — render the gallery +// in the preview slot instead of a single image (DEVICE / WIDGET fall back +// to `detail.image`). +const showGallery = + !isCompact && + detail.itemType === 'SOLUTION_TEMPLATE' && + detail.screenshots.length > 0; +const heroImg = !isCompact && !showGallery ? resolveImage(detail.image) : null; +// Compact variants render a colored icon tile next to the title (same as +// the compact ListingCard renderer, but at 80x80 to match the design). +// IotHubIcon handles the `mdi:`-prefix branching internally. +const tileColor = detail.color ?? '#4caf50'; +const placeholderIcon = isCompact ? getPlaceholderIcon(detail) : null; + +// Sub-title meta row — name for the listing's item type (not the subtype; +// hardwareType / widgetType / cfType / ruleChainType already surface in the +// Type chip in DetailMeta below). +const subTypeLabel = (() => { + switch (detail.itemType) { + case 'DEVICE': + return 'Device'; + case 'WIDGET': + return 'Widget'; + case 'SOLUTION_TEMPLATE': + return 'Solution Template'; + case 'CALCULATED_FIELD': + return 'Calculated Field'; + case 'ALARM_RULE': + return 'Alarm Rule'; + case 'RULE_CHAIN': + return 'Rule Chain'; + default: + return String(detail.itemType) + .toLowerCase() + .replace(/_/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()); + } +})(); + +const subTypeIcon = (() => { + switch (detail.itemType) { + case 'DEVICE': + return 'memory'; + case 'WIDGET': + return 'widgets'; + case 'SOLUTION_TEMPLATE': + return 'integration_instructions'; + case 'CALCULATED_FIELD': + return 'functions'; + case 'ALARM_RULE': + return 'notification_important'; + case 'RULE_CHAIN': + return 'account_tree'; + default: + return 'category'; + } +})(); + +const publishedDate = + detail.publishedTime != null + ? new Date(detail.publishedTime).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) + : null; + +// Display version label — prepend `v` when the API string doesn't already +// carry one (handles both `1.0.0` and `v1.0.0`). +const versionLabel = detail.version + ? detail.version.startsWith('v') + ? detail.version + : `v${detail.version}` + : null; + +// License is always shown last per the design sub-title row; +// listings on the IoT Hub are published under Apache-2.0. +const licenseLabel = 'Apache-2.0 license'; + +// Supported ThingsBoard version envelope (aggregated across variants in +// MpListingDetail.findDetailById) — empty when min is unknown. +const tbVersionLabel = getTbVersionLabel(detail.minTbVersion, detail.maxTbVersion); +--- + +
+ + + { + isCompact ? ( +
+
+ +
+

{detail.name}

+
    +
  • + + {subTypeLabel} +
  • +
  • + + {formatInstalls(detail.installCount)} +
  • + {versionLabel && ( + <> +
  • + + {versionLabel} +
  • + + )} + {publishedDate && ( + <> +
  • + + {publishedDate} +
  • + + )} +
  • + + {licenseLabel} +
  • + {tbVersionLabel && ( + <> +
  • + + {tbVersionLabel} +
  • + + )} + {detail.ceOnly && ( +
  • + CE +
  • + )} + {detail.peOnly && ( +
  • + PE +
  • + )} +
+
+
+ {detail.description && ( +
+ +
+ )} + +
+ ) : ( + <> +
+

{detail.name}

+
    +
  • + + {subTypeLabel} +
  • +
  • + + {formatInstalls(detail.installCount)} +
  • + {versionLabel && ( + <> +
  • + + {versionLabel} +
  • + + )} + {publishedDate && ( + <> +
  • + + {publishedDate} +
  • + + )} +
  • + + {licenseLabel} +
  • + {tbVersionLabel && ( + <> +
  • + + {tbVersionLabel} +
  • + + )} + {detail.ceOnly && ( +
  • + CE +
  • + )} + {detail.peOnly && ( +
  • + PE +
  • + )} +
+
+
+
+ {showGallery ? ( + + ) : heroImg ? ( + {detail.name} + ) : ( + +
+ {detail.description && ( +
+ +
+ )} + +
+
+ + ) + } +
+ + diff --git a/src/components/IotHub/DetailMeta.astro b/src/components/IotHub/DetailMeta.astro new file mode 100644 index 0000000000..d11bc2d7e2 --- /dev/null +++ b/src/components/IotHub/DetailMeta.astro @@ -0,0 +1,520 @@ +--- +import { + getSubtypeLabel, + getCreatorHref, + getInitials, + resolveAvatar, + type IotHubItemType, + type ListingDetail, +} from '@models/iot-hub'; +import '@fontsource/material-icons'; + +// Renders the 4-column "tags" row beneath the detail hero, per the design. +// Two variants — A: DEVICE / WIDGET / SOLUTION_TEMPLATE and B: +// CALCULATED_FIELD / RULE_CHAIN / ALARM_RULE — share the same layout and +// only differ in which subtype field drives the second/third columns. +// Chip color tint follows the design: type/secondary→blue, third→purple, +// use cases→green (via Starlight's `--sl-color-{blue,purple,green}-{low,high}` +// tokens, which auto-invert between light/dark themes). + +interface Props { + detail: ListingDetail; +} + +const { detail } = Astro.props; + +type Column = { label: string; values: string[]; tint: 'blue' | 'purple' | 'green' }; + +// Column 2 — secondary type (Hardware Type / Widget Type / Type). Raw API +// keys for widgetType / cfType / ruleChainType (`timeseries`, `SIMPLE`, +// `CORE`, …) go through `getSubtypeLabel` to become human-readable. +const itemType = detail.itemType as IotHubItemType; +const secondColumn: Column | null = (() => { + switch (detail.itemType) { + case 'DEVICE': + return detail.hardwareType + ? { label: 'Hardware Type', values: [detail.hardwareType], tint: 'blue' } + : null; + case 'WIDGET': + return detail.widgetType + ? { + label: 'Widget Type', + values: [getSubtypeLabel(itemType, detail.widgetType)], + tint: 'blue', + } + : null; + case 'CALCULATED_FIELD': + case 'ALARM_RULE': + // ALARM_RULE shares cfType (and cfTypeTranslations) with + // CALCULATED_FIELD — force the lookup itemType to CALCULATED_FIELD + // so the translation hits. + return detail.cfType + ? { + label: 'Type', + values: [getSubtypeLabel('CALCULATED_FIELD', detail.cfType)], + tint: 'blue', + } + : null; + case 'RULE_CHAIN': + return detail.ruleChainType + ? { + label: 'Type', + values: [getSubtypeLabel(itemType, detail.ruleChainType)], + tint: 'blue', + } + : null; + case 'SOLUTION_TEMPLATE': + default: + return detail.vendor + ? { label: 'Vendor', values: [detail.vendor], tint: 'blue' } + : null; + } +})(); + +// Column 3 — Connectivity (variant A: DEVICE/WIDGET/etc.) or Category (variant B). +const thirdColumn: Column | null = (() => { + if (detail.connectivity.length > 0) + return { label: 'Connectivity', values: detail.connectivity, tint: 'purple' }; + if (detail.categories.length > 0) + return { label: 'Category', values: detail.categories, tint: 'purple' }; + return null; +})(); + +// Column 4 — Use Cases. All entries render; the runtime chip-overflow +// script at the bottom of this file hides those that don't fit the +// available row width and appends a "+N" counter chip. +const useCasesColumn: Column | null = + detail.useCases.length > 0 + ? { label: 'Use Cases', values: detail.useCases, tint: 'green' } + : null; + +const creator = detail.creatorDisplayName || null; +const verified = detail.creatorVerified; +const creatorHref = detail.creatorId ? getCreatorHref(detail.creatorId) : null; +// Avatar resolves a relative `/api/resources/...` path or absolute URL; falls +// back to initials when unset. The creator block is a link to the creator page +// when we have an id, otherwise a plain container — same markup either way. +const avatarUrl = resolveAvatar(detail.creatorAvatarUrl); +const initials = creator ? getInitials(creator) : ''; +const CreatorTag = creatorHref ? 'a' : 'div'; + +const hasAnyColumn = creator || secondColumn || thirdColumn || useCasesColumn; +--- + +{ + hasAnyColumn && ( +
+ {creator && ( +
+ Creator + + + {creator} + +
+ )} + {secondColumn && ( +
+ {secondColumn.label} +
    + {secondColumn.values.map((v) => ( +
  • + {v} +
  • + ))} +
+
+ )} + {thirdColumn && ( +
+ {thirdColumn.label} +
    + {thirdColumn.values.map((v) => ( +
  • + {v} +
  • + ))} +
+
+ )} + {useCasesColumn && ( +
+ {useCasesColumn.label} +
    + {useCasesColumn.values.map((v) => ( +
  • + {v} +
  • + ))} + {/* Hidden until the runtime script measures and decides + how many chips overflow. Inline `display:none` so + the chip doesn't flash before the script runs. */} +
  • + +0 +
  • +
+
+ )} +
+ ) +} + + + + diff --git a/src/components/IotHub/DetailRuleNodes.astro b/src/components/IotHub/DetailRuleNodes.astro new file mode 100644 index 0000000000..9600e44fd1 --- /dev/null +++ b/src/components/IotHub/DetailRuleNodes.astro @@ -0,0 +1,126 @@ +--- +import type { IotHubNodeInfo } from '@models/iot-hub'; + +// Rule-chain node chips. Each chip uses `node.name` as label and a +// `--{type}` modifier class for the colored background + border. +// Rendered only for RULE_CHAIN listings (see [slug].astro). + +interface Props { + nodes: IotHubNodeInfo[]; +} + +const { nodes } = Astro.props; + +// Lowercase the type for class names (`FILTER` → `filter`), defaulting to +// `unknown` so a new backend value renders neutrally instead of unstyled. +const KNOWN_TYPES = new Set([ + 'enrichment', + 'filter', + 'transformation', + 'action', + 'analytics', + 'external', + 'flow', + 'unknown', +]); +const classFor = (type: string) => { + const t = type?.toLowerCase?.() ?? ''; + return KNOWN_TYPES.has(t) ? t : 'unknown'; +}; +--- + +{ + nodes.length > 0 && ( +
+ +
+ ) +} + + diff --git a/src/components/IotHub/FilterPanel.astro b/src/components/IotHub/FilterPanel.astro new file mode 100644 index 0000000000..6edc3d4b87 --- /dev/null +++ b/src/components/IotHub/FilterPanel.astro @@ -0,0 +1,817 @@ +--- +import IotHubChevron from './IotHubChevron.astro'; +import { + getSubtypeLabel, + IOT_HUB_STRINGS, + type ItemTypeFilterInfo, + type FilterParamInfo, + type IotHubItemType, +} from '@models/iot-hub'; + +const T = IOT_HUB_STRINGS.filterPanel; + +interface Props { + itemType: IotHubItemType; + filterInfo: ItemTypeFilterInfo; +} + +interface SectionOption { + key: string; + label: string; +} + +interface SectionGroup { + label: string | null; + options: SectionOption[]; +} + +interface Section { + key: string; + label: string; + groups: SectionGroup[]; + totalOptions: number; + searchable: boolean; + open: boolean; +} + +const SEARCH_THRESHOLD = 10; +const OVERFLOW_THRESHOLD = 8; +const GROUPING_THRESHOLD = 11; +const POPULAR_LIMIT = 10; + +const SUBTYPE_ITEM_TYPES: ReadonlySet = new Set([ + 'WIDGET', + 'CALCULATED_FIELD', + 'RULE_CHAIN', +]); + +const { itemType, filterInfo } = Astro.props; + +const isSearchable = (n: number): boolean => n >= SEARCH_THRESHOLD; +const passthrough = (opts: FilterParamInfo[]): SectionOption[] => + opts.map((o) => ({ key: o.key, label: o.key })); + +const buildFlatSection = ( + key: string, + label: string, + options: SectionOption[], + open: boolean +): Section => ({ + key, + label, + groups: [{ label: null, options }], + totalOptions: options.length, + searchable: isSearchable(options.length), + open, +}); + +const buildPopularitySection = ( + key: string, + label: string, + opts: FilterParamInfo[], + open: boolean +): Section => { + const options = passthrough(opts); + if (opts.length < GROUPING_THRESHOLD) { + return buildFlatSection(key, label, options, open); + } + const topKeys = new Set( + [...opts] + .sort((a, b) => b.totalInstallCount - a.totalInstallCount) + .slice(0, POPULAR_LIMIT) + .map((o) => o.key) + ); + const popular = options.filter((o) => topKeys.has(o.key)); + const rest = options.filter((o) => !topKeys.has(o.key)); + const groups: SectionGroup[] = []; + if (popular.length) groups.push({ label: T.mostPopular, options: popular }); + if (rest.length) groups.push({ label: T.all, options: rest }); + return { + key, + label, + groups, + totalOptions: opts.length, + searchable: isSearchable(opts.length), + open, + }; +}; + +const sections: Section[] = []; + +if (itemType === 'DEVICE') { + if (filterInfo.vendors.length > 0) { + sections.push( + buildPopularitySection('vendor', T.sections.vendor, filterInfo.vendors, true) + ); + } + if (filterInfo.hardwareTypes.length > 0) { + sections.push( + buildFlatSection( + 'hardwareType', + T.sections.hardwareType, + passthrough(filterInfo.hardwareTypes), + true + ) + ); + } + const connectivityGroups: SectionGroup[] = Object.entries(filterInfo.connectivities) + .map(([label, options]) => ({ label, options: passthrough(options) })) + .filter((g) => g.options.length > 0); + if (connectivityGroups.length > 0) { + const totalConnectivity = connectivityGroups.reduce( + (sum, g) => sum + g.options.length, + 0 + ); + sections.push({ + key: 'connectivity', + label: T.sections.connectivity, + groups: connectivityGroups, + totalOptions: totalConnectivity, + searchable: isSearchable(totalConnectivity), + open: false, + }); + } +} else { + if (filterInfo.types.length > 0 && SUBTYPE_ITEM_TYPES.has(itemType)) { + sections.push( + buildFlatSection( + 'type', + T.sections.type, + filterInfo.types.map((o) => ({ + key: o.key, + label: getSubtypeLabel(itemType, o.key), + })), + true + ) + ); + } + if (filterInfo.categories.length > 0) { + sections.push( + buildFlatSection( + 'category', + T.sections.category, + passthrough(filterInfo.categories), + true + ) + ); + } +} + +if (filterInfo.useCases.length > 0) { + sections.push( + buildPopularitySection('useCase', T.sections.useCase, filterInfo.useCases, false) + ); +} +--- + + + + + + diff --git a/src/components/IotHub/InstallButton.astro b/src/components/IotHub/InstallButton.astro new file mode 100644 index 0000000000..d5b2ca08e5 --- /dev/null +++ b/src/components/IotHub/InstallButton.astro @@ -0,0 +1,82 @@ +--- +import { getInstallVerb, type IotHubItemType } from '@models/iot-hub'; + +interface Props { + slug: string; + itemType: IotHubItemType; + name: string; + affiliateId?: string | null; + variant?: 'card' | 'hero'; +} + +const { slug, itemType, name, affiliateId = null, variant = 'card' } = Astro.props; +// "Connect" for devices, "Install" otherwise — same wording in both variants. +const label = getInstallVerb(itemType, variant); +--- + + + + + + diff --git a/src/components/IotHub/IotHubBreadcrumbs.astro b/src/components/IotHub/IotHubBreadcrumbs.astro new file mode 100644 index 0000000000..0ddca6a34c --- /dev/null +++ b/src/components/IotHub/IotHubBreadcrumbs.astro @@ -0,0 +1,81 @@ +--- +import IotHubChevron from './IotHubChevron.astro'; + +// Shared breadcrumb row for IoT Hub pages. The component owns only the +// inline row (chrome + separators); parents own the surrounding layout +// (header-clearance padding, gaps to sibling sections) so each page can +// keep its own vertical rhythm. + +interface Crumb { + label: string; + /** Render as `` when set; otherwise as the current-page span. */ + href?: string; +} + +interface Props { + items: Crumb[]; + /** Allow the row to wrap on narrow viewports (DetailHero needs this + * with three crumbs on small screens). */ + wrap?: boolean; +} + +const { items, wrap = false } = Astro.props; +--- + + + + diff --git a/src/components/IotHub/IotHubChevron.astro b/src/components/IotHub/IotHubChevron.astro new file mode 100644 index 0000000000..00922aa4af --- /dev/null +++ b/src/components/IotHub/IotHubChevron.astro @@ -0,0 +1,31 @@ +--- +interface Props { + direction: 'left' | 'right' | 'down'; + size?: number; + class?: string; +} + +const { direction, size = 20, class: className } = Astro.props; + +const paths: Record = { + left: 'm15 6-6 6 6 6', + right: 'm9 6 6 6-6 6', + down: 'm6 9 6 6 6-6', +}; +--- + + diff --git a/src/components/IotHub/IotHubFetchError.astro b/src/components/IotHub/IotHubFetchError.astro new file mode 100644 index 0000000000..7870b403d3 --- /dev/null +++ b/src/components/IotHub/IotHubFetchError.astro @@ -0,0 +1,125 @@ +--- +import fetchErrorIllustration from '@root/assets/iot-hub/search-fetch-error.svg'; +import { IOT_HUB_STRINGS } from '@models/iot-hub'; + +// Shown when the listings API call fails (network down, server +// unreachable, 5xx). Hidden by default; the dynamic-search script +// toggles `hidden` based on the most recent fetch outcome. +// +// The "Try again" button is a real + + + diff --git a/src/components/IotHub/IotHubHero.astro b/src/components/IotHub/IotHubHero.astro new file mode 100644 index 0000000000..06ab53829a --- /dev/null +++ b/src/components/IotHub/IotHubHero.astro @@ -0,0 +1,901 @@ +--- +import IotHubListingLinkTemplate from './IotHubListingLinkTemplate.astro'; +import IotHubRuntimeConfig from './IotHubRuntimeConfig.astro'; + +// Composite cluster SVGs — one per category, layered absolutely over the +// hero; the active one fades + scales in while the rest collapse out. +import devicesCluster from '@root/assets/iot-hub/hero-device-cluster.svg'; +import solutionsCluster from '@root/assets/iot-hub/hero-solution-template-cluster.svg'; +import widgetsCluster from '@root/assets/iot-hub/hero-widget-cluster.svg'; +import calcFieldsCluster from '@root/assets/iot-hub/hero-calculated-field-cluster.svg'; +import alarmRulesCluster from '@root/assets/iot-hub/hero-alarm-rule-cluster.svg'; +import ruleChainsCluster from '@root/assets/iot-hub/hero-rule-chain-cluster.svg'; + +// Each highlight is an anchor to its category index (same routing as the +// tiles below), and hovering one immediately makes it the active accent. +// Colors per the design; slugs mirror IOT_HUB_CATEGORIES. +// `clusterTop` is the cluster's `y` inside the design hero frame; +// since the hero frame starts at y=80 (header height), this is also the gap +// from the header bottom — added to `$header-height` in the CSS. +const highlights = [ + { slug: 'devices', label: 'Devices,', color: '#4c63cc', cluster: devicesCluster, clusterTop: 57 }, + { slug: 'solution-templates', label: 'Solution Templates,', color: '#2c6cb4', cluster: solutionsCluster, clusterTop: 40 }, + { slug: 'widgets', label: 'Widgets,', color: '#2c9755', cluster: widgetsCluster, clusterTop: 60 }, + { slug: 'calculated-fields', label: 'Calculated Fields,', color: '#3db5e0', cluster: calcFieldsCluster, clusterTop: 80 }, + { slug: 'alarm-rules', label: 'Alarm Rules', color: '#d7702f', cluster: alarmRulesCluster, clusterTop: 64 }, + { slug: 'rule-chains', label: '& Rule Chains', color: '#bb7ce9', cluster: ruleChainsCluster, clusterTop: 40 }, +]; +--- + +
+ { + highlights.map(({ slug, cluster, clusterTop }) => ( + + )) + } +
+

ThingsBoard IoT Hub

+

+ Discover ready-to-use + { + highlights.map(({ slug, label, color }) => ( + + {label} + + )) + } +

+ +
+ + +
+ + + + + + diff --git a/src/components/IotHub/IotHubIcon.astro b/src/components/IotHub/IotHubIcon.astro new file mode 100644 index 0000000000..06cee941a5 --- /dev/null +++ b/src/components/IotHub/IotHubIcon.astro @@ -0,0 +1,123 @@ +--- +import '@fontsource/material-icons'; +import { getMdiSvg, svgIcons } from '@util/icons'; + +// Renders an IoT Hub icon by name: +// * bare names registered in `svgIcons` (e.g. `thingsboard`) → SVG +// inlined from the in-memory registry +// * `mdi:`-prefixed names → SVG fetched from +// `${IOT_HUB_API_URL}/assets/mdi/{name}.svg` and inlined +// * everything else → Material Icons font glyph via ligature substitution +// +// MDI SVGs are fetched at build time and cached per worker so each unique +// icon hits the network once regardless of how many listings use it. +// +// PATTERN C TEMPLATE MODE — when used without `icon`, the component renders +// an empty wrapper `` that the runtime binder +// (bindIotHubIcon in iot-hub-icon-bind.ts) can fill later with the same +// three-path resolution logic. Used by IotHubListingLinkTemplate so the +// search popup's cloned cards get full icon support (including MDI). + +interface Props { + /** + * Icon name. Omit (or pass empty) to render template mode — an empty + * wrapper the JS binder fills at runtime. + */ + icon?: string; + /** + * Rendered icon size as a CSS dimension. Numbers are treated as px. + * @default '1em' + */ + size?: string | number; + class?: string; + /** + * Forwarded as the HTML `hidden` attribute on the wrapper span. The + * scoped `&[hidden]` rule below has higher specificity than the + * `.iot-hub-icon { display: inline-flex }` rule, so this reliably + * hides the icon in both static and dynamic (cloned) contexts. + */ + hidden?: boolean; +} + +const { icon, size = '1em', class: className, hidden } = Astro.props; +const sizeCss = typeof size === 'number' ? `${size}px` : size; + +const inlineSvg = icon && icon in svgIcons ? svgIcons[icon] : null; +const isMdi = !!icon && icon.startsWith('mdi:'); +const mdiName = isMdi && icon ? icon.slice(4) : null; +const svgMarkup = inlineSvg ?? (mdiName ? await getMdiSvg(mdiName) : null); + +// `mode` selects the rendered branch. `empty` is the Pattern C template +// shape — a blank wrapper that JS will populate. +const mode: 'svg' | 'font' | 'empty' = !icon + ? 'empty' + : svgMarkup !== null + ? 'svg' + : 'font'; +--- + +{ + mode === 'svg' ? ( +