(null);
+
+/**
+ * Hook to access the content list state and dispatch function.
+ *
+ * This is a low-level hook. For most use cases, prefer the feature-specific hooks
+ * like `useContentListSort`, `useContentListItems`, etc.
+ *
+ * @throws Error if used outside `ContentListProvider`.
+ * @returns The state context value including state, dispatch, and refetch.
+ */
+export const useContentListState = (): ContentListStateContextValue => {
+ const context = useContext(ContentListStateContext);
+ if (!context) {
+ throw new Error(
+ 'ContentListStateContext is missing. Ensure your component is wrapped with ContentListProvider.'
+ );
+ }
+ return context;
+};
diff --git a/src/platform/packages/shared/content-management/content_list/kbn-content-list-provider/src/stories/content_list_provider.stories.tsx b/src/platform/packages/shared/content-management/content_list/kbn-content-list-provider/src/stories/content_list_provider.stories.tsx
new file mode 100644
index 0000000000000..94e82363a914a
--- /dev/null
+++ b/src/platform/packages/shared/content-management/content_list/kbn-content-list-provider/src/stories/content_list_provider.stories.tsx
@@ -0,0 +1,268 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React, { useMemo } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+ EuiPanel,
+ EuiText,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiBadge,
+ EuiButton,
+ EuiButtonGroup,
+ EuiBasicTable,
+ EuiIcon,
+ EuiCallOut,
+} from '@elastic/eui';
+import { MOCK_DASHBOARDS, createMockFindItems } from '@kbn/content-list-mock-data/storybook';
+import { ContentListProvider, useContentListConfig } from '../context';
+import { useContentListItems } from '../state';
+import { useContentListSort } from '../features/sorting';
+import type { FindItemsParams, FindItemsResult } from '../datasource';
+
+interface StoryArgs {
+ entityName: string;
+ entityNamePlural: string;
+ enableSorting: boolean;
+ initialSortField: 'title' | 'updatedAt';
+ initialSortDirection: 'asc' | 'desc';
+ numberOfItems: number;
+ showConfig: boolean;
+}
+
+/**
+ * Demo component that displays the config context.
+ */
+const ConfigDisplay = () => {
+ const config = useContentListConfig();
+
+ return (
+
+
+ Config Context:
+
+ {JSON.stringify(
+ {
+ id: config.id,
+ labels: config.labels,
+ queryKeyScope: config.queryKeyScope,
+ features: config.features,
+ supports: config.supports,
+ },
+ null,
+ 2
+ )}
+
+
+
+ );
+};
+
+/**
+ * Demo component that displays items from the provider.
+ */
+const ItemsList = () => {
+ const { items, totalItems, refetch } = useContentListItems();
+
+ return (
+
+
+
+ {totalItems} items
+
+
+ refetch()}>
+ Refresh
+
+
+
+
+
+
+ {title} },
+ { field: 'description', name: 'Description' },
+ {
+ field: 'updatedAt',
+ name: 'Updated',
+ render: (date: Date | undefined) => (date ? date.toLocaleDateString() : '—'),
+ },
+ ]}
+ />
+
+ );
+};
+
+/**
+ * Demo component that displays sort controls.
+ */
+const SortControls = () => {
+ const { field, direction, setSort } = useContentListSort();
+ const { supports } = useContentListConfig();
+
+ if (!supports.sorting) {
+ return (
+
+ Sorting is disabled
+
+ );
+ }
+
+ const sortOptions = [
+ { id: 'title-asc', label: 'Title A-Z' },
+ { id: 'title-desc', label: 'Title Z-A' },
+ { id: 'updatedAt-desc', label: 'Recently updated' },
+ { id: 'updatedAt-asc', label: 'Oldest first' },
+ ];
+
+ return (
+
+
+
+
+
+ Sort by:
+
+
+ {
+ const [newField, newDirection] = id.split('-');
+ setSort(newField, newDirection as 'asc' | 'desc');
+ }}
+ buttonSize="compressed"
+ />
+
+
+ );
+};
+
+const meta: Meta = {
+ title: 'Content Management/Content List',
+ parameters: { layout: 'padded' },
+ argTypes: {
+ entityName: { control: 'text', description: 'Singular entity name' },
+ entityNamePlural: { control: 'text', description: 'Plural entity name' },
+ enableSorting: { control: 'boolean', description: 'Enable sorting feature' },
+ initialSortField: {
+ control: 'select',
+ options: ['title', 'updatedAt'],
+ description: 'Initial sort field',
+ },
+ initialSortDirection: {
+ control: 'radio',
+ options: ['asc', 'desc'],
+ description: 'Initial sort direction',
+ },
+ numberOfItems: {
+ control: { type: 'range', min: 0, max: 8, step: 1 },
+ description: 'Number of items to display',
+ },
+ showConfig: { control: 'boolean', description: 'Show config context panel' },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+/**
+ * Story wrapper component that handles stable prop references.
+ */
+const ProviderStory = ({ args }: { args: StoryArgs }) => {
+ // Memoize labels to maintain stable reference.
+ const labels = useMemo(
+ () => ({ entity: args.entityName, entityPlural: args.entityNamePlural }),
+ [args.entityName, args.entityNamePlural]
+ );
+
+ // Memoize dataSource to maintain stable reference.
+ const dataSource = useMemo(() => {
+ const mockFindItems = createMockFindItems({
+ items: MOCK_DASHBOARDS.slice(0, args.numberOfItems),
+ });
+
+ const findItems = async (params: FindItemsParams): Promise => {
+ const result = await mockFindItems({
+ searchQuery: params.searchQuery,
+ filters: {},
+ sort: params.sort ?? { field: 'title', direction: 'asc' },
+ page: params.page,
+ });
+ return {
+ items: result.items.map((item) => ({
+ id: item.id,
+ title: item.attributes.title,
+ description: item.attributes.description,
+ type: item.type,
+ updatedAt: item.updatedAt ? new Date(item.updatedAt) : undefined,
+ })),
+ total: result.total,
+ };
+ };
+
+ return { findItems };
+ }, [args.numberOfItems]);
+
+ // Memoize features to maintain stable reference.
+ const features = useMemo(
+ () =>
+ args.enableSorting
+ ? {
+ sorting: {
+ initialSort: { field: args.initialSortField, direction: args.initialSortDirection },
+ },
+ }
+ : { sorting: false as const },
+ [args.enableSorting, args.initialSortField, args.initialSortDirection]
+ );
+
+ // Key forces re-mount when configuration changes.
+ const key = `${args.enableSorting}-${args.initialSortField}-${args.initialSortDirection}-${args.numberOfItems}`;
+
+ return (
+
+ {args.showConfig && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ );
+};
+
+export const Provider: Story = {
+ args: {
+ entityName: 'dashboard',
+ entityNamePlural: 'dashboards',
+ enableSorting: true,
+ initialSortField: 'title',
+ initialSortDirection: 'asc',
+ numberOfItems: 8,
+ showConfig: true,
+ },
+ render: (args) => ,
+};
diff --git a/src/platform/packages/shared/content-management/content_list/kbn-content-list-provider/tsconfig.json b/src/platform/packages/shared/content-management/content_list/kbn-content-list-provider/tsconfig.json
new file mode 100644
index 0000000000000..de3dcebf89772
--- /dev/null
+++ b/src/platform/packages/shared/content-management/content_list/kbn-content-list-provider/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "@kbn/tsconfig-base/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "target/types"
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/react-query",
+ "@kbn/content-list-mock-data"
+ ]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 554c2e8f7a5e9..a9686c264e86f 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -256,6 +256,10 @@
"@kbn/content-connectors-plugin/*": ["x-pack/platform/plugins/shared/content_connectors/*"],
"@kbn/content-list-mock-data": ["src/platform/packages/shared/content-management/content_list/kbn-content-list-mock-data"],
"@kbn/content-list-mock-data/*": ["src/platform/packages/shared/content-management/content_list/kbn-content-list-mock-data/*"],
+ "@kbn/content-list-provider": ["src/platform/packages/shared/content-management/content_list/kbn-content-list-provider"],
+ "@kbn/content-list-provider/*": ["src/platform/packages/shared/content-management/content_list/kbn-content-list-provider/*"],
+ "@kbn/content-list-provider-client": ["src/platform/packages/shared/content-management/content_list/kbn-content-list-provider-client"],
+ "@kbn/content-list-provider-client/*": ["src/platform/packages/shared/content-management/content_list/kbn-content-list-provider-client/*"],
"@kbn/content-management-access-control-public": ["src/platform/packages/shared/content-management/access_control/access_control_public"],
"@kbn/content-management-access-control-public/*": ["src/platform/packages/shared/content-management/access_control/access_control_public/*"],
"@kbn/content-management-access-control-server": ["src/platform/packages/shared/content-management/access_control/access_control_server"],
diff --git a/yarn.lock b/yarn.lock
index a3d7d6d56f325..812cc1619dba0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5193,6 +5193,14 @@
version "0.0.0"
uid ""
+"@kbn/content-list-provider-client@link:src/platform/packages/shared/content-management/content_list/kbn-content-list-provider-client":
+ version "0.0.0"
+ uid ""
+
+"@kbn/content-list-provider@link:src/platform/packages/shared/content-management/content_list/kbn-content-list-provider":
+ version "0.0.0"
+ uid ""
+
"@kbn/content-management-access-control-public@link:src/platform/packages/shared/content-management/access_control/access_control_public":
version "0.0.0"
uid ""