@contentgrowth/content-widget 1.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +442 -0
  3. package/dist/astro/ContentList.astro +177 -0
  4. package/dist/astro/ContentViewer.astro +252 -0
  5. package/dist/astro/index.d.ts +9 -0
  6. package/dist/astro/index.d.ts.map +1 -0
  7. package/dist/astro/index.js +8 -0
  8. package/dist/core/client.d.ts +67 -0
  9. package/dist/core/client.d.ts.map +1 -0
  10. package/dist/core/client.js +217 -0
  11. package/dist/core/index.d.ts +8 -0
  12. package/dist/core/index.d.ts.map +1 -0
  13. package/dist/core/index.js +7 -0
  14. package/dist/core/utils.d.ts +32 -0
  15. package/dist/core/utils.d.ts.map +1 -0
  16. package/dist/core/utils.js +70 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +6 -0
  20. package/dist/react/ContentList.d.ts +12 -0
  21. package/dist/react/ContentList.d.ts.map +1 -0
  22. package/dist/react/ContentList.js +106 -0
  23. package/dist/react/ContentViewer.d.ts +12 -0
  24. package/dist/react/ContentViewer.d.ts.map +1 -0
  25. package/dist/react/ContentViewer.js +97 -0
  26. package/dist/react/hooks.d.ts +63 -0
  27. package/dist/react/hooks.d.ts.map +1 -0
  28. package/dist/react/hooks.js +140 -0
  29. package/dist/react/index.d.ts +9 -0
  30. package/dist/react/index.d.ts.map +1 -0
  31. package/dist/react/index.js +6 -0
  32. package/dist/styles.css +970 -0
  33. package/dist/types/index.d.ts +271 -0
  34. package/dist/types/index.d.ts.map +1 -0
  35. package/dist/types/index.js +16 -0
  36. package/dist/vue/ContentList.vue +166 -0
  37. package/dist/vue/ContentViewer.vue +137 -0
  38. package/dist/vue/composables.d.ts +64 -0
  39. package/dist/vue/composables.d.ts.map +1 -0
  40. package/dist/vue/composables.js +165 -0
  41. package/dist/vue/index.d.ts +10 -0
  42. package/dist/vue/index.d.ts.map +1 -0
  43. package/dist/vue/index.js +8 -0
  44. package/dist/widget/content-card.js +190 -0
  45. package/dist/widget/content-list.js +289 -0
  46. package/dist/widget/content-viewer.js +230 -0
  47. package/dist/widget/index.js +40 -0
  48. package/dist/widget/utils/api-client.js +154 -0
  49. package/dist/widget/utils/helpers.js +71 -0
  50. package/dist/widget/widget-js/content-card.js +190 -0
  51. package/dist/widget/widget-js/content-list.js +289 -0
  52. package/dist/widget/widget-js/content-viewer.js +230 -0
  53. package/dist/widget/widget-js/index.js +40 -0
  54. package/dist/widget/widget-js/utils/api-client.js +154 -0
  55. package/dist/widget/widget-js/utils/helpers.js +71 -0
  56. package/dist/widget/widget-js/widget.d.ts +24 -0
  57. package/dist/widget/widget-js/widget.js +240 -0
  58. package/dist/widget/widget.d.ts +24 -0
  59. package/dist/widget/widget.js +240 -0
  60. package/package.json +99 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Content Growth Content Widget - TypeScript Types
