@birdycom/omniaui-react 0.12.3 → 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;
@@ -1489,8 +1491,10 @@ interface PriceCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
1489
1491
  monthly: PriceCardPrice;
1490
1492
  /** Without a yearly price the toggle is not rendered. */
1491
1493
  yearly?: PriceCardPrice;
1494
+ /** Optional third option, e.g. a one-time price. Needs `yearly`. */
1495
+ lifetime?: PriceCardPrice;
1492
1496
  /** Period that is selected first (uncontrolled radio). */
1493
- defaultPeriod?: "monthly" | "yearly";
1497
+ defaultPeriod?: "monthly" | "yearly" | "lifetime";
1494
1498
  features?: PriceCardFeature[];
1495
1499
  /** Call to action, e.g. <a className="oc-btn oc-price-card__cta">. */
1496
1500
  cta?: ReactNode;
@@ -1498,8 +1502,9 @@ interface PriceCardProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
1498
1502
  /** Labels of the toggle. */
1499
1503
  monthlyLabel?: ReactNode;
1500
1504
  yearlyLabel?: ReactNode;
1505
+ lifetimeLabel?: ReactNode;
1501
1506
  }
1502
- /** `<article class="oc-price-card">`: plan card with a CSS-only monthly / yearly switch. */
1507
+ /** `<article class="oc-price-card">`: plan card with a CSS-only monthly / yearly (/ lifetime) switch. */
1503
1508
  declare const PriceCard: react.ForwardRefExoticComponent<PriceCardProps & react.RefAttributes<HTMLElement>>;
1504
1509
 
1505
1510
  type ProductMarkSize = "sm" | "default" | "lg" | "xl";
@@ -2164,7 +2169,7 @@ declare const UpgradeBanner: react.ForwardRefExoticComponent<UpgradeBannerProps
2164
2169
  interface UserProps extends HTMLAttributes<HTMLDivElement> {
2165
2170
  name: ReactNode;
2166
2171
  email?: ReactNode;
2167
- /** Initials shown in the avatar. */
2172
+ /** Initials shown in the avatar (default: from `name` when it is a string). */
2168
2173
  initials?: string;
2169
2174
  /** Avatar image URL (wins over initials). */
2170
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
  ] })
@@ -2976,12 +2959,14 @@ var PriceCard = forwardRef74(function PriceCard2({
2976
2959
  discount,
2977
2960
  monthly,
2978
2961
  yearly,
2962
+ lifetime,
2979
2963
  defaultPeriod = "monthly",
2980
2964
  features = [],
2981
2965
  cta,
2982
2966
  featured = false,
2983
2967
  monthlyLabel = "Monthly",
2984
2968
  yearlyLabel = "Yearly",
2969
+ lifetimeLabel = "Lifetime",
2985
2970
  className,
2986
2971
  ...rest
2987
2972
  }, ref) {
@@ -3002,10 +2987,16 @@ var PriceCard = forwardRef74(function PriceCard2({
3002
2987
  /* @__PURE__ */ jsx76("input", { type: "radio", name: group, value: "yearly", defaultChecked: defaultPeriod === "yearly" }),
3003
2988
  " ",
3004
2989
  yearlyLabel
2990
+ ] }),
2991
+ lifetime && /* @__PURE__ */ jsxs60("label", { className: "oc-segmented-item", children: [
2992
+ /* @__PURE__ */ jsx76("input", { type: "radio", name: group, value: "lifetime", defaultChecked: defaultPeriod === "lifetime" }),
2993
+ " ",
2994
+ lifetimeLabel
3005
2995
  ] })
3006
2996
  ] }),
3007
2997
  /* @__PURE__ */ jsx76(Price, { kind: "monthly", price: monthly }),
3008
2998
  yearly && /* @__PURE__ */ jsx76(Price, { kind: "yearly", price: yearly }),
2999
+ yearly && lifetime && /* @__PURE__ */ jsx76(Price, { kind: "lifetime", price: lifetime }),
3009
3000
  features.length > 0 && /* @__PURE__ */ jsx76("ul", { className: "oc-price-card__features", children: features.map((f, i) => /* @__PURE__ */ jsx76("li", { className: cx("oc-price-card__feature", f.excluded && "oc-price-card__feature--excluded"), children: f.label }, i)) }),
3010
3001
  cta
3011
3002
  ] });
@@ -3966,7 +3957,7 @@ import { forwardRef as forwardRef109 } from "react";
3966
3957
  import { jsx as jsx111, jsxs as jsxs91 } from "react/jsx-runtime";
