@robr0/design-system 0.8.0 → 0.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/README.md +3 -3
- package/components/Accordion/Accordion.js +1 -0
- package/components/AgentPlan/AgentPlan.css +2 -2
- package/components/AgentPlan/AgentPlan.js +1 -0
- package/components/AgentStatus/AgentStatus.js +1 -0
- package/components/AiButton/AiButton.js +1 -0
- package/components/AlertDialog/AlertDialog.js +1 -0
- package/components/AnchorNav/AnchorNav.css +63 -0
- package/components/AnchorNav/AnchorNav.d.ts +33 -0
- package/components/AnchorNav/AnchorNav.js +82 -0
- package/components/AppLayout/AppLayout.js +1 -0
- package/components/AppSidebar/AppSidebar.js +1 -0
- package/components/Avatar/Avatar.js +1 -0
- package/components/Button/Button.js +1 -0
- package/components/Card/Card.js +1 -0
- package/components/Carousel/Carousel.js +1 -0
- package/components/ChatThread/ChatThread.js +1 -0
- package/components/Checkbox/Checkbox.js +1 -0
- package/components/CodeBlock/CodeBlock.js +1 -0
- package/components/CodeDiff/CodeDiff.css +175 -0
- package/components/CodeDiff/CodeDiff.d.ts +58 -0
- package/components/CodeDiff/CodeDiff.js +114 -0
- package/components/ColorPicker/ColorPicker.js +1 -0
- package/components/Combobox/Combobox.js +1 -0
- package/components/CommandPalette/CommandPalette.js +1 -0
- package/components/Composer/Composer.js +1 -0
- package/components/ContactCard/ContactCard.js +1 -0
- package/components/ContextMenu/ContextMenu.js +1 -0
- package/components/DataTable/DataTable.js +1 -0
- package/components/DateInput/DateInput.js +1 -0
- package/components/DatePicker/DatePicker.js +1 -0
- package/components/Dialog/Dialog.js +1 -0
- package/components/Drawer/Drawer.js +1 -0
- package/components/Dropdown/Dropdown.js +1 -0
- package/components/DropdownMenu/DropdownMenu.css +1 -1
- package/components/DropdownMenu/DropdownMenu.js +1 -0
- package/components/EventCalendar/EventCalendar.js +1 -0
- package/components/Field/Field.js +1 -0
- package/components/Field/FieldContext.js +1 -0
- package/components/FileInput/FileInput.js +1 -0
- package/components/Input/Input.js +1 -0
- package/components/InterruptCard/InterruptCard.css +11 -21
- package/components/InterruptCard/InterruptCard.d.ts +5 -6
- package/components/MessageCard/MessageCard.css +1 -2
- package/components/ModelPicker/ModelPicker.css +3 -1
- package/components/ModelPicker/ModelPicker.js +1 -0
- package/components/NavList/NavList.js +1 -0
- package/components/NotificationCenter/NotificationCenter.js +1 -0
- package/components/NumberInput/NumberInput.css +154 -0
- package/components/NumberInput/NumberInput.d.ts +44 -0
- package/components/NumberInput/NumberInput.js +147 -0
- package/components/Pagination/Pagination.js +1 -0
- package/components/PinInput/PinInput.css +92 -0
- package/components/PinInput/PinInput.d.ts +46 -0
- package/components/PinInput/PinInput.js +161 -0
- package/components/Popover/Popover.css +5 -6
- package/components/Popover/Popover.js +1 -0
- package/components/RadioButton/RadioButton.js +1 -0
- package/components/Reasoning/Reasoning.js +1 -0
- package/components/SegmentedControl/SegmentedControl.js +1 -0
- package/components/SelectionCard/SelectionCard.js +1 -0
- package/components/ShaderField/ShaderField.js +1 -0
- package/components/ShaderField/useShaderField.js +1 -0
- package/components/Slider/Slider.js +1 -0
- package/components/Sparkline/Sparkline.css +52 -0
- package/components/Sparkline/Sparkline.d.ts +49 -0
- package/components/Sparkline/Sparkline.js +86 -0
- package/components/Stepper/Stepper.css +207 -0
- package/components/Stepper/Stepper.d.ts +41 -0
- package/components/Stepper/Stepper.js +61 -0
- package/components/Tabs/Tabs.js +1 -0
- package/components/TagInput/TagInput.css +160 -0
- package/components/TagInput/TagInput.d.ts +43 -0
- package/components/TagInput/TagInput.js +137 -0
- package/components/Textarea/Textarea.js +1 -0
- package/components/TimePicker/TimePicker.css +230 -0
- package/components/TimePicker/TimePicker.d.ts +46 -0
- package/components/TimePicker/TimePicker.js +246 -0
- package/components/Toast/Toast.js +1 -0
- package/components/ToggleGroup/ToggleGroup.js +1 -0
- package/components/ToggleSwitch/ToggleSwitch.js +1 -0
- package/components/ToolCall/ToolCall.css +7 -11
- package/components/ToolCall/ToolCall.js +1 -0
- package/components/Tooltip/Tooltip.js +1 -0
- package/components/TreeView/TreeView.css +119 -0
- package/components/TreeView/TreeView.d.ts +45 -0
- package/components/TreeView/TreeView.js +232 -0
- package/components/registry.d.ts +22 -6
- package/components/registry.js +3 -1
- package/components/registry.json +206 -20
- package/components/registry.json.d.ts +206 -20
- package/components/registry.json.js +2 -2
- package/index.d.ts +11 -2
- package/index.js +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
18
18
|
| Part | Description |
|
|
19
19
|
|---|---|
|
|
20
20
|
| **Portfolio website** (`/website`) | Next.js app with case studies, work history, writing, and about pages, all built exclusively with the design system components below. **[Live site →](https://robertritacca.com/)** |
|
|
21
|
-
| **Design system** (`/src`) | <!-- component-count -->
|
|
21
|
+
| **Design system** (`/src`) | <!-- component-count -->101<!-- /component-count --> React components, a three-tier token architecture, dark mode, a WebGL2 ambient background that themes itself from your tokens, and a full documentation site. Built to production standards. **[Storybook →](https://design-system-iota-one.vercel.app/?path=/docs/robr0-ds--docs)** |
|
|
22
22
|
| **AI layer** (`ai` components in `/src`, chat in `/website`, `/evals`) | A site-wide chat that answers questions about the work: built from the library's own `ai` components, grounded in a corpus generated from the site's published content, and scored by a golden-set eval. |
|
|
23
23
|
|
|
24
24
|
---
|
|
@@ -28,7 +28,7 @@ The **live site** is the portfolio built on the design system. **Storybook** is
|
|
|
28
28
|
### Components
|
|
29
29
|
|
|
30
30
|
<!-- component-list:start -->
|
|
31
|
-
Accordion · Agent plan · Agent status · AI button · Alert · Alert dialog · App layout · App sidebar · Avatar · Badge · Breadcrumb · Button · Button group · Card · Carousel ·
|
|
31
|
+
Accordion · Agent plan · Agent status · AI button · Alert · Alert dialog · Anchor nav · App layout · App sidebar · Area chart · Avatar · Badge · Bar chart · Breadcrumb · Button · Button group · Card · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Input · Instructions · Interrupt card · Kbd · Line chart · Link list · Message actions · Message card · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Pie chart · Pin input · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Reasoning · Scatter chart · Section title · Segmented control · Selection card · Shader field · Skeleton · Slider · Source chip · Sparkline · Spinner · Stacked bar chart · Stat · Stepper · Swatch · Table · Tabs · Tag input · Textarea · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Tooltip · Tree view · Treemap
|
|
32
32
|
<!-- component-list:end -->
|
|
33
33
|
|
|
34
34
|
### Ambient background
|
|
@@ -71,7 +71,7 @@ import '@robr0/design-system/tokens/tokens.css';
|
|
|
71
71
|
import { Button, Card, Badge } from '@robr0/design-system';
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Deep imports work too (`@robr0/design-system/components/Button/Button`).
|
|
74
|
+
Deep imports work too (`@robr0/design-system/components/Button/Button`). The Recharts-backed charts live behind a separate entry so the optional `recharts` peer dependency is only needed if you use them; Sparkline and ContributionGraph are dependency-free and export from the main barrel:
|
|
75
75
|
|
|
76
76
|
```tsx
|
|
77
77
|
import { BarChart, LineChart } from '@robr0/design-system/charts';
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
align-items: center;
|
|
30
30
|
gap: var(--gap-sm);
|
|
31
31
|
width: 100%;
|
|
32
|
-
padding: var(--padding-
|
|
32
|
+
padding: var(--padding-sm) var(--padding-md);
|
|
33
33
|
background: none;
|
|
34
34
|
border: none;
|
|
35
35
|
text-align: left;
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
display: flex;
|
|
122
122
|
flex-direction: column;
|
|
123
123
|
margin: 0;
|
|
124
|
-
padding: var(--padding-sm) var(--padding-
|
|
124
|
+
padding: var(--padding-sm-md) var(--padding-md);
|
|
125
125
|
list-style: none;
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
ANCHOR NAV
|
|
3
|
+
"On this page" list of anchor links with a
|
|
4
|
+
position indicator riding the left rail.
|
|
5
|
+
============================================ */
|
|
6
|
+
.ds-anchor-nav {
|
|
7
|
+
font-family: var(--font-family-primary);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ds-anchor-nav__header {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: var(--gap-xs);
|
|
14
|
+
margin: 0 0 var(--gap-sm);
|
|
15
|
+
font-size: var(--font-paragraph-em-size);
|
|
16
|
+
font-weight: var(--font-paragraph-em-weight);
|
|
17
|
+
line-height: var(--font-paragraph-em-line-height);
|
|
18
|
+
color: var(--color-text-primary);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.ds-anchor-nav__icon {
|
|
22
|
+
--icon-size: var(--icon-size-sm);
|
|
23
|
+
color: var(--color-icon-primary);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.ds-anchor-nav__list {
|
|
27
|
+
list-style: none;
|
|
28
|
+
margin: 0;
|
|
29
|
+
padding: 0;
|
|
30
|
+
border-left: var(--border-xs) solid var(--color-divider);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ds-anchor-nav__link {
|
|
34
|
+
display: block;
|
|
35
|
+
padding: var(--padding-xs) 0 var(--padding-xs) var(--padding-md);
|
|
36
|
+
/* Sits over the list rail; turns into the indicator when active */
|
|
37
|
+
margin-left: calc(var(--border-xs) * -1);
|
|
38
|
+
border-left: var(--border-md) solid transparent;
|
|
39
|
+
font-size: var(--font-paragraph-size);
|
|
40
|
+
font-weight: var(--font-paragraph-weight);
|
|
41
|
+
line-height: var(--font-paragraph-line-height);
|
|
42
|
+
color: var(--color-text-tertiary);
|
|
43
|
+
text-decoration: none;
|
|
44
|
+
transition:
|
|
45
|
+
color var(--motion-duration-fast) var(--motion-ease-standard),
|
|
46
|
+
border-color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ds-anchor-nav__link:hover {
|
|
50
|
+
color: var(--color-text-primary);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ds-anchor-nav__link:focus-visible {
|
|
54
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
55
|
+
outline-offset: -2px;
|
|
56
|
+
border-radius: var(--radius-xxs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ds-anchor-nav__link--active {
|
|
60
|
+
color: var(--color-text-primary);
|
|
61
|
+
font-weight: var(--font-paragraph-em-weight);
|
|
62
|
+
border-left-color: var(--color-text-primary);
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export interface AnchorNavItem {
|
|
3
|
+
/** The id of the element the item jumps to (rendered as `href="#id"`) */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Link text */
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
type AnchorNavOwnProps = {
|
|
9
|
+
/** The on-page sections to list, in document order */
|
|
10
|
+
items: AnchorNavItem[];
|
|
11
|
+
/** Header text above the list; pass an empty string to render no header */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Material Symbols icon name beside the header; pass an empty string for none */
|
|
14
|
+
icon?: string;
|
|
15
|
+
/** Controlled active item id — set it to drive the highlight yourself and skip scroll tracking */
|
|
16
|
+
activeId?: string;
|
|
17
|
+
/** Fires when the tracked (or clicked) active item changes */
|
|
18
|
+
onActiveChange?: (id: string) => void;
|
|
19
|
+
/** Distance in px from the viewport top at which a section counts as current, e.g. a sticky header's height */
|
|
20
|
+
offset?: number;
|
|
21
|
+
};
|
|
22
|
+
export interface AnchorNavProps extends AnchorNavOwnProps, Omit<React.ComponentPropsWithoutRef<'nav'>, keyof AnchorNavOwnProps> {
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* An "On this page" list of anchor links that tracks the reader's position.
|
|
26
|
+
*
|
|
27
|
+
* Uncontrolled by default: a scroll listener marks the last section whose top
|
|
28
|
+
* has passed `offset` as current. Pass `activeId` to control the highlight
|
|
29
|
+
* instead (no listener is attached). The active link carries
|
|
30
|
+
* `aria-current="location"`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const AnchorNav: React.ForwardRefExoticComponent<AnchorNavProps & React.RefAttributes<HTMLElement>>;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import React, { useRef, useImperativeHandle, useState, useCallback, useEffect } from "react";
|
|
4
|
+
import "./AnchorNav.css";
|
|
5
|
+
const AnchorNav = React.forwardRef(
|
|
6
|
+
({
|
|
7
|
+
items,
|
|
8
|
+
title = "On this page",
|
|
9
|
+
icon = "toc",
|
|
10
|
+
activeId,
|
|
11
|
+
onActiveChange,
|
|
12
|
+
offset = 96,
|
|
13
|
+
className,
|
|
14
|
+
...rest
|
|
15
|
+
}, ref) => {
|
|
16
|
+
const navRef = useRef(null);
|
|
17
|
+
useImperativeHandle(ref, () => navRef.current);
|
|
18
|
+
const isControlled = activeId !== void 0;
|
|
19
|
+
const [trackedId, setTrackedId] = useState(void 0);
|
|
20
|
+
const currentId = isControlled ? activeId : trackedId;
|
|
21
|
+
const onActiveChangeRef = useRef(onActiveChange);
|
|
22
|
+
onActiveChangeRef.current = onActiveChange;
|
|
23
|
+
const setActive = useCallback(
|
|
24
|
+
(id) => {
|
|
25
|
+
setTrackedId((prev) => {
|
|
26
|
+
if (id !== void 0 && id !== prev) onActiveChangeRef.current?.(id);
|
|
27
|
+
return id;
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
[]
|
|
31
|
+
);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (isControlled || items.length === 0) return;
|
|
34
|
+
let frame = 0;
|
|
35
|
+
const measure = () => {
|
|
36
|
+
frame = 0;
|
|
37
|
+
let current = items[0]?.id;
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
const el = document.getElementById(item.id);
|
|
40
|
+
if (!el) continue;
|
|
41
|
+
if (el.getBoundingClientRect().top <= offset + 1) current = item.id;
|
|
42
|
+
}
|
|
43
|
+
setActive(current);
|
|
44
|
+
};
|
|
45
|
+
const schedule = () => {
|
|
46
|
+
if (frame === 0) frame = window.requestAnimationFrame(measure);
|
|
47
|
+
};
|
|
48
|
+
measure();
|
|
49
|
+
window.addEventListener("scroll", schedule, { passive: true });
|
|
50
|
+
window.addEventListener("resize", schedule);
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener("scroll", schedule);
|
|
53
|
+
window.removeEventListener("resize", schedule);
|
|
54
|
+
if (frame !== 0) window.cancelAnimationFrame(frame);
|
|
55
|
+
};
|
|
56
|
+
}, [isControlled, items, offset, setActive]);
|
|
57
|
+
const classes = ["ds-anchor-nav", className].filter(Boolean).join(" ");
|
|
58
|
+
return /* @__PURE__ */ jsxs("nav", { ref: navRef, "aria-label": title || "On this page", className: classes, ...rest, children: [
|
|
59
|
+
title && /* @__PURE__ */ jsxs("p", { className: "ds-anchor-nav__header", children: [
|
|
60
|
+
icon && /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded ds-anchor-nav__icon", "aria-hidden": "true", children: icon }),
|
|
61
|
+
title
|
|
62
|
+
] }),
|
|
63
|
+
/* @__PURE__ */ jsx("ul", { className: "ds-anchor-nav__list", children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
64
|
+
"a",
|
|
65
|
+
{
|
|
66
|
+
href: `#${item.id}`,
|
|
67
|
+
className: item.id === currentId ? "ds-anchor-nav__link ds-anchor-nav__link--active" : "ds-anchor-nav__link",
|
|
68
|
+
"aria-current": item.id === currentId ? "location" : void 0,
|
|
69
|
+
onClick: () => {
|
|
70
|
+
if (!isControlled) setActive(item.id);
|
|
71
|
+
else onActiveChangeRef.current?.(item.id);
|
|
72
|
+
},
|
|
73
|
+
children: item.label
|
|
74
|
+
}
|
|
75
|
+
) }, item.id)) })
|
|
76
|
+
] });
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
AnchorNav.displayName = "AnchorNav";
|
|
80
|
+
export {
|
|
81
|
+
AnchorNav
|
|
82
|
+
};
|
package/components/Card/Card.js
CHANGED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
CODE DIFF COMPONENT
|
|
3
|
+
Unified diff view: added, removed, and context
|
|
4
|
+
lines with old/new line number gutters.
|
|
5
|
+
Note: the system typeface is Nunito Sans;
|
|
6
|
+
code is the one sanctioned monospace context.
|
|
7
|
+
============================================ */
|
|
8
|
+
|
|
9
|
+
.ds-codediff {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
width: 100%;
|
|
13
|
+
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
14
|
+
border-radius: var(--radius-md);
|
|
15
|
+
background-color: var(--color-bg-container-primary);
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ============================================
|
|
20
|
+
HEADER — filename + additions/deletions stats
|
|
21
|
+
============================================ */
|
|
22
|
+
|
|
23
|
+
.ds-codediff__header {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
gap: var(--gap-sm);
|
|
28
|
+
padding: var(--padding-xs) var(--padding-md);
|
|
29
|
+
border-bottom: var(--border-xs) solid var(--color-bg-container-border);
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ds-codediff__filename {
|
|
34
|
+
font-family: var(--font-family-code);
|
|
35
|
+
font-size: var(--font-paragraph-sm-size);
|
|
36
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
37
|
+
color: var(--color-text-secondary);
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
text-overflow: ellipsis;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.ds-codediff__stats {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: var(--gap-xs);
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
font-family: var(--font-paragraph-sm-em-family);
|
|
49
|
+
font-size: var(--font-paragraph-sm-em-size);
|
|
50
|
+
font-weight: var(--font-paragraph-sm-em-weight);
|
|
51
|
+
line-height: var(--font-paragraph-sm-em-line-height);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ds-codediff__stat--add {
|
|
55
|
+
color: var(--color-status-positive-text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ds-codediff__stat--remove {
|
|
59
|
+
color: var(--color-status-error-text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ============================================
|
|
63
|
+
SCROLL CONTAINER
|
|
64
|
+
Long lines scroll horizontally inside the
|
|
65
|
+
component; the tab stop makes that scroll
|
|
66
|
+
keyboard-reachable.
|
|
67
|
+
============================================ */
|
|
68
|
+
|
|
69
|
+
.ds-codediff__scroll {
|
|
70
|
+
overflow-x: auto;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ds-codediff__scroll:focus-visible {
|
|
74
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
75
|
+
outline-offset: -2px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ============================================
|
|
79
|
+
LINES
|
|
80
|
+
============================================ */
|
|
81
|
+
|
|
82
|
+
/* width: max-content + min-width: 100% so row tints
|
|
83
|
+
span the full width of the longest line, not just
|
|
84
|
+
the visible scrollport */
|
|
85
|
+
.ds-codediff__lines {
|
|
86
|
+
list-style: none;
|
|
87
|
+
margin: 0;
|
|
88
|
+
padding: var(--padding-xs) 0;
|
|
89
|
+
width: max-content;
|
|
90
|
+
min-width: 100%;
|
|
91
|
+
font-family: var(--font-family-code);
|
|
92
|
+
font-size: var(--font-paragraph-sm-size);
|
|
93
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
94
|
+
color: var(--color-text-primary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ds-codediff__line {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: baseline;
|
|
100
|
+
/* keeps blank diff lines from collapsing to zero height */
|
|
101
|
+
min-height: var(--font-paragraph-sm-line-height);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ============================================
|
|
105
|
+
GUTTERS & MARKER
|
|
106
|
+
Fixed-width columns in the monospace metric so
|
|
107
|
+
rows align; excluded from text selection so a
|
|
108
|
+
copied diff carries only the code.
|
|
109
|
+
============================================ */
|
|
110
|
+
|
|
111
|
+
.ds-codediff__gutter {
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
min-width: 4ch;
|
|
114
|
+
padding: 0 var(--padding-xxs);
|
|
115
|
+
text-align: right;
|
|
116
|
+
color: var(--color-text-tertiary);
|
|
117
|
+
user-select: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ds-codediff__marker {
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
width: 2ch;
|
|
123
|
+
text-align: center;
|
|
124
|
+
user-select: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ds-codediff__content {
|
|
128
|
+
white-space: pre;
|
|
129
|
+
padding-right: var(--padding-md);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ============================================
|
|
133
|
+
LINE VARIANTS
|
|
134
|
+
Added rows tint with the positive status
|
|
135
|
+
background, removed with the error one.
|
|
136
|
+
============================================ */
|
|
137
|
+
|
|
138
|
+
.ds-codediff__line--add {
|
|
139
|
+
background-color: var(--color-status-positive-bg);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.ds-codediff__line--add .ds-codediff__marker {
|
|
143
|
+
color: var(--color-status-positive-text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.ds-codediff__line--remove {
|
|
147
|
+
background-color: var(--color-status-error-bg);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ds-codediff__line--remove .ds-codediff__marker {
|
|
151
|
+
color: var(--color-status-error-text);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.ds-codediff__line--hunk {
|
|
155
|
+
background-color: var(--color-bg-container-secondary);
|
|
156
|
+
color: var(--color-text-tertiary);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ============================================
|
|
160
|
+
SCREEN-READER-ONLY TEXT
|
|
161
|
+
Announces the change type; the marker column
|
|
162
|
+
itself is decorative (aria-hidden).
|
|
163
|
+
============================================ */
|
|
164
|
+
|
|
165
|
+
.ds-codediff__sr-only {
|
|
166
|
+
position: absolute;
|
|
167
|
+
width: 1px;
|
|
168
|
+
height: 1px;
|
|
169
|
+
padding: 0;
|
|
170
|
+
margin: -1px;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
clip: rect(0, 0, 0, 0);
|
|
173
|
+
white-space: nowrap;
|
|
174
|
+
border: 0;
|
|
175
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** The kind of line a parsed diff row represents. */
|
|
3
|
+
export type CodeDiffLineType = 'add' | 'remove' | 'context' | 'hunk';
|
|
4
|
+
export interface CodeDiffLine {
|
|
5
|
+
/** Whether the line is an addition, a removal, unchanged context, or a hunk header */
|
|
6
|
+
type: CodeDiffLineType;
|
|
7
|
+
/** The line's text with its diff marker stripped (hunk headers keep the full `@@` line) */
|
|
8
|
+
content: string;
|
|
9
|
+
/** Line number in the old file, or null for additions and hunk headers */
|
|
10
|
+
oldLine: number | null;
|
|
11
|
+
/** Line number in the new file, or null for removals and hunk headers */
|
|
12
|
+
newLine: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface ParsedCodeDiff {
|
|
15
|
+
/** The diff's rows in order, with per-side line numbers computed */
|
|
16
|
+
lines: CodeDiffLine[];
|
|
17
|
+
/** Count of added lines */
|
|
18
|
+
additions: number;
|
|
19
|
+
/** Count of removed lines */
|
|
20
|
+
deletions: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse a unified-format diff body into typed lines with old/new line
|
|
24
|
+
* numbers. Lines starting with `+` are additions, `-` removals, `@@ … @@`
|
|
25
|
+
* hunk headers, and everything else context. File header lines (`+++`,
|
|
26
|
+
* `---`) and no-newline markers (``) carry no
|
|
27
|
+
* content and are skipped. A diff with no hunk headers is treated as one
|
|
28
|
+
* hunk starting at line 1.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseUnifiedDiff(diff: string): ParsedCodeDiff;
|
|
31
|
+
/** Props owned by CodeDiff itself — everything else falls through to the root div. */
|
|
32
|
+
type CodeDiffOwnProps = {
|
|
33
|
+
/**
|
|
34
|
+
* A unified-format diff body. Lines starting with `+` render as additions,
|
|
35
|
+
* `-` as removals, `@@ … @@` as hunk headers, everything else as context;
|
|
36
|
+
* `+++`/`---` file header lines are ignored. Without hunk headers the
|
|
37
|
+
* whole string is treated as one hunk starting at line 1.
|
|
38
|
+
*/
|
|
39
|
+
diff: string;
|
|
40
|
+
/** Filename shown in a header bar above the diff, with an additions/deletions summary */
|
|
41
|
+
filename?: string;
|
|
42
|
+
/** Show the old and new line number gutters */
|
|
43
|
+
showLineNumbers?: boolean;
|
|
44
|
+
/** Additional CSS classes */
|
|
45
|
+
className?: string;
|
|
46
|
+
};
|
|
47
|
+
export interface CodeDiffProps extends CodeDiffOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof CodeDiffOwnProps> {
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* CodeDiff component — unified diff view for code changes. Added lines tint
|
|
51
|
+
* with the positive status background, removed lines with the error one, and
|
|
52
|
+
* two gutters track old and new line numbers. Purely presentational: parsing
|
|
53
|
+
* happens in the exported `parseUnifiedDiff`, long lines scroll horizontally
|
|
54
|
+
* inside the block, and the change type is announced through visually hidden
|
|
55
|
+
* text while the marker column stays decorative.
|
|
56
|
+
*/
|
|
57
|
+
export declare const CodeDiff: React.ForwardRefExoticComponent<CodeDiffProps & React.RefAttributes<HTMLDivElement>>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import "./CodeDiff.css";
|
|
4
|
+
const HUNK_HEADER = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/;
|
|
5
|
+
function parseUnifiedDiff(diff) {
|
|
6
|
+
const rawLines = diff.replace(/\n$/, "").split("\n");
|
|
7
|
+
const lines = [];
|
|
8
|
+
let additions = 0;
|
|
9
|
+
let deletions = 0;
|
|
10
|
+
let oldLine = 1;
|
|
11
|
+
let newLine = 1;
|
|
12
|
+
for (const raw of rawLines) {
|
|
13
|
+
if (raw.startsWith("+++") || raw.startsWith("---") || raw.startsWith("\\")) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const hunk = raw.match(HUNK_HEADER);
|
|
17
|
+
if (hunk) {
|
|
18
|
+
oldLine = parseInt(hunk[1], 10);
|
|
19
|
+
newLine = parseInt(hunk[2], 10);
|
|
20
|
+
lines.push({ type: "hunk", content: raw, oldLine: null, newLine: null });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (raw.startsWith("+")) {
|
|
24
|
+
additions += 1;
|
|
25
|
+
lines.push({ type: "add", content: raw.slice(1), oldLine: null, newLine });
|
|
26
|
+
newLine += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (raw.startsWith("-")) {
|
|
30
|
+
deletions += 1;
|
|
31
|
+
lines.push({ type: "remove", content: raw.slice(1), oldLine, newLine: null });
|
|
32
|
+
oldLine += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
lines.push({
|
|
36
|
+
type: "context",
|
|
37
|
+
content: raw.startsWith(" ") ? raw.slice(1) : raw,
|
|
38
|
+
oldLine,
|
|
39
|
+
newLine
|
|
40
|
+
});
|
|
41
|
+
oldLine += 1;
|
|
42
|
+
newLine += 1;
|
|
43
|
+
}
|
|
44
|
+
return { lines, additions, deletions };
|
|
45
|
+
}
|
|
46
|
+
const CHANGE_LABELS = {
|
|
47
|
+
add: "Added line:",
|
|
48
|
+
remove: "Removed line:"
|
|
49
|
+
};
|
|
50
|
+
const CodeDiff = React.forwardRef(
|
|
51
|
+
({ diff, filename, showLineNumbers = true, className = "", ...rest }, ref) => {
|
|
52
|
+
const baseClass = "ds-codediff";
|
|
53
|
+
const { lines, additions, deletions } = parseUnifiedDiff(diff);
|
|
54
|
+
const classes = [baseClass, className].filter(Boolean).join(" ");
|
|
55
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
56
|
+
filename && /* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
|
|
57
|
+
/* @__PURE__ */ jsx("code", { className: `${baseClass}__filename`, children: filename }),
|
|
58
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__stats`, children: [
|
|
59
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__stat ${baseClass}__stat--add`, children: [
|
|
60
|
+
"+",
|
|
61
|
+
additions,
|
|
62
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: " added" })
|
|
63
|
+
] }),
|
|
64
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__stat ${baseClass}__stat--remove`, children: [
|
|
65
|
+
"-",
|
|
66
|
+
deletions,
|
|
67
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__sr-only`, children: " removed" })
|
|
68
|
+
] })
|
|
69
|
+
] })
|
|
70
|
+
] }),
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
"div",
|
|
73
|
+
{
|
|
74
|
+
className: `${baseClass}__scroll`,
|
|
75
|
+
tabIndex: 0,
|
|
76
|
+
role: "region",
|
|
77
|
+
"aria-label": filename ? `Code diff: ${filename}` : "Code diff",
|
|
78
|
+
children: /* @__PURE__ */ jsx("ol", { className: `${baseClass}__lines`, children: lines.map((line, index) => {
|
|
79
|
+
const changeLabel = CHANGE_LABELS[line.type];
|
|
80
|
+
return /* @__PURE__ */ jsxs(
|
|
81
|
+
"li",
|
|
82
|
+
{
|
|
83
|
+
className: [
|
|
84
|
+
`${baseClass}__line`,
|
|
85
|
+
line.type === "add" ? `${baseClass}__line--add` : "",
|
|
86
|
+
line.type === "remove" ? `${baseClass}__line--remove` : "",
|
|
87
|
+
line.type === "hunk" ? `${baseClass}__line--hunk` : ""
|
|
88
|
+
].filter(Boolean).join(" "),
|
|
89
|
+
children: [
|
|
90
|
+
showLineNumbers && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
91
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__gutter`, "aria-hidden": "true", children: line.oldLine ?? "" }),
|
|
92
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__gutter`, "aria-hidden": "true", children: line.newLine ?? "" })
|
|
93
|
+
] }),
|
|
94
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__marker`, "aria-hidden": "true", children: line.type === "add" ? "+" : line.type === "remove" ? "-" : "" }),
|
|
95
|
+
changeLabel && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__sr-only`, children: [
|
|
96
|
+
changeLabel,
|
|
97
|
+
" "
|
|
98
|
+
] }),
|
|
99
|
+
/* @__PURE__ */ jsx("span", { className: `${baseClass}__content`, children: line.content })
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
index
|
|
103
|
+
);
|
|
104
|
+
}) })
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
] });
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
CodeDiff.displayName = "CodeDiff";
|
|
111
|
+
export {
|
|
112
|
+
CodeDiff,
|
|
113
|
+
parseUnifiedDiff
|
|
114
|
+
};
|