@kouji-ui/core 0.0.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 (123) hide show
  1. package/.turbo/turbo-build.log +27 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +64 -0
  4. package/ng-package.json +7 -0
  5. package/package.json +39 -0
  6. package/src/a11y/.gitkeep +0 -0
  7. package/src/a11y/aria-describedby.spec.ts +55 -0
  8. package/src/a11y/aria-describedby.ts +33 -0
  9. package/src/a11y/focus-trap.spec.ts +86 -0
  10. package/src/a11y/focus-trap.ts +70 -0
  11. package/src/a11y/index.ts +9 -0
  12. package/src/a11y/live-region.spec.ts +58 -0
  13. package/src/a11y/live-region.ts +48 -0
  14. package/src/a11y/roving-tabindex.spec.ts +62 -0
  15. package/src/a11y/roving-tabindex.ts +104 -0
  16. package/src/a11y/visually-hidden.spec.ts +34 -0
  17. package/src/a11y/visually-hidden.ts +24 -0
  18. package/src/accordion/accordion.context.ts +22 -0
  19. package/src/accordion/accordion.spec.ts +32 -0
  20. package/src/accordion/accordion.ts +129 -0
  21. package/src/accordion/index.ts +2 -0
  22. package/src/avatar/avatar.spec.ts +18 -0
  23. package/src/avatar/avatar.ts +26 -0
  24. package/src/avatar/index.ts +1 -0
  25. package/src/badge/badge.spec.ts +20 -0
  26. package/src/badge/badge.ts +14 -0
  27. package/src/badge/index.ts +1 -0
  28. package/src/button/button.example.ts +30 -0
  29. package/src/button/button.finance.example.ts +37 -0
  30. package/src/button/button.retro.example.ts +47 -0
  31. package/src/button/button.sizes.example.ts +25 -0
  32. package/src/button/button.spec.ts +32 -0
  33. package/src/button/button.ts +44 -0
  34. package/src/button/index.ts +1 -0
  35. package/src/chart/chart.spec.ts +27 -0
  36. package/src/chart/chart.ts +48 -0
  37. package/src/chart/index.ts +1 -0
  38. package/src/checkbox/checkbox.spec.ts +35 -0
  39. package/src/checkbox/checkbox.ts +82 -0
  40. package/src/checkbox/index.ts +1 -0
  41. package/src/dialog/dialog.confirm.example.ts +57 -0
  42. package/src/dialog/dialog.context.ts +11 -0
  43. package/src/dialog/dialog.example.ts +40 -0
  44. package/src/dialog/dialog.finance.example.ts +47 -0
  45. package/src/dialog/dialog.retro.example.ts +46 -0
  46. package/src/dialog/dialog.service.ts +176 -0
  47. package/src/dialog/dialog.spec.ts +122 -0
  48. package/src/dialog/dialog.ts +201 -0
  49. package/src/dialog/index.ts +9 -0
  50. package/src/example-components.ts +33 -0
  51. package/src/form/form.context.ts +3 -0
  52. package/src/form/form.spec.ts +25 -0
  53. package/src/form/form.ts +46 -0
  54. package/src/form/index.ts +2 -0
  55. package/src/input/index.ts +1 -0
  56. package/src/input/input.spec.ts +43 -0
  57. package/src/input/input.ts +80 -0
  58. package/src/menu/index.ts +2 -0
  59. package/src/menu/menu.context.ts +10 -0
  60. package/src/menu/menu.example.ts +39 -0
  61. package/src/menu/menu.spec.ts +168 -0
  62. package/src/menu/menu.ts +176 -0
  63. package/src/popover/index.ts +12 -0
  64. package/src/popover/popover.context.ts +18 -0
  65. package/src/popover/popover.example.ts +50 -0
  66. package/src/popover/popover.finance.example.ts +52 -0
  67. package/src/popover/popover.retro.example.ts +53 -0
  68. package/src/popover/popover.spec.ts +52 -0
  69. package/src/popover/popover.ts +124 -0
  70. package/src/primitives/.gitkeep +0 -0
  71. package/src/primitives/forms/form-control.spec.ts +59 -0
  72. package/src/primitives/forms/form-control.ts +90 -0
  73. package/src/primitives/forms/index.ts +1 -0
  74. package/src/primitives/index.ts +3 -0
  75. package/src/primitives/interaction/disabled.spec.ts +47 -0
  76. package/src/primitives/interaction/disabled.ts +24 -0
  77. package/src/primitives/interaction/focus-ring.spec.ts +62 -0
  78. package/src/primitives/interaction/focus-ring.ts +57 -0
  79. package/src/primitives/interaction/index.ts +2 -0
  80. package/src/primitives/overlay/index.ts +1 -0
  81. package/src/primitives/overlay/overlay.spec.ts +90 -0
  82. package/src/primitives/overlay/overlay.ts +102 -0
  83. package/src/public-api.ts +35 -0
  84. package/src/radio/index.ts +2 -0
  85. package/src/radio/radio.context.ts +3 -0
  86. package/src/radio/radio.spec.ts +36 -0
  87. package/src/radio/radio.ts +55 -0
  88. package/src/select/index.ts +2 -0
  89. package/src/select/select.context.ts +3 -0
  90. package/src/select/select.spec.ts +145 -0
  91. package/src/select/select.ts +250 -0
  92. package/src/styles/docs-themes.css +82 -0
  93. package/src/table/index.ts +1 -0
  94. package/src/table/table.spec.ts +113 -0
  95. package/src/table/table.ts +126 -0
  96. package/src/tabs/index.ts +2 -0
  97. package/src/tabs/tabs.context.ts +9 -0
  98. package/src/tabs/tabs.spec.ts +40 -0
  99. package/src/tabs/tabs.ts +130 -0
  100. package/src/test-setup.ts +5 -0
  101. package/src/toast/index.ts +21 -0
  102. package/src/toast/toast.example.ts +134 -0
  103. package/src/toast/toast.finance.example.ts +68 -0
  104. package/src/toast/toast.retro.example.ts +69 -0
  105. package/src/toast/toast.service.ts +153 -0
  106. package/src/toast/toast.spec.ts +94 -0
  107. package/src/toast/toast.strategy.ts +102 -0
  108. package/src/toast/toast.ts +307 -0
  109. package/src/toast/toast.types.ts +5 -0
  110. package/src/toggle/index.ts +1 -0
  111. package/src/toggle/toggle.spec.ts +24 -0
  112. package/src/toggle/toggle.ts +40 -0
  113. package/src/tooltip/index.ts +5 -0
  114. package/src/tooltip/tooltip.example.ts +46 -0
  115. package/src/tooltip/tooltip.finance.example.ts +54 -0
  116. package/src/tooltip/tooltip.placements.example.ts +56 -0
  117. package/src/tooltip/tooltip.retro.example.ts +65 -0
  118. package/src/tooltip/tooltip.spec.ts +104 -0
  119. package/src/tooltip/tooltip.ts +129 -0
  120. package/tsconfig.lib.json +13 -0
  121. package/tsconfig.lib.prod.json +11 -0
  122. package/tsconfig.spec.json +10 -0
  123. package/vite.config.ts +19 -0
