niuma-ui 1.1.1 → 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,53 @@
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
+
40
+ ## [1.1.3] - 2026-08-14
41
+
42
+ ### 新增
43
+
44
+ - **RsDrawer 内边缘拖拽缩放**:默认 `resizable`(`size=full` 除外)。左右改宽、上下改高,可配 `minSize` / `maxSize`,支持键盘方向键。
45
+
46
+ ### 修复
47
+
48
+ - **RsDrawer 贴边定位**:覆盖 Reka DialogContent 默认居中,避免抽屉被压成标题条。
49
+
50
+ ## [1.1.2] - 2026-08-14
51
+
52
+ ### 修复
53
+
54
+ - **RsIcon 在 pnpm/npm 宿主中为空**:不再用 `import.meta.glob` 扫 `node_modules/@lucide/vue`(该路径在 pnpm 虚拟 store 下不存在)。改为 `import * as LucideVue from '@lucide/vue'`,走正常包解析,宿主安装 `niuma-ui` 即可使用图标。
55
+
9
56
  ## [1.1.1] - 2026-08-14
10
57
 
11
58
  ### 修复
@@ -106,7 +153,11 @@
106
153
 
107
154
  - 1.0 之前的私有 tag(如 `v0.1.0`)仅作历史记录;新接入请依赖 `v1.0.0` 及之后版本。
108
155
 
109
- [Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.1.1...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
159
+ [1.1.3]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.3
160
+ [1.1.2]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.2
110
161
  [1.1.1]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.1
111
162
  [1.1.0]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.0
112
163
  [1.0.2]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.0.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niuma-ui",
3
- "version": "1.1.1",
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: '保存' },
@@ -235,4 +235,52 @@ describe('RsDrawer', () => {
235
235
  expect(closeBtn.querySelector('.rs-btn__tooltip')?.textContent).toContain('Close')
236
236
  wrapper.unmount()
237
237
  })
238
+
239
+ it('renders an inner-edge resize handle by default', async () => {
240
+ const wrapper = mount(RsDrawer, {
241
+ props: { open: true, title: '帮助' },
242
+ attachTo: document.body,
243
+ })
244
+ await flushPromises()
245
+ const handle = document.body.querySelector('.rs-drawer__resize') as HTMLElement | null
246
+ expect(handle).toBeTruthy()
247
+ expect(handle?.getAttribute('aria-orientation')).toBe('vertical')
248
+ wrapper.unmount()
249
+ })
250
+
251
+ it('hides the resize handle when size is full or resizable is false', async () => {
252
+ const full = mount(RsDrawer, {
253
+ props: { open: true, title: '全屏', size: 'full' },
254
+ attachTo: document.body,
255
+ })
256
+ await flushPromises()
257
+ expect(document.body.querySelector('.rs-drawer__resize')).toBeNull()
258
+ full.unmount()
259
+
260
+ const locked = mount(RsDrawer, {
261
+ props: { open: true, title: '固定', resizable: false },
262
+ attachTo: document.body,
263
+ })
264
+ await flushPromises()
265
+ expect(document.body.querySelector('.rs-drawer__resize')).toBeNull()
266
+ locked.unmount()
267
+ })
268
+
269
+ it('widens a right drawer with ArrowLeft on the resize handle', async () => {
270
+ const wrapper = mount(RsDrawer, {
271
+ props: { open: true, title: '缩放', width: 360 },
272
+ attachTo: document.body,
273
+ })
274
+ await flushPromises()
275
+ const content = document.body.querySelector('.rs-drawer__content') as HTMLElement
276
+ Object.defineProperty(content, 'getBoundingClientRect', {
277
+ configurable: true,
278
+ value: () => ({ width: 360, height: 800, top: 0, left: 800, right: 1160, bottom: 800, x: 800, y: 0, toJSON: () => ({}) }),
279
+ })
280
+ const handle = document.body.querySelector('.rs-drawer__resize') as HTMLElement
281
+ await handle.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))
282
+ await flushPromises()
283
+ expect(content.style.width).toBe('376px')
284
+ wrapper.unmount()
285
+ })
238
286
  })
@@ -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
  }