3 Commits
6 changed files with 131 additions and 29 deletions
+11
View File
@@ -2,6 +2,17 @@
版本说明按倒序排列。提交信息遵循 [CONTRIBUTING.md](CONTRIBUTING.md) 的单行规范,因此每次改动"为什么这样改、影响面是什么"记在这里,而不是提交信息里。 版本说明按倒序排列。提交信息遵循 [CONTRIBUTING.md](CONTRIBUTING.md) 的单行规范,因此每次改动"为什么这样改、影响面是什么"记在这里,而不是提交信息里。
## v1.0.9
- 轻弹窗改成 **DSH 的浮层卡片**,跟随 DSH 的深色 / 浅色模式。原来用的是 DSH 的 `--dsw-alias-toast-bg`,而那个令牌在**两种模式下都是深灰**(浅色模式 `#353638`、深色模式 `#43454a`),所以在浅色页面里浮窗是一块接近纯黑的板子,和周围的卡片不是一种东西。现在照 DSH 自己的做法来(下面这些都是 shell 主题里真实存在、并且会随 `body[data-ds-dark-theme]` 一起切换的令牌):
- 底色 `--dsw-alias-bg-layer-2`:浅色 `#fff`、深色 `#2c2c2e`,和浮动菜单 / 弹窗用的是同一层;
- 发丝线与大阴影用 `--dsw-elevation-stroke-color` + `--dsw-elevation-prominent`(DSH 菜单的做法,发丝线取 `--dsw-alias-border-l1`:浅色 `#0000000a`、深色 `#ffffff0f`);
- 文字改 `--dsw-alias-label-primary` / `-secondary` / `-tertiary`,不再是写死的白字加 `opacity`;
- 入场动画用 shell 自己的 `--ds-transition-duration` 与 `--ds-ease-in-out`。
- 「查看」按钮改成 `--dsw-alias-state-business-primary`(浅色 `#4176e6`、深色 `#7aaaff`);原来是写死的 `--dsw-static-deepseek-400`,在白底上对比度不够、也不随主题变化。关闭按钮用 `--dsw-alias-label-tertiary`,hover 升到主文字色。三类图标仍是状态令牌:完成绿 `#22c55e`、授权琥珀 `#dd8629`、提问蓝(随主题)。
- 新增 `tests/unit/styles.test.js`(5 条)把两条不变量钉住:**样式表里不出现字面颜色、也不用 `--dsw-static-*` 这类不随主题切换的令牌**;弹窗必须用上面那套浮层配方,且**不许再出现 `--dsw-alias-toast-bg`**。以后谁把颜色写死、或又退回那个深色板子,测试会直接失败。
- 行为未改:位置(右上角)、尺寸、停留时间、hover 暂停、`Esc` / ✕ 关闭、同屏 3 条都不变,只换皮。
## v1.0.8 ## v1.0.8
- 修复:**授权与提问两类提醒从来不会弹**(只有「会话已完成」会弹)。原因不是判据写错了,而是投递前的「是否还值得提醒」那道闸门用错了事实: - 修复:**授权与提问两类提醒从来不会弹**(只有「会话已完成」会弹)。原因不是判据写错了,而是投递前的「是否还值得提醒」那道闸门用错了事实:
+4 -3
View File
@@ -37,7 +37,8 @@ DeepSeek Harness(DSH)插件:会话**需要你注意**时提醒你——窗
- 点「查看」跳到该会话; - 点「查看」跳到该会话;
- 6 秒后自动消失;鼠标停在上面期间不消失,移开后重新计时; - 6 秒后自动消失;鼠标停在上面期间不消失,移开后重新计时;
- 按 `Esc` 关掉最新的一条,或点 ✕ 关掉这一条; - 按 `Esc` 关掉最新的一条,或点 ✕ 关掉这一条;
- 同屏最多叠 3 条,更早的会被挤掉。 - 同屏最多叠 3 条,更早的会被挤掉;
- 轻弹窗是 DSH 的浮层卡片样式(和菜单、弹窗同一层底色与阴影),**跟随 DSH 的深色 / 浅色模式**。
## 设置 ## 设置
@@ -59,10 +60,10 @@ DeepSeek Harness(DSH)插件:会话**需要你注意**时提醒你——窗
在 DSH 的 **插件 → 添加插件** 的上方输入框里填: 在 DSH 的 **插件 → 添加插件** 的上方输入框里填:
```text ```text
https://gitea.iwake.top/dsh-plugin/session-notify.git#v1.0.8 https://gitea.iwake.top/dsh-plugin/session-notify.git#v1.0.9
``` ```
`#` 后面跟标签或提交,用来锁定版本;不写则取默认分支。跟随 1.x 最新版可以写 `#semver:^1.0.8`。仓库是公开的,不需要凭据,也不用改「安装源」。 `#` 后面跟标签或提交,用来锁定版本;不写则取默认分支。跟随 1.x 最新版可以写 `#semver:^1.0.9`。仓库是公开的,不需要凭据,也不用改「安装源」。
### 本地路径安装 ### 本地路径安装
+7 -7
View File
@@ -891,21 +891,21 @@ function seatTranslator(props, fallback) {
var CSS = ` var CSS = `
.dsn-stack{position:fixed;top:40px;right:24px;z-index:1100;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:none} .dsn-stack{position:fixed;top:40px;right:24px;z-index:1100;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:none}
@media (max-width:520px){.dsn-stack{right:12px}} @media (max-width:520px){.dsn-stack{right:12px}}
.dsn-toast{box-sizing:border-box;pointer-events:auto;display:flex;align-items:flex-start;gap:10px;width:max-content;max-width:min(420px,calc(100vw - 48px));padding:12px 12px 12px 16px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-toast-bg);color:var(--dsw-alias-toast-label);font-size:14px;line-height:22px;box-shadow:var(--dsw-shadow-lv3);animation:dsn-toast-in 160ms ease-out} .dsn-toast{box-sizing:border-box;pointer-events:auto;display:flex;align-items:flex-start;gap:10px;width:max-content;max-width:min(420px,calc(100vw - 48px));padding:12px 12px 12px 16px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-2);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px;animation:dsn-toast-in var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-out)}
@keyframes dsn-toast-in{from{opacity:0;transform:translateX(8px)}to{opacity:1;transform:translateX(0)}} @keyframes dsn-toast-in{from{opacity:0;transform:translateX(8px)}to{opacity:1;transform:translateX(0)}}
@media (prefers-reduced-motion: reduce){.dsn-toast{animation:none}} @media (prefers-reduced-motion: reduce){.dsn-toast{animation:none}}
.dsn-toast-icon{display:grid;place-items:center;flex:none;height:22px;color:var(--dsw-alias-state-warn-label)} .dsn-toast-icon{display:grid;place-items:center;flex:none;height:22px;color:var(--dsw-alias-state-warn-label)}
.dsn-toast-icon.is-completion{color:var(--dsw-alias-state-success-primary)} .dsn-toast-icon.is-completion{color:var(--dsw-alias-state-success-primary)}
.dsn-toast-icon.is-question{color:var(--dsw-alias-brand-primary)} .dsn-toast-icon.is-question{color:var(--dsw-alias-state-business-primary)}
.dsn-toast-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .dsn-toast-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.dsn-toast-title{font-weight:500;font-size:14px;line-height:22px} .dsn-toast-title{color:var(--dsw-alias-label-primary);font-weight:500;font-size:14px;line-height:22px}
.dsn-toast-desc{color:var(--dsw-alias-toast-label);opacity:.8;font-size:13px;line-height:20px;overflow-wrap:break-word} .dsn-toast-desc{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;overflow-wrap:break-word}
.dsn-toast-actions{display:flex;align-items:center;gap:2px;flex:none} .dsn-toast-actions{display:flex;align-items:center;gap:2px;flex:none}
.dsn-toast-action{display:inline-flex;align-items:center;height:28px;padding:0 8px;border:0;border-radius:var(--dsw-radius-sm);background:none;color:var(--dsw-static-deepseek-400);font:inherit;font-size:13px;line-height:20px;cursor:pointer} .dsn-toast-action{display:inline-flex;align-items:center;height:28px;padding:0 8px;border:0;border-radius:var(--dsw-radius-sm);background:none;color:var(--dsw-alias-state-business-primary);font:inherit;font-size:13px;line-height:20px;cursor:pointer}
.dsn-toast-action:hover{background:var(--dsw-alias-interactive-bg-hover)} .dsn-toast-action:hover{background:var(--dsw-alias-interactive-bg-hover)}
.dsn-toast-action:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px} .dsn-toast-action:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
.dsn-toast-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-toast-label);opacity:.8;cursor:pointer} .dsn-toast-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer}
.dsn-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);opacity:1} .dsn-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
.dsn-toast-close:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px} .dsn-toast-close:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
.dsn-page{display:flex;flex-direction:column;gap:28px;max-width:560px} .dsn-page{display:flex;flex-direction:column;gap:28px;max-width:560px}
.dsn-lede{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px} .dsn-lede{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@dsh-plugin/session-notify", "name": "@dsh-plugin/session-notify",
"version": "1.0.8", "version": "1.0.9",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "会话完成、需要授权、需要回答时提醒你:窗口不在前台用系统通知,窗口在前台用应用内轻弹窗。", "description": "会话完成、需要授权、需要回答时提醒你:窗口不在前台用系统通知,窗口在前台用应用内轻弹窗。",
+27 -18
View File
@@ -1,20 +1,29 @@
/** /**
* Every style declaration this package owns. * Every style declaration this package owns.
* *
* The popup copies the shipped toast surface (a fixed entry in the frame-wide * Both surfaces are built out of the theme's own vocabulary and nothing else, so
* overlay, the `--dsw-alias-toast-bg` / `--dsw-alias-toast-label` pair, * they follow the shell's light and dark modes without a branch of their own (the
* `--dsw-radius-lg`, `--dsw-shadow-lv3`) and adds a title line above the text. * theme swaps every `--dsw-alias-*` under `body[data-ds-dark-theme]`):
* *
* The settings page is a reading order, not a form dump: a lede that says how * - The popup is a floating card, using the same recipe the shipped menus and
* alerts travel, then one group per concern. Inside a group a title introduces a * modals use — `--dsw-alias-bg-layer-2` as the surface, the
* list whose rows put the subject on the left and the single control on the * `--dsw-elevation-stroke-color` / `--dsw-elevation-prominent` pair for its
* right, with hairlines only between rows; the status group is a two-column fact * hairline and shadow, `--dsw-alias-label-*` for text, and the shell's motion
* list, because four sentences stacked in one cell are not scannable. Class * tokens. It deliberately does NOT use `--dsw-alias-toast-bg`, which is a fixed
* names are renamed under `dsn-`; only the popup glyphs carry their own artwork * dark slate (#353638 in light mode) and would read as a black slab on a light
* colors. * page.
* - The settings page is a reading order, not a form dump: a lede that says how
* alerts travel, then one group per concern. Inside a group a title introduces
* a list whose rows put the subject on the left and the single control on the
* right, with hairlines only between rows; the status group is a two-column
* fact list, because four sentences stacked in one cell are not scannable.
*
* Class names are renamed under `dsn-`; only the popup glyphs carry their own
* artwork colors — and those are theme-aware state tokens too.
* *
* A plugin must not import a Harness Client package, so the declarations are * A plugin must not import a Harness Client package, so the declarations are
* this package's own text and reference theme tokens only. * this package's own text. `tests/unit/styles.test.js` holds both invariants:
* tokens only, and no theme-fixed color.
* *
* @module @dsh-plugin/session-notify/client/ui/styles * @module @dsh-plugin/session-notify/client/ui/styles
*/ */
@@ -22,21 +31,21 @@
export const CSS = ` export const CSS = `
.dsn-stack{position:fixed;top:40px;right:24px;z-index:1100;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:none} .dsn-stack{position:fixed;top:40px;right:24px;z-index:1100;display:flex;flex-direction:column;align-items:flex-end;gap:8px;pointer-events:none}
@media (max-width:520px){.dsn-stack{right:12px}} @media (max-width:520px){.dsn-stack{right:12px}}
.dsn-toast{box-sizing:border-box;pointer-events:auto;display:flex;align-items:flex-start;gap:10px;width:max-content;max-width:min(420px,calc(100vw - 48px));padding:12px 12px 12px 16px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-toast-bg);color:var(--dsw-alias-toast-label);font-size:14px;line-height:22px;box-shadow:var(--dsw-shadow-lv3);animation:dsn-toast-in 160ms ease-out} .dsn-toast{box-sizing:border-box;pointer-events:auto;display:flex;align-items:flex-start;gap:10px;width:max-content;max-width:min(420px,calc(100vw - 48px));padding:12px 12px 12px 16px;border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-bg-layer-2);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px;animation:dsn-toast-in var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-out)}
@keyframes dsn-toast-in{from{opacity:0;transform:translateX(8px)}to{opacity:1;transform:translateX(0)}} @keyframes dsn-toast-in{from{opacity:0;transform:translateX(8px)}to{opacity:1;transform:translateX(0)}}
@media (prefers-reduced-motion: reduce){.dsn-toast{animation:none}} @media (prefers-reduced-motion: reduce){.dsn-toast{animation:none}}
.dsn-toast-icon{display:grid;place-items:center;flex:none;height:22px;color:var(--dsw-alias-state-warn-label)} .dsn-toast-icon{display:grid;place-items:center;flex:none;height:22px;color:var(--dsw-alias-state-warn-label)}
.dsn-toast-icon.is-completion{color:var(--dsw-alias-state-success-primary)} .dsn-toast-icon.is-completion{color:var(--dsw-alias-state-success-primary)}
.dsn-toast-icon.is-question{color:var(--dsw-alias-brand-primary)} .dsn-toast-icon.is-question{color:var(--dsw-alias-state-business-primary)}
.dsn-toast-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .dsn-toast-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.dsn-toast-title{font-weight:500;font-size:14px;line-height:22px} .dsn-toast-title{color:var(--dsw-alias-label-primary);font-weight:500;font-size:14px;line-height:22px}
.dsn-toast-desc{color:var(--dsw-alias-toast-label);opacity:.8;font-size:13px;line-height:20px;overflow-wrap:break-word} .dsn-toast-desc{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;overflow-wrap:break-word}
.dsn-toast-actions{display:flex;align-items:center;gap:2px;flex:none} .dsn-toast-actions{display:flex;align-items:center;gap:2px;flex:none}
.dsn-toast-action{display:inline-flex;align-items:center;height:28px;padding:0 8px;border:0;border-radius:var(--dsw-radius-sm);background:none;color:var(--dsw-static-deepseek-400);font:inherit;font-size:13px;line-height:20px;cursor:pointer} .dsn-toast-action{display:inline-flex;align-items:center;height:28px;padding:0 8px;border:0;border-radius:var(--dsw-radius-sm);background:none;color:var(--dsw-alias-state-business-primary);font:inherit;font-size:13px;line-height:20px;cursor:pointer}
.dsn-toast-action:hover{background:var(--dsw-alias-interactive-bg-hover)} .dsn-toast-action:hover{background:var(--dsw-alias-interactive-bg-hover)}
.dsn-toast-action:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px} .dsn-toast-action:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
.dsn-toast-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-toast-label);opacity:.8;cursor:pointer} .dsn-toast-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:0;border-radius:var(--dsw-radius-sm);background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer}
.dsn-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);opacity:1} .dsn-toast-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
.dsn-toast-close:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px} .dsn-toast-close:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
.dsn-page{display:flex;flex-direction:column;gap:28px;max-width:560px} .dsn-page{display:flex;flex-direction:column;gap:28px;max-width:560px}
.dsn-lede{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px} .dsn-lede{margin:0;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
+81
View File
@@ -0,0 +1,81 @@
// The stylesheet's two contracts, both of which the popup broke once:
//
// 1. it may only paint with theme tokens — a literal color cannot follow the
// shell's light/dark switch, and a `--dsw-static-*` token is fixed by design;
// 2. the popup must use the same floating-surface recipe as the shipped menus and
// modals (surface + hairline + elevation + label tokens), not the fixed dark
// `--dsw-alias-toast-bg` that made it a black slab on a light page.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { CSS } from '../../src/client/ui/styles.js'
/** Every `property:value` declaration in the sheet, selector-less. */
function declarations(sheet) {
return [...sheet.matchAll(/([a-z-]+)\s*:\s*([^;{}]+);/g)].map((match) => ({ property: match[1], value: match[2].trim() }))
}
const COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|oklch|lab)\(|\b(?:white|black|red|green|blue|gray|grey)\b/
test('the stylesheet paints with tokens and never with a literal color', () => {
const properties = ['color', 'background', 'background-color', 'border', 'border-top', 'border-color', 'box-shadow', 'outline']
const painted = declarations(CSS).filter(({ property }) => properties.includes(property))
assert.ok(painted.length > 20, 'expected the sheet to carry its painted declarations')
for (const { property, value } of painted) {
// Keyword values carry no color of their own.
if (/^(none|transparent|inherit|currentColor)$/.test(value)) continue
assert.ok(!COLOR_LITERAL.test(value), `${property}: ${value} hardcodes a color instead of using a theme token`)
assert.ok(!value.includes('--dsw-static-'), `${property}: ${value} uses a theme-fixed static token that cannot follow dark mode`)
}
})
test('the popup is the shipped floating-card recipe, in both themes', () => {
const toast = CSS.match(/\.dsn-toast\{([^}]*)\}/)?.[1]
assert.ok(toast !== undefined, 'the popup rule is missing')
// The surface, the hairline and the shadow are what DSH's menus and modals use.
assert.ok(toast.includes('background:var(--dsw-alias-bg-layer-2)'), 'the popup must sit on the elevated layer surface')
assert.ok(toast.includes('--dsw-elevation-stroke-color:var(--dsw-alias-border-l1)'), 'the popup must take the menu hairline')
assert.ok(toast.includes('box-shadow:var(--dsw-elevation-prominent)'), 'the popup must take the prominent elevation')
assert.ok(toast.includes('color:var(--dsw-alias-label-primary)'), 'the popup text must be the primary label token')
assert.ok(toast.includes('border-radius:var(--dsw-radius-lg)'), 'the popup must take the large radius')
assert.ok(toast.includes('var(--ds-transition-duration') && toast.includes('var(--ds-ease-in-out'), 'the entrance must use the shell motion tokens')
// The one thing it must not do: the fixed dark toast slab, or any of its labels.
assert.ok(!toast.includes('--dsw-alias-toast-'), 'the popup must not use the theme-fixed toast surface')
assert.ok(!CSS.includes('--dsw-alias-toast-'), 'no rule may fall back to the fixed toast surface')
})
test('the popup reads as one card: title, body and both controls use label tokens', () => {
const rule = (name) => CSS.match(new RegExp(`\\.${name}\\{([^}]*)\\}`))?.[1] ?? ''
assert.ok(rule('dsn-toast-title').includes('var(--dsw-alias-label-primary)'))
assert.ok(rule('dsn-toast-desc').includes('var(--dsw-alias-label-secondary)'))
assert.ok(rule('dsn-toast-close').includes('color:var(--dsw-alias-label-tertiary)'))
assert.ok(rule('dsn-toast-action').includes('var(--dsw-alias-state-business-primary)'))
// Hover feedback comes from the shared interactive token, not a hand-rolled color.
assert.ok(rule('dsn-toast-action:hover').includes('var(--dsw-alias-interactive-bg-hover)'))
assert.ok(rule('dsn-toast-close:hover').includes('var(--dsw-alias-interactive-bg-hover)'))
})
test('the three popup glyphs stay distinguishable through state tokens', () => {
assert.ok(CSS.includes('.dsn-toast-icon{') && CSS.includes('var(--dsw-alias-state-warn-label)'))
assert.ok(CSS.includes('.dsn-toast-icon.is-completion{color:var(--dsw-alias-state-success-primary)}'))
assert.ok(CSS.includes('.dsn-toast-icon.is-question{color:var(--dsw-alias-state-business-primary)}'))
})
test('every class the components render has a rule, so nothing falls back to defaults', () => {
// Guards the redesign: a renamed class that the sheet no longer styles would
// render as an unstyled block instead of failing loudly.
const expected = [
'dsn-stack', 'dsn-toast', 'dsn-toast-icon', 'dsn-toast-icon is-completion', 'dsn-toast-icon is-question',
'dsn-toast-text', 'dsn-toast-title', 'dsn-toast-desc', 'dsn-toast-actions', 'dsn-toast-action', 'dsn-toast-close',
'dsn-page', 'dsn-lede', 'dsn-group', 'dsn-group-title', 'dsn-group-note', 'dsn-list', 'dsn-row', 'dsn-row-text',
'dsn-row-title', 'dsn-row-sub', 'dsn-state', 'dsn-dot', 'dsn-facts', 'dsn-fact-key', 'dsn-fact', 'dsn-fact-value',
'dsn-fact-title', 'dsn-switch', 'dsn-thumb', 'dsn-button',
]
for (const className of expected) {
const selector = `.${className.split(' ').join('.')}`
assert.ok(CSS.includes(`${selector}{`) || CSS.includes(`${selector}[`) || CSS.includes(`${selector}:`) || CSS.includes(`${selector}.`), `no rule for ${selector}`)
}
})