@sixthshift/design-system 0.1.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/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/styles.css +2 -0
- package/dist/theme.generated.css +438 -0
- package/package.json +238 -0
- package/src/components/Avatar/Avatar.tsx +19 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.tsx +59 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BarChart/BarChart.tsx +51 -0
- package/src/components/BarChart/index.ts +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
- package/src/components/Breadcrumb/index.ts +5 -0
- package/src/components/Button/Button.tsx +157 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Calendar/Calendar.tsx +433 -0
- package/src/components/Calendar/calendar.hooks.ts +329 -0
- package/src/components/Calendar/calendar.types.ts +117 -0
- package/src/components/Calendar/index.ts +28 -0
- package/src/components/Card/Card.tsx +47 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.tsx +144 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
- package/src/components/CheckboxGroup/index.ts +5 -0
- package/src/components/Code/Editor/Editor.tsx +161 -0
- package/src/components/Code/Editor/index.ts +2 -0
- package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
- package/src/components/Code/Editor/themes.ts +112 -0
- package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
- package/src/components/Code/ValidationStatus/index.ts +1 -0
- package/src/components/Code/Workspace/StatusBar.tsx +47 -0
- package/src/components/Code/Workspace/Toolbar.tsx +47 -0
- package/src/components/Code/Workspace/Workspace.tsx +109 -0
- package/src/components/Code/Workspace/index.ts +3 -0
- package/src/components/ColorDot/ColorDot.tsx +50 -0
- package/src/components/ColorDot/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.tsx +236 -0
- package/src/components/DatePicker/datepicker.hooks.ts +381 -0
- package/src/components/DatePicker/datepicker.types.ts +140 -0
- package/src/components/DatePicker/index.ts +12 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
- package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
- package/src/components/DateRangePicker/index.ts +2 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
- package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
- package/src/components/DateTimePicker/index.ts +2 -0
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
- package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
- package/src/components/DateTimeRangePicker/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +29 -0
- package/src/components/EmptyState/index.ts +1 -0
- package/src/components/Field/Field.tsx +60 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +55 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
- package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
- package/src/components/HeatMap/heat-map.utils.ts +15 -0
- package/src/components/HeatMap/index.ts +3 -0
- package/src/components/HoverCard/HoverCard.tsx +83 -0
- package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
- package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
- package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
- package/src/components/HoverCard/index.ts +3 -0
- package/src/components/Input/Input.tsx +48 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/LineChart/LineChart.tsx +251 -0
- package/src/components/LineChart/index.ts +1 -0
- package/src/components/Markdown/Markdown.tsx +40 -0
- package/src/components/Markdown/index.ts +1 -0
- package/src/components/Message/Message.tsx +59 -0
- package/src/components/Message/components/MessageBody.tsx +9 -0
- package/src/components/Message/components/MessageDescription.tsx +9 -0
- package/src/components/Message/components/MessageIcon.tsx +13 -0
- package/src/components/Message/components/MessageTitle.tsx +9 -0
- package/src/components/Message/index.ts +10 -0
- package/src/components/MetricList/MetricList.tsx +13 -0
- package/src/components/MetricList/MetricRow.tsx +30 -0
- package/src/components/MetricList/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +146 -0
- package/src/components/Modal/components/ModalBody.tsx +9 -0
- package/src/components/Modal/components/ModalContext.tsx +27 -0
- package/src/components/Modal/components/ModalFooter.tsx +9 -0
- package/src/components/Modal/components/ModalHeader.tsx +29 -0
- package/src/components/Modal/components/index.ts +9 -0
- package/src/components/Modal/index.ts +13 -0
- package/src/components/NavBottom/NavBottom.tsx +53 -0
- package/src/components/NavBottom/index.ts +3 -0
- package/src/components/NavSide/NavItem.tsx +39 -0
- package/src/components/NavSide/NavSide.tsx +40 -0
- package/src/components/NavSide/index.ts +2 -0
- package/src/components/NavSide/types.ts +36 -0
- package/src/components/Pagination/Pagination.tsx +86 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Popover/Popover.tsx +68 -0
- package/src/components/Popover/components/PopoverBody.tsx +32 -0
- package/src/components/Popover/components/PopoverClose.tsx +26 -0
- package/src/components/Popover/components/PopoverContext.tsx +24 -0
- package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
- package/src/components/Popover/index.ts +10 -0
- package/src/components/ProgressBar/ProgressBar.tsx +42 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.tsx +89 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
- package/src/components/RadioButtonGroup/index.ts +5 -0
- package/src/components/SearchInput/SearchInput.tsx +45 -0
- package/src/components/SearchInput/index.ts +2 -0
- package/src/components/Select/Select.tsx +289 -0
- package/src/components/Select/SelectDropdown.tsx +90 -0
- package/src/components/Select/SelectTrigger.tsx +130 -0
- package/src/components/Select/index.ts +3 -0
- package/src/components/Select/useSelectKeyboard.ts +111 -0
- package/src/components/Separator/Separator.tsx +20 -0
- package/src/components/Separator/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +136 -0
- package/src/components/Sheet/components/SheetBody.tsx +9 -0
- package/src/components/Sheet/components/SheetContext.tsx +23 -0
- package/src/components/Sheet/components/SheetFooter.tsx +9 -0
- package/src/components/Sheet/components/SheetHeader.tsx +29 -0
- package/src/components/Sheet/components/index.ts +9 -0
- package/src/components/Sheet/index.ts +13 -0
- package/src/components/Skeleton/Skeleton.tsx +7 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Sparkline/Sparkline.tsx +93 -0
- package/src/components/Sparkline/index.ts +1 -0
- package/src/components/Spinner/Spinner.tsx +41 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/StatsCard/StatsCard.tsx +40 -0
- package/src/components/StatsCard/index.ts +1 -0
- package/src/components/Switch/Switch.tsx +128 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +49 -0
- package/src/components/Tabs/components/TabsContext.tsx +35 -0
- package/src/components/Tabs/components/TabsList.tsx +98 -0
- package/src/components/Tabs/components/TabsPanels.tsx +26 -0
- package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/TagChip/TagChip.tsx +46 -0
- package/src/components/TagChip/index.ts +1 -0
- package/src/components/TagInput/TagInput.tsx +60 -0
- package/src/components/TagInput/index.ts +1 -0
- package/src/components/Text/Text.tsx +35 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/TextInline/TextInline.tsx +35 -0
- package/src/components/TextInline/index.ts +1 -0
- package/src/components/Textarea/Textarea.tsx +20 -0
- package/src/components/Textarea/index.ts +1 -0
- package/src/components/TimePicker/PeriodSelector.tsx +28 -0
- package/src/components/TimePicker/TimeColumn.tsx +62 -0
- package/src/components/TimePicker/TimePicker.tsx +323 -0
- package/src/components/TimePicker/index.ts +7 -0
- package/src/components/TimePicker/timepicker.hooks.ts +173 -0
- package/src/components/TimePicker/timepicker.types.ts +101 -0
- package/src/components/Toast/Toast.tsx +111 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Toggle/Toggle.tsx +72 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
- package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
- package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
- package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
- package/src/components/Tooltip/Tooltip.tsx +66 -0
- package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
- package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
- package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
- package/src/components/Tooltip/index.ts +9 -0
- package/src/date-time/arithmetic.ts +101 -0
- package/src/date-time/boundaries.ts +91 -0
- package/src/date-time/compare.ts +85 -0
- package/src/date-time/duration.ts +63 -0
- package/src/date-time/elapsed.ts +18 -0
- package/src/date-time/format.ts +722 -0
- package/src/date-time/index.ts +279 -0
- package/src/date-time/now.ts +70 -0
- package/src/date-time/parse.ts +170 -0
- package/src/date-time/serialize.ts +90 -0
- package/src/date-time/timezone.ts +47 -0
- package/src/date-time/validate.ts +278 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCollapsible.ts +16 -0
- package/src/hooks/useControllableState.ts +54 -0
- package/src/hooks/useDebouncedCallback.ts +21 -0
- package/src/hooks/useDebouncedState.ts +20 -0
- package/src/hooks/useDeviceFormFactor.ts +49 -0
- package/src/hooks/useLocalStorage.ts +67 -0
- package/src/hooks/useMergedFloatingRef.ts +29 -0
- package/src/hooks/usePresence.ts +127 -0
- package/src/hooks/useStack.ts +99 -0
- package/src/hooks/useTheme.ts +105 -0
- package/src/internal/Slot.tsx +79 -0
- package/src/internal/index.ts +2 -0
- package/src/internal/rovingFocus.ts +39 -0
- package/src/internal/types.ts +10 -0
- package/src/lib/EmptyBoundary.tsx +12 -0
- package/src/lib/ErrorBoundary.tsx +48 -0
- package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
- package/src/lib/contexts/ComponentsContext/index.ts +1 -0
- package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
- package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
- package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
- package/src/lib/contexts/OverlayContext/index.ts +6 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/utils.ts +10 -0
- package/src/lib/withEmpty.tsx +14 -0
- package/src/lib/withErrorBoundary.tsx +16 -0
- package/src/lib/withSuspense.tsx +16 -0
- package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
- package/src/styles/base.css +41 -0
- package/src/theme/index.ts +37 -0
- package/src/theme/palette.json +100 -0
- package/src/theme/palette.schema.json +40 -0
- package/src/theme/schema.ts +122 -0
- package/src/theme/theme.json +349 -0
- package/src/theme/theme.schema.json +876 -0
- package/src/theme/typography.json +7 -0
- package/src/theme/typography.schema.json +36 -0
- package/src/typography/Body.tsx +8 -0
- package/src/typography/Caption.tsx +8 -0
- package/src/typography/Code.tsx +8 -0
- package/src/typography/Display.tsx +8 -0
- package/src/typography/Emphasis.tsx +8 -0
- package/src/typography/Heading.tsx +23 -0
- package/src/typography/Label.tsx +10 -0
- package/src/typography/Lead.tsx +8 -0
- package/src/typography/Mono.tsx +8 -0
- package/src/typography/Muted.tsx +8 -0
- package/src/typography/SectionTitle.tsx +8 -0
- package/src/typography/Subtitle.tsx +8 -0
- package/src/typography/Timestamp.tsx +8 -0
- package/src/typography/index.ts +10 -0
- package/tailwind.config.ts +261 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Checkbox } from "@sixthshift/design-system/checkbox";
|
|
2
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
3
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import type { WritableRefObject } from "../../internal/types";
|
|
6
|
+
|
|
7
|
+
export type CheckboxGroupOption = {
|
|
8
|
+
/** Unique value for the option */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Display label */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Whether this option is disabled */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CheckboxGroupProps = Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "onBlur"> & {
|
|
17
|
+
/** Controlled selected values */
|
|
18
|
+
value?: string[];
|
|
19
|
+
/** Default selected values for uncontrolled mode */
|
|
20
|
+
defaultValue?: string[];
|
|
21
|
+
/** Available options */
|
|
22
|
+
options: readonly CheckboxGroupOption[];
|
|
23
|
+
/** Called when selection changes */
|
|
24
|
+
onValueChange?: (value: string[]) => void;
|
|
25
|
+
/** Layout orientation */
|
|
26
|
+
orientation?: "vertical" | "horizontal";
|
|
27
|
+
/** Visual variant */
|
|
28
|
+
variant?: "default" | "button";
|
|
29
|
+
/** Button appearance (only applies when variant="button") */
|
|
30
|
+
appearance?: "segmented" | "separate";
|
|
31
|
+
/** Input name for form submission */
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Disable all options */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Called when focus leaves the group entirely */
|
|
36
|
+
onBlur?: () => void;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
|
|
40
|
+
(
|
|
41
|
+
{
|
|
42
|
+
value: controlledValue,
|
|
43
|
+
defaultValue = [],
|
|
44
|
+
options,
|
|
45
|
+
onValueChange,
|
|
46
|
+
orientation = "vertical",
|
|
47
|
+
variant = "default",
|
|
48
|
+
appearance = "segmented",
|
|
49
|
+
name,
|
|
50
|
+
disabled,
|
|
51
|
+
onBlur,
|
|
52
|
+
className,
|
|
53
|
+
...props
|
|
54
|
+
},
|
|
55
|
+
ref
|
|
56
|
+
) => {
|
|
57
|
+
const [value, setValue] = useControllableState({
|
|
58
|
+
value: controlledValue,
|
|
59
|
+
defaultValue,
|
|
60
|
+
onChange: onValueChange,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const groupRef = React.useRef<HTMLDivElement | null>(null);
|
|
64
|
+
const mergedRef = React.useCallback(
|
|
65
|
+
(node: HTMLDivElement | null) => {
|
|
66
|
+
groupRef.current = node;
|
|
67
|
+
if (typeof ref === "function") ref(node);
|
|
68
|
+
else if (ref) (ref as WritableRefObject<HTMLDivElement | null>).current = node;
|
|
69
|
+
},
|
|
70
|
+
[ref]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const handleFocusOut = React.useCallback(
|
|
74
|
+
(e: React.FocusEvent) => {
|
|
75
|
+
if (!groupRef.current?.contains(e.relatedTarget as Node)) {
|
|
76
|
+
onBlur?.();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
[onBlur]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const handleOptionChange = (optionValue: string, checked: boolean) => {
|
|
83
|
+
if (checked) {
|
|
84
|
+
setValue([...value, optionValue]);
|
|
85
|
+
} else {
|
|
86
|
+
setValue(value.filter((v) => v !== optionValue));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (variant === "button") {
|
|
91
|
+
const isVertical = orientation === "vertical";
|
|
92
|
+
const isSegmented = appearance === "segmented";
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
// biome-ignore lint/a11y/useSemanticElements: role="group" is correct for grouping button-style checkboxes
|
|
96
|
+
<div
|
|
97
|
+
ref={mergedRef}
|
|
98
|
+
role="group"
|
|
99
|
+
onBlurCapture={handleFocusOut}
|
|
100
|
+
className={cn("inline-flex", isVertical ? "flex-col" : "flex-row", !isSegmented && (isVertical ? "gap-2" : "gap-2"), className)}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{options.map((option, index) => {
|
|
104
|
+
const isChecked = value.includes(option.value);
|
|
105
|
+
const isDisabled = disabled || option.disabled;
|
|
106
|
+
const isFirst = index === 0;
|
|
107
|
+
const isLast = index === options.length - 1;
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
// biome-ignore lint/a11y/useSemanticElements: Custom checkbox group with button role="checkbox" is WAI-ARIA best practice
|
|
111
|
+
<button
|
|
112
|
+
key={option.value}
|
|
113
|
+
type="button"
|
|
114
|
+
role="checkbox"
|
|
115
|
+
aria-checked={isChecked}
|
|
116
|
+
disabled={isDisabled}
|
|
117
|
+
onClick={() => handleOptionChange(option.value, !isChecked)}
|
|
118
|
+
className={cn(
|
|
119
|
+
"inline-flex items-center justify-center px-4 py-2 font-medium text-sm transition-colors",
|
|
120
|
+
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2",
|
|
121
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
122
|
+
isSegmented
|
|
123
|
+
? cn(
|
|
124
|
+
"border",
|
|
125
|
+
isVertical
|
|
126
|
+
? cn(isFirst && "rounded-t-md", isLast && "rounded-b-md", !isFirst && "-mt-px")
|
|
127
|
+
: cn(isFirst && "rounded-l-md", isLast && "rounded-r-md", !isFirst && "-ml-px")
|
|
128
|
+
)
|
|
129
|
+
: "rounded-md border",
|
|
130
|
+
isChecked ? "border-border-brand bg-bg-brand text-fg-on-brand" : "border-border-normal bg-bg-normal text-fg-normal hover:bg-bg-subtle"
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
{option.label}
|
|
134
|
+
</button>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
{name && value.map((v) => <input key={v} type="hidden" name={name} value={v} />)}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
// biome-ignore lint/a11y/useSemanticElements: role="group" is correct for checkbox group container
|
|
144
|
+
<div
|
|
145
|
+
ref={mergedRef}
|
|
146
|
+
role="group"
|
|
147
|
+
onBlurCapture={handleFocusOut}
|
|
148
|
+
className={cn("flex", orientation === "vertical" ? "flex-col gap-3" : "flex-row gap-4", className)}
|
|
149
|
+
{...props}
|
|
150
|
+
>
|
|
151
|
+
{options.map((option) => {
|
|
152
|
+
const isChecked = value.includes(option.value);
|
|
153
|
+
const isDisabled = disabled || option.disabled;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<Checkbox
|
|
157
|
+
key={option.value}
|
|
158
|
+
{...(name ? { name } : {})}
|
|
159
|
+
value={option.value}
|
|
160
|
+
checked={isChecked}
|
|
161
|
+
onCheckedChange={(checked) => handleOptionChange(option.value, checked as boolean)}
|
|
162
|
+
disabled={isDisabled}
|
|
163
|
+
label={option.label}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
CheckboxGroup.displayName = "CheckboxGroup";
|
|
172
|
+
|
|
173
|
+
export { CheckboxGroup };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import MonacoEditor from "@monaco-editor/react";
|
|
2
|
+
import type { editor } from "monaco-editor";
|
|
3
|
+
import type { HTMLAttributes } from "react";
|
|
4
|
+
import { useCallback, useRef } from "react";
|
|
5
|
+
import { useTheme } from "../../../hooks/useTheme";
|
|
6
|
+
import type { ValidationError } from "../ValidationStatus";
|
|
7
|
+
import { createSemanticTokensProvider, workerExtensionUrl } from "./semanticTokenProvider";
|
|
8
|
+
import { defineThemes, PA_THEME_DARK, PA_THEME_LIGHT } from "./themes";
|
|
9
|
+
|
|
10
|
+
let semanticTokensRegistered = false;
|
|
11
|
+
function registerSemanticTokenProviders(monaco: typeof import("monaco-editor")) {
|
|
12
|
+
if (semanticTokensRegistered) return;
|
|
13
|
+
semanticTokensRegistered = true;
|
|
14
|
+
|
|
15
|
+
// Extend the TS worker to expose getEncodedSemanticClassifications
|
|
16
|
+
// @ts-expect-error - monaco.languages.typescript is marked deprecated but still functional
|
|
17
|
+
monaco.languages.typescript.typescriptDefaults.setWorkerOptions({
|
|
18
|
+
customWorkerPath: workerExtensionUrl,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// @ts-expect-error - getTypeScriptWorker is marked deprecated but still functional
|
|
22
|
+
const getWorker = () => monaco.languages.typescript.getTypeScriptWorker();
|
|
23
|
+
monaco.languages.registerDocumentSemanticTokensProvider("typescript", createSemanticTokensProvider(getWorker));
|
|
24
|
+
monaco.languages.registerDocumentSemanticTokensProvider("javascript", createSemanticTokensProvider(getWorker));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type EditorProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange"> & {
|
|
28
|
+
value: string;
|
|
29
|
+
onChange: (value: string) => void;
|
|
30
|
+
onValidate?: ((errors: ValidationError[]) => void) | undefined;
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Custom type definitions to add to Monaco for IntelliSense
|
|
34
|
+
* Example: "declare const myApi: { method(): void };"
|
|
35
|
+
*/
|
|
36
|
+
typeDefinitions?: string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Custom TypeScript compiler options to merge with defaults
|
|
39
|
+
*/
|
|
40
|
+
compilerOptions?: Record<string, unknown> | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generic Monaco-based code editor with TypeScript support
|
|
45
|
+
*
|
|
46
|
+
* Features:
|
|
47
|
+
* - Syntax highlighting and IntelliSense
|
|
48
|
+
* - Custom type definitions via props
|
|
49
|
+
* - Validation with error reporting
|
|
50
|
+
* - Configurable compiler options
|
|
51
|
+
*/
|
|
52
|
+
export const Editor = ({ value, onChange, onValidate, readOnly = false, className, typeDefinitions, compilerOptions = {}, ...props }: EditorProps) => {
|
|
53
|
+
const { resolvedTheme } = useTheme();
|
|
54
|
+
const monacoTheme = resolvedTheme === "dark" ? PA_THEME_DARK : PA_THEME_LIGHT;
|
|
55
|
+
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
56
|
+
|
|
57
|
+
const handleEditorDidMount = useCallback(
|
|
58
|
+
(editor: editor.IStandaloneCodeEditor, monaco: typeof import("monaco-editor")) => {
|
|
59
|
+
editorRef.current = editor;
|
|
60
|
+
|
|
61
|
+
// Configure TypeScript compiler options
|
|
62
|
+
// @ts-expect-error - monaco.languages.typescript is marked deprecated but still functional
|
|
63
|
+
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
|
64
|
+
// @ts-expect-error - ScriptTarget is deprecated but still functional
|
|
65
|
+
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
|
66
|
+
// @ts-expect-error - ModuleResolutionKind is deprecated but still functional
|
|
67
|
+
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
|
68
|
+
// @ts-expect-error - ModuleKind is deprecated but still functional
|
|
69
|
+
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
|
70
|
+
// @ts-expect-error - JsxEmit is deprecated but still functional
|
|
71
|
+
jsx: monaco.languages.typescript.JsxEmit.React,
|
|
72
|
+
allowNonTsExtensions: true,
|
|
73
|
+
noEmit: true,
|
|
74
|
+
esModuleInterop: true,
|
|
75
|
+
allowJs: true,
|
|
76
|
+
typeRoots: ["node_modules/@types"],
|
|
77
|
+
// Merge custom compiler options
|
|
78
|
+
...compilerOptions,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Add custom type definitions if provided
|
|
82
|
+
if (typeDefinitions) {
|
|
83
|
+
// @ts-expect-error - typescriptDefaults is deprecated but still functional
|
|
84
|
+
monaco.languages.typescript.typescriptDefaults.addExtraLib(typeDefinitions, "ts:custom.d.ts");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Set up validation
|
|
88
|
+
const model = editor.getModel();
|
|
89
|
+
if (model && onValidate) {
|
|
90
|
+
monaco.editor.onDidChangeMarkers(() => {
|
|
91
|
+
const markers = monaco.editor.getModelMarkers({ resource: model.uri });
|
|
92
|
+
const errors: ValidationError[] = markers.map((marker: editor.IMarker) => ({
|
|
93
|
+
line: marker.startLineNumber,
|
|
94
|
+
column: marker.startColumn,
|
|
95
|
+
message: marker.message,
|
|
96
|
+
severity:
|
|
97
|
+
marker.severity === monaco.MarkerSeverity.Error
|
|
98
|
+
? ("error" as const)
|
|
99
|
+
: marker.severity === monaco.MarkerSeverity.Warning
|
|
100
|
+
? ("warning" as const)
|
|
101
|
+
: ("info" as const),
|
|
102
|
+
}));
|
|
103
|
+
onValidate(errors);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Focus editor
|
|
108
|
+
editor.focus();
|
|
109
|
+
},
|
|
110
|
+
[onValidate, typeDefinitions, compilerOptions]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const handleBeforeMount = useCallback((monaco: typeof import("monaco-editor")) => {
|
|
114
|
+
defineThemes(monaco);
|
|
115
|
+
registerSemanticTokenProviders(monaco);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const handleChange = useCallback(
|
|
119
|
+
(value: string | undefined) => {
|
|
120
|
+
onChange(value || "");
|
|
121
|
+
},
|
|
122
|
+
[onChange]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div {...props} className={className}>
|
|
127
|
+
<MonacoEditor
|
|
128
|
+
height="100%"
|
|
129
|
+
defaultLanguage="typescript"
|
|
130
|
+
value={value}
|
|
131
|
+
onChange={handleChange}
|
|
132
|
+
beforeMount={handleBeforeMount}
|
|
133
|
+
onMount={handleEditorDidMount}
|
|
134
|
+
theme={monacoTheme}
|
|
135
|
+
options={{
|
|
136
|
+
readOnly,
|
|
137
|
+
minimap: { enabled: false },
|
|
138
|
+
fontSize: 14,
|
|
139
|
+
lineNumbers: "on",
|
|
140
|
+
// Monaco defaults this to "dimmed" on Linux only, which greys the
|
|
141
|
+
// final line's number to ~1.4:1 against the editor background — an
|
|
142
|
+
// axe color-contrast failure, and a platform-dependent one at that.
|
|
143
|
+
// Pinning it "off" fixes the contrast and makes the editor render
|
|
144
|
+
// identically everywhere.
|
|
145
|
+
renderFinalNewline: "off",
|
|
146
|
+
scrollBeyondLastLine: false,
|
|
147
|
+
wordWrap: "on",
|
|
148
|
+
automaticLayout: true,
|
|
149
|
+
tabSize: 2,
|
|
150
|
+
formatOnPaste: true,
|
|
151
|
+
formatOnType: true,
|
|
152
|
+
quickSuggestions: true,
|
|
153
|
+
suggestOnTriggerCharacters: true,
|
|
154
|
+
acceptSuggestionOnEnter: "on",
|
|
155
|
+
snippetSuggestions: "top",
|
|
156
|
+
"semanticHighlighting.enabled": true,
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic token provider backed by TypeScript's real language service.
|
|
3
|
+
*
|
|
4
|
+
* Monaco's standalone editor doesn't wire up semantic tokens — that's a VS Code
|
|
5
|
+
* layer. We extend the TS worker to expose getEncodedSemanticClassifications,
|
|
6
|
+
* then map the result to Monaco's delta-encoded format.
|
|
7
|
+
*/
|
|
8
|
+
import type { editor, languages, Uri } from "monaco-editor";
|
|
9
|
+
|
|
10
|
+
/** Matches TypeScript's v2020 semantic classifier exactly. */
|
|
11
|
+
const legend: languages.SemanticTokensLegend = {
|
|
12
|
+
tokenTypes: ["class", "enum", "interface", "namespace", "typeParameter", "type", "parameter", "variable", "enumMember", "property", "function", "method"],
|
|
13
|
+
tokenModifiers: ["declaration", "static", "async", "readonly", "defaultLibrary", "local"],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type GetWorker = () => Promise<
|
|
17
|
+
(...uris: Uri[]) => Promise<{
|
|
18
|
+
getEncodedSemanticClassifications(fileName: string, start: number, length: number): Promise<{ spans: number[] }>;
|
|
19
|
+
}>
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export function createSemanticTokensProvider(getWorker: GetWorker): languages.DocumentSemanticTokensProvider {
|
|
23
|
+
return {
|
|
24
|
+
getLegend: () => legend,
|
|
25
|
+
|
|
26
|
+
async provideDocumentSemanticTokens(model: editor.ITextModel) {
|
|
27
|
+
const worker = await getWorker();
|
|
28
|
+
const client = await worker(model.uri);
|
|
29
|
+
const { spans } = await client.getEncodedSemanticClassifications(model.uri.toString(), 0, model.getValue().length);
|
|
30
|
+
|
|
31
|
+
const data: number[] = [];
|
|
32
|
+
let prevLine = 0;
|
|
33
|
+
let prevChar = 0;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i + 2 < spans.length; i += 3) {
|
|
36
|
+
const classification = spans[i + 2]!;
|
|
37
|
+
if (classification <= 255) continue; // legacy format — skip
|
|
38
|
+
|
|
39
|
+
const pos = model.getPositionAt(spans[i]!);
|
|
40
|
+
const line = pos.lineNumber - 1;
|
|
41
|
+
const char = pos.column - 1;
|
|
42
|
+
const deltaLine = line - prevLine;
|
|
43
|
+
|
|
44
|
+
data.push(deltaLine, deltaLine === 0 ? char - prevChar : char, spans[i + 1]!, (classification >> 8) - 1, classification & 255);
|
|
45
|
+
prevLine = line;
|
|
46
|
+
prevChar = char;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { data: new Uint32Array(data) };
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
releaseDocumentSemanticTokens() {},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Extends the TS worker to expose getEncodedSemanticClassifications. */
|
|
57
|
+
const WORKER_EXTENSION = `
|
|
58
|
+
self.customTSWorkerFactory = (TypeScriptWorker) => {
|
|
59
|
+
return class extends TypeScriptWorker {
|
|
60
|
+
async getEncodedSemanticClassifications(fileName, start, length) {
|
|
61
|
+
return this._languageService.getEncodedSemanticClassifications(
|
|
62
|
+
fileName, { start, length }, "2020"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
export const workerExtensionUrl = `data:text/javascript;charset=utf-8,${encodeURIComponent(WORKER_EXTENSION)}`;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { editor } from "monaco-editor";
|
|
2
|
+
|
|
3
|
+
export const PA_THEME_LIGHT = "pa-light";
|
|
4
|
+
export const PA_THEME_DARK = "pa-dark";
|
|
5
|
+
|
|
6
|
+
const lightTheme: editor.IStandaloneThemeData = {
|
|
7
|
+
base: "vs",
|
|
8
|
+
inherit: true,
|
|
9
|
+
rules: [
|
|
10
|
+
// Monarch tokenizer rules
|
|
11
|
+
{ token: "comment", foreground: "a0a1a7", fontStyle: "italic" },
|
|
12
|
+
{ token: "keyword", foreground: "a626a4" },
|
|
13
|
+
{ token: "string", foreground: "50a14f" },
|
|
14
|
+
{ token: "number", foreground: "986801" },
|
|
15
|
+
{ token: "delimiter", foreground: "383a42" },
|
|
16
|
+
{ token: "delimiter.bracket", foreground: "383a42" },
|
|
17
|
+
{ token: "tag", foreground: "e45649" },
|
|
18
|
+
{ token: "attribute.name", foreground: "986801" },
|
|
19
|
+
{ token: "attribute.value", foreground: "50a14f" },
|
|
20
|
+
{ token: "operator", foreground: "0184bc" },
|
|
21
|
+
{ token: "identifier", foreground: "e45649" },
|
|
22
|
+
|
|
23
|
+
// Semantic token rules (TypeScript language service)
|
|
24
|
+
{ token: "variable", foreground: "e45649" },
|
|
25
|
+
{ token: "variable.readonly", foreground: "986801" },
|
|
26
|
+
{ token: "parameter", foreground: "383a42" },
|
|
27
|
+
{ token: "property", foreground: "e45649" },
|
|
28
|
+
{ token: "property.readonly", foreground: "986801" },
|
|
29
|
+
{ token: "function", foreground: "4078f2" },
|
|
30
|
+
{ token: "method", foreground: "4078f2" },
|
|
31
|
+
{ token: "type", foreground: "c18401" },
|
|
32
|
+
{ token: "type.identifier", foreground: "c18401" },
|
|
33
|
+
{ token: "class", foreground: "c18401" },
|
|
34
|
+
{ token: "interface", foreground: "c18401" },
|
|
35
|
+
{ token: "enum", foreground: "c18401" },
|
|
36
|
+
{ token: "enumMember", foreground: "986801" },
|
|
37
|
+
{ token: "namespace", foreground: "e45649" },
|
|
38
|
+
],
|
|
39
|
+
colors: {
|
|
40
|
+
"editor.background": "#fafafa",
|
|
41
|
+
"editor.foreground": "#383a42",
|
|
42
|
+
"editor.lineHighlightBackground": "#f2f2f2",
|
|
43
|
+
// Gutter numbers are text as far as axe is concerned, so they carry the
|
|
44
|
+
// full 4.5:1 obligation rather than the 3:1 one non-text contrast gets.
|
|
45
|
+
// #9d9d9f managed 2.59 against this background and failed the story test.
|
|
46
|
+
"editorLineNumber.foreground": "#6f6f71",
|
|
47
|
+
"editorLineNumber.activeForeground": "#383a42",
|
|
48
|
+
"editor.selectionBackground": "#e5e5e6",
|
|
49
|
+
"editor.inactiveSelectionBackground": "#e5e5e680",
|
|
50
|
+
"editorCursor.foreground": "#526fff",
|
|
51
|
+
"editorIndentGuide.background": "#e8e8e8",
|
|
52
|
+
"editorIndentGuide.activeBackground": "#cacacc",
|
|
53
|
+
"editorBracketMatch.background": "#e5e5e6",
|
|
54
|
+
"editorBracketMatch.border": "#a0a1a7",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const darkTheme: editor.IStandaloneThemeData = {
|
|
59
|
+
base: "vs-dark",
|
|
60
|
+
inherit: true,
|
|
61
|
+
rules: [
|
|
62
|
+
// Monarch tokenizer rules
|
|
63
|
+
{ token: "comment", foreground: "5c6370", fontStyle: "italic" },
|
|
64
|
+
{ token: "keyword", foreground: "c678dd" },
|
|
65
|
+
{ token: "string", foreground: "98c379" },
|
|
66
|
+
{ token: "number", foreground: "d19a66" },
|
|
67
|
+
{ token: "delimiter", foreground: "abb2bf" },
|
|
68
|
+
{ token: "delimiter.bracket", foreground: "abb2bf" },
|
|
69
|
+
{ token: "tag", foreground: "e06c75" },
|
|
70
|
+
{ token: "attribute.name", foreground: "d19a66" },
|
|
71
|
+
{ token: "attribute.value", foreground: "98c379" },
|
|
72
|
+
{ token: "operator", foreground: "56b6c2" },
|
|
73
|
+
{ token: "identifier", foreground: "e06c75" },
|
|
74
|
+
|
|
75
|
+
// Semantic token rules (TypeScript language service)
|
|
76
|
+
{ token: "variable", foreground: "e06c75" },
|
|
77
|
+
{ token: "variable.readonly", foreground: "d19a66" },
|
|
78
|
+
{ token: "parameter", foreground: "abb2bf" },
|
|
79
|
+
{ token: "property", foreground: "e06c75" },
|
|
80
|
+
{ token: "property.readonly", foreground: "d19a66" },
|
|
81
|
+
{ token: "function", foreground: "61afef" },
|
|
82
|
+
{ token: "method", foreground: "61afef" },
|
|
83
|
+
{ token: "type", foreground: "e5c07b" },
|
|
84
|
+
{ token: "type.identifier", foreground: "e5c07b" },
|
|
85
|
+
{ token: "class", foreground: "e5c07b" },
|
|
86
|
+
{ token: "interface", foreground: "e5c07b" },
|
|
87
|
+
{ token: "enum", foreground: "e5c07b" },
|
|
88
|
+
{ token: "enumMember", foreground: "d19a66" },
|
|
89
|
+
{ token: "namespace", foreground: "e06c75" },
|
|
90
|
+
],
|
|
91
|
+
colors: {
|
|
92
|
+
"editor.background": "#282c34",
|
|
93
|
+
"editor.foreground": "#abb2bf",
|
|
94
|
+
"editor.lineHighlightBackground": "#2c313a",
|
|
95
|
+
// 4.77:1, up from #495162's 1.76. Same obligation as the light theme;
|
|
96
|
+
// this one was never caught only because no story exercises it.
|
|
97
|
+
"editorLineNumber.foreground": "#8f97a8",
|
|
98
|
+
"editorLineNumber.activeForeground": "#abb2bf",
|
|
99
|
+
"editor.selectionBackground": "#3e4451",
|
|
100
|
+
"editor.inactiveSelectionBackground": "#3e445180",
|
|
101
|
+
"editorCursor.foreground": "#528bff",
|
|
102
|
+
"editorIndentGuide.background": "#3b4048",
|
|
103
|
+
"editorIndentGuide.activeBackground": "#4b5263",
|
|
104
|
+
"editorBracketMatch.background": "#3e4451",
|
|
105
|
+
"editorBracketMatch.border": "#5c6370",
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export function defineThemes(monaco: typeof import("monaco-editor")) {
|
|
110
|
+
monaco.editor.defineTheme(PA_THEME_LIGHT, lightTheme);
|
|
111
|
+
monaco.editor.defineTheme(PA_THEME_DARK, darkTheme);
|
|
112
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Message } from "@sixthshift/design-system/message";
|
|
2
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
3
|
+
import { AlertCircle, AlertTriangle, CheckCircle, Info } from "lucide-react";
|
|
4
|
+
import type { ComponentProps, HTMLAttributes } from "react";
|
|
5
|
+
|
|
6
|
+
export type ValidationError = {
|
|
7
|
+
line: number;
|
|
8
|
+
column: number;
|
|
9
|
+
message: string;
|
|
10
|
+
severity: "error" | "warning" | "info";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ValidationStatusProps = HTMLAttributes<HTMLDivElement> & {
|
|
14
|
+
errors: ValidationError[];
|
|
15
|
+
/** Cap the rendered error rows. Excess is summarised as "…and N more". */
|
|
16
|
+
maxRows?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Intent = NonNullable<ComponentProps<typeof Message>["intent"]>;
|
|
20
|
+
|
|
21
|
+
const SEVERITY_INTENT: Record<ValidationError["severity"], Intent> = {
|
|
22
|
+
error: "danger",
|
|
23
|
+
warning: "warning",
|
|
24
|
+
info: "neutral",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const severityIcon = (severity: ValidationError["severity"]) => {
|
|
28
|
+
if (severity === "error") return <AlertCircle className="h-4 w-4" />;
|
|
29
|
+
if (severity === "warning") return <AlertTriangle className="h-4 w-4" />;
|
|
30
|
+
return <Info className="h-4 w-4" />;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const summarise = (errorCount: number, warningCount: number) => {
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
36
|
+
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
37
|
+
return parts.join(", ");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const ValidationStatus = ({ errors, maxRows = 5, className, ...props }: ValidationStatusProps) => {
|
|
41
|
+
if (errors.length === 0) {
|
|
42
|
+
return (
|
|
43
|
+
<div {...props} className={cn("flex flex-col", className)}>
|
|
44
|
+
<Message intent="success" icon={<CheckCircle className="h-4 w-4" />}>
|
|
45
|
+
No errors — code is valid
|
|
46
|
+
</Message>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const errorCount = errors.filter((e) => e.severity === "error").length;
|
|
52
|
+
const warningCount = errors.filter((e) => e.severity === "warning").length;
|
|
53
|
+
const visible = errors.slice(0, maxRows);
|
|
54
|
+
const overflow = errors.length - visible.length;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div {...props} className={cn("flex flex-col gap-2", className)}>
|
|
58
|
+
<Message intent="danger" icon={<AlertCircle className="h-4 w-4" />}>
|
|
59
|
+
{summarise(errorCount, warningCount)}
|
|
60
|
+
</Message>
|
|
61
|
+
|
|
62
|
+
{visible.map((error) => (
|
|
63
|
+
<Message
|
|
64
|
+
key={`${error.line}-${error.column}-${error.message}`}
|
|
65
|
+
intent={SEVERITY_INTENT[error.severity]}
|
|
66
|
+
icon={severityIcon(error.severity)}
|
|
67
|
+
size="sm"
|
|
68
|
+
title={error.message}
|
|
69
|
+
>
|
|
70
|
+
Line {error.line}, Column {error.column}
|
|
71
|
+
</Message>
|
|
72
|
+
))}
|
|
73
|
+
|
|
74
|
+
{overflow > 0 && (
|
|
75
|
+
<Message intent="neutral" size="sm">
|
|
76
|
+
…and {overflow} more
|
|
77
|
+
</Message>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type ValidationError, ValidationStatus, type ValidationStatusProps } from "./ValidationStatus";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type StatusBarItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type StatusBarProps = HTMLAttributes<HTMLDivElement> & {
|
|
12
|
+
items?: StatusBarItem[] | undefined;
|
|
13
|
+
leftContent?: ReactNode;
|
|
14
|
+
rightContent?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Status bar component for code editor showing metadata and status information
|
|
19
|
+
*/
|
|
20
|
+
export const StatusBar = ({ items = [], leftContent, rightContent, className, ...props }: StatusBarProps) => {
|
|
21
|
+
return (
|
|
22
|
+
<div {...props} className={`flex items-center justify-between border-border-normal border-t bg-bg-subtle px-4 py-1 text-xs ${className ?? ""}`}>
|
|
23
|
+
{/* Left content */}
|
|
24
|
+
<div className="flex items-center gap-4">
|
|
25
|
+
{leftContent}
|
|
26
|
+
{/* biome-ignore-start lint/a11y/noStaticElementInteractions: items are optionally clickable metadata chips — the API takes plain data and a per-item button would change the bar's layout */}
|
|
27
|
+
{/* biome-ignore-start lint/a11y/useKeyWithClickEvents: same constraint as above */}
|
|
28
|
+
{items.map((item) => (
|
|
29
|
+
<div
|
|
30
|
+
key={item.id}
|
|
31
|
+
className={`flex items-center gap-1 text-fg-subtle ${item.onClick ? "cursor-pointer hover:text-fg-normal" : ""}`}
|
|
32
|
+
onClick={item.onClick}
|
|
33
|
+
>
|
|
34
|
+
{item.icon && <span>{item.icon}</span>}
|
|
35
|
+
<span className="font-medium">{item.label}</span>
|
|
36
|
+
{item.value && <span>: {item.value}</span>}
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
{/* biome-ignore-end lint/a11y/useKeyWithClickEvents: see start marker */}
|
|
40
|
+
{/* biome-ignore-end lint/a11y/noStaticElementInteractions: see start marker */}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Right content */}
|
|
44
|
+
<div className="flex items-center gap-4 text-fg-subtle">{rightContent}</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|