Geist replaces Instrument Sans and is served from static/ instead of Google Fonts, so it works on installs with no internet access. Public help center pages are cached in Redis via fastcache with ETags, and any admin write clears the group so edits show up on the next load.
9.3 KiB
Libredesk Design System
Reference for colors, typography, spacing, radius, elevation, and component conventions across both apps (agent dashboard + livechat widget).
Source of truth:
- Tokens (CSS variables):
shared-ui/assets/styles/main.scss-:root, .lightand.darkblocks - Tailwind mapping:
tailwind.config.cjs-theme.extend.colorsandborderRadius - Primitives:
shared-ui/components/ui/(shadcn-vue)
Golden rule: never hardcode a value that a token exists for. Use bg-success, not
bg-green-600. Use rounded-lg, not rounded. Every token has a light and a dark value;
if you add one, define both.
1. Color tokens
HSL channel triples in CSS variables, consumed via hsl(var(--x)) (Tailwind classes like
bg-primary do it for you). Opacity modifiers work: bg-success/10, text-primary/80.
| Token | Light | Dark | Use for |
|---|---|---|---|
background |
0 0% 99.2% |
120 2.6% 7.6% |
app/content surface |
foreground |
0 0% 1% |
150 6% 93% |
primary text |
foreground-lighter |
0 0% 41% |
150 1% 60% |
idle sidebar nav items |
card / card-foreground |
0 0% 100% / 0 0% 1% |
120 2% 10% / 150 6% 93% |
raised card surface |
popover / popover-foreground |
0 0% 100% / 0 0% 1% |
120 2% 12% / 150 6% 93% |
menus, popovers, dropdowns |
primary / primary-foreground |
152 39% 30% / 0 0% 100% |
152 58% 54% / 120 2.6% 7.6% |
brand, active state, unread badges, primary buttons |
secondary / secondary-foreground |
0 0% 96% / 0 0% 1% |
150 3% 12% / 150 6% 93% |
secondary buttons, outgoing message bubbles |
muted / muted-foreground |
0 0% 96% / 0 0% 27% |
150 3% 13% / 120 1% 74% |
muted backgrounds, captions/meta text, secondary labels |
accent / accent-foreground |
0 0% 95% / 0 0% 1% |
150 3% 15% / 150 6% 93% |
hover and selected states |
destructive / destructive-foreground |
2 47% 46% / 9 100% 99% |
4 92% 74% / 12 38% 3% |
errors, delete, SLA breached, overdue, offline |
success / success-foreground |
142 72% 37% / 0 0% 98% |
142 55% 55% / 142 40% 10% |
positive/met, verified, online, connected, delivered |
warning / warning-foreground |
39 85% 43% / 24 45% 2% |
36 87% 62% / 24 45% 2% |
away, pending, SLA approaching, connecting (as background) |
warning-600 |
35 92% 33% |
36 87% 62% |
warning as TEXT/icon color. Plain warning fails 4.5:1 on light |
link |
210 100% 40% |
210 90% 66% |
links inside rendered email/message content only. UI links use .link-style |
border |
0 0% 91% |
150 2% 16% |
all borders/dividers |
input |
0 0% 85% |
150 2% 19% |
form field borders |
ring |
151 41% 45% |
152 45% 33% |
focus rings |
private |
35 90% 94% |
30 35% 18% |
private-note background tint |
canvas |
0 0% 82% |
120 3% 4% |
app gutter behind floating panels (deepest surface) |
Sidebar tokens (left nav chrome): sidebar-background matches background in both
themes, plus sidebar-foreground, sidebar-primary, sidebar-accent,
sidebar-accent-foreground, sidebar-border, sidebar-ring.
Chart tokens (unovis): --vis-primary-color: var(--primary),
--vis-secondary-color: var(--success), --vis-text-color: var(--muted-foreground).
Semantic status mapping
Color carries meaning; use the semantic token, never a raw palette color.
- success (green): SLA met, identity verified, agent online, widget connected, message delivered/read
- warning (amber): agent away, SLA approaching/remaining, widget connecting, no-internet banner
- destructive (red): error, delete, SLA breached, SLA overdue
- primary (green): brand identity, active nav, unread count badges, primary actions
- foreground / muted: neutral data (counts, totals, timestamps). Do not color a number unless the color means something. On the reports dashboard numbers are neutral; only met=success and breached=destructive are colored.
Deliberate exceptions
features/conversation/message/attachment/BubbleAttachmentItem.vuecolors attachment icons by file type (pdf=red, spreadsheet=green, doc=blue, archive=amber, audio=purple) with raw palette classes. The color is file-type identity, not status, and no token means "blue = document". Leave it.components/editor/TextEditor.vue- styles for rendered email HTML. Emails are standalone documents, not themed.features/admin/inbox/LivechatInboxForm.vue/LivechatWidgetPreview.vue- the customer-configurable widget brand color and its defaults.features/conversation/ReplyBox.vue- alinear-gradient(#000 0 0)CSS mask trick, not a color choice.
2. Surfaces and depth
Both themes use three tiers so panels read as floating, not flat:
canvas (deepest gutter behind the panels)
└─ background (app content + sidebar chrome, same value)
└─ card / popover (lifted by border + shadow-sm)
In light mode card is pure white against a near-white background. In dark mode card and
popover are lighter than the background, so lift comes from the surface, not the shadow.
3. Typography
Font: Geist. Sizes: text-xs 12 · text-sm 14 · text-base 16 · text-lg 18 ·
text-xl 20 · text-2xl 24 · text-3xl 30. Weights: 400 body · 500 labels · 600 headings ·
700 rare emphasis.
| Role | Style |
|---|---|
| Page / panel title | text-xl font-semibold |
| KPI / stat value | text-2xl (or text-3xl) font-bold tabular-nums |
| Section label | .sidebar-section-label utility, or the classes it applies |
| Body | text-sm |
| Caption / meta / helper | text-xs text-muted-foreground |
Use tabular-nums for any numeric column, timer, or metric to stop width jitter.
4. Radius
--radius = 0.5rem (8px). The Tailwind scale derives from it:
| Class | Value | Use for |
|---|---|---|
rounded-xl |
radius + 4 (12px) | large surfaces, widget window preview, dashboard message bubbles |
rounded-lg |
radius (8px) | cards, containers, dialogs |
rounded-md |
radius - 2 (6px) | buttons, inputs, chips, badges, small interactive |
rounded-sm |
radius - 4 (4px) | tiny insets |
rounded-full |
- | avatars, status dots, count badges, pills |
Never use bare rounded. It is Tailwind's fixed 4px and ignores the token.
5. Elevation
| Class | Use for |
|---|---|
shadow-sm |
cards, .box, default buttons |
shadow-md |
popovers, dropdown menus, hover/floating elements |
shadow-lg |
dialogs, modals, the widget window |
Never use bare shadow. Depth comes from the surface tiers and borders, not heavy
shadows.
6. Spacing
Follow a 4 / 8px rhythm for padding and gaps. Vertical section spacing tiers: 16 / 24 / 32 / 48. Keep the dense-desk feel; this is a high-volume support tool, not a marketing page.
7. Utilities
Defined in main.scss. Prefer them over repeating the class list.
| Utility | Expands to | Use for |
|---|---|---|
.box |
border shadow-sm rounded-lg |
the standard card surface |
.sidebar-section-label |
text-xs font-medium uppercase tracking-wider text-muted-foreground |
sidebar group headers (Views, Team Inboxes) |
.link-style |
text-muted-foreground underline underline-offset-4 hover:text-foreground |
UI links. Not brand-colored, so links in chrome stay quiet |
8. Components
Reuse shared-ui/components/ui/ primitives; do not hand-roll a styled <button>/<input>
when one exists.
Button - use the size variant, never an ad-hoc h-*:
| size | height | notes |
|---|---|---|
default |
h-9 | standard |
sm |
h-8 | dense (text-xs) |
xs |
h-7 | very dense |
lg |
h-10 | prominent |
icon |
h-9 w-9 | icon-only |
Variants: default (primary), destructive, outline, secondary, ghost, link.
h-8 w-8 ghost triggers for dense table row actions are an intentional pattern.
Every button inside a <form> that is not the submit button needs an explicit
type="button". HTML defaults to type="submit", so a typeless button submits the form on
click and gets activated when the user presses Enter in any input. This includes buttons in
child components rendered inside a parent's form.
Badge variants: default, secondary, destructive, success, outline.
AlertDialogAction takes a variant prop, so destructive confirms use
variant="destructive" instead of a hand-written class.
Sidebar nav items are font-medium text-foreground-lighter; hover and active states use
bg-sidebar-accent (hover at /50). Group headers use .sidebar-section-label.
Tooltip is bg-foreground text-background, not brand-colored.
Table row actions stay hidden until hover: [@media(hover:hover)]:opacity-0 with
group-hover/row:opacity-100, plus focus-within:!opacity-100 and
[&:has([data-state=open])]:!opacity-100. The media query keeps them visible on touch and
focus-within keeps them reachable by keyboard.
9. Checklist
Before merging UI work:
- No hardcoded palette colors (
bg-green-600,text-blue-500, ...). Only exception: file-type icons. - No bare
roundedorshadow. - Every non-submit button in a form has
type="button". - Color on a number or element means something, and is not decoration.
- Both light and dark verified.
- New shared words go through i18n (
i18n/en-US.json); reused nouns inglobals.terms. - Reused an existing
ui/primitive rather than building a variant.