@vobs/ui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-shell-dom.d.ts +6 -0
- package/dist/app-shell-dom.js +373 -33
- package/dist/app-shell.d.ts +55 -19
- package/dist/app-shell.js +63 -18
- package/dist/base.css +1 -2
- package/dist/complex-inputs.js +7 -3
- package/dist/components.css +235 -47
- package/dist/data-display.d.ts +31 -0
- package/dist/data-display.js +72 -29
- package/dist/dialog.d.ts +26 -0
- package/dist/dialog.js +94 -0
- package/dist/forms.js +10 -6
- package/dist/index.d.ts +11 -2
- package/dist/index.js +16 -8
- package/dist/overlay.d.ts +4 -0
- package/dist/overlay.js +10 -5
- package/dist/page-state.d.ts +23 -0
- package/dist/page-state.js +96 -0
- package/dist/theme-data.d.ts +31 -0
- package/dist/theme-data.js +43 -0
- package/dist/theme.d.ts +19 -1
- package/dist/theme.js +72 -33
- package/dist/tokens.css +121 -0
- package/dist/tree.js +11 -5
- package/dist/virtual-list.js +28 -11
- package/package.json +17 -9
package/dist/data-display.js
CHANGED
|
@@ -1268,32 +1268,59 @@ export function createPaginationPlans(options) {
|
|
|
1268
1268
|
...(options.siblingCount === undefined ? {} : { siblingCount: options.siblingCount }),
|
|
1269
1269
|
...(options.boundaryCount === undefined ? {} : { boundaryCount: options.boundaryCount }),
|
|
1270
1270
|
});
|
|
1271
|
+
const root = createUiDomPlan('nav', 'pagination', {
|
|
1272
|
+
...(options.className === undefined ? {} : { className: options.className }),
|
|
1273
|
+
attrs: {
|
|
1274
|
+
id: options.id,
|
|
1275
|
+
'aria-label': options.label ?? 'Pagination',
|
|
1276
|
+
...options.attrs,
|
|
1277
|
+
'data-page': page,
|
|
1278
|
+
'data-page-count': pageCount,
|
|
1279
|
+
'data-total-items': normalizeCount(options.totalItems),
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
const list = createUiDomPlan('ol', 'pagination-list', { attrs: { role: 'list' } });
|
|
1283
|
+
const previous = createPaginationControl('previous', page <= 1 ? 1 : page - 1, page <= 1, options.previousLabel ?? 'Previous page');
|
|
1284
|
+
const pages = items.map((item, index) => createPaginationItemPlan(item, index, options.pageLabel));
|
|
1285
|
+
const next = createPaginationControl('next', page >= pageCount ? pageCount : page + 1, page >= pageCount, options.nextLabel ?? 'Next page');
|
|
1286
|
+
const summary = options.summary ??
|
|
1287
|
+
formatPaginationSummary({
|
|
1288
|
+
...options,
|
|
1289
|
+
page,
|
|
1290
|
+
pageCount,
|
|
1291
|
+
});
|
|
1292
|
+
const summaryPlan = createUiDomPlan('output', 'pagination-summary', {
|
|
1293
|
+
attrs: {
|
|
1294
|
+
'aria-live': 'polite',
|
|
1295
|
+
'data-summary': summary,
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1271
1298
|
return {
|
|
1272
|
-
root
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
}
|
|
1299
|
+
root,
|
|
1300
|
+
list,
|
|
1301
|
+
previous,
|
|
1302
|
+
pages,
|
|
1303
|
+
next,
|
|
1304
|
+
summary: summaryPlan,
|
|
1305
|
+
model: {
|
|
1306
|
+
rootId: typeof root.attrs.id === 'string' ? root.attrs.id : '',
|
|
1307
|
+
rootClassName: root.className,
|
|
1308
|
+
page,
|
|
1309
|
+
pageCount,
|
|
1310
|
+
totalItems: normalizeCount(options.totalItems) ?? 0,
|
|
1311
|
+
previousClassName: previous.className,
|
|
1312
|
+
previousLabel: typeof previous.attrs['aria-label'] === 'string' ? previous.attrs['aria-label'] : '',
|
|
1313
|
+
previousPage: typeof previous.attrs['data-page'] === 'number' ? previous.attrs['data-page'] : 1,
|
|
1314
|
+
previousDisabled: previous.attrs.disabled === true || previous.attrs.disabled === '',
|
|
1315
|
+
listClassName: list.className,
|
|
1316
|
+
pages: items.map((item, index) => createPaginationItemModel(item, index, pages[index], options.pageLabel)),
|
|
1317
|
+
nextClassName: next.className,
|
|
1318
|
+
nextLabel: typeof next.attrs['aria-label'] === 'string' ? next.attrs['aria-label'] : '',
|
|
1319
|
+
nextPage: typeof next.attrs['data-page'] === 'number' ? next.attrs['data-page'] : pageCount,
|
|
1320
|
+
nextDisabled: next.attrs.disabled === true || next.attrs.disabled === '',
|
|
1321
|
+
summaryClassName: summaryPlan.className,
|
|
1322
|
+
summary,
|
|
1323
|
+
},
|
|
1297
1324
|
};
|
|
1298
1325
|
}
|
|
1299
1326
|
export function createPaginationItems(options) {
|
|
@@ -1837,7 +1864,7 @@ function createPaginationControl(control, page, disabled, label) {
|
|
|
1837
1864
|
},
|
|
1838
1865
|
});
|
|
1839
1866
|
}
|
|
1840
|
-
function createPaginationItemPlan(item, index) {
|
|
1867
|
+
function createPaginationItemPlan(item, index, pageLabel) {
|
|
1841
1868
|
if (item.kind === 'ellipsis') {
|
|
1842
1869
|
return createUiDomPlan('span', 'pagination-ellipsis', {
|
|
1843
1870
|
attrs: {
|
|
@@ -1852,13 +1879,26 @@ function createPaginationItemPlan(item, index) {
|
|
|
1852
1879
|
state: item.current === true ? 'current' : 'idle',
|
|
1853
1880
|
attrs: {
|
|
1854
1881
|
type: 'button',
|
|
1855
|
-
'aria-label': `Page ${page}`,
|
|
1882
|
+
'aria-label': pageLabel?.(page) ?? `Page ${page}`,
|
|
1856
1883
|
'aria-current': item.current === true ? 'page' : undefined,
|
|
1857
1884
|
'data-kind': 'page',
|
|
1858
1885
|
'data-page': page,
|
|
1859
1886
|
},
|
|
1860
1887
|
});
|
|
1861
1888
|
}
|
|
1889
|
+
function createPaginationItemModel(item, index, plan, pageLabel) {
|
|
1890
|
+
const page = item.page ?? 1;
|
|
1891
|
+
const isPage = item.kind === 'page';
|
|
1892
|
+
return {
|
|
1893
|
+
id: String(index),
|
|
1894
|
+
className: plan?.className ?? '',
|
|
1895
|
+
kind: item.kind,
|
|
1896
|
+
page,
|
|
1897
|
+
state: item.kind === 'ellipsis' ? 'ellipsis' : item.current === true ? 'current' : 'idle',
|
|
1898
|
+
current: item.current === true ? 'page' : undefined,
|
|
1899
|
+
label: isPage ? (pageLabel?.(page) ?? `Page ${page}`) : '',
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1862
1902
|
function createNoteLikePlans(kind, options) {
|
|
1863
1903
|
const tone = options.tone ?? 'info';
|
|
1864
1904
|
return {
|
|
@@ -2045,9 +2085,11 @@ function normalizeColumnPreferenceIds(ids, knownIds) {
|
|
|
2045
2085
|
if (ids === undefined)
|
|
2046
2086
|
return [];
|
|
2047
2087
|
const result = [];
|
|
2088
|
+
const seen = new Set();
|
|
2048
2089
|
for (const id of ids) {
|
|
2049
|
-
if (!knownIds.has(id) ||
|
|
2090
|
+
if (!knownIds.has(id) || seen.has(id))
|
|
2050
2091
|
continue;
|
|
2092
|
+
seen.add(id);
|
|
2051
2093
|
result.push(id);
|
|
2052
2094
|
}
|
|
2053
2095
|
return result;
|
|
@@ -2056,10 +2098,11 @@ function orderTableColumns(columns, orderIds) {
|
|
|
2056
2098
|
if (orderIds.length === 0)
|
|
2057
2099
|
return [...columns];
|
|
2058
2100
|
const columnById = new Map(columns.map((column) => [column.id, column]));
|
|
2101
|
+
const orderedIdSet = new Set(orderIds);
|
|
2059
2102
|
const ordered = orderIds
|
|
2060
2103
|
.map((id) => columnById.get(id))
|
|
2061
2104
|
.filter((column) => column !== undefined);
|
|
2062
|
-
const remaining = columns.filter((column) => !
|
|
2105
|
+
const remaining = columns.filter((column) => !orderedIdSet.has(column.id));
|
|
2063
2106
|
return [...ordered, ...remaining];
|
|
2064
2107
|
}
|
|
2065
2108
|
function tableColumnPreferenceSticky(column, stickyStartIds, stickyEndIds) {
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/ui
|
|
4
|
+
*/
|
|
5
|
+
import { type ComponentProps, type ComponentSlotContract } from '@vobs/runtime-dom';
|
|
6
|
+
import { type OverlayCloseReason } from './overlay.js';
|
|
7
|
+
export type DialogSize = 'lg' | 'md' | 'sm';
|
|
8
|
+
export interface DialogProps {
|
|
9
|
+
readonly open: boolean;
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly size?: DialogSize;
|
|
12
|
+
readonly modal?: boolean;
|
|
13
|
+
readonly 'title-id'?: string;
|
|
14
|
+
readonly 'description-id'?: string;
|
|
15
|
+
readonly 'surface-class'?: string;
|
|
16
|
+
readonly [key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface DialogEvents extends Record<string, (...args: never[]) => unknown> {
|
|
19
|
+
readonly close: (reason: OverlayCloseReason) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface DialogScope {
|
|
22
|
+
readonly props: ComponentProps<DialogProps>;
|
|
23
|
+
readonly emit: <Name extends keyof DialogEvents & string>(name: Name, ...args: Parameters<DialogEvents[Name]>) => void;
|
|
24
|
+
}
|
|
25
|
+
export type DialogSlots = Readonly<ComponentSlotContract>;
|
|
26
|
+
export declare const Dialog: import("@vobs/runtime-core").ComponentDefinition<DialogProps, DialogEvents, DialogScope, Readonly<Readonly<Record<string, Readonly<Record<string, unknown>>>>>>;
|
package/dist/dialog.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/ui
|
|
4
|
+
*/
|
|
5
|
+
import { effect } from '@vobs/reactivity';
|
|
6
|
+
import { COMPONENT_SLOTS, createCompiledTemplate, defineComponent, } from '@vobs/runtime-dom';
|
|
7
|
+
import { createOverlayStack } from './overlay.js';
|
|
8
|
+
export const Dialog = defineComponent({
|
|
9
|
+
template: createCompiledTemplate({
|
|
10
|
+
templateId: 'k-dialog',
|
|
11
|
+
sourceId: '@vobs/ui/dialog',
|
|
12
|
+
rootTag: 'div',
|
|
13
|
+
mount(owner, scope, environment) {
|
|
14
|
+
return mountDialog(owner, scope, environment);
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
props: ['open', 'id', 'size', 'modal', 'title-id', 'description-id', 'surface-class'],
|
|
18
|
+
emits: ['close'],
|
|
19
|
+
[COMPONENT_SLOTS]: { default: {} },
|
|
20
|
+
setup(context) {
|
|
21
|
+
return {
|
|
22
|
+
props: context.props,
|
|
23
|
+
emit: (name, ...args) => context.emit(name, ...args),
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
function mountDialog(owner, scope, environment) {
|
|
28
|
+
const root = document.createElement('div');
|
|
29
|
+
root.className = 'kui-overlay-root';
|
|
30
|
+
root.setAttribute('data-backdrop', 'true');
|
|
31
|
+
root.setAttribute('role', 'presentation');
|
|
32
|
+
const surface = document.createElement('section');
|
|
33
|
+
surface.className = 'kui-dialog';
|
|
34
|
+
surface.setAttribute('role', 'dialog');
|
|
35
|
+
surface.setAttribute('aria-modal', 'true');
|
|
36
|
+
root.append(surface);
|
|
37
|
+
environment.slots?.default?.(owner, surface);
|
|
38
|
+
const stack = createOverlayStack({ document });
|
|
39
|
+
let handle;
|
|
40
|
+
let wasOpen = false;
|
|
41
|
+
const stop = effect(() => {
|
|
42
|
+
const props = scope.props;
|
|
43
|
+
const open = props.open.value === true;
|
|
44
|
+
const id = readString(props.id.value, 'dialog');
|
|
45
|
+
const size = readSize(props.size?.value);
|
|
46
|
+
const modal = props.modal?.value !== false;
|
|
47
|
+
root.hidden = !open;
|
|
48
|
+
root.setAttribute('data-state', open ? 'open' : 'closed');
|
|
49
|
+
surface.id = id;
|
|
50
|
+
surface.setAttribute('data-size', size);
|
|
51
|
+
surface.setAttribute('aria-modal', String(modal));
|
|
52
|
+
setOptionalAttribute(surface, 'aria-labelledby', props['title-id']?.value);
|
|
53
|
+
setOptionalAttribute(surface, 'aria-describedby', props['description-id']?.value);
|
|
54
|
+
const surfaceClass = readString(props['surface-class']?.value, '');
|
|
55
|
+
surface.className = `kui-dialog${surfaceClass === '' ? '' : ` ${surfaceClass}`}`;
|
|
56
|
+
if (open === wasOpen)
|
|
57
|
+
return;
|
|
58
|
+
wasOpen = open;
|
|
59
|
+
if (open) {
|
|
60
|
+
handle = stack.open({
|
|
61
|
+
id,
|
|
62
|
+
kind: 'dialog',
|
|
63
|
+
modal,
|
|
64
|
+
element: surface,
|
|
65
|
+
managedElement: false,
|
|
66
|
+
closeOnOutside: true,
|
|
67
|
+
onClose: (reason) => scope.emit('close', reason),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
handle?.destroy();
|
|
72
|
+
handle = undefined;
|
|
73
|
+
}
|
|
74
|
+
}, { scheduler: environment.scheduler, flush: 'post' });
|
|
75
|
+
owner.own(stop);
|
|
76
|
+
owner.own(() => {
|
|
77
|
+
handle?.destroy();
|
|
78
|
+
stack.destroy();
|
|
79
|
+
});
|
|
80
|
+
return { root, refs: {} };
|
|
81
|
+
}
|
|
82
|
+
function readString(value, fallback) {
|
|
83
|
+
return typeof value === 'string' && value.trim() !== '' ? value : fallback;
|
|
84
|
+
}
|
|
85
|
+
function readSize(value) {
|
|
86
|
+
return value === 'sm' || value === 'lg' ? value : 'md';
|
|
87
|
+
}
|
|
88
|
+
function setOptionalAttribute(element, name, value) {
|
|
89
|
+
const normalized = typeof value === 'string' ? value.trim() : '';
|
|
90
|
+
if (normalized === '')
|
|
91
|
+
element.removeAttribute(name);
|
|
92
|
+
else
|
|
93
|
+
element.setAttribute(name, normalized);
|
|
94
|
+
}
|
package/dist/forms.js
CHANGED
|
@@ -361,14 +361,16 @@ export function createChoiceGroupCardOptionStateContract(options) {
|
|
|
361
361
|
const descriptionIds = normalizeKnownIds(options.descriptionIds ??
|
|
362
362
|
options.options
|
|
363
363
|
.filter((option) => option.description !== undefined)
|
|
364
|
-
.map((option) => option.id), knownIds)
|
|
364
|
+
.map((option) => option.id), knownIds);
|
|
365
|
+
const optionById = new Map(options.options.map((option) => [option.id, option]));
|
|
366
|
+
const filteredDescriptionIds = descriptionIds.filter((id) => optionById.get(id)?.description !== undefined);
|
|
365
367
|
const actionIds = normalizeKnownIds(options.actionIds, knownIds);
|
|
366
368
|
const actionSet = new Set(actionIds);
|
|
367
369
|
const actionDisabledIds = normalizeKnownIds(options.actionDisabledIds, knownIds).filter((id) => actionSet.has(id));
|
|
368
370
|
const cardSet = new Set(cardIds);
|
|
369
371
|
const selectedEmphasisSet = new Set(selectedEmphasisIds);
|
|
370
372
|
const disabledEmphasisSet = new Set(disabledEmphasisIds);
|
|
371
|
-
const descriptionSet = new Set(
|
|
373
|
+
const descriptionSet = new Set(filteredDescriptionIds);
|
|
372
374
|
const actionDisabledSet = new Set(actionDisabledIds);
|
|
373
375
|
const state = options.options.length === 0
|
|
374
376
|
? 'empty'
|
|
@@ -431,13 +433,13 @@ export function createChoiceGroupCardOptionStateContract(options) {
|
|
|
431
433
|
cardCount: cardIds.length,
|
|
432
434
|
selectedEmphasisCount: selectedEmphasisIds.length,
|
|
433
435
|
disabledEmphasisCount: disabledEmphasisIds.length,
|
|
434
|
-
descriptionCount:
|
|
436
|
+
descriptionCount: filteredDescriptionIds.length,
|
|
435
437
|
actionCount: actionIds.length,
|
|
436
438
|
actionDisabledCount: actionDisabledIds.length,
|
|
437
439
|
cardIds,
|
|
438
440
|
selectedEmphasisIds,
|
|
439
441
|
disabledEmphasisIds,
|
|
440
|
-
descriptionIds,
|
|
442
|
+
descriptionIds: filteredDescriptionIds,
|
|
441
443
|
actionIds,
|
|
442
444
|
actionDisabledIds,
|
|
443
445
|
attrs: {
|
|
@@ -453,7 +455,7 @@ export function createChoiceGroupCardOptionStateContract(options) {
|
|
|
453
455
|
'data-card-ids': cardIds.join(','),
|
|
454
456
|
'data-selected-emphasis-ids': selectedEmphasisIds.join(','),
|
|
455
457
|
'data-disabled-emphasis-ids': disabledEmphasisIds.join(','),
|
|
456
|
-
'data-description-ids':
|
|
458
|
+
'data-description-ids': filteredDescriptionIds.join(','),
|
|
457
459
|
'data-action-ids': actionIds.join(','),
|
|
458
460
|
'data-action-disabled-ids': actionDisabledIds.join(','),
|
|
459
461
|
},
|
|
@@ -875,9 +877,11 @@ function normalizeKnownIds(value, knownIds) {
|
|
|
875
877
|
if (value === undefined)
|
|
876
878
|
return [];
|
|
877
879
|
const result = [];
|
|
880
|
+
const seen = new Set();
|
|
878
881
|
for (const id of value) {
|
|
879
|
-
if (!knownIds.has(id) ||
|
|
882
|
+
if (!knownIds.has(id) || seen.has(id))
|
|
880
883
|
continue;
|
|
884
|
+
seen.add(id);
|
|
881
885
|
result.push(id);
|
|
882
886
|
}
|
|
883
887
|
return result;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,18 +5,27 @@
|
|
|
5
5
|
import { type ComponentRegistry, type InjectionScope } from '@vobs/runtime-dom';
|
|
6
6
|
import { type AppShellConfigInput } from './app-shell.js';
|
|
7
7
|
import { type UiIconSource } from './icons.js';
|
|
8
|
+
export { PageState, type PageStateEvents, type PageStateProps, type PageStateScope, type PageStateStatus, } from './page-state.js';
|
|
8
9
|
export * from './theme.js';
|
|
9
10
|
export * from './icons.js';
|
|
10
11
|
export * from './calendar.js';
|
|
11
12
|
export * from './calendar-picker.js';
|
|
13
|
+
export * from './dialog.js';
|
|
12
14
|
export * from './workbench.js';
|
|
13
|
-
export { AppShell, UiAppShellEnvironmentKey, createAppShellRegistry, defineAppShell, defineAppShellConfig, provideUiAppShells, resolveActiveAppShellNavItemId, resolveAppShellState, resolveAppShellStatus, type AppShellAccountConfig, type AppShellAccountMenuItem, type AppShellAccountMenuItemKind, type AppShellAccountMenuItemTone, type AppShellAction, type AppShellBrandConfig, type AppShellConfigInput, type AppShellContentMode, type AppShellDefinition, type AppShellDefinitionRecord, type AppShellNavigationConfig, type AppShellNavItem, type AppShellNavItemTone, type AppShellPageHeaderConfig, type AppShellProps, type AppShellRegistry, type AppShellRouteConfig, type AppShellSidebarMode, type AppShellStatusbarConfig, type AppShellThemeIconConfig, type AppShellThemeConfig, type AppShellTopbarConfig, type ResolvedAppShellState, type ResolvedAppShellStatus, } from './app-shell.js';
|
|
15
|
+
export { AppShell, UiAppShellEnvironmentKey, createAppShellRegistry, defineAppShell, defineAppShellConfig, provideUiAppShells, resolveActiveAppShellNavItemId, resolveAppShellState, resolveAppShellStatus, type AppShellAccountConfig, type AppShellAccountMenuItem, type AppShellAccountMenuItemKind, type AppShellAccountMenuItemTone, type AppShellAction, type AppShellBrandConfig, type AppShellConfigInput, type AppShellContentMode, type AppShellDefinition, type AppShellDefinitionRecord, type AppShellLanguageConfig, type AppShellLanguageOption, type AppShellNavigationConfig, type AppShellNavItem, type AppShellNavItemTone, type AppShellPageHeaderConfig, type AppShellProps, type AppShellRegistry, type AppShellRouteConfig, type AppShellRoutePageHeaderConfig, type AppShellSidebarMode, type AppShellStatusbarConfig, type AppShellThemeIconConfig, type AppShellThemeConfig, type AppShellText, type AppShellTextConfig, type AppShellTopbarConfig, type ResolvedAppShellPageHeader, type ResolvedAppShellState, type ResolvedAppShellStatus, resolveAppShellText, } from './app-shell.js';
|
|
14
16
|
export interface SetupUiOptions {
|
|
15
17
|
readonly icons?: true | UiIconSource;
|
|
16
18
|
readonly appShells?: AppShellConfigInput;
|
|
17
19
|
}
|
|
20
|
+
declare const uiComponents: {
|
|
21
|
+
'k-app-shell': ComponentRegistry[string];
|
|
22
|
+
'k-icon': ComponentRegistry[string];
|
|
23
|
+
'k-calendar-picker': ComponentRegistry[string];
|
|
24
|
+
'k-dialog': ComponentRegistry[string];
|
|
25
|
+
'k-page-state': ComponentRegistry[string];
|
|
26
|
+
};
|
|
18
27
|
export interface UiSetupEnvironment {
|
|
19
|
-
readonly components:
|
|
28
|
+
readonly components: typeof uiComponents;
|
|
20
29
|
readonly injections?: InjectionScope;
|
|
21
30
|
}
|
|
22
31
|
export declare function setupUi(options?: SetupUiOptions): UiSetupEnvironment;
|
package/dist/index.js
CHANGED
|
@@ -5,19 +5,25 @@
|
|
|
5
5
|
import { createInjectionScope, } from '@vobs/runtime-dom';
|
|
6
6
|
import { AppShell, provideUiAppShells } from './app-shell.js';
|
|
7
7
|
import { CalendarPicker } from './calendar-picker.js';
|
|
8
|
+
import { Dialog } from './dialog.js';
|
|
8
9
|
import { Icon, provideUiIcons } from './icons.js';
|
|
10
|
+
import { PageState } from './page-state.js';
|
|
11
|
+
export { PageState, } from './page-state.js';
|
|
9
12
|
export * from './theme.js';
|
|
10
13
|
export * from './icons.js';
|
|
11
14
|
export * from './calendar.js';
|
|
12
15
|
export * from './calendar-picker.js';
|
|
16
|
+
export * from './dialog.js';
|
|
13
17
|
export * from './workbench.js';
|
|
14
|
-
export { AppShell, UiAppShellEnvironmentKey, createAppShellRegistry, defineAppShell, defineAppShellConfig, provideUiAppShells, resolveActiveAppShellNavItemId, resolveAppShellState, resolveAppShellStatus, } from './app-shell.js';
|
|
18
|
+
export { AppShell, UiAppShellEnvironmentKey, createAppShellRegistry, defineAppShell, defineAppShellConfig, provideUiAppShells, resolveActiveAppShellNavItemId, resolveAppShellState, resolveAppShellStatus, resolveAppShellText, } from './app-shell.js';
|
|
19
|
+
const uiComponents = {
|
|
20
|
+
'k-app-shell': AppShell,
|
|
21
|
+
'k-icon': Icon,
|
|
22
|
+
'k-calendar-picker': CalendarPicker,
|
|
23
|
+
'k-dialog': Dialog,
|
|
24
|
+
'k-page-state': PageState,
|
|
25
|
+
};
|
|
15
26
|
export function setupUi(options = {}) {
|
|
16
|
-
const components = {
|
|
17
|
-
'k-app-shell': AppShell,
|
|
18
|
-
'k-icon': Icon,
|
|
19
|
-
'k-calendar-picker': CalendarPicker,
|
|
20
|
-
};
|
|
21
27
|
const injections = createInjectionScope();
|
|
22
28
|
if (options.icons !== undefined && options.icons !== true) {
|
|
23
29
|
provideUiIcons(injections, options.icons);
|
|
@@ -26,7 +32,9 @@ export function setupUi(options = {}) {
|
|
|
26
32
|
provideUiAppShells(injections, options.appShells);
|
|
27
33
|
}
|
|
28
34
|
if (options.icons === undefined || options.icons === true) {
|
|
29
|
-
return options.appShells === undefined
|
|
35
|
+
return options.appShells === undefined
|
|
36
|
+
? { components: uiComponents }
|
|
37
|
+
: { components: uiComponents, injections };
|
|
30
38
|
}
|
|
31
|
-
return { components, injections };
|
|
39
|
+
return { components: uiComponents, injections };
|
|
32
40
|
}
|
package/dist/overlay.d.ts
CHANGED
|
@@ -30,6 +30,10 @@ export interface OpenOverlayOptions {
|
|
|
30
30
|
readonly closeOnOutside?: boolean;
|
|
31
31
|
readonly element?: HTMLElement;
|
|
32
32
|
readonly initialFocus?: HTMLElement;
|
|
33
|
+
readonly autoFocus?: boolean;
|
|
34
|
+
readonly restoreFocusTarget?: HTMLElement;
|
|
35
|
+
/** Keep a caller-owned element in its current DOM position and leave it mounted on destroy. */
|
|
36
|
+
readonly managedElement?: boolean;
|
|
33
37
|
readonly onClose?: (reason: OverlayCloseReason) => void;
|
|
34
38
|
}
|
|
35
39
|
export interface OverlayHandle {
|
package/dist/overlay.js
CHANGED
|
@@ -47,13 +47,15 @@ export function createOverlayStack(options) {
|
|
|
47
47
|
open(openOptions) {
|
|
48
48
|
const previouslyFocused = activeHtmlElement(options.document);
|
|
49
49
|
const modal = openOptions.modal ?? (openOptions.kind === undefined || openOptions.kind === 'dialog');
|
|
50
|
-
const entry = createEntry(openOptions, modal, entries.length + 1, previouslyFocused);
|
|
50
|
+
const entry = createEntry(openOptions, modal, entries.length + 1, openOptions.restoreFocusTarget ?? previouslyFocused);
|
|
51
51
|
entries.push(entry);
|
|
52
|
-
if (portalTarget !== undefined &&
|
|
52
|
+
if (portalTarget !== undefined &&
|
|
53
|
+
entry.element !== undefined &&
|
|
54
|
+
openOptions.managedElement !== false)
|
|
53
55
|
portalTarget.append(entry.element);
|
|
54
56
|
if (modal)
|
|
55
57
|
lockScroll();
|
|
56
|
-
focusInitialElement(openOptions.initialFocus, entry.element);
|
|
58
|
+
focusInitialElement(openOptions.initialFocus, entry.element, openOptions.autoFocus === true);
|
|
57
59
|
return entry.handle;
|
|
58
60
|
},
|
|
59
61
|
destroy() {
|
|
@@ -98,7 +100,8 @@ export function createOverlayStack(options) {
|
|
|
98
100
|
entries.splice(index, 1);
|
|
99
101
|
if (modal)
|
|
100
102
|
unlockScroll();
|
|
101
|
-
openOptions.
|
|
103
|
+
if (openOptions.managedElement !== false)
|
|
104
|
+
openOptions.element?.remove();
|
|
102
105
|
if (openOptions.restoreFocus !== false)
|
|
103
106
|
previouslyFocused?.focus();
|
|
104
107
|
},
|
|
@@ -617,11 +620,13 @@ function activeHtmlElement(document) {
|
|
|
617
620
|
const active = document.activeElement;
|
|
618
621
|
return isHTMLElement(active) ? active : undefined;
|
|
619
622
|
}
|
|
620
|
-
function focusInitialElement(initialFocus, element) {
|
|
623
|
+
function focusInitialElement(initialFocus, element, autoFocus) {
|
|
621
624
|
if (initialFocus !== undefined) {
|
|
622
625
|
initialFocus.focus();
|
|
623
626
|
return;
|
|
624
627
|
}
|
|
628
|
+
if (!autoFocus)
|
|
629
|
+
return;
|
|
625
630
|
const target = element === undefined ? undefined : (firstFocusable(element) ?? element);
|
|
626
631
|
target?.focus();
|
|
627
632
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/ui
|
|
4
|
+
*/
|
|
5
|
+
import { type ComponentProps } from '@vobs/runtime-dom';
|
|
6
|
+
export type PageStateStatus = 'loading' | 'ready' | 'empty' | 'error' | 'submitting';
|
|
7
|
+
export interface PageStateProps {
|
|
8
|
+
readonly state: PageStateStatus;
|
|
9
|
+
readonly 'loading-label'?: string;
|
|
10
|
+
readonly 'error-title'?: string;
|
|
11
|
+
readonly 'error-description'?: string;
|
|
12
|
+
readonly 'error-message'?: string;
|
|
13
|
+
readonly 'retry-label'?: string;
|
|
14
|
+
readonly [key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface PageStateEvents extends Record<string, (...args: never[]) => unknown> {
|
|
17
|
+
readonly retry: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface PageStateScope {
|
|
20
|
+
readonly props: ComponentProps<PageStateProps>;
|
|
21
|
+
readonly emit: <Name extends keyof PageStateEvents & string>(name: Name, ...args: Parameters<PageStateEvents[Name]>) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare const PageState: import("@vobs/runtime-core").ComponentDefinition<PageStateProps, PageStateEvents, PageStateScope, Readonly<Record<string, Readonly<Record<string, unknown>>>>>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/ui
|
|
4
|
+
*/
|
|
5
|
+
import { effect } from '@vobs/reactivity';
|
|
6
|
+
import { createCompiledTemplate, defineComponent, } from '@vobs/runtime-dom';
|
|
7
|
+
export const PageState = defineComponent({
|
|
8
|
+
template: createCompiledTemplate({
|
|
9
|
+
templateId: 'k-page-state',
|
|
10
|
+
sourceId: '@vobs/ui/page-state',
|
|
11
|
+
rootTag: 'section',
|
|
12
|
+
mount(owner, scope, environment) {
|
|
13
|
+
return mountPageState(owner, scope, environment);
|
|
14
|
+
},
|
|
15
|
+
hydrate(owner, root, scope, environment) {
|
|
16
|
+
return mountPageState(owner, scope, environment, root);
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
props: [
|
|
20
|
+
'state',
|
|
21
|
+
'loading-label',
|
|
22
|
+
'error-title',
|
|
23
|
+
'error-description',
|
|
24
|
+
'error-message',
|
|
25
|
+
'retry-label',
|
|
26
|
+
],
|
|
27
|
+
emits: ['retry'],
|
|
28
|
+
setup(context) {
|
|
29
|
+
return {
|
|
30
|
+
props: context.props,
|
|
31
|
+
emit: (name, ...args) => context.emit(name, ...args),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
function mountPageState(owner, scope, environment, existingRoot) {
|
|
36
|
+
const root = existingRoot ?? document.createElement('section');
|
|
37
|
+
root.className = 'kui-page-state';
|
|
38
|
+
root.setAttribute('role', 'status');
|
|
39
|
+
root.setAttribute('aria-live', 'polite');
|
|
40
|
+
const stop = effect(() => {
|
|
41
|
+
renderPageState(root, scope);
|
|
42
|
+
}, { scheduler: environment.scheduler });
|
|
43
|
+
owner.own(stop);
|
|
44
|
+
return { root, refs: {} };
|
|
45
|
+
}
|
|
46
|
+
function renderPageState(root, scope) {
|
|
47
|
+
const state = readState(scope.props.state?.value);
|
|
48
|
+
const isError = state === 'error';
|
|
49
|
+
const isBlocking = state === 'loading' || isError;
|
|
50
|
+
root.hidden = !isBlocking;
|
|
51
|
+
root.setAttribute('data-state', state);
|
|
52
|
+
root.replaceChildren();
|
|
53
|
+
if (!isBlocking)
|
|
54
|
+
return;
|
|
55
|
+
const mark = document.createElement('span');
|
|
56
|
+
mark.className = 'kui-page-state-mark';
|
|
57
|
+
mark.setAttribute('aria-hidden', 'true');
|
|
58
|
+
mark.textContent = isError ? '!' : '...';
|
|
59
|
+
root.append(mark);
|
|
60
|
+
if (!isError) {
|
|
61
|
+
const loading = document.createElement('p');
|
|
62
|
+
loading.className = 'kui-page-state-loading';
|
|
63
|
+
loading.textContent = readString(scope.props['loading-label']?.value, 'Loading...');
|
|
64
|
+
root.append(loading);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const error = document.createElement('div');
|
|
68
|
+
error.className = 'kui-page-state-error';
|
|
69
|
+
const title = document.createElement('h2');
|
|
70
|
+
title.textContent = readString(scope.props['error-title']?.value, 'Unable to load');
|
|
71
|
+
const description = document.createElement('p');
|
|
72
|
+
description.textContent = readString(scope.props['error-description']?.value, 'Try again to reload this page.');
|
|
73
|
+
error.append(title, description);
|
|
74
|
+
const message = readString(scope.props['error-message']?.value, '');
|
|
75
|
+
if (message !== '') {
|
|
76
|
+
const detail = document.createElement('p');
|
|
77
|
+
detail.className = 'kui-page-state-message';
|
|
78
|
+
detail.textContent = message;
|
|
79
|
+
error.append(detail);
|
|
80
|
+
}
|
|
81
|
+
const retry = document.createElement('button');
|
|
82
|
+
retry.type = 'button';
|
|
83
|
+
retry.className = 'kui-button kui-button--solid kui-button--md';
|
|
84
|
+
retry.textContent = readString(scope.props['retry-label']?.value, 'Try again');
|
|
85
|
+
retry.addEventListener('click', () => scope.emit('retry'));
|
|
86
|
+
error.append(retry);
|
|
87
|
+
root.append(error);
|
|
88
|
+
}
|
|
89
|
+
function readState(value) {
|
|
90
|
+
return value === 'ready' || value === 'empty' || value === 'error' || value === 'submitting'
|
|
91
|
+
? value
|
|
92
|
+
: 'loading';
|
|
93
|
+
}
|
|
94
|
+
function readString(value, fallback) {
|
|
95
|
+
return typeof value === 'string' && value.trim() !== '' ? value : fallback;
|
|
96
|
+
}
|