3967
3958
  var User = forwardRef109(function User2({ name, email, initials, avatarSrc, avatarSize = "md", className, ...rest }, ref) {
3968
3959
  return /* @__PURE__ */ jsxs91("div", { ref, className: cx("oc-user", className), ...rest, children: [
3969
- /* @__PURE__ */ jsx111(Avatar, { size: avatarSize, src: avatarSrc, children: initials }),
3960
+ /* @__PURE__ */ jsx111(Avatar, { size: avatarSize, src: avatarSrc, children: initials ?? initialsOf(name) }),
3970
3961
  /* @__PURE__ */ jsxs91("span", { className: "oc-user-text", children: [
3971
3962
  /* @__PURE__ */ jsx111("span", { className: "oc-user-name", children: name }),
3972
3963
  email != null && /* @__PURE__ */ jsx111("span", { className: "oc-user-email", children: email })
@@ -4022,7 +4013,7 @@ import { jsx as jsx115, jsxs as jsxs94 } from "react/jsx-runtime";
4022
4013
  var BlockAppHeader = forwardRef113(function BlockAppHeader2({ toggleFor, toggleLabel = "Toggle sidebar", nav, navLabel = "Module navigation", actions, title, className, ...rest }, ref) {
4023
4014
  return /* @__PURE__ */ jsxs94("header", { ref, className: cx("oc-block-appbar", className), ...rest, children: [
4024
4015
  toggleFor && /* @__PURE__ */ jsxs94("label", { className: "oc-btn oc-btn--ghost oc-btn--icon", htmlFor: toggleFor, title: toggleLabel, children: [
4025
- /* @__PURE__ */ jsx115(Icon, { d: "M4 6h16M4 12h16M4 18h16" }),
4016
+ /* @__PURE__ */ jsx115(Icon, { d: "M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z" }),
4026
4017
  /* @__PURE__ */ jsx115("span", { className: "oc-visually-hidden", children: toggleLabel })
4027
4018
  ] }),
4028
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.3",
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
@@ -25,8 +25,10 @@ export interface PriceCardProps extends Omit<HTMLAttributes<HTMLElement>, "title
25
25
  monthly: PriceCardPrice;
26
26
  /** Without a yearly price the toggle is not rendered. */
27
27
  yearly?: PriceCardPrice;
28
+ /** Optional third option, e.g. a one-time price. Needs `yearly`. */
29
+ lifetime?: PriceCardPrice;
28
30
  /** Period that is selected first (uncontrolled radio). */
29
- defaultPeriod?: "monthly" | "yearly";
31
+ defaultPeriod?: "monthly" | "yearly" | "lifetime";
30
32
  features?: PriceCardFeature[];
31
33
  /** Call to action, e.g. <a className="oc-btn oc-price-card__cta">. */
32
34
  cta?: ReactNode;
@@ -34,9 +36,10 @@ export interface PriceCardProps extends Omit<HTMLAttributes<HTMLElement>, "title
34
36
  /** Labels of the toggle. */
35
37
  monthlyLabel?: ReactNode;
36
38
  yearlyLabel?: ReactNode;
39
+ lifetimeLabel?: ReactNode;
37
40
  }
38
41
 
39
- function Price({ kind, price }: { kind: "monthly" | "yearly"; price: PriceCardPrice }) {
42
+ function Price({ kind, price }: { kind: "monthly" | "yearly" | "lifetime"; price: PriceCardPrice }) {
40
43
  return (
41
44
  <p className={`oc-price-card__price oc-price-card__price--${kind}`}>
42
45
  <span className="oc-price-card__amount">{price.amount}</span>
@@ -47,10 +50,10 @@ function Price({ kind, price }: { kind: "monthly" | "yearly"; price: PriceCardPr
47
50
  );
48
51
  }
49
52
 
50
- /** `<article class="oc-price-card">`: plan card with a CSS-only monthly / yearly switch. */
53
+ /** `<article class="oc-price-card">`: plan card with a CSS-only monthly / yearly (/ lifetime) switch. */
51
54
  export const PriceCard = forwardRef<HTMLElement, PriceCardProps>(function PriceCard(
52
- { name, description, discount, monthly, yearly, defaultPeriod = "monthly", features = [], cta, featured = false,
53
- monthlyLabel = "Monthly", yearlyLabel = "Yearly", className, ...rest },
55
+ { name, description, discount, monthly, yearly, lifetime, defaultPeriod = "monthly", features = [], cta, featured = false,
56
+ monthlyLabel = "Monthly", yearlyLabel = "Yearly", lifetimeLabel = "Lifetime", className, ...rest },
54
57
  ref,
55
58
  ) {
56
59
  // Every card needs its own radio group name.
@@ -66,10 +69,12 @@ export const PriceCard = forwardRef<HTMLElement, PriceCardProps>(function PriceC
66
69
  <div className="oc-segmented oc-price-card__toggle" role="radiogroup" aria-label="Billing period">
67
70
  <label className="oc-segmented-item"><input type="radio" name={group} value="monthly" defaultChecked={defaultPeriod === "monthly"} /> {monthlyLabel}</label>
68
71
  <label className="oc-segmented-item"><input type="radio" name={group} value="yearly" defaultChecked={defaultPeriod === "yearly"} /> {yearlyLabel}</label>
72
+ {lifetime && <label className="oc-segmented-item"><input type="radio" name={group} value="lifetime" defaultChecked={defaultPeriod === "lifetime"} /> {lifetimeLabel}</label>}
69
73
  </div>
70
74
  )}
71
75
  <Price kind="monthly" price={monthly} />
72
76
  {yearly && <Price kind="yearly" price={yearly} />}
77
+ {yearly && lifetime && <Price kind="lifetime" price={lifetime} />}
73
78
  {features.length > 0 && (
74
79
  <ul className="oc-price-card__features">
75
80
  {features.map((f, i) => (
@@ -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));