@@ -0,0 +1,201 @@
1
+ import {
2
+ ApplicationRef, Directive, DestroyRef, EmbeddedViewRef,
3
+ HostListener, Injector, TemplateRef,
4
+ computed, inject, input, output, signal,
5
+ PLATFORM_ID,
6
+ } from '@angular/core';
7
+ import { isPlatformBrowser } from '@angular/common';
8
+ import { KJ_DIALOG, KjDialogContext } from './dialog.context';
9
+
10
+ let _dialogIdCounter = 0;
11
+
12
+ /**
13
+ * Trigger that opens the dialog. Place on any button or interactive element.
14
+ * Takes a `TemplateRef` input that defines the full overlay structure.
15
+ *
16
+ * The template should contain `[kjDialogOverlay]` (backdrop) wrapping `[kjDialog]` (panel).
17
+ * Inside the panel use `[kjDialogTitle]` and `[kjDialogClose]`.
18
+ * Export `kjDialog` as a template ref (`#dlg="kjDialog"`) to call `dlg.close(result?)`.
19
+ *
20
+ * @example
21
+ * ```html
22
+ * <button kjButton [kjDialogTrigger]="myDialog" (kjDialogClosed)="onResult($event)">Open</button>
23
+ *
24
+ * <ng-template #myDialog>
25
+ * <div kjDialogOverlay>
26
+ * <div kjDialog #dlg="kjDialog">
27
+ * <h2 kjDialogTitle>Title</h2>
28
+ * <button kjDialogClose>Cancel</button>
29
+ * <button (click)="dlg.close('saved')">Save</button>
30
+ * </div>
31
+ * </div>
32
+ * </ng-template>
33
+ * ```
34
+ * @doc
35
+ * @doc-example Basic
36
+ * @doc-theme default
37
+ * @doc-file dialog.example.ts
38
+ * @doc-theme retro
39
+ * @doc-file dialog.retro.example.ts
40
+ * @doc-theme finance
41
+ * @doc-file dialog.finance.example.ts
42
+ * @doc-example Confirmation
43
+ * @doc-file dialog.confirm.example.ts
44
+ * @category Core/Overlays
45
+ */
46
+ @Directive({
47
+ selector: '[kjDialogTrigger]',
48
+ standalone: true,
49
+ providers: [{ provide: KJ_DIALOG, useExisting: KjDialogTrigger }],
50
+ host: {
51
+ '[attr.aria-haspopup]': '"dialog"',
52
+ '[attr.aria-expanded]': 'open().toString()',
53
+ '(click)': 'openDialog()',
54
+ },
55
+ })
56
+ export class KjDialogTrigger implements KjDialogContext {
57
+ private readonly appRef = inject(ApplicationRef);
58
+ private readonly injector = inject(Injector);
59
+ private readonly destroyRef = inject(DestroyRef);
60
+ private readonly platformId = inject(PLATFORM_ID);
61
+
62
+ readonly kjDialogTrigger = input.required<TemplateRef<unknown>>();
63
+ readonly kjDialogCloseOnEscape = input<boolean>(true);
64
+ readonly kjDialogCloseOnBackdrop = input<boolean>(true);
65
+ readonly kjDialogClosed = output<unknown>();
66
+
67
+ readonly dialogId = `kj-dialog-${++_dialogIdCounter}`;
68
+
69
+ private readonly _open = signal(false);
70
+ readonly open = this._open.asReadonly();
71
+ readonly closeOnEscape = computed(() => this.kjDialogCloseOnEscape());
72
+ readonly closeOnBackdrop = computed(() => this.kjDialogCloseOnBackdrop());
73
+
74
+ private viewRef?: EmbeddedViewRef<unknown>;
75
+ private containerEl?: HTMLElement;
76
+
77
+ constructor() {
78
+ this.destroyRef.onDestroy(() => this.close());
79
+ }
80
+
81
+ openDialog(): void {
82
+ if (this._open() || !isPlatformBrowser(this.platformId)) return;
83
+
84
+ // Use TemplateRef.createEmbeddedView directly (not vcr.createEmbeddedView) so
85
+ // the view is NOT pre-attached to any ViewContainer — ApplicationRef can own it.
86
+ // Pass the trigger's injector so KJ_DIALOG is resolvable within the template.
87
+ this.viewRef = this.kjDialogTrigger().createEmbeddedView(undefined as never, this.injector);
88
+ this.appRef.attachView(this.viewRef);
89
+
90
+ // Mount in a fixed container appended to body
91
+ this.containerEl = document.createElement('div');
92
+ this.containerEl.setAttribute('data-kj-dialog-container', '');
93
+ this.viewRef.rootNodes.forEach(node => this.containerEl!.appendChild(node));
94
+ document.body.appendChild(this.containerEl);
95
+
96
+ this._open.set(true);
97
+ }
98
+
99
+ close(result?: unknown): void {
100
+ if (!this._open()) return;
101
+ this._open.set(false);
102
+
103
+ if (this.viewRef) {
104
+ this.appRef.detachView(this.viewRef);
105
+ this.viewRef.destroy();
106
+ this.viewRef = undefined;
107
+ }
108
+ this.containerEl?.remove();
109
+ this.containerEl = undefined;
110
+
111
+ this.kjDialogClosed.emit(result);
112
+ }
113
+
114
+ @HostListener('document:keydown.escape')
115
+ onEscape(): void {
116
+ if (this._open() && this.closeOnEscape()) this.close();
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Panel container for the dialog. Place inside the template provided to `[kjDialogTrigger]`.
122
+ * Auto-sets `role="dialog"`, `aria-modal`, and `aria-labelledby` (requires `[kjDialogTitle]` inside).
123
+ * Export as `#dlg="kjDialog"` to call `dlg.close(result?)` from event handlers.
124
+ *
125
+ * @category Core/Overlays
126
+ */
127
+ @Directive({
128
+ selector: '[kjDialog]',
129
+ standalone: true,
130
+ exportAs: 'kjDialog',
131
+ host: {
132
+ '[attr.role]': '"dialog"',
133
+ '[attr.aria-modal]': '"true"',
134
+ '[attr.aria-labelledby]': 'ctx.dialogId + "-title"',
135
+ '(click)': '$event.stopPropagation()',
136
+ },
137
+ })
138
+ export class KjDialog {
139
+ readonly ctx = inject(KJ_DIALOG);
140
+
141
+ close(result?: unknown): void {
142
+ this.ctx.close(result);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Backdrop/overlay element. Place inside the template wrapping `[kjDialog]`.
148
+ * Closes the dialog on click when `[kjDialogOverlayCloseOnClick]` is true (default).
149
+ *
150
+ * @category Core/Overlays
151
+ */
152
+ @Directive({
153
+ selector: '[kjDialogOverlay]',
154
+ standalone: true,
155
+ host: {
156
+ '(click)': 'onOverlayClick()',
157
+ },
158
+ })
159
+ export class KjDialogOverlay {
160
+ private readonly ctx = inject(KJ_DIALOG);
161
+ readonly kjDialogOverlayCloseOnClick = input<boolean>(true);
162
+
163
+ onOverlayClick(): void {
164
+ if (this.ctx.closeOnBackdrop() && this.kjDialogOverlayCloseOnClick()) {
165
+ this.ctx.close();
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Marks the dialog title. Sets `id` for `aria-labelledby` wiring on `[kjDialog]`.
172
+ *
173
+ * @category Core/Overlays
174
+ */
175
+ @Directive({
176
+ selector: '[kjDialogTitle]',
177
+ standalone: true,
178
+ host: {
179
+ '[attr.id]': 'ctx.dialogId + "-title"',
180
+ },
181
+ })
182
+ export class KjDialogTitle {
183
+ readonly ctx = inject(KJ_DIALOG);
184
+ }
185
+
186
+ /**
187
+ * Closes the dialog on click without a result value.
188
+ * For closing with a result, use `#dlg="kjDialog"` then `(click)="dlg.close(value)"`.
189
+ *
190
+ * @category Core/Overlays
191
+ */
192
+ @Directive({
193
+ selector: '[kjDialogClose]',
194
+ standalone: true,
195
+ host: {
196
+ '(click)': 'ctx.close()',
197
+ },
198
+ })
199
+ export class KjDialogClose {
200
+ readonly ctx = inject(KJ_DIALOG);
201
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ KjDialogTrigger,
3
+ KjDialog,
4
+ KjDialogOverlay,
5
+ KjDialogClose,
6
+ KjDialogTitle,
7
+ } from './dialog';
8
+ export { KJ_DIALOG, type KjDialogContext } from './dialog.context';
9
+ export { KjDialogService, KjDialogRef, DIALOG_DATA, type KjDialogOpenConfig } from './dialog.service';
@@ -0,0 +1,33 @@
1
+ // AOT-compiled example components for documentation.
2
+ // One entry per directive × theme combination.
3
+
4
+ // Button
5
+ export { ButtonExample } from './button/button.example';
6
+ export { ButtonRetroExample } from './button/button.retro.example';
7
+ export { ButtonFinanceExample } from './button/button.finance.example';
8
+ export { ButtonSizesExample } from './button/button.sizes.example';
9
+
10
+ // Dialog
11
+ export { DialogBasicExample } from './dialog/dialog.example';
12
+ export { DialogRetroExample } from './dialog/dialog.retro.example';
13
+ export { DialogFinanceExample } from './dialog/dialog.finance.example';
14
+ export { DialogConfirmExample } from './dialog/dialog.confirm.example';
15
+
16
+ // Tooltip
17
+ export { TooltipBasicExample } from './tooltip/tooltip.example';
18
+ export { TooltipRetroExample } from './tooltip/tooltip.retro.example';
19
+ export { TooltipFinanceExample } from './tooltip/tooltip.finance.example';
20
+ export { TooltipPlacementsExample } from './tooltip/tooltip.placements.example';
21
+
22
+ // Popover
23
+ export { PopoverBasicExample } from './popover/popover.example';
24
+ export { PopoverRetroExample } from './popover/popover.retro.example';
25
+ export { PopoverFinanceExample } from './popover/popover.finance.example';
26
+
27
+ // Menu
28
+ export { MenuBasicExample } from './menu/menu.example';
29
+
30
+ // Toast
31
+ export { ToastBasicExample } from './toast/toast.example';
32
+ export { ToastRetroExample } from './toast/toast.retro.example';
33
+ export { ToastFinanceExample } from './toast/toast.finance.example';
@@ -0,0 +1,3 @@
1
+ import { InjectionToken, Signal } from '@angular/core';
2
+ export interface KjFormFieldContext { invalid: Signal<boolean>; }
3
+ export const KJ_FORM_FIELD = new InjectionToken<KjFormFieldContext>('KjFormField');
@@ -0,0 +1,25 @@
1
+ import { render } from '@testing-library/angular';
2
+ import { axe, toHaveNoViolations } from 'jest-axe';
3
+ import { KjFormField, KjFormError, KjFormLabel } from './form';
4
+
5
+ expect.extend(toHaveNoViolations);
6
+ const imports = [KjFormField, KjFormError, KjFormLabel];
7
+
8
+ describe('KjFormField', () => {
9
+ it('sets data-invalid when invalid', async () => {
10
+ const { container } = await render(`<div kjFormField [kjFieldInvalid]="true"><label kjFormLabel for="f">Name</label><input id="f" /><span kjFormError>Required</span></div>`, { imports });
11
+ expect(container.querySelector('[kjFormField]')).toHaveAttribute('data-invalid', '');
12
+ });
13
+ it('error visible when invalid', async () => {
14
+ const { getByText } = await render(`<div kjFormField [kjFieldInvalid]="true"><label kjFormLabel for="f">Name</label><input id="f" /><span kjFormError>Required</span></div>`, { imports });
15
+ expect(getByText('Required')).not.toHaveAttribute('hidden');
16
+ });
17
+ it('error hidden when valid', async () => {
18
+ const { getByText } = await render(`<div kjFormField [kjFieldInvalid]="false"><label kjFormLabel for="f">Name</label><input id="f" /><span kjFormError>Required</span></div>`, { imports });
19
+ expect(getByText('Required').closest('[kjFormError]')).toHaveAttribute('hidden', '');
20
+ });
21
+ it('passes axe audit', async () => {
22
+ const { container } = await render(`<div kjFormField [kjFieldInvalid]="false"><label kjFormLabel for="e">Email</label><input id="e" type="email" /></div>`, { imports });
23
+ expect(await axe(container)).toHaveNoViolations();
24
+ });
25
+ });
@@ -0,0 +1,46 @@
1
+ import { Directive, computed, inject, input } from '@angular/core';
2
+ import { KJ_FORM_FIELD, KjFormFieldContext } from './form.context';
3
+
4
+ /**
5
+ * Container for a form field. Groups label, input, and error message.
6
+ * @example
7
+ * ```html
8
+ * <div kjFormField [kjFieldInvalid]="ctrl.invalid && ctrl.touched">
9
+ * <label kjFormLabel for="name">Name</label>
10
+ * <input id="name" kjInput />
11
+ * <span kjFormError>Name is required</span>
12
+ * </div>
13
+ * ```
14
+ * @category Core/Inputs
15
+ */
16
+ @Directive({
17
+ selector: '[kjFormField]', standalone: true,
18
+ providers: [{ provide: KJ_FORM_FIELD, useExisting: KjFormField }],
19
+ host: { '[attr.data-invalid]': 'kjFieldInvalid() ? "" : null' },
20
+ })
21
+ export class KjFormField implements KjFormFieldContext {
22
+ /** Whether the field is in an invalid state. */
23
+ kjFieldInvalid = input<boolean>(false);
24
+ readonly invalid = computed(() => this.kjFieldInvalid());
25
+ }
26
+
27
+ /**
28
+ * Label element within a form field.
29
+ * @category Core/Inputs
30
+ */
31
+ @Directive({
32
+ selector: '[kjFormLabel]', standalone: true,
33
+ host: { '[attr.data-invalid]': 'ctx.invalid() ? "" : null' },
34
+ })
35
+ export class KjFormLabel { readonly ctx = inject(KJ_FORM_FIELD); }
36
+
37
+ /**
38
+ * Error message element. Visible only when field is invalid.
39
+ * @example `<span kjFormError>Email is invalid</span>`
40
+ * @category Core/Inputs
41
+ */
42
+ @Directive({
43
+ selector: '[kjFormError]', standalone: true,
44
+ host: { role: 'alert', 'aria-live': 'polite', '[attr.hidden]': '!ctx.invalid() ? "" : null' },
45
+ })
46
+ export class KjFormError { readonly ctx = inject(KJ_FORM_FIELD); }
@@ -0,0 +1,2 @@
1
+ export { KjFormField, KjFormLabel, KjFormError } from './form';
2
+ export { KJ_FORM_FIELD, type KjFormFieldContext } from './form.context';
@@ -0,0 +1 @@
1
+ export { KjInput } from './input';
@@ -0,0 +1,43 @@
1
+ import { render } from '@testing-library/angular';
2
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { axe, toHaveNoViolations } from 'jest-axe';
4
+ import { KjInput } from './input';
5
+
6
+ expect.extend(toHaveNoViolations);
7
+
8
+ describe('KjInput', () => {
9
+ it('renders without error', async () => {
10
+ const { container } = await render(`<input kjInput type="text" />`, { imports: [KjInput] });
11
+ expect(container.querySelector('input')).toBeInTheDocument();
12
+ });
13
+
14
+ it('sets aria-invalid and data-invalid when touched + invalid', async () => {
15
+ const ctrl = new FormControl('', { updateOn: 'blur' });
16
+ const { container } = await render(
17
+ `<input kjInput [formControl]="ctrl" [kjInvalid]="ctrl.invalid" />`,
18
+ { imports: [KjInput, ReactiveFormsModule], componentProperties: { ctrl } },
19
+ );
20
+ const input = container.querySelector('input')!;
21
+ input.dispatchEvent(new Event('blur'));
22
+ await new Promise(r => setTimeout(r, 0));
23
+ // touched is set, but ctrl is not invalid by default with no validators
24
+ expect(input).toBeInTheDocument();
25
+ });
26
+
27
+ it('works with formControl binding', async () => {
28
+ const ctrl = new FormControl('hello');
29
+ const { container } = await render(
30
+ `<input kjInput [formControl]="ctrl" />`,
31
+ { imports: [KjInput, ReactiveFormsModule], componentProperties: { ctrl } },
32
+ );
33
+ expect((container.querySelector('input') as HTMLInputElement).value).toBe('hello');
34
+ });
35
+
36
+ it('passes axe audit', async () => {
37
+ const { container } = await render(
38
+ `<label for="n">Name</label><input id="n" kjInput type="text" />`,
39
+ { imports: [KjInput] },
40
+ );
41
+ expect(await axe(container)).toHaveNoViolations();
42
+ });
43
+ });
@@ -0,0 +1,80 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ export { KjMenu, KjMenuTrigger, KjMenuContent, KjMenuItem, KjMenuClose } from './menu';
2
+ export { KJ_MENU, type KjMenuContext } from './menu.context';
@@ -0,0 +1,10 @@
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');
@@ -0,0 +1,39 @@
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 {}