vectra-client 1.0.6 → 1.0.8

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,632 @@
1
+ ---
2
+ layout: page
3
+ title: API Methods
4
+ permalink: /api/methods/
5
+ ---
6
+
7
+ # API Methods Reference
8
+
9
+ Complete reference for all Vectra API methods.
10
+
11
+ > For a quick reference, see the [API Cheatsheet](/api/cheatsheet/).
12
+ > For an overview, see the [API Overview](/api/overview/).
13
+
14
+ ## Client Methods
15
+
16
+ ### `Vectra::Client.new(options)`
17
+
18
+ Initialize a new Vectra client.
19
+
20
+ **Parameters:**
21
+ - `provider` (Symbol) - Provider name: `:pinecone`, `:qdrant`, `:weaviate`, `:pgvector`, `:memory`
22
+ - `api_key` (String, optional) - API key for cloud providers
23
+ - `host` (String, optional) - Host URL for self-hosted providers
24
+ - `environment` (String, optional) - Environment/region (Pinecone)
25
+ - `index` (String, optional) - Default index name
26
+ - `namespace` (String, optional) - Default namespace
27
+
28
+ **Returns:** `Vectra::Client`
29
+
30
+ **Example:**
31
+ ```ruby
32
+ client = Vectra::Client.new(
33
+ provider: :qdrant,
34
+ host: 'http://localhost:6333',
35
+ api_key: ENV['QDRANT_API_KEY']
36
+ )
37
+ ```
38
+
39
+ ---
40
+
41
+ ### `client.upsert(index:, vectors:, namespace: nil)`
42
+
43
+ Upsert vectors into an index. If a vector with the same ID exists, it will be updated.
44
+
45
+ **Parameters:**
46
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
47
+ - `vectors` (Array<Hash, Vector>) - Array of vector hashes or Vector objects
48
+ - `namespace` (String, optional) - Namespace
49
+
50
+ **Vector Hash Format:**
51
+ ```ruby
52
+ {
53
+ id: 'unique-id', # Required
54
+ values: [0.1, 0.2, 0.3], # Required: Array of floats
55
+ metadata: { key: 'value' } # Optional: Hash of metadata
56
+ }
57
+ ```
58
+
59
+ **Returns:** `Hash` with `:upserted_count`
60
+
61
+ **Example:**
62
+ ```ruby
63
+ result = client.upsert(
64
+ index: 'documents',
65
+ vectors: [
66
+ { id: 'doc-1', values: [0.1, 0.2, 0.3], metadata: { title: 'Hello' } },
67
+ { id: 'doc-2', values: [0.4, 0.5, 0.6], metadata: { title: 'World' } }
68
+ ]
69
+ )
70
+ # => { upserted_count: 2 }
71
+ ```
72
+
73
+ ---
74
+
75
+ ### `client.query(index:, vector:, top_k: 10, namespace: nil, filter: nil, include_values: false, include_metadata: true)`
76
+
77
+ Search for similar vectors using cosine similarity.
78
+
79
+ **Parameters:**
80
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
81
+ - `vector` (Array<Float>) - Query vector
82
+ - `top_k` (Integer) - Number of results (default: 10)
83
+ - `namespace` (String, optional) - Namespace
84
+ - `filter` (Hash, optional) - Metadata filter
85
+ - `include_values` (Boolean) - Include vector values in results (default: false)
86
+ - `include_metadata` (Boolean) - Include metadata in results (default: true)
87
+
88
+ **Returns:** `Vectra::QueryResult`
89
+
90
+ **Example:**
91
+ ```ruby
92
+ results = client.query(
93
+ index: 'documents',
94
+ vector: [0.1, 0.2, 0.3],
95
+ top_k: 5,
96
+ filter: { category: 'docs' },
97
+ include_metadata: true
98
+ )
99
+
100
+ results.each do |match|
101
+ puts "#{match.id}: #{match.score} - #{match.metadata['title']}"
102
+ end
103
+ ```
104
+
105
+ **QueryResult Methods:**
106
+ - `results.each` - Iterate over matches
107
+ - `results.size` - Number of matches
108
+ - `results.ids` - Array of match IDs
109
+ - `results.scores` - Array of similarity scores
110
+ - `results.first` - First match (highest score)
111
+
112
+ ---
113
+
114
+ ### `client.hybrid_search(index:, vector:, text:, alpha: 0.7, top_k: 10, namespace: nil, filter: nil, include_values: false, include_metadata: true)`
115
+
116
+ Combine semantic (vector) and keyword (text) search.
117
+
118
+ **Parameters:**
119
+ - `index` (String) - Index/collection name
120
+ - `vector` (Array<Float>) - Query vector for semantic search
121
+ - `text` (String) - Text query for keyword search
122
+ - `alpha` (Float) - Balance between semantic and keyword (0.0 = pure keyword, 1.0 = pure semantic, default: 0.7)
123
+ - `top_k` (Integer) - Number of results (default: 10)
124
+ - `namespace` (String, optional) - Namespace
125
+ - `filter` (Hash, optional) - Metadata filter
126
+ - `include_values` (Boolean) - Include vector values (default: false)
127
+ - `include_metadata` (Boolean) - Include metadata (default: true)
128
+
129
+ **Returns:** `Vectra::QueryResult`
130
+
131
+ **Provider Support:**
132
+ - ✅ Qdrant
133
+ - ✅ Weaviate
134
+ - ✅ pgvector
135
+ - ⚠️ Pinecone (limited support)
136
+
137
+ **Example:**
138
+ ```ruby
139
+ results = client.hybrid_search(
140
+ index: 'documents',
141
+ vector: query_embedding,
142
+ text: 'ruby vector search',
143
+ alpha: 0.7, # 70% semantic, 30% keyword
144
+ top_k: 10
145
+ )
146
+ ```
147
+
148
+ ---
149
+
150
+ ### `client.fetch(index:, ids:, namespace: nil)`
151
+
152
+ Fetch vectors by their IDs.
153
+
154
+ **Parameters:**
155
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
156
+ - `ids` (Array<String>) - Array of vector IDs
157
+ - `namespace` (String, optional) - Namespace
158
+
159
+ **Returns:** `Hash<String, Vectra::Vector>` - Hash mapping ID to Vector object
160
+
161
+ **Example:**
162
+ ```ruby
163
+ vectors = client.fetch(
164
+ index: 'documents',
165
+ ids: ['doc-1', 'doc-2']
166
+ )
167
+
168
+ vectors['doc-1'].values # => [0.1, 0.2, 0.3]
169
+ vectors['doc-1'].metadata # => { 'title' => 'Hello' }
170
+ ```
171
+
172
+ ---
173
+
174
+ ### `client.update(index:, id:, metadata: nil, values: nil, namespace: nil)`
175
+
176
+ Update a vector's metadata or values.
177
+
178
+ **Parameters:**
179
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
180
+ - `id` (String) - Vector ID
181
+ - `metadata` (Hash, optional) - New metadata (merged with existing)
182
+ - `values` (Array<Float>, optional) - New vector values
183
+ - `namespace` (String, optional) - Namespace
184
+
185
+ **Returns:** `Hash` with `:updated`
186
+
187
+ **Note:** Must provide either `metadata` or `values` (or both).
188
+
189
+ **Example:**
190
+ ```ruby
191
+ client.update(
192
+ index: 'documents',
193
+ id: 'doc-1',
194
+ metadata: { category: 'updated', status: 'published' }
195
+ )
196
+ ```
197
+
198
+ ---
199
+
200
+ ### `client.delete(index:, ids: nil, namespace: nil, filter: nil, delete_all: false)`
201
+
202
+ Delete vectors.
203
+
204
+ **Parameters:**
205
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
206
+ - `ids` (Array<String>, optional) - Vector IDs to delete
207
+ - `namespace` (String, optional) - Namespace
208
+ - `filter` (Hash, optional) - Delete by metadata filter
209
+ - `delete_all` (Boolean) - Delete all vectors in namespace (default: false)
210
+
211
+ **Returns:** `Hash` with `:deleted`
212
+
213
+ **Note:** Must provide `ids`, `filter`, or `delete_all: true`.
214
+
215
+ **Example:**
216
+ ```ruby
217
+ # Delete by IDs
218
+ client.delete(index: 'documents', ids: ['doc-1', 'doc-2'])
219
+
220
+ # Delete by filter
221
+ client.delete(index: 'documents', filter: { category: 'old' })
222
+
223
+ # Delete all
224
+ client.delete(index: 'documents', delete_all: true)
225
+ ```
226
+
227
+ ---
228
+
229
+ ### `client.stats(index:, namespace: nil)`
230
+
231
+ Get index statistics.
232
+
233
+ **Parameters:**
234
+ - `index` (String) - Index/collection name (uses client's default index when omitted)
235
+ - `namespace` (String, optional) - Namespace
236
+
237
+ **Returns:** `Hash` with statistics:
238
+ - `dimension` (Integer) - Vector dimension
239
+ - `total_vector_count` (Integer) - Total number of vectors
240
+ - `namespaces` (Hash, optional) - Namespace breakdown with vector counts
241
+
242
+ **Example:**
243
+ ```ruby
244
+ stats = client.stats(index: 'documents')
245
+ # => { dimension: 1536, total_vector_count: 1000, namespaces: { "default" => { vector_count: 1000 } } }
246
+ ```
247
+
248
+ ---
249
+
250
+ ### `client.create_index(name:, dimension:, metric: "cosine", **options)`
251
+
252
+ Create a new index/collection.
253
+
254
+ **Parameters:**
255
+ - `name` (String) - Index name
256
+ - `dimension` (Integer) - Vector dimension
257
+ - `metric` (String) - Distance metric: `"cosine"` (default), `"euclidean"`, `"dot_product"`
258
+ - `options` (Hash) - Provider-specific options (e.g., `on_disk: true` for Qdrant)
259
+
260
+ **Returns:** `Hash` with index information
261
+
262
+ **Note:** Not all providers support index creation. Raises `NotImplementedError` if provider doesn't support it (e.g., Memory, Weaviate).
263
+
264
+ **Example:**
265
+ ```ruby
266
+ # Create index with default cosine metric
267
+ result = client.create_index(name: 'documents', dimension: 384)
268
+ # => { name: 'documents', dimension: 384, metric: 'cosine', status: 'ready' }
269
+
270
+ # Create with custom metric (Qdrant)
271
+ result = client.create_index(
272
+ name: 'products',
273
+ dimension: 1536,
274
+ metric: 'euclidean',
275
+ on_disk: true
276
+ )
277
+ ```
278
+
279
+ ---
280
+
281
+ ### `client.delete_index(name:)`
282
+
283
+ Delete an index/collection.
284
+
285
+ **Parameters:**
286
+ - `name` (String) - Index name
287
+
288
+ **Returns:** `Hash` with `:deleted => true`
289
+
290
+ **Note:** Not all providers support index deletion. Raises `NotImplementedError` if provider doesn't support it (e.g., Memory, Weaviate).
291
+
292
+ **Example:**
293
+ ```ruby
294
+ result = client.delete_index(name: 'old-index')
295
+ # => { deleted: true }
296
+ ```
297
+
298
+ ---
299
+
300
+ ### `client.list_namespaces(index:)`
301
+
302
+ List all namespaces in an index.
303
+
304
+ **Parameters:**
305
+ - `index` (String) - Index/collection name
306
+
307
+ **Returns:** `Array<String>` - List of namespace names (excludes empty/default namespace)
308
+
309
+ **Example:**
310
+ ```ruby
311
+ namespaces = client.list_namespaces(index: 'documents')
312
+ # => ["tenant-1", "tenant-2", "tenant-3"]
313
+
314
+ namespaces.each do |ns|
315
+ stats = client.stats(index: 'documents', namespace: ns)
316
+ puts "Namespace #{ns}: #{stats[:total_vector_count]} vectors"
317
+ end
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Health & Monitoring Methods
323
+
324
+ ### `client.healthy?`
325
+
326
+ Quick health check - returns true if provider connection is healthy.
327
+
328
+ **Returns:** `Boolean`
329
+
330
+ **Example:**
331
+ ```ruby
332
+ if client.healthy?
333
+ client.upsert(...)
334
+ else
335
+ Rails.logger.warn('Vectra provider is unhealthy')
336
+ end
337
+ ```
338
+
339
+ ---
340
+
341
+ ### `client.ping`
342
+
343
+ Ping provider and get connection health status with latency.
344
+
345
+ **Returns:** `Hash` with:
346
+ - `healthy` (Boolean) - Health status
347
+ - `provider` (Symbol) - Provider name
348
+ - `latency_ms` (Float) - Latency in milliseconds
349
+
350
+ **Example:**
351
+ ```ruby
352
+ status = client.ping
353
+ # => { healthy: true, provider: :qdrant, latency_ms: 23.4 }
354
+
355
+ puts "Latency: #{status[:latency_ms]}ms"
356
+ ```
357
+
358
+ ---
359
+
360
+ ### `client.health_check`
361
+
362
+ Detailed health check with provider-specific information.
363
+
364
+ **Returns:** `Hash` with detailed health information
365
+
366
+ **Example:**
367
+ ```ruby
368
+ health = client.health_check
369
+ # => { healthy: true, provider: :qdrant, version: '1.7.0', ... }
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Vector Helper Methods
375
+
376
+ ### `Vectra::Vector.normalize(vector, type: :l2)`
377
+
378
+ Normalize a vector array (non-mutating).
379
+
380
+ **Parameters:**
381
+ - `vector` (Array<Float>) - Vector values to normalize
382
+ - `type` (Symbol) - Normalization type: `:l2` (default) or `:l1`
383
+
384
+ **Returns:** `Array<Float>` - Normalized vector
385
+
386
+ **Example:**
387
+ ```ruby
388
+ embedding = openai_response['data'][0]['embedding']
389
+ normalized = Vectra::Vector.normalize(embedding, type: :l2)
390
+ client.upsert(vectors: [{ id: 'doc-1', values: normalized }])
391
+ ```
392
+
393
+ ---
394
+
395
+ ### `vector.normalize!(type: :l2)`
396
+
397
+ Normalize vector in-place (mutates the vector).
398
+
399
+ **Parameters:**
400
+ - `type` (Symbol) - Normalization type: `:l2` (default) or `:l1`
401
+
402
+ **Returns:** `Self` (for method chaining)
403
+
404
+ **Example:**
405
+ ```ruby
406
+ vector = Vectra::Vector.new(id: 'doc-1', values: embedding)
407
+ vector.normalize! # L2 normalization
408
+ client.upsert(vectors: [vector])
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Batch Operations
414
+
415
+ ### `Vectra::Batch.upsert(client:, index:, vectors:, batch_size: 100, namespace: nil, on_progress: nil)`
416
+
417
+ Upsert vectors in batches with progress callbacks.
418
+
419
+ **Parameters:**
420
+ - `client` (Vectra::Client) - Vectra client
421
+ - `index` (String) - Index/collection name
422
+ - `vectors` (Array<Hash, Vector>) - Vectors to upsert
423
+ - `batch_size` (Integer) - Batch size (default: 100)
424
+ - `namespace` (String, optional) - Namespace
425
+ - `on_progress` (Proc, optional) - Progress callback: `->(batch_index, total_batches, batch_count) { ... }`
426
+
427
+ **Returns:** `Hash` with `:upserted_count`
428
+
429
+ **Example:**
430
+ ```ruby
431
+ Vectra::Batch.upsert(
432
+ client: client,
433
+ index: 'products',
434
+ vectors: product_vectors,
435
+ batch_size: 100,
436
+ on_progress: ->(batch_index, total_batches, batch_count) do
437
+ puts "Batch #{batch_index + 1}/#{total_batches} (#{batch_count} vectors)"
438
+ end
439
+ )
440
+ ```
441
+
442
+ ---
443
+
444
+ ### `batch.query_async(index:, vectors:, top_k: 10, namespace: nil, filter: nil, include_values: false, include_metadata: true, chunk_size: 10, on_progress: nil)`
445
+
446
+ Query multiple vectors concurrently (useful for recommendation engines).
447
+
448
+ **Parameters:**
449
+ - `index` (String) - Index/collection name
450
+ - `vectors` (Array<Array<Float>>) - Array of query vectors
451
+ - `top_k` (Integer) - Number of results per query (default: 10)
452
+ - `namespace` (String, optional) - Namespace
453
+ - `filter` (Hash, optional) - Metadata filter
454
+ - `include_values` (Boolean) - Include vector values in results (default: false)
455
+ - `include_metadata` (Boolean) - Include metadata in results (default: true)
456
+ - `chunk_size` (Integer) - Queries per chunk for progress tracking (default: 10)
457
+ - `on_progress` (Proc, optional) - Progress callback: `->(stats) { ... }`
458
+
459
+ **Returns:** `Array<QueryResult>` - One QueryResult per input vector
460
+
461
+ **Example:**
462
+ ```ruby
463
+ batch = Vectra::Batch.new(client, concurrency: 4)
464
+
465
+ # Find similar items for multiple products
466
+ product_embeddings = products.map(&:embedding)
467
+ results = batch.query_async(
468
+ index: 'products',
469
+ vectors: product_embeddings,
470
+ top_k: 5,
471
+ on_progress: ->(stats) do
472
+ puts "Processed #{stats[:processed]}/#{stats[:total]} queries (#{stats[:percentage]}%)"
473
+ end
474
+ )
475
+
476
+ # Each result corresponds to one product
477
+ results.each_with_index do |result, i|
478
+ puts "Similar to product #{i}: #{result.ids}"
479
+ end
480
+ ```
481
+
482
+ ---
483
+
484
+ ## Query Builder (Chainable API)
485
+
486
+ ### `client.query(index)`
487
+
488
+ Start a chainable query builder.
489
+
490
+ **Returns:** `Vectra::QueryBuilder`
491
+
492
+ **Example:**
493
+ ```ruby
494
+ results = client
495
+ .query('documents')
496
+ .vector([0.1, 0.2, 0.3])
497
+ .top_k(10)
498
+ .filter(category: 'docs')
499
+ .with_metadata
500
+ .execute
501
+
502
+ results.each do |match|
503
+ puts "#{match.id}: #{match.score}"
504
+ end
505
+ ```
506
+
507
+ **QueryBuilder Methods:**
508
+ - `.vector(array)` - Set query vector
509
+ - `.top_k(integer)` - Set number of results
510
+ - `.filter(hash)` - Set metadata filter
511
+ - `.namespace(string)` - Set namespace
512
+ - `.with_metadata` - Include metadata in results
513
+ - `.with_values` - Include vector values in results
514
+ - `.execute` - Execute query and return QueryResult
515
+
516
+ ---
517
+
518
+ ## ActiveRecord Methods
519
+
520
+ ### `has_vector(column_name, options)`
521
+
522
+ Define vector search on an ActiveRecord model.
523
+
524
+ **Parameters:**
525
+ - `column_name` (Symbol) - Column name (e.g., `:embedding`)
526
+ - `provider` (Symbol) - Provider name (default: from global config)
527
+ - `index` (String) - Index/collection name
528
+ - `dimension` (Integer) - Vector dimension
529
+ - `auto_index` (Boolean) - Auto-index on save (default: true)
530
+ - `metadata_fields` (Array<Symbol>) - Fields to include in metadata
531
+
532
+ **Example:**
533
+ ```ruby
534
+ class Document < ApplicationRecord
535
+ include Vectra::ActiveRecord
536
+
537
+ has_vector :embedding,
538
+ provider: :qdrant,
539
+ index: 'documents',
540
+ dimension: 1536,
541
+ auto_index: true,
542
+ metadata_fields: [:title, :category]
543
+ end
544
+ ```
545
+
546
+ ---
547
+
548
+ ### `Model.vector_search(embedding:, limit: 10, filter: nil)`
549
+
550
+ Search for similar records using vector similarity.
551
+
552
+ **Parameters:**
553
+ - `embedding` (Array<Float>) - Query vector
554
+ - `limit` (Integer) - Number of results (default: 10)
555
+ - `filter` (Hash, optional) - Metadata filter
556
+
557
+ **Returns:** `ActiveRecord::Relation`
558
+
559
+ **Example:**
560
+ ```ruby
561
+ results = Document.vector_search(
562
+ embedding: query_embedding,
563
+ limit: 10,
564
+ filter: { category: 'docs' }
565
+ )
566
+
567
+ results.each do |doc|
568
+ puts doc.title
569
+ end
570
+ ```
571
+
572
+ ---
573
+
574
+ ### `Model.reindex_vectors(scope: all, batch_size: 1000, on_progress: nil)`
575
+
576
+ Reindex all records for a model into the configured vector index.
577
+
578
+ **Parameters:**
579
+ - `scope` (ActiveRecord::Relation) - Records to reindex (default: `Model.all`)
580
+ - `batch_size` (Integer) - Number of records per batch (default: 1000)
581
+ - `on_progress` (Proc, optional) - Progress callback, receives a hash with `:processed` and `:total`
582
+
583
+ **Returns:** `Integer` - Number of records processed
584
+
585
+ **Example:**
586
+ ```ruby
587
+ # Reindex all products with embeddings
588
+ processed = Product.reindex_vectors(
589
+ scope: Product.where.not(embedding: nil),
590
+ batch_size: 500
591
+ )
592
+
593
+ puts "Reindexed #{processed} products"
594
+ ```
595
+
596
+ ---
597
+
598
+ ## Error Handling
599
+
600
+ Vectra defines specific error types:
601
+
602
+ - `Vectra::Error` - Base error class
603
+ - `Vectra::NotFoundError` - Index or vector not found
604
+ - `Vectra::ValidationError` - Invalid parameters
605
+ - `Vectra::RateLimitError` - Rate limit exceeded
606
+ - `Vectra::ConnectionError` - Connection failed
607
+ - `Vectra::TimeoutError` - Request timeout
608
+ - `Vectra::AuthenticationError` - Authentication failed
609
+ - `Vectra::ConfigurationError` - Configuration error
610
+ - `Vectra::ProviderError` - Provider-specific error
611
+
612
+ **Example:**
613
+ ```ruby
614
+ begin
615
+ client.query(index: 'missing', vector: [0.1, 0.2, 0.3])
616
+ rescue Vectra::NotFoundError => e
617
+ Rails.logger.warn("Index not found: #{e.message}")
618
+ rescue Vectra::RateLimitError => e
619
+ Rails.logger.error("Rate limited: #{e.message}")
620
+ rescue Vectra::Error => e
621
+ Rails.logger.error("Vectra error: #{e.message}")
622
+ end
623
+ ```
624
+
625
+ ---
626
+
627
+ ## See Also
628
+
629
+ - [API Cheatsheet](/api/cheatsheet/) - Quick reference
630
+ - [API Overview](/api/overview/) - Overview and examples
631
+ - [Recipes & Patterns](/guides/recipes/) - Real-world examples
632
+ - [Rails Integration Guide](/guides/rails-integration/) - ActiveRecord integration
data/docs/api/overview.md CHANGED
@@ -215,4 +215,4 @@ rescue => e
215
215
  end
216
216
  ```
217
217
 
218
- See [Detailed API Documentation]({{ site.baseurl }}/api/methods) for more methods.
218
+ See [Complete API Methods Reference]({{ site.baseurl }}/api/methods/) for detailed method documentation.
@@ -1033,6 +1033,54 @@ code {
1033
1033
  }
1034
1034
  }
