@helloimjolopez-pathway/pathway-components 0.9.0 → 0.10.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/README.md +148 -0
- package/components/avatar/avatar.jsx +167 -0
- package/dist/pathway-components.js +36 -36
- package/package.json +10 -2
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Pathway Components
|
|
2
|
+
|
|
3
|
+
The Pathway Design System's UI components, for Ministry Brands products. Each one ships two ways from this one package:
|
|
4
|
+
|
|
5
|
+
- **Web components** (custom elements): one script tag, then plain HTML tags. Works in Blazor, Razor, Angular, Vue, React, jQuery or plain HTML.
|
|
6
|
+
- **React source** (`.jsx`): for React apps that want the components as React components.
|
|
7
|
+
|
|
8
|
+
The same components are on NuGet as [`Pathway.Components`](https://www.nuget.org/packages/Pathway.Components), at the same version.
|
|
9
|
+
|
|
10
|
+
| | |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Storybook (every component, live, with its tokens) | https://potential-bassoon-voz982n.pages.github.io/storybook/ (sign in with your Ministry Brands GitHub account) |
|
|
13
|
+
| Source repo | https://github.com/ministry-brands-llc/jolopez-collab-pathwayds (visible to everyone in the Ministry Brands GitHub organisation) |
|
|
14
|
+
| Developer guide, all stacks | [`docs/consuming-from-any-stack.md`](https://github.com/ministry-brands-llc/jolopez-collab-pathwayds/blob/main/docs/consuming-from-any-stack.md) |
|
|
15
|
+
| Tokens package (required) | [`@helloimjolopez-pathway/pathway-tokens`](https://www.npmjs.com/package/@helloimjolopez-pathway/pathway-tokens) · NuGet [`Pathway.DesignTokens`](https://www.nuget.org/packages/Pathway.DesignTokens) |
|
|
16
|
+
|
|
17
|
+
## What is in it
|
|
18
|
+
|
|
19
|
+
| Component | Web component | React import (`@helloimjolopez-pathway/pathway-components/…`) |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| NavShell (TopNav + SideNav + page area) | `<pathway-nav-shell>` | `…/nav-shell` → `NavShell` |
|
|
22
|
+
| TopNav | `<pathway-top-nav>` | `…/top-nav` → `TopNav` |
|
|
23
|
+
| SideNav | `<pathway-sidenav>` | `…/sidenav` → `SideNav` |
|
|
24
|
+
| Accounting side nav | `<pathway-accounting-sidenav>` | `…/accounting-side-nav` → `AccountingSideNav` |
|
|
25
|
+
| Page Template | `<pathway-page-template>` | `…/page-template` → `PageTemplate` |
|
|
26
|
+
| Button | `<pathway-button>` | `…/button` → `Button` |
|
|
27
|
+
| Badge | `<pathway-badge>` | `…/badge` → `Badge` |
|
|
28
|
+
| Tooltip | `<pathway-tooltip>` | `…/tooltip` → `Tooltip` |
|
|
29
|
+
| Toggle Button | `<pathway-toggle-button>` | `…/toggle-button` → `ToggleButton` |
|
|
30
|
+
| Radio | `<pathway-radio-group>` | `…/radio` → `RadioGroup` |
|
|
31
|
+
| BottomSheet | `<pathway-bottom-sheet>` | `…/bottom-sheet` → `BottomSheet` |
|
|
32
|
+
| Scroll-to-Enable | `<pathway-scroll-to-enable>` | `…/scroll-to-enable` → `ScrollToEnable` |
|
|
33
|
+
| Dropdown | (React only) | `…/dropdown` → `Dropdown` |
|
|
34
|
+
|
|
35
|
+
The web components are in three bundles under `dist/`:
|
|
36
|
+
|
|
37
|
+
| File | Registers |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `dist/pathway-components.js` | every tag above |
|
|
40
|
+
| `dist/pathway-sidenav.js` | `<pathway-sidenav>` only |
|
|
41
|
+
| `dist/pathway-accounting-sidenav.js` | `<pathway-accounting-sidenav>` only |
|
|
42
|
+
|
|
43
|
+
Each bundle is self-contained: React is inside it, so the host page needs no React.
|
|
44
|
+
|
|
45
|
+
## Set up a page (any stack)
|
|
46
|
+
|
|
47
|
+
Three things go on the host page, once.
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<!-- 1. Tokens: every colour, size and font in the components is a CSS custom property from this file -->
|
|
51
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens@12/dist/index.css">
|
|
52
|
+
|
|
53
|
+
<!-- 2. Fonts -->
|
|
54
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Red+Hat+Text:wght@300;400;500;600;700&display=swap">
|
|
55
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200">
|
|
56
|
+
|
|
57
|
+
<!-- 3. The components -->
|
|
58
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-components@0.9/dist/pathway-components.js"></script>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Pin an exact version in production (`@0.9.1`, not `@0.9`).
|
|
62
|
+
|
|
63
|
+
From npm instead of the CDN:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @helloimjolopez-pathway/pathway-components @helloimjolopez-pathway/pathway-tokens
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import "@helloimjolopez-pathway/pathway-tokens/css";
|
|
71
|
+
import "@helloimjolopez-pathway/pathway-components/elements"; // registers every tag
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
From NuGet (.NET): the files are static web assets, so a Razor or Blazor page references them as
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<link rel="stylesheet" href="_content/Pathway.DesignTokens/index.css">
|
|
78
|
+
<script type="module" src="_content/Pathway.Components/pathway-components.js"></script>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Light and Dark, Amplify and NewCo.** Dark is `data-theme="dark"` on any element (it applies to everything inside). NewCo is `data-brand="newco"` plus the NewCo layer: `@helloimjolopez-pathway/pathway-tokens@12/dist/brands/newco/index.css` after the Amplify one.
|
|
82
|
+
|
|
83
|
+
## The Accounting nav shell
|
|
84
|
+
|
|
85
|
+
The whole Accounting frame (TopNav without search or actions, the Accounting side nav with the Financials mark and application switcher, and the page area) is one tag:
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<pathway-nav-shell app-switcher active-app-id="financials-home" active-nav-id="dashboard">
|
|
89
|
+
<main>Your page</main>
|
|
90
|
+
</pathway-nav-shell>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Give it your real data as JavaScript properties, and listen for its events:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const shell = document.querySelector("pathway-nav-shell");
|
|
97
|
+
|
|
98
|
+
shell.org = { id: "org-1", name: "Grace Community Church", campus: "Knoxville", initials: "GC" };
|
|
99
|
+
shell.orgs = [ /* same shape, for the org switcher */ ];
|
|
100
|
+
shell.user = { name: "Jo Lopez", initials: "JL", email: "jo@example.org" };
|
|
101
|
+
// Optional: replace the built-in Accounting applications and their navs.
|
|
102
|
+
// shell.appSwitcher = { label, icon, home: { id, label, navItems }, apps: [ { id, label, navItems } ] };
|
|
103
|
+
|
|
104
|
+
shell.addEventListener("pathway-navigate", (e) => router.go(e.detail.id)); // a nav item was chosen
|
|
105
|
+
shell.addEventListener("pathway-app-select", (e) => loadApp(e.detail.id)); // an application was chosen
|
|
106
|
+
shell.addEventListener("pathway-org-select", (e) => switchOrg(e.detail.id));
|
|
107
|
+
shell.addEventListener("pathway-module-select", (e) => openModule(e.detail.id));
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Attributes on `<pathway-nav-shell>`:
|
|
111
|
+
|
|
112
|
+
| Attribute | What it does |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `app-switcher` | Turns the side nav's application switcher on. With it on, TopNav hides search and actions unless you say otherwise |
|
|
115
|
+
| `active-app-id` | The current application; updated when someone picks another |
|
|
116
|
+
| `active-nav-id` | The current page in the side nav; updated on navigation |
|
|
117
|
+
| `active-module-id` | The current module in TopNav (default `home`) |
|
|
118
|
+
| `default-collapsed` | Start with the side nav as the 72 px rail on desktop |
|
|
119
|
+
| `hide-search`, `hide-actions` | Hide TopNav's search, or its action icons |
|
|
120
|
+
|
|
121
|
+
Other properties: `modules`, `navItems` or `navSections` (for a non-Accounting module). Slots: the unnamed slot is the page; `nav-header-end` and `nav-footer` add to the side nav.
|
|
122
|
+
|
|
123
|
+
Only the side nav? Use `<pathway-accounting-sidenav active-app-id="financials-home">`, with the same `appSwitcher` property and the `pathway-app-select`, `pathway-navigate` and `pathway-collapse-change` events.
|
|
124
|
+
|
|
125
|
+
## React
|
|
126
|
+
|
|
127
|
+
```jsx
|
|
128
|
+
import "@helloimjolopez-pathway/pathway-tokens/css";
|
|
129
|
+
import { NavShell } from "@helloimjolopez-pathway/pathway-components/nav-shell";
|
|
130
|
+
import { ACCOUNTING_APP_SWITCHER } from "@helloimjolopez-pathway/pathway-components/accounting-side-nav";
|
|
131
|
+
|
|
132
|
+
<NavShell appSwitcher={ACCOUNTING_APP_SWITCHER} activeNavId="dashboard"
|
|
133
|
+
org={org} orgs={orgs} user={user}
|
|
134
|
+
onNavigate={(id) => go(id)} onOrgSelect={(id) => switchOrg(id)}>
|
|
135
|
+
<YourPage />
|
|
136
|
+
</NavShell>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`ACCOUNTING_APP_SWITCHER` is the built-in Accounting data (applications and their navs); pass your own object of the same shape to change it.
|
|
140
|
+
|
|
141
|
+
React and React DOM 17 or later are peer dependencies. The `.jsx` files are shipped as source, so your bundler compiles them (Vite, webpack with Babel, Next.js and esbuild all do by default).
|
|
142
|
+
|
|
143
|
+
## Taking it into your product
|
|
144
|
+
|
|
145
|
+
- **Depend on the package and pin the version.** You get fixes by bumping it, and nothing changes under you until you do. Every release is in the repo's history and on npm.
|
|
146
|
+
- **What your review will find:** the web component bundles contain React, React DOM and `react-aria-components`; the components make no network calls and store nothing; the only external requests are the fonts above, which you can self-host. Licence: CC0-1.0.
|
|
147
|
+
- **Accessibility is built in and should still be tested in your product:** keyboard focus rings, focus returning to triggers, Esc to close, 44 px touch targets, and hidden nav rows kept out of the tab order.
|
|
148
|
+
- **Report a problem or ask for a change** in the repo, where the component's spec (`components/<name>/<name>-spec.md`) records how it is meant to behave.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar: a person's or a household's photo, initials or icon. Avatar Group:
|
|
3
|
+
* several people in one spot, up to three overlapping, then +N.
|
|
4
|
+
*
|
|
5
|
+
* Figma `Avatar`, set 68:659, and `Avatar Group`, set 9704:74872, page
|
|
6
|
+
* "↳ Avatar" (5926:18314). Spec: components/avatar/avatar-spec.md.
|
|
7
|
+
*
|
|
8
|
+
* Props mirror the Figma properties (CLAUDE.md §10 rule 9):
|
|
9
|
+
* Size size XXXS 16, XXS 20, XS 24, S 32, M 40, L 56, XL 96.
|
|
10
|
+
* One pixel size per name for every content type.
|
|
11
|
+
* Content content "Image" | "Text" | "Icon". Optional: when omitted
|
|
12
|
+
* it follows the data, src, else text, else icon.
|
|
13
|
+
* An Image whose photo fails to load falls back the
|
|
14
|
+
* same way (Radix Avatar's Fallback model).
|
|
15
|
+
* Shape shape "Circle" for people, "Square" for households and
|
|
16
|
+
* things. Square radius grows with the size.
|
|
17
|
+
* Style avatarStyle "Fill" (brand tint) | "Stroke" (outline, dense
|
|
18
|
+
* lists). Image is Fill only.
|
|
19
|
+
* Text text the initials, at most two letters.
|
|
20
|
+
* Icon icon a Material Symbols Rounded ligature, FILL 1.
|
|
21
|
+
* "person", or "home" for a household.
|
|
22
|
+
* Show Badge showBadge the ringed status dot. badgeStatus picks Positive
|
|
23
|
+
* (online) or Attention (away). Always say the
|
|
24
|
+
* status in words too; the dot is never announced.
|
|
25
|
+
*
|
|
26
|
+
* ACCESSIBILITY. Not interactive and never focusable. Pass `name` when the
|
|
27
|
+
* Avatar stands alone: it becomes role img with that name. When the name is
|
|
28
|
+
* shown beside it, leave `name` out and the Avatar is hidden from screen
|
|
29
|
+
* readers, so the name is not read twice.
|
|
30
|
+
*/
|
|
31
|
+
import React, { useEffect, useState } from "react";
|
|
32
|
+
|
|
33
|
+
const C = (n) => `var(--semantic-color-${n})`;
|
|
34
|
+
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
35
|
+
const Y = (n) => `var(--semantic-type-${n})`;
|
|
36
|
+
|
|
37
|
+
export const AVATAR_SIZES = ["XXXS", "XXS", "XS", "S", "M", "L", "XL"];
|
|
38
|
+
export const AVATAR_CONTENTS = ["Image", "Text", "Icon"];
|
|
39
|
+
export const AVATAR_SHAPES = ["Circle", "Square"];
|
|
40
|
+
export const AVATAR_STYLES = ["Fill", "Stroke"];
|
|
41
|
+
export const AVATAR_BADGE_STATUS = ["Positive", "Attention"];
|
|
42
|
+
export const AVATAR_GROUP_SIZES = ["XS", "S", "M"];
|
|
43
|
+
|
|
44
|
+
/** Per size: the box, initials type, square radius, icon and badge. Sizes are measurements, not tokens. */
|
|
45
|
+
export const SIZES = {
|
|
46
|
+
XXXS: { px: 16, fontSize: Y("font-size-xxxs"), lineHeight: Y("line-height-xxxs-single"), radius: U("cornerradius-small"), icon: 10, badge: 6 },
|
|
47
|
+
XXS: { px: 20, fontSize: Y("font-size-xxxs"), lineHeight: Y("line-height-xxxs-single"), radius: U("cornerradius-small"), icon: 12, badge: 7 },
|
|
48
|
+
XS: { px: 24, fontSize: Y("font-size-xxs"), lineHeight: Y("line-height-xxs-single"), radius: U("cornerradius-small"), icon: 14, badge: 8 },
|
|
49
|
+
S: { px: 32, fontSize: Y("font-size-s"), lineHeight: Y("line-height-s-single"), radius: U("cornerradius-small"), icon: 20, badge: 10 },
|
|
50
|
+
M: { px: 40, fontSize: Y("font-size-r"), lineHeight: Y("line-height-r-single"), radius: U("cornerradius-base"), icon: 24, badge: 12 },
|
|
51
|
+
L: { px: 56, fontSize: Y("font-size-l"), lineHeight: Y("line-height-l-single"), radius: U("cornerradius-large"), icon: 34, badge: 16 },
|
|
52
|
+
XL: { px: 96, fontSize: Y("font-size-xxl"), lineHeight: Y("line-height-xxl-single"), radius: U("cornerradius-xlarge"), icon: 58, badge: 28 },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Colours and widths, all semantic tokens. */
|
|
56
|
+
export const T = {
|
|
57
|
+
fill: C("fill-static-brand-subtle"), // Text and Icon, Style Fill
|
|
58
|
+
foreground: C("foreground-static-brand-on-subtle"), // initials and icon
|
|
59
|
+
imageFill: C("fill-static-neutral-faint"), // behind the photo
|
|
60
|
+
stroke: C("stroke-static-neutral-base"), // Style Stroke
|
|
61
|
+
strokeWidth: U("borderwidth-thin"),
|
|
62
|
+
circle: U("cornerradius-full"),
|
|
63
|
+
badgeRing: C("stroke-static-neutral-mono"),
|
|
64
|
+
badgeRingW: U("borderwidth-base"),
|
|
65
|
+
badge: { Positive: C("fill-static-positive-strong"), Attention: C("fill-static-attention-strong") },
|
|
66
|
+
groupRing: C("fill-surface-elevated"), // Avatar Group, separates overlapping avatars
|
|
67
|
+
groupRingW: U("borderwidth-thick"),
|
|
68
|
+
family: Y("family-brand"),
|
|
69
|
+
weight: Y("weight-semibold"),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** First letters of the first and last name, at most two. */
|
|
73
|
+
export function initialsFrom(name = "") {
|
|
74
|
+
const parts = String(name).trim().split(/\s+/).filter(Boolean);
|
|
75
|
+
if (!parts.length) return "";
|
|
76
|
+
const first = parts[0][0] || "";
|
|
77
|
+
const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
|
|
78
|
+
return (first + last).toUpperCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function Avatar({
|
|
82
|
+
size = "M",
|
|
83
|
+
content,
|
|
84
|
+
shape = "Circle",
|
|
85
|
+
avatarStyle = "Fill",
|
|
86
|
+
src,
|
|
87
|
+
text,
|
|
88
|
+
icon = "person",
|
|
89
|
+
name,
|
|
90
|
+
showBadge = false,
|
|
91
|
+
badgeStatus = "Positive",
|
|
92
|
+
style,
|
|
93
|
+
...rest
|
|
94
|
+
}) {
|
|
95
|
+
const z = SIZES[size] || SIZES.M;
|
|
96
|
+
const [failed, setFailed] = useState(false);
|
|
97
|
+
useEffect(() => setFailed(false), [src]);
|
|
98
|
+
const letters = (text || "").slice(0, 2);
|
|
99
|
+
const resolved = content === "Icon" ? "Icon"
|
|
100
|
+
: content === "Text" ? (letters ? "Text" : "Icon")
|
|
101
|
+
: src && !failed ? "Image"
|
|
102
|
+
: letters ? "Text" : "Icon";
|
|
103
|
+
const look = resolved === "Image" ? "Fill" : avatarStyle;
|
|
104
|
+
const radius = shape === "Square" ? z.radius : T.circle;
|
|
105
|
+
const root = {
|
|
106
|
+
position: "relative", flex: "none", boxSizing: "border-box",
|
|
107
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
108
|
+
width: z.px, height: z.px, borderRadius: radius,
|
|
109
|
+
background: resolved === "Image" ? T.imageFill : look === "Stroke" ? "transparent" : T.fill,
|
|
110
|
+
boxShadow: look === "Stroke" ? `inset 0 0 0 ${T.strokeWidth} ${T.stroke}` : "none",
|
|
111
|
+
color: T.foreground, fontFamily: T.family, ...style,
|
|
112
|
+
};
|
|
113
|
+
const a11y = name ? { role: "img", "aria-label": name } : { "aria-hidden": true };
|
|
114
|
+
return (
|
|
115
|
+
<span data-pw-avatar="" data-size={size} data-content={resolved} style={root} {...a11y} {...rest}>
|
|
116
|
+
{resolved === "Image" && (
|
|
117
|
+
<img src={src} alt="" onError={() => setFailed(true)}
|
|
118
|
+
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block", borderRadius: radius }} />
|
|
119
|
+
)}
|
|
120
|
+
{resolved === "Text" && (
|
|
121
|
+
<span aria-hidden="true" style={{ fontSize: z.fontSize, lineHeight: z.lineHeight, fontWeight: T.weight, letterSpacing: 0, whiteSpace: "nowrap" }}>{letters}</span>
|
|
122
|
+
)}
|
|
123
|
+
{resolved === "Icon" && (
|
|
124
|
+
<span className="material-symbols-rounded" aria-hidden="true"
|
|
125
|
+
style={{ fontSize: z.icon, width: z.icon, height: z.icon, fontVariationSettings: "'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24",
|
|
126
|
+
lineHeight: 1 /* pathway-allow: the icon box is the glyph's em, sized by table 3.5 */ }}>{icon}</span>
|
|
127
|
+
)}
|
|
128
|
+
{showBadge && (
|
|
129
|
+
<span aria-hidden="true" data-pw-avatar-badge="" style={{
|
|
130
|
+
position: "absolute", right: 0, bottom: 0, boxSizing: "border-box", width: z.badge, height: z.badge,
|
|
131
|
+
borderRadius: T.circle, background: T.badge[badgeStatus] || T.badge.Positive,
|
|
132
|
+
boxShadow: `inset 0 0 0 ${T.badgeRingW} ${T.badgeRing}`,
|
|
133
|
+
}} />
|
|
134
|
+
)}
|
|
135
|
+
</span>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* AvatarGroup: up to `max` avatars (3), overlapping by a quarter of their size,
|
|
141
|
+
* each ringed in the surface colour, then a +N count of the rest. One role img
|
|
142
|
+
* named with the people shown and how many more.
|
|
143
|
+
*
|
|
144
|
+
* Figma: Size (XS, S, M) maps to size; Show Avatar 3 and Show more follow from
|
|
145
|
+
* how many people there are and `showMore`.
|
|
146
|
+
*/
|
|
147
|
+
export function AvatarGroup({ people = [], size = "XS", max = 3, showMore = true, label, style, ...rest }) {
|
|
148
|
+
const z = SIZES[size] || SIZES.XS;
|
|
149
|
+
const shown = people.slice(0, max);
|
|
150
|
+
const more = people.length - shown.length;
|
|
151
|
+
const named = shown.map((p) => p.name).filter(Boolean);
|
|
152
|
+
const aria = label || (named.join(", ") + (more > 0 ? `, and ${more} more` : ""));
|
|
153
|
+
const ring = { boxShadow: `0 0 0 ${T.groupRingW} ${T.groupRing}` };
|
|
154
|
+
return (
|
|
155
|
+
<span data-pw-avatar-group="" role="img" aria-label={aria} style={{ display: "inline-flex", alignItems: "center", ...style }} {...rest}>
|
|
156
|
+
{shown.map((p, i) => (
|
|
157
|
+
<Avatar key={(p.name || "") + i} size={size} src={p.src} text={p.text || initialsFrom(p.name)} icon={p.icon}
|
|
158
|
+
style={{ ...ring, marginLeft: i ? -z.px / 4 : 0 }} />
|
|
159
|
+
))}
|
|
160
|
+
{showMore && more > 0 && (
|
|
161
|
+
<Avatar size={size} content="Text" text={`+${more}`.slice(0, 3)} style={{ ...ring, marginLeft: shown.length ? -z.px / 4 : 0 }} />
|
|
162
|
+
)}
|
|
163
|
+
</span>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default Avatar;
|