niuma-ui 1.1.3 → 1.1.5

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/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.1.5] - 2026-08-19
10
+
11
+ ### 修复
12
+
13
+ - 命令式 `rsConfirm` / `openRsDialog`:销毁改为幂等,并在测试收尾走正规 `unmount`(不再先拆 Teleport 节点)。避免 Vue 卸载时 `nextSibling` 未处理异常导致 CI 失败。
14
+
15
+ ## [1.1.4] - 2026-08-19
16
+
17
+ ### 新增
18
+
19
+ - `RsDialog`:`layout="form"` 作为居中轻量表单/说明窗的正式布局;`layout="confirm"` 仍可用,开发环境继续警告并视作 form 的历史别名。确认/提示请用 `RsConfirmDialog` / `rsConfirm`。
20
+ - `RsSelect.fillSearchWithValue`:打开下拉时把当前选中项写入搜索框并参与过滤(默认 `false`,搜索框保持空白)。
21
+ - **RsIcon `postgres`**:PostgreSQL 官方大象品牌图标,token `--rs-icon-postgres-accent`。
22
+ - `MONACO_POSTGRESQL_LANGUAGE`(`postgresql`):官方 PostgreSQL 的 Monaco languageId,走 Bridge LSP;`RsMonacoEditor` 对 postgresql / clickhouse / sqlite / sqlserver / oracle 同样不再注册实例级补全。
23
+ - `RsTerminal`:`selectionChange` 事件;expose `getSelection` / `hasSelection` / `getGeometry` 与类型 `RsTerminalExpose` / `RsTerminalGeometry`。
24
+ - `src/dev/vite-xterm-deps.ts`:供宿主 Vite `optimizeDeps` 预构建 xterm,避免控制台首次加载 504。
25
+
26
+ ### 变更
27
+
28
+ - `RsSelect`:`filterOption` 默认 `true`(修复 Vue 把 boolean 联合类型当成 Boolean prop,未传入即 `false`、可搜索列表永远不过滤)。
29
+ - `RsSelect`:`searchable` 一律走本地过滤并关掉 Reka 内置 filter,避免手输关键字时匹配项被挡住。
30
+ - `RsTerminal`:`inheritAttrs: false`,`class` / `style` 落到 `.rs-terminal`;`wheelScrollModifier="shift"` 仅在备用屏(vim/top)把滚轮转成方向键,普通 shell 滚历史。
31
+ - `RsTableColumn.tooltip`:只写格子里看不到的额外提示;可见文本被截断时用 `ellipsis`。
32
+ - `RsMonacoEditor`:当前行高亮改为整行 `line`(不再只画 gutter)。
33
+
34
+ ### 修复
35
+
36
+ - `RsTable`:自定义 slot + `ellipsis` 也能挂溢出 tooltip;slot 不再 `max-width: 100%` 导致溢出检测失效。
37
+ - `RsTable` 共享 tooltip:z-index 改为 `--rs-z-dropdown`;`findTipHost` 兼容文本节点,避免悬停到文字时丢宿主。
38
+ - `RsTerminal`:宿主负字距不再让 xterm 行尾被裁;xterm 6 空 viewport 滚动条不再挡住最后一列;滑块对齐 RsScrollbar;flex 项 `min-width: 0` 以便列数随容器收缩。
39
+
9
40
  ## [1.1.3] - 2026-08-14
10
41
 
11
42
  ### 新增
@@ -122,7 +153,9 @@
122
153
 
123
154
  - 1.0 之前的私有 tag(如 `v0.1.0`)仅作历史记录;新接入请依赖 `v1.0.0` 及之后版本。
124
155
 
