@olympusoss/canvas 2.20.2 → 3.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 (214) hide show
  1. package/README.md +69 -35
  2. package/package.json +45 -177
  3. package/src/cn.ts +3 -0
  4. package/src/index.ts +12 -603
  5. package/src/theme.ts +62 -0
  6. package/src/tokens.ts +11 -0
  7. package/styles/base.css +17 -0
  8. package/styles/canvas.css +77 -52
  9. package/styles/components/alert.css +66 -0
  10. package/styles/components/app-shell.css +46 -0
  11. package/styles/components/avatar.css +22 -0
  12. package/styles/components/badge.css +83 -0
  13. package/styles/components/breadcrumb.css +35 -0
  14. package/styles/components/button-group.css +23 -0
  15. package/styles/components/button.css +107 -0
  16. package/styles/components/calendar.css +73 -0
  17. package/styles/components/card.css +58 -0
  18. package/styles/components/checkbox.css +55 -0
  19. package/styles/components/code-block.css +18 -0
  20. package/styles/components/combobox.css +75 -0
  21. package/styles/components/command.css +94 -0
  22. package/styles/components/data-table.css +142 -0
  23. package/styles/components/dialog.css +72 -0
  24. package/styles/components/dropdown.css +54 -0
  25. package/styles/components/empty-state.css +17 -0
  26. package/styles/components/field.css +27 -0
  27. package/styles/components/filter-panel.css +58 -0
  28. package/styles/components/form.css +27 -0
  29. package/styles/components/icon.css +8 -0
  30. package/styles/components/input-group.css +45 -0
  31. package/styles/components/input.css +56 -0
  32. package/styles/components/kbd.css +15 -0
  33. package/styles/components/page-header.css +52 -0
  34. package/styles/components/pagination.css +48 -0
  35. package/styles/components/popover.css +14 -0
  36. package/styles/components/radio.css +28 -0
  37. package/styles/components/row-menu.css +69 -0
  38. package/styles/components/section-card.css +49 -0
  39. package/styles/components/select.css +57 -0
  40. package/styles/components/separator.css +32 -0
  41. package/styles/components/sheet.css +70 -0
  42. package/styles/components/sidebar.css +146 -0
  43. package/styles/components/skeleton.css +32 -0
  44. package/styles/components/spinner.css +26 -0
  45. package/styles/components/stat-card.css +71 -0
  46. package/styles/components/stepper.css +63 -0
  47. package/styles/components/switch.css +45 -0
  48. package/styles/components/tabs.css +40 -0
  49. package/styles/components/textarea.css +31 -0
  50. package/styles/components/toast.css +95 -0
  51. package/styles/components/tooltip.css +53 -0
  52. package/styles/components/topbar.css +24 -0
  53. package/styles/components/typography.css +105 -0
  54. package/styles/patterns/backdrops.css +35 -0
  55. package/styles/patterns/density.css +66 -0
  56. package/styles/patterns/focus.css +22 -0
  57. package/styles/patterns/glass.css +85 -0
  58. package/styles/patterns/high-contrast.css +70 -0
  59. package/styles/patterns/reduced-motion.css +12 -0
  60. package/styles/patterns/scrollbar.css +10 -0
  61. package/styles/reset.css +89 -0
  62. package/styles/tokens/colors.css +106 -0
  63. package/styles/tokens/motion.css +33 -0
  64. package/styles/tokens/radius.css +10 -0
  65. package/styles/tokens/shadows.css +35 -0
  66. package/styles/tokens/spacing.css +19 -0
  67. package/styles/tokens/typography.css +6 -0
  68. package/styles/tokens/z-index.css +12 -0
  69. package/styles/utilities/display.css +66 -0
  70. package/styles/utilities/flexbox.css +240 -0
  71. package/styles/utilities/gap.css +288 -0
  72. package/styles/utilities/grid.css +138 -0
  73. package/styles/utilities/position.css +78 -0
  74. package/styles/utilities/sizing.css +138 -0
  75. package/tsconfig.json +20 -21
  76. package/src/components/atoms/README.md +0 -11
  77. package/src/components/atoms/aspect-ratio.tsx +0 -32
  78. package/src/components/atoms/avatar.tsx +0 -98
  79. package/src/components/atoms/badge.tsx +0 -44
  80. package/src/components/atoms/brand-mark.tsx +0 -74
  81. package/src/components/atoms/button.tsx +0 -105
  82. package/src/components/atoms/checkbox.tsx +0 -63
  83. package/src/components/atoms/flex-box.tsx +0 -105
  84. package/src/components/atoms/icon.tsx +0 -34
  85. package/src/components/atoms/input.tsx +0 -92
  86. package/src/components/atoms/label.tsx +0 -41
  87. package/src/components/atoms/logo.tsx +0 -89
  88. package/src/components/atoms/progress.tsx +0 -55
  89. package/src/components/atoms/radio-group.tsx +0 -122
  90. package/src/components/atoms/scroll-area.tsx +0 -106
  91. package/src/components/atoms/section.tsx +0 -48
  92. package/src/components/atoms/separator.tsx +0 -45
  93. package/src/components/atoms/skeleton.tsx +0 -17
  94. package/src/components/atoms/slider.tsx +0 -93
  95. package/src/components/atoms/spinner.tsx +0 -47
  96. package/src/components/atoms/switch.tsx +0 -60
  97. package/src/components/atoms/textarea.tsx +0 -78
  98. package/src/components/atoms/toggle.tsx +0 -80
  99. package/src/components/charts/activity-heatmap.tsx +0 -186
  100. package/src/components/charts/axes.tsx +0 -21
  101. package/src/components/charts/chart-container.tsx +0 -254
  102. package/src/components/charts/chart-legend.tsx +0 -67
  103. package/src/components/charts/chart-tooltip.tsx +0 -161
  104. package/src/components/charts/chart-types.tsx +0 -49
  105. package/src/components/charts/containers.tsx +0 -11
  106. package/src/components/charts/data.tsx +0 -16
  107. package/src/components/charts/details.tsx +0 -25
  108. package/src/components/charts/dot-pulse.tsx +0 -61
  109. package/src/components/charts/gauge.tsx +0 -106
  110. package/src/components/charts/grids.tsx +0 -8
  111. package/src/components/charts/index.ts +0 -62
  112. package/src/components/charts/labeled-bar-list.tsx +0 -85
  113. package/src/components/charts/metric-breakdown.tsx +0 -316
  114. package/src/components/charts/references.tsx +0 -8
  115. package/src/components/charts/service-health-list.tsx +0 -85
  116. package/src/components/charts/sparkline-area.tsx +0 -80
  117. package/src/components/charts/sparkline.tsx +0 -52
  118. package/src/components/charts/stacked-bar.tsx +0 -104
  119. package/src/components/charts/text.tsx +0 -10
  120. package/src/components/charts/world-heat-map-inner.tsx +0 -317
  121. package/src/components/charts/world-heat-map.tsx +0 -184
  122. package/src/components/molecules/README.md +0 -12
  123. package/src/components/molecules/action-bar.tsx +0 -73
  124. package/src/components/molecules/activity-item.tsx +0 -74
  125. package/src/components/molecules/alert.tsx +0 -86
  126. package/src/components/molecules/animated-background.tsx +0 -92
  127. package/src/components/molecules/auth-shell.tsx +0 -95
  128. package/src/components/molecules/brand-lockup.tsx +0 -48
  129. package/src/components/molecules/breadcrumb.tsx +0 -157
  130. package/src/components/molecules/button-group.tsx +0 -104
  131. package/src/components/molecules/calendar.tsx +0 -217
  132. package/src/components/molecules/card.tsx +0 -102
  133. package/src/components/molecules/client-brand.tsx +0 -95
  134. package/src/components/molecules/code-block.tsx +0 -86
  135. package/src/components/molecules/countdown-button.tsx +0 -92
  136. package/src/components/molecules/empty-state.tsx +0 -56
  137. package/src/components/molecules/error-state.tsx +0 -42
  138. package/src/components/molecules/field-display.tsx +0 -35
  139. package/src/components/molecules/input-otp.tsx +0 -74
  140. package/src/components/molecules/launcher-card.tsx +0 -152
  141. package/src/components/molecules/loading-state.tsx +0 -36
  142. package/src/components/molecules/notification-item.tsx +0 -67
  143. package/src/components/molecules/notification-list.tsx +0 -45
  144. package/src/components/molecules/number-badge.tsx +0 -53
  145. package/src/components/molecules/or-separator.tsx +0 -38
  146. package/src/components/molecules/page-header.tsx +0 -88
  147. package/src/components/molecules/page-tabs.tsx +0 -94
  148. package/src/components/molecules/pagination.tsx +0 -150
  149. package/src/components/molecules/password-input.tsx +0 -83
  150. package/src/components/molecules/password-strength-meter.tsx +0 -104
  151. package/src/components/molecules/phone-input.tsx +0 -200
  152. package/src/components/molecules/search-bar.tsx +0 -64
  153. package/src/components/molecules/secret-field.tsx +0 -158
  154. package/src/components/molecules/section-card.tsx +0 -91
  155. package/src/components/molecules/social-buttons.tsx +0 -165
  156. package/src/components/molecules/stat-card.tsx +0 -100
  157. package/src/components/molecules/status-badge.tsx +0 -42
  158. package/src/components/molecules/stepper.tsx +0 -96
  159. package/src/components/molecules/table.tsx +0 -157
  160. package/src/components/molecules/terminal.tsx +0 -74
  161. package/src/components/molecules/toggle-group.tsx +0 -145
  162. package/src/components/molecules/tooltip.tsx +0 -155
  163. package/src/components/molecules/user-avatar-chip.tsx +0 -71
  164. package/src/components/organisms/README.md +0 -14
  165. package/src/components/organisms/accordion.tsx +0 -154
  166. package/src/components/organisms/alert-dialog.tsx +0 -277
  167. package/src/components/organisms/carousel.tsx +0 -244
  168. package/src/components/organisms/collapsible.tsx +0 -69
  169. package/src/components/organisms/command.tsx +0 -144
  170. package/src/components/organisms/context-menu.tsx +0 -339
  171. package/src/components/organisms/dashboard-grid.tsx +0 -369
  172. package/src/components/organisms/data-table.tsx +0 -330
  173. package/src/components/organisms/dialog.tsx +0 -312
  174. package/src/components/organisms/drawer.tsx +0 -123
  175. package/src/components/organisms/dropdown-menu.tsx +0 -440
  176. package/src/components/organisms/editors/code-editor.tsx +0 -144
  177. package/src/components/organisms/editors/index.ts +0 -4
  178. package/src/components/organisms/editors/markdown-editor.tsx +0 -153
  179. package/src/components/organisms/editors/markdown-renderer.ts +0 -27
  180. package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
  181. package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
  182. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
  183. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
  184. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
  185. package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
  186. package/src/components/organisms/error-boundary.tsx +0 -61
  187. package/src/components/organisms/form.tsx +0 -174
  188. package/src/components/organisms/hover-card.tsx +0 -115
  189. package/src/components/organisms/menubar.tsx +0 -498
  190. package/src/components/organisms/navbar.tsx +0 -104
  191. package/src/components/organisms/navigation-menu.tsx +0 -235
  192. package/src/components/organisms/popover.tsx +0 -149
  193. package/src/components/organisms/resizable.tsx +0 -58
  194. package/src/components/organisms/schema-form.tsx +0 -232
  195. package/src/components/organisms/select.tsx +0 -309
  196. package/src/components/organisms/sheet.tsx +0 -265
  197. package/src/components/organisms/sidebar.tsx +0 -1040
  198. package/src/components/organisms/sonner.tsx +0 -96
  199. package/src/components/organisms/tabs.tsx +0 -133
  200. package/src/components/organisms/theme-provider.tsx +0 -101
  201. package/src/hooks/use-mobile.tsx +0 -19
  202. package/src/lib/portal-container.tsx +0 -35
  203. package/src/lib/utils.ts +0 -6
  204. package/src/native.ts +0 -23
  205. package/src/tokens/colors.ts +0 -91
  206. package/src/tokens/index.ts +0 -3
  207. package/src/tokens/spacing.ts +0 -55
  208. package/src/tokens/typography.ts +0 -27
  209. package/styles/dashboard-grid.css +0 -47
  210. package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
  211. package/styles/glass.css +0 -175
  212. package/styles/leaflet.css +0 -13
  213. package/styles/tokens.css +0 -317
  214. package/tailwind.config.ts +0 -70
