Establish NODE.DC design system baseline
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@nodedc/ui-core",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"files": ["dist", "styles.css"],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./styles.css": "./styles.css"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodedc/tokens": "0.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export type RgbTuple = readonly [number, number, number];
|
||||
|
||||
export interface AccentVariables {
|
||||
"--nodedc-accent-rgb": string;
|
||||
"--nodedc-on-accent-rgb": string;
|
||||
}
|
||||
|
||||
function clampChannel(value: number): number {
|
||||
return Math.max(0, Math.min(255, Math.round(value)));
|
||||
}
|
||||
|
||||
function linearChannel(channel: number): number {
|
||||
const normalized = clampChannel(channel) / 255;
|
||||
return normalized <= 0.04045
|
||||
? normalized / 12.92
|
||||
: ((normalized + 0.055) / 1.055) ** 2.4;
|
||||
}
|
||||
|
||||
export function relativeLuminance(rgb: RgbTuple): number {
|
||||
const [red, green, blue] = rgb.map(linearChannel) as [number, number, number];
|
||||
return red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
||||
}
|
||||
|
||||
export function readableTextRgb(rgb: RgbTuple): RgbTuple {
|
||||
return relativeLuminance(rgb) > 0.46 ? [11, 17, 23] : [247, 248, 244];
|
||||
}
|
||||
|
||||
export function rgbTuple(value: RgbTuple): string {
|
||||
return value.map(clampChannel).join(" ");
|
||||
}
|
||||
|
||||
export function createAccentVariables(accent: RgbTuple): AccentVariables {
|
||||
return {
|
||||
"--nodedc-accent-rgb": rgbTuple(accent),
|
||||
"--nodedc-on-accent-rgb": rgbTuple(readableTextRgb(accent)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
export type FloatingPlacement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
|
||||
|
||||
export interface RectLike {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface FloatingPositionOptions {
|
||||
anchor: RectLike;
|
||||
surfaceWidth: number;
|
||||
surfaceHeight: number;
|
||||
placement?: FloatingPlacement;
|
||||
offset?: number;
|
||||
viewportWidth?: number;
|
||||
viewportHeight?: number;
|
||||
viewportPadding?: number;
|
||||
}
|
||||
|
||||
export interface FloatingPosition {
|
||||
top: number;
|
||||
left: number;
|
||||
maxHeight: number;
|
||||
resolvedPlacement: FloatingPlacement;
|
||||
}
|
||||
|
||||
export function computeFloatingPosition({
|
||||
anchor,
|
||||
surfaceWidth,
|
||||
surfaceHeight,
|
||||
placement = "bottom-start",
|
||||
offset = 8,
|
||||
viewportWidth = typeof window === "undefined" ? 1920 : window.innerWidth,
|
||||
viewportHeight = typeof window === "undefined" ? 1080 : window.innerHeight,
|
||||
viewportPadding = 8,
|
||||
}: FloatingPositionOptions): FloatingPosition {
|
||||
const roomBelow = viewportHeight - anchor.bottom - offset - viewportPadding;
|
||||
const roomAbove = anchor.top - offset - viewportPadding;
|
||||
const requestedTop = placement.startsWith("top");
|
||||
const shouldFlip = requestedTop
|
||||
? surfaceHeight > roomAbove && roomBelow > roomAbove
|
||||
: surfaceHeight > roomBelow && roomAbove > roomBelow;
|
||||
const resolvedPlacement = shouldFlip
|
||||
? (`${requestedTop ? "bottom" : "top"}-${placement.endsWith("end") ? "end" : "start"}` as FloatingPlacement)
|
||||
: placement;
|
||||
|
||||
const alignedLeft = resolvedPlacement.endsWith("end")
|
||||
? anchor.right - surfaceWidth
|
||||
: anchor.left;
|
||||
const left = Math.min(
|
||||
Math.max(viewportPadding, alignedLeft),
|
||||
Math.max(viewportPadding, viewportWidth - surfaceWidth - viewportPadding),
|
||||
);
|
||||
|
||||
const availableHeight = resolvedPlacement.startsWith("top") ? roomAbove : roomBelow;
|
||||
const maxHeight = Math.max(96, availableHeight);
|
||||
const visibleHeight = Math.min(surfaceHeight, maxHeight);
|
||||
const top = resolvedPlacement.startsWith("top")
|
||||
? Math.max(viewportPadding, anchor.top - offset - visibleHeight)
|
||||
: Math.min(anchor.bottom + offset, viewportHeight - visibleHeight - viewportPadding);
|
||||
|
||||
return { top, left, maxHeight, resolvedPlacement };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./accent";
|
||||
export * from "./floating";
|
||||
export * from "./theme";
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createAccentVariables, type RgbTuple } from "./accent";
|
||||
|
||||
export type NodedcTheme = "dark" | "light";
|
||||
|
||||
export interface ApplyThemeOptions {
|
||||
theme?: NodedcTheme;
|
||||
accent?: RgbTuple;
|
||||
}
|
||||
|
||||
export function applyNodedcTheme(
|
||||
target: HTMLElement,
|
||||
{ theme = "dark", accent }: ApplyThemeOptions = {},
|
||||
): void {
|
||||
target.dataset.nodedcTheme = theme;
|
||||
target.dataset.nodedcUi = "true";
|
||||
|
||||
if (!accent) return;
|
||||
const variables = createAccentVariables(accent);
|
||||
Object.entries(variables).forEach(([property, value]) => {
|
||||
target.style.setProperty(property, value);
|
||||
});
|
||||
}
|
||||
|
||||
export function clearNodedcAccent(target: HTMLElement): void {
|
||||
target.style.removeProperty("--nodedc-accent-rgb");
|
||||
target.style.removeProperty("--nodedc-on-accent-rgb");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user