niuma-ui 1.0.0 → 1.0.2

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.
Files changed (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -0,0 +1,170 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { computed, defineComponent, h, nextTick, ref } from 'vue'
3
+ import { mount } from '@vue/test-utils'
4
+ import { useRsTableCore } from '../composables/useRsTableCore'
5
+ import type { RsTableColumn, RsTableRowData } from '../components/table-utils'
6
+
7
+ type Row = RsTableRowData & { id: string; name: string }
8
+
9
+ const columns: RsTableColumn<Row>[] = [{ key: 'name', title: 'Name' }]
10
+
11
+ describe('useRsTableCore performance guards', () => {
12
+ it('选中变化不重绑 analytics feature(避免图表订阅抖动)', async () => {
13
+ const setupCount = { n: 0 }
14
+ const data = ref<Row[]>([
15
+ { id: '1', name: 'a' },
16
+ { id: '2', name: 'b' },
17
+ ])
18
+ const selected = ref<string[]>([])
19
+
20
+ const Host = defineComponent({
21
+ setup() {
22
+ const core = useRsTableCore<Row>({
23
+ columns: () => columns,
24
+ data: () => data.value,
25
+ rowKey: () => 'id',
26
+ compact: () => false,
27
+ size: () => 'md',
28
+ columnOrder: () => undefined,
29
+ defaultColumnOrder: [],
30
+ columnFilters: () => undefined,
31
+ defaultColumnFilters: {},
32
+ initialColumnWidths: () => undefined,
33
+ resizable: () => false,
34
+ columnLayout: () => 'auto',
35
+ sort: () => undefined,
36
+ defaultSort: null,
37
+ sorts: () => undefined,
38
+ defaultSorts: [],
39
+ multiSort: () => false,
40
+ remoteSort: () => false,
41
+ filterText: () => undefined,
42
+ filterKeys: () => undefined,
43
+ groupBy: () => undefined,
44
+ groupLabel: () => undefined,
45
+ treeConfig: () => undefined,
46
+ expandable: () => false,
47
+ rowExpandable: () => undefined,
48
+ selectedRowKeys: () => selected.value,
49
+ defaultSelectedRowKeys: [],
50
+ expandedRowKeys: () => undefined,
51
+ defaultExpandedRowKeys: [],
52
+ rowSelectable: () => undefined,
53
+ height: () => 320,
54
+ layoutActive: () => true,
55
+ viewKey: () => undefined,
56
+ virtual: () => true,
57
+ virtualAutoThreshold: () => 50,
58
+ fill: () => false,
59
+ infinite: () => false,
60
+ virtualOnInfinite: () => true,
61
+ overscan: () => 4,
62
+ expandRowHeight: () => undefined,
63
+ rowHeight: () => undefined,
64
+ selectable: () => true,
65
+ editable: () => false,
66
+ contextMenu: () => true,
67
+ summaryFeatureEnabled: () => false,
68
+ features: () => [
69
+ {
70
+ id: 'probe',
71
+ setup() {
72
+ setupCount.n += 1
73
+ },
74
+ },
75
+ ],
76
+ emit: vi.fn() as any,
77
+ })
78
+ return () => h('div', { 'data-view': core.viewRows.value.length })
79
+ },
80
+ })
81
+
82
+ const wrapper = mount(Host)
83
+ expect(setupCount.n).toBe(1)
84
+
85
+ selected.value = ['1']
86
+ await nextTick()
87
+ selected.value = ['1', '2']
88
+ await nextTick()
89
+ expect(setupCount.n).toBe(1)
90
+
91
+ // data 引用替换不应因选中之外的原因反复拆 feature;仅 features 签名变化才重绑
92
+ data.value = [
93
+ { id: '1', name: 'a' },
94
+ { id: '2', name: 'b' },
95
+ { id: '3', name: 'c' },
96
+ ]
97
+ await nextTick()
98
+ expect(setupCount.n).toBe(1)
99
+
100
+ wrapper.unmount()
101
+ })
102
+
103
+ it('viewRows 与 dataRows 同源同趟(引用长度一致)', () => {
104
+ const Host = defineComponent({
105
+ setup() {
106
+ const core = useRsTableCore<Row>({
107
+ columns: () => columns,
108
+ data: () => [
109
+ { id: '1', name: 'a' },
110
+ { id: '2', name: 'b' },
111
+ ],
112
+ rowKey: () => 'id',
113
+ compact: () => false,
114
+ size: () => 'md',
115
+ columnOrder: () => undefined,
116
+ defaultColumnOrder: [],
117
+ columnFilters: () => undefined,
118
+ defaultColumnFilters: {},
119
+ initialColumnWidths: () => undefined,
120
+ resizable: () => false,
121
+ columnLayout: () => 'auto',
122
+ sort: () => undefined,
123
+ defaultSort: null,
124
+ sorts: () => undefined,
125
+ defaultSorts: [],
126
+ multiSort: () => false,
127
+ remoteSort: () => false,
128
+ filterText: () => undefined,
129
+ filterKeys: () => undefined,
130
+ groupBy: () => undefined,
131
+ groupLabel: () => undefined,
132
+ treeConfig: () => undefined,
133
+ expandable: () => false,
134
+ rowExpandable: () => undefined,
135
+ selectedRowKeys: () => undefined,
136
+ defaultSelectedRowKeys: [],
137
+ expandedRowKeys: () => undefined,
138
+ defaultExpandedRowKeys: [],
139
+ rowSelectable: () => undefined,
140
+ height: () => 320,
141
+ layoutActive: () => true,
142
+ viewKey: () => undefined,
143
+ virtual: () => false,
144
+ virtualAutoThreshold: () => 50,
145
+ fill: () => false,
146
+ infinite: () => false,
147
+ virtualOnInfinite: () => true,
148
+ overscan: () => 4,
149
+ expandRowHeight: () => undefined,
150
+ rowHeight: () => undefined,
151
+ selectable: () => false,
152
+ editable: () => false,
153
+ contextMenu: () => true,
154
+ summaryFeatureEnabled: () => false,
155
+ features: () => undefined,
156
+ emit: vi.fn() as any,
157
+ })
158
+ const ok = computed(
159
+ () =>
160
+ core.viewRows.value.length === core.dataRows.value.length &&
161
+ core.viewRows.value[0] === core.dataRows.value[0]?.row,
162
+ )
163
+ return () => h('div', { 'data-ok': String(ok.value) })
164
+ },
165
+ })
166
+ const wrapper = mount(Host)
167
+ expect(wrapper.attributes('data-ok')).toBe('true')
168
+ wrapper.unmount()
169
+ })
170
+ })
@@ -0,0 +1,212 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useRsI18n } from '../composables/useRsI18n'
4
+ import type { RsFeedbackTone } from './overlay-utils'
5
+ import RsIcon from './RsIcon.vue'
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ type?: RsFeedbackTone
10
+ title?: string
11
+ closable?: boolean
12
+ bordered?: boolean
13
+ }>(),
14
+ {
15
+ type: 'info',
16
+ closable: false,
17
+ bordered: true,
18
+ },
19
+ )
20
+
21
+ const emit = defineEmits<{
22
+ close: [event: MouseEvent]
23
+ }>()
24
+
25
+ const { t } = useRsI18n()
26
+
27
+ const iconName = computed(() => {
28
+ switch (props.type) {
29
+ case 'success':
30
+ return 'circle-check'
31
+ case 'warning':
32
+ return 'triangle-alert'
33
+ case 'danger':
34
+ return 'circle-alert'
35
+ case 'default':
36
+ return 'info'
37
+ default:
38
+ return 'info'
39
+ }
40
+ })
41
+
42
+ const rootClass = computed(() => [
43
+ 'rs-alert',
44
+ `rs-alert--${props.type}`,
45
+ {
46
+ 'rs-alert--bordered': props.bordered,
47
+ 'rs-alert--closable': props.closable,
48
+ },
49
+ ])
50
+
51
+ function onClose(event: MouseEvent): void {
52
+ emit('close', event)
53
+ }
54
+ </script>
55
+
56
+ <template>
57
+ <div :class="rootClass" role="alert">
58
+ <span class="rs-alert__icon" aria-hidden="true">
59
+ <slot name="icon">
60
+ <RsIcon :name="iconName" :size="16" />
61
+ </slot>
62
+ </span>
63
+ <div class="rs-alert__body">
64
+ <div v-if="title || $slots.title" class="rs-alert__title">
65
+ <slot name="title">{{ title }}</slot>
66
+ </div>
67
+ <div v-if="$slots.default" class="rs-alert__content">
68
+ <slot />
69
+ </div>
70
+ </div>
71
+ <button
72
+ v-if="closable"
73
+ type="button"
74
+ class="rs-alert__close"
75
+ :aria-label="t('alert.close')"
76
+ @click="onClose"
77
+ >
78
+ <RsIcon name="x" :size="14" />
79
+ </button>
80
+ </div>
81
+ </template>
82
+
83
+ <style scoped>
84
+ .rs-alert {
85
+ display: flex;
86
+ align-items: flex-start;
87
+ gap: var(--rs-alert-gap, var(--rs-space-sm));
88
+ padding: var(--rs-alert-padding-y, var(--rs-space-md))
89
+ var(--rs-alert-padding-x, var(--rs-space-lg));
90
+ border-radius: var(--rs-alert-radius, var(--rs-radius));
91
+ box-sizing: border-box;
92
+ }
93
+
94
+ .rs-alert--bordered {
95
+ border: 1px solid transparent;
96
+ }
97
+
98
+ .rs-alert__icon {
99
+ display: inline-flex;
100
+ flex-shrink: 0;
101
+ margin-top: 0.125rem;
102
+ }
103
+
104
+ .rs-alert__body {
105
+ flex: 1 1 auto;
106
+ min-width: 0;
107
+ }
108
+
109
+ .rs-alert__title {
110
+ font-size: var(--rs-font-size-sm);
111
+ font-weight: 600;
112
+ line-height: var(--rs-line-height-tight);
113
+ color: inherit;
114
+ }
115
+
116
+ .rs-alert__content {
117
+ margin-top: var(--rs-space-xs);
118
+ font-size: var(--rs-font-size-sm);
119
+ line-height: var(--rs-line-height-normal);
120
+ color: var(--rs-alert-content-fg, var(--rs-muted));
121
+ }
122
+
123
+ .rs-alert__close {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ margin: 0;
128
+ padding: 0.125rem;
129
+ border: 0;
130
+ border-radius: var(--rs-radius-sm);
131
+ background: transparent;
132
+ color: inherit;
133
+ cursor: pointer;
134
+ opacity: 0.7;
135
+ outline: none;
136
+ }
137
+
138
+ .rs-alert__close:hover {
139
+ opacity: 1;
140
+ background: color-mix(in srgb, currentColor 10%, transparent);
141
+ }
142
+
143
+ .rs-alert__close:focus-visible {
144
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
145
+ }
146
+
147
+ .rs-alert--default {
148
+ background: var(--rs-surface-hover);
149
+ color: var(--rs-text);
150
+ border-color: var(--rs-border);
151
+ }
152
+
153
+ .rs-alert--default .rs-alert__content {
154
+ color: var(--rs-alert-content-fg, var(--rs-muted));
155
+ }
156
+
157
+ .rs-alert--info {
158
+ background: var(--rs-info-container);
159
+ color: var(--rs-on-info-container);
160
+ border-color: color-mix(
161
+ in srgb,
162
+ var(--rs-info) var(--rs-alert-border-alpha, 35%),
163
+ transparent
164
+ );
165
+ }
166
+
167
+ .rs-alert--info .rs-alert__content {
168
+ color: color-mix(in srgb, var(--rs-on-info-container) 78%, transparent);
169
+ }
170
+
171
+ .rs-alert--success {
172
+ background: var(--rs-success-container);
173
+ color: var(--rs-on-success-container);
174
+ border-color: color-mix(
175
+ in srgb,
176
+ var(--rs-success) var(--rs-alert-border-alpha, 35%),
177
+ transparent
178
+ );
179
+ }
180
+
181
+ .rs-alert--success .rs-alert__content {
182
+ color: color-mix(in srgb, var(--rs-on-success-container) 78%, transparent);
183
+ }
184
+
185
+ .rs-alert--warning {
186
+ background: var(--rs-warning-container);
187
+ color: var(--rs-on-warning-container);
188
+ border-color: color-mix(
189
+ in srgb,
190
+ var(--rs-warning) var(--rs-alert-border-alpha, 35%),
191
+ transparent
192
+ );
193
+ }
194
+
195
+ .rs-alert--warning .rs-alert__content {
196
+ color: color-mix(in srgb, var(--rs-on-warning-container) 78%, transparent);
197
+ }
198
+
199
+ .rs-alert--danger {
200
+ background: var(--rs-danger-container);
201
+ color: var(--rs-on-danger-container);
202
+ border-color: color-mix(
203
+ in srgb,
204
+ var(--rs-danger) var(--rs-alert-border-alpha, 35%),
205
+ transparent
206
+ );
207
+ }
208
+
209
+ .rs-alert--danger .rs-alert__content {
210
+ color: color-mix(in srgb, var(--rs-on-danger-container) 78%, transparent);
211
+ }
212
+ </style>
@@ -2,19 +2,34 @@
2
2
  import { computed, nextTick, ref, useSlots, watch } from 'vue'