@@ -1,200 +0,0 @@
1
- "use client";
2
-
3
- import parsePhoneNumber, {
4
- type CountryCode,
5
- getCountries,
6
- getCountryCallingCode,
7
- isValidPhoneNumber,
8
- } from "libphonenumber-js";
9
- import * as React from "react";
10
-
11
- import { cn } from "../../lib/utils";
12
- import { Input } from "../atoms/input";
13
- import { Label } from "../atoms/label";
14
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../organisms/select";
15
-
16
- export interface PhoneInputProps {
17
- id: string;
18
- value?: string;
19
- onChange: (e164Value: string | undefined) => void;
20
- label?: string;
21
- placeholder?: string;
22
- disabled?: boolean;
23
- readonly?: boolean;
24
- required?: boolean;
25
- /** Default country ISO code if the value is empty. Default: "US". */
26
- defaultCountry?: CountryCode;
27
- /** Override the list of selectable countries (defaults to all). */
28
- countryCodes?: CountryCode[];
29
- /** Called with error string (or "" when valid/cleared). */
30
- onValidityChange?: (error: string) => void;
31
- className?: string;
32
- }
33
-
34
- interface CountryOption {
35
- code: CountryCode;
36
- name: string;
37
- callingCode: string;
38
- }
39
-
40
- /** Convert an ISO 3166-1 alpha-2 country code to its flag emoji. */
41
- function flagEmoji(code: string): string {
42
- const A = "A".charCodeAt(0);
43
- const offset = 0x1f1e6 - A;
44
- const upper = code.toUpperCase();
45
- /* c8 ignore next -- defensive: every CountryCode from libphonenumber is a 2-letter ISO code by construction; only reachable if a malformed string slips past TS at the call site */
46
- if (upper.length !== 2) return code;
47
- return String.fromCodePoint(upper.charCodeAt(0) + offset, upper.charCodeAt(1) + offset);
48
- }
49
-
50
- function buildCountryOptions(codes?: CountryCode[]): CountryOption[] {
51
- const list = codes ?? getCountries();
52
- const displayNames = new Intl.DisplayNames(["en"], { type: "region" });
53
- return list
54
- .map((code) => {
55
- const callingCode = getCountryCallingCode(code);
56
- /* c8 ignore next -- defensive: Intl.DisplayNames returns a name for every valid ISO country code in jsdom; the `|| code` fallback only fires if Intl data is stripped */
57
- const name = displayNames.of(code) || code;
58
- return { code, name, callingCode };
59
- })
60
- .sort((a, b) => a.name.localeCompare(b.name));
61
- }
62
-
63
- export function PhoneInput({
64
- id,
65
- value,
66
- onChange,
67
- label,
68
- placeholder,
69
- disabled,
70
- readonly,
71
- required,
72
- defaultCountry = "US",
73
- countryCodes,
74
- onValidityChange,
75
- className,
76
- }: PhoneInputProps) {
77
- const [selectedCountry, setSelectedCountry] = React.useState<CountryCode>(defaultCountry);
78
- const [localValue, setLocalValue] = React.useState("");
79
- const [error, setError] = React.useState("");
80
-
81
- const countryOptions = React.useMemo(() => buildCountryOptions(countryCodes), [countryCodes]);
82
-
83
- // Hydrate state from an external E.164 value.
84
- React.useEffect(() => {
85
- if (!value) {
86
- setLocalValue("");
87
- return;
88
- }
89
- try {
90
- const parsed = parsePhoneNumber(value);
91
- if (parsed) {
92
- if (parsed.country) setSelectedCountry(parsed.country);
93
- /* c8 ignore next -- defensive: a successfully-parsed PhoneNumber always exposes a nationalNumber; the `?? ""` fallback only fires if libphonenumber returns a partial object */
94
- setLocalValue(parsed.nationalNumber ?? "");
95
- } else {
96
- setLocalValue(value);
97
- }
98
- } catch {
99
- /* c8 ignore next -- libphonenumber never throws on arbitrary input in jsdom; only reachable if parser invariants change */
100
- setLocalValue(value);
101
- }
102
- }, [value]);
103
-
104
- const updateValidity = React.useCallback(
105
- (e164: string) => {
106
- if (!e164) {
107
- const err = required ? "Phone number is required" : "";
108
- setError(err);
109
- onValidityChange?.(err);
110
- return;
111
- }
112
- const valid = isValidPhoneNumber(e164);
113
- const err = valid ? "" : "Enter a valid phone number";
114
- setError(err);
115
- onValidityChange?.(err);
116
- },
117
- [required, onValidityChange],
118
- );
119
-
120
- const emit = (country: CountryCode, national: string) => {
121
- if (!national.trim()) {
122
- onChange(undefined);
123
- updateValidity("");
124
- return;
125
- }
126
- const callingCode = getCountryCallingCode(country);
127
- const e164 = `+${callingCode}${national.replace(/\D/g, "")}`;
128
- onChange(e164);
129
- updateValidity(e164);
130
- };
131
-
132
- return (
133
- <div className={cn("space-y-1.5", className)}>
134
- {label && (
135
- <Label htmlFor={id}>
136
- {label}
137
- {required && <span className="ml-0.5 text-destructive">*</span>}
138
- </Label>
139
- )}
140
- <div className="flex gap-2">
141
- <Select
142
- value={selectedCountry}
143
- onValueChange={(next) => {
144
- const code = next as CountryCode;
145
- setSelectedCountry(code);
146
- emit(code, localValue);
147
- }}
148
- disabled={disabled || readonly}
149
- >
150
- <SelectTrigger
151
- aria-label="Country"
152
- className="w-auto shrink-0 gap-1.5 px-2.5 font-mono text-sm"
153
- >
154
- <SelectValue>
155
- <span className="flex items-center gap-1.5">
156
- <span aria-hidden className="text-base leading-none">
157
- {flagEmoji(selectedCountry)}
158
- </span>
159
- <span>+{getCountryCallingCode(selectedCountry)}</span>
160
- </span>
161
- </SelectValue>
162
- </SelectTrigger>
163
- <SelectContent className="max-h-[320px] min-w-[260px]">
164
- {countryOptions.map((opt) => (
165
- <SelectItem key={opt.code} value={opt.code}>
166
- <span className="flex items-center gap-2">
167
- <span aria-hidden className="text-base leading-none">
168
- {flagEmoji(opt.code)}
169
- </span>
170
- <span>{opt.name}</span>
171
- <span className="font-mono text-xs text-muted-foreground">
172
- +{opt.callingCode}
173
- </span>
174
- </span>
175
- </SelectItem>
176
- ))}
177
- </SelectContent>
178
- </Select>
179
- <Input
180
- id={id}
181
- type="tel"
182
- inputMode="tel"
183
- value={localValue}
184
- onChange={(e) => {
185
- const v = e.target.value;
186
- setLocalValue(v);
187
- emit(selectedCountry, v);
188
- }}
189
- placeholder={placeholder}
190
- disabled={disabled}
191
- readOnly={readonly}
192
- className={cn("flex-1", error && "border-destructive focus-visible:ring-destructive")}
193
- />
194
- </div>
195
- {error && <p className="text-xs text-destructive">{error}</p>}
196
- </div>
197
- );
198
- }
199
-
200
- PhoneInput.displayName = "PhoneInput";
@@ -1,64 +0,0 @@
1
- "use client";
2
-
3
- import { Search, X } from "lucide-react";
4
- import * as React from "react";
5
-
6
- import { cn } from "../../lib/utils";
7
- import { Button } from "../atoms/button";
8
- import { Input } from "../atoms/input";
9
-
10
- export interface SearchBarProps
11
- extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
12
- value: string;
13
- onChange: (value: string) => void;
14
- onClear?: () => void;
15
- /**
16
- * Optional keyboard-shortcut hint rendered as a `<kbd>` on the right of
17
- * the input when empty. Hidden once the user types so the clear button
18
- * has the slot. Example: `shortcut="⌘K"`.
19
- */
20
- shortcut?: string;
21
- }
22
-
23
- const SearchBar = React.forwardRef<HTMLInputElement, SearchBarProps>(
24
- ({ value, onChange, onClear, shortcut, className, placeholder = "Search...", ...props }, ref) => {
25
- const showShortcut = !!shortcut && value.length === 0;
26
- return (
27
- <div className={cn("relative", className)}>
28
- <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
29
- <Input
30
- ref={ref}
31
- value={value}
32
- onChange={(e) => onChange(e.target.value)}
33
- placeholder={placeholder}
34
- className={cn("pl-9", showShortcut ? "pr-12" : "pr-9")}
35
- {...props}
36
- />
37
- {value && (
38
- <Button
39
- variant="ghost"
40
- size="icon"
41
- className="absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2"
42
- onClick={() => {
43
- onChange("");
44
- onClear?.();
45
- }}
46
- >
47
- <X className="h-3 w-3" />
48
- </Button>
49
- )}
50
- {showShortcut && (
51
- <kbd
52
- aria-hidden
53
- className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded border border-border bg-muted/40 px-1.5 font-mono text-[10px] text-muted-foreground"
54
- >
55
- {shortcut}
56
- </kbd>
57
- )}
58
- </div>
59
- );
60
- },
61
- );
62
- SearchBar.displayName = "SearchBar";
63
-
64
- export { SearchBar };
@@ -1,158 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
- import { Icon } from "../atoms/icon";
7
- import { Input } from "../atoms/input";
8
- import { Label } from "../atoms/label";
9
-
10
- export type SecretFieldStatus = "idle" | "validating" | "valid" | "invalid";
11
-
12
- export interface SecretFieldProps {
13
- id: string;
14
- value: string;
15
- onChange: (value: string) => void;
16
- /**
17
- * Async validator. Return `true` (or any string) on success; throw or return
18
- * `false` on failure. The thrown message is shown below the input.
19
- */
20
- onValidate?: (value: string) => Promise<boolean | string>;
21
- /**
22
- * Called after successful validation with the validated value.
23
- * Use this to persist the value once it's confirmed valid.
24
- */
25
- onSave?: (value: string) => void;
26
- /**
27
- * Notifies when the validation status changes. Useful for parent state.
28
- */
29
- onStatusChange?: (status: SecretFieldStatus) => void;
30
- placeholder?: string;
31
- label?: string;
32
- disabled?: boolean;
33
- /** Minimum length before auto-validation starts. Default 3. */
34
- minLength?: number;
35
- /** Show the reveal (eye) toggle. Default true. */
36
- revealable?: boolean;
37
- className?: string;
38
- }
39
-
40
- export function SecretField({
41
- id,
42
- value,
43
- onChange,
44
- onValidate,
45
- onSave,
46
- onStatusChange,
47
- placeholder,
48
- label,
49
- disabled,
50
- minLength = 3,
51
- revealable = true,
52
- className,
53
- }: SecretFieldProps) {
54
- const [visible, setVisible] = React.useState(false);
55
- const [status, setStatus] = React.useState<SecretFieldStatus>("idle");
56
- const [error, setError] = React.useState("");
57
- const debounceRef = React.useRef(0);
58
- const lastValidatedRef = React.useRef("");
59
-
60
- React.useEffect(() => {
61
- onStatusChange?.(status);
62
- }, [status, onStatusChange]);
63
-
64
- React.useEffect(() => {
65
- if (!onValidate) return;
66
- if (!value || value.length < minLength) {
67
- setStatus("idle");
68
- setError("");
69
- return;
70
- }
71
- /* c8 ignore next -- race-condition guard: only triggered by rapid input */
72
- if (value === lastValidatedRef.current) return;
73
-
74
- const id = ++debounceRef.current;
75
- setStatus("validating");
76
- setError("");
77
-
78
- const timer = setTimeout(async () => {
79
- /* c8 ignore next -- race-condition guard: debounce re-entry unreachable in tests */
80
- if (id !== debounceRef.current) return;
81
- try {
82
- const result = await onValidate(value);
83
- /* c8 ignore next -- race-condition guard: await-resolved stale id unreachable in tests */
84
- if (id !== debounceRef.current) return;
85
- if (result === true || typeof result === "string") {
86
- setStatus("valid");
87
- setError("");
88
- lastValidatedRef.current = value;
89
- onSave?.(value);
90
- } else {
91
- setStatus("invalid");
92
- setError("Validation failed");
93
- }
94
- } catch (err) {
95
- /* c8 ignore next -- race-condition guard: await-rejected stale id unreachable in tests */
96
- if (id !== debounceRef.current) return;
97
- setStatus("invalid");
98
- setError(err instanceof Error ? err.message : "Validation failed");
99
- }
100
- }, 800);
101
-
102
- return () => clearTimeout(timer);
103
- }, [value, minLength, onValidate, onSave]);
104
-
105
- const handleChange = (v: string) => {
106
- onChange(v);
107
- if (error) setError("");
108
- lastValidatedRef.current = "";
109
- };
110
-
111
- return (
112
- <div className={cn("space-y-1", className)}>
113
- {label && (
114
- <Label htmlFor={id} className="text-xs">
115
- {label}
116
- </Label>
117
- )}
118
- <div className="relative">
119
- <Input
120
- id={id}
121
- type={visible ? "text" : "password"}
122
- value={value}
123
- onChange={(e) => handleChange(e.target.value)}
124
- placeholder={placeholder}
125
- className={cn(
126
- "font-mono text-sm",
127
- revealable && "pr-16",
128
- !revealable && "pr-10",
129
- status === "invalid" && "border-destructive",
130
- status === "valid" && "border-green-500/50",
131
- )}
132
- disabled={disabled}
133
- />
134
- <div className="absolute right-2.5 top-1/2 -translate-y-1/2 flex items-center gap-1.5">
135
- {status === "validating" && (
136
- <Icon name="LoaderCircle" className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
137
- )}
138
- {status === "valid" && <Icon name="CircleCheck" className="h-3.5 w-3.5 text-green-500" />}
139
- {status === "invalid" && <Icon name="CircleX" className="h-3.5 w-3.5 text-destructive" />}
140
- {revealable && (
141
- <button
142
- type="button"
143
- onClick={() => setVisible((v) => !v)}
144
- aria-label={visible ? "Hide value" : "Show value"}
145
- className="text-muted-foreground hover:text-foreground transition-colors"
146
- tabIndex={-1}
147
- >
148
- <Icon name={visible ? "EyeOff" : "Eye"} className="h-4 w-4" />
149
- </button>
150
- )}
151
- </div>
152
- </div>
153
- {error && <p className="text-xs text-destructive">{error}</p>}
154
- </div>
155
- );
156
- }
157
-
158
- SecretField.displayName = "SecretField";
@@ -1,91 +0,0 @@
1
- import type * as React from "react";
2
-
3
- import { cn } from "../../lib/utils";
4
- import { Icon } from "../atoms/icon";
5
- import { Alert, AlertDescription } from "./alert";
6
- import { Card, CardContent, CardHeader } from "./card";
7
-
8
- export interface SectionCardProps {
9
- title?: string | React.ReactNode;
10
- subtitle?: string;
11
- /**
12
- * Icon rendered to the left of the title (16px Lucide via the Canvas
13
- * `<Icon>` atom is the canonical pattern).
14
- */
15
- icon?: React.ReactNode;
16
- /** Right-aligned action slot inside the header. */
17
- actions?: React.ReactNode;
18
- /** @deprecated Use `actions`. */
19
- headerActions?: React.ReactNode;
20
- children?: React.ReactNode;
21
- loading?: boolean;
22
- error?: string | boolean | null;
23
- emptyMessage?: string;
24
- padding?: boolean;
25
- className?: string;
26
- }
27
-
28
- export function SectionCard({
29
- title,
30
- subtitle,
31
- icon,
32
- actions,
33
- headerActions,
34
- children,
35
- loading = false,
36
- error,
37
- emptyMessage,
38
- padding = true,
39
- className,
40
- }: SectionCardProps) {
41
- const resolvedActions = actions ?? headerActions;
42
- const hasHeader = title || subtitle || icon || resolvedActions;
43
- return (
44
- <Card className={className}>
45
- {hasHeader && (
46
- <>
47
- <CardHeader className="flex flex-row items-center justify-between space-y-0 px-5 pb-3 pt-[18px]">
48
- <div className="flex flex-1 flex-col gap-1">
49
- {(title || icon) && (
50
- <div className="flex items-center gap-2">
51
- {icon}
52
- {title &&
53
- (typeof title === "string" ? (
54
- <span className="text-[15px] font-semibold leading-none">{title}</span>
55
- ) : (
56
- title
57
- ))}
58
- </div>
59
- )}
60
- {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
61
- </div>
62
- {resolvedActions && <div className="flex items-center gap-2">{resolvedActions}</div>}
63
- </CardHeader>
64
- <div data-slot="card-divider" className="mx-5 mb-3.5 h-px bg-border" />
65
- </>
66
- )}
67
- <CardContent className={cn(padding ? "px-5 pb-[18px] pt-0" : "p-0")}>
68
- {loading ? (
69
- <div className="flex items-center justify-center py-8">
70
- <Icon name="LoaderCircle" className="h-6 w-6 animate-spin text-muted-foreground" />
71
- </div>
72
- ) : error ? (
73
- <Alert variant="destructive">
74
- <Icon name="CircleX" className="h-4 w-4" />
75
- <AlertDescription>
76
- {typeof error === "string" ? error : "An error occurred"}
77
- </AlertDescription>
78
- </Alert>
79
- ) : emptyMessage && !children ? (
80
- <div className="flex items-center justify-center py-8 text-center">
81
- <p className="text-sm text-muted-foreground">{emptyMessage}</p>
82
- </div>
83
- ) : (
84
- children
85
- )}
86
- </CardContent>
87
- </Card>
88
- );
89
- }
90
-
91
- SectionCard.displayName = "SectionCard";
@@ -1,165 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
-
5
- import { cn } from "../../lib/utils";
6
- import { Button } from "../atoms/button";
7
-
8
- /* ──────────────────────────────────────────────────────────────────
9
- Brand glyphs. Tiny, currentColor for monochrome marks
10
- (GitHub, Microsoft outline, generic SSO). Multi-color marks
11
- (Google, Apple) inline their official palette.
12
- ────────────────────────────────────────────────────────────────── */
13
-
14
- const GitHubGlyph = () => (
15
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
16
- <title>GitHub</title>
17
- <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2c-3.2.69-3.87-1.54-3.87-1.54-.52-1.33-1.27-1.69-1.27-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.75 1.18 1.75 1.18 1.02 1.75 2.68 1.24 3.34.95.1-.74.4-1.24.72-1.52-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.08-.12-.29-.51-1.46.11-3.04 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.78 0c2.21-1.49 3.18-1.18 3.18-1.18.62 1.58.23 2.75.11 3.04.74.8 1.18 1.82 1.18 3.08 0 4.42-2.69 5.39-5.26 5.68.41.35.77 1.04.77 2.11v3.13c0 .31.21.67.79.56A11.51 11.51 0 0 0 23.5 12c0-6.35-5.15-11.5-11.5-11.5z" />
18
- </svg>
19
- );
20
-
21
- const GoogleGlyph = () => (
22
- <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
23
- <title>Google</title>
24
- <path
25
- fill="#4285F4"
26
- d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.76h3.56c2.08-1.92 3.28-4.74 3.28-8.09z"
27
- />
28
- <path
29
- fill="#34A853"
30
- d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.56-2.76c-.98.66-2.24 1.06-3.72 1.06-2.86 0-5.29-1.93-6.15-4.53H2.18v2.84A11 11 0 0 0 12 23z"
31
- />
32
- <path
33
- fill="#FBBC05"
34
- d="M5.85 14.11A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.35-2.11V7.05H2.18A11 11 0 0 0 1 12c0 1.78.43 3.46 1.18 4.95l3.67-2.84z"
35
- />
36
- <path
37
- fill="#EA4335"
38
- d="M12 5.38c1.62 0 3.06.56 4.2 1.64l3.15-3.15C17.45 2.1 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.05l3.67 2.84C6.71 7.31 9.14 5.38 12 5.38z"
39
- />
40
- </svg>
41
- );
42
-
43
- const AppleGlyph = () => (
44
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
45
- <title>Apple</title>
46
- <path d="M17.05 12.04c-.03-2.85 2.32-4.22 2.43-4.29-1.32-1.94-3.39-2.21-4.12-2.24-1.75-.18-3.42 1.03-4.31 1.03-.9 0-2.27-1.01-3.74-.98-1.92.03-3.7 1.12-4.69 2.83-2.01 3.47-.51 8.6 1.43 11.43.96 1.38 2.09 2.93 3.56 2.87 1.44-.06 1.98-.92 3.71-.92 1.73 0 2.22.92 3.74.89 1.54-.03 2.52-1.4 3.46-2.79 1.1-1.6 1.55-3.15 1.58-3.23-.03-.01-3.03-1.16-3.05-4.6zM14.27 3.65c.79-.96 1.32-2.29 1.18-3.62-1.14.05-2.52.76-3.34 1.71-.73.84-1.37 2.19-1.2 3.49 1.27.1 2.57-.65 3.36-1.58z" />
47
- </svg>
48
- );
49
-
50
- const MicrosoftGlyph = () => (
51
- <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
52
- <title>Microsoft</title>
53
- <path fill="#F25022" d="M1 1h10v10H1z" />
54
- <path fill="#7FBA00" d="M13 1h10v10H13z" />
55
- <path fill="#00A4EF" d="M1 13h10v10H1z" />
56
- <path fill="#FFB900" d="M13 13h10v10H13z" />
57
- </svg>
58
- );
59
-
60
- const SsoGlyph = () => (
61
- <svg
62
- width="16"
63
- height="16"
64
- viewBox="0 0 24 24"
65
- fill="none"
66
- stroke="currentColor"
67
- strokeWidth={2}
68
- strokeLinecap="round"
69
- strokeLinejoin="round"
70
- aria-hidden
71
- >
72
- <title>SSO</title>
73
- <rect x="3" y="11" width="18" height="11" rx="2" />
74
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
75
- </svg>
76
- );
77
-
78
- /**
79
- * Known social/SSO providers. Each entry pairs a glyph with a default
80
- * label; the caller can override the label per-button if needed.
81
- */
82
- export type SocialProvider = "github" | "google" | "apple" | "microsoft" | "sso";
83
-
84
- interface ProviderMeta {
85
- glyph: React.ReactNode;
86
- label: string;
87
- }
88
-
89
- const PROVIDERS: Record<SocialProvider, ProviderMeta> = {
90
- github: { glyph: <GitHubGlyph />, label: "Continue with GitHub" },
91
- google: { glyph: <GoogleGlyph />, label: "Continue with Google" },
92
- apple: { glyph: <AppleGlyph />, label: "Continue with Apple" },
93
- microsoft: { glyph: <MicrosoftGlyph />, label: "Continue with Microsoft" },
94
- sso: { glyph: <SsoGlyph />, label: "Continue with SSO" },
95
- };
96
-
97
- export interface SocialButtonProps
98
- extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
99
- /** Provider identifier. Picks the glyph and default label. */
100
- provider: SocialProvider;
101
- /** Override the default label (e.g. "Continue with Okta"). */
102
- label?: string;
103
- }
104
-
105
- /**
106
- * Single provider button. Outline variant with the provider glyph on the
107
- * leading edge. Use directly when you want one provider, or compose
108
- * several inside `SocialButtons`.
109
- */
110
- const SocialButton = React.forwardRef<HTMLButtonElement, SocialButtonProps>(
111
- ({ provider, label, className, ...props }, ref) => {
112
- const meta = PROVIDERS[provider];
113
- return (
114
- <Button
115
- ref={ref}
116
- type="button"
117
- variant="outline"
118
- className={cn("w-full justify-center gap-2", className)}
119
- {...props}
120
- >
121
- {meta.glyph}
122
- <span>{label ?? meta.label}</span>
123
- </Button>
124
- );
125
- },
126
- );
127
- SocialButton.displayName = "SocialButton";
128
-
129
- export interface SocialButtonsProps extends React.HTMLAttributes<HTMLDivElement> {
130
- /**
131
- * Provider list rendered vertically. Pass `["github", "google"]` for the
132
- * common CIAM case, or `["sso"]` for a single corporate SSO button.
133
- * Defaults to `["github", "google"]`.
134
- */
135
- providers?: SocialProvider[];
136
- /** Per-provider click handler. Receives the provider id. */
137
- onProviderClick?: (provider: SocialProvider) => void;
138
- /** When true, all buttons render disabled (e.g. while signing in). */
139
- disabled?: boolean;
140
- }
141
-
142
- /**
143
- * Vertical stack of social/SSO provider buttons. Typically placed at the
144
- * top of a sign-in or sign-up card, above an `OrSeparator`.
145
- *
146
- * Stays purely presentational: the consumer wires `onProviderClick` to
147
- * the OAuth2 initiation flow.
148
- */
149
- const SocialButtons = React.forwardRef<HTMLDivElement, SocialButtonsProps>(
150
- ({ providers = ["github", "google"], onProviderClick, disabled, className, ...props }, ref) => (
151
- <div ref={ref} className={cn("flex flex-col gap-2.5", className)} {...props}>
152
- {providers.map((p) => (
153
- <SocialButton
154
- key={p}
155
- provider={p}
156
- disabled={disabled}
157
- onClick={() => onProviderClick?.(p)}
158
- />
159
- ))}
160
- </div>
161
- ),
162
- );
163
- SocialButtons.displayName = "SocialButtons";
164
-
165
- export { SocialButton, SocialButtons };