@kitn.ai/chat
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
200 lines (192 loc) • 8.27 kB
CSS
/* Element-bundle stylesheet. Compiled to compiled.css and injected into each
custom element's shadow root. Mirrors .storybook/styles.css but scans src/. */
@import "tailwindcss";
@import "../../theme.css";
@plugin "@tailwindcss/typography";
@source "../";
@layer base {
/* Base typography is token-driven so a theme can restyle it. --kc-font-base is
the family for ALL text/UI (named "base", not "sans", so a serif body works);
--kc-tracking is global letter-spacing. Both pierce the Shadow DOM via the
var() fallback, exactly like the --kc-color-* tokens. */
:host {
display: block;
font-family: var(--kc-font-base, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
letter-spacing: var(--kc-tracking, normal);
}
/* Code/monospace surfaces use --kc-font-code (named for its ROLE — code — not
"mono", so any family works). Covers inline code, <pre>, and code blocks. */
code, kbd, samp, pre {
font-family: var(--kc-font-code, ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace);
}
* { border-color: var(--color-border); }
/* Native controls (date/time pickers, <select>, number spinners, the
checkbox/radio fallbacks, scrollbars) must follow the kit's light/dark
mode — NOT the OS — so e.g. the native calendar icon never renders black
on a dark surface. `color-scheme` drives that native rendering; we set it
from the resolved mode (the `.dark` scope the element factory toggles). */
:host { color-scheme: light; }
.dark { color-scheme: dark; }
/* One token-driven focus ring for EVERY focusable element in the shadow root.
Blue by default (via --color-ring), consistent in light + dark, and
overridable through --kc-color-ring. Uses `outline` (not a box-shadow
ring) so it never clips on overflow and follows the element's radius;
`:focus-visible` so it only appears for keyboard navigation, never on a
mouse click. Components that render their own ring set
`focus-visible:outline-none` (a utility in a later cascade layer) and win
over this — so there's no double ring, just a guaranteed blue fallback for
the many controls that have no explicit focus style. */
:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
/* Consistent cross-platform scrollbars for EVERY scroll region inside the
shadow root (message list, conversation list, menus, hover cards, code
blocks, the prompt textarea …). Thin + rounded + subtle, themed via tokens
for light/dark, thumb stronger on hover. Track stays transparent and
overflow is untouched, so no scrollbar is forced where none is needed.
Scoped to the shadow root, so it never restyles the host page. */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-scrollbar-thumb) transparent;
}
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover { background-color: var(--color-scrollbar-thumb-hover); }
*::-webkit-scrollbar-corner { background: transparent; }
}
@layer components {
/* Designed radio + checkbox (appearance-none) so the form controls read as
intentional, theme correctly in light/dark, and have comfortable targets —
replacing the weak native defaults. Used via `.kc-radio` / `.kc-checkbox`. */
.kc-radio,
.kc-checkbox {
appearance: none;
-webkit-appearance: none;
inline-size: 1.125rem;
block-size: 1.125rem;
margin: 0;
flex-shrink: 0;
cursor: pointer;
border: 1.5px solid var(--color-input);
background-color: var(--color-background);
display: inline-grid;
place-content: center;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.04);
transition: border-color 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease,
transform 0.1s ease;
}
.kc-radio { border-radius: 9999px; }
.kc-checkbox { border-radius: 0.3rem; }
.kc-radio::after {
content: "";
inline-size: 0.5rem;
block-size: 0.5rem;
border-radius: 9999px;
background-color: var(--color-primary);
transform: scale(0);
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.kc-radio:checked { border-color: var(--color-primary); }
.kc-radio:checked::after { transform: scale(1); }
.kc-checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.kc-checkbox::after {
content: "";
inline-size: 0.8rem;
block-size: 0.8rem;
background-color: var(--color-primary-foreground);
transform: scale(0);
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / contain no-repeat;
}
.kc-checkbox:checked::after { transform: scale(1); }
.kc-radio:hover:not(:disabled),
.kc-checkbox:hover:not(:disabled) {
border-color: var(--color-primary);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary) 9%, transparent);
}
.kc-radio:active:not(:disabled),
.kc-checkbox:active:not(:disabled) { transform: scale(0.92); }
.kc-checkbox:checked {
box-shadow: 0 1px 4px color-mix(in srgb, var(--color-primary) 26%, transparent);
}
.kc-radio:disabled,
.kc-checkbox:disabled { opacity: 0.5; cursor: not-allowed; }
/* Custom range slider: a filled track (--kc-range-fill set by the widget) and a
refined thumb that scales on hover/press with a focus halo — no native chrome. */
.kc-range {
appearance: none;
-webkit-appearance: none;
inline-size: 100%;
block-size: 1.25rem;
background: transparent;
cursor: pointer;
margin: 0;
}
.kc-range::-webkit-slider-runnable-track {
block-size: 0.375rem;
border-radius: 9999px;
border: none;
box-shadow: none;
background: linear-gradient(
to right,
var(--color-primary) var(--kc-range-fill, 0%),
var(--color-muted) var(--kc-range-fill, 0%)
);
}
.kc-range::-moz-range-track {
block-size: 0.375rem;
border-radius: 9999px;
border: none;
box-shadow: none;
background: var(--color-muted);
}
.kc-range::-moz-range-progress {
block-size: 0.375rem;
border-radius: 9999px;
background: var(--color-primary);
}
.kc-range::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
inline-size: 1.125rem;
block-size: 1.125rem;
border-radius: 9999px;
background: var(--color-background);
border: 2px solid var(--color-primary);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.18);
margin-block-start: -0.375rem;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.kc-range::-moz-range-thumb {
inline-size: 1.125rem;
block-size: 1.125rem;
border-radius: 9999px;
background: var(--color-background);
border: 2px solid var(--color-primary);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.18);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.kc-range:hover::-webkit-slider-thumb { transform: scale(1.12); }
.kc-range:hover::-moz-range-thumb { transform: scale(1.12); }
.kc-range:focus-visible { outline: none; }
.kc-range:focus-visible::-webkit-slider-thumb,
.kc-range:active::-webkit-slider-thumb {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-primary) 14%, transparent);
}
.kc-range:focus-visible::-moz-range-thumb,
.kc-range:active::-moz-range-thumb {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-primary) 14%, transparent);
}
.kc-range:disabled { opacity: 0.5; cursor: not-allowed; }
}