3
+ */
4
+ /**
5
+ * Article metadata
6
+ */
7
+ export interface Article {
8
+ uuid: string;
9
+ slug: string;
10
+ title: string;
11
+ category: string | null;
12
+ authorName: string;
13
+ publishedAt: number;
14
+ summary: string | null;
15
+ tags: string[];
16
+ wordCount: number;
17
+ }
18
+ /**
19
+ * Full article with content
20
+ */
21
+ export interface ArticleWithContent extends Article {
22
+ content: string;
23
+ }
24
+ /**
25
+ * Pagination metadata
26
+ */
27
+ export interface Pagination {
28
+ page: number;
29
+ limit: number;
30
+ total: number;
31
+ totalPages: number;
32
+ hasNext: boolean;
33
+ hasPrev: boolean;
34
+ }
35
+ /**
36
+ * Articles list response
37
+ */
38
+ export interface ArticlesResponse {
39
+ articles: Article[];
40
+ pagination: Pagination;
41
+ }
42
+ /**
43
+ * Category with count
44
+ */
45
+ export interface Category {
46
+ name: string;
47
+ count: number;
48
+ }
49
+ /**
50
+ * Tag with count
51
+ */
52
+ export interface Tag {
53
+ name: string;
54
+ normalized: string;
55
+ count: number;
56
+ }
57
+ /**
58
+ * Categories response
59
+ */
60
+ export interface CategoriesResponse {
61
+ categories: Category[];
62
+ }
63
+ /**
64
+ * Tags response
65
+ */
66
+ export interface TagsResponse {
67
+ tags: Tag[];
68
+ }
69
+ /**
70
+ * Client configuration
71
+ */
72
+ export interface ClientConfig {
73
+ /**
74
+ * Your Content Growth API key (pk_xxx)
75
+ */
76
+ apiKey: string;
77
+ /**
78
+ * API base URL
79
+ * @default 'https://api.content-growth.com'
80
+ */
81
+ baseUrl?: string;
82
+ /**
83
+ * Cache TTL in milliseconds
84
+ * @default 300000 (5 minutes)
85
+ */
86
+ cacheTTL?: number;
87
+ /**
88
+ * Enable debug logging
89
+ * @default false
90
+ */
91
+ debug?: boolean;
92
+ }
93
+ /**
94
+ * Options for listing articles
95
+ */
96
+ export interface ListArticlesOptions {
97
+ /**
98
+ * Page number (1-indexed)
99
+ * @default 1
100
+ */
101
+ page?: number;
102
+ /**
103
+ * Number of articles per page
104
+ * @default 12
105
+ */
106
+ limit?: number;
107
+ /**
108
+ * Filter by tags (array of tag names)
109
+ */
110
+ tags?: string[];
111
+ /**
112
+ * Filter by category
113
+ */
114
+ category?: string;
115
+ }
116
+ /**
117
+ * Layout mode for displaying articles
118
+ */
119
+ export type LayoutMode = 'cards' | 'rows';
120
+ /**
121
+ * Display density mode
122
+ */
123
+ export type DisplayMode = 'compact' | 'comfortable' | 'spacious';
124
+ /**
125
+ * Theme mode
126
+ */
127
+ export type Theme = 'light' | 'dark';
128
+ /**
129
+ * Component props for ContentList
130
+ */
131
+ export interface ContentListProps {
132
+ /**
133
+ * Your Content Growth API key
134
+ */
135
+ apiKey: string;
136
+ /**
137
+ * API base URL
138
+ * @default 'https://api.content-growth.com'
139
+ */
140
+ baseUrl?: string;
141
+ /**
142
+ * Layout mode
143
+ * @default 'cards'
144
+ */
145
+ layout?: LayoutMode;
146
+ /**
147
+ * Display density
148
+ * @default 'comfortable'
149
+ */
150
+ displayMode?: DisplayMode;
151
+ /**
152
+ * Theme
153
+ * @default 'light'
154
+ */
155
+ theme?: Theme;
156
+ /**
157
+ * Number of articles per page
158
+ * @default 12
159
+ */
160
+ pageSize?: number;
161
+ /**
162
+ * Filter by tags
163
+ */
164
+ tags?: string[];
165
+ /**
166
+ * Filter by category
167
+ */
168
+ category?: string;
169
+ /**
170
+ * Show pagination controls
171
+ * @default true
172
+ */
173
+ showPagination?: boolean;
174
+ /**
175
+ * URL pattern for article links
176
+ * Supports placeholders: {uuid}, {slug}, {category}
177
+ * @default '/articles/{uuid}'
178
+ * @example '/blog/{category}/{slug}'
179
+ */
180
+ linkPattern?: string;
181
+ /**
182
+ * Show article tags
183
+ * @default false
184
+ */
185
+ showTags?: boolean;
186
+ /**
187
+ * Show AI-generated summary in cards/rows
188
+ * @default true
189
+ */
190
+ showAiSummary?: boolean;
191
+ /**
192
+ * Maximum length of summary text in cards/rows (in characters)
193
+ * If not set, shows full summary
194
+ * @default undefined (no limit)
195
+ * @example 150
196
+ */
197
+ summaryMaxLength?: number;
198
+ /**
199
+ * Link target attribute
200
+ * Supports placeholders: {uuid}, {id} for article ID
201
+ * @default undefined (same tab)
202
+ * @example '_blank' for new tab
203
+ * @example '{uuid}' for article ID as target
204
+ */
205
+ linkTarget?: string;
206
+ /**
207
+ * Custom CSS class
208
+ */
209
+ class?: string;
210
+ }
211
+ /**
212
+ * Component props for ContentViewer
213
+ */
214
+ export interface ContentViewerProps {
215
+ /**
216
+ * Your Content Growth API key
217
+ */
218
+ apiKey: string;
219
+ /**
220
+ * Article UUID (use either uuid or slug)
221
+ */
222
+ uuid?: string;
223
+ /**
224
+ * Article slug (use either uuid or slug)
225
+ */
226
+ slug?: string;
227
+ /**
228
+ * API base URL
229
+ * @default 'https://api.content-growth.com'
230
+ */
231
+ baseUrl?: string;
232
+ /**
233
+ * Theme
234
+ * @default 'light'
235
+ */
236
+ theme?: Theme;
237
+ /**
238
+ * Show back button
239
+ * @default false
240
+ */
241
+ showBackButton?: boolean;
242
+ /**
243
+ * Back button URL
244
+ */
245
+ backUrl?: string;
246
+ /**
247
+ * Show AI-generated summary
248
+ * @default true
249
+ */
250
+ showAiSummary?: boolean;
251
+ /**
252
+ * Custom CSS class
253
+ */
254
+ class?: string;
255
+ }
256
+ /**
257
+ * Cache entry
258
+ */
259
+ export interface CacheEntry<T> {
260
+ data: T;
261
+ timestamp: number;
262
+ }
263
+ /**
264
+ * API Error
265
+ */
266
+ export declare class ContentGrowthError extends Error {
267
+ statusCode?: number | undefined;
268
+ response?: any | undefined;
269
+ constructor(message: string, statusCode?: number | undefined, response?: any | undefined);
270
+ }
271
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Content Growth Content Widget - TypeScript Types
3
+ */
4
+ /**
5
+ * API Error
6
+ */
7
+ export class ContentGrowthError extends Error {
8
+ statusCode;
9
+ response;
10
+ constructor(message, statusCode, response) {
11
+ super(message);
12
+ this.statusCode = statusCode;
13
+ this.response = response;
14
+ this.name = 'ContentGrowthError';
15
+ }
16
+ }
@@ -0,0 +1,166 @@
1
+ <template>
2
+ <div
3
+ :class="`cg-content-list cg-layout-${layout} cg-display-${displayMode} cg-theme-${theme} ${className}`"
4
+ data-cg-widget="list"
5
+ >
6
+ <div v-if="loading" class="cg-empty-state">
7
+ <p>Loading...</p>
8
+ </div>
9
+ <div v-else-if="articles.length === 0" class="cg-empty-state">
10
+ <p>No articles found.</p>
11
+ </div>
12
+ <template v-else>
13
+ <div :class="`cg-articles-grid ${layout === 'cards' ? 'cg-grid' : 'cg-list'}`">
14
+ <article v-for="article in articles" :key="article.uuid" class="cg-article-card">
15
+ <a :href="buildArticleUrl(article)" :target="buildLinkTarget(article)" class="cg-card-link">
16
+ <div class="cg-card-content">
17
+ <div v-if="article.category" class="cg-card-category">
18
+ <span class="cg-category-badge">{{ article.category }}</span>
19
+ </div>
20
+
21
+ <h2 class="cg-card-title">{{ article.title }}</h2>
22
+
23
+ <p v-if="showAiSummary && article.summary" class="cg-card-summary">
24
+ {{ truncateSummary(article.summary, summaryMaxLength) }}
25
+ </p>
26
+
27
+ <div class="cg-card-meta">
28
+ <span class="cg-meta-author">{{ article.authorName }}</span>
29
+ <span class="cg-meta-separator">•</span>
30
+ <time class="cg-meta-date" :datetime="new Date(article.publishedAt * 1000).toISOString()">
31
+ {{ formatDate(article.publishedAt) }}
32
+ </time>
33
+ <span class="cg-meta-separator">•</span>
34
+ <span class="cg-meta-reading-time">{{ calculateReadingTime(article.wordCount) }}</span>
35
+ </div>
36
+
37
+ <div v-if="showTags && article.tags && article.tags.length > 0" class="cg-card-tags">
38
+ <span v-for="tag in article.tags" :key="tag" class="cg-tag">{{ tag }}</span>
39
+ </div>
40
+ </div>
41
+ </a>
42
+ </article>
43
+ </div>
44
+
45
+ <div v-if="showPagination && totalPages > 1" class="cg-pagination">
46
+ <button
47
+ class="cg-pagination-btn"
48
+ @click="currentPage = Math.max(1, currentPage - 1)"
49
+ :disabled="currentPage === 1"
50
+ >
51
+ Previous
52
+ </button>
53
+
54
+ <span class="cg-pagination-info">
55
+ Page {{ currentPage }} of {{ totalPages }}
56
+ </span>
57
+
58
+ <button
59
+ class="cg-pagination-btn"
60
+ @click="currentPage = Math.min(totalPages, currentPage + 1)"
61
+ :disabled="currentPage === totalPages"
62
+ >
63
+ Next
64
+ </button>
65
+ </div>
66
+ </template>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { ref, watch, onMounted } from 'vue';
72
+ import { ContentGrowthClient } from '../core/client.js';
73
+ import { formatDate, calculateReadingTime } from '../core/utils.js';
74
+ import type { ContentListProps, Article } from '../types/index.js';
75
+
76
+ export interface VueContentListProps extends Omit<ContentListProps, 'class'> {
77
+ className?: string;
78
+ }
79
+
80
+ const props = withDefaults(defineProps<VueContentListProps>(), {
81
+ layout: 'cards',
82
+ displayMode: 'comfortable',
83
+ theme: 'light',
84
+ pageSize: 12,
85
+ tags: () => [],
86
+ showPagination: true,
87
+ linkPattern: '/articles/{uuid}',
88
+ showTags: false,
89
+ showAiSummary: true,
90
+ className: ''
91
+ });
92
+
93
+ const articles = ref<Article[]>([]);
94
+ const currentPage = ref(1);
95
+ const totalPages = ref(1);
96
+ const loading = ref(true);
97
+
98
+ const fetchArticles = async () => {
99
+ loading.value = true;
100
+ try {
101
+ const client = new ContentGrowthClient({
102
+ apiKey: props.apiKey,
103
+ baseUrl: props.baseUrl
104
+ });
105
+
106
+ // Process tags
107
+ let processedTags: string[] | undefined;
108
+ const tagsProp = props.tags as string[] | string | undefined;
109
+ if (tagsProp) {
110
+ if (Array.isArray(tagsProp)) {
111
+ processedTags = tagsProp;
112
+ } else if (typeof tagsProp === 'string') {
113
+ processedTags = tagsProp.split(',').map((t: string) => t.trim()).filter(Boolean);
114
+ }
115
+ }
116
+
117
+ const result = await client.listArticles({
118
+ page: currentPage.value,
119
+ limit: props.pageSize,
120
+ tags: processedTags,
121
+ category: props.category
122
+ });
123
+
124
+ articles.value = result.articles;
125
+ totalPages.value = result.pagination.totalPages;
126
+ } catch (error) {
127
+ console.error('Error fetching articles:', error);
128
+ } finally {
129
+ loading.value = false;
130
+ }
131
+ };
132
+
133
+ // Truncate summary text
134
+ const truncateSummary = (text: string | null, maxLength?: number): string => {
135
+ if (!text) return '';
136
+ if (!maxLength || text.length <= maxLength) return text;
137
+ return text.substring(0, maxLength).trim() + '...';
138
+ };
139
+
140
+ // Build article URL from pattern
141
+ const buildArticleUrl = (article: Article): string => {
142
+ return props.linkPattern
143
+ .replace('{uuid}', article.uuid)
144
+ .replace('{slug}', article.slug)
145
+ .replace('{category}', article.category || '');
146
+ };
147
+
148
+ // Build link target from pattern
149
+ const buildLinkTarget = (article: Article): string | undefined => {
150
+ if (!props.linkTarget) return undefined;
151
+ return props.linkTarget
152
+ .replace('{uuid}', article.uuid)
153
+ .replace('{id}', article.uuid);
154
+ };
155
+
156
+ onMounted(() => {
157
+ fetchArticles();
158
+ });
159
+
160
+ watch(
161
+ () => [props.apiKey, props.baseUrl, currentPage.value, props.pageSize, props.tags, props.category],
162
+ () => {
163
+ fetchArticles();
164
+ }
165
+ );
166
+ </script>
@@ -0,0 +1,137 @@
1
+ <template>
2
+ <div
3
+ :class="`cg-content-viewer cg-theme-${theme} ${className}`"
4
+ data-cg-widget="post"
5
+ >
6
+ <div v-if="loading" class="cg-empty-state">
7
+ <p>Loading...</p>
8
+ </div>
9
+ <div v-else-if="error || !article" class="cg-empty-state">
10
+ <p>{{ error || 'Article not found' }}</p>
11
+ </div>
12
+ <article v-else>
13
+ <div v-if="showBackButton" class="cg-post-header">
14
+ <a :href="backUrl" class="cg-back-btn">
15
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
16
+ <path d="M12 16L6 10L12 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
17
+ </svg>
18
+ Back to articles
19
+ </a>
20
+ </div>
21
+
22
+ <header class="cg-post-meta">
23
+ <div v-if="article.category" class="cg-post-category">
24
+ <span class="cg-category-badge">{{ article.category }}</span>
25
+ </div>
26
+
27
+ <h1 class="cg-post-title">{{ article.title }}</h1>
28
+
29
+ <div v-if="showAiSummary && article.summary" class="cg-ai-summary">
30
+ <div class="cg-ai-summary-header">
31
+ <svg class="cg-ai-summary-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
32
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
33
+ </svg>
34
+ <span class="cg-ai-summary-label">AI Generated Summary</span>
35
+ </div>
36
+ <p class="cg-ai-summary-text">{{ article.summary }}</p>
37
+ </div>
38
+
39
+ <div class="cg-post-info">
40
+ <span class="cg-info-author">{{ article.authorName }}</span>
41
+ <span class="cg-info-separator">•</span>
42
+ <time class="cg-info-date" :datetime="new Date(article.publishedAt * 1000).toISOString()">
43
+ {{ formatDate(article.publishedAt) }}
44
+ </time>
45
+ <span class="cg-info-separator">•</span>
46
+ <span class="cg-info-reading-time">{{ calculateReadingTime(article.wordCount) }}</span>
47
+ </div>
48
+
49
+ <div v-if="article.tags.length > 0" class="cg-post-tags">
50
+ <span v-for="tag in article.tags" :key="tag" class="cg-tag">{{ tag }}</span>
51
+ </div>
52
+ </header>
53
+
54
+ <div class="cg-post-content" v-html="contentHtml"></div>
55
+ </article>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import { ref, onMounted, computed } from 'vue';
61
+ import { ContentGrowthClient } from '../core/client.js';
62
+ import { formatDate, calculateReadingTime } from '../core/utils.js';
63
+ import { marked } from 'marked';
64
+ import type { ContentViewerProps, ArticleWithContent } from '../types/index.js';
65
+
66
+ export interface VueContentViewerProps extends Omit<ContentViewerProps, 'class'> {
67
+ className?: string;
68
+ }
69
+
70
+ const props = withDefaults(defineProps<VueContentViewerProps>(), {
71
+ theme: 'light',
72
+ showBackButton: false,
73
+ backUrl: '/articles',
74
+ showAiSummary: true,
75
+ className: ''
76
+ });
77
+
78
+ const article = ref<ArticleWithContent | null>(null);
79
+ const loading = ref(true);
80
+ const error = ref<string | null>(null);
81
+
82
+ // Process markdown content to handle custom image syntax
83
+ const processImageSyntax = (markdown: string): string => {
84
+ // Match: ![alt](url =widthxheight)
85
+ return markdown.replace(
86
+ /!\[([^\]]*)\]\(([^\s)]+)\s+=(\d+)x(\d+)\)/g,
87
+ (match, alt, url, width, height) => {
88
+ return `![${alt}](${url}){width="${width}" height="${height}"}`;
89
+ }
90
+ );
91
+ };
92
+
93
+ // Configure marked to handle image attributes
94
+ marked.use({
95
+ renderer: {
96
+ image(href, title, text) {
97
+ // Extract width/height from {width="x" height="y"} syntax
98
+ const attrMatch = text.match(/\{width="(\d+)"\s+height="(\d+)"\}/);
99
+ if (attrMatch) {
100
+ const cleanText = text.replace(/\{[^}]+\}/, '').trim();
101
+ return `<img src="${href}" alt="${cleanText}" width="${attrMatch[1]}" height="${attrMatch[2]}" ${title ? `title="${title}"` : ''} />`;
102
+ }
103
+ return `<img src="${href}" alt="${text}" ${title ? `title="${title}"` : ''} />`;
104
+ }
105
+ }
106
+ });
107
+
108
+ const contentHtml = computed(() => {
109
+ if (!article.value) return '';
110
+ const processedContent = processImageSyntax(article.value.content);
111
+ return marked(processedContent);
112
+ });
113
+
114
+ onMounted(async () => {
115
+ if (!props.uuid && !props.slug) {
116
+ error.value = 'Either uuid or slug must be provided';
117
+ loading.value = false;
118
+ return;
119
+ }
120
+
121
+ loading.value = true;
122
+ try {
123
+ const client = new ContentGrowthClient({
124
+ apiKey: props.apiKey,
125
+ baseUrl: props.baseUrl
126
+ });
127
+ const fetchedArticle = props.slug
128
+ ? await client.getArticleBySlug(props.slug)
129
+ : await client.getArticle(props.uuid!);
130
+ article.value = fetchedArticle;
131
+ } catch (err) {
132
+ error.value = err instanceof Error ? err.message : 'Failed to load article';
133
+ } finally {
134
+ loading.value = false;
135
+ }
136
+ });
137
+ </script>
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Vue Composables for Content Growth API
3
+ */
4
+ import { type Ref } from 'vue';
5
+ import type { Article, ArticleWithContent, Pagination, Category, Tag, ListArticlesOptions } from '../types/index.js';
6
+ export interface UseArticlesOptions extends ListArticlesOptions {
7
+ apiKey: string;
8
+ baseUrl?: string;
9
+ }
10
+ export interface UseArticlesResult {
11
+ articles: Ref<Article[]>;
12
+ pagination: Ref<Pagination | null>;
13
+ loading: Ref<boolean>;
14
+ error: Ref<Error | null>;
15
+ refetch: () => Promise<void>;
16
+ }
17
+ /**
18
+ * Composable to fetch articles list
19
+ */
20
+ export declare function useArticles(options: UseArticlesOptions): UseArticlesResult;
21
+ export interface UseArticleOptions {
22
+ apiKey: string;
23
+ uuid: string;
24
+ baseUrl?: string;
25
+ }
26
+ export interface UseArticleResult {
27
+ article: Ref<ArticleWithContent | null>;
28
+ loading: Ref<boolean>;
29
+ error: Ref<Error | null>;
30
+ refetch: () => Promise<void>;
31
+ }
32
+ /**
33
+ * Composable to fetch a single article
34
+ */
35
+ export declare function useArticle(options: UseArticleOptions): UseArticleResult;
36
+ export interface UseCategoriesOptions {
37
+ apiKey: string;
38
+ baseUrl?: string;
39
+ }
40
+ export interface UseCategoriesResult {
41
+ categories: Ref<Category[]>;
42
+ loading: Ref<boolean>;
43
+ error: Ref<Error | null>;
44
+ refetch: () => Promise<void>;
45
+ }
46
+ /**
47
+ * Composable to fetch categories
48
+ */
49
+ export declare function useCategories(options: UseCategoriesOptions): UseCategoriesResult;
50
+ export interface UseTagsOptions {
51
+ apiKey: string;
52
+ baseUrl?: string;
53
+ }
54
+ export interface UseTagsResult {
55
+ tags: Ref<Tag[]>;
56
+ loading: Ref<boolean>;
57
+ error: Ref<Error | null>;
58
+ refetch: () => Promise<void>;
59
+ }
60
+ /**
61
+ * Composable to fetch tags
62
+ */
63
+ export declare function useTags(options: UseTagsOptions): UseTagsResult;
64
+ //# sourceMappingURL=composables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composables.d.ts","sourceRoot":"","sources":["../../src/vue/composables.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAc,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AAE3C,OAAO,KAAK,EACV,OAAO,EACP,kBAAkB,EAClB,UAAU,EACV,QAAQ,EACR,GAAG,EACH,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IACzB,UAAU,EAAE,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB,CAgD1E;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,GAAG,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACxC,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAwCvE;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5B,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CAwChF;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,aAAa,CAwC9D"}