@dreamtree-org/twreact-ui 1.3.1 → 1.3.3
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/DESIGN.md +2 -2
- package/README.md +90 -5
- package/ai-skills/dreamtree-ui.md +43 -4
- package/bin/cli.mjs +72 -4
- package/charts/package.json +5 -0
- package/dist/_shared/cn-DPPQUJu1.js +138 -0
- package/dist/ai-schema.json +639 -1
- package/dist/charts/index.d.ts +9 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/index.js +797 -0
- package/dist/components/charts/AreaChart.d.ts +4 -0
- package/dist/components/charts/AreaChart.d.ts.map +1 -0
- package/dist/components/charts/BarChart.d.ts +4 -0
- package/dist/components/charts/BarChart.d.ts.map +1 -0
- package/dist/components/charts/CartesianChart.d.ts +5 -0
- package/dist/components/charts/CartesianChart.d.ts.map +1 -0
- package/dist/components/charts/ChartFrame.d.ts +18 -0
- package/dist/components/charts/ChartFrame.d.ts.map +1 -0
- package/dist/components/charts/DonutChart.d.ts +4 -0
- package/dist/components/charts/DonutChart.d.ts.map +1 -0
- package/dist/components/charts/LineChart.d.ts +4 -0
- package/dist/components/charts/LineChart.d.ts.map +1 -0
- package/dist/components/charts/Sparkline.d.ts +5 -0
- package/dist/components/charts/Sparkline.d.ts.map +1 -0
- package/dist/components/charts/chartTheme.d.ts +36 -0
- package/dist/components/charts/chartTheme.d.ts.map +1 -0
- package/dist/components/core/Table/MobileCard.d.ts.map +1 -1
- package/dist/components/core/Table/MobileFilters.d.ts.map +1 -1
- package/dist/components/core/Table/Table.d.ts.map +1 -1
- package/dist/components/rich-text/RichTextEditor.d.ts +4 -0
- package/dist/components/rich-text/RichTextEditor.d.ts.map +1 -0
- package/dist/components/rich-text/RichTextToolbar.d.ts +219 -0
- package/dist/components/rich-text/RichTextToolbar.d.ts.map +1 -0
- package/dist/components/utility/Stepper.d.ts.map +1 -1
- package/dist/esm/charts/index.js +5 -0
- package/dist/esm/components/blocks/StatCard.js +1 -1
- package/dist/esm/components/charts/AreaChart.js +16 -0
- package/dist/esm/components/charts/BarChart.js +16 -0
- package/dist/esm/components/charts/CartesianChart.js +255 -0
- package/dist/esm/components/charts/ChartFrame.js +170 -0
- package/dist/esm/components/charts/DonutChart.js +122 -0
- package/dist/esm/components/charts/LineChart.js +16 -0
- package/dist/esm/components/charts/Sparkline.js +113 -0
- package/dist/esm/components/charts/chartTheme.js +126 -0
- package/dist/esm/components/core/Select.js +2 -2
- package/dist/esm/components/core/Table/MobileCard.js +5 -2
- package/dist/esm/components/core/Table/MobileFilters.js +10 -3
- package/dist/esm/components/core/Table/Table.js +10 -7
- package/dist/esm/components/rich-text/RichTextEditor.js +244 -0
- package/dist/esm/components/rich-text/RichTextToolbar.js +397 -0
- package/dist/esm/components/utility/Stepper.js +6 -4
- package/dist/esm/rich-text/index.js +1 -0
- package/dist/index.css +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +50 -22
- package/dist/index.js +50 -22
- package/dist/rich-text/index.d.ts +5 -0
- package/dist/rich-text/index.d.ts.map +1 -0
- package/dist/rich-text/index.js +654 -0
- package/dist/tokens/brands/practicepro.json +1 -0
- package/dist/tokens/brands/studio.css +14 -0
- package/dist/tokens/brands/studio.json +21 -0
- package/dist/types/charts.d.ts +97 -0
- package/dist/types/charts.d.ts.map +1 -0
- package/dist/types/rich-text.d.ts +50 -0
- package/dist/types/rich-text.d.ts.map +1 -0
- package/doc/README.md +13 -0
- package/mcp/catalog.snapshot.json +1016 -10
- package/mcp/server-core.mjs +30 -1
- package/package.json +39 -2
- package/rich-text/package.json +5 -0
- package/tokens/brand.schema.json +612 -0
- package/tokens/brands/practicepro.json +1 -0
- package/tokens/brands/studio.json +21 -0
- package/tokens/lib.cjs +131 -7
package/tokens/lib.cjs
CHANGED
|
@@ -17,12 +17,76 @@ function listBrands() {
|
|
|
17
17
|
return fs.readdirSync(BRANDS_DIR).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5)).sort();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const isBrandPath = (s) => /[\\/]/.test(s) || s.endsWith('.json');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load a brand. A product keeps its brand in its own repo as dreamtree.brand.json and passes the path;
|
|
24
|
+
* a bare name reads the brands shipped here (only "default" is meant to stay).
|
|
25
|
+
*/
|
|
26
|
+
function loadBrand(nameOrPath) {
|
|
27
|
+
const file = isBrandPath(nameOrPath) ? path.resolve(nameOrPath) : path.join(BRANDS_DIR, `${nameOrPath}.json`);
|
|
22
28
|
if (!fs.existsSync(file)) {
|
|
23
|
-
throw new Error(`
|
|
29
|
+
if (isBrandPath(nameOrPath)) throw new Error(`No brand file at ${file}. Create one with: npx @dreamtree-org/twreact-ui brand init`);
|
|
30
|
+
throw new Error(`Unknown brand "${nameOrPath}". A product's brand lives in its repo: pass the path to its dreamtree.brand.json. Shipped brands: ${listBrands().join(', ') || '(none)'}.`);
|
|
31
|
+
}
|
|
32
|
+
let brand;
|
|
33
|
+
try {
|
|
34
|
+
brand = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new Error(`${file} is not valid JSON (${err.message}).`);
|
|
24
37
|
}
|
|
25
|
-
|
|
38
|
+
const errors = validateBrand(brand);
|
|
39
|
+
if (errors.length) throw new Error(`${file} is not a valid brand file:\n- ${errors.join('\n- ')}`);
|
|
40
|
+
return brand;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The brand's id (new files: "id"; the shipped files use "name"). */
|
|
44
|
+
const brandId = (brand) => brand.id || brand.name;
|
|
45
|
+
/** The brand's display name (new files: "name"; the shipped files use "label"). */
|
|
46
|
+
const brandLabel = (brand) => (brand.id ? brand.name : brand.label) || brandId(brand);
|
|
47
|
+
|
|
48
|
+
const HEX_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
49
|
+
const BRAND_KEYS = ['$schema', '$description', '$deprecated', 'id', 'name', 'label', 'logo', 'color', 'role', 'font', 'marketing'];
|
|
50
|
+
|
|
51
|
+
/** Every problem with a brand file, each phrased as the fix. Empty when the file is valid. */
|
|
52
|
+
function validateBrand(brand, tokens = loadTokens()) {
|
|
53
|
+
const errors = [];
|
|
54
|
+
if (!brand || typeof brand !== 'object' || Array.isArray(brand)) return ['The brand file must be a JSON object.'];
|
|
55
|
+
const id = brandId(brand);
|
|
56
|
+
if (typeof id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(id)) errors.push('Set "id" to a lowercase slug, e.g. "practicepro".');
|
|
57
|
+
for (const key of Object.keys(brand)) if (!BRAND_KEYS.includes(key)) errors.push(`Remove "${key}": allowed keys are ${BRAND_KEYS.join(', ')}.`);
|
|
58
|
+
const knownScales = Object.keys(scales(tokens));
|
|
59
|
+
for (const [scale, steps] of Object.entries(brand.color || {})) {
|
|
60
|
+
if (!knownScales.includes(scale)) {
|
|
61
|
+
errors.push(`color.${scale} is not a scale. Use one of: ${knownScales.join(', ')}.`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const [step, hex] of Object.entries(steps || {})) {
|
|
65
|
+
if (!SCALE_STEPS.includes(step)) errors.push(`color.${scale}.${step} is not a step. Steps are ${SCALE_STEPS.join(', ')}.`);
|
|
66
|
+
else if (typeof hex !== 'string' || !HEX_RE.test(hex)) errors.push(`color.${scale}.${step} must be a hex colour like "#0d7377", not ${JSON.stringify(hex)}.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const knownRoles = Object.keys(roles(tokens));
|
|
70
|
+
for (const [name, value] of Object.entries(brand.role || {})) {
|
|
71
|
+
if (!knownRoles.includes(name)) {
|
|
72
|
+
errors.push(`role.${name} is not a role. Use one of: ${knownRoles.join(', ')}.`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const values = typeof value === 'string' ? [value] : value && typeof value === 'object' ? [value.light, value.dark].filter((v) => v !== undefined) : [value];
|
|
76
|
+
for (const v of values) if (typeof v !== 'string' || !HEX_RE.test(v)) errors.push(`role.${name} must be a hex colour or { "light": hex, "dark": hex }, not ${JSON.stringify(value)}.`);
|
|
77
|
+
}
|
|
78
|
+
if (brand.font !== undefined) {
|
|
79
|
+
const sans = brand.font && brand.font.sans;
|
|
80
|
+
if (!Array.isArray(sans) || !sans.length || !sans.every((f) => typeof f === 'string')) errors.push('font.sans must be a list of font families, e.g. ["Inter", "system-ui", "sans-serif"].');
|
|
81
|
+
}
|
|
82
|
+
for (const [key, v] of Object.entries(brand.marketing || {})) {
|
|
83
|
+
if (!['primary', 'accent', 'panel'].includes(key)) errors.push(`marketing.${key} is not used. Allowed: primary, accent, panel.`);
|
|
84
|
+
else if (typeof v !== 'string' || !HEX_RE.test(v)) errors.push(`marketing.${key} must be a hex colour.`);
|
|
85
|
+
}
|
|
86
|
+
for (const [key, v] of Object.entries(brand.logo || {})) {
|
|
87
|
+
if (typeof v !== 'string') errors.push(`logo.${key} must be a path or URL string.`);
|
|
88
|
+
}
|
|
89
|
+
return errors;
|
|
26
90
|
}
|
|
27
91
|
|
|
28
92
|
function hexToChannels(hex) {
|
|
@@ -63,14 +127,14 @@ function applyBrand(tokens, brand) {
|
|
|
63
127
|
if (!brand) return tokens;
|
|
64
128
|
const next = JSON.parse(JSON.stringify(tokens));
|
|
65
129
|
for (const [scale, steps] of Object.entries(brand.color || {})) {
|
|
66
|
-
if (!next.color[scale]) throw new Error(`Brand "${brand
|
|
130
|
+
if (!next.color[scale]) throw new Error(`Brand "${brandId(brand)}" sets unknown colour scale "${scale}". Known: ${Object.keys(scales(tokens)).join(', ')}.`);
|
|
67
131
|
for (const [step, hex] of Object.entries(steps)) {
|
|
68
|
-
if (!SCALE_STEPS.includes(step)) throw new Error(`Brand "${brand
|
|
132
|
+
if (!SCALE_STEPS.includes(step)) throw new Error(`Brand "${brandId(brand)}" sets ${scale}.${step}; steps are ${SCALE_STEPS.join(', ')}.`);
|
|
69
133
|
next.color[scale][step] = { $value: hex };
|
|
70
134
|
}
|
|
71
135
|
}
|
|
72
136
|
for (const [name, value] of Object.entries(brand.role || {})) {
|
|
73
|
-
if (!next.role[name]) throw new Error(`Brand "${brand
|
|
137
|
+
if (!next.role[name]) throw new Error(`Brand "${brandId(brand)}" sets unknown role "${name}". Known: ${Object.keys(roles(tokens)).join(', ')}.`);
|
|
74
138
|
// A plain string overrides light only; dark keeps the shared value unless {light, dark} is given.
|
|
75
139
|
const current = next.role[name];
|
|
76
140
|
const light = typeof value === 'string' ? value : value.light || current.$value;
|
|
@@ -81,6 +145,61 @@ function applyBrand(tokens, brand) {
|
|
|
81
145
|
return next;
|
|
82
146
|
}
|
|
83
147
|
|
|
148
|
+
/** JSON Schema for dreamtree.brand.json, derived from the tokens (shipped as tokens/brand.schema.json). */
|
|
149
|
+
function brandSchema(tokens = loadTokens()) {
|
|
150
|
+
const hex = { type: 'string', pattern: '^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$' };
|
|
151
|
+
const scale = { type: 'object', additionalProperties: false, properties: Object.fromEntries(SCALE_STEPS.map((s) => [s, hex])) };
|
|
152
|
+
const roleValue = { oneOf: [hex, { type: 'object', additionalProperties: false, properties: { light: hex, dark: hex } }] };
|
|
153
|
+
return {
|
|
154
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
155
|
+
$id: 'https://unpkg.com/@dreamtree-org/twreact-ui/tokens/brand.schema.json',
|
|
156
|
+
title: 'Dreamtree brand (dreamtree.brand.json)',
|
|
157
|
+
description: "A product's look, kept in the product's own repo. Build its CSS with `npx @dreamtree-org/twreact-ui brand build`.",
|
|
158
|
+
type: 'object',
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
required: ['id'],
|
|
161
|
+
properties: {
|
|
162
|
+
$schema: { type: 'string' },
|
|
163
|
+
$description: { type: 'string' },
|
|
164
|
+
$deprecated: { type: 'string' },
|
|
165
|
+
id: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*$', description: 'Lowercase slug, e.g. "practicepro".' },
|
|
166
|
+
name: { type: 'string', description: 'Display name, e.g. "PracticePro".' },
|
|
167
|
+
label: { type: 'string', description: 'Legacy display name (brands shipped in twreact-ui).' },
|
|
168
|
+
logo: { type: 'object', additionalProperties: { type: 'string' }, description: 'Paths or URLs, e.g. { "mark": "public/brand/mark.svg" }.' },
|
|
169
|
+
color: { type: 'object', additionalProperties: false, properties: Object.fromEntries(Object.keys(scales(tokens)).map((s) => [s, scale])) },
|
|
170
|
+
role: { type: 'object', additionalProperties: false, properties: Object.fromEntries(Object.keys(roles(tokens)).map((r) => [r, roleValue])) },
|
|
171
|
+
font: { type: 'object', additionalProperties: false, properties: { sans: { type: 'array', items: { type: 'string' }, minItems: 1 } } },
|
|
172
|
+
marketing: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
description: 'Landing and sign-in pages (landing-kit). Defaults: primary = color.primary.600, accent = color.primary.300, panel = role.shell.',
|
|
176
|
+
properties: { primary: hex, accent: hex, panel: hex },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const block = (selector, decls) =>
|
|
183
|
+
`${selector} {\n${Object.entries(decls).map(([k, v]) => ` ${k}: ${v};`).join('\n')}\n}\n`;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* CSS for only the variables a brand overrides, light and dark. Load it after twreact-ui's CSS.
|
|
187
|
+
* `scope` limits it to one part of the page (e.g. ".studio-ui") instead of :root.
|
|
188
|
+
*/
|
|
189
|
+
function brandCss(tokens, brand, { scope } = {}) {
|
|
190
|
+
const base = cssVariables(tokens);
|
|
191
|
+
const branded = cssVariables(applyBrand(tokens, brand));
|
|
192
|
+
const pick = (a, b) => Object.fromEntries(Object.entries(b).filter(([k, v]) => a[k] !== v));
|
|
193
|
+
const light = pick(base.light, branded.light);
|
|
194
|
+
const dark = pick(base.dark, branded.dark);
|
|
195
|
+
const lightSel = scope || ':root';
|
|
196
|
+
const darkSel = scope ? DARK_SELECTOR.split(',').map((s) => `${s.trim()} ${scope}`).join(', ') : DARK_SELECTOR;
|
|
197
|
+
let css = `/* Brand "${brandId(brand)}": only the variables it overrides. Load after twreact-ui's CSS. */\n`;
|
|
198
|
+
if (Object.keys(light).length) css += block(lightSel, light);
|
|
199
|
+
if (Object.keys(dark).length) css += '\n' + block(darkSel, dark);
|
|
200
|
+
return css;
|
|
201
|
+
}
|
|
202
|
+
|
|
84
203
|
const varName = (name, step) => (step ? `--dt-${name}-${step}` : `--dt-${name}`);
|
|
85
204
|
const colorRef = (name, step) => `rgb(var(${varName(name, step)}) / <alpha-value>)`;
|
|
86
205
|
|
|
@@ -118,6 +237,11 @@ module.exports = {
|
|
|
118
237
|
loadTokens,
|
|
119
238
|
loadBrand,
|
|
120
239
|
listBrands,
|
|
240
|
+
validateBrand,
|
|
241
|
+
brandSchema,
|
|
242
|
+
brandId,
|
|
243
|
+
brandLabel,
|
|
244
|
+
brandCss,
|
|
121
245
|
applyBrand,
|
|
122
246
|
hexToChannels,
|
|
123
247
|
scales,
|