Merge branch 'themefixes'
This commit is contained in:
commit
a97761b69a
13 changed files with 324 additions and 35 deletions
|
|
@ -13,6 +13,7 @@ Clone of [T3 Chat](https://t3.chat/)
|
|||
- Privacy mode for streams and screen-sharing
|
||||
- Markdown rendered messages with syntax highlighting
|
||||
- Chat sharing
|
||||
- Keyboard shortcuts
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ Clone of [T3 Chat](https://t3.chat/)
|
|||
- [x] Syntax highlighting with Shiki/markdown renderer
|
||||
- [ ] Eliminate FOUC
|
||||
- [x] Cascade deletes
|
||||
- [ ] Google Auth
|
||||
- [x] Google Auth
|
||||
- [ ] Fix light mode (urgh)
|
||||
- [x] Privacy mode
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
"@fontsource-variable/open-sans": "^5.2.6",
|
||||
"better-auth": "^1.2.9",
|
||||
"convex-helpers": "^0.1.94",
|
||||
"hastscript": "^9.0.1",
|
||||
"markdown-it-async": "^2.2.0",
|
||||
"openai": "^5.3.0",
|
||||
"zod": "^3.25.64"
|
||||
|
|
|
|||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
|
|
@ -38,6 +38,9 @@ importers:
|
|||
convex-helpers:
|
||||
specifier: ^0.1.94
|
||||
version: 0.1.94(convex@1.24.8)(typescript@5.8.3)(zod@3.25.64)
|
||||
hastscript:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
markdown-it-async:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
|
|
@ -1634,12 +1637,18 @@ packages:
|
|||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
hastscript@9.0.1:
|
||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4072,6 +4081,10 @@ snapshots:
|
|||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
|
@ -4090,6 +4103,14 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hastscript@9.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-parse-selector: 4.0.0
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
|
||||
html-encoding-sniffer@4.0.0:
|
||||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
|
|
|||
160
src/app.css
160
src/app.css
|
|
@ -8,42 +8,41 @@
|
|||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(0.9754 0.0084 325.6414);
|
||||
--foreground: oklch(0.3257 0.1161 325.0372);
|
||||
--card: oklch(0.9754 0.0084 325.6414);
|
||||
--card-foreground: oklch(0.3257 0.1161 325.0372);
|
||||
/* Improved light theme with better harmony */
|
||||
--background: oklch(0.99 0.005 280);
|
||||
--foreground: oklch(0.15 0.02 280);
|
||||
--card: oklch(0.98 0.008 280);
|
||||
--card-foreground: oklch(0.15 0.02 280);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.3257 0.1161 325.0372);
|
||||
--popover-foreground: oklch(0.15 0.02 280);
|
||||
--primary: oklch(0.5797 0.1194 237.7893);
|
||||
--heading: oklch(0.5797 0.1194 237.7893);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.8696 0.0675 334.8991);
|
||||
--secondary-foreground: oklch(0.4448 0.1341 324.7991);
|
||||
--muted: oklch(0.9395 0.026 331.5454);
|
||||
--muted-foreground: oklch(0.4924 0.1244 324.4523);
|
||||
--accent: oklch(0.8696 0.0675 334.8991);
|
||||
--accent-foreground: oklch(0.4448 0.1341 324.7991);
|
||||
--secondary: oklch(0.92 0.015 280);
|
||||
--secondary-foreground: oklch(0.25 0.03 280);
|
||||
--muted: oklch(0.94 0.012 280);
|
||||
--muted-foreground: oklch(0.45 0.025 280);
|
||||
--accent: oklch(0.92 0.015 280);
|
||||
--accent-foreground: oklch(0.25 0.03 280);
|
||||
--destructive: oklch(0.5248 0.1368 20.8317);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.8568 0.0829 328.911);
|
||||
--input: oklch(0.8517 0.0558 336.6002);
|
||||
--ring: oklch(0.5916 0.218 0.5844);
|
||||
--border: oklch(0.88 0.02 280);
|
||||
--input: oklch(0.96 0.01 280);
|
||||
--ring: oklch(0.5797 0.1194 237.7893);
|
||||
--chart-1: oklch(0.6038 0.2363 344.4657);
|
||||
--chart-2: oklch(0.4445 0.2251 300.6246);
|
||||
--chart-3: oklch(0.379 0.0438 226.1538);
|
||||
--chart-4: oklch(0.833 0.1185 88.3461);
|
||||
--chart-5: oklch(0.7843 0.1256 58.9964);
|
||||
/* Subtle blue shift for sidebar colors in light mode */
|
||||
--sidebar: oklch(0.936 0.0288 280); /* Original hue 320.5788 -> 280 (more blue) */
|
||||
--sidebar-foreground: oklch(0.4948 0.1909 285); /* Original hue 354.5435 -> 285 */
|
||||
--sidebar-primary: oklch(0.3963 0.0251 270); /* Original hue 285.1962 -> 270 */
|
||||
--sidebar-primary-foreground: oklch(
|
||||
0.9668 0.0124 337.5228
|
||||
); /* No change, keep white/near white */
|
||||
--sidebar-accent: oklch(0.9789 0.0013 280); /* Original hue 106.4235 -> 280 */
|
||||
--sidebar-accent-foreground: oklch(0.3963 0.0251 270); /* Original hue 285.1962 -> 270 */
|
||||
--sidebar-border: oklch(0.9383 0.0026 280); /* Original hue 48.7178 -> 280 */
|
||||
--sidebar-ring: oklch(0.5916 0.218 0.5844);
|
||||
/* Harmonized sidebar colors for light mode */
|
||||
--sidebar: oklch(0.97 0.008 280);
|
||||
--sidebar-foreground: oklch(0.2 0.025 280);
|
||||
--sidebar-primary: oklch(0.5797 0.1194 237.7893);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.94 0.012 280);
|
||||
--sidebar-accent-foreground: oklch(0.25 0.03 280);
|
||||
--sidebar-border: oklch(0.9 0.015 280);
|
||||
--sidebar-ring: oklch(0.5797 0.1194 237.7893);
|
||||
--radius: 0.5rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
|
|
@ -82,13 +81,13 @@
|
|||
--chart-4: oklch(0.6193 0.2029 312.7422);
|
||||
--chart-5: oklch(0.6118 0.2093 6.1387);
|
||||
/* Subtle blue shift for sidebar colors in dark mode */
|
||||
--sidebar: oklch(0.1693 0.0143 280); /* Original hue 331.0475 -> 260 */
|
||||
--sidebar-foreground: oklch(0.8607 0.0293 265); /* Original hue 343.6612 -> 265 */
|
||||
--sidebar-primary: oklch(0.4882 0.2172 250); /* Original hue 264.3763 -> 250 */
|
||||
--sidebar-primary-foreground: oklch(1 0 0); /* No change, keep white */
|
||||
--sidebar-accent: oklch(0.2337 0.0261 260); /* Original hue 338.1961 -> 260 */
|
||||
--sidebar-accent-foreground: oklch(0.9674 0.0013 250); /* Original hue 286.3752 -> 250 */
|
||||
--sidebar-border: oklch(0 0 0); /* No change, keep black */
|
||||
--sidebar: oklch(0.1693 0.0143 280);
|
||||
--sidebar-foreground: oklch(0.8607 0.0293 265);
|
||||
--sidebar-primary: oklch(0.4882 0.2172 250);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.2337 0.0261 260);
|
||||
--sidebar-accent-foreground: oklch(0.9674 0.0013 250);
|
||||
--sidebar-border: oklch(0 0 0);
|
||||
--sidebar-ring: oklch(0.5797 0.1194 237.7893);
|
||||
--radius: 0.5rem;
|
||||
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||
|
|
@ -476,3 +475,100 @@
|
|||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Copy button */
|
||||
pre:has(code) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre button.copy {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
|
||||
@apply bg-background/80 hover:bg-background border-border hover:border-border/60 hover:text-foreground rounded-md border shadow-sm hover:shadow-md;
|
||||
|
||||
/* Improved hover and focus states */
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
@apply bg-background;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0px);
|
||||
transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@apply ring-ring ring-offset-background ring-2 ring-offset-2 outline-none;
|
||||
}
|
||||
|
||||
& span {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: transparent !important;
|
||||
}
|
||||
& svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
& .ready {
|
||||
background-image: url(/icons/copy.svg);
|
||||
}
|
||||
|
||||
& .success {
|
||||
display: none;
|
||||
background-image: url(/icons/copy-success.svg);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.copied {
|
||||
/* Light mode success state (dark blue text on light background) */
|
||||
@apply bg-primary/10 border-primary/30 text-primary;
|
||||
|
||||
& .success {
|
||||
display: block;
|
||||
animation: copySuccess 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
& .ready {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode adjustments for copy button success state */
|
||||
.dark pre button.copy.copied {
|
||||
/* Dark mode success state (light blue text on dark background) */
|
||||
@apply bg-primary/20 border-primary/40 text-primary-foreground;
|
||||
/* Using primary-foreground which is white/light for better visibility */
|
||||
}
|
||||
|
||||
/* Copy success animation */
|
||||
@keyframes copySuccess {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
src/app.html
10
src/app.html
|
|
@ -8,6 +8,16 @@
|
|||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<script>
|
||||
// check mode-watcher-mode variable, set body bg accordingly
|
||||
// TODO: make this smarter pls
|
||||
const mode = localStorage.getItem('mode-watcher-mode');
|
||||
if (mode === 'dark') {
|
||||
document.body.style.backgroundColor = 'oklch(0.2409 0.0201 307.5346)';
|
||||
} else {
|
||||
// document.body.style.backgroundColor = 'oklch(0.99 0.005 280)';
|
||||
}
|
||||
</script>
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
44
src/lib/components/ui/code-block/code-block.svelte
Normal file
44
src/lib/components/ui/code-block/code-block.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { UseClipboard } from '$lib/hooks/use-clipboard.svelte';
|
||||
import { cn } from '$lib/utils/utils';
|
||||
import CheckIcon from '~icons/lucide/check';
|
||||
import CopyIcon from '~icons/lucide/copy';
|
||||
import { scale } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
language?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { code, language, class: className }: Props = $props();
|
||||
|
||||
const clipboard = new UseClipboard();
|
||||
|
||||
function copyCode() {
|
||||
clipboard.copy(code);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={cn('group relative', className)}>
|
||||
<div class="absolute right-2 top-2 z-10">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="size-8 p-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onclick={copyCode}
|
||||
>
|
||||
{#if clipboard.status === 'success'}
|
||||
<div in:scale={{ duration: 200, start: 0.8 }}>
|
||||
<CheckIcon class="size-3.5" />
|
||||
</div>
|
||||
{:else}
|
||||
<CopyIcon class="size-3.5" />
|
||||
{/if}
|
||||
<span class="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</script>
|
||||
1
src/lib/components/ui/code-block/index.ts
Normal file
1
src/lib/components/ui/code-block/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as CodeBlock } from './code-block.svelte';
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
export function isString(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
export function isHtmlElement(value: unknown): value is HTMLElement {
|
||||
return value instanceof HTMLElement;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { fromAsyncCodeToHtml } from '@shikijs/markdown-it/async';
|
||||
import { h } from 'hastscript';
|
||||
import MarkdownItAsync from 'markdown-it-async';
|
||||
import { codeToHtml } from 'shiki';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
|
@ -14,6 +15,72 @@ md.use(
|
|||
light: 'github-light-default',
|
||||
dark: 'github-dark-default',
|
||||
},
|
||||
transformers: [
|
||||
{
|
||||
name: 'shiki-transformer-copy-button',
|
||||
pre(node) {
|
||||
const copyIcon = h(
|
||||
'svg',
|
||||
{
|
||||
width: '24',
|
||||
height: '24',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '2',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
[
|
||||
h('rect', { width: '14', height: '14', x: '8', y: '8', rx: '2', ry: '2' }),
|
||||
h('path', {
|
||||
d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2',
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
const checkIcon = h(
|
||||
'svg',
|
||||
{
|
||||
width: '24',
|
||||
height: '24',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': '2',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
},
|
||||
[h('path', { d: 'M20 6 9 17l-5-5' })]
|
||||
);
|
||||
|
||||
const button = h(
|
||||
'button',
|
||||
{
|
||||
class: 'copy',
|
||||
'data-code': this.source,
|
||||
onclick: `
|
||||
navigator.clipboard.writeText(this.dataset.code);
|
||||
this.classList.add('copied');
|
||||
setTimeout(() => this.classList.remove('copied'), ${3000})
|
||||
`,
|
||||
},
|
||||
[
|
||||
h('span', { class: 'ready', style: 'background-color: transparent !important;' }, [
|
||||
copyIcon,
|
||||
]),
|
||||
h(
|
||||
'span',
|
||||
{ class: 'success', style: 'background-color: transparent !important;' },
|
||||
[checkIcon]
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
node.children.push(button);
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
import LockOpenIcon from '~icons/lucide/lock-open';
|
||||
import StopIcon from '~icons/lucide/square';
|
||||
import SearchModal from './search-modal.svelte';
|
||||
import { isHtmlElement } from '$lib/utils/is.js';
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
|
|
@ -408,6 +409,21 @@
|
|||
<title>Chat | Thom.chat</title>
|
||||
</svelte:head>
|
||||
|
||||
<svelte:document
|
||||
onclick={(e) => {
|
||||
const el = e.target as HTMLElement;
|
||||
const closestCopyButton = el.closest('.copy[data-code]');
|
||||
if (!isHtmlElement(closestCopyButton)) return;
|
||||
|
||||
const code = closestCopyButton.dataset.code;
|
||||
if (!code) return;
|
||||
|
||||
navigator.clipboard.writeText(code);
|
||||
closestCopyButton.classList.add('copied');
|
||||
setTimeout(() => closestCopyButton.classList.remove('copied'), 3000);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Sidebar.Root
|
||||
bind:open={sidebarOpen}
|
||||
class="h-screen overflow-clip"
|
||||
|
|
@ -554,7 +570,8 @@
|
|||
class={[
|
||||
'bg-background/50 text-foreground dark:bg-secondary/20 relative flex w-full flex-col items-stretch gap-2 rounded-t-xl border border-b-0 border-white/70 pt-3 pb-3 outline-8 dark:border-white/10',
|
||||
'transition duration-200',
|
||||
'outline-primary/1 group-focus-within:outline-primary/10',
|
||||
'outline-primary/10 group-focus-within:outline-primary/20',
|
||||
'dark:outline-primary/1 dark:group-focus-within:outline-primary/10',
|
||||
]}
|
||||
style="box-shadow: rgba(0, 0, 0, 0.1) 0px 80px 50px 0px, rgba(0, 0, 0, 0.07) 0px 50px 30px 0px, rgba(0, 0, 0, 0.06) 0px 30px 15px 0px, rgba(0, 0, 0, 0.04) 0px 15px 8px, rgba(0, 0, 0, 0.04) 0px 6px 4px, rgba(0, 0, 0, 0.02) 0px 2px 2px;"
|
||||
onsubmit={(e) => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
base: 'prose rounded-xl p-2',
|
||||
variants: {
|
||||
role: {
|
||||
user: 'bg-secondary/50 border border-secondary/70 px-3 py-2 !text-primary-foreground self-end',
|
||||
user: 'bg-secondary/50 border border-secondary/70 px-3 py-2 !text-black/80 dark:!text-primary-foreground self-end',
|
||||
assistant: 'text-foreground',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
12
static/icons/copy-success.svg
Normal file
12
static/icons/copy-success.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-check-icon lucide-check"><path d="M20 6 9 17l-5-5" /></svg
|
||||
>
|
||||
|
After Width: | Height: | Size: 273 B |
15
static/icons/copy.svg
Normal file
15
static/icons/copy.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-copy-icon lucide-copy"
|
||||
><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path
|
||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||
/></svg
|
||||
>
|
||||
|
After Width: | Height: | Size: 373 B |
Loading…
Add table
Reference in a new issue