merge: stack assistant on mobile boards
This commit is contained in:
1326 files changed
+131355
-46380
No files matched your search
@@ -8,7 +8,49 @@ export const backgroundImageRepeats = createDefinition(["repeat", "repeat-x", "r
|
||||
export const backgroundImageSizes = createDefinition(["cover", "contain"], {
|
||||
defaultValue: "cover",
|
||||
});
|
||||
export const layoutRoles = createDefinition(["mobile", "base", "custom"], { defaultValue: "custom" });
|
||||
|
||||
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
|
||||
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
|
||||
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
|
||||
export type LayoutRole = inferDefinitionType<typeof layoutRoles>;
|
||||
|
||||
const MAX_LAYOUT_BREAKPOINT = 32767;
|
||||
const compareBoardLayouts = (
|
||||
layoutA: { id: string; breakpoint: number; columnCount: number },
|
||||
layoutB: { id: string; breakpoint: number; columnCount: number },
|
||||
) =>
|
||||
layoutA.breakpoint - layoutB.breakpoint ||
|
||||
layoutA.columnCount - layoutB.columnCount ||
|
||||
layoutA.id.localeCompare(layoutB.id);
|
||||
|
||||
export const normalizeBoardLayoutRoles = <
|
||||
TLayout extends { id: string; breakpoint: number; columnCount: number; role?: LayoutRole },
|
||||
>(
|
||||
sourceLayouts: TLayout[],
|
||||
): Array<TLayout & { role: LayoutRole }> => {
|
||||
const mobileLayouts = sourceLayouts.filter((layout) => layout.role === "mobile");
|
||||
const baseLayouts = sourceLayouts.filter((layout) => layout.role === "base");
|
||||
const mobileLayout = mobileLayouts.length === 1 ? mobileLayouts[0] : undefined;
|
||||
const baseLayout = baseLayouts.length === 1 ? baseLayouts[0] : undefined;
|
||||
const orderedLayouts =
|
||||
mobileLayout && baseLayout
|
||||
? [
|
||||
mobileLayout,
|
||||
...sourceLayouts.filter((layout) => layout.role === "custom").toSorted(compareBoardLayouts),
|
||||
baseLayout,
|
||||
]
|
||||
: sourceLayouts.toSorted(compareBoardLayouts);
|
||||
|
||||
let previousBreakpoint = -1;
|
||||
return orderedLayouts.map((layout, index) => {
|
||||
const remainingLayouts = orderedLayouts.length - index - 1;
|
||||
const breakpoint =
|
||||
index === 0
|
||||
? 0
|
||||
: Math.min(MAX_LAYOUT_BREAKPOINT - remainingLayouts, Math.max(previousBreakpoint + 1, layout.breakpoint));
|
||||
const role: LayoutRole = index === 0 ? "mobile" : index === orderedLayouts.length - 1 ? "base" : "custom";
|
||||
previousBreakpoint = breakpoint;
|
||||
return { ...layout, breakpoint, role };
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { HomarrDocumentationPath } from "./homarr-docs-sitemap";
|
||||
|
||||
const documentationBaseUrl = "https://homarr.dev";
|
||||
const defaultDocumentationBaseUrl = "https://homarr.dev";
|
||||
|
||||
const getDocumentationBaseUrl = () => {
|
||||
const browserValue =
|
||||
typeof document === "undefined"
|
||||
? undefined
|
||||
: document.querySelector<HTMLMetaElement>('meta[name="homarr-website-url"]')?.content;
|
||||
const serverValue = typeof process === "undefined" ? undefined : process.env.HOMARR_WEBSITE_URL;
|
||||
return (browserValue || serverValue || defaultDocumentationBaseUrl).replace(/\/+$/u, "");
|
||||
};
|
||||
|
||||
// Please use the method so the path can be checked!
|
||||
export const createDocumentationLink = (
|
||||
@@ -8,7 +17,7 @@ export const createDocumentationLink = (
|
||||
hashTag?: `#${string}`,
|
||||
queryParams?: Record<string, string>,
|
||||
) => {
|
||||
const url = `${documentationBaseUrl}${path}`;
|
||||
const url = `${getDocumentationBaseUrl()}${path}`;
|
||||
const params = queryParams ? `?${new URLSearchParams(queryParams)}` : "";
|
||||
return `${url}${params}${hashTag ?? ""}`;
|
||||
};
|
||||
@@ -1,2 +1,33 @@
|
||||
export const sectionKinds = ["category", "empty", "dynamic"] as const;
|
||||
export const sectionKinds = ["empty", "container"] as const;
|
||||
export type SectionKind = (typeof sectionKinds)[number];
|
||||
|
||||
export const boardLanes = ["left", "main", "right"] as const;
|
||||
export type BoardLane = (typeof boardLanes)[number];
|
||||
|
||||
export const rootSectionOffsets = {
|
||||
left: -1,
|
||||
main: 0,
|
||||
right: 1,
|
||||
} as const satisfies Record<BoardLane, number>;
|
||||
|
||||
export const getRootSectionLane = (xOffset: number | null | undefined): BoardLane => {
|
||||
if (xOffset === rootSectionOffsets.left) return "left";
|
||||
if (xOffset === rootSectionOffsets.right) return "right";
|
||||
return "main";
|
||||
};
|
||||
|
||||
export const getBoardLaneColumnCount = (
|
||||
layout: {
|
||||
columnCount: number;
|
||||
leftGutterColumnCount?: number | null;
|
||||
rightGutterColumnCount?: number | null;
|
||||
},
|
||||
lane: BoardLane,
|
||||
) => {
|
||||
const total = Math.max(1, Math.floor(layout.columnCount));
|
||||
const left = Math.min(Math.max(0, Math.floor(layout.leftGutterColumnCount ?? 0)), total - 1);
|
||||
const right = Math.min(Math.max(0, Math.floor(layout.rightGutterColumnCount ?? 0)), total - left - 1);
|
||||
if (lane === "left") return left;
|
||||
if (lane === "right") return right;
|
||||
return total - left - right;
|
||||
};
|
||||
@@ -30,6 +30,21 @@ describe("createDocumentationLink should generate correct URLs", () => {
|
||||
});
|
||||
|
||||
describe("createDocumentationLink parameter validation", () => {
|
||||
test("uses the server-rendered website URL at runtime", () => {
|
||||
const meta = document.createElement("meta");
|
||||
meta.name = "homarr-website-url";
|
||||
meta.content = "https://docs.example.com/base/";
|
||||
document.head.append(meta);
|
||||
|
||||
try {
|
||||
expect(createDocumentationLink("/docs/getting-started")).toBe(
|
||||
"https://docs.example.com/base/docs/getting-started",
|
||||
);
|
||||
} finally {
|
||||
meta.remove();
|
||||
}
|
||||
});
|
||||
|
||||
test("should work with only path parameter", () => {
|
||||
const result = createDocumentationLink("/docs/getting-started");
|
||||
expect(result).toBe("https://homarr.dev/docs/getting-started");
|
||||
|
||||
@@ -44,12 +44,12 @@ export const widgetIntegrationSupport: Partial<Record<WidgetKind, readonly Integ
|
||||
speedtestTracker: ["speedtestTracker"],
|
||||
uptimeKuma: ["uptimeKuma"],
|
||||
audioStats: ["navidrome", "audiobookshelf"],
|
||||
umami: ["umami"],
|
||||
vpn: getIntegrationKindsByCategory("vpn"),
|
||||
ups: getIntegrationKindsByCategory("ups"),
|
||||
archiveTeamWarrior: ["archiveTeamWarrior"],
|
||||
anchorNote: ["anchor"],
|
||||
traefik: ["traefik"],
|
||||
umami: ["umami"],
|
||||
ups: getIntegrationKindsByCategory("ups"),
|
||||
};
|
||||
|
||||
/** Widgets that remain useful without a configured integration. */
|
||||
|
||||
Reference in new issue
Block a user