@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.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +54 -42
  2. package/CHANGELOG.md +33 -0
  3. package/dist/browser/components/templates/TemplatesBrowseControls.js +37 -22
  4. package/dist/browser/components/templates/TemplatesCatalogSection.js +29 -6
  5. package/dist/browser/components/templates/TemplatesClientPage.js +269 -89
  6. package/dist/browser/components/templates/TemplatesOverlays.js +2874 -0
  7. package/dist/browser/components/templates/index.js +301 -121
  8. package/dist/browser/components/templates/template-catalog.js +5 -3
  9. package/dist/browser/components/templates/template-filters.js +99 -0
  10. package/dist/browser/components/templates/template-tag-visibility.js +40 -0
  11. package/dist/browser/components/templates/useTemplateBrowseState.js +191 -0
  12. package/dist/browser/index.js +301 -121
  13. package/dist/components/templates/TemplatesBrowseControls.d.ts +7 -2
  14. package/dist/components/templates/TemplatesBrowseControls.js +37 -22
  15. package/dist/components/templates/TemplatesCatalogSection.d.ts +4 -1
  16. package/dist/components/templates/TemplatesCatalogSection.js +29 -6
  17. package/dist/components/templates/TemplatesClientPage.js +269 -89
  18. package/dist/components/templates/TemplatesOverlays.d.ts +10 -0
  19. package/dist/components/templates/TemplatesOverlays.js +2869 -0
  20. package/dist/components/templates/index.js +301 -121
  21. package/dist/components/templates/template-catalog.d.ts +1 -0
  22. package/dist/components/templates/template-catalog.js +5 -3
  23. package/dist/components/templates/template-filters.d.ts +12 -0
  24. package/dist/components/templates/template-filters.js +94 -0
  25. package/dist/components/templates/template-tag-visibility.d.ts +10 -0
  26. package/dist/components/templates/template-tag-visibility.js +35 -0
  27. package/dist/components/templates/useTemplateBrowseState.d.ts +22 -0
  28. package/dist/components/templates/useTemplateBrowseState.js +186 -0
  29. package/dist/index.js +301 -121
  30. package/dist/node/components/templates/TemplatesBrowseControls.js +37 -22
  31. package/dist/node/components/templates/TemplatesCatalogSection.js +29 -6
  32. package/dist/node/components/templates/TemplatesClientPage.js +269 -89
  33. package/dist/node/components/templates/TemplatesOverlays.js +2869 -0
  34. package/dist/node/components/templates/index.js +301 -121
  35. package/dist/node/components/templates/template-catalog.js +5 -3
  36. package/dist/node/components/templates/template-filters.js +94 -0
  37. package/dist/node/components/templates/template-tag-visibility.js +35 -0
  38. package/dist/node/components/templates/useTemplateBrowseState.js +186 -0
  39. package/dist/node/index.js +301 -121
  40. package/package.json +82 -26
  41. package/src/components/templates/TemplatesBrowseControls.tsx +59 -35
  42. package/src/components/templates/TemplatesCatalogSection.tsx +29 -4
  43. package/src/components/templates/TemplatesClientPage.tsx +41 -97
  44. package/src/components/templates/TemplatesOverlays.tsx +65 -0
  45. package/src/components/templates/template-catalog.test.ts +96 -0
  46. package/src/components/templates/template-catalog.ts +14 -6
  47. package/src/components/templates/template-filters.ts +57 -0
  48. package/src/components/templates/template-tag-visibility.ts +58 -0
  49. 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 matchesSearch && matchesTag;
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
+ }