Commit
•
8811ee0
1
Parent(s):
e6bc023
Syntax highlighting + copy to clipboard button (#30)
Browse files* add highlight.js for syntax highlighting + add CodeBlock and CopyToClipboard components
* use clipboard.js for CopyToClipboard button + add Tooltip component
* do not include Tailwind classes in template since it can't parse them
* remove hack to strip model messages "endoftext" for now since it breaks code blocks
* move styles.css to /styles/main.css
* fix import + remove unused component
* auto detect language if not returned by model
* remove clipboard.js dependency and use native API
* rename icons with Icon prefix
---------
Co-authored-by: Eliott C <[email protected]>
- package-lock.json +9 -0
- package.json +1 -0
- src/lib/components/CopyToClipBoardBtn.svelte +50 -0
- src/lib/components/Tooltip.svelte +22 -0
- src/lib/components/chat/ChatMessage.svelte +69 -2
- src/lib/components/icons/{Chevron.svelte → IconChevron.svelte} +0 -0
- src/lib/components/icons/IconCopy.svelte +22 -0
- src/routes/+layout.svelte +1 -1
- src/routes/conversation/[id]/+page.svelte +1 -5
- src/styles/highlight-js.css +178 -0
- src/{styles.css → styles/main.css} +16 -0
package-lock.json
CHANGED
@@ -11,6 +11,7 @@
|
|
11 |
"@huggingface/inference": "^2.0.0-rc2",
|
12 |
"autoprefixer": "^10.4.14",
|
13 |
"date-fns": "^2.29.3",
|
|
|
14 |
"marked": "^4.3.0",
|
15 |
"mongodb": "^5.3.0",
|
16 |
"postcss": "^8.4.21",
|
@@ -2124,6 +2125,14 @@
|
|
2124 |
"node": ">=8"
|
2125 |
}
|
2126 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2127 |
"node_modules/ignore": {
|
2128 |
"version": "5.2.4",
|
2129 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
|
|
11 |
"@huggingface/inference": "^2.0.0-rc2",
|
12 |
"autoprefixer": "^10.4.14",
|
13 |
"date-fns": "^2.29.3",
|
14 |
+
"highlight.js": "^11.7.0",
|
15 |
"marked": "^4.3.0",
|
16 |
"mongodb": "^5.3.0",
|
17 |
"postcss": "^8.4.21",
|
|
|
2125 |
"node": ">=8"
|
2126 |
}
|
2127 |
},
|
2128 |
+
"node_modules/highlight.js": {
|
2129 |
+
"version": "11.7.0",
|
2130 |
+
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz",
|
2131 |
+
"integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==",
|
2132 |
+
"engines": {
|
2133 |
+
"node": ">=12.0.0"
|
2134 |
+
}
|
2135 |
+
},
|
2136 |
"node_modules/ignore": {
|
2137 |
"version": "5.2.4",
|
2138 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
package.json
CHANGED
@@ -34,6 +34,7 @@
|
|
34 |
"@huggingface/inference": "^2.0.0-rc2",
|
35 |
"autoprefixer": "^10.4.14",
|
36 |
"date-fns": "^2.29.3",
|
|
|
37 |
"marked": "^4.3.0",
|
38 |
"mongodb": "^5.3.0",
|
39 |
"postcss": "^8.4.21",
|
|
|
34 |
"@huggingface/inference": "^2.0.0-rc2",
|
35 |
"autoprefixer": "^10.4.14",
|
36 |
"date-fns": "^2.29.3",
|
37 |
+
"highlight.js": "^11.7.0",
|
38 |
"marked": "^4.3.0",
|
39 |
"mongodb": "^5.3.0",
|
40 |
"postcss": "^8.4.21",
|
src/lib/components/CopyToClipBoardBtn.svelte
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { onDestroy } from 'svelte';
|
3 |
+
|
4 |
+
import IconCopy from './icons/IconCopy.svelte';
|
5 |
+
import Tooltip from './Tooltip.svelte';
|
6 |
+
|
7 |
+
export let classNames = '';
|
8 |
+
export let value: string;
|
9 |
+
|
10 |
+
let isSuccess = false;
|
11 |
+
let timeout: any;
|
12 |
+
|
13 |
+
const handleClick = async () => {
|
14 |
+
// writeText() can be unavailable or fail in some cases (iframe, etc) so we try/catch
|
15 |
+
try {
|
16 |
+
await navigator.clipboard.writeText(value);
|
17 |
+
|
18 |
+
isSuccess = true;
|
19 |
+
if (timeout) {
|
20 |
+
clearTimeout(timeout);
|
21 |
+
}
|
22 |
+
timeout = setTimeout(() => {
|
23 |
+
isSuccess = false;
|
24 |
+
}, 1000);
|
25 |
+
} catch (err) {
|
26 |
+
console.error(err);
|
27 |
+
}
|
28 |
+
};
|
29 |
+
|
30 |
+
onDestroy(() => {
|
31 |
+
if (timeout) {
|
32 |
+
clearTimeout(timeout);
|
33 |
+
}
|
34 |
+
});
|
35 |
+
</script>
|
36 |
+
|
37 |
+
<button
|
38 |
+
class="btn text-sm rounded-lg border py-2 px-2 shadow-sm border-gray-200 active:shadow-inner dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-400 transition-all {classNames}
|
39 |
+
{!isSuccess && 'text-gray-200 dark:text-gray-200'}
|
40 |
+
{isSuccess && 'text-green-500'}
|
41 |
+
"
|
42 |
+
title={'Copy to clipboard'}
|
43 |
+
type="button"
|
44 |
+
on:click={handleClick}
|
45 |
+
>
|
46 |
+
<span class="relative">
|
47 |
+
<IconCopy />
|
48 |
+
<Tooltip classNames={isSuccess ? 'opacity-100' : 'opacity-0'} />
|
49 |
+
</span>
|
50 |
+
</button>
|
src/lib/components/Tooltip.svelte
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let classNames = "";
|
3 |
+
export let label = "Copied";
|
4 |
+
export let position = "left-1/2 top-full transform -translate-x-1/2 translate-y-2";
|
5 |
+
</script>
|
6 |
+
|
7 |
+
<div
|
8 |
+
class="
|
9 |
+
pointer-events-none absolute rounded bg-black py-1 px-2 font-normal leading-tight text-white shadow transition-opacity
|
10 |
+
{position}
|
11 |
+
{classNames}
|
12 |
+
"
|
13 |
+
>
|
14 |
+
<div
|
15 |
+
class="absolute bottom-full left-1/2 h-0 w-0 -translate-x-1/2 transform border-4 border-t-0 border-black"
|
16 |
+
style="
|
17 |
+
border-left-color: transparent;
|
18 |
+
border-right-color: transparent;
|
19 |
+
"
|
20 |
+
/>
|
21 |
+
{label}
|
22 |
+
</div>
|
src/lib/components/chat/ChatMessage.svelte
CHANGED
@@ -1,8 +1,74 @@
|
|
1 |
<script lang="ts">
|
2 |
import { marked } from 'marked';
|
3 |
-
import type { Message } from '$lib/
|
|
|
|
|
|
|
4 |
|
5 |
export let message: Message;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
</script>
|
7 |
|
8 |
{#if message.from === 'assistant'}
|
@@ -14,8 +80,9 @@
|
|
14 |
/>
|
15 |
<div
|
16 |
class="group relative rounded-2xl px-5 py-3.5 border border-gray-100 bg-gradient-to-br from-gray-50 dark:from-gray-800/40 dark:border-gray-800 prose text-gray-600 dark:text-gray-300"
|
|
|
17 |
>
|
18 |
-
{@html
|
19 |
</div>
|
20 |
</div>
|
21 |
{/if}
|
|
|
1 |
<script lang="ts">
|
2 |
import { marked } from 'marked';
|
3 |
+
import type { Message } from '$lib/Types';
|
4 |
+
import { afterUpdate } from 'svelte';
|
5 |
+
|
6 |
+
import CopyToClipBoardBtn from '../CopyToClipBoardBtn.svelte';
|
7 |
|
8 |
export let message: Message;
|
9 |
+
let html = '';
|
10 |
+
let el: HTMLElement;
|
11 |
+
|
12 |
+
const renderer = new marked.Renderer();
|
13 |
+
|
14 |
+
// Add wrapper to code blocks
|
15 |
+
renderer.code = (code, lang) => {
|
16 |
+
return `
|
17 |
+
<div class="code-block">
|
18 |
+
<pre>
|
19 |
+
<code class="language-${lang}">${code}</code>
|
20 |
+
</pre>
|
21 |
+
</div>
|
22 |
+
`.replaceAll('\t', '');
|
23 |
+
};
|
24 |
+
|
25 |
+
const handleParsed = (err: Error | null, parsedHtml: string) => {
|
26 |
+
if (err) {
|
27 |
+
console.error(err);
|
28 |
+
} else {
|
29 |
+
html = parsedHtml;
|
30 |
+
}
|
31 |
+
};
|
32 |
+
|
33 |
+
const options: marked.MarkedOptions = {
|
34 |
+
...marked.getDefaults(),
|
35 |
+
gfm: true,
|
36 |
+
highlight: (code, lang, callback) => {
|
37 |
+
import('highlight.js').then(
|
38 |
+
({ default: hljs }) => {
|
39 |
+
const language = hljs.getLanguage(lang);
|
40 |
+
callback?.(null, hljs.highlightAuto(code, language?.aliases).value);
|
41 |
+
},
|
42 |
+
(err) => {
|
43 |
+
console.error(err);
|
44 |
+
callback?.(err);
|
45 |
+
}
|
46 |
+
);
|
47 |
+
},
|
48 |
+
renderer
|
49 |
+
};
|
50 |
+
|
51 |
+
$: marked(message.content, options, handleParsed);
|
52 |
+
|
53 |
+
afterUpdate(() => {
|
54 |
+
if (el) {
|
55 |
+
const codeBlocks = el.querySelectorAll('.code-block');
|
56 |
+
|
57 |
+
// Add copy to clipboard button to each code block
|
58 |
+
codeBlocks.forEach((block) => {
|
59 |
+
if (block.classList.contains('has-copy-btn')) return;
|
60 |
+
|
61 |
+
new CopyToClipBoardBtn({
|
62 |
+
target: block,
|
63 |
+
props: {
|
64 |
+
value: (block as HTMLElement).innerText ?? '',
|
65 |
+
classNames: 'absolute top-2 right-2'
|
66 |
+
}
|
67 |
+
});
|
68 |
+
block.classList.add('has-copy-btn');
|
69 |
+
});
|
70 |
+
}
|
71 |
+
});
|
72 |
</script>
|
73 |
|
74 |
{#if message.from === 'assistant'}
|
|
|
80 |
/>
|
81 |
<div
|
82 |
class="group relative rounded-2xl px-5 py-3.5 border border-gray-100 bg-gradient-to-br from-gray-50 dark:from-gray-800/40 dark:border-gray-800 prose text-gray-600 dark:text-gray-300"
|
83 |
+
bind:this={el}
|
84 |
>
|
85 |
+
{@html html}
|
86 |
</div>
|
87 |
</div>
|
88 |
{/if}
|
src/lib/components/icons/{Chevron.svelte → IconChevron.svelte}
RENAMED
File without changes
|
src/lib/components/icons/IconCopy.svelte
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let classNames = "";
|
3 |
+
</script>
|
4 |
+
|
5 |
+
<svg
|
6 |
+
class={classNames}
|
7 |
+
xmlns="http://www.w3.org/2000/svg"
|
8 |
+
aria-hidden="true"
|
9 |
+
fill="currentColor"
|
10 |
+
focusable="false"
|
11 |
+
role="img"
|
12 |
+
width="1em"
|
13 |
+
height="1em"
|
14 |
+
preserveAspectRatio="xMidYMid meet"
|
15 |
+
viewBox="0 0 32 32"
|
16 |
+
>
|
17 |
+
<path
|
18 |
+
d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"
|
19 |
+
transform="translate(0)"
|
20 |
+
/>
|
21 |
+
<path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)" /><rect fill="none" width="32" height="32" />
|
22 |
+
</svg>
|
src/routes/+layout.svelte
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import '../styles.css';
|
3 |
import type { LayoutData } from './$types';
|
4 |
|
5 |
export let data: LayoutData;
|
|
|
1 |
<script lang="ts">
|
2 |
+
import '../styles/main.css';
|
3 |
import type { LayoutData } from './$types';
|
4 |
|
5 |
export let data: LayoutData;
|
src/routes/conversation/[id]/+page.svelte
CHANGED
@@ -46,12 +46,8 @@
|
|
46 |
// First token has a space at the beginning, trim it
|
47 |
messages = [...messages, { from: 'assistant', content: data.token.text.trimStart() }];
|
48 |
} else {
|
49 |
-
|
50 |
-
|
51 |
-
lastMessage.content += isEndOfText ? data.token.text.replace('<', '') : data.token.text;
|
52 |
messages = [...messages];
|
53 |
-
|
54 |
-
if (isEndOfText) break;
|
55 |
}
|
56 |
}
|
57 |
}
|
|
|
46 |
// First token has a space at the beginning, trim it
|
47 |
messages = [...messages, { from: 'assistant', content: data.token.text.trimStart() }];
|
48 |
} else {
|
49 |
+
lastMessage.content += data.token.text;
|
|
|
|
|
50 |
messages = [...messages];
|
|
|
|
|
51 |
}
|
52 |
}
|
53 |
}
|
src/styles/highlight-js.css
ADDED
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@import 'highlight.js/styles/atom-one-light';
|
2 |
+
|
3 |
+
/* Dark Theme */
|
4 |
+
/*
|
5 |
+
Night Owl for highlight.js (c) Carl Baxter <[email protected]>
|
6 |
+
An adaptation of Sarah Drasner's Night Owl VS Code Theme
|
7 |
+
https://github.com/sdras/night-owl-vscode-theme
|
8 |
+
Copyright (c) 2018 Sarah Drasner
|
9 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
10 |
+
of this software and associated documentation files (the "Software"), to deal
|
11 |
+
in the Software without restriction, including without limitation the rights
|
12 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13 |
+
copies of the Software, and to permit persons to whom the Software is
|
14 |
+
furnished to do so, subject to the following conditions:
|
15 |
+
The above copyright notice and this permission notice shall be included in all
|
16 |
+
copies or substantial portions of the Software.
|
17 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23 |
+
SOFTWARE.
|
24 |
+
*/
|
25 |
+
|
26 |
+
.dark .hljs {
|
27 |
+
display: block;
|
28 |
+
overflow-x: auto;
|
29 |
+
padding: 0.5em;
|
30 |
+
background: #011627;
|
31 |
+
color: #d6deeb;
|
32 |
+
}
|
33 |
+
|
34 |
+
/* General Purpose */
|
35 |
+
.dark .hljs-keyword {
|
36 |
+
color: #c792ea;
|
37 |
+
font-style: italic;
|
38 |
+
}
|
39 |
+
.dark .hljs-built_in {
|
40 |
+
color: #addb67;
|
41 |
+
font-style: italic;
|
42 |
+
}
|
43 |
+
.dark .hljs-type {
|
44 |
+
color: #82aaff;
|
45 |
+
}
|
46 |
+
.dark .hljs-literal {
|
47 |
+
color: #ff5874;
|
48 |
+
}
|
49 |
+
.dark .hljs-number {
|
50 |
+
color: #f78c6c;
|
51 |
+
}
|
52 |
+
.dark .hljs-regexp {
|
53 |
+
color: #5ca7e4;
|
54 |
+
}
|
55 |
+
.dark .hljs-string {
|
56 |
+
color: #ecc48d;
|
57 |
+
}
|
58 |
+
.dark .hljs-subst {
|
59 |
+
color: #d3423e;
|
60 |
+
}
|
61 |
+
.dark .hljs-symbol {
|
62 |
+
color: #82aaff;
|
63 |
+
}
|
64 |
+
.dark .hljs-class {
|
65 |
+
color: #ffcb8b;
|
66 |
+
}
|
67 |
+
.dark .hljs-function {
|
68 |
+
color: #82aaff;
|
69 |
+
}
|
70 |
+
.dark .hljs-title {
|
71 |
+
color: #dcdcaa;
|
72 |
+
font-style: italic;
|
73 |
+
}
|
74 |
+
.dark .hljs-params {
|
75 |
+
color: #7fdbca;
|
76 |
+
}
|
77 |
+
|
78 |
+
/* Meta */
|
79 |
+
.dark .hljs-comment {
|
80 |
+
color: #637777;
|
81 |
+
font-style: italic;
|
82 |
+
}
|
83 |
+
.dark .hljs-doctag {
|
84 |
+
color: #7fdbca;
|
85 |
+
}
|
86 |
+
.dark .hljs-meta {
|
87 |
+
color: #82aaff;
|
88 |
+
}
|
89 |
+
.dark .hljs-meta-keyword {
|
90 |
+
color: #82aaff;
|
91 |
+
}
|
92 |
+
.dark .hljs-meta-string {
|
93 |
+
color: #ecc48d;
|
94 |
+
}
|
95 |
+
|
96 |
+
/* Tags, attributes, config */
|
97 |
+
.dark .hljs-section {
|
98 |
+
color: #82b1ff;
|
99 |
+
}
|
100 |
+
.dark .hljs-tag,
|
101 |
+
.dark .hljs-name,
|
102 |
+
.dark .hljs-builtin-name {
|
103 |
+
color: #7fdbca;
|
104 |
+
}
|
105 |
+
.dark .hljs-attr {
|
106 |
+
color: #7fdbca;
|
107 |
+
}
|
108 |
+
.dark .hljs-attribute {
|
109 |
+
color: #80cbc4;
|
110 |
+
}
|
111 |
+
.dark .hljs-variable {
|
112 |
+
color: #addb67;
|
113 |
+
}
|
114 |
+
|
115 |
+
/* Markup */
|
116 |
+
.dark .hljs-bullet {
|
117 |
+
color: #d9f5dd;
|
118 |
+
}
|
119 |
+
.dark .hljs-code {
|
120 |
+
color: #80cbc4;
|
121 |
+
}
|
122 |
+
.dark .hljs-emphasis {
|
123 |
+
color: #c792ea;
|
124 |
+
font-style: italic;
|
125 |
+
}
|
126 |
+
.dark .hljs-strong {
|
127 |
+
color: #addb67;
|
128 |
+
font-weight: bold;
|
129 |
+
}
|
130 |
+
.dark .hljs-formula {
|
131 |
+
color: #c792ea;
|
132 |
+
}
|
133 |
+
.dark .hljs-link {
|
134 |
+
color: #ff869a;
|
135 |
+
}
|
136 |
+
.dark .hljs-quote {
|
137 |
+
color: #697098;
|
138 |
+
font-style: italic;
|
139 |
+
}
|
140 |
+
|
141 |
+
/* CSS */
|
142 |
+
.dark .hljs-selector-tag {
|
143 |
+
color: #ff6363;
|
144 |
+
}
|
145 |
+
|
146 |
+
.dark .hljs-selector-id {
|
147 |
+
color: #fad430;
|
148 |
+
}
|
149 |
+
|
150 |
+
.dark .hljs-selector-class {
|
151 |
+
color: #addb67;
|
152 |
+
font-style: italic;
|
153 |
+
}
|
154 |
+
|
155 |
+
.dark .hljs-selector-attr,
|
156 |
+
.dark .hljs-selector-pseudo {
|
157 |
+
color: #c792ea;
|
158 |
+
font-style: italic;
|
159 |
+
}
|
160 |
+
|
161 |
+
/* Templates */
|
162 |
+
.dark .hljs-template-tag {
|
163 |
+
color: #c792ea;
|
164 |
+
}
|
165 |
+
.dark .hljs-template-variable {
|
166 |
+
color: #addb67;
|
167 |
+
}
|
168 |
+
|
169 |
+
/* diff */
|
170 |
+
.dark .hljs-addition {
|
171 |
+
color: #addb67ff;
|
172 |
+
font-style: italic;
|
173 |
+
}
|
174 |
+
|
175 |
+
.dark .hljs-deletion {
|
176 |
+
color: #ef535090;
|
177 |
+
font-style: italic;
|
178 |
+
}
|
src/{styles.css → styles/main.css}
RENAMED
@@ -1,9 +1,25 @@
|
|
|
|
|
|
1 |
@tailwind base;
|
2 |
@tailwind components;
|
3 |
@tailwind utilities;
|
4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
@layer utilities {
|
6 |
.scrollbar-custom {
|
7 |
@apply !scrollbar-thin !scrollbar-w-1 !scrollbar-thumb-rounded-full !scrollbar-track-transparent !scrollbar-thumb-black/10 dark:!scrollbar-thumb-white/10;
|
8 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
}
|
|
|
1 |
+
@import './highlight-js.css';
|
2 |
+
|
3 |
@tailwind base;
|
4 |
@tailwind components;
|
5 |
@tailwind utilities;
|
6 |
|
7 |
+
@layer components {
|
8 |
+
.btn {
|
9 |
+
@apply cursor-pointer select-none inline-flex justify-center items-center whitespace-nowrap border focus:outline-none focus:ring;
|
10 |
+
}
|
11 |
+
}
|
12 |
+
|
13 |
@layer utilities {
|
14 |
.scrollbar-custom {
|
15 |
@apply !scrollbar-thin !scrollbar-w-1 !scrollbar-thumb-rounded-full !scrollbar-track-transparent !scrollbar-thumb-black/10 dark:!scrollbar-thumb-white/10;
|
16 |
}
|
17 |
+
|
18 |
+
.code-block {
|
19 |
+
@apply relative bg-gray-100 dark:bg-gray-950 rounded-lg my-4;
|
20 |
+
}
|
21 |
+
|
22 |
+
.code-block > pre {
|
23 |
+
@apply overflow-auto px-5 py-3.5 text-gray-500 dark:text-gray-400;
|
24 |
+
}
|
25 |
}
|