@contractspec/bundle.marketing 3.8.9 → 3.8.10
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.
- package/.turbo/turbo-build.log +54 -42
- package/CHANGELOG.md +33 -0
- package/dist/browser/components/templates/TemplatesBrowseControls.js +37 -22
- package/dist/browser/components/templates/TemplatesCatalogSection.js +29 -6
- package/dist/browser/components/templates/TemplatesClientPage.js +269 -89
- package/dist/browser/components/templates/TemplatesOverlays.js +2874 -0
- package/dist/browser/components/templates/index.js +301 -121
- package/dist/browser/components/templates/template-catalog.js +5 -3
- package/dist/browser/components/templates/template-filters.js +99 -0
- package/dist/browser/components/templates/template-tag-visibility.js +40 -0
- package/dist/browser/components/templates/useTemplateBrowseState.js +191 -0
- package/dist/browser/index.js +301 -121
- package/dist/components/templates/TemplatesBrowseControls.d.ts +7 -2
- package/dist/components/templates/TemplatesBrowseControls.js +37 -22
- package/dist/components/templates/TemplatesCatalogSection.d.ts +4 -1
- package/dist/components/templates/TemplatesCatalogSection.js +29 -6
- package/dist/components/templates/TemplatesClientPage.js +269 -89
- package/dist/components/templates/TemplatesOverlays.d.ts +10 -0
- package/dist/components/templates/TemplatesOverlays.js +2869 -0
- package/dist/components/templates/index.js +301 -121
- package/dist/components/templates/template-catalog.d.ts +1 -0
- package/dist/components/templates/template-catalog.js +5 -3
- package/dist/components/templates/template-filters.d.ts +12 -0
- package/dist/components/templates/template-filters.js +94 -0
- package/dist/components/templates/template-tag-visibility.d.ts +10 -0
- package/dist/components/templates/template-tag-visibility.js +35 -0
- package/dist/components/templates/useTemplateBrowseState.d.ts +22 -0
- package/dist/components/templates/useTemplateBrowseState.js +186 -0
- package/dist/index.js +301 -121
- package/dist/node/components/templates/TemplatesBrowseControls.js +37 -22
- package/dist/node/components/templates/TemplatesCatalogSection.js +29 -6
- package/dist/node/components/templates/TemplatesClientPage.js +269 -89
- package/dist/node/components/templates/TemplatesOverlays.js +2869 -0
- package/dist/node/components/templates/index.js +301 -121
- package/dist/node/components/templates/template-catalog.js +5 -3
- package/dist/node/components/templates/template-filters.js +94 -0
- package/dist/node/components/templates/template-tag-visibility.js +35 -0
- package/dist/node/components/templates/useTemplateBrowseState.js +186 -0
- package/dist/node/index.js +301 -121
- package/package.json +82 -26
- package/src/components/templates/TemplatesBrowseControls.tsx +59 -35
- package/src/components/templates/TemplatesCatalogSection.tsx +29 -4
- package/src/components/templates/TemplatesClientPage.tsx +41 -97
- package/src/components/templates/TemplatesOverlays.tsx +65 -0
- package/src/components/templates/template-catalog.test.ts +96 -0
- package/src/components/templates/template-catalog.ts +14 -6
- package/src/components/templates/template-filters.ts +57 -0
- package/src/components/templates/template-tag-visibility.ts +58 -0
- package/src/components/templates/useTemplateBrowseState.ts +101 -0
|
@@ -4,11 +4,16 @@ import {
|
|
|
4
4
|
buildLocalTemplateCatalog,
|
|
5
5
|
matchesTemplateFilters,
|
|
6
6
|
} from './template-catalog';
|
|
7
|
+
import { buildTemplateFilterState } from './template-filters';
|
|
7
8
|
import { NEW_TEMPLATE_IDS } from './template-new';
|
|
8
9
|
import {
|
|
9
10
|
getAvailableTemplateSources,
|
|
10
11
|
isRegistryConfigured,
|
|
11
12
|
} from './template-source';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_VISIBLE_TEMPLATE_TAGS,
|
|
15
|
+
getVisibleTemplateTagFacets,
|
|
16
|
+
} from './template-tag-visibility';
|
|
12
17
|
|
|
13
18
|
describe('template catalog', () => {
|
|
14
19
|
test('includes every public example exposed as a template', () => {
|
|
@@ -51,6 +56,97 @@ describe('template catalog', () => {
|
|
|
51
56
|
)
|
|
52
57
|
).toBe(true);
|
|
53
58
|
});
|
|
59
|
+
|
|
60
|
+
test('derives tag facets from the templates remaining after search', () => {
|
|
61
|
+
const catalog = buildLocalTemplateCatalog(listExamples(), listTemplates());
|
|
62
|
+
const state = buildTemplateFilterState(
|
|
63
|
+
catalog,
|
|
64
|
+
'agent',
|
|
65
|
+
null,
|
|
66
|
+
(template) => template
|
|
67
|
+
);
|
|
68
|
+
const tags = state.tagFacets.map((facet) => facet.tag);
|
|
69
|
+
|
|
70
|
+
expect(state.searchScopedTemplates.length).toBeGreaterThan(0);
|
|
71
|
+
expect(tags).toContain('agents');
|
|
72
|
+
expect(tags).not.toContain('billing');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('applies selected tags after search scoping', () => {
|
|
76
|
+
const catalog = buildLocalTemplateCatalog(listExamples(), listTemplates());
|
|
77
|
+
const state = buildTemplateFilterState(
|
|
78
|
+
catalog,
|
|
79
|
+
'agent',
|
|
80
|
+
'telegram',
|
|
81
|
+
(template) => template
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(state.searchScopedTemplates.length).toBeGreaterThan(
|
|
85
|
+
state.finalTemplates.length
|
|
86
|
+
);
|
|
87
|
+
expect(
|
|
88
|
+
state.finalTemplates.every((template) =>
|
|
89
|
+
template.tags.includes('telegram')
|
|
90
|
+
)
|
|
91
|
+
).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('caps default tag visibility and keeps selected hidden tags visible', () => {
|
|
95
|
+
const tagFacets = Array.from(
|
|
96
|
+
{ length: DEFAULT_VISIBLE_TEMPLATE_TAGS + 2 },
|
|
97
|
+
(_, index) => ({
|
|
98
|
+
tag: `tag-${index}`,
|
|
99
|
+
count: DEFAULT_VISIBLE_TEMPLATE_TAGS + 2 - index,
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
const { visibleTagFacets, hiddenTagFacets } = getVisibleTemplateTagFacets(
|
|
103
|
+
tagFacets,
|
|
104
|
+
'tag-11',
|
|
105
|
+
false
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(visibleTagFacets).toHaveLength(DEFAULT_VISIBLE_TEMPLATE_TAGS + 1);
|
|
109
|
+
expect(visibleTagFacets.some((facet) => facet.tag === 'tag-11')).toBe(true);
|
|
110
|
+
expect(hiddenTagFacets.some((facet) => facet.tag === 'tag-11')).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('recomputes source-specific tags from the active source only', () => {
|
|
114
|
+
const localTemplates = [
|
|
115
|
+
{
|
|
116
|
+
title: 'Local agent console',
|
|
117
|
+
description: 'Agent workflows',
|
|
118
|
+
tags: ['agents', 'local'],
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
const registryTemplates = [
|
|
122
|
+
{
|
|
123
|
+
title: 'Registry recipe app',
|
|
124
|
+
description: 'Cooking workflows',
|
|
125
|
+
tags: ['recipes', 'community'],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
const localState = buildTemplateFilterState(
|
|
129
|
+
localTemplates,
|
|
130
|
+
'',
|
|
131
|
+
null,
|
|
132
|
+
(template) => template
|
|
133
|
+
);
|
|
134
|
+
const registryState = buildTemplateFilterState(
|
|
135
|
+
registryTemplates,
|
|
136
|
+
'',
|
|
137
|
+
null,
|
|
138
|
+
(template) => template
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(localState.tagFacets.map((facet) => facet.tag)).toEqual([
|
|
142
|
+
'agents',
|
|
143
|
+
'local',
|
|
144
|
+
]);
|
|
145
|
+
expect(registryState.tagFacets.map((facet) => facet.tag)).toEqual([
|
|
146
|
+
'community',
|
|
147
|
+
'recipes',
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
54
150
|
});
|
|
55
151
|
|
|
56
152
|
describe('template source configuration', () => {
|
|
@@ -79,6 +79,16 @@ export function matchesTemplateFilters(
|
|
|
79
79
|
template: TemplateFilterCandidate,
|
|
80
80
|
search: string,
|
|
81
81
|
selectedTag: string | null
|
|
82
|
+
): boolean {
|
|
83
|
+
return (
|
|
84
|
+
matchesTemplateSearch(template, search) &&
|
|
85
|
+
(selectedTag === null || template.tags.includes(selectedTag))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function matchesTemplateSearch(
|
|
90
|
+
template: TemplateFilterCandidate,
|
|
91
|
+
search: string
|
|
82
92
|
): boolean {
|
|
83
93
|
const haystack = [
|
|
84
94
|
template.title,
|
|
@@ -88,13 +98,11 @@ export function matchesTemplateFilters(
|
|
|
88
98
|
.join(' ')
|
|
89
99
|
.toLowerCase();
|
|
90
100
|
const searchTokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
91
|
-
const matchesSearch =
|
|
92
|
-
searchTokens.length === 0 ||
|
|
93
|
-
searchTokens.every((token) => haystack.includes(token));
|
|
94
|
-
const matchesTag =
|
|
95
|
-
selectedTag === null || template.tags.includes(selectedTag);
|
|
96
101
|
|
|
97
|
-
return
|
|
102
|
+
return (
|
|
103
|
+
searchTokens.length === 0 ||
|
|
104
|
+
searchTokens.every((token) => haystack.includes(token))
|
|
105
|
+
);
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
export function formatExampleKindLabel(kind: ExampleKind): string {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { matchesTemplateSearch } from './template-catalog';
|
|
2
|
+
import type { TemplateTagFacet } from './template-tag-visibility';
|
|
3
|
+
|
|
4
|
+
export interface TemplateFilterCandidate {
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
tags: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TemplateFilterState<TTemplate> {
|
|
11
|
+
searchScopedTemplates: TTemplate[];
|
|
12
|
+
finalTemplates: TTemplate[];
|
|
13
|
+
tagFacets: TemplateTagFacet[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildTemplateFilterState<TTemplate>(
|
|
17
|
+
templates: readonly TTemplate[],
|
|
18
|
+
search: string,
|
|
19
|
+
selectedTag: string | null,
|
|
20
|
+
getCandidate: (template: TTemplate) => TemplateFilterCandidate
|
|
21
|
+
): TemplateFilterState<TTemplate> {
|
|
22
|
+
const searchScopedTemplates = templates.filter((template) =>
|
|
23
|
+
matchesTemplateSearch(getCandidate(template), search)
|
|
24
|
+
);
|
|
25
|
+
const finalTemplates =
|
|
26
|
+
selectedTag === null
|
|
27
|
+
? searchScopedTemplates
|
|
28
|
+
: searchScopedTemplates.filter((template) =>
|
|
29
|
+
getCandidate(template).tags.includes(selectedTag)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
searchScopedTemplates,
|
|
34
|
+
finalTemplates,
|
|
35
|
+
tagFacets: buildTemplateTagFacets(searchScopedTemplates, getCandidate),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildTemplateTagFacets<TTemplate>(
|
|
40
|
+
templates: readonly TTemplate[],
|
|
41
|
+
getCandidate: (template: TTemplate) => TemplateFilterCandidate
|
|
42
|
+
): TemplateTagFacet[] {
|
|
43
|
+
const counts = new Map<string, number>();
|
|
44
|
+
|
|
45
|
+
for (const template of templates) {
|
|
46
|
+
for (const tag of new Set(getCandidate(template).tags)) {
|
|
47
|
+
counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...counts.entries()]
|
|
52
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
53
|
+
.sort(
|
|
54
|
+
(left, right) =>
|
|
55
|
+
right.count - left.count || left.tag.localeCompare(right.tag)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const DEFAULT_VISIBLE_TEMPLATE_TAGS = 10;
|
|
2
|
+
|
|
3
|
+
export interface TemplateTagFacet {
|
|
4
|
+
tag: string;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface VisibleTemplateTagFacets {
|
|
9
|
+
visibleTagFacets: TemplateTagFacet[];
|
|
10
|
+
hiddenTagFacets: TemplateTagFacet[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getVisibleTemplateTagFacets(
|
|
14
|
+
tagFacets: readonly TemplateTagFacet[],
|
|
15
|
+
selectedTag: string | null,
|
|
16
|
+
expanded: boolean,
|
|
17
|
+
visibleCount = DEFAULT_VISIBLE_TEMPLATE_TAGS
|
|
18
|
+
): VisibleTemplateTagFacets {
|
|
19
|
+
if (expanded) {
|
|
20
|
+
return {
|
|
21
|
+
visibleTagFacets: pinSelectedTagFacet(tagFacets, selectedTag),
|
|
22
|
+
hiddenTagFacets: [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const visibleTagFacets = pinSelectedTagFacet(
|
|
27
|
+
tagFacets.slice(0, visibleCount),
|
|
28
|
+
selectedTag,
|
|
29
|
+
tagFacets
|
|
30
|
+
);
|
|
31
|
+
const visibleTags = new Set(visibleTagFacets.map((facet) => facet.tag));
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
visibleTagFacets,
|
|
35
|
+
hiddenTagFacets: tagFacets.filter((facet) => !visibleTags.has(facet.tag)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pinSelectedTagFacet(
|
|
40
|
+
tagFacets: readonly TemplateTagFacet[],
|
|
41
|
+
selectedTag: string | null,
|
|
42
|
+
fallbackTagFacets: readonly TemplateTagFacet[] = tagFacets
|
|
43
|
+
): TemplateTagFacet[] {
|
|
44
|
+
if (
|
|
45
|
+
selectedTag === null ||
|
|
46
|
+
tagFacets.some((facet) => facet.tag === selectedTag)
|
|
47
|
+
) {
|
|
48
|
+
return [...tagFacets];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
...tagFacets,
|
|
53
|
+
fallbackTagFacets.find((facet) => facet.tag === selectedTag) ?? {
|
|
54
|
+
tag: selectedTag,
|
|
55
|
+
count: 0,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRegistryTemplates } from '@contractspec/lib.example-shared-ui';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { buildLocalTemplateCatalog } from './template-catalog';
|
|
6
|
+
import { buildTemplateFilterState } from './template-filters';
|
|
7
|
+
import {
|
|
8
|
+
getAvailableTemplateSources,
|
|
9
|
+
isRegistryConfigured,
|
|
10
|
+
type TemplateSource,
|
|
11
|
+
} from './template-source';
|
|
12
|
+
import { getVisibleTemplateTagFacets } from './template-tag-visibility';
|
|
13
|
+
|
|
14
|
+
const REGISTRY_URL = process.env.NEXT_PUBLIC_CONTRACTSPEC_REGISTRY_URL;
|
|
15
|
+
|
|
16
|
+
export function useTemplateBrowseState() {
|
|
17
|
+
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
|
18
|
+
const [search, setSearch] = useState('');
|
|
19
|
+
const [source, setSource] = useState<TemplateSource>('local');
|
|
20
|
+
const [showAllTags, setShowAllTags] = useState(false);
|
|
21
|
+
const registryConfigured = isRegistryConfigured(REGISTRY_URL);
|
|
22
|
+
const availableSources = getAvailableTemplateSources(REGISTRY_URL);
|
|
23
|
+
const localTemplates = useMemo(() => buildLocalTemplateCatalog(), []);
|
|
24
|
+
const localTemplateById = useMemo(
|
|
25
|
+
() => new Map(localTemplates.map((template) => [template.id, template])),
|
|
26
|
+
[localTemplates]
|
|
27
|
+
);
|
|
28
|
+
const { data: registryTemplates = [], isLoading: registryLoading } =
|
|
29
|
+
useRegistryTemplates();
|
|
30
|
+
const localFilterState = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
buildTemplateFilterState(
|
|
33
|
+
localTemplates,
|
|
34
|
+
search,
|
|
35
|
+
selectedTag,
|
|
36
|
+
(template) => ({
|
|
37
|
+
title: template.title,
|
|
38
|
+
description: template.description,
|
|
39
|
+
tags: template.tags,
|
|
40
|
+
})
|
|
41
|
+
),
|
|
42
|
+
[localTemplates, search, selectedTag]
|
|
43
|
+
);
|
|
44
|
+
const registryFilterState = useMemo(
|
|
45
|
+
() =>
|
|
46
|
+
buildTemplateFilterState(
|
|
47
|
+
registryTemplates,
|
|
48
|
+
search,
|
|
49
|
+
selectedTag,
|
|
50
|
+
(template) => ({
|
|
51
|
+
title: template.name,
|
|
52
|
+
description: template.description,
|
|
53
|
+
tags: template.tags,
|
|
54
|
+
})
|
|
55
|
+
),
|
|
56
|
+
[registryTemplates, search, selectedTag]
|
|
57
|
+
);
|
|
58
|
+
const activeFilterState =
|
|
59
|
+
source === 'registry' ? registryFilterState : localFilterState;
|
|
60
|
+
const suppressTagRail =
|
|
61
|
+
source === 'registry' &&
|
|
62
|
+
(registryLoading || registryTemplates.length === 0);
|
|
63
|
+
const { visibleTagFacets, hiddenTagFacets } = useMemo(
|
|
64
|
+
() =>
|
|
65
|
+
getVisibleTemplateTagFacets(
|
|
66
|
+
activeFilterState.tagFacets,
|
|
67
|
+
selectedTag,
|
|
68
|
+
showAllTags
|
|
69
|
+
),
|
|
70
|
+
[activeFilterState.tagFacets, selectedTag, showAllTags]
|
|
71
|
+
);
|
|
72
|
+
const showTagFilters =
|
|
73
|
+
!suppressTagRail &&
|
|
74
|
+
(visibleTagFacets.length > 0 || hiddenTagFacets.length > 0);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
setShowAllTags(false);
|
|
78
|
+
}, [search, showTagFilters, source]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
selectedTag,
|
|
82
|
+
setSelectedTag,
|
|
83
|
+
search,
|
|
84
|
+
setSearch,
|
|
85
|
+
source,
|
|
86
|
+
setSource,
|
|
87
|
+
showAllTags,
|
|
88
|
+
setShowAllTags,
|
|
89
|
+
registryConfigured,
|
|
90
|
+
availableSources,
|
|
91
|
+
localTemplates,
|
|
92
|
+
localTemplateById,
|
|
93
|
+
registryTemplates,
|
|
94
|
+
registryLoading,
|
|
95
|
+
localFilterState,
|
|
96
|
+
registryFilterState,
|
|
97
|
+
visibleTagFacets,
|
|
98
|
+
hiddenTagFacets,
|
|
99
|
+
showTagFilters,
|
|
100
|
+
};
|
|
101
|
+
}
|