tembro 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/components/display/chat.cjs +1 -0
- package/dist/components/display/chat.d.ts +94 -0
- package/dist/components/display/chat.js +1 -0
- package/dist/components/display/index.d.ts +1 -0
- package/dist/components/display/kanban.d.ts +33 -1
- package/dist/components/layout/sidebar.d.ts +24 -2
- package/dist/components/modern/rich-text-editor.d.ts +13 -1
- package/dist/components/theme-provider.d.ts +19 -6
- package/dist/components/ui/badge/index.d.ts +8 -2
- package/dist/src/components/display/activity-feed.cjs +1 -1
- package/dist/src/components/display/activity-feed.js +4 -1
- package/dist/src/components/display/chat.cjs +1 -0
- package/dist/src/components/display/chat.js +372 -0
- package/dist/src/components/display/index.cjs +1 -1
- package/dist/src/components/display/index.js +17 -16
- package/dist/src/components/display/kanban.cjs +1 -1
- package/dist/src/components/display/kanban.js +321 -176
- package/dist/src/components/display/timeline.cjs +1 -1
- package/dist/src/components/display/timeline.js +5 -5
- package/dist/src/components/inputs/tag-input.cjs +1 -1
- package/dist/src/components/inputs/tag-input.js +6 -2
- package/dist/src/components/layout/index.cjs +1 -1
- package/dist/src/components/layout/index.js +4 -4
- package/dist/src/components/layout/public.cjs +1 -1
- package/dist/src/components/layout/public.js +4 -4
- package/dist/src/components/layout/sidebar.cjs +1 -1
- package/dist/src/components/layout/sidebar.js +326 -228
- package/dist/src/components/modern/rich-text-editor.cjs +1 -1
- package/dist/src/components/modern/rich-text-editor.js +216 -89
- package/dist/src/components/theme-provider.cjs +1 -1
- package/dist/src/components/theme-provider.js +58 -36
- package/dist/src/components/ui/badge/index.cjs +1 -1
- package/dist/src/components/ui/badge/index.js +59 -47
- package/dist/src/components/ui/input/clearable.cjs +1 -1
- package/dist/src/components/ui/input/clearable.js +5 -2
- package/dist/src/components/ui/input/primitive.cjs +1 -1
- package/dist/src/components/ui/input/primitive.js +1 -1
- package/dist/src/index.cjs +1 -1
- package/dist/src/index.js +87 -86
- package/dist/src/public-component-surface.cjs +1 -1
- package/dist/src/public-component-surface.js +5 -0
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +3 -3
- package/packages/cli/vendor/src/components/display/activity-feed.tsx +1 -1
- package/packages/cli/vendor/src/components/display/chat.tsx +237 -0
- package/packages/cli/vendor/src/components/display/index.ts +1 -0
- package/packages/cli/vendor/src/components/display/kanban.tsx +219 -72
- package/packages/cli/vendor/src/components/display/timeline.tsx +10 -10
- package/packages/cli/vendor/src/components/inputs/tag-input.tsx +3 -2
- package/packages/cli/vendor/src/components/layout/sidebar.tsx +212 -51
- package/packages/cli/vendor/src/components/modern/rich-text-editor.tsx +115 -32
- package/packages/cli/vendor/src/components/theme-provider.tsx +82 -36
- package/packages/cli/vendor/src/components/ui/badge/index.tsx +29 -8
- package/packages/cli/vendor/src/components/ui/input/clearable.tsx +2 -2
- package/packages/cli/vendor/src/components/ui/input/primitive.tsx +1 -1
- package/packages/cli/vendor/src/public-component-surface.ts +1 -0
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +2 -2
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
- package/packages/cli/vendor/templates/styles/globals.css +91 -10
- package/registry.json +3 -1
|
@@ -7,8 +7,13 @@ import Link from "@tiptap/extension-link"
|
|
|
7
7
|
import Placeholder from "@tiptap/extension-placeholder"
|
|
8
8
|
import {
|
|
9
9
|
BoldIcon,
|
|
10
|
-
|
|
10
|
+
Code2Icon,
|
|
11
|
+
EraserIcon,
|
|
12
|
+
Heading1Icon,
|
|
11
13
|
Heading2Icon,
|
|
14
|
+
Heading3Icon,
|
|
15
|
+
MinusIcon,
|
|
16
|
+
PilcrowIcon,
|
|
12
17
|
ItalicIcon,
|
|
13
18
|
LinkIcon,
|
|
14
19
|
ListIcon,
|
|
@@ -31,6 +36,14 @@ export type RichTextEditorProps = Omit<React.ComponentProps<"div">, "defaultValu
|
|
|
31
36
|
editable?: boolean
|
|
32
37
|
autoFocus?: boolean
|
|
33
38
|
minHeight?: number
|
|
39
|
+
maxHeight?: number
|
|
40
|
+
maxLength?: number
|
|
41
|
+
toolbar?: boolean
|
|
42
|
+
stickyToolbar?: boolean
|
|
43
|
+
toolbarSize?: "compact" | "default"
|
|
44
|
+
features?: RichTextFeature[]
|
|
45
|
+
showCharacterCount?: boolean
|
|
46
|
+
showWordCount?: boolean
|
|
34
47
|
onLinkRequest?: (currentHref?: string) => string | null | undefined
|
|
35
48
|
labels?: {
|
|
36
49
|
editor?: string
|
|
@@ -38,7 +51,32 @@ export type RichTextEditorProps = Omit<React.ComponentProps<"div">, "defaultValu
|
|
|
38
51
|
}
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
export type RichTextFeature =
|
|
55
|
+
| "history"
|
|
56
|
+
| "bold"
|
|
57
|
+
| "italic"
|
|
58
|
+
| "strike"
|
|
59
|
+
| "code"
|
|
60
|
+
| "heading1"
|
|
61
|
+
| "heading2"
|
|
62
|
+
| "heading3"
|
|
63
|
+
| "paragraph"
|
|
64
|
+
| "bulletList"
|
|
65
|
+
| "orderedList"
|
|
66
|
+
| "blockquote"
|
|
67
|
+
| "codeBlock"
|
|
68
|
+
| "horizontalRule"
|
|
69
|
+
| "link"
|
|
70
|
+
| "clearFormatting"
|
|
71
|
+
|
|
72
|
+
const defaultFeatures: RichTextFeature[] = [
|
|
73
|
+
"history", "bold", "italic", "strike", "code", "heading1", "heading2", "heading3", "paragraph",
|
|
74
|
+
"bulletList", "orderedList", "blockquote", "codeBlock", "horizontalRule", "link", "clearFormatting",
|
|
75
|
+
]
|
|
76
|
+
|
|
41
77
|
type ToolbarAction = {
|
|
78
|
+
feature: RichTextFeature
|
|
79
|
+
group: string
|
|
42
80
|
label: string
|
|
43
81
|
icon: React.ComponentType<{ className?: string }>
|
|
44
82
|
active?: boolean
|
|
@@ -54,23 +92,42 @@ function contentForEditor(value: string, output: "text" | "html") {
|
|
|
54
92
|
return output === "html" ? value : `<p>${escapeHtml(value)}</p>`
|
|
55
93
|
}
|
|
56
94
|
|
|
57
|
-
function EditorToolbar({ editor, onLinkRequest, label }: { editor: Editor; onLinkRequest?: RichTextEditorProps["onLinkRequest"]; label: string }) {
|
|
95
|
+
function EditorToolbar({ editor, onLinkRequest, label, features = defaultFeatures, sticky = false, size = "default" }: { editor: Editor; onLinkRequest?: RichTextEditorProps["onLinkRequest"]; label: string; features?: RichTextFeature[]; sticky?: boolean; size?: "compact" | "default" }) {
|
|
96
|
+
const [, refresh] = React.useReducer((value) => value + 1, 0)
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
const update = () => refresh()
|
|
99
|
+
editor.on("transaction", update)
|
|
100
|
+
editor.on("selectionUpdate", update)
|
|
101
|
+
return () => {
|
|
102
|
+
editor.off("transaction", update)
|
|
103
|
+
editor.off("selectionUpdate", update)
|
|
104
|
+
}
|
|
105
|
+
}, [editor])
|
|
106
|
+
|
|
58
107
|
const linkHref = editor.getAttributes("link").href as string | undefined
|
|
59
108
|
const actions: ToolbarAction[] = [
|
|
60
|
-
{ label: "Undo", icon: Undo2Icon, disabled: !editor.can().undo(), run: () => editor.chain().focus().undo().run() },
|
|
61
|
-
{ label: "Redo", icon: Redo2Icon, disabled: !editor.can().redo(), run: () => editor.chain().focus().redo().run() },
|
|
62
|
-
{ label: "Bold", icon: BoldIcon, active: editor.isActive("bold"), run: () => editor.chain().focus().toggleBold().run() },
|
|
63
|
-
{ label: "Italic", icon: ItalicIcon, active: editor.isActive("italic"), run: () => editor.chain().focus().toggleItalic().run() },
|
|
64
|
-
{ label: "Strike", icon: StrikethroughIcon, active: editor.isActive("strike"), run: () => editor.chain().focus().toggleStrike().run() },
|
|
65
|
-
{ label: "Inline code", icon:
|
|
66
|
-
{ label: "
|
|
67
|
-
{ label: "
|
|
68
|
-
{ label: "
|
|
69
|
-
{ label: "
|
|
109
|
+
{ feature: "history", group: "history", label: "Undo", icon: Undo2Icon, disabled: !editor.can().undo(), run: () => editor.chain().focus().undo().run() },
|
|
110
|
+
{ feature: "history", group: "history", label: "Redo", icon: Redo2Icon, disabled: !editor.can().redo(), run: () => editor.chain().focus().redo().run() },
|
|
111
|
+
{ feature: "bold", group: "marks", label: "Bold", icon: BoldIcon, active: editor.isActive("bold"), run: () => editor.chain().focus().toggleBold().run() },
|
|
112
|
+
{ feature: "italic", group: "marks", label: "Italic", icon: ItalicIcon, active: editor.isActive("italic"), run: () => editor.chain().focus().toggleItalic().run() },
|
|
113
|
+
{ feature: "strike", group: "marks", label: "Strike", icon: StrikethroughIcon, active: editor.isActive("strike"), run: () => editor.chain().focus().toggleStrike().run() },
|
|
114
|
+
{ feature: "code", group: "marks", label: "Inline code", icon: Code2Icon, active: editor.isActive("code"), run: () => editor.chain().focus().toggleCode().run() },
|
|
115
|
+
{ feature: "paragraph", group: "blocks", label: "Paragraph", icon: PilcrowIcon, active: editor.isActive("paragraph"), run: () => editor.chain().focus().setParagraph().run() },
|
|
116
|
+
{ feature: "heading1", group: "blocks", label: "Heading 1", icon: Heading1Icon, active: editor.isActive("heading", { level: 1 }), run: () => editor.chain().focus().toggleHeading({ level: 1 }).run() },
|
|
117
|
+
{ feature: "heading2", group: "blocks", label: "Heading 2", icon: Heading2Icon, active: editor.isActive("heading", { level: 2 }), run: () => editor.chain().focus().toggleHeading({ level: 2 }).run() },
|
|
118
|
+
{ feature: "heading3", group: "blocks", label: "Heading 3", icon: Heading3Icon, active: editor.isActive("heading", { level: 3 }), run: () => editor.chain().focus().toggleHeading({ level: 3 }).run() },
|
|
119
|
+
{ feature: "bulletList", group: "lists", label: "Bullet list", icon: ListIcon, active: editor.isActive("bulletList"), run: () => editor.chain().focus().toggleBulletList().run() },
|
|
120
|
+
{ feature: "orderedList", group: "lists", label: "Ordered list", icon: ListOrderedIcon, active: editor.isActive("orderedList"), run: () => editor.chain().focus().toggleOrderedList().run() },
|
|
121
|
+
{ feature: "blockquote", group: "lists", label: "Blockquote", icon: QuoteIcon, active: editor.isActive("blockquote"), run: () => editor.chain().focus().toggleBlockquote().run() },
|
|
122
|
+
{ feature: "codeBlock", group: "insert", label: "Code block", icon: Code2Icon, active: editor.isActive("codeBlock"), run: () => editor.chain().focus().toggleCodeBlock().run() },
|
|
123
|
+
{ feature: "horizontalRule", group: "insert", label: "Horizontal rule", icon: MinusIcon, run: () => editor.chain().focus().setHorizontalRule().run() },
|
|
124
|
+
{ feature: "clearFormatting", group: "clear", label: "Clear formatting", icon: EraserIcon, run: () => editor.chain().focus().unsetAllMarks().clearNodes().run() },
|
|
70
125
|
]
|
|
71
126
|
|
|
72
|
-
if (onLinkRequest) {
|
|
127
|
+
if (onLinkRequest && features.includes("link")) {
|
|
73
128
|
actions.push({
|
|
129
|
+
feature: "link",
|
|
130
|
+
group: "link",
|
|
74
131
|
label: "Set link",
|
|
75
132
|
icon: LinkIcon,
|
|
76
133
|
active: editor.isActive("link"),
|
|
@@ -84,26 +141,31 @@ function EditorToolbar({ editor, onLinkRequest, label }: { editor: Editor; onLin
|
|
|
84
141
|
}
|
|
85
142
|
|
|
86
143
|
if (editor.isActive("link")) {
|
|
87
|
-
actions.push({ label: "Remove link", icon: UnlinkIcon, run: () => editor.chain().focus().unsetLink().run() })
|
|
144
|
+
actions.push({ feature: "link", group: "link", label: "Remove link", icon: UnlinkIcon, run: () => editor.chain().focus().unsetLink().run() })
|
|
88
145
|
}
|
|
89
146
|
|
|
147
|
+
const visibleActions = actions.filter((action) => features.includes(action.feature))
|
|
148
|
+
|
|
90
149
|
return (
|
|
91
|
-
<div role="toolbar" aria-label={label} className="flex flex-wrap gap-1 border-b p-1.5">
|
|
92
|
-
{
|
|
150
|
+
<div role="toolbar" aria-label={label} data-size={size} data-sticky={sticky || undefined} className={cn("flex flex-wrap items-center gap-1 border-b bg-background/95 p-1.5", sticky && "sticky top-0 z-10 backdrop-blur", size === "compact" && "flex-nowrap overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden")}>
|
|
151
|
+
{visibleActions.map((action, index) => {
|
|
93
152
|
const Icon = action.icon
|
|
153
|
+
const showSeparator = index > 0 && visibleActions[index - 1]?.group !== action.group
|
|
94
154
|
return (
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
155
|
+
<React.Fragment key={action.label}>
|
|
156
|
+
{showSeparator ? <span role="separator" aria-orientation="vertical" className="mx-0.5 h-5 w-px shrink-0 bg-border" /> : null}
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
title={action.label}
|
|
160
|
+
aria-label={action.label}
|
|
161
|
+
aria-pressed={action.active || undefined}
|
|
162
|
+
disabled={action.disabled}
|
|
163
|
+
className={cn("grid size-8 shrink-0 place-items-center rounded-md p-0 text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-accent aria-pressed:text-foreground disabled:pointer-events-none disabled:opacity-40", size === "compact" && "size-7")}
|
|
164
|
+
onClick={action.run}
|
|
165
|
+
>
|
|
166
|
+
<Icon className="size-4" />
|
|
167
|
+
</button>
|
|
168
|
+
</React.Fragment>
|
|
107
169
|
)
|
|
108
170
|
})}
|
|
109
171
|
</div>
|
|
@@ -119,6 +181,14 @@ function MountedRichTextEditor({
|
|
|
119
181
|
editable = true,
|
|
120
182
|
autoFocus = false,
|
|
121
183
|
minHeight = 144,
|
|
184
|
+
maxHeight,
|
|
185
|
+
maxLength,
|
|
186
|
+
toolbar = true,
|
|
187
|
+
stickyToolbar = false,
|
|
188
|
+
toolbarSize = "default",
|
|
189
|
+
features = defaultFeatures,
|
|
190
|
+
showCharacterCount = false,
|
|
191
|
+
showWordCount = false,
|
|
122
192
|
onLinkRequest,
|
|
123
193
|
labels,
|
|
124
194
|
className,
|
|
@@ -145,6 +215,14 @@ function MountedRichTextEditor({
|
|
|
145
215
|
onValueChange?.(output === "html" ? nextEditor.getHTML() : nextEditor.getText())
|
|
146
216
|
},
|
|
147
217
|
})
|
|
218
|
+
const [, refreshCount] = React.useReducer((value) => value + 1, 0)
|
|
219
|
+
|
|
220
|
+
React.useEffect(() => {
|
|
221
|
+
if (!editor) return
|
|
222
|
+
const update = () => refreshCount()
|
|
223
|
+
editor.on("update", update)
|
|
224
|
+
return () => { editor.off("update", update) }
|
|
225
|
+
}, [editor])
|
|
148
226
|
|
|
149
227
|
React.useEffect(() => {
|
|
150
228
|
if (!editor || value === undefined) return
|
|
@@ -160,16 +238,21 @@ function MountedRichTextEditor({
|
|
|
160
238
|
<div
|
|
161
239
|
data-slot="rich-text-editor"
|
|
162
240
|
data-disabled={!editable || undefined}
|
|
163
|
-
|
|
241
|
+
data-over-limit={editor && maxLength !== undefined && editor.getText().length > maxLength || undefined}
|
|
242
|
+
className={cn("overflow-hidden rounded-md border bg-background shadow-sm transition-[border-color,box-shadow] focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/25 data-[disabled]:opacity-60 data-[over-limit=true]:border-destructive", className)}
|
|
164
243
|
{...props}
|
|
165
244
|
>
|
|
166
|
-
{editor && editable ? <EditorToolbar editor={editor} onLinkRequest={onLinkRequest} label={labels?.toolbar ?? "Text formatting"} /> : null}
|
|
245
|
+
{editor && editable && toolbar ? <EditorToolbar editor={editor} onLinkRequest={onLinkRequest} label={labels?.toolbar ?? "Text formatting"} features={features} sticky={stickyToolbar} size={toolbarSize} /> : null}
|
|
167
246
|
<EditorContent
|
|
168
247
|
editor={editor}
|
|
169
248
|
aria-label={labels?.editor ?? "Rich text editor"}
|
|
170
|
-
className="[&_.tiptap]:px-3 [&_.tiptap]:py-
|
|
171
|
-
style={{ minHeight }}
|
|
249
|
+
className="overflow-y-auto [&_.tiptap]:min-h-[var(--editor-min-height)] [&_.tiptap]:px-3.5 [&_.tiptap]:py-3 [&_.tiptap]:text-sm [&_.tiptap]:leading-6 [&_.tiptap]:outline-none [&_.tiptap>*+*]:mt-2 [&_.tiptap_a]:text-primary [&_.tiptap_a]:underline [&_.tiptap_blockquote]:border-l-2 [&_.tiptap_blockquote]:border-primary/50 [&_.tiptap_blockquote]:pl-3 [&_.tiptap_code]:rounded [&_.tiptap_code]:bg-muted [&_.tiptap_code]:px-1 [&_.tiptap_code]:py-0.5 [&_.tiptap_h1]:text-2xl [&_.tiptap_h1]:font-semibold [&_.tiptap_h2]:text-xl [&_.tiptap_h2]:font-semibold [&_.tiptap_h3]:text-lg [&_.tiptap_h3]:font-semibold [&_.tiptap_hr]:my-4 [&_.tiptap_hr]:border-border [&_.tiptap_ol]:list-decimal [&_.tiptap_ol]:pl-6 [&_.tiptap_p.is-editor-empty:first-child:before]:pointer-events-none [&_.tiptap_p.is-editor-empty:first-child:before]:float-left [&_.tiptap_p.is-editor-empty:first-child:before]:h-0 [&_.tiptap_p.is-editor-empty:first-child:before]:text-muted-foreground [&_.tiptap_p.is-editor-empty:first-child:before]:content-[attr(data-placeholder)] [&_.tiptap_pre]:overflow-x-auto [&_.tiptap_pre]:rounded-md [&_.tiptap_pre]:bg-muted [&_.tiptap_pre]:p-3 [&_.tiptap_ul]:list-disc [&_.tiptap_ul]:pl-6"
|
|
250
|
+
style={{ "--editor-min-height": `${minHeight}px`, maxHeight } as React.CSSProperties}
|
|
172
251
|
/>
|
|
252
|
+
{editor && (showCharacterCount || showWordCount || maxLength !== undefined) ? <div data-slot="rich-text-editor-footer" className="flex items-center justify-end gap-3 border-t bg-muted/20 px-3 py-1.5 text-[11px] text-muted-foreground">
|
|
253
|
+
{showWordCount ? <span>{editor.getText().trim() ? editor.getText().trim().split(/\s+/).length : 0} words</span> : null}
|
|
254
|
+
{showCharacterCount || maxLength !== undefined ? <span className={cn(maxLength !== undefined && editor.getText().length > maxLength && "font-medium text-destructive")}>{editor.getText().length}{maxLength !== undefined ? `/${maxLength}` : " characters"}</span> : null}
|
|
255
|
+
</div> : null}
|
|
173
256
|
</div>
|
|
174
257
|
)
|
|
175
258
|
}
|
|
@@ -1,74 +1,120 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
|
|
5
|
-
type
|
|
6
|
-
type
|
|
5
|
+
export type ThemeName = string
|
|
6
|
+
export type ThemeMode = ThemeName | "system"
|
|
7
|
+
export type ThemeColorScheme = "light" | "dark"
|
|
8
|
+
|
|
9
|
+
export type ThemeProviderProps = {
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
themes?: ThemeName[]
|
|
12
|
+
defaultTheme?: ThemeMode
|
|
13
|
+
forcedTheme?: ThemeName
|
|
14
|
+
storageKey?: string
|
|
15
|
+
enableSystem?: boolean
|
|
16
|
+
attribute?: "class" | "data-theme" | "both"
|
|
17
|
+
colorSchemes?: Record<ThemeName, ThemeColorScheme>
|
|
18
|
+
disableTransitionOnChange?: boolean
|
|
19
|
+
onThemeChange?: (theme: ThemeMode, resolvedTheme: ThemeName) => void
|
|
20
|
+
}
|
|
7
21
|
|
|
8
22
|
type ThemeContextValue = {
|
|
9
23
|
theme: ThemeMode
|
|
10
|
-
resolvedTheme:
|
|
24
|
+
resolvedTheme: ThemeName
|
|
25
|
+
systemTheme: ThemeColorScheme
|
|
26
|
+
themes: ThemeName[]
|
|
11
27
|
mounted: boolean
|
|
12
28
|
setTheme: (theme: ThemeMode) => void
|
|
13
29
|
toggleTheme: () => void
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
const THEME_STORAGE_KEY = "tembro-theme"
|
|
33
|
+
const defaultThemes = ["light", "dark"]
|
|
34
|
+
const defaultColorSchemes: Record<string, ThemeColorScheme> = { light: "light", dark: "dark", dim: "dark" }
|
|
17
35
|
|
|
18
36
|
const ThemeContext = React.createContext<ThemeContextValue | null>(null)
|
|
19
37
|
|
|
20
|
-
function getSystemTheme():
|
|
38
|
+
function getSystemTheme(): ThemeColorScheme {
|
|
21
39
|
if (typeof window === "undefined") return "light"
|
|
22
40
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
export function ThemeProvider({
|
|
44
|
+
children,
|
|
45
|
+
themes = defaultThemes,
|
|
46
|
+
defaultTheme = "system",
|
|
47
|
+
forcedTheme,
|
|
48
|
+
storageKey = THEME_STORAGE_KEY,
|
|
49
|
+
enableSystem = true,
|
|
50
|
+
attribute = "both",
|
|
51
|
+
colorSchemes = defaultColorSchemes,
|
|
52
|
+
disableTransitionOnChange = true,
|
|
53
|
+
onThemeChange,
|
|
54
|
+
}: ThemeProviderProps) {
|
|
55
|
+
const availableThemes = React.useMemo(() => Array.from(new Set(themes.length ? themes : defaultThemes)), [themes])
|
|
56
|
+
const [theme, setThemeState] = React.useState<ThemeMode>(() => {
|
|
57
|
+
if (typeof window === "undefined") return defaultTheme
|
|
58
|
+
const stored = window.localStorage.getItem(storageKey)
|
|
59
|
+
if (stored === "system" && enableSystem) return stored
|
|
60
|
+
if (stored && availableThemes.includes(stored)) return stored
|
|
61
|
+
return defaultTheme
|
|
62
|
+
})
|
|
36
63
|
const [mounted, setMounted] = React.useState(false)
|
|
37
|
-
const [systemTheme, setSystemTheme] = React.useState<
|
|
38
|
-
const
|
|
64
|
+
const [systemTheme, setSystemTheme] = React.useState<ThemeColorScheme>(getSystemTheme)
|
|
65
|
+
const appliedClassRef = React.useRef<string | undefined>(undefined)
|
|
66
|
+
const activeTheme = forcedTheme ?? theme
|
|
67
|
+
const resolvedTheme = activeTheme === "system"
|
|
68
|
+
? (availableThemes.includes(systemTheme) ? systemTheme : availableThemes[0] ?? "light")
|
|
69
|
+
: activeTheme
|
|
70
|
+
const colorScheme = colorSchemes[resolvedTheme] ?? defaultColorSchemes[resolvedTheme] ?? (resolvedTheme === "dark" ? "dark" : "light")
|
|
39
71
|
|
|
40
|
-
React.useEffect(() => {
|
|
41
|
-
setMounted(true)
|
|
42
|
-
}, [])
|
|
72
|
+
React.useEffect(() => { setMounted(true) }, [])
|
|
43
73
|
|
|
44
74
|
React.useEffect(() => {
|
|
75
|
+
if (!enableSystem) return
|
|
45
76
|
const media = window.matchMedia("(prefers-color-scheme: dark)")
|
|
46
77
|
const updateSystemTheme = () => setSystemTheme(media.matches ? "dark" : "light")
|
|
47
|
-
|
|
48
78
|
updateSystemTheme()
|
|
49
79
|
media.addEventListener("change", updateSystemTheme)
|
|
50
80
|
return () => media.removeEventListener("change", updateSystemTheme)
|
|
51
|
-
}, [])
|
|
81
|
+
}, [enableSystem])
|
|
52
82
|
|
|
53
83
|
React.useEffect(() => {
|
|
54
84
|
const root = window.document.documentElement
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
let restoreTransitions: (() => void) | undefined
|
|
86
|
+
if (disableTransitionOnChange) {
|
|
87
|
+
const style = document.createElement("style")
|
|
88
|
+
style.textContent = "*,*::before,*::after{transition:none!important}"
|
|
89
|
+
document.head.append(style)
|
|
90
|
+
void window.getComputedStyle(root).opacity
|
|
91
|
+
restoreTransitions = () => window.setTimeout(() => style.remove(), 0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (attribute === "class" || attribute === "both") {
|
|
95
|
+
if (appliedClassRef.current) root.classList.remove(appliedClassRef.current)
|
|
96
|
+
root.classList.remove("light", "dark")
|
|
97
|
+
root.classList.add(resolvedTheme)
|
|
98
|
+
if (colorScheme === "dark" && resolvedTheme !== "dark") root.classList.add("dark")
|
|
99
|
+
appliedClassRef.current = resolvedTheme
|
|
100
|
+
}
|
|
101
|
+
if (attribute === "data-theme" || attribute === "both") root.dataset.theme = resolvedTheme
|
|
102
|
+
root.style.colorScheme = colorScheme
|
|
103
|
+
if (!forcedTheme) window.localStorage.setItem(storageKey, theme)
|
|
104
|
+
onThemeChange?.(theme, resolvedTheme)
|
|
105
|
+
restoreTransitions?.()
|
|
106
|
+
}, [attribute, colorScheme, disableTransitionOnChange, forcedTheme, onThemeChange, resolvedTheme, storageKey, theme])
|
|
59
107
|
|
|
60
108
|
const setTheme = React.useCallback((nextTheme: ThemeMode) => {
|
|
61
|
-
setThemeState(nextTheme)
|
|
62
|
-
}, [])
|
|
109
|
+
if (nextTheme === "system" ? enableSystem : availableThemes.includes(nextTheme)) setThemeState(nextTheme)
|
|
110
|
+
}, [availableThemes, enableSystem])
|
|
63
111
|
|
|
64
112
|
const toggleTheme = React.useCallback(() => {
|
|
65
|
-
|
|
66
|
-
|
|
113
|
+
const cycle: ThemeMode[] = [...availableThemes, ...(enableSystem ? ["system" as const] : [])]
|
|
114
|
+
setThemeState((current) => cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? availableThemes[0] ?? "light")
|
|
115
|
+
}, [availableThemes, enableSystem])
|
|
67
116
|
|
|
68
|
-
const value = React.useMemo(
|
|
69
|
-
() => ({ theme, resolvedTheme, mounted, setTheme, toggleTheme }),
|
|
70
|
-
[mounted, resolvedTheme, setTheme, theme, toggleTheme]
|
|
71
|
-
)
|
|
117
|
+
const value = React.useMemo<ThemeContextValue>(() => ({ theme, resolvedTheme, systemTheme, themes: availableThemes, mounted, setTheme, toggleTheme }), [availableThemes, mounted, resolvedTheme, setTheme, systemTheme, theme, toggleTheme])
|
|
72
118
|
|
|
73
119
|
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
74
120
|
}
|
|
@@ -47,7 +47,7 @@ const badgeVariants = cva(
|
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
type BadgeProps = useRender.ComponentProps<"span"> &
|
|
50
|
-
VariantProps<typeof badgeVariants> & {
|
|
50
|
+
Omit<VariantProps<typeof badgeVariants>, "dot"> & {
|
|
51
51
|
label?: React.ReactNode
|
|
52
52
|
count?: React.ReactNode
|
|
53
53
|
status?: "neutral" | "info" | "success" | "warning" | "danger" | "muted"
|
|
@@ -57,6 +57,12 @@ type BadgeProps = useRender.ComponentProps<"span"> &
|
|
|
57
57
|
removeLabel?: string
|
|
58
58
|
leftIcon?: React.ReactNode
|
|
59
59
|
rightIcon?: React.ReactNode
|
|
60
|
+
avatar?: React.ReactNode
|
|
61
|
+
dot?: boolean | React.ReactNode
|
|
62
|
+
showDot?: boolean
|
|
63
|
+
dotPosition?: "start" | "end"
|
|
64
|
+
pulse?: boolean
|
|
65
|
+
interactive?: boolean
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
function Badge({
|
|
@@ -74,6 +80,11 @@ function Badge({
|
|
|
74
80
|
removeLabel = "Remove badge",
|
|
75
81
|
leftIcon,
|
|
76
82
|
rightIcon,
|
|
83
|
+
avatar,
|
|
84
|
+
showDot,
|
|
85
|
+
dotPosition = "start",
|
|
86
|
+
pulse = false,
|
|
87
|
+
interactive = false,
|
|
77
88
|
children,
|
|
78
89
|
render,
|
|
79
90
|
onKeyDown,
|
|
@@ -81,18 +92,24 @@ function Badge({
|
|
|
81
92
|
}: BadgeProps) {
|
|
82
93
|
const resolvedTone = status ?? tone
|
|
83
94
|
const resolvedLabel = label ?? children
|
|
84
|
-
const
|
|
95
|
+
const resolvedShowDot = showDot ?? Boolean(dot || status === "success" || status === "warning" || status === "danger" || status === "info")
|
|
96
|
+
const dotNode = resolvedShowDot ? (
|
|
97
|
+
<span data-slot="badge-dot" data-pulse={pulse || undefined}>
|
|
98
|
+
{React.isValidElement(dot) ? dot : null}
|
|
99
|
+
</span>
|
|
100
|
+
) : null
|
|
85
101
|
|
|
86
102
|
return useRender({
|
|
87
103
|
defaultTagName: "span",
|
|
88
104
|
props: mergeProps<"span">(
|
|
89
105
|
{
|
|
90
106
|
className: cn(
|
|
91
|
-
badgeVariants({ variant, tone: resolvedTone, size, dot:
|
|
107
|
+
badgeVariants({ variant, tone: resolvedTone, size, dot: resolvedShowDot }),
|
|
92
108
|
selected && "ring-2 ring-ring/45 ring-offset-1",
|
|
109
|
+
interactive && "cursor-pointer",
|
|
93
110
|
className
|
|
94
111
|
),
|
|
95
|
-
tabIndex: removable ? 0 : undefined,
|
|
112
|
+
tabIndex: removable || interactive ? 0 : undefined,
|
|
96
113
|
onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => {
|
|
97
114
|
if (removable && (event.key === "Backspace" || event.key === "Delete")) {
|
|
98
115
|
event.preventDefault()
|
|
@@ -102,8 +119,9 @@ function Badge({
|
|
|
102
119
|
},
|
|
103
120
|
children: (
|
|
104
121
|
<>
|
|
105
|
-
{
|
|
106
|
-
{
|
|
122
|
+
{dotPosition === "start" ? dotNode : null}
|
|
123
|
+
{avatar ? <span data-slot="badge-avatar" className="grid shrink-0 place-items-center overflow-hidden rounded-full">{avatar}</span> : null}
|
|
124
|
+
{leftIcon ? <span data-icon="inline-start" data-slot="badge-icon" className="grid shrink-0 place-items-center">{leftIcon}</span> : null}
|
|
107
125
|
{resolvedLabel ? <span data-slot="badge-label">{resolvedLabel}</span> : null}
|
|
108
126
|
{count != null ? (
|
|
109
127
|
<span
|
|
@@ -112,7 +130,8 @@ function Badge({
|
|
|
112
130
|
{count}
|
|
113
131
|
</span>
|
|
114
132
|
) : null}
|
|
115
|
-
{rightIcon ? <span data-icon="inline-end" data-slot="badge-icon" className="
|
|
133
|
+
{rightIcon ? <span data-icon="inline-end" data-slot="badge-icon" className="grid shrink-0 place-items-center">{rightIcon}</span> : null}
|
|
134
|
+
{dotPosition === "end" ? dotNode : null}
|
|
116
135
|
{removable ? (
|
|
117
136
|
<button
|
|
118
137
|
type="button"
|
|
@@ -136,6 +155,8 @@ function Badge({
|
|
|
136
155
|
"data-size": size ?? "default",
|
|
137
156
|
"data-removable": removable || undefined,
|
|
138
157
|
"data-selected": selected || undefined,
|
|
158
|
+
"data-interactive": interactive || undefined,
|
|
159
|
+
"data-pulse": pulse || undefined,
|
|
139
160
|
} as React.HTMLAttributes<HTMLSpanElement>,
|
|
140
161
|
props
|
|
141
162
|
),
|
|
@@ -145,7 +166,7 @@ function Badge({
|
|
|
145
166
|
variant,
|
|
146
167
|
tone: resolvedTone,
|
|
147
168
|
size,
|
|
148
|
-
dot:
|
|
169
|
+
dot: resolvedShowDot,
|
|
149
170
|
},
|
|
150
171
|
})
|
|
151
172
|
}
|
|
@@ -98,12 +98,12 @@ const ClearableInput = React.forwardRef<HTMLInputElement, ClearableInputProps>(
|
|
|
98
98
|
type="button"
|
|
99
99
|
data-slot="clearable-input-clear"
|
|
100
100
|
aria-label={clearLabel}
|
|
101
|
-
className="
|
|
101
|
+
className="grid size-7 shrink-0 place-items-center rounded-[var(--radius-sm)] border border-transparent bg-transparent p-0 leading-none text-muted-foreground/74 transition hover:border-border/60 hover:bg-muted/58 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
102
102
|
onClick={handleClearClick}
|
|
103
103
|
onMouseDown={handleClearMouseDown}
|
|
104
104
|
onDoubleClick={handleClearMouseDown}
|
|
105
105
|
>
|
|
106
|
-
<XIcon className="size-
|
|
106
|
+
<XIcon className="size-3.5" strokeWidth={2} />
|
|
107
107
|
</button>
|
|
108
108
|
)}
|
|
109
109
|
</>
|
|
@@ -13,7 +13,7 @@ function InputPrimitive({ className, type, ...props }: InputPrimitiveProps) {
|
|
|
13
13
|
type={type}
|
|
14
14
|
data-slot="input"
|
|
15
15
|
className={cn(
|
|
16
|
-
"h-11 w-full min-w-0 rounded-[var(--aui-control-radius,var(--radius-md))] border border-[color:color-mix(in_oklch,var(--aui-control-border-strong,var(--input)),var(--foreground)_6%)] bg-[color:var(--aui-control-surface,var(--background))] px-3.5 py-2.5 text-sm font-normal text-foreground shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04))] transition-[background-color,border-color,box-shadow,color] outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/74 hover:border-[color:color-mix(in_oklch,var(--aui-control-hover-border,var(--ring)),var(--foreground)_10%)] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] focus-visible:border-[color:var(--ring)] focus-visible:ring-0 focus-visible:shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_4px_var(--aui-focus-ring-soft,transparent)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-[color:color-mix(in_oklch,var(--border),transparent_18%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:text-muted-foreground disabled:shadow-none disabled:opacity-100 read-only:bg-[color:var(--aui-control-surface-readonly,var(--muted))]",
|
|
16
|
+
"h-11 w-full min-w-0 rounded-[var(--aui-control-radius,var(--radius-md))] border border-[color:color-mix(in_oklch,var(--aui-control-border-strong,var(--input)),var(--foreground)_6%)] bg-[color:var(--aui-control-surface,var(--background))] px-3.5 py-2.5 text-sm font-normal text-foreground shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04))] transition-[background-color,border-color,box-shadow,color] outline-none [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground/74 hover:border-[color:color-mix(in_oklch,var(--aui-control-hover-border,var(--ring)),var(--foreground)_10%)] hover:bg-[color:var(--aui-control-surface-hover,var(--background))] focus-visible:border-[color:var(--ring)] focus-visible:ring-0 focus-visible:shadow-[var(--aui-shadow-xs,0_1px_2px_rgba(15,23,42,0.04)),0_0_0_1px_var(--aui-focus-ring,var(--ring)),0_0_0_4px_var(--aui-focus-ring-soft,transparent)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:border-[color:color-mix(in_oklch,var(--border),transparent_18%)] disabled:bg-[color:var(--aui-control-surface-disabled,var(--muted))] disabled:text-muted-foreground disabled:shadow-none disabled:opacity-100 read-only:bg-[color:var(--aui-control-surface-readonly,var(--muted))]",
|
|
17
17
|
className
|
|
18
18
|
)}
|
|
19
19
|
{...props}
|
|
@@ -34,6 +34,7 @@ export const documentedPublicComponentSurfaces: readonly PublicComponentSurfaceE
|
|
|
34
34
|
{ slug: "code-block", registryName: "code-block", surface: "documented" },
|
|
35
35
|
{ slug: "data-state", registryName: "data-state", surface: "documented" },
|
|
36
36
|
{ slug: "kanban", registryName: "kanban", surface: "documented" },
|
|
37
|
+
{ slug: "chat", registryName: "chat", surface: "documented" },
|
|
37
38
|
{ slug: "list", registryName: "list", surface: "documented" },
|
|
38
39
|
{ slug: "description-list", registryName: "description-list", surface: "documented" },
|
|
39
40
|
{ slug: "statistic", registryName: "statistic", surface: "documented" },
|
|
@@ -25,7 +25,7 @@ export function HeroSection() {
|
|
|
25
25
|
<div className="flex flex-wrap gap-2">
|
|
26
26
|
<Button leftIcon={<PlusIcon className="size-4" />}>Create</Button>
|
|
27
27
|
<Button variant="outline" leftIcon={<SettingsIcon className="size-4" />}>Settings</Button>
|
|
28
|
-
<CopyButton value="npx tembro@4.0
|
|
28
|
+
<CopyButton value="npx tembro@4.1.0 list --json">Copy list command</CopyButton>
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ export function HeroSection() {
|
|
|
33
33
|
<StatisticCard label="Registry components" value={moduleCount} change="visible below" trend="up" description="tembro add <name>" />
|
|
34
34
|
<StatisticCard label="Local source files" value="152" change="with hooks/lib" trend="up" description="installed by CLI" />
|
|
35
35
|
<StatisticCard label="Categories" value={registryGroups.length} change="all shown" trend="up" description="actions to wizard" />
|
|
36
|
-
<StatisticCard label="Build" value="Pass" change="doctor pass" trend="up" description="tembro@4.0
|
|
36
|
+
<StatisticCard label="Build" value="Pass" change="doctor pass" trend="up" description="tembro@4.1.0" />
|
|
37
37
|
</StatisticGrid>
|
|
38
38
|
</div>
|
|
39
39
|
</section>
|