niuma-ui 1.3.8 → 1.3.9

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.
@@ -0,0 +1,188 @@
1
+ import { useRsI18n } from "../composables/useRsI18n.js";
2
+ import { computeScrollTopForTarget, flattenAnchorItems, hrefToAnchorId, pickActiveAnchorHref, resolveAnchorContainer, scrollContainerTo, targetTopInContainer } from "./anchor-utils.js";
3
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, defineComponent, mergeModels, nextTick, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, ref, renderList, toDisplayString, unref, useModel, watch } from "vue";
4
+ //#region src/components/RsAnchor.vue?vue&type=script&setup=true&lang.ts
5
+ var _hoisted_1 = ["aria-label"];
6
+ var _hoisted_2 = {
7
+ key: 0,
8
+ class: "rs-anchor__rail",
9
+ "aria-hidden": "true"
10
+ };
11
+ var _hoisted_3 = { class: "rs-anchor__list" };
12
+ var _hoisted_4 = [
13
+ "href",
14
+ "data-rs-anchor-href",
15
+ "onClick"
16
+ ];
17
+ var RsAnchor_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
18
+ name: "RsAnchor",
19
+ __name: "RsAnchor",
20
+ props: /*@__PURE__*/ mergeModels({
21
+ items: {},
22
+ offset: { default: 12 },
23
+ targetOffset: {},
24
+ bounds: { default: 8 },
25
+ affix: {
26
+ type: Boolean,
27
+ default: true
28
+ },
29
+ lineless: {
30
+ type: Boolean,
31
+ default: false
32
+ },
33
+ changeHash: {
34
+ type: Boolean,
35
+ default: true
36
+ },
37
+ getContainer: {}
38
+ }, {
39
+ "modelValue": { default: "" },
40
+ "modelModifiers": {}
41
+ }),
42
+ emits: /*@__PURE__*/ mergeModels(["change", "click"], ["update:modelValue"]),
43
+ setup(__props, { emit: __emit }) {
44
+ const props = __props;
45
+ const activeHref = useModel(__props, "modelValue");
46
+ const emit = __emit;
47
+ const { t } = useRsI18n();
48
+ const rootRef = ref(null);
49
+ const inkStyle = ref({});
50
+ const flatItems = computed(() => flattenAnchorItems(props.items));
51
+ const resolvedTargetOffset = computed(() => props.targetOffset ?? props.offset);
52
+ let scrollTarget = null;
53
+ let ticking = false;
54
+ function findTarget(href) {
55
+ const id = hrefToAnchorId(href);
56
+ if (!id || typeof document === "undefined") return null;
57
+ return document.getElementById(id);
58
+ }
59
+ function findLinkByHref(root, href) {
60
+ const links = root.querySelectorAll("[data-rs-anchor-href]");
61
+ for (const link of links) if (link.dataset.rsAnchorHref === href) return link;
62
+ return null;
63
+ }
64
+ function collectTops(container) {
65
+ return flatItems.value.map((item) => {
66
+ const el = findTarget(item.href);
67
+ if (!el) return null;
68
+ return {
69
+ href: item.href,
70
+ top: targetTopInContainer(el, container)
71
+ };
72
+ }).filter((entry) => Boolean(entry));
73
+ }
74
+ function setActive(href) {
75
+ if (!href || href === activeHref.value) return;
76
+ activeHref.value = href;
77
+ emit("change", href);
78
+ }
79
+ function syncInk() {
80
+ const root = rootRef.value;
81
+ if (!root || props.lineless) return;
82
+ const current = activeHref.value || flatItems.value[0]?.href;
83
+ if (!current) {
84
+ inkStyle.value = { opacity: "0" };
85
+ return;
86
+ }
87
+ const link = findLinkByHref(root, current);
88
+ if (!link) {
89
+ inkStyle.value = { opacity: "0" };
90
+ return;
91
+ }
92
+ inkStyle.value = {
93
+ opacity: "1",
94
+ transform: `translateY(${link.offsetTop}px)`,
95
+ height: `${link.offsetHeight}px`
96
+ };
97
+ }
98
+ function syncFromScroll() {
99
+ if (!scrollTarget) return;
100
+ const next = pickActiveAnchorHref(collectTops(scrollTarget), props.offset + props.bounds);
101
+ if (next) setActive(next);
102
+ nextTick(syncInk);
103
+ }
104
+ function onScroll() {
105
+ if (ticking) return;
106
+ ticking = true;
107
+ requestAnimationFrame(() => {
108
+ ticking = false;
109
+ syncFromScroll();
110
+ });
111
+ }
112
+ function bindContainer() {
113
+ unbindContainer();
114
+ try {
115
+ scrollTarget = resolveAnchorContainer(props.getContainer);
116
+ } catch {
117
+ scrollTarget = null;
118
+ return;
119
+ }
120
+ scrollTarget.addEventListener("scroll", onScroll, { passive: true });
121
+ syncFromScroll();
122
+ }
123
+ function unbindContainer() {
124
+ scrollTarget?.removeEventListener("scroll", onScroll);
125
+ scrollTarget = null;
126
+ }
127
+ function onSelect(href, event) {
128
+ event.preventDefault();
129
+ emit("click", href, event);
130
+ const target = findTarget(href);
131
+ const container = scrollTarget ?? resolveAnchorContainer(props.getContainer);
132
+ if (target) scrollContainerTo(container, computeScrollTopForTarget(target, container, resolvedTargetOffset.value));
133
+ setActive(href);
134
+ if (props.changeHash && typeof history !== "undefined") {
135
+ const id = hrefToAnchorId(href);
136
+ const url = new URL(window.location.href);
137
+ url.hash = id;
138
+ history.replaceState(history.state, "", `${url.pathname}${url.search}#${id}`);
139
+ }
140
+ nextTick(syncInk);
141
+ }
142
+ onMounted(() => {
143
+ bindContainer();
144
+ window.addEventListener("resize", onScroll, { passive: true });
145
+ });
146
+ onUnmounted(() => {
147
+ unbindContainer();
148
+ window.removeEventListener("resize", onScroll);
149
+ });
150
+ watch(() => [
151
+ props.items,
152
+ props.getContainer,
153
+ props.offset,
154
+ props.bounds
155
+ ], () => {
156
+ nextTick(bindContainer);
157
+ }, { deep: true });
158
+ watch(activeHref, () => {
159
+ nextTick(syncInk);
160
+ });
161
+ return (_ctx, _cache) => {
162
+ return flatItems.value.length ? (openBlock(), createElementBlock("nav", {
163
+ key: 0,
164
+ ref_key: "rootRef",
165
+ ref: rootRef,
166
+ class: normalizeClass(["rs-anchor", {
167
+ "rs-anchor--affix": __props.affix,
168
+ "rs-anchor--lineless": __props.lineless
169
+ }]),
170
+ "aria-label": unref(t)("anchor.label")
171
+ }, [!__props.lineless ? (openBlock(), createElementBlock("div", _hoisted_2, [createElementVNode("span", {
172
+ class: "rs-anchor__ink",
173
+ style: normalizeStyle(inkStyle.value)
174
+ }, null, 4)])) : createCommentVNode("", true), createElementVNode("div", _hoisted_3, [(openBlock(true), createElementBlock(Fragment, null, renderList(flatItems.value, (item) => {
175
+ return openBlock(), createElementBlock("a", {
176
+ key: item.href,
177
+ class: normalizeClass(["rs-anchor__link", { "rs-anchor__link--active": item.href === activeHref.value }]),
178
+ style: normalizeStyle({ paddingInlineStart: `${.7 + item.depth * .75}rem` }),
179
+ href: item.href,
180
+ "data-rs-anchor-href": item.href,
181
+ onClick: ($event) => onSelect(item.href, $event)
182
+ }, toDisplayString(item.title), 15, _hoisted_4);
183
+ }), 128))])], 10, _hoisted_1)) : createCommentVNode("", true);
184
+ };
185
+ }
186
+ });
187
+ //#endregion
188
+ export { RsAnchor_vue_vue_type_script_setup_true_lang_default as default };
@@ -0,0 +1,8 @@
1
+ import './RsAnchor.css'
2
+ import _plugin_vue_export_helper_default from "../_virtual/_plugin-vue_export-helper.js";
3
+ import RsAnchor_vue_vue_type_script_setup_true_lang_default from "./RsAnchor.impl.js";
4
+
5
+ //#region src/components/RsAnchor.vue
6
+ var RsAnchor_default = /*#__PURE__*/ _plugin_vue_export_helper_default(RsAnchor_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-d8c4280b"]]);
7
+ //#endregion
8
+ export { RsAnchor_default as default };
@@ -1,5 +1,5 @@
1
1
 
2
- .rs-terminal[data-v-db06bdc1] {
2
+ .rs-terminal[data-v-27605f60] {
3
3
  position: relative;
4
4
  height: 100%;
5
5
  min-height: 0;
@@ -16,13 +16,13 @@
16
16
  0 1px 3px color-mix(in srgb, #000 32%, transparent);
17
17
  overflow: hidden;
18
18
  }
19
- [data-rs-theme='light'] .rs-terminal[data-v-db06bdc1] {
19
+ [data-rs-theme='light'] .rs-terminal[data-v-27605f60] {
20
20
  box-shadow:
21
21
  inset 0 1px 0 color-mix(in srgb, #fff 80%, transparent),
22
22
  inset 0 0 0 1px color-mix(in srgb, #000 4%, transparent),
23
23
  0 1px 4px color-mix(in srgb, #000 8%, transparent);
24
24
  }
25
- .rs-terminal__host[data-v-db06bdc1] {
25
+ .rs-terminal__host[data-v-27605f60] {
26
26
  position: relative;
27
27
  width: 100%;
28
28
  height: 100%;
@@ -30,15 +30,15 @@
30
30
  /* FitAddon 取整余量露底时,与单元格同色(勿用 shell 灰底) */
31
31
  background: var(--rs-terminal-bg);
32
32
  }
33
- .rs-terminal__loading[data-v-db06bdc1],
34
- .rs-terminal__overlay[data-v-db06bdc1] {
33
+ .rs-terminal__loading[data-v-27605f60],
34
+ .rs-terminal__overlay[data-v-27605f60] {
35
35
  position: absolute;
36
36
  inset: 0;
37
37
  display: flex;
38
38
  align-items: center;
39
39
  justify-content: center;
40
40
  }
41
- .rs-terminal__overlay[data-v-db06bdc1] {
41
+ .rs-terminal__overlay[data-v-27605f60] {
42
42
  flex-direction: column;
43
43
  gap: var(--rs-space-sm);
44
44
  padding: var(--rs-space-lg);
@@ -51,13 +51,13 @@
51
51
  font-weight: var(--rs-font-weight-regular);
52
52
  line-height: var(--rs-line-height-normal);
53
53
  }
54
- .rs-terminal__overlay--interactive[data-v-db06bdc1] {
54
+ .rs-terminal__overlay--interactive[data-v-27605f60] {
55
55
  pointer-events: auto;
56
56
  }
57
- .rs-terminal__overlay-action[data-v-db06bdc1] {
57
+ .rs-terminal__overlay-action[data-v-27605f60] {
58
58
  pointer-events: auto;
59
59
  }
60
- .rs-terminal__search[data-v-db06bdc1] {
60
+ .rs-terminal__search[data-v-27605f60] {
61
61
  position: absolute;
62
62
  top: var(--rs-space-xs);
63
63
  right: var(--rs-space-xs);
@@ -70,7 +70,7 @@
70
70
  border-radius: var(--rs-radius-sm);
71
71
  background: color-mix(in srgb, var(--rs-terminal-bg) 92%, transparent);
72
72
  }
73
- .rs-terminal__search-input[data-v-db06bdc1] {
73
+ .rs-terminal__search-input[data-v-27605f60] {
74
74
  width: 12rem;
75
75
  padding: 0.2rem 0.4rem;
76
76
  border: 1px solid var(--rs-terminal-border);
@@ -81,7 +81,7 @@
81
81
  font-size: var(--rs-font-size-xs);
82
82
  outline: none;
83
83
  }
84
- .rs-terminal__search-btn[data-v-db06bdc1] {
84
+ .rs-terminal__search-btn[data-v-27605f60] {
85
85
  padding: 0.2rem 0.4rem;
86
86
  border: 1px solid var(--rs-terminal-border);
87
87
  border-radius: var(--rs-radius-sm);
@@ -98,7 +98,7 @@
98
98
  * DOM 渲染器于是按 `cellWidth - 测量值` 给每个字符补正字距,行盒却仍是 cols × cellWidth,
99
99
  * 逐列累积后整行溢出十几到几十像素,被行盒的 overflow:hidden 切掉行尾几个字符。
100
100
  */
101
- .rs-terminal[data-v-db06bdc1] .xterm {
101
+ .rs-terminal[data-v-27605f60] .xterm {
102
102
  width: 100%;
103
103
  height: 100%;
104
104
  line-height: normal;
@@ -109,7 +109,7 @@
109
109
  * xterm 6 官方仍给空的 .xterm-viewport 写 overflow-y:scroll + 背景 #000。
110
110
  * 真正滚动的是 .xterm-scrollable-element;空层滚动条会挡住最后一列。
111
111
  */
112
- .rs-terminal[data-v-db06bdc1] .xterm .xterm-viewport {
112
+ .rs-terminal[data-v-27605f60] .xterm .xterm-viewport {
113
113
  overflow: hidden;
114
114
  background-color: transparent;
115
115
  }
@@ -119,7 +119,7 @@
119
119
  * 的圆角胶囊不一致。轨道宽度由 overviewRuler.width 决定,改它会给每个终端实例多建一块
120
120
  * overview ruler 画布,因此只重绘滑块:热区仍是 14px,靠透明边框内缩出 10px 可见胶囊。
121
121
  */
122
- .rs-terminal[data-v-db06bdc1] .xterm-scrollable-element > .scrollbar > .slider {
122
+ .rs-terminal[data-v-27605f60] .xterm-scrollable-element > .scrollbar > .slider {
123
123
  box-sizing: border-box;
124
124
  border: var(--rs-scrollbar-padding, 2px) solid transparent;
125
125
  border-radius: var(--rs-radius-full);
@@ -127,18 +127,18 @@
127
127
  background-clip: padding-box;
128
128
  transition: background-color var(--rs-transition-fast);
129
129
  }
130
- .rs-terminal[data-v-db06bdc1] .xterm-scrollable-element > .scrollbar > .slider:hover {
130
+ .rs-terminal[data-v-27605f60] .xterm-scrollable-element > .scrollbar > .slider:hover {
131
131
  background-color: color-mix(in srgb, var(--rs-primary) 40%, var(--rs-muted));
132
132
  }
133
- .rs-terminal[data-v-db06bdc1] .xterm-scrollable-element > .scrollbar > .slider.active {
133
+ .rs-terminal[data-v-27605f60] .xterm-scrollable-element > .scrollbar > .slider.active {
134
134
  background-color: color-mix(in srgb, var(--rs-primary) 55%, var(--rs-muted));
135
135
  }
136
- .rs-terminal--zebra[data-v-db06bdc1] .xterm-rows,
137
- .rs-terminal--zebra[data-v-db06bdc1] .xterm-rows > div,
138
- .rs-terminal--zebra[data-v-db06bdc1] .xterm-row {
136
+ .rs-terminal--zebra[data-v-27605f60] .xterm-rows,
137
+ .rs-terminal--zebra[data-v-27605f60] .xterm-rows > div,
138
+ .rs-terminal--zebra[data-v-27605f60] .xterm-row {
139
139
  background-color: transparent !important;
140
140
  }
141
- .rs-terminal--zebra[data-v-db06bdc1] .xterm-scrollable-element {
141
+ .rs-terminal--zebra[data-v-27605f60] .xterm-scrollable-element {
142
142
  background-color: transparent !important;
143
143
  background-image: repeating-linear-gradient(
144
144
  to bottom,
@@ -149,12 +149,12 @@
149
149
  );
150
150
  background-attachment: local;
151
151
  }
152
- .rs-terminal--zebra[data-v-db06bdc1] .xterm-screen {
152
+ .rs-terminal--zebra[data-v-27605f60] .xterm-screen {
153
153
  background-color: transparent !important;
154
154
  }
155
- .rs-terminal[data-v-db06bdc1] .xterm-fg-257 {
155
+ .rs-terminal[data-v-27605f60] .xterm-fg-257 {
156
156
  color: var(--rs-terminal-bg) !important;
157
157
  }
158
- .rs-terminal[data-v-db06bdc1] .xterm-bg-257 {
158
+ .rs-terminal[data-v-27605f60] .xterm-bg-257 {
159
159
  background-color: var(--rs-terminal-fg) !important;
160
160
  }
@@ -1,5 +1,6 @@
1
1
  import type { ITheme } from '@xterm/xterm';
2
2
  import '@xterm/xterm/css/xterm.css';
3
+ import type { RsContextMenuItem } from './context-menu-utils';
3
4
  import { type RsTerminalAction, type RsTerminalExpose, type RsTerminalThemeMode } from './terminal-utils';
4
5
  import { type RsTerminalWheelScrollModifier } from './terminal-wheel';
5
6
  type RsTerminalFontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
@@ -24,6 +25,11 @@ type __VLS_Props = {
24
25
  contextMenu?: boolean;
25
26
  /** 右键菜单是否展示「询问 AI」(仅 emit,业务侧自行处理) */
26
27
  showAskAi?: boolean;
28
+ /**
29
+ * 追加到内置右键菜单(复制/粘贴等)与「清空」之间。
30
+ * `key` 勿与 copy / paste / selectAll / search / askAi / clear 冲突。
31
+ */
32
+ extraContextMenuItems?: RsContextMenuItem[];
27
33
  /**
28
34
  * 右键是否自动选中光标下单词。
29
35
  * SSH/vim/less 等 TUI 场景建议 false,避免冲掉用户已拖选的大段文本。
@@ -67,6 +73,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, RsTerminalE
67
73
  ready: () => any;
68
74
  askAi: (text: string) => any;
69
75
  action: (action: RsTerminalAction) => any;
76
+ extraSelect: (key: string) => any;
70
77
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
71
78
  onData?: ((data: string) => any) | undefined;
72
79
  onResize?: ((payload: {
@@ -77,6 +84,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, RsTerminalE
77
84
  onReady?: (() => any) | undefined;
78
85
  onAskAi?: ((text: string) => any) | undefined;
79
86
  onAction?: ((action: RsTerminalAction) => any) | undefined;
87
+ onExtraSelect?: ((key: string) => any) | undefined;
80
88
  }>, {
81
89
  theme: Partial<ITheme>;
82
90
  loading: boolean;
@@ -88,6 +96,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, RsTerminalE
88
96
  allowTransparency: boolean;
89
97
  themeMode: RsTerminalThemeMode;
90
98
  showAskAi: boolean;
99
+ extraContextMenuItems: RsContextMenuItem[];
91
100
  rightClickSelectsWord: boolean;
92
101
  scrollback: number;
93
102
  convertEol: boolean;
@@ -56,6 +56,7 @@ var RsTerminal_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
56
56
  type: Boolean,
57
57
  default: false
58
58
  },
59
+ extraContextMenuItems: { default: () => [] },
59
60
  rightClickSelectsWord: {
60
61
  type: Boolean,
61
62
  default: true
@@ -94,7 +95,8 @@ var RsTerminal_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
94
95
  "resize",
95
96
  "action",
96
97
  "askAi",
97
- "selectionChange"
98
+ "selectionChange",
99
+ "extraSelect"
98
100
  ],
99
101
  setup(__props, { expose: __expose, emit: __emit }) {
100
102
  const props = __props;
@@ -177,6 +179,11 @@ var RsTerminal_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
177
179
  icon: "bot",
178
180
  disabled: !hasSelection.value && !menuSelectionSnapshot.value
179
181
  });
182
+ if (props.extraContextMenuItems?.length) items.push({
183
+ key: "sep-extra",
184
+ label: "",
185
+ separator: true
186
+ }, ...props.extraContextMenuItems);
180
187
  items.push({
181
188
  key: "sep-1",
182
189
  label: "",
@@ -438,8 +445,20 @@ var RsTerminal_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defin
438
445
  emit("action", "askAi");
439
446
  }
440
447
  }
448
+ const BUILTIN_TERMINAL_ACTIONS = /* @__PURE__ */ new Set([
449
+ "copy",
450
+ "paste",
451
+ "selectAll",
452
+ "clear",
453
+ "askAi",
454
+ "search"
455
+ ]);
441
456
  function onContextMenuSelect(key) {
442
- runTerminalAction(key);
457
+ if (BUILTIN_TERMINAL_ACTIONS.has(key)) {
458
+ runTerminalAction(key);
459
+ return;
460
+ }
461
+ emit("extraSelect", key);
443
462
  }
444
463
  function attachShortcuts() {
445
464
  if (!terminal) return;
@@ -3,6 +3,6 @@ import _plugin_vue_export_helper_default from "../_virtual/_plugin-vue_export-he
3
3
  import RsTerminal_vue_vue_type_script_setup_true_lang_default from "./RsTerminal.impl.js";
4
4
 
5
5
  //#region src/components/RsTerminal.vue
6
- var RsTerminal_default = /*#__PURE__*/ _plugin_vue_export_helper_default(RsTerminal_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-db06bdc1"]]);
6
+ var RsTerminal_default = /*#__PURE__*/ _plugin_vue_export_helper_default(RsTerminal_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-27605f60"]]);
7
7
  //#endregion
8
8
  export { RsTerminal_default as default };
@@ -0,0 +1,26 @@
1
+ export interface RsAnchorItem {
2
+ href: string;
3
+ title: string;
4
+ children?: RsAnchorItem[];
5
+ }
6
+ export interface RsAnchorFlatItem {
7
+ href: string;
8
+ title: string;
9
+ depth: number;
10
+ }
11
+ /** `#overview` / `path#overview` → `overview` */
12
+ export declare function hrefToAnchorId(href: string): string;
13
+ export declare function flattenAnchorItems(items: RsAnchorItem[], depth?: number): RsAnchorFlatItem[];
14
+ /**
15
+ * 滚动监听:取最后一个 top <= offset 的标题。
16
+ * entries 须按文档顺序(与 DOM 一致),top 为相对滚动容器顶部的距离。
17
+ */
18
+ export declare function pickActiveAnchorHref(entries: ReadonlyArray<{
19
+ href: string;
20
+ top: number;
21
+ }>, offset: number): string;
22
+ export declare function resolveAnchorContainer(getContainer?: () => HTMLElement | Window | null | undefined): HTMLElement | Window;
23
+ export declare function readContainerScrollTop(container: HTMLElement | Window): number;
24
+ export declare function targetTopInContainer(target: HTMLElement, container: HTMLElement | Window): number;
25
+ export declare function scrollContainerTo(container: HTMLElement | Window, top: number): void;
26
+ export declare function computeScrollTopForTarget(target: HTMLElement, container: HTMLElement | Window, targetOffset: number): number;
@@ -0,0 +1,63 @@
1
+ //#region src/components/anchor-utils.ts
2
+ /** `#overview` / `path#overview` → `overview` */
3
+ function hrefToAnchorId(href) {
4
+ const raw = href.trim();
5
+ return (raw.includes("#") ? raw.slice(raw.indexOf("#") + 1) : raw).replace(/^#/, "");
6
+ }
7
+ function flattenAnchorItems(items, depth = 0) {
8
+ const out = [];
9
+ for (const item of items) {
10
+ out.push({
11
+ href: item.href,
12
+ title: item.title,
13
+ depth
14
+ });
15
+ if (item.children?.length) out.push(...flattenAnchorItems(item.children, depth + 1));
16
+ }
17
+ return out;
18
+ }
19
+ /**
20
+ * 滚动监听:取最后一个 top <= offset 的标题。
21
+ * entries 须按文档顺序(与 DOM 一致),top 为相对滚动容器顶部的距离。
22
+ */
23
+ function pickActiveAnchorHref(entries, offset) {
24
+ if (entries.length === 0) return "";
25
+ let active = entries[0]?.href ?? "";
26
+ for (const entry of entries) if (entry.top <= offset) active = entry.href;
27
+ return active;
28
+ }
29
+ function resolveAnchorContainer(getContainer) {
30
+ const resolved = getContainer?.();
31
+ if (resolved) return resolved;
32
+ if (typeof window !== "undefined") return window;
33
+ throw new Error("RsAnchor: no scroll container");
34
+ }
35
+ function readContainerScrollTop(container) {
36
+ if (container instanceof Window) return container.scrollY;
37
+ return container.scrollTop;
38
+ }
39
+ function targetTopInContainer(target, container) {
40
+ const targetRect = target.getBoundingClientRect();
41
+ if (container instanceof Window) return targetRect.top;
42
+ return targetRect.top - container.getBoundingClientRect().top;
43
+ }
44
+ function scrollContainerTo(container, top) {
45
+ if (container instanceof Window) {
46
+ container.scrollTo({
47
+ top,
48
+ behavior: "smooth"
49
+ });
50
+ return;
51
+ }
52
+ container.scrollTo({
53
+ top,
54
+ behavior: "smooth"
55
+ });
56
+ }
57
+ function computeScrollTopForTarget(target, container, targetOffset) {
58
+ const current = readContainerScrollTop(container);
59
+ const relativeTop = targetTopInContainer(target, container);
60
+ return Math.max(0, current + relativeTop - targetOffset);
61
+ }
62
+ //#endregion
63
+ export { computeScrollTopForTarget, flattenAnchorItems, hrefToAnchorId, pickActiveAnchorHref, readContainerScrollTop, resolveAnchorContainer, scrollContainerTo, targetTopInContainer };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@ export { default as RsConfigProvider } from './components/RsConfigProvider.js';
2
2
  export { default as RsBadge } from './components/RsBadge.js';
3
3
  export { default as RsContainer } from './components/RsContainer.js';
4
4
  export { default as RsBreadcrumb } from './components/RsBreadcrumb.js';
5
+ export { default as RsAnchor } from './components/RsAnchor.js';
6
+ export type { RsAnchorFlatItem, RsAnchorItem } from './components/anchor-utils';
7
+ export { flattenAnchorItems, hrefToAnchorId, pickActiveAnchorHref, } from './components/anchor-utils';
5
8
  export { default as RsToolbar } from './components/RsToolbar.js';
6
9
  export { default as RsButton } from './components/RsButton.js';
7
10
  export type { RsButtonTone, RsButtonVariant } from './components/button-utils';
package/dist/index.js CHANGED
@@ -49,6 +49,7 @@ export { RS_TIME_SECONDS_FORMAT } from './lib/rs-dayjs.js'
49
49
  export { RS_TOAST_DEFAULT_GAP } from './components/overlay-utils.js'
50
50
  export { RS_TOAST_DEFAULT_POSITION } from './components/overlay-utils.js'
51
51
  export { default as RsAlert } from './components/RsAlert.js'
52
+ export { default as RsAnchor } from './components/RsAnchor.js'
52
53
  export { default as RsAutoComplete } from './components/RsAutoComplete.js'
53
54
  export { default as RsAvatar } from './components/RsAvatar.js'
54
55
  export { default as RsBadge } from './components/RsBadge.js'
@@ -191,6 +192,7 @@ export { filterTableRows } from './components/table-utils.js'
191
192
  export { filterTableTreeRows } from './components/table-utils.js'
192
193
  export { filterTreeNodes } from './components/tree-utils.js'
193
194
  export { fixedCellStyle } from './components/table-utils.js'
195
+ export { flattenAnchorItems } from './components/anchor-utils.js'
194
196
  export { flattenTreeNodeIds } from './components/tree-utils.js'
195
197
  export { flattenVisibleCountRough } from './composables/useRsTableVirtual.js'
196
198
  export { flattenVisibleTableTreeEntries } from './components/table-utils.js'
@@ -235,6 +237,7 @@ export { hasStableTableTreeRowKey } from './components/table-utils.js'
235
237
  export { hasTableSummaryConfig } from './components/table/table-summary-utils.js'
236
238
  export { hasTableTreeChildren } from './components/table-utils.js'
237
239
  export { hasTreeChildren } from './components/tree-utils.js'
240
+ export { hrefToAnchorId } from './components/anchor-utils.js'
238
241
  export { inferLogLevel } from './components/log-utils.js'
239
242
  export { injectExpandRows } from './components/table-utils.js'
240
243
  export { inputRuleMessageKeys } from './components/input-rules.js'
@@ -300,6 +303,7 @@ export { parseLocalDateTimeToUtcIso } from './lib/iso-local-datetime.js'
300
303
  export { parseNumberInput } from './components/input-number-utils.js'
301
304
  export { parseRsLogLevel } from './components/log-utils.js'
302
305
  export { parseTimeValue } from './components/time-picker-utils.js'
306
+ export { pickActiveAnchorHref } from './components/anchor-utils.js'
303
307
  export { placeAnchoredPopup } from './components/overlay-utils.js'
304
308
  export { prefetchClipboardText } from './utils/rs-clipboard.js'
305
309
  export { prewarmCodeMirrorEditor } from './components/code-mirror-lang.js'