@treeline-money/plugin-sdk 0.1.0

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,266 @@
1
+ /**
2
+ * Treeline Plugin SDK
3
+ *
4
+ * TypeScript types and interfaces for building Treeline plugins.
5
+ * Install: npm install @treeline-money/plugin-sdk
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /**
10
+ * Plugin manifest describing the plugin's metadata and permissions.
11
+ */
12
+ export interface PluginManifest {
13
+ /** Unique identifier (e.g., "subscriptions", "goals") */
14
+ id: string;
15
+ /** Display name */
16
+ name: string;
17
+ /** Version string (semver) */
18
+ version: string;
19
+ /** Short description */
20
+ description: string;
21
+ /** Author name or organization */
22
+ author: string;
23
+ /** Optional icon (emoji or icon name) */
24
+ icon?: string;
25
+ /** Permissions this plugin requires */
26
+ permissions?: PluginPermissions;
27
+ }
28
+ /**
29
+ * Permissions a plugin can request.
30
+ */
31
+ export interface PluginPermissions {
32
+ /** Table permissions for this plugin */
33
+ tables?: {
34
+ /** Tables this plugin can SELECT from */
35
+ read?: string[];
36
+ /** Tables this plugin can INSERT/UPDATE/DELETE */
37
+ write?: string[];
38
+ /** Tables this plugin can CREATE/DROP (must match sys_plugin_{id}_* pattern) */
39
+ create?: string[];
40
+ };
41
+ }
42
+ /**
43
+ * The SDK object passed to plugin views via props.
44
+ *
45
+ * @example
46
+ * ```svelte
47
+ * <script lang="ts">
48
+ * import type { PluginSDK } from '@treeline-money/plugin-sdk';
49
+ *
50
+ * interface Props {
51
+ * sdk: PluginSDK;
52
+ * }
53
+ * const { sdk }: Props = $props();
54
+ *
55
+ * // Query transactions
56
+ * const transactions = await sdk.query('SELECT * FROM transactions LIMIT 10');
57
+ *
58
+ * // Show a toast
59
+ * sdk.toast.success('Data loaded!');
60
+ * </script>
61
+ * ```
62
+ */
63
+ export interface PluginSDK {
64
+ /**
65
+ * Execute a read-only SQL query against the database.
66
+ * @param sql - SQL SELECT query
67
+ * @returns Array of row objects
68
+ */
69
+ query: <T = Record<string, unknown>>(sql: string) => Promise<T[]>;
70
+ /**
71
+ * Execute a write SQL query (INSERT/UPDATE/DELETE).
72
+ * Restricted to tables allowed in plugin permissions.
73
+ * @param sql - SQL write query
74
+ * @returns Object with rowsAffected count
75
+ */
76
+ execute: (sql: string) => Promise<{
77
+ rowsAffected: number;
78
+ }>;
79
+ /**
80
+ * Toast notification methods.
81
+ */
82
+ toast: {
83
+ /** Show an info toast */
84
+ show: (message: string, description?: string) => void;
85
+ /** Show a success toast */
86
+ success: (message: string, description?: string) => void;
87
+ /** Show an error toast */
88
+ error: (message: string, description?: string) => void;
89
+ /** Show a warning toast */
90
+ warning: (message: string, description?: string) => void;
91
+ /** Show an info toast */
92
+ info: (message: string, description?: string) => void;
93
+ };
94
+ /**
95
+ * Navigate to another view.
96
+ * @param viewId - The view ID to open
97
+ * @param props - Optional props to pass to the view
98
+ */
99
+ openView: (viewId: string, props?: Record<string, unknown>) => void;
100
+ /**
101
+ * Subscribe to data refresh events (called after sync/import).
102
+ * @param callback - Function to call when data is refreshed
103
+ * @returns Unsubscribe function
104
+ */
105
+ onDataRefresh: (callback: () => void) => () => void;
106
+ /**
107
+ * Emit a data refresh event. Call this after modifying data
108
+ * so other views can update.
109
+ */
110
+ emitDataRefresh: () => void;
111
+ /**
112
+ * Update the badge count shown on this plugin's sidebar item.
113
+ * @param count - Badge count (0 or undefined to hide)
114
+ */
115
+ updateBadge: (count: number | undefined) => void;
116
+ /**
117
+ * Theme utilities.
118
+ */
119
+ theme: {
120
+ /** Get current theme ("light" or "dark") */
121
+ current: () => "light" | "dark";
122
+ /** Subscribe to theme changes */
123
+ subscribe: (callback: (theme: string) => void) => () => void;
124
+ };
125
+ /**
126
+ * Platform-aware modifier key display string.
127
+ * Returns "Cmd" on Mac, "Ctrl" on Windows/Linux.
128
+ */
129
+ modKey: "Cmd" | "Ctrl";
130
+ /**
131
+ * Format a keyboard shortcut for display.
132
+ * Converts "mod+p" to "⌘P" on Mac or "Ctrl+P" on Windows.
133
+ * @param shortcut - Shortcut string (e.g., "mod+shift+p")
134
+ */
135
+ formatShortcut: (shortcut: string) => string;
136
+ /**
137
+ * Plugin settings (persisted, scoped to plugin ID).
138
+ */
139
+ settings: {
140
+ /** Get all settings for this plugin */
141
+ get: <T extends Record<string, unknown>>() => Promise<T>;
142
+ /** Save settings for this plugin */
143
+ set: <T extends Record<string, unknown>>(settings: T) => Promise<void>;
144
+ };
145
+ /**
146
+ * Plugin state (ephemeral, scoped to plugin ID).
147
+ * Use for runtime state that doesn't need to persist.
148
+ */
149
+ state: {
150
+ /** Read plugin state */
151
+ read: <T>() => Promise<T | null>;
152
+ /** Write plugin state */
153
+ write: <T>(state: T) => Promise<void>;
154
+ };
155
+ /**
156
+ * Currency formatting utilities.
157
+ */
158
+ currency: {
159
+ /** Format amount with currency symbol (e.g., "$1,234.56") */
160
+ format: (amount: number, currency?: string) => string;
161
+ /** Format compactly for large amounts (e.g., "$1.2M") */
162
+ formatCompact: (amount: number, currency?: string) => string;
163
+ /** Format just the number without symbol (e.g., "1,234.56") */
164
+ formatAmount: (amount: number) => string;
165
+ /** Get symbol for a currency code (e.g., "USD" -> "$") */
166
+ getSymbol: (currency: string) => string;
167
+ /** Get the user's configured currency code */
168
+ getUserCurrency: () => string;
169
+ /** List of supported currency codes */
170
+ supportedCurrencies: string[];
171
+ };
172
+ }
173
+ /**
174
+ * Sidebar section definition.
175
+ */
176
+ export interface SidebarSection {
177
+ /** Section ID */
178
+ id: string;
179
+ /** Section title (shown as header) */
180
+ title: string;
181
+ /** Sort order (lower = higher) */
182
+ order: number;
183
+ }
184
+ /**
185
+ * Sidebar item definition.
186
+ */
187
+ export interface SidebarItem {
188
+ /** Unique ID */
189
+ id: string;
190
+ /** Display label */
191
+ label: string;
192
+ /** Icon (emoji or icon name) */
193
+ icon: string;
194
+ /** Section this belongs to */
195
+ sectionId: string;
196
+ /** View to open when clicked */
197
+ viewId: string;
198
+ /** Keyboard shortcut hint */
199
+ shortcut?: string;
200
+ /** Sort order within section */
201
+ order?: number;
202
+ }
203
+ /**
204
+ * View definition for plugin views.
205
+ */
206
+ export interface ViewDefinition {
207
+ /** Unique view ID */
208
+ id: string;
209
+ /** Display name (shown in tab) */
210
+ name: string;
211
+ /** Icon for tab */
212
+ icon: string;
213
+ /**
214
+ * Mount function that renders into the target element.
215
+ * @param target - DOM element to render into
216
+ * @param props - Props including the SDK
217
+ * @returns Cleanup function to call when unmounting
218
+ */
219
+ mount: (target: HTMLElement, props: {
220
+ sdk: PluginSDK;
221
+ }) => () => void;
222
+ /** Can multiple instances be open? */
223
+ allowMultiple?: boolean;
224
+ }
225
+ /**
226
+ * Command definition for the command palette.
227
+ */
228
+ export interface Command {
229
+ /** Unique command ID */
230
+ id: string;
231
+ /** Display name */
232
+ name: string;
233
+ /** Category for grouping */
234
+ category?: string;
235
+ /** Keyboard shortcut */
236
+ shortcut?: string;
237
+ /** Function to execute */
238
+ execute: () => void | Promise<void>;
239
+ }
240
+ /**
241
+ * Plugin context provided during activation.
242
+ */
243
+ export interface PluginContext {
244
+ /** Register a sidebar section */
245
+ registerSidebarSection: (section: SidebarSection) => void;
246
+ /** Register a sidebar item */
247
+ registerSidebarItem: (item: SidebarItem) => void;
248
+ /** Register a view */
249
+ registerView: (view: ViewDefinition) => void;
250
+ /** Register a command */
251
+ registerCommand: (command: Command) => void;
252
+ /** Open a view */
253
+ openView: (viewId: string, props?: Record<string, unknown>) => void;
254
+ }
255
+ /**
256
+ * Plugin interface that all plugins must implement.
257
+ */
258
+ export interface Plugin {
259
+ /** Plugin manifest */
260
+ manifest: PluginManifest;
261
+ /** Called when plugin is activated */
262
+ activate: (ctx: PluginContext) => void | Promise<void>;
263
+ /** Called when plugin is deactivated */
264
+ deactivate?: () => void | Promise<void>;
265
+ }
266
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Treeline Plugin SDK
3
+ *
4
+ * TypeScript types and interfaces for building Treeline plugins.
5
+ * Install: npm install @treeline-money/plugin-sdk
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ export {};
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@treeline-money/plugin-sdk",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for building Treeline plugins",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./styles": "./styles/plugin.css"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "styles"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "treeline",
25
+ "plugin",
26
+ "sdk",
27
+ "personal-finance"
28
+ ],
29
+ "author": "Treeline Money",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/treeline-money/treeline.git",
34
+ "directory": "plugin-sdk"
35
+ },
36
+ "devDependencies": {
37
+ "typescript": "^5.0.0"
38
+ }
39
+ }
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Treeline Plugin Styles
3
+ *
4
+ * Shared CSS classes for community plugins to match the app's look and feel.
5
+ * Import this in your plugin: import '@treeline-money/plugin-sdk/styles'
6
+ *
7
+ * All classes are prefixed with `tl-` to avoid conflicts.
8
+ */
9
+
10
+ /* ============================================================================
11
+ * Layout
12
+ * ============================================================================ */
13
+
14
+ .tl-view {
15
+ height: 100%;
16
+ display: flex;
17
+ flex-direction: column;
18
+ background: var(--bg-primary);
19
+ color: var(--text-primary);
20
+ font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
21
+ }
22
+
23
+ .tl-header {
24
+ display: flex;
25
+ justify-content: space-between;
26
+ align-items: flex-start;
27
+ padding: var(--spacing-lg, 16px);
28
+ background: var(--bg-secondary);
29
+ border-bottom: 1px solid var(--border-primary);
30
+ }
31
+
32
+ .tl-header-left {
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: var(--spacing-xs, 4px);
36
+ }
37
+
38
+ .tl-header-right {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: var(--spacing-md, 12px);
42
+ }
43
+
44
+ .tl-title {
45
+ font-size: 16px;
46
+ font-weight: 600;
47
+ color: var(--text-primary);
48
+ margin: 0;
49
+ }
50
+
51
+ .tl-subtitle {
52
+ font-size: 13px;
53
+ color: var(--text-muted);
54
+ margin: 0;
55
+ }
56
+
57
+ .tl-content {
58
+ flex: 1;
59
+ overflow-y: auto;
60
+ padding: var(--spacing-lg, 16px);
61
+ }
62
+
63
+ /* ============================================================================
64
+ * Buttons
65
+ * ============================================================================ */
66
+
67
+ .tl-btn {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ gap: 6px;
72
+ padding: 8px 14px;
73
+ border-radius: var(--radius-sm, 4px);
74
+ font-size: 13px;
75
+ font-weight: 500;
76
+ cursor: pointer;
77
+ border: none;
78
+ transition: all 0.15s;
79
+ white-space: nowrap;
80
+ }
81
+
82
+ .tl-btn:disabled {
83
+ opacity: 0.5;
84
+ cursor: not-allowed;
85
+ }
86
+
87
+ .tl-btn-primary {
88
+ background: var(--accent-primary);
89
+ color: var(--bg-primary);
90
+ }
91
+
92
+ .tl-btn-primary:hover:not(:disabled) {
93
+ opacity: 0.9;
94
+ }
95
+
96
+ .tl-btn-secondary {
97
+ background: var(--bg-tertiary);
98
+ color: var(--text-primary);
99
+ border: 1px solid var(--border-primary);
100
+ }
101
+
102
+ .tl-btn-secondary:hover:not(:disabled) {
103
+ background: var(--bg-secondary);
104
+ }
105
+
106
+ .tl-btn-danger {
107
+ background: var(--accent-danger);
108
+ color: white;
109
+ }
110
+
111
+ .tl-btn-danger:hover:not(:disabled) {
112
+ opacity: 0.9;
113
+ }
114
+
115
+ .tl-btn-text {
116
+ background: transparent;
117
+ color: var(--text-secondary);
118
+ padding: 4px 8px;
119
+ }
120
+
121
+ .tl-btn-text:hover:not(:disabled) {
122
+ color: var(--text-primary);
123
+ background: var(--bg-tertiary);
124
+ }
125
+
126
+ .tl-btn-icon {
127
+ width: 28px;
128
+ height: 28px;
129
+ padding: 0;
130
+ background: transparent;
131
+ border: none;
132
+ border-radius: var(--radius-sm, 4px);
133
+ color: var(--text-secondary);
134
+ cursor: pointer;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ font-size: 14px;
139
+ transition: all 0.15s;
140
+ }
141
+
142
+ .tl-btn-icon:hover:not(:disabled) {
143
+ background: var(--bg-tertiary);
144
+ color: var(--text-primary);
145
+ }
146
+
147
+ /* ============================================================================
148
+ * Cards
149
+ * ============================================================================ */
150
+
151
+ .tl-cards {
152
+ display: grid;
153
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
154
+ gap: var(--spacing-md, 12px);
155
+ margin-bottom: var(--spacing-lg, 16px);
156
+ }
157
+
158
+ .tl-card {
159
+ background: var(--bg-secondary);
160
+ border: 1px solid var(--border-primary);
161
+ border-radius: var(--radius-md, 6px);
162
+ padding: var(--spacing-md, 12px);
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: var(--spacing-xs, 4px);
166
+ }
167
+
168
+ .tl-card-label {
169
+ font-size: 11px;
170
+ color: var(--text-muted);
171
+ text-transform: uppercase;
172
+ letter-spacing: 0.5px;
173
+ font-weight: 500;
174
+ }
175
+
176
+ .tl-card-value {
177
+ font-size: 24px;
178
+ font-weight: 600;
179
+ color: var(--text-primary);
180
+ }
181
+
182
+ .tl-card-value-sm {
183
+ font-size: 18px;
184
+ }
185
+
186
+ /* ============================================================================
187
+ * Tables
188
+ * ============================================================================ */
189
+
190
+ .tl-table-container {
191
+ overflow-x: auto;
192
+ }
193
+
194
+ .tl-table {
195
+ width: 100%;
196
+ border-collapse: collapse;
197
+ font-size: 13px;
198
+ }
199
+
200
+ .tl-table th {
201
+ text-align: left;
202
+ padding: var(--spacing-sm, 8px) var(--spacing-md, 12px);
203
+ font-size: 11px;
204
+ font-weight: 600;
205
+ color: var(--text-muted);
206
+ text-transform: uppercase;
207
+ letter-spacing: 0.5px;
208
+ border-bottom: 2px solid var(--border-primary);
209
+ white-space: nowrap;
210
+ }
211
+
212
+ .tl-table th.tl-sortable {
213
+ cursor: pointer;
214
+ user-select: none;
215
+ transition: color 0.15s;
216
+ }
217
+
218
+ .tl-table th.tl-sortable:hover {
219
+ color: var(--text-primary);
220
+ }
221
+
222
+ .tl-table th.tl-sorted {
223
+ color: var(--accent-primary);
224
+ }
225
+
226
+ .tl-table td {
227
+ padding: var(--spacing-sm, 8px) var(--spacing-md, 12px);
228
+ border-bottom: 1px solid var(--border-primary);
229
+ color: var(--text-primary);
230
+ }
231
+
232
+ .tl-table tbody tr:hover {
233
+ background: var(--bg-secondary);
234
+ }
235
+
236
+ .tl-table tbody tr.tl-selected {
237
+ background: var(--bg-active);
238
+ }
239
+
240
+ .tl-table tbody tr.tl-muted {
241
+ opacity: 0.5;
242
+ }
243
+
244
+ /* Table cell types */
245
+ .tl-cell-mono {
246
+ font-family: var(--font-mono);
247
+ }
248
+
249
+ .tl-cell-number {
250
+ font-family: var(--font-mono);
251
+ text-align: right;
252
+ }
253
+
254
+ .tl-cell-date {
255
+ color: var(--text-muted);
256
+ font-size: 12px;
257
+ }
258
+
259
+ .tl-cell-positive {
260
+ color: var(--color-positive);
261
+ font-family: var(--font-mono);
262
+ }
263
+
264
+ .tl-cell-negative {
265
+ color: var(--color-negative);
266
+ font-family: var(--font-mono);
267
+ }
268
+
269
+ .tl-cell-actions {
270
+ display: flex;
271
+ gap: var(--spacing-xs, 4px);
272
+ justify-content: flex-end;
273
+ }
274
+
275
+ /* ============================================================================
276
+ * Badges
277
+ * ============================================================================ */
278
+
279
+ .tl-badge {
280
+ display: inline-block;
281
+ padding: 3px 8px;
282
+ background: var(--bg-tertiary);
283
+ color: var(--accent-primary);
284
+ font-size: 10px;
285
+ font-weight: 600;
286
+ border-radius: var(--radius-sm, 4px);
287
+ text-transform: uppercase;
288
+ letter-spacing: 0.3px;
289
+ }
290
+
291
+ .tl-badge-success {
292
+ background: rgba(63, 185, 80, 0.15);
293
+ color: var(--accent-success);
294
+ }
295
+
296
+ .tl-badge-warning {
297
+ background: rgba(210, 153, 34, 0.15);
298
+ color: var(--accent-warning);
299
+ }
300
+
301
+ .tl-badge-danger {
302
+ background: rgba(248, 81, 73, 0.15);
303
+ color: var(--accent-danger);
304
+ }
305
+
306
+ .tl-badge-muted {
307
+ background: var(--bg-tertiary);
308
+ color: var(--text-muted);
309
+ }
310
+
311
+ /* ============================================================================
312
+ * Form Elements
313
+ * ============================================================================ */
314
+
315
+ .tl-input {
316
+ padding: 8px 12px;
317
+ background: var(--bg-primary);
318
+ border: 1px solid var(--border-primary);
319
+ border-radius: var(--radius-sm, 4px);
320
+ color: var(--text-primary);
321
+ font-size: 13px;
322
+ font-family: inherit;
323
+ transition: border-color 0.15s;
324
+ }
325
+
326
+ .tl-input:focus {
327
+ outline: none;
328
+ border-color: var(--accent-primary);
329
+ }
330
+
331
+ .tl-input::placeholder {
332
+ color: var(--text-muted);
333
+ }
334
+
335
+ .tl-select {
336
+ padding: 8px 28px 8px 12px;
337
+ background: var(--bg-primary);
338
+ border: 1px solid var(--border-primary);
339
+ border-radius: var(--radius-sm, 4px);
340
+ color: var(--text-primary);
341
+ font-size: 13px;
342
+ font-family: inherit;
343
+ appearance: none;
344
+ -webkit-appearance: none;
345
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239ca3af' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
346
+ background-repeat: no-repeat;
347
+ background-position: right 8px center;
348
+ cursor: pointer;
349
+ transition: border-color 0.15s;
350
+ }
351
+
352
+ .tl-select:focus {
353
+ outline: none;
354
+ border-color: var(--accent-primary);
355
+ }
356
+
357
+ .tl-select option {
358
+ background: var(--bg-secondary);
359
+ color: var(--text-primary);
360
+ }
361
+
362
+ .tl-checkbox {
363
+ display: flex;
364
+ align-items: center;
365
+ gap: var(--spacing-sm, 8px);
366
+ font-size: 13px;
367
+ color: var(--text-secondary);
368
+ cursor: pointer;
369
+ }
370
+
371
+ .tl-checkbox input {
372
+ accent-color: var(--accent-primary);
373
+ }
374
+
375
+ .tl-label {
376
+ font-size: 12px;
377
+ font-weight: 500;
378
+ color: var(--text-secondary);
379
+ margin-bottom: var(--spacing-xs, 4px);
380
+ }
381
+
382
+ .tl-form-group {
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: var(--spacing-xs, 4px);
386
+ margin-bottom: var(--spacing-md, 12px);
387
+ }
388
+
389
+ /* ============================================================================
390
+ * Empty State
391
+ * ============================================================================ */
392
+
393
+ .tl-empty {
394
+ display: flex;
395
+ flex-direction: column;
396
+ align-items: center;
397
+ justify-content: center;
398
+ text-align: center;
399
+ padding: var(--spacing-xl, 24px);
400
+ color: var(--text-muted);
401
+ min-height: 200px;
402
+ }
403
+
404
+ .tl-empty-icon {
405
+ font-size: 48px;
406
+ margin-bottom: var(--spacing-md, 12px);
407
+ opacity: 0.5;
408
+ }
409
+
410
+ .tl-empty-title {
411
+ font-size: 16px;
412
+ font-weight: 600;
413
+ color: var(--text-primary);
414
+ margin-bottom: var(--spacing-sm, 8px);
415
+ }
416
+
417
+ .tl-empty-message {
418
+ font-size: 13px;
419
+ max-width: 400px;
420
+ }
421
+
422
+ /* ============================================================================
423
+ * Loading State
424
+ * ============================================================================ */
425
+
426
+ .tl-loading {
427
+ display: flex;
428
+ flex-direction: column;
429
+ align-items: center;
430
+ justify-content: center;
431
+ padding: var(--spacing-xl, 24px);
432
+ color: var(--text-muted);
433
+ gap: var(--spacing-md, 12px);
434
+ }
435
+
436
+ .tl-spinner {
437
+ width: 24px;
438
+ height: 24px;
439
+ border: 2px solid var(--border-primary);
440
+ border-top-color: var(--accent-primary);
441
+ border-radius: 50%;
442
+ animation: tl-spin 0.8s linear infinite;
443
+ }
444
+
445
+ @keyframes tl-spin {
446
+ to {
447
+ transform: rotate(360deg);
448
+ }
449
+ }
450
+
451
+ /* ============================================================================
452
+ * Utility Classes
453
+ * ============================================================================ */
454
+
455
+ .tl-mono {
456
+ font-family: var(--font-mono);
457
+ }
458
+
459
+ .tl-muted {
460
+ color: var(--text-muted);
461
+ }
462
+
463
+ .tl-positive {
464
+ color: var(--color-positive);
465
+ }
466
+
467
+ .tl-negative {
468
+ color: var(--color-negative);
469
+ }
470
+
471
+ .tl-text-sm {
472
+ font-size: 12px;
473
+ }
474
+
475
+ .tl-text-xs {
476
+ font-size: 11px;
477
+ }
478
+
479
+ .tl-font-semibold {
480
+ font-weight: 600;
481
+ }
482
+
483
+ .tl-truncate {
484
+ overflow: hidden;
485
+ text-overflow: ellipsis;
486
+ white-space: nowrap;
487
+ }
488
+
489
+ .tl-gap-sm {
490
+ gap: var(--spacing-sm, 8px);
491
+ }
492
+
493
+ .tl-gap-md {
494
+ gap: var(--spacing-md, 12px);
495
+ }
496
+
497
+ .tl-mt-md {
498
+ margin-top: var(--spacing-md, 12px);
499
+ }
500
+
501
+ .tl-mb-md {
502
+ margin-bottom: var(--spacing-md, 12px);
503
+ }