@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,80 +0,0 @@
1
- import { Directive, ElementRef, effect, inject, input } from '@angular/core';
2
- import { KjDisabled, KjFocusRing, KjFormControl } from '../primitives';
3
-
4
- /**
5
- * Enhances a native `<input>` with Angular forms integration, disabled/invalid state, and focus-ring.
6
- * Supports `formControl`, `formControlName`, and `ngModel` bindings.
7
- *
8
- * @example
9
- * ```html
10
- * <input kjInput type="email" [formControl]="emailCtrl" [kjInvalid]="emailCtrl.invalid" />
11
- * ```
12
- * @doc
13
- * @doc-file input.example.ts
14
- * ```typescript
15
- * import { Component } from '@angular/core';
16
- * import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
17
- * import { KjInput } from '@kouji-ui/core';
18
- *
19
- * @Component({
20
- * standalone: true,
21
- * imports: [KjInput, ReactiveFormsModule],
22
- * styles: [`
23
- * .form { padding: 2rem; background: #0c0c0c; display: flex; flex-direction: column; gap: 1rem; max-width: 400px; }
24
- * [kjInput] { padding: 0.625rem 0.875rem; background: #111; border: 1px solid #222; color: #f0ede6; font-family: 'JetBrains Mono', monospace; font-size: 0.875rem; width: 100%; }
25
- * [kjInput]:focus { outline: none; border-color: #b8f500; }
26
- * [aria-invalid="true"] { border-color: #ef4444; }
27
- * .error { color: #ef4444; font-size: 0.75rem; font-family: 'JetBrains Mono', monospace; }
28
- * label { color: #888; font-size: 0.75rem; font-family: 'JetBrains Mono', monospace; }
29
- * `],
30
- * template: `
31
- * <div class="form">
32
- * <label for="email">Email</label>
33
- * <input id="email" kjInput type="email" [formControl]="emailCtrl"
34
- * [kjInvalid]="emailCtrl.invalid && emailCtrl.touched"
35
- * placeholder="you@example.com" />
36
- * @if (emailCtrl.invalid && emailCtrl.touched) {
37
- * <span class="error">Please enter a valid email.</span>
38
- * }
39
- * <label for="disabled">Disabled input</label>
40
- * <input id="disabled" kjInput type="text" [kjDisabled]="true" value="Not editable" />
41
- * </div>
42
- * `,
43
- * })
44
- * export class InputExampleComponent {
45
- * emailCtrl = new FormControl('', [Validators.email, Validators.required]);
46
- * }
47
- * ```
48
- * @category Core/Inputs
49
- */
50
- @Directive({
51
- selector: '[kjInput]',
52
- standalone: true,
53
- hostDirectives: [
54
- { directive: KjDisabled, inputs: ['kjDisabled'] },
55
- KjFocusRing,
56
- KjFormControl,
57
- ],
58
- host: {
59
- '[attr.aria-invalid]': 'formCtrl.touched() && kjInvalid() ? "true" : null',
60
- '[attr.data-invalid]': 'formCtrl.touched() && kjInvalid() ? "" : null',
61
- '[attr.disabled]': 'formCtrl.disabled() ? "" : null',
62
- '(input)': 'formCtrl.notifyChange($any($event.target).value)',
63
- '(blur)': 'formCtrl.notifyTouched()',
64
- },
65
- })
66
- export class KjInput {
67
- readonly formCtrl = inject(KjFormControl);
68
- private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);
69
-
70
- /** Whether the input is in an invalid state. Combined with `touched` for ARIA. */
71
- kjInvalid = input<boolean>(false);
72
-
73
- constructor() {
74
- // Reflect the CVA value signal back to the native input element.
75
- effect(() => {
76
- const val = this.formCtrl.value();
77
- this.el.nativeElement.value = val == null ? '' : String(val);
78
- });
79
- }
80
- }
package/src/menu/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjMenuClose } from './menu';
2
- export { KJ_MENU, type KjMenuContext } from './menu.context';
@@ -1,10 +0,0 @@
1
- import { InjectionToken, Signal } from '@angular/core';
2
-
3
- /** Context shared between menu directives. */
4
- export interface KjMenuContext {
5
- readonly open: Signal<boolean>;
6
- toggle(): void;
7
- close(): void;
8
- }
9
-
10
- export const KJ_MENU = new InjectionToken<KjMenuContext>('KjMenu');
@@ -1,39 +0,0 @@
1
- import { Component } from '@angular/core';
2
- import { KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem } from './menu';
3
- import { KjButton } from '../button/button';
4
-
5
- @Component({
6
- selector: 'kj-example-menu-basic',
7
- standalone: true,
8
- imports: [KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjButton],
9
- styles: [`
10
- :host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: #0c0c0c; font-family: 'JetBrains Mono', monospace; min-height: 220px; }
11
- .anchor { position: relative; display: inline-block; }
12
- button[kjButton] { padding: 0.5rem 1.25rem; border: 1px solid #444; background: transparent; color: #f0ede6; cursor: pointer; font-family: inherit; font-size: 0.875rem; }
13
- button[kjButton]:hover { border-color: #b8f500; color: #b8f500; }
14
- [kjMenuContent] {
15
- position: absolute; top: calc(100% + 4px); left: 0; z-index: 20;
16
- background: #1a1a1a; border: 1px solid #333; padding: 0.25rem 0; min-width: 10rem;
17
- display: flex; flex-direction: column;
18
- }
19
- [kjMenuContent][hidden] { display: none; }
20
- [kjMenuItem] {
21
- background: transparent; border: none; color: #f0ede6; font-family: inherit;
22
- font-size: 0.875rem; padding: 0.5rem 1rem; text-align: left; cursor: pointer; width: 100%;
23
- }
24
- [kjMenuItem]:hover, [kjMenuItem]:focus { background: #252525; color: #b8f500; outline: none; }
25
- [kjMenuItem][disabled] { color: #555; cursor: not-allowed; }
26
- `],
27
- template: `
28
- <div class="anchor" kjMenu>
29
- <button kjButton kjMenuTrigger>Actions</button>
30
- <div kjMenuContent>
31
- <button kjMenuItem>Edit</button>
32
- <button kjMenuItem>Duplicate</button>
33
- <button kjMenuItem>Archive</button>
34
- <button kjMenuItem disabled>Delete</button>
35
- </div>
36
- </div>
37
- `,
38
- })
39
- export class MenuBasicExample {}
@@ -1,168 +0,0 @@
1
- import { render, fireEvent } from '@testing-library/angular';
2
- import { axe, toHaveNoViolations } from 'jest-axe';
3
- import { KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjMenuClose } from './menu';
4
-
5
- expect.extend(toHaveNoViolations);
6
-
7
- const imports = [KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjMenuClose];
8
-
9
- const template = `
10
- <div kjMenu>
11
- <button kjMenuTrigger>Actions</button>
12
- <div kjMenuContent>
13
- <button kjMenuItem>Edit</button>
14
- <button kjMenuItem>Delete</button>
15
- </div>
16
- </div>`;
17
-
18
- describe('KjMenu', () => {
19
- it('renders trigger button', async () => {
20
- const { getByRole } = await render(template, { imports });
21
- expect(getByRole('button', { name: 'Actions' })).toBeInTheDocument();
22
- });
23
-
24
- it('trigger has aria-haspopup="menu"', async () => {
25
- const { getByRole } = await render(template, { imports });
26
- expect(getByRole('button', { name: 'Actions' })).toHaveAttribute('aria-haspopup', 'menu');
27
- });
28
-
29
- it('trigger has aria-expanded="false" initially', async () => {
30
- const { getByRole } = await render(template, { imports });
31
- expect(getByRole('button', { name: 'Actions' })).toHaveAttribute('aria-expanded', 'false');
32
- });
33
-
34
- it('menu content is hidden initially', async () => {
35
- const { getByRole } = await render(template, { imports });
36
- const menu = getByRole('menu', { hidden: true });
37
- expect(menu).toHaveAttribute('hidden');
38
- });
39
-
40
- it('opens on trigger click', async () => {
41
- const { getByRole } = await render(template, { imports });
42
- fireEvent.click(getByRole('button', { name: 'Actions' }));
43
- const menu = getByRole('menu');
44
- expect(menu).not.toHaveAttribute('hidden');
45
- });
46
-
47
- it('sets aria-expanded="true" when open', async () => {
48
- const { getByRole } = await render(template, { imports });
49
- fireEvent.click(getByRole('button', { name: 'Actions' }));
50
- expect(getByRole('button', { name: 'Actions' })).toHaveAttribute('aria-expanded', 'true');
51
- });
52
-
53
- it('closes on second trigger click (toggle)', async () => {
54
- const { getByRole } = await render(template, { imports });
55
- const trigger = getByRole('button', { name: 'Actions' });
56
- fireEvent.click(trigger);
57
- fireEvent.click(trigger);
58
- const menu = getByRole('menu', { hidden: true });
59
- expect(menu).toHaveAttribute('hidden');
60
- });
61
-
62
- it('closes on Escape key', async () => {
63
- const { getByRole } = await render(template, { imports });
64
- fireEvent.click(getByRole('button', { name: 'Actions' }));
65
- fireEvent.keyDown(document, { key: 'Escape' });
66
- const menu = getByRole('menu', { hidden: true });
67
- expect(menu).toHaveAttribute('hidden');
68
- });
69
-
70
- it('closes on click outside', async () => {
71
- const { getByRole } = await render(template, { imports });
72
- fireEvent.click(getByRole('button', { name: 'Actions' }));
73
- fireEvent.click(document.body);
74
- const menu = getByRole('menu', { hidden: true });
75
- expect(menu).toHaveAttribute('hidden');
76
- });
77
-
78
- it('closes when a menu item is clicked', async () => {
79
- const { getByRole } = await render(template, { imports });
80
- fireEvent.click(getByRole('button', { name: 'Actions' }));
81
- fireEvent.click(getByRole('menuitem', { name: 'Edit' }));
82
- const menu = getByRole('menu', { hidden: true });
83
- expect(menu).toHaveAttribute('hidden');
84
- });
85
-
86
- it('menu items have role="menuitem"', async () => {
87
- const { getAllByRole } = await render(template, { imports });
88
- fireEvent.click(getAllByRole('button')[0]);
89
- const items = getAllByRole('menuitem');
90
- expect(items).toHaveLength(2);
91
- });
92
-
93
- it('navigates down with ArrowDown', async () => {
94
- const { getByRole, getAllByRole } = await render(template, { imports });
95
- fireEvent.click(getByRole('button', { name: 'Actions' }));
96
- const menu = getByRole('menu');
97
- const items = getAllByRole('menuitem');
98
- items[0].focus();
99
- fireEvent.keyDown(menu, { key: 'ArrowDown' });
100
- expect(document.activeElement).toBe(items[1]);
101
- });
102
-
103
- it('navigates up with ArrowUp', async () => {
104
- const { getByRole, getAllByRole } = await render(template, { imports });
105
- fireEvent.click(getByRole('button', { name: 'Actions' }));
106
- const menu = getByRole('menu');
107
- const items = getAllByRole('menuitem');
108
- items[1].focus();
109
- fireEvent.keyDown(menu, { key: 'ArrowUp' });
110
- expect(document.activeElement).toBe(items[0]);
111
- });
112
-
113
- it('wraps around from last to first on ArrowDown', async () => {
114
- const { getByRole, getAllByRole } = await render(template, { imports });
115
- fireEvent.click(getByRole('button', { name: 'Actions' }));
116
- const menu = getByRole('menu');
117
- const items = getAllByRole('menuitem');
118
- items[items.length - 1].focus();
119
- fireEvent.keyDown(menu, { key: 'ArrowDown' });
120
- expect(document.activeElement).toBe(items[0]);
121
- });
122
-
123
- it('focuses first item on Home key', async () => {
124
- const { getByRole, getAllByRole } = await render(template, { imports });
125
- fireEvent.click(getByRole('button', { name: 'Actions' }));
126
- const menu = getByRole('menu');
127
- const items = getAllByRole('menuitem');
128
- items[1].focus();
129
- fireEvent.keyDown(menu, { key: 'Home' });
130
- expect(document.activeElement).toBe(items[0]);
131
- });
132
-
133
- it('focuses last item on End key', async () => {
134
- const { getByRole, getAllByRole } = await render(template, { imports });
135
- fireEvent.click(getByRole('button', { name: 'Actions' }));
136
- const menu = getByRole('menu');
137
- const items = getAllByRole('menuitem');
138
- items[0].focus();
139
- fireEvent.keyDown(menu, { key: 'End' });
140
- expect(document.activeElement).toBe(items[items.length - 1]);
141
- });
142
-
143
- it('kjMenuClose closes the menu on click', async () => {
144
- const closeTemplate = `
145
- <div kjMenu>
146
- <button kjMenuTrigger>Open</button>
147
- <div kjMenuContent>
148
- <button kjMenuClose>Cancel</button>
149
- </div>
150
- </div>`;
151
- const { getByRole } = await render(closeTemplate, { imports });
152
- fireEvent.click(getByRole('button', { name: 'Open' }));
153
- fireEvent.click(getByRole('button', { name: 'Cancel' }));
154
- const menu = getByRole('menu', { hidden: true });
155
- expect(menu).toHaveAttribute('hidden');
156
- });
157
-
158
- it('passes axe audit when closed', async () => {
159
- const { container } = await render(template, { imports });
160
- expect(await axe(container)).toHaveNoViolations();
161
- });
162
-
163
- it('passes axe audit when open', async () => {
164
- const { container, getByRole } = await render(template, { imports });
165
- fireEvent.click(getByRole('button', { name: 'Actions' }));
166
- expect(await axe(container)).toHaveNoViolations();
167
- });
168
- });
package/src/menu/menu.ts DELETED
@@ -1,176 +0,0 @@
1
- import { Directive, ElementRef, inject, signal } from '@angular/core';
2
- import { KJ_MENU, KjMenuContext } from './menu.context';
3
-
4
- /**
5
- * Root menu container. Manages open/close state and provides context to child directives.
6
- * Pairs with `[kjMenuTrigger]`, `[kjMenuContent]`, and `[kjMenuItem]`.
7
- *
8
- * Click outside or Escape closes the menu. Clicking the trigger toggles it.
9
- * Arrow-key navigation, Home/End, and typeahead are handled by `[kjMenuContent]`.
10
- *
11
- * @example
12
- * ```html
13
- * <div kjMenu>
14
- * <button kjMenuTrigger>Actions</button>
15
- * <div kjMenuContent>
16
- * <button kjMenuItem>Edit</button>
17
- * <button kjMenuItem>Delete</button>
18
- * </div>
19
- * </div>
20
- * ```
21
- * @doc
22
- * @doc-example Basic
23
- * @doc-theme default
24
- * @doc-file menu.example.ts
25
- * @category Core/Navigation
26
- */
27
- @Directive({
28
- selector: '[kjMenu]',
29
- standalone: true,
30
- providers: [{ provide: KJ_MENU, useExisting: KjMenu }],
31
- exportAs: 'kjMenu',
32
- })
33
- export class KjMenu implements KjMenuContext {
34
- private readonly _open = signal(false);
35
- readonly open = this._open.asReadonly();
36
-
37
- toggle(): void { this._open.update(v => !v); }
38
- close(): void { this._open.set(false); }
39
- }
40
-
41
- /**
42
- * Trigger element that toggles the parent menu.
43
- * Automatically sets `aria-haspopup` and `aria-expanded` on the host element.
44
- *
45
- * @category Core/Navigation
46
- */
47
- @Directive({
48
- selector: '[kjMenuTrigger]',
49
- standalone: true,
50
- host: {
51
- '[attr.aria-haspopup]': '"menu"',
52
- '[attr.aria-expanded]': 'ctx.open().toString()',
53
- // stopPropagation prevents the document:click on kjMenuContent from immediately closing
54
- '(click)': '$event.stopPropagation(); ctx.toggle()',
55
- },
56
- })
57
- export class KjMenuTrigger {
58
- readonly ctx = inject(KJ_MENU);
59
- }
60
-
61
- /**
62
- * Menu content panel. Hidden when closed via the `hidden` attribute.
63
- * Provides keyboard navigation: Arrow keys, Home/End, Escape, and typeahead.
64
- * Click-outside dismissal is handled via a document-level click listener.
65
- *
66
- * @category Core/Navigation
67
- */
68
- @Directive({
69
- selector: '[kjMenuContent]',
70
- standalone: true,
71
- host: {
72
- 'role': 'menu',
73
- '[attr.hidden]': '!ctx.open() ? "" : null',
74
- // Prevent clicks inside from propagating to document click handler
75
- '(click)': '$event.stopPropagation()',
76
- // Keyboard navigation within the menu
77
- '(keydown)': 'onKeydown($event)',
78
- // Close on Escape key
79
- '(document:keydown.escape)': 'ctx.open() && ctx.close()',
80
- // Close on any click outside (trigger stopPropagation prevents self-close)
81
- '(document:click)': 'ctx.open() && ctx.close()',
82
- },
83
- })
84
- export class KjMenuContent {
85
- private readonly el = inject(ElementRef<HTMLElement>);
86
- readonly ctx = inject(KJ_MENU);
87
-
88
- onKeydown(e: KeyboardEvent): void {
89
- const items = this.getFocusableItems();
90
- if (!items.length) return;
91
- const current = items.indexOf(document.activeElement as HTMLElement);
92
-
93
- switch (e.key) {
94
- case 'ArrowDown':
95
- e.preventDefault();
96
- items[(current + 1) % items.length].focus();
97
- break;
98
- case 'ArrowUp':
99
- e.preventDefault();
100
- items[(current - 1 + items.length) % items.length].focus();
101
- break;
102
- case 'Home':
103
- e.preventDefault();
104
- items[0].focus();
105
- break;
106
- case 'End':
107
- e.preventDefault();
108
- items[items.length - 1].focus();
109
- break;
110
- default: {
111
- // Type-ahead: jump to next item starting with pressed character
112
- const char = e.key.length === 1 ? e.key.toLowerCase() : null;
113
- if (char) {
114
- const match =
115
- items.find((item, i) =>
116
- i > current && item.textContent?.trim().toLowerCase().startsWith(char)
117
- ) ??
118
- items.find(item =>
119
- item.textContent?.trim().toLowerCase().startsWith(char)
120
- );
121
- match?.focus();
122
- }
123
- }
124
- }
125
- }
126
-
127
- private getFocusableItems(): HTMLElement[] {
128
- const nodes = this.el.nativeElement.querySelectorAll('[kjMenuItem]');
129
- return (Array.from(nodes) as HTMLElement[]).filter(el => !el.hasAttribute('disabled'));
130
- }
131
- }
132
-
133
- /**
134
- * Individual menu item. Closes the menu on click or keyboard activation (Enter/Space).
135
- *
136
- * @example
137
- * ```html
138
- * <button kjMenuItem>Delete</button>
139
- * ```
140
- * @category Core/Navigation
141
- */
142
- @Directive({
143
- selector: '[kjMenuItem]',
144
- standalone: true,
145
- host: {
146
- 'role': 'menuitem',
147
- '[attr.tabindex]': '"0"',
148
- '(click)': 'ctx.close()',
149
- '(keydown.enter)': '$event.preventDefault(); ctx.close()',
150
- '(keydown.space)': '$event.preventDefault(); ctx.close()',
151
- },
152
- })
153
- export class KjMenuItem {
154
- readonly ctx = inject(KJ_MENU);
155
- }
156
-
157
- /**
158
- * Closes the parent menu when clicked. Convenience directive for non-item close buttons.
159
- * Place anywhere inside `[kjMenu]`.
160
- *
161
- * @example
162
- * ```html
163
- * <button kjMenuClose>Cancel</button>
164
- * ```
165
- * @category Core/Navigation
166
- */
167
- @Directive({
168
- selector: '[kjMenuClose]',
169
- standalone: true,
170
- host: {
171
- '(click)': 'ctx.close()',
172
- },
173
- })
174
- export class KjMenuClose {
175
- readonly ctx = inject(KJ_MENU);
176
- }
@@ -1,12 +0,0 @@
1
- export {
2
- KjPopover,
3
- KjPopoverTrigger,
4
- KjPopoverContent,
5
- KjPopoverClose,
6
- } from './popover';
7
- export {
8
- KJ_POPOVER,
9
- type KjPopoverContext,
10
- type KjPopoverSide,
11
- type KjPopoverAlign,
12
- } from './popover.context';
@@ -1,18 +0,0 @@
1
- import { InjectionToken, Signal, InputSignal } from '@angular/core';
2
-
3
- /** Preferred side for the popover relative to its trigger. */
4
- export type KjPopoverSide = 'top' | 'bottom' | 'left' | 'right';
5
- /** Alignment of the popover along the cross-axis. */
6
- export type KjPopoverAlign = 'start' | 'center' | 'end';
7
-
8
- /** Context shared between popover directives. */
9
- export interface KjPopoverContext {
10
- readonly open: Signal<boolean>;
11
- readonly popoverId: string;
12
- kjPopoverSide: Signal<KjPopoverSide> | InputSignal<KjPopoverSide>;
13
- kjPopoverAlign: Signal<KjPopoverAlign> | InputSignal<KjPopoverAlign>;
14
- toggle: () => void;
15
- hide: () => void;
16
- }
17
-
18
- export const KJ_POPOVER = new InjectionToken<KjPopoverContext>('KjPopover');
@@ -1,50 +0,0 @@
1
- import { Component } from '@angular/core';
2
- import {
3
- KjPopover, KjPopoverTrigger,
4
- KjPopoverContent, KjPopoverClose,
5
- } from './popover';
6
- import { KjButton } from '../button/button';
7
-
8
- @Component({
9
- selector: 'kj-example-popover-basic',
10
- standalone: true,
11
- imports: [
12
- KjPopover, KjPopoverTrigger,
13
- KjPopoverContent, KjPopoverClose,
14
- KjButton,
15
- ],
16
- styleUrls: ['../styles/docs-themes.css'],
17
- styles: [`
18
- :host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: var(--kj-bg); font-family: var(--kj-font); min-height: 220px; }
19
- .anchor { position: relative; display: inline-block; }
20
- button[kjButton] { padding: 0.5rem 1.25rem; border: 1px solid var(--kj-border); background: transparent; color: var(--kj-text); cursor: pointer; font-family: inherit; font-size: 0.875rem; }
21
- button[kjButton]:hover { border-color: var(--kj-accent); color: var(--kj-accent); }
22
- [kjPopoverContent] {
23
- position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
24
- background: var(--kj-surface); border: 1px solid var(--kj-border); padding: 1rem; min-width: 14rem;
25
- color: var(--kj-text);
26
- }
27
- [kjPopoverContent][hidden] { display: none; }
28
- [kjPopoverContent][data-align="end"] { left: auto; right: 0; }
29
- .popover-title { font-size: 0.875rem; font-weight: 600; margin: 0 0 0.5rem; color: var(--kj-accent); }
30
- .popover-body { font-size: 0.8125rem; color: var(--kj-text-muted); margin: 0 0 1rem; line-height: 1.5; }
31
- .popover-footer { display: flex; justify-content: flex-end; gap: 0.5rem; }
32
- .popover-footer button[kjButton] { padding: 0.35rem 0.875rem; font-size: 0.8125rem; }
33
- [data-variant="default"] { border-color: var(--kj-accent); color: var(--kj-accent-on); background: var(--kj-accent); }
34
- [data-variant="ghost"] { border-color: transparent; background: transparent; color: var(--kj-text-muted); }
35
- `],
36
- template: `
37
- <div class="anchor" kjPopover>
38
- <button kjButton kjPopoverTrigger>Open Popover</button>
39
- <div kjPopoverContent>
40
- <p class="popover-title">Notification Settings</p>
41
- <p class="popover-body">Control how and when you receive notifications from this app.</p>
42
- <div class="popover-footer">
43
- <button kjButton kjPopoverClose [kjVariant]="'ghost'">Cancel</button>
44
- <button kjButton kjPopoverClose [kjVariant]="'default'">Save</button>
45
- </div>
46
- </div>
47
- </div>
48
- `,
49
- })
50
- export class PopoverBasicExample {}
@@ -1,52 +0,0 @@
1
- import { Component } from '@angular/core';
2
- import {
3
- KjPopover, KjPopoverTrigger,
4
- KjPopoverContent, KjPopoverClose,
5
- } from './popover';
6
-
7
- @Component({
8
- selector: 'kj-example-popover-finance',
9
- standalone: true,
10
- imports: [
11
- KjPopover, KjPopoverTrigger,
12
- KjPopoverContent, KjPopoverClose,
13
- ],
14
- styleUrls: ['../styles/docs-themes.css'],
15
- host: { class: 'kj-theme-finance' },
16
- styles: [`
17
- :host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: var(--kj-bg); font-family: var(--kj-font); min-height: 220px; }
18
- .anchor { position: relative; display: inline-block; }
19
- button {
20
- padding: 0.4rem 1rem; background: var(--kj-accent); color: var(--kj-accent-on); border: 1px solid var(--kj-accent);
21
- border-radius: var(--kj-radius-md); cursor: pointer; font-family: inherit; font-size: 0.875rem; font-weight: 500;
22
- }
23
- button:hover { background: #2563eb; }
24
- [kjPopoverContent] {
25
- position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
26
- background: var(--kj-surface); border: 1px solid var(--kj-border); border-radius: var(--kj-radius-lg);
27
- box-shadow: var(--kj-shadow-md); padding: 1rem; min-width: 14rem;
28
- }
29
- [kjPopoverContent][hidden] { display: none; }
30
- .popover-title { font-size: 0.875rem; font-weight: 600; color: var(--kj-text); margin: 0 0 0.375rem; }
31
- .popover-body { font-size: 0.8125rem; color: var(--kj-text-muted); margin: 0 0 1rem; line-height: 1.5; }
32
- .popover-footer { display: flex; justify-content: flex-end; gap: 0.5rem; }
33
- .btn-cancel { padding: 0.35rem 0.875rem; background: var(--kj-surface); color: #374151; border: 1px solid var(--kj-border); border-radius: var(--kj-radius-sm); cursor: pointer; font-size: 0.8125rem; font-weight: 500; }
34
- .btn-cancel:hover { background: #f3f4f6; }
35
- .btn-primary { padding: 0.35rem 0.875rem; background: var(--kj-accent); color: var(--kj-accent-on); border: 1px solid var(--kj-accent); border-radius: var(--kj-radius-sm); cursor: pointer; font-size: 0.8125rem; font-weight: 500; }
36
- .btn-primary:hover { background: #2563eb; }
37
- `],
38
- template: `
39
- <div class="anchor" kjPopover>
40
- <button kjPopoverTrigger>Preferences</button>
41
- <div kjPopoverContent>
42
- <p class="popover-title">Notification Preferences</p>
43
- <p class="popover-body">Choose how you receive alerts and account notifications.</p>
44
- <div class="popover-footer">
45
- <button class="btn-cancel" kjPopoverClose>Cancel</button>
46
- <button class="btn-primary" kjPopoverClose>Apply</button>
47
- </div>
48
- </div>
49
- </div>
50
- `,
51
- })
52
- export class PopoverFinanceExample {}
@@ -1,53 +0,0 @@
1
- import { Component } from '@angular/core';
2
- import {
3
- KjPopover, KjPopoverTrigger,
4
- KjPopoverContent, KjPopoverClose,
5
- } from './popover';
6
-
7
- @Component({
8
- selector: 'kj-example-popover-retro',
9
- standalone: true,
10
- imports: [
11
- KjPopover, KjPopoverTrigger,
12
- KjPopoverContent, KjPopoverClose,
13
- ],
14
- styleUrls: ['../styles/docs-themes.css'],
15
- host: { class: 'kj-theme-retro' },
16
- styles: [`
17
- :host { display: flex; align-items: flex-start; justify-content: center; padding: 3rem 2rem 10rem; background: var(--kj-bg); font-family: var(--kj-font); min-height: 220px; color: var(--kj-text); }
18
- .anchor { position: relative; display: inline-block; }
19
- button {
20
- padding: 0.4rem 1rem; font-family: var(--kj-font); font-size: 0.8rem; font-weight: 700;
21
- letter-spacing: 0.06em; text-transform: uppercase; background: var(--kj-surface); color: var(--kj-text);
22
- border: var(--kj-btn-border); border-radius: 0; cursor: pointer;
23
- box-shadow: var(--kj-shadow-sm); transition: var(--kj-transition);
24
- }
25
- button:hover { transform: translate(-1px, -1px); box-shadow: var(--kj-shadow-md); }
26
- [kjPopoverContent] {
27
- position: absolute; top: calc(100% + 8px); left: 0; z-index: 20;
28
- background: var(--kj-bg); border: var(--kj-btn-border); box-shadow: var(--kj-shadow-hard);
29
- padding: 1rem; min-width: 14rem; font-family: var(--kj-font);
30
- }
31
- [kjPopoverContent][hidden] { display: none; }
32
- .popover-title { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 0.5rem; border-bottom: var(--kj-btn-border); padding-bottom: 0.375rem; color: var(--kj-text); }
33
- .popover-body { font-size: 0.75rem; color: var(--kj-text-muted); margin: 0.5rem 0 1rem; line-height: 1.6; }
34
- .popover-footer { display: flex; justify-content: flex-end; gap: 0.5rem; }
35
- .popover-footer button { padding: 0.3rem 0.75rem; font-size: 0.7rem; box-shadow: var(--kj-shadow-sm); }
36
- .popover-footer button:hover { box-shadow: var(--kj-shadow-md); }
37
- .btn-primary { background: var(--kj-accent); color: var(--kj-accent-on); border-color: var(--kj-border); }
38
- `],
39
- template: `
40
- <div class="anchor" kjPopover>
41
- <button kjPopoverTrigger>Settings</button>
42
- <div kjPopoverContent>
43
- <p class="popover-title">Notifications</p>
44
- <p class="popover-body">Configure notification frequency and delivery preferences.</p>
45
- <div class="popover-footer">
46
- <button kjPopoverClose>Cancel</button>
47
- <button kjPopoverClose class="btn-primary">Save</button>
48
- </div>
49
- </div>
50
- </div>
51
- `,
52
- })
53
- export class PopoverRetroExample {}