@birdycom/omniaui-react 0.12.4 → 0.12.5

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/dist/index.d.ts CHANGED
@@ -920,11 +920,13 @@ interface HeadingProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
920
920
  declare const Heading: react.ForwardRefExoticComponent<HeadingProps & react.RefAttributes<HTMLDivElement>>;
921
921
 
922
922
  interface IconProps extends Omit<SVGProps<SVGSVGElement>, "d"> {
923
- /** SVG path data (24x24 viewBox, stroke icon). Use `children` for several shapes. */
923
+ /** SVG path data on a 24x24 viewBox: a webicon path (MDI, MDI Light or Material Symbols), e.g. the path of mdi:menu. Use `children` for several shapes. */
924
924
  d?: string;
925
+ /** Draw `d` as a 2px line instead of a filled shape (older line icons). */
926
+ line?: boolean;
925
927
  }
926
- /** Inline 24x24 stroke icon (`currentColor`), the shape used by the snippets. */
927
- declare function Icon({ d, children, className, ...rest }: IconProps): react.JSX.Element;
928
+ /** Inline 24x24 icon in `currentColor`: a filled webicon path (the snippets use MDI), drawn without iconify-icon. */
929
+ declare function Icon({ d, line, children, className, ...rest }: IconProps): react.JSX.Element;
928
930
 
