Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, within } from "storybook/test";

import { BuildInfoFooter } from "./BuildInfoFooter";

import type { Meta, StoryObj } from "@storybook/react-vite";

const meta = {
title: "Design Patterns/Build Info Footer",
component: BuildInfoFooter,
parameters: {
layout: "fullscreen",
zephyr: { testCaseId: "" },
},
tags: ["autodocs"],
args: {
version: "1.4.2",
commitSha: "a1b2c3d",
},
} satisfies Meta<typeof BuildInfoFooter>;

export default meta;
type Story = StoryObj<typeof meta>;

/** App version + commit SHA. */
export const Default: Story = {
parameters: {
zephyr: { testCaseId: "SW-T5653" },
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step("renders the version and commit", async () => {
expect(canvasElement.querySelector('[data-slot="build-info-footer"]')).toBeInTheDocument();
expect(canvas.getByText("v1.4.2")).toBeVisible();
expect(canvas.getByText("a1b2c3d")).toBeVisible();
});
},
};

/** Only what you pass renders — here just the version. */
export const VersionOnly: Story = {
args: {
commitSha: undefined,
},
parameters: {
zephyr: { testCaseId: "SW-T5654" },
},
};
49 changes: 49 additions & 0 deletions src/components/composed/BuildInfoFooter/BuildInfoFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from "react";

import { cn } from "@/lib/utils";

export interface BuildInfoFooterProps
extends Omit<React.ComponentProps<"footer">, "children"> {
/** Application version (rendered as `v{version}`) */
version?: string;
/** Short commit SHA */
commitSha?: string;
}

/**
* A subtle full-width footer bar for application build metadata — the app
* version and commit SHA. Both fields are optional and only render when
* provided. Meant to sit at the bottom of a page/app.
*/
export function BuildInfoFooter({
version,
commitSha,
className,
...props
}: BuildInfoFooterProps) {
const meta = [version && `v${version}`, commitSha].filter(
(x): x is string => Boolean(x)
);

return (
<footer
data-slot="build-info-footer"
className={cn(
"flex w-full flex-wrap items-center justify-end gap-2 border-t border-border bg-muted/30 px-4 py-2 text-xs text-muted-foreground tabular-nums",
className
)}
{...props}
>
{meta.map((item, i) => (
<React.Fragment key={item}>
{i > 0 && (
<span aria-hidden className="text-muted-foreground/40">
·
</span>
)}
<span>{item}</span>
</React.Fragment>
))}
</footer>
);
}
2 changes: 2 additions & 0 deletions src/components/composed/BuildInfoFooter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { BuildInfoFooter } from "./BuildInfoFooter";
export type { BuildInfoFooterProps } from "./BuildInfoFooter";
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "@/index.css";

// Composed
export * from "@/components/composed/AppShellSimple";
export * from "@/components/composed/BuildInfoFooter";
export * from "@/components/composed/FormPatterns";
export * from "@/components/composed/StatCard";
export * from "@/components/composed/DataAppShell";
Expand Down
Loading