♻️ refactor(vscode): 代码阅读的种类图标改成插件自己定义的一组(契约里的封闭名字集合),webview 只带这些图标,不再打包 lucide 全套
This commit is contained in:
1 parent
c1d0dd3753
commit
b186b007a4
11 files changed
+139
-47
No files matched your search
@@ -18,7 +18,7 @@
|
||||
| 扩展方式 | 写代码注册:语言插件、翻译插件都实现约定接口,通过本插件 `activate()` 导出的 API 注册;内置实现也走同一个 API |
|
||||
| 多种翻译怎么选 | 预览顶部筛选栏最右边的下拉框,整个文件一起换,按语言记住选择;只有一种时不显示 |
|
||||
| 块筛选 | 预览顶部钉住一条筛选栏,按块的属性(种类、访问级别、异步……)筛;属性由语言插件按和语言无关的 id 报(通用的由宿主从 facts 推),名字由翻译插件起,webview 不认识任何语言;筛选整个工作区共用、记住 |
|
||||
| 块的种类 | 块左边只画种类图标(lucide 图标,和一行字等高,种类色),种类名是鼠标停上去的提示和读屏念的文字,不再写字;内置种类的图标、颜色、名字由插件定(图标在 webview 一处、颜色在 CSS、名字由翻译插件起),库规则的种类由规则给(`KindBadge` 的名字、颜色、图标)。见「预览(代码块树)」 |
|
||||
| 块的种类 | 块左边只画种类图标(插件自己定义的一组 lucide 图标里的一个,和一行字等高,种类色),种类名是鼠标停上去的提示和读屏念的文字,不再写字;内置种类的图标、颜色、名字由插件定(图标在 webview 一处、颜色在 CSS、名字由翻译插件起),库规则的种类由规则给(`KindBadge` 的名字、颜色、图标)。见「预览(代码块树)」 |
|
||||
| 侧栏点文件 | 支持的语言默认打开成「预览」;不支持的语言用普通编辑器打开 |
|
||||
| 块内修改 | 改的就是文件本身:未保存标记、⌘S 保存、⌘Z 撤销都照常,切回「预览」按新代码重新翻译 |
|
||||
| 折叠 | 有子块的块、属性表里带子行的行、带下一层的子行(字典 / 对象里嵌套的字典 / 对象)能折叠;函数块(含方法、嵌套函数)、类块、枚举块、异常块和库规则的卡片默认收起主体、属性表照常显示,其余块、属性行和子行默认展开;用户点过的按他点的记住 |
|
||||
@@ -115,7 +115,7 @@
|
||||
- 每一句是一块:导入、函数、类、枚举、异常、类型、字段、常量、变量、条件、否则、循环、尝试、捕获、最终、返回、语句、注释。
|
||||
- 嵌套的块放在父块下面、往右缩进;每一层左边一条竖线(颜色 `--vscode-editorIndentGuide-background`)。
|
||||
- 块的组成(从左到右):折叠箭头(只有有子块的块才显示,其余占位对齐)、种类图标、内容、右上角一排图标按钮。
|
||||
- 种类图标(原型 `kindBadge`、画板「Prototype / 块种类图标」):块左边只画图标、不带字,和一行字等高(20px 的框,图形 16px,lucide 的线宽 2.2),颜色是种类色;种类的名字(翻译插件给 `kind` 值起的,见「筛选」;库规则的种类是规则给的 `badge.label`)写在悬停提示(`title`,同块右上角按钮的悬停提示)和 `aria-label` 里,`role="img"`。和折叠箭头、内容同一行顶对齐。每种内置种类由插件定图标、颜色、名字:图标是 lucide 图标名,只在 webview 的一处(`kindIcons.tsx` 的 `KIND_ICONS`);颜色在 `codeReader.css` 的 `--cr-kind-*`(见「颜色」);名字由翻译插件给。同一类语义共用一个颜色,图标区分具体是哪种:
|
||||
- 种类图标(原型 `kindBadge`、画板「Prototype / 块种类图标」):块左边只画图标、不带字,和一行字等高(20px 的框,图形 16px,lucide 的线宽 2.2),颜色是种类色;种类的名字(翻译插件给 `kind` 值起的,见「筛选」;库规则的种类是规则给的 `badge.label`)写在悬停提示(`title`,同块右上角按钮的悬停提示)和 `aria-label` 里,`role="img"`。和折叠箭头、内容同一行顶对齐。每种内置种类由插件定图标、颜色、名字:图标从插件定义的一组图标里取(见下),内置种类 → 图标只在 webview 的一处(`kindIcons.tsx` 的 `KIND_ICONS`);颜色在 `codeReader.css` 的 `--cr-kind-*`(见「颜色」);名字由翻译插件给。同一类语义共用一个颜色,图标区分具体是哪种:
|
||||
|
||||
| 种类 | 图标 | 种类 | 图标 | 种类 | 图标 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
@@ -126,7 +126,9 @@
|
||||
| enum 枚举 | `list-ordered` | else 否则 | `corner-down-right` | statement 语句 | `code` |
|
||||
| exception 异常 | `triangle-alert` | loop 循环 | `repeat` | comment 注释 | `message-square-text` |
|
||||
|
||||
库规则的种类用规则给的图标(`KindBadge.icon`):SQLAlchemy 数据表 `database`、数据类 `package`、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`。webview 按 lucide 图标名画(`lucide-react` 的全套图标),认不出的名字画虚线方块(`square-dashed`)。
|
||||
库规则的种类用规则给的图标(`KindBadge.icon`):SQLAlchemy 数据表 `database`、数据类 `package`、Celery 应用 `server-cog`、定时任务 `calendar-clock`、FastAPI 接口 `globe`。
|
||||
|
||||
这组图标由插件自己定义,是一个封闭的集合:契约里的 `KIND_ICON_NAMES`(上表 18 个加库规则用的 5 个,lucide 的 kebab-case 名字),类型 `KindIconName` 是它们的联合,`KindBadge.icon` 只能写其中一个,写别的是类型错误。webview 给每个名字配一个逐个 import 的 lucide 图标组件(`kindIcons.tsx` 的 `KIND_ICON_COMPONENTS`,打包只带这些,预览脚本约 157 kB);webview 的 `KindIconName` 照契约手抄,`kindIconNames.test.ts` 核对两边一样。规则要新图标就把名字加进这组、在 webview 配上图标,没有按任意名字找图标的路。
|
||||
- 右上角的图标按钮(原型组件 `Reader / BlockTools`,普通块、属性表块、库规则的卡片共用):只有图标、不带字,名字写在悬停提示(`title`)和 `aria-label` 里;整排是 `role="group"`(`aria-label="块操作"`)。先是「问 AI」(sparkles 图标,点开 / 收起块下面的问答区,展开时带按下的底色、`aria-expanded`;文件不在工作区文件夹里时没有,见「问 AI」),再是「编辑 / 预览」切换:预览时铅笔「编辑」,编辑时眼睛「预览」、带按下的底色(`--vscode-toolbar-activeBackground`)。按钮宽 24px、高 20px(和种类图标同高),图标 14px、`descriptionForeground` 色;悬停 `--vscode-toolbar-hoverBackground`,键盘聚焦描 1px `--vscode-focusBorder`;Tab 能走到,回车 / 空格切换。
|
||||
- 种类图标和底色按种类区分(颜色见「颜色」);注释块虚线框。
|
||||
- 内容两种状态,每块单独切:
|
||||
@@ -662,8 +664,12 @@ export interface Translator {
|
||||
/** 一种颜色在暗色、亮色主题下各用哪个;高对比主题跟着暗 / 亮走。 */
|
||||
export interface ThemeColor { dark: string, light: string }
|
||||
|
||||
/** 库规则的种类:块左边只画图标(种类色),label 是悬停提示和读屏念的名字,筛选栏里也叫它;icon 是 lucide 图标名(kebab-case,如 database)。 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: string }
|
||||
/** 种类图标:插件自己定义的一组 lucide 图标(kebab-case 名字);前 18 个给内置种类,后面的给库规则。规则要新图标就加进这组,没有按任意名字找图标的路。 */
|
||||
export const KIND_ICON_NAMES = ['import', 'square-function', /* …18 个内置种类的… */ 'database', 'package', 'server-cog', 'calendar-clock', 'globe'] as const
|
||||
export type KindIconName = (typeof KIND_ICON_NAMES)[number]
|
||||
|
||||
/** 库规则的种类:块左边只画图标(种类色),label 是悬停提示和读屏念的名字,筛选栏里也叫它;icon 只能从 KindIconName 里挑。 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
|
||||
|
||||
/** 库规则认领一块后给出的替身,下面挂着原块留下的子块:规则自己的卡片,或认成的现成属性表块。 */
|
||||
export type RuleBlock = RuleCard | RuleFacts
|
||||
@@ -1355,7 +1361,7 @@ vscode/src/codeReader/
|
||||
editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights)
|
||||
editor/occurrenceNavigation.ts 跳到下一处 / 上一处高亮:命令、上下文键
|
||||
index.ts registerCodeReader(context): CodeReaderApi
|
||||
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,块的种类图标(内置种类 → lucide 图标名的唯一一处、按名字画图标)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts)
|
||||
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,块的种类图标(这组图标名 → 图标组件、内置种类 → 图标名,都只在这一处)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts)
|
||||
```
|
||||
|
||||
## 测试
|
||||
@@ -1405,7 +1411,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
||||
- 工作副本(`test/git/reviewWorktree.test.ts`,真的 git:bare 的 origin 上推 `refs/pull/7/head`):拉 PR 的提交建不挂分支的 worktree(父目录不在也建、本机不多出分支),目录手删后同一路径能再建;快进:PR 往前走了且干净 → 快进,已经最新 / 本地在它之上 → 不动,只有未跟踪的文件照样快进,有没提交的改动、本地另有提交 → 不动且改动留着,目录不是工作副本的根 → 报错、外面的仓库不动。
|
||||
- 复制出什么(`explorer/copyText.test.ts`):文件、目录的名字 / 相对路径 / 绝对路径;不带后缀只去最后一个扩展名(`a.test.ts`、`archive.tar.gz`),`.env` 原样、`.env.local` → `.env`、没有扩展名的原样;相对路径总用 `/`,绝对路径照本机写法(Windows 反斜杠)。
|
||||
- 块右上角的图标按钮(webview `BlockTools.test.tsx`,渲染成 HTML):一组一个按钮、没有字、`title` 和 `aria-label` 是「编辑」/「预览」、有图标、编辑时带按下标记;给了「问 AI」时它排在前面、只有图标、问答区展开时带按下标记和 `aria-expanded`。
|
||||
- 块的种类图标(webview `kindIcons.test.tsx`,渲染成 HTML):内置种类的图标和原型一一对应、都是 lucide 里有的图标;图标名按 kebab-case 认,认不出的画虚线方块;只有图标、没有字,`role="img"`,`title` 和 `aria-label` 是种类名,20px 的框、16px 的图形;库规则的卡片用规则给的图标。各库规则的测试核对 `badge` 的名字、颜色、图标。
|
||||
- 块的种类图标(webview `kindIcons.test.tsx`,渲染成 HTML):内置种类的图标和原型一一对应;这组里每个名字画的就是 lucide 里同名的图标;webview 配了图标的名字和契约的 `KIND_ICON_NAMES` 一样、没有多的(`kindIconNames.test.ts`);只有图标、没有字,`role="img"`,`title` 和 `aria-label` 是种类名,20px 的框、16px 的图形;库规则的卡片用规则给的图标。各库规则的测试核对 `badge` 的名字、颜色、图标。
|
||||
- 问 AI:
|
||||
- 位置记号(`qa/anchor.test.ts`,真实的 Python 解析):有名字 / 没名字的块的记号、同一层重复的编号、两个函数里一样的条件互不影响;上面插几行、改空白记号不变;改别的函数的名字只动它和它里面的块;没名字的块自己的源码改了记号变、有名字的块改签名不变。
|
||||
- 存法(`qa/qaMap.test.ts`):记一次、删一次、删光不挂 / 不记;文件缺失、解析失败、不是对象当没有;认不出的块和问答跳过、没会话 id 的照认;再问时接着这块最近那条的会话(删了最近的接着剩下里最近的,没问过、没记会话 id 的重新开);用哪个 profile 答的跟着这条存、读得回来,没有的、空的、不是字符串的当没有;手改的顺序、重复的块读进来整理好;写出来排好序、两格缩进、读得回来、删光留 `{}`;改名、挪目录、删除跟着走。图片:文件名跟着这条存、读得回来,跑到别处的路径、不是字符串的名字跳过;删一次问答它的图片跟着删、别的留着;源文件删了图片跟着删,改名、挪目录图片不动。
|
||||
|
||||
@@ -414,10 +414,39 @@ export interface Translator {
|
||||
export interface ThemeColor { dark: string, light: string }
|
||||
|
||||
/**
|
||||
* 库规则的种类在预览里的样子:块左边只画图标(种类色),名字是悬停提示和读屏念的文字,筛选栏里这个种类也叫它。
|
||||
* icon 是 lucide 图标名(https://lucide.dev/icons 上的 kebab-case 名字,如 `database`)。
|
||||
* 种类图标:插件自己定义的一组 lucide 图标(https://lucide.dev/icons 上的 kebab-case 名字)。前 18 个给内置种类,后面的给库规则的种类;
|
||||
* 库规则的种类(KindBadge.icon)只能从这组里挑,要新图标就加进这组(webview 的 kindIcons.tsx 同时给它配上图标),没有按任意名字找图标的路。
|
||||
*/
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: string }
|
||||
export const KIND_ICON_NAMES = [
|
||||
'import',
|
||||
'square-function',
|
||||
'box',
|
||||
'braces',
|
||||
'list-ordered',
|
||||
'triangle-alert',
|
||||
'tag',
|
||||
'lock',
|
||||
'variable',
|
||||
'split',
|
||||
'corner-down-right',
|
||||
'repeat',
|
||||
'shield',
|
||||
'shield-alert',
|
||||
'shield-check',
|
||||
'corner-down-left',
|
||||
'code',
|
||||
'message-square-text',
|
||||
'database',
|
||||
'package',
|
||||
'server-cog',
|
||||
'calendar-clock',
|
||||
'globe',
|
||||
] as const
|
||||
|
||||
export type KindIconName = (typeof KIND_ICON_NAMES)[number]
|
||||
|
||||
/** 库规则的种类在预览里的样子:块左边只画图标(种类色),名字是悬停提示和读屏念的文字,筛选栏里这个种类也叫它。 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
|
||||
|
||||
/**
|
||||
* 库规则认领一块后给出的替身,预览里它代替原来那块,下面挂着原块留下的子块:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IndexedFile, IndexEntry, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
|
||||
import type { IndexedFile, IndexEntry, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
|
||||
import type { Call } from './pythonTree'
|
||||
import { aliasesOf, callOf, lastName, stringContent, stringValue, syntaxChildren } from './pythonTree'
|
||||
import { hint, joined, name, plain, range, word } from './segments'
|
||||
@@ -21,8 +21,8 @@ const APP_KIND = 'celery.app'
|
||||
const BEAT_KIND = 'celery.beat'
|
||||
const APP_COLOR: ThemeColor = { dark: '#98c379', light: '#4d7f17' }
|
||||
const BEAT_COLOR: ThemeColor = { dark: '#56b6c2', light: '#127f8c' }
|
||||
const APP_ICON = 'server-cog'
|
||||
const BEAT_ICON = 'calendar-clock'
|
||||
const APP_ICON: KindIconName = 'server-cog'
|
||||
const BEAT_ICON: KindIconName = 'calendar-clock'
|
||||
|
||||
/** `Celery(...)` 的位置参数依次是哪个参数(celery.app.base.Celery.__init__)。 */
|
||||
const APP_PARAMETERS = ['main', 'loader', 'backend', 'amqp', 'events', 'log', 'control', 'set_as_current', 'tasks', 'broker', 'include', 'changes', 'config_source', 'fixups', 'task_cls', 'autofinalize', 'namespace', 'strict_typing']
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access, ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||
import type { Access, ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor } from '../contract'
|
||||
import type { Call, LibraryNames } from './pythonTree'
|
||||
import { memberDocs, withLeadingComments } from './children'
|
||||
import { aliasesOf, callOf, canonicalName, genericOf, libraryName, libraryNames, stringValue, syntaxChildren, unwrapType } from './pythonTree'
|
||||
@@ -16,7 +16,7 @@ const LIBRARY = 'dataclasses'
|
||||
const TYPE_LIBRARIES = ['dataclasses', 'typing', 'typing_extensions']
|
||||
const KIND = 'dataclasses.dataclass'
|
||||
const COLOR: ThemeColor = { dark: '#ef7fb0', light: '#b3306c' }
|
||||
const ICON = 'package'
|
||||
const ICON: KindIconName = 'package'
|
||||
|
||||
/** `@dataclass(...)` 各参数不写时的值:写成这个值等于没写,「选项」里不讲。 */
|
||||
const OPTION_DEFAULTS: Record<string, boolean> = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
|
||||
import type { FunctionBlock, ImportFacts, IndexEntry, Inline, KindIconName, LibraryRule, Node, ParameterFacts, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
|
||||
import type { Call, LibraryNames } from './pythonTree'
|
||||
import { callOf, genericOf, lastName, libraryName, stringContent, syntaxChildren, unwrapType } from './pythonTree'
|
||||
import { hint, joined, name, range, word } from './segments'
|
||||
@@ -14,7 +14,7 @@ import { hint, joined, name, range, word } from './segments'
|
||||
const RULE_ID = 'fastapi'
|
||||
const KIND = 'fastapi.route'
|
||||
const COLOR: ThemeColor = { dark: '#61afef', light: '#1a6fb5' }
|
||||
const ICON = 'globe'
|
||||
const ICON: KindIconName = 'globe'
|
||||
/** 认路由对象、参数标记、HTTPException 时看从这几个库导入的名字(HTTPException、status 也可以从 starlette 来)。 */
|
||||
const NAME_LIBRARIES = ['fastapi', 'starlette']
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, TokenRole } from '../contract'
|
||||
import type { ClassBlock, CodeBlock, Inline, KindIconName, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, TokenRole } from '../contract'
|
||||
import type { Call } from './pythonTree'
|
||||
import { withLeadingComments } from './children'
|
||||
import { aliasesOf, callOf, canonicalName, genericOf, lastName, stringValue, syntaxChildren, unwrapType } from './pythonTree'
|
||||
@@ -20,7 +20,7 @@ const SCHEMA_ITEMS = new Set(['ForeignKey', 'Sequence', 'Identity', 'Computed',
|
||||
const SIZE_KEYWORDS = ['length', 'precision', 'scale']
|
||||
const TABLE_KIND = 'sqlalchemy.table'
|
||||
const TABLE_COLOR: ThemeColor = { dark: '#e8a860', light: '#a3590d' }
|
||||
const TABLE_ICON = 'database'
|
||||
const TABLE_ICON: KindIconName = 'database'
|
||||
|
||||
/** 一种显示语言的用词。 */
|
||||
interface Words {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CSSProperties, ReactElement } from 'react'
|
||||
import type { GroupStyle, Inline, SegmentGroup } from './types'
|
||||
import { useContext } from 'react'
|
||||
import { DisplayNameContext, shownName } from './displayNames'
|
||||
import { KIND_ICONS, lucideIcon } from './kindIcons'
|
||||
import { KIND_ICON_COMPONENTS, KIND_ICONS } from './kindIcons'
|
||||
import { isCurrent, segmentOccurrence } from './occurrences'
|
||||
import { HighlightContext } from './useOccurrences'
|
||||
|
||||
@@ -43,7 +43,7 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement
|
||||
}
|
||||
|
||||
/** 框的样式 → 框头图标:遍历同「循环」块的图标;颜色在 codeReader.css 的 .cr-group[data-style] 里。 */
|
||||
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: lucideIcon(KIND_ICONS.loop) }
|
||||
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: KIND_ICON_COMPONENTS[KIND_ICONS.loop] }
|
||||
|
||||
/** 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。 */
|
||||
function GroupBox({ group }: { group: SegmentGroup }): ReactElement {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { KIND_ICON_NAMES } from '../../../src/codeReader/contract'
|
||||
import { KIND_ICON_COMPONENTS } from './kindIcons'
|
||||
|
||||
// webview 的类型照扩展端手抄一份(types.ts);这组图标名两边对不上时,规则给的图标在页面上就没有图,这里核对
|
||||
describe('种类图标这组名字', () => {
|
||||
it('webview 给扩展端 contract 里的每个名字都配了图标,没有多的', () => {
|
||||
expect(Object.keys(KIND_ICON_COMPONENTS).sort()).toEqual([...KIND_ICON_NAMES].sort())
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { BlockView } from './types'
|
||||
import { Database, SquareDashed, SquareFunction } from 'lucide-react'
|
||||
import type { BlockView, KindIconName } from './types'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { KIND_ICONS, KindIcon, lucideIcon } from './kindIcons'
|
||||
import { KIND_ICON_COMPONENTS, KIND_ICONS, KindIcon } from './kindIcons'
|
||||
|
||||
function render(block: Pick<BlockView, 'kind' | 'badge'>, name: string): string {
|
||||
return renderToStaticMarkup(<KindIcon block={block} name={name} />)
|
||||
@@ -33,14 +32,13 @@ describe('块的种类图标', () => {
|
||||
statement: 'code',
|
||||
comment: 'message-square-text',
|
||||
})
|
||||
for (const name of Object.values(KIND_ICONS))
|
||||
expect(lucideIcon(name), name).not.toBe(SquareDashed)
|
||||
})
|
||||
|
||||
it('lucide 图标名按 kebab-case 认;认不出的名字画虚线方块', () => {
|
||||
expect(lucideIcon('square-function')).toBe(SquareFunction)
|
||||
expect(lucideIcon('database')).toBe(Database)
|
||||
expect(lucideIcon('no-such-icon')).toBe(SquareDashed)
|
||||
it('这组图标里每个名字画的就是 lucide 里同名的图标', () => {
|
||||
for (const name of Object.keys(KIND_ICON_COMPONENTS) as KindIconName[]) {
|
||||
const html = render({ kind: 'sqlalchemy.table', badge: { label: name, color: { dark: '#fff', light: '#000' }, icon: name } }, name)
|
||||
expect(html, name).toContain(`class="lucide lucide-${name}"`)
|
||||
}
|
||||
})
|
||||
|
||||
it('只画图标不带字,名字在悬停提示和 aria-label 里;20px 的框、16px 的图形', () => {
|
||||
|
||||
@@ -1,13 +1,64 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { BlockKind, BlockView } from './types'
|
||||
import { icons, SquareDashed } from 'lucide-react'
|
||||
import type { BlockKind, BlockView, KindIconName } from './types'
|
||||
import {
|
||||
Box,
|
||||
Braces,
|
||||
CalendarClock,
|
||||
Code,
|
||||
CornerDownLeft,
|
||||
CornerDownRight,
|
||||
Database,
|
||||
Globe,
|
||||
Import,
|
||||
ListOrdered,
|
||||
Lock,
|
||||
MessageSquareText,
|
||||
Package,
|
||||
Repeat,
|
||||
ServerCog,
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Split,
|
||||
SquareFunction,
|
||||
Tag,
|
||||
TriangleAlert,
|
||||
Variable,
|
||||
} from 'lucide-react'
|
||||
|
||||
/** 插件定义的这组种类图标,每个名字一个 lucide 图标(逐个 import,打包只带这些)。 */
|
||||
export const KIND_ICON_COMPONENTS: Record<KindIconName, LucideIcon> = {
|
||||
'import': Import,
|
||||
'square-function': SquareFunction,
|
||||
'box': Box,
|
||||
'braces': Braces,
|
||||
'list-ordered': ListOrdered,
|
||||
'triangle-alert': TriangleAlert,
|
||||
'tag': Tag,
|
||||
'lock': Lock,
|
||||
'variable': Variable,
|
||||
'split': Split,
|
||||
'corner-down-right': CornerDownRight,
|
||||
'repeat': Repeat,
|
||||
'shield': Shield,
|
||||
'shield-alert': ShieldAlert,
|
||||
'shield-check': ShieldCheck,
|
||||
'corner-down-left': CornerDownLeft,
|
||||
'code': Code,
|
||||
'message-square-text': MessageSquareText,
|
||||
'database': Database,
|
||||
'package': Package,
|
||||
'server-cog': ServerCog,
|
||||
'calendar-clock': CalendarClock,
|
||||
'globe': Globe,
|
||||
}
|
||||
|
||||
/**
|
||||
* 内置种类的图标(lucide 图标名,和库规则给的 badge.icon 同一种写法):块左边只画图标,名字是悬停提示。
|
||||
* 内置种类的图标:块左边只画图标,名字是悬停提示。
|
||||
* 同一类语义共用一个颜色(codeReader.css 的 --cr-kind-*:常量 / 变量 / 字段都是数据,尝试 / 捕获都是异常处理),图标区分具体是哪种。
|
||||
*/
|
||||
export const KIND_ICONS: Record<BlockKind, string> = {
|
||||
export const KIND_ICONS: Record<BlockKind, KindIconName> = {
|
||||
import: 'import',
|
||||
function: 'square-function',
|
||||
class: 'box',
|
||||
@@ -28,21 +79,13 @@ export const KIND_ICONS: Record<BlockKind, string> = {
|
||||
comment: 'message-square-text',
|
||||
}
|
||||
|
||||
const LUCIDE: Record<string, LucideIcon | undefined> = icons
|
||||
|
||||
/** lucide 图标名(kebab-case,`square-function`)→ 图标组件;认不出的名字(规则写错了)画虚线方块。 */
|
||||
export function lucideIcon(name: string): LucideIcon {
|
||||
return LUCIDE[name.replace(/(?:^|-)([a-z0-9])/g, (_, first: string) => first.toUpperCase())] ?? SquareDashed
|
||||
}
|
||||
|
||||
const BUILT_IN: Partial<Record<string, string>> = KIND_ICONS
|
||||
|
||||
/**
|
||||
* 块左边的种类:只画图标,和一行字等高(20px 的框、16px 的图形),颜色是种类色(--kind)。图标是库规则给的(badge.icon),
|
||||
* 没有就按内置种类查 KIND_ICONS;名字(翻译插件给种类起的,库规则的种类是规则给的)写在悬停提示(title)和读屏念的 aria-label 里。
|
||||
* 没有就是内置种类的(KIND_ICONS,没带 badge 的块 kind 是 BlockKind);名字(翻译插件给种类起的,库规则的种类是规则给的)
|
||||
* 写在悬停提示(title)和读屏念的 aria-label 里。
|
||||
*/
|
||||
export function KindIcon({ block, name }: { block: Pick<BlockView, 'kind' | 'badge'>, name: string }): ReactElement {
|
||||
const Icon = lucideIcon(block.badge?.icon ?? BUILT_IN[block.kind] ?? '')
|
||||
const Icon = KIND_ICON_COMPONENTS[block.badge?.icon ?? KIND_ICONS[block.kind as BlockKind]]
|
||||
return (
|
||||
<span className="cr-kind flex size-5 shrink-0 items-center justify-center" role="img" title={name} aria-label={name}>
|
||||
<Icon size={16} strokeWidth={2.2} aria-hidden />
|
||||
|
||||
@@ -73,8 +73,14 @@ export interface PropertyRow extends Defines {
|
||||
/** 一种颜色在暗色、亮色主题下各用哪个(CSS 颜色值);高对比主题跟着暗 / 亮走 */
|
||||
export interface ThemeColor { dark: string, light: string }
|
||||
|
||||
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和 lucide 图标名;块左边只画图标 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: string }
|
||||
/** 种类图标:插件自己定义的一组 lucide 图标(同扩展端 contract 的 KIND_ICON_NAMES),每个名字在 kindIcons.tsx 里配一个图标 */
|
||||
export type KindIconName
|
||||
= | 'import' | 'square-function' | 'box' | 'braces' | 'list-ordered' | 'triangle-alert' | 'tag' | 'lock' | 'variable'
|
||||
| 'split' | 'corner-down-right' | 'repeat' | 'shield' | 'shield-alert' | 'shield-check' | 'corner-down-left' | 'code'
|
||||
| 'message-square-text' | 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe'
|
||||
|
||||
/** 库规则的种类:名字(悬停提示、读屏念的文字)、颜色和图标(从 KindIconName 里挑);块左边只画图标 */
|
||||
export interface KindBadge { label: string, color: ThemeColor, icon: KindIconName }
|
||||
|
||||
interface BlockViewBase {
|
||||
/** 在树里的位置,例如 '6.1.0' */
|
||||
|
||||
Reference in new issue
Block a user