@dbx-tools/ui-appkit 0.6.55 → 0.6.57
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/README.md +26 -2
- package/index.ts +2 -0
- package/package.json +3 -3
- package/src/react/brand-picker.tsx +278 -0
- package/src/react/index.ts +1 -0
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ Key features:
|
|
|
12
12
|
- Shared Vite plugin factory for React and Tailwind v4.
|
|
13
13
|
- Stable `@dbx-tools/ui-appkit/react` re-export of AppKit's React component
|
|
14
14
|
primitives for feature packages.
|
|
15
|
+
- `BrandPicker`, a controlled AppKit-native editor for portable identity,
|
|
16
|
+
color tokens, document metadata, and assets.
|
|
15
17
|
- AppKit UI stylesheet import path for host applications and feature packages.
|
|
16
18
|
- Streamdown/code-block styling used by streaming chat and Markdown surfaces.
|
|
17
19
|
- One place to evolve UI build assumptions for feature packages such as
|
|
@@ -50,6 +52,28 @@ The stylesheet imports Tailwind and Streamdown styles, then adds the shiki CSS
|
|
|
50
52
|
variable shim used by Streamdown code-block spans. Feature UI packages should
|
|
51
53
|
import this once and add their own `@source` directives for local class names.
|
|
52
54
|
|
|
55
|
+
## Edit A Live Brand
|
|
56
|
+
|
|
57
|
+
`BrandPicker` emits only complete, schema-valid `BrandContext` values. Feed the
|
|
58
|
+
result back into `BrandProvider` to update AppKit tokens, document metadata,
|
|
59
|
+
brand assets, and brand-aware feature UI together.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { brand } from "@dbx-tools/shared-core";
|
|
63
|
+
import { BrandPicker } from "@dbx-tools/ui-appkit/react";
|
|
64
|
+
import { BrandProvider } from "@dbx-tools/ui-branding/react";
|
|
65
|
+
import { useState } from "react";
|
|
66
|
+
|
|
67
|
+
export function BrandSettings() {
|
|
68
|
+
const [context, setContext] = useState(brand.defaultBrandContext);
|
|
69
|
+
return (
|
|
70
|
+
<BrandProvider context={context} applyToDocument>
|
|
71
|
+
<BrandPicker value={context} onChange={setContext} />
|
|
72
|
+
</BrandProvider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
53
77
|
## Build Feature UI Packages
|
|
54
78
|
|
|
55
79
|
Feature packages should depend on this package instead of each owning their own
|
|
@@ -64,8 +88,8 @@ look for:
|
|
|
64
88
|
## Module
|
|
65
89
|
|
|
66
90
|
- `./vite` - `appkitUiVitePlugins()` for React + Tailwind v4 Vite projects.
|
|
67
|
-
- `./react` - AppKit React UI kit re-export
|
|
91
|
+
- `./react` - AppKit React UI kit re-export plus the controlled `BrandPicker`.
|
|
68
92
|
- `./styles.css` - Tailwind/Streamdown/shiki base stylesheet.
|
|
69
93
|
|
|
70
94
|
App-specific React components should live in feature UI packages that import this
|
|
71
|
-
foundation.
|
|
95
|
+
foundation. Cross-feature AppKit utilities such as `BrandPicker` live here.
|
package/index.ts
CHANGED
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export * as reactAppkitUi from "./src/react/appkit-ui.ts";
|
|
6
|
+
export * as reactBrandPicker from "./src/react/brand-picker.tsx";
|
|
7
|
+
export type { BrandPreset, BrandPickerProps } from "./src/react/brand-picker.tsx";
|
package/package.json
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@databricks/appkit-ui": "^0.43.1",
|
|
28
|
-
"@dbx-tools/shared-core": "0.6.
|
|
29
|
-
"@dbx-tools/ui-branding": "0.6.
|
|
28
|
+
"@dbx-tools/shared-core": "0.6.57",
|
|
29
|
+
"@dbx-tools/ui-branding": "0.6.57",
|
|
30
30
|
"react": "^19.2.4",
|
|
31
31
|
"react-dom": "^19.2.4",
|
|
32
32
|
"streamdown": "^2.5.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
|
-
"version": "0.6.
|
|
39
|
+
"version": "0.6.57",
|
|
40
40
|
"type": "module",
|
|
41
41
|
"exports": {
|
|
42
42
|
"./react": "./src/react/index.ts",
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { brand, object } from "@dbx-tools/shared-core";
|
|
2
|
+
import { useBrand } from "@dbx-tools/ui-branding/react";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
Input,
|
|
11
|
+
Label,
|
|
12
|
+
Separator,
|
|
13
|
+
cn,
|
|
14
|
+
} from "@databricks/appkit-ui/react";
|
|
15
|
+
import { useEffect, useId, useMemo, useState } from "react";
|
|
16
|
+
|
|
17
|
+
const COLOR_FIELDS = [
|
|
18
|
+
["primary", "Primary"],
|
|
19
|
+
["primaryHover", "Primary hover"],
|
|
20
|
+
["accent", "Accent"],
|
|
21
|
+
["foreground", "Foreground"],
|
|
22
|
+
["background", "Background"],
|
|
23
|
+
["surface", "Surface"],
|
|
24
|
+
["muted", "Muted"],
|
|
25
|
+
["border", "Border"],
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
type ColorField = (typeof COLOR_FIELDS)[number][0];
|
|
29
|
+
|
|
30
|
+
/** One complete brand option shown above the editable fields. */
|
|
31
|
+
export interface BrandPreset {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
context: brand.BrandContextInput;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Props for {@link BrandPicker}. */
|
|
39
|
+
export interface BrandPickerProps {
|
|
40
|
+
/** Current brand. Defaults to the nearest `BrandProvider` context. */
|
|
41
|
+
value?: brand.BrandContextInput;
|
|
42
|
+
/** Receives each valid edited or selected brand context. */
|
|
43
|
+
onChange: (value: brand.BrandContext) => void;
|
|
44
|
+
/** Optional complete brand choices rendered as quick-select buttons. */
|
|
45
|
+
presets?: readonly BrandPreset[];
|
|
46
|
+
/** Value restored by Reset. Defaults to the dbx-tools brand. */
|
|
47
|
+
resetValue?: brand.BrandContextInput;
|
|
48
|
+
/** Show editable icon, logo, and favicon references. */
|
|
49
|
+
showAssets?: boolean;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const validColor = (value: string, fallback: string): string =>
|
|
54
|
+
/^#(?:[\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i.test(value) ? value : fallback;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Edit a portable `BrandContext` with AppKit-native controls.
|
|
58
|
+
*
|
|
59
|
+
* The picker keeps incomplete text locally while emitting only schema-valid
|
|
60
|
+
* contexts, so controlled hosts can apply changes directly to a
|
|
61
|
+
* `BrandProvider` without guarding every keystroke.
|
|
62
|
+
*/
|
|
63
|
+
export function BrandPicker({
|
|
64
|
+
value,
|
|
65
|
+
onChange,
|
|
66
|
+
presets = [],
|
|
67
|
+
resetValue = brand.defaultBrandContext,
|
|
68
|
+
showAssets = true,
|
|
69
|
+
className,
|
|
70
|
+
}: BrandPickerProps) {
|
|
71
|
+
const { context: inherited } = useBrand();
|
|
72
|
+
const current = useMemo(() => brand.parseBrandContext(value ?? inherited), [inherited, value]);
|
|
73
|
+
const reset = useMemo(() => brand.parseBrandContext(resetValue), [resetValue]);
|
|
74
|
+
const resolvedPresets = useMemo(
|
|
75
|
+
() =>
|
|
76
|
+
presets.map((preset) => ({ ...preset, context: brand.parseBrandContext(preset.context) })),
|
|
77
|
+
[presets],
|
|
78
|
+
);
|
|
79
|
+
const [draft, setDraft] = useState(current);
|
|
80
|
+
const id = useId();
|
|
81
|
+
|
|
82
|
+
useEffect(() => setDraft(current), [current]);
|
|
83
|
+
|
|
84
|
+
const update = (next: brand.BrandContext) => {
|
|
85
|
+
setDraft(next);
|
|
86
|
+
const parsed = brand.BrandContextSchema.safeParse(next);
|
|
87
|
+
if (parsed.success) onChange(parsed.data);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const updateIdentity = (field: "name" | "shortName" | "tagline", next: string) =>
|
|
91
|
+
update({ ...draft, [field]: next });
|
|
92
|
+
|
|
93
|
+
const updateColor = (field: ColorField, next: string) =>
|
|
94
|
+
update({ ...draft, colors: { ...draft.colors, [field]: next } });
|
|
95
|
+
|
|
96
|
+
const updateAsset = (group: "icon" | "logo", mode: "light" | "dark", next: string) =>
|
|
97
|
+
update({
|
|
98
|
+
...draft,
|
|
99
|
+
assets: {
|
|
100
|
+
...draft.assets,
|
|
101
|
+
[group]: { ...draft.assets[group], [mode]: next || undefined },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Card className={cn("overflow-hidden", className)}>
|
|
107
|
+
<CardHeader>
|
|
108
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
109
|
+
<div>
|
|
110
|
+
<CardTitle>Brand picker</CardTitle>
|
|
111
|
+
<CardDescription>
|
|
112
|
+
Update identity, AppKit color tokens, document metadata, and email presentation.
|
|
113
|
+
</CardDescription>
|
|
114
|
+
</div>
|
|
115
|
+
<Button
|
|
116
|
+
type="button"
|
|
117
|
+
size="sm"
|
|
118
|
+
variant="outline"
|
|
119
|
+
onClick={() => {
|
|
120
|
+
setDraft(reset);
|
|
121
|
+
onChange(reset);
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
Reset
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
<CardContent className="space-y-6">
|
|
129
|
+
{resolvedPresets.length ? (
|
|
130
|
+
<section className="space-y-2" aria-label="Brand presets">
|
|
131
|
+
<Label>Presets</Label>
|
|
132
|
+
<div className="grid gap-2 sm:grid-cols-3">
|
|
133
|
+
{resolvedPresets.map((preset) => {
|
|
134
|
+
const active = object.deepEqual(current, preset.context);
|
|
135
|
+
return (
|
|
136
|
+
<Button
|
|
137
|
+
key={preset.id}
|
|
138
|
+
type="button"
|
|
139
|
+
variant={active ? "default" : "outline"}
|
|
140
|
+
className="h-auto justify-start px-3 py-2 text-left"
|
|
141
|
+
onClick={() => {
|
|
142
|
+
setDraft(preset.context);
|
|
143
|
+
onChange(preset.context);
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<span>
|
|
147
|
+
<span className="block text-sm font-medium">{preset.label}</span>
|
|
148
|
+
{preset.description ? (
|
|
149
|
+
<span className="block text-xs font-normal opacity-75">
|
|
150
|
+
{preset.description}
|
|
151
|
+
</span>
|
|
152
|
+
) : null}
|
|
153
|
+
</span>
|
|
154
|
+
</Button>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</div>
|
|
158
|
+
</section>
|
|
159
|
+
) : null}
|
|
160
|
+
|
|
161
|
+
<Separator />
|
|
162
|
+
|
|
163
|
+
<section className="space-y-3">
|
|
164
|
+
<div>
|
|
165
|
+
<h3 className="text-sm font-medium text-foreground">Identity</h3>
|
|
166
|
+
<p className="text-xs text-muted-foreground">
|
|
167
|
+
Used by navigation, metadata, and email.
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
171
|
+
<div className="space-y-1.5">
|
|
172
|
+
<Label htmlFor={`${id}-name`}>Name</Label>
|
|
173
|
+
<Input
|
|
174
|
+
id={`${id}-name`}
|
|
175
|
+
value={draft.name}
|
|
176
|
+
onChange={(event) => updateIdentity("name", event.currentTarget.value)}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="space-y-1.5">
|
|
180
|
+
<Label htmlFor={`${id}-short-name`}>Short name</Label>
|
|
181
|
+
<Input
|
|
182
|
+
id={`${id}-short-name`}
|
|
183
|
+
value={draft.shortName}
|
|
184
|
+
onChange={(event) => updateIdentity("shortName", event.currentTarget.value)}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="space-y-1.5 sm:col-span-2">
|
|
188
|
+
<Label htmlFor={`${id}-tagline`}>Tagline</Label>
|
|
189
|
+
<Input
|
|
190
|
+
id={`${id}-tagline`}
|
|
191
|
+
value={draft.tagline}
|
|
192
|
+
onChange={(event) => updateIdentity("tagline", event.currentTarget.value)}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
<Separator />
|
|
199
|
+
|
|
200
|
+
<section className="space-y-3">
|
|
201
|
+
<div>
|
|
202
|
+
<h3 className="text-sm font-medium text-foreground">Color system</h3>
|
|
203
|
+
<p className="text-xs text-muted-foreground">
|
|
204
|
+
Valid hex values are emitted immediately to the active brand provider.
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
208
|
+
{COLOR_FIELDS.map(([field, label]) => (
|
|
209
|
+
<div key={field} className="space-y-1.5">
|
|
210
|
+
<Label htmlFor={`${id}-${field}`}>{label}</Label>
|
|
211
|
+
<div className="flex gap-2">
|
|
212
|
+
<Input
|
|
213
|
+
type="color"
|
|
214
|
+
aria-label={`${label} color`}
|
|
215
|
+
value={validColor(draft.colors[field], current.colors[field])}
|
|
216
|
+
className="h-9 w-12 shrink-0 cursor-pointer p-1"
|
|
217
|
+
onChange={(event) => updateColor(field, event.currentTarget.value)}
|
|
218
|
+
/>
|
|
219
|
+
<Input
|
|
220
|
+
id={`${id}-${field}`}
|
|
221
|
+
value={draft.colors[field]}
|
|
222
|
+
spellCheck={false}
|
|
223
|
+
onChange={(event) => updateColor(field, event.currentTarget.value)}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</section>
|
|
230
|
+
|
|
231
|
+
{showAssets ? (
|
|
232
|
+
<>
|
|
233
|
+
<Separator />
|
|
234
|
+
<section className="space-y-3">
|
|
235
|
+
<div>
|
|
236
|
+
<h3 className="text-sm font-medium text-foreground">Assets</h3>
|
|
237
|
+
<p className="text-xs text-muted-foreground">
|
|
238
|
+
Package paths, data URLs, and hosted URLs are supported.
|
|
239
|
+
</p>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
242
|
+
{(["icon", "logo"] as const).flatMap((group) =>
|
|
243
|
+
(["light", "dark"] as const).map((mode) => (
|
|
244
|
+
<div key={`${group}-${mode}`} className="space-y-1.5">
|
|
245
|
+
<Label htmlFor={`${id}-${group}-${mode}`}>
|
|
246
|
+
{group === "icon" ? "Icon" : "Logo"} · {mode}
|
|
247
|
+
</Label>
|
|
248
|
+
<Input
|
|
249
|
+
id={`${id}-${group}-${mode}`}
|
|
250
|
+
value={draft.assets[group][mode] ?? ""}
|
|
251
|
+
spellCheck={false}
|
|
252
|
+
onChange={(event) => updateAsset(group, mode, event.currentTarget.value)}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
)),
|
|
256
|
+
)}
|
|
257
|
+
<div className="space-y-1.5 sm:col-span-2">
|
|
258
|
+
<Label htmlFor={`${id}-favicon`}>Favicon</Label>
|
|
259
|
+
<Input
|
|
260
|
+
id={`${id}-favicon`}
|
|
261
|
+
value={draft.assets.favicon}
|
|
262
|
+
spellCheck={false}
|
|
263
|
+
onChange={(event) =>
|
|
264
|
+
update({
|
|
265
|
+
...draft,
|
|
266
|
+
assets: { ...draft.assets, favicon: event.currentTarget.value },
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</section>
|
|
273
|
+
</>
|
|
274
|
+
) : null}
|
|
275
|
+
</CardContent>
|
|
276
|
+
</Card>
|
|
277
|
+
);
|
|
278
|
+
}
|
package/src/react/index.ts
CHANGED