mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-06 02:25:24 +00:00
feat: shared tile grid for plugin pages and many-tile dashboard cards (SDK 1.0.26)
This commit is contained in:
1 parent
1c4fa52de3
commit
9613b787a7
22 files changed
+1997
-7
No files matched your search
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* A tile on a 12 column grid. x and w are columns, y is unscaled px from the
|
||||
* top (the grid settles it), h is a fixed height in unscaled px or null for
|
||||
* the tile's natural height.
|
||||
*/
|
||||
export interface TileSlot {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h?: number | null;
|
||||
}
|
||||
|
||||
/** A tile with a resolved height, as the grid draws it. */
|
||||
export interface TileBox {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export const TILE_COLUMNS = 12;
|
||||
export const MIN_TILE_COLUMNS = 2;
|
||||
export const MIN_TILE_HEIGHT = 48;
|
||||
export const MAX_TILE_HEIGHT = 10000;
|
||||
/** y given to a new tile so it settles under everything else. */
|
||||
export const BOTTOM = 1_000_000;
|
||||
|
||||
export const clampTileColumns = (w: number, min = MIN_TILE_COLUMNS) =>
|
||||
Math.max(min, Math.min(TILE_COLUMNS, Math.round(w) || min));
|
||||
|
||||
const overlapsX = (a: TileBox, b: TileBox) =>
|
||||
a.x < b.x + b.w && b.x < a.x + a.w;
|
||||
|
||||
/**
|
||||
* Settles tiles top to bottom in (y, x) order. Each tile sits right under the
|
||||
* lowest tile placed before it in the columns it covers. Tiles never move
|
||||
* sideways and never jump above a tile that came before them.
|
||||
*/
|
||||
export function settleTiles(boxes: TileBox[], gap = 0): TileBox[] {
|
||||
const order = [...boxes].sort(
|
||||
(a, b) => a.y - b.y || a.x - b.x || (a.id < b.id ? -1 : 1),
|
||||
);
|
||||
const placed: TileBox[] = [];
|
||||
for (const box of order) {
|
||||
const y = placed.reduce(
|
||||
(top, p) => (overlapsX(p, box) ? Math.max(top, p.y + p.h + gap) : top),
|
||||
0,
|
||||
);
|
||||
placed.push({ ...box, y });
|
||||
}
|
||||
return placed;
|
||||
}
|
||||
|
||||
export function tileBoxes(
|
||||
slots: TileSlot[],
|
||||
heights: Record<string, number>,
|
||||
fallback = 160,
|
||||
): TileBox[] {
|
||||
return slots.map((slot) => ({
|
||||
id: slot.id,
|
||||
x: slot.x,
|
||||
y: slot.y,
|
||||
w: slot.w,
|
||||
h: slot.h ?? heights[slot.id] ?? fallback,
|
||||
}));
|
||||
}
|
||||
|
||||
export function tileBottom(boxes: TileBox[], gap = 0) {
|
||||
return boxes.reduce((max, box) => Math.max(max, box.y + box.h + gap), 0);
|
||||
}
|
||||
|
||||
/** Drops broken entries and duplicates and keeps every tile inside the grid. */
|
||||
export function sanitizeTiles(
|
||||
value: unknown,
|
||||
min = MIN_TILE_COLUMNS,
|
||||
): TileSlot[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const seen = new Set<string>();
|
||||
const out: TileSlot[] = [];
|
||||
for (const raw of value) {
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
const slot = raw as Record<string, unknown>;
|
||||
const id = typeof slot.id === "string" ? slot.id : "";
|
||||
if (!id || id.length > 300 || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
const w = clampTileColumns(Number(slot.w), min);
|
||||
const x = Math.max(
|
||||
0,
|
||||
Math.min(TILE_COLUMNS - w, Math.round(Number(slot.x)) || 0),
|
||||
);
|
||||
const y = Math.max(0, Number(slot.y) || 0);
|
||||
const h =
|
||||
slot.h == null || !Number.isFinite(Number(slot.h))
|
||||
? null
|
||||
: Math.max(
|
||||
MIN_TILE_HEIGHT,
|
||||
Math.min(MAX_TILE_HEIGHT, Math.round(Number(slot.h))),
|
||||
);
|
||||
out.push({ id, x, y, w, h });
|
||||
}
|
||||
return out.slice(0, 200);
|
||||
}
|
||||
|
||||
/** Adds a tile at the bottom, at the left. */
|
||||
export function appendTile(
|
||||
slots: TileSlot[],
|
||||
tile: { id: string; w?: number; h?: number | null },
|
||||
): TileSlot[] {
|
||||
if (slots.some((slot) => slot.id === tile.id)) return slots;
|
||||
return [
|
||||
...slots,
|
||||
{
|
||||
id: tile.id,
|
||||
x: 0,
|
||||
y: BOTTOM,
|
||||
w: clampTileColumns(tile.w ?? 4),
|
||||
h: tile.h ?? null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function removeTile(slots: TileSlot[], id: string): TileSlot[] {
|
||||
return slots.filter((slot) => slot.id !== id);
|
||||
}
|
||||
|
||||
/** Writes settled positions back, in reading order. */
|
||||
export function tilesFromBoxes(
|
||||
slots: TileSlot[],
|
||||
boxes: TileBox[],
|
||||
): TileSlot[] {
|
||||
const byId = new Map(boxes.map((box) => [box.id, box]));
|
||||
return slots
|
||||
.map((slot) => {
|
||||
const box = byId.get(slot.id);
|
||||
return box ? { ...slot, x: box.x, y: Math.round(box.y), w: box.w } : slot;
|
||||
})
|
||||
.sort((a, b) => a.y - b.y || a.x - b.x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays tiles out left to right in rows, for layouts that only know an order
|
||||
* and a width (old dashboards, generated defaults).
|
||||
*/
|
||||
export function flowTiles(
|
||||
tiles: { id: string; w: number; h?: number | null }[],
|
||||
rowHeight = 160,
|
||||
): TileSlot[] {
|
||||
const out: TileSlot[] = [];
|
||||
let x = 0;
|
||||
let row = 0;
|
||||
for (const tile of tiles) {
|
||||
const w = clampTileColumns(tile.w);
|
||||
if (x + w > TILE_COLUMNS) {
|
||||
x = 0;
|
||||
row++;
|
||||
}
|
||||
out.push({ id: tile.id, x, y: row * rowHeight, w, h: tile.h ?? null });
|
||||
x += w;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A tile's box in whole screen px. */
|
||||
export interface PixelRect {
|
||||
l: number;
|
||||
t: number;
|
||||
r: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
/** A 1 px line the grid draws, as a box. */
|
||||
export interface GridLine {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
function mergeSpans(spans: [number, number][]): [number, number][] {
|
||||
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
||||
const out: [number, number][] = [];
|
||||
for (const [start, end] of sorted) {
|
||||
const last = out[out.length - 1];
|
||||
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
|
||||
else out.push([start, end]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The lines between edge to edge tiles, each drawn once. A line sits on the
|
||||
* first px column or row past a tile, so it covers the edge of whatever is
|
||||
* next to it. The outer left, top and right edges are left out. With
|
||||
* `openBottom` (the grid runs to the bottom of the screen) so is the line
|
||||
* along the very bottom; a column that ends higher keeps its bottom line.
|
||||
* Lines reach one px further so corners join.
|
||||
*/
|
||||
export function gridLines(
|
||||
rects: PixelRect[],
|
||||
width: number,
|
||||
openBottom = false,
|
||||
): GridLine[] {
|
||||
const lowest = Math.max(0, ...rects.map((rect) => rect.b));
|
||||
const end = openBottom ? lowest : Infinity;
|
||||
const vertical = new Map<number, [number, number][]>();
|
||||
const horizontal = new Map<number, [number, number][]>();
|
||||
const add = (
|
||||
map: Map<number, [number, number][]>,
|
||||
at: number,
|
||||
span: [number, number],
|
||||
) => {
|
||||
if (span[1] <= span[0]) return;
|
||||
map.set(at, [...(map.get(at) ?? []), span]);
|
||||
};
|
||||
for (const rect of rects) {
|
||||
if (rect.r <= rect.l || rect.b <= rect.t) continue;
|
||||
const down: [number, number] = [rect.t, Math.min(end, rect.b + 1)];
|
||||
if (rect.l > 0) add(vertical, rect.l, down);
|
||||
if (rect.r < width) add(vertical, rect.r, down);
|
||||
const across: [number, number] = [rect.l, Math.min(width, rect.r + 1)];
|
||||
if (rect.t > 0) add(horizontal, rect.t, across);
|
||||
if (rect.b < end) add(horizontal, rect.b, across);
|
||||
}
|
||||
const lines: GridLine[] = [];
|
||||
for (const [x, spans] of vertical)
|
||||
for (const [start, end] of mergeSpans(spans))
|
||||
lines.push({ x, y: start, w: 1, h: end - start });
|
||||
for (const [y, spans] of horizontal)
|
||||
for (const [start, end] of mergeSpans(spans))
|
||||
lines.push({ x: start, y, w: end - start, h: 1 });
|
||||
return lines;
|
||||
}
|
||||
Reference in new issue
Block a user