@happyvertical/smrt-ui 0.34.9 → 0.35.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.
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Shows a label with a prominent value, optionally with a count badge
|
|
6
6
|
* and link functionality.
|
|
7
7
|
*/
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
8
9
|
import { ripple } from '../../actions/ripple.js';
|
|
9
10
|
import { Icon } from '../display/index.js';
|
|
10
11
|
|
|
@@ -19,8 +20,18 @@ export interface Props {
|
|
|
19
20
|
highlight?: boolean;
|
|
20
21
|
/** Make card clickable with href */
|
|
21
22
|
href?: string;
|
|
22
|
-
/** Optional icon
|
|
23
|
+
/** Optional icon name, rendered via the {@link Icon} component. */
|
|
23
24
|
icon?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Optional custom icon content, rendered as a Svelte snippet. Use this
|
|
27
|
+
* escape hatch for bespoke icon markup. It replaces the former raw-SVG
|
|
28
|
+
* **string** support, removed in #1591: SummaryCard no longer renders any
|
|
29
|
+
* consumer-supplied value via `{@html}` (an XSS sink). `iconContent` is
|
|
30
|
+
* rendered as a snippet, so the trust boundary moves to the consumer — the
|
|
31
|
+
* snippet's contents are whatever the consumer authors. When both
|
|
32
|
+
* `iconContent` and `icon` are provided, `iconContent` wins.
|
|
33
|
+
*/
|
|
34
|
+
iconContent?: Snippet;
|
|
24
35
|
/** Value color variant */
|
|
25
36
|
variant?: 'default' | 'success' | 'warning' | 'danger';
|
|
26
37
|
}
|
|
@@ -32,6 +43,7 @@ const {
|
|
|
32
43
|
highlight = false,
|
|
33
44
|
href,
|
|
34
45
|
icon,
|
|
46
|
+
iconContent,
|
|
35
47
|
variant = 'default',
|
|
36
48
|
}: Props = $props();
|
|
37
49
|
|
|
@@ -48,10 +60,16 @@ const valueColorClass = $derived.by(() => {
|
|
|
48
60
|
return 'color-default';
|
|
49
61
|
}
|
|
50
62
|
});
|
|
51
|
-
|
|
52
|
-
const isSvg = $derived(icon?.startsWith('<svg'));
|
|
53
63
|
</script>
|
|
54
64
|
|
|
65
|
+
{#snippet cardIcon()}
|
|
66
|
+
{#if iconContent}
|
|
67
|
+
<span class="card-icon">{@render iconContent()}</span>
|
|
68
|
+
{:else if icon}
|
|
69
|
+
<span class="card-icon"><Icon name={icon} size={24} /></span>
|
|
70
|
+
{/if}
|
|
71
|
+
{/snippet}
|
|
72
|
+
|
|
55
73
|
{#if href}
|
|
56
74
|
<a
|
|
57
75
|
class="summary-card"
|
|
@@ -60,15 +78,7 @@ const isSvg = $derived(icon?.startsWith('<svg'));
|
|
|
60
78
|
{href}
|
|
61
79
|
use:ripple
|
|
62
80
|
>
|
|
63
|
-
{
|
|
64
|
-
<span class="card-icon">
|
|
65
|
-
{#if isSvg}
|
|
66
|
-
{@html icon}
|
|
67
|
-
{:else}
|
|
68
|
-
<Icon name={icon} size={24} />
|
|
69
|
-
{/if}
|
|
70
|
-
</span>
|
|
71
|
-
{/if}
|
|
81
|
+
{@render cardIcon()}
|
|
72
82
|
<div class="card-content">
|
|
73
83
|
<span class="card-label">
|
|
74
84
|
{label}
|
|
@@ -84,15 +94,7 @@ const isSvg = $derived(icon?.startsWith('<svg'));
|
|
|
84
94
|
</a>
|
|
85
95
|
{:else}
|
|
86
96
|
<div class="summary-card" class:highlight>
|
|
87
|
-
{
|
|
88
|
-
<span class="card-icon">
|
|
89
|
-
{#if isSvg}
|
|
90
|
-
{@html icon}
|
|
91
|
-
{:else}
|
|
92
|
-
<Icon name={icon} size={24} />
|
|
93
|
-
{/if}
|
|
94
|
-
</span>
|
|
95
|
-
{/if}
|
|
97
|
+
{@render cardIcon()}
|
|
96
98
|
<div class="card-content">
|
|
97
99
|
<span class="card-label">
|
|
98
100
|
{label}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SummaryCard - Statistic/summary display card refactored for Material 3
|
|
3
|
+
*
|
|
4
|
+
* Shows a label with a prominent value, optionally with a count badge
|
|
5
|
+
* and link functionality.
|
|
6
|
+
*/
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
1
8
|
export interface Props {
|
|
2
9
|
/** Card label */
|
|
3
10
|
label: string;
|
|
@@ -9,8 +16,18 @@ export interface Props {
|
|
|
9
16
|
highlight?: boolean;
|
|
10
17
|
/** Make card clickable with href */
|
|
11
18
|
href?: string;
|
|
12
|
-
/** Optional icon
|
|
19
|
+
/** Optional icon name, rendered via the {@link Icon} component. */
|
|
13
20
|
icon?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional custom icon content, rendered as a Svelte snippet. Use this
|
|
23
|
+
* escape hatch for bespoke icon markup. It replaces the former raw-SVG
|
|
24
|
+
* **string** support, removed in #1591: SummaryCard no longer renders any
|
|
25
|
+
* consumer-supplied value via `{@html}` (an XSS sink). `iconContent` is
|
|
26
|
+
* rendered as a snippet, so the trust boundary moves to the consumer — the
|
|
27
|
+
* snippet's contents are whatever the consumer authors. When both
|
|
28
|
+
* `iconContent` and `icon` are provided, `iconContent` wins.
|
|
29
|
+
*/
|
|
30
|
+
iconContent?: Snippet;
|
|
14
31
|
/** Value color variant */
|
|
15
32
|
variant?: 'default' | 'success' | 'warning' | 'danger';
|
|
16
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SummaryCard.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/layout/SummaryCard.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SummaryCard.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/layout/SummaryCard.svelte.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;CACxD;AA4ED,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* axe-clean output across a couple of states.
|
|
7
7
|
*/
|
|
8
8
|
import { render, screen } from '@testing-library/svelte';
|
|
9
|
+
import { createRawSnippet } from 'svelte';
|
|
9
10
|
import { describe, expect, it } from 'vitest';
|
|
10
11
|
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
11
12
|
import SummaryCard from '../SummaryCard.svelte';
|
|
@@ -37,15 +38,36 @@ describe('SummaryCard', () => {
|
|
|
37
38
|
});
|
|
38
39
|
expect(container.querySelector('.card-count')).toBeNull();
|
|
39
40
|
});
|
|
40
|
-
it('renders
|
|
41
|
+
it('renders an icon via the Icon component when icon is a known name', () => {
|
|
42
|
+
const { container } = render(SummaryCard, {
|
|
43
|
+
// `search` is a real Icon preset, so the rendered <path> must carry a
|
|
44
|
+
// non-empty `d` — proving the icon-name path actually resolved (an
|
|
45
|
+
// unknown name would still render an <svg> but with an empty path).
|
|
46
|
+
props: { label: 'Visitors', value: 99, icon: 'search' },
|
|
47
|
+
});
|
|
48
|
+
const iconPath = container.querySelector('.card-icon svg path');
|
|
49
|
+
expect(iconPath?.getAttribute('d')).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
it('does NOT inject a raw SVG string as markup (no {@html} sink) — #1591', () => {
|
|
41
52
|
const { container } = render(SummaryCard, {
|
|
42
53
|
props: {
|
|
43
54
|
label: 'Visitors',
|
|
44
55
|
value: 99,
|
|
45
|
-
|
|
56
|
+
// A would-be XSS payload: the string is forwarded to <Icon name>, never
|
|
57
|
+
// rendered as HTML, so the attacker-controlled element must be absent.
|
|
58
|
+
icon: '<svg data-evil="1" viewBox="0 0 24 24"></svg>',
|
|
46
59
|
},
|
|
47
60
|
});
|
|
48
|
-
expect(container.querySelector('
|
|
61
|
+
expect(container.querySelector('svg[data-evil]')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
it('renders custom icon markup via the iconContent snippet', () => {
|
|
64
|
+
const iconContent = createRawSnippet(() => ({
|
|
65
|
+
render: () => '<span data-testid="custom-icon">★</span>',
|
|
66
|
+
}));
|
|
67
|
+
const { container } = render(SummaryCard, {
|
|
68
|
+
props: { label: 'Starred', value: 1, iconContent },
|
|
69
|
+
});
|
|
70
|
+
expect(container.querySelector('.card-icon [data-testid="custom-icon"]')).not.toBeNull();
|
|
49
71
|
});
|
|
50
72
|
it('renders as a clickable link when href is provided', () => {
|
|
51
73
|
render(SummaryCard, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.1",
|
|
4
4
|
"description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"dependencies": {
|
|
116
116
|
"esm-env": "^1.2.2",
|
|
117
|
-
"@happyvertical/smrt-types": "0.
|
|
117
|
+
"@happyvertical/smrt-types": "0.35.1"
|
|
118
118
|
},
|
|
119
119
|
"peerDependencies": {
|
|
120
120
|
"svelte": "^5.18.2"
|