@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.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/styles.css +2 -0
  4. package/dist/theme.generated.css +438 -0
  5. package/package.json +238 -0
  6. package/src/components/Avatar/Avatar.tsx +19 -0
  7. package/src/components/Avatar/index.ts +1 -0
  8. package/src/components/Badge/Badge.tsx +59 -0
  9. package/src/components/Badge/index.ts +1 -0
  10. package/src/components/BarChart/BarChart.tsx +51 -0
  11. package/src/components/BarChart/index.ts +1 -0
  12. package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
  13. package/src/components/Breadcrumb/index.ts +5 -0
  14. package/src/components/Button/Button.tsx +157 -0
  15. package/src/components/Button/index.ts +1 -0
  16. package/src/components/Calendar/Calendar.tsx +433 -0
  17. package/src/components/Calendar/calendar.hooks.ts +329 -0
  18. package/src/components/Calendar/calendar.types.ts +117 -0
  19. package/src/components/Calendar/index.ts +28 -0
  20. package/src/components/Card/Card.tsx +47 -0
  21. package/src/components/Card/index.ts +1 -0
  22. package/src/components/Checkbox/Checkbox.tsx +144 -0
  23. package/src/components/Checkbox/index.ts +1 -0
  24. package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
  25. package/src/components/CheckboxGroup/index.ts +5 -0
  26. package/src/components/Code/Editor/Editor.tsx +161 -0
  27. package/src/components/Code/Editor/index.ts +2 -0
  28. package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
  29. package/src/components/Code/Editor/themes.ts +112 -0
  30. package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
  31. package/src/components/Code/ValidationStatus/index.ts +1 -0
  32. package/src/components/Code/Workspace/StatusBar.tsx +47 -0
  33. package/src/components/Code/Workspace/Toolbar.tsx +47 -0
  34. package/src/components/Code/Workspace/Workspace.tsx +109 -0
  35. package/src/components/Code/Workspace/index.ts +3 -0
  36. package/src/components/ColorDot/ColorDot.tsx +50 -0
  37. package/src/components/ColorDot/index.ts +1 -0
  38. package/src/components/DatePicker/DatePicker.tsx +236 -0
  39. package/src/components/DatePicker/datepicker.hooks.ts +381 -0
  40. package/src/components/DatePicker/datepicker.types.ts +140 -0
  41. package/src/components/DatePicker/index.ts +12 -0
  42. package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
  43. package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
  44. package/src/components/DateRangePicker/index.ts +2 -0
  45. package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
  46. package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
  47. package/src/components/DateTimePicker/index.ts +2 -0
  48. package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
  49. package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
  50. package/src/components/DateTimeRangePicker/index.ts +2 -0
  51. package/src/components/EmptyState/EmptyState.tsx +29 -0
  52. package/src/components/EmptyState/index.ts +1 -0
  53. package/src/components/Field/Field.tsx +60 -0
  54. package/src/components/Field/index.ts +1 -0
  55. package/src/components/FormField/FormField.tsx +55 -0
  56. package/src/components/FormField/index.ts +2 -0
  57. package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
  58. package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
  59. package/src/components/HeatMap/heat-map.utils.ts +15 -0
  60. package/src/components/HeatMap/index.ts +3 -0
  61. package/src/components/HoverCard/HoverCard.tsx +83 -0
  62. package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
  63. package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
  64. package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
  65. package/src/components/HoverCard/index.ts +3 -0
  66. package/src/components/Input/Input.tsx +48 -0
  67. package/src/components/Input/index.ts +1 -0
  68. package/src/components/LineChart/LineChart.tsx +251 -0
  69. package/src/components/LineChart/index.ts +1 -0
  70. package/src/components/Markdown/Markdown.tsx +40 -0
  71. package/src/components/Markdown/index.ts +1 -0
  72. package/src/components/Message/Message.tsx +59 -0
  73. package/src/components/Message/components/MessageBody.tsx +9 -0
  74. package/src/components/Message/components/MessageDescription.tsx +9 -0
  75. package/src/components/Message/components/MessageIcon.tsx +13 -0
  76. package/src/components/Message/components/MessageTitle.tsx +9 -0
  77. package/src/components/Message/index.ts +10 -0
  78. package/src/components/MetricList/MetricList.tsx +13 -0
  79. package/src/components/MetricList/MetricRow.tsx +30 -0
  80. package/src/components/MetricList/index.ts +2 -0
  81. package/src/components/Modal/Modal.tsx +146 -0
  82. package/src/components/Modal/components/ModalBody.tsx +9 -0
  83. package/src/components/Modal/components/ModalContext.tsx +27 -0
  84. package/src/components/Modal/components/ModalFooter.tsx +9 -0
  85. package/src/components/Modal/components/ModalHeader.tsx +29 -0
  86. package/src/components/Modal/components/index.ts +9 -0
  87. package/src/components/Modal/index.ts +13 -0
  88. package/src/components/NavBottom/NavBottom.tsx +53 -0
  89. package/src/components/NavBottom/index.ts +3 -0
  90. package/src/components/NavSide/NavItem.tsx +39 -0
  91. package/src/components/NavSide/NavSide.tsx +40 -0
  92. package/src/components/NavSide/index.ts +2 -0
  93. package/src/components/NavSide/types.ts +36 -0
  94. package/src/components/Pagination/Pagination.tsx +86 -0
  95. package/src/components/Pagination/index.ts +2 -0
  96. package/src/components/Popover/Popover.tsx +68 -0
  97. package/src/components/Popover/components/PopoverBody.tsx +32 -0
  98. package/src/components/Popover/components/PopoverClose.tsx +26 -0
  99. package/src/components/Popover/components/PopoverContext.tsx +24 -0
  100. package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
  101. package/src/components/Popover/index.ts +10 -0
  102. package/src/components/ProgressBar/ProgressBar.tsx +42 -0
  103. package/src/components/ProgressBar/index.ts +1 -0
  104. package/src/components/RadioButton/RadioButton.tsx +89 -0
  105. package/src/components/RadioButton/index.ts +1 -0
  106. package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
  107. package/src/components/RadioButtonGroup/index.ts +5 -0
  108. package/src/components/SearchInput/SearchInput.tsx +45 -0
  109. package/src/components/SearchInput/index.ts +2 -0
  110. package/src/components/Select/Select.tsx +289 -0
  111. package/src/components/Select/SelectDropdown.tsx +90 -0
  112. package/src/components/Select/SelectTrigger.tsx +130 -0
  113. package/src/components/Select/index.ts +3 -0
  114. package/src/components/Select/useSelectKeyboard.ts +111 -0
  115. package/src/components/Separator/Separator.tsx +20 -0
  116. package/src/components/Separator/index.ts +1 -0
  117. package/src/components/Sheet/Sheet.tsx +136 -0
  118. package/src/components/Sheet/components/SheetBody.tsx +9 -0
  119. package/src/components/Sheet/components/SheetContext.tsx +23 -0
  120. package/src/components/Sheet/components/SheetFooter.tsx +9 -0
  121. package/src/components/Sheet/components/SheetHeader.tsx +29 -0
  122. package/src/components/Sheet/components/index.ts +9 -0
  123. package/src/components/Sheet/index.ts +13 -0
  124. package/src/components/Skeleton/Skeleton.tsx +7 -0
  125. package/src/components/Skeleton/index.ts +1 -0
  126. package/src/components/Sparkline/Sparkline.tsx +93 -0
  127. package/src/components/Sparkline/index.ts +1 -0
  128. package/src/components/Spinner/Spinner.tsx +41 -0
  129. package/src/components/Spinner/index.ts +1 -0
  130. package/src/components/StatsCard/StatsCard.tsx +40 -0
  131. package/src/components/StatsCard/index.ts +1 -0
  132. package/src/components/Switch/Switch.tsx +128 -0
  133. package/src/components/Switch/index.ts +1 -0
  134. package/src/components/Tabs/Tabs.tsx +49 -0
  135. package/src/components/Tabs/components/TabsContext.tsx +35 -0
  136. package/src/components/Tabs/components/TabsList.tsx +98 -0
  137. package/src/components/Tabs/components/TabsPanels.tsx +26 -0
  138. package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
  139. package/src/components/Tabs/index.ts +3 -0
  140. package/src/components/TagChip/TagChip.tsx +46 -0
  141. package/src/components/TagChip/index.ts +1 -0
  142. package/src/components/TagInput/TagInput.tsx +60 -0
  143. package/src/components/TagInput/index.ts +1 -0
  144. package/src/components/Text/Text.tsx +35 -0
  145. package/src/components/Text/index.ts +1 -0
  146. package/src/components/TextInline/TextInline.tsx +35 -0
  147. package/src/components/TextInline/index.ts +1 -0
  148. package/src/components/Textarea/Textarea.tsx +20 -0
  149. package/src/components/Textarea/index.ts +1 -0
  150. package/src/components/TimePicker/PeriodSelector.tsx +28 -0
  151. package/src/components/TimePicker/TimeColumn.tsx +62 -0
  152. package/src/components/TimePicker/TimePicker.tsx +323 -0
  153. package/src/components/TimePicker/index.ts +7 -0
  154. package/src/components/TimePicker/timepicker.hooks.ts +173 -0
  155. package/src/components/TimePicker/timepicker.types.ts +101 -0
  156. package/src/components/Toast/Toast.tsx +111 -0
  157. package/src/components/Toast/index.ts +1 -0
  158. package/src/components/Toggle/Toggle.tsx +72 -0
  159. package/src/components/Toggle/index.ts +1 -0
  160. package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
  161. package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
  162. package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
  163. package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
  164. package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
  165. package/src/components/ToggleGroup/index.ts +2 -0
  166. package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
  167. package/src/components/Tooltip/Tooltip.tsx +66 -0
  168. package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
  169. package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
  170. package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
  171. package/src/components/Tooltip/index.ts +9 -0
  172. package/src/date-time/arithmetic.ts +101 -0
  173. package/src/date-time/boundaries.ts +91 -0
  174. package/src/date-time/compare.ts +85 -0
  175. package/src/date-time/duration.ts +63 -0
  176. package/src/date-time/elapsed.ts +18 -0
  177. package/src/date-time/format.ts +722 -0
  178. package/src/date-time/index.ts +279 -0
  179. package/src/date-time/now.ts +70 -0
  180. package/src/date-time/parse.ts +170 -0
  181. package/src/date-time/serialize.ts +90 -0
  182. package/src/date-time/timezone.ts +47 -0
  183. package/src/date-time/validate.ts +278 -0
  184. package/src/hooks/index.ts +10 -0
  185. package/src/hooks/useCollapsible.ts +16 -0
  186. package/src/hooks/useControllableState.ts +54 -0
  187. package/src/hooks/useDebouncedCallback.ts +21 -0
  188. package/src/hooks/useDebouncedState.ts +20 -0
  189. package/src/hooks/useDeviceFormFactor.ts +49 -0
  190. package/src/hooks/useLocalStorage.ts +67 -0
  191. package/src/hooks/useMergedFloatingRef.ts +29 -0
  192. package/src/hooks/usePresence.ts +127 -0
  193. package/src/hooks/useStack.ts +99 -0
  194. package/src/hooks/useTheme.ts +105 -0
  195. package/src/internal/Slot.tsx +79 -0
  196. package/src/internal/index.ts +2 -0
  197. package/src/internal/rovingFocus.ts +39 -0
  198. package/src/internal/types.ts +10 -0
  199. package/src/lib/EmptyBoundary.tsx +12 -0
  200. package/src/lib/ErrorBoundary.tsx +48 -0
  201. package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
  202. package/src/lib/contexts/ComponentsContext/index.ts +1 -0
  203. package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
  204. package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
  205. package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
  206. package/src/lib/contexts/OverlayContext/index.ts +6 -0
  207. package/src/lib/format.ts +51 -0
  208. package/src/lib/utils.ts +10 -0
  209. package/src/lib/withEmpty.tsx +14 -0
  210. package/src/lib/withErrorBoundary.tsx +16 -0
  211. package/src/lib/withSuspense.tsx +16 -0
  212. package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
  213. package/src/styles/base.css +41 -0
  214. package/src/theme/index.ts +37 -0
  215. package/src/theme/palette.json +100 -0
  216. package/src/theme/palette.schema.json +40 -0
  217. package/src/theme/schema.ts +122 -0
  218. package/src/theme/theme.json +349 -0
  219. package/src/theme/theme.schema.json +876 -0
  220. package/src/theme/typography.json +7 -0
  221. package/src/theme/typography.schema.json +36 -0
  222. package/src/typography/Body.tsx +8 -0
  223. package/src/typography/Caption.tsx +8 -0
  224. package/src/typography/Code.tsx +8 -0
  225. package/src/typography/Display.tsx +8 -0
  226. package/src/typography/Emphasis.tsx +8 -0
  227. package/src/typography/Heading.tsx +23 -0
  228. package/src/typography/Label.tsx +10 -0
  229. package/src/typography/Lead.tsx +8 -0
  230. package/src/typography/Mono.tsx +8 -0
  231. package/src/typography/Muted.tsx +8 -0
  232. package/src/typography/SectionTitle.tsx +8 -0
  233. package/src/typography/Subtitle.tsx +8 -0
  234. package/src/typography/Timestamp.tsx +8 -0
  235. package/src/typography/index.ts +10 -0
  236. 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,5 @@
1
+ export {
2
+ CheckboxGroup,
3
+ type CheckboxGroupOption,
4
+ type CheckboxGroupProps,
5
+ } from "./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,2 @@
1
+ export type { ValidationError } from "../ValidationStatus";
2
+ export { Editor, type EditorProps } from "./Editor";
@@ -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
+ };