Extract shared home and environment settings for product reuse

This commit is contained in:
Codex
2026-09-07 22:54:17 +03:00
parent 26a1bf72a2
commit b10fd5d645
21 changed files with 1431 additions and 13 deletions
+84
View File
@@ -0,0 +1,84 @@
export type EnvironmentMediaKind = "image" | "video";
export type EnvironmentMediaSource = "file" | "url";
export interface EnvironmentMediaItem {
id: string;
source: EnvironmentMediaSource;
url: string | null;
mediaKind: EnvironmentMediaKind | null;
fileName: string | null;
}
export interface EnvironmentBackground {
enabled: boolean;
imageDurationSeconds: number;
items: EnvironmentMediaItem[];
}
export interface EnvironmentPage {
headerLabel: string;
eyebrow: string;
title: string;
description: string;
primaryWorkspaceId: string | null;
secondaryWorkspaceId: string | null;
background: EnvironmentBackground;
}
export interface EnvironmentSettings { revision: number; pages: Record<string, EnvironmentPage> }
export interface UploadedEnvironmentMedia { url: string; fileName: string; mediaKind: EnvironmentMediaKind }
export interface EnvironmentAction { id: string; label: string; description?: string }
export interface EnvironmentSurface { id: string; home?: boolean; description?: string; actions: readonly EnvironmentAction[] }
export const defaultEnvironmentImageDurationSeconds = 10;
export const maxEnvironmentMediaItems = 24;
export function createEnvironmentMediaItem(): EnvironmentMediaItem {
return {
id: `media-${crypto.randomUUID()}`,
source: "file",
url: null,
mediaKind: null,
fileName: null,
};
}
export function appendEnvironmentMediaItem(
background: EnvironmentBackground,
item: EnvironmentMediaItem = createEnvironmentMediaItem(),
): EnvironmentBackground {
if (
background.items.length >= maxEnvironmentMediaItems
|| background.items.some((candidate) => candidate.id === item.id)
) {
return background;
}
return {
...background,
enabled: background.items.length === 0 ? true : background.enabled,
items: [...background.items, item],
};
}
export function removeEnvironmentMediaItem(
background: EnvironmentBackground,
itemId: string,
): EnvironmentBackground {
const items = background.items.filter((item) => item.id !== itemId);
if (items.length === background.items.length) return background;
return {
...background,
enabled: items.length > 0 && background.enabled,
items,
};
}
export function inferEnvironmentMediaKind(url: string): EnvironmentMediaKind {
return /\.(mp4|webm|mov)(?:[?#].*)?$/i.test(url) ? "video" : "image";
}
export function cloneEnvironmentSettings(settings: EnvironmentSettings): EnvironmentSettings {
return { ...settings, pages: Object.fromEntries(Object.entries(settings.pages).map(([id, page]) => [id, {
...page, background: { ...page.background, items: page.background.items.map(item => ({ ...item })) },
}])) };
}
+1
View File
@@ -3,3 +3,4 @@ export * from "./floating.js";
export * from "./glass.js";
export * from "./theme.js";
export * from "./toolbar.js";
export * from "./environment.js";
+348
View File
@@ -185,6 +185,26 @@
background: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 84%, white);
}
.nodedc-button[data-variant="primary"][data-tone="neutral"] {
--nodedc-button-bg: var(--nodedc-neutral-action-bg);
--nodedc-button-color: var(--nodedc-neutral-action-color);
box-shadow: none;
}
.nodedc-button[data-variant="primary"][data-tone="neutral"]:hover:not(:disabled) {
background: var(--nodedc-neutral-action-hover);
}
.nodedc-button[data-variant="primary"][data-tone="neutral"]:disabled {
background: var(--nodedc-neutral-action-disabled-bg);
color: var(--nodedc-neutral-action-disabled-color);
opacity: 1;
}
.nodedc-button[data-variant="primary"][data-tone="neutral"]:focus-visible {
box-shadow: inset 0 0 0 2px var(--nodedc-neutral-action-color);
}
.nodedc-button[data-variant="ghost"] {
background: transparent;
box-shadow: none;
@@ -3850,3 +3870,331 @@ textarea.nodedc-field__control {
.nodedc-status[data-variant="indicator"][data-tone="warning"]::before { background: rgb(var(--nodedc-warning-rgb)); }
.nodedc-status[data-variant="indicator"][data-tone="danger"]::before { background: rgb(var(--nodedc-danger-rgb)); }
.nodedc-status[data-variant="indicator"][data-tone="accent"]::before { background: rgb(var(--nodedc-accent-rgb)); }
/* Shared product landing and environment settings. */
.nodedc-landing-stage {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
border-radius: var(--nodedc-radius-card);
background: var(--nodedc-canvas-soft);
}
.nodedc-landing-stage__media,
.nodedc-landing-stage__shade {
position: absolute;
inset: 0;
}
.nodedc-landing-stage__media img,
.nodedc-landing-stage__media video {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.nodedc-landing-stage__shade {
z-index: 1;
background:
linear-gradient(90deg, rgb(5 6 8 / 0.82) 0%, rgb(5 6 8 / 0.54) 46%, rgb(5 6 8 / 0.24) 100%),
linear-gradient(0deg, rgb(5 6 8 / 0.58), transparent 38%);
pointer-events: none;
}
.nodedc-landing-stage:not([data-has-media="true"]) .nodedc-landing-stage__shade {
background: none;
}
.nodedc-landing-stage__copy {
position: absolute;
z-index: 2;
top: 50%;
left: clamp(2rem, 5vw, 6rem);
width: min(39rem, 50%);
transform: translateY(-55%);
}
.nodedc-landing-stage__copy h1 {
margin: 0.75rem 0 1rem;
color: var(--nodedc-text-primary);
font-size: clamp(3rem, 7vw, 7.6rem);
font-weight: 600;
letter-spacing: -0.072em;
line-height: 0.87;
}
.nodedc-landing-stage__copy p {
max-width: 34rem;
margin: 0;
color: var(--nodedc-text-secondary);
font-size: clamp(0.8rem, 1vw, 1rem);
line-height: 1.6;
}
.nodedc-landing-stage__actions {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
margin-top: 1.6rem;
}
.nodedc-landing-stage__status {
position: absolute;
z-index: 2;
top: 2.4rem;
right: 2.4rem;
display: grid;
width: min(24rem, 30vw);
gap: 0.6rem;
border-radius: 1.25rem;
background: rgb(10 10 12 / 0.58);
padding: 1rem;
backdrop-filter: blur(18px);
}
.nodedc-landing-stage__status > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.nodedc-landing-stage__status p {
margin: 0.15rem 0 0;
color: var(--nodedc-text-muted);
font-size: 0.66rem;
line-height: 1.45;
}
.nodedc-landing-stage__footer {
position: absolute;
z-index: 2;
right: 2.4rem;
bottom: 1.8rem;
left: 2.4rem;
display: flex;
justify-content: space-between;
gap: 1rem;
color: var(--nodedc-text-muted);
font-size: 0.57rem;
font-weight: 780;
letter-spacing: 0.12em;
}
.nodedc-environment-settings {
display: grid;
gap: 1rem;
}
.nodedc-environment-settings__copy {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.nodedc-environment-settings__editor {
display: grid;
gap: 1rem;
}
.nodedc-environment-settings__surface {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(18rem, 24rem);
align-items: center;
gap: 1rem;
}
.nodedc-environment-settings__surface > span {
color: var(--nodedc-text-secondary);
font-size: 0.74rem;
font-weight: 650;
}
.nodedc-environment-settings__surface .nodedc-select-anchor,
.nodedc-environment-settings__surface .nodedc-select {
width: 100%;
}
.nodedc-environment-settings__quick-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem;
}
.nodedc-environment-settings__quick-actions > div {
display: grid;
gap: 0.4rem;
}
.nodedc-environment-settings__quick-actions span {
color: var(--nodedc-text-secondary);
font-size: 0.68rem;
font-weight: 650;
}
.nodedc-environment-settings__quick-actions .nodedc-select-anchor,
.nodedc-environment-settings__quick-actions .nodedc-select {
width: 100%;
}
.nodedc-environment-media-playlist {
display: grid;
min-width: 0;
gap: 0.85rem;
}
.nodedc-environment-media-playlist__head {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.nodedc-environment-media-playlist__head > div {
display: grid;
min-width: 0;
gap: 0.22rem;
}
.nodedc-environment-media-playlist__head span {
color: var(--nodedc-text-secondary);
font-size: var(--nodedc-font-size-sm);
font-weight: var(--nodedc-font-weight-medium);
}
.nodedc-environment-media-playlist__head p,
.nodedc-environment-media-playlist__empty,
.nodedc-environment-media-playlist__timing > span,
.nodedc-environment-media-playlist__error {
margin: 0;
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-xs);
line-height: 1.4;
}
.nodedc-environment-media-playlist__error {
color: rgb(var(--nodedc-danger-rgb));
}
.nodedc-environment-media-playlist__items {
display: grid;
min-width: 0;
gap: 0.7rem;
}
.nodedc-environment-media-playlist__item {
display: grid;
min-width: 0;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 0.4rem;
}
.nodedc-environment-media-playlist__item-actions {
display: flex;
align-items: center;
gap: 0.1rem;
padding-top: 1.65rem;
}
.nodedc-environment-media-playlist__timing {
display: grid;
gap: 0.42rem;
}
@media (max-width: 760px) {
.nodedc-environment-settings__copy,
.nodedc-environment-settings__quick-actions {
grid-template-columns: 1fr;
}
.nodedc-environment-settings__surface {
grid-template-columns: 1fr;
}
.nodedc-environment-media-playlist__item {
grid-template-columns: minmax(0, 1fr);
}
.nodedc-environment-media-playlist__item-actions {
justify-self: end;
padding-top: 0;
}
}
@media (max-width: 1480px) {
.nodedc-landing-stage__copy {
width: min(35rem, 54%);
}
}
@media (max-width: 1040px) {
.nodedc-landing-stage__status {
width: 20rem;
max-width: 36vw;
}
}
@media (max-width: 1040px) {
.nodedc-landing-stage__copy h1 {
font-size: clamp(2.7rem, 7vw, 5.6rem);
}
}
@media (max-width: 760px) {
.nodedc-landing-stage__copy {
top: 42%;
right: 1.2rem;
left: 1.2rem;
width: auto;
}
}
@media (max-width: 760px) {
.nodedc-landing-stage__copy h1 {
font-size: clamp(2.8rem, 14vw, 5rem);
}
}
@media (max-width: 760px) {
.nodedc-landing-stage__status {
top: auto;
right: 1rem;
bottom: 3.3rem;
left: 1rem;
width: auto;
max-width: none;
}
}
@media (max-width: 760px) {
.nodedc-landing-stage__footer {
right: 1rem;
bottom: 1.1rem;
left: 1rem;
}
}
@media (max-width: 760px) {
.nodedc-landing-stage__footer span:first-child {
display: none;
}
}
@media (max-height: 720px) and (min-width: 761px) {
.nodedc-landing-stage__copy h1 {
font-size: clamp(2.7rem, 6vw, 5rem);
}
}
@media (max-height: 720px) and (min-width: 761px) {
.nodedc-landing-stage__copy p {
font-size: 0.76rem;
}
}
@media (max-height: 720px) and (min-width: 761px) {
.nodedc-landing-stage__actions {
margin-top: 1rem;
}
}
@media (max-height: 720px) and (min-width: 761px) {
.nodedc-landing-stage__status {
top: 1.2rem;
right: 1.2rem;
}
}
.nodedc-landing-stage__eyebrow { color: var(--nodedc-text-muted); font-size: var(--nodedc-font-size-xs); font-weight: var(--nodedc-font-weight-strong); letter-spacing: .12em; }