Refine SEO light theme and environment controls
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodedc/tokens",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"type": "module",
|
||||
"files": ["tokens.json", "tokens.css", "themes.css"],
|
||||
"exports": {
|
||||
|
||||
@@ -31,6 +31,19 @@
|
||||
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.075), 0 22px 72px rgba(0, 0, 0, 0.42);
|
||||
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(0, 0, 0, 0.42), 0 6px 18px rgba(0, 0, 0, 0.2);
|
||||
--nodedc-focus-surface: rgba(var(--nodedc-accent-rgb), 0.14);
|
||||
--nodedc-glass-outline: rgba(255, 255, 255, 0.11);
|
||||
--nodedc-app-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)), rgba(10, 10, 13, 0.9);
|
||||
--nodedc-app-panel-shadow: 0 34px 110px rgba(0, 0, 0, 0.42);
|
||||
--nodedc-app-panel-shadow-strong: 0 34px 110px rgba(0, 0, 0, 0.52);
|
||||
--nodedc-panel-action-border: rgba(255, 255, 255, 0.22);
|
||||
--nodedc-panel-action-border-hover: rgba(255, 255, 255, 0.3);
|
||||
--nodedc-panel-item-bg: rgba(255, 255, 255, 0.04);
|
||||
--nodedc-panel-item-hover-bg: rgba(255, 255, 255, 0.085);
|
||||
--nodedc-panel-item-active-bg: rgba(255, 255, 255, 0.115);
|
||||
--nodedc-panel-icon-bg: rgba(255, 255, 255, 0.045);
|
||||
--nodedc-panel-active-icon-bg: rgba(247, 248, 244, 0.96);
|
||||
--nodedc-panel-active-icon-color: rgb(var(--nodedc-on-accent-rgb));
|
||||
--nodedc-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
[data-nodedc-theme="light"] {
|
||||
@@ -65,4 +78,17 @@
|
||||
--nodedc-glass-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16);
|
||||
--nodedc-glass-dropdown-shadow: 0 22px 60px rgba(24, 32, 29, 0.2), 0 6px 18px rgba(24, 32, 29, 0.1);
|
||||
--nodedc-focus-surface: rgba(var(--nodedc-accent-rgb), 0.1);
|
||||
--nodedc-glass-outline: rgba(24, 32, 29, 0.12);
|
||||
--nodedc-app-panel-surface: linear-gradient(180deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 255, 255, 0.22) 100%), rgba(255, 255, 255, 0.48);
|
||||
--nodedc-app-panel-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 22px 76px rgba(24, 32, 29, 0.16);
|
||||
--nodedc-app-panel-shadow-strong: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 24px 82px rgba(24, 32, 29, 0.2);
|
||||
--nodedc-panel-action-border: rgba(24, 32, 29, 0.12);
|
||||
--nodedc-panel-action-border-hover: rgba(24, 32, 29, 0.22);
|
||||
--nodedc-panel-item-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.44), rgba(255, 255, 255, 0.16)), rgba(255, 255, 255, 0.28);
|
||||
--nodedc-panel-item-hover-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.24)), rgba(255, 255, 255, 0.38);
|
||||
--nodedc-panel-item-active-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0.28)), rgba(255, 255, 255, 0.46);
|
||||
--nodedc-panel-icon-bg: rgba(255, 255, 255, 0.38);
|
||||
--nodedc-panel-active-icon-bg: rgba(32, 32, 34, 0.96);
|
||||
--nodedc-panel-active-icon-color: rgba(247, 248, 244, 0.96);
|
||||
--nodedc-scrollbar-thumb: rgba(24, 32, 29, 0.22);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
--nodedc-inspector-button-radius: 1rem;
|
||||
--nodedc-select-toggle-width: 2.875rem;
|
||||
--nodedc-select-gap: 0.5rem;
|
||||
--nodedc-icon-glyph-scale: 0.9;
|
||||
--nodedc-icon-glyph-scale: 0.78;
|
||||
|
||||
--nodedc-space-1: 0.25rem;
|
||||
--nodedc-space-2: 0.5rem;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodedc/ui-core",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"type": "module",
|
||||
"files": ["dist", "styles.css"],
|
||||
"main": "./dist/index.js",
|
||||
@@ -17,6 +17,6 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodedc/tokens": "0.5.0"
|
||||
"@nodedc/tokens": "0.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
+146
-37
@@ -723,24 +723,137 @@ textarea.nodedc-field__control {
|
||||
box-shadow: var(--nodedc-glass-control-shadow);
|
||||
}
|
||||
|
||||
.nodedc-color-field__picker {
|
||||
position: relative;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
overflow: hidden;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: var(--nodedc-color-value, #000);
|
||||
.nodedc-color-field__dropdown {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nodedc-color-field__picker input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
.nodedc-color-field__picker {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
outline: 0;
|
||||
background: var(--nodedc-color-value, #000);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nodedc-color-field__picker[data-open="true"],
|
||||
.nodedc-color-field__picker:focus-visible {
|
||||
box-shadow: 0 0 0 2px rgba(var(--nodedc-accent-rgb), 0.3), 0 0 0 3px var(--nodedc-canvas);
|
||||
}
|
||||
|
||||
.nodedc-color-field__picker:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.nodedc-color-palette-surface {
|
||||
width: min(18.5rem, calc(100vw - 1rem));
|
||||
overflow: visible;
|
||||
border-radius: var(--nodedc-radius-card);
|
||||
background: var(--nodedc-glass-panel-surface-strong);
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.nodedc-color-palette {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.nodedc-color-palette__spectrum {
|
||||
position: relative;
|
||||
aspect-ratio: 1.5 / 1;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--nodedc-glass-outline);
|
||||
border-radius: var(--nodedc-radius-control);
|
||||
outline: 0;
|
||||
background:
|
||||
linear-gradient(to top, rgba(0, 0, 0, 1), transparent),
|
||||
linear-gradient(to right, rgba(255, 255, 255, 1), var(--nodedc-color-hue));
|
||||
cursor: crosshair;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.nodedc-color-palette__spectrum:focus-visible {
|
||||
box-shadow: 0 0 0 2px rgba(var(--nodedc-accent-rgb), 0.28);
|
||||
}
|
||||
|
||||
.nodedc-color-palette__marker {
|
||||
position: absolute;
|
||||
top: var(--nodedc-color-v);
|
||||
left: var(--nodedc-color-s);
|
||||
width: 0.82rem;
|
||||
height: 0.82rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.96);
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.46);
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.nodedc-color-palette__hue {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: var(--nodedc-font-size-xs);
|
||||
font-weight: var(--nodedc-font-weight-medium);
|
||||
}
|
||||
|
||||
.nodedc-color-palette__hue input {
|
||||
width: 100%;
|
||||
height: 0.7rem;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
outline: 0;
|
||||
background: linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nodedc-color-palette__hue input::-webkit-slider-thumb {
|
||||
width: 0.88rem;
|
||||
height: 0.88rem;
|
||||
appearance: none;
|
||||
border: 2px solid rgba(255, 255, 255, 0.96);
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: rgba(12, 12, 15, 0.92);
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.46);
|
||||
}
|
||||
|
||||
.nodedc-color-palette__hue input::-moz-range-thumb {
|
||||
width: 0.68rem;
|
||||
height: 0.68rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.96);
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: rgba(12, 12, 15, 0.92);
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.46);
|
||||
}
|
||||
|
||||
.nodedc-color-palette__presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.nodedc-color-palette__presets button {
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid var(--nodedc-glass-outline);
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
outline: 0;
|
||||
background: var(--nodedc-palette-swatch);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nodedc-color-palette__presets button[aria-pressed="true"],
|
||||
.nodedc-color-palette__presets button:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--nodedc-canvas), 0 0 0 3px rgb(var(--nodedc-accent-rgb));
|
||||
}
|
||||
|
||||
.nodedc-color-field__text {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
@@ -1117,12 +1230,10 @@ textarea.nodedc-field__control {
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: var(--nodedc-radius-card);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)),
|
||||
rgba(10, 10, 13, 0.9);
|
||||
background: var(--nodedc-app-panel-surface);
|
||||
color: var(--nodedc-text-primary);
|
||||
padding: 1rem;
|
||||
box-shadow: 0 34px 110px rgba(0, 0, 0, 0.42);
|
||||
box-shadow: var(--nodedc-app-panel-shadow);
|
||||
backdrop-filter: blur(28px);
|
||||
-webkit-backdrop-filter: blur(28px);
|
||||
animation: nodedc-application-panel-in 460ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
@@ -1186,7 +1297,7 @@ textarea.nodedc-field__control {
|
||||
width: 2.92rem;
|
||||
height: 2.92rem;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border: 1px solid var(--nodedc-panel-action-border);
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
@@ -1196,7 +1307,7 @@ textarea.nodedc-field__control {
|
||||
|
||||
.nodedc-application-panel__action:hover,
|
||||
.nodedc-application-panel__action:focus-visible {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
border-color: var(--nodedc-panel-action-border-hover);
|
||||
background: var(--nodedc-glass-control-hover);
|
||||
color: var(--nodedc-text-primary);
|
||||
}
|
||||
@@ -1212,7 +1323,7 @@ textarea.nodedc-field__control {
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
||||
scrollbar-color: var(--nodedc-scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
.nodedc-admin-panel {
|
||||
@@ -1226,12 +1337,10 @@ textarea.nodedc-field__control {
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: var(--nodedc-radius-card);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)),
|
||||
rgba(10, 10, 13, 0.9);
|
||||
background: var(--nodedc-app-panel-surface);
|
||||
color: var(--nodedc-text-primary);
|
||||
padding: 1.1rem;
|
||||
box-shadow: 0 34px 110px rgba(0, 0, 0, 0.52);
|
||||
box-shadow: var(--nodedc-app-panel-shadow-strong);
|
||||
backdrop-filter: blur(28px);
|
||||
-webkit-backdrop-filter: blur(28px);
|
||||
animation: nodedc-admin-panel-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
@@ -1281,7 +1390,7 @@ textarea.nodedc-field__control {
|
||||
width: 2.92rem;
|
||||
height: 2.92rem;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border: 1px solid var(--nodedc-panel-action-border);
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
@@ -1290,7 +1399,7 @@ textarea.nodedc-field__control {
|
||||
}
|
||||
|
||||
.nodedc-admin-panel__close:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
border-color: var(--nodedc-panel-action-border-hover);
|
||||
background: var(--nodedc-glass-control-hover);
|
||||
color: var(--nodedc-text-primary);
|
||||
}
|
||||
@@ -1322,13 +1431,13 @@ textarea.nodedc-field__control {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: var(--nodedc-panel-item-bg);
|
||||
color: var(--nodedc-text-secondary);
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.nodedc-admin-context[data-active="true"] {
|
||||
background: rgba(74, 74, 74, 0.5);
|
||||
background: var(--nodedc-panel-item-active-bg);
|
||||
color: var(--nodedc-text-primary);
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1378,7 +1487,7 @@ textarea.nodedc-field__control {
|
||||
flex: 0 0 2.92rem;
|
||||
place-items: center;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: var(--nodedc-panel-icon-bg);
|
||||
color: var(--nodedc-text-secondary);
|
||||
}
|
||||
|
||||
@@ -1432,7 +1541,7 @@ textarea.nodedc-field__control {
|
||||
gap: 0.86rem;
|
||||
border: 0;
|
||||
border-radius: var(--nodedc-radius-circle);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: var(--nodedc-panel-item-bg);
|
||||
color: var(--nodedc-text-secondary);
|
||||
padding: 5px 0.9rem 5px calc(5px + 2.92rem + 0.86rem);
|
||||
font: inherit;
|
||||
@@ -1449,22 +1558,22 @@ textarea.nodedc-field__control {
|
||||
}
|
||||
|
||||
.nodedc-admin-panel__nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.085);
|
||||
background: var(--nodedc-panel-item-hover-bg);
|
||||
color: var(--nodedc-text-primary);
|
||||
opacity: 0.92;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nodedc-admin-panel__nav-item[data-active="true"] {
|
||||
background: rgba(255, 255, 255, 0.115);
|
||||
background: var(--nodedc-panel-item-active-bg);
|
||||
color: var(--nodedc-text-primary);
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.048), 0 10px 22px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: var(--nodedc-glass-control-shadow), 0 10px 22px rgba(24, 32, 29, 0.12);
|
||||
}
|
||||
|
||||
.nodedc-admin-panel__nav-item:focus-visible {
|
||||
outline: 0;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 10px 22px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: inset 0 0 0 1px var(--nodedc-glass-outline), 0 10px 22px rgba(24, 32, 29, 0.12);
|
||||
}
|
||||
|
||||
.nodedc-app-shell__navigation > .nodedc-admin-panel {
|
||||
@@ -1475,13 +1584,13 @@ textarea.nodedc-field__control {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 5px;
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
background: var(--nodedc-panel-icon-bg);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.nodedc-admin-panel__nav-item[data-active="true"] .nodedc-admin-panel__nav-icon {
|
||||
background: rgba(247, 248, 244, 0.96);
|
||||
color: rgb(var(--nodedc-on-accent-rgb));
|
||||
background: var(--nodedc-panel-active-icon-bg);
|
||||
color: var(--nodedc-panel-active-icon-color);
|
||||
}
|
||||
|
||||
.nodedc-admin-panel__footer {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodedc/ui-dom",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"type": "module",
|
||||
"files": ["dist"],
|
||||
"main": "./dist/index.js",
|
||||
@@ -16,6 +16,6 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodedc/ui-core": "0.5.0"
|
||||
"@nodedc/ui-core": "0.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nodedc/ui-react",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"type": "module",
|
||||
"files": ["dist"],
|
||||
"main": "./dist/index.js",
|
||||
@@ -16,7 +16,7 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nodedc/ui-core": "0.5.0",
|
||||
"@nodedc/ui-core": "0.5.1",
|
||||
"lucide-react": "^0.468.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -97,7 +97,7 @@ export interface IconProps extends Omit<LucideProps, "ref"> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function Icon({ name, label, size = 17, strokeWidth = 1.7, ...props }: IconProps) {
|
||||
export function Icon({ name, label, size = 16, strokeWidth = 1.6, ...props }: IconProps) {
|
||||
const IconComponent = icons[name];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CSSProperties, InputHTMLAttributes } from "react";
|
||||
import { useEffect, useState, type CSSProperties, type InputHTMLAttributes, type PointerEvent } from "react";
|
||||
import { cn } from "./cn.js";
|
||||
import { Dropdown } from "./Dropdown.js";
|
||||
|
||||
export interface RangeControlProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> {
|
||||
label: string;
|
||||
@@ -52,18 +53,159 @@ export interface ColorFieldProps extends Omit<InputHTMLAttributes<HTMLInputEleme
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function ColorField({ value, onChange, label = "Цвет", className, ...props }: ColorFieldProps) {
|
||||
const style = { "--nodedc-color-value": value } as CSSProperties;
|
||||
type Hsv = { h: number; s: number; v: number };
|
||||
|
||||
const palettePresets = ["#ff2f92", "#ff6caf", "#d746ff", "#7a3cff", "#4ea4ff", "#35d7c1", "#c3ff66", "#ffffff", "#404040", "#111116"];
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function parseHexColor(value: string): [number, number, number] | null {
|
||||
const match = /^#?([0-9a-f]{6})$/i.exec(value.trim());
|
||||
if (!match) return null;
|
||||
const hex = match[1];
|
||||
return [
|
||||
Number.parseInt(hex.slice(0, 2), 16),
|
||||
Number.parseInt(hex.slice(2, 4), 16),
|
||||
Number.parseInt(hex.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
|
||||
function hexToHsv(value: string): Hsv | null {
|
||||
const rgb = parseHexColor(value);
|
||||
if (!rgb) return null;
|
||||
const [red, green, blue] = rgb.map((channel) => channel / 255) as [number, number, number];
|
||||
const max = Math.max(red, green, blue);
|
||||
const min = Math.min(red, green, blue);
|
||||
const delta = max - min;
|
||||
let hue = 0;
|
||||
if (delta) {
|
||||
if (max === red) hue = 60 * (((green - blue) / delta) % 6);
|
||||
else if (max === green) hue = 60 * ((blue - red) / delta + 2);
|
||||
else hue = 60 * ((red - green) / delta + 4);
|
||||
}
|
||||
return { h: (hue + 360) % 360, s: max === 0 ? 0 : (delta / max) * 100, v: max * 100 };
|
||||
}
|
||||
|
||||
function hsvToHex({ h, s, v }: Hsv): string {
|
||||
const hue = ((h % 360) + 360) % 360;
|
||||
const saturation = clamp(s, 0, 100) / 100;
|
||||
const value = clamp(v, 0, 100) / 100;
|
||||
const chroma = value * saturation;
|
||||
const component = (hue / 60) % 2;
|
||||
const match = chroma * (1 - Math.abs(component - 1));
|
||||
const base = hue < 60 ? [chroma, match, 0]
|
||||
: hue < 120 ? [match, chroma, 0]
|
||||
: hue < 180 ? [0, chroma, match]
|
||||
: hue < 240 ? [0, match, chroma]
|
||||
: hue < 300 ? [match, 0, chroma]
|
||||
: [chroma, 0, match];
|
||||
const offset = value - chroma;
|
||||
return `#${base.map((channel) => Math.round((channel + offset) * 255).toString(16).padStart(2, "0")).join("")}`;
|
||||
}
|
||||
|
||||
export function ColorField({ value, onChange, label = "Цвет", className, disabled = false, ...props }: ColorFieldProps) {
|
||||
const validColor = hexToHsv(value);
|
||||
const [hsv, setHsv] = useState<Hsv>(() => validColor ?? { h: 330, s: 82, v: 100 });
|
||||
const style = { "--nodedc-color-value": validColor ? hsvToHex(validColor) : hsvToHex(hsv) } as CSSProperties;
|
||||
|
||||
useEffect(() => {
|
||||
if (validColor) setHsv(validColor);
|
||||
}, [value, validColor?.h, validColor?.s, validColor?.v]);
|
||||
|
||||
const commit = (next: Hsv) => {
|
||||
const normalized = { h: clamp(next.h, 0, 360), s: clamp(next.s, 0, 100), v: clamp(next.v, 0, 100) };
|
||||
setHsv(normalized);
|
||||
onChange(hsvToHex(normalized));
|
||||
};
|
||||
|
||||
const updateSpectrum = (event: PointerEvent<HTMLDivElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
commit({ ...hsv, s: ((event.clientX - rect.left) / rect.width) * 100, v: 100 - ((event.clientY - rect.top) / rect.height) * 100 });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("nodedc-color-field", className)} style={style}>
|
||||
<label className="nodedc-color-field__picker" aria-label={label}>
|
||||
<input type="color" value={value} onChange={(event) => onChange(event.target.value)} {...props} />
|
||||
</label>
|
||||
<Dropdown
|
||||
className="nodedc-color-field__dropdown"
|
||||
placement="bottom-start"
|
||||
minWidth={296}
|
||||
width={296}
|
||||
disabled={disabled}
|
||||
surfaceRole="dialog"
|
||||
surfaceClassName="nodedc-color-palette-surface"
|
||||
trigger={({ open, toggle, setTriggerRef, surfaceId }) => (
|
||||
<button
|
||||
ref={setTriggerRef}
|
||||
type="button"
|
||||
className="nodedc-color-field__picker"
|
||||
aria-label={`Выбрать ${label}`}
|
||||
aria-haspopup="dialog"
|
||||
aria-controls={surfaceId}
|
||||
aria-expanded={open}
|
||||
data-open={open ? "true" : undefined}
|
||||
disabled={disabled}
|
||||
onClick={toggle}
|
||||
><span aria-hidden="true" /></button>
|
||||
)}
|
||||
>
|
||||
<div className="nodedc-color-palette" aria-label={`Палитра: ${label}`}>
|
||||
<div
|
||||
className="nodedc-color-palette__spectrum"
|
||||
role="slider"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
aria-label={`${label}: насыщенность и яркость`}
|
||||
aria-valuetext={hsvToHex(hsv)}
|
||||
style={{ "--nodedc-color-hue": hsvToHex({ h: hsv.h, s: 100, v: 100 }), "--nodedc-color-s": `${hsv.s}%`, "--nodedc-color-v": `${100 - hsv.v}%` } as CSSProperties}
|
||||
onPointerDown={(event) => {
|
||||
if (disabled) return;
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
updateSpectrum(event);
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) updateSpectrum(event);
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
const delta = event.shiftKey ? 10 : 2;
|
||||
if (event.key === "ArrowLeft") { event.preventDefault(); commit({ ...hsv, s: hsv.s - delta }); }
|
||||
if (event.key === "ArrowRight") { event.preventDefault(); commit({ ...hsv, s: hsv.s + delta }); }
|
||||
if (event.key === "ArrowDown") { event.preventDefault(); commit({ ...hsv, v: hsv.v - delta }); }
|
||||
if (event.key === "ArrowUp") { event.preventDefault(); commit({ ...hsv, v: hsv.v + delta }); }
|
||||
}}
|
||||
><span className="nodedc-color-palette__marker" aria-hidden="true" /></div>
|
||||
<label className="nodedc-color-palette__hue">
|
||||
<span>Оттенок</span>
|
||||
<input type="range" min={0} max={360} value={Math.round(hsv.h)} aria-label={`${label}: оттенок`} disabled={disabled} onChange={(event) => commit({ ...hsv, h: Number(event.target.value) })} />
|
||||
</label>
|
||||
<div className="nodedc-color-palette__presets" aria-label="Быстрые цвета">
|
||||
{palettePresets.map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
type="button"
|
||||
aria-label={`Выбрать ${preset}`}
|
||||
aria-pressed={hsvToHex(hsv).toLowerCase() === preset}
|
||||
style={{ "--nodedc-palette-swatch": preset } as CSSProperties}
|
||||
onClick={() => {
|
||||
const next = hexToHsv(preset);
|
||||
if (next) commit(next);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
<input
|
||||
className="nodedc-color-field__text"
|
||||
value={value}
|
||||
aria-label={`${label}: HEX`}
|
||||
disabled={disabled}
|
||||
spellCheck={false}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user