@lablup/ui-common 0.1.0-alpha.16 → 0.1.0-alpha.17

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.
@@ -13,6 +13,15 @@ export interface StatCardTrend {
13
13
  export interface StatCardProps {
14
14
  /** Short label rendered above the value (UPPERCASE styled) */
15
15
  label: string;
16
+ /**
17
+ * Rendered in place of `label` when the label is not plain text: a glossary
18
+ * term carrying its definition, a unit badge, an info affordance.
19
+ *
20
+ * `label` stays required and stays the accessible name, so the name is a
21
+ * string the consumer wrote rather than whatever text happens to fall out of
22
+ * a node. Pass the same words in both.
23
+ */
24
+ labelNode?: ReactNode;
16
25
  /** Primary metric value (number is coerced to localised string) */
17
26
  value: string | number;
18
27
  /** Optional suffix appended to the value (e.g. "/ 4", "GB") */
@@ -54,6 +63,6 @@ export interface StatCardProps {
54
63
  /** Visual weight. Default: "default" */
55
64
  emphasis?: StatCardEmphasis;
56
65
  }
57
- declare function StatCardComponent({ label, value, valueSuffix, hint, icon, tone, trend, onClick, loading, ariaLabel, className, testId, format, animate, sparkline, emphasis, }: StatCardProps): JSX.Element;
66
+ declare function StatCardComponent({ label, labelNode, value, valueSuffix, hint, icon, tone, trend, onClick, loading, ariaLabel, className, testId, format, animate, sparkline, emphasis, }: StatCardProps): JSX.Element;
58
67
  export declare const StatCard: import('react').MemoExoticComponent<typeof StatCardComponent>;
59
68
  export {};
@@ -30,76 +30,76 @@ var d = {
30
30
  function f(e, t) {
31
31
  return typeof e == "number" ? t ? t(e) : e.toLocaleString() : e;
32
32
  }
33
- function p({ label: r, value: i, valueSuffix: a, hint: o, icon: l, tone: p = "default", trend: m, onClick: h, loading: g = !1, ariaLabel: _, className: v = "", testId: y, format: b, animate: x = !1, sparkline: S, emphasis: C = "default" }) {
34
- let w = n(), T = typeof i == "number", E = x && T && !g && !w, D = u(T ? i : 0, E), O = f(T && E ? D : i, b), k = _ ?? (g ? r : `${r}: ${f(i, b)}${a ?? ""}`), A = [
33
+ function p({ label: r, labelNode: i, value: a, valueSuffix: o, hint: l, icon: p, tone: m = "default", trend: h, onClick: g, loading: _ = !1, ariaLabel: v, className: y = "", testId: b, format: x, animate: S = !1, sparkline: C, emphasis: w = "default" }) {
34
+ let T = n(), E = typeof a == "number", D = S && E && !_ && !T, O = u(E ? a : 0, D), k = f(E && D ? O : a, x), A = v ?? (_ ? r : `${r}: ${f(a, x)}${o ?? ""}`), j = [
35
35
  "stat-card",
36
36
  "corner-accent",
37
- `stat-card--tone-${p}`,
38
- C === "default" ? "" : `stat-card--${C}`,
39
- v
37
+ `stat-card--tone-${m}`,
38
+ w === "default" ? "" : `stat-card--${w}`,
39
+ y
40
40
  ].filter(Boolean).join(" ");
41
41
  return /* @__PURE__ */ c(e, {
42
- className: A,
43
- onClick: h,
44
- role: h ? void 0 : "group",
45
- ariaLabel: k,
46
- testId: y,
42
+ className: j,
43
+ onClick: g,
44
+ role: g ? void 0 : "group",
45
+ ariaLabel: A,
46
+ testId: b,
47
47
  children: [
48
48
  /* @__PURE__ */ c("div", {
49
49
  className: "stat-card__header",
50
50
  children: [/* @__PURE__ */ s("span", {
51
51
  className: "stat-card__label",
52
- children: r
53
- }), l && /* @__PURE__ */ s("span", {
52
+ children: i ?? r
53
+ }), p && /* @__PURE__ */ s("span", {
54
54
  className: "stat-card__icon",
55
55
  "aria-hidden": "true",
56
- children: l
56
+ children: p
57
57
  })]
58
58
  }),
59
59
  /* @__PURE__ */ c("div", {
60
60
  className: "stat-card__body",
61
- children: [g ? /* @__PURE__ */ s(t, {
61
+ children: [_ ? /* @__PURE__ */ s(t, {
62
62
  width: "60%",
63
63
  height: "2rem"
64
- }) : S ? /* @__PURE__ */ c("span", {
64
+ }) : C ? /* @__PURE__ */ c("span", {
65
65
  className: "stat-card__value-line",
66
66
  children: [/* @__PURE__ */ c("span", {
67
67
  className: "stat-card__value-row",
68
68
  children: [/* @__PURE__ */ s("span", {
69
69
  className: "stat-card__value",
70
- children: O
71
- }), a && /* @__PURE__ */ s("span", {
70
+ children: k
71
+ }), o && /* @__PURE__ */ s("span", {
72
72
  className: "stat-card__value-suffix",
73
- children: a
73
+ children: o
74
74
  })]
75
75
  }), /* @__PURE__ */ s("span", {
76
76
  className: "stat-card__sparkline",
77
- children: S
77
+ children: C
78
78
  })]
79
79
  }) : /* @__PURE__ */ c("span", {
80
80
  className: "stat-card__value-row",
81
81
  children: [/* @__PURE__ */ s("span", {
82
82
  className: "stat-card__value",
83
- children: O
84
- }), a && /* @__PURE__ */ s("span", {
83
+ children: k
84
+ }), o && /* @__PURE__ */ s("span", {
85
85
  className: "stat-card__value-suffix",
86
- children: a
86
+ children: o
87
87
  })]
88
- }), m && !g && /* @__PURE__ */ c("span", {
89
- className: `stat-card__trend stat-card__trend--${m.direction}`,
90
- "aria-label": m.ariaLabel ?? m.label,
88
+ }), h && !_ && /* @__PURE__ */ c("span", {
89
+ className: `stat-card__trend stat-card__trend--${h.direction}`,
90
+ "aria-label": h.ariaLabel ?? h.label,
91
91
  children: [/* @__PURE__ */ s("span", {
92
92
  "aria-hidden": "true",
93
- children: d[m.direction]
94
- }), /* @__PURE__ */ s("span", { children: m.label })]
93
+ children: d[h.direction]
94
+ }), /* @__PURE__ */ s("span", { children: h.label })]
95
95
  })]
96
96
  }),
97
- o != null && /* @__PURE__ */ s("div", {
97
+ l != null && /* @__PURE__ */ s("div", {
98
98
  className: "stat-card__hint",
99
- children: g ? /* @__PURE__ */ s(t, {
99
+ children: _ ? /* @__PURE__ */ s(t, {
100
100
  width: "80%",
101
101
  height: "0.85rem"
102
- }) : o
102
+ }) : l
103
103
  })
104
104
  ]
105
105
  });
@@ -1 +1 @@
1
- {"version":3,"file":"StatCard2.js","names":[],"sources":["../../../src/components/StatCard/StatCard.tsx"],"sourcesContent":["/**\n * StatCard Component\n *\n * Compact metric card for dashboards. Renders a label, a large numeric\n * value (with optional unit/suffix), and optional icon, hint, trend, or\n * accent tone. Wraps `BaseCard` so it inherits hover/focus/click affordances\n * and design tokens consistently with the rest of the common library.\n *\n * Designed for cross-page reuse: Squad dashboard, Statistics page, Cowork\n * dashboard, etc.\n */\n\nimport { memo, useEffect, useRef, useState, type ReactNode, type JSX } from \"react\";\nimport { usePrefersReducedMotion } from \"../../hooks/usePrefersReducedMotion\";\nimport { BaseCard } from \"../BaseCard\";\nimport { Skeleton } from \"../Skeleton\";\nimport \"./StatCard.css\";\n\nexport type StatCardEmphasis = \"compact\" | \"default\" | \"prominent\";\n\nexport type StatCardTone = \"default\" | \"success\" | \"warning\" | \"danger\" | \"info\";\n\nexport type StatCardTrendDirection = \"up\" | \"down\" | \"flat\";\n\nexport interface StatCardTrend {\n /** Direction indicator (up / down / flat) */\n direction: StatCardTrendDirection;\n /** Human-readable delta label (e.g. \"+12%\", \"-3\", \"0\") */\n label: string;\n /** Optional ARIA description for screen readers */\n ariaLabel?: string;\n}\n\nexport interface StatCardProps {\n /** Short label rendered above the value (UPPERCASE styled) */\n label: string;\n /** Primary metric value (number is coerced to localised string) */\n value: string | number;\n /** Optional suffix appended to the value (e.g. \"/ 4\", \"GB\") */\n valueSuffix?: string;\n /** Optional helper line under the value (e.g. context, secondary metric) */\n hint?: ReactNode;\n /** Optional leading icon node, rendered inside an accent badge */\n icon?: ReactNode;\n /** Visual tone, mirrors Badge variants. Default: \"default\" */\n tone?: StatCardTone;\n /** Optional trend indicator under the value */\n trend?: StatCardTrend;\n /** Click handler. Renders the card as keyboard-accessible button. */\n onClick?: () => void;\n /** Render skeletons instead of value/label content while loading */\n loading?: boolean;\n /** Optional aria label override (defaults to label + value) */\n ariaLabel?: string;\n /** Extra class name applied to the root */\n className?: string;\n /** Test id for unit tests */\n testId?: string;\n /**\n * Formatter for numeric values (e.g. abbreviate to \"1.2K\", append \"%\").\n * Defaults to `toLocaleString`. Ignored when `value` is a string.\n */\n format?: (value: number) => string;\n /**\n * Count up to a numeric value on mount and on change. Off by default so\n * existing consumers render the final value immediately; opt in for\n * dashboard hero metrics. Suppressed under `prefers-reduced-motion`.\n */\n animate?: boolean;\n /**\n * Optional trailing visual on the value line (e.g. a sparkline). Kept as a\n * slot so the card does not depend on any particular chart component.\n */\n sparkline?: ReactNode;\n /** Visual weight. Default: \"default\" */\n emphasis?: StatCardEmphasis;\n}\n\nconst ANIMATION_DURATION_MS = 800;\n\n/**\n * Counts from the previous value to `target` with an ease-out-cubic curve.\n * When `enabled` is false the target is returned directly and no animation\n * frame is ever scheduled.\n */\nfunction useAnimatedValue(target: number, enabled: boolean): number {\n const [displayValue, setDisplayValue] = useState(enabled ? 0 : target);\n const previousValue = useRef(enabled ? 0 : target);\n const frameRef = useRef<number | null>(null);\n\n useEffect(() => {\n if (!enabled) {\n previousValue.current = target;\n setDisplayValue(target);\n return;\n }\n\n const startValue = previousValue.current;\n const startTime = performance.now();\n\n const step = (now: number): void => {\n const progress = Math.min((now - startTime) / ANIMATION_DURATION_MS, 1);\n const eased = 1 - Math.pow(1 - progress, 3);\n setDisplayValue(Math.round(startValue + (target - startValue) * eased));\n\n if (progress < 1) {\n frameRef.current = requestAnimationFrame(step);\n } else {\n previousValue.current = target;\n }\n };\n\n frameRef.current = requestAnimationFrame(step);\n\n return () => {\n if (frameRef.current !== null) {\n cancelAnimationFrame(frameRef.current);\n }\n };\n }, [target, enabled]);\n\n return enabled ? displayValue : target;\n}\n\nconst TREND_GLYPH: Record<StatCardTrendDirection, string> = {\n up: \"▲\",\n down: \"▼\",\n flat: \"•\",\n};\n\nfunction formatValue(\n value: string | number,\n format?: (value: number) => string,\n): string {\n if (typeof value === \"number\") {\n return format ? format(value) : value.toLocaleString();\n }\n return value;\n}\n\nfunction StatCardComponent({\n label,\n value,\n valueSuffix,\n hint,\n icon,\n tone = \"default\",\n trend,\n onClick,\n loading = false,\n ariaLabel,\n className = \"\",\n testId,\n format,\n animate = false,\n sparkline,\n emphasis = \"default\",\n}: StatCardProps): JSX.Element {\n const prefersReducedMotion = usePrefersReducedMotion();\n const isNumeric = typeof value === \"number\";\n const shouldAnimate = animate && isNumeric && !loading && !prefersReducedMotion;\n const animatedValue = useAnimatedValue(isNumeric ? value : 0, shouldAnimate);\n\n const displayValue = formatValue(\n isNumeric && shouldAnimate ? animatedValue : value,\n format,\n );\n\n // The aria-label always describes the settled value, never an in-flight\n // animation frame, so assistive tech is not read a counting-up number.\n const composedAriaLabel =\n ariaLabel ??\n (loading ? label : `${label}: ${formatValue(value, format)}${valueSuffix ?? \"\"}`);\n\n const rootClass = [\n \"stat-card\",\n \"corner-accent\",\n `stat-card--tone-${tone}`,\n emphasis !== \"default\" ? `stat-card--${emphasis}` : \"\",\n className,\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <BaseCard\n className={rootClass}\n onClick={onClick}\n role={onClick ? undefined : \"group\"}\n ariaLabel={composedAriaLabel}\n testId={testId}\n >\n <div className=\"stat-card__header\">\n <span className=\"stat-card__label\">{label}</span>\n {icon && (\n <span className=\"stat-card__icon\" aria-hidden=\"true\">\n {icon}\n </span>\n )}\n </div>\n\n <div className=\"stat-card__body\">\n {loading ? (\n <Skeleton width=\"60%\" height=\"2rem\" />\n ) : sparkline ? (\n // Only wrap when a sparkline is present, so cards without one keep\n // the exact DOM they had before the slot existed.\n <span className=\"stat-card__value-line\">\n <span className=\"stat-card__value-row\">\n <span className=\"stat-card__value\">{displayValue}</span>\n {valueSuffix && (\n <span className=\"stat-card__value-suffix\">{valueSuffix}</span>\n )}\n </span>\n <span className=\"stat-card__sparkline\">{sparkline}</span>\n </span>\n ) : (\n <span className=\"stat-card__value-row\">\n <span className=\"stat-card__value\">{displayValue}</span>\n {valueSuffix && (\n <span className=\"stat-card__value-suffix\">{valueSuffix}</span>\n )}\n </span>\n )}\n\n {trend && !loading && (\n <span\n className={`stat-card__trend stat-card__trend--${trend.direction}`}\n aria-label={trend.ariaLabel ?? trend.label}\n >\n <span aria-hidden=\"true\">{TREND_GLYPH[trend.direction]}</span>\n <span>{trend.label}</span>\n </span>\n )}\n </div>\n\n {hint !== undefined && hint !== null && (\n <div className=\"stat-card__hint\">\n {loading ? <Skeleton width=\"80%\" height=\"0.85rem\" /> : hint}\n </div>\n )}\n </BaseCard>\n );\n}\n\nexport const StatCard = memo(StatCardComponent);\n"],"mappings":"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"}
1
+ {"version":3,"file":"StatCard2.js","names":[],"sources":["../../../src/components/StatCard/StatCard.tsx"],"sourcesContent":["/**\n * StatCard Component\n *\n * Compact metric card for dashboards. Renders a label, a large numeric\n * value (with optional unit/suffix), and optional icon, hint, trend, or\n * accent tone. The label can be a node (`labelNode`) while the accessible name\n * stays the plain `label` string. Wraps `BaseCard` so it inherits hover/focus/click affordances\n * and design tokens consistently with the rest of the common library.\n *\n * Designed for cross-page reuse: Squad dashboard, Statistics page, Cowork\n * dashboard, etc.\n */\n\nimport { memo, useEffect, useRef, useState, type ReactNode, type JSX } from \"react\";\nimport { usePrefersReducedMotion } from \"../../hooks/usePrefersReducedMotion\";\nimport { BaseCard } from \"../BaseCard\";\nimport { Skeleton } from \"../Skeleton\";\nimport \"./StatCard.css\";\n\nexport type StatCardEmphasis = \"compact\" | \"default\" | \"prominent\";\n\nexport type StatCardTone = \"default\" | \"success\" | \"warning\" | \"danger\" | \"info\";\n\nexport type StatCardTrendDirection = \"up\" | \"down\" | \"flat\";\n\nexport interface StatCardTrend {\n /** Direction indicator (up / down / flat) */\n direction: StatCardTrendDirection;\n /** Human-readable delta label (e.g. \"+12%\", \"-3\", \"0\") */\n label: string;\n /** Optional ARIA description for screen readers */\n ariaLabel?: string;\n}\n\nexport interface StatCardProps {\n /** Short label rendered above the value (UPPERCASE styled) */\n label: string;\n /**\n * Rendered in place of `label` when the label is not plain text: a glossary\n * term carrying its definition, a unit badge, an info affordance.\n *\n * `label` stays required and stays the accessible name, so the name is a\n * string the consumer wrote rather than whatever text happens to fall out of\n * a node. Pass the same words in both.\n */\n labelNode?: ReactNode;\n /** Primary metric value (number is coerced to localised string) */\n value: string | number;\n /** Optional suffix appended to the value (e.g. \"/ 4\", \"GB\") */\n valueSuffix?: string;\n /** Optional helper line under the value (e.g. context, secondary metric) */\n hint?: ReactNode;\n /** Optional leading icon node, rendered inside an accent badge */\n icon?: ReactNode;\n /** Visual tone, mirrors Badge variants. Default: \"default\" */\n tone?: StatCardTone;\n /** Optional trend indicator under the value */\n trend?: StatCardTrend;\n /** Click handler. Renders the card as keyboard-accessible button. */\n onClick?: () => void;\n /** Render skeletons instead of value/label content while loading */\n loading?: boolean;\n /** Optional aria label override (defaults to label + value) */\n ariaLabel?: string;\n /** Extra class name applied to the root */\n className?: string;\n /** Test id for unit tests */\n testId?: string;\n /**\n * Formatter for numeric values (e.g. abbreviate to \"1.2K\", append \"%\").\n * Defaults to `toLocaleString`. Ignored when `value` is a string.\n */\n format?: (value: number) => string;\n /**\n * Count up to a numeric value on mount and on change. Off by default so\n * existing consumers render the final value immediately; opt in for\n * dashboard hero metrics. Suppressed under `prefers-reduced-motion`.\n */\n animate?: boolean;\n /**\n * Optional trailing visual on the value line (e.g. a sparkline). Kept as a\n * slot so the card does not depend on any particular chart component.\n */\n sparkline?: ReactNode;\n /** Visual weight. Default: \"default\" */\n emphasis?: StatCardEmphasis;\n}\n\nconst ANIMATION_DURATION_MS = 800;\n\n/**\n * Counts from the previous value to `target` with an ease-out-cubic curve.\n * When `enabled` is false the target is returned directly and no animation\n * frame is ever scheduled.\n */\nfunction useAnimatedValue(target: number, enabled: boolean): number {\n const [displayValue, setDisplayValue] = useState(enabled ? 0 : target);\n const previousValue = useRef(enabled ? 0 : target);\n const frameRef = useRef<number | null>(null);\n\n useEffect(() => {\n if (!enabled) {\n previousValue.current = target;\n setDisplayValue(target);\n return;\n }\n\n const startValue = previousValue.current;\n const startTime = performance.now();\n\n const step = (now: number): void => {\n const progress = Math.min((now - startTime) / ANIMATION_DURATION_MS, 1);\n const eased = 1 - Math.pow(1 - progress, 3);\n setDisplayValue(Math.round(startValue + (target - startValue) * eased));\n\n if (progress < 1) {\n frameRef.current = requestAnimationFrame(step);\n } else {\n previousValue.current = target;\n }\n };\n\n frameRef.current = requestAnimationFrame(step);\n\n return () => {\n if (frameRef.current !== null) {\n cancelAnimationFrame(frameRef.current);\n }\n };\n }, [target, enabled]);\n\n return enabled ? displayValue : target;\n}\n\nconst TREND_GLYPH: Record<StatCardTrendDirection, string> = {\n up: \"▲\",\n down: \"▼\",\n flat: \"•\",\n};\n\nfunction formatValue(\n value: string | number,\n format?: (value: number) => string,\n): string {\n if (typeof value === \"number\") {\n return format ? format(value) : value.toLocaleString();\n }\n return value;\n}\n\nfunction StatCardComponent({\n label,\n labelNode,\n value,\n valueSuffix,\n hint,\n icon,\n tone = \"default\",\n trend,\n onClick,\n loading = false,\n ariaLabel,\n className = \"\",\n testId,\n format,\n animate = false,\n sparkline,\n emphasis = \"default\",\n}: StatCardProps): JSX.Element {\n const prefersReducedMotion = usePrefersReducedMotion();\n const isNumeric = typeof value === \"number\";\n const shouldAnimate = animate && isNumeric && !loading && !prefersReducedMotion;\n const animatedValue = useAnimatedValue(isNumeric ? value : 0, shouldAnimate);\n\n const displayValue = formatValue(\n isNumeric && shouldAnimate ? animatedValue : value,\n format,\n );\n\n // The aria-label always describes the settled value, never an in-flight\n // animation frame, so assistive tech is not read a counting-up number.\n const composedAriaLabel =\n ariaLabel ??\n (loading ? label : `${label}: ${formatValue(value, format)}${valueSuffix ?? \"\"}`);\n\n const rootClass = [\n \"stat-card\",\n \"corner-accent\",\n `stat-card--tone-${tone}`,\n emphasis !== \"default\" ? `stat-card--${emphasis}` : \"\",\n className,\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <BaseCard\n className={rootClass}\n onClick={onClick}\n role={onClick ? undefined : \"group\"}\n ariaLabel={composedAriaLabel}\n testId={testId}\n >\n <div className=\"stat-card__header\">\n <span className=\"stat-card__label\">{labelNode ?? label}</span>\n {icon && (\n <span className=\"stat-card__icon\" aria-hidden=\"true\">\n {icon}\n </span>\n )}\n </div>\n\n <div className=\"stat-card__body\">\n {loading ? (\n <Skeleton width=\"60%\" height=\"2rem\" />\n ) : sparkline ? (\n // Only wrap when a sparkline is present, so cards without one keep\n // the exact DOM they had before the slot existed.\n <span className=\"stat-card__value-line\">\n <span className=\"stat-card__value-row\">\n <span className=\"stat-card__value\">{displayValue}</span>\n {valueSuffix && (\n <span className=\"stat-card__value-suffix\">{valueSuffix}</span>\n )}\n </span>\n <span className=\"stat-card__sparkline\">{sparkline}</span>\n </span>\n ) : (\n <span className=\"stat-card__value-row\">\n <span className=\"stat-card__value\">{displayValue}</span>\n {valueSuffix && (\n <span className=\"stat-card__value-suffix\">{valueSuffix}</span>\n )}\n </span>\n )}\n\n {trend && !loading && (\n <span\n className={`stat-card__trend stat-card__trend--${trend.direction}`}\n aria-label={trend.ariaLabel ?? trend.label}\n >\n <span aria-hidden=\"true\">{TREND_GLYPH[trend.direction]}</span>\n <span>{trend.label}</span>\n </span>\n )}\n </div>\n\n {hint !== undefined && hint !== null && (\n <div className=\"stat-card__hint\">\n {loading ? <Skeleton width=\"80%\" height=\"0.85rem\" /> : hint}\n </div>\n )}\n </BaseCard>\n );\n}\n\nexport const StatCard = memo(StatCardComponent);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lablup/ui-common",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "Shared, product-neutral UI components and design tokens for Lablup products",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Lablup Inc.",