@santi020k/lumen-core 0.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/LICENSE +21 -0
- package/README.md +62 -0
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/dist/code.d.ts +18 -0
- package/dist/code.d.ts.map +1 -0
- package/dist/code.js +136 -0
- package/dist/components.d.ts +29 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +132 -0
- package/dist/data.d.ts +58 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +144 -0
- package/dist/figma.d.ts +54 -0
- package/dist/figma.d.ts.map +1 -0
- package/dist/figma.js +192 -0
- package/dist/icons.d.ts +13 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +61 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/props.d.ts +10 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +5 -0
- package/dist/rich-text.d.ts +21 -0
- package/dist/rich-text.d.ts.map +1 -0
- package/dist/rich-text.js +30 -0
- package/dist/schedule.d.ts +49 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +98 -0
- package/dist/theme-builder.d.ts +32 -0
- package/dist/theme-builder.d.ts.map +1 -0
- package/dist/theme-builder.js +97 -0
- package/dist/theme.d.ts +53 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +328 -0
- package/dist/tokens.d.ts +55 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +54 -0
- package/package.json +97 -0
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const createScheduleSlots = (dates, events, resources = []) => {
|
|
2
|
+
const resourceList = resources.length ? resources : [undefined];
|
|
3
|
+
return dates.flatMap(date => resourceList.map(resource => ({
|
|
4
|
+
date,
|
|
5
|
+
events: events.filter(event => event.start.startsWith(date) &&
|
|
6
|
+
(!resource || event.resourceId === resource.id)),
|
|
7
|
+
...(resource ? { resource } : {})
|
|
8
|
+
})));
|
|
9
|
+
};
|
|
10
|
+
export const moveScheduleEvent = (events, eventId, nextStart, nextEnd, resourceId) => events.map(event => event.id === eventId
|
|
11
|
+
? {
|
|
12
|
+
...event,
|
|
13
|
+
end: nextEnd,
|
|
14
|
+
...(resourceId ? { resourceId } : {}),
|
|
15
|
+
start: nextStart
|
|
16
|
+
}
|
|
17
|
+
: event);
|
|
18
|
+
export const expandRecurringScheduleEvent = (event, count, intervalDays = 7) => Array.from({ length: count }, (_, index) => {
|
|
19
|
+
const start = new Date(event.start);
|
|
20
|
+
const end = new Date(event.end);
|
|
21
|
+
start.setDate(start.getDate() + index * intervalDays);
|
|
22
|
+
end.setDate(end.getDate() + index * intervalDays);
|
|
23
|
+
return {
|
|
24
|
+
...event,
|
|
25
|
+
end: end.toISOString(),
|
|
26
|
+
id: index === 0 ? event.id : `${event.id}-${index + 1}`,
|
|
27
|
+
start: start.toISOString()
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
export const scheduleEventsOverlap = (a, b) => a.id !== b.id &&
|
|
31
|
+
(!a.resourceId || !b.resourceId || a.resourceId === b.resourceId) &&
|
|
32
|
+
new Date(a.start).getTime() < new Date(b.end).getTime() &&
|
|
33
|
+
new Date(b.start).getTime() < new Date(a.end).getTime();
|
|
34
|
+
export const getScheduleConflicts = (events) => {
|
|
35
|
+
const conflicts = [];
|
|
36
|
+
for (const [index, event] of events.entries()) {
|
|
37
|
+
for (const nextEvent of events.slice(index + 1)) {
|
|
38
|
+
if (scheduleEventsOverlap(event, nextEvent)) {
|
|
39
|
+
conflicts.push({ a: event, b: nextEvent });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return conflicts;
|
|
44
|
+
};
|
|
45
|
+
export const canPlaceScheduleEvent = (events, event) => !events.some(nextEvent => scheduleEventsOverlap(event, nextEvent));
|
|
46
|
+
const clampTime = (value, min, max) => {
|
|
47
|
+
const minTime = min ? new Date(min).getTime() : undefined;
|
|
48
|
+
const maxTime = max ? new Date(max).getTime() : undefined;
|
|
49
|
+
return Math.min(maxTime ?? value, Math.max(minTime ?? value, value));
|
|
50
|
+
};
|
|
51
|
+
const snapTime = (value, snapMinutes = 15) => {
|
|
52
|
+
const snapMs = Math.max(1, snapMinutes) * 60_000;
|
|
53
|
+
return Math.round(value / snapMs) * snapMs;
|
|
54
|
+
};
|
|
55
|
+
export const resizeScheduleEvent = (event, nextDateTime, options = {}) => {
|
|
56
|
+
const edge = options.edge ?? 'end';
|
|
57
|
+
const start = new Date(event.start).getTime();
|
|
58
|
+
const end = new Date(event.end).getTime();
|
|
59
|
+
const next = clampTime(snapTime(new Date(nextDateTime).getTime(), options.snapMinutes), options.min, options.max);
|
|
60
|
+
const nextStart = edge === 'start' ? Math.min(next, end - 60_000) : start;
|
|
61
|
+
const nextEnd = edge === 'end' ? Math.max(next, start + 60_000) : end;
|
|
62
|
+
return {
|
|
63
|
+
...event,
|
|
64
|
+
end: new Date(nextEnd).toISOString(),
|
|
65
|
+
start: new Date(nextStart).toISOString()
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
export const resizeScheduleEvents = (events, eventId, nextDateTime, options = {}) => events.map(event => event.id === eventId
|
|
69
|
+
? resizeScheduleEvent(event, nextDateTime, options)
|
|
70
|
+
: event);
|
|
71
|
+
const isScheduleEvent = (value) => {
|
|
72
|
+
if (!value || typeof value !== 'object')
|
|
73
|
+
return false;
|
|
74
|
+
const event = value;
|
|
75
|
+
return typeof event.end === 'string' &&
|
|
76
|
+
typeof event.id === 'string' &&
|
|
77
|
+
typeof event.start === 'string' &&
|
|
78
|
+
typeof event.title === 'string' &&
|
|
79
|
+
(event.resourceId === undefined || typeof event.resourceId === 'string');
|
|
80
|
+
};
|
|
81
|
+
export const serializeScheduleEvents = (events) => JSON.stringify(events);
|
|
82
|
+
export const parseScheduleEvents = (value) => {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(value);
|
|
85
|
+
return Array.isArray(parsed) ? parsed.filter(isScheduleEvent) : [];
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
export const createScheduleStorageKey = (name) => `lumen:schedule:${name.trim().toLowerCase().replaceAll(/\s+/g, '-')}`;
|
|
92
|
+
export const saveScheduleEvents = (storage, key, events) => {
|
|
93
|
+
storage.setItem(key, serializeScheduleEvents(events));
|
|
94
|
+
};
|
|
95
|
+
export const loadScheduleEvents = (storage, key) => {
|
|
96
|
+
const value = storage.getItem(key);
|
|
97
|
+
return value ? parseScheduleEvents(value) : [];
|
|
98
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type LumenThemeTokens } from './theme.js';
|
|
2
|
+
export type LumenThemeBuilderExportFormat = 'css' | 'figma' | 'tokens';
|
|
3
|
+
export type LumenThemeBuilderMode = 'generated' | 'manual';
|
|
4
|
+
export type LumenThemeBuilderScheme = 'dark' | 'light';
|
|
5
|
+
export interface LumenThemeBuilderOptions {
|
|
6
|
+
accentHue?: number | string | null;
|
|
7
|
+
hue?: number | string | null;
|
|
8
|
+
mode?: string | null;
|
|
9
|
+
primaryColor?: string | null;
|
|
10
|
+
scheme?: string | null;
|
|
11
|
+
secondaryColor?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface LumenThemeBuilderResult {
|
|
14
|
+
accentHue: number;
|
|
15
|
+
hue: number;
|
|
16
|
+
mode: LumenThemeBuilderMode;
|
|
17
|
+
scheme: LumenThemeBuilderScheme;
|
|
18
|
+
tokens: LumenThemeTokens;
|
|
19
|
+
}
|
|
20
|
+
export declare const coerceThemeBuilderExportFormat: (value?: string | null) => LumenThemeBuilderExportFormat;
|
|
21
|
+
export declare const coerceThemeBuilderMode: (value?: string | null) => LumenThemeBuilderMode;
|
|
22
|
+
export declare const coerceThemeBuilderScheme: (value?: string | null) => LumenThemeBuilderScheme;
|
|
23
|
+
export declare const normalizeThemeBuilderHue: (value: number | string | null | undefined, fallback?: number) => number;
|
|
24
|
+
export declare const normalizeThemeBuilderHex: (value: string) => string | null;
|
|
25
|
+
export declare const themeBuilderHexToHsl: (value: string) => {
|
|
26
|
+
hue: number;
|
|
27
|
+
value: string;
|
|
28
|
+
} | null;
|
|
29
|
+
export declare const createThemeBuilderTokens: (options?: LumenThemeBuilderOptions) => LumenThemeBuilderResult;
|
|
30
|
+
export declare const exportThemeBuilderCss: (tokens: LumenThemeTokens, scheme: LumenThemeBuilderScheme) => string;
|
|
31
|
+
export declare const exportThemeBuilderValue: (tokens: LumenThemeTokens, scheme: LumenThemeBuilderScheme, format: LumenThemeBuilderExportFormat) => string;
|
|
32
|
+
//# sourceMappingURL=theme-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-builder.d.ts","sourceRoot":"","sources":["../src/theme-builder.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAA;AAEnB,MAAM,MAAM,6BAA6B,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtE,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,QAAQ,CAAA;AAE1D,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,OAAO,CAAA;AAEtD,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAClC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,qBAAqB,CAAA;IAC3B,MAAM,EAAE,uBAAuB,CAAA;IAC/B,MAAM,EAAE,gBAAgB,CAAA;CACzB;AAED,eAAO,MAAM,8BAA8B,GACzC,QAAQ,MAAM,GAAG,IAAI,KACpB,6BAIF,CAAA;AAED,eAAO,MAAM,sBAAsB,GACjC,QAAQ,MAAM,GAAG,IAAI,KACpB,qBAC0C,CAAA;AAE7C,eAAO,MAAM,wBAAwB,GACnC,QAAQ,MAAM,GAAG,IAAI,KACpB,uBACkC,CAAA;AAErC,eAAO,MAAM,wBAAwB,GACnC,OAAO,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EACzC,iBAAY,KACX,MAMF,CAAA;AAED,eAAO,MAAM,wBAAwB,GAAI,OAAO,MAAM,KAAG,MAAM,GAAG,IAQjE,CAAA;AAED,eAAO,MAAM,oBAAoB,GAC/B,OAAO,MAAM,KACZ;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAiCnC,CAAA;AAED,eAAO,MAAM,wBAAwB,GACnC,UAAS,wBAA6B,KACrC,uBAuCF,CAAA;AAED,eAAO,MAAM,qBAAqB,GAChC,QAAQ,gBAAgB,EACxB,QAAQ,uBAAuB,KAC9B,MAA8E,CAAA;AAEjF,eAAO,MAAM,uBAAuB,GAClC,QAAQ,gBAAgB,EACxB,QAAQ,uBAAuB,EAC/B,QAAQ,6BAA6B,KACpC,MAaF,CAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* eslint-disable complexity */
|
|
2
|
+
import { exportThemeDesignTokens, exportThemeFigmaVariables } from './figma.js';
|
|
3
|
+
import { createThemeFromHue, exportThemeCss } from './theme.js';
|
|
4
|
+
export const coerceThemeBuilderExportFormat = (value) => {
|
|
5
|
+
if (value === 'figma' || value === 'tokens')
|
|
6
|
+
return value;
|
|
7
|
+
return 'css';
|
|
8
|
+
};
|
|
9
|
+
export const coerceThemeBuilderMode = (value) => value === 'manual' ? 'manual' : 'generated';
|
|
10
|
+
export const coerceThemeBuilderScheme = (value) => value === 'dark' ? 'dark' : 'light';
|
|
11
|
+
export const normalizeThemeBuilderHue = (value, fallback = 0) => {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
if (!Number.isFinite(parsed))
|
|
14
|
+
return fallback;
|
|
15
|
+
return ((Math.round(parsed) % 360) + 360) % 360;
|
|
16
|
+
};
|
|
17
|
+
export const normalizeThemeBuilderHex = (value) => {
|
|
18
|
+
const hex = value.trim().replace(/^#/, '');
|
|
19
|
+
if (/^[\da-f]{3}$/i.test(hex)) {
|
|
20
|
+
return `#${hex.charAt(0)}${hex.charAt(0)}${hex.charAt(1)}${hex.charAt(1)}${hex.charAt(2)}${hex.charAt(2)}`.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
return /^[\da-f]{6}$/i.test(hex) ? `#${hex.toLowerCase()}` : null;
|
|
23
|
+
};
|
|
24
|
+
export const themeBuilderHexToHsl = (value) => {
|
|
25
|
+
const normalized = normalizeThemeBuilderHex(value);
|
|
26
|
+
if (!normalized)
|
|
27
|
+
return null;
|
|
28
|
+
const red = Number.parseInt(normalized.slice(1, 3), 16) / 255;
|
|
29
|
+
const green = Number.parseInt(normalized.slice(3, 5), 16) / 255;
|
|
30
|
+
const blue = Number.parseInt(normalized.slice(5, 7), 16) / 255;
|
|
31
|
+
const max = Math.max(red, green, blue);
|
|
32
|
+
const min = Math.min(red, green, blue);
|
|
33
|
+
const lightness = (max + min) / 2;
|
|
34
|
+
const delta = max - min;
|
|
35
|
+
let hue = 0;
|
|
36
|
+
let saturation = 0;
|
|
37
|
+
if (delta !== 0) {
|
|
38
|
+
saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
|
39
|
+
if (max === red) {
|
|
40
|
+
hue = 60 * (((green - blue) / delta) % 6);
|
|
41
|
+
}
|
|
42
|
+
else if (max === green) {
|
|
43
|
+
hue = 60 * ((blue - red) / delta + 2);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
hue = 60 * ((red - green) / delta + 4);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const normalizedHue = normalizeThemeBuilderHue(hue);
|
|
50
|
+
return {
|
|
51
|
+
hue: normalizedHue,
|
|
52
|
+
value: `${normalizedHue} ${Math.round(saturation * 100)}% ${Math.round(lightness * 100)}%`
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export const createThemeBuilderTokens = (options = {}) => {
|
|
56
|
+
const hue = normalizeThemeBuilderHue(options.hue);
|
|
57
|
+
const accentHue = normalizeThemeBuilderHue(options.accentHue, normalizeThemeBuilderHue(hue + 150));
|
|
58
|
+
const mode = coerceThemeBuilderMode(options.mode);
|
|
59
|
+
const scheme = coerceThemeBuilderScheme(options.scheme);
|
|
60
|
+
const primary = options.primaryColor ? themeBuilderHexToHsl(options.primaryColor) : null;
|
|
61
|
+
const secondary = options.secondaryColor ? themeBuilderHexToHsl(options.secondaryColor) : null;
|
|
62
|
+
const baseHue = mode === 'manual' ? primary?.hue ?? hue : hue;
|
|
63
|
+
const baseAccentHue = mode === 'manual' ? secondary?.hue ?? accentHue : accentHue;
|
|
64
|
+
const tokens = createThemeFromHue(baseHue, {
|
|
65
|
+
accentHue: baseAccentHue,
|
|
66
|
+
scheme
|
|
67
|
+
});
|
|
68
|
+
if (mode === 'manual') {
|
|
69
|
+
if (primary) {
|
|
70
|
+
tokens.brand = primary.value;
|
|
71
|
+
tokens['brand-solid'] = primary.value;
|
|
72
|
+
}
|
|
73
|
+
if (secondary) {
|
|
74
|
+
tokens.accent = secondary.value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
accentHue: baseAccentHue,
|
|
79
|
+
hue: baseHue,
|
|
80
|
+
mode,
|
|
81
|
+
scheme,
|
|
82
|
+
tokens
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export const exportThemeBuilderCss = (tokens, scheme) => exportThemeCss(tokens).replace('{', `{\n color-scheme: ${scheme};`);
|
|
86
|
+
export const exportThemeBuilderValue = (tokens, scheme, format) => {
|
|
87
|
+
if (format === 'figma') {
|
|
88
|
+
return JSON.stringify(exportThemeFigmaVariables(tokens, {
|
|
89
|
+
collectionName: 'Lumen',
|
|
90
|
+
modeName: scheme === 'dark' ? 'Dark' : 'Light'
|
|
91
|
+
}), null, 2);
|
|
92
|
+
}
|
|
93
|
+
if (format === 'tokens') {
|
|
94
|
+
return JSON.stringify(exportThemeDesignTokens(tokens), null, 2);
|
|
95
|
+
}
|
|
96
|
+
return exportThemeBuilderCss(tokens, scheme);
|
|
97
|
+
};
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type LumenThemeTokens = Record<string, string>;
|
|
2
|
+
export interface LumenContrastScore {
|
|
3
|
+
ratio: number;
|
|
4
|
+
wcagAA: boolean;
|
|
5
|
+
wcagAAA: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface LumenThemeContrastSuggestion {
|
|
8
|
+
background: string;
|
|
9
|
+
foreground: string;
|
|
10
|
+
ratio: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const lumenSemanticColorTokenNames: readonly ["canvas", "surface", "surface-muted", "surface-strong", "line", "ink", "ink-soft", "ink-muted", "brand", "brand-solid", "brand-soft", "accent", "success", "warning", "danger"];
|
|
13
|
+
export declare const lumenGlassColorTokenNames: readonly ["glass-bg", "glass-bg-subtle", "glass-bg-strong", "glass-border", "glass-highlight", "glass-edge", "glass-edge-soft", "glass-shade", "glass-refraction"];
|
|
14
|
+
export declare const lumenGlassEffectTokenNames: readonly ["glass-blur", "glass-saturate", "glass-brightness", "glass-shadow"];
|
|
15
|
+
export declare const lumenGlassTokenNames: readonly ["glass-bg", "glass-bg-subtle", "glass-bg-strong", "glass-border", "glass-highlight", "glass-blur", "glass-saturate", "glass-brightness", "glass-edge", "glass-edge-soft", "glass-shade", "glass-refraction", "glass-shadow"];
|
|
16
|
+
export declare const lumenRadiusTokenNames: readonly ["ui-radius-sm", "ui-radius", "ui-radius-lg"];
|
|
17
|
+
export declare const lumenShadowTokenNames: readonly ["ui-shadow-sm", "ui-shadow-md", "ui-shadow-lg"];
|
|
18
|
+
export declare const lumenTypographyTokenNames: readonly ["ui-font"];
|
|
19
|
+
export declare const lumenMotionTokenNames: readonly ["ui-duration-fast", "ui-duration", "ui-duration-slow", "ui-ease", "ui-ease-emphasized"];
|
|
20
|
+
export declare const lumenStructureTokenNames: readonly ["ui-radius-sm", "ui-radius", "ui-radius-lg", "ui-shadow-sm", "ui-shadow-md", "ui-shadow-lg", "ui-font", "ui-duration-fast", "ui-duration", "ui-duration-slow", "ui-ease", "ui-ease-emphasized"];
|
|
21
|
+
export declare const lumenColorTokenNames: readonly ["canvas", "surface", "surface-muted", "surface-strong", "line", "ink", "ink-soft", "ink-muted", "brand", "brand-solid", "brand-soft", "accent", "success", "warning", "danger", "glass-bg", "glass-bg-subtle", "glass-bg-strong", "glass-border", "glass-highlight", "glass-edge", "glass-edge-soft", "glass-shade", "glass-refraction"];
|
|
22
|
+
export declare const lumenTokenNames: readonly ["canvas", "surface", "surface-muted", "surface-strong", "line", "ink", "ink-soft", "ink-muted", "brand", "brand-solid", "brand-soft", "accent", "success", "warning", "danger", "glass-bg", "glass-bg-subtle", "glass-bg-strong", "glass-border", "glass-highlight", "glass-blur", "glass-saturate", "glass-brightness", "glass-edge", "glass-edge-soft", "glass-shade", "glass-refraction", "glass-shadow", "ui-radius-sm", "ui-radius", "ui-radius-lg", "ui-shadow-sm", "ui-shadow-md", "ui-shadow-lg", "ui-font", "ui-duration-fast", "ui-duration", "ui-duration-slow", "ui-ease", "ui-ease-emphasized"];
|
|
23
|
+
export type LumenSemanticColorTokenName = typeof lumenSemanticColorTokenNames[number];
|
|
24
|
+
export type LumenGlassColorTokenName = typeof lumenGlassColorTokenNames[number];
|
|
25
|
+
export type LumenGlassEffectTokenName = typeof lumenGlassEffectTokenNames[number];
|
|
26
|
+
export type LumenGlassTokenName = typeof lumenGlassTokenNames[number];
|
|
27
|
+
export type LumenColorTokenName = typeof lumenColorTokenNames[number];
|
|
28
|
+
export type LumenRadiusTokenName = typeof lumenRadiusTokenNames[number];
|
|
29
|
+
export type LumenShadowTokenName = typeof lumenShadowTokenNames[number];
|
|
30
|
+
export type LumenTypographyTokenName = typeof lumenTypographyTokenNames[number];
|
|
31
|
+
export type LumenMotionTokenName = typeof lumenMotionTokenNames[number];
|
|
32
|
+
export type LumenStructureTokenName = typeof lumenStructureTokenNames[number];
|
|
33
|
+
export type LumenThemeTokenName = typeof lumenTokenNames[number];
|
|
34
|
+
export declare const exportThemeCss: (tokens: LumenThemeTokens, selector?: string) => string;
|
|
35
|
+
export declare const parseThemeCss: (css: string) => LumenThemeTokens;
|
|
36
|
+
export declare const createThemePalette: (brand: string, accent?: string) => LumenThemeTokens;
|
|
37
|
+
export interface LumenThemeFromHueOptions {
|
|
38
|
+
accentHue?: number;
|
|
39
|
+
scheme?: 'dark' | 'light';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Derives a complete token set from a single brand hue.
|
|
43
|
+
* Neutral surfaces pick up a faint tint of the hue; the accent sits 150deg
|
|
44
|
+
* away on the wheel. Status colors stay fixed for recognizability.
|
|
45
|
+
* Mirrored by the ThemeBuilder runtime enhancement in lumen-astro.
|
|
46
|
+
*/
|
|
47
|
+
export declare const createThemeFromHue: (hue: number, options?: LumenThemeFromHueOptions) => LumenThemeTokens;
|
|
48
|
+
export declare const getContrastRatio: (foreground: string, background: string) => number;
|
|
49
|
+
export declare const scoreThemeContrast: (tokens: LumenThemeTokens, foreground?: string, background?: string) => LumenContrastScore;
|
|
50
|
+
export declare const mergeThemeTokens: (tokens: LumenThemeTokens, overrides: Partial<LumenThemeTokens>) => LumenThemeTokens;
|
|
51
|
+
export declare const suggestReadableInk: (background: string) => LumenThemeContrastSuggestion;
|
|
52
|
+
export declare const tuneThemeContrast: (tokens: LumenThemeTokens, foreground?: string, background?: string) => LumenThemeTokens;
|
|
53
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
export const lumenSemanticColorTokenNames = [
|
|
2
|
+
'canvas',
|
|
3
|
+
'surface',
|
|
4
|
+
'surface-muted',
|
|
5
|
+
'surface-strong',
|
|
6
|
+
'line',
|
|
7
|
+
'ink',
|
|
8
|
+
'ink-soft',
|
|
9
|
+
'ink-muted',
|
|
10
|
+
'brand',
|
|
11
|
+
'brand-solid',
|
|
12
|
+
'brand-soft',
|
|
13
|
+
'accent',
|
|
14
|
+
'success',
|
|
15
|
+
'warning',
|
|
16
|
+
'danger'
|
|
17
|
+
];
|
|
18
|
+
export const lumenGlassColorTokenNames = [
|
|
19
|
+
'glass-bg',
|
|
20
|
+
'glass-bg-subtle',
|
|
21
|
+
'glass-bg-strong',
|
|
22
|
+
'glass-border',
|
|
23
|
+
'glass-highlight',
|
|
24
|
+
'glass-edge',
|
|
25
|
+
'glass-edge-soft',
|
|
26
|
+
'glass-shade',
|
|
27
|
+
'glass-refraction'
|
|
28
|
+
];
|
|
29
|
+
export const lumenGlassEffectTokenNames = [
|
|
30
|
+
'glass-blur',
|
|
31
|
+
'glass-saturate',
|
|
32
|
+
'glass-brightness',
|
|
33
|
+
'glass-shadow'
|
|
34
|
+
];
|
|
35
|
+
export const lumenGlassTokenNames = [
|
|
36
|
+
'glass-bg',
|
|
37
|
+
'glass-bg-subtle',
|
|
38
|
+
'glass-bg-strong',
|
|
39
|
+
'glass-border',
|
|
40
|
+
'glass-highlight',
|
|
41
|
+
'glass-blur',
|
|
42
|
+
'glass-saturate',
|
|
43
|
+
'glass-brightness',
|
|
44
|
+
'glass-edge',
|
|
45
|
+
'glass-edge-soft',
|
|
46
|
+
'glass-shade',
|
|
47
|
+
'glass-refraction',
|
|
48
|
+
'glass-shadow'
|
|
49
|
+
];
|
|
50
|
+
export const lumenRadiusTokenNames = [
|
|
51
|
+
'ui-radius-sm',
|
|
52
|
+
'ui-radius',
|
|
53
|
+
'ui-radius-lg'
|
|
54
|
+
];
|
|
55
|
+
export const lumenShadowTokenNames = [
|
|
56
|
+
'ui-shadow-sm',
|
|
57
|
+
'ui-shadow-md',
|
|
58
|
+
'ui-shadow-lg'
|
|
59
|
+
];
|
|
60
|
+
export const lumenTypographyTokenNames = [
|
|
61
|
+
'ui-font'
|
|
62
|
+
];
|
|
63
|
+
export const lumenMotionTokenNames = [
|
|
64
|
+
'ui-duration-fast',
|
|
65
|
+
'ui-duration',
|
|
66
|
+
'ui-duration-slow',
|
|
67
|
+
'ui-ease',
|
|
68
|
+
'ui-ease-emphasized'
|
|
69
|
+
];
|
|
70
|
+
export const lumenStructureTokenNames = [
|
|
71
|
+
...lumenRadiusTokenNames,
|
|
72
|
+
...lumenShadowTokenNames,
|
|
73
|
+
...lumenTypographyTokenNames,
|
|
74
|
+
...lumenMotionTokenNames
|
|
75
|
+
];
|
|
76
|
+
export const lumenColorTokenNames = [
|
|
77
|
+
...lumenSemanticColorTokenNames,
|
|
78
|
+
...lumenGlassColorTokenNames
|
|
79
|
+
];
|
|
80
|
+
export const lumenTokenNames = [
|
|
81
|
+
...lumenSemanticColorTokenNames,
|
|
82
|
+
...lumenGlassTokenNames,
|
|
83
|
+
...lumenStructureTokenNames
|
|
84
|
+
];
|
|
85
|
+
export const exportThemeCss = (tokens, selector = ':root') => {
|
|
86
|
+
const lines = lumenTokenNames
|
|
87
|
+
.filter(token => tokens[token])
|
|
88
|
+
.map(token => {
|
|
89
|
+
const value = tokens[token] ?? '';
|
|
90
|
+
return ` --${token}: ${value};`;
|
|
91
|
+
});
|
|
92
|
+
return `${selector} {\n${lines.join('\n')}\n}`;
|
|
93
|
+
};
|
|
94
|
+
export const parseThemeCss = (css) => {
|
|
95
|
+
const tokens = {};
|
|
96
|
+
for (const token of lumenTokenNames) {
|
|
97
|
+
const match = new RegExp(`--${token}:\\s*([^;]+);`).exec(css);
|
|
98
|
+
if (match?.[1]) {
|
|
99
|
+
tokens[token] = match[1].trim();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return tokens;
|
|
103
|
+
};
|
|
104
|
+
const normalizeHue = (hue) => ((Math.round(hue) % 360) + 360) % 360;
|
|
105
|
+
const getHueFromToken = (value) => {
|
|
106
|
+
const hue = Number(value.trim().split(/\s+/)[0]);
|
|
107
|
+
return Number.isFinite(hue) ? normalizeHue(hue) : 221;
|
|
108
|
+
};
|
|
109
|
+
const createGlassThemeTokens = (hue, scheme = 'light') => {
|
|
110
|
+
const h = normalizeHue(hue);
|
|
111
|
+
if (scheme === 'dark') {
|
|
112
|
+
return {
|
|
113
|
+
'glass-bg': `${h} 20% 13% / 0.78`,
|
|
114
|
+
'glass-bg-strong': `${h} 20% 13% / 0.9`,
|
|
115
|
+
'glass-bg-subtle': `${h} 20% 13% / 0.64`,
|
|
116
|
+
'glass-blur': '22px',
|
|
117
|
+
'glass-border': `${h} 30% 92% / 0.24`,
|
|
118
|
+
'glass-brightness': '1',
|
|
119
|
+
'glass-edge': `${h} 40% 98% / 0.24`,
|
|
120
|
+
'glass-edge-soft': `${h} 40% 98% / 0.08`,
|
|
121
|
+
'glass-highlight': `${h} 30% 96% / 0.1`,
|
|
122
|
+
'glass-refraction': `${h} 91% 56% / 0.1`,
|
|
123
|
+
'glass-saturate': '1.7',
|
|
124
|
+
'glass-shade': `${h} 40% 3% / 0.3`,
|
|
125
|
+
'glass-shadow': '0 1px 2px hsl(0 0% 0% / 0.3), 0 16px 40px hsl(0 0% 0% / 0.34), 0 40px 96px hsl(0 0% 0% / 0.38)'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
'glass-bg': '0 0% 100% / 0.55',
|
|
130
|
+
'glass-bg-strong': '0 0% 100% / 0.72',
|
|
131
|
+
'glass-bg-subtle': '0 0% 100% / 0.38',
|
|
132
|
+
'glass-blur': '22px',
|
|
133
|
+
'glass-border': `${h} 30% 100% / 0.55`,
|
|
134
|
+
'glass-brightness': '1.05',
|
|
135
|
+
'glass-edge': '0 0% 100% / 0.9',
|
|
136
|
+
'glass-edge-soft': '0 0% 100% / 0.34',
|
|
137
|
+
'glass-highlight': '0 0% 100% / 0.85',
|
|
138
|
+
'glass-refraction': `${h} 83% 53% / 0.08`,
|
|
139
|
+
'glass-saturate': '1.7',
|
|
140
|
+
'glass-shade': `${h} 47% 11% / 0.07`,
|
|
141
|
+
'glass-shadow': `0 1px 2px hsl(${h} 47% 11% / 0.08), 0 12px 32px hsl(${h} 47% 11% / 0.12), 0 32px 80px hsl(${h} 47% 11% / 0.14)`
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const lumenRadiusTokens = {
|
|
145
|
+
'ui-radius': '0.625rem',
|
|
146
|
+
'ui-radius-lg': '0.75rem',
|
|
147
|
+
'ui-radius-sm': '0.375rem'
|
|
148
|
+
};
|
|
149
|
+
const lumenTypographyTokens = {
|
|
150
|
+
'ui-font': 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
151
|
+
};
|
|
152
|
+
const lumenMotionTokens = {
|
|
153
|
+
'ui-duration': '160ms',
|
|
154
|
+
'ui-duration-fast': '120ms',
|
|
155
|
+
'ui-duration-slow': '300ms',
|
|
156
|
+
'ui-ease': 'cubic-bezier(0.32, 0.72, 0, 1)',
|
|
157
|
+
'ui-ease-emphasized': 'cubic-bezier(0.22, 1, 0.36, 1)'
|
|
158
|
+
};
|
|
159
|
+
const createShadowTokens = (hue, scheme = 'light') => {
|
|
160
|
+
if (scheme === 'dark') {
|
|
161
|
+
return {
|
|
162
|
+
'ui-shadow-lg': '0 28px 80px hsl(0 0% 0% / 0.36)',
|
|
163
|
+
'ui-shadow-md': '0 12px 32px hsl(0 0% 0% / 0.24)',
|
|
164
|
+
'ui-shadow-sm': '0 1px 2px hsl(0 0% 0% / 0.18)'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const h = normalizeHue(hue);
|
|
168
|
+
return {
|
|
169
|
+
'ui-shadow-lg': `0 24px 64px hsl(${h} 47% 11% / 0.14)`,
|
|
170
|
+
'ui-shadow-md': `0 8px 24px hsl(${h} 47% 11% / 0.08)`,
|
|
171
|
+
'ui-shadow-sm': `0 1px 2px hsl(${h} 47% 11% / 0.06)`
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
const createStructureTokens = (hue, scheme = 'light') => ({
|
|
175
|
+
...lumenRadiusTokens,
|
|
176
|
+
...createShadowTokens(hue, scheme),
|
|
177
|
+
...lumenTypographyTokens,
|
|
178
|
+
...lumenMotionTokens
|
|
179
|
+
});
|
|
180
|
+
export const createThemePalette = (brand, accent = brand) => ({
|
|
181
|
+
accent,
|
|
182
|
+
brand,
|
|
183
|
+
'brand-soft': brand.replace(/(\d+)%$/, '96%'),
|
|
184
|
+
'brand-solid': brand,
|
|
185
|
+
canvas: '0 0% 100%',
|
|
186
|
+
danger: '0 84% 60%',
|
|
187
|
+
ink: '222 47% 11%',
|
|
188
|
+
'ink-muted': '215 12% 48%',
|
|
189
|
+
'ink-soft': '215 16% 32%',
|
|
190
|
+
line: '220 13% 86%',
|
|
191
|
+
success: '142 71% 36%',
|
|
192
|
+
surface: '0 0% 100%',
|
|
193
|
+
'surface-muted': '220 14% 96%',
|
|
194
|
+
'surface-strong': '220 13% 91%',
|
|
195
|
+
warning: '38 92% 50%',
|
|
196
|
+
...createGlassThemeTokens(getHueFromToken(brand)),
|
|
197
|
+
...createStructureTokens(getHueFromToken(brand))
|
|
198
|
+
});
|
|
199
|
+
/**
|
|
200
|
+
* Derives a complete token set from a single brand hue.
|
|
201
|
+
* Neutral surfaces pick up a faint tint of the hue; the accent sits 150deg
|
|
202
|
+
* away on the wheel. Status colors stay fixed for recognizability.
|
|
203
|
+
* Mirrored by the ThemeBuilder runtime enhancement in lumen-astro.
|
|
204
|
+
*/
|
|
205
|
+
export const createThemeFromHue = (hue, options = {}) => {
|
|
206
|
+
const h = normalizeHue(hue);
|
|
207
|
+
const accentHue = normalizeHue(options.accentHue ?? h + 150);
|
|
208
|
+
if (options.scheme === 'dark') {
|
|
209
|
+
return {
|
|
210
|
+
accent: `${accentHue} 70% 48%`,
|
|
211
|
+
brand: `${h} 88% 60%`,
|
|
212
|
+
'brand-soft': `${h} 80% 16%`,
|
|
213
|
+
'brand-solid': `${h} 88% 46%`,
|
|
214
|
+
canvas: `${h} 24% 8%`,
|
|
215
|
+
danger: '0 84% 65%',
|
|
216
|
+
ink: `${h} 20% 96%`,
|
|
217
|
+
'ink-muted': `${h} 10% 62%`,
|
|
218
|
+
'ink-soft': `${h} 14% 77%`,
|
|
219
|
+
line: `${h} 14% 26%`,
|
|
220
|
+
success: '142 69% 45%',
|
|
221
|
+
surface: `${h} 18% 11%`,
|
|
222
|
+
'surface-muted': `${h} 16% 15%`,
|
|
223
|
+
'surface-strong': `${h} 14% 22%`,
|
|
224
|
+
warning: '38 92% 55%',
|
|
225
|
+
...createGlassThemeTokens(h, 'dark'),
|
|
226
|
+
...createStructureTokens(h, 'dark')
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
accent: `${accentHue} 70% 40%`,
|
|
231
|
+
brand: `${h} 85% 53%`,
|
|
232
|
+
'brand-soft': `${h} 90% 96%`,
|
|
233
|
+
'brand-solid': `${h} 85% 45%`,
|
|
234
|
+
canvas: `${h} 20% 99%`,
|
|
235
|
+
danger: '0 84% 60%',
|
|
236
|
+
ink: `${h} 40% 11%`,
|
|
237
|
+
'ink-muted': `${h} 10% 48%`,
|
|
238
|
+
'ink-soft': `${h} 14% 32%`,
|
|
239
|
+
line: `${h} 14% 86%`,
|
|
240
|
+
success: '142 71% 36%',
|
|
241
|
+
surface: `${h} 20% 100%`,
|
|
242
|
+
'surface-muted': `${h} 16% 96%`,
|
|
243
|
+
'surface-strong': `${h} 14% 91%`,
|
|
244
|
+
warning: '38 92% 50%',
|
|
245
|
+
...createGlassThemeTokens(h),
|
|
246
|
+
...createStructureTokens(h)
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const hslToRgb = (value) => {
|
|
250
|
+
const [hue = 0, saturation = 0, lightness = 0] = value
|
|
251
|
+
.replaceAll('%', '')
|
|
252
|
+
.split(/\s+/)
|
|
253
|
+
.map(Number);
|
|
254
|
+
const s = saturation / 100;
|
|
255
|
+
const l = lightness / 100;
|
|
256
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
257
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
|
|
258
|
+
const m = l - c / 2;
|
|
259
|
+
let rgb = [c, 0, x];
|
|
260
|
+
if (hue < 60) {
|
|
261
|
+
rgb = [c, x, 0];
|
|
262
|
+
}
|
|
263
|
+
else if (hue < 120) {
|
|
264
|
+
rgb = [x, c, 0];
|
|
265
|
+
}
|
|
266
|
+
else if (hue < 180) {
|
|
267
|
+
rgb = [0, c, x];
|
|
268
|
+
}
|
|
269
|
+
else if (hue < 240) {
|
|
270
|
+
rgb = [0, x, c];
|
|
271
|
+
}
|
|
272
|
+
else if (hue < 300) {
|
|
273
|
+
rgb = [x, 0, c];
|
|
274
|
+
}
|
|
275
|
+
const [r, g, b] = rgb;
|
|
276
|
+
return [
|
|
277
|
+
Math.round((r + m) * 255),
|
|
278
|
+
Math.round((g + m) * 255),
|
|
279
|
+
Math.round((b + m) * 255)
|
|
280
|
+
];
|
|
281
|
+
};
|
|
282
|
+
const getRelativeLuminance = ([r, g, b]) => {
|
|
283
|
+
const [red, green, blue] = [r, g, b].map(channel => {
|
|
284
|
+
const value = channel / 255;
|
|
285
|
+
return value <= 0.03928
|
|
286
|
+
? value / 12.92
|
|
287
|
+
: ((value + 0.055) / 1.055) ** 2.4;
|
|
288
|
+
});
|
|
289
|
+
return 0.2126 * (red ?? 0) + 0.7152 * (green ?? 0) + 0.0722 * (blue ?? 0);
|
|
290
|
+
};
|
|
291
|
+
export const getContrastRatio = (foreground, background) => {
|
|
292
|
+
const foregroundLuminance = getRelativeLuminance(hslToRgb(foreground));
|
|
293
|
+
const backgroundLuminance = getRelativeLuminance(hslToRgb(background));
|
|
294
|
+
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
295
|
+
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
296
|
+
return Number(((lighter + 0.05) / (darker + 0.05)).toFixed(2));
|
|
297
|
+
};
|
|
298
|
+
export const scoreThemeContrast = (tokens, foreground = 'ink', background = 'canvas') => {
|
|
299
|
+
const ratio = getContrastRatio(tokens[foreground] ?? '0 0% 0%', tokens[background] ?? '0 0% 100%');
|
|
300
|
+
return {
|
|
301
|
+
ratio,
|
|
302
|
+
wcagAA: ratio >= 4.5,
|
|
303
|
+
wcagAAA: ratio >= 7
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
export const mergeThemeTokens = (tokens, overrides) => ({
|
|
307
|
+
...tokens,
|
|
308
|
+
...Object.fromEntries(Object.entries(overrides).filter((entry) => Boolean(entry[1])))
|
|
309
|
+
});
|
|
310
|
+
export const suggestReadableInk = (background) => {
|
|
311
|
+
const darkForeground = '0 0% 0%';
|
|
312
|
+
const lightForeground = '0 0% 100%';
|
|
313
|
+
const darkRatio = getContrastRatio(darkForeground, background);
|
|
314
|
+
const lightRatio = getContrastRatio(lightForeground, background);
|
|
315
|
+
return darkRatio >= lightRatio
|
|
316
|
+
? { background, foreground: darkForeground, ratio: darkRatio }
|
|
317
|
+
: { background, foreground: lightForeground, ratio: lightRatio };
|
|
318
|
+
};
|
|
319
|
+
export const tuneThemeContrast = (tokens, foreground = 'ink', background = 'canvas') => {
|
|
320
|
+
const current = scoreThemeContrast(tokens, foreground, background);
|
|
321
|
+
if (current.wcagAA)
|
|
322
|
+
return { ...tokens };
|
|
323
|
+
const suggestion = suggestReadableInk(tokens[background] ?? '0 0% 100%');
|
|
324
|
+
return {
|
|
325
|
+
...tokens,
|
|
326
|
+
[foreground]: suggestion.foreground
|
|
327
|
+
};
|
|
328
|
+
};
|