raft-ui 0.0.6 → 0.0.7

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 (131) hide show
  1. package/CHANGELOG.md +2 -8
  2. package/{src → dist}/styles.css +0 -2
  3. package/package.json +3 -4
  4. package/src/components/alert-dialog/alert-dialog.tsx +0 -215
  5. package/src/components/alert-dialog/index.ts +0 -30
  6. package/src/components/avatar/avatar.tsx +0 -375
  7. package/src/components/avatar/index.ts +0 -17
  8. package/src/components/badge/badge.tsx +0 -393
  9. package/src/components/badge/index.ts +0 -2
  10. package/src/components/banner/banner.tsx +0 -269
  11. package/src/components/banner/index.ts +0 -8
  12. package/src/components/bottom-sheet/bottom-sheet.tsx +0 -152
  13. package/src/components/bottom-sheet/index.ts +0 -22
  14. package/src/components/button/button.tsx +0 -499
  15. package/src/components/button/index.ts +0 -2
  16. package/src/components/checkbox/checkbox-indicator.tsx +0 -128
  17. package/src/components/checkbox/checkbox.tsx +0 -350
  18. package/src/components/checkbox/index.ts +0 -2
  19. package/src/components/combobox/combobox.tsx +0 -603
  20. package/src/components/combobox/index.ts +0 -52
  21. package/src/components/context-menu/context-menu.tsx +0 -380
  22. package/src/components/context-menu/index.ts +0 -34
  23. package/src/components/conversation-preview-card/conversation-preview-card.tsx +0 -336
  24. package/src/components/conversation-preview-card/index.ts +0 -32
  25. package/src/components/description-list/description-list.tsx +0 -116
  26. package/src/components/description-list/index.ts +0 -14
  27. package/src/components/dialog/dialog.tsx +0 -152
  28. package/src/components/dialog/index.ts +0 -26
  29. package/src/components/dropdown-menu/dropdown-menu.tsx +0 -405
  30. package/src/components/dropdown-menu/index.ts +0 -38
  31. package/src/components/empty-state/empty-state.tsx +0 -177
  32. package/src/components/empty-state/index.ts +0 -16
  33. package/src/components/field/field.tsx +0 -125
  34. package/src/components/field/index.ts +0 -18
  35. package/src/components/inline-code/index.ts +0 -2
  36. package/src/components/inline-code/inline-code.tsx +0 -35
  37. package/src/components/input/index.ts +0 -2
  38. package/src/components/input/input.tsx +0 -76
  39. package/src/components/input-group/index.ts +0 -7
  40. package/src/components/input-group/input-group.tsx +0 -215
  41. package/src/components/kbd/index.ts +0 -2
  42. package/src/components/kbd/kbd.tsx +0 -43
  43. package/src/components/label/index.ts +0 -2
  44. package/src/components/label/label.tsx +0 -99
  45. package/src/components/lightbox/index.ts +0 -24
  46. package/src/components/lightbox/lightbox.tsx +0 -245
  47. package/src/components/list-item/index.ts +0 -20
  48. package/src/components/list-item/list-item.tsx +0 -263
  49. package/src/components/media-list-item/index.ts +0 -20
  50. package/src/components/media-list-item/media-list-item.tsx +0 -169
  51. package/src/components/notification-center/index.ts +0 -39
  52. package/src/components/notification-center/notification-center.tsx +0 -558
  53. package/src/components/panel-header/index.ts +0 -22
  54. package/src/components/panel-header/panel-header.tsx +0 -199
  55. package/src/components/popover/index.ts +0 -30
  56. package/src/components/popover/popover.tsx +0 -248
  57. package/src/components/preview-shell/index.ts +0 -2
  58. package/src/components/preview-shell/preview-shell.tsx +0 -51
  59. package/src/components/quoted-message-card/index.ts +0 -36
  60. package/src/components/quoted-message-card/quoted-message-card.tsx +0 -391
  61. package/src/components/radio-group/index.ts +0 -2
  62. package/src/components/radio-group/radio-group.tsx +0 -119
  63. package/src/components/section-header/index.ts +0 -16
  64. package/src/components/section-header/section-header.tsx +0 -117
  65. package/src/components/section-label/index.ts +0 -2
  66. package/src/components/section-label/section-label.tsx +0 -38
  67. package/src/components/segmented-control/index.ts +0 -12
  68. package/src/components/segmented-control/segmented-control.tsx +0 -129
  69. package/src/components/select/index.ts +0 -40
  70. package/src/components/select/select.tsx +0 -382
  71. package/src/components/separator/index.ts +0 -2
  72. package/src/components/separator/separator.tsx +0 -40
  73. package/src/components/spinner/index.ts +0 -2
  74. package/src/components/spinner/spinner.tsx +0 -228
  75. package/src/components/status/index.ts +0 -2
  76. package/src/components/status/status.tsx +0 -152
  77. package/src/components/tabs/index.ts +0 -24
  78. package/src/components/tabs/tabs.tsx +0 -451
  79. package/src/components/text/index.ts +0 -2
  80. package/src/components/text/text.tsx +0 -131
  81. package/src/components/textarea/index.ts +0 -2
  82. package/src/components/textarea/textarea.tsx +0 -136
  83. package/src/components/toggle-group/index.ts +0 -7
  84. package/src/components/toggle-group/toggle-group.tsx +0 -111
  85. package/src/components/tooltip/index.ts +0 -8
  86. package/src/components/tooltip/tooltip.tsx +0 -212
  87. package/src/index.ts +0 -429
  88. package/src/legacy/AttentionDot.tsx +0 -28
  89. package/src/legacy/AvatarListRow.tsx +0 -87
  90. package/src/legacy/AvatarSlot.tsx +0 -157
  91. package/src/legacy/Badge.tsx +0 -95
  92. package/src/legacy/Banner.tsx +0 -74
  93. package/src/legacy/BottomSheet.tsx +0 -76
  94. package/src/legacy/CheckMarker.tsx +0 -72
  95. package/src/legacy/Checkbox.tsx +0 -81
  96. package/src/legacy/ConversationPreviewCard.tsx +0 -169
  97. package/src/legacy/DismissBackdrop.tsx +0 -68
  98. package/src/legacy/EmptyState.tsx +0 -63
  99. package/src/legacy/ExperimentalBadge.tsx +0 -19
  100. package/src/legacy/FormField.tsx +0 -106
  101. package/src/legacy/KeyValueRow.tsx +0 -67
  102. package/src/legacy/Lightbox.tsx +0 -119
  103. package/src/legacy/NotificationCenter.tsx +0 -217
  104. package/src/legacy/PanelHeader.tsx +0 -94
  105. package/src/legacy/PreviewShell.tsx +0 -40
  106. package/src/legacy/QuotedMessageCard.tsx +0 -181
  107. package/src/legacy/ReorderablePanelTabs.tsx +0 -156
  108. package/src/legacy/SandboxedPreviewFrame.tsx +0 -59
  109. package/src/legacy/SearchInput.tsx +0 -23
  110. package/src/legacy/SectionEyebrow.tsx +0 -60
  111. package/src/legacy/SectionHeader.tsx +0 -68
  112. package/src/legacy/SegmentedControl.tsx +0 -74
  113. package/src/legacy/SlugInput.tsx +0 -29
  114. package/src/legacy/Spinner.tsx +0 -89
  115. package/src/legacy/StatusDot.tsx +0 -51
  116. package/src/legacy/SurfaceListItem.tsx +0 -32
  117. package/src/legacy/Textarea.tsx +0 -137
  118. package/src/legacy/button.tsx +0 -100
  119. package/src/legacy/context-menu-divider.tsx +0 -12
  120. package/src/legacy/menu-item.tsx +0 -66
  121. package/src/legacy/selection-popover.tsx +0 -212
  122. package/src/legacy/server-switcher-menu.tsx +0 -203
  123. package/src/legacy/source.css +0 -240
  124. package/src/lib/cn.ts +0 -1
  125. package/src/lib/styled-props.ts +0 -11
  126. package/src/lib/theme/theme-context.ts +0 -28
  127. package/src/lib/theme/theme-provider.tsx +0 -181
  128. package/src/lib/theme/use-theme.ts +0 -12
  129. package/src/lib/tv.ts +0 -29
  130. package/src/lib/without-children.ts +0 -6
  131. package/src/wip.ts +0 -291
