@meistrari/tela-build 1.50.0 → 1.50.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/animations.md +161 -0
- package/docs/interfaces.md +457 -0
- package/docs/surfaces.md +127 -0
- package/docs/tokens.md +92 -0
- package/docs/typography.md +35 -0
- package/lib/__tests__/doc-generator.test.ts +26 -2
- package/package.json +2 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Animations
|
|
2
|
+
|
|
3
|
+
Principles for deciding when to animate, choosing easing and duration, using springs, and building staggered enter animations.
|
|
4
|
+
|
|
5
|
+
Before writing any animation code, answer these questions in order:
|
|
6
|
+
|
|
7
|
+
### 1. Should this animate at all?
|
|
8
|
+
|
|
9
|
+
**Ask:** How often will users see this animation?
|
|
10
|
+
|
|
11
|
+
| Frequency | Decision |
|
|
12
|
+
| ----------------------------------------------------------- | ---------------------------- |
|
|
13
|
+
| 100+ times/day (keyboard shortcuts, command palette toggle) | No animation. Ever. |
|
|
14
|
+
| Tens of times/day (hover effects, list navigation) | Remove or drastically reduce |
|
|
15
|
+
| Occasional (modals, drawers, toasts) | Standard animation |
|
|
16
|
+
| Rare/first-time (onboarding, feedback forms, celebrations) | Can add delight |
|
|
17
|
+
|
|
18
|
+
**Never animate keyboard-initiated actions.** These actions are repeated hundreds of times daily. Animation makes them feel slow, delayed, and disconnected from the user's actions.
|
|
19
|
+
|
|
20
|
+
Raycast has no open/close animation. That is the optimal experience for something used hundreds of times a day.
|
|
21
|
+
|
|
22
|
+
### 2. What is the purpose?
|
|
23
|
+
|
|
24
|
+
Every animation must have a clear answer to "why does this animate?"
|
|
25
|
+
|
|
26
|
+
Valid purposes:
|
|
27
|
+
|
|
28
|
+
- **Spatial consistency**: toast enters and exits from the same direction, making swipe-to-dismiss feel intuitive
|
|
29
|
+
- **State indication**: a morphing feedback button shows the state change
|
|
30
|
+
- **Explanation**: a marketing animation that shows how a feature works
|
|
31
|
+
- **Feedback**: a button scales down on press, confirming the interface heard the user
|
|
32
|
+
- **Preventing jarring changes**: elements appearing or disappearing without transition feel broken
|
|
33
|
+
|
|
34
|
+
If the purpose is just "it looks cool" and the user will see it often, don't animate.
|
|
35
|
+
|
|
36
|
+
### 3. What easing should it use?
|
|
37
|
+
|
|
38
|
+
Is the element entering or exiting?
|
|
39
|
+
Yes → ease-out (starts fast, feels responsive)
|
|
40
|
+
No →
|
|
41
|
+
Is it moving/morphing on screen?
|
|
42
|
+
Yes → ease-in-out (natural acceleration/deceleration)
|
|
43
|
+
Is it a hover/color change?
|
|
44
|
+
Yes → ease
|
|
45
|
+
Is it constant motion (marquee, progress bar)?
|
|
46
|
+
Yes → linear
|
|
47
|
+
Default → ease-out
|
|
48
|
+
|
|
49
|
+
**Critical: use custom easing curves.** The built-in CSS easings are too weak. They lack the punch that makes animations feel intentional.
|
|
50
|
+
|
|
51
|
+
**Never use ease-in for UI animations.** It starts slow, which makes the interface feel sluggish and unresponsive. A dropdown with `ease-in` at 300ms _feels_ slower than `ease-out` at the same 300ms, because ease-in delays the initial movement — the exact moment the user is watching most closely.
|
|
52
|
+
|
|
53
|
+
**Easing curve resources:** Don't create curves from scratch. Use [easing.dev](https://easing.dev/) or [easings.co](https://easings.co/) to find stronger custom variants of standard easings.
|
|
54
|
+
|
|
55
|
+
### 4. How fast should it be?
|
|
56
|
+
|
|
57
|
+
| Element | Duration |
|
|
58
|
+
| ------------------------ | ------------- |
|
|
59
|
+
| Button press feedback | 100-160ms |
|
|
60
|
+
| Tooltips, small popovers | 125-200ms |
|
|
61
|
+
| Dropdowns, selects | 150-250ms |
|
|
62
|
+
| Modals, drawers | 200-500ms |
|
|
63
|
+
| Marketing/explanatory | Can be longer |
|
|
64
|
+
|
|
65
|
+
**Rule: UI animations should stay under 300ms.** A 180ms dropdown feels more responsive than a 400ms one. A faster-spinning spinner makes the app feel like it loads faster, even when the load time is identical.
|
|
66
|
+
|
|
67
|
+
### Perceived performance
|
|
68
|
+
|
|
69
|
+
Speed in animation is not just about feeling snappy — it directly affects how users perceive your app's performance:
|
|
70
|
+
|
|
71
|
+
- A **fast-spinning spinner** makes loading feel faster (same load time, different perception)
|
|
72
|
+
- A **180ms select** animation feels more responsive than a **400ms** one
|
|
73
|
+
- **Instant tooltips** after the first one is open (skip delay + skip animation) make the whole toolbar feel faster
|
|
74
|
+
|
|
75
|
+
The perception of speed matters as much as actual speed. Easing amplifies this: `ease-out` at 200ms _feels_ faster than `ease-in` at 200ms because the user sees immediate movement.
|
|
76
|
+
|
|
77
|
+
## Spring Animations
|
|
78
|
+
|
|
79
|
+
Springs feel more natural than duration-based animations because they simulate real physics. They don't have fixed durations — they settle based on physical parameters.
|
|
80
|
+
|
|
81
|
+
### When to use springs
|
|
82
|
+
|
|
83
|
+
- Drag interactions with momentum
|
|
84
|
+
- Elements that should feel "alive" (like Apple's Dynamic Island)
|
|
85
|
+
- Gestures that can be interrupted mid-animation
|
|
86
|
+
- Decorative mouse-tracking interactions
|
|
87
|
+
|
|
88
|
+
This works because the animation is **decorative** — it doesn't serve a function. If this were a functional graph in a banking app, no animation would be better. Know when decoration helps and when it hinders.
|
|
89
|
+
|
|
90
|
+
### Spring configuration
|
|
91
|
+
|
|
92
|
+
**Apple's approach (recommended — easier to reason about):**
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
{ type: "spring", duration: 0.5, bounce: 0.2 }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Traditional physics (more control):**
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
{ type: "spring", mass: 1, stiffness: 100, damping: 10 }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Keep bounce subtle (0.1-0.3) when used. Avoid bounce in most UI contexts. Use it for drag-to-dismiss and playful interactions.
|
|
105
|
+
|
|
106
|
+
## Enter Animations: Split and Stagger
|
|
107
|
+
|
|
108
|
+
Don't animate a single large container. Break content into semantic chunks and animate each individually.
|
|
109
|
+
|
|
110
|
+
### Step by Step
|
|
111
|
+
|
|
112
|
+
1. **Split** into logical groups (title, description, buttons)
|
|
113
|
+
2. **Stagger** with ~100ms delay between groups
|
|
114
|
+
3. **For titles**, consider splitting into individual words with ~80ms stagger
|
|
115
|
+
4. **Combine** `opacity`, `blur`, and `translateY` for the enter effect
|
|
116
|
+
|
|
117
|
+
### Code Example
|
|
118
|
+
|
|
119
|
+
```vue
|
|
120
|
+
<script setup lang="ts">
|
|
121
|
+
const itemVariants = {
|
|
122
|
+
hidden: { opacity: 0, y: 12, filter: 'blur(4px)' },
|
|
123
|
+
visible: { opacity: 1, y: 0, filter: 'blur(0px)' },
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<Motion
|
|
129
|
+
initial="hidden"
|
|
130
|
+
animate="visible"
|
|
131
|
+
:variants="{ visible: { transition: { staggerChildren: 0.1 } } }"
|
|
132
|
+
>
|
|
133
|
+
<Motion as="h1" :variants="itemVariants">
|
|
134
|
+
Welcome
|
|
135
|
+
</Motion>
|
|
136
|
+
|
|
137
|
+
<Motion as="p" :variants="itemVariants">
|
|
138
|
+
A description of the page.
|
|
139
|
+
</Motion>
|
|
140
|
+
|
|
141
|
+
<Motion :variants="itemVariants">
|
|
142
|
+
<TelaButton>Get started</TelaButton>
|
|
143
|
+
</Motion>
|
|
144
|
+
</Motion>
|
|
145
|
+
</template>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### When It Breaks
|
|
149
|
+
|
|
150
|
+
Don't use `initial={false}` when the component relies on its `initial` prop to set up a first-time enter animation, like a staggered page hero or a loading state. In those cases, removing the initial animation skips the entire entrance.
|
|
151
|
+
|
|
152
|
+
```vue
|
|
153
|
+
// Bad — initial={false} would skip the staggered page enter entirely
|
|
154
|
+
<AnimatePresence initial={false}>
|
|
155
|
+
<Motion initial="hidden" animate="visible" :variants="{...}">
|
|
156
|
+
...
|
|
157
|
+
</Motion>
|
|
158
|
+
</AnimatePresence>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Verify the component still looks right on a full page refresh before applying this.
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# Interfaces
|
|
2
|
+
|
|
3
|
+
Build with craft and consistency. Every decision is a design decision.
|
|
4
|
+
|
|
5
|
+
This document has two halves:
|
|
6
|
+
|
|
7
|
+
1. **Philosophy** — how to think about an interface _before_ you write it.
|
|
8
|
+
2. **Tela Design System** — the concrete rules for building interfaces in this codebase.
|
|
9
|
+
|
|
10
|
+
If you're here to look up a component rule, skip to [Tela Design System](#tela-design-system). If you're starting a new surface from scratch, read from the top.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Part 1 — Philosophy
|
|
15
|
+
|
|
16
|
+
## Intent First
|
|
17
|
+
|
|
18
|
+
Before writing a single line of code, answer these out loud.
|
|
19
|
+
|
|
20
|
+
| Question | Bad answer | Good answer |
|
|
21
|
+
| ----------------------- | ------------------- | ----------------------------------------------------------------------- |
|
|
22
|
+
| **Who is this human?** | "Users" | A teacher at 7am. A founder between calls. A dev debugging at midnight. |
|
|
23
|
+
| **What must they do?** | "Use the dashboard" | Grade submissions. Find the broken deploy. Approve the payment. |
|
|
24
|
+
| **How should it feel?** | "Clean and modern" | Warm like a notebook. Cold like a terminal. Dense like a trading floor. |
|
|
25
|
+
|
|
26
|
+
If you can't answer with specifics — stop. Ask. Do not guess. Do not default.
|
|
27
|
+
|
|
28
|
+
### Every Choice Must Be Intentional
|
|
29
|
+
|
|
30
|
+
For every decision, explain **why**: layout, color temperature, typeface, spacing scale, information hierarchy.
|
|
31
|
+
|
|
32
|
+
**The swap test:** If you replaced your choices with the most common alternatives and nothing felt meaningfully different — you defaulted, not designed.
|
|
33
|
+
|
|
34
|
+
### Intent Must Be Systemic
|
|
35
|
+
|
|
36
|
+
Stating "warm" and using cold colors is a failure. Intent is a constraint that shapes _everything_.
|
|
37
|
+
|
|
38
|
+
- **Warm** → surfaces, text, borders, accents, typography — all warm
|
|
39
|
+
- **Dense** → spacing, type size, information architecture — all dense
|
|
40
|
+
- **Calm** → motion, contrast, color saturation — all calm
|
|
41
|
+
|
|
42
|
+
## Where Defaults Hide
|
|
43
|
+
|
|
44
|
+
Defaults disguise themselves as infrastructure — the parts that "just need to work."
|
|
45
|
+
|
|
46
|
+
**Typography** feels like a container. But it _is_ your design. A bakery tool and a trading terminal both need "readable type" — but warm and handmade is not cold and precise. If you're reaching for your usual font, you're not designing.
|
|
47
|
+
|
|
48
|
+
**Navigation** feels like scaffolding. But navigation _is_ your product. Where you are, where you can go, what matters most. A floating page is a component demo, not software.
|
|
49
|
+
|
|
50
|
+
**Data** feels like presentation. But a number on screen is not design. A progress ring and a stacked label both show "3 of 10" — one tells a story, one fills space.
|
|
51
|
+
|
|
52
|
+
**Token names** feel like implementation. But `--ink` and `--parchment` evoke a world. `--gray-700` evokes a template. Your tokens should hint at the product.
|
|
53
|
+
|
|
54
|
+
> The moment you stop asking "why this?" is the moment defaults take over.
|
|
55
|
+
|
|
56
|
+
## Product Domain Exploration
|
|
57
|
+
|
|
58
|
+
Generic process: `Task type → Visual template → Theme`
|
|
59
|
+
Crafted process: `Task type → Product domain → Signature → Structure + Expression`
|
|
60
|
+
|
|
61
|
+
Before proposing any direction, produce all three:
|
|
62
|
+
|
|
63
|
+
| Output | Description |
|
|
64
|
+
| ------------- | ---------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| **Domain** | Concepts, metaphors, vocabulary from this product's world. Not features — territory. Minimum 5. |
|
|
66
|
+
| **Signature** | One element — visual, structural, or interaction — that could _only_ exist for this product. |
|
|
67
|
+
| **Defaults** | 3 obvious choices for this interface type (visual AND structural). Name them so you can reject them. |
|
|
68
|
+
|
|
69
|
+
Your direction must reference domain concepts, your signature element, and what replaces each default.
|
|
70
|
+
|
|
71
|
+
**The identity test:** Remove the product name from your proposal. Can someone identify what it's for? If not — it's generic.
|
|
72
|
+
|
|
73
|
+
### Worked example — a workflow run detail page
|
|
74
|
+
|
|
75
|
+
Bad (generic): "Tabs across the top for Overview, Logs, Variables. Status pill in the corner. Metric cards above the fold."
|
|
76
|
+
|
|
77
|
+
Good:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Domain: dag · step · run · trigger · input/output contract · retry · branch · checkpoint
|
|
81
|
+
Signature: the run is a timeline of steps — each step's input and output are always visible on
|
|
82
|
+
the same row, so the contract between steps reads as naturally as the execution does.
|
|
83
|
+
Rejecting: tabs (fragments context) → single scrollable timeline
|
|
84
|
+
status pill in a corner → status inline with each step row
|
|
85
|
+
metric cards above the fold → summary synthesized from the timeline itself
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Every direction the agent then proposes — typography, spacing, card shape, motion — must serve that timeline-with-visible-contracts signature. If it doesn't, it's a default in disguise.
|
|
89
|
+
|
|
90
|
+
## Craft Foundations
|
|
91
|
+
|
|
92
|
+
Every pattern has infinite expressions. **No interface should look the same.**
|
|
93
|
+
|
|
94
|
+
A metric display could be: hero number, inline stat, sparkline, gauge, progress bar, delta, trend badge — or something new.
|
|
95
|
+
|
|
96
|
+
Before building a surface, ask:
|
|
97
|
+
|
|
98
|
+
1. What's the ONE thing users do most here?
|
|
99
|
+
2. What products solve similar problems brilliantly? Study them.
|
|
100
|
+
3. Why would this interface feel designed for its purpose — not templated?
|
|
101
|
+
|
|
102
|
+
## Checkpoint — before a surface leaves your hands
|
|
103
|
+
|
|
104
|
+
You do not need to state intent before every `<div>`. You _do_ need to state it once per surface (a page, a modal, a large component) — and once before you ship it.
|
|
105
|
+
|
|
106
|
+
**When you start the surface, write this in a comment or PR description:**
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Intent: [who is this human, what must they do, how should it feel]
|
|
110
|
+
Depth: [borders / shadows / layered — and WHY this fits the intent]
|
|
111
|
+
Surfaces: [your elevation scale — and WHY this fits the intent]
|
|
112
|
+
Typography:[your typeface — and WHY it fits the intent]
|
|
113
|
+
Spacing: [your base unit]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If you can't explain **why** for each choice — you're defaulting.
|
|
117
|
+
|
|
118
|
+
**Before showing the surface to the user**, ask: _"If they said this lacks craft, what would they mean?"_ That thing you just thought of — **fix it first.** Then run the four checks:
|
|
119
|
+
|
|
120
|
+
| Check | Question |
|
|
121
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
|
122
|
+
| **Swap test** | If you swapped the typeface or layout for the most common alternative — would anyone notice? |
|
|
123
|
+
| **Squint test** | Blur your eyes. Is hierarchy still perceptible? Anything jumping harshly? |
|
|
124
|
+
| **Signature test** | Can you point to five specific elements where your signature appears? Not "the overall feel" — actual components. |
|
|
125
|
+
| **Token test** | Read your CSS variables out loud. Do they sound like this product's world, or any project? |
|
|
126
|
+
|
|
127
|
+
If any check fails — iterate before showing.
|
|
128
|
+
|
|
129
|
+
## Design Principles
|
|
130
|
+
|
|
131
|
+
### Spacing
|
|
132
|
+
|
|
133
|
+
Pick a base unit. Use only multiples.
|
|
134
|
+
|
|
135
|
+
| Context | Use |
|
|
136
|
+
| ------------------------ | ------------------ |
|
|
137
|
+
| Icon gaps, tight inline | micro (4px) |
|
|
138
|
+
| Within buttons and cards | component (8–12px) |
|
|
139
|
+
| Between groups | section (16–24px) |
|
|
140
|
+
| Between distinct areas | major (32px+) |
|
|
141
|
+
|
|
142
|
+
### Other Principles
|
|
143
|
+
|
|
144
|
+
**Layout width** — Don't cap page containers with an arbitrary `max-w-*`. A number like `max-w-1280px` is almost always a habit, not a decision — it shrinks the canvas for no stated reason and fights the app shell that already constrains width. Let content, grid, and surrounding layout determine width. If you do cap, state what breaks without the cap (line length for prose, column count for dense tables) and pick the value from that constraint.
|
|
145
|
+
|
|
146
|
+
**Padding** — Keep it symmetrical. Asymmetry only when content demands it.
|
|
147
|
+
|
|
148
|
+
**Border radius** — Sharper = technical. Rounder = friendly. Build a scale: small for inputs/buttons, medium for cards, large for modals. Don't mix randomly.
|
|
149
|
+
|
|
150
|
+
**Cards** — A metric card doesn't have to look like a plan card. Design each card's internal structure for its content. Keep surface treatment consistent: same border weight, shadow depth, corner radius, padding scale.
|
|
151
|
+
|
|
152
|
+
**Controls** — Native `<select>` and `<input type="date">` can't be styled. Always build custom components.
|
|
153
|
+
|
|
154
|
+
**Icons** — Icons clarify, not decorate. If removing an icon loses no meaning — remove it. One icon set. Give standalone icons a subtle background container.
|
|
155
|
+
|
|
156
|
+
**Animation** — Fast micro-interactions, smooth easing. Use deceleration. Avoid bounce in professional interfaces.
|
|
157
|
+
|
|
158
|
+
**States** — Every interactive element needs: default, hover, active, focus, disabled. Every data state: loading, empty, error.
|
|
159
|
+
|
|
160
|
+
**Navigation** — Screens need grounding. A floating data table is a component demo. Include where you are in the app, location indicators, user context.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
# Part 2 — Tela Design System
|
|
165
|
+
|
|
166
|
+
## Components
|
|
167
|
+
|
|
168
|
+
### Page Shell
|
|
169
|
+
|
|
170
|
+
Every page mounts inside the same shell. Never rebuild it.
|
|
171
|
+
|
|
172
|
+
- Need a sidebar? **Use `TelaSidebar`.** Never hand-roll a sidebar, `<nav>`, or custom rail.
|
|
173
|
+
- Need a header? **Use `TelaHeader`.** Never build page chrome from scratch.
|
|
174
|
+
|
|
175
|
+
| Page type | Shell |
|
|
176
|
+
| ------------------------------------------------ | ------------- |
|
|
177
|
+
| Root page (index, list, dashboard) | `TelaSidebar` |
|
|
178
|
+
| Details page (single resource, editor, settings) | `TelaHeader` |
|
|
179
|
+
|
|
180
|
+
Root pages get the sidebar for navigation and context. Details pages get the header for location (back action, title, primary action) — the sidebar is not part of a details surface.
|
|
181
|
+
|
|
182
|
+
### Layouts
|
|
183
|
+
|
|
184
|
+
Before building a new page from scratch, check for an existing **layout template** and use it if one fits. Layouts package the entire page structure — scroll model, header, columns, footer — so surfaces stay consistent and you don't re-assemble (or re-debug) that scaffolding by hand.
|
|
185
|
+
|
|
186
|
+
| Layout | Use for | Docs |
|
|
187
|
+
| --- | --- | --- |
|
|
188
|
+
| `TelaHome` (`home.vue`) | Root index / list / dashboard pages with a sidebar rail. | `components/tela/home/home.mdx` |
|
|
189
|
+
| `TelaDetails` (`details.vue`) | Full-screen detail / record pages and fullscreen modals. | `components/tela/details/details.mdx` |
|
|
190
|
+
|
|
191
|
+
**`TelaHome`** — a flex-row shell: a sticky `TelaSidebar` beside a scrolling `TelaHomeContent` column that stacks a page title, a metric-card row (`TelaHomeMetrics`), a filter toolbar (`TelaHomeToolbar`), and a data table. It does **not** own scroll — the sidebar pins itself (`sticky top-0 h-screen`) and the page scrolls as one, no `overflow` container or height hack. Expandable detail rows are an opt-in enhancement.
|
|
192
|
+
|
|
193
|
+
**`TelaDetails`** — a sticky `TelaHeader` + a single scroll container + a primary content column beside a sticky context column, with an optional confirm footer. Two documented variants — a two-column body, and a hero header above the body.
|
|
194
|
+
|
|
195
|
+
This table is the source of truth for layout templates. Rules:
|
|
196
|
+
|
|
197
|
+
- If a layout fits the surface, use it. Don't hand-roll an equivalent.
|
|
198
|
+
- If none fits, fall back to the [Page Shell](#page-shell) primitives (`TelaSidebar` / `TelaHeader`) — don't invent a new full-page structure inline.
|
|
199
|
+
- When you build a new reusable layout, document it and add a row here so it becomes a discoverable template.
|
|
200
|
+
|
|
201
|
+
### Button
|
|
202
|
+
|
|
203
|
+
- **Never** use `variant="ghost"` on `TelaButton` — use `secondary` instead
|
|
204
|
+
- **Never** use `size="sm"` — always `md` (default) or `lg`
|
|
205
|
+
- Since `md` is default, omit the `size` prop unless you need `lg`
|
|
206
|
+
- Icon API: `icon="i-…"` for the name, `leading` as a boolean for position — never `leading="i-…"`
|
|
207
|
+
|
|
208
|
+
### IconButton
|
|
209
|
+
|
|
210
|
+
- **Never** use `color="primary"` on `TelaIconButton` — always `color="secondary"`
|
|
211
|
+
- The default is `primary`, so always set it explicitly
|
|
212
|
+
|
|
213
|
+
### Select
|
|
214
|
+
|
|
215
|
+
- **Never** place icons before text in select options
|
|
216
|
+
- **Always** set `trigger-class` to constrain width — never let the trigger overflow or expand the layout
|
|
217
|
+
|
|
218
|
+
| Context | Value |
|
|
219
|
+
| ----------------------- | ------------------------------------ |
|
|
220
|
+
| Inside modals and forms | `trigger-class="w-full"` |
|
|
221
|
+
| In toolbars | `trigger-class="w-160px"` (or fixed) |
|
|
222
|
+
|
|
223
|
+
### Dropdown vs Select
|
|
224
|
+
|
|
225
|
+
| Scenario | Component |
|
|
226
|
+
| ----------------------------------------------- | ------------------ |
|
|
227
|
+
| Export, duplicate, archive, configure | `TelaDropdownMenu` |
|
|
228
|
+
| Choose environment, select status, pick a model | `TelaSelectMenu` |
|
|
229
|
+
| Navigation links and actions | `TelaDropdownMenu` |
|
|
230
|
+
| Filtering or form field with options | `TelaSelectMenu` |
|
|
231
|
+
|
|
232
|
+
**The test:** If clicking triggers a side effect → Dropdown. If it updates a bound value → Select.
|
|
233
|
+
|
|
234
|
+
**Dropdown items:** Always include an `icon` on every `TelaDropdownMenu` item. Icons give each action a visual identity and make the menu scannable.
|
|
235
|
+
|
|
236
|
+
### Input & Button Heights
|
|
237
|
+
|
|
238
|
+
Always use `size="md"` (default) for both `TelaInput` and `TelaButton` when they appear together. Never mix sizes in the same row — it creates misaligned rows.
|
|
239
|
+
|
|
240
|
+
### Search
|
|
241
|
+
|
|
242
|
+
Search fields never have a submit button. Always an inline `TelaInput` that filters as the user types.
|
|
243
|
+
|
|
244
|
+
```vue
|
|
245
|
+
<!-- Correct — inline filter -->
|
|
246
|
+
<TelaInput v-model="query" placeholder="Search..." hide-label />
|
|
247
|
+
|
|
248
|
+
<!-- Wrong — never pair search with a submit button -->
|
|
249
|
+
<div flex gap-8px>
|
|
250
|
+
<TelaInput v-model="query" placeholder="Search..." hide-label />
|
|
251
|
+
<TelaButton>Search</TelaButton>
|
|
252
|
+
</div>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Filters
|
|
256
|
+
|
|
257
|
+
Always include **Apply / Clear** action buttons in filter panels. Never auto-apply on change.
|
|
258
|
+
|
|
259
|
+
- Apply: default primary `TelaButton`
|
|
260
|
+
- Clear: `variant="secondary"` `TelaButton`
|
|
261
|
+
- Multiple options: always `TelaSelectMenu` — never toggles or checkboxes
|
|
262
|
+
|
|
263
|
+
### Status
|
|
264
|
+
|
|
265
|
+
Always use `<TelaStatus />` for any status indicator. Never build custom status with icons and color classes.
|
|
266
|
+
|
|
267
|
+
### Table
|
|
268
|
+
|
|
269
|
+
- Never wrap `TelaTable` in `TelaCard` — tables are their own surface.
|
|
270
|
+
- **ALWAYS wrap the table in `<div mx--16px>`** so cell padding doesn't indent the table content from the header / title row above it. The negative margin cancels the cell's horizontal padding, pulling the first column flush with the surrounding content. This is the default — a table that sits directly inside a padded surface and skips the wrap will look misaligned (its content nudged in from everything above it).
|
|
271
|
+
|
|
272
|
+
```vue
|
|
273
|
+
<div mx--16px>
|
|
274
|
+
<TelaTable>
|
|
275
|
+
<!-- ... -->
|
|
276
|
+
</TelaTable>
|
|
277
|
+
</div>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**The one exception — interactive rows with a hanging affordance.** When rows have an trigger component that hangs into the left gutter (e.g. the expand caret on clickable/expandable rows in the [`TelaHome`](?path=/docs/layout-home--docs) example, paired with `:has-scroll-area="false"`), do **not** add `mx--16px` — the gutter padding is what gives that affordance room and keeps it from being clipped. So: **wrap plain tables every time; only skip the wrap when a hanging row affordance needs the gutter.**
|
|
281
|
+
|
|
282
|
+
### Modal
|
|
283
|
+
|
|
284
|
+
Always use `TelaModal`. `TelaDialog` is deprecated — never use it.
|
|
285
|
+
|
|
286
|
+
```vue
|
|
287
|
+
<TelaModal
|
|
288
|
+
v-model="isOpen"
|
|
289
|
+
modal-width="md"
|
|
290
|
+
:compact="true"
|
|
291
|
+
:hide-dividers="true"
|
|
292
|
+
:is-close-icon="false"
|
|
293
|
+
>
|
|
294
|
+
<div flex="~ col" w-full gap-16px>
|
|
295
|
+
<!-- Header -->
|
|
296
|
+
<div flex="~ row justify-between" items-start>
|
|
297
|
+
<div flex="~ col" gap-4px>
|
|
298
|
+
<h4 heading-h4-semibold text-primary>Dialog Title</h4>
|
|
299
|
+
<p body-14-regular text-secondary>Dialog subtitle or description.</p>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<!-- Close Button -->
|
|
303
|
+
<TelaIconButton
|
|
304
|
+
icon="i-ph-x"
|
|
305
|
+
size="md"
|
|
306
|
+
color="secondary"
|
|
307
|
+
outline-none
|
|
308
|
+
p-8px mt--12px mr--16px
|
|
309
|
+
@click="isOpen = false"
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<!-- Content -->
|
|
314
|
+
<div flex="~ col" gap-8px>
|
|
315
|
+
<!-- ... -->
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<!-- Footer -->
|
|
319
|
+
<div flex gap-8px justify-end>
|
|
320
|
+
<TelaButton variant="secondary" @click="isOpen = false">Cancel</TelaButton>
|
|
321
|
+
<TelaButton>Submit</TelaButton>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</TelaModal>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Rules:**
|
|
328
|
+
|
|
329
|
+
- Never pass `title` to `TelaModal` — build the header yourself inside the slot
|
|
330
|
+
- Always use `:compact="true"` and `:hide-dividers="true"`
|
|
331
|
+
- Always use `:is-close-icon="false"` — build the close button manually
|
|
332
|
+
- Close button negative margins = **half the modal padding** → `mt--{p/2} mr--{p/2}`
|
|
333
|
+
- Footer: `flex justify-end gap-8px`
|
|
334
|
+
|
|
335
|
+
**Width — all controls inside a modal must fill the modal:**
|
|
336
|
+
|
|
337
|
+
```vue
|
|
338
|
+
<!-- Correct -->
|
|
339
|
+
<TelaInput v-model="val" label="Name" w-full />
|
|
340
|
+
|
|
341
|
+
<TelaTextarea v-model="val" w-full />
|
|
342
|
+
|
|
343
|
+
<TelaToggleGroup v-model="val" :options="opts" w-full />
|
|
344
|
+
|
|
345
|
+
<TelaSelectMenu v-model="val" :options="opts" trigger-class="w-full" />
|
|
346
|
+
|
|
347
|
+
<TelaCombobox v-model="val" :options="opts" trigger-class="w-full" />
|
|
348
|
+
|
|
349
|
+
<!-- Wrong — w-full on root doesn't reach the trigger -->
|
|
350
|
+
<TelaSelectMenu v-model="val" :options="opts" w-full />
|
|
351
|
+
|
|
352
|
+
<TelaCombobox v-model="val" :options="opts" w-full />
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Card
|
|
356
|
+
|
|
357
|
+
| Card type | Prop |
|
|
358
|
+
| ----------------------------- | ----------- |
|
|
359
|
+
| Large, standard, medium cards | `size="md"` |
|
|
360
|
+
| Small / minor cards | `size="sm"` |
|
|
361
|
+
|
|
362
|
+
Always use the `size` prop to control card spacing and radius. Do not use `content-padding` or `border-radius` props.
|
|
363
|
+
|
|
364
|
+
**Rule: use `size="md"` for large content, otherwise use `size="sm"`.** `md` is for primary surfaces — the hero card on a dashboard, a settings panel, a section that carries the page. `sm` is for minor surfaces — KPI cards, nested cards, compact side panels, anything supporting the primary surface. Defaulting everything to `md` flattens hierarchy; defaulting everything to `sm` starves the page of presence. Pick based on what the card is, not by feel.
|
|
365
|
+
|
|
366
|
+
Do not pass attributify spacing/radius props (for example `p-*`, `px-*`, `py-*`, `rounded-*`) to `<TelaCard>`. Those attributes are intentionally ignored to keep `size` deterministic; if you need a forced override, use `class` with the important modifier (for example `class="!p-0"`).
|
|
367
|
+
|
|
368
|
+
Cards in grids must use `h-full` for consistent heights.
|
|
369
|
+
|
|
370
|
+
```vue
|
|
371
|
+
<!-- Standard card -->
|
|
372
|
+
<TelaCard size="md" h-full>
|
|
373
|
+
<h3>Card Title</h3>
|
|
374
|
+
<p>Card content</p>
|
|
375
|
+
</TelaCard>
|
|
376
|
+
|
|
377
|
+
<!-- Minor card -->
|
|
378
|
+
<TelaCard size="sm">
|
|
379
|
+
<p text-secondary mb-2>Capacity</p>
|
|
380
|
+
<p heading-h2-semibold>20</p>
|
|
381
|
+
</TelaCard>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### Gap with Large Text
|
|
385
|
+
|
|
386
|
+
Small gaps look like accidents next to large typography. Use at least `gap-12px` when a container holds a large typographic element — prefer `gap-16px` when the large text is the primary content.
|
|
387
|
+
|
|
388
|
+
```vue
|
|
389
|
+
<!-- Wrong — too tight -->
|
|
390
|
+
<div flex flex-col gap-4px>
|
|
391
|
+
<span body-12-medium text-secondary>Total Requests</span>
|
|
392
|
+
<span heading-h2-semibold text-contrast>128,430</span>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<!-- Correct -->
|
|
396
|
+
<div flex flex-col gap-12px>
|
|
397
|
+
<span body-12-medium text-secondary>Total Requests</span>
|
|
398
|
+
<span heading-h2-semibold text-contrast>128,430</span>
|
|
399
|
+
</div>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Styling
|
|
403
|
+
|
|
404
|
+
Use direct attributes for static styles. Use `class` only for dynamic or conditional styles.
|
|
405
|
+
|
|
406
|
+
```vue
|
|
407
|
+
<!-- Static — direct attributes -->
|
|
408
|
+
<div flex items-center gap-16px p-20px bg-muted rounded-12px>
|
|
409
|
+
<span text-primary heading-h3-semibold>Title</span>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<!-- Dynamic — use class -->
|
|
413
|
+
<div :class="isActive ? 'bg-blue-600' : 'bg-muted'">
|
|
414
|
+
Content
|
|
415
|
+
</div>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Never duplicate attributes on the same element:
|
|
419
|
+
|
|
420
|
+
```vue
|
|
421
|
+
<!-- Wrong -->
|
|
422
|
+
<div flex="~ 1" flex="~ col">
|
|
423
|
+
...
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<!-- Correct -->
|
|
427
|
+
<div flex flex-1 flex-col>
|
|
428
|
+
...
|
|
429
|
+
</div>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Do / Don't
|
|
433
|
+
|
|
434
|
+
**Do:**
|
|
435
|
+
|
|
436
|
+
- Use semantic tokens over raw color values
|
|
437
|
+
- Use `0.5px` borders on all components
|
|
438
|
+
- Apply `h-full` to cards in grid/list layouts
|
|
439
|
+
- Use `12px` radius for `size="sm"` cards and `8px` for nested elements/badges
|
|
440
|
+
- Use direct attributes for static styles
|
|
441
|
+
- Use `<TelaStatus />` for all status indicators
|
|
442
|
+
- Use `text-primary` or `text-secondary` for all text content
|
|
443
|
+
- Use `TelaSelectMenu` for filters with multiple options
|
|
444
|
+
|
|
445
|
+
**Don't:**
|
|
446
|
+
|
|
447
|
+
- Use `TelaDialog` — always `TelaModal`
|
|
448
|
+
- Use `class` for static styles
|
|
449
|
+
- Use legacy colors (`colors.dark`, `colors.base`, `colors.caution`, `colors.negative`, `colors.black`)
|
|
450
|
+
- Use border widths other than `0.5px`
|
|
451
|
+
- Use arbitrary spacing, radius, or color values
|
|
452
|
+
- Cap page containers with a default `max-w-*` — let content and the app shell decide width
|
|
453
|
+
- Wrap `TelaTable` in `TelaCard` — tables are their own surface
|
|
454
|
+
- Build custom status indicators
|
|
455
|
+
- Use `text-success` or `text-error` on text — reserved for `<TelaStatus />`
|
|
456
|
+
- Use standalone icons in interfaces — text and labels are always sufficient
|
|
457
|
+
- **Exception:** icons are allowed inside `<TelaButton />` via the `leading` prop
|
package/docs/surfaces.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Surfaces
|
|
2
|
+
|
|
3
|
+
Layering, elevation, depth, and borders — the invisible structure of craft.
|
|
4
|
+
|
|
5
|
+
## Concentric Border Radius
|
|
6
|
+
|
|
7
|
+
When nesting rounded elements, the outer radius must equal the inner radius plus the padding between them:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
outerRadius = innerRadius + padding
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This rule is most useful when nested surfaces are close together. If padding is larger than `24px`, treat the layers as separate surfaces and choose each radius independently instead of forcing strict concentric math.
|
|
14
|
+
|
|
15
|
+
### Example
|
|
16
|
+
|
|
17
|
+
```vue
|
|
18
|
+
// Good — outer radius accounts for padding
|
|
19
|
+
<div rounded-16px p-8px> {/* 16px radius, 8px padding */}
|
|
20
|
+
<div rounded-8px> {/* 8px radius = 16 - 8 ✓ */}
|
|
21
|
+
...
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
// Bad — same radius on both
|
|
26
|
+
<div rounded-16px p-8px>
|
|
27
|
+
<div rounded-16px> {/* same radius, looks off */}
|
|
28
|
+
...
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Mismatched border radius on nested elements is one of the most common things that makes interfaces feel off. Always calculate concentrically.
|
|
34
|
+
|
|
35
|
+
## Borders
|
|
36
|
+
|
|
37
|
+
Borders should disappear when you're not looking, but be findable when you need structure.
|
|
38
|
+
|
|
39
|
+
- Always `0.5px` width — no exceptions
|
|
40
|
+
- Use semantic tokens: `border` → `border-subtle` → `border-strong`
|
|
41
|
+
- **The squint test**: Blur your eyes. Perceive hierarchy? Nothing jumping harshly? Craft whispers.
|
|
42
|
+
|
|
43
|
+
## Dividers
|
|
44
|
+
|
|
45
|
+
A divider is a border drawn as its own element — so it follows the same `0.5px` rule as borders.
|
|
46
|
+
|
|
47
|
+
- Always `h-0.5px` for a horizontal rule, `w-0.5px` for a vertical one — **never** `h-1px` / `w-1px`. A `1px` line is twice as heavy as every border on the page and breaks the hairline rhythm.
|
|
48
|
+
- Color it with a border token (`bg-border`, `bg-border-subtle`, `bg-border-strong`), not a raw gray.
|
|
49
|
+
- Prefer spacing over a divider when a gap alone separates the content (see [Negative space](#negative-space-is-a-positive-choice)).
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<!-- Correct -->
|
|
53
|
+
<div h-0.5px w-full bg-border />
|
|
54
|
+
|
|
55
|
+
<!-- Wrong — 1px (h-px / h-1px) is heavier than every border on the page -->
|
|
56
|
+
<div h-px w-full bg-border />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Depth — Pick One and Commit
|
|
60
|
+
|
|
61
|
+
| Strategy | Character | Best for |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Borders-only | Clean, technical | Dense tools |
|
|
64
|
+
| Subtle shadows | Soft lift | Approachable products |
|
|
65
|
+
| Layered shadows | Premium, dimensional | Cards needing presence |
|
|
66
|
+
| Surface color shifts | Tint-based hierarchy | No shadows |
|
|
67
|
+
|
|
68
|
+
Do not mix approaches.
|
|
69
|
+
|
|
70
|
+
## Spacing, Gaps & Negative Space
|
|
71
|
+
|
|
72
|
+
Spacing is not decoration — it is the language of hierarchy. A single `gap` value applied to everything flattens a layout. Related content stops reading as related; major sections stop reading as major. Before choosing a gap, ask what is equal and what is nested.
|
|
73
|
+
|
|
74
|
+
### Group before you gap
|
|
75
|
+
|
|
76
|
+
**Equal spacing between siblings implies equal importance.** If two rows are structurally siblings under the same `gap`, the eye reads them as peers. Two metric-card rows at the same level under `gap-40px` become two separate sections, not one metrics surface.
|
|
77
|
+
|
|
78
|
+
The fix is structural, not numeric: wrap related blocks in their own container and give that container a tighter internal `gap`. Reserve the larger `gap` for genuinely distinct sections.
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
// Bad — hero cards and summary cards read as two unrelated sections
|
|
82
|
+
<main flex="~ col" gap-40px>
|
|
83
|
+
<div>page header</div>
|
|
84
|
+
<div grid="~ cols-3">...hero cards...</div> // 40px away from the next row
|
|
85
|
+
<div grid="~ cols-3">...summary cards...</div> // 40px away from the table
|
|
86
|
+
<div>...table...</div>
|
|
87
|
+
</main>
|
|
88
|
+
|
|
89
|
+
// Good — hero + summary grouped into one metrics section; 40px only between major sections
|
|
90
|
+
<main flex="~ col" gap-40px>
|
|
91
|
+
<div>page header</div>
|
|
92
|
+
|
|
93
|
+
<div flex="~ col" gap-16px> // metrics section
|
|
94
|
+
<div grid="~ cols-3">...hero cards...</div>
|
|
95
|
+
<div grid="~ cols-3">...summary cards...</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div>...table...</div>
|
|
99
|
+
</main>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Gap hierarchy
|
|
103
|
+
|
|
104
|
+
Pick at least two gap values per page and use them consistently:
|
|
105
|
+
|
|
106
|
+
| Level | Gap | Use for |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| **Major sections** | `gap-40px` | Between page header, primary content blocks, and trailing sections (e.g. header → metrics → table) |
|
|
109
|
+
| **Related groups** | `gap-16px` | Between sibling surfaces inside the same section (e.g. hero row + summary strip) |
|
|
110
|
+
| **Within a card** | `gap-12px` / `gap-16px` | Between label, value, and supporting text inside a card |
|
|
111
|
+
| **Large + small typography** | `gap-12px` minimum, `gap-16px` preferred | Whenever a container pairs a label with a hero number or heading — never `gap-4px` or `gap-8px`, which read as accidents |
|
|
112
|
+
|
|
113
|
+
If every gap on the page is the same, you have not made a hierarchy decision — you have deferred it.
|
|
114
|
+
|
|
115
|
+
### Negative space is a positive choice
|
|
116
|
+
|
|
117
|
+
- Empty space around a hero number is what makes it feel heavy. Do not fill silence with decoration.
|
|
118
|
+
- Asymmetric padding reads as a mistake. Keep padding symmetrical unless the content demands otherwise.
|
|
119
|
+
- A section break is earned by contrast — change in surface, in density, or in gap. Do not insert dividers where spacing alone would do the job.
|
|
120
|
+
|
|
121
|
+
### Checklist before shipping
|
|
122
|
+
|
|
123
|
+
- Can you point to at least two distinct `gap` values on the page? If not, the hierarchy is flat.
|
|
124
|
+
- Are logical groups wrapped in their own container, or are they leaking into the page-level gap?
|
|
125
|
+
- Squint at the layout. Do major sections separate naturally, or does everything feel like one list?
|
|
126
|
+
|
|
127
|
+
If any answer is no — regroup before tweaking numbers.
|
package/docs/tokens.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Tokens
|
|
2
|
+
|
|
3
|
+
Tokens are design decisions expressed as code. If a token exists for what you need, use it. Always.
|
|
4
|
+
|
|
5
|
+
## Semantic Tokens First
|
|
6
|
+
|
|
7
|
+
Semantic tokens describe the role of a color, not its value. Roles survive theme changes, brand updates, and redesigns. Values don't.
|
|
8
|
+
|
|
9
|
+
```vue
|
|
10
|
+
<!-- Correct — semantic tokens -->
|
|
11
|
+
<span text-primary>Label</span>
|
|
12
|
+
<div bg-muted />
|
|
13
|
+
<div border />
|
|
14
|
+
|
|
15
|
+
<!-- Wrong — raw values for things tokens already cover -->
|
|
16
|
+
<span class="text-gray-200">Label</span>
|
|
17
|
+
<div class="bg-[#1a1a1a]" />
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Raw Palettes — Sparingly
|
|
21
|
+
|
|
22
|
+
When no semantic token covers your use case, raw palette values are allowed. Use them as a last resort, not a shortcut.
|
|
23
|
+
|
|
24
|
+
```vue
|
|
25
|
+
<div bg-gray-800 />
|
|
26
|
+
<div bg-blue-600 />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If a raw value will recur, it belongs in the token system — not scattered across components.
|
|
30
|
+
|
|
31
|
+
## Legacy Colors — Never
|
|
32
|
+
|
|
33
|
+
These are deprecated. If you encounter them, replace with the semantic equivalent.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Never use these
|
|
37
|
+
colors.dark
|
|
38
|
+
colors.base
|
|
39
|
+
colors.caution
|
|
40
|
+
colors.negative
|
|
41
|
+
colors.black
|
|
42
|
+
colors.gray[20] // or any numeric gray shade
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Token Reference
|
|
46
|
+
|
|
47
|
+
### Background
|
|
48
|
+
|
|
49
|
+
Surfaces stack. Use the right background token for each layer — not by feel, but by role.
|
|
50
|
+
|
|
51
|
+
- **`bg`** — Base layer. App background and primary surfaces. (`DT.colors.background`)
|
|
52
|
+
- **`bg-subtle`** — One level up. Secondary panels, sidebars. (`DT.colors.background.subtle`)
|
|
53
|
+
- **`bg-muted`** — Inputs, cards, contained areas. (`DT.colors.background.muted`)
|
|
54
|
+
- **`bg-lowered`** — Below the base. Subtle separators, recessed areas. (`DT.colors.background.lowered`)
|
|
55
|
+
- **`bg-success`** — Success state backgrounds in badges and alerts. (`DT.colors.background.success`)
|
|
56
|
+
- **`bg-error`** — Error state backgrounds in badges and alerts. (`DT.colors.background.error`)
|
|
57
|
+
- **`bg-warning`** — Warning state backgrounds in badges and alerts. (`DT.colors.background.warning`)
|
|
58
|
+
|
|
59
|
+
### Text
|
|
60
|
+
|
|
61
|
+
For typography, default to `text-primary` (content) and `text-secondary` (support) — see [Typography → Color](./typography.md#color--primary-and-secondary-only). The remaining text tokens are for specific roles, not for expressing hierarchy in copy.
|
|
62
|
+
|
|
63
|
+
- **`text-primary`** — Default body text. Most content. (`DT.colors.text.primary`)
|
|
64
|
+
- **`text-secondary`** — Supporting text, metadata, labels. (`DT.colors.text.secondary`)
|
|
65
|
+
- **`text-tertiary`** — Placeholders, disabled states — not "quieter body text". (`DT.colors.text.tertiary`)
|
|
66
|
+
- **`text-subtle`** — Very low emphasis, over muted surfaces only. (`DT.colors.text.subtle`)
|
|
67
|
+
- **`text-contrast`** — Reserved; primary is already the default heading color. (`DT.colors.text.contrast`)
|
|
68
|
+
- **`text-icon`** — Icon color applied via text utility. (`DT.colors.text.icon`)
|
|
69
|
+
- **`text-success`** — Success messaging and status indicators. (`DT.colors.text.success`)
|
|
70
|
+
- **`text-error`** — Error messaging, destructive action labels. (`DT.colors.text.error`)
|
|
71
|
+
- **`text-warning`** — Warning messaging, caution indicators. (`DT.colors.text.warning`)
|
|
72
|
+
- **`text-pending`** — In-progress and pending state indicators. (`DT.colors.text.pending`)
|
|
73
|
+
|
|
74
|
+
### Border
|
|
75
|
+
|
|
76
|
+
Borders should disappear when you're not looking for them. Use the right intensity for the boundary.
|
|
77
|
+
|
|
78
|
+
- **`border`** — Default. Most component borders. (`DT.colors.border`)
|
|
79
|
+
- **`border-subtle`** — Quiet dividers on light surfaces. (`DT.colors.border.subtle`)
|
|
80
|
+
- **`border-strong`** — Emphasized. Containers that need presence. (`DT.colors.border.strong`)
|
|
81
|
+
- **`border-accent`** — High-emphasis accent borders. (`DT.colors.border.accent`)
|
|
82
|
+
- **`border-inverse`** — Borders on dark/inverted surfaces. (`DT.colors.border.inverse`)
|
|
83
|
+
|
|
84
|
+
### Icon
|
|
85
|
+
|
|
86
|
+
Icons follow the same emphasis hierarchy as text — match the icon level to its context.
|
|
87
|
+
|
|
88
|
+
- **`icon`** — Default icon color. (`DT.colors.icon`)
|
|
89
|
+
- **`icon-secondary`** — Supporting icons, slightly de-emphasized. (`DT.colors.icon.secondary`)
|
|
90
|
+
- **`icon-tertiary`** — Low-emphasis icons, metadata level. (`DT.colors.icon.tertiary`)
|
|
91
|
+
- **`icon-subtle`** — Disabled or de-emphasized icons. (`DT.colors.icon.subtle`)
|
|
92
|
+
- **`icon-inverse`** — Icons on dark/inverted surfaces. (`DT.colors.icon.inverse`)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Typography
|
|
2
|
+
|
|
3
|
+
Typography tweaks for a more polished UI.
|
|
4
|
+
|
|
5
|
+
## Heading Tokens
|
|
6
|
+
|
|
7
|
+
Each heading level has three weight variants. Pick based on what the content needs to communicate, not habit.
|
|
8
|
+
|
|
9
|
+
## Headings
|
|
10
|
+
|
|
11
|
+
| Token | Usage |
|
|
12
|
+
| --------------------- | ---------------- |
|
|
13
|
+
| `heading-h1-semibold` | Page title |
|
|
14
|
+
| `heading-h2-semibold` | Section |
|
|
15
|
+
| `heading-h3-semibold` | Subsection |
|
|
16
|
+
| `heading-h4-semibold` | Minor heading |
|
|
17
|
+
| `heading-h5-semibold` | Small heading |
|
|
18
|
+
| `heading-h6-semibold` | Smallest heading |
|
|
19
|
+
|
|
20
|
+
## Body
|
|
21
|
+
|
|
22
|
+
| Token | Usage |
|
|
23
|
+
| ------------------------------------ | ------------------------------------- |
|
|
24
|
+
| `body-20-regular` / `body-20-medium` | Long-form, pairs with `h1` |
|
|
25
|
+
| `body-16-regular` / `body-16-medium` | Primary body, pairs with `h2` |
|
|
26
|
+
| `body-14-regular` / `body-14-medium` | Supporting text, pairs with `h3`/`h4` |
|
|
27
|
+
| `body-12-regular` / `body-12-medium` | Captions, labels, metadata |
|
|
28
|
+
|
|
29
|
+
## Weights
|
|
30
|
+
|
|
31
|
+
- **Regular (400)** — body text
|
|
32
|
+
- **Medium (460)** — labels, emphasis
|
|
33
|
+
- **Semibold (580)** — headings, key actions
|
|
34
|
+
|
|
35
|
+
Use all three. If you only use regular and semibold, the hierarchy is flat.
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
|
-
import {
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, join, resolve } from 'pathe'
|
|
4
5
|
import matter from 'gray-matter'
|
|
5
6
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
6
7
|
import { generateDocsToDirectory } from '../doc-generator'
|
|
7
8
|
import { TypeResolver } from '../type-resolver'
|
|
8
9
|
|
|
10
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
9
11
|
const tempDirs: string[] = []
|
|
10
12
|
|
|
11
13
|
afterEach(() => {
|
|
@@ -30,4 +32,26 @@ describe('generateDocsToDirectory', () => {
|
|
|
30
32
|
})
|
|
31
33
|
expect(skillMd).toContain('description: "')
|
|
32
34
|
})
|
|
35
|
+
|
|
36
|
+
it('appends package docs to the tela-build skill', () => {
|
|
37
|
+
const outDir = mkdtempSync(join(tmpdir(), 'tela-build-docs-'))
|
|
38
|
+
const layerPath = mkdtempSync(join(tmpdir(), 'tela-build-layer-'))
|
|
39
|
+
tempDirs.push(outDir, layerPath)
|
|
40
|
+
|
|
41
|
+
mkdirSync(join(layerPath, 'docs'), { recursive: true })
|
|
42
|
+
writeFileSync(join(layerPath, 'docs/interfaces.md'), '# Interfaces\n\nInterface guidance.', 'utf-8')
|
|
43
|
+
|
|
44
|
+
generateDocsToDirectory([], new TypeResolver(outDir), outDir, layerPath)
|
|
45
|
+
|
|
46
|
+
const skillMd = readFileSync(join(outDir, 'tela-build', 'SKILL.md'), 'utf-8')
|
|
47
|
+
|
|
48
|
+
expect(skillMd).toContain('# Interfaces')
|
|
49
|
+
expect(skillMd).toContain('Interface guidance.')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('ships package docs used by generated skills', () => {
|
|
53
|
+
const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8'))
|
|
54
|
+
|
|
55
|
+
expect(packageJson.files).toContain('docs')
|
|
56
|
+
})
|
|
33
57
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/tela-build",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"app.config.ts",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"components.json",
|
|
9
9
|
"composables",
|
|
10
10
|
"css",
|
|
11
|
+
"docs",
|
|
11
12
|
"lib",
|
|
12
13
|
"modules",
|
|
13
14
|
"nuxt.config.ts",
|