diff --git a/packages/fiori/cypress/specs/Timeline.cy.tsx b/packages/fiori/cypress/specs/Timeline.cy.tsx index 96b57949b5cd..feb3cc5758e2 100644 --- a/packages/fiori/cypress/specs/Timeline.cy.tsx +++ b/packages/fiori/cypress/specs/Timeline.cy.tsx @@ -1,12 +1,14 @@ import Timeline from "../../src/Timeline.js"; +import type { TimelineSearchEventDetail, TimelineSortEventDetail } from "../../src/Timeline.js"; import TimelineItem from "../../src/TimelineItem.js"; import TimelineGroupItem from "../../src/TimelineGroupItem.js"; +import TimelineHeaderBar from "../../src/TimelineHeaderBar.js"; +import TimelineFilterOption from "../../src/TimelineFilterOption.js"; import accept from "@ui5/webcomponents-icons/dist/accept.js"; import calendar from "@ui5/webcomponents-icons/dist/calendar.js"; import messageInformation from "@ui5/webcomponents-icons/dist/message-information.js"; import Label from "@ui5/webcomponents/dist/Label.js"; import Avatar from "@ui5/webcomponents/dist/Avatar.js"; -import UI5Element from "@ui5/webcomponents-base"; import Button from "@ui5/webcomponents/dist/Button.js"; import Input from "@ui5/webcomponents/dist/Input.js"; @@ -478,11 +480,12 @@ describe("Timeline - getFocusDomRef", () => { ); - cy.get("[ui5-timeline], #firstItem") - .then(($el) => { - const timeline = $el[0], - firstItem = $el[1]; - expect(timeline.getFocusDomRef()).to.equal(firstItem.getFocusDomRef()); + cy.get("[ui5-timeline]") + .then(($timeline) => { + cy.get("#firstItem") + .then(($firstItem) => { + expect($timeline[0].getFocusDomRef()).to.equal($firstItem[0].getFocusDomRef()); + }); }); }); @@ -495,21 +498,313 @@ describe("Timeline - getFocusDomRef", () => { ); - cy.get("[ui5-timeline]") - .as("timeline"); - cy.get("[ui5-timeline]") .find("#lastItem") .realClick(); - cy.get("[ui5-timeline], #lastItem") - .then(($el) => { - const timeline = $el[0], - lastItem = $el[1]; - expect(timeline.getFocusDomRef()).to.equal(lastItem.getFocusDomRef()); + cy.get("[ui5-timeline]") + .then(($timeline) => { + cy.get("#lastItem") + .then(($lastItem) => { + expect($timeline[0].getFocusDomRef()).to.equal($lastItem[0].getFocusDomRef()); + }); }); }); }); +describe("Timeline Header Bar", () => { + describe("Search functionality", () => { + it("should show header bar when slotted", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-timeline]") + .shadow() + .find(".ui5-timeline-header-bar-wrapper") + .should("exist"); + }); + + it("should fire search event when user types in search input", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-timeline]").then($timeline => { + $timeline.get(0).addEventListener("search", cy.stub().as("searchEvent")); + }); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-input]") + .realClick(); + + cy.realType("Meeting"); + + cy.get("@searchEvent").should("have.been.called"); + }); + + it("should include search value in event detail", () => { + cy.mount( + + + + + ); + + let searchValue = ""; + cy.get("[ui5-timeline]").then($timeline => { + $timeline.get(0).addEventListener("search", (e: CustomEvent) => { + searchValue = e.detail.value; + }); + }); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-input]") + .realClick(); + + cy.realType("Test"); + + cy.wrap(null).then(() => { + expect(searchValue).to.equal("Test"); + }); + }); + }); + + describe("Filter functionality", () => { + it("should show filter dropdown when showFilter is true", () => { + cy.mount( + + + + + + + + ); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-select]") + .should("exist"); + }); + + it("should fire filter event when filter selection changes", () => { + cy.mount( + + + + + + + + ); + + cy.get("[ui5-timeline]").then($timeline => { + $timeline.get(0).addEventListener("filter", cy.stub().as("filterEvent")); + }); + + // Click on the select to open it + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-select]") + .realClick(); + + // Use keyboard to select next option + cy.realPress("ArrowDown"); + cy.realPress("Enter"); + + cy.get("@filterEvent").should("have.been.called"); + }); + }); + + describe("Sort functionality", () => { + it("should show sort button when showSort is true", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-button]") + .should("exist"); + }); + + it("should fire sort event when user clicks sort button", () => { + cy.mount( + + + + + ); + cy.get("[ui5-timeline]").then($timeline => { + $timeline.get(0).addEventListener("sort", cy.stub().as("sortEvent")); + }); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-button]") + .realClick(); + + cy.get("@sortEvent").should("have.been.called"); + }); + + it("should toggle sort order on consecutive clicks", () => { + cy.mount( + + + + + ); + + const sortOrders: string[] = []; + cy.get("[ui5-timeline]").then($timeline => { + $timeline.get(0).addEventListener("sort", (e: CustomEvent) => { + sortOrders.push(e.detail.sortOrder); + }); + }); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-button]") + .as("sortButton"); + + cy.get("@sortButton").realClick(); + cy.get("@sortButton").realClick(); + cy.get("@sortButton").realClick(); + + cy.wrap(null).then(() => { + expect(sortOrders).to.deep.equal(["Ascending", "Descending", "Ascending"]); + }); + }); + }); + + describe("Accessibility", () => { + it("should have correct ARIA role on header bar", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find(".ui5-timeline-header-bar") + .should("have.attr", "role", "toolbar"); + }); + it("should have accessible name on search input", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-input]") + .should("have.attr", "accessible-name"); + }); + }); + + describe("Combined features", () => { + it("should support all features together", () => { + cy.mount( + + + + + + + + + + ); + + // Search input should exist + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-input]") + .should("exist"); + + // Filter select should exist + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-select]") + .should("exist"); + + // Sort button should exist + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-button]") + .should("exist"); + }); + }); + + describe("Application-side filtering", () => { + it("application can remove items from DOM based on search event", () => { + cy.mount( + + + + + + ); + + // Application handles filtering by removing non-matching items from DOM + cy.get("[ui5-timeline]").then($timeline => { + const timeline = $timeline.get(0) as Timeline; + const allItems = Array.from(timeline.querySelectorAll("[ui5-timeline-item]")) as TimelineItem[]; + + timeline.addEventListener("search", (e: CustomEvent) => { + const searchValue = e.detail.value.toLowerCase(); + + if (searchValue === "") { + // Restore all items when search is cleared + allItems.forEach(item => { + if (!item.parentElement) { + timeline.appendChild(item); + } + }); + } else { + // Remove non-matching items from DOM + allItems.forEach(item => { + const titleText = item.titleText?.toLowerCase() || ""; + if (!titleText.includes(searchValue)) { + item.remove(); + } else if (!item.parentElement) { + timeline.appendChild(item); + } + }); + } + }); + }); + + // Type in search + cy.get("[ui5-timeline-header-bar]") + .shadow() + .find("[ui5-input]") + .realClick(); + + cy.realType("Meeting"); + + // Application filtered - only matching item remains in DOM + cy.get("[ui5-timeline-item]").should("have.length", 1); + cy.get("[ui5-timeline-item]").eq(0).should("have.attr", "title-text", "Meeting with John"); + }); + }); +}); \ No newline at end of file diff --git a/packages/fiori/src/Timeline.ts b/packages/fiori/src/Timeline.ts index 77aed3ff5e68..3ae6e1f87f8e 100644 --- a/packages/fiori/src/Timeline.ts +++ b/packages/fiori/src/Timeline.ts @@ -1,5 +1,5 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; -import type { DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import type { DefaultSlot, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; @@ -21,7 +21,10 @@ import type ToggleButton from "@ui5/webcomponents/dist/ToggleButton.js"; import "./TimelineItem.js"; import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; import NavigationMode from "@ui5/webcomponents-base/dist/types/NavigationMode.js"; -import { TIMELINE_ARIA_LABEL, TIMELINE_LOAD_MORE_BUTTON_TEXT } from "./generated/i18n/i18n-defaults.js"; +import { + TIMELINE_ARIA_LABEL, + TIMELINE_LOAD_MORE_BUTTON_TEXT, +} from "./generated/i18n/i18n-defaults.js"; import TimelineTemplate from "./TimelineTemplate.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import debounce from "@ui5/webcomponents-base/dist/util/debounce.js"; @@ -35,6 +38,8 @@ import TimelineLayout from "./types/TimelineLayout.js"; import TimelineGrowingMode from "./types/TimelineGrowingMode.js"; import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js"; import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; +import type TimelineHeaderBar from "./TimelineHeaderBar.js"; +import type { TimelineHeaderBarSearchEventDetail, TimelineHeaderBarFilterEventDetail, TimelineHeaderBarSortEventDetail } from "./TimelineHeaderBar.js"; /** * Interface for components that may be slotted inside `ui5-timeline` as items @@ -54,8 +59,17 @@ interface ITimelineItem extends UI5Element, ITabbable { isNextItemGroup?: boolean; firstItemInTimeline?: boolean; effectiveRole?: string; + titleText?: string; + name?: string; + subtitleText?: string; } +type TimelineSearchEventDetail = TimelineHeaderBarSearchEventDetail; + +type TimelineFilterEventDetail = TimelineHeaderBarFilterEventDetail; + +type TimelineSortEventDetail = TimelineHeaderBarSortEventDetail; + const SHORT_LINE_WIDTH = "ShortLineWidth"; const LARGE_LINE_WIDTH = "LargeLineWidth"; const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms @@ -70,6 +84,15 @@ const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms * These entries can be generated by the system (for example, value XY changed from A to B), or added manually. * There are two distinct variants of the timeline: basic and social. The basic timeline is read-only, * while the social timeline offers a high level of interaction and collaboration, and is integrated within SAP Jam. + * + * ### Header Bar + * + * The Timeline supports a `header-bar` slot for search, filter, and sort functionality. + * Use the `ui5-timeline-header-bar` component in this slot. + * The Timeline fires `search`, `filter`, and `sort` events that the application should handle + * by adding, removing, or reordering items in the DOM. The Timeline itself does not perform + * filtering or sorting — it renders whatever items are provided in the default slot. + * * @constructor * @extends UI5Element * @public @@ -94,9 +117,55 @@ const GROWING_WITH_SCROLL_DEBOUNCE_RATE = 250; // ms bubbles: true, }) +/** + * Fired when the user performs a search in the header bar. + * + * **Note:** The Timeline does not perform filtering. The application should handle + * this event and add/remove items from the DOM to reflect the search results. + * + * @param {string} value The search value entered by the user. + * @public + * @since 2.20.0 + */ +@event("search", { + bubbles: true, +}) + +/** + * Fired when the user changes filter selection in the header bar. + * + * **Note:** The Timeline does not perform filtering. The application should handle + * this event and add/remove items from the DOM to reflect the filter selection. + * + * @param {string} filterBy The filter category. + * @param {string[]} selectedOptions The selected filter option texts. + * @public + * @since 2.20.0 + */ +@event("filter", { + bubbles: true, +}) + +/** + * Fired when the user changes sort order in the header bar. + * + * **Note:** The Timeline does not perform sorting. The application should handle + * this event and reorder the items in the DOM accordingly. + * + * @param {string} sortOrder The sort order ("Ascending" or "Descending"). + * @public + * @since 2.20.0 + */ +@event("sort", { + bubbles: true, +}) + class Timeline extends UI5Element { eventDetails!: { "load-more": void, + "search": TimelineSearchEventDetail, + "filter": TimelineFilterEventDetail, + "sort": TimelineSortEventDetail, } /** * Defines the items orientation. @@ -167,6 +236,19 @@ class Timeline extends UI5Element { @slot({ type: HTMLElement, individualSlots: true, "default": true }) items!: DefaultSlot; + /** + * Defines the header bar of the timeline. + * Use `ui5-timeline-header-bar` for filtering, sorting, and search functionality. + * + * **Note:** The Timeline fires `search`, `filter`, and `sort` events when the user interacts + * with the header bar. The application should handle these events to filter/sort the items. + * + * @public + * @since 2.20.0 + */ + @slot() + headerBar!: Slot; + @query(".ui5-timeline-end-marker") timelineEndMarker!: HTMLElement; @@ -215,6 +297,14 @@ class Timeline extends UI5Element { return this.growing === TimelineGrowingMode.Button; } + get _hasHeaderBar(): boolean { + return this.headerBar.length > 0; + } + + onExitDOM() { + this.unobserveTimelineEnd(); + } + onAfterRendering() { if (this.growsOnScroll) { this.observeTimelineEnd(); @@ -225,10 +315,6 @@ class Timeline extends UI5Element { this.growingIntersectionObserver = this.getIntersectionObserver(); } - onExitDOM() { - this.unobserveTimelineEnd(); - } - async observeTimelineEnd() { if (!this.timeLineEndObserved) { await renderFinished(); @@ -473,4 +559,7 @@ Timeline.define(); export default Timeline; export type { ITimelineItem, + TimelineSearchEventDetail, + TimelineFilterEventDetail, + TimelineSortEventDetail, }; diff --git a/packages/fiori/src/TimelineFilterOption.ts b/packages/fiori/src/TimelineFilterOption.ts new file mode 100644 index 000000000000..f5b68f9a02c1 --- /dev/null +++ b/packages/fiori/src/TimelineFilterOption.ts @@ -0,0 +1,47 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; + +/** + * @class + * + * ### Overview + * + * The `ui5-timeline-filter-option` component defines individual filter values within a `ui5-timeline-header-bar`. + * It represents a single selectable option that users can choose to filter timeline items. + * + * ### Usage + * + * The `ui5-timeline-filter-option` is used as a child component within `ui5-timeline-header-bar`. + * Each option represents a specific value that can be used for filtering. + * + * ### ES6 Module Import + * + * `import "@ui5/webcomponents-fiori/dist/TimelineFilterOption.js";` + * @constructor + * @extends UI5Element + * @since 2.20.0 + * @public + */ +@customElement("ui5-timeline-filter-option") +class TimelineFilterOption extends UI5Element { + /** + * Defines the text of the filter option. + * @default "" + * @public + */ + @property() + text = ""; + + /** + * Defines if the filter option is selected. + * @default false + * @public + */ + @property({ type: Boolean }) + selected = false; +} + +TimelineFilterOption.define(); + +export default TimelineFilterOption; diff --git a/packages/fiori/src/TimelineHeaderBar.ts b/packages/fiori/src/TimelineHeaderBar.ts new file mode 100644 index 000000000000..eb7881c88c64 --- /dev/null +++ b/packages/fiori/src/TimelineHeaderBar.ts @@ -0,0 +1,266 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import property from "@ui5/webcomponents-base/dist/decorators/property.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; +import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; +import type Input from "@ui5/webcomponents/dist/Input.js"; +import type Select from "@ui5/webcomponents/dist/Select.js"; +import type TimelineSortOrder from "./types/TimelineSortOrder.js"; +import type TimelineFilterOption from "./TimelineFilterOption.js"; + +// Import icons to register them +import "@ui5/webcomponents-icons/dist/sort.js"; +import "@ui5/webcomponents-icons/dist/sort-ascending.js"; +import "@ui5/webcomponents-icons/dist/sort-descending.js"; + +import TimelineHeaderBarTemplate from "./TimelineHeaderBarTemplate.js"; +import TimelineHeaderBarCss from "./generated/themes/TimelineHeaderBar.css.js"; + +import { + TIMELINE_HEADER_BAR_ACCESSIBLE_NAME, + TIMELINE_SEARCH_PLACEHOLDER, + TIMELINE_SEARCH_ACCESSIBLE_NAME, + TIMELINE_FILTER_ACCESSIBLE_NAME, + TIMELINE_SORT_ASCENDING_TOOLTIP, + TIMELINE_SORT_DESCENDING_TOOLTIP, + TIMELINE_SORT_ACCESSIBLE_NAME, +} from "./generated/i18n/i18n-defaults.js"; + +type TimelineHeaderBarSearchEventDetail = { + value: string; +}; + +type TimelineHeaderBarFilterEventDetail = { + filterBy: string; + selectedOptions: string[]; +}; + +type TimelineHeaderBarSortEventDetail = { + sortOrder: string; +}; + +/** + * @class + * + * ### Overview + * + * The `ui5-timeline-header-bar` component provides search, filter, and sort functionality + * for the `ui5-timeline` component. It is designed to be slotted into the `header-bar` slot + * of the Timeline. + * + * ### Usage + * + * The component fires events (`search`, `filter`, `sort`) that the application should handle + * to filter/sort the timeline items. The Timeline component itself does not perform any + * filtering or sorting - this is the responsibility of the application. + * + * ### ES6 Module Import + * + * `import "@ui5/webcomponents-fiori/dist/TimelineHeaderBar.js";` + * + * @constructor + * @extends UI5Element + * @public + * @since 2.20.0 + */ +@customElement({ + tag: "ui5-timeline-header-bar", + languageAware: true, + renderer: jsxRenderer, + template: TimelineHeaderBarTemplate, + styles: TimelineHeaderBarCss, +}) + +/** + * Fired when the user performs a search. + * + * @param {string} value The search value entered by the user. + * @public + */ +@event("search", { + bubbles: true, +}) + +/** + * Fired when the user changes filter selection. + * + * @param {string} filterBy The filter category. + * @param {string[]} selectedOptions The selected filter option texts. + * @public + */ +@event("filter", { + bubbles: true, +}) + +/** + * Fired when the user changes sort order. + * + * @param {string} sortOrder The sort order ("Ascending" or "Descending"). + * @public + */ +@event("sort", { + bubbles: true, +}) + +class TimelineHeaderBar extends UI5Element { + eventDetails!: { + "search": TimelineHeaderBarSearchEventDetail, + "filter": TimelineHeaderBarFilterEventDetail, + "sort": TimelineHeaderBarSortEventDetail, + }; + + /** + * Shows the search input field. + * @default false + * @public + */ + @property({ type: Boolean }) + showSearch = false; + + /** + * Shows the filter dropdown. + * @default false + * @public + */ + @property({ type: Boolean }) + showFilter = false; + + /** + * Shows the sort button. + * @default false + * @public + */ + @property({ type: Boolean }) + showSort = false; + + /** + * Shows the filter by date option. + * @default false + * @public + */ + @property({ type: Boolean }) + showFilterByDate = false; + + /** + * The current filter category label. + * @default "" + * @public + */ + @property() + filterBy = ""; + + /** + * The current search value. + * @default "" + * @public + */ + @property() + searchValue = ""; + + /** + * The current sort order. + * @default "None" + * @public + */ + @property() + sortOrder: `${TimelineSortOrder}` = "None"; + + /** + * Filter options to display in the filter dropdown. + * @public + */ + @slot({ type: HTMLElement, "default": true, invalidateOnChildChange: true }) + filterOptions!: Slot; + + @i18n("@ui5/webcomponents-fiori") + static i18nBundle: I18nBundle; + + get _headerBarAccessibleName() { + return TimelineHeaderBar.i18nBundle.getText(TIMELINE_HEADER_BAR_ACCESSIBLE_NAME); + } + + get _searchPlaceholder() { + return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SEARCH_PLACEHOLDER); + } + + get _searchAccessibleName() { + return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SEARCH_ACCESSIBLE_NAME); + } + + get _filterAccessibleName() { + return TimelineHeaderBar.i18nBundle.getText(TIMELINE_FILTER_ACCESSIBLE_NAME); + } + + get _sortAccessibleName() { + return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SORT_ACCESSIBLE_NAME); + } + + get _sortTooltip() { + if (this.sortOrder === "Ascending") { + return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SORT_DESCENDING_TOOLTIP); + } + return TimelineHeaderBar.i18nBundle.getText(TIMELINE_SORT_ASCENDING_TOOLTIP); + } + + get _sortIcon() { + if (this.sortOrder === "Ascending") { + return "sort-ascending"; + } + if (this.sortOrder === "Descending") { + return "sort-descending"; + } + return "sort"; + } + + _onSearchInput(e: CustomEvent) { + const value = (e.target as Input).value; + this.searchValue = value; + this.fireDecoratorEvent("search", { value }); + } + + _onFilterChange(e: CustomEvent) { + const select = e.target as Select; + const selectedOption = select.selectedOption; + const selectedText = selectedOption?.textContent?.trim() || ""; + + // Update the selected state of filter options + this.filterOptions.forEach(option => { + option.selected = option.text === selectedText; + }); + + const selectedOptions = this.filterOptions + .filter(option => option.selected) + .map(option => option.text); + + this.fireDecoratorEvent("filter", { + filterBy: this.filterBy, + selectedOptions, + }); + } + + _onSortClick() { + // Toggle sort order: None -> Ascending -> Descending -> Ascending + if (this.sortOrder === "None" || this.sortOrder === "Descending") { + this.sortOrder = "Ascending"; + } else { + this.sortOrder = "Descending"; + } + + this.fireDecoratorEvent("sort", { + sortOrder: this.sortOrder, + }); + } +} + +TimelineHeaderBar.define(); + +export default TimelineHeaderBar; +export type { + TimelineHeaderBarSearchEventDetail, + TimelineHeaderBarFilterEventDetail, + TimelineHeaderBarSortEventDetail, +}; diff --git a/packages/fiori/src/TimelineHeaderBarTemplate.tsx b/packages/fiori/src/TimelineHeaderBarTemplate.tsx new file mode 100644 index 000000000000..b443b6249b53 --- /dev/null +++ b/packages/fiori/src/TimelineHeaderBarTemplate.tsx @@ -0,0 +1,66 @@ +import Input from "@ui5/webcomponents/dist/Input.js"; +import Button from "@ui5/webcomponents/dist/Button.js"; +import Select from "@ui5/webcomponents/dist/Select.js"; +import Option from "@ui5/webcomponents/dist/Option.js"; +import Icon from "@ui5/webcomponents/dist/Icon.js"; +import type TimelineHeaderBar from "./TimelineHeaderBar.js"; +import search from "@ui5/webcomponents-icons/dist/search.js"; + +export default function TimelineHeaderBarTemplate(this: TimelineHeaderBar) { + return ( + + ); +} diff --git a/packages/fiori/src/TimelineItem.ts b/packages/fiori/src/TimelineItem.ts index 4b0a26dce81f..77931dcdffb7 100644 --- a/packages/fiori/src/TimelineItem.ts +++ b/packages/fiori/src/TimelineItem.ts @@ -161,6 +161,8 @@ class TimelineItem extends UI5Element implements ITimelineItem { lastItem = false; /** + * Used internally by TimelineGroupItem for collapse/expand mechanics. + * Applications should not use this for filtering — instead, add/remove items from the DOM. * @private */ @property({ type: Boolean }) diff --git a/packages/fiori/src/TimelineTemplate.tsx b/packages/fiori/src/TimelineTemplate.tsx index 7818da88a07a..be0d318d4293 100644 --- a/packages/fiori/src/TimelineTemplate.tsx +++ b/packages/fiori/src/TimelineTemplate.tsx @@ -14,6 +14,13 @@ export default function TimelineTemplate(this: Timeline) { onFocusIn={this._onfocusin} onKeyDown={this._onkeydown} > + {/* Header Bar Slot */} + {this._hasHeaderBar && ( +
+ +
+ )} + Timeline with Various Timeline Item States + +
+

