diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.ssr.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.ssr.test.ts new file mode 100644 index 000000000..9a6a869b1 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.ssr.test.ts @@ -0,0 +1,54 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { RenderResult } from "@canonical/svelte-ssr-test"; +import { render } from "@canonical/svelte-ssr-test"; +import type { ComponentProps } from "svelte"; +import { describe, expect, it } from "vitest"; +import Component from "./Timeline.svelte"; + +describe("Timeline SSR", () => { + const baseProps = {} satisfies ComponentProps; + + it("doesn't throw", () => { + expect(() => { + render(Component, { props: { ...baseProps } }); + }).not.toThrow(); + }); + + it("renders", () => { + const page = render(Component, { props: { ...baseProps } }); + expect(componentLocator(page)).toBeInstanceOf(page.window.HTMLOListElement); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", (attribute, expected) => { + const page = render(Component, { + props: { ...baseProps, [attribute]: expected }, + }); + expect(componentLocator(page).getAttribute(attribute)).toBe(expected); + }); + + it("applies classes", () => { + const page = render(Component, { + props: { ...baseProps, class: "test-class" }, + }); + expect(componentLocator(page).classList).toContain("test-class"); + expect(componentLocator(page).classList).toContain("ds"); + expect(componentLocator(page).classList).toContain("timeline"); + }); + + it("applies style", () => { + const page = render(Component, { + props: { ...baseProps, style: "color: orange;" }, + }); + expect(componentLocator(page).style.color).toBe("orange"); + }); + }); +}); + +function componentLocator(page: RenderResult): HTMLElement { + return page.getByRole("list"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.stories.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.stories.svelte new file mode 100644 index 000000000..ea1a66ab8 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.stories.svelte @@ -0,0 +1,124 @@ + + + + + + {#snippet titleRow()} + + added 1 commit and filed 2 issues + {#snippet date()} + + {/snippet} + + {/snippet} + Implementation of a new feature for Launchpad bug templates + + + {#snippet marker()} + + {/snippet} + {#snippet titleRow()} + + did things that are really complex and will probably take a while to + explain + {#snippet date()} + + {/snippet} + + {/snippet} +
+ Here is some custom content +
+
+ + {#snippet titleRow()} + + did some amazing things + {#snippet date()} + + {/snippet} + + {/snippet} + + + {#snippet titleRow()} + + did so many things that it could be a blog post, but let's keep it + short for now and just say that it was a lot + {#snippet date()} + + {/snippet} + + {/snippet} + + + {#snippet marker()} + + {/snippet} + {#snippet titleRow()} + + raised a flag + {#snippet date()} + + {/snippet} + + {/snippet} + The flag was raised + + + Show all + + + {#snippet marker()} + + {/snippet} + {#snippet titleRow()} + + The MP was Merged + {#snippet date()} + + {/snippet} + + {/snippet} + + +
+ Content goes here +
+
+ + Show more + Show all + +
+
diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.svelte new file mode 100644 index 000000000..7c61f5653 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.svelte @@ -0,0 +1,51 @@ + + + + +
    + {@render children?.()} +
+ + diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.svelte.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.svelte.test.ts new file mode 100644 index 000000000..ebb76bc8f --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/Timeline.svelte.test.ts @@ -0,0 +1,50 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { ComponentProps } from "svelte"; +import { describe, expect, it } from "vitest"; +import type { Locator } from "vitest/browser"; +import type { RenderResult } from "vitest-browser-svelte"; +import { render } from "vitest-browser-svelte"; +import Component from "./Timeline.svelte"; + +describe("Timeline component", () => { + const baseProps = {} satisfies ComponentProps; + + it("renders", async () => { + const page = render(Component, { ...baseProps }); + await expect.element(componentLocator(page)).toBeInTheDocument(); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", async (attribute, expected) => { + const page = render(Component, { ...baseProps, [attribute]: expected }); + await expect + .element(componentLocator(page)) + .toHaveAttribute(attribute, expected); + }); + + it("applies classes", async () => { + const page = render(Component, { ...baseProps, class: "test-class" }); + await expect.element(componentLocator(page)).toHaveClass("test-class"); + await expect.element(componentLocator(page)).toHaveClass("ds"); + await expect.element(componentLocator(page)).toHaveClass("timeline"); + }); + + it("applies style", async () => { + const page = render(Component, { + ...baseProps, + style: "color: orange;", + }); + await expect + .element(componentLocator(page)) + .toHaveStyle({ color: "orange" }); + }); + }); +}); + +function componentLocator(page: RenderResult): Locator { + return page.getByRole("list"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.ssr.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.ssr.test.ts new file mode 100644 index 000000000..aa928adf3 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.ssr.test.ts @@ -0,0 +1,130 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { RenderResult } from "@canonical/svelte-ssr-test"; +import { render } from "@canonical/svelte-ssr-test"; +import type { ComponentProps } from "svelte"; +import { createRawSnippet } from "svelte"; +import { describe, expect, it } from "vitest"; +import Component from "./Event.svelte"; + +describe("Event SSR", () => { + const children = createRawSnippet(() => ({ + render: () => `Child Content`, + })); + + const titleRow = createRawSnippet(() => ({ + render: () => `Title Row Content`, + })); + + const baseProps = {} satisfies ComponentProps; + + describe("basics", () => { + it("doesn't throw", () => { + expect(() => { + render(Component, { props: { ...baseProps } }); + }).not.toThrow(); + }); + + it("renders", () => { + const page = render(Component, { props: { ...baseProps } }); + expect(componentLocator(page)).toBeInstanceOf(page.window.HTMLLIElement); + }); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", (attribute, value) => { + const page = render(Component, { + props: { ...baseProps, [attribute]: value }, + }); + expect(componentLocator(page).getAttribute(attribute)).toBe(value); + }); + + it("applies classes", () => { + const page = render(Component, { + props: { + ...baseProps, + class: "test-class", + }, + }); + const element = componentLocator(page); + expect(element.classList).toContain("ds"); + expect(element.classList).toContain("timeline-event"); + expect(element.classList).toContain("test-class"); + }); + + it("applies style", () => { + const page = render(Component, { + props: { + ...baseProps, + style: "color: orange;", + }, + }); + expect(componentLocator(page).style.color).toBe("orange"); + }); + }); + + describe("Renders", () => { + it("with children", () => { + const page = render(Component, { + props: { + ...baseProps, + children, + }, + }); + expect(page.getByText("Child Content")).toBeInstanceOf( + page.window.HTMLElement, + ); + }); + + it("with title row", () => { + const page = render(Component, { + props: { + ...baseProps, + titleRow, + }, + }); + expect(page.getByText("Title Row Content")).toBeInstanceOf( + page.window.HTMLElement, + ); + }); + + describe("Marker", () => { + it("empty", () => { + const page = render(Component, { props: { ...baseProps } }); + const element = componentLocator(page); + expect(element.classList).toContain("marker-empty"); + }); + + it("small", () => { + const page = render(Component, { + props: { + ...baseProps, + markerSize: "small", + marker: { userName: "John Doe" }, + }, + }); + const element = componentLocator(page); + expect(element.classList).toContain("marker-small"); + }); + + it("large", () => { + const page = render(Component, { + props: { + ...baseProps, + markerSize: "large", + marker: { userName: "John Doe" }, + }, + }); + const element = componentLocator(page); + expect(element.classList).toContain("marker-large"); + }); + }); + }); +}); + +function componentLocator(page: RenderResult): HTMLLIElement { + return page.getByRole("listitem"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.stories.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.stories.svelte new file mode 100644 index 000000000..6baba10a4 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.stories.svelte @@ -0,0 +1,130 @@ + + +{#snippet date()} + +{/snippet} + + + {#snippet template({ children: _, titleRow: __, ...args })} + + + {#snippet titleRow()} + + did something very special, that will be remembered forever + + {/snippet} + Description of the thing that was done + + + {/snippet} + + + + {#snippet template({ children: _, titleRow: __, marker: ___, ...args })} + + + {#snippet marker()} + + {/snippet} + {#snippet titleRow()} + + next to a small icon marker + + {/snippet} + + + {#snippet marker()} + + {/snippet} + {#snippet titleRow()} + + next to a large icon marker + + {/snippet} + + + {/snippet} + + + + {#snippet template({ children: _, titleRow: __, ...args })} + + + {#snippet titleRow()} + + next to a small user avatar + + {/snippet} + + + {#snippet titleRow()} + + next to a large user avatar + + {/snippet} + + + {/snippet} + + + + {#snippet template()} + + +
+ Custom content goes here +
+
+
+ {/snippet} +
diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.svelte new file mode 100644 index 000000000..6d1873451 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.svelte @@ -0,0 +1,54 @@ + + + + +
  • +
    + {#if typeof marker === "function"} + {@render marker()} + {:else if marker} + + {/if} +
    + {#if children || titleRow} +
    + {#if titleRow} +
    + {@render titleRow?.()} +
    + {/if} + {#if children} +
    + {@render children()} +
    + {/if} +
    + {/if} +
  • diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.svelte.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.svelte.test.ts new file mode 100644 index 000000000..042e7c7ae --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/Event.svelte.test.ts @@ -0,0 +1,107 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { ComponentProps } from "svelte"; +import { createRawSnippet } from "svelte"; +import { describe, expect, it } from "vitest"; +import type { Locator } from "vitest/browser"; +import type { RenderResult } from "vitest-browser-svelte"; +import { render } from "vitest-browser-svelte"; +import Component from "./Event.svelte"; + +describe("Event component", () => { + const children = createRawSnippet(() => ({ + render: () => `Child Content`, + })); + + const titleRow = createRawSnippet(() => ({ + render: () => `Title Row Content`, + })); + + const baseProps = {} satisfies ComponentProps; + + describe("basics", () => { + it("renders", async () => { + const page = render(Component, { ...baseProps }); + await expect.element(componentLocator(page)).toBeInTheDocument(); + }); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", async (attribute, value) => { + const page = render(Component, { ...baseProps, [attribute]: value }); + await expect + .element(componentLocator(page)) + .toHaveAttribute(attribute, value); + }); + + it("applies classes", async () => { + const page = render(Component, { ...baseProps, class: "test-class" }); + const element = componentLocator(page); + await expect.element(element).toHaveClass("ds"); + await expect.element(element).toHaveClass("timeline-event"); + await expect.element(element).toHaveClass("test-class"); + }); + + it("applies style", async () => { + const page = render(Component, { ...baseProps, style: "color: orange;" }); + await expect + .element(componentLocator(page)) + .toHaveStyle({ color: "orange" }); + }); + }); + + describe("Renders", () => { + it("renders children", async () => { + const page = render(Component, { ...baseProps, children }); + await expect.element(page.getByText("Child Content")).toBeInTheDocument(); + }); + + it("renders title row", async () => { + const page = render(Component, { ...baseProps, titleRow }); + await expect + .element(page.getByText("Title Row Content")) + .toBeInTheDocument(); + await expect + .element(componentLocator(page)) + .toHaveClass("with-title-row"); + }); + + describe("Marker", () => { + it("empty by default", async () => { + const page = render(Component, { ...baseProps }); + const element = componentLocator(page); + await expect.element(element).toHaveClass("marker-empty"); + expect(element.element().querySelector(".marker")).toBeInTheDocument(); + }); + + it("small", async () => { + const page = render(Component, { + ...baseProps, + marker: { userName: "John Doe" }, + markerSize: "small", + }); + const element = componentLocator(page); + await expect.element(element).toHaveClass("marker-small"); + expect(element.element().querySelector(".marker")).toBeInTheDocument(); + }); + + it("large", async () => { + const page = render(Component, { + ...baseProps, + marker: { userName: "John Doe" }, + markerSize: "large", + }); + const element = componentLocator(page); + await expect.element(element).toHaveClass("marker-large"); + expect(element.element().querySelector(".marker")).toBeInTheDocument(); + }); + }); + }); +}); + +function componentLocator(page: RenderResult): Locator { + return page.getByRole("listitem"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.ssr.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.ssr.test.ts new file mode 100644 index 000000000..955afb2dc --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.ssr.test.ts @@ -0,0 +1,75 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { RenderResult } from "@canonical/svelte-ssr-test"; +import { render } from "@canonical/svelte-ssr-test"; +import type { ComponentProps } from "svelte"; +import { createRawSnippet } from "svelte"; +import { describe, expect, it } from "vitest"; +import Component from "./TitleRow.svelte"; + +describe("TitleRow SSR", () => { + const baseProps = { + children: createRawSnippet(() => ({ + render: () => "Title Row Content", + })), + date: createRawSnippet(() => ({ + render: () => "2023-03-15", + })), + } satisfies ComponentProps; + + it("doesn't throw", () => { + expect(() => { + render(Component, { props: { ...baseProps } }); + }).not.toThrow(); + }); + + it("renders", () => { + const page = render(Component, { props: { ...baseProps } }); + const element = componentLocator(page); + expect(element).toBeInstanceOf(page.window.HTMLElement); + expect(componentLocator(page).textContent).toContain("Title Row Content"); + expect(componentLocator(page).textContent).toContain("2023-03-15"); + }); + + it("renders leadingText", () => { + const page = render(Component, { + props: { ...baseProps, leadingText: "Leading Text" }, + }); + expect(componentLocator(page).textContent).toContain("Leading Text"); + }); + + describe("Basic attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", (attribute, value) => { + const page = render(Component, { + props: { ...baseProps, [attribute]: value }, + }); + const element = componentLocator(page); + expect(element.getAttribute(attribute)).toBe(value); + }); + + it("applies style", () => { + const page = render(Component, { + props: { ...baseProps, style: "color: orange;" }, + }); + const element = componentLocator(page); + expect(element.getAttribute("style")).toContain("color: orange;"); + }); + + it("applies class", () => { + const page = render(Component, { + props: { ...baseProps, class: "test-class" }, + }); + const element = componentLocator(page); + expect(element.classList.contains("ds")).toBe(true); + expect(element.classList.contains("timeline-title-row")).toBe(true); + expect(element.classList.contains("test-class")).toBe(true); + }); + }); +}); + +function componentLocator(page: RenderResult): HTMLElement { + return page.getByTestId("title-row"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.stories.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.stories.svelte new file mode 100644 index 000000000..25651b5e5 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.stories.svelte @@ -0,0 +1,38 @@ + + + + {#snippet template({ children: _, date: __, ...args })} + + {#snippet date()} + + {/snippet} + added labels: Don't merge, Maintenance, Review: QA needed + + {/snippet} + diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.svelte new file mode 100644 index 000000000..8d240294f --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.svelte @@ -0,0 +1,32 @@ + + + + +
    + + {#if leadingText} + {leadingText} + {/if} + + {@render children()} + + + {@render date()} +
    diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.svelte.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.svelte.test.ts new file mode 100644 index 000000000..deba05345 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/TitleRow.svelte.test.ts @@ -0,0 +1,67 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { ComponentProps } from "svelte"; +import { createRawSnippet } from "svelte"; +import { describe, expect, it } from "vitest"; +import type { Locator } from "vitest/browser"; +import type { RenderResult } from "vitest-browser-svelte"; +import { render } from "vitest-browser-svelte"; +import Component from "./TitleRow.svelte"; + +describe("TitleRow component", () => { + const baseProps = { + children: createRawSnippet(() => ({ + render: () => "Title Row Content", + })), + date: createRawSnippet(() => ({ + render: () => "2023-03-15", + })), + } satisfies ComponentProps; + + it("renders", async () => { + const page = render(Component, baseProps); + await expect.element(componentLocator(page)).toBeInTheDocument(); + await expect + .element(page.getByText("Title Row Content")) + .toBeInTheDocument(); + }); + + it("renders leadingText", async () => { + const page = render(Component, { + ...baseProps, + leadingText: "Leading Text", + }); + await expect.element(page.getByText("Leading Text")).toBeInTheDocument(); + }); + + describe("Basic attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", async (attribute, value) => { + const page = render(Component, { ...baseProps, [attribute]: value }); + await expect + .element(componentLocator(page)) + .toHaveAttribute(attribute, value); + }); + + it("applies style", async () => { + const page = render(Component, { ...baseProps, style: "color: orange;" }); + await expect + .element(componentLocator(page)) + .toHaveStyle("color: orange;"); + }); + + it("applies class", async () => { + const page = render(Component, { ...baseProps, class: "test-class" }); + const element = componentLocator(page); + await expect.element(element).toHaveClass("ds"); + await expect.element(element).toHaveClass("timeline-title-row"); + await expect.element(element).toHaveClass("test-class"); + }); + }); +}); + +function componentLocator(page: RenderResult): Locator { + return page.getByTestId("title-row"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/index.ts new file mode 100644 index 000000000..09b6fb064 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/index.ts @@ -0,0 +1,4 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +export { default as TitleRow } from "./TitleRow.svelte"; +export * from "./types.js"; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/styles.css b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/styles.css new file mode 100644 index 000000000..f79a762f2 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/styles.css @@ -0,0 +1,38 @@ +.ds.timeline-title-row { + --dimension-gap-timeline-event-title-row: var( + --lp-dimension-spacing-inline-xs + ); + /* TODO(@Enzo): Add a missing token */ + --dimension-basis-timeline-event-title-row-description: 10rem; + + display: flex; + align-items: baseline; + + font: var(--lp-typography-paragraph-s); + line-height: var(--typography-line-height-timeline-event-title-row); + gap: var(--dimension-gap-timeline-event-title-row); + + > .content { + display: flex; + flex-grow: 1; + align-items: baseline; + flex-wrap: wrap; + + gap: var(--dimension-gap-timeline-event-title-row); + + > .leading-text { + color: var(--lp-color-text-default); + } + + > .description { + color: var(--lp-color-text-muted); + flex-basis: var(--dimension-basis-timeline-event-title-row-description); + flex-grow: 1; + } + } + + > .date { + color: var(--lp-color-text-muted); + flex: none; + } +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/types.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/types.ts new file mode 100644 index 000000000..d0cce44da --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/TitleRow/types.ts @@ -0,0 +1,21 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import type { Snippet } from "svelte"; +import type { SvelteHTMLElements } from "svelte/elements"; + +type BaseProps = SvelteHTMLElements["div"]; + +export interface TitleRowProps extends BaseProps { + /** + * The text to display at the start of the title row. + */ + leadingText?: string; + /** + * Main content to be displayed in the title row. + */ + children: Snippet<[]>; + /** + * The date to display at the end of the title row. + */ + date: Snippet<[]>; +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/index.ts new file mode 100644 index 000000000..d5dd8a7b6 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/common/index.ts @@ -0,0 +1 @@ +export * from "./TitleRow/index.js"; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/index.ts new file mode 100644 index 000000000..d5a64d67b --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/index.ts @@ -0,0 +1,26 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import { TitleRow } from "./common/index.js"; +import { default as EventRoot } from "./Event.svelte"; + +const Event = EventRoot as typeof EventRoot & { + /** + * `Timeline.Event.TitleRow` is used to display a title row within a timeline event. + * @example + * ```svelte + * + * added labels: Don't merge, Maintenance, Review: QA needed + * {#snippet date()} + * + * {/snippet} + * + * ``` + */ + TitleRow: typeof TitleRow; +}; + +Event.TitleRow = TitleRow; + +export type { TitleRowProps } from "./common/index.js"; +export * from "./types.js"; +export { Event }; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/styles.css b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/styles.css new file mode 100644 index 000000000..d9d717a03 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/styles.css @@ -0,0 +1,115 @@ +.ds.timeline-event { + --typography-line-height-timeline-event-title-row: var( + --lp-typography-line-height-s + ); + + grid-column: 1 / -1; + display: grid; + grid-template-columns: subgrid; + grid-template-rows: 1fr; + + > * { + grid-row: 1; + } + + &.marker-small { + --marker-size: var(--lp-dimension-size-s); + } + + &.marker-large { + --marker-size: var(--lp-dimension-size-l); + } + + &.marker-empty { + --marker-size: var(--lp-dimension-size-xxxs); + } + + --marker-title-row-alignment-difference: 0px; + --marker-alignment-adjustment: 0px; + + &.with-title-row { + /* Only calculate this if necessary */ + --marker-title-row-alignment-difference: calc( + var(--typography-line-height-timeline-event-title-row) / + 2 - + var(--marker-size) / + 2 + ); + + /* If the marker is bigger that the title row, there's no need to adjust its alignment */ + --marker-alignment-adjustment: max( + var(--marker-title-row-alignment-difference), + 0px + ); + } + + &:not(:only-child) { + &::before { + content: ""; + display: block; + grid-column: marker; + grid-row: 1; + background-color: var(--color-background-timeline-line); + width: var(--dimension-width-timeline-line); + justify-self: center; + + /* Span the whole height of the event and reach over the gap to the next one */ + height: calc(100% + var(--dimension-gap-row-timeline)); + } + + &:first-child::before { + /* Start the line at the marker */ + height: calc( + 100% + + var(--dimension-gap-row-timeline) - + var(--marker-alignment-adjustment) + ); + margin-block-start: var(--marker-alignment-adjustment); + } + + &:last-child::before { + /* End the line at the marker */ + height: var(--marker-alignment-adjustment); + } + } + + > .marker { + grid-column: marker; + justify-self: center; + display: grid; + place-content: center; + align-self: start; + + width: var(--marker-size); + height: var(--marker-size); + border: var(--lp-dimension-stroke-thickness-default) solid + var(--lp-color-border-default); + background-color: var(--lp-color-background-alt); + + margin-block-start: var(--marker-alignment-adjustment); + + .ds.user-avatar { + width: 100%; + height: 100%; + border: none; + background-color: transparent; + } + } + + > .content { + grid-column: content; + align-self: start; + + > .title-row { + /* If the title row is bigger than the marker, no adjustment is needed */ + margin-block-start: max( + calc(var(--marker-title-row-alignment-difference) * -1), + 0px + ); + + &:not(:only-child) { + margin-block-end: var(--lp-dimension-spacing-block-xs); + } + } + } +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/types.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/types.ts new file mode 100644 index 000000000..bfd980199 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/Event/types.ts @@ -0,0 +1,28 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import type { Snippet } from "svelte"; +import type { SvelteHTMLElements } from "svelte/elements"; +import type { UserOptions } from "../../../UserAvatar/index.js"; + +type BaseProps = SvelteHTMLElements["li"]; + +export interface EventProps extends BaseProps { + /** + * The marker to be displayed over the timeline's line. Can be a user avatar or an icon. If not specified, an "empty" marker will be used. + */ + marker?: UserOptions | Snippet; + /** + * The size of the marker. Has no effect if `marker` is not specified. + * + * @default "small" + */ + markerSize?: "small" | "large"; + /** + * Content to be displayed in the event's title row. Consider using ``. If you wish to provide other content, and want the marker to be aligned with the first line of text, override `--typography-line-height-timeline-event-title-row` with the line height of your content. + */ + titleRow?: Snippet; + /** + * Content to be displayed in the event's body. + */ + children?: Snippet; +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.ssr.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.ssr.test.ts new file mode 100644 index 000000000..56829e5de --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.ssr.test.ts @@ -0,0 +1,98 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { RenderResult } from "@canonical/svelte-ssr-test"; +import { render } from "@canonical/svelte-ssr-test"; +import type { ComponentProps } from "svelte"; +import { describe, expect, it } from "vitest"; +import Component from "./HiddenEvents.svelte"; +import { multipleLinks, oneLink } from "./test.fixtures.svelte"; + +describe("HiddenEvents SSR", () => { + const baseProps = { + numHidden: 888, + } satisfies ComponentProps; + + describe("basics", () => { + it("doesn't throw", () => { + expect(() => { + render(Component, { props: { ...baseProps } }); + }).not.toThrow(); + }); + + it("renders", () => { + const page = render(Component, { props: { ...baseProps } }); + expect(componentLocator(page)).toBeInstanceOf(page.window.HTMLLIElement); + expect(componentLocator(page).textContent).toContain("888"); + }); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", (attribute, value) => { + const page = render(Component, { + props: { ...baseProps, numHidden: 0, [attribute]: value }, + }); + expect(componentLocator(page).getAttribute(attribute)).toBe(value); + }); + + it("applies classes", () => { + const page = render(Component, { + props: { + ...baseProps, + numHidden: 0, + class: "test-class", + }, + }); + const element = componentLocator(page); + expect(element.classList).toContain("ds"); + expect(element.classList).toContain("timeline-hidden-events"); + expect(element.classList).toContain("test-class"); + }); + + it("applies style", () => { + const page = render(Component, { + props: { + ...baseProps, + numHidden: 0, + style: "color: orange;", + }, + }); + expect(componentLocator(page).style.color).toBe("orange"); + }); + }); + + describe("Links", () => { + it("renders child links", () => { + const page = render(Component, { + props: { + ...baseProps, + children: oneLink, + }, + }); + const link = page.getByRole("link"); + expect(link.textContent).toContain("Show more"); + expect(link.getAttribute("href")).toBe("/show-more"); + }); + + it("renders multiple child links", () => { + const page = render(Component, { + props: { + ...baseProps, + children: multipleLinks, + }, + }); + const links = page.getAllByRole("link"); + expect(links).toHaveLength(2); + expect(links[0].textContent).toContain("Show more"); + expect(links[0].getAttribute("href")).toBe("/show-more"); + expect(links[1].textContent).toContain("Show all"); + expect(links[1].getAttribute("href")).toBe("/show-all"); + }); + }); +}); + +function componentLocator(page: RenderResult): HTMLLIElement { + return page.getByRole("listitem"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.stories.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.stories.svelte new file mode 100644 index 000000000..9b87b4bd4 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.stories.svelte @@ -0,0 +1,88 @@ + + +{#snippet dummyChild()} +
    +{/snippet} + + + {#snippet template(args)} + + + Show all + + + {/snippet} + + + + {#snippet template(args)} + + + Show more + Show all + + + {/snippet} + + + + {#snippet template(args)} + + + Show all + + {@render dummyChild()} + + {/snippet} + + + + {#snippet template(args)} + + {@render dummyChild()} + + Show all + + + {/snippet} + + + + {#snippet template(args)} + + {@render dummyChild()} + + Show more + Show all + + {@render dummyChild()} + + {/snippet} + diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.svelte new file mode 100644 index 000000000..99bfb3253 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.svelte @@ -0,0 +1,24 @@ + + + + +
  • +
    + + {numHidden} hidden + + {@render children?.()} +
    +
  • diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.svelte.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.svelte.test.ts new file mode 100644 index 000000000..18eda4d2c --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/HiddenEvents.svelte.test.ts @@ -0,0 +1,90 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { ComponentProps } from "svelte"; +import { describe, expect, it } from "vitest"; +import type { Locator } from "vitest/browser"; +import type { RenderResult } from "vitest-browser-svelte"; +import { render } from "vitest-browser-svelte"; +import Component from "./HiddenEvents.svelte"; +import { multipleLinks, oneLink } from "./test.fixtures.svelte"; + +describe("HiddenEvents component", () => { + const baseProps = { + numHidden: 0, + } satisfies ComponentProps; + + describe("basics", () => { + it("renders", async () => { + const page = render(Component, { ...baseProps }); + await expect.element(componentLocator(page)).toBeInTheDocument(); + }); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", async (attribute, value) => { + const page = render(Component, { ...baseProps, [attribute]: value }); + await expect + .element(componentLocator(page)) + .toHaveAttribute(attribute, value); + }); + + it("applies classes", async () => { + const page = render(Component, { ...baseProps, class: "test-class" }); + const element = componentLocator(page); + await expect.element(element).toHaveClass("ds"); + await expect.element(element).toHaveClass("timeline-hidden-events"); + await expect.element(element).toHaveClass("test-class"); + }); + + it("applies style", async () => { + const page = render(Component, { ...baseProps, style: "color: orange;" }); + await expect + .element(componentLocator(page)) + .toHaveStyle({ color: "orange" }); + }); + }); + + describe("Renders", () => { + it("hidden events number", async () => { + const page = render(Component, { ...baseProps, numHidden: 888 }); + await expect.element(componentLocator(page)).toHaveTextContent("888"); + }); + + describe("Links", () => { + it("without the links by default", async () => { + const page = render(Component, { ...baseProps }); + await expect.element(page.getByRole("link")).not.toBeInTheDocument(); + }); + + it("renders child links", async () => { + const page = render(Component, { + ...baseProps, + children: oneLink, + }); + await expect + .element(page.getByRole("link", { name: "Show more" })) + .toHaveAttribute("href", "/show-more"); + }); + + it("renders multiple child links", async () => { + const page = render(Component, { + ...baseProps, + children: multipleLinks, + }); + await expect + .element(page.getByRole("link", { name: "Show more" })) + .toHaveAttribute("href", "/show-more"); + await expect + .element(page.getByRole("link", { name: "Show all" })) + .toHaveAttribute("href", "/show-all"); + }); + }); + }); +}); + +function componentLocator(page: RenderResult): Locator { + return page.getByRole("listitem"); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.ssr.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.ssr.test.ts new file mode 100644 index 000000000..8fb5b2b14 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.ssr.test.ts @@ -0,0 +1,60 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import { render } from "@canonical/svelte-ssr-test"; +import type { ComponentProps } from "svelte"; +import { createRawSnippet } from "svelte"; +import { describe, expect, it } from "vitest"; +import Component from "./Link.svelte"; + +describe("HiddenEvents.Link SSR", () => { + const baseProps = { + href: "/show-all", + children: createRawSnippet(() => ({ + render: () => "Show all", + })), + } satisfies ComponentProps; + + it("doesn't throw", () => { + expect(() => { + render(Component, { props: { ...baseProps } }); + }).not.toThrow(); + }); + + it("renders", () => { + const page = render(Component, { props: { ...baseProps } }); + const link = page.getByRole("link"); + expect(link.textContent).toContain("Show all"); + expect(link.getAttribute("href")).toBe("/show-all"); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", (attribute, value) => { + const page = render(Component, { + props: { ...baseProps, [attribute]: value }, + }); + expect(page.getByRole("link").getAttribute(attribute)).toBe(value); + }); + + it("applies class", () => { + const page = render(Component, { + props: { ...baseProps, class: "test-class" }, + }); + const link = page.getByRole("link"); + expect(link.classList.contains("ds")).toBe(true); + expect(link.classList.contains("timeline-hidden-events-link")).toBe(true); + expect(link.classList.contains("test-class")).toBe(true); + }); + + it("applies style", () => { + const page = render(Component, { + props: { ...baseProps, style: "color: orange;" }, + }); + expect(page.getByRole("link").getAttribute("style")).toContain( + "color: orange;", + ); + }); + }); +}); diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.stories.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.stories.svelte new file mode 100644 index 000000000..af60940ad --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.stories.svelte @@ -0,0 +1,22 @@ + + + + {#snippet template(args)} + Show all + {/snippet} + diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.svelte new file mode 100644 index 000000000..6f5ee25a6 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.svelte.test.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.svelte.test.ts new file mode 100644 index 000000000..1f7c09464 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/Link.svelte.test.ts @@ -0,0 +1,59 @@ +/* @canonical/generator-ds 0.10.0-experimental.5 */ + +import type { ComponentProps } from "svelte"; +import { createRawSnippet } from "svelte"; +import { describe, expect, it } from "vitest"; +import { render } from "vitest-browser-svelte"; +import Component from "./Link.svelte"; + +describe("HiddenEvents.Link component", () => { + const baseProps = { + href: "/show-all", + children: createRawSnippet(() => ({ + render: () => "Show all", + })), + } satisfies ComponentProps; + + it("renders", async () => { + const page = render(Component, baseProps); + await expect.element(page.getByRole("link")).toBeInTheDocument(); + await expect.element(page.getByText("Show all")).toBeInTheDocument(); + }); + + describe("attributes", () => { + it.each([ + ["id", "test-id"], + ["aria-label", "test-aria-label"], + ])("applies %s", async (attribute, value) => { + const page = render(Component, { ...baseProps, [attribute]: value }); + await expect + .element(page.getByRole("link")) + .toHaveAttribute(attribute, value); + }); + + it("applies href", async () => { + const page = render(Component, { ...baseProps, href: "/show-more" }); + await expect + .element(page.getByRole("link")) + .toHaveAttribute("href", "/show-more"); + }); + + it("applies class", async () => { + const page = render(Component, { ...baseProps, class: "test-class" }); + const element = page.getByRole("link"); + await expect.element(element).toHaveClass("ds"); + await expect.element(element).toHaveClass("timeline-hidden-events-link"); + await expect.element(element).toHaveClass("test-class"); + }); + + it("applies style", async () => { + const page = render(Component, { + ...baseProps, + style: "color: orange;", + }); + await expect + .element(page.getByRole("link")) + .toHaveStyle("color: orange;"); + }); + }); +}); diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/index.ts new file mode 100644 index 000000000..4b538cecd --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/index.ts @@ -0,0 +1,4 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +export { default as Link } from "./Link.svelte"; +export * from "./types.js"; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/styles.css b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/styles.css new file mode 100644 index 000000000..e2d73537e --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/styles.css @@ -0,0 +1,9 @@ +.ds.timeline-hidden-events-link { + font: var(--lp-typography-paragraph-xs); +} + +.ds.timeline-hidden-events-link-separator { + font: var(--lp-typography-paragraph-xs); + color: var(--lp-color-text-muted); + margin-inline: var(--lp-dimension-spacing-inline-xs); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/types.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/types.ts new file mode 100644 index 000000000..0993ef4fd --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/Link/types.ts @@ -0,0 +1,5 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import type { LinkProps as BaseLinkProps } from "../../../../../Link/types.js"; + +export type LinkProps = Omit; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/index.ts new file mode 100644 index 000000000..c8ee96751 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/common/index.ts @@ -0,0 +1 @@ +export * from "./Link/index.js"; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/index.ts new file mode 100644 index 000000000..8452185ec --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/index.ts @@ -0,0 +1,23 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import { Link } from "./common/index.js"; +import { default as HiddenEventsRoot } from "./HiddenEvents.svelte"; + +const HiddenEvents = HiddenEventsRoot as typeof HiddenEventsRoot & { + /** + * `Timeline.HiddenEvents.Link` wraps the `Link` for use inside `Timeline.HiddenEvents`. + * @example + * ```svelte + * + * Show all + * + * ``` + */ + Link: typeof Link; +}; + +HiddenEvents.Link = Link; + +export type { LinkProps as HiddenEventsLinkProps } from "./common/index.js"; +export * from "./types.js"; +export { HiddenEvents }; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/styles.css b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/styles.css new file mode 100644 index 000000000..6d6c7a25f --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/styles.css @@ -0,0 +1,42 @@ +.ds.timeline-hidden-events { + position: relative; + grid-column: 1 / -1; + display: grid; + grid-template-columns: subgrid; + font: var(--lp-typography-paragraph-xs-strong); + + &:not(:last-child)::after { + /* Line spanning to the next element */ + content: ""; + position: absolute; + display: block; + grid-column: marker; + background-color: var(--color-background-timeline-line); + width: var(--dimension-width-timeline-line); + justify-self: center; + height: var(--dimension-gap-row-timeline); + top: 100%; + } + + > div { + grid-column: 1 / -1; + justify-self: start; + display: flex; + padding-block: var(--lp-dimension-spacing-block-xs); + padding-inline: var(--lp-dimension-spacing-inline-m); + border-block: var(--lp-dimension-stroke-thickness-default) solid + var(--lp-color-border-default); + } + + &:first-child { + > div { + border-block-start: none; + } + } + + &:last-child { + > div { + border-block-end: none; + } + } +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/test.fixtures.svelte b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/test.fixtures.svelte new file mode 100644 index 000000000..ad9668bdc --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/test.fixtures.svelte @@ -0,0 +1,15 @@ + + +{#snippet oneLink()} + Show more +{/snippet} + +{#snippet multipleLinks()} + Show more + Show all +{/snippet} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/types.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/types.ts new file mode 100644 index 000000000..3b6f58e33 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/HiddenEvents/types.ts @@ -0,0 +1,12 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import type { SvelteHTMLElements } from "svelte/elements"; + +type BaseProps = SvelteHTMLElements["li"]; + +export interface HiddenEventsProps extends BaseProps { + /** + * The number of hidden events. + */ + numHidden: number; +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/index.ts new file mode 100644 index 000000000..faf34cd4d --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/common/index.ts @@ -0,0 +1,2 @@ +export * from "./Event/index.js"; +export * from "./HiddenEvents/index.js"; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/index.ts new file mode 100644 index 000000000..cb00e5651 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/index.ts @@ -0,0 +1,51 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import { Event, HiddenEvents } from "./common/index.js"; +import { default as TimelineRoot } from "./Timeline.svelte"; + +const Timeline = TimelineRoot as typeof TimelineRoot & { + /** + * `Timeline.Event` is a component that represents a single event on the timeline. It displays a marker that can optionally show an icon or an avatar (both can either be `small` or `large`). + * + * The marker and the first line of the optional `titleRow` are meant to be aligned vertically. This is automatically handled if `` is used. If you wish to provide other content and want this behavior to persist, override `--typography-line-height-timeline-event-title-row` CSS variable with the line height of your content. + * @example + * ```svelte + * + * {#snippet titleRow()} + * + * did something + * {#snippet date()} + * + * {/snippet} + * + * {/snippet} + * and here is some additional content. + * + * ``` + */ + Event: typeof Event; + /** + * `Timeline.HiddenEvents` component provides a way to inform the user, that not all events are visible in the timeline. It displays a message indicating the number of events hidden from the view and optionally allows the user to display more or all the hidden events. + * @example + * ```svelte + * + * Show all + * + * ``` + */ + HiddenEvents: typeof HiddenEvents; +}; + +Timeline.Event = Event; +Timeline.HiddenEvents = HiddenEvents; + +export type { + EventProps as TimelineEventProps, + HiddenEventsLinkProps as TimelineHiddenEventsLinkProps, + HiddenEventsProps as TimelineHiddenEventsProps, + TitleRowProps as TimelineTitleRowProps, +} from "./common/index.js"; +export * from "./types.js"; +export { Timeline }; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/styles.css b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/styles.css new file mode 100644 index 000000000..4e6240f49 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/styles.css @@ -0,0 +1,12 @@ +.ds.timeline { + --dimension-gap-row-timeline: calc(var(--lp-dimension-spacing-block-s) * 2); + --color-background-timeline-line: var(--lp-color-border-default); + --dimension-width-timeline-line: var(--lp-dimension-stroke-thickness-default); + + display: grid; + grid-template-columns: [marker-start] auto [marker-end content-start] 1fr [content-end]; + list-style: none; + + row-gap: var(--dimension-gap-row-timeline); + column-gap: var(--lp-dimension-spacing-inline-m); +} diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/types.ts b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/types.ts new file mode 100644 index 000000000..a04f40ce7 --- /dev/null +++ b/packages/svelte/ds-app-launchpad/src/lib/components/Timeline/types.ts @@ -0,0 +1,5 @@ +/* @canonical/generator-ds 0.10.0-experimental.2 */ + +import type { SvelteHTMLElements } from "svelte/elements"; + +export type TimelineProps = SvelteHTMLElements["ol"]; diff --git a/packages/svelte/ds-app-launchpad/src/lib/components/index.ts b/packages/svelte/ds-app-launchpad/src/lib/components/index.ts index 67cbde94b..cdaf14e35 100644 --- a/packages/svelte/ds-app-launchpad/src/lib/components/index.ts +++ b/packages/svelte/ds-app-launchpad/src/lib/components/index.ts @@ -14,4 +14,5 @@ export * from "./Spinner/index.js"; export * from "./Switch/index.js"; export * from "./Textarea/index.js"; export * from "./TextInput/index.js"; +export * from "./Timeline/index.js"; export * from "./UserAvatar/index.js";