@happyvertical/smrt-marketing 0.39.16

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 (67) hide show
  1. package/AGENTS.md +79 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +67 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/CampaignChannelCollection.d.ts +9 -0
  8. package/dist/collections/CampaignChannelCollection.d.ts.map +1 -0
  9. package/dist/collections/CampaignCollection.d.ts +10 -0
  10. package/dist/collections/CampaignCollection.d.ts.map +1 -0
  11. package/dist/collections/CampaignMetricSnapshotCollection.d.ts +15 -0
  12. package/dist/collections/CampaignMetricSnapshotCollection.d.ts.map +1 -0
  13. package/dist/collections/index.d.ts +4 -0
  14. package/dist/collections/index.d.ts.map +1 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +625 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/manifest.json +1173 -0
  20. package/dist/models/Campaign.d.ts +31 -0
  21. package/dist/models/Campaign.d.ts.map +1 -0
  22. package/dist/models/CampaignChannel.d.ts +22 -0
  23. package/dist/models/CampaignChannel.d.ts.map +1 -0
  24. package/dist/models/CampaignMetricSnapshot.d.ts +29 -0
  25. package/dist/models/CampaignMetricSnapshot.d.ts.map +1 -0
  26. package/dist/models/index.d.ts +4 -0
  27. package/dist/models/index.d.ts.map +1 -0
  28. package/dist/services/BudgetPacingService.d.ts +25 -0
  29. package/dist/services/BudgetPacingService.d.ts.map +1 -0
  30. package/dist/services/CampaignLifecycleService.d.ts +19 -0
  31. package/dist/services/CampaignLifecycleService.d.ts.map +1 -0
  32. package/dist/services/MetricIngestionService.d.ts +16 -0
  33. package/dist/services/MetricIngestionService.d.ts.map +1 -0
  34. package/dist/services/index.d.ts +4 -0
  35. package/dist/services/index.d.ts.map +1 -0
  36. package/dist/smrt-knowledge.json +875 -0
  37. package/dist/svelte/__tests__/components.test.js +115 -0
  38. package/dist/svelte/__tests__/helpers.test.js +94 -0
  39. package/dist/svelte/components/BudgetPacing.svelte +129 -0
  40. package/dist/svelte/components/BudgetPacing.svelte.d.ts +10 -0
  41. package/dist/svelte/components/BudgetPacing.svelte.d.ts.map +1 -0
  42. package/dist/svelte/components/CampaignDetail.svelte +208 -0
  43. package/dist/svelte/components/CampaignDetail.svelte.d.ts +14 -0
  44. package/dist/svelte/components/CampaignDetail.svelte.d.ts.map +1 -0
  45. package/dist/svelte/components/CampaignList.svelte +140 -0
  46. package/dist/svelte/components/CampaignList.svelte.d.ts +11 -0
  47. package/dist/svelte/components/CampaignList.svelte.d.ts.map +1 -0
  48. package/dist/svelte/components/ChannelMix.svelte +138 -0
  49. package/dist/svelte/components/ChannelMix.svelte.d.ts +10 -0
  50. package/dist/svelte/components/ChannelMix.svelte.d.ts.map +1 -0
  51. package/dist/svelte/components/MarketingDashboard.svelte +129 -0
  52. package/dist/svelte/components/MarketingDashboard.svelte.d.ts +13 -0
  53. package/dist/svelte/components/MarketingDashboard.svelte.d.ts.map +1 -0
  54. package/dist/svelte/format.d.ts +8 -0
  55. package/dist/svelte/format.d.ts.map +1 -0
  56. package/dist/svelte/format.js +42 -0
  57. package/dist/svelte/index.d.ts +8 -0
  58. package/dist/svelte/index.d.ts.map +1 -0
  59. package/dist/svelte/index.js +7 -0
  60. package/dist/svelte/types.d.ts +73 -0
  61. package/dist/svelte/types.d.ts.map +1 -0
  62. package/dist/svelte/types.js +80 -0
  63. package/dist/types.d.ts +61 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +29 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +79 -0