Timeline with Header Bar - Full Featured Demo

+

Complete example with Search, Filter, and Sort - all features working together.

+
+ Event Log: Interact with the header bar... +
+
+ Visible Items: 6 of 6 +
+
+ + + + + + + + + Quarterly planning session with the development team + + + Review pull request for authentication module + + + Discussion about project requirements + + + Update API documentation for new endpoints + + + Deploy hotfix v2.5.1 to production environment + + + Daily standup meeting - sprint progress review + + +
+
+ +
+

Timeline with Search Only

+

Search filters items by title, name, and subtitle text in real-time.

+
+ + + + Plan next sprint tasks and assign story points + + + New user dashboard feature with analytics + + + Demo new features to client stakeholders + + + Deploy v2.5.0 to production environment + + +
+
+ +
+

Timeline with Sort Only

+

Click the sort button to toggle between ascending (oldest first) and descending (newest first) order.

+
+ Current Order: None +
+
+ + + + Daily standup - sprint day 3 + + + Review new component designs + + + Discuss microservices migration + + + End of sprint retrospective + + +
+
+ +
+

Timeline with Filter Only

+

Filter items by priority level. Groups are hidden when they have no visible items.

+
+ + + + + + + + + + Fix authentication bypass vulnerability + + + Review new search functionality + + + Update user guide for v2.5 + + + + + Deploy v2.5.0 to production + + + Weekly team synchronization + + + +
+
+ +
+

