optimized webchat
This commit is contained in:
@@ -1,23 +1,80 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { WerkChatSession } from "@kontextwerk/web-sdk"
|
import { onDestroy } from "svelte"
|
||||||
|
import { WerkChatSession, type ChatMessage } from "@kontextwerk/web-sdk"
|
||||||
import InputRow from "./InputRow.svelte"
|
import InputRow from "./InputRow.svelte"
|
||||||
import Messages from "./Messages.svelte"
|
import Messages from "./Messages.svelte"
|
||||||
function generateUniqueId() {
|
function generateUniqueId() {
|
||||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||||
}
|
}
|
||||||
let chat = $derived(
|
let chat: WerkChatSession
|
||||||
|
let messages = $state<ChatMessage[]>([])
|
||||||
|
let isStreaming = $state(false)
|
||||||
|
|
||||||
|
const refreshFromSession = () => {
|
||||||
|
if (!chat) return
|
||||||
|
messages = [...chat.messages]
|
||||||
|
isStreaming = chat.isStreaming
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSession = () =>
|
||||||
new WerkChatSession(generateUniqueId(), {
|
new WerkChatSession(generateUniqueId(), {
|
||||||
url: "https://2schat-server.kontextwerk.info/api/v1/chatbot/stream",
|
url: "https://2schat-server.kontextwerk.info/api/v1/chatbot/stream",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
events: {
|
||||||
|
onOpen: refreshFromSession,
|
||||||
|
onToken: refreshFromSession,
|
||||||
|
onHeartbeat: refreshFromSession,
|
||||||
|
onFinal: refreshFromSession,
|
||||||
|
onError: refreshFromSession,
|
||||||
|
onEvent: refreshFromSession,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
chat = createSession()
|
||||||
|
refreshFromSession()
|
||||||
|
|
||||||
|
const sendMessage = (text: string) => {
|
||||||
|
const trimmed = text.trim()
|
||||||
|
if (!trimmed || !chat) return
|
||||||
|
const promise = chat.generateResponse(trimmed)
|
||||||
|
refreshFromSession()
|
||||||
|
void promise
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Chat response failed", err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refreshFromSession()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
chat?.abortActiveStream?.()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="chat-wrapper">
|
<div class="chat-wrapper">
|
||||||
<Messages {chat} />
|
<Messages
|
||||||
<InputRow {chat} />
|
{messages}
|
||||||
|
streaming={isStreaming}
|
||||||
|
/>
|
||||||
|
<InputRow
|
||||||
|
onSend={sendMessage}
|
||||||
|
disabled={isStreaming}
|
||||||
|
quickMessages={[
|
||||||
|
{
|
||||||
|
title: "Erzähl mir was über",
|
||||||
|
highlight: "Kontextwerk",
|
||||||
|
message: "Erzähl mir bitte mehr über Kontextwerk.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ich möchte euch",
|
||||||
|
highlight: "kontaktieren",
|
||||||
|
message: "Ich möchte euch kontaktieren. Welche Möglichkeiten gibt es?",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
@@ -1,43 +1,91 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { mdiSendVariantOutline } from "@mdi/js"
|
import { mdiSendVariantOutline } from "@mdi/js"
|
||||||
import Icon from "../widgets/Icon.svelte"
|
import Icon from "../widgets/Icon.svelte"
|
||||||
import type { WerkChatSession } from "@kontextwerk/web-sdk"
|
import { tick } from "svelte"
|
||||||
let { chat }: { chat: WerkChatSession } = $props()
|
let {
|
||||||
|
onSend,
|
||||||
|
disabled = false,
|
||||||
|
quickMessages = [
|
||||||
|
{
|
||||||
|
title: "Erzähl mir was über",
|
||||||
|
highlight: "Kontextwerk",
|
||||||
|
message: "Erzähl mir bitte mehr über Kontextwerk.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ich möchte euch",
|
||||||
|
highlight: "kontaktieren",
|
||||||
|
message: "Ich möchte euch kontaktieren. Welche Möglichkeiten gibt es?",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}: {
|
||||||
|
onSend: (message: string) => void
|
||||||
|
disabled?: boolean
|
||||||
|
quickMessages?: Array<{ title: string; highlight: string; message: string }>
|
||||||
|
} = $props()
|
||||||
|
|
||||||
let value = $state("")
|
let value = $state("")
|
||||||
let textareaEl: HTMLTextAreaElement = $state(null)
|
let textareaEl: HTMLTextAreaElement | null = $state(null)
|
||||||
function autoResize() {
|
function autoResize() {
|
||||||
if (textareaEl) {
|
if (textareaEl) {
|
||||||
textareaEl.style.height = "auto"
|
textareaEl.style.height = "auto"
|
||||||
textareaEl.style.height = textareaEl.scrollHeight + "px"
|
textareaEl.style.height = textareaEl.scrollHeight + "px"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let submittedOnce = $state(false)
|
||||||
|
const submit = async () => {
|
||||||
|
submittedOnce = true
|
||||||
|
|
||||||
|
const text = value.trim()
|
||||||
|
if (!text || disabled) return
|
||||||
|
onSend(text)
|
||||||
|
value = ""
|
||||||
|
await tick()
|
||||||
|
autoResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
void tick().then(autoResize)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
|
{#if quickMessages.length && !submittedOnce}
|
||||||
|
<div class="quick-actions">
|
||||||
|
{#each quickMessages as quick, index (index)}
|
||||||
|
<button
|
||||||
|
class="quick-chip"
|
||||||
|
type="button"
|
||||||
|
{disabled}
|
||||||
|
onclick={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
onSend(quick.message)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="title">{quick.title}</span>
|
||||||
|
<span class="highlight">{quick.highlight}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Schreibe eine Nachricht..."
|
placeholder="Schreibe eine Nachricht..."
|
||||||
bind:value
|
bind:value
|
||||||
rows="1"
|
rows="1"
|
||||||
bind:this={textareaEl}
|
bind:this={textareaEl}
|
||||||
oninput={autoResize}
|
oninput={autoResize}
|
||||||
|
{disabled}
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (value.trim()) {
|
void submit()
|
||||||
chat.generateResponse(value.trim())
|
|
||||||
value = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
></textarea>
|
></textarea>
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
onclick={() => {
|
onclick={() => void submit()}
|
||||||
if (value.trim()) {
|
disabled={disabled || !value.trim()}
|
||||||
chat.generateResponse(value.trim())
|
|
||||||
value = ""
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-label="Nachricht senden"
|
aria-label="Nachricht senden"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -54,6 +102,59 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0px 0.6rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
|
||||||
|
.quick-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.65rem 0.95rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-200);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid var(--bg-100);
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 0.25rem;
|
||||||
|
transition:
|
||||||
|
background 0.3s ease,
|
||||||
|
transform 0.3s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--bg-100);
|
||||||
|
border: 1px solid var(--bg-200);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--neutral-white);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 3.6rem;
|
min-height: 3.6rem;
|
||||||
@@ -65,6 +166,8 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: white;
|
color: white;
|
||||||
|
border-top: 1px solid white;
|
||||||
|
transition: border-top 0.3s ease;
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-top: 1px solid var(--primary-100);
|
border-top: 1px solid var(--primary-100);
|
||||||
@@ -79,6 +182,10 @@
|
|||||||
min-width: unset;
|
min-width: unset;
|
||||||
background-color: var(--primary-100);
|
background-color: var(--primary-100);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +1,141 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { WerkChatSession } from "@kontextwerk/web-sdk"
|
import type { ChatMessage } from "@kontextwerk/web-sdk"
|
||||||
let {
|
let {
|
||||||
chat,
|
messages = [],
|
||||||
|
streaming,
|
||||||
}: {
|
}: {
|
||||||
chat: WerkChatSession
|
messages?: ChatMessage[]
|
||||||
|
streaming: boolean
|
||||||
} = $props()
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div class="messages">
|
||||||
{#each chat.messages as message, idx (idx)}
|
{#each messages as message, idx (idx)}
|
||||||
<div
|
{#if message.role === "assistant"}
|
||||||
class="message"
|
<div class="message assistant">
|
||||||
class:assistant={message.role === "assistant"}
|
<div class="bubble assistant-bubble">
|
||||||
class:user={message.role === "user"}
|
{@html message.content || ""}
|
||||||
>
|
</div>
|
||||||
{@html message.content}
|
</div>
|
||||||
</div>
|
{:else if message.role === "user"}
|
||||||
|
<div class="message user">
|
||||||
|
<div class="bubble user-bubble">
|
||||||
|
{@html message.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="message system">
|
||||||
|
<div class="bubble system-bubble">
|
||||||
|
{@html message.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if streaming}
|
||||||
|
<div class="message assistant typing-indicator">
|
||||||
|
<div class="bubble assistant-bubble">
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
<span class="dot"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
.messages {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.9rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
color: var(--text-reverse-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 85%;
|
||||||
|
|
||||||
|
&.assistant {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.system {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
padding: 0.85rem 1.05rem;
|
||||||
|
border-radius: 1.1rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
box-shadow: 0 0.8rem 2rem rgba(0, 0, 0, 0.08);
|
||||||
|
position: relative;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
&.assistant-bubble {
|
||||||
|
background: white;
|
||||||
|
color: var(--text-reverse-100);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.user-bubble {
|
||||||
|
background: linear-gradient(135deg, var(--primary-100), var(--primary-200));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.system-bubble {
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
color: var(--text-reverse-100);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator .bubble {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator .dot {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
animation: typingAnimation 1.4s infinite both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator .dot:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator .dot:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typingAnimation {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message {
|
||||||
|
max-width: 92%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import ProductCategoryFrame from "../widgets/ProductCategoryFrame.svelte"
|
import ProductCategoryFrame from "../widgets/ProductCategoryFrame.svelte"
|
||||||
import CrinkledSection from "../CrinkledSection.svelte"
|
import CrinkledSection from "../CrinkledSection.svelte"
|
||||||
import { createVoicebotPreviewController } from "../voicebotDemo/voicebotPreviewController"
|
import { createVoicebotPreviewController } from "../voicebotDemo/voicebotPreviewController"
|
||||||
|
import Label from "../widgets/Label.svelte"
|
||||||
|
|
||||||
const voiceProperties: Array<{ title: string; icon: string; color: string }> = [
|
const voiceProperties: Array<{ title: string; icon: string; color: string }> = [
|
||||||
{
|
{
|
||||||
@@ -65,22 +66,23 @@
|
|||||||
aria-pressed={$status === "connected"}
|
aria-pressed={$status === "connected"}
|
||||||
aria-busy={$status === "connecting"}
|
aria-busy={$status === "connecting"}
|
||||||
aria-label="Voicebot Demo starten"
|
aria-label="Voicebot Demo starten"
|
||||||
on:click={() => void toggle()}
|
onclick={() => void toggle()}
|
||||||
on:keydown={handleKeydown}
|
onkeydown={handleKeydown}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/media/iphone.png"
|
src="/media/iphone.png"
|
||||||
alt="Kontextwerk is calling"
|
alt="Kontextwerk is calling"
|
||||||
/>
|
/>
|
||||||
<div class="shadow"></div>
|
<div class="shadow"></div>
|
||||||
<div
|
|
||||||
class="voice-overlay"
|
|
||||||
data-status={$status}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
<span>{$statusHint}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={() => void toggle()}
|
||||||
|
class="voice-status"
|
||||||
|
data-status={$status}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<Label label={$statusHint} />
|
||||||
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ProductCategoryFrame>
|
</ProductCategoryFrame>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -111,40 +113,6 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voice-overlay {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1.2rem;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
padding: 0.45rem 1.1rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(13, 12, 12, 0.8);
|
|
||||||
color: white;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
pointer-events: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition:
|
|
||||||
background 0.2s ease,
|
|
||||||
color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-overlay[data-status="connected"] {
|
|
||||||
background: rgba(76, 175, 80, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-overlay[data-status="connecting"] {
|
|
||||||
background: rgba(255, 152, 0, 0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-overlay[data-status="error"] {
|
|
||||||
background: rgba(235, 87, 87, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
@@ -154,4 +122,11 @@
|
|||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.voice-status {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const createVoicebotPreviewController = (): VoicebotPreviewController =>
|
|||||||
const statusHint = derived([statusStore, errorStore], ([$status, $error]) => {
|
const statusHint = derived([statusStore, errorStore], ([$status, $error]) => {
|
||||||
switch ($status) {
|
switch ($status) {
|
||||||
case "idle":
|
case "idle":
|
||||||
return "Tippen, um die Voice-Demo zu starten"
|
return "Tippen, um die Demo zu starten"
|
||||||
case "connecting":
|
case "connecting":
|
||||||
return "Verbindung wird aufgebaut …"
|
return "Verbindung wird aufgebaut …"
|
||||||
case "connected":
|
case "connected":
|
||||||
|
|||||||
66
frontend/src/lib/components/widgets/Label.svelte
Normal file
66
frontend/src/lib/components/widgets/Label.svelte
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
} = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<img
|
||||||
|
src="/media/bottomRightCrinkleRed.svg"
|
||||||
|
id="lefternCrinkle"
|
||||||
|
alt="crinkle"
|
||||||
|
/>
|
||||||
|
<em>
|
||||||
|
{label}
|
||||||
|
</em>
|
||||||
|
<img
|
||||||
|
id="righternCrinkle"
|
||||||
|
src="/media/topLeftCrinkleRed.svg"
|
||||||
|
alt="crinkle"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
span {
|
||||||
|
background: var(--primary-100);
|
||||||
|
padding: 6px 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: fit-content;
|
||||||
|
gap: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
#righternCrinkle {
|
||||||
|
right: -7px;
|
||||||
|
}
|
||||||
|
em {
|
||||||
|
color: var(--neutral-white);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 14px;
|
||||||
|
font-family: "Outfit", sans-serif;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
#lefternCrinkle {
|
||||||
|
left: -7px;
|
||||||
|
}
|
||||||
|
&.smallVersion {
|
||||||
|
#righternCrinkle {
|
||||||
|
right: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-size: 0.7em;
|
||||||
|
line-height: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user