@sarimarcus/content-sites-ui 0.5.0 → 0.6.0-rc.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.
- package/CHANGELOG.md +4 -0
- package/README.md +13 -1
- package/package.json +2 -2
- package/src/primitives/Blockquote.astro +30 -0
- package/src/primitives/Button.astro +38 -0
- package/src/primitives/Callout.astro +63 -0
- package/src/primitives/FeatureList.astro +124 -0
- package/src/primitives/LinkArrow.astro +2 -3
- package/src/primitives/SectionHeader.astro +22 -0
- package/src/primitives/index.ts +5 -0
- package/src/primitives/newTab.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All packages in the platform release in lockstep; entries are per release version.
|
|
4
4
|
|
|
5
|
+
## 0.6.0-rc.0 (2026-09-30)
|
|
6
|
+
|
|
7
|
+
- `./primitives`: `Button` (variants `primary`, `secondary`, `hero`, `ghost`, `secondarySolid`; typed anchor passthrough), `Callout` (`tip`, `warning`, `info`, `success`), `FeatureList` (`color` is a theme role; heading only when titled), `Blockquote`, `SectionHeader`; Button and LinkArrow share one new-tab rule (SLA-2821).
|
|
8
|
+
|
|
5
9
|
## 0.5.0 (2026-09-29)
|
|
6
10
|
|
|
7
11
|
- Stable release of 0.5.0-rc.0, unchanged: adopted on all three sites with every page identical outside class attributes and every class change a declared brand→role pair; screenshots unchanged (SLA-2821).
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ text arrives through props and slots.
|
|
|
11
11
|
## Primitives
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, PILL_BASE, isBadgeLength, PRICE_LABEL_MAX } from '@sarimarcus/content-sites-ui/primitives';
|
|
14
|
+
import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, Button, Callout, FeatureList, Blockquote, SectionHeader, PILL_BASE, isBadgeLength, PRICE_LABEL_MAX } from '@sarimarcus/content-sites-ui/primitives';
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
- `Icon`: any Lucide icon by kebab-case name.
|
|
@@ -29,6 +29,18 @@ import { Icon, Badge, Chip, ArrowLabel, SectionDivider, LinkArrow, PILL_BASE, is
|
|
|
29
29
|
- `LinkArrow`: a text link with a trailing arrow. `color` is `primary` (default), `secondary`, `accent` or `onImage`;
|
|
30
30
|
`size` is `sm` (default) or `base`. An `http(s)` href opens in a new tab with `rel="noopener noreferrer"`; an
|
|
31
31
|
explicit `target` (`_blank` or `_self`) or `rel` overrides that. Attributes render as `href target rel class`.
|
|
32
|
+
- `Button`: a link styled as a button. `variant` is `primary` (default, solid), `secondary` (an outline in the
|
|
33
|
+
primary colour), `secondarySolid` (solid in the secondary colour), `hero` (frosted glass over a photo) or `ghost`
|
|
34
|
+
(for dark themes). Same new-tab rule as LinkArrow: an `http(s)` href gets `target="_blank"`, and a new tab gets
|
|
35
|
+
`rel="noopener noreferrer"`, unless `target` or `rel` is given. Attributes render as `href rel target class`,
|
|
36
|
+
then every other anchor attribute (`data-*`, `aria-*`, `slot`) in the order passed.
|
|
37
|
+
- `Callout`: a boxed note with an icon. `type` is `info` (default), `tip`, `warning` or `success`; `title` is optional.
|
|
38
|
+
- `FeatureList`: a checklist or a row of tags from `items` (rendered with `set:html`). `color` is `primary`
|
|
39
|
+
(default), `secondary` or `accent`; `container` boxes it, `compact` tightens it, `headingLevel` sets the heading
|
|
40
|
+
(default `h2`), which renders only for a non-empty `title`; `footer` adds a line under a border.
|
|
41
|
+
- `Blockquote`: a centred pull-quote, `quote` plus an optional `source`.
|
|
42
|
+
- `SectionHeader`: an icon tile (slot `icon`), an `h2` `title` and an optional `action` slot. `iconBg` and
|
|
43
|
+
`iconColor` are raw classes the site supplies.
|
|
32
44
|
- `PILL_BASE`: the shared pill geometry. `isBadgeLength`, `BADGE_MAX_CHARS`, `PRICE_LABEL_MAX`: the label-length
|
|
33
45
|
limits a call site branches on (pill or prose); a price-range schema `.max()` must equal `PRICE_LABEL_MAX`.
|
|
34
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarimarcus/content-sites-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-rc.0",
|
|
4
4
|
"description": "Universal Astro UI components, layout shell and design tokens shared by the content sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"@lucide/astro": "^1.23.0",
|
|
43
43
|
"astro": "^7.0.6",
|
|
44
44
|
"tailwindcss": "^4.1.18",
|
|
45
|
-
"@sarimarcus/content-sites-core": "0.
|
|
45
|
+
"@sarimarcus/content-sites-core": "0.6.0-rc.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Blockquote — editorial pull-quote for article/guide pages.
|
|
4
|
+
*
|
|
5
|
+
* Bold typographic treatment: large display serif, generous whitespace,
|
|
6
|
+
* no card/box/border. The size and space ARE the design.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <Blockquote quote="The market is where you belong." source="Local saying" />
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
quote: string;
|
|
14
|
+
source?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { quote, source, class: className = '' } = Astro.props;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<figure class:list={['py-10 sm:py-14 text-center max-w-measure mx-auto', className]}>
|
|
22
|
+
<blockquote class="text-2xl sm:text-3xl text-foreground/80 leading-snug font-display">
|
|
23
|
+
“{quote}”
|
|
24
|
+
</blockquote>
|
|
25
|
+
{source && (
|
|
26
|
+
<figcaption class="mt-4 text-xs tracking-widest uppercase text-primary/60">
|
|
27
|
+
{source}
|
|
28
|
+
</figcaption>
|
|
29
|
+
)}
|
|
30
|
+
</figure>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { newTab } from './newTab';
|
|
3
|
+
// A link styled as a button. Extends the anchor attributes so data-* (the GA4 tracking hooks) and aria-*
|
|
4
|
+
// reach the <a>, without an index signature that would swallow a mistyped prop name.
|
|
5
|
+
interface Props extends astroHTML.JSX.AnchorHTMLAttributes {
|
|
6
|
+
href: string;
|
|
7
|
+
variant?: 'primary' | 'secondary' | 'secondarySolid' | 'hero' | 'ghost';
|
|
8
|
+
class?: string;
|
|
9
|
+
/** Overrides the http(s) detection, which opens off-site links in a new tab. */
|
|
10
|
+
target?: string;
|
|
11
|
+
/** Explicit rel, required for affiliate/sponsored links. Defaults to 'noopener noreferrer' on a new tab. */
|
|
12
|
+
rel?: string;
|
|
13
|
+
}
|
|
14
|
+
const { href, variant = 'primary', rel, target, class: className = '', ...rest } = Astro.props;
|
|
15
|
+
|
|
16
|
+
const { target: resolvedTarget, rel: resolvedRel } = newTab(href, target, rel);
|
|
17
|
+
|
|
18
|
+
const base = 'inline-flex items-center justify-center gap-2 px-5 py-3 rounded-lg font-medium transition-[background-color,border-color,color,box-shadow,transform] duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary';
|
|
19
|
+
|
|
20
|
+
const variants = {
|
|
21
|
+
primary: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary-strong hover:shadow-md active:translate-y-px',
|
|
22
|
+
// An outline in the primary colour.
|
|
23
|
+
secondary: 'border border-primary/40 text-primary hover:bg-primary/8 hover:border-primary/70',
|
|
24
|
+
secondarySolid: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary-strong hover:shadow-md active:translate-y-px',
|
|
25
|
+
hero: 'text-overlay-foreground bg-overlay-foreground/15 border border-overlay-foreground/30 backdrop-blur-sm shadow-lg hover:bg-overlay-foreground/25 hover:border-overlay-foreground/50 hover:shadow-xl',
|
|
26
|
+
// For dark themes (dark card, light foreground) over a photo or tinted card; on a light theme it reads as a pale outline.
|
|
27
|
+
ghost: 'border border-card/45 bg-card/8 text-foreground hover:border-card hover:bg-card/16 hover:text-foreground',
|
|
28
|
+
};
|
|
29
|
+
---
|
|
30
|
+
<a
|
|
31
|
+
href={href}
|
|
32
|
+
rel={resolvedRel}
|
|
33
|
+
target={resolvedTarget}
|
|
34
|
+
class:list={[base, variants[variant], className]}
|
|
35
|
+
{...rest}
|
|
36
|
+
>
|
|
37
|
+
<slot />
|
|
38
|
+
</a>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Callout — highlighted info box for tips, warnings, and notices.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <Callout type="tip" title="Good to know">
|
|
7
|
+
* Content here...
|
|
8
|
+
* </Callout>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Icon from './Icon.astro';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
type?: 'tip' | 'warning' | 'info' | 'success';
|
|
15
|
+
title?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { type = 'info', title, class: className = '' } = Astro.props;
|
|
20
|
+
|
|
21
|
+
const styles: Record<string, { border: string; bg: string; icon: string; iconName: string }> = {
|
|
22
|
+
tip: {
|
|
23
|
+
border: 'border-primary/40',
|
|
24
|
+
bg: 'bg-primary/5',
|
|
25
|
+
icon: 'text-primary',
|
|
26
|
+
iconName: 'star',
|
|
27
|
+
},
|
|
28
|
+
warning: {
|
|
29
|
+
border: 'border-accent/40',
|
|
30
|
+
bg: 'bg-accent/5',
|
|
31
|
+
icon: 'text-accent-strong',
|
|
32
|
+
iconName: 'sun',
|
|
33
|
+
},
|
|
34
|
+
info: {
|
|
35
|
+
border: 'border-secondary/40',
|
|
36
|
+
bg: 'bg-secondary/5',
|
|
37
|
+
icon: 'text-secondary',
|
|
38
|
+
iconName: 'info',
|
|
39
|
+
},
|
|
40
|
+
// `success` adds no fourth hue: it is the strong end of the secondary role over the muted surface,
|
|
41
|
+
// distinct from `info`'s tinted box, with `circle-check` carrying the meaning.
|
|
42
|
+
success: {
|
|
43
|
+
border: 'border-secondary-strong/50',
|
|
44
|
+
bg: 'bg-muted',
|
|
45
|
+
icon: 'text-secondary-strong',
|
|
46
|
+
iconName: 'circle-check',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const s = styles[type] || styles.info;
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
<div class:list={[`p-4 border ${s.border} ${s.bg} rounded-lg`, className]}>
|
|
54
|
+
<div class="flex gap-3">
|
|
55
|
+
<Icon name={s.iconName} class={`w-5 h-5 ${s.icon} shrink-0 mt-0.5`} />
|
|
56
|
+
<div class="min-w-0">
|
|
57
|
+
{title && <p class="font-medium text-foreground mb-1">{title}</p>}
|
|
58
|
+
<div class="text-sm text-muted-foreground leading-relaxed">
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* FeatureList — reusable list display for specialties, ingredients, features.
|
|
4
|
+
*
|
|
5
|
+
* `items[]` and `footer` render with set:html, so callers may pass inline markup
|
|
6
|
+
* (already-parsed markdown from parseMarkdownInline) or plain text. Plain strings
|
|
7
|
+
* are unaffected: at the time of the change the 9,923 item strings in content
|
|
8
|
+
* JSON contained no angle brackets and no entity-like ampersands, and the
|
|
9
|
+
* rendered list output was byte-identical across all 462 list items on the three
|
|
10
|
+
* active sites. Callers passing author-written markdown must parse it first —
|
|
11
|
+
* see the `checklist` branch of the site's interlude dispatcher.
|
|
12
|
+
*
|
|
13
|
+
* The heading renders only for a non-empty `title`.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* <FeatureList title="Specialities" items={bar.specialties} icon="star" />
|
|
17
|
+
* <FeatureList title="House specialities" items={restaurant.specialties} variant="tags" color="accent" container />
|
|
18
|
+
* <FeatureList title="Main ingredients" items={dish.mainIngredients} icon="star" color="accent" container footer="<strong>Allergens:</strong> gluten, dairy" />
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import Icon from './Icon.astro';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
title?: string;
|
|
25
|
+
items: string[];
|
|
26
|
+
variant?: 'checklist' | 'tags';
|
|
27
|
+
color?: 'primary' | 'accent' | 'secondary';
|
|
28
|
+
icon?: string;
|
|
29
|
+
container?: boolean;
|
|
30
|
+
/** Tighter padding/margin for space-constrained contexts (e.g. article interludes). */
|
|
31
|
+
compact?: boolean;
|
|
32
|
+
footer?: string;
|
|
33
|
+
headingLevel?: 'h2' | 'h3' | 'h4';
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
title,
|
|
39
|
+
items,
|
|
40
|
+
variant = 'checklist',
|
|
41
|
+
color = 'primary',
|
|
42
|
+
icon,
|
|
43
|
+
container = false,
|
|
44
|
+
compact = false,
|
|
45
|
+
footer,
|
|
46
|
+
headingLevel = 'h2',
|
|
47
|
+
class: className = '',
|
|
48
|
+
} = Astro.props;
|
|
49
|
+
|
|
50
|
+
const Heading = headingLevel;
|
|
51
|
+
|
|
52
|
+
const colorMap = {
|
|
53
|
+
primary: {
|
|
54
|
+
heading: 'text-primary',
|
|
55
|
+
icon: 'text-primary',
|
|
56
|
+
containerBg: 'bg-primary/5 border border-primary/20',
|
|
57
|
+
itemBg: 'bg-muted/30',
|
|
58
|
+
tagBg: 'bg-primary/10',
|
|
59
|
+
footerBorder: 'border-primary/10',
|
|
60
|
+
},
|
|
61
|
+
accent: {
|
|
62
|
+
heading: 'text-accent',
|
|
63
|
+
icon: 'text-accent',
|
|
64
|
+
containerBg: 'bg-accent/5 border border-accent/20',
|
|
65
|
+
itemBg: 'bg-card border border-border',
|
|
66
|
+
tagBg: 'bg-accent/10',
|
|
67
|
+
footerBorder: 'border-accent/10',
|
|
68
|
+
},
|
|
69
|
+
secondary: {
|
|
70
|
+
heading: 'text-secondary',
|
|
71
|
+
icon: 'text-secondary',
|
|
72
|
+
containerBg: 'bg-secondary/5 border border-secondary/20',
|
|
73
|
+
itemBg: 'bg-muted/30',
|
|
74
|
+
tagBg: 'bg-secondary/10',
|
|
75
|
+
footerBorder: 'border-secondary/10',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const colors = colorMap[color];
|
|
80
|
+
|
|
81
|
+
const titleClass = container
|
|
82
|
+
? `text-xl font-display font-semibold text-foreground ${compact ? 'mb-3' : 'mb-5'} flex items-center gap-2`
|
|
83
|
+
: 'text-xl font-medium mb-5 flex items-center gap-2 font-display';
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
{items.length > 0 && (
|
|
87
|
+
<div
|
|
88
|
+
class:list={[
|
|
89
|
+
container ? `${compact ? 'p-4 sm:p-5 lg:p-6' : 'p-6 lg:p-8'} ${colors.containerBg} rounded-xl` : '',
|
|
90
|
+
className,
|
|
91
|
+
]}
|
|
92
|
+
data-pagefind-ignore
|
|
93
|
+
>
|
|
94
|
+
{title && (
|
|
95
|
+
<Heading class={titleClass}>
|
|
96
|
+
{icon && <Icon name={icon} class={`w-5 h-5 ${colors.icon}`} />}
|
|
97
|
+
{container ? title : <span class={colors.heading}>{title}</span>}
|
|
98
|
+
</Heading>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{variant === 'checklist' ? (
|
|
102
|
+
<ul class:list={['grid grid-cols-1 sm:grid-cols-2', compact ? 'gap-2' : 'gap-2.5']}>
|
|
103
|
+
{items.map((item) => (
|
|
104
|
+
<li class:list={['flex items-start gap-3 rounded-lg', compact ? 'px-3 py-2.5' : 'px-4 py-3', colors.itemBg]}>
|
|
105
|
+
<Icon name="check" class={`w-5 h-5 ${colors.icon} shrink-0 mt-0.5`} />
|
|
106
|
+
<span class="text-sm text-foreground leading-relaxed" set:html={item} />
|
|
107
|
+
</li>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
) : (
|
|
111
|
+
<div class="flex flex-wrap gap-2">
|
|
112
|
+
{items.map((item) => (
|
|
113
|
+
<span class:list={['px-4 py-2 rounded-lg text-sm font-medium text-foreground', colors.tagBg]} set:html={item} />
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{footer && (
|
|
119
|
+
<div class={`mt-5 pt-5 border-t ${colors.footerBorder}`}>
|
|
120
|
+
<p class="text-base text-muted-foreground" set:html={footer} />
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { newTab } from './newTab';
|
|
2
3
|
interface Props {
|
|
3
4
|
href: string;
|
|
4
5
|
size?: 'sm' | 'base';
|
|
@@ -11,9 +12,7 @@ interface Props {
|
|
|
11
12
|
}
|
|
12
13
|
const { href, size = 'sm', color = 'primary', target: targetProp, rel: relProp, class: className = '' } = Astro.props;
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
const target = targetProp ?? (isExternal ? '_blank' : undefined);
|
|
16
|
-
const rel = relProp ?? (target === '_blank' ? 'noopener noreferrer' : undefined);
|
|
15
|
+
const { target, rel } = newTab(href, targetProp, relProp);
|
|
17
16
|
|
|
18
17
|
const sizeClass = size === 'base' ? 'text-base font-medium' : 'text-sm font-medium';
|
|
19
18
|
const colorClass = {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
/** Background colour class for the icon tile, owned by the site. */
|
|
5
|
+
iconBg: string;
|
|
6
|
+
/** Text colour class for the icon, owned by the site. */
|
|
7
|
+
iconColor: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { title, iconBg, iconColor, class: className = '' } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class:list={["flex items-center gap-3 mb-8", className]}>
|
|
15
|
+
<div class:list={["w-10 h-10 rounded-lg flex items-center justify-center", iconBg]}>
|
|
16
|
+
<span class:list={["w-5 h-5 [&>svg]:w-full [&>svg]:h-full", iconColor]}>
|
|
17
|
+
<slot name="icon" />
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
<h2 class="text-2xl sm:text-3xl font-normal font-display">{title}</h2>
|
|
21
|
+
<slot name="action" />
|
|
22
|
+
</div>
|
package/src/primitives/index.ts
CHANGED
|
@@ -5,5 +5,10 @@ export { default as Chip } from './Chip.astro';
|
|
|
5
5
|
export { default as ArrowLabel } from './ArrowLabel.astro';
|
|
6
6
|
export { default as SectionDivider } from './SectionDivider.astro';
|
|
7
7
|
export { default as LinkArrow } from './LinkArrow.astro';
|
|
8
|
+
export { default as Button } from './Button.astro';
|
|
9
|
+
export { default as Callout } from './Callout.astro';
|
|
10
|
+
export { default as FeatureList } from './FeatureList.astro';
|
|
11
|
+
export { default as Blockquote } from './Blockquote.astro';
|
|
12
|
+
export { default as SectionHeader } from './SectionHeader.astro';
|
|
8
13
|
export { PILL_BASE } from './pillRecipe';
|
|
9
14
|
export { BADGE_MAX_CHARS, PRICE_LABEL_MAX, isBadgeLength } from './badgeText';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Link target and rel shared by Button and LinkArrow: an http(s) href opens in a new tab unless `target` is given,
|
|
2
|
+
* and a new tab gets `noopener noreferrer` unless `rel` is given. */
|
|
3
|
+
export function newTab(href: string, target?: string, rel?: string) {
|
|
4
|
+
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
5
|
+
const resolvedTarget = target ?? (isExternal ? '_blank' : undefined);
|
|
6
|
+
return { target: resolvedTarget, rel: rel ?? (resolvedTarget === '_blank' ? 'noopener noreferrer' : undefined) };
|
|
7
|
+
}
|