125
- [Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.1.3...HEAD
156
+ [Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.1.5...HEAD
157
+ [1.1.5]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.5
158
+ [1.1.4]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.4
126
159
  [1.1.3]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.3
127
160
  [1.1.2]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.2
128
161
  [1.1.1]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niuma-ui",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "Vue 3 设计系统与 Rs* 组件库(--rs-* token)",
@@ -107,10 +107,23 @@ describe('RsDialog', () => {
107
107
  await flushPromises()
108
108
  const content = document.body.querySelector('.rs-dialog__content')
109
109
  expect(content?.classList.contains('rs-dialog__content--confirm')).toBe(true)
110
+ expect(content?.classList.contains('rs-dialog__content--form')).toBe(true)
110
111
  expect(content?.classList.contains('rs-dialog__content--sm')).toBe(true)
111
112
  wrapper.unmount()
112
113
  })
113
114
 
115
+ it('applies form layout without confirm deprecation class-only alias', async () => {
116
+ const wrapper = mount(RsDialog, {
117
+ props: { open: true, title: '表单', width: 'sm', layout: 'form', resizable: false, fullscreenable: false },
118
+ attachTo: document.body,
119
+ })
120
+ await flushPromises()
121
+ const content = document.body.querySelector('.rs-dialog__content')
122
+ expect(content?.classList.contains('rs-dialog__content--form')).toBe(true)
123
+ expect(content?.classList.contains('rs-dialog__content--confirm')).toBe(false)
124
+ wrapper.unmount()
125
+ })
126
+
114
127
  it('renders footer slot', async () => {
115
128
  const wrapper = mount(RsDialog, {
116
129
  props: { open: true, title: '保存' },
@@ -109,6 +109,15 @@ describe('RsIcon', () => {
109
109
  expect(isRsIconName('sqlserver')).toBe(true)
110
110
  })
111
111
 
112
+ it('loads custom postgres icon', () => {
113
+ const icon = resolveLucideIcon('postgres')
114
+ expect(icon).toBeTruthy()
115
+ const wrapper = mount(RsIcon, { props: { name: 'postgres' } })
116
+ expect(wrapper.find('svg.rs-icon').exists()).toBe(true)
117
+ expect(wrapper.find('.postgres-icon__mark').exists()).toBe(true)
118
+ expect(isRsIconName('postgres')).toBe(true)
119
+ })
120
+
112
121
  it('loads arbitrary lucide icon by name', () => {
113
122
  const icon = resolveLucideIcon('trash-2')
114
123
  expect(icon).toBeTruthy()
@@ -205,6 +214,11 @@ describe('RsIcon', () => {
205
214
  expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-sqlserver-accent: #34C759')
206
215
  })
207
216
 
217
+ it('applies accent color to custom postgres icon', () => {
218
+ const wrapper = mount(RsIcon, { props: { name: 'postgres', color: '#34C759' } })
219
+ expect(wrapper.find('svg').attributes('style')).toContain('--rs-icon-postgres-accent: #34C759')
220
+ })
221
+
208
222
  it('applies rotate transform', () => {
209
223
  const wrapper = mount(RsIcon, { props: { name: 'chevron-right', rotate: 90 } })
210
224
  expect(wrapper.find('svg').attributes('style')).toContain('rotate(90deg)')
@@ -296,6 +296,68 @@ describe('RsSelect', () => {
296
296
  expect(root.props('ignoreFilter')).toBe(true)
297
297
  })
298
298
 
299
+ it('enables manual filter when searchable so query hides non-matches', async () => {
300
+ const typeOptions = [
301
+ { label: 'INT', value: 'INT' },
302
+ { label: 'BIGINT', value: 'BIGINT' },
303
+ { label: 'NVARCHAR', value: 'NVARCHAR' },
304
+ { label: 'NVARCHAR(MAX)', value: 'NVARCHAR(MAX)' },
305
+ ]
306
+ const wrapper = mount(RsSelect, {
307
+ props: {
308
+ options: typeOptions,
309
+ modelValue: 'BIGINT',
310
+ searchable: true,
311
+ creatable: true,
312
+ },
313
+ attachTo: document.body,
314
+ })
315
+ const root = wrapper.getComponent({ name: 'ComboboxRoot' })
316
+ expect(root.props('ignoreFilter')).toBe(true)
317
+
318
+ await wrapper.find('.rs-select__trigger').trigger('click')
319
+ await flushPromises()
320
+ const input = document.querySelector('.rs-select__search') as HTMLInputElement | null
321
+ expect(input).toBeTruthy()
322
+ input!.value = 'NVA'
323
+ input!.dispatchEvent(new Event('input', { bubbles: true }))
324
+ await flushPromises()
325
+
326
+ const labels = [...document.querySelectorAll('.rs-select__item-label')].map(
327
+ (el) => el.textContent?.trim() ?? '',
328
+ )
329
+ expect(labels).toContain('NVARCHAR')
330
+ expect(labels).toContain('NVARCHAR(MAX)')
331
+ expect(labels).not.toContain('INT')
332
+ expect(labels).not.toContain('BIGINT')
333
+ expect(document.querySelector('.rs-select__item--create')?.textContent).toContain('NVA')
334
+ wrapper.unmount()
335
+ })
336
+
337
+ it('filterOption false keeps the full list while typing', async () => {
338
+ const wrapper = mount(RsSelect, {
339
+ props: {
340
+ options,
341
+ modelValue: '',
342
+ searchable: true,
343
+ filterOption: false,
344
+ },
345
+ attachTo: document.body,
346
+ })
347
+ await wrapper.find('.rs-select__trigger').trigger('click')
348
+ await flushPromises()
349
+ const input = document.querySelector('.rs-select__search') as HTMLInputElement | null
350
+ input!.value = 'zzz'
351
+ input!.dispatchEvent(new Event('input', { bubbles: true }))
352
+ await flushPromises()
353
+ const labels = [...document.querySelectorAll('.rs-select__item-label')].map(
354
+ (el) => el.textContent?.trim() ?? '',
355
+ )
356
+ expect(labels).toContain('GPT-4o')
357
+ expect(labels).toContain('Claude')
358
+ wrapper.unmount()
359
+ })
360
+
299
361
  it('creatable enables search and commits typed value on Enter', async () => {
300
362
  const wrapper = mount(RsSelect, {
301
363
  props: { options, modelValue: '', creatable: true },
@@ -574,6 +636,40 @@ describe('RsSelect', () => {
574
636
  wrapper.unmount()
575
637
  })
576
638
 
639
+ it('does not fill search with the selected value when opening', async () => {
640
+ const wrapper = mount(RsSelect, {
641
+ props: { options, modelValue: 'claude', searchable: true },
642
+ attachTo: document.body,
643
+ })
644
+ await wrapper.find('.rs-select__trigger').trigger('click')
645
+ await flushPromises()
646
+ const input = document.querySelector('.rs-select__search') as HTMLInputElement | null
647
+ expect(input?.value).toBe('')
648
+ const labels = [...document.querySelectorAll('.rs-select__item-label')].map(
649
+ (el) => el.textContent?.trim() ?? '',
650
+ )
651
+ expect(labels).toContain('GPT-4o')
652
+ expect(labels).toContain('Claude')
653
+ wrapper.unmount()
654
+ })
655
+
656
+ it('fills search with the selected label when fillSearchWithValue', async () => {
657
+ const wrapper = mount(RsSelect, {
658
+ props: { options, modelValue: 'claude', searchable: true, fillSearchWithValue: true },
659
+ attachTo: document.body,
660
+ })
661
+ await wrapper.find('.rs-select__trigger').trigger('click')
662
+ await flushPromises()
663
+ const input = document.querySelector('.rs-select__search') as HTMLInputElement | null
664
+ expect(input?.value).toBe('Claude')
665
+ const labels = [...document.querySelectorAll('.rs-select__item-label')].map(
666
+ (el) => el.textContent?.trim() ?? '',
667
+ )
668
+ expect(labels).toContain('Claude')
669
+ expect(labels).not.toContain('GPT-4o')
670
+ wrapper.unmount()
671
+ })
672
+
577
673
  it('renders dropdownRender instead of the default panel body', async () => {
578
674
  const wrapper = mount(RsSelect, {
579
675
  props: { options, modelValue: '' },
@@ -372,6 +372,23 @@ describe('RsTable', () => {
372
372
  expect(cell.attributes('style')).toContain('width: 80px')
373
373
  })
374
374
 
375
+ it('attaches overflow tooltip on slotted ellipsis columns', () => {
376
+ const wrapper = mount(RsTable, {
377
+ props: {
378
+ columns: [{ key: 'name', title: '名称', ellipsis: true, width: 80 }],
379
+ data: [{ id: '1', name: '很长的名字' }],
380
+ },
381
+ slots: {
382
+ name: ({ row }: { row: { name: string } }) => row.name,
383
+ },
384
+ })
385
+ const tip = wrapper.find('.rs-table__cell-tip')
386
+ expect(tip.exists()).toBe(true)
387
+ expect(tip.attributes('data-rs-table-tip-mode')).toBe('overflow')
388
+ expect(tip.attributes('data-rs-table-tip-text')).toBe('很长的名字')
389
+ wrapper.unmount()
390
+ })
391
+
375
392
  it('shows shared cell tooltip on ellipsis overflow hover', async () => {
376
393
  vi.useFakeTimers()
377
394
  const wrapper = mount(RsTable, {
@@ -1,13 +1,15 @@
1
1
  import { h } from 'vue'
2
2
  import { afterEach, describe, expect, it } from 'vitest'
3
3
  import { flushPromises } from '@vue/test-utils'
4
- import { openRsDialog, rsConfirm } from '../composables/createRsDialog'
4
+ import {
5
+ destroyAllRsDialogHosts,
6
+ openRsDialog,
7
+ rsConfirm,
8
+ } from '../composables/createRsDialog'
5
9
 
6
10
  describe('createRsDialog', () => {
7
11
  afterEach(() => {
8
- document.body.querySelectorAll('.rs-dialog__content, .rs-confirm-dialog__content').forEach((el) => {
9
- el.closest('body > div')?.remove()
10
- })
12
+ destroyAllRsDialogHosts()
11
13
  })
12
14
 
13
15
  it('rsConfirm resolves true when confirm clicked', async () => {
@@ -83,6 +85,17 @@ describe('createRsDialog', () => {
83
85
  await expect(promise).resolves.toBe(true)
84
86
  })
85
87
 
88
+ it('destroy does not throw after portal node was removed', async () => {
89
+ const handle = openRsDialog({
90
+ title: '残留',
91
+ showOverlay: false,
92
+ body: () => 'portal',
93
+ })
94
+ await flushPromises()
95
+ document.body.querySelector('.rs-dialog__content')?.closest('body > div')?.remove()
96
+ expect(() => handle.destroy()).not.toThrow()
97
+ })
98
+
86
99
  it('openRsDialog mounts builtin footer and can destroy', async () => {
87
100
  const handle = openRsDialog({
88
101
  title: '命令式',
@@ -47,13 +47,14 @@ const props = withDefaults(
47
47
  /**
48
48
  * 宽度:`sm` / `md` / `lg`,或 number(px) / `px` / `rem` / `%`。
49
49
  * window:打开时折成像素后居中(`%` 相对视口扣除 inset),之后可拖拽缩放。
50
- * confirm:自定义宽度按 CSS 写入。
50
+ * form / confirm:自定义宽度按 CSS 写入。
51
51
  */
52
52
  width?: RsDialogWidth
53
53
  tone?: RsFeedbackTone
54
54
  /**
55
- * window:可拖拽/缩放工作窗(推荐)。
56
- * confirm:历史兼容的居中轻量窗;新代码的确认/提示请用 RsConfirmDialog。
55
+ * window:可拖拽/缩放工作窗。
56
+ * form:居中轻量表单/说明窗(高度随内容,默认不可缩放)。
57
+ * confirm:历史别名,等同 form;确认/提示请用 RsConfirmDialog。
57
58
  */
58
59
  layout?: RsDialogLayout
59
60
  draggable?: boolean
@@ -143,8 +144,8 @@ const attrs = useAttrs()
143
144
  const slots = useSlots()
144
145
  const { t } = useRsI18n()
145
146
 
146
- const isWindowLayout = computed(() => props.layout === 'window')
147
- const isConfirmLayout = computed(() => props.layout === 'confirm')
147
+ const isCompactLayout = computed(() => props.layout === 'form' || props.layout === 'confirm')
148
+ const isWindowLayout = computed(() => !isCompactLayout.value)
148
149
  const enableDraggable = computed(() => props.draggable && isWindowLayout.value)
149
150
  const enableResizable = computed(() => props.resizable && isWindowLayout.value)
150
151
  const deferBodyMount = computed(() => props.deferBodyMount ?? isWindowLayout.value)
@@ -263,7 +264,8 @@ const contentClass = computed(() => {
263
264
  ? `rs-dialog__content--${props.width}`
264
265
  : 'rs-dialog__content--custom-width'
265
266
  return [
266
- `rs-dialog__content--${props.layout}`,
267
+ isCompactLayout.value ? 'rs-dialog__content--form' : 'rs-dialog__content--window',
268
+ props.layout === 'confirm' ? 'rs-dialog__content--confirm' : undefined,
267
269
  presetClass,
268
270
  `rs-dialog__content--tone-${props.tone}`,
269
271
  {
@@ -368,7 +370,7 @@ const {
368
370
  initialWidth: initialWidthPx,
369
371
  draggable: enableDraggable,
370
372
  resizable: enableResizable,
371
- compact: isConfirmLayout,
373
+ compact: isCompactLayout,
372
374
  boundsTransition: toRef(props, 'boundsTransition'),
373
375
  })
374
376
 
@@ -551,7 +553,8 @@ defineExpose({
551
553
  color: var(--rs-dialog-title-fg);
552
554
  }
553
555
 
554
- /* 亮色 confirm:轻微 vibrancy;window 用实底避免 CEF 下 blur 合成开销 */
556
+ /* 亮色 form:轻微 vibrancy;window 用实底避免 CEF 下 blur 合成开销 */
557
+ [data-rs-theme='light'] .rs-dialog__content--form,
555
558
  [data-rs-theme='light'] .rs-dialog__content--confirm {
556
559
  background: color-mix(in srgb, var(--rs-dialog-bg) 94%, transparent);
557
560
  backdrop-filter: blur(24px) saturate(180%);
@@ -560,10 +563,12 @@ defineExpose({
560
563
  [data-rs-theme='light'] .rs-dialog__content--window {
561
564
  background: var(--rs-dialog-bg);
562
565
  }
563
- /* confirm 布局:缩放 + 淡入(居中,含 translate);苹果 sheet 缓动曲线 */
566
+ /* form 布局:缩放 + 淡入(居中,含 translate);苹果 sheet 缓动曲线 */
567
+ .rs-dialog__content--form[data-state='open'],
564
568
  .rs-dialog__content--confirm[data-state='open'] {
565
569
  animation: rs-dialog-pop-in 240ms cubic-bezier(0.32, 0.72, 0, 1);
566
570
  }
571
+ .rs-dialog__content--form[data-state='closed'],
567
572
  .rs-dialog__content--confirm[data-state='closed'] {
568
573
  animation: rs-dialog-pop-out 180ms ease;
569
574
  }
@@ -10,6 +10,11 @@ import {
10
10
  MONACO_MYSQL_LANGUAGE,
11
11
  MONACO_DAMENG_LANGUAGE,
12
12
  MONACO_KINGBASE_LANGUAGE,
13
+ MONACO_POSTGRESQL_LANGUAGE,
14
+ MONACO_CLICKHOUSE_LANGUAGE,
15
+ MONACO_SQLITE_LSP_LANGUAGE,
16
+ MONACO_SQLSERVER_LANGUAGE,
17
+ MONACO_ORACLE_LANGUAGE,
13
18
  } from '../monaco/languages'
14
19
  /** VS Code 式调试装饰样式(断点 / 当前行);业务组件勿再自定义 glyph CSS */
15
20
  import '../monaco/debug-decorations.css'
@@ -220,11 +225,16 @@ function completionPrefix(line: string): string {
220
225
  }
221
226
 
222
227
  function isManagedSqlDialect(language: string | undefined): boolean {
223
- // mysql / dameng / kingbase:Bridge LSP 接管补全;勿再注册实例级 Provider
228
+ // Bridge LSP 接管补全;勿再注册实例级 Provider / wordBased
224
229
  return (
225
230
  language === MONACO_MYSQL_LANGUAGE ||
226
231
  language === MONACO_DAMENG_LANGUAGE ||
227
- language === MONACO_KINGBASE_LANGUAGE
232
+ language === MONACO_KINGBASE_LANGUAGE ||
233
+ language === MONACO_POSTGRESQL_LANGUAGE ||
234
+ language === MONACO_CLICKHOUSE_LANGUAGE ||
235
+ language === MONACO_SQLITE_LSP_LANGUAGE ||
236
+ language === MONACO_SQLSERVER_LANGUAGE ||
237
+ language === MONACO_ORACLE_LANGUAGE
228
238
  )
229
239
  }
230
240
 
@@ -377,7 +387,7 @@ function initEditor(): void {
377
387
  wordWrap: 'on',
378
388
  bracketPairColorization: { enabled: true },
379
389
  padding: { top: 8, bottom: 8 },
380
- renderLineHighlight: 'gutter',
390
+ renderLineHighlight: 'line',
381
391
  smoothScrolling: true,
382
392
  cursorSmoothCaretAnimation: 'on',
383
393
  folding: true,
@@ -388,7 +398,7 @@ function initEditor(): void {
388
398
  lineDecorationsWidth: props.glyphMargin ? 16 : 4,
389
399
  overviewRulerLanes: 0,
390
400
  hideCursorInOverviewRuler: true,
391
- // Shell:业务 completionRequest;mysql/dameng/kingbase:Bridge LSP;避免 wordBased 抢补全
401
+ // Shell:业务 completionRequest;SQL 方言:Bridge LSP;避免 wordBased 抢补全
392
402
  quickSuggestions: props.language === MONACO_MONGODB_SHELL_LANGUAGE
393
403
  ? { other: true, comments: false, strings: true }
394
404
  : true,
@@ -90,6 +90,9 @@ const props = withDefaults(
90
90
  block?: boolean
91
91
  invalid?: boolean
92
92
  showValidateMessage?: boolean
93
+ /**
94
+ * 自定义过滤;`false` 关闭。默认 `true`(必须显式默认,否则 Vue 会把 boolean 联合类型当成 Boolean prop,未传入即 `false`,可搜索列表永远不过滤)。
95
+ */
93
96
  filterOption?: RsSelectFilterOption | boolean
94
97
  optionFilterProp?: RsSelectOptionFilterProp
95
98
  maxTagCount?: number
@@ -101,6 +104,11 @@ const props = withDefaults(
101
104
  maxTagTooltip?: boolean
102
105
  tokenSeparators?: string[]
103
106
  autoClearSearchValue?: boolean
107
+ /**
108
+ * 打开下拉时把当前选中项写入搜索框并参与过滤。
109
+ * 默认 false:搜索框保持空白(Reka 挂载时会写选中值,需显式清掉)。
110
+ */
111
+ fillSearchWithValue?: boolean
104
112
  showArrow?: boolean
105
113
  listHeight?: number
106
114
  placement?: RsSelectPlacement
@@ -126,11 +134,13 @@ const props = withDefaults(
126
134
  loading: false,
127
135
  matchTriggerWidth: false,
128
136
  block: false,
137
+ filterOption: true,
129
138
  optionFilterProp: 'label',
130
139
  optionLabelProp: 'label',
131
140
  labelInValue: false,
132
141
  maxTagTooltip: true,
133
142
  autoClearSearchValue: true,
143
+ fillSearchWithValue: false,
134
144
  showArrow: true,
135
145
  listHeight: 256,
136
146
  placement: 'bottom',