@tnotesjs/ui 0.4.2 → 0.4.4
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/dist/alerts/icons.d.ts +8 -0
- package/dist/alerts/icons.js +29 -0
- package/dist/markdown/lineBreaks.d.ts +47 -0
- package/dist/markdown/lineBreaks.js +78 -0
- package/dist/swiper/hydrate.d.ts +4 -1
- package/dist/swiper/hydrate.js +9 -4
- package/package.json +23 -11
- package/src/alerts/icons.ts +32 -0
- package/src/browser/fullscreenLayer.ts +16 -0
- package/src/components/ContributionHeatmap/ContributionHeatmap.vue +638 -0
- package/src/components/ImagePreview/ImagePreview.vue +29 -6
- package/src/components/Mermaid/Mermaid.vue +5 -1
- package/src/components/Mindmap/InlineRuns.ts +4 -1
- package/src/components/Mindmap/Mindmap.vue +27 -12
- package/src/components/Mindmap/editor/AppIcon.vue +0 -3
- package/src/components/Mindmap/editor/MindmapView.vue +16 -1
- package/src/components/Mindmap/editor/OutlineView.vue +44 -3
- package/src/components/Mindmap/editor/RichInlineEditor.vue +56 -22
- package/src/components/Mindmap/editor/SearchBar.vue +8 -2
- package/src/components/Mindmap/editor/SelectionToolbar.vue +2 -7
- package/src/components/Mindmap/editor/mindmapClipboard.ts +4 -4
- package/src/components/Mindmap/inlineOptions.ts +21 -0
- package/src/components/WordList/WordList.vue +29 -9
- package/src/components/WordList/wordListStorage.ts +21 -0
- package/src/index.ts +3 -0
- package/src/markdown/lineBreaks.ts +133 -0
- package/src/styles/prose.css +138 -30
- package/src/swiper/hydrate.ts +15 -4
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kb-level line-break mode (`tnotes.json` → `markdown.lineBreaks`), shared by
|
|
3
|
+
* the SSG and Desk's card renderers so both draw the same note the same way.
|
|
4
|
+
*
|
|
5
|
+
* `visual` keeps what the editor shows: a single newline renders as `<br>`, and
|
|
6
|
+
* every blank line beyond the first between two top-level blocks becomes a
|
|
7
|
+
* one-line spacer; inside callout bodies (title and closing fence included)
|
|
8
|
+
* every blank line does. Lists and quotes keep CommonMark spacing; their inner blank lines
|
|
9
|
+
* decide loose/tight and have no stable visual meaning. Fences, tables and
|
|
10
|
+
* HTML are single tokens, so their contents are never touched.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type LineBreakMode = 'commonmark' | 'visual'
|
|
14
|
+
|
|
15
|
+
export const BLANK_LINE_CLASS = 'tn-blank-line'
|
|
16
|
+
|
|
17
|
+
export const BLANK_LINE_HTML = `<div class="${BLANK_LINE_CLASS}" aria-hidden="true"></div>\n`
|
|
18
|
+
|
|
19
|
+
export function resolveLineBreakMode(value: unknown): LineBreakMode {
|
|
20
|
+
return value === 'visual' ? 'visual' : 'commonmark'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Structural subset of markdown-it's Token, so this module stays dependency-free. */
|
|
24
|
+
interface BlockToken {
|
|
25
|
+
type: string
|
|
26
|
+
block: boolean
|
|
27
|
+
nesting: number
|
|
28
|
+
level: number
|
|
29
|
+
map: [number, number] | null
|
|
30
|
+
content: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CoreState {
|
|
34
|
+
src: string
|
|
35
|
+
tokens: BlockToken[]
|
|
36
|
+
Token: new (type: string, tag: string, nesting: -1 | 0 | 1) => BlockToken
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface MarkdownLike {
|
|
40
|
+
set(options: { breaks: boolean }): unknown
|
|
41
|
+
core: { ruler: { push(name: string, rule: (state: CoreState) => void): void } }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const CALLOUT_OPEN = /^container_(?:info|tip|warning|danger|details)_open$/
|
|
45
|
+
const CONTAINER_FENCE = /^\s*:{3,}\s*$/
|
|
46
|
+
|
|
47
|
+
/** Callout bodies get spacers too; other containers render their own way. */
|
|
48
|
+
export function isSpacedContainer(type: string): boolean {
|
|
49
|
+
return CALLOUT_OPEN.test(type)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Scope {
|
|
53
|
+
level: number
|
|
54
|
+
/** Line after the previous block in this scope; null before the first block. */
|
|
55
|
+
lastEnd: number | null
|
|
56
|
+
close?: string
|
|
57
|
+
/** Callout bodies: the closing `:::` line, which spaces like a block. */
|
|
58
|
+
closeLine?: number
|
|
59
|
+
/**
|
|
60
|
+
* Callout paragraphs have no margins standing in for the first blank line,
|
|
61
|
+
* so every blank line there is a spacer; top-level runs drop one.
|
|
62
|
+
*/
|
|
63
|
+
free: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function blockEnd(token: BlockToken, lines: readonly string[]): number {
|
|
67
|
+
const [start, mapEnd] = token.map!
|
|
68
|
+
let end = mapEnd
|
|
69
|
+
// markdown-it-container ends its map on the closing `:::` line itself.
|
|
70
|
+
if (token.type.startsWith('container_') && CONTAINER_FENCE.test(lines[end] ?? '')) end += 1
|
|
71
|
+
// Lists and quotes swallow one trailing blank line.
|
|
72
|
+
while (end > start + 1 && !(lines[end - 1] ?? '').trim()) end -= 1
|
|
73
|
+
return end
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Inserts `tn-blank-line` spacers for the blank lines `visual` keeps. */
|
|
77
|
+
export function insertBlankLineSpacers(
|
|
78
|
+
state: CoreState,
|
|
79
|
+
spaced: (type: string) => boolean = isSpacedContainer
|
|
80
|
+
): void {
|
|
81
|
+
const lines = state.src.split('\n')
|
|
82
|
+
const out: BlockToken[] = []
|
|
83
|
+
const stack: Scope[] = [{ level: 0, lastEnd: null, free: 1 }]
|
|
84
|
+
const pushSpacers = (count: number, level: number): void => {
|
|
85
|
+
for (; count > 0; count -= 1) {
|
|
86
|
+
const spacer = new state.Token('html_block', '', 0)
|
|
87
|
+
spacer.block = true
|
|
88
|
+
spacer.level = level
|
|
89
|
+
spacer.content = BLANK_LINE_HTML
|
|
90
|
+
out.push(spacer)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const token of state.tokens) {
|
|
94
|
+
const scope = stack[stack.length - 1]!
|
|
95
|
+
if (scope.close && token.type === scope.close && token.level === scope.level - 1) {
|
|
96
|
+
if (scope.closeLine !== undefined && scope.lastEnd !== null) {
|
|
97
|
+
pushSpacers(scope.closeLine - scope.lastEnd - scope.free, scope.level)
|
|
98
|
+
}
|
|
99
|
+
stack.pop()
|
|
100
|
+
out.push(token)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (token.block && token.map && token.nesting !== -1 && token.level === scope.level) {
|
|
104
|
+
if (scope.lastEnd !== null) {
|
|
105
|
+
pushSpacers(token.map[0] - scope.lastEnd - scope.free, scope.level)
|
|
106
|
+
}
|
|
107
|
+
const end = blockEnd(token, lines)
|
|
108
|
+
scope.lastEnd = end
|
|
109
|
+
out.push(token)
|
|
110
|
+
if (token.nesting === 1 && spaced(token.type)) {
|
|
111
|
+
// The title line spaces like a block, so does a closing fence.
|
|
112
|
+
const closed = CONTAINER_FENCE.test(lines[end - 1] ?? '') && end - 1 > token.map[0]
|
|
113
|
+
stack.push({
|
|
114
|
+
level: token.level + 1,
|
|
115
|
+
lastEnd: token.map[0] + 1,
|
|
116
|
+
close: token.type.replace(/_open$/, '_close'),
|
|
117
|
+
closeLine: closed ? end - 1 : undefined,
|
|
118
|
+
free: 0
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
out.push(token)
|
|
124
|
+
}
|
|
125
|
+
state.tokens = out
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Applies the mode to a markdown-it instance; `commonmark` leaves it untouched. */
|
|
129
|
+
export function useLineBreaks(md: MarkdownLike, mode: LineBreakMode): void {
|
|
130
|
+
if (mode !== 'visual') return
|
|
131
|
+
md.set({ breaks: true })
|
|
132
|
+
md.core.ruler.push('tn_blank_lines', (state) => insertBlankLineSpacers(state))
|
|
133
|
+
}
|
package/src/styles/prose.css
CHANGED
|
@@ -14,6 +14,32 @@
|
|
|
14
14
|
margin: 16px 0;
|
|
15
15
|
line-height: var(--tn-prose-line-height);
|
|
16
16
|
}
|
|
17
|
+
/* Visual line breaks: each blank line beyond the first adds one editor line.
|
|
18
|
+
The first spacer cancels the preceding block's 16px margin so N spacers add
|
|
19
|
+
exactly N line heights on top of the normal paragraph gap. */
|
|
20
|
+
:is(.tn-prose, .vp-doc) :where(.tn-blank-line) {
|
|
21
|
+
height: var(--tn-prose-line-height);
|
|
22
|
+
margin: 0;
|
|
23
|
+
}
|
|
24
|
+
:is(.tn-prose, .vp-doc) :where(:not(.tn-blank-line) + .tn-blank-line) {
|
|
25
|
+
margin-top: -16px;
|
|
26
|
+
}
|
|
27
|
+
/* Callout bodies keep every blank line as a spacer, so the spacer replaces the
|
|
28
|
+
neighbouring blocks' own margins instead of adding to them. */
|
|
29
|
+
:is(.tn-prose, .vp-doc) :where(.tn-custom-block > .tn-blank-line) {
|
|
30
|
+
margin-top: 0;
|
|
31
|
+
}
|
|
32
|
+
:is(.tn-prose, .vp-doc) .tn-custom-block > .tn-blank-line + *,
|
|
33
|
+
:is(.tn-prose, .vp-doc) .tn-custom-block > .tn-blank-line + * > :first-child,
|
|
34
|
+
:is(.tn-prose, .vp-doc) .tn-custom-block > .tn-blank-line + * > :first-child > :first-child {
|
|
35
|
+
margin-top: 0;
|
|
36
|
+
}
|
|
37
|
+
/* Inner margins collapse out through the block (nested lists, quoted paragraphs). */
|
|
38
|
+
:is(.tn-prose, .vp-doc) .tn-custom-block > :has(+ .tn-blank-line),
|
|
39
|
+
:is(.tn-prose, .vp-doc) .tn-custom-block > :has(+ .tn-blank-line) > :last-child,
|
|
40
|
+
:is(.tn-prose, .vp-doc) .tn-custom-block > :has(+ .tn-blank-line) > :last-child > :last-child {
|
|
41
|
+
margin-bottom: 0;
|
|
42
|
+
}
|
|
17
43
|
:is(.tn-prose, .vp-doc) :where(h1, h2, h3, h4, h5, h6) {
|
|
18
44
|
font-family: var(--tn-font-sans);
|
|
19
45
|
font-weight: var(--tn-heading-weight);
|
|
@@ -57,17 +83,17 @@
|
|
|
57
83
|
color: var(--tn-c-link);
|
|
58
84
|
text-decoration: none;
|
|
59
85
|
}
|
|
60
|
-
:is(.tn-prose, .vp-doc) :where(ul, ol) {
|
|
86
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(ul, ol) {
|
|
61
87
|
padding-left: 1.5rem;
|
|
62
88
|
margin: 16px 0;
|
|
63
89
|
}
|
|
64
|
-
:is(.tn-prose, .vp-doc) :where(li) {
|
|
90
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(li) {
|
|
65
91
|
line-height: var(--tn-prose-line-height);
|
|
66
92
|
}
|
|
67
|
-
:is(.tn-prose, .vp-doc) :where(li::marker) {
|
|
93
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(li::marker) {
|
|
68
94
|
color: var(--tn-c-text);
|
|
69
95
|
}
|
|
70
|
-
:is(.tn-prose, .vp-doc) :where(li > ul, li > ol) {
|
|
96
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(li > ul, li > ol) {
|
|
71
97
|
margin: 0;
|
|
72
98
|
}
|
|
73
99
|
/* 任务列表复选框:对齐语雀的方框(16px / 圆角 4px / 选中填充品牌绿 + 白色对勾 /
|
|
@@ -75,10 +101,10 @@
|
|
|
75
101
|
`appearance: none` 自绘;同时去掉 `.task-list-item` 的项目符号,避免出现
|
|
76
102
|
「◦ ☐ 文本」两层符号。对勾用 data-URI 而不是 `::after`:`input` 是替换元素,
|
|
77
103
|
Firefox 不生成伪元素。 */
|
|
78
|
-
:is(.tn-prose, .vp-doc) :where(.task-list-item) {
|
|
104
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(.task-list-item) {
|
|
79
105
|
list-style: none;
|
|
80
106
|
}
|
|
81
|
-
:is(.tn-prose, .vp-doc) :where(.task-list-item-checkbox) {
|
|
107
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(.task-list-item-checkbox) {
|
|
82
108
|
appearance: none;
|
|
83
109
|
-webkit-appearance: none;
|
|
84
110
|
box-sizing: border-box;
|
|
@@ -97,15 +123,15 @@
|
|
|
97
123
|
transition: all 0.3s;
|
|
98
124
|
cursor: pointer;
|
|
99
125
|
}
|
|
100
|
-
:is(.tn-prose, .vp-doc) :where(.task-list-item-checkbox:hover) {
|
|
126
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(.task-list-item-checkbox:hover) {
|
|
101
127
|
border-color: var(--tn-c-task-check);
|
|
102
128
|
}
|
|
103
|
-
:is(.tn-prose, .vp-doc) :where(.task-list-item-checkbox:checked) {
|
|
129
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(.task-list-item-checkbox:checked) {
|
|
104
130
|
border-color: var(--tn-c-task-check);
|
|
105
131
|
background-color: var(--tn-c-task-check);
|
|
106
132
|
background-size: 12px 12px;
|
|
107
133
|
}
|
|
108
|
-
:is(.tn-prose, .vp-doc) :where(:not(pre) > code) {
|
|
134
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(:not(pre) > code) {
|
|
109
135
|
color: inherit;
|
|
110
136
|
background: var(--tn-c-inline-code-bg);
|
|
111
137
|
border: 1px solid var(--tn-c-inline-code-border);
|
|
@@ -116,19 +142,19 @@
|
|
|
116
142
|
font-size: 1em;
|
|
117
143
|
font-weight: 400;
|
|
118
144
|
}
|
|
119
|
-
:is(.tn-prose, .vp-doc) :where(mark) {
|
|
145
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(mark) {
|
|
120
146
|
color: inherit;
|
|
121
147
|
background: var(--tn-c-mark-bg);
|
|
122
148
|
border-radius: 2px;
|
|
123
149
|
padding: 0 2px;
|
|
124
150
|
}
|
|
125
|
-
:is(.tn-prose, .vp-doc) :where(blockquote) {
|
|
151
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(blockquote) {
|
|
126
152
|
margin: 16px 0;
|
|
127
153
|
border-left: 2px solid var(--tn-c-divider);
|
|
128
154
|
padding: 0 0 0 16px;
|
|
129
155
|
color: var(--tn-c-text-2);
|
|
130
156
|
}
|
|
131
|
-
:is(.tn-prose, .vp-doc) :where(table) {
|
|
157
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(table) {
|
|
132
158
|
border-collapse: collapse;
|
|
133
159
|
margin: 16px 0;
|
|
134
160
|
/* 铺满内容列:与 Desk 可视化编辑器里的表格宽度一致(编辑器里表格宽度 = 内容列宽)。
|
|
@@ -137,13 +163,13 @@
|
|
|
137
163
|
width: 100%;
|
|
138
164
|
max-width: 100%;
|
|
139
165
|
}
|
|
140
|
-
:is(.tn-prose, .vp-doc) :where(th, td) {
|
|
166
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(th, td) {
|
|
141
167
|
border: 1px solid var(--tn-c-divider);
|
|
142
168
|
padding: 8px 16px;
|
|
143
169
|
font-size: 14px;
|
|
144
170
|
line-height: 24px;
|
|
145
171
|
}
|
|
146
|
-
:is(.tn-prose, .vp-doc) :where(th) {
|
|
172
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(th) {
|
|
147
173
|
font-weight: 600;
|
|
148
174
|
background: var(--tn-c-bg-soft);
|
|
149
175
|
/* 表头默认左对齐:浏览器对 <th> 的 UA 默认是居中,而编辑器里 Milkdown 给单元格写了
|
|
@@ -152,11 +178,11 @@
|
|
|
152
178
|
因此显式对齐不受影响(实测保留 left / center / right)。 */
|
|
153
179
|
text-align: left;
|
|
154
180
|
}
|
|
155
|
-
:is(.tn-prose, .vp-doc) :where(td p, th p) {
|
|
181
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(td p, th p) {
|
|
156
182
|
margin: 0;
|
|
157
183
|
line-height: 24px;
|
|
158
184
|
}
|
|
159
|
-
:is(.tn-prose, .vp-doc) :where(hr) {
|
|
185
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card .tn-custom-block) :where(hr) {
|
|
160
186
|
margin: 16px 0;
|
|
161
187
|
border: 0;
|
|
162
188
|
border-top: 1px solid var(--tn-c-divider);
|
|
@@ -209,37 +235,67 @@
|
|
|
209
235
|
text-align: center;
|
|
210
236
|
}
|
|
211
237
|
|
|
212
|
-
/* Callouts —
|
|
238
|
+
/* Callouts — GitHub Alerts:左侧色条 + 图标/标题,正文与周围连续,没有整块底色。 */
|
|
213
239
|
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block) {
|
|
240
|
+
--tn-alert: var(--tn-c-text-2);
|
|
214
241
|
box-sizing: border-box;
|
|
215
242
|
margin: 16px 0;
|
|
216
|
-
padding:
|
|
217
|
-
border:
|
|
218
|
-
border-
|
|
243
|
+
padding: 2px 0 2px 16px;
|
|
244
|
+
border: none;
|
|
245
|
+
border-left: 0.25em solid var(--tn-alert);
|
|
246
|
+
border-radius: 0;
|
|
247
|
+
background: none;
|
|
219
248
|
color: var(--tn-c-text);
|
|
220
249
|
font-size: inherit;
|
|
221
250
|
line-height: 1.5;
|
|
222
251
|
}
|
|
223
252
|
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.tip) {
|
|
224
|
-
|
|
253
|
+
--tn-alert: var(--tn-c-success);
|
|
225
254
|
}
|
|
226
|
-
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.info)
|
|
227
|
-
|
|
228
|
-
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.details) {
|
|
229
|
-
background-color: var(--tn-c-hover);
|
|
255
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.info) {
|
|
256
|
+
--tn-alert: var(--tn-c-link);
|
|
230
257
|
}
|
|
231
258
|
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.warning) {
|
|
232
|
-
|
|
259
|
+
--tn-alert: var(--tn-c-warning);
|
|
233
260
|
}
|
|
234
261
|
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.danger) {
|
|
235
|
-
|
|
262
|
+
--tn-alert: var(--tn-c-danger);
|
|
263
|
+
}
|
|
264
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.details) {
|
|
265
|
+
--tn-alert: var(--tn-c-text-2);
|
|
266
|
+
}
|
|
267
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-alert-icon) {
|
|
268
|
+
display: inline-flex;
|
|
269
|
+
flex: none;
|
|
270
|
+
color: var(--tn-alert);
|
|
271
|
+
line-height: 0;
|
|
272
|
+
}
|
|
273
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-alert-icon svg) {
|
|
274
|
+
width: 16px;
|
|
275
|
+
height: 16px;
|
|
276
|
+
display: block;
|
|
236
277
|
}
|
|
237
278
|
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block-title) {
|
|
238
|
-
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 8px;
|
|
282
|
+
margin: 0 0 4px;
|
|
239
283
|
font-size: inherit;
|
|
240
284
|
font-weight: 600;
|
|
241
285
|
line-height: 1.5;
|
|
286
|
+
color: var(--tn-alert);
|
|
287
|
+
}
|
|
288
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-container-source) {
|
|
289
|
+
margin: 8px 0;
|
|
290
|
+
padding: 8px 12px;
|
|
291
|
+
border-radius: 6px;
|
|
292
|
+
background: var(--tn-c-bg-soft);
|
|
242
293
|
color: var(--tn-c-text);
|
|
294
|
+
font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
295
|
+
font-size: 13px;
|
|
296
|
+
line-height: 1.5;
|
|
297
|
+
white-space: pre-wrap;
|
|
298
|
+
overflow-x: auto;
|
|
243
299
|
}
|
|
244
300
|
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block > p:not(.tn-custom-block-title)) {
|
|
245
301
|
margin: 0;
|
|
@@ -251,6 +307,7 @@
|
|
|
251
307
|
margin-bottom: 0;
|
|
252
308
|
}
|
|
253
309
|
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block > summary) {
|
|
310
|
+
position: relative;
|
|
254
311
|
display: flex;
|
|
255
312
|
align-items: center;
|
|
256
313
|
gap: 8px;
|
|
@@ -268,7 +325,57 @@
|
|
|
268
325
|
:where(details.tn-custom-block > summary::-webkit-details-marker) {
|
|
269
326
|
display: none;
|
|
270
327
|
}
|
|
271
|
-
|
|
328
|
+
/* 折叠折线和标题折叠一样挂在色条左外侧(悬停或收起时显示),图标和标题留在其它提示块的位置。
|
|
329
|
+
right 要越过 details 的左内边距 16px 和 0.25em 色条。 */
|
|
330
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block > summary > .tn-details-fold) {
|
|
331
|
+
position: absolute;
|
|
332
|
+
top: 0;
|
|
333
|
+
bottom: 0;
|
|
334
|
+
right: calc(100% + 16px + 0.25em);
|
|
335
|
+
display: inline-flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
justify-content: center;
|
|
338
|
+
width: 20px;
|
|
339
|
+
border-radius: 4px;
|
|
340
|
+
color: var(--tn-c-text-2);
|
|
341
|
+
line-height: 0;
|
|
342
|
+
opacity: 0;
|
|
343
|
+
transition: opacity 120ms ease;
|
|
344
|
+
}
|
|
345
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
346
|
+
:where(details.tn-custom-block > summary:hover > .tn-details-fold) {
|
|
347
|
+
color: var(--tn-c-brand);
|
|
348
|
+
background: var(--tn-c-hover);
|
|
349
|
+
}
|
|
350
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
351
|
+
:where(
|
|
352
|
+
details.tn-custom-block:not([open]) > summary > .tn-details-fold,
|
|
353
|
+
details.tn-custom-block:hover > summary > .tn-details-fold,
|
|
354
|
+
details.tn-custom-block > summary:focus-visible > .tn-details-fold
|
|
355
|
+
) {
|
|
356
|
+
opacity: 1;
|
|
357
|
+
}
|
|
358
|
+
@media (hover: none) {
|
|
359
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
360
|
+
:where(details.tn-custom-block > summary > .tn-details-fold) {
|
|
361
|
+
opacity: 1;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
365
|
+
:where(details.tn-custom-block > summary > .tn-details-fold svg) {
|
|
366
|
+
width: 12px;
|
|
367
|
+
height: 12px;
|
|
368
|
+
transform: none;
|
|
369
|
+
transform-origin: 50% 50%;
|
|
370
|
+
transition: transform 120ms ease;
|
|
371
|
+
}
|
|
372
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
373
|
+
:where(details.tn-custom-block[open] > summary > .tn-details-fold svg) {
|
|
374
|
+
transform: rotate(90deg);
|
|
375
|
+
}
|
|
376
|
+
/* 旧版无 SVG 的 summary(缓存页等)仍用 CSS 折线,避免裸标题 */
|
|
377
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
378
|
+
:where(details.tn-custom-block > summary:not(:has(.tn-details-fold))::before) {
|
|
272
379
|
content: '';
|
|
273
380
|
flex: none;
|
|
274
381
|
box-sizing: border-box;
|
|
@@ -282,7 +389,8 @@
|
|
|
282
389
|
transform-origin: 50% 50%;
|
|
283
390
|
transition: transform 120ms ease;
|
|
284
391
|
}
|
|
285
|
-
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
392
|
+
:is(.tn-prose, .vp-doc, .cm-lp-card)
|
|
393
|
+
:where(details.tn-custom-block[open] > summary:not(:has(.tn-details-fold))::before) {
|
|
286
394
|
margin-top: -0.05em;
|
|
287
395
|
transform: rotate(45deg);
|
|
288
396
|
}
|
package/src/swiper/hydrate.ts
CHANGED
|
@@ -61,7 +61,10 @@ const READY = 'tnSwiperReady'
|
|
|
61
61
|
* already-hydrated roots are skipped via `data-tn-swiper-ready`.
|
|
62
62
|
* When `root` itself is a `.tn-swiper`, it is included.
|
|
63
63
|
*/
|
|
64
|
-
export function hydrateTnSwipers(
|
|
64
|
+
export function hydrateTnSwipers(
|
|
65
|
+
root: ParentNode = document,
|
|
66
|
+
options?: { activeIndex?: number; onActivate?: (index: number) => void }
|
|
67
|
+
): number {
|
|
65
68
|
const hosts: HTMLElement[] = []
|
|
66
69
|
if (root instanceof HTMLElement && root.classList.contains('tn-swiper')) {
|
|
67
70
|
hosts.push(root)
|
|
@@ -74,7 +77,7 @@ export function hydrateTnSwipers(root: ParentNode = document): number {
|
|
|
74
77
|
if (seen.has(host)) continue
|
|
75
78
|
seen.add(host)
|
|
76
79
|
if (host.dataset[READY] === 'true') continue
|
|
77
|
-
if (hydrateOne(host)) {
|
|
80
|
+
if (hydrateOne(host, options)) {
|
|
78
81
|
host.dataset[READY] = 'true'
|
|
79
82
|
count += 1
|
|
80
83
|
}
|
|
@@ -82,7 +85,10 @@ export function hydrateTnSwipers(root: ParentNode = document): number {
|
|
|
82
85
|
return count
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
function hydrateOne(
|
|
88
|
+
function hydrateOne(
|
|
89
|
+
host: HTMLElement,
|
|
90
|
+
options?: { activeIndex?: number; onActivate?: (index: number) => void }
|
|
91
|
+
): boolean {
|
|
86
92
|
if (host.classList.contains('is-empty')) return true
|
|
87
93
|
const tabs = host.querySelector<HTMLElement>('.tn-swiper-tabs')
|
|
88
94
|
const wrapper = host.querySelector<HTMLElement>('.swiper-wrapper')
|
|
@@ -117,6 +123,7 @@ function hydrateOne(host: HTMLElement): boolean {
|
|
|
117
123
|
slide.classList.toggle('is-active', on)
|
|
118
124
|
slide.hidden = !on
|
|
119
125
|
})
|
|
126
|
+
options?.onActivate?.(index)
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
const activeIndex = (): number => {
|
|
@@ -156,6 +163,10 @@ function hydrateOne(host: HTMLElement): boolean {
|
|
|
156
163
|
tabsEl.remove()
|
|
157
164
|
}
|
|
158
165
|
|
|
159
|
-
|
|
166
|
+
const start = Math.min(
|
|
167
|
+
Math.max(0, options?.activeIndex ?? 0),
|
|
168
|
+
Math.max(0, slides.length - 1)
|
|
169
|
+
)
|
|
170
|
+
activate(start)
|
|
160
171
|
return true
|
|
161
172
|
}
|