raft-ui 0.0.6 → 0.0.8

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 (138) hide show
  1. package/CHANGELOG.md +7 -5
  2. package/dist/index.d.mts +618 -2
  3. package/dist/index.mjs +1205 -12
  4. package/{src → dist}/styles.css +0 -2
  5. package/{src/lib/styled-props.ts → dist/tv-UUMwNowu.d.mts} +6 -1
  6. package/dist/use-theme-Y_h1DKll.mjs +34 -0
  7. package/dist/wip.d.mts +15 -133
  8. package/dist/wip.mjs +23 -296
  9. package/package.json +4 -4
  10. package/dist/button-CJqMhTPQ.mjs +0 -621
  11. package/dist/index-CpGciYPW.d.mts +0 -267
  12. package/src/components/alert-dialog/alert-dialog.tsx +0 -215
  13. package/src/components/alert-dialog/index.ts +0 -30
  14. package/src/components/avatar/avatar.tsx +0 -375
  15. package/src/components/avatar/index.ts +0 -17
  16. package/src/components/badge/badge.tsx +0 -393
  17. package/src/components/badge/index.ts +0 -2
  18. package/src/components/banner/banner.tsx +0 -269
  19. package/src/components/banner/index.ts +0 -8
  20. package/src/components/bottom-sheet/bottom-sheet.tsx +0 -152
  21. package/src/components/bottom-sheet/index.ts +0 -22
  22. package/src/components/button/button.tsx +0 -499
  23. package/src/components/button/index.ts +0 -2
  24. package/src/components/checkbox/checkbox-indicator.tsx +0 -128
  25. package/src/components/checkbox/checkbox.tsx +0 -350
  26. package/src/components/checkbox/index.ts +0 -2
  27. package/src/components/combobox/combobox.tsx +0 -603
  28. package/src/components/combobox/index.ts +0 -52
  29. package/src/components/context-menu/context-menu.tsx +0 -380
  30. package/src/components/context-menu/index.ts +0 -34
  31. package/src/components/conversation-preview-card/conversation-preview-card.tsx +0 -336
  32. package/src/components/conversation-preview-card/index.ts +0 -32
  33. package/src/components/description-list/description-list.tsx +0 -116
  34. package/src/components/description-list/index.ts +0 -14
  35. package/src/components/dialog/dialog.tsx +0 -152
  36. package/src/components/dialog/index.ts +0 -26
  37. package/src/components/dropdown-menu/dropdown-menu.tsx +0 -405
  38. package/src/components/dropdown-menu/index.ts +0 -38
  39. package/src/components/empty-state/empty-state.tsx +0 -177
  40. package/src/components/empty-state/index.ts +0 -16
  41. package/src/components/field/field.tsx +0 -125
  42. package/src/components/field/index.ts +0 -18
  43. package/src/components/inline-code/index.ts +0 -2
  44. package/src/components/inline-code/inline-code.tsx +0 -35
  45. package/src/components/input/index.ts +0 -2
  46. package/src/components/input/input.tsx +0 -76
  47. package/src/components/input-group/index.ts +0 -7
  48. package/src/components/input-group/input-group.tsx +0 -215
  49. package/src/components/kbd/index.ts +0 -2
  50. package/src/components/kbd/kbd.tsx +0 -43
  51. package/src/components/label/index.ts +0 -2
  52. package/src/components/label/label.tsx +0 -99
  53. package/src/components/lightbox/index.ts +0 -24
  54. package/src/components/lightbox/lightbox.tsx +0 -245
  55. package/src/components/list-item/index.ts +0 -20
  56. package/src/components/list-item/list-item.tsx +0 -263
  57. package/src/components/media-list-item/index.ts +0 -20
  58. package/src/components/media-list-item/media-list-item.tsx +0 -169
  59. package/src/components/notification-center/index.ts +0 -39
  60. package/src/components/notification-center/notification-center.tsx +0 -558
  61. package/src/components/panel-header/index.ts +0 -22
  62. package/src/components/panel-header/panel-header.tsx +0 -199
  63. package/src/components/popover/index.ts +0 -30
  64. package/src/components/popover/popover.tsx +0 -248
  65. package/src/components/preview-shell/index.ts +0 -2
  66. package/src/components/preview-shell/preview-shell.tsx +0 -51
  67. package/src/components/quoted-message-card/index.ts +0 -36
  68. package/src/components/quoted-message-card/quoted-message-card.tsx +0 -391
  69. package/src/components/radio-group/index.ts +0 -2
  70. package/src/components/radio-group/radio-group.tsx +0 -119
  71. package/src/components/section-header/index.ts +0 -16
  72. package/src/components/section-header/section-header.tsx +0 -117
  73. package/src/components/section-label/index.ts +0 -2
  74. package/src/components/section-label/section-label.tsx +0 -38
  75. package/src/components/segmented-control/index.ts +0 -12
  76. package/src/components/segmented-control/segmented-control.tsx +0 -129
  77. package/src/components/select/index.ts +0 -40
  78. package/src/components/select/select.tsx +0 -382
  79. package/src/components/separator/index.ts +0 -2
  80. package/src/components/separator/separator.tsx +0 -40
  81. package/src/components/spinner/index.ts +0 -2
  82. package/src/components/spinner/spinner.tsx +0 -228
  83. package/src/components/status/index.ts +0 -2
  84. package/src/components/status/status.tsx +0 -152
  85. package/src/components/tabs/index.ts +0 -24
  86. package/src/components/tabs/tabs.tsx +0 -451
  87. package/src/components/text/index.ts +0 -2
  88. package/src/components/text/text.tsx +0 -131
  89. package/src/components/textarea/index.ts +0 -2
  90. package/src/components/textarea/textarea.tsx +0 -136
  91. package/src/components/toggle-group/index.ts +0 -7
  92. package/src/components/toggle-group/toggle-group.tsx +0 -111
  93. package/src/components/tooltip/index.ts +0 -8
  94. package/src/components/tooltip/tooltip.tsx +0 -212
  95. package/src/index.ts +0 -429
  96. package/src/legacy/AttentionDot.tsx +0 -28
  97. package/src/legacy/AvatarListRow.tsx +0 -87
  98. package/src/legacy/AvatarSlot.tsx +0 -157
  99. package/src/legacy/Badge.tsx +0 -95
  100. package/src/legacy/Banner.tsx +0 -74
  101. package/src/legacy/BottomSheet.tsx +0 -76
  102. package/src/legacy/CheckMarker.tsx +0 -72
  103. package/src/legacy/Checkbox.tsx +0 -81
  104. package/src/legacy/ConversationPreviewCard.tsx +0 -169
  105. package/src/legacy/DismissBackdrop.tsx +0 -68
  106. package/src/legacy/EmptyState.tsx +0 -63
  107. package/src/legacy/ExperimentalBadge.tsx +0 -19
  108. package/src/legacy/FormField.tsx +0 -106
  109. package/src/legacy/KeyValueRow.tsx +0 -67
  110. package/src/legacy/Lightbox.tsx +0 -119
  111. package/src/legacy/NotificationCenter.tsx +0 -217
  112. package/src/legacy/PanelHeader.tsx +0 -94
  113. package/src/legacy/PreviewShell.tsx +0 -40
  114. package/src/legacy/QuotedMessageCard.tsx +0 -181
  115. package/src/legacy/ReorderablePanelTabs.tsx +0 -156
  116. package/src/legacy/SandboxedPreviewFrame.tsx +0 -59
  117. package/src/legacy/SearchInput.tsx +0 -23
  118. package/src/legacy/SectionEyebrow.tsx +0 -60
  119. package/src/legacy/SectionHeader.tsx +0 -68
  120. package/src/legacy/SegmentedControl.tsx +0 -74
  121. package/src/legacy/SlugInput.tsx +0 -29
  122. package/src/legacy/Spinner.tsx +0 -89
  123. package/src/legacy/StatusDot.tsx +0 -51
  124. package/src/legacy/SurfaceListItem.tsx +0 -32
  125. package/src/legacy/Textarea.tsx +0 -137
  126. package/src/legacy/button.tsx +0 -100
  127. package/src/legacy/context-menu-divider.tsx +0 -12
  128. package/src/legacy/menu-item.tsx +0 -66
  129. package/src/legacy/selection-popover.tsx +0 -212
  130. package/src/legacy/server-switcher-menu.tsx +0 -203
  131. package/src/legacy/source.css +0 -240
  132. package/src/lib/cn.ts +0 -1
  133. package/src/lib/theme/theme-context.ts +0 -28
  134. package/src/lib/theme/theme-provider.tsx +0 -181
  135. package/src/lib/theme/use-theme.ts +0 -12
  136. package/src/lib/tv.ts +0 -29
  137. package/src/lib/without-children.ts +0 -6
  138. 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,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;
@@ -1,6 +0,0 @@
1
- /**
2
- * Element props without `children` — for leaf components (dots, markers) that
3
- * render no caller content. React ships `ComponentPropsWithoutRef` but no
4
- * equivalent that subtracts children, so this names the one-off `Omit`.
5
- */
6
- export type WithoutChildren<P> = Omit<P, "children">;