@verbatims/sdk 1.4.1 → 1.6.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.
@@ -6,6 +6,7 @@ import { ReferencesResource } from '../references'
6
6
  import { TagsResource } from '../tags'
7
7
  import { CollectionsResource } from '../collections'
8
8
  import { SearchResource } from '../search'
9
+ import { ThemesResource } from '../themes'
9
10
 
10
11
  function mockResponse(data: unknown, status = 200) {
11
12
  return new Response(JSON.stringify(data), {
@@ -338,6 +339,205 @@ describe('CollectionsResource', () => {
338
339
  })
339
340
  })
340
341
 
342
+ describe('ThemesResource', () => {
343
+ let fetchFn: ReturnType<typeof vi.fn>
344
+ let themes: ThemesResource
345
+
346
+ beforeEach(() => {
347
+ const mock = createMockClient()
348
+ fetchFn = mock.fetchFn
349
+ themes = new ThemesResource(mock.client)
350
+ })
351
+
352
+ it('getActive calls GET /themes/active', async () => {
353
+ fetchFn.mockResolvedValue(mockResponse({
354
+ success: true,
355
+ data: { id: 1, slug: 'summer', name: 'Summer', description: null, language: 'en', isActive: true, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 },
356
+ }))
357
+
358
+ const result = await themes.getActive({ language: 'en' })
359
+ const [url] = fetchFn.mock.calls[0]
360
+ expect(url).toContain('/themes/active')
361
+ const parsed = new URL(url, 'http://localhost')
362
+ expect(parsed.searchParams.get('language')).toBe('en')
363
+ expect(result.data?.slug).toBe('summer')
364
+ })
365
+
366
+ it('getFeed calls GET /themes/:slug/feed', async () => {
367
+ fetchFn.mockResolvedValue(mockResponse({
368
+ success: true,
369
+ data: { theme: { id: 1, slug: 'summer', name: 'Summer', description: null, language: 'en', isActive: true, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 }, quotes: [], authors: [], references: [], total: 0 },
370
+ }))
371
+
372
+ await themes.getFeed('summer', { language: 'fr' })
373
+ const [url] = fetchFn.mock.calls[0]
374
+ expect(url).toContain('/themes/summer/feed')
375
+ const parsed = new URL(url, 'http://localhost')
376
+ expect(parsed.searchParams.get('language')).toBe('fr')
377
+ })
378
+
379
+ it('list calls GET /themes with params', async () => {
380
+ fetchFn.mockResolvedValue(mockResponse({
381
+ success: true,
382
+ data: [{ id: 1, slug: 'summer', name: 'Summer', description: null, language: 'en', isActive: true, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 }],
383
+ pagination: { page: 1, limit: 10, total: 1, totalPages: 1, hasMore: false },
384
+ }))
385
+
386
+ const result = await themes.list({ search: 'summer' })
387
+ const [url] = fetchFn.mock.calls[0]
388
+ expect(url).toContain('/themes')
389
+ const parsed = new URL(url, 'http://localhost')
390
+ expect(parsed.searchParams.get('search')).toBe('summer')
391
+ expect(result.data).toHaveLength(1)
392
+ })
393
+
394
+ it('paginate yields themes across pages', async () => {
395
+ fetchFn
396
+ .mockResolvedValueOnce(mockResponse({
397
+ success: true,
398
+ data: [{ id: 1, slug: 'a', name: 'A', description: null, language: null, isActive: false, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 }],
399
+ pagination: { page: 1, limit: 1, total: 2, totalPages: 2, hasMore: true },
400
+ }))
401
+ .mockResolvedValueOnce(mockResponse({
402
+ success: true,
403
+ data: [{ id: 2, slug: 'b', name: 'B', description: null, language: null, isActive: false, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 }],
404
+ pagination: { page: 2, limit: 1, total: 2, totalPages: 2, hasMore: false },
405
+ }))
406
+
407
+ const results = []
408
+ for await (const item of themes.paginate()) {
409
+ results.push(item)
410
+ }
411
+ expect(results).toHaveLength(2)
412
+ })
413
+
414
+ it('get calls GET /themes/:id', async () => {
415
+ fetchFn.mockResolvedValue(mockResponse({
416
+ success: true,
417
+ data: { id: 5, slug: 'test', name: 'Test', description: null, language: null, isActive: false, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0, filters: [], translations: [] },
418
+ }))
419
+
420
+ await themes.get(5)
421
+ const [url] = fetchFn.mock.calls[0]
422
+ expect(url).toContain('/themes/5')
423
+ })
424
+
425
+ it('create calls POST /themes', async () => {
426
+ fetchFn.mockResolvedValue(mockResponse({
427
+ success: true,
428
+ data: { id: 1, slug: 'new-theme', name: 'New Theme', description: null, language: 'en', isActive: false, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 },
429
+ }))
430
+
431
+ await themes.create({ slug: 'new-theme', name: 'New Theme', language: 'en' })
432
+ const [, opts] = fetchFn.mock.calls[0]
433
+ expect(opts.method).toBe('POST')
434
+ expect(JSON.parse(opts.body).slug).toBe('new-theme')
435
+ })
436
+
437
+ it('update calls PUT /themes/:id', async () => {
438
+ fetchFn.mockResolvedValue(mockResponse({
439
+ success: true,
440
+ data: { id: 1, slug: 'updated', name: 'Updated', description: null, language: 'en', isActive: false, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 5, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 },
441
+ }))
442
+
443
+ await themes.update(1, { priority: 5 })
444
+ const [, opts] = fetchFn.mock.calls[0]
445
+ expect(opts.method).toBe('PUT')
446
+ })
447
+
448
+ it('delete calls DELETE /themes/:id', async () => {
449
+ fetchFn.mockResolvedValue(mockResponse({ success: true }))
450
+ await themes.delete(1)
451
+ const [url, opts] = fetchFn.mock.calls[0]
452
+ expect(url).toContain('/themes/1')
453
+ expect(opts.method).toBe('DELETE')
454
+ })
455
+
456
+ it('activate calls PUT /themes/:id/activate', async () => {
457
+ fetchFn.mockResolvedValue(mockResponse({
458
+ success: true,
459
+ data: { id: 1, slug: 'test', name: 'Test', description: null, language: null, isActive: true, isDefault: false, scheduledStart: null, scheduledEnd: null, priority: 0, config: null, createdAt: '2024-01-01', updatedAt: '2024-01-01', filters_count: 0 },
460
+ }))
461
+
462
+ await themes.activate(1, true)
463
+ const [, opts] = fetchFn.mock.calls[0]
464
+ expect(opts.method).toBe('PUT')
465
+ expect(JSON.parse(opts.body)).toEqual({ is_active: true })
466
+ })
467
+
468
+ it('addFilter calls POST /themes/:id/filters', async () => {
469
+ fetchFn.mockResolvedValue(mockResponse({
470
+ success: true,
471
+ data: { id: 10, themeId: 1, type: 'keyword', value: 'test', matchMode: 'any' },
472
+ }))
473
+
474
+ await themes.addFilter(1, { type: 'keyword', value: 'test' })
475
+ const [, opts] = fetchFn.mock.calls[0]
476
+ expect(opts.method).toBe('POST')
477
+ expect(JSON.parse(opts.body)).toEqual({ type: 'keyword', value: 'test' })
478
+ })
479
+
480
+ it('removeFilter calls DELETE /themes/:id/filters/:fid', async () => {
481
+ fetchFn.mockResolvedValue(mockResponse({ success: true }))
482
+ await themes.removeFilter(1, 5)
483
+ const [url, opts] = fetchFn.mock.calls[0]
484
+ expect(url).toContain('/themes/1/filters/5')
485
+ expect(opts.method).toBe('DELETE')
486
+ })
487
+
488
+ it('filterSuggestions calls GET /themes/filter-suggestions', async () => {
489
+ fetchFn.mockResolvedValue(mockResponse({
490
+ success: true,
491
+ data: [{ label: 'Test Tag', value: 'test' }],
492
+ }))
493
+
494
+ await themes.filterSuggestions({ q: 'test', type: 'tag_name' })
495
+ const [url] = fetchFn.mock.calls[0]
496
+ expect(url).toContain('/themes/filter-suggestions')
497
+ const parsed = new URL(url, 'http://localhost')
498
+ expect(parsed.searchParams.get('q')).toBe('test')
499
+ expect(parsed.searchParams.get('type')).toBe('tag_name')
500
+ })
501
+
502
+ it('filterRecommendations calls POST /themes/filter-recommendations', async () => {
503
+ fetchFn.mockResolvedValue(mockResponse({
504
+ success: true,
505
+ data: [{ type: 'tag', value: 'wisdom', label: 'Wisdom' }],
506
+ }))
507
+
508
+ await themes.filterRecommendations({ name: 'Summer', filters: [{ type: 'keyword', value: 'sun' }] })
509
+ const [, opts] = fetchFn.mock.calls[0]
510
+ expect(opts.method).toBe('POST')
511
+ expect(JSON.parse(opts.body).name).toBe('Summer')
512
+ })
513
+
514
+ it('suggestName calls POST /themes/suggest-name', async () => {
515
+ fetchFn.mockResolvedValue(mockResponse({
516
+ success: true,
517
+ data: { name: 'Summer Vibes', slug: 'summer-vibes', description: 'A summer theme' },
518
+ }))
519
+
520
+ await themes.suggestName('summer')
521
+ const [, opts] = fetchFn.mock.calls[0]
522
+ expect(opts.method).toBe('POST')
523
+ expect(JSON.parse(opts.body).name).toBe('summer')
524
+ })
525
+
526
+ it('suggestions calls GET /themes/suggestions', async () => {
527
+ fetchFn.mockResolvedValue(mockResponse({
528
+ success: true,
529
+ data: [{ type: 'tag', name: 'Summer', slug: 'summer', description: 'Summer theme', color_primary: '#FF0000', color_secondary: '#00FF00', filters: [{ type: 'keyword', value: 'summer', match_mode: 'any' }] }],
530
+ }))
531
+
532
+ await themes.suggestions({ ai: true, tags: 'summer,beach' })
533
+ const [url] = fetchFn.mock.calls[0]
534
+ expect(url).toContain('/themes/suggestions')
535
+ const parsed = new URL(url, 'http://localhost')
536
+ expect(parsed.searchParams.get('ai')).toBe('true')
537
+ expect(parsed.searchParams.get('tags')).toBe('summer,beach')
538
+ })
539
+ })
540
+
341
541
  describe('SearchResource', () => {
342
542
  let fetchFn: ReturnType<typeof vi.fn>
343
543
  let search: SearchResource
@@ -4,3 +4,4 @@ export { ReferencesResource } from './references'
4
4
  export { TagsResource } from './tags'
5
5
  export { CollectionsResource } from './collections'
6
6
  export { SearchResource } from './search'
7
+ export { ThemesResource } from './themes'
@@ -0,0 +1,214 @@
1
+ import { z } from 'zod/v4'
2
+ import type { VerbatimsClient } from '../client'
3
+ import { apiResponseSchema } from '../types'
4
+ import { paginate } from '../pagination'
5
+ import type {
6
+ Theme,
7
+ ThemeFilter,
8
+ ThemeSuggestion,
9
+ ThemeFeed,
10
+ ThemeWithDetails,
11
+ ListThemesParams,
12
+ CreateThemeData,
13
+ UpdateThemeData,
14
+ AddFilterData,
15
+ FilterSuggestion,
16
+ FilterRecommendationsData,
17
+ FilterRecommendation,
18
+ ThemeNameSuggestion,
19
+ ThemeSuggestionItem,
20
+ ActiveThemeParams,
21
+ FeedParams,
22
+ ThemeSuggestionsQuery,
23
+ } from '../types'
24
+
25
+ const themeSchema = z.object({
26
+ id: z.number(),
27
+ slug: z.string(),
28
+ name: z.string(),
29
+ description: z.string().nullable(),
30
+ language: z.string().nullable(),
31
+ isActive: z.boolean(),
32
+ isDefault: z.boolean(),
33
+ scheduledStart: z.string().nullable(),
34
+ scheduledEnd: z.string().nullable(),
35
+ priority: z.number(),
36
+ config: z.record(z.string(), z.unknown()).nullable(),
37
+ createdAt: z.string(),
38
+ updatedAt: z.string(),
39
+ filters_count: z.number(),
40
+ pending_suggestions_count: z.number().optional(),
41
+ })
42
+
43
+ const themeListResponseSchema = apiResponseSchema(z.array(themeSchema))
44
+ const themeSingleResponseSchema = apiResponseSchema(themeSchema)
45
+
46
+ const themeFilterSchema = z.object({
47
+ id: z.number(),
48
+ themeId: z.number(),
49
+ type: z.string(),
50
+ value: z.string(),
51
+ matchMode: z.enum(['any', 'all']),
52
+ })
53
+
54
+ const themeSuggestionSchema = z.object({
55
+ id: z.number(),
56
+ themeId: z.number(),
57
+ enrichmentJobId: z.number().nullable(),
58
+ type: z.enum(['tag', 'author', 'reference']),
59
+ suggestedValue: z.string(),
60
+ context: z.string().nullable(),
61
+ status: z.enum(['pending', 'accepted', 'rejected']),
62
+ createdBy: z.number().nullable(),
63
+ reviewedBy: z.number().nullable(),
64
+ reviewedAt: z.string().nullable(),
65
+ createdAt: z.string(),
66
+ updatedAt: z.string(),
67
+ })
68
+
69
+ const themeFeedSchema = z.object({
70
+ theme: themeSchema,
71
+ quotes: z.array(z.object({
72
+ id: z.number(),
73
+ name: z.string(),
74
+ language: z.string(),
75
+ created_at: z.string(),
76
+ views_count: z.number(),
77
+ likes_count: z.number(),
78
+ updated_at: z.string(),
79
+ author: z.object({ id: z.number(), name: z.string() }).optional(),
80
+ reference: z.object({ id: z.number(), name: z.string() }).optional(),
81
+ })),
82
+ authors: z.array(z.object({
83
+ id: z.number(),
84
+ name: z.string(),
85
+ job: z.string(),
86
+ description: z.string(),
87
+ image_url: z.string(),
88
+ likes_count: z.number(),
89
+ })),
90
+ references: z.array(z.object({
91
+ id: z.number(),
92
+ name: z.string(),
93
+ description: z.string(),
94
+ primary_type: z.string(),
95
+ image_url: z.string(),
96
+ likes_count: z.number(),
97
+ })),
98
+ total: z.number(),
99
+ })
100
+
101
+ type ThemeItem = z.infer<typeof themeSchema>
102
+
103
+ export class ThemesResource {
104
+ constructor(private client: VerbatimsClient) {}
105
+
106
+ async getActive(params?: ActiveThemeParams) {
107
+ return this.client.get('/themes/active', { params: params as Record<string, unknown> }, apiResponseSchema(themeSchema.nullable()))
108
+ }
109
+
110
+ async getFeed(slug: string, params?: FeedParams) {
111
+ return this.client.get(`/themes/${slug}/feed`, { params: params as Record<string, unknown> }, apiResponseSchema(themeFeedSchema.nullable()))
112
+ }
113
+
114
+ async list(params?: ListThemesParams) {
115
+ return this.client.get('/themes', { params: params as Record<string, unknown> }, themeListResponseSchema)
116
+ }
117
+
118
+ paginate(params?: ListThemesParams): AsyncGenerator<ThemeItem> {
119
+ return paginate<ThemeItem>((page) =>
120
+ this.list({ ...params, page }).then(r => ({
121
+ data: r.data,
122
+ pagination: r.pagination,
123
+ }))
124
+ )
125
+ }
126
+
127
+ async get(id: number) {
128
+ return this.client.get(`/themes/${id}`, {}, apiResponseSchema(themeSchema.and(z.object({
129
+ filters: z.array(themeFilterSchema),
130
+ translations: z.array(z.object({
131
+ id: z.number(),
132
+ themeId: z.number(),
133
+ language: z.string(),
134
+ name: z.string(),
135
+ description: z.string().nullable(),
136
+ })),
137
+ }))))
138
+ }
139
+
140
+ async create(data: CreateThemeData) {
141
+ return this.client.post('/themes', data, {}, themeSingleResponseSchema)
142
+ }
143
+
144
+ async update(id: number, data: UpdateThemeData) {
145
+ return this.client.put(`/themes/${id}`, data, {}, themeSingleResponseSchema)
146
+ }
147
+
148
+ async delete(id: number) {
149
+ return this.client.delete(`/themes/${id}`, {}, apiResponseSchema(z.undefined()))
150
+ }
151
+
152
+ async activate(id: number, isActive: boolean) {
153
+ return this.client.put(`/themes/${id}/activate`, { is_active: isActive }, {}, themeSingleResponseSchema)
154
+ }
155
+
156
+ async setDefault(id: number, isDefault: boolean) {
157
+ return this.client.put(`/themes/${id}/default`, { is_default: isDefault }, {}, themeSingleResponseSchema)
158
+ }
159
+
160
+ async addFilter(id: number, data: AddFilterData) {
161
+ return this.client.post(`/themes/${id}/filters`, data, {}, apiResponseSchema(themeFilterSchema))
162
+ }
163
+
164
+ async removeFilter(id: number, filterId: number) {
165
+ return this.client.delete(`/themes/${id}/filters/${filterId}`, {}, apiResponseSchema(z.undefined()))
166
+ }
167
+
168
+ async listSuggestions(id: number) {
169
+ return this.client.get(`/themes/${id}/suggestions`, {}, apiResponseSchema(z.array(themeSuggestionSchema)))
170
+ }
171
+
172
+ async reviewSuggestion(id: number, suggestionId: number, action: 'accepted' | 'rejected') {
173
+ return this.client.put(`/themes/${id}/suggestions/${suggestionId}`, { action }, {}, apiResponseSchema(themeSuggestionSchema))
174
+ }
175
+
176
+ async filterSuggestions(params: { q: string; type: string }) {
177
+ return this.client.get('/themes/filter-suggestions', { params: params as Record<string, unknown> }, apiResponseSchema(z.array(z.object({
178
+ label: z.string(),
179
+ value: z.string(),
180
+ }))))
181
+ }
182
+
183
+ async filterRecommendations(data: FilterRecommendationsData) {
184
+ return this.client.post('/themes/filter-recommendations', data, {}, apiResponseSchema(z.array(z.object({
185
+ type: z.string(),
186
+ value: z.string(),
187
+ label: z.string(),
188
+ }))))
189
+ }
190
+
191
+ async suggestName(name: string) {
192
+ return this.client.post('/themes/suggest-name', { name }, {}, apiResponseSchema(z.object({
193
+ name: z.string(),
194
+ slug: z.string(),
195
+ description: z.string(),
196
+ })))
197
+ }
198
+
199
+ async suggestions(params?: ThemeSuggestionsQuery) {
200
+ return this.client.get('/themes/suggestions', { params: params as Record<string, unknown> }, apiResponseSchema(z.array(z.object({
201
+ type: z.enum(['tag', 'author', 'reference']),
202
+ name: z.string(),
203
+ slug: z.string(),
204
+ description: z.string(),
205
+ color_primary: z.string(),
206
+ color_secondary: z.string(),
207
+ filters: z.array(z.object({
208
+ type: z.string(),
209
+ value: z.string(),
210
+ match_mode: z.literal('any'),
211
+ })),
212
+ }))))
213
+ }
214
+ }
package/src/types.ts CHANGED
@@ -235,3 +235,186 @@ export interface CreateCollectionData {
235
235
  description?: string | null
236
236
  is_public?: boolean
237
237
  }
238
+
239
+ // --- Theme types ---
240
+
241
+ export interface Theme {
242
+ id: number
243
+ slug: string
244
+ name: string
245
+ description: string | null
246
+ language: string | null
247
+ isActive: boolean
248
+ isDefault: boolean
249
+ scheduledStart: string | null
250
+ scheduledEnd: string | null
251
+ priority: number
252
+ config: Record<string, unknown> | null
253
+ createdAt: string
254
+ updatedAt: string
255
+ filters_count: number
256
+ pending_suggestions_count?: number
257
+ }
258
+
259
+ export interface ThemeFilter {
260
+ id: number
261
+ themeId: number
262
+ type: string
263
+ value: string
264
+ matchMode: 'any' | 'all'
265
+ }
266
+
267
+ export interface ThemeTranslation {
268
+ id: number
269
+ themeId: number
270
+ language: string
271
+ name: string
272
+ description: string | null
273
+ }
274
+
275
+ export interface ThemeSuggestion {
276
+ id: number
277
+ themeId: number
278
+ enrichmentJobId: number | null
279
+ type: 'tag' | 'author' | 'reference'
280
+ suggestedValue: string
281
+ context: string | null
282
+ status: 'pending' | 'accepted' | 'rejected'
283
+ createdBy: number | null
284
+ reviewedBy: number | null
285
+ reviewedAt: string | null
286
+ createdAt: string
287
+ updatedAt: string
288
+ }
289
+
290
+ export interface ThemeFeedQuote {
291
+ id: number
292
+ name: string
293
+ language: string
294
+ created_at: string
295
+ views_count: number
296
+ likes_count: number
297
+ updated_at: string
298
+ author?: { id: number; name: string }
299
+ reference?: { id: number; name: string }
300
+ }
301
+
302
+ export interface ThemeFeedAuthor {
303
+ id: number
304
+ name: string
305
+ job: string
306
+ description: string
307
+ image_url: string
308
+ likes_count: number
309
+ }
310
+
311
+ export interface ThemeFeedReference {
312
+ id: number
313
+ name: string
314
+ description: string
315
+ primary_type: string
316
+ image_url: string
317
+ likes_count: number
318
+ }
319
+
320
+ export interface ThemeFeed {
321
+ theme: Theme
322
+ quotes: ThemeFeedQuote[]
323
+ authors: ThemeFeedAuthor[]
324
+ references: ThemeFeedReference[]
325
+ total: number
326
+ }
327
+
328
+ export interface ThemeWithDetails extends Theme {
329
+ filters: ThemeFilter[]
330
+ translations: ThemeTranslation[]
331
+ }
332
+
333
+ export interface ListThemesParams {
334
+ page?: number
335
+ limit?: number
336
+ search?: string
337
+ sort_by?: 'priority' | 'name' | 'slug'
338
+ sort_order?: 'asc' | 'desc'
339
+ }
340
+
341
+ export interface CreateThemeData {
342
+ slug: string
343
+ name: string
344
+ description?: string | null
345
+ language?: string | null
346
+ translations?: Array<{ language: string; name: string; description?: string | null }>
347
+ is_active?: boolean
348
+ is_default?: boolean
349
+ priority?: number
350
+ scheduled_start?: string | null
351
+ scheduled_end?: string | null
352
+ config?: Record<string, unknown> | null
353
+ }
354
+
355
+ export interface UpdateThemeData {
356
+ slug?: string
357
+ name?: string
358
+ description?: string | null
359
+ language?: string | null
360
+ is_active?: boolean
361
+ is_default?: boolean
362
+ priority?: number
363
+ scheduled_start?: string | null
364
+ scheduled_end?: string | null
365
+ config?: Record<string, unknown> | string | null
366
+ translations?: Array<{ language: string; name: string; description?: string | null }>
367
+ }
368
+
369
+ export interface AddFilterData {
370
+ type: string
371
+ value: string
372
+ match_mode?: 'any' | 'all'
373
+ }
374
+
375
+ export interface FilterSuggestion {
376
+ label: string
377
+ value: string
378
+ }
379
+
380
+ export interface FilterRecommendationsData {
381
+ name?: string
382
+ filters?: Array<{ type: string; value: string }>
383
+ }
384
+
385
+ export interface FilterRecommendation {
386
+ type: string
387
+ value: string
388
+ label: string
389
+ }
390
+
391
+ export interface ThemeNameSuggestion {
392
+ name: string
393
+ slug: string
394
+ description: string
395
+ }
396
+
397
+ export interface ThemeSuggestionItem {
398
+ type: 'tag' | 'author' | 'reference'
399
+ name: string
400
+ slug: string
401
+ description: string
402
+ color_primary: string
403
+ color_secondary: string
404
+ filters: Array<{ type: string; value: string; match_mode: 'any' }>
405
+ }
406
+
407
+ export interface ActiveThemeParams {
408
+ language?: string
409
+ theme?: string
410
+ }
411
+
412
+ export interface FeedParams {
413
+ language?: string
414
+ }
415
+
416
+ export interface ThemeSuggestionsQuery {
417
+ ai?: boolean
418
+ tags?: string
419
+ language?: string
420
+ }