Timeline with Combined Search and Filter

+

Both search and filter work together - items must match both criteria to be visible.

+
+ + + + + + + + + Integrate payment gateway API + + + Live product demonstration for customers + + + Scheduled downtime for database upgrade + + + Quarterly security assessment + + + Outdoor team building activity + + + Software license expiring soon + + +
+
+ + diff --git a/packages/fiori/test/pages/TimelineHeaderBar.html b/packages/fiori/test/pages/TimelineHeaderBar.html new file mode 100644 index 000000000000..1ea6e41fb0ec --- /dev/null +++ b/packages/fiori/test/pages/TimelineHeaderBar.html @@ -0,0 +1,817 @@ + + + + + + Timeline Header Bar - Application-Driven Filtering + + + + + + + + + + + +
+ +

Project Activity Feed

+

+ A real-world project tracker. The application stores all items in memory and + re-renders only the matching subset into the DOM on every search, filter, or sort interaction. +

+ +
+ Last action + +
+ +
+
+
+ + Showing 8 of 8 items +
+ +
+ +
+ Try searching deploy, filtering by Development, or sorting by date. + Items are added and removed from the DOM — no hidden attribute is used. +
+ +
+ + + + + + + + + + Kick-off for Sprint 24. Estimated velocity: 42 story points. + + + + Replaced legacy session handling with JWT tokens. All 127 tests green. + + + + Discussed Q2 roadmap priorities. Action items documented in Confluence. + + + + Added OpenAPI specs for /payments and /webhooks endpoints. + + + + Staging deployment successful. Smoke tests passed in 3m 22s. + + + + Fixed N+1 query in dashboard endpoint. Latency p99 dropped from 1.2s to 180ms. + + + + Production deploy complete. Zero-downtime rolling update across 12 pods. + + + + Retro on v2.5.1 cycle. Key takeaway: improve staging parity with prod. + + +
+
+
+ + +
+ +

