@kouji-ui/core 0.0.4 → 0.1.2
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/fesm2022/kouji-ui-core.mjs +26789 -0
- package/fesm2022/kouji-ui-core.mjs.map +1 -0
- package/icon/icon.css +40 -0
- package/package.json +20 -9
- package/types/kouji-ui-core.d.ts +13545 -0
- package/typography/prose.css +312 -0
- package/.turbo/turbo-build.log +0 -27
- package/CHANGELOG.md +0 -21
- package/ng-package.json +0 -7
- package/src/a11y/.gitkeep +0 -0
- package/src/a11y/aria-describedby.spec.ts +0 -55
- package/src/a11y/aria-describedby.ts +0 -33
- package/src/a11y/focus-trap.spec.ts +0 -86
- package/src/a11y/focus-trap.ts +0 -70
- package/src/a11y/index.ts +0 -9
- package/src/a11y/live-region.spec.ts +0 -58
- package/src/a11y/live-region.ts +0 -48
- package/src/a11y/roving-tabindex.spec.ts +0 -62
- package/src/a11y/roving-tabindex.ts +0 -104
- package/src/a11y/visually-hidden.spec.ts +0 -34
- package/src/a11y/visually-hidden.ts +0 -24
- package/src/accordion/accordion.context.ts +0 -22
- package/src/accordion/accordion.spec.ts +0 -32
- package/src/accordion/accordion.ts +0 -129
- package/src/accordion/index.ts +0 -2
- package/src/avatar/avatar.spec.ts +0 -18
- package/src/avatar/avatar.ts +0 -26
- package/src/avatar/index.ts +0 -1
- package/src/badge/badge.spec.ts +0 -20
- package/src/badge/badge.ts +0 -14
- package/src/badge/index.ts +0 -1
- package/src/button/button.example.ts +0 -30
- package/src/button/button.finance.example.ts +0 -37
- package/src/button/button.retro.example.ts +0 -47
- package/src/button/button.sizes.example.ts +0 -25
- package/src/button/button.spec.ts +0 -32
- package/src/button/button.ts +0 -44
- package/src/button/index.ts +0 -1
- package/src/chart/chart.spec.ts +0 -27
- package/src/chart/chart.ts +0 -48
- package/src/chart/index.ts +0 -1
- package/src/checkbox/checkbox.spec.ts +0 -35
- package/src/checkbox/checkbox.ts +0 -82
- package/src/checkbox/index.ts +0 -1
- package/src/dialog/dialog.confirm.example.ts +0 -57
- package/src/dialog/dialog.context.ts +0 -11
- package/src/dialog/dialog.example.ts +0 -40
- package/src/dialog/dialog.finance.example.ts +0 -47
- package/src/dialog/dialog.retro.example.ts +0 -46
- package/src/dialog/dialog.service.ts +0 -176
- package/src/dialog/dialog.spec.ts +0 -122
- package/src/dialog/dialog.ts +0 -201
- package/src/dialog/index.ts +0 -9
- package/src/example-components.ts +0 -33
- package/src/form/form.context.ts +0 -3
- package/src/form/form.spec.ts +0 -25
- package/src/form/form.ts +0 -46
- package/src/form/index.ts +0 -2
- package/src/input/index.ts +0 -1
- package/src/input/input.spec.ts +0 -43
- package/src/input/input.ts +0 -80
- package/src/menu/index.ts +0 -2
- package/src/menu/menu.context.ts +0 -10
- package/src/menu/menu.example.ts +0 -39
- package/src/menu/menu.spec.ts +0 -168
- package/src/menu/menu.ts +0 -176
- package/src/popover/index.ts +0 -12
- package/src/popover/popover.context.ts +0 -18
- package/src/popover/popover.example.ts +0 -50
- package/src/popover/popover.finance.example.ts +0 -52
- package/src/popover/popover.retro.example.ts +0 -53
- package/src/popover/popover.spec.ts +0 -52
- package/src/popover/popover.ts +0 -124
- package/src/primitives/.gitkeep +0 -0
- package/src/primitives/forms/form-control.spec.ts +0 -59
- package/src/primitives/forms/form-control.ts +0 -90
- package/src/primitives/forms/index.ts +0 -1
- package/src/primitives/index.ts +0 -3
- package/src/primitives/interaction/disabled.spec.ts +0 -47
- package/src/primitives/interaction/disabled.ts +0 -24
- package/src/primitives/interaction/focus-ring.spec.ts +0 -62
- package/src/primitives/interaction/focus-ring.ts +0 -57
- package/src/primitives/interaction/index.ts +0 -2
- package/src/primitives/overlay/index.ts +0 -1
- package/src/primitives/overlay/overlay.spec.ts +0 -90
- package/src/primitives/overlay/overlay.ts +0 -106
- package/src/public-api.ts +0 -35
- package/src/radio/index.ts +0 -2
- package/src/radio/radio.context.ts +0 -3
- package/src/radio/radio.spec.ts +0 -36
- package/src/radio/radio.ts +0 -55
- package/src/select/index.ts +0 -2
- package/src/select/select.context.ts +0 -3
- package/src/select/select.spec.ts +0 -145
- package/src/select/select.ts +0 -250
- package/src/styles/docs-themes.css +0 -82
- package/src/table/index.ts +0 -1
- package/src/table/table.spec.ts +0 -113
- package/src/table/table.ts +0 -126
- package/src/tabs/index.ts +0 -2
- package/src/tabs/tabs.context.ts +0 -9
- package/src/tabs/tabs.spec.ts +0 -40
- package/src/tabs/tabs.ts +0 -130
- package/src/test-setup.ts +0 -5
- package/src/toast/index.ts +0 -21
- package/src/toast/toast.example.ts +0 -134
- package/src/toast/toast.finance.example.ts +0 -68
- package/src/toast/toast.retro.example.ts +0 -69
- package/src/toast/toast.service.ts +0 -153
- package/src/toast/toast.spec.ts +0 -95
- package/src/toast/toast.strategy.ts +0 -102
- package/src/toast/toast.ts +0 -307
- package/src/toast/toast.types.ts +0 -5
- package/src/toggle/index.ts +0 -1
- package/src/toggle/toggle.spec.ts +0 -24
- package/src/toggle/toggle.ts +0 -40
- package/src/tooltip/index.ts +0 -5
- package/src/tooltip/tooltip.example.ts +0 -46
- package/src/tooltip/tooltip.finance.example.ts +0 -54
- package/src/tooltip/tooltip.placements.example.ts +0 -56
- package/src/tooltip/tooltip.retro.example.ts +0 -65
- package/src/tooltip/tooltip.spec.ts +0 -104
- package/src/tooltip/tooltip.ts +0 -129
- package/tsconfig.lib.json +0 -13
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -10
- package/vite.config.ts +0 -19
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/* ─────────────────────────────────────────────────────────────
|
|
2
|
-
kouji-ui docs example themes
|
|
3
|
-
Three theme layers: kj.tokens (defaults) + kj.theme (overrides)
|
|
4
|
-
──────────────────────────────────────────────────────────── */
|
|
5
|
-
|
|
6
|
-
@layer kj.tokens, kj.theme;
|
|
7
|
-
|
|
8
|
-
/* ── Default (dark) tokens — applied to :root and .kj-theme-default ── */
|
|
9
|
-
@layer kj.tokens {
|
|
10
|
-
:root,
|
|
11
|
-
.kj-theme-default {
|
|
12
|
-
--kj-bg: #0c0c0c;
|
|
13
|
-
--kj-surface: #1a1a1a;
|
|
14
|
-
--kj-text: #f0ede6;
|
|
15
|
-
--kj-text-muted: #888;
|
|
16
|
-
--kj-border: #333;
|
|
17
|
-
--kj-accent: #b8f500;
|
|
18
|
-
--kj-accent-on: #0c0c0c;
|
|
19
|
-
--kj-destructive: #ef4444;
|
|
20
|
-
--kj-radius-sm: 0px;
|
|
21
|
-
--kj-radius-md: 0px;
|
|
22
|
-
--kj-radius-lg: 0px;
|
|
23
|
-
--kj-shadow-sm: none;
|
|
24
|
-
--kj-shadow-md: none;
|
|
25
|
-
--kj-shadow-hard: none;
|
|
26
|
-
--kj-font: 'JetBrains Mono', monospace;
|
|
27
|
-
--kj-btn-border: none;
|
|
28
|
-
--kj-btn-outline-border: 1px solid rgba(240, 237, 230, 0.6);
|
|
29
|
-
--kj-btn-ghost-color: rgba(240, 237, 230, 0.55);
|
|
30
|
-
--kj-backdrop: rgba(0, 0, 0, 0.75);
|
|
31
|
-
--kj-transition: opacity 0.15s;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/* ── Theme overrides ── */
|
|
36
|
-
@layer kj.theme {
|
|
37
|
-
.kj-theme-retro {
|
|
38
|
-
--kj-bg: #fef9c3;
|
|
39
|
-
--kj-surface: #fef9c3;
|
|
40
|
-
--kj-text: #000;
|
|
41
|
-
--kj-text-muted: #444;
|
|
42
|
-
--kj-border: #000;
|
|
43
|
-
--kj-accent: #16a34a;
|
|
44
|
-
--kj-accent-on: #fff;
|
|
45
|
-
--kj-destructive: #dc2626;
|
|
46
|
-
--kj-radius-sm: 0px;
|
|
47
|
-
--kj-radius-md: 0px;
|
|
48
|
-
--kj-radius-lg: 0px;
|
|
49
|
-
--kj-shadow-sm: 2px 2px 0 #000;
|
|
50
|
-
--kj-shadow-md: 4px 4px 0 #000;
|
|
51
|
-
--kj-shadow-hard: 6px 6px 0 #000;
|
|
52
|
-
--kj-font: 'Courier New', monospace;
|
|
53
|
-
--kj-btn-border: 2px solid #000;
|
|
54
|
-
--kj-btn-outline-border: 2px solid #000;
|
|
55
|
-
--kj-btn-ghost-color: #555;
|
|
56
|
-
--kj-backdrop: rgba(0, 0, 0, 0.6);
|
|
57
|
-
--kj-transition: transform 0.08s, box-shadow 0.08s;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.kj-theme-finance {
|
|
61
|
-
--kj-bg: #f9fafb;
|
|
62
|
-
--kj-surface: #fff;
|
|
63
|
-
--kj-text: #111827;
|
|
64
|
-
--kj-text-muted: #6b7280;
|
|
65
|
-
--kj-border: #e5e7eb;
|
|
66
|
-
--kj-accent: #3b82f6;
|
|
67
|
-
--kj-accent-on: #fff;
|
|
68
|
-
--kj-destructive: #ef4444;
|
|
69
|
-
--kj-radius-sm: 4px;
|
|
70
|
-
--kj-radius-md: 6px;
|
|
71
|
-
--kj-radius-lg: 12px;
|
|
72
|
-
--kj-shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
73
|
-
--kj-shadow-md: 0 8px 30px rgba(0, 0, 0, 0.1);
|
|
74
|
-
--kj-shadow-hard: 0 20px 60px rgba(0, 0, 0, 0.12);
|
|
75
|
-
--kj-font: system-ui, -apple-system, sans-serif;
|
|
76
|
-
--kj-btn-border: 1px solid transparent;
|
|
77
|
-
--kj-btn-outline-border: 1px solid #6b7280;
|
|
78
|
-
--kj-btn-ghost-color: #374151;
|
|
79
|
-
--kj-backdrop: rgba(0, 0, 0, 0.4);
|
|
80
|
-
--kj-transition: background 0.12s;
|
|
81
|
-
}
|
|
82
|
-
}
|
package/src/table/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { KjTable, KjTableHeader, KJ_TABLE, type KjSortDirection } from './table';
|
package/src/table/table.spec.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { Component } from '@angular/core';
|
|
2
|
-
import { ColumnDef } from '@tanstack/angular-table';
|
|
3
|
-
import { render } from '@testing-library/angular';
|
|
4
|
-
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
5
|
-
import { KjTable, KjTableHeader } from './table';
|
|
6
|
-
|
|
7
|
-
expect.extend(toHaveNoViolations);
|
|
8
|
-
|
|
9
|
-
interface User { name: string; age: number; }
|
|
10
|
-
|
|
11
|
-
@Component({
|
|
12
|
-
standalone: true,
|
|
13
|
-
imports: [KjTable, KjTableHeader],
|
|
14
|
-
template: `
|
|
15
|
-
<table [kjTable]="columns" [kjTableData]="data" #tbl="kjTable">
|
|
16
|
-
<thead>
|
|
17
|
-
@for (hg of tbl.table().getHeaderGroups(); track hg.id) {
|
|
18
|
-
<tr>
|
|
19
|
-
@for (h of hg.headers; track h.id) {
|
|
20
|
-
<th kjTableHeader [kjHeader]="h" scope="col">{{ h.id }}</th>
|
|
21
|
-
}
|
|
22
|
-
</tr>
|
|
23
|
-
}
|
|
24
|
-
</thead>
|
|
25
|
-
<tbody>
|
|
26
|
-
@for (row of tbl.table().getRowModel().rows; track row.id) {
|
|
27
|
-
<tr>
|
|
28
|
-
@for (cell of row.getVisibleCells(); track cell.id) {
|
|
29
|
-
<td>{{ cell.getValue() }}</td>
|
|
30
|
-
}
|
|
31
|
-
</tr>
|
|
32
|
-
}
|
|
33
|
-
</tbody>
|
|
34
|
-
</table>`,
|
|
35
|
-
})
|
|
36
|
-
class TableTestComponent {
|
|
37
|
-
columns: ColumnDef<User>[] = [
|
|
38
|
-
{ accessorKey: 'name', header: 'Name' },
|
|
39
|
-
{ accessorKey: 'age', header: 'Age' },
|
|
40
|
-
];
|
|
41
|
-
data: User[] = [
|
|
42
|
-
{ name: 'Alice', age: 30 },
|
|
43
|
-
{ name: 'Bob', age: 25 },
|
|
44
|
-
];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe('KjTable', () => {
|
|
48
|
-
it('renders table headers', async () => {
|
|
49
|
-
const { container } = await render(TableTestComponent);
|
|
50
|
-
const headers = container.querySelectorAll('th');
|
|
51
|
-
expect(headers.length).toBe(2);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('renders data rows', async () => {
|
|
55
|
-
const { container } = await render(TableTestComponent);
|
|
56
|
-
const rows = container.querySelectorAll('tbody tr');
|
|
57
|
-
expect(rows.length).toBe(2);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('renders cell data', async () => {
|
|
61
|
-
const { getByText } = await render(TableTestComponent);
|
|
62
|
-
expect(getByText('Alice')).toBeInTheDocument();
|
|
63
|
-
expect(getByText('Bob')).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('passes axe audit', async () => {
|
|
67
|
-
const { container } = await render(TableTestComponent);
|
|
68
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('sortable header has tabindex=0', async () => {
|
|
72
|
-
const { container } = await render(TableTestComponent);
|
|
73
|
-
const ths = container.querySelectorAll('th');
|
|
74
|
-
ths.forEach(th => expect(th).toHaveAttribute('tabindex', '0'));
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('sortable header has aria-sort="none" initially', async () => {
|
|
78
|
-
const { container } = await render(TableTestComponent);
|
|
79
|
-
const ths = container.querySelectorAll('th');
|
|
80
|
-
ths.forEach(th => expect(th).toHaveAttribute('aria-sort', 'none'));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('non-sortable column header has no cursor:pointer', async () => {
|
|
84
|
-
@Component({
|
|
85
|
-
standalone: true,
|
|
86
|
-
imports: [KjTable, KjTableHeader],
|
|
87
|
-
template: `
|
|
88
|
-
<table [kjTable]="columns" [kjTableData]="data" #tbl="kjTable">
|
|
89
|
-
<thead>
|
|
90
|
-
@for (hg of tbl.table().getHeaderGroups(); track hg.id) {
|
|
91
|
-
<tr>
|
|
92
|
-
@for (h of hg.headers; track h.id) {
|
|
93
|
-
<th kjTableHeader [kjHeader]="h" scope="col">{{ h.id }}</th>
|
|
94
|
-
}
|
|
95
|
-
</tr>
|
|
96
|
-
}
|
|
97
|
-
</thead>
|
|
98
|
-
<tbody></tbody>
|
|
99
|
-
</table>`,
|
|
100
|
-
})
|
|
101
|
-
class NoSortTableComponent {
|
|
102
|
-
columns: ColumnDef<User>[] = [
|
|
103
|
-
{ accessorKey: 'name', header: 'Name', enableSorting: false },
|
|
104
|
-
];
|
|
105
|
-
data: User[] = [{ name: 'Alice', age: 30 }];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const { container } = await render(NoSortTableComponent);
|
|
109
|
-
const th = container.querySelector('th')!;
|
|
110
|
-
expect(th.style.cursor).not.toBe('pointer');
|
|
111
|
-
expect(th.hasAttribute('data-sort')).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
});
|
package/src/table/table.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ColumnDef,
|
|
3
|
-
RowData,
|
|
4
|
-
SortingState,
|
|
5
|
-
createAngularTable,
|
|
6
|
-
getCoreRowModel,
|
|
7
|
-
getSortedRowModel,
|
|
8
|
-
type Table,
|
|
9
|
-
} from '@tanstack/angular-table';
|
|
10
|
-
import { Directive, InjectionToken, computed, input, signal } from '@angular/core';
|
|
11
|
-
|
|
12
|
-
/** Injection token providing the TanStack table instance to child directives. */
|
|
13
|
-
export const KJ_TABLE = new InjectionToken<KjTable<unknown>>('KjTable');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Wraps TanStack Angular Table. Accepts `kjTable` (columns) and `kjTableData` signal inputs and
|
|
17
|
-
* exposes the table instance (rows, headers, sorting) via the `KJ_TABLE` context token.
|
|
18
|
-
*
|
|
19
|
-
* Use `table` to access the full TanStack `Table<T>` instance in templates via `exportAs: 'kjTable'`.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```html
|
|
23
|
-
* <table [kjTable]="columns" [kjTableData]="rows" #tbl="kjTable">
|
|
24
|
-
* <thead>
|
|
25
|
-
* @for (headerGroup of tbl.table().getHeaderGroups(); track headerGroup.id) {
|
|
26
|
-
* <tr>
|
|
27
|
-
* @for (header of headerGroup.headers; track header.id) {
|
|
28
|
-
* <th kjTableHeader [kjHeader]="header">
|
|
29
|
-
* {{ header.column.columnDef.header }}
|
|
30
|
-
* </th>
|
|
31
|
-
* }
|
|
32
|
-
* </tr>
|
|
33
|
-
* }
|
|
34
|
-
* </thead>
|
|
35
|
-
* </table>
|
|
36
|
-
* ```
|
|
37
|
-
* @category Core/Data
|
|
38
|
-
*/
|
|
39
|
-
@Directive({
|
|
40
|
-
selector: '[kjTable]',
|
|
41
|
-
standalone: true,
|
|
42
|
-
exportAs: 'kjTable',
|
|
43
|
-
providers: [{ provide: KJ_TABLE, useExisting: KjTable }],
|
|
44
|
-
})
|
|
45
|
-
export class KjTable<TData extends RowData = unknown> {
|
|
46
|
-
/** Column definitions for the table. */
|
|
47
|
-
kjTable = input.required<ColumnDef<TData>[]>();
|
|
48
|
-
|
|
49
|
-
/** Data rows for the table. */
|
|
50
|
-
kjTableData = input<TData[]>([]);
|
|
51
|
-
|
|
52
|
-
private readonly _sorting = signal<SortingState>([]);
|
|
53
|
-
|
|
54
|
-
/** The TanStack table instance. Access rows, headers, and state here. */
|
|
55
|
-
readonly table: () => Table<TData> = createAngularTable<TData>(() => ({
|
|
56
|
-
data: this.kjTableData(),
|
|
57
|
-
columns: this.kjTable(),
|
|
58
|
-
state: { sorting: this._sorting() },
|
|
59
|
-
onSortingChange: (updater) => {
|
|
60
|
-
const next = typeof updater === 'function' ? updater(this._sorting()) : updater;
|
|
61
|
-
this._sorting.set(next);
|
|
62
|
-
},
|
|
63
|
-
getCoreRowModel: getCoreRowModel(),
|
|
64
|
-
getSortedRowModel: getSortedRowModel(),
|
|
65
|
-
}));
|
|
66
|
-
|
|
67
|
-
/** Current sorting state as a signal. */
|
|
68
|
-
readonly sorting = this._sorting.asReadonly();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Sort direction for table columns. */
|
|
72
|
-
export type KjSortDirection = 'asc' | 'desc';
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Marks a `<th>` as a sortable column header. Sets `aria-sort` based on TanStack sort state.
|
|
76
|
-
* Clicking toggles sort direction only when the column has sorting enabled.
|
|
77
|
-
* If `enableSorting: false` is set on the column def, the header renders as plain text.
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* ```html
|
|
81
|
-
* <th kjTableHeader [kjHeader]="header" scope="col">Name</th>
|
|
82
|
-
* ```
|
|
83
|
-
* @category Core/Data
|
|
84
|
-
*/
|
|
85
|
-
@Directive({
|
|
86
|
-
selector: '[kjTableHeader]',
|
|
87
|
-
standalone: true,
|
|
88
|
-
host: {
|
|
89
|
-
'[style.cursor]': 'canSort() ? "pointer" : null',
|
|
90
|
-
'[attr.tabindex]': 'canSort() ? "0" : null',
|
|
91
|
-
'[attr.aria-sort]': 'canSort() ? ariaSort() : null',
|
|
92
|
-
'[attr.data-sort]': 'canSort() ? sortDir() : null',
|
|
93
|
-
'(click)': 'onHeaderClick()',
|
|
94
|
-
'(keydown.enter)': 'onHeaderClick()',
|
|
95
|
-
'(keydown.space)': '$event.preventDefault(); onHeaderClick()',
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
export class KjTableHeader<TData extends RowData = unknown> {
|
|
99
|
-
/** The TanStack header object for this column. Pass from the table's getHeaderGroups(). */
|
|
100
|
-
kjHeader = input<ReturnType<Table<TData>['getHeaderGroups']>[0]['headers'][0] | undefined>(undefined);
|
|
101
|
-
|
|
102
|
-
/** Whether this column supports sorting, derived from TanStack column state. */
|
|
103
|
-
readonly canSort = computed(() => this.kjHeader()?.column.getCanSort() ?? false);
|
|
104
|
-
|
|
105
|
-
/** Current sort direction, or null when unsorted. */
|
|
106
|
-
readonly sortDir = computed((): KjSortDirection | null => {
|
|
107
|
-
const h = this.kjHeader();
|
|
108
|
-
if (!h) return null;
|
|
109
|
-
const sorted = h.column.getIsSorted();
|
|
110
|
-
if (sorted === 'asc') return 'asc';
|
|
111
|
-
if (sorted === 'desc') return 'desc';
|
|
112
|
-
return null;
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
/** ARIA sort attribute value derived from sort direction. */
|
|
116
|
-
readonly ariaSort = computed(() => {
|
|
117
|
-
const d = this.sortDir();
|
|
118
|
-
if (d === 'asc') return 'ascending';
|
|
119
|
-
if (d === 'desc') return 'descending';
|
|
120
|
-
return 'none';
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
onHeaderClick(): void {
|
|
124
|
-
if (this.canSort()) this.kjHeader()?.column.toggleSorting();
|
|
125
|
-
}
|
|
126
|
-
}
|
package/src/tabs/index.ts
DELETED
package/src/tabs/tabs.context.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { InjectionToken, Signal } from '@angular/core';
|
|
2
|
-
|
|
3
|
-
/** Shared context passed from KjTabs to child tab/panel directives. */
|
|
4
|
-
export interface KjTabsContext {
|
|
5
|
-
value: Signal<string>;
|
|
6
|
-
activate: (value: string) => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const KJ_TABS = new InjectionToken<KjTabsContext>('KjTabs');
|
package/src/tabs/tabs.spec.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { render, fireEvent } from '@testing-library/angular';
|
|
2
|
-
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
3
|
-
import { KjTabs, KjTabList, KjTab, KjTabPanel } from './tabs';
|
|
4
|
-
|
|
5
|
-
expect.extend(toHaveNoViolations);
|
|
6
|
-
|
|
7
|
-
const imports = [KjTabs, KjTabList, KjTab, KjTabPanel];
|
|
8
|
-
const template = `
|
|
9
|
-
<div kjTabs [kjTabsValue]="'tab1'">
|
|
10
|
-
<div kjTabList aria-label="Demo tabs">
|
|
11
|
-
<button kjTab [kjTabValue]="'tab1'">Tab 1</button>
|
|
12
|
-
<button kjTab [kjTabValue]="'tab2'">Tab 2</button>
|
|
13
|
-
</div>
|
|
14
|
-
<div kjTabPanel [kjPanelFor]="'tab1'">Content 1</div>
|
|
15
|
-
<div kjTabPanel [kjPanelFor]="'tab2'">Content 2</div>
|
|
16
|
-
</div>`;
|
|
17
|
-
|
|
18
|
-
describe('KjTabs', () => {
|
|
19
|
-
it('active tab has aria-selected=true', async () => {
|
|
20
|
-
const { getAllByRole } = await render(template, { imports });
|
|
21
|
-
expect(getAllByRole('tab')[0]).toHaveAttribute('aria-selected', 'true');
|
|
22
|
-
expect(getAllByRole('tab')[1]).toHaveAttribute('aria-selected', 'false');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('inactive panel is hidden', async () => {
|
|
26
|
-
const { getByText } = await render(template, { imports });
|
|
27
|
-
expect(getByText('Content 2').closest('[kjTabPanel]')).toHaveAttribute('hidden', '');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('clicking a tab activates it', async () => {
|
|
31
|
-
const { getAllByRole } = await render(template, { imports });
|
|
32
|
-
fireEvent.click(getAllByRole('tab')[1]);
|
|
33
|
-
expect(getAllByRole('tab')[1]).toHaveAttribute('aria-selected', 'true');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('passes axe audit', async () => {
|
|
37
|
-
const { container } = await render(template, { imports });
|
|
38
|
-
expect(await axe(container)).toHaveNoViolations();
|
|
39
|
-
});
|
|
40
|
-
});
|
package/src/tabs/tabs.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { Directive, computed, inject, input, model } from '@angular/core';
|
|
2
|
-
import { KJ_TABS, KjTabsContext } from './tabs.context';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Root tabs container. Manages active tab value.
|
|
6
|
-
* @example
|
|
7
|
-
* ```html
|
|
8
|
-
* <div kjTabs [(kjTabsValue)]="activeTab"><div kjTabList>...</div></div>
|
|
9
|
-
* ```
|
|
10
|
-
* @doc
|
|
11
|
-
* @doc-file tabs.example.ts
|
|
12
|
-
* ```typescript
|
|
13
|
-
* import { Component, signal } from '@angular/core';
|
|
14
|
-
* import { KjTabs, KjTabList, KjTab, KjTabPanel } from '@kouji-ui/core';
|
|
15
|
-
*
|
|
16
|
-
* @Component({
|
|
17
|
-
* standalone: true,
|
|
18
|
-
* imports: [KjTabs, KjTabList, KjTab, KjTabPanel],
|
|
19
|
-
* styles: [`
|
|
20
|
-
* .tabs { background: #0c0c0c; padding: 2rem; }
|
|
21
|
-
* [kjTabList] { display: flex; border-bottom: 1px solid #1e1e1e; margin-bottom: 1.5rem; }
|
|
22
|
-
* [kjTab] { background: none; border: none; border-bottom: 2px solid transparent; color: #555; font-family: 'JetBrains Mono', monospace; font-size: 0.875rem; padding: 0.625rem 1.25rem; cursor: pointer; transition: color 0.15s, border-color 0.15s; margin-bottom: -1px; }
|
|
23
|
-
* [kjTab][aria-selected="true"] { color: #f0ede6; border-bottom-color: #b8f500; }
|
|
24
|
-
* [kjTab]:hover { color: #f0ede6; }
|
|
25
|
-
* [kjTabPanel] { color: #666; font-family: 'JetBrains Mono', monospace; font-size: 0.875rem; line-height: 1.6; }
|
|
26
|
-
* [hidden] { display: none; }
|
|
27
|
-
* `],
|
|
28
|
-
* template: `
|
|
29
|
-
* <div class="tabs">
|
|
30
|
-
* <div kjTabs [(kjTabsValue)]="activeTab" aria-label="Example tabs">
|
|
31
|
-
* <div kjTabList>
|
|
32
|
-
* <button kjTab [kjTabValue]="'tab1'">Overview</button>
|
|
33
|
-
* <button kjTab [kjTabValue]="'tab2'">API</button>
|
|
34
|
-
* <button kjTab [kjTabValue]="'tab3'">Examples</button>
|
|
35
|
-
* </div>
|
|
36
|
-
* <div kjTabPanel [kjPanelFor]="'tab1'">The overview tab content goes here.</div>
|
|
37
|
-
* <div kjTabPanel [kjPanelFor]="'tab2'">API reference content goes here.</div>
|
|
38
|
-
* <div kjTabPanel [kjPanelFor]="'tab3'">Code examples go here.</div>
|
|
39
|
-
* </div>
|
|
40
|
-
* </div>
|
|
41
|
-
* `,
|
|
42
|
-
* })
|
|
43
|
-
* export class TabsExampleComponent {
|
|
44
|
-
* activeTab = signal('tab1');
|
|
45
|
-
* }
|
|
46
|
-
* ```
|
|
47
|
-
* @category Core/Navigation
|
|
48
|
-
*/
|
|
49
|
-
@Directive({
|
|
50
|
-
selector: '[kjTabs]',
|
|
51
|
-
standalone: true,
|
|
52
|
-
providers: [{ provide: KJ_TABS, useExisting: KjTabs }],
|
|
53
|
-
})
|
|
54
|
-
export class KjTabs implements KjTabsContext {
|
|
55
|
-
/** Two-way bound active tab value. */
|
|
56
|
-
kjTabsValue = model<string>('');
|
|
57
|
-
|
|
58
|
-
/** Read-only signal of the current active tab value. */
|
|
59
|
-
readonly value = this.kjTabsValue.asReadonly();
|
|
60
|
-
|
|
61
|
-
/** Activates the tab with the given value. */
|
|
62
|
-
activate(val: string): void {
|
|
63
|
-
this.kjTabsValue.set(val);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Container for tab triggers. Sets `role="tablist"`.
|
|
69
|
-
* @category Core/Navigation
|
|
70
|
-
*/
|
|
71
|
-
@Directive({
|
|
72
|
-
selector: '[kjTabList]',
|
|
73
|
-
standalone: true,
|
|
74
|
-
host: { role: 'tablist' },
|
|
75
|
-
})
|
|
76
|
-
export class KjTabList {}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Individual tab trigger. Sets `role="tab"` and manages `aria-selected`.
|
|
80
|
-
* @example `<button kjTab [kjTabValue]="'profile'">Profile</button>`
|
|
81
|
-
* @category Core/Navigation
|
|
82
|
-
*/
|
|
83
|
-
@Directive({
|
|
84
|
-
selector: '[kjTab]',
|
|
85
|
-
standalone: true,
|
|
86
|
-
host: {
|
|
87
|
-
role: 'tab',
|
|
88
|
-
'[attr.aria-selected]': 'active().toString()',
|
|
89
|
-
'[attr.data-active]': 'active() ? "" : null',
|
|
90
|
-
'[attr.tabindex]': 'active() ? "0" : "-1"',
|
|
91
|
-
'(click)': 'activate()',
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
export class KjTab {
|
|
95
|
-
private readonly ctx = inject(KJ_TABS);
|
|
96
|
-
|
|
97
|
-
/** The value this tab represents. */
|
|
98
|
-
kjTabValue = input.required<string>();
|
|
99
|
-
|
|
100
|
-
/** Whether this tab is currently active. */
|
|
101
|
-
readonly active = computed(() => this.ctx.value() === this.kjTabValue());
|
|
102
|
-
|
|
103
|
-
/** Activates this tab by updating the shared context. */
|
|
104
|
-
activate(): void {
|
|
105
|
-
this.ctx.activate(this.kjTabValue());
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Tab panel content. Hidden when not active.
|
|
111
|
-
* @example `<div kjTabPanel [kjPanelFor]="'profile'">Profile content</div>`
|
|
112
|
-
* @category Core/Navigation
|
|
113
|
-
*/
|
|
114
|
-
@Directive({
|
|
115
|
-
selector: '[kjTabPanel]',
|
|
116
|
-
standalone: true,
|
|
117
|
-
host: {
|
|
118
|
-
role: 'tabpanel',
|
|
119
|
-
'[attr.hidden]': 'hidden() ? "" : null',
|
|
120
|
-
},
|
|
121
|
-
})
|
|
122
|
-
export class KjTabPanel {
|
|
123
|
-
private readonly ctx = inject(KJ_TABS);
|
|
124
|
-
|
|
125
|
-
/** The tab value this panel corresponds to. */
|
|
126
|
-
kjPanelFor = input.required<string>();
|
|
127
|
-
|
|
128
|
-
/** Whether this panel is currently hidden. */
|
|
129
|
-
readonly hidden = computed(() => this.ctx.value() !== this.kjPanelFor());
|
|
130
|
-
}
|
package/src/test-setup.ts
DELETED
package/src/toast/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
KjToast,
|
|
3
|
-
KjToastViewport,
|
|
4
|
-
KjToastClose,
|
|
5
|
-
type KjToastRenderable,
|
|
6
|
-
type KjToastVariant,
|
|
7
|
-
type KjToastContext,
|
|
8
|
-
type KjToastOptions,
|
|
9
|
-
type KjToastPositionX,
|
|
10
|
-
type KjToastPositionY,
|
|
11
|
-
} from './toast';
|
|
12
|
-
export { KjToastService, type KjToastItem, type KjToastTemplateContext } from './toast.service';
|
|
13
|
-
export {
|
|
14
|
-
KJ_TOAST_STRATEGY,
|
|
15
|
-
KJ_TOAST_SONNER_STRATEGY,
|
|
16
|
-
KJ_TOAST_LIST_STRATEGY,
|
|
17
|
-
type KjToastStrategy,
|
|
18
|
-
provideKjToastStrategy,
|
|
19
|
-
provideKjToastSonnerStrategy,
|
|
20
|
-
provideKjToastListStrategy,
|
|
21
|
-
} from './toast.strategy';
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { Component, inject } from '@angular/core';
|
|
2
|
-
import { NgTemplateOutlet } from '@angular/common';
|
|
3
|
-
import { KjToastViewport, KjToast } from './toast';
|
|
4
|
-
import { KjToastService } from './toast.service';
|
|
5
|
-
import { KjButton } from '../button/button';
|
|
6
|
-
|
|
7
|
-
@Component({
|
|
8
|
-
selector: 'kj-example-toast-basic',
|
|
9
|
-
standalone: true,
|
|
10
|
-
imports: [KjToastViewport, KjToast, KjButton, NgTemplateOutlet],
|
|
11
|
-
styleUrls: ['../styles/docs-themes.css'],
|
|
12
|
-
styles: [`
|
|
13
|
-
:host { display: block; padding: 2rem; background: var(--kj-bg); font-family: var(--kj-font); min-height: 360px; position: relative; overflow: hidden; }
|
|
14
|
-
.row { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
|
15
|
-
button[kjButton] { padding: 0.4rem 1rem; border: none; cursor: pointer; font-family: inherit; font-size: 0.8125rem; }
|
|
16
|
-
button[kjButton][data-variant="success"] { background: #4ade80; color: #0c0c0c; }
|
|
17
|
-
button[kjButton][data-variant="destructive"] { background: #ef4444; color: #fff; }
|
|
18
|
-
button[kjButton][data-variant="warning"] { background: #fb923c; color: #0c0c0c; }
|
|
19
|
-
button[kjButton][data-variant="default"] { background: #b8f500; color: #0c0c0c; }
|
|
20
|
-
|
|
21
|
-
/* ── Viewport: positioned via [data-position-*] ── */
|
|
22
|
-
[kjToastViewport] {
|
|
23
|
-
position: absolute;
|
|
24
|
-
width: 22rem; max-width: calc(100% - 2rem);
|
|
25
|
-
list-style: none; padding: 0; margin: 0;
|
|
26
|
-
z-index: var(--kj-toast-z-index);
|
|
27
|
-
}
|
|
28
|
-
[kjToastViewport][data-position-y="bottom"] { bottom: 1rem; }
|
|
29
|
-
[kjToastViewport][data-position-y="top"] { top: 1rem; }
|
|
30
|
-
[kjToastViewport][data-position-x="end"] { right: 1rem; }
|
|
31
|
-
[kjToastViewport][data-position-x="start"] { left: 1rem; }
|
|
32
|
-
|
|
33
|
-
/* Each <li> wraps a toast — absolutely positioned so siblings overlap.
|
|
34
|
-
The viewport itself stays a 0-height anchor; the front toast pegs to its edge. */
|
|
35
|
-
[kjToastViewport] li {
|
|
36
|
-
position: absolute; left: 0; right: 0;
|
|
37
|
-
transition: transform 0.4s cubic-bezier(0.21, 0.61, 0.35, 1),
|
|
38
|
-
opacity 0.3s ease;
|
|
39
|
-
}
|
|
40
|
-
[kjToastViewport][data-position-y="bottom"] li { bottom: 0; }
|
|
41
|
-
[kjToastViewport][data-position-y="top"] li { top: 0; }
|
|
42
|
-
|
|
43
|
-
/* ── COLLAPSED stack: only the front toast is fully visible.
|
|
44
|
-
Each older toast scales down 5% per level, lifts upward by index*gap,
|
|
45
|
-
and shrinks to the front toast's height. ── */
|
|
46
|
-
[kjToastViewport][data-expanded="false"] [kjToast]:not([data-front="true"]) {
|
|
47
|
-
height: var(--kj-toast-front-height);
|
|
48
|
-
overflow: hidden;
|
|
49
|
-
transform: translateY(calc((var(--kj-toast-index) - 1) * var(--kj-toast-gap) * -1))
|
|
50
|
-
scale(calc(1 - (var(--kj-toast-index) - 1) * 0.06));
|
|
51
|
-
opacity: calc(1 - (var(--kj-toast-index) - 1) * 0.25);
|
|
52
|
-
}
|
|
53
|
-
[kjToastViewport][data-position-y="top"][data-expanded="false"] [kjToast]:not([data-front="true"]) {
|
|
54
|
-
transform: translateY(calc((var(--kj-toast-index) - 1) * var(--kj-toast-gap)))
|
|
55
|
-
scale(calc(1 - (var(--kj-toast-index) - 1) * 0.06));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* ── EXPANDED: each toast lifts by its measured height + gap, summed for older toasts.
|
|
59
|
-
Uses --kj-toast-before (older count visible behind). ── */
|
|
60
|
-
[kjToastViewport][data-expanded="true"] [kjToast] {
|
|
61
|
-
transform: translateY(calc((var(--kj-toast-index) - 1) * (var(--kj-toast-height, 0px) + var(--kj-toast-gap)) * -1));
|
|
62
|
-
}
|
|
63
|
-
[kjToastViewport][data-position-y="top"][data-expanded="true"] [kjToast] {
|
|
64
|
-
transform: translateY(calc((var(--kj-toast-index) - 1) * (var(--kj-toast-height, 0px) + var(--kj-toast-gap))));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* ── Toast appearance ── */
|
|
68
|
-
[kjToast] {
|
|
69
|
-
display: flex; align-items: flex-start; gap: 0.75rem;
|
|
70
|
-
padding: 0.875rem 1rem; font-size: 0.8125rem;
|
|
71
|
-
border-radius: 8px;
|
|
72
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255, 255, 255, 0.04) inset;
|
|
73
|
-
animation: enter 0.35s cubic-bezier(0.21, 0.61, 0.35, 1);
|
|
74
|
-
}
|
|
75
|
-
@keyframes enter {
|
|
76
|
-
from { opacity: 0; transform: translateY(100%) scale(0.95); }
|
|
77
|
-
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
78
|
-
}
|
|
79
|
-
.icon { font-size: 1rem; line-height: 1.2; flex-shrink: 0; }
|
|
80
|
-
.body { flex: 1; min-width: 0; line-height: 1.4; }
|
|
81
|
-
.title { font-weight: 600; margin-bottom: 0.125rem; }
|
|
82
|
-
.close {
|
|
83
|
-
background: none; border: none; cursor: pointer; opacity: 0.5; font-size: 1.1rem;
|
|
84
|
-
color: inherit; padding: 0; line-height: 1; flex-shrink: 0;
|
|
85
|
-
}
|
|
86
|
-
.close:hover { opacity: 1; }
|
|
87
|
-
|
|
88
|
-
[kjToast][data-variant="success"] { background: #052e16; color: #86efac; border: 1px solid #166534; }
|
|
89
|
-
[kjToast][data-variant="destructive"] { background: #2c0a0a; color: #fca5a5; border: 1px solid #991b1b; }
|
|
90
|
-
[kjToast][data-variant="warning"] { background: #2c1a04; color: #fcd34d; border: 1px solid #92400e; }
|
|
91
|
-
[kjToast][data-variant="default"] { background: #1a1a1a; color: #f0ede6; border: 1px solid #2a2a2a; }
|
|
92
|
-
[kjToast][data-variant="success"] .icon { color: #4ade80; }
|
|
93
|
-
[kjToast][data-variant="destructive"] .icon { color: #f87171; }
|
|
94
|
-
[kjToast][data-variant="warning"] .icon { color: #fb923c; }
|
|
95
|
-
[kjToast][data-variant="default"] .icon { color: #b8f500; }
|
|
96
|
-
`],
|
|
97
|
-
template: `
|
|
98
|
-
<div class="row">
|
|
99
|
-
<button kjButton [attr.data-variant]="'success'" (click)="toast.success('Changes saved successfully!')">Success</button>
|
|
100
|
-
<button kjButton [attr.data-variant]="'destructive'" (click)="toast.error('Something went wrong!')">Error</button>
|
|
101
|
-
<button kjButton [attr.data-variant]="'warning'" (click)="toast.warning('Session expires in 5 minutes.')">Warning</button>
|
|
102
|
-
<button kjButton [attr.data-variant]="'default'" (click)="toast.info('New update available.')">Info</button>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
<ol kjToastViewport [kjToastDefaultTemplate]="defaultTpl" #vp="kjToastViewport" aria-label="Notifications">
|
|
106
|
-
@for (r of vp.renderable(); track r.id) {
|
|
107
|
-
<li><ng-container *ngTemplateOutlet="r.template; context: r.context" /></li>
|
|
108
|
-
}
|
|
109
|
-
</ol>
|
|
110
|
-
|
|
111
|
-
<ng-template #defaultTpl let-ctx>
|
|
112
|
-
<div kjToast [kjToastVariant]="ctx.variant" [kjToastId]="ctx.id">
|
|
113
|
-
<span class="icon" aria-hidden="true">{{ iconFor(ctx.variant) }}</span>
|
|
114
|
-
<div class="body">
|
|
115
|
-
@if (ctx.title) { <div class="title">{{ ctx.title }}</div> }
|
|
116
|
-
<span>{{ ctx.message }}</span>
|
|
117
|
-
</div>
|
|
118
|
-
<button class="close" (click)="ctx.dismiss()" aria-label="Dismiss">×</button>
|
|
119
|
-
</div>
|
|
120
|
-
</ng-template>
|
|
121
|
-
`,
|
|
122
|
-
})
|
|
123
|
-
export class ToastBasicExample {
|
|
124
|
-
readonly toast = inject(KjToastService);
|
|
125
|
-
|
|
126
|
-
iconFor(variant: 'default' | 'success' | 'destructive' | 'warning'): string {
|
|
127
|
-
switch (variant) {
|
|
128
|
-
case 'success': return '✓';
|
|
129
|
-
case 'destructive': return '✕';
|
|
130
|
-
case 'warning': return '!';
|
|
131
|
-
default: return 'ℹ';
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|