@@ -1,203 +0,0 @@
1
- import { useEffect, useRef, type ComponentPropsWithRef, type ReactNode } from "react";
2
- import { Button as BaseButton } from "@base-ui/react/button";
3
- import { Check, Plus } from "lucide-react";
4
- import { tv } from "../lib/tv";
5
- import { useThemeFamily } from "../lib/theme/use-theme";
6
-
7
- // "Switch server" dropdown content: joined servers + join-community actions + a
8
- // "switch or create" action. App-domain data and navigation stay caller-owned,
9
- // but the source menu's UI primitive contract (open gate, outside click, ESC,
10
- // current unread hiding) is preserved here. `card-brutal` maps to border-2
11
- // border-black bg-white --shadow-hover.
12
- const serverSwitcherMenu = tv({
13
- slots: {
14
- root: ["z-50 flex flex-col overflow-hidden"],
15
- scroller: ["min-h-0 flex-1 overflow-y-auto overscroll-contain"],
16
- serverRow: [
17
- "flex w-full items-center gap-2 px-3 py-2 [@media(max-height:600px)]:py-1",
18
- "text-sm transition-colors",
19
- ],
20
- check: ["shrink-0"],
21
- serverText: ["min-w-0 flex-1 text-left"],
22
- serverName: ["truncate"],
23
- serverSlug: ["truncate font-mono text-xs text-black/40"],
24
- unread: [
25
- "ml-auto shrink-0 rounded border border-black bg-brutal-pink px-1.5 py-0.5",
26
- "text-[10px] font-bold leading-none text-white",
27
- ],
28
- actionRow: [
29
- "flex w-full items-center justify-start gap-2 px-3 py-2 [@media(max-height:600px)]:py-1",
30
- "text-left text-sm transition-colors",
31
- ],
32
- actionLabel: ["min-w-0 flex-1 text-left"],
33
- },
34
- variants: {
35
- theme: {
36
- brutal: {
37
- root: ["border-2 border-black bg-white shadow-[var(--shadow-hover)]"],
38
- serverRow: ["font-medium text-black hover:bg-brutal-yellow"],
39
- actionRow: ["font-bold text-black hover:bg-brutal-pink"],
40
- },
41
- // Elegant styling is deferred; see the component TODO.
42
- elegant: {},
43
- },
44
- current: {
45
- true: {
46
- serverRow: "font-bold",
47
- check: "opacity-100",
48
- },
49
- false: {
50
- serverRow: "",
51
- check: "opacity-0",
52
- },
53
- },
54
- },
55
- defaultVariants: {
56
- current: false,
57
- },
58
- });
59
-
60
- export type ServerSwitcherServer = {
61
- id: string;
62
- name: ReactNode;
63
- slug: string;
64
- };
65
-
66
- export type ServerSwitcherJoinOption = {
67
- slug: string;
68
- label: ReactNode;
69
- };
70
-
71
- export type ServerSwitcherMenuProps = Omit<ComponentPropsWithRef<"div">, "children"> & {
72
- /** Matches the source menu's open gate. Omit to render as plain composed content. */
73
- open?: boolean;
74
- /** Called by outside click, ESC, server select, and switch/create. */
75
- onClose?: () => void;
76
- servers: ServerSwitcherServer[];
77
- currentServerId?: string;
78
- /** Unread counts keyed by server id. The current server hides its own count. */
79
- serverUnreadCounts?: Record<string, number>;
80
- onSelectServer: (server: ServerSwitcherServer) => void;
81
- /** Community join actions, rendered above "Switch or create server". */
82
- joinOptions?: ServerSwitcherJoinOption[];
83
- onJoinCommunity?: (slug: string) => void;
84
- onSwitchOrCreate?: () => void;
85
- testId?: string;
86
- };
87
-
88
- export function ServerSwitcherMenu({
89
- servers,
90
- currentServerId,
91
- serverUnreadCounts = {},
92
- onSelectServer,
93
- joinOptions = [],
94
- onJoinCommunity,
95
- onSwitchOrCreate,
96
- open = true,
97
- onClose,
98
- className,
99
- testId = "server-switcher-menu",
100
- onMouseDown,
101
- ...props
102
- }: ServerSwitcherMenuProps) {
103
- const theme = useThemeFamily();
104
- const menuRef = useRef<HTMLDivElement | null>(null);
105
- const onCloseRef = useRef(onClose);
106
- const { root, scroller, actionRow, actionLabel } = serverSwitcherMenu({ theme });
107
-
108
- useEffect(() => {
109
- onCloseRef.current = onClose;
110
- }, [onClose]);
111
-
112
- useEffect(() => {
113
- if (!open || !onClose) return;
114
- const handleMouseDown = (event: MouseEvent) => {
115
- if (menuRef.current?.contains(event.target as Node)) return;
116
- onCloseRef.current?.();
117
- };
118
- const handleKeyDown = (event: KeyboardEvent) => {
119
- if (event.key === "Escape") onCloseRef.current?.();
120
- };
121
- document.addEventListener("mousedown", handleMouseDown);
122
- document.addEventListener("keydown", handleKeyDown);
123
- return () => {
124
- document.removeEventListener("mousedown", handleMouseDown);
125
- document.removeEventListener("keydown", handleKeyDown);
126
- };
127
- }, [open, onClose]);
128
-
129
- if (!open) return null;
130
-
131
- return (
132
- <div
133
- {...props}
134
- ref={menuRef}
135
- data-slot="server-switcher-menu"
136
- data-testid={testId}
137
- className={root({ className })}
138
- onMouseDown={(event) => {
139
- onMouseDown?.(event);
140
- event.stopPropagation();
141
- }}
142
- >
143
- <div className={scroller()}>
144
- {servers.map((server) => {
145
- const isCurrent = server.id === currentServerId;
146
- const unread = serverUnreadCounts[server.id] || 0;
147
- const {
148
- serverRow: serverRowSlot,
149
- check,
150
- serverText,
151
- serverName,
152
- serverSlug,
153
- unread: unreadSlot,
154
- } = serverSwitcherMenu({ theme, current: isCurrent });
155
- return (
156
- <BaseButton
157
- key={server.id}
158
- type="button"
159
- onClick={() => {
160
- onSelectServer(server);
161
- onClose?.();
162
- }}
163
- className={serverRowSlot()}
164
- >
165
- <Check size={14} className={check()} />
166
- <div className={serverText()}>
167
- <div className={serverName()}>{server.name}</div>
168
- <div className={serverSlug()}>/{server.slug}</div>
169
- </div>
170
- {!isCurrent && unread > 0 && <span className={unreadSlot()}>{unread}</span>}
171
- </BaseButton>
172
- );
173
- })}
174
- </div>
175
-
176
- <div className="border-t-2 border-black" />
177
-
178
- {joinOptions.map((option) => (
179
- <BaseButton
180
- key={option.slug}
181
- type="button"
182
- onClick={() => onJoinCommunity?.(option.slug)}
183
- className={actionRow()}
184
- >
185
- <Plus size={14} />
186
- <span className={actionLabel()}>{option.label}</span>
187
- </BaseButton>
188
- ))}
189
-
190
- <BaseButton
191
- type="button"
192
- onClick={() => {
193
- onClose?.();
194
- onSwitchOrCreate?.();
195
- }}
196
- className={actionRow()}
197
- >
198
- <Plus size={14} />
199
- <span className={actionLabel()}>Switch or Create Server</span>
200
- </BaseButton>
201
- </div>
202
- );
203
- }
@@ -1,240 +0,0 @@
1
- .btn-brutal,
2
- .btn-brutal-sm,
3
- .card-brutal,
4
- .input-brutal {
5
- --shadow: 2px 2px 0px var(--color-black);
6
- --shadow-hover: 4px 4px 0px var(--color-black);
7
- --shadow-active: 1px 1px 0px var(--color-black);
8
- --shadow-lg: 6px 6px 0px var(--color-black);
9
- }
10
-
11
- .card-brutal {
12
- border: 2px solid var(--color-black);
13
- background-color: var(--color-white);
14
- box-shadow: var(--shadow-hover);
15
- }
16
-
17
- .btn-brutal {
18
- border: 2px solid var(--color-black);
19
- font-family: var(--heading-font);
20
- font-weight: 700;
21
- box-shadow: var(--shadow-hover);
22
- transition-property: all;
23
- transition-duration: 100ms;
24
- }
25
-
26
- .btn-brutal:hover {
27
- transform: translateY(-1px);
28
- box-shadow: var(--shadow-lg);
29
- }
30
-
31
- .btn-brutal:active {
32
- transform: translate(2px, 2px);
33
- box-shadow: var(--shadow-active);
34
- }
35
-
36
- .btn-brutal:disabled {
37
- pointer-events: none;
38
- cursor: not-allowed;
39
- opacity: 0.5;
40
- }
41
-
42
- .btn-brutal-sm {
43
- border: 2px solid var(--color-black);
44
- font-family: var(--heading-font);
45
- font-weight: 700;
46
- box-shadow: var(--shadow);
47
- transition-property: all;
48
- transition-duration: 100ms;
49
- }
50
-
51
- .btn-brutal-sm:hover {
52
- transform: translateY(-1px);
53
- box-shadow: var(--shadow-hover);
54
- }
55
-
56
- .btn-brutal-sm:active {
57
- transform: translate(1px, 1px);
58
- box-shadow: var(--shadow-active);
59
- }
60
-
61
- .btn-brutal-sm:disabled {
62
- pointer-events: none;
63
- opacity: 0.4;
64
- box-shadow: var(--shadow);
65
- transform: none;
66
- }
67
-
68
- .shadow-brutal-sm {
69
- box-shadow: var(--shadow);
70
- }
71
-
72
- .hover\:shadow-brutal-sm:hover {
73
- box-shadow: var(--shadow);
74
- }
75
-
76
- .active\:shadow-brutal-sm:active {
77
- box-shadow: var(--shadow);
78
- }
79
-
80
- .shadow-brutal {
81
- box-shadow: var(--shadow-hover);
82
- }
83
-
84
- .shadow-brutal-lg {
85
- box-shadow: var(--shadow-lg);
86
- }
87
-
88
- .focus-within\:shadow-brutal:focus-within {
89
- box-shadow: var(--shadow-hover);
90
- }
91
-
92
- .scrollbar-none {
93
- scrollbar-width: none;
94
- }
95
-
96
- .scrollbar-none::-webkit-scrollbar {
97
- display: none;
98
- }
99
-
100
- .h-panel-header {
101
- height: 62px;
102
- }
103
-
104
- .size-icon-header {
105
- width: 36px;
106
- height: 36px;
107
- }
108
-
109
- @layer components {
110
- .input-brutal {
111
- border: 2px solid var(--color-black);
112
- background-color: var(--color-white);
113
- padding: 0.5rem 0.75rem;
114
- font-family: var(--heading-font);
115
- box-shadow: var(--shadow);
116
- }
117
-
118
- .input-brutal:focus {
119
- outline: none;
120
- box-shadow: var(--shadow-hover);
121
- }
122
- }
123
-
124
- @layer utilities {
125
- .border-brutal-red {
126
- border-color: var(--color-brutal-red-400);
127
- }
128
-
129
- .border-brutal-yellow {
130
- border-color: var(--color-brutal-yellow-400);
131
- }
132
-
133
- .ring-brutal-red\/60 {
134
- --tw-ring-color: oklch(0.711 0.1682 28.04 / 0.6);
135
- }
136
-
137
- .bg-brutal-yellow {
138
- background-color: var(--color-brutal-yellow-400);
139
- }
140
-
141
- .bg-brutal-yellow\/30 {
142
- background-color: oklch(0.8829 0.1623 91.9 / 0.3);
143
- }
144
-
145
- .bg-brutal-yellow\/45 {
146
- background-color: oklch(0.8829 0.1623 91.9 / 0.45);
147
- }
148
-
149
- .bg-brutal-pink {
150
- background-color: var(--color-brutal-pink-400);
151
- }
152
-
153
- .bg-brutal-pink\/35 {
154
- background-color: oklch(0.7489 0.1623 0.7 / 0.35);
155
- }
156
-
157
- .bg-brutal-cyan {
158
- background-color: var(--color-brutal-cyan-400);
159
- }
160
-
161
- .bg-brutal-cyan\/15 {
162
- background-color: oklch(0.7833 0.1353 219.21 / 0.15);
163
- }
164
-
165
- .bg-brutal-cyan\/35 {
166
- background-color: oklch(0.7833 0.1353 219.21 / 0.35);
167
- }
168
-
169
- .bg-brutal-lavender {
170
- background-color: var(--color-brutal-purple-300);
171
- }
172
-
173
- .bg-brutal-lavender\/30 {
174
- background-color: oklch(0.783 0.0783 294.53 / 0.3);
175
- }
176
-
177
- .bg-brutal-lavender\/45 {
178
- background-color: oklch(0.783 0.0783 294.53 / 0.45);
179
- }
180
-
181
- .bg-brutal-orange {
182
- background-color: var(--color-brutal-orange-400);
183
- }
184
-
185
- .bg-brutal-orange\/20 {
186
- background-color: oklch(0.7855 0.1227 50.11 / 0.2);
187
- }
188
-
189
- .bg-brutal-orange\/30 {
190
- background-color: oklch(0.7855 0.1227 50.11 / 0.3);
191
- }
192
-
193
- .bg-brutal-orange\/40 {
194
- background-color: oklch(0.7855 0.1227 50.11 / 0.4);
195
- }
196
-
197
- .bg-brutal-lime {
198
- background-color: var(--color-brutal-lime-400);
199
- }
200
-
201
- .bg-brutal-lime\/20 {
202
- background-color: oklch(0.8272 0.1354 130.07 / 0.2);
203
- }
204
-
205
- .bg-brutal-lime\/45 {
206
- background-color: oklch(0.8272 0.1354 130.07 / 0.45);
207
- }
208
-
209
- .bg-brutal-red {
210
- background-color: var(--color-brutal-red-400);
211
- }
212
-
213
- .bg-brutal-red\/5 {
214
- background-color: oklch(0.711 0.1682 28.04 / 0.05);
215
- }
216
-
217
- .bg-brutal-red\/20 {
218
- background-color: oklch(0.711 0.1682 28.04 / 0.2);
219
- }
220
-
221
- .bg-brutal-stone {
222
- background-color: var(--color-brutal-stone-400);
223
- }
224
-
225
- .bg-brutal-cream {
226
- background-color: var(--color-brutal-cream-100);
227
- }
228
-
229
- .text-brutal-pink {
230
- color: var(--color-brutal-pink-400);
231
- }
232
-
233
- .text-brutal-red {
234
- color: var(--color-brutal-red-400);
235
- }
236
-
237
- .text-brutal-yellow {
238
- color: var(--color-brutal-yellow-400);
239
- }
240
- }
package/src/lib/cn.ts DELETED
@@ -1 +0,0 @@
1
- export { cn } from "cnfast";
@@ -1,11 +0,0 @@
1
- /**
2
- * Base UI components type `className` as `string | ((state) => string)`, but our
3
- * tv() recipes only accept a plain string. `StyledProps` narrows a Base UI
4
- * component's props to a string `className` so they can flow into a recipe.
5
- *
6
- * The state-callback form is intentionally dropped: our components express every
7
- * state through `data-*` variants in the recipe, so a caller never needs it.
8
- */
9
- export type StyledProps<P> = Omit<P, "className"> & {
10
- className?: string;
11
- };
@@ -1,28 +0,0 @@
1
- import { createContext } from "react";
2
-
3
- /** Visual style family. Mode (light/dark/system) is a separate axis. */
4
- export type Theme = "brutal" | "elegant";
5
- export type ThemeMode = "light" | "dark" | "system";
6
- export type ResolvedThemeMode = Exclude<ThemeMode, "system">;
7
- export type SetThemeOptions = {
8
- mode?: ThemeMode;
9
- };
10
- export type SetTheme = {
11
- (theme: "brutal"): void;
12
- (theme: "elegant", options?: SetThemeOptions): void;
13
- };
14
-
15
- export type ThemeState = {
16
- theme: Theme;
17
- setTheme: SetTheme;
18
- mode: ThemeMode;
19
- resolvedMode: ResolvedThemeMode;
20
- };
21
-
22
- export const ThemeContext = createContext<Theme>("brutal");
23
- export const ThemeStateContext = createContext<ThemeState>({
24
- theme: "brutal",
25
- setTheme: () => undefined,
26
- mode: "light",
27
- resolvedMode: "light",
28
- });
@@ -1,181 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- useCallback,
5
- useEffect,
6
- useMemo,
7
- useState,
8
- useSyncExternalStore,
9
- type ReactNode,
10
- } from "react";
11
- import {
12
- ThemeContext,
13
- ThemeStateContext,
14
- type ResolvedThemeMode,
15
- type SetTheme,
16
- type SetThemeOptions,
17
- type Theme,
18
- type ThemeState,
19
- type ThemeMode,
20
- } from "./theme-context";
21
-
22
- export interface ThemeProviderProps {
23
- /**
24
- * Controlled visual style family. Keep using this for isolated previews and
25
- * component matrix surfaces that should not own global DOM state.
26
- */
27
- theme?: Theme;
28
- defaultTheme?: Theme;
29
- onThemeChange?: (theme: Theme) => void;
30
- /** Controlled light/dark behavior for the elegant family. */
31
- mode?: ThemeMode;
32
- defaultMode?: ThemeMode;
33
- onModeChange?: (mode: ThemeMode) => void;
34
- /**
35
- * Persist uncontrolled family and mode selections. The mode key appends
36
- * "-mode" unless `modeStorageKey` is provided.
37
- */
38
- storageKey?: string;
39
- modeStorageKey?: string;
40
- /**
41
- * Sync the active family/mode onto `document.documentElement`.
42
- * Defaults to true for app-level providers. Set to false for nested previews
43
- * or component islands that should not mutate the app root.
44
- */
45
- syncDom?: boolean;
46
- children: ReactNode;
47
- }
48
-
49
- function readStoredTheme(storageKey: string | undefined, fallback: Theme): Theme {
50
- if (typeof window === "undefined" || !storageKey) {
51
- return fallback;
52
- }
53
-
54
- const stored = window.localStorage.getItem(storageKey);
55
- return stored === "brutal" || stored === "elegant" ? stored : fallback;
56
- }
57
-
58
- function readStoredMode(storageKey: string | undefined, fallback: ThemeMode): ThemeMode {
59
- if (typeof window === "undefined" || !storageKey) {
60
- return fallback;
61
- }
62
-
63
- const stored = window.localStorage.getItem(storageKey);
64
- return stored === "light" || stored === "dark" || stored === "system" ? stored : fallback;
65
- }
66
-
67
- function writeStorage(key: string | undefined, value: string) {
68
- if (typeof window !== "undefined" && key) {
69
- window.localStorage.setItem(key, value);
70
- }
71
- }
72
-
73
- function subscribeSystemMode(onStoreChange: () => void) {
74
- if (typeof window === "undefined") {
75
- return () => undefined;
76
- }
77
-
78
- const media = window.matchMedia("(prefers-color-scheme: dark)");
79
- media.addEventListener("change", onStoreChange);
80
- return () => media.removeEventListener("change", onStoreChange);
81
- }
82
-
83
- function getSystemMode(): ResolvedThemeMode {
84
- if (typeof window === "undefined") {
85
- return "light";
86
- }
87
-
88
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
89
- }
90
-
91
- export function ThemeProvider({
92
- theme,
93
- defaultTheme = "brutal",
94
- onThemeChange,
95
- mode,
96
- defaultMode = "light",
97
- onModeChange,
98
- storageKey,
99
- modeStorageKey = storageKey ? `${storageKey}-mode` : undefined,
100
- syncDom = true,
101
- children,
102
- }: ThemeProviderProps) {
103
- const [uncontrolledTheme, setUncontrolledTheme] = useState<Theme>(() =>
104
- readStoredTheme(storageKey, defaultTheme),
105
- );
106
- const [uncontrolledMode, setUncontrolledMode] = useState<ThemeMode>(() =>
107
- readStoredMode(modeStorageKey, defaultMode),
108
- );
109
- const activeTheme = theme ?? uncontrolledTheme;
110
- const activeMode = mode ?? uncontrolledMode;
111
- const systemMode = useSyncExternalStore(
112
- subscribeSystemMode,
113
- getSystemMode,
114
- () => "light" as ResolvedThemeMode,
115
- );
116
- const resolvedMode: ResolvedThemeMode = activeMode === "system" ? systemMode : activeMode;
117
-
118
- const commitTheme = useCallback(
119
- (nextTheme: Theme) => {
120
- if (theme === undefined) {
121
- setUncontrolledTheme(nextTheme);
122
- writeStorage(storageKey, nextTheme);
123
- }
124
- onThemeChange?.(nextTheme);
125
- },
126
- [onThemeChange, storageKey, theme],
127
- );
128
-
129
- const commitMode = useCallback(
130
- (nextMode: ThemeMode) => {
131
- if (mode === undefined) {
132
- setUncontrolledMode(nextMode);
133
- writeStorage(modeStorageKey, nextMode);
134
- }
135
- onModeChange?.(nextMode);
136
- },
137
- [mode, modeStorageKey, onModeChange],
138
- );
139
-
140
- const setTheme = useCallback<SetTheme>(
141
- (nextTheme: Theme, options?: SetThemeOptions) => {
142
- commitTheme(nextTheme);
143
- if (nextTheme === "brutal") {
144
- commitMode("light");
145
- return;
146
- }
147
- if (options?.mode) {
148
- commitMode(options.mode);
149
- }
150
- },
151
- [commitMode, commitTheme],
152
- );
153
-
154
- const themeState = useMemo<ThemeState>(
155
- () => ({
156
- theme: activeTheme,
157
- setTheme,
158
- mode: activeMode,
159
- resolvedMode,
160
- }),
161
- [activeMode, activeTheme, resolvedMode, setTheme],
162
- );
163
-
164
- useEffect(() => {
165
- if (!syncDom) {
166
- return;
167
- }
168
-
169
- const root = document.documentElement;
170
- root.dataset.theme = activeTheme;
171
- root.classList.toggle("light", activeTheme === "elegant" && activeMode === "light");
172
- root.classList.toggle("dark", activeTheme === "elegant" && activeMode === "dark");
173
- root.style.colorScheme = activeTheme === "elegant" ? resolvedMode : "light";
174
- }, [activeMode, activeTheme, resolvedMode, syncDom]);
175
-
176
- return (
177
- <ThemeStateContext.Provider value={themeState}>
178
- <ThemeContext.Provider value={activeTheme}>{children}</ThemeContext.Provider>
179
- </ThemeStateContext.Provider>
180
- );
181
- }
@@ -1,12 +0,0 @@
1
- import { use } from "react";
2
- import { ThemeContext, ThemeStateContext, type Theme, type ThemeState } from "./theme-context";
3
-
4
- /** Read the active style family for theme-aware component recipes. */
5
- export function useThemeFamily(): Theme {
6
- return use(ThemeContext);
7
- }
8
-
9
- /** Read and update the application-level RUI theme selection. */
10
- export function useTheme(): ThemeState {
11
- return use(ThemeStateContext);
12
- }
package/src/lib/tv.ts DELETED
@@ -1,29 +0,0 @@
1
- import { cn } from "cnfast";
2
- import { tv as tvBase, type TVLite } from "tailwind-variants/lite";
3
-
4
- export type { VariantProps } from "tailwind-variants/lite";
5
-
6
- type SlotFunction = (props?: unknown) => string;
7
- type SlotRecord = Record<string, SlotFunction>;
8
- type RecipeFunction = ((props?: unknown) => string | SlotRecord) & Record<string, unknown>;
9
-
10
- export const tv: TVLite = ((options: unknown) => {
11
- const recipe = tvBase(options as never) as unknown as RecipeFunction;
12
-
13
- const wrappedRecipe = ((props?: unknown) => {
14
- const result = recipe(props);
15
-
16
- if (typeof result === "string") {
17
- return cn(result);
18
- }
19
-
20
- return Object.fromEntries(
21
- Object.entries(result).map(([slotName, slot]) => [
22
- slotName,
23
- (slotProps?: unknown) => cn(slot(slotProps)),
24
- ]),
25
- );
26
- }) as RecipeFunction;
27
-
28
- return Object.assign(wrappedRecipe, recipe);
29
- }) as unknown as TVLite;