Support Ticket History

+

+ Search removes non-matching items from the DOM and restores them when cleared. + Try typing auth or payment. +

+ +
+
+ + + + + Users report 403 errors when authenticating via Safari 17.2 with SSO enabled. + + + + Stripe webhook responses exceeding 30s timeout under peak load. + + + + Export report omits custom fields added after v2.4 migration. + + + + JWT refresh endpoint returns 401, causing infinite retry in the client SDK. + + + + Bar chart overlaps axis labels when dataset exceeds 50 entries. + + +
+
+
+ + +
+ +

Build Pipeline Log

+

+ Sort reorders items in the DOM via appendChild. The connector lines + remain intact because the Timeline recalculates layout on every render. +

+ +
+ Sort order + Default +
+ +
+
+ + + + + TypeScript compilation succeeded in 47s. Zero errors. + + + + ESLint passed with 3 warnings (no-unused-vars). + + + + 1,247 tests passed. Coverage: 94.2% (+0.3%). + + + + 2 failures in /api/webhooks suite. Retry scheduled. + + + + Retry succeeded. Flaky test isolated and quarantined. + + + + Deployed to staging cluster. Health checks green. + + +
+
+
+ + +
+ +

Incident Response Log

+

+ Filter by severity. When all items in a group are filtered out, the entire + group is removed from the DOM — not hidden. +

+ +
+
+ + + + + + + + + + + Primary DB node unresponsive. Automatic failover to replica completed in 8s. + + + + p99 latency exceeded 2s threshold for /search endpoint (3.1s observed). + + + + Scaled from 8 to 12 pods in us-east-1 due to traffic surge. + + + + + + Wildcard cert for *.api.example.com expires in 72 hours. Auto-renewal failed. + + + + /var/log partition at 87% capacity on worker-node-03. + + + + Updated lodash from 4.17.20 to 4.17.21 (security patch). + + + +
+
+
+ + +
+ +

Team Activity Stream

+

+ Both criteria apply simultaneously. An item must match the search text and + the selected filter to remain in the DOM. +

+ +
+ Active filters + Category: All +
+ +
+
+ + + + + + + + + + New REST endpoints for user preference storage. Includes rate limiting. + + + + Approved with minor suggestions on error handling. + + + + Event listeners were not being cleaned up on disconnect. + + + + Canary at 5% traffic. Error rate: 0.02% (baseline: 0.03%). + + + + Requested changes: accessibility contrast ratios need adjustment. + + + + v2.6.0 fully rolled out. All health checks passing. + + + + Moved auth logic to shared middleware. Reduced duplication across 14 routes. + + +
+
+
+ + +
+ +

Horizontal Timeline — Search + Sort

+

+ Validates that connector lines render correctly in horizontal layout after + items are removed and re-added. Try searching deploy then clearing. +

+ +
+
+ + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Mixed Layout — Flat Items + Groups Interleaved

+

+ Tests ordering preservation when flat items and groups are interleaved. + Filter by category, then select "All" again — items must return to their original positions. +

+ +
+ Status + All items visible +
+ +
+
+ + + + + + + + + + This flat item should always appear first. + + + + + + + + + + + + + + This flat item should stay between the two groups. + + + + + + + + + + + + + + This flat item should always appear last. + + +
+
+
+ + +
+ +

Empty State — No Matching Results

+

+ Search for xyznonexistent to remove all items. The timeline + should be empty without errors. Clear search to restore all items in order. +

+ +
+
+ + + + + Content A + + + + Content B + + + + Content C + + +
+
+
+ + + + + diff --git a/packages/fiori/test/pages/TimelineShowcase.html b/packages/fiori/test/pages/TimelineShowcase.html new file mode 100644 index 000000000000..4575cb0489fe --- /dev/null +++ b/packages/fiori/test/pages/TimelineShowcase.html @@ -0,0 +1,1112 @@ + + + + + + Timeline Architecture Showcase + + + + + + + + +
+ Architecture Showcase +

ui5-timeline Header Bar

+

+ Search, filter, and sort for Timeline. This page walks through every layer of the + architecture: how components communicate, how events flow, and how the application + drives the DOM. Every demo is live and interactive. +

+
+ + + + + +
+ + +
+ Chapter 1 +

Architecture Overview

+

+ The Timeline does not filter, sort, or search anything. It renders whatever items + exist in the DOM. All data operations are the application's responsibility. The header bar + is a thin UI layer that fires events. +

+ +

