diff --git a/doc/_extensions/zephyr/domain/__init__.py b/doc/_extensions/zephyr/domain/__init__.py
index 8525acaadd7..ab2edf4cd5b 100644
--- a/doc/_extensions/zephyr/domain/__init__.py
+++ b/doc/_extensions/zephyr/domain/__init__.py
@@ -724,6 +724,7 @@ class BoardDirective(SphinxDirective):
board_node = BoardNode(id=board_name)
board_node["full_name"] = board["full_name"]
board_node["vendor"] = vendors.get(board["vendor"], board["vendor"])
+ board_node["revision_default"] = board["revision_default"]
board_node["supported_features"] = board["supported_features"]
board_node["archs"] = board["archs"]
board_node["socs"] = board["socs"]
@@ -825,18 +826,39 @@ class BoardSupportedHardwareDirective(SphinxDirective):
"""
result_nodes.append(nodes.raw("", html_contents, format="html"))
+ tables_container = nodes.container(ids=[f"{board_node['id']}-hw-features"])
+ result_nodes.append(tables_container)
+
+ board_json = json.dumps(
+ {
+ "board_name": board_node["id"],
+ "revision_default": board_node["revision_default"],
+ "targets": list(supported_features.keys()),
+ }
+ )
+ result_nodes.append(
+ nodes.raw(
+ "",
+ f"""""",
+ format="html",
+ )
+ )
+
for target, features in sorted(supported_features.items()):
if not features:
continue
- target_heading = nodes.section(ids=[f"{board_node['id']}-{target}-hw-features"])
+ target_heading = nodes.section(ids=[f"{board_node['id']}-{target}-hw-features-section"])
heading = nodes.title()
heading += nodes.literal(text=target)
heading += nodes.Text(" target")
target_heading += heading
- result_nodes.append(target_heading)
+ tables_container += target_heading
- table = nodes.table(classes=["colwidths-given", "hardware-features"])
+ table = nodes.table(
+ classes=["colwidths-given", "hardware-features"],
+ ids=[f"{board_node['id']}-{target}-hw-features-table"],
+ )
tgroup = nodes.tgroup(cols=4)
tgroup += nodes.colspec(colwidth=15, classes=["type"])
@@ -965,7 +987,7 @@ class BoardSupportedHardwareDirective(SphinxDirective):
tgroup += tbody
table += tgroup
- result_nodes.append(table)
+ tables_container += table
return result_nodes
diff --git a/doc/_extensions/zephyr/domain/static/css/board.css b/doc/_extensions/zephyr/domain/static/css/board.css
index c5fbc205d4d..d2d90b9fe5c 100644
--- a/doc/_extensions/zephyr/domain/static/css/board.css
+++ b/doc/_extensions/zephyr/domain/static/css/board.css
@@ -105,6 +105,79 @@
}
}
+.board-target-selector {
+ margin-bottom: 1em;
+ padding: 15px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ position: relative;
+}
+
+.board-target-selector > div {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.static-value {
+ padding: 5px 0px;
+ color: var(--body-color);
+ font-family: var(--monospace-font-family);
+ font-size: 0.9em;
+}
+
+.separator {
+ font-size: 16px;
+ font-weight: bold;
+ margin: 0 4px;
+}
+
+.board-target-selector select {
+ background-color: var(--input-background-color);
+ color: var(--body-color);
+ border-radius: 4px;
+ font-family: var(--monospace-font-family);
+ font-size: 0.9em;
+ box-shadow: none;
+ transition: none;
+}
+
+.board-target-selector select:focus {
+ border-color: var(--input-focus-border-color);
+}
+
+.copy-button {
+ background: transparent;
+ border: 1px solid var(--input-border-color);
+ border-radius: 4px;
+ padding: 4px 8px;
+ cursor: pointer;
+ font-size: 0.8em;
+ color: var(--body-color);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ transition: all 0.2s ease;
+}
+
+.copy-button:hover {
+ background: var(--input-background-color);
+ border-color: var(--input-focus-border-color);
+}
+
+.copy-button.copied {
+ background: var(--admonition-note-title-background-color);
+ color: var(--admonition-note-title-color);
+ border-color: var(--admonition-note-title-color);
+}
+
+.copy-button svg {
+ width: 18px;
+ height: 18px;
+ fill: currentColor;
+}
.hardware-features {
th {
@@ -221,4 +294,4 @@
display: flex;
align-items: center;
gap: 6px;
-}
\ No newline at end of file
+}
diff --git a/doc/_extensions/zephyr/domain/static/js/board.js b/doc/_extensions/zephyr/domain/static/js/board.js
index a748d64ef2b..6ff36a5b0f2 100644
--- a/doc/_extensions/zephyr/domain/static/js/board.js
+++ b/doc/_extensions/zephyr/domain/static/js/board.js
@@ -3,4 +3,246 @@
* SPDX-License-Identifier: Apache-2.0
*/
-/* file intentionally left blank */
+(() => {
+ // SVG icons for copy button
+ const COPY_ICON = ``;
+ const CHECK_ICON = ``;
+
+ // DOM element creation helper
+ const createElement = (tag, className, text) => {
+ const element = document.createElement(tag);
+ if (className) element.className = className;
+ if (text) element.textContent = text;
+ return element;
+ };
+
+ // Target parsing helper
+ const parseTargetString = (targetString) => {
+ const [boardWithRev, ...qualifiers] = targetString.split('/');
+ const [board, revision] = boardWithRev.split('@');
+ return {
+ board: board || '',
+ revision: revision || '',
+ qualifier: qualifiers.join('/') || ''
+ };
+ };
+
+ // Select element creation helper
+ const createSelect = (options, selectedValue) => {
+ const select = createElement('select');
+ options.sort().forEach(value => {
+ const option = createElement('option', null, value);
+ option.value = value;
+ option.selected = (value === selectedValue);
+ select.appendChild(option);
+ });
+ return select;
+ };
+
+ // Main initialization function
+ const initializeBoardSelector = () => {
+ const container = document.querySelector('.container[id$="-hw-features"]');
+ if (!container) return console.error('Container element not found');
+
+ const components = {
+ boards: new Set([board_data.board_name]),
+ revisions: new Set(),
+ qualifiers: new Set()
+ };
+
+ board_data.targets.forEach(target => {
+ const { revision, qualifier } = parseTargetString(target);
+ if (revision) components.revisions.add(revision);
+ if (qualifier) components.qualifiers.add(qualifier);
+ });
+
+ const initialValues = parseTargetString(board_data.targets[0]);
+ if (board_data.revision_default) {
+ initialValues.revision = board_data.revision_default;
+ }
+
+ const selector = createElement('div', 'board-target-selector');
+ selector.appendChild(createElement('div', 'static-value', initialValues.board));
+
+ // Add revision selector if needed
+ let revisionSelect;
+ if (components.revisions.size > 0) {
+ selector.appendChild(createElement('div', 'separator', '@'));
+ revisionSelect = createSelect(
+ Array.from(components.revisions),
+ initialValues.revision
+ );
+ selector.appendChild(revisionSelect);
+ }
+
+ // Add qualifier selector or static value
+ let qualifierSelect;
+ if (components.qualifiers.size > 0) {
+ selector.appendChild(createElement('div', 'separator', '/'));
+ if (components.qualifiers.size === 1) {
+ selector.appendChild(createElement('div', 'static-value', initialValues.qualifier));
+ } else {
+ qualifierSelect = createSelect(
+ Array.from(components.qualifiers),
+ null
+ );
+ selector.appendChild(qualifierSelect);
+ }
+ }
+
+ // Add copy button
+ const copyButton = createElement('button', 'copy-button');
+ copyButton.innerHTML = COPY_ICON;
+ copyButton.addEventListener('click', handleCopyButtonClick(
+ initialValues,
+ revisionSelect,
+ qualifierSelect,
+ copyButton
+ ));
+ selector.appendChild(copyButton);
+
+ // Insert selector into DOM and set up event listeners
+ container.parentNode.insertBefore(selector, container);
+ selector.querySelectorAll('select').forEach(select => {
+ select.addEventListener('change', () => {
+ updateSelectOptions(initialValues, revisionSelect, qualifierSelect);
+ updateDisplayedTable(revisionSelect, qualifierSelect);
+
+ });
+ });
+
+ // Initial display setup
+ initializeDisplay();
+ // updateDisplayedTable(revisionSelect, qualifierSelect);
+ };
+
+ // Copy button click handler
+ const handleCopyButtonClick = (initialValues, revisionSelect, qualifierSelect, button) => async () => {
+ try {
+ const target = buildTargetString(initialValues, revisionSelect, qualifierSelect);
+ await navigator.clipboard.writeText(target);
+
+ button.classList.add('copied');
+ button.innerHTML = CHECK_ICON;
+
+ setTimeout(() => {
+ button.classList.remove('copied');
+ button.innerHTML = COPY_ICON;
+ }, 1000);
+ } catch (error) {
+ console.error('Failed to copy text:', error);
+ }
+ };
+
+ // Build target string from current selections
+ const buildTargetString = (initialValues, revisionSelect, qualifierSelect) => {
+ const parts = [initialValues.board];
+
+ if (revisionSelect) {
+ parts.push(`@${revisionSelect.value}`);
+ } else if (initialValues.revision) {
+ parts.push(`@${initialValues.revision}`);
+ }
+
+ if (qualifierSelect) {
+ parts.push(`/${qualifierSelect.value}`);
+ } else if (initialValues.qualifier) {
+ parts.push(`/${initialValues.qualifier}`);
+ }
+
+ return parts.join('');
+ };
+
+ // Update displayed table based on selections
+ const updateDisplayedTable = (revisionSelect, qualifierSelect) => {
+ const currentTarget = buildTargetString(
+ parseTargetString(board_data.targets[0]),
+ revisionSelect,
+ qualifierSelect
+ );
+
+ console.log(currentTarget);
+
+ document.querySelectorAll('section[id$="-hw-features-section"]').forEach(section => {
+ // Find the matching target in board_data.targets
+ const isMatch = section.id.replace(/${board_data.name}/).replace(/-hw-features-section$/, '').endsWith(currentTarget);
+
+ const table = document.querySelector(`table[id="${section.id.replace('-section', '-table')}"]`);
+ const wrapper = table?.closest('.wy-table-responsive');
+
+
+ if (table) table.style.display = isMatch ? 'table' : 'none';
+ if (wrapper) wrapper.style.display = isMatch ? 'block' : 'none';
+ });
+ };
+
+ // Initial display setup
+ const initializeDisplay = () => {
+ document.querySelectorAll('section[id$="-hw-features-section"]').forEach(section => section.style.display = 'none');
+ document.querySelectorAll('table.hardware-features').forEach((table, index) => {
+ const wrapper = table.closest('.wy-table-responsive');
+ table.style.display = index === 0 ? 'table' : 'none';
+ if (wrapper) wrapper.style.display = index === 0 ? 'block' : 'none';
+ });
+ updateDisplayedTable(revisionSelect, qualifierSelect);
+ };
+
+ // Update select options based on valid combinations
+ const updateSelectOptions = (initialValues, revisionSelect, qualifierSelect) => {
+ if (revisionSelect) {
+ const currentBoard = initialValues.board;
+ const validRevisions = new Set();
+
+ // Find valid revisions for current board
+ board_data.targets.forEach(target => {
+ const { board, revision } = parseTargetString(target);
+ if (board === currentBoard && revision) {
+ validRevisions.add(revision);
+ }
+ });
+
+ // Update revision select options
+ Array.from(revisionSelect.options).forEach(option => {
+ option.disabled = !validRevisions.has(option.value);
+ if (option.disabled && option.selected) {
+ // If current selection is invalid, select first valid option
+ const firstValid = Array.from(revisionSelect.options).find(opt => !opt.disabled);
+ if (firstValid) firstValid.selected = true;
+ }
+ });
+ }
+
+ if (qualifierSelect) {
+ const currentBoard = initialValues.board;
+ const currentRevision = revisionSelect ? revisionSelect.value : initialValues.revision;
+ const validQualifiers = new Set();
+
+ // Find valid qualifiers for current board and revision
+ board_data.targets.forEach(target => {
+ const { board, revision, qualifier } = parseTargetString(target);
+ if (board === currentBoard &&
+ (!revision || revision === currentRevision) &&
+ qualifier) {
+ validQualifiers.add(qualifier);
+ }
+ });
+
+ // Update qualifier select options
+ Array.from(qualifierSelect.options).forEach(option => {
+ option.disabled = !validQualifiers.has(option.value);
+ if (option.disabled && option.selected) {
+ // If current selection is invalid, select first valid option
+ const firstValid = Array.from(qualifierSelect.options).find(opt => !opt.disabled);
+ if (firstValid) firstValid.selected = true;
+ }
+ });
+ }
+ };
+
+ // Start initialization when DOM is ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeBoardSelector);
+ } else {
+ initializeBoardSelector();
+ }
+})();
diff --git a/doc/_scripts/gen_boards_catalog.py b/doc/_scripts/gen_boards_catalog.py
index fdce5f0777f..02075e2c5d5 100755
--- a/doc/_scripts/gen_boards_catalog.py
+++ b/doc/_scripts/gen_boards_catalog.py
@@ -325,6 +325,7 @@ def get_catalog(generate_hw_features=False):
"vendor": vendor,
"archs": list(archs),
"socs": list(socs),
+ "revision_default": board.revision_default,
"supported_features": supported_features,
"image": guess_image(board),
}