@dbx-tools/ui-appkit 0.6.56 → 0.6.58

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 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 for feature packages and hosts.
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.56",
29
- "@dbx-tools/ui-branding": "0.6.56",
28
+ "@dbx-tools/shared-core": "0.6.58",
29
+ "@dbx-tools/ui-branding": "0.6.58",
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.56",
39
+ "version": "0.6.58",
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
+ }
@@ -1 +1,2 @@
1
1
  export * from "./appkit-ui.ts";
2
+ export * from "./brand-picker.tsx";