@@ -0,0 +1,115 @@
1
+ import { fireEvent, render, screen } from '@testing-library/svelte';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import BudgetPacing from '../components/BudgetPacing.svelte';
4
+ import CampaignDetail from '../components/CampaignDetail.svelte';
5
+ import CampaignList from '../components/CampaignList.svelte';
6
+ import ChannelMix from '../components/ChannelMix.svelte';
7
+ import MarketingDashboard from '../components/MarketingDashboard.svelte';
8
+ const campaign = {
9
+ id: 'campaign-1',
10
+ campaignKey: 'summer-launch',
11
+ name: 'Summer launch',
12
+ objective: 'lead_generation',
13
+ status: 'active',
14
+ startAt: '2026-07-01T00:00:00.000Z',
15
+ endAt: '2026-07-31T00:00:00.000Z',
16
+ budgetCents: 10_000,
17
+ currency: 'USD',
18
+ spendCents: 2_500,
19
+ impressions: 4_000,
20
+ clicks: 80,
21
+ conversions: 4,
22
+ leads: 12,
23
+ channelCount: 2,
24
+ };
25
+ const channels = [
26
+ {
27
+ id: 'channel-social',
28
+ channelKind: 'social',
29
+ channelRef: 'social:post-1',
30
+ label: 'Social post',
31
+ status: 'active',
32
+ allocatedBudgetCents: 7_500,
33
+ spendCents: 2_000,
34
+ impressions: 3_000,
35
+ clicks: 60,
36
+ leads: 10,
37
+ },
38
+ {
39
+ id: 'channel-ad',
40
+ channelKind: 'ads',
41
+ channelRef: 'ads:campaign-1',
42
+ label: 'Paid campaign',
43
+ status: 'scheduled',
44
+ allocatedBudgetCents: 2_500,
45
+ spendCents: 500,
46
+ impressions: 1_000,
47
+ clicks: 20,
48
+ leads: 2,
49
+ },
50
+ ];
51
+ describe('marketing Svelte surfaces', () => {
52
+ it('renders dashboard totals from public campaign props', () => {
53
+ render(MarketingDashboard, {
54
+ props: { campaigns: [campaign], locale: 'en-US' },
55
+ });
56
+ expect(screen.getByText('1 active')).toBeInTheDocument();
57
+ expect(screen.getByText('2')).toBeInTheDocument();
58
+ expect(screen.getByText('12')).toBeInTheDocument();
59
+ expect(screen.getByText('$25.00 spent')).toBeInTheDocument();
60
+ expect(screen.getByText('of $100.00 budget')).toBeInTheDocument();
61
+ });
62
+ it('renders campaign rows and reports selection through its callback', async () => {
63
+ const onSelect = vi.fn();
64
+ render(CampaignList, {
65
+ props: { campaigns: [campaign], locale: 'en-US', onSelect },
66
+ });
67
+ expect(screen.getByRole('table')).toBeInTheDocument();
68
+ expect(screen.getByText('Summer launch')).toBeInTheDocument();
69
+ await fireEvent.click(screen.getByRole('button', { name: 'View' }));
70
+ expect(onSelect).toHaveBeenCalledWith('campaign-1');
71
+ });
72
+ it('renders campaign details and generic channel references', () => {
73
+ const detail = {
74
+ ...campaign,
75
+ description: 'A cross-channel launch.',
76
+ };
77
+ render(CampaignDetail, {
78
+ props: { campaign: detail, channels, locale: 'en-US' },
79
+ });
80
+ expect(screen.getByRole('heading', { name: 'Summer launch' })).toBeInTheDocument();
81
+ expect(screen.getByText('A cross-channel launch.')).toBeInTheDocument();
82
+ expect(screen.getByText('Social post')).toBeInTheDocument();
83
+ expect(screen.getByText('social:post-1')).toBeInTheDocument();
84
+ });
85
+ it('renders channel mix percentages and cent-based spend', () => {
86
+ render(ChannelMix, {
87
+ props: { channels, currency: 'USD', locale: 'en-US' },
88
+ });
89
+ expect(screen.getByRole('heading', { name: 'Channel mix' })).toBeInTheDocument();
90
+ expect(screen.getByText('$20.00 spent')).toBeInTheDocument();
91
+ expect(screen.getByText('80% of mix')).toBeInTheDocument();
92
+ expect(screen.getByText('$5.00 spent')).toBeInTheDocument();
93
+ expect(screen.getByText('20% of mix')).toBeInTheDocument();
94
+ });
95
+ it('renders budget pacing state and formatted evidence', () => {
96
+ const pacing = {
97
+ campaignId: campaign.id,
98
+ currency: 'USD',
99
+ budgetCents: 10_000,
100
+ spendCents: 4_000,
101
+ remainingCents: 6_000,
102
+ expectedSpendCents: 3_500,
103
+ varianceCents: 500,
104
+ budgetFraction: 0.4,
105
+ elapsedFraction: 0.35,
106
+ status: 'on_track',
107
+ };
108
+ render(BudgetPacing, { props: { pacing, locale: 'en-US' } });
109
+ expect(screen.getByRole('heading', { name: 'Budget pacing' })).toBeInTheDocument();
110
+ expect(screen.getByText('On track')).toBeInTheDocument();
111
+ expect(screen.getByText('$40.00 spent')).toBeInTheDocument();
112
+ expect(screen.getByText('40%')).toBeInTheDocument();
113
+ expect(screen.getByText('$60.00')).toBeInTheDocument();
114
+ });
115
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatCents } from '../format.js';
3
+ import { calculateChannelMix, campaignStatusBadgeVariant, cappedBudgetProgress, pacingStatusBadgeVariant, summarizeMarketingDashboard, } from '../types.js';
4
+ describe('marketing Svelte helpers', () => {
5
+ it('keeps currency formatting render-safe for invalid inputs', () => {
6
+ expect(formatCents(12_345, 'NOT_A_CODE', 'en-US')).toBe('123.45 NOT_A_CODE');
7
+ expect(formatCents(Number.NaN, 'USD', 'en-US')).toBe('—');
8
+ });
9
+ it('summarizes campaign metrics without mixing currencies', () => {
10
+ const summary = summarizeMarketingDashboard([
11
+ {
12
+ id: 'campaign-1',
13
+ campaignKey: 'one',
14
+ name: 'One',
15
+ status: 'active',
16
+ budgetCents: 100_000,
17
+ spendCents: 40_000,
18
+ currency: 'CAD',
19
+ channelCount: 2,
20
+ impressions: 10_000,
21
+ clicks: 500,
22
+ conversions: 20,
23
+ leads: 12,
24
+ },
25
+ {
26
+ id: 'campaign-2',
27
+ campaignKey: 'two',
28
+ name: 'Two',
29
+ status: 'completed',
30
+ budgetCents: 50_000,
31
+ spendCents: 45_000,
32
+ currency: 'CAD',
33
+ channelCount: 1,
34
+ leads: 5,
35
+ },
36
+ {
37
+ id: 'campaign-3',
38
+ campaignKey: 'three',
39
+ name: 'Three',
40
+ status: 'draft',
41
+ budgetCents: 80_000,
42
+ spendCents: 0,
43
+ currency: 'USD',
44
+ },
45
+ ]);
46
+ expect(summary.activeCampaignCount).toBe(1);
47
+ expect(summary.channelCount).toBe(3);
48
+ expect(summary.leads).toBe(17);
49
+ expect(summary.currencyTotals).toEqual([
50
+ { currency: 'CAD', budgetCents: 150_000, spendCents: 85_000 },
51
+ { currency: 'USD', budgetCents: 80_000, spendCents: 0 },
52
+ ]);
53
+ });
54
+ it('calculates channel spend and allocation fractions independently', () => {
55
+ const mix = calculateChannelMix([
56
+ {
57
+ id: 'ads',
58
+ channelKind: 'ad_group',
59
+ channelRef: 'ag-1',
60
+ status: 'active',
61
+ allocatedBudgetCents: 75_000,
62
+ spendCents: 30_000,
63
+ },
64
+ {
65
+ id: 'social',
66
+ channelKind: 'social_post',
67
+ channelRef: 'post-1',
68
+ status: 'active',
69
+ allocatedBudgetCents: 25_000,
70
+ spendCents: 10_000,
71
+ },
72
+ ]);
73
+ expect(mix[0]?.spendFraction).toBe(0.75);
74
+ expect(mix[0]?.allocationFraction).toBe(0.75);
75
+ expect(mix[1]?.spendFraction).toBe(0.25);
76
+ });
77
+ it('caps visual progress while preserving status semantics', () => {
78
+ expect(cappedBudgetProgress({
79
+ campaignId: 'campaign-1',
80
+ currency: 'CAD',
81
+ budgetCents: 100,
82
+ spendCents: 125,
83
+ remainingCents: -25,
84
+ expectedSpendCents: 50,
85
+ varianceCents: 75,
86
+ budgetFraction: 1.25,
87
+ elapsedFraction: 0.5,
88
+ status: 'over_budget',
89
+ })).toBe(1);
90
+ expect(pacingStatusBadgeVariant('over_budget')).toBe('error');
91
+ expect(pacingStatusBadgeVariant('on_track')).toBe('success');
92
+ expect(campaignStatusBadgeVariant('paused')).toBe('warning');
93
+ });
94
+ });
@@ -0,0 +1,129 @@
1
+ <script lang="ts">
2
+ import { Progress } from '@happyvertical/smrt-ui/feedback';
3
+ import { Badge, Card } from '@happyvertical/smrt-ui/ui';
4
+ import { formatCents, formatPercent, humanizeKey } from '../format.js';
5
+ import type { BudgetPacingView } from '../types.js';
6
+ import { cappedBudgetProgress, pacingStatusBadgeVariant } from '../types.js';
7
+
8
+ export interface Props {
9
+ pacing?: BudgetPacingView | null;
10
+ locale?: string;
11
+ label?: string;
12
+ }
13
+
14
+ let { pacing = null, locale, label = 'Budget pacing' }: Props = $props();
15
+ const progress = $derived(pacing ? cappedBudgetProgress(pacing) : 0);
16
+ </script>
17
+
18
+ {#if !pacing}
19
+ <p class="empty">No budget pacing is available.</p>
20
+ {:else}
21
+ <Card padding="sm" variant="outlined">
22
+ <section class="budget-pacing">
23
+ <header>
24
+ <h3>{label}</h3>
25
+ <Badge variant={pacingStatusBadgeVariant(pacing.status)} size="sm">
26
+ {humanizeKey(pacing.status)}
27
+ </Badge>
28
+ </header>
29
+
30
+ <div class="progress-label">
31
+ <span>{formatCents(pacing.spendCents, pacing.currency, locale)} spent</span>
32
+ <span>
33
+ {pacing.budgetFraction === null
34
+ ? 'No budget'
35
+ : formatPercent(pacing.budgetFraction, locale)}
36
+ </span>
37
+ </div>
38
+ <Progress value={progress} max={1} label={label} />
39
+
40
+ <dl class="facts">
41
+ <div>
42
+ <dt>Budget</dt>
43
+ <dd>{formatCents(pacing.budgetCents, pacing.currency, locale)}</dd>
44
+ </div>
45
+ <div>
46
+ <dt>Remaining</dt>
47
+ <dd class:negative={pacing.remainingCents < 0}>
48
+ {formatCents(pacing.remainingCents, pacing.currency, locale)}
49
+ </dd>
50
+ </div>
51
+ <div>
52
+ <dt>Expected by now</dt>
53
+ <dd>
54
+ {pacing.expectedSpendCents === null
55
+ ? '—'
56
+ : formatCents(pacing.expectedSpendCents, pacing.currency, locale)}
57
+ </dd>
58
+ </div>
59
+ <div>
60
+ <dt>Variance</dt>
61
+ <dd class:negative={(pacing.varianceCents ?? 0) > 0}>
62
+ {pacing.varianceCents === null
63
+ ? '—'
64
+ : formatCents(pacing.varianceCents, pacing.currency, locale)}
65
+ </dd>
66
+ </div>
67
+ </dl>
68
+ </section>
69
+ </Card>
70
+ {/if}
71
+
72
+ <style>
73
+ .budget-pacing {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: var(--smrt-spacing-2);
77
+ }
78
+
79
+ header,
80
+ .progress-label {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ gap: var(--smrt-spacing-3);
85
+ }
86
+
87
+ h3,
88
+ .empty {
89
+ margin: 0;
90
+ }
91
+
92
+ h3 {
93
+ color: var(--smrt-color-on-surface);
94
+ font: var(--smrt-typography-title-medium-font);
95
+ }
96
+
97
+ .empty,
98
+ .progress-label,
99
+ dt {
100
+ color: var(--smrt-color-on-surface-variant);
101
+ }
102
+
103
+ .empty {
104
+ font: var(--smrt-typography-body-medium-font);
105
+ }
106
+
107
+ .progress-label,
108
+ dt {
109
+ font: var(--smrt-typography-body-small-font);
110
+ }
111
+
112
+ .facts {
113
+ display: grid;
114
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
115
+ gap: var(--smrt-spacing-3);
116
+ margin: var(--smrt-spacing-3) 0 0;
117
+ }
118
+
119
+ .facts dd {
120
+ margin: var(--smrt-spacing-1) 0 0;
121
+ color: var(--smrt-color-on-surface);
122
+ font: var(--smrt-typography-body-medium-font);
123
+ font-weight: var(--smrt-typography-weight-medium);
124
+ }
125
+
126
+ .facts dd.negative {
127
+ color: var(--smrt-color-error);
128
+ }
129
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { BudgetPacingView } from '../types.js';
2
+ export interface Props {
3
+ pacing?: BudgetPacingView | null;
4
+ locale?: string;
5
+ label?: string;
6
+ }
7
+ declare const BudgetPacing: import("svelte").Component<Props, {}, "">;
8
+ type BudgetPacing = ReturnType<typeof BudgetPacing>;
9
+ export default BudgetPacing;
10
+ //# sourceMappingURL=BudgetPacing.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BudgetPacing.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/BudgetPacing.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuED,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,208 @@
1
+ <script lang="ts">
2
+ import { Badge, Card } from '@happyvertical/smrt-ui/ui';
3
+ import type { Snippet } from 'svelte';
4
+ import {
5
+ formatCents,
6
+ formatDateRange,
7
+ formatNumber,
8
+ humanizeKey,
9
+ } from '../format.js';
10
+ import type { CampaignChannelView, CampaignDetailView } from '../types.js';
11
+ import { campaignStatusBadgeVariant } from '../types.js';
12
+
13
+ export interface Props {
14
+ campaign?: CampaignDetailView | null;
15
+ channels?: CampaignChannelView[];
16
+ locale?: string;
17
+ pacing?: Snippet;
18
+ channelMix?: Snippet;
19
+ children?: Snippet;
20
+ }
21
+
22
+ let {
23
+ campaign = null,
24
+ channels = [],
25
+ locale,
26
+ pacing,
27
+ channelMix,
28
+ children,
29
+ }: Props = $props();
30
+ </script>
31
+
32
+ {#if !campaign}
33
+ <p class="empty">Select a campaign to see its details.</p>
34
+ {:else}
35
+ <article class="campaign-detail">
36
+ <Card variant="outlined">
37
+ <header class="header">
38
+ <div>
39
+ <p class="eyebrow">{campaign.campaignKey}</p>
40
+ <h2>{campaign.name}</h2>
41
+ {#if campaign.description}<p class="description">{campaign.description}</p>{/if}
42
+ </div>
43
+ <Badge variant={campaignStatusBadgeVariant(campaign.status)}>
44
+ {humanizeKey(campaign.status)}
45
+ </Badge>
46
+ </header>
47
+
48
+ <dl class="facts">
49
+ <div>
50
+ <dt>Objective</dt>
51
+ <dd>{campaign.objective ? humanizeKey(campaign.objective) : '—'}</dd>
52
+ </div>
53
+ <div>
54
+ <dt>Schedule</dt>
55
+ <dd>{formatDateRange(campaign.startAt, campaign.endAt, locale)}</dd>
56
+ </div>
57
+ <div>
58
+ <dt>Budget</dt>
59
+ <dd>{formatCents(campaign.budgetCents, campaign.currency, locale)}</dd>
60
+ </div>
61
+ <div>
62
+ <dt>Spend</dt>
63
+ <dd>{formatCents(campaign.spendCents ?? 0, campaign.currency, locale)}</dd>
64
+ </div>
65
+ <div>
66
+ <dt>Impressions</dt>
67
+ <dd>{formatNumber(campaign.impressions ?? 0, locale)}</dd>
68
+ </div>
69
+ <div>
70
+ <dt>Leads</dt>
71
+ <dd>{formatNumber(campaign.leads ?? 0, locale)}</dd>
72
+ </div>
73
+ </dl>
74
+ </Card>
75
+
76
+ {#if channels.length > 0}
77
+ <Card padding="sm">
78
+ <section class="channels" aria-label="Campaign channels">
79
+ <h3>Executions</h3>
80
+ <ul>
81
+ {#each channels as channel (channel.id)}
82
+ <li>
83
+ <span>
84
+ <span class="channel-name">{channel.label ?? humanizeKey(channel.channelKind)}</span>
85
+ <span class="channel-ref">{channel.channelRef}</span>
86
+ </span>
87
+ <Badge variant={campaignStatusBadgeVariant(channel.status)} size="sm">
88
+ {humanizeKey(channel.status)}
89
+ </Badge>
90
+ </li>
91
+ {/each}
92
+ </ul>
93
+ </section>
94
+ </Card>
95
+ {/if}
96
+
97
+ {#if pacing}<section aria-label="Budget pacing">{@render pacing()}</section>{/if}
98
+ {#if channelMix}<section aria-label="Channel mix">{@render channelMix()}</section>{/if}
99
+ {@render children?.()}
100
+ </article>
101
+ {/if}
102
+
103
+ <style>
104
+ .campaign-detail {
105
+ display: flex;
106
+ flex-direction: column;
107
+ gap: var(--smrt-spacing-4);
108
+ }
109
+
110
+ .empty,
111
+ .description,
112
+ .channel-ref {
113
+ color: var(--smrt-color-on-surface-variant);
114
+ }
115
+
116
+ .empty,
117
+ .description {
118
+ margin: 0;
119
+ font: var(--smrt-typography-body-medium-font);
120
+ }
121
+
122
+ .header {
123
+ display: flex;
124
+ align-items: flex-start;
125
+ justify-content: space-between;
126
+ gap: var(--smrt-spacing-4);
127
+ }
128
+
129
+ .eyebrow {
130
+ margin: 0;
131
+ color: var(--smrt-color-on-surface-variant);
132
+ font: var(--smrt-typography-label-medium-font);
133
+ }
134
+
135
+ h2,
136
+ h3 {
137
+ margin: 0;
138
+ color: var(--smrt-color-on-surface);
139
+ }
140
+
141
+ h2 {
142
+ font: var(--smrt-typography-headline-small-font);
143
+ }
144
+
145
+ h3 {
146
+ font: var(--smrt-typography-title-medium-font);
147
+ }
148
+
149
+ .description {
150
+ margin-top: var(--smrt-spacing-2);
151
+ }
152
+
153
+ .facts {
154
+ display: grid;
155
+ grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
156
+ gap: var(--smrt-spacing-4);
157
+ margin: var(--smrt-spacing-5) 0 0;
158
+ }
159
+
160
+ .facts dt {
161
+ color: var(--smrt-color-on-surface-variant);
162
+ font: var(--smrt-typography-label-medium-font);
163
+ }
164
+
165
+ .facts dd {
166
+ margin: var(--smrt-spacing-1) 0 0;
167
+ color: var(--smrt-color-on-surface);
168
+ font: var(--smrt-typography-body-large-font);
169
+ font-weight: var(--smrt-typography-weight-medium);
170
+ }
171
+
172
+ .channels h3 {
173
+ margin-bottom: var(--smrt-spacing-3);
174
+ }
175
+
176
+ ul {
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: var(--smrt-spacing-2);
180
+ margin: 0;
181
+ padding: 0;
182
+ list-style: none;
183
+ }
184
+
185
+ li {
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: space-between;
189
+ gap: var(--smrt-spacing-3);
190
+ padding-bottom: var(--smrt-spacing-2);
191
+ border-bottom: 1px solid var(--smrt-color-outline-variant);
192
+ }
193
+
194
+ .channel-name,
195
+ .channel-ref {
196
+ display: block;
197
+ }
198
+
199
+ .channel-name {
200
+ color: var(--smrt-color-on-surface);
201
+ font: var(--smrt-typography-body-medium-font);
202
+ font-weight: var(--smrt-typography-weight-medium);
203
+ }
204
+
205
+ .channel-ref {
206
+ font: var(--smrt-typography-body-small-font);
207
+ }
208
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { CampaignChannelView, CampaignDetailView } from '../types.js';
3
+ export interface Props {
4
+ campaign?: CampaignDetailView | null;
5
+ channels?: CampaignChannelView[];
6
+ locale?: string;
7
+ pacing?: Snippet;
8
+ channelMix?: Snippet;
9
+ children?: Snippet;
10
+ }
11
+ declare const CampaignDetail: import("svelte").Component<Props, {}, "">;
12
+ type CampaignDetail = ReturnType<typeof CampaignDetail>;
13
+ export default CampaignDetail;
14
+ //# sourceMappingURL=CampaignDetail.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampaignDetail.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/CampaignDetail.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAOtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAI3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA8FD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}