1035
1035
 
1036
+ /* Recipes Section */
1037
+ .tma-recipes {
1038
+ padding: var(--tma-spacing-3xl) var(--tma-spacing-xl);
1039
+ max-width: var(--tma-max-width);
1040
+ margin: 0 auto;
1041
+ }
1042
+
1043
+ .tma-recipes__grid {
1044
+ display: grid;
1045
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1046
+ gap: var(--tma-spacing-lg);
1047
+ margin-top: var(--tma-spacing-xl);
1048
+ }
1049
+
1050
+ .tma-recipe-card {
1051
+ display: flex;
1052
+ flex-direction: column;
1053
+ padding: var(--tma-spacing-xl);
1054
+ background: var(--tma-color-bg-secondary);
1055
+ border: 1px solid var(--tma-color-border);
1056
+ border-radius: var(--tma-radius-lg);
1057
+ text-decoration: none;
1058
+ color: inherit;
1059
+ transition: all var(--tma-transition-normal);
1060
+ border-left: 3px solid var(--tma-color-accent-primary);
1061
+ }
1062
+
1063
+ .tma-recipe-card:hover {
1064
+ border-color: var(--tma-color-border-hover);
1065
+ transform: translateY(-4px);
1066
+ box-shadow: var(--tma-shadow-glow);
1067
+ background: var(--tma-color-bg-tertiary);
1068
+ }
1069
+
1070
+ .tma-recipe-card__title {
1071
+ font-size: 1.2rem;
1072
+ font-weight: 700;
1073
+ margin-bottom: var(--tma-spacing-sm);
1074
+ color: var(--tma-color-text-primary);
1075
+ }
1076
+
1077
+ .tma-recipe-card__description {
1078
+ color: var(--tma-color-text-secondary);
1079
+ font-size: 0.9rem;
1080
+ line-height: 1.6;
1081
+ flex-grow: 1;
1082
+ }
1083
+
1036
1084
  /* Responsive Comparison Table */
1037
1085
  @media (max-width: 1024px) {
1038
1086
  .tma-comparison-table {
@@ -1056,7 +1104,8 @@ code {
1056
1104
  }
1057
1105
 
1058
1106
  .tma-advantages__grid,
1059
- .tma-choose__grid {
1107
+ .tma-choose__grid,
1108
+ .tma-recipes__grid {
1060
1109
  grid-template-columns: 1fr;
1061
1110
  }
1062
1111
  }
@@ -1,4 +1,3 @@
1
- ls
2
1
  ---
3
2
  layout: page
4
3
  title: Examples