fix: refine header layout editor
This commit is contained in:
1 parent
e68a1491ae
commit
2dbeb792a7
3 files changed
+160
-246
No files matched your search
+62
-122
@@ -1,13 +1,7 @@
|
||||
.composer {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse 70% 42% at 50% 0%,
|
||||
color-mix(in srgb, var(--mantine-primary-color-light) 32%, transparent),
|
||||
transparent 72%
|
||||
),
|
||||
var(--mantine-color-body);
|
||||
background: var(--mantine-color-body);
|
||||
}
|
||||
|
||||
.heading,
|
||||
@@ -22,7 +16,7 @@
|
||||
.stage {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 20rem;
|
||||
min-height: 21rem;
|
||||
padding: var(--mantine-spacing-md);
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
background-image: radial-gradient(var(--mantine-color-default-border) 0.75px, transparent 0.75px);
|
||||
@@ -31,8 +25,8 @@
|
||||
|
||||
.device {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 15rem;
|
||||
width: min(100%, 31.2rem);
|
||||
min-height: 18rem;
|
||||
overflow: hidden;
|
||||
background: var(--mantine-color-body);
|
||||
box-shadow: var(--mantine-shadow-lg);
|
||||
@@ -41,107 +35,89 @@
|
||||
min-height 180ms ease;
|
||||
}
|
||||
|
||||
.stage[data-size="mobile"] .device {
|
||||
width: min(100%, 24rem);
|
||||
min-height: 18rem;
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 5rem;
|
||||
min-height: 5rem;
|
||||
border-bottom: 1px solid var(--mantine-color-default-border);
|
||||
background: color-mix(in srgb, var(--mantine-color-body) 94%, transparent);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.stripScroller {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-color: var(--mantine-color-default-border) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.sortableStrip {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.25rem;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.35rem var(--mantine-spacing-sm) 0.45rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zoneOriginLabel {
|
||||
position: absolute;
|
||||
inset-block-start: 0.85rem;
|
||||
inset-inline-start: var(--mantine-spacing-sm);
|
||||
color: var(--mantine-color-dimmed);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zoneDelimiter {
|
||||
.zoneTable {
|
||||
display: grid;
|
||||
grid-template-rows: 1.2rem 1.75rem;
|
||||
place-items: center;
|
||||
gap: 0.2rem;
|
||||
width: 3.5rem;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
padding-block: 0.25rem 0.4rem;
|
||||
flex: 0 0 auto;
|
||||
user-select: none;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.zoneColumn {
|
||||
min-width: 0;
|
||||
border-inline-end: 1px solid var(--mantine-color-default-border);
|
||||
transition: background-color 120ms ease;
|
||||
}
|
||||
|
||||
.zoneLabel {
|
||||
.zoneColumn:last-child {
|
||||
border-inline-end: 0;
|
||||
}
|
||||
|
||||
.zoneColumn[data-drop-target="true"] {
|
||||
background: var(--mantine-primary-color-light);
|
||||
}
|
||||
|
||||
.zoneColumn[data-drop-target="true"] .zoneItems::after {
|
||||
content: "";
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border: 1px dashed var(--mantine-primary-color-filled);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
background: color-mix(in srgb, var(--mantine-primary-color-light) 65%, transparent);
|
||||
}
|
||||
|
||||
.zoneColumnHeader {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 1.75rem;
|
||||
margin: 0;
|
||||
color: var(--mantine-color-dimmed);
|
||||
border-bottom: 1px solid var(--mantine-color-default-border);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
transition: color 120ms ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.zoneLine {
|
||||
height: 1.5rem;
|
||||
border-color: var(--mantine-color-default-border);
|
||||
transition:
|
||||
height 120ms ease,
|
||||
border-color 120ms ease;
|
||||
}
|
||||
|
||||
.zoneDelimiter[data-drop-target="true"] {
|
||||
background: color-mix(in srgb, var(--mantine-primary-color-light) 48%, transparent);
|
||||
}
|
||||
|
||||
.zoneDelimiter[data-drop-target="true"] .zoneLabel {
|
||||
color: var(--mantine-primary-color-filled);
|
||||
}
|
||||
|
||||
.zoneDelimiter[data-drop-target="true"] .zoneLine {
|
||||
height: 1.75rem;
|
||||
border-color: var(--mantine-primary-color-filled);
|
||||
.zoneItems {
|
||||
display: flex;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
min-height: 3.25rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.sortableItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
padding-block: 1.55rem 0.125rem;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sortableItem[data-drop-target="true"]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block: 0.2rem;
|
||||
inset-inline-start: -0.2rem;
|
||||
z-index: 1;
|
||||
width: 2px;
|
||||
border-radius: var(--mantine-radius-xl);
|
||||
background: var(--mantine-primary-color-filled);
|
||||
box-shadow: 0 0 0 2px var(--mantine-primary-color-light);
|
||||
}
|
||||
|
||||
.sortableItem[data-dragging="true"] {
|
||||
opacity: 0.2;
|
||||
}
|
||||
@@ -216,28 +192,6 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mobileBurger {
|
||||
display: none;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
gap: 3px;
|
||||
place-content: center;
|
||||
align-self: flex-end;
|
||||
margin: 0 0.125rem 0.125rem 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.mobileBurger span {
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1px;
|
||||
background: var(--mantine-color-text);
|
||||
}
|
||||
|
||||
.stage[data-size="mobile"] .mobileBurger {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.hiddenPreview {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
@@ -252,7 +206,7 @@
|
||||
}
|
||||
|
||||
.dashboardPreview {
|
||||
height: 11.25rem;
|
||||
height: 13rem;
|
||||
overflow: hidden;
|
||||
opacity: 0.86;
|
||||
}
|
||||
@@ -262,18 +216,6 @@
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
.stage[data-size="mobile"] .sortableStrip {
|
||||
padding-inline: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.stage[data-size="mobile"] .zoneLabel {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.stage[data-size="mobile"] .dashboardPreview {
|
||||
height: 14.25rem;
|
||||
}
|
||||
|
||||
.tray {
|
||||
border-top: 1px solid var(--mantine-color-default-border);
|
||||
}
|
||||
@@ -296,9 +238,7 @@
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.device,
|
||||
.zoneDelimiter,
|
||||
.zoneLabel,
|
||||
.zoneLine,
|
||||
.zoneColumn,
|
||||
.itemButton {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
+97
-123
@@ -1,18 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers";
|
||||
import { type ReactNode, useMemo, useState } from "react";
|
||||
import { PointerActivationConstraints } from "@dnd-kit/dom";
|
||||
import { SortableKeyboardPlugin } from "@dnd-kit/dom/sortable";
|
||||
import type { DragEndEvent } from "@dnd-kit/react";
|
||||
import { DragDropProvider, DragOverlay, KeyboardSensor, PointerSensor } from "@dnd-kit/react";
|
||||
import { isSortable, useSortable } from "@dnd-kit/react/sortable";
|
||||
import { DragDropProvider, DragOverlay, KeyboardSensor, PointerSensor, useDroppable } from "@dnd-kit/react";
|
||||
import { useSortable } from "@dnd-kit/react/sortable";
|
||||
import {
|
||||
ActionIcon,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Group,
|
||||
Menu,
|
||||
Paper,
|
||||
@@ -71,11 +70,19 @@ interface HeaderComposerProps {
|
||||
mobileHomeBoardId: string | null;
|
||||
}
|
||||
|
||||
type PreviewSize = "desktop" | "mobile";
|
||||
interface HeaderItemDragData {
|
||||
kind: "item";
|
||||
itemKey: string;
|
||||
zone: HeaderZoneId;
|
||||
index: number;
|
||||
}
|
||||
|
||||
type HeaderStripEntry =
|
||||
| { id: string; kind: "divider"; zone: HeaderZoneId }
|
||||
| { id: string; item: HeaderItem; kind: "item" };
|
||||
interface HeaderZoneDropData {
|
||||
kind: "zone";
|
||||
zone: HeaderZoneId;
|
||||
}
|
||||
|
||||
type HeaderDropData = HeaderItemDragData | HeaderZoneDropData;
|
||||
|
||||
const headerSortableSensors = [
|
||||
PointerSensor.configure({
|
||||
@@ -83,17 +90,16 @@ const headerSortableSensors = [
|
||||
}),
|
||||
KeyboardSensor,
|
||||
];
|
||||
const headerSortableModifiers = [RestrictToHorizontalAxis];
|
||||
const headerSortableGroup = "header-composer";
|
||||
const headerSortablePlugins = [SortableKeyboardPlugin];
|
||||
const headerGuideZones = ["left", "center", "right"] as const;
|
||||
|
||||
export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHomeBoardId }: HeaderComposerProps) => {
|
||||
const [previewSize, setPreviewSize] = useState<PreviewSize>("desktop");
|
||||
export const HeaderComposer = ({ value, onChange, boards, homeBoardId }: HeaderComposerProps) => {
|
||||
const [draggedItemKey, setDraggedItemKey] = useState<string | null>(null);
|
||||
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
|
||||
const t = useI18n("management.page.user.setting.general.header");
|
||||
const boardsById = useMemo(() => new Map(boards.map((board) => [board.id, board])), [boards]);
|
||||
const activeItems = getHeaderItems(value.zones);
|
||||
const stripEntries = createHeaderStripEntries(value.zones);
|
||||
const activeItemKeySet = new Set(activeItems.map(getHeaderItemKey));
|
||||
const inactiveBuiltinIds = headerBuiltinItemIds.filter((itemId) => {
|
||||
const item = createBuiltinHeaderItem(itemId);
|
||||
@@ -104,9 +110,14 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHom
|
||||
);
|
||||
const draggedItem = findHeaderItem(activeItems, draggedItemKey);
|
||||
const selectedItem = findHeaderItem(activeItems, selectedItemKey);
|
||||
let previewBoardId = homeBoardId;
|
||||
if (previewSize === "mobile") previewBoardId = mobileHomeBoardId ?? homeBoardId;
|
||||
const previewBoard = boardsById.get(previewBoardId ?? "") ?? boards.at(0);
|
||||
const previewBoard = boardsById.get(homeBoardId ?? "") ?? boards.at(0);
|
||||
|
||||
const toggleSelectedItem = (itemKey: string) => {
|
||||
setSelectedItemKey((currentItemKey) => {
|
||||
if (currentItemKey === itemKey) return null;
|
||||
return itemKey;
|
||||
});
|
||||
};
|
||||
|
||||
const itemLabel = (item: HeaderItem) => {
|
||||
if (item.type === "board") return boardsById.get(item.boardId)?.name ?? t("items.boardUnavailable");
|
||||
@@ -138,11 +149,22 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHom
|
||||
const handleDragEnd = ({ canceled, operation }: DragEndEvent) => {
|
||||
setDraggedItemKey(null);
|
||||
const source = operation.source;
|
||||
if (canceled || !isSortable(source)) return;
|
||||
if (source.initialIndex === source.index) return;
|
||||
const target = operation.target;
|
||||
if (canceled || !source || !target) return;
|
||||
|
||||
const nextEntries = moveHeaderStripEntry(stripEntries, source.initialIndex, source.index);
|
||||
updateZones(createHeaderZonesFromStripEntries(nextEntries));
|
||||
const sourceData = source.data as HeaderItemDragData;
|
||||
const targetData = target.data as HeaderDropData;
|
||||
if (sourceData.kind !== "item") return;
|
||||
|
||||
let targetIndex = value.zones[targetData.zone].length;
|
||||
if (targetData.kind === "item") {
|
||||
if (targetData.itemKey === sourceData.itemKey) return;
|
||||
targetIndex = targetData.index;
|
||||
}
|
||||
|
||||
const nextZones = moveHeaderItem(value.zones, sourceData.itemKey, targetData.zone, targetIndex);
|
||||
if (nextZones === value.zones) return;
|
||||
updateZones(nextZones);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -163,15 +185,6 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHom
|
||||
</Text>
|
||||
</Stack>
|
||||
<Group gap="xs">
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
value={previewSize}
|
||||
onChange={(next) => setPreviewSize(next as PreviewSize)}
|
||||
data={[
|
||||
{ value: "desktop", label: t("device.desktop") },
|
||||
{ value: "mobile", label: t("device.mobile") },
|
||||
]}
|
||||
/>
|
||||
<Tooltip label={value.visible ? t("action.hide") : t("action.show")}>
|
||||
<ActionIcon
|
||||
size="lg"
|
||||
@@ -185,7 +198,7 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHom
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Box className={classes.stage} data-size={previewSize} data-visible={value.visible}>
|
||||
<Box className={classes.stage} data-visible={value.visible}>
|
||||
<Paper className={classes.device} radius="lg" withBorder>
|
||||
{value.visible ? (
|
||||
<DragDropProvider
|
||||
@@ -193,43 +206,27 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHom
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className={classes.previewHeader}>
|
||||
<div className={classes.stripScroller}>
|
||||
<div className={classes.sortableStrip}>
|
||||
<Text className={classes.zoneOriginLabel} component="span">
|
||||
{t("zones.left")}
|
||||
</Text>
|
||||
<MobileBurger />
|
||||
{stripEntries.map((entry, index) => {
|
||||
if (entry.kind === "divider") {
|
||||
return (
|
||||
<SortableZoneDivider
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
index={index}
|
||||
label={t(`zones.${entry.zone}` as never)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const item = entry.item;
|
||||
return (
|
||||
<div className={classes.zoneTable} onClick={() => setSelectedItemKey(null)}>
|
||||
{headerGuideZones.map((zone) => (
|
||||
<HeaderZoneColumn key={zone} zone={zone} label={t(`zones.${zone}` as never)}>
|
||||
{value.zones[zone].map((item, index) => (
|
||||
<SortableHeaderItem
|
||||
key={entry.id}
|
||||
key={getHeaderItemKey(item)}
|
||||
item={item}
|
||||
zone={zone}
|
||||
index={index}
|
||||
label={itemLabel(item)}
|
||||
previewSize={previewSize}
|
||||
searchDisplay={value.searchDisplay}
|
||||
boardLogo={
|
||||
item.type === "board" ? (boardsById.get(item.boardId)?.logoImageUrl ?? null) : null
|
||||
}
|
||||
configureLabel={t("action.configure", { item: itemLabel(item) })}
|
||||
selected={selectedItemKey === entry.id}
|
||||
onSelect={setSelectedItemKey}
|
||||
selected={selectedItemKey === getHeaderItemKey(item)}
|
||||
onSelect={toggleSelectedItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</HeaderZoneColumn>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DragOverlay dropAnimation={null}>
|
||||
@@ -237,7 +234,7 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHom
|
||||
<HeaderItemPreview
|
||||
item={draggedItem}
|
||||
label={itemLabel(draggedItem)}
|
||||
compact={previewSize === "mobile"}
|
||||
compact={false}
|
||||
searchDisplay={value.searchDisplay}
|
||||
boardLogo={
|
||||
draggedItem.type === "board" ? boardsById.get(draggedItem.boardId)?.logoImageUrl : null
|
||||
@@ -378,49 +375,33 @@ export const HeaderComposer = ({ value, onChange, boards, homeBoardId, mobileHom
|
||||
);
|
||||
};
|
||||
|
||||
interface SortableZoneDividerProps {
|
||||
entry: Extract<HeaderStripEntry, { kind: "divider" }>;
|
||||
index: number;
|
||||
interface HeaderZoneColumnProps {
|
||||
zone: HeaderZoneId;
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const SortableZoneDivider = ({ entry, index, label }: SortableZoneDividerProps) => {
|
||||
const { isDropTarget, ref } = useSortable({
|
||||
id: entry.id,
|
||||
index,
|
||||
group: headerSortableGroup,
|
||||
data: { kind: entry.kind, zone: entry.zone },
|
||||
disabled: { draggable: true },
|
||||
const HeaderZoneColumn = ({ zone, label, children }: HeaderZoneColumnProps) => {
|
||||
const { isDropTarget, ref } = useDroppable({
|
||||
id: `header-zone:${zone}`,
|
||||
data: { kind: "zone", zone } satisfies HeaderZoneDropData,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classes.zoneDelimiter}
|
||||
data-divider-zone={entry.zone}
|
||||
data-drop-target={isDropTarget || undefined}
|
||||
>
|
||||
<Text className={classes.zoneLabel} component="span">
|
||||
<section ref={ref} className={classes.zoneColumn} data-drop-target={isDropTarget || undefined}>
|
||||
<Text className={classes.zoneColumnHeader} component="h4">
|
||||
{label}
|
||||
</Text>
|
||||
<Divider className={classes.zoneLine} orientation="vertical" />
|
||||
</div>
|
||||
<div className={classes.zoneItems}>{children}</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileBurger = () => (
|
||||
<div className={classes.mobileBurger} aria-hidden>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SortableHeaderItemProps {
|
||||
item: HeaderItem;
|
||||
zone: HeaderZoneId;
|
||||
index: number;
|
||||
label: string;
|
||||
previewSize: PreviewSize;
|
||||
searchDisplay: HeaderPreferences["searchDisplay"];
|
||||
boardLogo: string | null;
|
||||
configureLabel: string;
|
||||
@@ -430,9 +411,9 @@ interface SortableHeaderItemProps {
|
||||
|
||||
const SortableHeaderItem = ({
|
||||
item,
|
||||
zone,
|
||||
index,
|
||||
label,
|
||||
previewSize,
|
||||
searchDisplay,
|
||||
boardLogo,
|
||||
configureLabel,
|
||||
@@ -443,10 +424,10 @@ const SortableHeaderItem = ({
|
||||
const { handleRef, isDragging, isDropTarget, ref } = useSortable({
|
||||
id: itemKey,
|
||||
index,
|
||||
group: headerSortableGroup,
|
||||
data: { kind: "item", item },
|
||||
group: `${headerSortableGroup}:${zone}`,
|
||||
data: { kind: "item", itemKey, zone, index } satisfies HeaderItemDragData,
|
||||
sensors: headerSortableSensors,
|
||||
modifiers: headerSortableModifiers,
|
||||
plugins: headerSortablePlugins,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -463,19 +444,19 @@ const SortableHeaderItem = ({
|
||||
type="button"
|
||||
className={classes.itemButton}
|
||||
data-kind={item.type === "builtin" ? item.id : "board"}
|
||||
data-compact={previewSize === "mobile" || undefined}
|
||||
data-wide={
|
||||
item.type === "builtin" && item.id === "search" && searchDisplay === "input" && previewSize === "desktop"
|
||||
? true
|
||||
: undefined
|
||||
(item.type === "builtin" && item.id === "search" && searchDisplay === "input") || undefined
|
||||
}
|
||||
aria-label={configureLabel}
|
||||
onClick={() => onSelect(itemKey)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSelect(itemKey);
|
||||
}}
|
||||
>
|
||||
<HeaderItemPreview
|
||||
item={item}
|
||||
label={label}
|
||||
compact={previewSize === "mobile"}
|
||||
compact={false}
|
||||
searchDisplay={searchDisplay}
|
||||
boardLogo={boardLogo}
|
||||
/>
|
||||
@@ -554,37 +535,30 @@ const PreviewItem = ({
|
||||
return <Icon size={20} stroke={1.7} />;
|
||||
};
|
||||
|
||||
const createHeaderStripEntries = (zones: HeaderZones): HeaderStripEntry[] => [
|
||||
...zones.left.map((item) => ({ id: getHeaderItemKey(item), item, kind: "item" as const })),
|
||||
{ id: "header-divider:center", kind: "divider", zone: "center" },
|
||||
...zones.center.map((item) => ({ id: getHeaderItemKey(item), item, kind: "item" as const })),
|
||||
{ id: "header-divider:right", kind: "divider", zone: "right" },
|
||||
...zones.right.map((item) => ({ id: getHeaderItemKey(item), item, kind: "item" as const })),
|
||||
];
|
||||
const moveHeaderItem = (
|
||||
zones: HeaderZones,
|
||||
itemKey: string,
|
||||
targetZone: HeaderZoneId,
|
||||
requestedTargetIndex: number,
|
||||
): HeaderZones => {
|
||||
let sourceItem: HeaderItem | undefined;
|
||||
|
||||
const createHeaderZonesFromStripEntries = (entries: HeaderStripEntry[]): HeaderZones => {
|
||||
const zones: HeaderZones = { left: [], center: [], right: [] };
|
||||
let activeZone: HeaderZoneId = "left";
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.kind === "divider") {
|
||||
activeZone = entry.zone;
|
||||
continue;
|
||||
}
|
||||
zones[activeZone].push(entry.item);
|
||||
for (const zone of headerGuideZones) {
|
||||
sourceItem = zones[zone].find((item) => getHeaderItemKey(item) === itemKey);
|
||||
if (sourceItem) break;
|
||||
}
|
||||
|
||||
return zones;
|
||||
};
|
||||
if (!sourceItem) return zones;
|
||||
|
||||
const moveHeaderStripEntry = (entries: HeaderStripEntry[], from: number, to: number): HeaderStripEntry[] => {
|
||||
if (from === to || from < 0 || from >= entries.length || to < 0 || to >= entries.length) return entries;
|
||||
|
||||
const reorderedEntries = entries.slice();
|
||||
const [movedEntry] = reorderedEntries.splice(from, 1);
|
||||
if (!movedEntry || movedEntry.kind !== "item") return entries;
|
||||
reorderedEntries.splice(to, 0, movedEntry);
|
||||
return reorderedEntries;
|
||||
const nextZones: HeaderZones = {
|
||||
left: zones.left.filter((item) => getHeaderItemKey(item) !== itemKey),
|
||||
center: zones.center.filter((item) => getHeaderItemKey(item) !== itemKey),
|
||||
right: zones.right.filter((item) => getHeaderItemKey(item) !== itemKey),
|
||||
};
|
||||
const targetItems = nextZones[targetZone];
|
||||
const targetIndex = Math.max(0, Math.min(requestedTargetIndex, targetItems.length));
|
||||
nextZones[targetZone] = [...targetItems.slice(0, targetIndex), sourceItem, ...targetItems.slice(targetIndex)];
|
||||
return nextZones;
|
||||
};
|
||||
|
||||
const findHeaderItem = (items: HeaderItem[], id: string | null): HeaderItem | undefined => {
|
||||
|
||||
@@ -5690,7 +5690,7 @@
|
||||
"header": {
|
||||
"title": "Header studio",
|
||||
"badge": "Personal",
|
||||
"description": "Drag controls anywhere in the row. Center and Right are fixed boundaries; everything before Center belongs to Left.",
|
||||
"description": "Drag controls anywhere in the row. Left, Center, and Right stay fixed while controls shift around them.",
|
||||
"device": {
|
||||
"desktop": "Desktop",
|
||||
"mobile": "Mobile"
|
||||
|
||||
Reference in new issue
Block a user