@olympusoss/canvas 2.8.6 → 2.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olympusoss/canvas",
3
- "version": "2.8.6",
3
+ "version": "2.9.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -2,6 +2,8 @@ import * as React from "react";
2
2
 
3
3
  import { cn } from "../../lib/utils";
4
4
 
5
+ const DEFAULT_WEEKDAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] as const;
6
+
5
7
  export interface ActivityHeatmapProps extends React.HTMLAttributes<HTMLDivElement> {
6
8
  /**
7
9
  * Cell values in row-major order. Each entry is a number in `[0, 1]`
@@ -81,11 +83,16 @@ export const ActivityHeatmap = React.forwardRef<HTMLDivElement, ActivityHeatmapP
81
83
  const fromLabel = legendObj?.fromLabel ?? "Fewer";
82
84
  const toLabel = legendObj?.toLabel ?? "More";
83
85
  const showLegend = legend !== false;
86
+ // 7-row grids default to weekday labels (Mon–Sun) — covers the common
87
+ // GitHub-style yearly contribution pattern without each consumer
88
+ // re-declaring the array.
89
+ const resolvedRowLabels =
90
+ rowLabels ?? (data.length === 7 ? DEFAULT_WEEKDAY_LABELS.slice() : undefined);
84
91
 
85
92
  return (
86
93
  <div ref={ref} className={cn("w-full", className)} {...props}>
87
94
  <div className="flex gap-2">
88
- {rowLabels && rowLabels.length > 0 && (
95
+ {resolvedRowLabels && resolvedRowLabels.length > 0 && (
89
96
  <div
90
97
  className="grid text-[10px] tabular-nums text-muted-foreground"
91
98
  style={{
@@ -96,7 +103,7 @@ export const ActivityHeatmap = React.forwardRef<HTMLDivElement, ActivityHeatmapP
96
103
  >
97
104
  {Array.from({ length: data.length }, (_, i) => (
98
105
  <span key={`row-label-${i}`} className="flex items-center leading-none">
99
- {rowLabels[i] ?? ""}
106
+ {resolvedRowLabels[i] ?? ""}
100
107
  </span>
101
108
  ))}
102
109
  </div>
@@ -29,6 +29,36 @@ const PALETTE_TARGETS = new Set<unknown>([
29
29
 
30
30
  const PALETTE_SIZE = 5;
31
31
 
32
+ function nextColour(counter: { i: number }) {
33
+ const idx = counter.i++ % PALETTE_SIZE;
34
+ return `hsl(var(--chart-${idx + 1}))`;
35
+ }
36
+
37
+ /**
38
+ * Walk a Pie's `<Cell>` children and inject per-cell palette fills when none
39
+ * is set, so each slice gets a distinct hue without requiring the consumer to
40
+ * write `<Cell fill={...} />` for every entry.
41
+ */
42
+ function paintPieCells(children: React.ReactNode, counter: { i: number }): React.ReactNode {
43
+ return React.Children.map(children, (child) => {
44
+ if (!React.isValidElement(child)) return child;
45
+ const cellProps = child.props as { fill?: unknown };
46
+ if (child.type === RechartsPrimitive.Cell && cellProps.fill === undefined) {
47
+ return React.cloneElement(child, { fill: nextColour(counter) } as Partial<typeof cellProps>);
48
+ }
49
+ return child;
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Inject per-row `fill` on Funnel data when the consumer didn't supply one.
55
+ * Recharts Funnel reads each datum's `fill` to colour the stage, so we map
56
+ * the array and assign palette colours in order.
57
+ */
58
+ function paintFunnelData<T extends { fill?: unknown }>(data: T[], counter: { i: number }): T[] {
59
+ return data.map((row) => (row.fill === undefined ? { ...row, fill: nextColour(counter) } : row));
60
+ }
61
+
32
62
  /**
33
63
  * Recursively walk the children, cloning any data primitive that's missing
34
64
  * `fill`/`stroke` and injecting an `hsl(var(--chart-N))` default. Counter is
@@ -37,12 +67,41 @@ const PALETTE_SIZE = 5;
37
67
  function applyPalette(children: React.ReactNode, counter: { i: number }): React.ReactNode {
38
68
  return React.Children.map(children, (child) => {
39
69
  if (!React.isValidElement(child)) return child;
40
- const props = child.props as { fill?: unknown; stroke?: unknown; children?: React.ReactNode };
70
+ const props = child.props as {
71
+ fill?: unknown;
72
+ stroke?: unknown;
73
+ children?: React.ReactNode;
74
+ data?: unknown;
75
+ label?: unknown;
76
+ };
77
+
78
+ // Pie: walk Cell children and assign per-slice palette colours so each
79
+ // slice is distinct. Also default `label={true}` so slices render their
80
+ // data-key value at the perimeter without needing a `<LabelList>`.
81
+ if (child.type === RechartsPrimitive.Pie) {
82
+ const next: Record<string, unknown> = {};
83
+ if (props.children !== undefined) {
84
+ next.children = paintPieCells(props.children, counter);
85
+ } else if (props.fill === undefined && props.stroke === undefined) {
86
+ const colour = nextColour(counter);
87
+ next.fill = colour;
88
+ next.stroke = colour;
89
+ }
90
+ if (props.label === undefined) next.label = true;
91
+ return React.cloneElement(child, next as Partial<typeof props>);
92
+ }
93
+
94
+ // Funnel: distribute palette colours across the data array (Recharts
95
+ // reads `fill` from each datum, not from the <Funnel> element).
96
+ if (child.type === RechartsPrimitive.Funnel && Array.isArray(props.data)) {
97
+ return React.cloneElement(child, {
98
+ data: paintFunnelData(props.data as { fill?: unknown }[], counter),
99
+ } as Partial<typeof props>);
100
+ }
41
101
 
42
102
  if (PALETTE_TARGETS.has(child.type)) {
43
103
  if (props.fill === undefined && props.stroke === undefined) {
44
- const idx = counter.i++ % PALETTE_SIZE;
45
- const colour = `hsl(var(--chart-${idx + 1}))`;
104
+ const colour = nextColour(counter);
46
105
  // Pie/Radar/Area also benefit from a matching stroke; the
47
106
  // component decides which one applies (Bar reads fill, Line
48
107
  // reads stroke, etc.).
@@ -115,7 +174,7 @@ const ChartContainer = React.forwardRef<
115
174
  data-chart={chartId}
116
175
  ref={ref}
117
176
  className={cn(
118
- "flex aspect-video w-full justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
177
+ "flex aspect-video w-full justify-center text-xs [&_.recharts-cartesian-axis-line]:stroke-border [&_.recharts-cartesian-axis-tick-line]:stroke-transparent [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
119
178
  className,
120
179
  )}
121
180
  {...props}
@@ -10,11 +10,18 @@ export interface CodeBlockProps extends React.HTMLAttributes<HTMLDivElement> {
10
10
  code: string;
11
11
  language?: string;
12
12
  showCopy?: boolean;
13
+ /**
14
+ * `"light"` (default) renders against `bg-muted`; `"dark"` switches to a
15
+ * near-black terminal palette (`#0a0a0b` background, `#e4e4e7` text) for
16
+ * use on marketing surfaces.
17
+ */
18
+ theme?: "light" | "dark";
13
19
  }
14
20
 
15
21
  const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
16
- ({ code, language, showCopy = true, className, ...props }, ref) => {
22
+ ({ code, language, showCopy = true, theme = "light", className, ...props }, ref) => {
17
23
  const [copied, setCopied] = React.useState(false);
24
+ const isDark = theme === "dark";
18
25
 
19
26
  const handleCopy = React.useCallback(async () => {
20
27
  await navigator.clipboard.writeText(code);
@@ -23,20 +30,50 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
23
30
  }, [code]);
24
31
 
25
32
  return (
26
- <div ref={ref} className={cn("relative rounded-md bg-muted", className)} {...props}>
33
+ <div
34
+ ref={ref}
35
+ className={cn(
36
+ "relative overflow-hidden rounded-md",
37
+ isDark ? "border border-border" : "bg-muted",
38
+ className,
39
+ )}
40
+ style={isDark ? { background: "#0a0a0b" } : undefined}
41
+ {...props}
42
+ >
27
43
  {(language || showCopy) && (
28
- <div className="flex items-center justify-between border-b px-4 py-2">
29
- {language && (
30
- <span className="text-xs font-medium text-muted-foreground">{language}</span>
44
+ <div
45
+ className={cn(
46
+ "flex items-center justify-between px-4 py-2",
47
+ isDark ? undefined : "border-b",
48
+ )}
49
+ style={isDark ? { borderBottom: "1px solid #222" } : undefined}
50
+ >
51
+ {language ? (
52
+ <span
53
+ className={cn("text-xs font-medium", !isDark && "text-muted-foreground")}
54
+ style={isDark ? { color: "#6b7280" } : undefined}
55
+ >
56
+ {language}
57
+ </span>
58
+ ) : (
59
+ <span />
31
60
  )}
32
61
  {showCopy && (
33
- <Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleCopy}>
62
+ <Button
63
+ variant="ghost"
64
+ size="icon"
65
+ className={cn(
66
+ "h-6 w-6",
67
+ isDark && "text-zinc-400 hover:bg-white/5 hover:text-zinc-100",
68
+ )}
69
+ onClick={handleCopy}
70
+ >
34
71
  {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
35
72
  </Button>
36
73
  )}
37
74
  </div>
38
75
  )}
39
- <pre className="overflow-x-auto p-4">
76
+ <pre className="overflow-x-auto p-4" style={isDark ? { color: "#e4e4e7" } : undefined}>
40
77
  <code className="text-sm">{code}</code>
41
78
  </pre>
42
79
  </div>
@@ -0,0 +1,152 @@
1
+ // molecules: can import tokens/, lib/utils, atoms/.
2
+ "use client";
3
+
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ const TONE_STYLES = {
9
+ default: {
10
+ bg: "hsl(var(--primary) / 0.1)",
11
+ fg: "hsl(var(--primary))",
12
+ ring: "hsl(var(--primary) / 0.2)",
13
+ },
14
+ indigo: {
15
+ bg: "hsl(231 92% 96%)",
16
+ fg: "hsl(231 60% 38%)",
17
+ ring: "hsl(231 80% 60% / 0.25)",
18
+ },
19
+ violet: {
20
+ bg: "hsl(262 90% 96%)",
21
+ fg: "hsl(262 55% 42%)",
22
+ ring: "hsl(262 80% 60% / 0.25)",
23
+ },
24
+ slate: {
25
+ bg: "hsl(230 25% 95%)",
26
+ fg: "hsl(230 30% 30%)",
27
+ ring: "hsl(230 20% 60% / 0.25)",
28
+ },
29
+ } as const;
30
+
31
+ export type LauncherCardTone = keyof typeof TONE_STYLES;
32
+
33
+ export interface LauncherCardProps {
34
+ /**
35
+ * Top-left identifier — typically a letter (`"C"`) or a small icon.
36
+ */
37
+ badge: React.ReactNode;
38
+ /** Card heading. */
39
+ title: string;
40
+ /** One-or-two sentence body copy below the title. */
41
+ description: string;
42
+ /**
43
+ * Colour key for the badge background, foreground, and hover ring.
44
+ * Defaults to `"default"`, which uses the `--primary` token.
45
+ */
46
+ tone?: LauncherCardTone;
47
+ /**
48
+ * If set, the entire card becomes a link with a hover-lift effect.
49
+ * Pair with `linkComponent` to route through a framework-specific Link
50
+ * component (e.g. Next.js).
51
+ */
52
+ href?: string;
53
+ /**
54
+ * `target` attribute applied when `href` is set. Defaults to `"_self"`.
55
+ */
56
+ target?: React.HTMLAttributeAnchorTarget;
57
+ /**
58
+ * `rel` attribute applied when `href` is set. Defaults to `"noopener"`
59
+ * when `target === "_blank"`.
60
+ */
61
+ rel?: string;
62
+ /**
63
+ * Override the rendered link element when `href` is set
64
+ * (e.g. `Link` from `next/link`). Falls back to `<a>`.
65
+ */
66
+ linkComponent?: React.ElementType;
67
+ /**
68
+ * Footer slot. Typically a small CTA row (e.g. arrow link) or, when the
69
+ * card is non-interactive, an inline status / detail block.
70
+ */
71
+ children?: React.ReactNode;
72
+ className?: string;
73
+ }
74
+
75
+ const SHARED_CLASSES =
76
+ "group flex flex-col gap-4 rounded-[14px] border border-border bg-card p-6 text-card-foreground";
77
+
78
+ const INTERACTIVE_CLASSES =
79
+ "no-underline transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-[0_20px_40px_-20px_var(--launcher-tone-ring),0_8px_16px_-8px_rgb(0_0_0/0.08)] hover:[border-color:var(--launcher-tone-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
80
+
81
+ const LauncherCard = React.forwardRef<HTMLElement, LauncherCardProps>(
82
+ (
83
+ {
84
+ badge,
85
+ title,
86
+ description,
87
+ tone = "default",
88
+ href,
89
+ target,
90
+ rel,
91
+ linkComponent,
92
+ children,
93
+ className,
94
+ ...props
95
+ },
96
+ ref,
97
+ ) => {
98
+ const t = TONE_STYLES[tone];
99
+ const interactive = Boolean(href);
100
+ const toneVars = {
101
+ "--launcher-tone-fg": t.fg,
102
+ "--launcher-tone-ring": t.ring,
103
+ } as React.CSSProperties;
104
+
105
+ const inner = (
106
+ <>
107
+ <div className="flex items-center gap-3.5">
108
+ <div
109
+ className="flex h-11 w-11 items-center justify-center rounded-[10px] text-lg font-semibold tracking-tight"
110
+ style={{ background: t.bg, color: t.fg }}
111
+ >
112
+ {badge}
113
+ </div>
114
+ <div className="text-[17px] font-semibold tracking-tight">{title}</div>
115
+ </div>
116
+ <p className="m-0 flex-1 text-sm leading-relaxed text-muted-foreground">{description}</p>
117
+ {children != null && <div className="mt-1">{children}</div>}
118
+ </>
119
+ );
120
+
121
+ if (interactive) {
122
+ const LinkEl = linkComponent || "a";
123
+ const resolvedRel = rel ?? (target === "_blank" ? "noopener" : undefined);
124
+ return (
125
+ <LinkEl
126
+ ref={ref as React.Ref<HTMLAnchorElement>}
127
+ href={href}
128
+ target={target}
129
+ rel={resolvedRel}
130
+ className={cn(SHARED_CLASSES, INTERACTIVE_CLASSES, className)}
131
+ style={toneVars}
132
+ {...props}
133
+ >
134
+ {inner}
135
+ </LinkEl>
136
+ );
137
+ }
138
+
139
+ return (
140
+ <div
141
+ ref={ref as React.Ref<HTMLDivElement>}
142
+ className={cn(SHARED_CLASSES, className)}
143
+ {...props}
144
+ >
145
+ {inner}
146
+ </div>
147
+ );
148
+ },
149
+ );
150
+ LauncherCard.displayName = "LauncherCard";
151
+
152
+ export { LauncherCard };
@@ -0,0 +1,73 @@
1
+ // molecules: can import tokens/, lib/utils, atoms/.
2
+ import * as React from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ export interface TerminalProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
7
+ /**
8
+ * Optional label rendered in the macOS-style chrome strip, next to the
9
+ * traffic-light dots. Typical use: a working directory + command
10
+ * (e.g. `~/Olympus · octl`).
11
+ */
12
+ title?: React.ReactNode;
13
+ /**
14
+ * Terminal body. Renders inside a `<pre>` with mono font and 1.7 line
15
+ * height. Free-form — drop plain text, ANSI-coloured `<span>`s, or any
16
+ * inline highlights you want.
17
+ */
18
+ children: React.ReactNode;
19
+ className?: string;
20
+ }
21
+
22
+ const Terminal = React.forwardRef<HTMLDivElement, TerminalProps>(
23
+ ({ title, children, className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn(
27
+ "overflow-hidden rounded-xl border border-border shadow-[0_30px_60px_-20px_rgb(0_0_0/0.35)]",
28
+ className,
29
+ )}
30
+ style={{ background: "#0a0a0b" }}
31
+ {...props}
32
+ >
33
+ <div
34
+ className="flex items-center gap-1.5 px-3"
35
+ style={{
36
+ height: 32,
37
+ background: "#141417",
38
+ borderBottom: "1px solid #222",
39
+ }}
40
+ >
41
+ <span
42
+ aria-hidden="true"
43
+ className="inline-block h-2.5 w-2.5 rounded-full"
44
+ style={{ background: "#ff5f57" }}
45
+ />
46
+ <span
47
+ aria-hidden="true"
48
+ className="inline-block h-2.5 w-2.5 rounded-full"
49
+ style={{ background: "#febc2e" }}
50
+ />
51
+ <span
52
+ aria-hidden="true"
53
+ className="inline-block h-2.5 w-2.5 rounded-full"
54
+ style={{ background: "#28c840" }}
55
+ />
56
+ {title != null && (
57
+ <span className="ml-2.5 font-mono text-[11px]" style={{ color: "#6b7280" }}>
58
+ {title}
59
+ </span>
60
+ )}
61
+ </div>
62
+ <pre
63
+ className="m-0 overflow-x-auto p-5 font-mono text-[12.5px] leading-[1.7]"
64
+ style={{ color: "#e4e4e7" }}
65
+ >
66
+ {children}
67
+ </pre>
68
+ </div>
69
+ ),
70
+ );
71
+ Terminal.displayName = "Terminal";
72
+
73
+ export { Terminal };
package/src/index.ts CHANGED
@@ -189,6 +189,11 @@ export {
189
189
  InputOTPSeparator,
190
190
  InputOTPSlot,
191
191
  } from "./components/molecules/input-otp";
192
+ export {
193
+ LauncherCard,
194
+ type LauncherCardProps,
195
+ type LauncherCardTone,
196
+ } from "./components/molecules/launcher-card";
192
197
  export {
193
198
  LoadingState,
194
199
  type LoadingStateProps,
@@ -260,6 +265,7 @@ export {
260
265
  TableHeader,
261
266
  TableRow,
262
267
  } from "./components/molecules/table";
268
+ export { Terminal, type TerminalProps } from "./components/molecules/terminal";
263
269
  export { ToggleGroup, ToggleGroupItem } from "./components/molecules/toggle-group";
264
270
  export {
265
271
  Tooltip,