929
931
  interface ImageZoomProps extends HTMLAttributes<HTMLDivElement> {
930
932
  src?: string;
@@ -2167,7 +2169,7 @@ declare const UpgradeBanner: react.ForwardRefExoticComponent<UpgradeBannerProps
2167
2169
  interface UserProps extends HTMLAttributes<HTMLDivElement> {
2168
2170
  name: ReactNode;
2169
2171
  email?: ReactNode;
2170
- /** Initials shown in the avatar. */
2172
+ /** Initials shown in the avatar (default: from `name` when it is a string). */
2171
2173
  initials?: string;
2172
2174
  /** Avatar image URL (wins over initials). */
2173
2175
  avatarSrc?: string;
package/dist/index.js CHANGED
@@ -115,6 +115,9 @@ function mod(base, ...modifiers) {
115
115
  function attrs(record) {
116
116
  return record;
117
117
  }
118
+ function initialsOf(name) {
119
+ return typeof name === "string" ? name.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("") : "";
120
+ }
118
121
  function callAll(...fns) {
119
122
  return (e) => fns.forEach((fn) => fn?.(e));
120
123
  }
@@ -2094,22 +2097,9 @@ var Heading = forwardRef46(function Heading2({ title, description, level = 2, si
2094
2097
 
2095
2098
  // src/components/Icon.tsx
2096
2099
  import { jsx as jsx48 } from "react/jsx-runtime";
2097
- function Icon({ d, children, className, ...rest }) {
2098
- return /* @__PURE__ */ jsx48(
2099
- "svg",
2100
- {
2101
- className,
2102
- viewBox: "0 0 24 24",
2103
- fill: "none",
2104
- stroke: "currentColor",
2105
- strokeWidth: 2,
2106
- strokeLinecap: "round",
2107
- strokeLinejoin: "round",
2108
- "aria-hidden": "true",
2109
- ...rest,
2110
- children: d ? /* @__PURE__ */ jsx48("path", { d }) : children
2111
- }
2112
- );
2100
+ function Icon({ d, line = false, children, className, ...rest }) {
2101
+ const paint = line ? { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" } : { fill: "currentColor" };
2102
+ return /* @__PURE__ */ jsx48("svg", { className, viewBox: "0 0 24 24", "aria-hidden": "true", ...paint, ...rest, children: d ? /* @__PURE__ */ jsx48("path", { d }) : children });
2113
2103
  }
2114
2104
 
2115
2105
  // src/components/ImageZoom.tsx
@@ -2163,10 +2153,7 @@ var InputGroup = forwardRef49(function InputGroup2({ prefix, suffix, className,
2163
2153
  // src/components/LanguageSelect.tsx
2164
2154
  import { forwardRef as forwardRef50 } from "react";
2165
2155
  import { jsx as jsx52, jsxs as jsxs42 } from "react/jsx-runtime";
2166
- var Globe = () => /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
2167
- /* @__PURE__ */ jsx52("circle", { cx: "12", cy: "12", r: "9" }),
2168
- /* @__PURE__ */ jsx52("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })
2169
- ] });
2156
+ var Globe = () => /* @__PURE__ */ jsx52("svg", { "data-icon": "mdi:web", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx52("path", { d: "M16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2s.06-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.92 7.92 0 0 1 9.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8 8 0 0 1 5.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.7 15.7 0 0 0-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2" }) });
2170
2157
  var LanguageSelect = forwardRef50(function LanguageSelect2({ options, value, defaultValue, onChange, currency, ghost, icon, labelClassName, className, ...rest }, ref) {
2171
2158
  const [current, setCurrent] = useControllable(value, defaultValue ?? options[0]?.value ?? "", onChange);
2172
2159
  return /* @__PURE__ */ jsxs42("label", { className: cx("oc-language-select", ghost && "oc-language-select--ghost", currency && "oc-language-select--currency", labelClassName), children: [
@@ -2426,8 +2413,8 @@ var MediaRow = forwardRef60(function MediaRow2({ title, href, meta, thumb, curre
2426
2413
  // src/components/Message.tsx
2427
2414
  import { forwardRef as forwardRef61 } from "react";
2428
2415
  import { Fragment as Fragment14, jsx as jsx63, jsxs as jsxs50 } from "react/jsx-runtime";
2429
- var svg = (d, width = "2") => /* @__PURE__ */ jsx63("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: width, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx63("path", { d }) });
2430
- var ICONS = { reply: "M9 14 4 9l5-5M4 9h10a6 6 0 0 1 6 6v4", delete: "M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" };
2416
+ var svg = (d) => /* @__PURE__ */ jsx63("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx63("path", { d }) });
2417
+ var ICONS = { reply: "M8 9.8v.9l1.7.3c2.6.4 4.5 1.4 5.9 2.7c-1.7-.5-3.5-.8-5.6-.8H8v1.3L5.8 12zM10 5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11", delete: "M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3zM7 6h10v13H7zm2 2v9h2V8zm4 0v9h2V8z" };
2431
2418
  var STATUS = { sent: "Sent", read: "Read", failed: "Failed" };
2432
2419
  function MessageAttachments({ items }) {
2433
2420
  const thumbs = items.every((a) => a.kind === "thumb");
@@ -2461,7 +2448,7 @@ var Message = forwardRef61(function Message2({ direction = "in", sender, avatar,
2461
2448
  (time != null || status) && /* @__PURE__ */ jsxs50("p", { className: "oc-message__meta", children: [
2462
2449
  time != null && /* @__PURE__ */ jsx63("time", { dateTime: datetime, children: time }),
2463
2450
  status && /* @__PURE__ */ jsxs50("span", { className: cx("oc-message__status", status !== "sent" && `oc-message__status--${status}`), children: [
2464
- statusIcon && svg("M2 12.5 7 17.5 18 6.5M13 16.5l1 1L22.5 9", "2.2"),
2451
+ statusIcon && svg("M.41 13.41L6 19l1.41-1.42L1.83 12m20.41-6.42L11.66 16.17L7.5 12l-1.43 1.41L11.66 19l12-12M18 7l-1.41-1.42l-6.35 6.35l1.42 1.41z"),
2465
2452
  statusLabel ?? STATUS[status]
2466
2453
  ] })
2467
2454
  ] })
@@ -2646,11 +2633,7 @@ var MessageThread = forwardRef62(function MessageThread2({
2646
2633
  (status || menu) && /* @__PURE__ */ jsxs51("div", { className: "oc-message-thread__actions", children: [
2647
2634
  status && /* @__PURE__ */ jsx64("span", { className: cx("oc-badge", status.variant && `oc-badge--${status.variant}`), children: status.label }),
2648
2635
  menu && /* @__PURE__ */ jsxs51(Fragment15, { children: [
2649
- /* @__PURE__ */ jsx64("button", { type: "button", className: "oc-message-thread__icon-btn", popoverTarget: `${base}-menu`, "aria-label": "Conversation actions", children: /* @__PURE__ */ jsxs51("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [
2650
- /* @__PURE__ */ jsx64("circle", { cx: "12", cy: "5", r: "1.8" }),
2651
- /* @__PURE__ */ jsx64("circle", { cx: "12", cy: "12", r: "1.8" }),
2652
- /* @__PURE__ */ jsx64("circle", { cx: "12", cy: "19", r: "1.8" })
2653
- ] }) }),
2636
+ /* @__PURE__ */ jsx64("button", { type: "button", className: "oc-message-thread__icon-btn", popoverTarget: `${base}-menu`, "aria-label": "Conversation actions", children: /* @__PURE__ */ jsx64("svg", { "data-icon": "mdi:dots-vertical", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx64("path", { d: "M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2" }) }) }),
2654
2637
  /* @__PURE__ */ jsx64("div", { className: "oc-dropdown-menu", id: `${base}-menu`, ...attrs({ popover: "" }), role: "menu", children: menu.map((m, i) => "separator" in m ? /* @__PURE__ */ jsx64("div", { className: "oc-dropdown-separator", role: "separator" }, i) : /* @__PURE__ */ jsx64("button", { type: "button", className: cx("oc-dropdown-item", m.destructive && "oc-dropdown-item--destructive"), role: "menuitem", onClick: () => onMenu?.(m.name), children: m.label }, i)) })
2655
2638
  ] })
2656
2639
  ] })
@@ -3974,7 +3957,7 @@ import { forwardRef as forwardRef109 } from "react";
3974
3957
  import { jsx as jsx111, jsxs as jsxs91 } from "react/jsx-runtime";
3975
3958
  var User = forwardRef109(function User2({ name, email, initials, avatarSrc, avatarSize = "md", className, ...rest }, ref) {
3976
3959
  return /* @__PURE__ */ jsxs91("div", { ref, className: cx("oc-user", className), ...rest, children: [
3977
- /* @__PURE__ */ jsx111(Avatar, { size: avatarSize, src: avatarSrc, children: initials }),
3960
+ /* @__PURE__ */ jsx111(Avatar, { size: avatarSize, src: avatarSrc, children: initials ?? initialsOf(name) }),
3978
3961
  /* @__PURE__ */ jsxs91("span", { className: "oc-user-text", children: [
3979
3962
  /* @__PURE__ */ jsx111("span", { className: "oc-user-name", children: name }),
3980
3963
  email != null && /* @__PURE__ */ jsx111("span", { className: "oc-user-email", children: email })
@@ -4030,7 +4013,7 @@ import { jsx as jsx115, jsxs as jsxs94 } from "react/jsx-runtime";
4030
4013
  var BlockAppHeader = forwardRef113(function BlockAppHeader2({ toggleFor, toggleLabel = "Toggle sidebar", nav, navLabel = "Module navigation", actions, title, className, ...rest }, ref) {
4031
4014
  return /* @__PURE__ */ jsxs94("header", { ref, className: cx("oc-block-appbar", className), ...rest, children: [
4032
4015
  toggleFor && /* @__PURE__ */ jsxs94("label", { className: "oc-btn oc-btn--ghost oc-btn--icon", htmlFor: toggleFor, title: toggleLabel, children: [
4033
- /* @__PURE__ */ jsx115(Icon, { d: "M4 6h16M4 12h16M4 18h16" }),
4016
+ /* @__PURE__ */ jsx115(Icon, { d: "M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z" }),
4034
4017
  /* @__PURE__ */ jsx115("span", { className: "oc-visually-hidden", children: toggleLabel })
4035
4018
  ] }),
4036
4019
  title != null && /* @__PURE__ */ jsx115("span", { className: "oc-block-appbar-title", children: title }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@birdycom/omniaui-react",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "OmniaUI for React: typed function components and page blocks that render the exact @birdycom/omniaui and @birdycom/omniaui-blocks markup.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@ export const BlockAppHeader = forwardRef<HTMLElement, BlockAppHeaderProps>(funct
32
32
  <header ref={ref} className={cx("oc-block-appbar", className)} {...rest}>
33
33
  {toggleFor && (
34
34
  <label className="oc-btn oc-btn--ghost oc-btn--icon" htmlFor={toggleFor} title={toggleLabel}>
35
- <Icon d="M4 6h16M4 12h16M4 18h16" />
35
+ <Icon d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z" />
36
36
  <span className="oc-visually-hidden">{toggleLabel}</span>
37
37
  </label>
38
38
  )}
@@ -1,24 +1,19 @@
1
1
  import type { SVGProps } from "react";
2
2
 
3
3
  export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "d"> {
4
- /** SVG path data (24x24 viewBox, stroke icon). Use `children` for several shapes. */
4
+ /** SVG path data on a 24x24 viewBox: a webicon path (MDI, MDI Light or Material Symbols), e.g. the path of mdi:menu. Use `children` for several shapes. */
5
5
  d?: string;
6
+ /** Draw `d` as a 2px line instead of a filled shape (older line icons). */
7
+ line?: boolean;
6
8
  }
7
9
 
8
- /** Inline 24x24 stroke icon (`currentColor`), the shape used by the snippets. */
9
- export function Icon({ d, children, className, ...rest }: IconProps) {
10
+ /** Inline 24x24 icon in `currentColor`: a filled webicon path (the snippets use MDI), drawn without iconify-icon. */
11
+ export function Icon({ d, line = false, children, className, ...rest }: IconProps) {
12
+ const paint = line
13
+ ? { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const }
14
+ : { fill: "currentColor" };
10
15
  return (
11
- <svg
12
- className={className}
13
- viewBox="0 0 24 24"
14
- fill="none"
15
- stroke="currentColor"
16
- strokeWidth={2}
17
- strokeLinecap="round"
18
- strokeLinejoin="round"
19
- aria-hidden="true"
20
- {...rest}
21
- >
16
+ <svg className={className} viewBox="0 0 24 24" aria-hidden="true" {...paint} {...rest}>
22
17
  {d ? <path d={d} /> : children}
23
18
  </svg>
24
19
  );
@@ -30,10 +30,7 @@ export interface LanguageSelectProps extends Omit<SelectHTMLAttributes<HTMLSelec
30
30
 
31
31
  /** Small globe drawn with currentColor. */
32
32
  const Globe = () => (
33
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
34
- <circle cx="12" cy="12" r="9" />
35
- <path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />
36
- </svg>
33
+ <svg data-icon="mdi:web" viewBox="0 0 24 24" fill="currentColor"><path d="M16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2s.06-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.92 7.92 0 0 1 9.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8 8 0 0 1 5.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.7 15.7 0 0 0-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg>
37
34
  );
38
35
 
39
36
  /** `.oc-language-select`: native select in a pill with a leading icon. */
@@ -48,10 +48,11 @@ export interface MessageData {
48
48
  id?: string;
49
49
  }
50
50
 
51
- const svg = (d: string, width = "2") => (
52
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={width} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={d} /></svg>
51
+ /** A webicon (MDI path data, 24x24). */
52
+ const svg = (d: string) => (
53
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d={d} /></svg>
53
54
  );
54
- const ICONS: Record<string, string> = { reply: "M9 14 4 9l5-5M4 9h10a6 6 0 0 1 6 6v4", delete: "M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6" };
55
+ const ICONS: Record<string, string> = { reply: "M8 9.8v.9l1.7.3c2.6.4 4.5 1.4 5.9 2.7c-1.7-.5-3.5-.8-5.6-.8H8v1.3L5.8 12zM10 5l-7 7l7 7v-4.1c5 0 8.5 1.6 11 5.1c-1-5-4-10-11-11", delete: "M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3zM7 6h10v13H7zm2 2v9h2V8zm4 0v9h2V8z" };
55
56
  const STATUS = { sent: "Sent", read: "Read", failed: "Failed" };
56
57
 
57
58
  /** `.oc-message__attachments`: thumbnails share one `.oc-attachment-list`, files and images stand alone. */
@@ -93,7 +94,7 @@ export const Message = forwardRef<HTMLElement, MessageProps>(function Message(
93
94
  {time != null && <time dateTime={datetime}>{time}</time>}
94
95
  {status && (
95
96
  <span className={cx("oc-message__status", status !== "sent" && `oc-message__status--${status}`)}>
96
- {statusIcon && svg("M2 12.5 7 17.5 18 6.5M13 16.5l1 1L22.5 9", "2.2")}{statusLabel ?? STATUS[status]}
97
+ {statusIcon && svg("M.41 13.41L6 19l1.41-1.42L1.83 12m20.41-6.42L11.66 16.17L7.5 12l-1.43 1.41L11.66 19l12-12M18 7l-1.41-1.42l-6.35 6.35l1.42 1.41z")}{statusLabel ?? STATUS[status]}
97
98
  </span>
98
99
  )}
99
100
  </p>
@@ -132,7 +132,7 @@ export const MessageThread = forwardRef<HTMLElement, MessageThreadProps>(functio
132
132
  {menu && (
133
133
  <>
134
134
  <button type="button" className="oc-message-thread__icon-btn" popoverTarget={`${base}-menu`} aria-label="Conversation actions">
135
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.8" /><circle cx="12" cy="12" r="1.8" /><circle cx="12" cy="19" r="1.8" /></svg>
135
+ <svg data-icon="mdi:dots-vertical" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2"/></svg>
136
136
  </button>
137
137
  <div className="oc-dropdown-menu" id={`${base}-menu`} {...attrs({ popover: "" })} role="menu">
138
138
  {menu.map((m, i) => "separator" in m
@@ -1,13 +1,13 @@
1
1
  import { forwardRef } from "react";
2
2
  import type { HTMLAttributes, ReactNode } from "react";
3
- import { cx } from "../utils";
3
+ import { cx, initialsOf } from "../utils";
4
4
  import { Avatar } from "./Avatar";
5
5
  import type { AvatarSize } from "./Avatar";
6
6
 
7
7
  export interface UserProps extends HTMLAttributes<HTMLDivElement> {
8
8
  name: ReactNode;
9
9
  email?: ReactNode;
10
- /** Initials shown in the avatar. */
10
+ /** Initials shown in the avatar (default: from `name` when it is a string). */
11
11
  initials?: string;
12
12
  /** Avatar image URL (wins over initials). */
13
13
  avatarSrc?: string;
@@ -21,7 +21,7 @@ export const User = forwardRef<HTMLDivElement, UserProps>(function User(
21
21
  ) {
22
22
  return (
23
23
  <div ref={ref} className={cx("oc-user", className)} {...rest}>
24
- <Avatar size={avatarSize} src={avatarSrc}>{initials}</Avatar>
24
+ <Avatar size={avatarSize} src={avatarSrc}>{initials ?? initialsOf(name)}</Avatar>
25
25
  <span className="oc-user-text">
26
26
  <span className="oc-user-name">{name}</span>
27
27
  {email != null && <span className="oc-user-email">{email}</span>}
package/src/utils.ts CHANGED
@@ -19,6 +19,13 @@ export function attrs(record: Record<string, string | undefined>): object {
19
19
 
20
20
  export type Slot = ReactNode;
21
21
 
22
+ /** "Ada Lovelace" -> "AL": the first letters of the first two words (as the Blade components do). */
23
+ export function initialsOf(name: unknown): string {
24
+ return typeof name === "string"
25
+ ? name.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("")
26
+ : "";
27
+ }
28
+
22
29
  /** Call a handler, then call another one. */
23
30
  export function callAll<E>(...fns: Array<((e: E) => void) | undefined>) {
24
31
  return (e: E) => fns.forEach((fn) => fn?.(e));