+ This is a deliberate design choice. Web Components follow the platform convention: + a <select> element doesn't fetch its own data, and neither does a + <ui5-timeline>. The application owns the data; the component owns the rendering. +

+ + +
+
+
+
User
+
Types / Clicks
+
+
+ + interaction +
+
+
Component
+
TimelineHeaderBar
+
+
+ + fires event +
+
+
Parent
+
Timeline
+
+
+ + bubbles up +
+
+
Handler
+
Application
+
+
+ + mutates DOM +
+
+
Result
+
Re-render
+
+
+
+ + +
+ +
+

Filter Select

+

Fires filter event with {filterBy, selectedOptions[]}. Options are defined declaratively via <ui5-timeline-filter-option> children.

+
+
+

Sort Button

+

Toggles between Ascending and Descending. Fires sort event with {sortOrder}. The application reorders DOM nodes via appendChild.

+
+
+

Application Logic

+

The glue. Listens to events, decides which items match, and reconciles the DOM. The Timeline re-renders automatically when its children change.

+
+
+ +
+
+

Declarative Slot

+

The header bar lives in a named headerBar slot. The Timeline detects it via _hasHeaderBar and renders a wrapper div.

+
+
+

Event Bubbling

+

Events from the header bar bubble through the Timeline to the application. bubbles: true means you listen on the Timeline, not the header bar.

+
+
+

DOM Reconciliation

+

Items removed from the DOM disappear instantly. Items added back trigger the Timeline's onBeforeRendering which recalculates layout, lines, and roles.

+
+
+
+ + + +
+ Chapter 2 +

Event Pipeline — Live

+

+ Interact with the timeline below. Every event is logged in real time, and the pipeline + diagram highlights each step as it executes. This is the actual component running. +

+ + +
+
Event flow
+
+
+
1
+
User Input
+
keystroke / click
+
+
+
2
+
HeaderBar
+
fires event
+
+
+
3
+
Timeline
+
bubbles up
+
+
+
4
+
App Handler
+
filter logic
+
+
+
5
+
DOM Update
+
remove / append
+
+
+
+ + +
+
Event Log (live)
+
+ + +
+ + Showing 8 of 8 items +
+ + +
+
Live Demo — Project Activity Feed
+
+ + + + + + + + + + Kick-off for Sprint 24. Estimated velocity: 42 story points. + + + + Replaced legacy session handling with JWT tokens. All 127 tests green. + + + + Discussed Q2 roadmap priorities. Action items documented in Confluence. + + + + Added OpenAPI specs for /payments and /webhooks endpoints. + + + + Staging deployment successful. Smoke tests passed in 3m 22s. + + + + Fixed N+1 query in dashboard endpoint. Latency p99 dropped from 1.2s to 180ms. + + + + Production deploy complete. Zero-downtime rolling update across 12 pods. + + + + Retro on v2.5.1 cycle. Key takeaway: improve staging parity with prod. + + +
+
+ +
+
Try it
+

+ Type deploy in the search field, then select Releases + from the filter. Watch the pipeline animate step by step and the event log fill up. + Then click the sort button to reverse chronological order. +

+
+
+ + + + + + + +
+ Chapter 4 +

Filter — Category Selection with Groups

+

+ Filter options are declared as child elements. When the user selects a category, + the application removes non-matching items. If all items in a group are removed, + the group element itself is removed from the DOM. +

+ +

+ The TimelineFilterOption component is minimal: just text + and selected properties. The header bar reads them to populate a + <ui5-select> dropdown. This keeps filter options declarative and + serializable in HTML. +

+ + +
+
+ Declaring filter options in HTML + HTML +
+
<ui5-timeline-header-bar slot="headerBar" + show-filter filter-by="Severity"> + + <ui5-timeline-filter-option text="All" selected></ui5-timeline-filter-option> + <ui5-timeline-filter-option text="Critical"></ui5-timeline-filter-option> + <ui5-timeline-filter-option text="Warning"></ui5-timeline-filter-option> + +</ui5-timeline-header-bar>
+
+ + +
+
Live Demo — Incident Response Log (with Groups)
+
+ + + + + + + + + + + Primary DB node unresponsive. Automatic failover to replica completed in 8s. + + + + p99 latency exceeded 2s threshold for /search endpoint (3.1s observed). + + + + Scaled from 8 to 12 pods in us-east-1 due to traffic surge. + + + + + + Wildcard cert for *.api.example.com expires in 72 hours. Auto-renewal failed. + + + + /var/log partition at 87% capacity on worker-node-03. + + + + Updated lodash from 4.17.20 to 4.17.21 (security patch). + + + +
+
+ +
+
Try it
+

+ Select Critical from the dropdown. Both groups remain because each has + at least one critical item. Select Info — both groups still show, + each with one info item. Select All to restore everything. +

+
+
+ + + +
+ Chapter 5 +

Sort — Chronological Reordering

+

+ Sort reorders existing DOM nodes via appendChild. Moving a node + does not destroy or recreate it — event listeners and component state survive. + The Timeline recalculates connector lines on every render. +

+ +

+ The sort button cycles through Ascending (oldest first) and + Descending (newest first). The icon changes to reflect the current order. + Internally, the header bar tracks sortOrder as a property and toggles it + in _onSortClick. +

