https://github.com/neovici/cosmoz-queue
Cosmoz Queue to manage a list of entitites
https://github.com/neovici/cosmoz-queue
Last synced: 3 months ago
JSON representation
Cosmoz Queue to manage a list of entitites
- Host: GitHub
- URL: https://github.com/neovici/cosmoz-queue
- Owner: Neovici
- Created: 2025-12-12T06:42:01.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-01-23T12:41:01.000Z (5 months ago)
- Last Synced: 2026-01-24T04:38:00.637Z (5 months ago)
- Language: TypeScript
- Size: 1.91 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# @neovici/cosmoz-queue
A hooks-based UI component library for building **master-detail views** with list, split, and queue navigation modes. Built on [`@pionjs/pion`](https://github.com/pionjs/pion) and [`lit-html`](https://lit.dev/docs/libraries/standalone-templates/).
> **What is a "queue"?** Not a data structure -- a UI pattern. Users work through a list of items one-by-one, like processing a queue of tasks. The component provides three view modes: a data table, a side-by-side split, and a full-screen sequential navigator.
## Installation
```sh
npm install @neovici/cosmoz-queue
```
Peer dependencies: `@pionjs/pion`, `lit-html`, `i18next`.
## Quick start
The `queue()` factory is the recommended high-level API. It composes all the internal hooks and renders the complete queue UI -- tabs, navigation, list, and detail view.
```ts
import { queue } from '@neovici/cosmoz-queue';
import { spread } from '@open-wc/lit-helpers';
import { component, useCallback } from '@pionjs/pion';
import { html } from 'lit-html';
import { ref } from 'lit-html/directives/ref.js';
import { fetchOrderDetails$ } from './api';
import type { OrderListItem } from './types';
const OrderListQueue = ({ heading }: { heading: string }) =>
queue({
// Title displayed above the list
heading,
// Unique key for persisting table column settings
settingsId: 'order-list',
// URL hash parameter names (enables deep-linking and back/forward)
idHashParam: 'id',
tabHashParam: 'qtab',
// Async function that fetches full details for a list item.
// Results are memoized per item identity.
details: useCallback(
(item: OrderListItem) => fetchOrderDetails$(item.id),
[],
),
// Render the list component. `thru` contains bindings that wire
// the omnitable events to queue state (items, selection, clicks).
// Spread them onto your list component.
list: (thru, { onRef }) =>
html``,
// Render the detail view. `nav` contains prev/next buttons --
// place them in a slot or wherever navigation belongs in your view.
view: (thru, { nav }) =>
html`${nav}`,
// Render a loading skeleton shown while `details` resolves.
loader: (thru, { nav }) =>
html`${nav}`,
});
customElements.define('order-list-queue', component(OrderListQueue));
```
### `queue()` props
| Prop | Type | Default | Description |
|---|---|---|---|
| `heading` | `string` | -- | Title displayed above the tabs |
| `settingsId` | `string?` | -- | Key for persisting omnitable column settings |
| `idHashParam` | `string` | `'qid'` | URL hash param for the selected item ID |
| `tabHashParam` | `string` | `'qtab'` | URL hash param for the active tab |
| `details` | `(item: I) => PromiseLike` | -- | Fetches full details for a list item |
| `api` | `(id: string, item: I) => string` | -- | Alternative to `details`: returns a URL, queue fetches JSON internally |
| `list` | `(thru, props) => TemplateResult` | -- | Renders the list component |
| `view` | `(thru, props) => TemplateResult` | -- | Renders the detail view |
| `loader` | `(thru, props) => TemplateResult` | -- | Renders the loading skeleton |
| `pagination` | `Pagination?` | -- | Server-side pagination config |
| `fallback` | `string?` | -- | Default tab when none is set in URL hash |
| `split` | `SplitOpts?` | -- | Split.js configuration (sizes, min sizes) |
| `afterHeading` | `unknown?` | -- | Extra content rendered after the heading |
Use **`details`** when you control the fetch (most common). Use **`api`** when you want the queue to handle fetching via URL.
### The `thru` bindings
The `list`, `view`, and `loader` render functions receive a `thru` object as their first argument. This object contains property and event bindings that wire the child component to queue state:
**For `list`:**
- `.settingsId`, `.exposedParts` -- table configuration
- `@visible-items-changed`, `@selected-items-changed`, `@total-available-changed` -- sync items/selection back to queue
- `@omnitable-item-click` -- item click handler
- `@async-simple-action` -- action completion handler
**For `view`:**
- `.item` -- the current detail item (list item or resolved detail)
- `.hideActions` -- whether to hide actions (true when items are selected in split mode)
- `@async-simple-action` -- action completion handler
Spread these onto your component with `${spread(thru)}` from `@open-wc/lit-helpers`.
## View modes
The queue provides three tab-based view modes:
| Mode | Tab name | Behavior |
|---|---|---|
| **List** | `overview` | Full-width data table (`cosmoz-omnitable`). The default view. |
| **Split** | `split` | Side-by-side: resizable list on the left, detail view on the right. Disabled on mobile. |
| **Queue** | `queue` | Full-screen detail view with prev/next navigation and keyboard arrow keys. |
The active tab is synced to the URL hash (e.g. `#qtab=split&id=abc-123`), enabling deep-linking and browser back/forward navigation.
On mobile viewports, the split tab is disabled and falls back to queue mode automatically.
## List module
The `@neovici/cosmoz-queue/list` entry point provides a managed `cosmoz-omnitable` with built-in state management, data fetching, pagination, and action rendering.
### `listCore()`
The high-level factory that combines `useListCore()` + `renderListCore()`:
```ts
import { itemClick } from '@neovici/cosmoz-queue';
import { column, listCore, style } from '@neovici/cosmoz-queue/list';
import { component, html } from '@pionjs/pion';
import { t } from 'i18next';
import type { OrderListItem } from './types';
interface Props {
exposedParts: string;
api: (params: { pathLocator: string }) => Promise<{
items: OrderListItem[];
metaData: { totalAvailable: number };
}>;
}
const OrderListCore = ({ exposedParts, api }: Props) => {
return listCore({
// Unique key for persisting column settings
settingsId: 'order-list-core',
// CSS parts to expose for external styling
exposedParts,
// When false, omnitable applies its own local filtering
noLocal: false,
// Column definitions. The tuple is [factory, deps] -- same pattern
// as useMemo. The factory returns an object where each key becomes
// a column name.
columns: [
() => ({
title: column({
render: ({ name }) =>
html`
html`${item.title}`}
>`,
}),
status: column({
render: ({ name }) =>
html``,
}),
createdAt: column({
render: ({ name }) =>
html``,
}),
}),
[], // dependency array (empty = compute once)
],
// Reactive query parameters. Recomputed when deps change,
// which triggers a new data fetch.
params: [() => ({ pathLocator: '/some/path' }), []],
// Data fetcher. Receives { params, page, pageSize }.
// Must return { items, total }.
list$: [
({ params }) =>
api(params).then((r) => ({
items: r.items ?? [],
total: r.metaData?.totalAvailable ?? 0,
})),
[api],
],
// Batch actions shown when items are selected
actions: [myAction()],
});
};
customElements.define(
'order-list-core',
component(OrderListCore, { styleSheets: [style] }),
);
```
### `listCore()` props
| Prop | Type | Description |
|---|---|---|
| `settingsId` | `string` | Persistence key for column settings |
| `exposedParts` | `string?` | CSS `exportparts` value |
| `noLocal` | `boolean` | Skip omnitable local filtering (default: `true`) |
| `columns` | `[() => Columns, deps[]]` | Column definitions (memoized) |
| `params` | `[(opts) => Params, deps[]]` | Query parameters (memoized). The `opts` include `{ filters, descending, sortOn, columns }` |
| `list$` | `[(props) => Promise<{ items, total }>, deps[]]` | Data fetcher. Receives `{ params, page, pageSize }` |
| `pageSize` | `number?` | Items per page for "load more" (default: `50`) |
| `actions` | `Action[]?` | Batch actions |
| `content` | `(opts) => Renderable` | Extra content inside the omnitable |
| `hashParam` | `string?` | URL hash param for omnitable state |
| `csvFilename` | `string?` | Filename for CSV export |
| `enabledColumns` | `string[]?` | Initially visible columns |
### `column()`
Type-safe column definition helper:
```ts
import { column } from '@neovici/cosmoz-queue/list';
const myColumn = column({
// Optional ordering hint
order: 1,
// Optional sort key
sort: 'name',
// Optional filter transform: receives the raw filter value,
// returns the value sent to the API
filter: (value: string) => ({ name: value }),
// Render function -- receives { name } where name is the object key
render: ({ name }) =>
html``,
});
```
### `useListCore()` / `useListCoreState()`
For cases where you need more control over the list without `renderListCore()`:
- **`useListCore(props)`** -- manages columns, params, data fetching, pagination, and form dialog state. Returns `{ data$, columns, loadMore, dialog, open, ...state }`.
- **`useListCoreState(defaults?)`** -- lower-level state: `filters`, `sortOn`, `descending`, `groupOn`, `selectedItems`, plus their setters and `setTotalAvailable`.
## Actions module
The `@neovici/cosmoz-queue/actions` entry point provides a declarative system for defining user actions that open form dialogs.
### Defining an action
```ts
import { action, Action, defaultButton } from '@neovici/cosmoz-queue/actions';
import { t } from 'i18next';
import { when } from 'lit-html/directives/when.js';
// action() is an identity function that provides type inference
export const approveOrder = action({
// Label shown on the button
title: () => t('Approve'),
// Optional: filter which items this action applies to.
// If provided, non-applicable items are excluded and the button
// shows a count badge like "Approve (3/5)".
applicable: (item) => item.status === 'pending',
// Optional: custom button renderer. Defaults to `defaultButton()`.
// Return `nothing` to hide the button conditionally.
button: (opts) =>
when(opts.items.length >= 1, () => defaultButton(opts)),
// Dialog configuration. Opens a `cosmoz-form` dialog.
// `items` contains only the applicable items.
dialog: ({ items, title }) => ({
heading: title,
description: t('Approve the selected orders'),
fields: [
// cosmoz-form field definitions
],
initial: {},
onSave: async (values) => {
await approveOrders(items.map((i) => i.id), values);
},
}),
});
```
### Rendering actions
Actions are rendered automatically by `listCore()` when passed as the `actions` prop. For manual rendering (e.g. in a detail view's bottom bar):
```ts
import { renderActions } from '@neovici/cosmoz-queue/actions';
import { approveOrder } from './actions';
// In a view component:
const bottomBar = renderActions({ items: [currentItem], open })([
approveOrder,
]);
```
### Action types
```ts
interface Action {
title: () => string;
applicable?: (item: TItem) => boolean;
button?: (opts: Action & ActionOpts) => unknown;
dialog: (opts: DialogOpts) => Dialogable | Promise;
}
interface ActionOpts {
items: TItem[];
open: (dialog: Dialogable) => void;
slot?: string;
}
```
## Advanced: composing with hooks
When `queue()` is too opinionated, use the individual hooks directly.
### `useQueue()` + `renderQueue()`
```ts
import { useQueue, renderQueue, renderNav } from '@neovici/cosmoz-queue';
const MyQueue = ({ heading }: { heading: string }) => {
const {
index, mobile, tabnav, items, setItems,
setSelected, setTotalAvailable, totalAvailable,
onItemClick, nav,
} = useQueue({
idHashParam: 'id',
tabHashParam: 'tab',
});
return renderQueue({
heading,
mobile,
index,
items,
tabnav,
totalAvailable,
nav,
list: html``,
renderItem: ({ item, nav }) =>
html`${nav}`,
renderLoader: ({ item, nav }) =>
html`${nav}`,
});
};
```
### Individual hooks
| Hook | Import | Purpose |
|---|---|---|
| `useTabs({ items, hashParam, mobile, fallback })` | `@neovici/cosmoz-queue` | Manages overview/split/queue tabs. Returns `{ tabnav, activeTab }`. |
| `useDataNav(items, opts)` | `@neovici/cosmoz-queue` | Item navigation with prev/next, URL hash sync. Returns `{ item, setItem, next, prev, forward, index }`. |
| `useSplit({ activeTab, ...splitOpts })` | `@neovici/cosmoz-queue` | Initializes Split.js when in split mode. |
| `useListState()` | `@neovici/cosmoz-queue` | Creates `items`, `selected`, `totalAvailable` state with setters. |
| `useListSSE({ entity, params, list$ })` | `@neovici/cosmoz-queue` | Subscribes to Server-Sent Events (`cosmoz-${entity}-updated`) for real-time list updates. |
| `useFetchActions({ pathLocator, selected, api })` | `@neovici/cosmoz-queue` | Fetches available actions for selected items from an API. Returns `{ actions, actionRows, actionsFetching }`. |
| `useAsyncAction(nav)` | `@neovici/cosmoz-queue` | Handles async action completion with automatic item removal from the list. Returns `{ listRef, onAsyncSimpleAction }`. |
| `usePagination()` | `@neovici/cosmoz-queue` | URL hash-based page state. Returns `{ page, onPage }`. |
## Utilities
### Fetch helpers (`@neovici/cosmoz-queue/util/fetch`)
Pre-configured `fetch` wrapper with CORS and credentials:
```ts
import {
fetch,
setBaseInit,
handleJSON,
RequestError,
} from '@neovici/cosmoz-queue/util/fetch';
// Configure default headers (call once at app startup)
setBaseInit({
headers: { 'X-Custom-Header': 'value' },
// Or use dynamic headers:
getHeaders: () => ({ Authorization: `Bearer ${getToken()}` }),
});
// fetch() includes mode: 'cors', credentials: 'include' by default
const response = await fetch('/api/orders');
const data = await handleJSON(response);
```
`RequestError` extends `Error` with `.response` and `.data` properties for structured error handling.
### `itemClick()`
Makes list cells clickable, dispatching `omnitable-item-click` events that the queue listens for:
```ts
import { itemClick } from '@neovici/cosmoz-queue';
// In an omnitable cell renderer:
html`
${item.title}
`;
```
The `activate` option specifies which tab to switch to. The queue picks the first non-disabled tab from the array.
### Other utilities
| Function | Description |
|---|---|
| `getItems(items, selected)` | Returns `selected` if non-empty, otherwise `items` |
| `touch(list, id)` | Forces an omnitable item refresh by replacing it with a shallow copy |
## Architecture
```
queue() factory
|
+---> useQueue() State orchestration
| |
| +---> useListState() items, selected, totalAvailable
| +---> useTabs() overview | split | queue
| +---> useDataNav() current item, prev/next, URL hash
| +---> useKeyNav() arrow key navigation
| +---> useSplit() Split.js initialization
| +---> useUpdates() list-item-remove events
|
+---> useAsyncAction() Post-action item removal
|
+---> renderQueue() Template composition
|
+---> cosmoz-tabs-next Tab bar (List / Split / Queue)
+---> renderStats() "3-5 of 120"
+---> renderPagination() Page prev/next
+--->
+---> list cosmoz-omnitable (user-provided)
+---> cosmoz-slider Animated detail view
+---> renderSlide() --> renderView()
|
+---> details() Async fetch
+---> renderItem or renderLoader
```
## Entry points
| Import path | Description |
|---|---|
| `@neovici/cosmoz-queue` | Main: `queue()`, `useQueue()`, `renderQueue()`, navigation hooks, SSE, utilities |
| `@neovici/cosmoz-queue/actions` | `action()`, `renderActions()`, `defaultButton()`, `actionCount()` |
| `@neovici/cosmoz-queue/list` | `listCore()`, `column()`, `useListCore()`, `useListCoreState()`, `renderListCore()` |
| `@neovici/cosmoz-queue/list/more` | `useMore()` -- progressive "load more" pagination |
| `@neovici/cosmoz-queue/list/more/render` | `renderLoadMore()` -- "Load more" button |
| `@neovici/cosmoz-queue/util/fetch` | `fetch()`, `setBaseInit()`, `handleJSON()`, `RequestError` |
## Deprecation notices
### `api` property → `details`
The `api` property on `useQueue()` / `queue()` is **deprecated** and will be
removed in v2.0.0. Use `details` instead:
```ts
// Before (deprecated)
queue({ api: (id, item) => apiUrl(`items/${id}`) })
// After
queue({ details: (item) => fetch(apiUrl(`items/${item.id}`)).then(r => r.json()) })
```
See [Migration guide](docs/migration-api-to-details.md) for more patterns.
## License
[Apache-2.0](LICENSE)