@tnotesjs/ui 0.1.1 → 0.1.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 (41) hide show
  1. package/README.md +52 -29
  2. package/dist/code/highlight.d.ts +42 -0
  3. package/dist/code/highlight.js +137 -0
  4. package/dist/components/Footprints/parse.d.ts +17 -0
  5. package/dist/components/Footprints/parse.js +105 -0
  6. package/dist/components/Mindmap/markdown.d.ts +20 -0
  7. package/dist/components/Mindmap/markdown.js +60 -0
  8. package/dist/markdown/image.d.ts +33 -0
  9. package/dist/markdown/image.js +83 -0
  10. package/package.json +46 -6
  11. package/src/browser/clipboard.ts +32 -0
  12. package/src/browser/theme.ts +22 -0
  13. package/src/code/highlight.test.ts +55 -0
  14. package/src/code/highlight.ts +170 -0
  15. package/src/components/Badge/Badge.vue +43 -0
  16. package/src/components/CodeBlock/CodeBlock.vue +172 -0
  17. package/src/components/CodeGroup/CodeGroup.vue +96 -0
  18. package/src/components/ImagePreview/ImagePreview.vue +315 -0
  19. package/src/components/Mermaid/Mermaid.test.ts +54 -0
  20. package/src/components/Mermaid/Mermaid.vue +229 -56
  21. package/src/components/Mermaid/adoptLiveSvg.test.ts +16 -0
  22. package/src/components/Mermaid/adoptLiveSvg.ts +15 -0
  23. package/src/components/Mindmap/Mindmap.vue +2 -0
  24. package/src/components/sharedMedia.test.ts +139 -0
  25. package/src/entries/bilibili-video.ts +1 -0
  26. package/src/entries/footprints.ts +1 -0
  27. package/src/entries/mermaid.ts +1 -0
  28. package/src/entries/mindmap.ts +1 -0
  29. package/src/entries/notes-table.ts +2 -0
  30. package/src/entries/word-list.ts +1 -0
  31. package/src/icons/check.svg +3 -0
  32. package/src/icons/copy.svg +4 -0
  33. package/src/index.ts +31 -18
  34. package/src/markdown/image.test.ts +88 -0
  35. package/src/markdown/image.ts +115 -0
  36. package/src/styles/code.css +184 -0
  37. package/src/styles/prose.css +211 -0
  38. package/src/styles/swiper.css +151 -0
  39. package/src/styles/tokens.css +54 -18
  40. package/src/swiper/hydrate.test.ts +65 -0
  41. package/src/swiper/hydrate.ts +164 -0
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ formatImageAttrs,
5
+ formatImageWidthAttr,
6
+ normalizeImageAlign,
7
+ normalizeImageWidth,
8
+ parseImageAttrs,
9
+ parseImageWidthAttr,
10
+ pasteDisplayWidthPx,
11
+ serializeImageMarkdown
12
+ } from './image'
13
+
14
+ describe('image markdown helpers', () => {
15
+ it('accepts w and width, with or without a percent sign', () => {
16
+ expect(parseImageWidthAttr(' {w=50%} ')).toEqual({ width: '50%', rest: '' })
17
+ expect(parseImageWidthAttr('{width=33}')).toEqual({ width: '33%', rest: '' })
18
+ expect(parseImageWidthAttr('not-an-attr')).toEqual({
19
+ width: '',
20
+ rest: 'not-an-attr'
21
+ })
22
+ })
23
+
24
+ it('folds pasted bitmap width by device pixel ratio', () => {
25
+ expect(pasteDisplayWidthPx(800, 2)).toBe('400px')
26
+ expect(pasteDisplayWidthPx(400, 1)).toBe('400px')
27
+ expect(pasteDisplayWidthPx(20, 2)).toBe('')
28
+ expect(pasteDisplayWidthPx(0, 2)).toBe('')
29
+ })
30
+
31
+ it('accepts pixel widths from drag-resize', () => {
32
+ expect(parseImageWidthAttr('{w=400px}')).toEqual({ width: '400px', rest: '' })
33
+ expect(normalizeImageWidth('400px')).toBe('400px')
34
+ expect(normalizeImageWidth('8px')).toBe('')
35
+ })
36
+
37
+ it('parses alignment beside width in one brace group', () => {
38
+ expect(parseImageAttrs('{w=50% align=center}')).toEqual({
39
+ width: '50%',
40
+ align: 'center',
41
+ rest: ''
42
+ })
43
+ expect(parseImageAttrs('{align=right}')).toEqual({
44
+ width: '',
45
+ align: 'right',
46
+ rest: ''
47
+ })
48
+ expect(parseImageAttrs('{w=400px,align=center}')).toEqual({
49
+ width: '400px',
50
+ align: 'center',
51
+ rest: ''
52
+ })
53
+ expect(normalizeImageAlign('middle')).toBe('center')
54
+ expect(formatImageAttrs({ width: '400px', align: 'center' })).toBe(
55
+ '{w=400px align=center}'
56
+ )
57
+ expect(formatImageAttrs({ align: 'left' })).toBe('')
58
+ })
59
+
60
+ it('rejects empty, zero, and over-wide values', () => {
61
+ expect(normalizeImageWidth('')).toBe('')
62
+ expect(normalizeImageWidth('0%')).toBe('')
63
+ expect(normalizeImageWidth('120%')).toBe('')
64
+ expect(normalizeImageWidth('auto')).toBe('')
65
+ })
66
+
67
+ it('serializes caption as alt and omits natural width', () => {
68
+ expect(
69
+ serializeImageMarkdown({
70
+ alt: '知识库资源示意',
71
+ src: '../assets/pixel.svg',
72
+ width: '50%'
73
+ })
74
+ ).toBe('![知识库资源示意](../assets/pixel.svg) {w=50%}')
75
+ expect(serializeImageMarkdown({ alt: '', src: '../assets/pixel.svg' })).toBe(
76
+ '![](../assets/pixel.svg)'
77
+ )
78
+ expect(formatImageWidthAttr('100')).toBe('{w=100%}')
79
+ expect(
80
+ serializeImageMarkdown({
81
+ alt: '',
82
+ src: '../assets/shot.png',
83
+ width: '400px',
84
+ align: 'right'
85
+ })
86
+ ).toBe('![](../assets/shot.png) {w=400px align=right}')
87
+ })
88
+ })
@@ -0,0 +1,115 @@
1
+ /** Shared image caption, width, and alignment helpers for SSG and Desk. */
2
+
3
+ const ATTR_BLOCK = /^\s*\{([^}]*)\}\s*$/i
4
+
5
+ export type ImageAlign = 'left' | 'center' | 'right'
6
+
7
+ export interface ImageMarkdown {
8
+ alt: string
9
+ src: string
10
+ title?: string
11
+ /** Normalized CSS size, e.g. `50%` or `400px`. Empty means natural width. */
12
+ width?: string
13
+ /** Omitted or `left` is the default and is not written to markdown. */
14
+ align?: ImageAlign
15
+ }
16
+
17
+ export interface ImageAttrs {
18
+ width: string
19
+ align: ImageAlign
20
+ rest: string
21
+ }
22
+
23
+ export function normalizeImageAlign(raw: string): ImageAlign {
24
+ const value = raw.trim().toLowerCase()
25
+ if (value === 'center' || value === 'middle') return 'center'
26
+ if (value === 'right') return 'right'
27
+ return 'left'
28
+ }
29
+
30
+ export function parseImageAttrs(text: string): ImageAttrs {
31
+ const match = text.match(ATTR_BLOCK)
32
+ if (!match) return { width: '', align: 'left', rest: text }
33
+ const body = match[1].trim()
34
+ if (!body) return { width: '', align: 'left', rest: '' }
35
+ if (!/(?:^|[\s,])(?:w|width|align|a)\s*=/i.test(` ${body}`)) {
36
+ return { width: '', align: 'left', rest: text }
37
+ }
38
+ let width = ''
39
+ let align: ImageAlign = 'left'
40
+ for (const part of body.split(/[\s,]+/).filter(Boolean)) {
41
+ const eq = part.indexOf('=')
42
+ if (eq < 0) continue
43
+ const key = part.slice(0, eq).trim().toLowerCase()
44
+ const value = part.slice(eq + 1).trim()
45
+ if (key === 'w' || key === 'width') width = normalizeImageWidth(value)
46
+ else if (key === 'align' || key === 'a') align = normalizeImageAlign(value)
47
+ }
48
+ return { width, align, rest: '' }
49
+ }
50
+
51
+ export function parseImageWidthAttr(text: string): {
52
+ width: string
53
+ rest: string
54
+ } {
55
+ const parsed = parseImageAttrs(text)
56
+ return { width: parsed.width, rest: parsed.rest }
57
+ }
58
+
59
+ export function normalizeImageWidth(raw: string): string {
60
+ const value = raw.trim()
61
+ if (!value) return ''
62
+ const pixels = value.match(/^(\d+(?:\.\d+)?)px$/i)
63
+ if (pixels) {
64
+ const amount = Number(pixels[1])
65
+ if (!Number.isFinite(amount) || amount < 16) return ''
66
+ return `${Math.round(amount)}px`
67
+ }
68
+ const percent = value.match(/^(\d+(?:\.\d+)?)%?$/)
69
+ if (!percent) return ''
70
+ const amount = Number(percent[1])
71
+ if (!Number.isFinite(amount) || amount <= 0 || amount > 100) return ''
72
+ const normalized = Number.isInteger(amount)
73
+ ? String(amount)
74
+ : String(Math.round(amount * 100) / 100)
75
+ return `${normalized}%`
76
+ }
77
+
78
+ /** Logical CSS px for a pasted bitmap, folding device pixel ratio (4K/retina). */
79
+ export function pasteDisplayWidthPx(
80
+ naturalWidth: number,
81
+ devicePixelRatio = 1
82
+ ): string {
83
+ if (!Number.isFinite(naturalWidth) || naturalWidth <= 0) return ''
84
+ const dpr =
85
+ Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1
86
+ return normalizeImageWidth(`${Math.round(naturalWidth / dpr)}px`)
87
+ }
88
+
89
+ export function formatImageWidthAttr(width: string): string {
90
+ return formatImageAttrs({ width, align: 'left' })
91
+ }
92
+
93
+ export function formatImageAttrs(attrs: {
94
+ width?: string
95
+ align?: ImageAlign | string
96
+ }): string {
97
+ const width = normalizeImageWidth(attrs.width ?? '')
98
+ const align = normalizeImageAlign(attrs.align ?? '')
99
+ const parts: string[] = []
100
+ if (width) parts.push(`w=${width}`)
101
+ if (align !== 'left') parts.push(`align=${align}`)
102
+ return parts.length > 0 ? `{${parts.join(' ')}}` : ''
103
+ }
104
+
105
+ export function serializeImageMarkdown(image: ImageMarkdown): string {
106
+ const alt = image.alt.replaceAll('[', '').replaceAll(']', '')
107
+ const title = image.title?.trim()
108
+ ? ` "${image.title.replaceAll('"', '')}"`
109
+ : ''
110
+ const suffix = formatImageAttrs({
111
+ width: image.width ?? '',
112
+ align: image.align ?? 'left'
113
+ })
114
+ return `![${alt}](${image.src}${title})${suffix ? ` ${suffix}` : ''}`
115
+ }
@@ -0,0 +1,184 @@
1
+ .tn-code-block {
2
+ margin: 1rem 0;
3
+ border: 1px solid var(--tn-c-divider);
4
+ border-radius: 8px;
5
+ background: var(--tn-c-bg-soft);
6
+ color: var(--tn-c-text);
7
+ overflow: hidden;
8
+ }
9
+ .tn-code-block__header,
10
+ .tn-code-fullscreen > header {
11
+ box-sizing: border-box;
12
+ display: flex;
13
+ align-items: center;
14
+ gap: 12px;
15
+ height: 38px;
16
+ min-height: 38px;
17
+ padding: 0 12px;
18
+ border-bottom: 1px solid var(--tn-c-divider);
19
+ font: 13px/24px var(--tn-font-sans);
20
+ }
21
+ .tn-code-block__title,
22
+ .tn-code-fullscreen > header > span {
23
+ flex: 1;
24
+ min-width: 0;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ white-space: nowrap;
28
+ }
29
+ .tn-code-block__language {
30
+ color: var(--tn-c-text-2);
31
+ }
32
+ .tn-code-block button,
33
+ .tn-code-group__tabs button,
34
+ .tn-code-fullscreen button {
35
+ color: var(--tn-c-text);
36
+ font: inherit;
37
+ border: 0;
38
+ background: transparent;
39
+ cursor: pointer;
40
+ padding: 4px 8px;
41
+ border-radius: 4px;
42
+ }
43
+ .tn-code-block button.tn-code-block__icon-btn {
44
+ box-sizing: border-box;
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ flex: none;
49
+ width: 28px;
50
+ height: 28px;
51
+ padding: 4px 8px;
52
+ }
53
+ .tn-code-block__icon-btn svg {
54
+ display: block;
55
+ flex: none;
56
+ width: 12px;
57
+ height: 16px;
58
+ }
59
+ .tn-code-block button:hover,
60
+ .tn-code-group__tabs button:hover,
61
+ .tn-code-fullscreen button:hover {
62
+ background: var(--tn-c-hover);
63
+ }
64
+ .tn-code-block button:focus-visible,
65
+ .tn-code-group button:focus-visible,
66
+ .tn-code-fullscreen button:focus-visible {
67
+ outline: 2px solid var(--tn-c-brand);
68
+ outline-offset: -2px;
69
+ }
70
+ .tn-code-block pre {
71
+ background: transparent;
72
+ margin: 0;
73
+ padding: 8px 0;
74
+ max-height: 480px;
75
+ overflow: auto;
76
+ font: 14px/24px var(--tn-font-mono);
77
+ tab-size: 2;
78
+ }
79
+ .tn-code-block code {
80
+ font: inherit;
81
+ padding: 0;
82
+ background: transparent;
83
+ color: inherit;
84
+ }
85
+ .tn-code-highlight .line {
86
+ display: inline-block;
87
+ min-width: 100%;
88
+ padding: 0 20px;
89
+ box-sizing: border-box;
90
+ }
91
+ .tn-code-block.has-line-numbers .line::before {
92
+ content: attr(data-line);
93
+ display: inline-block;
94
+ width: 3ch;
95
+ padding-right: 16px;
96
+ text-align: right;
97
+ color: var(--tn-c-text-2);
98
+ user-select: none;
99
+ }
100
+ .tn-code-highlight {
101
+ color: var(--shiki-light);
102
+ background-color: var(--tn-c-bg-soft);
103
+ }
104
+ .tn-code-highlight span:not(.line) {
105
+ color: var(--shiki-light);
106
+ background-color: transparent;
107
+ }
108
+ html.dark:not([data-theme="light"]) .tn-code-highlight,
109
+ html[data-theme="dark"] .tn-code-highlight {
110
+ color: var(--shiki-dark);
111
+ background-color: var(--tn-c-bg-soft);
112
+ }
113
+ html.dark:not([data-theme="light"]) .tn-code-highlight span:not(.line),
114
+ html[data-theme="dark"] .tn-code-highlight span:not(.line) {
115
+ color: var(--shiki-dark);
116
+ background-color: transparent;
117
+ }
118
+ .tn-code-highlight .line.highlighted {
119
+ background-color: var(--tn-c-highlight);
120
+ }
121
+ .tn-code-block__plain {
122
+ padding: 16px !important;
123
+ }
124
+ .tn-code-block__error {
125
+ color: var(--tn-c-danger);
126
+ padding: 8px 12px;
127
+ font-size: 13px;
128
+ }
129
+ .tn-code-group {
130
+ position: relative;
131
+ margin: 1rem 0;
132
+ }
133
+ .tn-code-group__tabs {
134
+ position: absolute;
135
+ top: 0;
136
+ left: 0;
137
+ z-index: 1;
138
+ box-sizing: border-box;
139
+ display: flex;
140
+ align-items: center;
141
+ overflow-x: auto;
142
+ gap: 4px;
143
+ height: 38px;
144
+ max-width: calc(100% - 148px);
145
+ padding: 0 8px;
146
+ border-bottom: 0;
147
+ }
148
+ .tn-code-group__tabs button {
149
+ flex: none;
150
+ border-radius: 4px;
151
+ border-bottom: 2px solid transparent;
152
+ padding: 4px 10px;
153
+ }
154
+ .tn-code-group__tabs button[aria-selected="true"] {
155
+ color: var(--tn-c-brand);
156
+ border-bottom-color: var(--tn-c-brand);
157
+ }
158
+ .tn-code-group .tn-code-block__title {
159
+ visibility: hidden;
160
+ }
161
+ .tn-code-group__panel > .tn-code-block {
162
+ margin: 0;
163
+ }
164
+ .tn-code-fullscreen {
165
+ position: fixed;
166
+ inset: 0;
167
+ z-index: 10000;
168
+ background: var(--tn-c-bg);
169
+ color: var(--tn-c-text);
170
+ overflow: auto;
171
+ overscroll-behavior: contain;
172
+ }
173
+ .tn-code-fullscreen > header {
174
+ position: sticky;
175
+ top: 0;
176
+ background: var(--tn-c-bg);
177
+ z-index: 1;
178
+ }
179
+ .tn-code-fullscreen > .tn-code-block {
180
+ margin: 16px;
181
+ }
182
+ .tn-code-fullscreen .tn-code-block pre {
183
+ max-height: none;
184
+ }
@@ -0,0 +1,211 @@
1
+ /* Host-neutral typography; editing hit areas belong to the host. */
2
+ :is(.tn-prose, .vp-doc) {
3
+ color: var(--tn-c-text);
4
+ font-family: var(--tn-font-sans);
5
+ font-size: var(--tn-prose-size);
6
+ font-optical-sizing: auto;
7
+ font-synthesis: style;
8
+ line-height: 1.5;
9
+ -webkit-font-smoothing: antialiased;
10
+ }
11
+ :is(.tn-prose, .vp-doc) :where(p) {
12
+ margin: 16px 0;
13
+ line-height: var(--tn-prose-line-height);
14
+ }
15
+ :is(.tn-prose, .vp-doc) :where(h1, h2, h3, h4, h5, h6) {
16
+ font-family: var(--tn-font-sans);
17
+ font-weight: var(--tn-heading-weight);
18
+ margin: 16px 0;
19
+ border: 0;
20
+ padding: 0;
21
+ scroll-margin-top: 80px;
22
+ }
23
+ :is(.tn-prose, .vp-doc) :where(h1) {
24
+ font-size: var(--tn-heading-1-size);
25
+ line-height: var(--tn-heading-1-line);
26
+ }
27
+ :is(.tn-prose, .vp-doc) :where(h2) {
28
+ font-size: var(--tn-heading-2-size);
29
+ line-height: var(--tn-heading-2-line);
30
+ }
31
+ :is(.tn-prose, .vp-doc) :where(h3) {
32
+ font-size: var(--tn-heading-3-size);
33
+ line-height: var(--tn-heading-3-line);
34
+ }
35
+ :is(.tn-prose, .vp-doc) :where(h4) {
36
+ font-size: var(--tn-heading-4-size);
37
+ line-height: var(--tn-heading-4-line);
38
+ }
39
+ :is(.tn-prose, .vp-doc) :where(h5) {
40
+ font-size: var(--tn-heading-5-size);
41
+ line-height: var(--tn-heading-5-line);
42
+ }
43
+ :is(.tn-prose, .vp-doc) :where(h6) {
44
+ font-size: var(--tn-heading-6-size);
45
+ line-height: var(--tn-heading-6-line);
46
+ }
47
+ :is(.tn-prose, .vp-doc) :where(a) {
48
+ color: var(--tn-c-brand);
49
+ text-decoration: none;
50
+ font-weight: 500;
51
+ }
52
+ :is(.tn-prose, .vp-doc) :where(a:hover) {
53
+ color: var(--tn-c-brand-hover);
54
+ text-decoration: underline;
55
+ }
56
+ :is(.tn-prose, .vp-doc) :where(ul, ol) {
57
+ padding-left: 1.5rem;
58
+ margin: 16px 0;
59
+ }
60
+ :is(.tn-prose, .vp-doc) :where(li) {
61
+ line-height: var(--tn-prose-line-height);
62
+ }
63
+ :is(.tn-prose, .vp-doc) :where(li::marker) {
64
+ color: var(--tn-c-text);
65
+ }
66
+ :is(.tn-prose, .vp-doc) :where(li > ul, li > ol) {
67
+ margin: 0;
68
+ }
69
+ :is(.tn-prose, .vp-doc) :where(:not(pre) > code) {
70
+ color: var(--tn-c-brand);
71
+ background: var(--tn-c-default-soft);
72
+ border-radius: 4px;
73
+ font: 0.875em var(--tn-font-mono);
74
+ padding: 3px 6px;
75
+ }
76
+ :is(.tn-prose, .vp-doc) :where(blockquote) {
77
+ margin: 16px 0;
78
+ border-left: 2px solid var(--tn-c-divider);
79
+ padding: 0 0 0 16px;
80
+ color: var(--tn-c-text-2);
81
+ }
82
+ :is(.tn-prose, .vp-doc) :where(table) {
83
+ border-collapse: collapse;
84
+ margin: 16px 0;
85
+ }
86
+ :is(.tn-prose, .vp-doc) :where(th, td) {
87
+ border: 1px solid var(--tn-c-divider);
88
+ padding: 8px 16px;
89
+ font-size: 14px;
90
+ line-height: 24px;
91
+ }
92
+ :is(.tn-prose, .vp-doc) :where(th) {
93
+ font-weight: 600;
94
+ background: var(--tn-c-bg-soft);
95
+ }
96
+ :is(.tn-prose, .vp-doc) :where(td p, th p) {
97
+ margin: 0;
98
+ line-height: 24px;
99
+ }
100
+ :is(.tn-prose, .vp-doc) :where(hr) {
101
+ margin: 16px 0;
102
+ border: 0;
103
+ border-top: 1px solid var(--tn-c-text-2);
104
+ }
105
+ :is(.tn-prose, .vp-doc) :where(img) {
106
+ max-width: 100%;
107
+ height: auto;
108
+ }
109
+ :is(.tn-prose, .vp-doc) :where(figure.tn-image) {
110
+ display: block;
111
+ width: fit-content;
112
+ max-width: 100%;
113
+ margin: 16px 0;
114
+ text-align: center;
115
+ }
116
+ :is(.tn-prose, .vp-doc) :where(figure.tn-image.tn-image--center) {
117
+ margin-left: auto;
118
+ margin-right: auto;
119
+ }
120
+ :is(.tn-prose, .vp-doc) :where(figure.tn-image.tn-image--right) {
121
+ margin-left: auto;
122
+ }
123
+ :is(.tn-prose, .vp-doc) :where(figure.tn-image > img) {
124
+ display: block;
125
+ width: 100%;
126
+ height: auto;
127
+ }
128
+ :is(.tn-prose, .vp-doc) :where(figure.tn-image > figcaption) {
129
+ margin: 8px 0 0;
130
+ color: var(--tn-c-text-2);
131
+ font-size: 13px;
132
+ line-height: 20px;
133
+ text-align: center;
134
+ }
135
+
136
+ /* Callouts — same soft-tint cards as Desk visual (no left accent bar). */
137
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block) {
138
+ box-sizing: border-box;
139
+ margin: 16px 0;
140
+ padding: 12px 16px;
141
+ border: 1px solid transparent;
142
+ border-radius: 8px;
143
+ color: var(--tn-c-text);
144
+ font-size: inherit;
145
+ line-height: 1.5;
146
+ }
147
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block.tip) {
148
+ background-color: var(--tn-c-highlight);
149
+ }
150
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block.info),
151
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block.note),
152
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block.details) {
153
+ background-color: var(--tn-c-hover);
154
+ }
155
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block.warning) {
156
+ background-color: var(--tn-c-warning-soft);
157
+ }
158
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block.danger) {
159
+ background-color: var(--tn-c-danger-soft);
160
+ }
161
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block-title) {
162
+ margin: 0 0 2px;
163
+ font-size: inherit;
164
+ font-weight: 600;
165
+ line-height: 1.5;
166
+ color: var(--tn-c-text);
167
+ }
168
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block > p:not(.tn-custom-block-title)) {
169
+ margin: 0;
170
+ padding: 4px 0;
171
+ font-size: 1em;
172
+ line-height: 1.5;
173
+ }
174
+ :is(.tn-prose, .vp-doc) :where(.tn-custom-block > :last-child) {
175
+ margin-bottom: 0;
176
+ }
177
+ :is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary) {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 8px;
181
+ margin: 0 0 8px;
182
+ cursor: pointer;
183
+ list-style: none;
184
+ user-select: none;
185
+ font-weight: 600;
186
+ line-height: 1.5;
187
+ }
188
+ :is(.tn-prose, .vp-doc) :where(details.tn-custom-block:not([open]) > summary) {
189
+ margin-bottom: 0;
190
+ }
191
+ :is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary::-webkit-details-marker) {
192
+ display: none;
193
+ }
194
+ :is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary::before) {
195
+ content: "";
196
+ flex: none;
197
+ box-sizing: border-box;
198
+ width: 0.42em;
199
+ height: 0.42em;
200
+ margin-top: 0.05em;
201
+ border-right: 1.5px solid currentColor;
202
+ border-bottom: 1.5px solid currentColor;
203
+ color: var(--tn-c-text-2);
204
+ transform: rotate(-45deg);
205
+ transform-origin: 50% 50%;
206
+ transition: transform 120ms ease;
207
+ }
208
+ :is(.tn-prose, .vp-doc) :where(details.tn-custom-block[open] > summary::before) {
209
+ margin-top: -0.05em;
210
+ transform: rotate(45deg);
211
+ }