@huaweidevtools/aishell-dsh-brand 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -26,6 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  //#endregion
27
27
  const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
28
28
  const react = __toESM(require("react"));
29
+ const react_dom = __toESM(require("react-dom"));
29
30
 
30
31
  //#region \0dsh-css:/home/eushing/opensource/aishell-dsh-plugins/packages/aishell-brand/src/client/brand.module.css.mjs
31
32
  const css = ".NP1U9q_mark{background:url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDQgNDQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI0NC4wMDAwMDAiIGhlaWdodD0iNDQuMDAwMDAwIiBmaWxsPSJub25lIj4KCTxkZWZzPgoJCTxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnRfbGluZWFyXzAiIHgxPSIzOC41IiB4Mj0iMi4xOTk5OTY5NSIgeTE9IjEuOTA3MzQ4NjNlLTA2IiB5Mj0iMzkuNTk5OTkwOCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgoJCQk8c3RvcCBzdG9wLWNvbG9yPSJyZ2IoNTMsMTM3LDI1NSkiIG9mZnNldD0iMCIgc3RvcC1vcGFjaXR5PSIxIiAvPgoJCQk8c3RvcCBzdG9wLWNvbG9yPSJyZ2IoMTY5LDM4LDI1NSkiIG9mZnNldD0iMSIgc3RvcC1vcGFjaXR5PSIxIiAvPgoJCTwvbGluZWFyR3JhZGllbnQ+Cgk8L2RlZnM+Cgk8cmVjdCBpZD0iaWNvbl9BSVNoZWxsIiB3aWR0aD0iNDQuMDAwMDAwIiBoZWlnaHQ9IjQ0LjAwMDAwMCIgeD0iMC4wMDAwMDAiIHk9IjAuMDAwMDAwIiBmaWxsPSJyZ2IoMjU1LDI1NSwyNTUpIiBmaWxsLW9wYWNpdHk9IjAiIC8+Cgk8cGF0aCBpZD0i5ZCI5bm2IiBkPSJNMzcuNDU3NiAwLjI5OTczOEMzNy4zNzc1IDAuMTM5ODc3IDM3LjI4NjEgMC4wNDI4MTk2IDM3LjE4MzIgMC4wMDg1NjM5M0MzNy4wOTE3IC0wLjAxNDI3MzIgMzcuMDA2IDAuMDA4NTYzOTQgMzYuOTI1OSAwLjA3NzA3NTRDMzYuODQ1OSAwLjE0NTU4NyAzNi43ODg3IDAuMjE5ODA4IDM2Ljc1NDUgMC4yOTk3MzhDMzYuNDU3MiAxLjg2NDA4IDM1LjczNjkgMy4yMTcxOCAzNC41OTM3IDQuMzU5MDRDMzMuNDYxOCA1LjQ4OTQ4IDMyLjA5NTYgNi4yMjAyNyAzMC40OTUgNi41NTE0MUMzMC4zOTIxIDYuNTc0MjQgMzAuMzE3OCA2LjY0Mjc1IDMwLjI3MiA2Ljc1Njk0QzMwLjIzNzcgNi44NzExMyAzMC4yNDM1IDYuOTczODkgMzAuMjg5MiA3LjA2NTI0QzMwLjM0NjQgNy4xNTY1OSAzMC40MTUgNy4yMDIyNiAzMC40OTUgNy4yMDIyNkMzMS4yMDc4IDcuMzUyOTYgMzEuODc3OSA3LjU4NzU5IDMyLjUwNTUgNy45MDYxOEwzMi41MDU1IDcuOTA2MkwzMi41MDU2IDcuOTA2MjNDMzMuMjU3IDguMjg3NzEgMzMuOTQ3MyA4Ljc4OTU1IDM0LjU3NjUgOS40MTE3NkMzNS43MzEyIDEwLjU0MjIgMzYuNDU3MiAxMS44ODk2IDM2Ljc1NDUgMTMuNDUzOUMzNi44MTE2IDEzLjU5MSAzNi44OTc0IDEzLjY4MjMgMzcuMDExNyAxMy43MjhDMzcuMTE0NiAxMy43NjIyIDM3LjIwNjEgMTMuNzU2NSAzNy4yODYxIDEzLjcxMDlDMzcuMzc3NSAxMy42NTM4IDM3LjQzNDcgMTMuNTY4MSAzNy40NTc2IDEzLjQ1MzlDMzcuNzg5MSAxMS44ODk2IDM4LjUxNTEgMTAuNTQyMiAzOS42MzU1IDkuNDExNzZDNDAuNzY3NCA4LjI2OTkgNDIuMTI3OSA3LjUzMzQgNDMuNzE3IDcuMjAyMjZDNDMuODE5OSA3LjE3OTQzIDQzLjkgNy4xMjIzMyA0My45NTcxIDcuMDMwOTlDNDQuMDE0MyA2LjkyODIyIDQ0LjAxNDMgNi44MzExNiA0My45NTcxIDYuNzM5ODFDNDMuOSA2LjYzNzA1IDQzLjgxOTkgNi41NzQyNCA0My43MTcgNi41NTE0MUM0Mi4xNTA3IDYuMjIwMjcgNDAuNzk1OSA1LjQ4OTQ4IDM5LjY1MjcgNC4zNTkwNEMzOC41MjA4IDMuMjE3MTggMzcuNzg5MSAxLjg2NDA4IDM3LjQ1NzYgMC4yOTk3MzhaTTQyLjYyNSAxNi40OTk5TDQyLjYyNSAzM0M0Mi42MjUgMzMuOTQ5MyA0Mi40NTcyIDM0LjgyOSA0Mi4xMjE2IDM1LjYzOTJDNDEuNzg2IDM2LjQ0OTUgNDEuMjgyNiAzNy4xOTAyIDQwLjYxMTQgMzcuODYxNEMzOS45NDAyIDM4LjUzMjYgMzkuMTk5NCAzOS4wMzYgMzguMzg5MiAzOS4zNzE2QzM3LjU3OSAzOS43MDcyIDM2LjY5OTIgMzkuODc1IDM1Ljc1IDM5Ljg3NUwxMC45OTk5IDM5Ljg3NUw4LjI0OTk1IDM5Ljg3NUM3LjMwMDczIDM5Ljg3NSA2LjQyMTAyIDM5LjcwNzIgNS42MTA4MSAzOS4zNzE2QzQuODAwNTggMzkuMDM2IDQuMDU5ODUgMzguNTMyNiAzLjM4ODYzIDM3Ljg2MTRDMi43MTc0MiAzNy4xOTAyIDIuMjE0MDIgMzYuNDQ5NCAxLjg3ODQxIDM1LjYzOTJDMS41NDI4IDM0LjgyOSAxLjM3NSAzMy45NDkzIDEuMzc1IDMzTDEuMzc1IDExQzEuMzc1IDEwLjA1MDggMS41NDI4IDkuMTcxMDYgMS44Nzg0IDguMzYwODNMMS44Nzg0MSA4LjM2MDgyQzIuMjE0MDIgNy41NTA1OSAyLjcxNzQzIDYuODA5ODcgMy4zODg2NCA2LjEzODY2QzQuMDU5ODUgNS40Njc0NSA0LjgwMDU3IDQuOTY0MDQgNS42MTA3OSA0LjYyODQzTDUuNjEwOCA0LjYyODQzQzYuNDIxMDMgNC4yOTI4MiA3LjMwMDc2IDQuMTI1MDIgOC4yNSA0LjEyNTAyTDI3LjQ5OTkgNC4xMjVDMjguMjY5OSA0LjEyNSAyOC44NzQ5IDQuNzMgMjguODc0OSA1LjVDMjguODc0OSA2LjI3IDI4LjI2OTkgNi44NzUgMjcuNDk5OSA2Ljg3NUw4LjI1IDYuODc1MDJDNy42ODA0NSA2Ljg3NTAyIDcuMTUyNjEgNi45NzU3IDYuNjY2NDggNy4xNzcwNkM2LjE4MDM0IDcuMzc4NDMgNS43MzU5MSA3LjY4MDQ3IDUuMzMzMTggOC4wODMyQzQuOTMwNDYgOC40ODU5MyA0LjYyODQxIDguOTMwMzYgNC40MjcwNSA5LjQxNjQ5QzQuMjI1NjggOS45MDI2MyA0LjEyNSAxMC40MzA1IDQuMTI1IDExTDQuMTI1IDMzQzQuMTI1IDMzLjU2OTYgNC4yMjU2OCAzNC4wOTc0IDQuNDI3MDUgMzQuNTgzNUM0LjYyODQxIDM1LjA2OTcgNC45MzA0NSAzNS41MTQxIDUuMzMzMTcgMzUuOTE2OEM1LjczNTg4IDM2LjMxOTUgNi4xODAyOCAzNi42MjE2IDYuNjY2MzkgMzYuODIyOUM3LjE1MjU2IDM3LjAyNDMgNy42ODA0MiAzNy4xMjUgOC4yNDk5OSAzNy4xMjVMMTAuOTk5OSAzNy4xMjVMMzUuNzUgMzcuMTI1QzM2LjMxOTUgMzcuMTI1IDM2Ljg0NzQgMzcuMDI0MyAzNy4zMzM1IDM2LjgyM0MzNy44MTk3IDM2LjYyMTYgMzguMjY0MSAzNi4zMTk2IDM4LjY2NjggMzUuOTE2OEMzOS4wNjk1IDM1LjUxNDEgMzkuMzcxNiAzNS4wNjk3IDM5LjU3MyAzNC41ODM1QzM5Ljc3NDMgMzQuMDk3NCAzOS44NzUgMzMuNTY5NiAzOS44NzUgMzNMMzkuODc1IDE2LjQ5OTlDMzkuODc1IDE1LjcyOTkgNDAuNDggMTUuMTI0OSA0MS4yNSAxNS4xMjQ5QzQyLjAyIDE1LjEyNDkgNDIuNjI1IDE1LjcyOTkgNDIuNjI1IDE2LjQ5OTlaTTEwLjAyNzcgMTIuNzc3N0MxMC41NzIyIDEyLjIzMzMgMTEuNDI3OCAxMi4yMzMzIDExLjk3MjMgMTIuNzc3N0wyMC4yMjIyIDIxLjAyNzdDMjAuNDkwNyAyMS4yOTYyIDIwLjYyNDkgMjEuNjIwMyAyMC42MjQ5IDIyQzIwLjYyNDkgMjIuMzc5NyAyMC40OTA3IDIyLjcwMzcgMjAuMjIyMiAyMi45NzIyTDExLjk3MjIgMzEuMjIyM0MxMS40Mjc3IDMxLjc2NjggMTAuNTcyMSAzMS43NjY4IDEwLjAyNzcgMzEuMjIyM0M5LjQ4MzE4IDMwLjY3NzkgOS40ODMxNyAyOS44MjIzIDEwLjAyNzYgMjkuMjc3OEwxNy4zMDU0IDIyTDEwLjAyNzcgMTQuNzIyM0M5LjQ4MzI1IDE0LjE3NzggOS40ODMyNiAxMy4zMjIyIDEwLjAyNzcgMTIuNzc3N1pNMjAuNjI1IDI4Ljg3NUMyMC42MjUgMjguMTA1IDIxLjIzIDI3LjUgMjIgMjcuNUwzMyAyNy41QzMzLjc3IDI3LjUgMzQuMzc1IDI4LjEwNSAzNC4zNzUgMjguODc1QzM0LjM3NSAyOS42NDUgMzMuNzcgMzAuMjUgMzMgMzAuMjVMMjIgMzAuMjVDMjEuMjMgMzAuMjUgMjAuNjI1IDI5LjY0NSAyMC42MjUgMjguODc1WiIgZmlsbD0icmdiKDAsMCwwKSIgZmlsbC1vcGFjaXR5PSIwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIC8+Cgk8cGF0aCBpZD0i5ZCI5bm2IiBkPSJNMzcuNDU3NiAwLjI5OTczOEMzNy4zNzc1IDAuMTM5ODc3IDM3LjI4NjEgMC4wNDI4MTk2IDM3LjE4MzIgMC4wMDg1NjM5M0MzNy4wOTE3IC0wLjAxNDI3MzIgMzcuMDA2IDAuMDA4NTYzOTQgMzYuOTI1OSAwLjA3NzA3NTRDMzYuODQ1OSAwLjE0NTU4NyAzNi43ODg3IDAuMjE5ODA4IDM2Ljc1NDUgMC4yOTk3MzhDMzYuNDU3MiAxLjg2NDA4IDM1LjczNjkgMy4yMTcxOCAzNC41OTM3IDQuMzU5MDRDMzMuNDYxOCA1LjQ4OTQ4IDMyLjA5NTYgNi4yMjAyNyAzMC40OTUgNi41NTE0MUMzMC4zOTIxIDYuNTc0MjQgMzAuMzE3OCA2LjY0Mjc1IDMwLjI3MiA2Ljc1Njk0QzMwLjIzNzcgNi44NzExMyAzMC4yNDM1IDYuOTczODkgMzAuMjg5MiA3LjA2NTI0QzMwLjM0NjQgNy4xNTY1OSAzMC40MTUgNy4yMDIyNiAzMC40OTUgNy4yMDIyNkMzMS4yMDc4IDcuMzUyOTYgMzEuODc3OSA3LjU4NzU5IDMyLjUwNTUgNy45MDYxOEwzMi41MDU1IDcuOTA2MkwzMi41MDU2IDcuOTA2MjNDMzMuMjU3IDguMjg3NzEgMzMuOTQ3MyA4Ljc4OTU1IDM0LjU3NjUgOS40MTE3NkMzNS43MzEyIDEwLjU0MjIgMzYuNDU3MiAxMS44ODk2IDM2Ljc1NDUgMTMuNDUzOUMzNi44MTE2IDEzLjU5MSAzNi44OTc0IDEzLjY4MjMgMzcuMDExNyAxMy43MjhDMzcuMTE0NiAxMy43NjIyIDM3LjIwNjEgMTMuNzU2NSAzNy4yODYxIDEzLjcxMDlDMzcuMzc3NSAxMy42NTM4IDM3LjQzNDcgMTMuNTY4MSAzNy40NTc2IDEzLjQ1MzlDMzcuNzg5MSAxMS44ODk2IDM4LjUxNTEgMTAuNTQyMiAzOS42MzU1IDkuNDExNzZDNDAuNzY3NCA4LjI2OTkgNDIuMTI3OSA3LjUzMzQgNDMuNzE3IDcuMjAyMjZDNDMuODE5OSA3LjE3OTQzIDQzLjkgNy4xMjIzMyA0My45NTcxIDcuMDMwOTlDNDQuMDE0MyA2LjkyODIyIDQ0LjAxNDMgNi44MzExNiA0My45NTcxIDYuNzM5ODFDNDMuOSA2LjYzNzA1IDQzLjgxOTkgNi41NzQyNCA0My43MTcgNi41NTE0MUM0Mi4xNTA3IDYuMjIwMjcgNDAuNzk1OSA1LjQ4OTQ4IDM5LjY1MjcgNC4zNTkwNEMzOC41MjA4IDMuMjE3MTggMzcuNzg5MSAxLjg2NDA4IDM3LjQ1NzYgMC4yOTk3MzhaTTQyLjYyNSAxNi40OTk5TDQyLjYyNSAzM0M0Mi42MjUgMzMuOTQ5MyA0Mi40NTcyIDM0LjgyOSA0Mi4xMjE2IDM1LjYzOTJDNDEuNzg2IDM2LjQ0OTUgNDEuMjgyNiAzNy4xOTAyIDQwLjYxMTQgMzcuODYxNEMzOS45NDAyIDM4LjUzMjYgMzkuMTk5NCAzOS4wMzYgMzguMzg5MiAzOS4zNzE2QzM3LjU3OSAzOS43MDcyIDM2LjY5OTIgMzkuODc1IDM1Ljc1IDM5Ljg3NUwxMC45OTk5IDM5Ljg3NUw4LjI0OTk1IDM5Ljg3NUM3LjMwMDczIDM5Ljg3NSA2LjQyMTAyIDM5LjcwNzIgNS42MTA4MSAzOS4zNzE2QzQuODAwNTggMzkuMDM2IDQuMDU5ODUgMzguNTMyNiAzLjM4ODYzIDM3Ljg2MTRDMi43MTc0MiAzNy4xOTAyIDIuMjE0MDIgMzYuNDQ5NCAxLjg3ODQxIDM1LjYzOTJDMS41NDI4IDM0LjgyOSAxLjM3NSAzMy45NDkzIDEuMzc1IDMzTDEuMzc1IDExQzEuMzc1IDEwLjA1MDggMS41NDI4IDkuMTcxMDYgMS44Nzg0IDguMzYwODNMMS44Nzg0MSA4LjM2MDgyQzIuMjE0MDIgNy41NTA1OSAyLjcxNzQzIDYuODA5ODcgMy4zODg2NCA2LjEzODY2QzQuMDU5ODUgNS40Njc0NSA0LjgwMDU3IDQuOTY0MDQgNS42MTA3OSA0LjYyODQzTDUuNjEwOCA0LjYyODQzQzYuNDIxMDMgNC4yOTI4MiA3LjMwMDc2IDQuMTI1MDIgOC4yNSA0LjEyNTAyTDI3LjQ5OTkgNC4xMjVDMjguMjY5OSA0LjEyNSAyOC44NzQ5IDQuNzMgMjguODc0OSA1LjVDMjguODc0OSA2LjI3IDI4LjI2OTkgNi44NzUgMjcuNDk5OSA2Ljg3NUw4LjI1IDYuODc1MDJDNy42ODA0NSA2Ljg3NTAyIDcuMTUyNjEgNi45NzU3IDYuNjY2NDggNy4xNzcwNkM2LjE4MDM0IDcuMzc4NDMgNS43MzU5MSA3LjY4MDQ3IDUuMzMzMTggOC4wODMyQzQuOTMwNDYgOC40ODU5MyA0LjYyODQxIDguOTMwMzYgNC40MjcwNSA5LjQxNjQ5QzQuMjI1NjggOS45MDI2MyA0LjEyNSAxMC40MzA1IDQuMTI1IDExTDQuMTI1IDMzQzQuMTI1IDMzLjU2OTYgNC4yMjU2OCAzNC4wOTc0IDQuNDI3MDUgMzQuNTgzNUM0LjYyODQxIDM1LjA2OTcgNC45MzA0NSAzNS41MTQxIDUuMzMzMTcgMzUuOTE2OEM1LjczNTg4IDM2LjMxOTUgNi4xODAyOCAzNi42MjE2IDYuNjY2MzkgMzYuODIyOUM3LjE1MjU2IDM3LjAyNDMgNy42ODA0MiAzNy4xMjUgOC4yNDk5OSAzNy4xMjVMMTAuOTk5OSAzNy4xMjVMMzUuNzUgMzcuMTI1QzM2LjMxOTUgMzcuMTI1IDM2Ljg0NzQgMzcuMDI0MyAzNy4zMzM1IDM2LjgyM0MzNy44MTk3IDM2LjYyMTYgMzguMjY0MSAzNi4zMTk2IDM4LjY2NjggMzUuOTE2OEMzOS4wNjk1IDM1LjUxNDEgMzkuMzcxNiAzNS4wNjk3IDM5LjU3MyAzNC41ODM1QzM5Ljc3NDMgMzQuMDk3NCAzOS44NzUgMzMuNTY5NiAzOS44NzUgMzNMMzkuODc1IDE2LjQ5OTlDMzkuODc1IDE1LjcyOTkgNDAuNDggMTUuMTI0OSA0MS4yNSAxNS4xMjQ5QzQyLjAyIDE1LjEyNDkgNDIuNjI1IDE1LjcyOTkgNDIuNjI1IDE2LjQ5OTlaTTEwLjAyNzcgMTIuNzc3N0MxMC41NzIyIDEyLjIzMzMgMTEuNDI3OCAxMi4yMzMzIDExLjk3MjMgMTIuNzc3N0wyMC4yMjIyIDIxLjAyNzdDMjAuNDkwNyAyMS4yOTYyIDIwLjYyNDkgMjEuNjIwMyAyMC42MjQ5IDIyQzIwLjYyNDkgMjIuMzc5NyAyMC40OTA3IDIyLjcwMzcgMjAuMjIyMiAyMi45NzIyTDExLjk3MjIgMzEuMjIyM0MxMS40Mjc3IDMxLjc2NjggMTAuNTcyMSAzMS43NjY4IDEwLjAyNzcgMzEuMjIyM0M5LjQ4MzE4IDMwLjY3NzkgOS40ODMxNyAyOS44MjIzIDEwLjAyNzYgMjkuMjc3OEwxNy4zMDU0IDIyTDEwLjAyNzcgMTQuNzIyM0M5LjQ4MzI1IDE0LjE3NzggOS40ODMyNiAxMy4zMjIyIDEwLjAyNzcgMTIuNzc3N1pNMjAuNjI1IDI4Ljg3NUMyMC42MjUgMjguMTA1IDIxLjIzIDI3LjUgMjIgMjcuNUwzMyAyNy41QzMzLjc3IDI3LjUgMzQuMzc1IDI4LjEwNSAzNC4zNzUgMjguODc1QzM0LjM3NSAyOS42NDUgMzMuNzcgMzAuMjUgMzMgMzAuMjVMMjIgMzAuMjVDMjEuMjMgMzAuMjUgMjAuNjI1IDI5LjY0NSAyMC42MjUgMjguODc1WiIgZmlsbD0idXJsKCNwYWludF9saW5lYXJfMCkiIGZpbGwtcnVsZT0iZXZlbm9kZCIgLz4KPC9zdmc+Cg==) 50%/contain no-repeat;display:inline-block}.NP1U9q_name{vertical-align:middle;align-items:center;min-width:0;height:24px;display:inline-flex}.NP1U9q_name:before{content:\"AI Shell\";letter-spacing:.04em;color:var(--dsw-alias-label-primary,#1a1a1a);white-space:nowrap;font-size:18px;font-weight:600;line-height:24px}";