3
3
  import type { RsComponentSize, RsRadius } from '../theme/types'
4
4
  import { RS_COMPONENT_SIZE_ICON_PX } from '../theme/types'
5
+ import {
6
+ supportsRsButtonTone,
7
+ type RsButtonTone,
8
+ type RsButtonVariant,
9
+ } from './button-utils'
5
10
  import { useResolvedRsComponentSize } from './resolve-size'
6
11
  import { rsRadiusCss, useResolvedRsRadius } from './resolve-radius'
7
12
  import RsIcon from './RsIcon.vue'
8
13
 
14
+ export type { RsButtonTone, RsButtonVariant } from './button-utils'
15
+
9
16
  const props = withDefaults(
10
17
  defineProps<{
11
- variant?: 'primary' | 'default' | 'secondary' | 'ghost' | 'danger' | 'link'
18
+ variant?: RsButtonVariant
19
+ /**
20
+ * 语义色调:配合 text / ghost / link 使用。
21
+ * 例:variant="text" tone="primary" → 紫色文字按钮(工具栏 quaternary)。
22
+ * @default 'neutral'(text/ghost);link 未传 tone 时保持主色链接观感
23
+ */
24
+ tone?: RsButtonTone
12
25
  size?: RsComponentSize
13
26
  /** 圆角档位;默认 full(胶囊)。直角 UI 传 `none`。 */
14
27
  radius?: RsRadius
15
28
  type?: 'button' | 'submit' | 'reset'
16
29
  disabled?: boolean
17
30
  loading?: boolean
31
+ /** 是否显示外边框;text 变体默认无边框 */
32
+ bordered?: boolean
18
33
  /** 前缀图标(Lucide kebab-case 名称) */
19
34
  icon?: string
20
35
  iconSize?: number
@@ -50,7 +65,36 @@ const rootStyle = computed(() => ({
50
65
 
51
66
  const hasLabel = computed(() => Boolean(slots.default))
52
67
 
53
- const resolvedVariant = computed(() => (props.variant === 'secondary' ? 'default' : props.variant))
68
+ const resolvedVariant = computed(() =>
69
+ props.variant === 'secondary' ? ('default' as const) : (props.variant ?? 'primary'),
70
+ )
71
+
72
+ /** text 默认无边框;其它变体默认有边框(可由 bordered 覆盖) */
73
+ const resolvedBordered = computed(() => {
74
+ if (props.bordered !== undefined) return props.bordered
75
+ return resolvedVariant.value !== 'text' && resolvedVariant.value !== 'link'
76
+ })
77
+
78
+ /**
79
+ * 可着色变体上的 tone。
80
+ * link 未显式传 tone 时视为 primary,保持历史链接主色。
81
+ */
82
+ const resolvedTone = computed<RsButtonTone | null>(() => {
83
+ if (!supportsRsButtonTone(resolvedVariant.value)) return null
84
+ if (props.tone) return props.tone
85
+ if (resolvedVariant.value === 'link') return 'primary'
86
+ return 'neutral'
87
+ })
88
+
89
+ const rootClass = computed(() => ({
90
+ [`rs-btn--${resolvedVariant.value}`]: true,
91
+ [`rs-btn--${resolvedSize.value}`]: true,
92
+ [`rs-btn--tone-${resolvedTone.value}`]: Boolean(resolvedTone.value),
93
+ 'rs-btn--icon-only': props.iconOnly,
94
+ 'rs-btn--borderless': !resolvedBordered.value,
95
+ 'rs-btn--reveal-label': props.revealLabel && props.icon && hasLabel.value,
96
+ 'rs-btn--loading': props.loading,
97
+ }))
54
98
 
55
99
  /** 仅在有 tooltip 文案或 icon-only 默认插槽文案时展示内置悬浮层 */
56
100
  const showFloatingText = computed(
@@ -104,13 +148,7 @@ watch(
104
148
  :type="type"
105
149
  :disabled="disabled"
106
150
  class="rs-btn"
107
- :class="{
108
- [`rs-btn--${resolvedVariant}`]: true,
109
- [`rs-btn--${resolvedSize}`]: true,
110
- 'rs-btn--icon-only': iconOnly,
111
- 'rs-btn--reveal-label': revealLabel && icon && hasLabel,
112
- 'rs-btn--loading': loading,
113
- }"
151
+ :class="rootClass"
114
152
  :style="rootStyle"
115
153
  :aria-busy="loading || undefined"
116
154
  :aria-disabled="disabled || loading || undefined"
@@ -160,7 +198,7 @@ watch(
160
198
  font-size: var(--rs-font-size-sm);
161
199
  font-weight: 500;
162
200
  letter-spacing: 0.01em;
163
- line-height: var(--rs-line-height-tight);
201
+ line-height: 1;
164
202
  white-space: nowrap;
165
203
  cursor: pointer;
166
204
  transition:
@@ -207,8 +245,9 @@ watch(
207
245
  border-color: var(--rs-border);
208
246
  background: var(--rs-surface);
209
247
  }
210
- .rs-btn--loading.rs-btn--ghost {
211
- color: var(--rs-text);
248
+ .rs-btn--loading.rs-btn--ghost,
249
+ .rs-btn--loading.rs-btn--text {
250
+ color: var(--rs-btn-tone, var(--rs-text));
212
251
  background: var(--rs-surface-hover);
213
252
  }
214
253
  .rs-btn:focus-visible {
@@ -241,27 +280,35 @@ watch(
241
280
  box-shadow: none;
242
281
  }
243
282
  .rs-btn--default {
244
- background: var(--rs-surface);
245
- border-color: var(--rs-border);
283
+ border: 1px solid var(--rs-btn-outline-border, var(--rs-border));
284
+ background: var(--rs-btn-secondary-bg, var(--rs-surface-hover));
246
285
  color: var(--rs-text);
247
- box-shadow: var(--rs-shadow-sm);
286
+ box-shadow: none;
248
287
  }
249
288
  .rs-btn--default:hover:not(:disabled) {
250
289
  color: var(--rs-primary);
251
290
  border-color: var(--rs-primary);
291
+ background: var(--rs-btn-secondary-bg-hover, var(--rs-primary-container));
252
292
  }
253
293
  .rs-btn--default:active:not(:disabled) {
254
294
  color: var(--rs-primary-hover);
255
295
  border-color: var(--rs-primary-hover);
296
+ background: var(--rs-btn-secondary-bg-active, var(--rs-primary-container));
256
297
  }
257
298
  .rs-btn--ghost {
258
299
  background: transparent;
259
- border-color: var(--rs-border);
260
- color: var(--rs-text);
300
+ border: 1px solid var(--rs-btn-outline-border, var(--rs-border));
301
+ color: var(--rs-btn-tone, var(--rs-text));
261
302
  }
262
303
  .rs-btn--ghost:hover:not(:disabled) {
263
- background: var(--rs-surface-hover);
264
- border-color: var(--rs-border);
304
+ background: color-mix(in srgb, var(--rs-btn-tone, var(--rs-text)) 8%, transparent);
305
+ border-color: var(--rs-btn-outline-border, var(--rs-border));
306
+ color: var(--rs-btn-tone-hover, var(--rs-btn-tone, var(--rs-text)));
307
+ }
308
+ .rs-btn--borderless,
309
+ .rs-btn--borderless:hover:not(:disabled),
310
+ .rs-btn--borderless:active:not(:disabled) {
311
+ border-color: transparent;
265
312
  }
266
313
  .rs-btn--danger {
267
314
  background: var(--rs-danger-container);
@@ -272,19 +319,80 @@ watch(
272
319
  border-color: var(--rs-danger);
273
320
  background: color-mix(in srgb, var(--rs-danger) 20%, var(--rs-danger-container));
274
321
  }
322
+
323
+ /* 行内链接:可下划线;色调由 tone 控制(默认 primary) */
275
324
  .rs-btn--link {
276
325
  min-height: auto;
277
326
  padding: 0;
278
327
  border-color: transparent;
279
328
  border-radius: 0;
280
329
  background: transparent;
281
- color: var(--rs-primary);
330
+ color: var(--rs-btn-tone, var(--rs-primary));
282
331
  box-shadow: none;
332
+ font-weight: 500;
283
333
  }
284
334
  .rs-btn--link:hover:not(:disabled) {
285
- color: var(--rs-primary-hover);
335
+ color: var(--rs-btn-tone-hover, var(--rs-primary-hover));
286
336
  text-decoration: underline;
287
337
  }
338
+
339
+ /**
340
+ * text:工具栏 / 表格操作 quaternary
341
+ * 无底无边,靠 tone 着色;hover 使用同色浅底
342
+ */
343
+ .rs-btn--text {
344
+ min-height: var(--rs-control-height-sm);
345
+ padding: 0 var(--rs-space-sm);
346
+ border-color: transparent;
347
+ background: transparent;
348
+ box-shadow: none;
349
+ font-weight: 400;
350
+ color: var(--rs-btn-tone, var(--rs-text));
351
+ }
352
+ .rs-btn--text.rs-btn--ssm {
353
+ min-height: var(--rs-control-height-ssm);
354
+ }
355
+ .rs-btn--text.rs-btn--md {
356
+ min-height: var(--rs-control-height-md);
357
+ }
358
+ .rs-btn--text.rs-btn--lg {
359
+ min-height: var(--rs-control-height-lg);
360
+ }
361
+ .rs-btn--text:hover:not(:disabled) {
362
+ background: color-mix(in srgb, var(--rs-btn-tone, var(--rs-text)) 8%, transparent);
363
+ border-color: transparent;
364
+ color: var(--rs-btn-tone-hover, var(--rs-btn-tone, var(--rs-text)));
365
+ }
366
+ .rs-btn--text:active:not(:disabled) {
367
+ background: color-mix(in srgb, var(--rs-btn-tone, var(--rs-text)) 12%, transparent);
368
+ }
369
+
370
+ /* tone:写入 CSS 变量,供 text / ghost / link 消费 */
371
+ .rs-btn--tone-neutral {
372
+ --rs-btn-tone: var(--rs-text);
373
+ --rs-btn-tone-hover: var(--rs-text);
374
+ }
375
+ .rs-btn--tone-primary {
376
+ --rs-btn-tone: var(--rs-primary);
377
+ --rs-btn-tone-hover: var(--rs-primary-hover);
378
+ }
379
+ .rs-btn--tone-danger {
380
+ --rs-btn-tone: var(--rs-danger);
381
+ --rs-btn-tone-hover: var(--rs-danger);
382
+ }
383
+ .rs-btn--tone-success {
384
+ --rs-btn-tone: var(--rs-success, #18a058);
385
+ --rs-btn-tone-hover: var(--rs-success, #18a058);
386
+ }
387
+ .rs-btn--tone-warning {
388
+ --rs-btn-tone: var(--rs-warning, #f0a020);
389
+ --rs-btn-tone-hover: var(--rs-warning, #f0a020);
390
+ }
391
+ .rs-btn--tone-info {
392
+ --rs-btn-tone: var(--rs-info, #2080f0);
393
+ --rs-btn-tone-hover: var(--rs-info, #2080f0);
394
+ }
395
+
288
396
  .rs-btn__spinner {
289
397
  display: inline-flex;
290
398
  align-items: center;
@@ -309,6 +417,11 @@ watch(
309
417
  border-color: color-mix(in srgb, var(--rs-primary) 28%, transparent);
310
418
  border-top-color: var(--rs-primary);
311
419
  }
420
+ .rs-btn--text .rs-btn__spinner-ring,
421
+ .rs-btn--link .rs-btn__spinner-ring {
422
+ border-color: color-mix(in srgb, var(--rs-btn-tone, var(--rs-primary)) 28%, transparent);
423
+ border-top-color: var(--rs-btn-tone, var(--rs-primary));
424
+ }
312
425
  .rs-btn--primary .rs-btn__spinner-ring {
313
426
  border-color: color-mix(in srgb, currentcolor 35%, transparent);
314
427
  border-top-color: currentcolor;
@@ -318,9 +431,20 @@ watch(
318
431
  border-top-color: var(--rs-danger);
319
432
  }
320
433
  .rs-btn__icon {
434
+ display: inline-flex;
321
435
  flex-shrink: 0;
436
+ align-items: center;
437
+ justify-content: center;
438
+ line-height: 1;
439
+ color: inherit;
322
440
  }
441
+ /* 插槽内自定义图标+文案时也能水平对齐 */
323
442
  .rs-btn__label {
443
+ display: inline-flex;
444
+ align-items: center;
445
+ gap: inherit;
446
+ min-width: 0;
447
+ line-height: 1;
324
448
  white-space: nowrap;
325
449
  }
326
450
  .rs-btn__label--reveal {