@stackone/malachite 2.51.1 → 2.52.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/COMPONENTS.md +3 -3
- package/dist/components/AppLogo.js +46 -26
- package/dist/components/AppLogo.js.map +1 -1
- package/dist/components/Avatar.js +66 -31
- package/dist/components/Avatar.js.map +1 -1
- package/dist/components/CardStrip.js +383 -327
- package/dist/components/CardStrip.js.map +1 -1
- package/dist/components/cardStripReader.js +39 -0
- package/dist/components/cardStripReader.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/package.json +1 -1
package/COMPONENTS.md
CHANGED
|
@@ -13,7 +13,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
13
13
|
- **`DisclosureArrow`** — The expand/collapse triangle disclosure UI shares (`Accordion`, `Surface.ExpandableRow`). Owns its colour states so they never drift between usages: `text-primary` when expanded, `text-placeholder` otherwise, and dark on hover inside a `group` ancestor — put `group` on the clickable header or row so that hover state activates. _Key props:_ `expanded`, `size`, `className`.
|
|
14
14
|
- **`Card`** — Generic content card.
|
|
15
15
|
- **`SelectableCard`** — A card that is one option in a radio group — eyebrow, optional status tag, title, description, a count label and a top-right action slot. Use inside a `CardStrip`. Selection is controlled by the consumer: pass `selected`, which drives both the visual treatment and `aria-checked`. A selected card meets the sheet below the way a browser tab meets its toolbar — its bottom corners flare outward onto the strip’s rule — so its outline is one SVG path rather than borders, which would split into two lines at the tangent. `variant` picks the palette: `profile` (green), `library` (neutral) or `editing` (amber, for a profile with unsaved changes). `actions` stands in the card’s top-right corner, on the eyebrow row, and renders outside the control so a menu or icon button inside it stays clickable and keeps its own tab stop. Nothing else marks that corner: the flared outline, the tint and the green eyebrow already say which card is selected, so a radio there would only repeat them. `onDismiss` adds an x after `actions`, tinted like the eyebrow so it reads as part of the card — for discarding a draft card; `dismissDisabledReason` disables it and explains why in its tooltip. `isDefault` marks the card as the account default with a pin beside the title, named for assistive tech and tooltipped for everyone else — it belongs with the name it qualifies rather than among the actions, which read as things to press. `address` puts a one-line chip above the count for a copyable address such as a server URL: its `label` is cut short with an ellipsis, and its full `value` shows in a tooltip and is copied on click, without selecting the card. It takes the place of `description`, since the card has room for one or the other. For a row-shaped selectable use `Surface.Row` instead. _Key props:_ `value`, `title`, `titleIcon`, `isDefault`, `variant` (`profile` | `library` | `editing`), `eyebrow`, `status` (`label`, `type`), `description`, `address` (`label`, `value`), `countLabel`, `actions`, `onDismiss` (`dismissLabel`, `dismissDisabledReason`), `selected`, `loading` (`loadingLabel`, `loadingHint`), `disabled`, `dimmed`, `onSelect`, `tabIndex`, `aria-label`, `className`, `style`.
|
|
16
|
-
- **`CardStrip`** — Horizontally scrolling radio group of cards, with edge arrows that appear only when the content overflows, an edge fade, scroll-snap and arrow-key navigation matching `SegmentedControl`. Owns interaction only: it finds its options by looking for `role="radio"` descendants carrying `data-value`, so any such child works (`SelectableCard` emits both). Pass a non-selectable terminal tile as `CardStrip.AddTile` — it sits inside the group but outside the radio set, and must be a direct child or it scrolls with the cards. Use `leading` to pin a card that should not scroll away; the strip tells the cards which slot they are in, so a pinned `SelectableCard` drops the left rule the scrolling ones draw. Set `bleed` when the strip sits in a `PageLayout`: the strip stretches past the horizontal padding of the layout so its bottom rule runs the full width of the page card, while the cards stay aligned to the gutter. With no cards at all the strip degenerates to its pinned slots alone — no trough, no scroller and no arrows — with the add tile standing beside `leading` and taking `primary` emphasis automatically, since adding the first card is the only thing on offer. _Key props:_ `value`, `tone` (`profile` | `library` | `editing` — read off the selected card unless set), `onChange`, `children`, `leading`, `arrows`, `fadeEdges`, `snap`, `bleed`, `scrollSelectedIntoView`, `aria-label`, `aria-labelledby`, `className`, `style`. `CardStrip.AddTile`: `label`, `icon`, `onClick`, `disabled`, `emphasis` (`primary` | `secondary` — left to the strip unless set: `primary` while there are no cards, `secondary` once there are), `className`.
|
|
16
|
+
- **`CardStrip`** — Horizontally scrolling radio group of cards, with edge arrows that appear only when the content overflows, an edge fade, scroll-snap and arrow-key navigation matching `SegmentedControl`. Owns interaction only: it finds its options by looking for `role="radio"` descendants carrying `data-value`, so any such child works (`SelectableCard` emits both). Pass a non-selectable terminal tile as `CardStrip.AddTile` — it sits inside the group but outside the radio set, and must be a direct child or it scrolls with the cards. Use `leading` to pin a card that should not scroll away; the strip tells the cards which slot they are in, so a pinned `SelectableCard` drops the left rule the scrolling ones draw. A strip too narrow to show a card beside `leading` scrolls it with the cards rather than squeezing them, so the selected card always has its full width. Set `bleed` when the strip sits in a `PageLayout`: the strip stretches past the horizontal padding of the layout so its bottom rule runs the full width of the page card, while the cards stay aligned to the gutter. With no cards at all the strip degenerates to its pinned slots alone — no trough, no scroller and no arrows — with the add tile standing beside `leading` and taking `primary` emphasis automatically, since adding the first card is the only thing on offer. _Key props:_ `value`, `tone` (`profile` | `library` | `editing` — read off the selected card unless set), `onChange`, `children`, `leading`, `arrows`, `fadeEdges`, `snap`, `bleed`, `scrollSelectedIntoView`, `aria-label`, `aria-labelledby`, `className`, `style`. `CardStrip.AddTile`: `label`, `icon`, `onClick`, `disabled`, `emphasis` (`primary` | `secondary` — left to the strip unless set: `primary` while there are no cards, `secondary` once there are), `className`.
|
|
17
17
|
- **`SplitScreenLayout`** — Two-pane full-screen layout (content + preview/aside) with an optional dismiss button.
|
|
18
18
|
- **`PageLayout`** — Standard page chrome: a bordered card holding a breadcrumb bar, an optional alert strip, and a header block (subtitle, title + suffix, description) above the page body. Used as `PageLayout.Regular`; `PageLayout.Empty` is the chrome-only variant for pre-auth and error screens, with `headerActions` and `background` slots. Actions are declared as data (`buttons`) rather than children, and any that do not fit collapse into an overflow `ActionMenu` via `OverflowRow`. A client-side-routed app must pass `renderLink` so crumbs and the back button go through its router instead of reloading the document. Both layouts render a `main` landmark, demoted via `role` when the app owns its own. Deliberately not a `Surface` — the page card is 16px and carries no shadow. _Key props:_ `PageLayout.Regular`: `breadcrumbLinks`, `breadcrumbActions`, `renderLink`, `title`, `titleSuffix`, `subtitle`, `description`, `buttons`, `loading`, `centered`, `noPadding`, `contentGap` (`small` | `medium` | `large` | `xlarge`, default `small`), `alert`, `role`, `aria-label`, `aria-labelledby`, `className`, `style`. `PageLayout.Empty`: `title`, `centered`, `uniformPadding`, `headerActions`, `background`, `role`, `aria-label`, `aria-labelledby`, `className`, `style`.
|
|
19
19
|
- **`WizardLayout`** — Full-screen wizard scaffold: top bar (back button, right-aligned stepper, trailing slot), pinned `header` slot, centered scrolling content column, and a pinned footer (above sticky body content). `footerGlass` gives the footer a frosted translucent + backdrop-blur treatment. _Key props:_ `showBack`, `onBack`, `stepper`, `trailing`, `header`, `footer`, `footerGlass`, `center`, `contentWidth`, `minWidth`.
|
|
@@ -108,8 +108,8 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
|
|
|
108
108
|
|
|
109
109
|
- **`Table`** — Data table with columns, sizing, pagination and row selection. `column.align` places a column's header as well as its cells. What lands on the aligned edge is the last thing in the header row rather than the title: the sort caret on a sortable column, the info icon on one carrying `info` without a sorter. `headerVariant` picks the header layout, and only `inline` lines header and body up exactly, because both are cells of one `<table>`. `detached` builds them from flex rows, where a sortable header cell is 32px narrower than a non-sortable one — its padding sits on the inner button rather than on the flex item — so its header cells sit 15–17px off their column at any alignment; reach for `inline` where exact alignment matters. Sorting is internal, so the array passed as `data` is not the order rendered once a column is sorted: `onVisibleOrderChange` reports the rows in render order — the whole of `data`, compared by `rowKey` so an equal order never re-fires — for a consumer stepping a master-detail view the way the user sees it. _Key props:_ `TableColumns` (incl. `align`), `TableSize`, `TableType`, `TableRowSelection`, `TablePaginationPosition`, `headerVariant`, `onVisibleOrderChange`.
|
|
110
110
|
- **`Statistic`** — Displays a statistic value with a label; primary/secondary sizes. _Key props:_ `StatisticValueSize`.
|
|
111
|
-
- **`Avatar`** — Logo/initial avatar. Renders the image when available, otherwise a coloured tile with the first letter of `name
|
|
112
|
-
- **`AppLogo`** — A connector's mark, optionally with its name and version tag. Builds on `Avatar`, so a missing or broken logo falls back to a
|
|
111
|
+
- **`Avatar`** — Logo/initial avatar. Renders the image when available, otherwise a coloured tile with the first letter of `name`, or with `fallback="placeholder"` the generic "no logo available" tile. Encapsulates the fallback colour. _Key props:_ `src`, `name`, `size`, `shape="square" | "circle"`, `fallback="initial" | "placeholder"`.
|
|
112
|
+
- **`AppLogo`** — A connector's mark, optionally with its name and version tag. Builds on `Avatar`, so a missing or broken logo falls back to the generic "no logo available" placeholder, an accent tile with a white dot. The gap tightens below 18px so the mark and label still read as one unit. _Key props:_ `src`, `name`, `showName`, `size`, `nameSize`, `nameWeight`, `version`, `truncate`, `className`.
|
|
113
113
|
- **`ConnectionHeader`** — Two tilted logo tiles (from → to) showing a connection between apps. _Key props:_ `from`, `to` (`ConnectionEndpoint`: `icon`, `label`, `background`).
|
|
114
114
|
- **`CodeBlock`** — Syntax-styled code block. _Key props:_ `code`.
|
|
115
115
|
- **`Stepper`** — Progress dots/segments for a multi-step flow; optionally clickable. _Key props:_ `steps`, `active`, `onChange`.
|
|
@@ -1,43 +1,63 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { Avatar as
|
|
3
|
-
import { Tag as
|
|
1
|
+
import { jsx as e, jsxs as p } from "react/jsx-runtime";
|
|
2
|
+
import { Avatar as s } from "./Avatar.js";
|
|
3
|
+
import { Tag as d } from "./Tag.js";
|
|
4
4
|
import { Typography as m } from "./Typography/index.js";
|
|
5
|
-
import { getFontWeight as
|
|
6
|
-
const
|
|
7
|
-
src:
|
|
8
|
-
name:
|
|
9
|
-
showName:
|
|
5
|
+
import { getFontWeight as g, getFontSize as u } from "./Typography/textStyles.js";
|
|
6
|
+
const W = ({
|
|
7
|
+
src: l,
|
|
8
|
+
name: t,
|
|
9
|
+
showName: c = !0,
|
|
10
10
|
size: r = 24,
|
|
11
|
-
nameSize:
|
|
12
|
-
nameWeight:
|
|
13
|
-
version:
|
|
14
|
-
truncate:
|
|
15
|
-
className:
|
|
11
|
+
nameSize: a = "medium",
|
|
12
|
+
nameWeight: i = "normal",
|
|
13
|
+
version: o,
|
|
14
|
+
truncate: f = !1,
|
|
15
|
+
className: n = ""
|
|
16
16
|
}) => {
|
|
17
17
|
const h = r <= 18 ? "gap-2" : "gap-3";
|
|
18
|
-
return
|
|
18
|
+
return c ? (
|
|
19
19
|
// `max-w-full` is what makes `truncate` work — without it the box is floored at the name's min-content width.
|
|
20
|
-
/* @__PURE__ */
|
|
21
|
-
/* @__PURE__ */
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
/* @__PURE__ */ p("span", { className: `inline-flex items-center ${h} max-w-full ${n}`.trim(), children: [
|
|
21
|
+
/* @__PURE__ */ e(
|
|
22
|
+
s,
|
|
23
|
+
{
|
|
24
|
+
src: l,
|
|
25
|
+
name: t,
|
|
26
|
+
size: r,
|
|
27
|
+
shape: "square",
|
|
28
|
+
fallback: "placeholder",
|
|
29
|
+
className: "shrink-0"
|
|
30
|
+
}
|
|
31
|
+
),
|
|
32
|
+
/* @__PURE__ */ p("span", { className: "inline-flex items-center gap-2 min-w-0", children: [
|
|
33
|
+
f ? /* @__PURE__ */ e(
|
|
24
34
|
m.EllipsisText,
|
|
25
35
|
{
|
|
26
36
|
maxWidth: "100%",
|
|
27
|
-
tooltip:
|
|
37
|
+
tooltip: t,
|
|
28
38
|
style: {
|
|
29
|
-
fontSize: u(
|
|
30
|
-
fontWeight:
|
|
39
|
+
fontSize: u(a),
|
|
40
|
+
fontWeight: g(i)
|
|
31
41
|
},
|
|
32
|
-
children:
|
|
42
|
+
children: t
|
|
33
43
|
}
|
|
34
|
-
) : /* @__PURE__ */
|
|
35
|
-
|
|
44
|
+
) : /* @__PURE__ */ e(m.Text, { size: a, fontWeight: i, children: t }),
|
|
45
|
+
o ? /* @__PURE__ */ e("span", { className: "shrink-0", children: /* @__PURE__ */ e(d, { value: o, textTransform: "none", paddingSize: "compact" }) }) : null
|
|
36
46
|
] })
|
|
37
47
|
] })
|
|
38
|
-
) : /* @__PURE__ */
|
|
48
|
+
) : /* @__PURE__ */ e(
|
|
49
|
+
s,
|
|
50
|
+
{
|
|
51
|
+
src: l,
|
|
52
|
+
name: t,
|
|
53
|
+
size: r,
|
|
54
|
+
shape: "square",
|
|
55
|
+
fallback: "placeholder",
|
|
56
|
+
className: n
|
|
57
|
+
}
|
|
58
|
+
);
|
|
39
59
|
};
|
|
40
60
|
export {
|
|
41
|
-
|
|
61
|
+
W as default
|
|
42
62
|
};
|
|
43
63
|
//# sourceMappingURL=AppLogo.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppLogo.js","sources":["../../src/components/AppLogo.tsx"],"sourcesContent":["import React from 'react';\nimport Avatar from './Avatar';\nimport Tag from './Tag';\nimport Typography from './Typography';\nimport type { FontSize, FontWeight } from './Typography/Text';\nimport { getFontSize, getFontWeight } from './Typography/textStyles';\n\nexport interface AppLogoProps {\n /** Connector logo URL. Falls back to a
|
|
1
|
+
{"version":3,"file":"AppLogo.js","sources":["../../src/components/AppLogo.tsx"],"sourcesContent":["import React from 'react';\nimport Avatar from './Avatar';\nimport Tag from './Tag';\nimport Typography from './Typography';\nimport type { FontSize, FontWeight } from './Typography/Text';\nimport { getFontSize, getFontWeight } from './Typography/textStyles';\n\nexport interface AppLogoProps {\n /** Connector logo URL. Falls back to a \"no logo available\" placeholder when missing or broken. */\n src?: string;\n /** Connector name — the accessible label and the visible text. */\n name: string;\n /** Renders the name beside the mark. Omit for a bare logo (e.g. inside a dense row). */\n showName?: boolean;\n /** Pixel size of the square mark. */\n size?: number;\n nameSize?: FontSize;\n /** Defaults to `normal`. Raise it when the logo doubles as a page title. */\n nameWeight?: FontWeight;\n /** Rendered as a tag after the name, e.g. `v1.2.0` or `Legacy`. */\n version?: string;\n /** Truncates a long name with an ellipsis and reveals the full value on hover. */\n truncate?: boolean;\n className?: string;\n}\n\nconst AppLogo: React.FC<AppLogoProps> = ({\n src,\n name,\n showName = true,\n size = 24,\n nameSize = 'medium',\n nameWeight = 'normal',\n version,\n truncate = false,\n className = '',\n}) => {\n const gap = size <= 18 ? 'gap-2' : 'gap-3';\n\n if (!showName) {\n return (\n <Avatar\n src={src}\n name={name}\n size={size}\n shape=\"square\"\n fallback=\"placeholder\"\n className={className}\n />\n );\n }\n\n return (\n // `max-w-full` is what makes `truncate` work — without it the box is floored at the name's min-content width.\n <span className={`inline-flex items-center ${gap} max-w-full ${className}`.trim()}>\n <Avatar\n src={src}\n name={name}\n size={size}\n shape=\"square\"\n fallback=\"placeholder\"\n className=\"shrink-0\"\n />\n <span className=\"inline-flex items-center gap-2 min-w-0\">\n {truncate ? (\n <Typography.EllipsisText\n maxWidth=\"100%\"\n tooltip={name}\n style={{\n fontSize: getFontSize(nameSize),\n fontWeight: getFontWeight(nameWeight),\n }}\n >\n {name}\n </Typography.EllipsisText>\n ) : (\n <Typography.Text size={nameSize} fontWeight={nameWeight}>\n {name}\n </Typography.Text>\n )}\n {version ? (\n <span className=\"shrink-0\">\n <Tag value={version} textTransform=\"none\" paddingSize=\"compact\" />\n </span>\n ) : null}\n </span>\n </span>\n );\n};\n\nexport default AppLogo;\n"],"names":["AppLogo","src","name","showName","size","nameSize","nameWeight","version","truncate","className","gap","jsxs","jsx","Avatar","Typography","getFontSize","getFontWeight","Tag"],"mappings":";;;;;AA0BA,MAAMA,IAAkC,CAAC;AAAA,EACrC,KAAAC;AAAA,EACA,MAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,MAAAC,IAAO;AAAA,EACP,UAAAC,IAAW;AAAA,EACX,YAAAC,IAAa;AAAA,EACb,SAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,WAAAC,IAAY;AAChB,MAAM;AACF,QAAMC,IAAMN,KAAQ,KAAK,UAAU;AAEnC,SAAKD;AAAA;AAAA,IAeD,gBAAAQ,EAAC,UAAK,WAAW,4BAA4BD,CAAG,eAAeD,CAAS,GAAG,KAAA,GACvE,UAAA;AAAA,MAAA,gBAAAG;AAAA,QAACC;AAAA,QAAA;AAAA,UACG,KAAAZ;AAAA,UACA,MAAAC;AAAA,UACA,MAAAE;AAAA,UACA,OAAM;AAAA,UACN,UAAS;AAAA,UACT,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,MAEd,gBAAAO,EAAC,QAAA,EAAK,WAAU,0CACX,UAAA;AAAA,QAAAH,IACG,gBAAAI;AAAA,UAACE,EAAW;AAAA,UAAX;AAAA,YACG,UAAS;AAAA,YACT,SAASZ;AAAA,YACT,OAAO;AAAA,cACH,UAAUa,EAAYV,CAAQ;AAAA,cAC9B,YAAYW,EAAcV,CAAU;AAAA,YAAA;AAAA,YAGvC,UAAAJ;AAAA,UAAA;AAAA,QAAA,sBAGJY,EAAW,MAAX,EAAgB,MAAMT,GAAU,YAAYC,GACxC,UAAAJ,GACL;AAAA,QAEHK,IACG,gBAAAK,EAAC,QAAA,EAAK,WAAU,YACZ,UAAA,gBAAAA,EAACK,GAAA,EAAI,OAAOV,GAAS,eAAc,QAAO,aAAY,UAAA,CAAU,GACpE,IACA;AAAA,MAAA,EAAA,CACR;AAAA,IAAA,EAAA,CACJ;AAAA,MA7CI,gBAAAK;AAAA,IAACC;AAAA,IAAA;AAAA,MACG,KAAAZ;AAAA,MACA,MAAAC;AAAA,MACA,MAAAE;AAAA,MACA,OAAM;AAAA,MACN,UAAS;AAAA,MACT,WAAAK;AAAA,IAAA;AAAA,EAAA;AAyChB;"}
|
|
@@ -1,61 +1,96 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
import { useState as
|
|
3
|
-
const
|
|
1
|
+
import { jsx as i } from "react/jsx-runtime";
|
|
2
|
+
import { useState as g } from "react";
|
|
3
|
+
const p = (e) => {
|
|
4
4
|
let r = 0;
|
|
5
5
|
for (let t = 0; t < e.length; t += 1)
|
|
6
6
|
r = e.charCodeAt(t) + ((r << 5) - r);
|
|
7
7
|
return `hsl(${Math.abs(r) % 360}, 55%, 52%)`;
|
|
8
|
-
},
|
|
8
|
+
}, b = ({
|
|
9
9
|
src: e,
|
|
10
10
|
name: r,
|
|
11
11
|
size: t = 24,
|
|
12
|
-
shape:
|
|
13
|
-
|
|
12
|
+
shape: h = "square",
|
|
13
|
+
fallback: c = "initial",
|
|
14
|
+
className: o = ""
|
|
14
15
|
}) => {
|
|
15
|
-
const [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
const [f, u] = g(void 0), n = (r == null ? void 0 : r.trim()) ?? "", a = h === "circle" ? "50%" : `${Math.round(t * 0.25)}px`;
|
|
17
|
+
if (e && f !== e)
|
|
18
|
+
return /* @__PURE__ */ i(
|
|
19
|
+
"img",
|
|
20
|
+
{
|
|
21
|
+
src: e,
|
|
22
|
+
alt: n,
|
|
23
|
+
loading: "lazy",
|
|
24
|
+
decoding: "async",
|
|
25
|
+
onError: () => u(e),
|
|
26
|
+
className: o,
|
|
27
|
+
style: {
|
|
28
|
+
width: `${t}px`,
|
|
29
|
+
height: `${t}px`,
|
|
30
|
+
borderRadius: a,
|
|
31
|
+
objectFit: "contain",
|
|
32
|
+
flexShrink: 0
|
|
33
|
+
}
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
);
|
|
36
|
+
const l = n ? { role: "img", "aria-label": n } : { "aria-hidden": !0 };
|
|
37
|
+
if (c === "placeholder") {
|
|
38
|
+
const d = `${Math.round(t * 0.25)}px`;
|
|
39
|
+
return /* @__PURE__ */ i(
|
|
40
|
+
"div",
|
|
41
|
+
{
|
|
42
|
+
...l,
|
|
43
|
+
className: o,
|
|
44
|
+
style: {
|
|
45
|
+
width: `${t}px`,
|
|
46
|
+
height: `${t}px`,
|
|
47
|
+
flexShrink: 0,
|
|
48
|
+
borderRadius: a,
|
|
49
|
+
display: "flex",
|
|
50
|
+
alignItems: "center",
|
|
51
|
+
justifyContent: "center",
|
|
52
|
+
background: "var(--malachite-accent-background, rgb(0, 175, 102))"
|
|
53
|
+
},
|
|
54
|
+
children: /* @__PURE__ */ i(
|
|
55
|
+
"span",
|
|
56
|
+
{
|
|
57
|
+
style: {
|
|
58
|
+
width: d,
|
|
59
|
+
height: d,
|
|
60
|
+
borderRadius: "50%",
|
|
61
|
+
background: "rgba(255, 255, 255, 0.8)"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return /* @__PURE__ */ i(
|
|
34
69
|
"div",
|
|
35
70
|
{
|
|
36
|
-
...
|
|
37
|
-
className:
|
|
71
|
+
...l,
|
|
72
|
+
className: o,
|
|
38
73
|
style: {
|
|
39
74
|
width: `${t}px`,
|
|
40
75
|
height: `${t}px`,
|
|
41
76
|
flexShrink: 0,
|
|
42
|
-
borderRadius:
|
|
77
|
+
borderRadius: a,
|
|
43
78
|
display: "flex",
|
|
44
79
|
alignItems: "center",
|
|
45
80
|
justifyContent: "center",
|
|
46
|
-
background:
|
|
81
|
+
background: p(n),
|
|
47
82
|
color: "#ffffff",
|
|
48
83
|
fontWeight: 600,
|
|
49
84
|
fontSize: `${Math.round(t * 0.45)}px`,
|
|
50
85
|
lineHeight: 1,
|
|
51
86
|
textTransform: "uppercase"
|
|
52
87
|
},
|
|
53
|
-
children:
|
|
88
|
+
children: n.charAt(0) || "?"
|
|
54
89
|
}
|
|
55
90
|
);
|
|
56
91
|
};
|
|
57
92
|
export {
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
b as Avatar,
|
|
94
|
+
b as default
|
|
60
95
|
};
|
|
61
96
|
//# sourceMappingURL=Avatar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.js","sources":["../../src/components/Avatar.tsx"],"sourcesContent":["import React, { useState } from 'react';\n\nexport type AvatarShape = 'square' | 'circle';\n\nexport interface AvatarProps {\n /** Image URL (logo/photo). Falls back to
|
|
1
|
+
{"version":3,"file":"Avatar.js","sources":["../../src/components/Avatar.tsx"],"sourcesContent":["import React, { useState } from 'react';\n\nexport type AvatarShape = 'square' | 'circle';\n\n/** `initial` suits a named entity (a person, a model); `placeholder` says \"no logo available\". */\nexport type AvatarFallback = 'initial' | 'placeholder';\n\nexport interface AvatarProps {\n /** Image URL (logo/photo). Falls back to `fallback` when missing or it fails to load. */\n src?: string;\n /** Used for the initial fallback and as the image alt / accessible label. */\n name?: string;\n /** Pixel size of the (square) avatar. */\n size?: number;\n shape?: AvatarShape;\n /** What shows when `src` is missing or fails to load. Defaults to `initial`. */\n fallback?: AvatarFallback;\n className?: string;\n}\n\n// Deterministic, evenly-distributed hue from the label so the same entity always gets\n// the same colour. Saturation/lightness are fixed for a consistent, on-brand feel.\n// NOTE: avatar identity colours (this hue and the white text below) are intentionally\n// NOT theme tokens — they are a generated identity palette, an explicit exception to\n// the \"colours come from theme tokens\" rule (there is no avatar-colour token group).\nconst backgroundFor = (seed: string): string => {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = seed.charCodeAt(index) + ((hash << 5) - hash);\n }\n return `hsl(${Math.abs(hash) % 360}, 55%, 52%)`;\n};\n\n/**\n * A logo/initial avatar. Renders `src` when available, otherwise a coloured tile\n * with the first letter of `name`. Encapsulates the fallback colour so consumers\n * never hand-roll it.\n */\nconst Avatar: React.FC<AvatarProps> = ({\n src,\n name,\n size = 24,\n shape = 'square',\n fallback = 'initial',\n className = '',\n}) => {\n // Track which src failed (not a boolean) so switching `src` to a new URL retries the\n // image instead of staying stuck on the fallback.\n const [failedSrc, setFailedSrc] = useState<string | undefined>(undefined);\n const label = name?.trim() ?? '';\n const borderRadius = shape === 'circle' ? '50%' : `${Math.round(size * 0.25)}px`;\n\n if (src && failedSrc !== src) {\n return (\n <img\n src={src}\n alt={label}\n loading=\"lazy\"\n decoding=\"async\"\n onError={() => setFailedSrc(src)}\n className={className}\n style={{\n width: `${size}px`,\n height: `${size}px`,\n borderRadius,\n objectFit: 'contain',\n flexShrink: 0,\n }}\n />\n );\n }\n\n // With a label the tile is an image with an accessible name; without one it carries\n // no information, so mark it decorative (aria-hidden, no role) to avoid an unnamed\n // image in the accessibility tree.\n const tileSemantics = label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true };\n\n if (fallback === 'placeholder') {\n const dotSize = `${Math.round(size * 0.25)}px`;\n return (\n <div\n {...tileSemantics}\n className={className}\n style={{\n width: `${size}px`,\n height: `${size}px`,\n flexShrink: 0,\n borderRadius,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--malachite-accent-background, rgb(0, 175, 102))',\n }}\n >\n {/* The website's and docs' \"no logo\" mark: its white dot is part of the mark, not a theme colour. */}\n <span\n style={{\n width: dotSize,\n height: dotSize,\n borderRadius: '50%',\n background: 'rgba(255, 255, 255, 0.8)',\n }}\n />\n </div>\n );\n }\n\n return (\n <div\n {...tileSemantics}\n className={className}\n style={{\n width: `${size}px`,\n height: `${size}px`,\n flexShrink: 0,\n borderRadius,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: backgroundFor(label),\n color: '#ffffff',\n fontWeight: 600,\n fontSize: `${Math.round(size * 0.45)}px`,\n lineHeight: 1,\n textTransform: 'uppercase',\n }}\n >\n {label.charAt(0) || '?'}\n </div>\n );\n};\n\nexport default Avatar;\nexport { Avatar };\n"],"names":["backgroundFor","seed","hash","index","Avatar","src","name","size","shape","fallback","className","failedSrc","setFailedSrc","useState","label","borderRadius","jsx","tileSemantics","dotSize"],"mappings":";;AAyBA,MAAMA,IAAgB,CAACC,MAAyB;AAC5C,MAAIC,IAAO;AACX,WAASC,IAAQ,GAAGA,IAAQF,EAAK,QAAQE,KAAS;AAC9C,IAAAD,IAAOD,EAAK,WAAWE,CAAK,MAAMD,KAAQ,KAAKA;AAEnD,SAAO,OAAO,KAAK,IAAIA,CAAI,IAAI,GAAG;AACtC,GAOME,IAAgC,CAAC;AAAA,EACnC,KAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC,IAAO;AAAA,EACP,OAAAC,IAAQ;AAAA,EACR,UAAAC,IAAW;AAAA,EACX,WAAAC,IAAY;AAChB,MAAM;AAGF,QAAM,CAACC,GAAWC,CAAY,IAAIC,EAA6B,MAAS,GAClEC,KAAQR,KAAA,gBAAAA,EAAM,WAAU,IACxBS,IAAeP,MAAU,WAAW,QAAQ,GAAG,KAAK,MAAMD,IAAO,IAAI,CAAC;AAE5E,MAAIF,KAAOM,MAAcN;AACrB,WACI,gBAAAW;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,KAAAX;AAAA,QACA,KAAKS;AAAA,QACL,SAAQ;AAAA,QACR,UAAS;AAAA,QACT,SAAS,MAAMF,EAAaP,CAAG;AAAA,QAC/B,WAAAK;AAAA,QACA,OAAO;AAAA,UACH,OAAO,GAAGH,CAAI;AAAA,UACd,QAAQ,GAAGA,CAAI;AAAA,UACf,cAAAQ;AAAA,UACA,WAAW;AAAA,UACX,YAAY;AAAA,QAAA;AAAA,MAChB;AAAA,IAAA;AAQZ,QAAME,IAAgBH,IAAQ,EAAE,MAAM,OAAO,cAAcA,EAAA,IAAU,EAAE,eAAe,GAAA;AAEtF,MAAIL,MAAa,eAAe;AAC5B,UAAMS,IAAU,GAAG,KAAK,MAAMX,IAAO,IAAI,CAAC;AAC1C,WACI,gBAAAS;AAAA,MAAC;AAAA,MAAA;AAAA,QACI,GAAGC;AAAA,QACJ,WAAAP;AAAA,QACA,OAAO;AAAA,UACH,OAAO,GAAGH,CAAI;AAAA,UACd,QAAQ,GAAGA,CAAI;AAAA,UACf,YAAY;AAAA,UACZ,cAAAQ;AAAA,UACA,SAAS;AAAA,UACT,YAAY;AAAA,UACZ,gBAAgB;AAAA,UAChB,YAAY;AAAA,QAAA;AAAA,QAIhB,UAAA,gBAAAC;AAAA,UAAC;AAAA,UAAA;AAAA,YACG,OAAO;AAAA,cACH,OAAOE;AAAA,cACP,QAAQA;AAAA,cACR,cAAc;AAAA,cACd,YAAY;AAAA,YAAA;AAAA,UAChB;AAAA,QAAA;AAAA,MACJ;AAAA,IAAA;AAAA,EAGZ;AAEA,SACI,gBAAAF;AAAA,IAAC;AAAA,IAAA;AAAA,MACI,GAAGC;AAAA,MACJ,WAAAP;AAAA,MACA,OAAO;AAAA,QACH,OAAO,GAAGH,CAAI;AAAA,QACd,QAAQ,GAAGA,CAAI;AAAA,QACf,YAAY;AAAA,QACZ,cAAAQ;AAAA,QACA,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,gBAAgB;AAAA,QAChB,YAAYf,EAAcc,CAAK;AAAA,QAC/B,OAAO;AAAA,QACP,YAAY;AAAA,QACZ,UAAU,GAAG,KAAK,MAAMP,IAAO,IAAI,CAAC;AAAA,QACpC,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,MAGlB,UAAAO,EAAM,OAAO,CAAC,KAAK;AAAA,IAAA;AAAA,EAAA;AAGhC;"}
|