@@ -277,6 +278,7 @@ const SUGGESTIONS = [
277
278
  {
278
279
  id: "inventory",
279
280
  icon: "cloud",
281
+ hue: { tile: "var(--dsw-static-blue-400)" },
280
282
  titleKey: "suggestion.inventory.title",
281
283
  glossKey: "suggestion.inventory.gloss",
282
284
  promptKey: "suggestion.inventory.prompt"
@@ -284,6 +286,7 @@ const SUGGESTIONS = [
284
286
  {
285
287
  id: "deploy",
286
288
  icon: "deploy",
289
+ hue: { tile: "var(--dsw-static-green-500)" },
287
290
  titleKey: "suggestion.deploy.title",
288
291
  glossKey: "suggestion.deploy.gloss",
289
292
  promptKey: "suggestion.deploy.prompt"
@@ -291,6 +294,7 @@ const SUGGESTIONS = [
291
294
  {
292
295
  id: "enterprise",
293
296
  icon: "architecture",
297
+ hue: { tile: "rgb(139, 118, 246)" },
294
298
  titleKey: "suggestion.enterprise.title",
295
299
  glossKey: "suggestion.enterprise.gloss",
296
300
  promptKey: "suggestion.enterprise.prompt"
@@ -298,6 +302,7 @@ const SUGGESTIONS = [
298
302
  {
299
303
  id: "billing",
300
304
  icon: "cost",
305
+ hue: { tile: "var(--dsw-static-amber-600)" },
301
306
  titleKey: "suggestion.billing.title",
302
307
  glossKey: "suggestion.billing.gloss",
303
308
  promptKey: "suggestion.billing.prompt"
@@ -483,11 +488,290 @@ function ringStep(pos, delta, n) {
483
488
  };
484
489
  }
485
490
 
491
+ //#endregion
492
+ //#region src/client/autoplay.ts
493
+ /**
494
+ * Auto-rotation policy for the example-prompt strip.
495
+ *
496
+ * Kept apart from the component and free of DOM, for the same reason
497
+ * `carousel.ts` is: the DECISION is what needs pinning down, and a decision
498
+ * buried in an effect can only be checked by watching a browser.
499
+ *
500
+ * ## Why the strip advances on its own
501
+ *
502
+ * The strip shows three of four cards, so a card is always off screen and a
503
+ * reader who never touches the arrows never learns it exists. Rotation is what
504
+ * makes the set self-evident. The cost is that content moves without being
505
+ * asked to, which is why every reason to stop is collected here rather than
506
+ * decided piecemeal at the call site.
507
+ *
508
+ * ## Every reason to stop
509
+ *
510
+ * - `engaged` — the pointer is over the strip or focus is inside it. Someone
511
+ * reaching for a card must not have it slide out from under the cursor;
512
+ * this is the pause the feature was asked for, and `focus` is the half of
513
+ * it that covers a reader who never uses a pointer.
514
+ * - `reducedMotion` — the reader asked the platform for less movement. An
515
+ * element that moves by itself for more than five seconds is exactly what
516
+ * `prefers-reduced-motion` exists to suppress, and a hover pause cannot help
517
+ * the reader who cannot hover. Honouring it is what keeps the feature from
518
+ * being motion no one consented to.
519
+ * - `visible` — the tab is hidden. A background tab still fires timers, so
520
+ * without this the strip would churn through slides nobody can see and land
521
+ * somewhere arbitrary on return.
522
+ * - `ready` — the ring has not been measured yet. Advancing before the step
523
+ * is known would move the track by a wrong distance; see the measurement
524
+ * note in `SuggestionCards.tsx`.
525
+ * - `ring` — three cards or fewer all fit, so there is nowhere to advance to.
526
+ * A strip with nothing to scroll must not appear to scroll.
527
+ *
528
+ * @module @huaweidevtools/aishell-dsh-brand/client/autoplay
529
+ */
530
+ /**
531
+ * How long each card is shown before the strip advances.
532
+ *
533
+ * Fixed rather than configurable: this is a hint at what the product can do,
534
+ * not a reading surface, and a setting nobody asked for is a setting nobody
535
+ * finds.
536
+ */
537
+ const AUTOPLAY_INTERVAL_MS = 1500;
538
+ /**
539
+ * Whether the strip should be advancing on its own right now.
540
+ *
541
+ * Expressed as one predicate rather than a chain of early returns inside the
542
+ * timer, so that adding a reason to stop is a change to this list and its
543
+ * test — not a change to the effect that schedules the ticks.
544
+ */
545
+ function autoplayActive(inputs) {
546
+ return inputs.ring && inputs.ready && !inputs.engaged && !inputs.reducedMotion && inputs.visible;
547
+ }
548
+
549
+ //#endregion
550
+ //#region src/client/hero-content.ts
551
+ /**
552
+ * The mount point the suggestion strip renders into, inside the hero.
553
+ *
554
+ * ## Why a portal, and not CSS placement
555
+ *
556
+ * The strip is a `conversation.input.dock` entry, and that band renders BELOW
557
+ * the hero workspace row in the composer stack. Measured on the live page
558
+ * (1280×900, DSH 0.2.x):
559
+ *
560
+ * hero (headline) y=288 h=74
561
+ * hero workspace row y=374 h=28
562
+ * conversation.input.dock y=411 h=80
563
+ * the input card y=491
564
+ *
565
+ * The product wants the cards directly under the headline, and the workspace
566
+ * row sits between. A negative margin cannot cross it — the row would be
567
+ * covered — and the row's own subtree is not reachable from the dock, since
568
+ * the two are separate children of the composer stack.
569
+ *
570
+ * The hero, however, already renders an empty content div for exactly this
571
+ * purpose. Its stack is: headline → (the "Powered by" label) → `_body`, where
572
+ * `_body` is `flex-direction: column` and left empty by the official shell.
573
+ * That is the hero's content area, and putting the cards there is using the
574
+ * slot as intended rather than fighting the layout.
575
+ *
576
+ * ## How the mount is found
577
+ *
578
+ * By the hero's own class suffix, from the headline outward — `[class*="_body"]`
579
+ * alone is ambiguous (the conversation root has a `_body` too, and the live
580
+ * page has two). So the search starts at a `[class*="_headline"]`, steps to its
581
+ * parent (the hero stack), and takes that stack's own `_body` child. The
582
+ * headline is unique on the page and is what makes the walk unambiguous.
583
+ *
584
+ * Children are walked directly rather than with `:scope >`. The two are
585
+ * equivalent here, and the direct walk keeps these helpers working against the
586
+ * hand-rolled DOM stub the specs use (which has no selector engine) instead of
587
+ * leaving the module's real logic untested.
588
+ *
589
+ * The classes are CSS-module hashes (`pXSMma_body`), hence the suffix match.
590
+ * A base bump that renames them fails closed: {@link findHeroContent} returns
591
+ * null, the portal renders nothing, and the arrows and cards simply do not
592
+ * appear — the strip is never left in the wrong parent.
593
+ *
594
+ * @module @huaweidevtools/aishell-dsh-brand/client/hero-content
595
+ */
596
+ /** The first direct child whose class contains `fragment`. */
597
+ function childWithClass(parent, fragment) {
598
+ for (const child of Array.from(parent.children)) if (child.className.includes(fragment)) return child;
599
+ return null;
600
+ }
601
+ /** The first direct child carrying `data-<name>`. */
602
+ function childWithData(parent, attribute) {
603
+ for (const child of Array.from(parent.children)) if (child.hasAttribute(attribute)) return child;
604
+ return null;
605
+ }
606
+ /** The child marker, so a disposer can find exactly the node it made. */
607
+ const MOUNT_ATTRIBUTE = "data-aishell-suggestions-mount";
608
+ /**
609
+ * The hero's content area, or `null` when the hero is not on screen.
610
+ *
611
+ * Returns null rather than throwing: a New Session that has not painted its
612
+ * hero yet is a normal state (and the strip retries on the next frame), not an
613
+ * error.
614
+ */
615
+ function findHeroContent(target) {
616
+ if (typeof target.querySelector !== "function") return null;
617
+ const headline = target.querySelector("[class*=\"_headline\"]");
618
+ const stack = headline?.parentElement ?? null;
619
+ if (stack === null) return null;
620
+ return childWithClass(stack, "_body");
621
+ }
622
+ /**
623
+ * A container for the strip inside the hero content area.
624
+ *
625
+ * The hero body is `display: block` and zero-height, so the strip cannot rely
626
+ * on it for layout and must not inherit that block context. The wrapper is
627
+ * created here rather than rendered by React into the official div, because
628
+ * React does not own that node — it owns only what the portal puts inside.
629
+ */
630
+ function ensureHeroMount(content) {
631
+ const existing = childWithData(content, MOUNT_ATTRIBUTE);
632
+ if (existing !== null) return existing;
633
+ const mount = content.ownerDocument.createElement("div");
634
+ mount.setAttribute(MOUNT_ATTRIBUTE, "");
635
+ content.appendChild(mount);
636
+ return mount;
637
+ }
638
+ /**
639
+ * Remove the mount this module created, if it is still there.
640
+ *
641
+ * Only the mount is removed — never the host div, which belongs to the shell
642
+ * and is reused on the next New Session.
643
+ */
644
+ function removeHeroMount(content) {
645
+ childWithData(content, MOUNT_ATTRIBUTE)?.remove();
646
+ }
647
+
486
648
  //#endregion
487
649
  //#region src/client/SuggestionCards.tsx
488
650
  /** How many cards a full-width strip shows at once. */
489
651
  const VISIBLE = 3;
490
- /** One card. */
652
+ /**
653
+ * How much of a card's hue is washed into its own background, in percent.
654
+ *
655
+ * The card's ground is the shell's translucent interactive pair (neutral), and
656
+ * this mixes the card's hue into BOTH ends of that pair so a card carries a
657
+ * trace of its tile's colour without the hover turning into a colour change.
658
+ *
659
+ * 6 is measured rather than picked. Three things constrain it, and the numbers
660
+ * below are computed from the real tokens in both themes:
661
+ *
662
+ * - LEGIBILITY. The gloss text is the binding constraint, not the title. On
663
+ * the light theme's ground the gloss measures 3.33 at 0%, 3.14 at 8%, and
664
+ * falls to 2.90 at 14% — under the 3:1 bar. So the ceiling is ~12%.
665
+ * - VISIBILITY. A tint is not worth its cost below the just-noticeable
666
+ * difference (~2.3 dE). The card moves 3.4–5.1 dE away from its own
667
+ * untinted self at 6%, and only 1.7 dE at 3%, so the floor is ~4%.
668
+ * - THE TILE'S SEPARATION. The tile sits ON this ground, so tinting the
669
+ * ground eats the tile's contrast. Amber is the tightest case: 2.51 at 0%,
670
+ * 2.39 at 6%, 2.31 at 10%. It survives, but this is why the percentage
671
+ * cannot be pushed for looks.
672
+ *
673
+ * 6% is the low end of the safe band, chosen deliberately: the tint is there to
674
+ * warm the card toward its subject, not to become a colour block. It is not a
675
+ * way to tell the cards apart — at 6% two different cards are only 2.4 dE
676
+ * apart, barely at the threshold. The tile is what identifies a card.
677
+ */
678
+ const HUE_WASH = 6;
679
+ /**
680
+ * The hero's content element, kept current across the hero's lifecycle.
681
+ *
682
+ * The hero mounts, unmounts and is REPLACED on each New Session, so the node
683
+ * found at first paint goes stale. A `MutationObserver` is the only thing that
684
+ * sees a replacement: the observer re-scans, and the scan is cheap because it
685
+ * returns immediately while the current mount is still connected.
686
+ *
687
+ * @param active - whether the strip should be mounted at all.
688
+ * @returns the element to portal into, or `null` while the hero is absent.
689
+ */
690
+ function useHeroMount(active) {
691
+ const [mount, setMount] = (0, react.useState)(null);
692
+ const mountRef = (0, react.useRef)(null);
693
+ (0, react.useEffect)(() => {
694
+ if (!active) {
695
+ setMount(null);
696
+ return;
697
+ }
698
+ let cancelled = false;
699
+ const scan = () => {
700
+ if (cancelled) return;
701
+ const current = mountRef.current;
702
+ if (current !== null && current.isConnected) {
703
+ setMount((seen) => seen === current ? seen : current);
704
+ return;
705
+ }
706
+ const content = findHeroContent(document);
707
+ if (content === null) {
708
+ mountRef.current = null;
709
+ setMount(null);
710
+ return;
711
+ }
712
+ const next = ensureHeroMount(content);
713
+ if (next !== mountRef.current) {
714
+ mountRef.current = next;
715
+ setMount(next);
716
+ }
717
+ };
718
+ scan();
719
+ const observer = new MutationObserver(scan);
720
+ observer.observe(document.body, {
721
+ childList: true,
722
+ subtree: true
723
+ });
724
+ return () => {
725
+ cancelled = true;
726
+ observer.disconnect();
727
+ };
728
+ }, [active]);
729
+ (0, react.useEffect)(() => () => {
730
+ const current = mountRef.current;
731
+ mountRef.current = null;
732
+ if (current === null) return;
733
+ removeHeroMount(current.parentElement ?? current);
734
+ }, []);
735
+ return mount;
736
+ }
737
+ /** Whether the reader has asked the platform for reduced motion. */
738
+ function useReducedMotion() {
739
+ const [reduced, setReduced] = (0, react.useState)(false);
740
+ (0, react.useEffect)(() => {
741
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
742
+ setReduced(query.matches);
743
+ const onChange = (event) => {
744
+ setReduced(event.matches);
745
+ };
746
+ query.addEventListener("change", onChange);
747
+ return () => {
748
+ query.removeEventListener("change", onChange);
749
+ };
750
+ }, []);
751
+ return reduced;
752
+ }
753
+ /** Whether the tab is on screen. A hidden tab must not churn through slides. */
754
+ function useTabVisible() {
755
+ const [visible, setVisible] = (0, react.useState)(true);
756
+ (0, react.useEffect)(() => {
757
+ const onVisibility = () => {
758
+ setVisible(document.visibilityState === "visible");
759
+ };
760
+ onVisibility();
761
+ document.addEventListener("visibilitychange", onVisibility);
762
+ return () => {
763
+ document.removeEventListener("visibilitychange", onVisibility);
764
+ };
765
+ }, []);
766
+ return visible;
767
+ }
768
+ /**
769
+ * One card.
770
+ *
771
+ * A button, so keyboard activation, focus handling and disabled semantics come
772
+ * for free. The icon is a decorative tile marked `aria-hidden`; the title and
773
+ * gloss carry the meaning.
774
+ */
491
775
  function Card({ suggestion, onPick, t }) {
492
776
  const [hover, setHover] = (0, react.useState)(false);
493
777
  const [focus, setFocus] = (0, react.useState)(false);
@@ -513,39 +797,50 @@ function Card({ suggestion, onPick, t }) {
513
797
  flex: `0 0 calc((100cqw - ${VISIBLE - 1} * var(--dsh-suggestion-gap)) / ${VISIBLE})`,
514
798
  display: "flex",
515
799
  alignItems: "flex-start",
516
- gap: 10,
800
+ gap: 12,
517
801
  boxSizing: "border-box",
518
802
  minWidth: 0,
519
- padding: "12px 14px",
803
+ padding: "14px",
520
804
  textAlign: "left",
521
805
  font: "inherit",
522
806
  cursor: "pointer",
523
807
  color: "var(--dsw-alias-label-primary)",
524
- background: lit ? "var(--dsw-alias-interactive-bg-hover)" : "var(--dsw-alias-bg-module-platform)",
525
- border: `1px solid ${lit ? "var(--dsw-alias-border-l2)" : "var(--dsw-alias-border-l3)"}`,
526
- borderRadius: 10,
527
- transition: "background 120ms ease, border-color 120ms ease",
808
+ background: lit ? `color-mix(in srgb, ${suggestion.hue.tile} ${HUE_WASH}%, var(--dsw-alias-interactive-bg-active))` : `color-mix(in srgb, ${suggestion.hue.tile} ${HUE_WASH}%, var(--dsw-alias-interactive-bg-hover))`,
809
+ border: "none",
810
+ borderRadius: "var(--dsw-radius-md, 12px)",
811
+ transition: "background-color 120ms ease",
528
812
  outline: focus ? "2px solid var(--dsw-alias-state-business-primary)" : "none",
529
813
  outlineOffset: 2
530
814
  },
531
815
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
816
+ "aria-hidden": true,
532
817
  style: {
533
- color: "var(--dsw-alias-label-secondary)",
534
- marginTop: 1
818
+ flex: "none",
819
+ display: "grid",
820
+ placeItems: "center",
821
+ width: 32,
822
+ height: 32,
823
+ marginTop: 1,
824
+ borderRadius: "var(--dsw-radius-sm, 8px)",
825
+ background: suggestion.hue.tile,
826
+ color: "var(--dsw-static-neutral-bluish-1000, #0f1115)"
535
827
  },
536
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SuggestionIconArt, { icon: suggestion.icon })
828
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SuggestionIconArt, {
829
+ icon: suggestion.icon,
830
+ size: 17
831
+ })
537
832
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
538
833
  style: {
539
834
  display: "flex",
540
835
  flexDirection: "column",
541
- gap: 2,
836
+ gap: 3,
542
837
  minWidth: 0
543
838
  },
544
839
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
545
840
  style: {
546
841
  fontSize: "var(--dsh-content-font-size-secondary, 13px)",
547
- fontWeight: 500,
548
- lineHeight: "20px",
842
+ fontWeight: 600,
843
+ lineHeight: "19px",
549
844
  overflow: "hidden",
550
845
  textOverflow: "ellipsis",
551
846
  whiteSpace: "nowrap"
@@ -554,7 +849,7 @@ function Card({ suggestion, onPick, t }) {
554
849
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
555
850
  style: {
556
851
  fontSize: "12px",
557
- lineHeight: "16px",
852
+ lineHeight: "17px",
558
853
  color: "var(--dsw-alias-label-tertiary)",
559
854
  display: "-webkit-box",
560
855
  WebkitLineClamp: 2,
@@ -620,8 +915,40 @@ function SuggestionCards(props) {
620
915
  const trackRef = (0, react.useRef)(null);
621
916
  const posRef = (0, react.useRef)(ringStart(n));
622
917
  const stepRef = (0, react.useRef)(0);
623
- const [animating, setAnimating] = (0, react.useState)(true);
624
918
  const [ready, setReady] = (0, react.useState)(!ring);
919
+ const [engaged, setEngaged] = (0, react.useState)(false);
920
+ const reducedMotion = useReducedMotion();
921
+ const visible = useTabVisible();
922
+ const mount = useHeroMount(blank);
923
+ /**
924
+ * The slide's own animation, written by this component and nobody else.
925
+ *
926
+ * `transform` and `transition` are BOTH owned by the imperative code below
927
+ * and are deliberately absent from the `style` prop further down. They have
928
+ * to have exactly one owner: a value declared in JSX is only re-applied when
929
+ * React sees it CHANGE, so the first time the effect writes
930
+ * `track.style.transition = 'none'` (or clears it with `''`), React's
931
+ * declared value is gone and React has no reason to write it again. The
932
+ * track then keeps the browser default — `all 0s` — and every "slide" lands
933
+ * instantly with a hard cut. That is a silent failure: positions, wrapping
934
+ * and the ring all still measure correct, so only watching the motion
935
+ * reveals it.
936
+ *
937
+ * A slow, eased curve rather than a short linear one: the strip shows one
938
+ * card leaving and one arriving, and at 320ms with a snappy curve it reads
939
+ * as a jump cut rather than travel.
940
+ *
941
+ * The curve is `cubic-bezier(.33,0,.15,1)`, chosen for its END VELOCITIES
942
+ * and not its looks on a graph. Measured as distance-per-unit-time (1.0
943
+ * being a linear slide) it starts at 0.4, peaks near 2.7, and finishes at
944
+ * 0.05 — eased away from rest and brought gently to rest. The obvious
945
+ * "smooth" pick, `cubic-bezier(.16,.84,.28,1)` (a material-style fast-out),
946
+ * starts at 4.39: it covers 44% of the distance in the first 10% of the
947
+ * time and then crawls. On a row of cards that reads as a snap followed by
948
+ * a stall, which is the opposite of what a carousel wants.
949
+ */
950
+ const SLIDE_MS = 520;
951
+ const SLIDE_EASING = "cubic-bezier(.33,0,.15,1)";
625
952
  /** Place the track at a position with no transition. */
626
953
  const snapTo = (0, react.useCallback)((pos) => {
627
954
  const track = trackRef.current;
@@ -633,16 +960,24 @@ function SuggestionCards(props) {
633
960
  const slideTo = (0, react.useCallback)((pos) => {
634
961
  const track = trackRef.current;
635
962
  if (track === null) return;
636
- track.style.transition = "";
963
+ track.style.transition = `transform ${SLIDE_MS}ms ${SLIDE_EASING}`;
637
964
  track.style.transform = `translate3d(${-pos * stepRef.current}px, 0, 0)`;
638
965
  }, []);
966
+ const autoplayTimerRef = (0, react.useRef)(0);
967
+ const latestStepRef = (0, react.useRef)(() => {});
639
968
  /**
640
- * Measure one step, then place the ring at its start.
969
+ * Schedule the next auto-rotation tick one full interval from now,
970
+ * replacing any tick already pending.
641
971
  *
642
- * The step is read from a real card rather than computed from the viewport
643
- * width so the fraction in `Card`'s flex-basis and this number cannot drift
644
- * apart, and so a breakpoint change needs no constant updated here.
972
+ * Called by every `step`, which is what makes the cadence immune to a tick
973
+ * landing mid-slide.
645
974
  */
975
+ const bumpAutoplay = (0, react.useCallback)(() => {
976
+ window.clearTimeout(autoplayTimerRef.current);
977
+ autoplayTimerRef.current = window.setTimeout(() => {
978
+ latestStepRef.current(1);
979
+ }, AUTOPLAY_INTERVAL_MS);
980
+ }, []);
646
981
  /**
647
982
  * Re-measure one step and re-apply the current position.
648
983
  *
@@ -667,17 +1002,11 @@ function SuggestionCards(props) {
667
1002
  if (!(step$1 > 0)) return false;
668
1003
  const changed = step$1 !== stepRef.current;
669
1004
  stepRef.current = step$1;
670
- if (changed) {
671
- setAnimating(false);
672
- snapTo(posRef.current);
673
- window.requestAnimationFrame(() => {
674
- setAnimating(true);
675
- });
676
- }
1005
+ if (changed) snapTo(posRef.current);
677
1006
  return true;
678
1007
  }, [snapTo]);
679
1008
  (0, react.useEffect)(() => {
680
- if (!blank || !ring) return;
1009
+ if (!blank || !ring || mount === null) return;
681
1010
  let cancelled = false;
682
1011
  const settle = () => {
683
1012
  if (cancelled) return;
@@ -686,11 +1015,9 @@ function SuggestionCards(props) {
686
1015
  return;
687
1016
  }
688
1017
  posRef.current = ringStart(n);
689
- setAnimating(false);
690
1018
  snapTo(posRef.current);
691
1019
  window.requestAnimationFrame(() => {
692
1020
  if (cancelled) return;
693
- setAnimating(true);
694
1021
  setReady(true);
695
1022
  const viewport = viewportRef.current;
696
1023
  if (viewport !== null) {
@@ -712,56 +1039,85 @@ function SuggestionCards(props) {
712
1039
  blank,
713
1040
  ring,
714
1041
  n,
1042
+ mount,
715
1043
  remeasure,
716
1044
  snapTo
717
1045
  ]);
718
1046
  /**
719
- * Apply whichever rewind the settling position owes, then re-arm the
720
- * transition.
1047
+ * Apply whichever rewind the settling position owes.
1048
+ *
1049
+ * Runs when the track reaches its destination, never before the slide:
1050
+ * jumping to the twin position first would put the destination on screen
1051
+ * immediately and then animate away from it, so a click at the seam would
1052
+ * look like it skipped a card. See the module note in carousel.ts.
1053
+ *
1054
+ * The rewind lands on a position showing exactly the cards already on
1055
+ * screen, so `snapTo`'s lack of animation is invisible.
721
1056
  *
722
- * Runs on `transitionend`, never before the slide: jumping to the twin
723
- * position first would put the destination on screen immediately and then
724
- * animate away from it, so a click at the seam would look like it skipped
725
- * a card. See the module note in carousel.ts.
1057
+ * Only a COMPLETED transition reaches here: an interrupted one fires
1058
+ * `transitioncancel` instead, so there is no partial-position case to
1059
+ * filter out. A person clicking faster than a slide therefore skips this
1060
+ * handler entirely — the rewind they owe is settled by `ringStep` at the
1061
+ * start of their next press, which snaps the same invisible twin.
726
1062
  */
727
- const onSettle = (0, react.useCallback)(() => {
1063
+ const onSettle = (0, react.useCallback)((event) => {
728
1064
  if (!ring) return;
1065
+ if (event.target !== trackRef.current) return;
1066
+ if (event.propertyName !== "transform") return;
729
1067
  const owed = rewindTo(posRef.current, n);
730
1068
  if (owed === null) return;
731
1069
  posRef.current = owed;
732
- setAnimating(false);
733
1070
  snapTo(owed);
734
- window.requestAnimationFrame(() => {
735
- setAnimating(true);
736
- });
737
1071
  }, [
738
1072
  ring,
739
1073
  n,
740
1074
  snapTo
741
1075
  ]);
742
- /** One arrow press. */
1076
+ /** One step of the ring, from an arrow or from auto-rotation. */
743
1077
  const step = (0, react.useCallback)((delta) => {
744
1078
  if (!ring || !ready) return;
1079
+ bumpAutoplay();
745
1080
  const { from, to } = ringStep(posRef.current, delta, n);
746
1081
  if (from !== posRef.current) {
747
1082
  posRef.current = from;
748
- setAnimating(false);
749
1083
  snapTo(from);
750
1084
  }
751
1085
  posRef.current = to;
752
- window.requestAnimationFrame(() => {
753
- setAnimating(true);
754
- slideTo(to);
755
- });
1086
+ slideTo(to);
756
1087
  }, [
757
1088
  ring,
758
1089
  ready,
759
1090
  n,
760
1091
  snapTo,
761
- slideTo
1092
+ slideTo,
1093
+ bumpAutoplay
1094
+ ]);
1095
+ (0, react.useEffect)(() => {
1096
+ latestStepRef.current = step;
1097
+ }, [step]);
1098
+ (0, react.useEffect)(() => {
1099
+ if (!autoplayActive({
1100
+ ring,
1101
+ ready,
1102
+ engaged,
1103
+ reducedMotion,
1104
+ visible
1105
+ })) return;
1106
+ bumpAutoplay();
1107
+ return () => {
1108
+ window.clearTimeout(autoplayTimerRef.current);
1109
+ };
1110
+ }, [
1111
+ ring,
1112
+ ready,
1113
+ engaged,
1114
+ reducedMotion,
1115
+ visible,
1116
+ bumpAutoplay
762
1117
  ]);
763
1118
  if (!blank) return null;
764
1119
  if (n === 0) return null;
1120
+ if (mount === null) return null;
765
1121
  const cards = ring ? Array.from({ length: ringLength(n) }, (_, i) => SUGGESTIONS[i % n]) : SUGGESTIONS;
766
1122
  const strip = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
767
1123
  ref: viewportRef,
@@ -781,9 +1137,7 @@ function SuggestionCards(props) {
781
1137
  alignItems: "stretch",
782
1138
  gap: "var(--dsh-suggestion-gap)",
783
1139
  width: "max-content",
784
- willChange: "transform",
785
- transition: animating ? "transform 320ms cubic-bezier(.22,.61,.36,1)" : "none",
786
- transform: "translate3d(0, 0, 0)"
1140
+ willChange: "transform"
787
1141
  },
788
1142
  children: cards.map((suggestion, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
789
1143
  suggestion,
@@ -792,14 +1146,27 @@ function SuggestionCards(props) {
792
1146
  }, `${i}:${suggestion.id}`))
793
1147
  })
794
1148
  });
795
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1149
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1150
+ onPointerEnter: () => {
1151
+ setEngaged(true);
1152
+ },
1153
+ onPointerLeave: () => {
1154
+ setEngaged(false);
1155
+ },
1156
+ onFocus: () => {
1157
+ setEngaged(true);
1158
+ },
1159
+ onBlur: (event) => {
1160
+ const next = event.relatedTarget;
1161
+ if (next === null || !event.currentTarget.contains(next)) setEngaged(false);
1162
+ },
796
1163
  style: {
797
- ["--dsh-suggestion-gap"]: "8px",
1164
+ ["--dsh-suggestion-gap"]: "10px",
798
1165
  display: "flex",
799
1166
  alignItems: "center",
800
1167
  gap: 6,
801
1168
  minWidth: 0,
802
- margin: "4px 0 8px"
1169
+ margin: "12px 0 22px"
803
1170
  },
804
1171
  children: [
805
1172
  ring && ready && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Arrow, {
@@ -818,7 +1185,7 @@ function SuggestionCards(props) {
818
1185
  }
819
1186
  })
820
1187
  ]
821
- });
1188
+ }), mount);
822
1189
  }
823
1190
 
824
1191
  //#endregion
@@ -1,40 +1,88 @@
1
1
  /**
2
2
  * Example-prompt cards for the New-Session hero.
3
3
  *
4
- * ## Where this renders, and why there
4
+ * ## Where this renders, and why it takes a portal to get there
5
5
  *
6
- * `conversation.input.dock` is the band between the hero workspace row and
7
- * the input card. It is a `list` slot, so this entry composes beside
8
- * whatever else occupies the band instead of competing for a seat — the
9
- * hero row's own seats (`conversation.hero.workspace`,
10
- * `conversation.hero.agentPreset`) are `single` and already held by official
11
- * occupants, which is the trap the account chip next door documents.
6
+ * The entry occupies `conversation.input.dock`, the band between the hero
7
+ * workspace row and the input card. That is where the strip is MOUNTED — but
8
+ * not where it is DRAWN: the product wants the cards directly under the hero
9
+ * headline, and the workspace row sits between the two.
10
+ *
11
+ * Measured on the live page (1280×900):
12
+ *
13
+ * hero (headline) y=288 h=74
14
+ * hero workspace row y=374 h=28
15
+ * conversation.input.dock y=411 h=80
16
+ * the input card y=491
17
+ *
18
+ * The row cannot be crossed with a margin — it would be covered — and its DOM
19
+ * is a sibling subtree, unreachable from the dock. So the strip renders
20
+ * through `createPortal` into the hero's own content div, the empty `_body`
21
+ * the official shell leaves for exactly this. See `hero-content.ts` for how
22
+ * that mount is found and why it fails closed.
23
+ *
24
+ * The dock entry is still what supplies the slot props (`useSession`,
25
+ * `inputActions`, `t`), so the registration is unchanged; the portal only
26
+ * relocates the output. What the dock band itself renders is nothing.
12
27
  *
13
28
  * ## When it renders
14
29
  *
15
- * The band itself renders in both the New-Session hero and the docked
16
- * session composer — it only needs a live input zone — so the gate is
17
- * `session.blank`, the same condition the account chip uses. `blank` is true
18
- * from the moment a New Session opens until the first turn is accepted:
19
- * exactly the window the cards exist for. Once the first message is sent the
20
- * strip renders nothing and the composer takes over.
21
- *
22
- * ## Carousel
23
- *
24
- * Three cards or fewer sit side by side with no controls — a strip with
25
- * nothing to scroll should not offer scrolling. More than three become a
26
- * carousel: the row scrolls horizontally, arrows step it one card at a
27
- * time, and it wraps at both ends. Wrapping rather than disabling the
28
- * arrow at an end keeps the two controls symmetric (an arrow that is
29
- * sometimes dead is a control a person has to think about), and it is what
30
- * the feature was asked for.
31
- *
32
- * The arrows hold the scroll container's position in a ref and clamp it
33
- * themselves rather than reading scrollLeft back from the DOM: the read
34
- * happens before the browser has applied a pending smooth scroll, so a
35
- * second click during the animation would compute from a stale position and
36
- * step backwards. The container's own scroll event re-syncs the ref, so a
37
- * manual trackpad scroll cannot desynchronize it.
30
+ * The dock band renders in BOTH the hero and the docked composer (it only
31
+ * needs a live input zone), so the gate is `session.blank` — the same
32
+ * condition the account chip next door uses. `blank` is true from the moment a
33
+ * New Session opens until the first turn is accepted: exactly the window the
34
+ * cards exist for. Once the first message is sent the strip renders nothing
35
+ * and the composer takes over.
36
+ *
37
+ * ## Why it does not look like the input card
38
+ *
39
+ * The first version read as a second input box, and the reason was measurable
40
+ * rather than a matter of taste. Both surfaces were built from the same two
41
+ * ingredients the shell reserves for a composer:
42
+ *
43
+ * - the same neutral one step apart — the card was `bg-module-platform`
44
+ * (`#353638` in dark) and the composer `specific-input-major` (`#2c2c2e`),
45
+ * both rungs of one ramp, so the two read as siblings;
46
+ * - the same box-shadow token. `--dsw-elevation-soft` expands to
47
+ * `var(--dsw-elevation-stroke), …`, and that stroke is a
48
+ * `0 0 0 .5px` ring — so "no border" had not removed the outline at all,
49
+ * it had only moved it into the shadow. Both surfaces carry a hairline.
50
+ *
51
+ * The fix is to use the shell's vocabulary for a CHOICE rather than for a
52
+ * FIELD. Every pickable surface in the product — the option list in the
53
+ * question card (`border-radius: var(--dsw-radius-md)`, `background: 0 0`),
54
+ * the command and trigger lists, the portalled menu — draws itself the same
55
+ * way: a transparent ground, a `radius-md` corner, and a fill that appears
56
+ * only on hover or selection. A field, by contrast, is an opaque well inset
57
+ * into the page.
58
+ *
59
+ * So a card here is:
60
+ *
61
+ * - TRANSLUCENT, not opaque and not empty. The shell explains this exact
62
+ * choice for `SegmentedControl`: a translucent fill "reads as a place
63
+ * rather than a second button". Opaque is what made the card a well;
64
+ * fully transparent is invisible on a white page, where the light theme
65
+ * would leave nothing but floating text. The tint is the shell's own
66
+ * interactive pair — `interactive-bg-hover` at rest, `interactive-bg-active`
67
+ * on hover — so hovering reads as the same material intensifying;
68
+ * - `radius-md` (12px), the choice radius, against the composer's 28px
69
+ * `radius-panel`;
70
+ * - no shadow at rest. The shell gives an elevated surface a shadow only
71
+ * when it FLOATS over other content (a menu, a hovercard, a modal); a
72
+ * card sitting inline in the page gets none, so neither does this.
73
+ *
74
+ * The icon tile keeps an accent tint — `Tag`'s info tone and the plan
75
+ * package's chip both paint `color-mix(… business-primary 10%, transparent)`
76
+ * — on a square with `radius-sm`, matching those chips' geometry.
77
+ *
78
+ * ## Auto-rotation
79
+ *
80
+ * The strip shows three of four cards, so the fourth would otherwise be
81
+ * invisible to anyone who does not touch the arrows. It advances one card
82
+ * every {@link AUTOPLAY_INTERVAL_MS}, pausing on hover, on keyboard focus, in
83
+ * a background tab, and under `prefers-reduced-motion`. The policy itself
84
+ * lives in `autoplay.ts`, kept pure so the list of reasons to stop is testable
85
+ * rather than buried in an effect.
38
86
  *
39
87
  * @module @huaweidevtools/aishell-dsh-brand/client/SuggestionCards
40
88
  */
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Auto-rotation policy for the example-prompt strip.
3
+ *
4
+ * Kept apart from the component and free of DOM, for the same reason
5
+ * `carousel.ts` is: the DECISION is what needs pinning down, and a decision
6
+ * buried in an effect can only be checked by watching a browser.
7
+ *
8
+ * ## Why the strip advances on its own
9
+ *
10
+ * The strip shows three of four cards, so a card is always off screen and a
11
+ * reader who never touches the arrows never learns it exists. Rotation is what
12
+ * makes the set self-evident. The cost is that content moves without being
13
+ * asked to, which is why every reason to stop is collected here rather than
14
+ * decided piecemeal at the call site.
15
+ *
16
+ * ## Every reason to stop
17
+ *
18
+ * - `engaged` — the pointer is over the strip or focus is inside it. Someone
19
+ * reaching for a card must not have it slide out from under the cursor;
20
+ * this is the pause the feature was asked for, and `focus` is the half of
21
+ * it that covers a reader who never uses a pointer.
22
+ * - `reducedMotion` — the reader asked the platform for less movement. An
23
+ * element that moves by itself for more than five seconds is exactly what
24
+ * `prefers-reduced-motion` exists to suppress, and a hover pause cannot help
25
+ * the reader who cannot hover. Honouring it is what keeps the feature from
26
+ * being motion no one consented to.
27
+ * - `visible` — the tab is hidden. A background tab still fires timers, so
28
+ * without this the strip would churn through slides nobody can see and land
29
+ * somewhere arbitrary on return.
30
+ * - `ready` — the ring has not been measured yet. Advancing before the step
31
+ * is known would move the track by a wrong distance; see the measurement
32
+ * note in `SuggestionCards.tsx`.
33
+ * - `ring` — three cards or fewer all fit, so there is nowhere to advance to.
34
+ * A strip with nothing to scroll must not appear to scroll.
35
+ *
36
+ * @module @huaweidevtools/aishell-dsh-brand/client/autoplay
37
+ */
38
+ /**
39
+ * How long each card is shown before the strip advances.
40
+ *
41
+ * Fixed rather than configurable: this is a hint at what the product can do,
42
+ * not a reading surface, and a setting nobody asked for is a setting nobody
43
+ * finds.
44
+ */
45
+ export declare const AUTOPLAY_INTERVAL_MS = 1500;
46
+ /** Everything that can hold the strip still. */
47
+ export interface AutoplayInputs {
48
+ /** More cards than fit, so there is somewhere to advance to. */
49
+ readonly ring: boolean;
50
+ /** The ring has been measured and the track can be moved a known distance. */
51
+ readonly ready: boolean;
52
+ /** The pointer is over the strip, or focus is inside it. */
53
+ readonly engaged: boolean;
54
+ /** The reader asked for reduced motion. */
55
+ readonly reducedMotion: boolean;
56
+ /** The tab is on screen. */
57
+ readonly visible: boolean;
58
+ }
59
+ /**
60
+ * Whether the strip should be advancing on its own right now.
61
+ *
62
+ * Expressed as one predicate rather than a chain of early returns inside the
63
+ * timer, so that adding a reason to stop is a change to this list and its
64
+ * test — not a change to the effect that schedules the ticks.
65
+ */
66
+ export declare function autoplayActive(inputs: AutoplayInputs): boolean;
67
+ //# sourceMappingURL=autoplay.d.ts.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The mount point the suggestion strip renders into, inside the hero.
3
+ *
4
+ * ## Why a portal, and not CSS placement
5
+ *
6
+ * The strip is a `conversation.input.dock` entry, and that band renders BELOW
7
+ * the hero workspace row in the composer stack. Measured on the live page
8
+ * (1280×900, DSH 0.2.x):
9
+ *
10
+ * hero (headline) y=288 h=74
11
+ * hero workspace row y=374 h=28
12
+ * conversation.input.dock y=411 h=80
13
+ * the input card y=491
14
+ *
15
+ * The product wants the cards directly under the headline, and the workspace
16
+ * row sits between. A negative margin cannot cross it — the row would be
17
+ * covered — and the row's own subtree is not reachable from the dock, since
18
+ * the two are separate children of the composer stack.
19
+ *
20
+ * The hero, however, already renders an empty content div for exactly this
21
+ * purpose. Its stack is: headline → (the "Powered by" label) → `_body`, where
22
+ * `_body` is `flex-direction: column` and left empty by the official shell.
23
+ * That is the hero's content area, and putting the cards there is using the
24
+ * slot as intended rather than fighting the layout.
25
+ *
26
+ * ## How the mount is found
27
+ *
28
+ * By the hero's own class suffix, from the headline outward — `[class*="_body"]`
29
+ * alone is ambiguous (the conversation root has a `_body` too, and the live
30
+ * page has two). So the search starts at a `[class*="_headline"]`, steps to its
31
+ * parent (the hero stack), and takes that stack's own `_body` child. The
32
+ * headline is unique on the page and is what makes the walk unambiguous.
33
+ *
34
+ * Children are walked directly rather than with `:scope >`. The two are
35
+ * equivalent here, and the direct walk keeps these helpers working against the
36
+ * hand-rolled DOM stub the specs use (which has no selector engine) instead of
37
+ * leaving the module's real logic untested.
38
+ *
39
+ * The classes are CSS-module hashes (`pXSMma_body`), hence the suffix match.
40
+ * A base bump that renames them fails closed: {@link findHeroContent} returns
41
+ * null, the portal renders nothing, and the arrows and cards simply do not
42
+ * appear — the strip is never left in the wrong parent.
43
+ *
44
+ * @module @huaweidevtools/aishell-dsh-brand/client/hero-content
45
+ */
46
+ /**
47
+ * The hero's content area, or `null` when the hero is not on screen.
48
+ *
49
+ * Returns null rather than throwing: a New Session that has not painted its
50
+ * hero yet is a normal state (and the strip retries on the next frame), not an
51
+ * error.
52
+ */
53
+ export declare function findHeroContent(target: Document): HTMLElement | null;
54
+ /**
55
+ * A container for the strip inside the hero content area.
56
+ *
57
+ * The hero body is `display: block` and zero-height, so the strip cannot rely
58
+ * on it for layout and must not inherit that block context. The wrapper is
59
+ * created here rather than rendered by React into the official div, because
60
+ * React does not own that node — it owns only what the portal puts inside.
61
+ */
62
+ export declare function ensureHeroMount(content: HTMLElement): HTMLElement;
63
+ /**
64
+ * Remove the mount this module created, if it is still there.
65
+ *
66
+ * Only the mount is removed — never the host div, which belongs to the shell
67
+ * and is reused on the next New Session.
68
+ */
69
+ export declare function removeHeroMount(content: HTMLElement): void;
70
+ //# sourceMappingURL=hero-content.d.ts.map
@@ -44,6 +44,8 @@ export interface Suggestion {
44
44
  readonly id: string;
45
45
  /** Icon key resolved by {@link SuggestionIconArt}. */
46
46
  readonly icon: SuggestionIcon;
47
+ /** The tile colour, measured to work in both themes. See {@link SuggestionHue}. */
48
+ readonly hue: SuggestionHue;
47
49
  /** Dictionary key for the card's one-line title. */
48
50
  readonly titleKey: AishellBrandLocaleKey;
49
51
  /** Dictionary key for the line under the title. */
@@ -53,6 +55,58 @@ export interface Suggestion {
53
55
  }
54
56
  /** The icon set this strip can draw. Keys are local to this plugin. */
55
57
  export type SuggestionIcon = 'cloud' | 'deploy' | 'architecture' | 'cost';
58
+ /**
59
+ * The tile colour for one card, as the shell's own token names.
60
+ *
61
+ * ## Why each card carries its own hue
62
+ *
63
+ * The first version drew every icon in one accent, on the reasoning that four
64
+ * hues beside the brand mark would read as noise. That was a guess, and the
65
+ * shell contradicts it: `FileTypeIcon` gives every item in a row its own hue
66
+ * (`code` blue, `excel` green, `folder` amber, `image` violet), which is the
67
+ * same situation — a row of small glyphs read at a glance — and it chooses
68
+ * per-item colour. Colour is what makes such a row scannable rather than a
69
+ * wall of identical tiles.
70
+ *
71
+ * ## These are STATIC tokens, not the semantic ones
72
+ *
73
+ * `--dsw-alias-state-success-primary` and friends mean "the operation
74
+ * succeeded" / "this needs attention". A card about cost is not a warning, so
75
+ * borrowing the warning colour would be a lie the palette tells. The `static`
76
+ * family is the one intended for decorative identity, which is exactly what a
77
+ * card's hue is — and `FileTypeIcon` reaches for the same family.
78
+ *
79
+ * ## The values were chosen by measurement, in both themes
80
+ *
81
+ * Violet is the exception: the design platform ships NO violet token, so
82
+ * `FileTypeIcon` defines one locally (`rgb(139, 118, 246)`) for its own
83
+ * artwork. The same value is reused here rather than inventing a second one,
84
+ * and it is the only literal colour in this plugin.
85
+ *
86
+ * The four were picked to clear three bars at once, in both themes:
87
+ *
88
+ * - glyph on its tile >= 4.5 — measured 5.42 at worst (violet);
89
+ * - tile against the card >= 1.6 — measured 2.05 at worst;
90
+ * - worst pairwise colour distance >= 30 (CIE76 dE) — measured 38.7.
91
+ *
92
+ * The last bar is the one that matters for "four colours, not one blur": an
93
+ * obvious-looking pick like blue + violet measures only 17.5 apart, which is
94
+ * why this is measured rather than eyeballed.
95
+ */
96
+ export interface SuggestionHue {
97
+ /**
98
+ * The tile fill. A SOLID hue, with the glyph in `--dsw-alias-label-primary-
99
+ * foreground` on top.
100
+ *
101
+ * A tint of the hue was the first attempt and it measured badly: a glyph
102
+ * drawn in a hue on top of a tint of that same hue has almost nothing to
103
+ * separate them, so the contrast was 1.8–2.8 — and RAISING the tint made it
104
+ * worse, since more of the hue behind the glyph means less difference from
105
+ * it. A solid tile puts the glyph on full-strength colour instead, which
106
+ * measured 5.4–9.9 for the same hues.
107
+ */
108
+ readonly tile: string;
109
+ }
56
110
  /**
57
111
  * The cards, in display order.
58
112
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huaweidevtools/aishell-dsh-brand",
3
3
  "description": "AI Shell brand reskin for DSH web — replaces sidebar and hero brand marks with AI Shell SVG",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -51,7 +51,9 @@
51
51
  "@deepseek-ai/dsh-client-ui-sidebar": "0.2.0-rc.1 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1",
52
52
  "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1",
53
53
  "@types/react": "~18.3.1",
54
+ "@types/react-dom": "~18.3.1",
54
55
  "react": "^18.2.0",
56
+ "react-dom": "^18.2.0",
55
57
  "tsdown": "^0.10.0",
56
58
  "typescript": "^5.7.0"
57
59
  },