@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.
Files changed (127) hide show
  1. package/fesm2022/kouji-ui-core.mjs +26789 -0
  2. package/fesm2022/kouji-ui-core.mjs.map +1 -0
  3. package/icon/icon.css +40 -0
  4. package/package.json +20 -9
  5. package/types/kouji-ui-core.d.ts +13545 -0
  6. package/typography/prose.css +312 -0
  7. package/.turbo/turbo-build.log +0 -27
  8. package/CHANGELOG.md +0 -21
  9. package/ng-package.json +0 -7
  10. package/src/a11y/.gitkeep +0 -0
  11. package/src/a11y/aria-describedby.spec.ts +0 -55
  12. package/src/a11y/aria-describedby.ts +0 -33
  13. package/src/a11y/focus-trap.spec.ts +0 -86
  14. package/src/a11y/focus-trap.ts +0 -70
  15. package/src/a11y/index.ts +0 -9
  16. package/src/a11y/live-region.spec.ts +0 -58
  17. package/src/a11y/live-region.ts +0 -48
  18. package/src/a11y/roving-tabindex.spec.ts +0 -62
  19. package/src/a11y/roving-tabindex.ts +0 -104
  20. package/src/a11y/visually-hidden.spec.ts +0 -34
  21. package/src/a11y/visually-hidden.ts +0 -24
  22. package/src/accordion/accordion.context.ts +0 -22
  23. package/src/accordion/accordion.spec.ts +0 -32
  24. package/src/accordion/accordion.ts +0 -129
  25. package/src/accordion/index.ts +0 -2
  26. package/src/avatar/avatar.spec.ts +0 -18
  27. package/src/avatar/avatar.ts +0 -26
  28. package/src/avatar/index.ts +0 -1
  29. package/src/badge/badge.spec.ts +0 -20
  30. package/src/badge/badge.ts +0 -14
  31. package/src/badge/index.ts +0 -1
  32. package/src/button/button.example.ts +0 -30
  33. package/src/button/button.finance.example.ts +0 -37
  34. package/src/button/button.retro.example.ts +0 -47
  35. package/src/button/button.sizes.example.ts +0 -25
  36. package/src/button/button.spec.ts +0 -32
  37. package/src/button/button.ts +0 -44
  38. package/src/button/index.ts +0 -1
  39. package/src/chart/chart.spec.ts +0 -27
  40. package/src/chart/chart.ts +0 -48
  41. package/src/chart/index.ts +0 -1
  42. package/src/checkbox/checkbox.spec.ts +0 -35
  43. package/src/checkbox/checkbox.ts +0 -82
  44. package/src/checkbox/index.ts +0 -1
  45. package/src/dialog/dialog.confirm.example.ts +0 -57
  46. package/src/dialog/dialog.context.ts +0 -11
  47. package/src/dialog/dialog.example.ts +0 -40
  48. package/src/dialog/dialog.finance.example.ts +0 -47
  49. package/src/dialog/dialog.retro.example.ts +0 -46
  50. package/src/dialog/dialog.service.ts +0 -176
  51. package/src/dialog/dialog.spec.ts +0 -122
  52. package/src/dialog/dialog.ts +0 -201
  53. package/src/dialog/index.ts +0 -9
  54. package/src/example-components.ts +0 -33
  55. package/src/form/form.context.ts +0 -3
  56. package/src/form/form.spec.ts +0 -25
  57. package/src/form/form.ts +0 -46
  58. package/src/form/index.ts +0 -2
  59. package/src/input/index.ts +0 -1
  60. package/src/input/input.spec.ts +0 -43
  61. package/src/input/input.ts +0 -80
  62. package/src/menu/index.ts +0 -2
  63. package/src/menu/menu.context.ts +0 -10
  64. package/src/menu/menu.example.ts +0 -39
  65. package/src/menu/menu.spec.ts +0 -168
  66. package/src/menu/menu.ts +0 -176
  67. package/src/popover/index.ts +0 -12
  68. package/src/popover/popover.context.ts +0 -18
  69. package/src/popover/popover.example.ts +0 -50
  70. package/src/popover/popover.finance.example.ts +0 -52
  71. package/src/popover/popover.retro.example.ts +0 -53
  72. package/src/popover/popover.spec.ts +0 -52
  73. package/src/popover/popover.ts +0 -124
  74. package/src/primitives/.gitkeep +0 -0
  75. package/src/primitives/forms/form-control.spec.ts +0 -59
  76. package/src/primitives/forms/form-control.ts +0 -90
  77. package/src/primitives/forms/index.ts +0 -1
  78. package/src/primitives/index.ts +0 -3
  79. package/src/primitives/interaction/disabled.spec.ts +0 -47
  80. package/src/primitives/interaction/disabled.ts +0 -24
  81. package/src/primitives/interaction/focus-ring.spec.ts +0 -62
  82. package/src/primitives/interaction/focus-ring.ts +0 -57
  83. package/src/primitives/interaction/index.ts +0 -2
  84. package/src/primitives/overlay/index.ts +0 -1
  85. package/src/primitives/overlay/overlay.spec.ts +0 -90
  86. package/src/primitives/overlay/overlay.ts +0 -106
  87. package/src/public-api.ts +0 -35
  88. package/src/radio/index.ts +0 -2
  89. package/src/radio/radio.context.ts +0 -3
  90. package/src/radio/radio.spec.ts +0 -36
  91. package/src/radio/radio.ts +0 -55
  92. package/src/select/index.ts +0 -2
  93. package/src/select/select.context.ts +0 -3
  94. package/src/select/select.spec.ts +0 -145
  95. package/src/select/select.ts +0 -250
  96. package/src/styles/docs-themes.css +0 -82
  97. package/src/table/index.ts +0 -1
  98. package/src/table/table.spec.ts +0 -113
  99. package/src/table/table.ts +0 -126
  100. package/src/tabs/index.ts +0 -2
  101. package/src/tabs/tabs.context.ts +0 -9
  102. package/src/tabs/tabs.spec.ts +0 -40
  103. package/src/tabs/tabs.ts +0 -130
  104. package/src/test-setup.ts +0 -5
  105. package/src/toast/index.ts +0 -21
  106. package/src/toast/toast.example.ts +0 -134
  107. package/src/toast/toast.finance.example.ts +0 -68
  108. package/src/toast/toast.retro.example.ts +0 -69
  109. package/src/toast/toast.service.ts +0 -153
  110. package/src/toast/toast.spec.ts +0 -95
  111. package/src/toast/toast.strategy.ts +0 -102
  112. package/src/toast/toast.ts +0 -307
  113. package/src/toast/toast.types.ts +0 -5
  114. package/src/toggle/index.ts +0 -1
  115. package/src/toggle/toggle.spec.ts +0 -24
  116. package/src/toggle/toggle.ts +0 -40
  117. package/src/tooltip/index.ts +0 -5
  118. package/src/tooltip/tooltip.example.ts +0 -46
  119. package/src/tooltip/tooltip.finance.example.ts +0 -54
  120. package/src/tooltip/tooltip.placements.example.ts +0 -56
  121. package/src/tooltip/tooltip.retro.example.ts +0 -65
  122. package/src/tooltip/tooltip.spec.ts +0 -104
  123. package/src/tooltip/tooltip.ts +0 -129
  124. package/tsconfig.lib.json +0 -13
  125. package/tsconfig.lib.prod.json +0 -11
  126. package/tsconfig.spec.json +0 -10
  127. 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
- }
@@ -1 +0,0 @@
1
- export { KjTable, KjTableHeader, KJ_TABLE, type KjSortDirection } from './table';
@@ -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
- });
@@ -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
@@ -1,2 +0,0 @@
1
- export { KjTabs, KjTabList, KjTab, KjTabPanel } from './tabs';
2
- export { KJ_TABS, type KjTabsContext } from './tabs.context';
@@ -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');
@@ -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
@@ -1,5 +0,0 @@
1
- import '@analogjs/vitest-angular/setup-zone';
2
- import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
3
- import '@testing-library/jest-dom';
4
-
5
- setupTestBed({ zoneless: false });
@@ -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
- }