Add live product catalog to docs site
Espressif product data fetched at build time via Content Layer API and rendered as browsable Starlight pages. Alpine.js provides client-side filtering by series, type, connectivity, and status. - Products content collection with inline API loader + Zod schema - Filterable browse page at /products/ with card grid - Individual spec sheet pages at /products/[id]/ (94 products) - Separate CSS with dark/light theme support - Sidebar section linking to catalog - Cross-references to MCP tool reference docs
This commit is contained in:
parent
dab9c6848e
commit
f8dfa90fba
@ -1,5 +1,6 @@
|
|||||||
import { defineConfig } from "astro/config";
|
import { defineConfig } from "astro/config";
|
||||||
import starlight from "@astrojs/starlight";
|
import starlight from "@astrojs/starlight";
|
||||||
|
import alpinejs from "@astrojs/alpinejs";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import sitemap from "@astrojs/sitemap";
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ export default defineConfig({
|
|||||||
telemetry: false,
|
telemetry: false,
|
||||||
devToolbar: { enabled: false },
|
devToolbar: { enabled: false },
|
||||||
integrations: [
|
integrations: [
|
||||||
|
alpinejs(),
|
||||||
starlight({
|
starlight({
|
||||||
title: "mcesptool",
|
title: "mcesptool",
|
||||||
description:
|
description:
|
||||||
@ -20,7 +22,7 @@ export default defineConfig({
|
|||||||
href: "https://git.supported.systems/MCP/mcesptool",
|
href: "https://git.supported.systems/MCP/mcesptool",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
customCss: ["./src/styles/global.css"],
|
customCss: ["./src/styles/global.css", "./src/styles/products.css"],
|
||||||
sidebar: [
|
sidebar: [
|
||||||
{
|
{
|
||||||
label: "Tutorials",
|
label: "Tutorials",
|
||||||
@ -82,12 +84,22 @@ export default defineConfig({
|
|||||||
label: "Server Tools",
|
label: "Server Tools",
|
||||||
slug: "reference/server-tools",
|
slug: "reference/server-tools",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Product Catalog",
|
||||||
|
slug: "reference/product-catalog",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ label: "Resources", slug: "reference/resources" },
|
{ label: "Resources", slug: "reference/resources" },
|
||||||
{ label: "Configuration", slug: "reference/configuration" },
|
{ label: "Configuration", slug: "reference/configuration" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Product Catalog",
|
||||||
|
items: [
|
||||||
|
{ label: "Browse Products", link: "/products/" },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Explanation",
|
label: "Explanation",
|
||||||
autogenerate: { directory: "explanation" },
|
autogenerate: { directory: "explanation" },
|
||||||
|
|||||||
43
docs-site/package-lock.json
generated
43
docs-site/package-lock.json
generated
@ -8,14 +8,26 @@
|
|||||||
"name": "mcesptool-docs",
|
"name": "mcesptool-docs",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/alpinejs": "^0.4.9",
|
||||||
"@astrojs/sitemap": "^3.3.1",
|
"@astrojs/sitemap": "^3.3.1",
|
||||||
"@astrojs/starlight": "^0.37.6",
|
"@astrojs/starlight": "^0.37.6",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
|
"alpinejs": "^3.15.8",
|
||||||
"astro": "^5.7.10",
|
"astro": "^5.7.10",
|
||||||
"sharp": "^0.33.0",
|
"sharp": "^0.33.0",
|
||||||
"tailwindcss": "^4.1.3"
|
"tailwindcss": "^4.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@astrojs/alpinejs": {
|
||||||
|
"version": "0.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@astrojs/alpinejs/-/alpinejs-0.4.9.tgz",
|
||||||
|
"integrity": "sha512-fvKBAugn7yIngEKfdk6vL3ZlcVKtQvFXCZznG28OikGanKN5W+PkRPIdKaW/0gThRU2FyCemgzyHgyFjsH8dTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/alpinejs": "^3.0.0",
|
||||||
|
"alpinejs": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@astrojs/compiler": {
|
"node_modules/@astrojs/compiler": {
|
||||||
"version": "2.13.1",
|
"version": "2.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz",
|
||||||
@ -2026,6 +2038,13 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/alpinejs": {
|
||||||
|
"version": "3.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.11.tgz",
|
||||||
|
"integrity": "sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@ -2125,6 +2144,21 @@
|
|||||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
@ -2146,6 +2180,15 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/alpinejs": {
|
||||||
|
"version": "3.15.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz",
|
||||||
|
"integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "~3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-align": {
|
"node_modules/ansi-align": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
||||||
|
|||||||
@ -9,9 +9,11 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/alpinejs": "^0.4.9",
|
||||||
"@astrojs/sitemap": "^3.3.1",
|
"@astrojs/sitemap": "^3.3.1",
|
||||||
"@astrojs/starlight": "^0.37.6",
|
"@astrojs/starlight": "^0.37.6",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
|
"alpinejs": "^3.15.8",
|
||||||
"astro": "^5.7.10",
|
"astro": "^5.7.10",
|
||||||
"sharp": "^0.33.0",
|
"sharp": "^0.33.0",
|
||||||
"tailwindcss": "^4.1.3"
|
"tailwindcss": "^4.1.3"
|
||||||
|
|||||||
@ -1,7 +1,140 @@
|
|||||||
import { defineCollection } from "astro:content";
|
import { defineCollection, z } from "astro:content";
|
||||||
import { docsLoader } from "@astrojs/starlight/loaders";
|
import { docsLoader } from "@astrojs/starlight/loaders";
|
||||||
import { docsSchema } from "@astrojs/starlight/schema";
|
import { docsSchema } from "@astrojs/starlight/schema";
|
||||||
|
|
||||||
|
function parseMemoryField(value: string | number | null | undefined): [number, string] {
|
||||||
|
if (typeof value === "number") return [value, ""];
|
||||||
|
if (!value || ["N/A", "NA", "-", "\u2014"].includes(value)) return [0, ""];
|
||||||
|
const parts = value.split(",").map((s) => s.trim());
|
||||||
|
const size = parseInt(parts[0], 10);
|
||||||
|
return [isNaN(size) ? 0 : size, parts[1] || ""];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSramKb(value: string | number | null | undefined): number {
|
||||||
|
if (typeof value === "number") return value;
|
||||||
|
if (!value || ["N/A", "NA", "-", "\u2014"].includes(value)) return 0;
|
||||||
|
const n = parseInt(value.split(",")[0].trim(), 10);
|
||||||
|
return isNaN(n) ? 0 : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCapability(value: string | null | undefined): boolean {
|
||||||
|
if (!value) return false;
|
||||||
|
const v = value.trim().toLowerCase();
|
||||||
|
return !["", "n/a", "na", "-", "\u2014", "no", "0"].includes(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStatus(value: string): string {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
"mass production": "Mass Production",
|
||||||
|
nrnd: "NRND",
|
||||||
|
eol: "EOL",
|
||||||
|
replaced: "Replaced",
|
||||||
|
sample: "Sample",
|
||||||
|
};
|
||||||
|
return mapping[value.trim().toLowerCase()] || value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const products = defineCollection({
|
||||||
|
loader: async () => {
|
||||||
|
const resp = await fetch(
|
||||||
|
"https://products.espressif.com/api/user/products?language=en",
|
||||||
|
);
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`Espressif API returned ${resp.status}`);
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
const results: any[] = data.results || [];
|
||||||
|
|
||||||
|
return results.map((p: any) => {
|
||||||
|
const [flashMb, flashType] = parseMemoryField(p.flash);
|
||||||
|
const [psramMb, psramType] = parseMemoryField(p.psram);
|
||||||
|
|
||||||
|
const id = (p.name || "unknown")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: p.name || "",
|
||||||
|
name_new: p.nameNew || "",
|
||||||
|
type: p.type || "",
|
||||||
|
series: p.seriesName || "",
|
||||||
|
status: normalizeStatus(p.status || ""),
|
||||||
|
mpn: p.mpn || "",
|
||||||
|
dimensions: p.dimensions || "",
|
||||||
|
wifi: p.wifi || "",
|
||||||
|
wifi6: p.wifi6 || "",
|
||||||
|
bluetooth: p.bluetooth || "",
|
||||||
|
thread_zigbee: p.threadZigbee || "",
|
||||||
|
pins: String(p.pins || ""),
|
||||||
|
frequency_mhz: String(p.freq || ""),
|
||||||
|
sram_kb: parseSramKb(p.sram),
|
||||||
|
rom_kb: String(p.rom || ""),
|
||||||
|
flash_mb: flashMb,
|
||||||
|
flash_type: flashType,
|
||||||
|
psram_mb: psramMb,
|
||||||
|
psram_type: psramType,
|
||||||
|
gpio: parseInt(String(p.gpio || 0), 10) || 0,
|
||||||
|
operating_temp: p.operatingTemp || "",
|
||||||
|
voltage_range: p.voltageRange || "",
|
||||||
|
antenna: p.antenna || "",
|
||||||
|
size_type: p.sizeType || "",
|
||||||
|
release_time: p.releaseTime || "",
|
||||||
|
idf_supports: p.idfSupports || "",
|
||||||
|
spq: String(p.spq || ""),
|
||||||
|
moq: String(p.moq || ""),
|
||||||
|
lead_time: p.leadTime || "",
|
||||||
|
eccn_code: p.eccnCode || "",
|
||||||
|
hs_code: p.hsCode || "",
|
||||||
|
replaced_by: p.replacedByName || "",
|
||||||
|
has_wifi: hasCapability(p.wifi),
|
||||||
|
has_bluetooth: hasCapability(p.bluetooth),
|
||||||
|
has_thread_zigbee: hasCapability(p.threadZigbee),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
schema: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
name_new: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
series: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
mpn: z.string(),
|
||||||
|
dimensions: z.string(),
|
||||||
|
wifi: z.string(),
|
||||||
|
wifi6: z.string(),
|
||||||
|
bluetooth: z.string(),
|
||||||
|
thread_zigbee: z.string(),
|
||||||
|
pins: z.string(),
|
||||||
|
frequency_mhz: z.string(),
|
||||||
|
sram_kb: z.number(),
|
||||||
|
rom_kb: z.string(),
|
||||||
|
flash_mb: z.number(),
|
||||||
|
flash_type: z.string(),
|
||||||
|
psram_mb: z.number(),
|
||||||
|
psram_type: z.string(),
|
||||||
|
gpio: z.number(),
|
||||||
|
operating_temp: z.string(),
|
||||||
|
voltage_range: z.string(),
|
||||||
|
antenna: z.string(),
|
||||||
|
size_type: z.string(),
|
||||||
|
release_time: z.string(),
|
||||||
|
idf_supports: z.string(),
|
||||||
|
spq: z.string(),
|
||||||
|
moq: z.string(),
|
||||||
|
lead_time: z.string(),
|
||||||
|
eccn_code: z.string(),
|
||||||
|
hs_code: z.string(),
|
||||||
|
replaced_by: z.string(),
|
||||||
|
has_wifi: z.boolean(),
|
||||||
|
has_bluetooth: z.boolean(),
|
||||||
|
has_thread_zigbee: z.boolean(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||||
|
products,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -103,6 +103,16 @@ All tools follow a consistent pattern: they return a JSON object with a `success
|
|||||||
| `esp_list_tools` | List all tools by category |
|
| `esp_list_tools` | List all tools by category |
|
||||||
| `esp_health_check` | Environment health check |
|
| `esp_health_check` | Environment health check |
|
||||||
|
|
||||||
|
### Product Catalog (5 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_product_search` | Search products by series, type, connectivity, memory, GPIO, temp, status |
|
||||||
|
| `esp_product_info` | Full spec sheet for a product (fuzzy-matched by name or MPN) |
|
||||||
|
| `esp_chip_compare` | Side-by-side comparison of 2-4 products |
|
||||||
|
| `esp_product_recommend` | Recommendations based on use case description |
|
||||||
|
| `esp_product_availability` | Filter by production status, lead time, and MOQ |
|
||||||
|
|
||||||
## Additional References
|
## Additional References
|
||||||
|
|
||||||
<CardGrid>
|
<CardGrid>
|
||||||
|
|||||||
326
docs-site/src/content/docs/reference/product-catalog.mdx
Normal file
326
docs-site/src/content/docs/reference/product-catalog.mdx
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
---
|
||||||
|
title: Product Catalog
|
||||||
|
description: Chip and module discovery, comparison, and procurement planning
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Product Catalog component provides 5 tools for discovering Espressif chips and modules, comparing specifications, getting recommendations, and checking availability. Data is fetched from [Espressif's public product API](https://products.espressif.com/) and cached in-memory.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The catalog is **lazy-loaded** on first use and cached with a 24-hour TTL. The initial call may take a few seconds while the catalog is fetched. Subsequent calls within the same server session are instant.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Product lookups use **fuzzy matching** on name and MPN (Manufacturer Part Number). You don't need the exact product name -- partial matches and close spellings work. For example, `"S3-WROOM"` will match `"ESP32-S3-WROOM-1-N16R8"`.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_product_search
|
||||||
|
|
||||||
|
Search the Espressif product catalog by specs, capabilities, or keyword. All filters are optional and combine with AND logic. Returns matching products with key specs.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `series` | `str \| None` | `None` | Chip family filter, e.g. `"ESP32-S3"`, `"ESP32-C6"`. |
|
||||||
|
| `product_type` | `str \| None` | `None` | `"SoC"` or `"Module"`. |
|
||||||
|
| `wifi` | `bool \| None` | `None` | Filter to products with (or without) Wi-Fi. |
|
||||||
|
| `bluetooth` | `bool \| None` | `None` | Filter to products with (or without) Bluetooth. |
|
||||||
|
| `thread_zigbee` | `bool \| None` | `None` | Filter to products with (or without) Thread/Zigbee (802.15.4). |
|
||||||
|
| `min_flash_mb` | `int \| None` | `None` | Minimum flash size in MB. |
|
||||||
|
| `min_psram_mb` | `int \| None` | `None` | Minimum PSRAM size in MB. |
|
||||||
|
| `min_sram_kb` | `int \| None` | `None` | Minimum SRAM size in KB. |
|
||||||
|
| `min_gpio` | `int \| None` | `None` | Minimum GPIO pin count. |
|
||||||
|
| `max_temp_c` | `int \| None` | `None` | Required upper operating temperature (e.g. `105` for industrial). |
|
||||||
|
| `status` | `str \| None` | `None` | Production status: `"Mass Production"`, `"NRND"`, `"EOL"`, `"Sample"`. |
|
||||||
|
| `antenna` | `str \| None` | `None` | Antenna type: `"PCB"`, `"IPEX"`, etc. |
|
||||||
|
| `keyword` | `str \| None` | `None` | Fuzzy search on product name or MPN. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_product_search", {
|
||||||
|
"series": "ESP32-S3",
|
||||||
|
"product_type": "Module",
|
||||||
|
"wifi": True,
|
||||||
|
"min_flash_mb": 8,
|
||||||
|
"status": "Mass Production"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 5,
|
||||||
|
"filters_applied": {
|
||||||
|
"series": "ESP32-S3",
|
||||||
|
"product_type": "Module",
|
||||||
|
"wifi": true,
|
||||||
|
"min_flash_mb": 8,
|
||||||
|
"status": "Mass Production"
|
||||||
|
},
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "ESP32-S3-WROOM-1-N8",
|
||||||
|
"type": "Module",
|
||||||
|
"series": "ESP32-S3",
|
||||||
|
"status": "Mass Production",
|
||||||
|
"wifi": true,
|
||||||
|
"bluetooth": true,
|
||||||
|
"thread_zigbee": false,
|
||||||
|
"flash_mb": 8,
|
||||||
|
"psram_mb": 0,
|
||||||
|
"sram_kb": 512,
|
||||||
|
"gpio": 36,
|
||||||
|
"antenna": "PCB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `filters_applied` object echoes back only the filters that were actually provided, making it easy to confirm what was searched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_product_info
|
||||||
|
|
||||||
|
Get the full specification sheet for a specific Espressif product. The product is looked up by name or MPN using fuzzy matching.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `name` | `str` | -- | Product name or MPN, e.g. `"ESP32-S3-WROOM-1"`. **Required**. Fuzzy-matched. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_product_info", {
|
||||||
|
"name": "ESP32-S3-WROOM-1"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "ESP32-S3-WROOM-1-N16R8",
|
||||||
|
"name_new": "",
|
||||||
|
"type": "Module",
|
||||||
|
"series": "ESP32-S3",
|
||||||
|
"status": "Mass Production",
|
||||||
|
"mpn": "ESP32-S3-WROOM-1-N16R8",
|
||||||
|
"dimensions": "18x25.5x3.1",
|
||||||
|
"wifi": "802.11 b/g/n",
|
||||||
|
"wifi6": "",
|
||||||
|
"bluetooth": "BLE 5.0",
|
||||||
|
"thread_zigbee": "",
|
||||||
|
"pins": "38",
|
||||||
|
"frequency_mhz": "240",
|
||||||
|
"sram_kb": 512,
|
||||||
|
"rom_kb": "384",
|
||||||
|
"flash_mb": 16,
|
||||||
|
"flash_type": "Quad",
|
||||||
|
"psram_mb": 8,
|
||||||
|
"psram_type": "Octal",
|
||||||
|
"gpio": 36,
|
||||||
|
"operating_temp": "-40~85C",
|
||||||
|
"voltage_range": "3.0~3.6V",
|
||||||
|
"antenna": "PCB",
|
||||||
|
"size_type": "18x25.5",
|
||||||
|
"release_time": "2021-12",
|
||||||
|
"idf_supports": ">=v4.4",
|
||||||
|
"spq": "1400",
|
||||||
|
"moq": "1400",
|
||||||
|
"lead_time": "8-10 weeks",
|
||||||
|
"eccn_code": "5A992.c",
|
||||||
|
"ccatsz_code": "",
|
||||||
|
"hs_code": "8542390000",
|
||||||
|
"pre_firmware": "",
|
||||||
|
"replaced_by": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If no product matches, the response includes an `error` field and a `suggestion` to try `esp_product_search` with a keyword.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_chip_compare
|
||||||
|
|
||||||
|
Compare 2-4 Espressif products side-by-side. The response highlights fields that differ between the products and groups fields that are identical, making it easy to spot the key differences between chip families or module variants.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `products` | `list[str]` | -- | List of 2-4 product names to compare. **Required**. Each name is fuzzy-matched. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_chip_compare", {
|
||||||
|
"products": ["ESP32-S3-WROOM-1-N16R8", "ESP32-C6-WROOM-1-N8"]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"products_compared": ["ESP32-S3-WROOM-1-N16R8", "ESP32-C6-WROOM-1-N8"],
|
||||||
|
"differences": {
|
||||||
|
"series": {
|
||||||
|
"ESP32-S3-WROOM-1-N16R8": "ESP32-S3",
|
||||||
|
"ESP32-C6-WROOM-1-N8": "ESP32-C6"
|
||||||
|
},
|
||||||
|
"flash_mb": {
|
||||||
|
"ESP32-S3-WROOM-1-N16R8": 16,
|
||||||
|
"ESP32-C6-WROOM-1-N8": 8
|
||||||
|
},
|
||||||
|
"thread_zigbee": {
|
||||||
|
"ESP32-S3-WROOM-1-N16R8": "",
|
||||||
|
"ESP32-C6-WROOM-1-N8": "802.15.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"type": "Module",
|
||||||
|
"status": "Mass Production",
|
||||||
|
"antenna": "PCB"
|
||||||
|
},
|
||||||
|
"full_specs": { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `differences` object maps each differing field to a dict of `{product_name: value}`. The `common` object contains fields where all compared products have the same value. `full_specs` includes the complete detail record for each product.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_product_recommend
|
||||||
|
|
||||||
|
Get chip or module recommendations for a use case. Describe what you're building and any constraints, and this tool filters the catalog to matching products ranked by fit.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `use_case` | `str` | -- | What you're building, e.g. `"battery-powered BLE sensor"`. **Required**. |
|
||||||
|
| `constraints` | `str \| None` | `None` | Technical constraints, e.g. `"needs PSRAM, temp to 105C"`. |
|
||||||
|
| `prefer_module` | `bool` | `True` | Prefer modules over bare SoCs. Falls back to SoCs if no modules match. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_product_recommend", {
|
||||||
|
"use_case": "battery-powered BLE sensor with 8MB flash",
|
||||||
|
"constraints": "needs PSRAM, industrial temperature"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"use_case": "battery-powered BLE sensor with 8MB flash",
|
||||||
|
"constraints_parsed": {
|
||||||
|
"wifi": false,
|
||||||
|
"bluetooth": true,
|
||||||
|
"thread_zigbee": false,
|
||||||
|
"psram_required": true,
|
||||||
|
"industrial_temp": true,
|
||||||
|
"min_flash_mb": 8,
|
||||||
|
"min_psram_mb": null,
|
||||||
|
"prefer_module": true
|
||||||
|
},
|
||||||
|
"count": 3,
|
||||||
|
"recommendations": [
|
||||||
|
{
|
||||||
|
"name": "ESP32-S3-WROOM-1-N8R8",
|
||||||
|
"type": "Module",
|
||||||
|
"series": "ESP32-S3",
|
||||||
|
"status": "Mass Production",
|
||||||
|
"wifi": true,
|
||||||
|
"bluetooth": true,
|
||||||
|
"thread_zigbee": false,
|
||||||
|
"flash_mb": 8,
|
||||||
|
"psram_mb": 8,
|
||||||
|
"sram_kb": 512,
|
||||||
|
"gpio": 36,
|
||||||
|
"antenna": "PCB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `constraints_parsed` object shows how the natural-language inputs were interpreted into filter criteria. Up to 15 recommendations are returned.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Recommendations only include products in **Mass Production** status. NRND, EOL, and Sample parts are excluded to avoid suggesting products that may be difficult to source.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_product_availability
|
||||||
|
|
||||||
|
Check product availability and procurement details. Filter by production status, lead time, and minimum order quantity to find products you can actually source.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `series` | `str \| None` | `None` | Chip family filter, e.g. `"ESP32-S3"`. |
|
||||||
|
| `status` | `str` | `"Mass Production"` | Production status filter. |
|
||||||
|
| `max_lead_weeks` | `int \| None` | `None` | Maximum acceptable lead time in weeks. |
|
||||||
|
| `max_moq` | `int \| None` | `None` | Maximum acceptable minimum order quantity. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_product_availability", {
|
||||||
|
"series": "ESP32-C3",
|
||||||
|
"max_lead_weeks": 12,
|
||||||
|
"max_moq": 500
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"filters": {
|
||||||
|
"series": "ESP32-C3",
|
||||||
|
"status": "Mass Production",
|
||||||
|
"max_lead_weeks": 12,
|
||||||
|
"max_moq": 500
|
||||||
|
},
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "ESP32-C3-WROOM-02-N4",
|
||||||
|
"type": "Module",
|
||||||
|
"series": "ESP32-C3",
|
||||||
|
"status": "Mass Production",
|
||||||
|
"wifi": true,
|
||||||
|
"bluetooth": true,
|
||||||
|
"thread_zigbee": false,
|
||||||
|
"flash_mb": 4,
|
||||||
|
"psram_mb": 0,
|
||||||
|
"sram_kb": 400,
|
||||||
|
"gpio": 22,
|
||||||
|
"antenna": "PCB",
|
||||||
|
"lead_time": "8-10 weeks",
|
||||||
|
"moq": "200",
|
||||||
|
"spq": "200",
|
||||||
|
"mpn": "ESP32-C3-WROOM-02-N4",
|
||||||
|
"replaced_by": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Use this tool to identify alternatives for EOL or NRND parts. Set `status` to `"NRND"` or `"EOL"` and check the `replaced_by` field for Espressif's suggested replacements.
|
||||||
|
</Aside>
|
||||||
@ -5,7 +5,7 @@ description: Real-time MCP resources exposed by mcesptool
|
|||||||
|
|
||||||
import { Aside } from '@astrojs/starlight/components';
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
mcesptool exposes 3 MCP resources that provide real-time server state without requiring tool invocations. Resources are read-only and can be accessed by any MCP client using the `read_resource` protocol method.
|
mcesptool exposes 4 MCP resources that provide real-time server state without requiring tool invocations. Resources are read-only and can be accessed by any MCP client using the `read_resource` protocol method.
|
||||||
|
|
||||||
<Aside type="tip">
|
<Aside type="tip">
|
||||||
Resources are lighter-weight than tools -- they do not run subprocess commands or interact with hardware. Use them for dashboard displays, status polling, or client-side configuration checks.
|
Resources are lighter-weight than tools -- they do not run subprocess commands or interact with hardware. Use them for dashboard displays, status polling, or client-side configuration checks.
|
||||||
@ -158,3 +158,65 @@ result = await client.read_resource("esp://capabilities")
|
|||||||
<Aside type="note">
|
<Aside type="note">
|
||||||
The `esp_chip_support` list reflects what esptool can communicate with. QEMU emulation supports a smaller subset -- see the [QEMU Manager supported chips table](/reference/qemu-manager/#supported-chips).
|
The `esp_chip_support` list reflects what esptool can communicate with. QEMU emulation supports a smaller subset -- see the [QEMU Manager supported chips table](/reference/qemu-manager/#supported-chips).
|
||||||
</Aside>
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp://products/catalog
|
||||||
|
|
||||||
|
Complete Espressif product catalog with summarized specs for every SoC and module. Data is fetched from [Espressif's public product API](https://products.espressif.com/) and cached in-memory with a 24-hour TTL.
|
||||||
|
|
||||||
|
For filtered queries, use the [Product Catalog tools](/reference/product-catalog/) instead.
|
||||||
|
|
||||||
|
### URI
|
||||||
|
|
||||||
|
```
|
||||||
|
esp://products/catalog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.read_resource("esp://products/catalog")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "ESP32-S3-WROOM-1-N16R8",
|
||||||
|
"type": "Module",
|
||||||
|
"series": "ESP32-S3",
|
||||||
|
"status": "Mass Production",
|
||||||
|
"wifi": true,
|
||||||
|
"bluetooth": true,
|
||||||
|
"thread_zigbee": false,
|
||||||
|
"flash_mb": 16,
|
||||||
|
"psram_mb": 8,
|
||||||
|
"sram_kb": 512,
|
||||||
|
"gpio": 36,
|
||||||
|
"antenna": "PCB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `name` | `str` | Product name as listed in Espressif's catalog. |
|
||||||
|
| `type` | `str` | `"SoC"` or `"Module"`. |
|
||||||
|
| `series` | `str` | Chip family, e.g. `"ESP32-S3"`, `"ESP32-C6"`. |
|
||||||
|
| `status` | `str` | Production status: `"Mass Production"`, `"NRND"`, `"EOL"`, `"Sample"`. |
|
||||||
|
| `wifi` | `bool` | Whether the product supports Wi-Fi. |
|
||||||
|
| `bluetooth` | `bool` | Whether the product supports Bluetooth/BLE. |
|
||||||
|
| `thread_zigbee` | `bool` | Whether the product supports Thread/Zigbee (802.15.4). |
|
||||||
|
| `flash_mb` | `int` | Integrated flash size in MB (0 if none). |
|
||||||
|
| `psram_mb` | `int` | Integrated PSRAM size in MB (0 if none). |
|
||||||
|
| `sram_kb` | `int` | Internal SRAM size in KB. |
|
||||||
|
| `gpio` | `int` | Number of GPIO pins. |
|
||||||
|
| `antenna` | `str` | Antenna type (e.g. `"PCB"`, `"IPEX"`, or empty for SoCs). |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The catalog resource returns the full, unfiltered product list. For large catalogs (200+ products) this can be a significant payload. Use the [search](/reference/product-catalog/#esp_product_search) and [availability](/reference/product-catalog/#esp_product_availability) tools for targeted queries.
|
||||||
|
</Aside>
|
||||||
|
|||||||
238
docs-site/src/pages/products/[id].astro
Normal file
238
docs-site/src/pages/products/[id].astro
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
import type { GetStaticPaths } from "astro";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||||
|
|
||||||
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
|
const products = await getCollection("products");
|
||||||
|
return products.map((product) => ({
|
||||||
|
params: { id: product.id },
|
||||||
|
props: { product },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const { product } = Astro.props;
|
||||||
|
const d = product.data;
|
||||||
|
|
||||||
|
function statusClass(status: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
"Mass Production": "badge-mass-production",
|
||||||
|
Sample: "badge-sample",
|
||||||
|
NRND: "badge-nrnd",
|
||||||
|
EOL: "badge-eol",
|
||||||
|
Replaced: "badge-replaced",
|
||||||
|
};
|
||||||
|
return map[status] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayValue(val: string | number | undefined | null): string {
|
||||||
|
if (val === undefined || val === null || val === "" || val === 0) return "\u2014";
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function memoryDisplay(mb: number, type: string): string {
|
||||||
|
if (mb === 0) return "\u2014";
|
||||||
|
return `${mb} MB${type ? ` (${type})` : ""}`;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<StarlightPage
|
||||||
|
frontmatter={{
|
||||||
|
title: d.name,
|
||||||
|
description: `${d.name} — ${d.type} from the ${d.series} family. Full specifications, memory, connectivity, and procurement details.`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a href="/products/" class="product-back-link">← Back to catalog</a>
|
||||||
|
|
||||||
|
<div class="product-detail-header">
|
||||||
|
<div class="product-detail-badges">
|
||||||
|
{d.series && <span class="badge badge-series">{d.series}</span>}
|
||||||
|
{d.type && (
|
||||||
|
<span class={`badge ${d.type === "SoC" ? "badge-soc" : "badge-module"}`}>
|
||||||
|
{d.type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{d.status && <span class={`badge ${statusClass(d.status)}`}>{d.status}</span>}
|
||||||
|
{d.antenna && <span class="conn-badge">{d.antenna}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{d.name_new && d.name_new !== d.name && (
|
||||||
|
<p style="font-size: 0.875rem; color: var(--sl-color-gray-3); margin-block-end: 1.5rem;">
|
||||||
|
Also known as: <strong style="color: var(--sl-color-white);">{d.name_new}</strong>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="product-spec-grid">
|
||||||
|
<div class="spec-section">
|
||||||
|
<h3>Memory</h3>
|
||||||
|
<table class="spec-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>SRAM</th>
|
||||||
|
<td>{d.sram_kb > 0 ? `${d.sram_kb} KB` : "\u2014"}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>ROM</th>
|
||||||
|
<td>{displayValue(d.rom_kb) !== "\u2014" ? `${d.rom_kb} KB` : "\u2014"}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Flash</th>
|
||||||
|
<td>{memoryDisplay(d.flash_mb, d.flash_type)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>PSRAM</th>
|
||||||
|
<td>{memoryDisplay(d.psram_mb, d.psram_type)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-section">
|
||||||
|
<h3>Connectivity</h3>
|
||||||
|
<table class="spec-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Wi-Fi</th>
|
||||||
|
<td>
|
||||||
|
{d.has_wifi ? (
|
||||||
|
<span class="conn-badge conn-badge-wifi">{d.wifi}</span>
|
||||||
|
) : "\u2014"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{d.wifi6 && (
|
||||||
|
<tr>
|
||||||
|
<th>Wi-Fi 6</th>
|
||||||
|
<td><span class="conn-badge conn-badge-wifi">{d.wifi6}</span></td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
<tr>
|
||||||
|
<th>Bluetooth</th>
|
||||||
|
<td>
|
||||||
|
{d.has_bluetooth ? (
|
||||||
|
<span class="conn-badge conn-badge-bt">{d.bluetooth}</span>
|
||||||
|
) : "\u2014"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Thread / Zigbee</th>
|
||||||
|
<td>
|
||||||
|
{d.has_thread_zigbee ? (
|
||||||
|
<span class="conn-badge conn-badge-thread">{d.thread_zigbee}</span>
|
||||||
|
) : "\u2014"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-section">
|
||||||
|
<h3>Physical</h3>
|
||||||
|
<table class="spec-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Pins</th>
|
||||||
|
<td>{displayValue(d.pins)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>GPIO</th>
|
||||||
|
<td>{d.gpio > 0 ? d.gpio : "\u2014"}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Frequency</th>
|
||||||
|
<td>{displayValue(d.frequency_mhz) !== "\u2014" ? `${d.frequency_mhz} MHz` : "\u2014"}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Dimensions</th>
|
||||||
|
<td>{displayValue(d.dimensions)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Antenna</th>
|
||||||
|
<td>{displayValue(d.antenna)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Package</th>
|
||||||
|
<td>{displayValue(d.size_type)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Operating Temp</th>
|
||||||
|
<td>{displayValue(d.operating_temp)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Voltage Range</th>
|
||||||
|
<td>{displayValue(d.voltage_range)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-section">
|
||||||
|
<h3>Procurement</h3>
|
||||||
|
<table class="spec-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>MPN</th>
|
||||||
|
<td>{displayValue(d.mpn)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>MOQ</th>
|
||||||
|
<td>{displayValue(d.moq)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>SPQ</th>
|
||||||
|
<td>{displayValue(d.spq)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Lead Time</th>
|
||||||
|
<td>{displayValue(d.lead_time)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>ECCN</th>
|
||||||
|
<td>{displayValue(d.eccn_code)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>HS Code</th>
|
||||||
|
<td>{displayValue(d.hs_code)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(d.release_time || d.idf_supports || d.replaced_by) && (
|
||||||
|
<div class="spec-section" style="margin-block-end: 2rem;">
|
||||||
|
<h3>Lifecycle</h3>
|
||||||
|
<table class="spec-table">
|
||||||
|
<tbody>
|
||||||
|
{d.release_time && (
|
||||||
|
<tr>
|
||||||
|
<th>Release Date</th>
|
||||||
|
<td>{d.release_time}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{d.idf_supports && (
|
||||||
|
<tr>
|
||||||
|
<th>IDF Support</th>
|
||||||
|
<td>{d.idf_supports}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{d.replaced_by && (
|
||||||
|
<tr>
|
||||||
|
<th>Replaced By</th>
|
||||||
|
<td>
|
||||||
|
<a href={`/products/${d.replaced_by.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}/`}>
|
||||||
|
{d.replaced_by}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<hr style="border-color: var(--sl-color-gray-5); margin-block: 2rem;" />
|
||||||
|
<p style="font-size: 0.875rem; color: var(--sl-color-gray-3);">
|
||||||
|
Query this data programmatically via the <a href="/reference/product-catalog/">Product Catalog MCP tools</a>.
|
||||||
|
</p>
|
||||||
|
</StarlightPage>
|
||||||
246
docs-site/src/pages/products/index.astro
Normal file
246
docs-site/src/pages/products/index.astro
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||||
|
|
||||||
|
const allProducts = await getCollection("products");
|
||||||
|
|
||||||
|
// Extract unique series for filter chips
|
||||||
|
const allSeries = [...new Set(allProducts.map((p) => p.data.series).filter(Boolean))].sort();
|
||||||
|
const allStatuses = [...new Set(allProducts.map((p) => p.data.status).filter(Boolean))].sort();
|
||||||
|
|
||||||
|
// Serialize products for Alpine.js
|
||||||
|
const productsJson = JSON.stringify(
|
||||||
|
allProducts.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
...p.data,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<StarlightPage
|
||||||
|
frontmatter={{
|
||||||
|
title: "Product Catalog",
|
||||||
|
description: "Browse the complete Espressif ESP32 and ESP8266 product catalog — SoCs, modules, and dev kits with full specifications.",
|
||||||
|
template: "splash",
|
||||||
|
hero: {
|
||||||
|
title: "Espressif Product Catalog",
|
||||||
|
tagline: "Browse all ESP32 and ESP8266 chips and modules. Data sourced live from Espressif at build time.",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div x-data={`catalog(${productsJson})`}>
|
||||||
|
<div class="product-filters">
|
||||||
|
<div class="filter-row">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="filter-search"
|
||||||
|
placeholder="Search by name, MPN, or series..."
|
||||||
|
x-model="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-label">Type</span>
|
||||||
|
<div class="filter-group">
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: typeFilter === '' }"
|
||||||
|
@click="typeFilter = ''"
|
||||||
|
>All</button>
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: typeFilter === 'SoC' }"
|
||||||
|
@click="typeFilter = typeFilter === 'SoC' ? '' : 'SoC'"
|
||||||
|
>SoC</button>
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: typeFilter === 'Module' }"
|
||||||
|
@click="typeFilter = typeFilter === 'Module' ? '' : 'Module'"
|
||||||
|
>Module</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="filter-sep"></span>
|
||||||
|
|
||||||
|
<span class="filter-label">Connectivity</span>
|
||||||
|
<div class="filter-group">
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: wifiFilter }"
|
||||||
|
@click="wifiFilter = !wifiFilter"
|
||||||
|
>Wi-Fi</button>
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: btFilter }"
|
||||||
|
@click="btFilter = !btFilter"
|
||||||
|
>Bluetooth</button>
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: threadFilter }"
|
||||||
|
@click="threadFilter = !threadFilter"
|
||||||
|
>Thread/Zigbee</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-label">Series</span>
|
||||||
|
<div class="filter-group">
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: seriesFilter === '' }"
|
||||||
|
@click="seriesFilter = ''"
|
||||||
|
>All</button>
|
||||||
|
{allSeries.map((s) => (
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
x-bind:class={`{ active: seriesFilter === '${s}' }`}
|
||||||
|
x-on:click={`seriesFilter = seriesFilter === '${s}' ? '' : '${s}'`}
|
||||||
|
>{s}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-row">
|
||||||
|
<span class="filter-label">Status</span>
|
||||||
|
<div class="filter-group">
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ active: statusFilter === '' }"
|
||||||
|
@click="statusFilter = ''"
|
||||||
|
>All</button>
|
||||||
|
{allStatuses.map((s) => (
|
||||||
|
<button
|
||||||
|
class="filter-chip"
|
||||||
|
x-bind:class={`{ active: statusFilter === '${s}' }`}
|
||||||
|
x-on:click={`statusFilter = statusFilter === '${s}' ? '' : '${s}'`}
|
||||||
|
>{s}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-count">
|
||||||
|
Showing <strong x-text="filtered.length"></strong> of <strong>{allProducts.length}</strong> products
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-grid">
|
||||||
|
<template x-for="p in filtered" :key="p.id">
|
||||||
|
<a :href="`/products/${p.id}/`" class="product-card">
|
||||||
|
<div class="product-card-header">
|
||||||
|
<span class="product-card-name" x-text="p.name"></span>
|
||||||
|
</div>
|
||||||
|
<div class="product-card-badges">
|
||||||
|
<span class="badge badge-series" x-text="p.series" x-show="p.series"></span>
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
:class="p.type === 'SoC' ? 'badge-soc' : 'badge-module'"
|
||||||
|
x-text="p.type"
|
||||||
|
x-show="p.type"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
:class="statusClass(p.status)"
|
||||||
|
x-text="p.status"
|
||||||
|
x-show="p.status"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<dl class="product-card-specs">
|
||||||
|
<template x-if="p.flash_mb > 0">
|
||||||
|
<div>
|
||||||
|
<dt>Flash</dt>
|
||||||
|
<dd x-text="p.flash_mb + ' MB' + (p.flash_type ? ' ' + p.flash_type : '')"></dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="p.psram_mb > 0">
|
||||||
|
<div>
|
||||||
|
<dt>PSRAM</dt>
|
||||||
|
<dd x-text="p.psram_mb + ' MB' + (p.psram_type ? ' ' + p.psram_type : '')"></dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="p.sram_kb > 0">
|
||||||
|
<div>
|
||||||
|
<dt>SRAM</dt>
|
||||||
|
<dd x-text="p.sram_kb + ' KB'"></dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="p.gpio > 0">
|
||||||
|
<div>
|
||||||
|
<dt>GPIO</dt>
|
||||||
|
<dd x-text="p.gpio"></dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
<div class="product-card-connectivity">
|
||||||
|
<span class="conn-badge conn-badge-wifi" x-show="p.has_wifi" x-text="'Wi-Fi'"></span>
|
||||||
|
<span class="conn-badge conn-badge-bt" x-show="p.has_bluetooth" x-text="'BT'"></span>
|
||||||
|
<span class="conn-badge conn-badge-thread" x-show="p.has_thread_zigbee" x-text="'Thread/Zigbee'"></span>
|
||||||
|
<span class="conn-badge" x-show="p.antenna" x-text="p.antenna"></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-empty" x-show="filtered.length === 0">
|
||||||
|
<p>No products match your current filters. Try broadening your search.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.Alpine.data("catalog", (products: any[]) => ({
|
||||||
|
products,
|
||||||
|
search: "",
|
||||||
|
typeFilter: "",
|
||||||
|
seriesFilter: "",
|
||||||
|
statusFilter: "",
|
||||||
|
wifiFilter: false,
|
||||||
|
btFilter: false,
|
||||||
|
threadFilter: false,
|
||||||
|
|
||||||
|
get filtered() {
|
||||||
|
let result = this.products;
|
||||||
|
|
||||||
|
if (this.search) {
|
||||||
|
const q = this.search.toLowerCase();
|
||||||
|
result = result.filter(
|
||||||
|
(p: any) =>
|
||||||
|
p.name.toLowerCase().includes(q) ||
|
||||||
|
p.mpn.toLowerCase().includes(q) ||
|
||||||
|
p.series.toLowerCase().includes(q),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.typeFilter) {
|
||||||
|
result = result.filter((p: any) => p.type === this.typeFilter);
|
||||||
|
}
|
||||||
|
if (this.seriesFilter) {
|
||||||
|
result = result.filter((p: any) => p.series === this.seriesFilter);
|
||||||
|
}
|
||||||
|
if (this.statusFilter) {
|
||||||
|
result = result.filter((p: any) => p.status === this.statusFilter);
|
||||||
|
}
|
||||||
|
if (this.wifiFilter) {
|
||||||
|
result = result.filter((p: any) => p.has_wifi);
|
||||||
|
}
|
||||||
|
if (this.btFilter) {
|
||||||
|
result = result.filter((p: any) => p.has_bluetooth);
|
||||||
|
}
|
||||||
|
if (this.threadFilter) {
|
||||||
|
result = result.filter((p: any) => p.has_thread_zigbee);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
statusClass(status: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
"Mass Production": "badge-mass-production",
|
||||||
|
Sample: "badge-sample",
|
||||||
|
NRND: "badge-nrnd",
|
||||||
|
EOL: "badge-eol",
|
||||||
|
Replaced: "badge-replaced",
|
||||||
|
};
|
||||||
|
return map[status] || "";
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</StarlightPage>
|
||||||
438
docs-site/src/styles/products.css
Normal file
438
docs-site/src/styles/products.css
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
/* Product Catalog — filter bar, card grid, detail pages */
|
||||||
|
|
||||||
|
/* ── Filter bar ─────────────────────────────────────────── */
|
||||||
|
.product-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-block-end: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--sl-color-gray-6);
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-filters .filter-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-filters .filter-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--sl-color-gray-3);
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-filters .filter-sep {
|
||||||
|
width: 1px;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: var(--sl-color-gray-5);
|
||||||
|
margin-inline: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.625rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip:hover {
|
||||||
|
border-color: var(--sl-color-accent);
|
||||||
|
color: var(--sl-color-accent-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip.active {
|
||||||
|
background: var(--sl-color-accent);
|
||||||
|
border-color: var(--sl-color-accent);
|
||||||
|
color: var(--sl-color-black);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-search {
|
||||||
|
flex: 1 1 12rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--sl-color-black);
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-search::placeholder {
|
||||||
|
color: var(--sl-color-gray-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-search:focus {
|
||||||
|
border-color: var(--sl-color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row + .filter-row {
|
||||||
|
margin-block-start: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Result count ───────────────────────────────────────── */
|
||||||
|
.product-count {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--sl-color-gray-3);
|
||||||
|
margin-block-end: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-count strong {
|
||||||
|
color: var(--sl-color-accent-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card grid ──────────────────────────────────────────── */
|
||||||
|
.product-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.product-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.product-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--sl-color-gray-6);
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover {
|
||||||
|
border-color: var(--sl-color-accent);
|
||||||
|
box-shadow: 0 0 0 1px var(--sl-color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-block-end: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-name {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
margin-block-end: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-specs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.25rem 1rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
margin-block-end: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-specs dt {
|
||||||
|
color: var(--sl-color-gray-3);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-specs dd {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-connectivity {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-block-start: auto;
|
||||||
|
padding-block-start: 0.5rem;
|
||||||
|
border-block-start: 1px solid var(--sl-color-gray-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Badges ─────────────────────────────────────────────── */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
border-radius: 9999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Series badges */
|
||||||
|
.badge-series {
|
||||||
|
background: color-mix(in srgb, var(--sl-color-accent) 15%, transparent);
|
||||||
|
color: var(--sl-color-accent-high);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--sl-color-accent) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type badges */
|
||||||
|
.badge-soc {
|
||||||
|
background: color-mix(in srgb, #3b82f6 15%, transparent);
|
||||||
|
color: #93c5fd;
|
||||||
|
border: 1px solid color-mix(in srgb, #3b82f6 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-module {
|
||||||
|
background: color-mix(in srgb, #8b5cf6 15%, transparent);
|
||||||
|
color: #c4b5fd;
|
||||||
|
border: 1px solid color-mix(in srgb, #8b5cf6 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badges */
|
||||||
|
.badge-mass-production {
|
||||||
|
background: color-mix(in srgb, var(--sl-color-accent) 15%, transparent);
|
||||||
|
color: var(--sl-color-accent-high);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--sl-color-accent) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-sample {
|
||||||
|
background: color-mix(in srgb, #3b82f6 15%, transparent);
|
||||||
|
color: #93c5fd;
|
||||||
|
border: 1px solid color-mix(in srgb, #3b82f6 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-nrnd {
|
||||||
|
background: color-mix(in srgb, #f59e0b 15%, transparent);
|
||||||
|
color: #fcd34d;
|
||||||
|
border: 1px solid color-mix(in srgb, #f59e0b 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-eol {
|
||||||
|
background: color-mix(in srgb, #ef4444 15%, transparent);
|
||||||
|
color: #fca5a5;
|
||||||
|
border: 1px solid color-mix(in srgb, #ef4444 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-replaced {
|
||||||
|
background: color-mix(in srgb, #ef4444 15%, transparent);
|
||||||
|
color: #fca5a5;
|
||||||
|
border: 1px solid color-mix(in srgb, #ef4444 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connectivity badges */
|
||||||
|
.conn-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: color-mix(in srgb, var(--sl-color-gray-4) 12%, transparent);
|
||||||
|
color: var(--sl-color-gray-2);
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn-badge-wifi {
|
||||||
|
background: color-mix(in srgb, #06b6d4 12%, transparent);
|
||||||
|
color: #67e8f9;
|
||||||
|
border-color: color-mix(in srgb, #06b6d4 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn-badge-bt {
|
||||||
|
background: color-mix(in srgb, #3b82f6 12%, transparent);
|
||||||
|
color: #93c5fd;
|
||||||
|
border-color: color-mix(in srgb, #3b82f6 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conn-badge-thread {
|
||||||
|
background: color-mix(in srgb, #22c55e 12%, transparent);
|
||||||
|
color: #86efac;
|
||||||
|
border-color: color-mix(in srgb, #22c55e 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Detail page ────────────────────────────────────────── */
|
||||||
|
.product-detail-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-block-end: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-detail-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-spec-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-block-end: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.product-spec-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-section {
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--sl-color-gray-6);
|
||||||
|
border: 1px solid var(--sl-color-gray-5);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-section h3 {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--sl-color-accent-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-table tr + tr {
|
||||||
|
border-block-start: 1px solid var(--sl-color-gray-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-table th {
|
||||||
|
text-align: start;
|
||||||
|
padding: 0.375rem 0.75rem 0.375rem 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--sl-color-gray-3);
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-table td {
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--sl-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Back link ──────────────────────────────────────────── */
|
||||||
|
.product-back-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--sl-color-accent-high);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-block-end: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Empty state ────────────────────────────────────────── */
|
||||||
|
.product-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: var(--sl-color-gray-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-empty p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Light theme overrides ──────────────────────────────── */
|
||||||
|
:root[data-theme="light"] .badge-soc {
|
||||||
|
background: color-mix(in srgb, #3b82f6 10%, transparent);
|
||||||
|
color: #1d4ed8;
|
||||||
|
border-color: color-mix(in srgb, #3b82f6 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .badge-module {
|
||||||
|
background: color-mix(in srgb, #8b5cf6 10%, transparent);
|
||||||
|
color: #6d28d9;
|
||||||
|
border-color: color-mix(in srgb, #8b5cf6 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .badge-nrnd {
|
||||||
|
background: color-mix(in srgb, #f59e0b 10%, transparent);
|
||||||
|
color: #b45309;
|
||||||
|
border-color: color-mix(in srgb, #f59e0b 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .badge-eol,
|
||||||
|
:root[data-theme="light"] .badge-replaced {
|
||||||
|
background: color-mix(in srgb, #ef4444 10%, transparent);
|
||||||
|
color: #b91c1c;
|
||||||
|
border-color: color-mix(in srgb, #ef4444 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .badge-sample {
|
||||||
|
background: color-mix(in srgb, #3b82f6 10%, transparent);
|
||||||
|
color: #1d4ed8;
|
||||||
|
border-color: color-mix(in srgb, #3b82f6 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .conn-badge-wifi {
|
||||||
|
background: color-mix(in srgb, #06b6d4 10%, transparent);
|
||||||
|
color: #0e7490;
|
||||||
|
border-color: color-mix(in srgb, #06b6d4 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .conn-badge-bt {
|
||||||
|
background: color-mix(in srgb, #3b82f6 10%, transparent);
|
||||||
|
color: #1d4ed8;
|
||||||
|
border-color: color-mix(in srgb, #3b82f6 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .conn-badge-thread {
|
||||||
|
background: color-mix(in srgb, #22c55e 10%, transparent);
|
||||||
|
color: #15803d;
|
||||||
|
border-color: color-mix(in srgb, #22c55e 20%, transparent);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user