@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.
@@ -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
@@ -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 { join } from 'pathe'
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.0",
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",