@tensor_1/react 2.12.0 → 2.14.2
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 +16 -0
- package/dist/calendar-model.d.ts +31 -0
- package/dist/calendar-model.js +89 -0
- package/dist/color-model.d.ts +20 -0
- package/dist/color-model.js +74 -0
- package/dist/components/BrandProfiles.d.ts +45 -0
- package/dist/components/BrandProfiles.js +150 -0
- package/dist/components/BuilderCanvas.d.ts +34 -0
- package/dist/components/BuilderCanvas.js +117 -0
- package/dist/components/ColorPicker.d.ts +17 -0
- package/dist/components/ColorPicker.js +158 -0
- package/dist/components/DateRangePicker.d.ts +23 -0
- package/dist/components/DateRangePicker.js +123 -0
- package/dist/components/DiffViewer.d.ts +18 -0
- package/dist/components/DiffViewer.js +93 -0
- package/dist/components/LicensePositionBar.d.ts +17 -0
- package/dist/components/LicensePositionBar.js +50 -0
- package/dist/components/MatchReview.d.ts +39 -0
- package/dist/components/MatchReview.js +120 -0
- package/dist/components/MetricInput.d.ts +23 -0
- package/dist/components/MetricInput.js +92 -0
- package/dist/components/RelationshipGraph.d.ts +30 -0
- package/dist/components/RelationshipGraph.js +138 -0
- package/dist/components/RuleBuilder.d.ts +45 -0
- package/dist/components/RuleBuilder.js +163 -0
- package/dist/components/SchedulingCalendar.d.ts +42 -0
- package/dist/components/SchedulingCalendar.js +124 -0
- package/dist/diff-model.d.ts +23 -0
- package/dist/diff-model.js +50 -0
- package/dist/index.cjs +1753 -2
- package/dist/index.d.cts +439 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.js +91 -1
- package/dist/json-value.d.ts +10 -0
- package/dist/json-value.js +19 -0
- package/dist/metric-model.d.ts +9 -0
- package/dist/metric-model.js +25 -0
- package/dist/relationship-graph-model.d.ts +31 -0
- package/dist/relationship-graph-model.js +68 -0
- package/dist/rule-builder-model.d.ts +41 -0
- package/dist/rule-builder-model.js +101 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,4 +40,20 @@ import { Chart } from "@tensor_1/react/chart/preset";
|
|
|
40
40
|
Large fixed-height tables can use `useVirtualRows(rows, { rowHeight })`; attach its `containerRef` to
|
|
41
41
|
`TableWrap` and render the returned row window between spacer rows.
|
|
42
42
|
|
|
43
|
+
## UIX-V3 capability components
|
|
44
|
+
|
|
45
|
+
Phase 46.9 adds eleven controlled, domain-neutral components:
|
|
46
|
+
|
|
47
|
+
- Authoring: `RuleBuilder` and `BuilderCanvas`.
|
|
48
|
+
- Time: `SchedulingCalendar` and `DateRangePicker`.
|
|
49
|
+
- Relationships and review: `RelationshipGraph` and `MatchReview`.
|
|
50
|
+
- Configuration: `MetricInput`, `LicensePositionBar`, `BrandProfiles` (also
|
|
51
|
+
`BrandProfileEditor`), `DiffViewer`, and `ColorPicker`.
|
|
52
|
+
|
|
53
|
+
Their serializable helpers are exported from the main entry as well: immutable
|
|
54
|
+
rule-tree operations, calendar/time-zone helpers, bounded graph layout and
|
|
55
|
+
traversal, three-way JSON diffing, metric parsing/stepping, color conversion and
|
|
56
|
+
contrast checks, plus brand-profile apply/restore helpers. No graph or date
|
|
57
|
+
runtime dependency is required.
|
|
58
|
+
|
|
43
59
|
Ships **ESM + CJS + types**, with per-file `"use client"` so it's safe under React Server Components. Part of the **[UIx v2 styleguide](https://github.com/COx-ORG-H/UIx)**.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface CalendarDay {
|
|
2
|
+
date: string;
|
|
3
|
+
day: number;
|
|
4
|
+
inMonth: boolean;
|
|
5
|
+
weekday: number;
|
|
6
|
+
}
|
|
7
|
+
interface DateRangeValue {
|
|
8
|
+
start?: string;
|
|
9
|
+
end?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ZonedDateSpan {
|
|
12
|
+
start: string;
|
|
13
|
+
end: string;
|
|
14
|
+
}
|
|
15
|
+
declare function toDateKey(date: Date): string;
|
|
16
|
+
declare function addCalendarDays(value: string, amount: number): string;
|
|
17
|
+
declare function addCalendarMonths(value: string, amount: number): string;
|
|
18
|
+
declare function startOfMonth(value: string): string;
|
|
19
|
+
declare function buildMonthGrid(month: string, weekStartsOn?: 0 | 1): CalendarDay[];
|
|
20
|
+
declare function zonedDateKey(instant: string | Date, timeZone: string): string;
|
|
21
|
+
declare function zonedDateSpan(start: string, end: string, timeZone: string): ZonedDateSpan;
|
|
22
|
+
declare function enumerateDateSpan(span: ZonedDateSpan, limit?: number): string[];
|
|
23
|
+
declare function selectRangeDate(current: DateRangeValue, date: string): DateRangeValue;
|
|
24
|
+
declare function isDateInRange(date: string, value: DateRangeValue): boolean;
|
|
25
|
+
declare function isDateUnavailable(date: string, options: {
|
|
26
|
+
min?: string;
|
|
27
|
+
max?: string;
|
|
28
|
+
isDisabled?: (date: string) => boolean;
|
|
29
|
+
}): boolean;
|
|
30
|
+
|
|
31
|
+
export { type CalendarDay, type DateRangeValue, type ZonedDateSpan, addCalendarDays, addCalendarMonths, buildMonthGrid, enumerateDateSpan, isDateInRange, isDateUnavailable, selectRangeDate, startOfMonth, toDateKey, zonedDateKey, zonedDateSpan };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
function fromDateKey(value) {
|
|
2
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
3
|
+
if (!match) throw new TypeError(`Invalid ISO date: ${value}`);
|
|
4
|
+
return new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
5
|
+
}
|
|
6
|
+
function toDateKey(date) {
|
|
7
|
+
return `${date.getUTCFullYear().toString().padStart(4, "0")}-${(date.getUTCMonth() + 1).toString().padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`;
|
|
8
|
+
}
|
|
9
|
+
function addCalendarDays(value, amount) {
|
|
10
|
+
const date = fromDateKey(value);
|
|
11
|
+
date.setUTCDate(date.getUTCDate() + amount);
|
|
12
|
+
return toDateKey(date);
|
|
13
|
+
}
|
|
14
|
+
function addCalendarMonths(value, amount) {
|
|
15
|
+
const date = fromDateKey(value);
|
|
16
|
+
const day = date.getUTCDate();
|
|
17
|
+
date.setUTCDate(1);
|
|
18
|
+
date.setUTCMonth(date.getUTCMonth() + amount);
|
|
19
|
+
const last = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate();
|
|
20
|
+
date.setUTCDate(Math.min(day, last));
|
|
21
|
+
return toDateKey(date);
|
|
22
|
+
}
|
|
23
|
+
function startOfMonth(value) {
|
|
24
|
+
return `${value.slice(0, 7)}-01`;
|
|
25
|
+
}
|
|
26
|
+
function buildMonthGrid(month, weekStartsOn = 1) {
|
|
27
|
+
const first = fromDateKey(startOfMonth(month));
|
|
28
|
+
const offset = (first.getUTCDay() - weekStartsOn + 7) % 7;
|
|
29
|
+
const start = addCalendarDays(toDateKey(first), -offset);
|
|
30
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
31
|
+
const date = addCalendarDays(start, index);
|
|
32
|
+
const parsed = fromDateKey(date);
|
|
33
|
+
return {
|
|
34
|
+
date,
|
|
35
|
+
day: parsed.getUTCDate(),
|
|
36
|
+
inMonth: date.slice(0, 7) === month.slice(0, 7),
|
|
37
|
+
weekday: parsed.getUTCDay()
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function zonedDateKey(instant, timeZone) {
|
|
42
|
+
const date = instant instanceof Date ? instant : new Date(instant);
|
|
43
|
+
if (Number.isNaN(date.getTime())) throw new TypeError(`Invalid instant: ${String(instant)}`);
|
|
44
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
45
|
+
timeZone,
|
|
46
|
+
year: "numeric",
|
|
47
|
+
month: "2-digit",
|
|
48
|
+
day: "2-digit"
|
|
49
|
+
}).formatToParts(date);
|
|
50
|
+
const get = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
51
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
52
|
+
}
|
|
53
|
+
function zonedDateSpan(start, end, timeZone) {
|
|
54
|
+
const startKey = zonedDateKey(start, timeZone);
|
|
55
|
+
const endKey = zonedDateKey(end, timeZone);
|
|
56
|
+
return startKey <= endKey ? { start: startKey, end: endKey } : { start: endKey, end: startKey };
|
|
57
|
+
}
|
|
58
|
+
function enumerateDateSpan(span, limit = 370) {
|
|
59
|
+
const dates = [];
|
|
60
|
+
let current = span.start;
|
|
61
|
+
while (current <= span.end && dates.length < limit) {
|
|
62
|
+
dates.push(current);
|
|
63
|
+
current = addCalendarDays(current, 1);
|
|
64
|
+
}
|
|
65
|
+
return dates;
|
|
66
|
+
}
|
|
67
|
+
function selectRangeDate(current, date) {
|
|
68
|
+
if (!current.start || current.end) return { start: date };
|
|
69
|
+
return date < current.start ? { start: date, end: current.start } : { start: current.start, end: date };
|
|
70
|
+
}
|
|
71
|
+
function isDateInRange(date, value) {
|
|
72
|
+
return !!value.start && !!value.end && date >= value.start && date <= value.end;
|
|
73
|
+
}
|
|
74
|
+
function isDateUnavailable(date, options) {
|
|
75
|
+
return !!options.min && date < options.min || !!options.max && date > options.max || !!options.isDisabled?.(date);
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
addCalendarDays,
|
|
79
|
+
addCalendarMonths,
|
|
80
|
+
buildMonthGrid,
|
|
81
|
+
enumerateDateSpan,
|
|
82
|
+
isDateInRange,
|
|
83
|
+
isDateUnavailable,
|
|
84
|
+
selectRangeDate,
|
|
85
|
+
startOfMonth,
|
|
86
|
+
toDateKey,
|
|
87
|
+
zonedDateKey,
|
|
88
|
+
zonedDateSpan
|
|
89
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface RgbColor {
|
|
2
|
+
r: number;
|
|
3
|
+
g: number;
|
|
4
|
+
b: number;
|
|
5
|
+
}
|
|
6
|
+
interface HsvColor {
|
|
7
|
+
h: number;
|
|
8
|
+
s: number;
|
|
9
|
+
v: number;
|
|
10
|
+
}
|
|
11
|
+
declare function normalizeHex(value: string): string | undefined;
|
|
12
|
+
declare function hexToRgb(value: string): RgbColor | undefined;
|
|
13
|
+
declare function rgbToHex({ r, g, b }: RgbColor): string;
|
|
14
|
+
declare function rgbToHsv({ r, g, b }: RgbColor): HsvColor;
|
|
15
|
+
declare function hsvToRgb({ h, s, v }: HsvColor): RgbColor;
|
|
16
|
+
declare function hsvToHex(value: HsvColor): string;
|
|
17
|
+
declare function contrastRatio(background: string, foreground: string): number | undefined;
|
|
18
|
+
declare function meetsContrast(background: string, foreground: string, minimum?: number): boolean;
|
|
19
|
+
|
|
20
|
+
export { type HsvColor, type RgbColor, contrastRatio, hexToRgb, hsvToHex, hsvToRgb, meetsContrast, normalizeHex, rgbToHex, rgbToHsv };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
2
|
+
function normalizeHex(value) {
|
|
3
|
+
const raw = value.trim().replace(/^#/, "");
|
|
4
|
+
if (/^[\da-f]{3}$/i.test(raw)) return `#${raw.split("").map((char) => char + char).join("").toUpperCase()}`;
|
|
5
|
+
if (/^[\da-f]{6}$/i.test(raw)) return `#${raw.toUpperCase()}`;
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
function hexToRgb(value) {
|
|
9
|
+
const hex = normalizeHex(value);
|
|
10
|
+
if (!hex) return void 0;
|
|
11
|
+
return {
|
|
12
|
+
r: Number.parseInt(hex.slice(1, 3), 16),
|
|
13
|
+
g: Number.parseInt(hex.slice(3, 5), 16),
|
|
14
|
+
b: Number.parseInt(hex.slice(5, 7), 16)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function rgbToHex({ r, g, b }) {
|
|
18
|
+
return `#${[r, g, b].map((value) => Math.round(clamp(value, 0, 255)).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
|
|
19
|
+
}
|
|
20
|
+
function rgbToHsv({ r, g, b }) {
|
|
21
|
+
const values = [r, g, b].map((value) => clamp(value, 0, 255) / 255);
|
|
22
|
+
const max = Math.max(...values);
|
|
23
|
+
const min = Math.min(...values);
|
|
24
|
+
const delta = max - min;
|
|
25
|
+
let h = 0;
|
|
26
|
+
if (delta !== 0) {
|
|
27
|
+
if (max === values[0]) h = 60 * ((values[1] - values[2]) / delta % 6);
|
|
28
|
+
else if (max === values[1]) h = 60 * ((values[2] - values[0]) / delta + 2);
|
|
29
|
+
else h = 60 * ((values[0] - values[1]) / delta + 4);
|
|
30
|
+
}
|
|
31
|
+
if (h < 0) h += 360;
|
|
32
|
+
return { h, s: max === 0 ? 0 : delta / max, v: max };
|
|
33
|
+
}
|
|
34
|
+
function hsvToRgb({ h, s, v }) {
|
|
35
|
+
const hue = (h % 360 + 360) % 360;
|
|
36
|
+
const sat = clamp(s, 0, 1);
|
|
37
|
+
const value = clamp(v, 0, 1);
|
|
38
|
+
const chroma = value * sat;
|
|
39
|
+
const x = chroma * (1 - Math.abs(hue / 60 % 2 - 1));
|
|
40
|
+
const m = value - chroma;
|
|
41
|
+
const sector = Math.floor(hue / 60);
|
|
42
|
+
const rgb = sector === 0 ? [chroma, x, 0] : sector === 1 ? [x, chroma, 0] : sector === 2 ? [0, chroma, x] : sector === 3 ? [0, x, chroma] : sector === 4 ? [x, 0, chroma] : [chroma, 0, x];
|
|
43
|
+
return { r: (rgb[0] + m) * 255, g: (rgb[1] + m) * 255, b: (rgb[2] + m) * 255 };
|
|
44
|
+
}
|
|
45
|
+
function hsvToHex(value) {
|
|
46
|
+
return rgbToHex(hsvToRgb(value));
|
|
47
|
+
}
|
|
48
|
+
function relativeLuminance({ r, g, b }) {
|
|
49
|
+
const linear = [r, g, b].map((channel) => {
|
|
50
|
+
const value = channel / 255;
|
|
51
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
52
|
+
});
|
|
53
|
+
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
|
54
|
+
}
|
|
55
|
+
function contrastRatio(background, foreground) {
|
|
56
|
+
const bg = hexToRgb(background);
|
|
57
|
+
const fg = hexToRgb(foreground);
|
|
58
|
+
if (!bg || !fg) return void 0;
|
|
59
|
+
const [light, dark] = [relativeLuminance(bg), relativeLuminance(fg)].sort((a, b) => b - a);
|
|
60
|
+
return (light + 0.05) / (dark + 0.05);
|
|
61
|
+
}
|
|
62
|
+
function meetsContrast(background, foreground, minimum = 4.5) {
|
|
63
|
+
return (contrastRatio(background, foreground) ?? 0) >= minimum;
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
contrastRatio,
|
|
67
|
+
hexToRgb,
|
|
68
|
+
hsvToHex,
|
|
69
|
+
hsvToRgb,
|
|
70
|
+
meetsContrast,
|
|
71
|
+
normalizeHex,
|
|
72
|
+
rgbToHex,
|
|
73
|
+
rgbToHsv
|
|
74
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface BrandProfileTypography {
|
|
4
|
+
sans?: string;
|
|
5
|
+
heading?: string;
|
|
6
|
+
mono?: string;
|
|
7
|
+
}
|
|
8
|
+
interface BrandProfileLogo {
|
|
9
|
+
src: string;
|
|
10
|
+
alt: string;
|
|
11
|
+
}
|
|
12
|
+
interface BrandProfile {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
brand: string;
|
|
16
|
+
brandForeground: string;
|
|
17
|
+
typography?: BrandProfileTypography;
|
|
18
|
+
logo?: BrandProfileLogo;
|
|
19
|
+
}
|
|
20
|
+
interface AppliedBrandProfileSnapshot {
|
|
21
|
+
profileId?: string;
|
|
22
|
+
properties: Record<string, string>;
|
|
23
|
+
logoSrc?: string;
|
|
24
|
+
logoAlt?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Apply only the existing UIx brand/font slots and readable logo metadata. */
|
|
27
|
+
declare function applyBrandProfile(profile: BrandProfile, target?: HTMLElement | null): AppliedBrandProfileSnapshot | undefined;
|
|
28
|
+
declare function restoreBrandProfile(snapshot: AppliedBrandProfileSnapshot, target?: HTMLElement | null): void;
|
|
29
|
+
interface BrandProfileEditorProps {
|
|
30
|
+
value: BrandProfile;
|
|
31
|
+
onChange: (profile: BrandProfile) => void;
|
|
32
|
+
profiles?: BrandProfile[];
|
|
33
|
+
onSelectProfile?: (profile: BrandProfile) => void;
|
|
34
|
+
onApply?: (profile: BrandProfile) => void;
|
|
35
|
+
applyTarget?: HTMLElement | null;
|
|
36
|
+
onLogoFile?: (file: File) => void;
|
|
37
|
+
colorPresets?: string[];
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Small controlled editor composition for serializable UIx brand profiles. */
|
|
41
|
+
declare function BrandProfileEditor({ value, onChange, profiles, onSelectProfile, onApply, applyTarget, onLogoFile, colorPresets, className, }: BrandProfileEditorProps): react.JSX.Element;
|
|
42
|
+
/** Canonical Phase 46.9 export; equivalent to BrandProfileEditor. */
|
|
43
|
+
declare const BrandProfiles: typeof BrandProfileEditor;
|
|
44
|
+
|
|
45
|
+
export { type AppliedBrandProfileSnapshot, type BrandProfile, BrandProfileEditor, type BrandProfileEditorProps, type BrandProfileLogo, type BrandProfileTypography, BrandProfiles, applyBrandProfile, restoreBrandProfile };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { normalizeHex } from "../color-model.js";
|
|
6
|
+
import { ColorPicker } from "./ColorPicker.js";
|
|
7
|
+
const PROFILE_PROPERTIES = {
|
|
8
|
+
brand: "--uix-brand",
|
|
9
|
+
brandForeground: "--uix-brand-fg",
|
|
10
|
+
sans: "--uix-font-sans",
|
|
11
|
+
heading: "--uix-font-heading",
|
|
12
|
+
mono: "--uix-font-mono"
|
|
13
|
+
};
|
|
14
|
+
const EMPTY_PROFILES = [];
|
|
15
|
+
const EMPTY_COLORS = [];
|
|
16
|
+
function applyBrandProfile(profile, target) {
|
|
17
|
+
const element = target ?? (typeof document === "undefined" ? void 0 : document.documentElement);
|
|
18
|
+
if (!element) return void 0;
|
|
19
|
+
const brand = normalizeHex(profile.brand);
|
|
20
|
+
const foreground = normalizeHex(profile.brandForeground);
|
|
21
|
+
if (!brand || !foreground) throw new TypeError("Brand profiles require valid hex brand and foreground colors.");
|
|
22
|
+
const properties = {};
|
|
23
|
+
Object.values(PROFILE_PROPERTIES).forEach((property) => {
|
|
24
|
+
properties[property] = element.style.getPropertyValue(property);
|
|
25
|
+
});
|
|
26
|
+
const snapshot = {
|
|
27
|
+
profileId: element.dataset.uixBrandProfile,
|
|
28
|
+
properties,
|
|
29
|
+
logoSrc: element.dataset.uixBrandLogoSrc,
|
|
30
|
+
logoAlt: element.dataset.uixBrandLogoAlt
|
|
31
|
+
};
|
|
32
|
+
element.style.setProperty(PROFILE_PROPERTIES.brand, brand);
|
|
33
|
+
element.style.setProperty(PROFILE_PROPERTIES.brandForeground, foreground);
|
|
34
|
+
const typography = profile.typography ?? {};
|
|
35
|
+
for (const [key, property] of Object.entries({ sans: PROFILE_PROPERTIES.sans, heading: PROFILE_PROPERTIES.heading, mono: PROFILE_PROPERTIES.mono })) {
|
|
36
|
+
const value = typography[key];
|
|
37
|
+
if (value) element.style.setProperty(property, value);
|
|
38
|
+
else element.style.removeProperty(property);
|
|
39
|
+
}
|
|
40
|
+
element.dataset.uixBrandProfile = profile.id;
|
|
41
|
+
if (profile.logo) {
|
|
42
|
+
element.dataset.uixBrandLogoSrc = profile.logo.src;
|
|
43
|
+
element.dataset.uixBrandLogoAlt = profile.logo.alt;
|
|
44
|
+
} else {
|
|
45
|
+
delete element.dataset.uixBrandLogoSrc;
|
|
46
|
+
delete element.dataset.uixBrandLogoAlt;
|
|
47
|
+
}
|
|
48
|
+
return snapshot;
|
|
49
|
+
}
|
|
50
|
+
function restoreBrandProfile(snapshot, target) {
|
|
51
|
+
const element = target ?? (typeof document === "undefined" ? void 0 : document.documentElement);
|
|
52
|
+
if (!element) return;
|
|
53
|
+
Object.entries(snapshot.properties).forEach(([property, value]) => {
|
|
54
|
+
if (value) element.style.setProperty(property, value);
|
|
55
|
+
else element.style.removeProperty(property);
|
|
56
|
+
});
|
|
57
|
+
if (snapshot.profileId) element.dataset.uixBrandProfile = snapshot.profileId;
|
|
58
|
+
else delete element.dataset.uixBrandProfile;
|
|
59
|
+
if (snapshot.logoSrc) element.dataset.uixBrandLogoSrc = snapshot.logoSrc;
|
|
60
|
+
else delete element.dataset.uixBrandLogoSrc;
|
|
61
|
+
if (snapshot.logoAlt) element.dataset.uixBrandLogoAlt = snapshot.logoAlt;
|
|
62
|
+
else delete element.dataset.uixBrandLogoAlt;
|
|
63
|
+
}
|
|
64
|
+
function BrandProfileEditor({
|
|
65
|
+
value,
|
|
66
|
+
onChange,
|
|
67
|
+
profiles = EMPTY_PROFILES,
|
|
68
|
+
onSelectProfile,
|
|
69
|
+
onApply,
|
|
70
|
+
applyTarget,
|
|
71
|
+
onLogoFile,
|
|
72
|
+
colorPresets = EMPTY_COLORS,
|
|
73
|
+
className
|
|
74
|
+
}) {
|
|
75
|
+
const [announcement, setAnnouncement] = useState("");
|
|
76
|
+
const update = (patch) => onChange({ ...value, ...patch });
|
|
77
|
+
const apply = () => {
|
|
78
|
+
if (applyTarget !== null) applyBrandProfile(value, applyTarget);
|
|
79
|
+
onApply?.(value);
|
|
80
|
+
setAnnouncement(`${value.name} applied.`);
|
|
81
|
+
};
|
|
82
|
+
const onFile = (event) => {
|
|
83
|
+
const file = event.currentTarget.files?.[0];
|
|
84
|
+
if (file) onLogoFile?.(file);
|
|
85
|
+
};
|
|
86
|
+
const previewStyle = {
|
|
87
|
+
"--uix-brand": normalizeHex(value.brand) ?? value.brand,
|
|
88
|
+
"--uix-brand-fg": normalizeHex(value.brandForeground) ?? value.brandForeground,
|
|
89
|
+
...value.typography?.sans ? { "--uix-font-sans": value.typography.sans } : {},
|
|
90
|
+
...value.typography?.heading ? { "--uix-font-heading": value.typography.heading } : {}
|
|
91
|
+
};
|
|
92
|
+
return /* @__PURE__ */ jsxs("section", { className: cx("uix-brand-profiles", className), "aria-label": "Brand profile editor", children: [
|
|
93
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-brand-profiles__form", children: [
|
|
94
|
+
profiles.length > 0 && /* @__PURE__ */ jsxs("label", { className: "uix-field__label", children: [
|
|
95
|
+
"Profile",
|
|
96
|
+
/* @__PURE__ */ jsx("select", { className: "uix-select", value: value.id, onChange: (event) => {
|
|
97
|
+
const selected = profiles.find((profile) => profile.id === event.currentTarget.value);
|
|
98
|
+
if (selected) onSelectProfile?.(selected);
|
|
99
|
+
}, children: profiles.map((profile) => /* @__PURE__ */ jsx("option", { value: profile.id, children: profile.name }, profile.id)) })
|
|
100
|
+
] }),
|
|
101
|
+
/* @__PURE__ */ jsxs("label", { className: "uix-field__label", children: [
|
|
102
|
+
"Name",
|
|
103
|
+
/* @__PURE__ */ jsx("input", { className: "uix-input", value: value.name, onChange: (event) => update({ name: event.currentTarget.value }) })
|
|
104
|
+
] }),
|
|
105
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-brand-profiles__colors", children: [
|
|
106
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
107
|
+
"Brand",
|
|
108
|
+
/* @__PURE__ */ jsx(ColorPicker, { label: "Brand color", value: value.brand, foreground: value.brandForeground, presets: colorPresets, onChange: (brand) => update({ brand }) })
|
|
109
|
+
] }),
|
|
110
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
111
|
+
"On brand",
|
|
112
|
+
/* @__PURE__ */ jsx(ColorPicker, { label: "Brand foreground color", value: value.brandForeground, foreground: value.brand, presets: colorPresets, onChange: (brandForeground) => update({ brandForeground }) })
|
|
113
|
+
] })
|
|
114
|
+
] }),
|
|
115
|
+
/* @__PURE__ */ jsxs("label", { className: "uix-field__label", children: [
|
|
116
|
+
"Body font",
|
|
117
|
+
/* @__PURE__ */ jsx("input", { className: "uix-input", value: value.typography?.sans ?? "", onChange: (event) => update({ typography: { ...value.typography, sans: event.currentTarget.value || void 0 } }) })
|
|
118
|
+
] }),
|
|
119
|
+
/* @__PURE__ */ jsxs("label", { className: "uix-field__label", children: [
|
|
120
|
+
"Heading font",
|
|
121
|
+
/* @__PURE__ */ jsx("input", { className: "uix-input", value: value.typography?.heading ?? "", onChange: (event) => update({ typography: { ...value.typography, heading: event.currentTarget.value || void 0 } }) })
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ jsxs("label", { className: "uix-file-upload uix-brand-profiles__upload", children: [
|
|
124
|
+
"Logo file",
|
|
125
|
+
/* @__PURE__ */ jsx("input", { type: "file", accept: "image/*", onChange: onFile })
|
|
126
|
+
] }),
|
|
127
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--primary", onClick: apply, children: "Apply profile" }),
|
|
128
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", "aria-live": "polite", children: announcement })
|
|
129
|
+
] }),
|
|
130
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-brand-profiles__preview", style: previewStyle, "aria-label": `Live preview of ${value.name}`, children: [
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-brand-profiles__preview-header", children: [
|
|
132
|
+
value.logo ? /* @__PURE__ */ jsx("img", { src: value.logo.src, alt: value.logo.alt }) : /* @__PURE__ */ jsx("span", { className: "uix-brand-profiles__logo-placeholder", children: "Logo" }),
|
|
133
|
+
/* @__PURE__ */ jsx("strong", { children: value.name || "Brand preview" })
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-brand-profiles__preview-body", children: [
|
|
136
|
+
/* @__PURE__ */ jsx("h3", { children: "Readable, serializable branding" }),
|
|
137
|
+
/* @__PURE__ */ jsx("p", { children: "Accent, links, rings, and muted states continue through the existing UIx token chain." }),
|
|
138
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--primary", children: "Primary action" }),
|
|
139
|
+
/* @__PURE__ */ jsx("a", { href: "#brand-profile-preview", children: "Preview link" })
|
|
140
|
+
] })
|
|
141
|
+
] })
|
|
142
|
+
] });
|
|
143
|
+
}
|
|
144
|
+
const BrandProfiles = BrandProfileEditor;
|
|
145
|
+
export {
|
|
146
|
+
BrandProfileEditor,
|
|
147
|
+
BrandProfiles,
|
|
148
|
+
applyBrandProfile,
|
|
149
|
+
restoreBrandProfile
|
|
150
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface BuilderCanvasItem {
|
|
5
|
+
id: string;
|
|
6
|
+
type: string;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
properties?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
interface BuilderPaletteItem {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
create: () => BuilderCanvasItem;
|
|
16
|
+
}
|
|
17
|
+
interface BuilderCanvasProps {
|
|
18
|
+
items: BuilderCanvasItem[];
|
|
19
|
+
onItemsChange: (items: BuilderCanvasItem[]) => void;
|
|
20
|
+
palette: BuilderPaletteItem[];
|
|
21
|
+
selectedId?: string;
|
|
22
|
+
onSelect?: (id: string | undefined) => void;
|
|
23
|
+
renderItem?: (item: BuilderCanvasItem, selected: boolean) => ReactNode;
|
|
24
|
+
renderProperties?: (item: BuilderCanvasItem, onChange: (item: BuilderCanvasItem) => void) => ReactNode;
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
onRetry?: () => void;
|
|
28
|
+
emptyMessage?: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Palette + sortable canvas + property panel scaffold. Owns interaction, never persistence. */
|
|
32
|
+
declare function BuilderCanvas({ items, onItemsChange, palette, selectedId: controlledSelectedId, onSelect, renderItem, renderProperties, loading, error, onRetry, emptyMessage, className, }: BuilderCanvasProps): react.JSX.Element;
|
|
33
|
+
|
|
34
|
+
export { BuilderCanvas, type BuilderCanvasItem, type BuilderCanvasProps, type BuilderPaletteItem };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { reorder } from "../table-engine.js";
|
|
6
|
+
function BuilderCanvas({
|
|
7
|
+
items,
|
|
8
|
+
onItemsChange,
|
|
9
|
+
palette,
|
|
10
|
+
selectedId: controlledSelectedId,
|
|
11
|
+
onSelect,
|
|
12
|
+
renderItem,
|
|
13
|
+
renderProperties,
|
|
14
|
+
loading,
|
|
15
|
+
error,
|
|
16
|
+
onRetry,
|
|
17
|
+
emptyMessage = "Add an item from the palette to begin.",
|
|
18
|
+
className
|
|
19
|
+
}) {
|
|
20
|
+
const [internalSelectedId, setInternalSelectedId] = useState();
|
|
21
|
+
const [draggedId, setDraggedId] = useState();
|
|
22
|
+
const propertyHeadingRef = useRef(null);
|
|
23
|
+
const shouldFocusProperties = useRef(false);
|
|
24
|
+
const selectedId = controlledSelectedId ?? internalSelectedId;
|
|
25
|
+
const selected = items.find((item) => item.id === selectedId);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (shouldFocusProperties.current && selected) {
|
|
28
|
+
propertyHeadingRef.current?.focus();
|
|
29
|
+
shouldFocusProperties.current = false;
|
|
30
|
+
}
|
|
31
|
+
}, [selected]);
|
|
32
|
+
const select = (id, focusProperties = true) => {
|
|
33
|
+
shouldFocusProperties.current = focusProperties;
|
|
34
|
+
if (controlledSelectedId === void 0) setInternalSelectedId(id);
|
|
35
|
+
onSelect?.(id);
|
|
36
|
+
};
|
|
37
|
+
const move = (id, direction) => {
|
|
38
|
+
const index = items.findIndex((item) => item.id === id);
|
|
39
|
+
const target = index + direction;
|
|
40
|
+
if (index < 0 || target < 0 || target >= items.length) return;
|
|
41
|
+
onItemsChange(reorder(items, index, target));
|
|
42
|
+
};
|
|
43
|
+
const add = (paletteItem) => {
|
|
44
|
+
const item = paletteItem.create();
|
|
45
|
+
onItemsChange([...items, item]);
|
|
46
|
+
select(item.id);
|
|
47
|
+
};
|
|
48
|
+
const remove = (id) => {
|
|
49
|
+
onItemsChange(items.filter((item) => item.id !== id));
|
|
50
|
+
if (selectedId === id) select(void 0, false);
|
|
51
|
+
};
|
|
52
|
+
const drop = (event, targetId) => {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
if (!draggedId || draggedId === targetId) return;
|
|
55
|
+
const from = items.findIndex((item) => item.id === draggedId);
|
|
56
|
+
const to = items.findIndex((item) => item.id === targetId);
|
|
57
|
+
if (from >= 0 && to >= 0) onItemsChange(reorder(items, from, to));
|
|
58
|
+
setDraggedId(void 0);
|
|
59
|
+
};
|
|
60
|
+
return /* @__PURE__ */ jsxs("section", { className: cx("uix-builder-canvas", className), "aria-label": "Builder canvas", children: [
|
|
61
|
+
/* @__PURE__ */ jsxs("aside", { className: "uix-builder-canvas__palette", "aria-label": "Item palette", children: [
|
|
62
|
+
/* @__PURE__ */ jsx("h3", { children: "Palette" }),
|
|
63
|
+
palette.length === 0 ? /* @__PURE__ */ jsx("p", { children: "No item types available." }) : /* @__PURE__ */ jsx("ul", { children: palette.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => add(item), children: [
|
|
64
|
+
/* @__PURE__ */ jsx("strong", { children: item.label }),
|
|
65
|
+
item.description && /* @__PURE__ */ jsx("span", { children: item.description }),
|
|
66
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "Add" })
|
|
67
|
+
] }) }, item.id)) })
|
|
68
|
+
] }),
|
|
69
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-builder-canvas__workspace", children: [
|
|
70
|
+
/* @__PURE__ */ jsx("h3", { children: "Canvas" }),
|
|
71
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "uix-builder-canvas__state", role: "status", children: "Loading canvas\u2026" }) : error ? /* @__PURE__ */ jsxs("div", { className: "uix-builder-canvas__state", role: "alert", children: [
|
|
72
|
+
/* @__PURE__ */ jsx("p", { children: error }),
|
|
73
|
+
onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
|
|
74
|
+
] }) : items.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "uix-builder-canvas__state", children: [
|
|
75
|
+
/* @__PURE__ */ jsx("p", { children: emptyMessage }),
|
|
76
|
+
palette[0] && /* @__PURE__ */ jsxs("button", { type: "button", className: "uix-btn uix-btn--primary", onClick: () => add(palette[0]), children: [
|
|
77
|
+
"Add ",
|
|
78
|
+
palette[0].label
|
|
79
|
+
] })
|
|
80
|
+
] }) : /* @__PURE__ */ jsx("ol", { className: "uix-builder-canvas__items", children: items.map((item, index) => /* @__PURE__ */ jsxs(
|
|
81
|
+
"li",
|
|
82
|
+
{
|
|
83
|
+
draggable: true,
|
|
84
|
+
onDragStart: () => setDraggedId(item.id),
|
|
85
|
+
onDragEnd: () => setDraggedId(void 0),
|
|
86
|
+
onDragOver: (event) => event.preventDefault(),
|
|
87
|
+
onDrop: (event) => drop(event, item.id),
|
|
88
|
+
className: cx("uix-builder-canvas__item", selectedId === item.id && "uix-builder-canvas__item--selected"),
|
|
89
|
+
children: [
|
|
90
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-builder-canvas__item-main", "aria-pressed": selectedId === item.id, onClick: () => select(item.id), children: renderItem?.(item, selectedId === item.id) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
91
|
+
/* @__PURE__ */ jsx("strong", { children: item.label }),
|
|
92
|
+
item.description && /* @__PURE__ */ jsx("span", { children: item.description })
|
|
93
|
+
] }) }),
|
|
94
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-builder-canvas__item-actions", children: [
|
|
95
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => move(item.id, -1), disabled: index === 0, "aria-label": `Move ${item.label} up`, children: "Up" }),
|
|
96
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => move(item.id, 1), disabled: index === items.length - 1, "aria-label": `Move ${item.label} down`, children: "Down" }),
|
|
97
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => remove(item.id), "aria-label": `Remove ${item.label}`, children: "Remove" })
|
|
98
|
+
] })
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
item.id
|
|
102
|
+
)) })
|
|
103
|
+
] }),
|
|
104
|
+
/* @__PURE__ */ jsxs("aside", { className: "uix-builder-canvas__properties", "aria-label": "Item properties", children: [
|
|
105
|
+
/* @__PURE__ */ jsx("h3", { ref: propertyHeadingRef, tabIndex: -1, children: "Properties" }),
|
|
106
|
+
!selected ? /* @__PURE__ */ jsx("p", { children: "Select a canvas item to edit its properties." }) : renderProperties?.(selected, (updated) => onItemsChange(items.map((item) => item.id === updated.id ? updated : item))) ?? /* @__PURE__ */ jsxs("dl", { children: [
|
|
107
|
+
/* @__PURE__ */ jsx("dt", { children: "Type" }),
|
|
108
|
+
/* @__PURE__ */ jsx("dd", { children: selected.type }),
|
|
109
|
+
/* @__PURE__ */ jsx("dt", { children: "Label" }),
|
|
110
|
+
/* @__PURE__ */ jsx("dd", { children: selected.label })
|
|
111
|
+
] })
|
|
112
|
+
] })
|
|
113
|
+
] });
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
BuilderCanvas
|
|
117
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface ColorPickerProps {
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
foreground: string;
|
|
7
|
+
presets?: string[];
|
|
8
|
+
recent?: string[];
|
|
9
|
+
minimumContrast?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Token-friendly HSV/hex picker with a fully keyboard-operable path and contrast feedback. */
|
|
15
|
+
declare function ColorPicker({ value, onChange, foreground, presets, recent, minimumContrast, label, disabled, className, }: ColorPickerProps): react.JSX.Element;
|
|
16
|
+
|
|
17
|
+
export { ColorPicker, type ColorPickerProps };
|