+ + +
+
+ TimelineHeaderBar.ts — sort toggle + TypeScript +
+
_onSortClick() { + // Toggle: None -> Ascending -> Descending -> Ascending + if (this.sortOrder === "None" || this.sortOrder === "Descending") { + this.sortOrder = "Ascending"; + } else { + this.sortOrder = "Descending"; + } + + this.fireDecoratorEvent("sort", { + sortOrder: this.sortOrder, + }); +} + +// Application handler: +timeline.addEventListener("sort", e => { + const order = e.detail.sortOrder; + const sorted = [...items].sort((a, b) => { + const da = new Date(a.dataset.date); + const db = new Date(b.dataset.date); + return order === "Ascending" ? da - db : db - da; + }); + sorted.forEach(item => timeline.appendChild(item)); +});
+
+ + +
+ + Default order +
+ +
+
Live Demo — Build Pipeline Log
+
+ + + + + TypeScript compilation succeeded in 47s. Zero errors. + + + + ESLint passed with 3 warnings (no-unused-vars). + + + + 1,247 tests passed. Coverage: 94.2% (+0.3%). + + + + 2 failures in /api/webhooks suite. Retry scheduled. + + + + Retry succeeded. Flaky test isolated and quarantined. + + + + Deployed to staging cluster. Health checks green. + + +
+
+ +
+
Try it
+

+ Click the sort button once — items reorder oldest-first (Ascending). + Click again — newest-first (Descending). Notice the connector lines remain + correct because the Timeline recalculates layout on every render cycle. +

+
+
+ + + +
+ Chapter 6 +

Combined — Search + Filter + Sort Together

+

+ All three mechanisms compose naturally. The application maintains a state object with + search, filter, and sortOrder fields. Every + event triggers a single apply() function that reconciles the DOM. +

+ + +
+
+ Application handler — combined state + JavaScript +
+
const state = { search: "", filter: "All", sortOrder: "None" }; + +function apply() { + // 1. Filter: keep only matching items in the DOM + mgr.reconcile(item => { + const text = item.titleText.toLowerCase(); + const searchOk = !state.search || text.includes(state.search); + const filterOk = state.filter === "All" + || item.dataset.category === state.filter; + return searchOk && filterOk; + }); + + // 2. Sort: reorder remaining DOM nodes + if (state.sortOrder !== "None") { + mgr.sort((a, b) => { + const da = new Date(a.dataset.date); + const db = new Date(b.dataset.date); + return state.sortOrder === "Ascending" ? da - db : db - da; + }); + } +} + +// 3. Wire up all three events to the same apply() +timeline.addEventListener("search", e => { + state.search = e.detail.value.toLowerCase(); + apply(); +}); +timeline.addEventListener("filter", e => { + state.filter = e.detail.selectedOptions[0] || "All"; + apply(); +}); +timeline.addEventListener("sort", e => { + state.sortOrder = e.detail.sortOrder; + apply(); +});
+
+ + +
+ + Category: All +
+ + +
+
Live Demo — Team Activity Stream
+
+ + + + + + + + + + New REST endpoints for user preference storage. Includes rate limiting. + + + + Approved with minor suggestions on error handling. + + + + Event listeners were not being cleaned up on disconnect. + + + + Canary at 5% traffic. Error rate: 0.02% (baseline: 0.03%). + + + + Requested changes: accessibility contrast ratios need adjustment. + + + + v2.6.0 fully rolled out. All health checks passing. + + + + Moved auth logic to shared middleware. Reduced duplication across 14 routes. + + +
+
+ +
+
Try it
+

+ Select Code from the filter, then type auth in search. + Only the auth middleware refactor remains. Click sort to flip the order. Clear search + and select All to restore the full timeline. +

+
+
+ + + +
+ Chapter 7 +

Code Patterns & Best Practices

+

+ Key patterns to follow when implementing Timeline header bar functionality. +

+ +
+
+

Do: Remove items from DOM

+
// Items not matching the filter
+// are fully removed from the DOM.
+item.remove();
+
+// Re-add when they match again.
+timeline.appendChild(item);
+
+
+

Don't: Hide items with CSS

+
// This leaves stale layout data
+// and breaks connector lines.
+item.style.display = "none";
+
+// The Timeline still counts hidden
+// items in onBeforeRendering.
+
+
+ +
+
+

Do: Listen on the Timeline

+
// Events bubble up from the
+// header bar through the Timeline.
+timeline.addEventListener("search",
+  e => { /* handle */ });
+
+
+

Don't: Listen on the HeaderBar

+
// You'd need to reach into the
+// Timeline's slot to get the ref.
+// Events bubble anyway.
+headerBar.addEventListener("search",
+  e => { /* fragile */ });
+
+
+ +
+
+

Do: Preserve original order

+
// Store items on first use, then
+// always remove all + re-add matching
+// in original document order.
+const allItems = [...timeline
+  .querySelectorAll("[ui5-timeline-item]")
+];
+
+
+

Don't: Toggle items individually

+
// Items already in the DOM stay at
+// their current position. Restored
+// items get appended at the end.
+// Result: wrong order after filter
+// clear.
+
+
+ + +

+ The header bar feature spans these files. Each has a single, focused responsibility: +

+ +
+
+ Component file structure + Files +
+
packages/fiori/src/ + Timeline.ts // Parent: fires events, owns headerBar slot + TimelineTemplate.tsx // Template: renders slot wrapper when present + TimelineHeaderBar.ts // Header: search/filter/sort UI + event firing + TimelineHeaderBarTemplate.tsx // Template: Input, Select, Button composition + TimelineFilterOption.ts // Minimal: text + selected properties + TimelineItem.ts // Item: titleText, name, icon, state, etc. + TimelineGroupItem.ts // Group: collapse/expand, child management + +packages/fiori/src/types/ + TimelineSortOrder.ts // Enum: None | Ascending | Descending + TimelineLayout.ts // Enum: Vertical | Horizontal + +packages/fiori/src/themes/ + Timeline.css // Root layout, scroll container + TimelineHeaderBar.css // Toolbar flex layout, spacing
+
+
+ +
+ + + +
+

UI5 Web Components — Timeline Header Bar Architecture Showcase

+
+ + + + + + + diff --git a/packages/fiori/test/pages/styles/TimelineHeaderBar.css b/packages/fiori/test/pages/styles/TimelineHeaderBar.css new file mode 100644 index 000000000000..3a35207ab751 --- /dev/null +++ b/packages/fiori/test/pages/styles/TimelineHeaderBar.css @@ -0,0 +1,178 @@ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; +} + +body.timeline-header-bar-page { + background-color: var(--sapBackgroundColor, #f5f6f7); + font-family: "72", "72full", Arial, Helvetica, sans-serif; + color: var(--sapTextColor, #32363a); + padding-bottom: 4rem; +} + +/* ── Page header ── */ +.page-header { + background: var(--sapShell_Background, #354a5f); + padding: 1.5rem 2.5rem; + border-bottom: 2px solid var(--sapShell_BorderColor, #2c3e50); +} + +.page-header h1 { + font-size: 1.5rem; + font-weight: 400; + color: var(--sapShell_TextColor, #fff); + letter-spacing: 0.02em; + font-family: "72", "72full", Arial, Helvetica, sans-serif; +} + +.page-header p { + font-size: 0.8125rem; + color: var(--sapShell_InteractiveTextColor, #d1e8ff); + margin-top: 0.25rem; + font-family: "72", "72full", Arial, Helvetica, sans-serif; + opacity: 0.85; +} + +/* ── Section layout ── */ +.demo-section { + max-width: 56rem; + margin: 2rem auto; + padding: 0 2rem; +} + +.demo-section + .demo-section { + margin-top: 2.5rem; +} + +.section-label { + display: inline-block; + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--sapContent_LabelColor, #6a6d70); + margin-bottom: 0.375rem; +} + +.section-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--sapTitleColor, #32363a); + margin-bottom: 0.25rem; +} + +.section-desc { + font-size: 0.8125rem; + color: var(--sapContent_LabelColor, #6a6d70); + line-height: 1.5; + margin-bottom: 1.25rem; +} + +/* ── Card container for each demo ── */ +.demo-card { + background: var(--sapGroup_ContentBackground, #fff); + border: 1px solid var(--sapGroup_ContentBorderColor, #d9d9d9); + border-radius: 0.75rem; + overflow: hidden; +} + +.demo-card-body { + padding: 1rem; +} + +/* ── Event log ── */ +.event-log { + background: var(--sapList_Background, #fff); + border: 1px solid var(--sapGroup_ContentBorderColor, #d9d9d9); + border-radius: 0.5rem; + padding: 0.625rem 0.875rem; + margin-bottom: 1rem; + display: flex; + align-items: baseline; + gap: 0.5rem; + min-height: 2.25rem; +} + +.event-log-label { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--sapContent_LabelColor, #6a6d70); + white-space: nowrap; +} + +.event-log-value { + font-size: 0.8125rem; + color: var(--sapTextColor, #32363a); + font-family: "72Mono", "72Monofull", "Courier New", monospace; +} + +/* ── Status bar ── */ +.status-bar { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.625rem 1rem; + background: var(--sapObjectHeader_Background, #f7f7f7); + border-bottom: 1px solid var(--sapGroup_ContentBorderColor, #d9d9d9); + font-size: 0.8125rem; +} + +.status-item { + display: flex; + align-items: center; + gap: 0.375rem; +} + +.status-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--sapPositiveColor, #256f3a); +} + +.status-dot--filtered { + background: var(--sapInformativeColor, #0a6ed1); +} + +.status-count { + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +/* ── Scenario description inside card ── */ +.scenario-note { + padding: 0.75rem 1rem; + background: var(--sapInformationBackground, #e8f0fa); + border-bottom: 1px solid var(--sapInformativeColor, #0a6ed1); + font-size: 0.8125rem; + color: var(--sapTextColor, #32363a); + line-height: 1.5; +} + +.scenario-note code { + font-family: "72Mono", "72Monofull", "Courier New", monospace; + font-size: 0.75rem; + background: rgba(0, 0, 0, 0.06); + padding: 0.1em 0.35em; + border-radius: 3px; +} + +/* ── Timeline container constraint ── */ +.timeline-container { + padding: 1rem; +} + +.timeline-container--scroll { + max-height: 28rem; + overflow-y: auto; +} diff --git a/packages/fiori/test/pages/styles/TimelineShowcase.css b/packages/fiori/test/pages/styles/TimelineShowcase.css new file mode 100644 index 000000000000..d7cce8fab039 --- /dev/null +++ b/packages/fiori/test/pages/styles/TimelineShowcase.css @@ -0,0 +1,777 @@ +/* ════════════════════════════════════════════════════ + Timeline Architecture Showcase — Styles + ════════════════════════════════════════════════════ */ + +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + scroll-behavior: smooth; +} + +body.showcase-page { + background-color: #fafbfc; + font-family: "72", "72full", Arial, Helvetica, sans-serif; + color: #32363a; + line-height: 1.6; +} + +/* ── Utility ── */ +.mono { font-family: "72Mono", "72Monofull", "Courier New", monospace; } +.muted { color: #6a6d70; } + +/* ════════════════════════════════════════════ + Hero / Opening + ════════════════════════════════════════════ */ +.hero { + background: #1a2733; + padding: 4rem 2rem 3.5rem; + text-align: center; + position: relative; + overflow: hidden; +} + +.hero::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent); +} + +.hero-tag { + display: inline-block; + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #7eb8f0; + margin-bottom: 1rem; + padding: 0.25rem 0.75rem; + border: 1px solid rgba(126, 184, 240, 0.3); + border-radius: 3px; +} + +.hero h1 { + font-size: 2rem; + font-weight: 300; + color: #ffffff; + letter-spacing: -0.01em; + margin-bottom: 0.5rem; +} + +.hero h1 strong { + font-weight: 600; +} + +.hero-sub { + font-size: 0.875rem; + color: rgba(255,255,255,0.55); + max-width: 36rem; + margin: 0 auto; + line-height: 1.65; +} + +/* ════════════════════════════════════════════ + Table of Contents + ════════════════════════════════════════════ */ +.toc { + max-width: 52rem; + margin: -1.5rem auto 0; + padding: 0 2rem; + position: relative; + z-index: 1; +} + +.toc-inner { + background: #fff; + border: 1px solid #e0e3e6; + border-radius: 0.5rem; + padding: 1.25rem 1.5rem; + display: flex; + gap: 2rem; + align-items: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); +} + +.toc-label { + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #6a6d70; + white-space: nowrap; + flex-shrink: 0; +} + +.toc-links { + display: flex; + gap: 0.25rem; + flex-wrap: wrap; +} + +.toc-links a { + font-size: 0.8125rem; + color: #0a6ed1; + text-decoration: none; + padding: 0.25rem 0.625rem; + border-radius: 4px; + transition: background 0.15s; +} + +.toc-links a:hover { + background: #e8f0fa; +} + +/* ════════════════════════════════════════════ + Content wrapper + ════════════════════════════════════════════ */ +.content { + max-width: 52rem; + margin: 0 auto; + padding: 0 2rem; +} + +/* ════════════════════════════════════════════ + Chapter / Section system + ════════════════════════════════════════════ */ +.chapter { + margin-top: 4rem; + scroll-margin-top: 2rem; +} + +.chapter + .chapter { + padding-top: 3rem; + border-top: 1px solid #e8ebee; +} + +.chapter-number { + display: inline-block; + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #0a6ed1; + margin-bottom: 0.5rem; +} + +.chapter h2 { + font-size: 1.375rem; + font-weight: 600; + color: #1a2733; + margin-bottom: 0.375rem; + letter-spacing: -0.01em; +} + +.chapter-lead { + font-size: 0.875rem; + color: #6a6d70; + line-height: 1.65; + margin-bottom: 2rem; + max-width: 42rem; +} + +/* ── Prose paragraphs ── */ +.prose { + font-size: 0.875rem; + color: #3b4148; + line-height: 1.75; + margin-bottom: 1.5rem; + max-width: 42rem; +} + +.prose code { + font-family: "72Mono", "72Monofull", "Courier New", monospace; + font-size: 0.8125rem; + background: #f0f2f4; + padding: 0.1em 0.4em; + border-radius: 3px; + color: #0a6ed1; +} + +.prose strong { + font-weight: 600; + color: #1a2733; +} + +/* ════════════════════════════════════════════ + Architecture Diagram + ════════════════════════════════════════════ */ +.arch-diagram { + background: #fff; + border: 1px solid #e0e3e6; + border-radius: 0.625rem; + padding: 2rem 1.5rem; + margin-bottom: 2rem; + overflow-x: auto; +} + +.arch-flow { + display: flex; + align-items: center; + gap: 0; + justify-content: center; + min-width: fit-content; +} + +.arch-box { + text-align: center; + padding: 0.875rem 1.25rem; + border-radius: 0.5rem; + min-width: 8.5rem; + flex-shrink: 0; +} + +.arch-box--user { + background: #e8f0fa; + border: 1px solid #b3d4f7; +} + +.arch-box--header { + background: #fff3e0; + border: 1px solid #f5c86a; +} + +.arch-box--timeline { + background: #e6f4ea; + border: 1px solid #93d4a3; +} + +.arch-box--app { + background: #fce4ec; + border: 1px solid #f5a0b4; +} + +.arch-box--dom { + background: #f3e5f5; + border: 1px solid #ce93d8; +} + +.arch-box-label { + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #6a6d70; + margin-bottom: 0.25rem; +} + +.arch-box-name { + font-size: 0.8125rem; + font-weight: 600; + color: #1a2733; +} + +.arch-arrow { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 0.5rem; + flex-shrink: 0; +} + +.arch-arrow-line { + font-size: 0.875rem; + color: #9aa0a6; + letter-spacing: 0.1em; +} + +.arch-arrow-label { + font-size: 0.625rem; + color: #6a6d70; + white-space: nowrap; + margin-top: 0.125rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* ════════════════════════════════════════════ + Code blocks + ════════════════════════════════════════════ */ +.code-block { + background: #1a2733; + border-radius: 0.5rem; + overflow: hidden; + margin-bottom: 1.5rem; +} + +.code-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 1rem; + background: rgba(255,255,255,0.05); + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.code-filename { + font-size: 0.75rem; + color: rgba(255,255,255,0.5); + font-family: "72Mono", "72Monofull", monospace; +} + +.code-lang { + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #7eb8f0; + padding: 0.15rem 0.5rem; + border: 1px solid rgba(126,184,240,0.3); + border-radius: 3px; +} + +.code-body { + padding: 1rem 1.25rem; + overflow-x: auto; + font-size: 0.8125rem; + line-height: 1.65; + color: #d4dbe4; + font-family: "72Mono", "72Monofull", "Courier New", monospace; + white-space: pre; + tab-size: 2; +} + +.code-body .kw { color: #c792ea; } +.code-body .fn { color: #82aaff; } +.code-body .str { color: #c3e88d; } +.code-body .cmt { color: #546e7a; } +.code-body .tag { color: #f07178; } +.code-body .attr { color: #ffcb6b; } +.code-body .val { color: #c3e88d; } +.code-body .punct { color: #89ddff; } +.code-body .num { color: #f78c6c; } +.code-body .type { color: #ffcb6b; } + +/* ════════════════════════════════════════════ + Live Demo Card + ════════════════════════════════════════════ */ +.demo-card { + background: #fff; + border: 1px solid #e0e3e6; + border-radius: 0.625rem; + margin-bottom: 1.5rem; +} + +.demo-label { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1rem; + background: #f7f8f9; + border-bottom: 1px solid #e8ebee; + border-radius: 0.625rem 0.625rem 0 0; + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #6a6d70; +} + +.demo-label .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #36b37e; +} + +.demo-body { + padding: 1rem; +} + +/* ════════════════════════════════════════════ + Event Pipeline Visualizer + ════════════════════════════════════════════ */ +.pipeline { + margin-bottom: 1.5rem; +} + +.pipeline-header { + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #6a6d70; + margin-bottom: 0.75rem; +} + +.pipeline-steps { + display: flex; + gap: 0; + align-items: stretch; +} + +.pipeline-step { + flex: 1; + text-align: center; + padding: 0.75rem 0.5rem; + border: 1px solid #e0e3e6; + background: #fff; + position: relative; + transition: background 0.4s, border-color 0.4s, transform 0.3s; +} + +.pipeline-step:first-child { + border-radius: 0.375rem 0 0 0.375rem; +} + +.pipeline-step:last-child { + border-radius: 0 0.375rem 0.375rem 0; +} + +.pipeline-step + .pipeline-step { + border-left: none; +} + +.pipeline-step-num { + font-size: 0.625rem; + font-weight: 700; + color: #9aa0a6; + margin-bottom: 0.25rem; + transition: color 0.4s; +} + +.pipeline-step-name { + font-size: 0.75rem; + font-weight: 600; + color: #3b4148; + margin-bottom: 0.125rem; +} + +.pipeline-step-detail { + font-size: 0.625rem; + color: #9aa0a6; + font-family: "72Mono", "72Monofull", monospace; +} + +/* Active pipeline step animation */ +.pipeline-step.active { + background: #e8f0fa; + border-color: #0a6ed1; + z-index: 1; + transform: scale(1.02); +} + +.pipeline-step.active + .pipeline-step { + border-left: 1px solid #0a6ed1; +} + +.pipeline-step.active .pipeline-step-num { + color: #0a6ed1; +} + +.pipeline-step.done { + background: #e6f4ea; + border-color: #36b37e; +} + +.pipeline-step.done .pipeline-step-num { + color: #36b37e; +} + +/* ════════════════════════════════════════════ + Event Log (live) + ════════════════════════════════════════════ */ +.event-stream { + background: #1a2733; + border-radius: 0.375rem; + padding: 0.75rem 1rem; + margin-bottom: 1.5rem; + max-height: 7.5rem; + overflow-y: auto; +} + +.event-stream-label { + font-size: 0.625rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(255,255,255,0.35); + margin-bottom: 0.5rem; +} + +.event-stream-entry { + font-size: 0.75rem; + font-family: "72Mono", "72Monofull", monospace; + color: #d4dbe4; + padding: 0.2rem 0; + opacity: 0; + transform: translateY(-4px); + animation: streamIn 0.3s forwards; +} + +.event-stream-entry .ts { + color: #546e7a; +} + +.event-stream-entry .ev { + color: #c792ea; +} + +.event-stream-entry .payload { + color: #c3e88d; +} + +@keyframes streamIn { + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ════════════════════════════════════════════ + Anatomy callouts + ════════════════════════════════════════════ */ +.anatomy { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 2rem; +} + +.anatomy-item { + padding: 1rem 1.25rem; + background: #fff; + border: 1px solid #e0e3e6; + border-radius: 0.5rem; + border-left: 3px solid #0a6ed1; +} + +.anatomy-item--search { border-left-color: #0a6ed1; } +.anatomy-item--filter { border-left-color: #f5a623; } +.anatomy-item--sort { border-left-color: #36b37e; } +.anatomy-item--app { border-left-color: #e74c3c; } + +.anatomy-item h4 { + font-size: 0.8125rem; + font-weight: 600; + color: #1a2733; + margin-bottom: 0.25rem; +} + +.anatomy-item p { + font-size: 0.75rem; + color: #6a6d70; + line-height: 1.5; +} + +.anatomy-item code { + font-family: "72Mono", "72Monofull", monospace; + font-size: 0.6875rem; + background: #f0f2f4; + padding: 0.1em 0.35em; + border-radius: 3px; +} + +/* ════════════════════════════════════════════ + Status pill + ════════════════════════════════════════════ */ +.status-pill { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.75rem; + padding: 0.25rem 0.75rem; + border-radius: 100px; + background: #e6f4ea; + color: #256f3a; + font-weight: 600; + margin-bottom: 1rem; + transition: all 0.3s; +} + +.status-pill .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #36b37e; +} + +.status-pill--filtered { + background: #e8f0fa; + color: #0a6ed1; +} + +.status-pill--filtered .dot { + background: #0a6ed1; +} + +/* ════════════════════════════════════════════ + Scenario callout + ════════════════════════════════════════════ */ +.scenario { + background: #f7f8f9; + border: 1px solid #e0e3e6; + border-radius: 0.5rem; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; +} + +.scenario-title { + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #6a6d70; + margin-bottom: 0.5rem; +} + +.scenario p { + font-size: 0.8125rem; + color: #3b4148; + line-height: 1.6; +} + +.scenario code { + font-family: "72Mono", "72Monofull", monospace; + font-size: 0.75rem; + background: #e8ebee; + padding: 0.1em 0.35em; + border-radius: 3px; +} + +/* ════════════════════════════════════════════ + Key concept cards + ════════════════════════════════════════════ */ +.concepts { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + margin-bottom: 2rem; +} + +.concept { + padding: 1.25rem; + background: #fff; + border: 1px solid #e0e3e6; + border-radius: 0.5rem; +} + +.concept h4 { + font-size: 0.8125rem; + font-weight: 600; + color: #1a2733; + margin-bottom: 0.375rem; +} + +.concept p { + font-size: 0.75rem; + color: #6a6d70; + line-height: 1.55; +} + +/* ════════════════════════════════════════════ + Comparison / Do & Don't + ════════════════════════════════════════════ */ +.compare-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 2rem; +} + +.compare-card { + padding: 1rem 1.25rem; + border-radius: 0.5rem; + border: 1px solid; +} + +.compare-card--do { + background: #e6f4ea; + border-color: #93d4a3; +} + +.compare-card--dont { + background: #fce4ec; + border-color: #f5a0b4; +} + +.compare-card h4 { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + margin-bottom: 0.5rem; +} + +.compare-card--do h4 { color: #256f3a; } +.compare-card--dont h4 { color: #b71c1c; } + +.compare-card pre { + font-size: 0.75rem; + font-family: "72Mono", "72Monofull", monospace; + line-height: 1.55; + white-space: pre-wrap; + color: #3b4148; +} + +/* ════════════════════════════════════════════ + Footer + ════════════════════════════════════════════ */ +.page-footer { + margin-top: 5rem; + padding: 2rem; + text-align: center; + border-top: 1px solid #e8ebee; +} + +.page-footer p { + font-size: 0.75rem; + color: #9aa0a6; +} + +/* ════════════════════════════════════════════ + Scroll-triggered animations + ════════════════════════════════════════════ */ +.reveal { + opacity: 0; + transform: translateY(16px); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.reveal.visible { + opacity: 1; + transform: translateY(0); +} + +/* staggered children */ +.reveal-stagger > * { + opacity: 0; + transform: translateY(12px); + transition: opacity 0.4s ease, transform 0.4s ease; +} + +.reveal-stagger.visible > *:nth-child(1) { transition-delay: 0s; } +.reveal-stagger.visible > *:nth-child(2) { transition-delay: 0.08s; } +.reveal-stagger.visible > *:nth-child(3) { transition-delay: 0.16s; } +.reveal-stagger.visible > *:nth-child(4) { transition-delay: 0.24s; } +.reveal-stagger.visible > *:nth-child(5) { transition-delay: 0.32s; } + +.reveal-stagger.visible > * { + opacity: 1; + transform: translateY(0); +} + +/* ════════════════════════════════════════════ + Responsive + ════════════════════════════════════════════ */ +@media (max-width: 640px) { + .hero { padding: 2.5rem 1.5rem 2rem; } + .hero h1 { font-size: 1.5rem; } + .content { padding: 0 1rem; } + .toc { padding: 0 1rem; } + .toc-inner { flex-direction: column; gap: 0.75rem; align-items: flex-start; } + .anatomy { grid-template-columns: 1fr; } + .concepts { grid-template-columns: 1fr; } + .compare-grid { grid-template-columns: 1fr; } + .arch-flow { flex-wrap: wrap; justify-content: center; gap: 0.5rem; } + .arch-arrow { display: none; } +}