@kraft.stitch/angular 0.0.0-stage → 0.0.1

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 (51) hide show
  1. package/.github/workflows/npm-publish.yml +33 -0
  2. package/README.md +2 -3
  3. package/ng-package.json +10 -0
  4. package/package.json +27 -4
  5. package/src/lib/Buttons/button/button.ts +152 -0
  6. package/src/lib/Buttons/button-group/button-group.ts +129 -0
  7. package/src/lib/Buttons/chips/chips.ts +131 -0
  8. package/src/lib/Buttons/fab/fab.ts +96 -0
  9. package/src/lib/Calendars/datepicker/datepicker.ts +105 -0
  10. package/src/lib/Calendars/daterange/daterange.ts +112 -0
  11. package/src/lib/Calendars/timepicker/timepicker.ts +117 -0
  12. package/src/lib/Dropdowns/dropdown/dropdown.ts +121 -0
  13. package/src/lib/Dropdowns/listbox/listbox.ts +116 -0
  14. package/src/lib/Inputs/checkbox/checkbox.ts +126 -0
  15. package/src/lib/Inputs/colour-picker/colour-picker.ts +101 -0
  16. package/src/lib/Inputs/input/input.ts +139 -0
  17. package/src/lib/Inputs/otp-input/otp-input.ts +113 -0
  18. package/src/lib/Inputs/radio/radio.ts +115 -0
  19. package/src/lib/Inputs/rating/rating.ts +104 -0
  20. package/src/lib/Inputs/slider/slider.ts +121 -0
  21. package/src/lib/Inputs/switch/switch.ts +100 -0
  22. package/src/lib/Inputs/textarea/textarea.ts +129 -0
  23. package/src/lib/Inputs/theme-switch/theme-switch.ts +61 -0
  24. package/src/lib/Inputs/uploader/uploader.ts +97 -0
  25. package/src/lib/Layout/avatar/avatar.ts +66 -0
  26. package/src/lib/Layout/card/card.ts +88 -0
  27. package/src/lib/Layout/code-view/code-view.ts +77 -0
  28. package/src/lib/Layout/dialog/dialog.ts +80 -0
  29. package/src/lib/Layout/not-found/not-found.ts +73 -0
  30. package/src/lib/Layout/sidebar/sidebar.ts +97 -0
  31. package/src/lib/Layout/table/table.ts +81 -0
  32. package/src/lib/Layout/tooltip/tooltip.ts +48 -0
  33. package/src/lib/Navigation/accordion/accordion.ts +80 -0
  34. package/src/lib/Navigation/breadcrumb/breadcrumb.ts +58 -0
  35. package/src/lib/Navigation/carousel/carousel.ts +70 -0
  36. package/src/lib/Navigation/context-menu/context-menu.ts +79 -0
  37. package/src/lib/Navigation/nav-bar/nav-bar.ts +72 -0
  38. package/src/lib/Navigation/pagination/pagination.ts +82 -0
  39. package/src/lib/Navigation/stepper/stepper.ts +77 -0
  40. package/src/lib/Navigation/tab/tab.ts +71 -0
  41. package/src/lib/Navigation/toolbar/toolbar.ts +60 -0
  42. package/src/lib/Navigation/top-nav-bar/top-nav-bar.ts +71 -0
  43. package/src/lib/Navigation/tree-view/tree-view.ts +75 -0
  44. package/src/lib/Notification/badge/badge.ts +55 -0
  45. package/src/lib/Notification/loader/loader.ts +41 -0
  46. package/src/lib/Notification/message/message.ts +60 -0
  47. package/src/lib/Notification/progress/progress.ts +65 -0
  48. package/src/lib/Notification/spinner/spinner.ts +50 -0
  49. package/src/lib/Notification/toast/toast.ts +56 -0
  50. package/src/public-api.ts +62 -0
  51. package/tsconfig.json +23 -0
@@ -0,0 +1,33 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ - uses: actions/setup-node@v5
17
+ with:
18
+ node-version: 24
19
+
20
+ publish-npm:
21
+ needs: build
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v5
25
+ - uses: actions/setup-node@v5
26
+ with:
27
+ node-version: 24
28
+ registry-url: https://registry.npmjs.org/
29
+ - run: npm ci
30
+ - run: npm run build
31
+ - run: npm publish --access public
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/README.md CHANGED
@@ -1,3 +1,2 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # ks-ui-angular
2
+ Hold the wrapper all the angular components available in the ks design system
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/ng-packagr/ng-packagr/main/ng-package.schema.json",
3
+ "dest": "dist",
4
+ "allowedNonPeerDependencies": [
5
+ "@kraft.stitch/core"
6
+ ],
7
+ "lib": {
8
+ "entryFile": "src/public-api.ts"
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@kraft.stitch/angular",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "Hand-written Angular wrapper for the <ks-button> Stencil custom element from @kraft.stitch/core — proof-of-concept for the WebComponents wrapper pattern (core in Stencil, framework-idiomatic Input/Output/DI in the wrapper).",
5
+ "license": "MIT",
6
+ "main": "src/public-api.ts",
7
+ "types": "src/public-api.ts",
8
+ "scripts": {
9
+ "build": "ng-packagr -p ng-package.json",
10
+ "pack": "npm run build && cd dist && npm pack"
11
+ },
12
+ "peerDependencies": {
13
+ "@angular/core": "^21.2.0 || ^22.0.0",
14
+ "@angular/common": "^21.2.0 || ^22.0.0",
15
+ "@angular/forms": "^21.2.0 || ^22.0.0"
16
+ },
17
+ "dependencies": {
18
+ "@kraft.stitch/core": "^0.0.2"
19
+ },
20
+ "devDependencies": {
21
+ "@angular/common": "^21.2.0",
22
+ "@angular/compiler-cli": "^21.2.0",
23
+ "@angular/core": "^21.2.0",
24
+ "@angular/forms": "^21.2.0",
25
+ "ng-packagr": "^21.2.0",
26
+ "typescript": "~5.9.2"
27
+ },
28
+ "sideEffects": false
29
+ }
@@ -0,0 +1,152 @@
1
+ // Side-effect import: registers the `ks-button` tag in the browser's
2
+ // CustomElementRegistry (via Stencil's `auto-define-custom-elements`
3
+ // behavior in `core/stencil.config.ts`). Nothing in THIS file renders the
4
+ // button — see the class doc comment below for exactly what does.
5
+ import '@kraft.stitch/core/dist/components/ks-button.js';
6
+
7
+ import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, effect, inject, input, output } from '@angular/core';
8
+ import type { KsBaseSize, KsVariant } from '@kraft.stitch/core';
9
+
10
+ /** DOM shape of the real `<ks-button>` custom element, typed for the wrapper. */
11
+ type KsButtonElement = HTMLElement & {
12
+ icon: string;
13
+ name: string;
14
+ variant: KsVariant;
15
+ size: KsBaseSize;
16
+ isRounded: boolean;
17
+ disabled: boolean;
18
+ };
19
+
20
+ /**
21
+ * Angular wrapper around the `<ks-button>` Stencil custom element.
22
+ *
23
+ * ## How this actually renders a button (no markup lives in this file)
24
+ *
25
+ * 1. A parent template writes `<ks-button name="Save" (ksClick)="save()">`.
26
+ * 2. Angular's renderer creates that host node with
27
+ * `document.createElement('ks-button')`. Because the side-effect import
28
+ * above already ran `customElements.define('ks-button', ...)`, the
29
+ * **browser itself** immediately upgrades that node into an instance of
30
+ * Stencil's compiled `KsButton` class — this is standard Custom Elements
31
+ * behavior, nothing Angular-specific.
32
+ * 3. That upgrade runs Stencil's `connectedCallback` → `render()`, which
33
+ * injects the real `<button>…</button>` markup as children of the host
34
+ * node. This happens **regardless of Angular** — it's the same thing
35
+ * that would happen if you dropped `<ks-button>` in a plain HTML file.
36
+ * 4. `KsButtonComponent` (this class) is a *second, independent* thing
37
+ * attached to that same host node — Angular doesn't care that the node
38
+ * is also a custom element instance. Its only job is translation:
39
+ * push signal-input values onto the element's real DOM properties, and
40
+ * re-emit the native `clicked` DOM event as an Angular `output()`.
41
+ *
42
+ * `template: ''` (no `<ng-content>`) is intentional: `ks-button`'s Stencil
43
+ * `render()` has no `<slot>`, so there is nothing to project — content
44
+ * written between `<ks-button>` tags wouldn't be displayed by the real
45
+ * component anyway. Content projection would only make sense here if the
46
+ * wrapped Stencil component actually declared a `<slot>`.
47
+ *
48
+ * ## Why the output is called `ksClick`, NOT `clicked`
49
+ *
50
+ * `<ks-button>` (the Stencil element) dispatches a **native** DOM
51
+ * `CustomEvent` literally named `clicked` on the host node. It is tempting
52
+ * to expose the wrapper's own output using that exact same name so the
53
+ * public API "mirrors" the native event — but doing so creates a **name
54
+ * collision** that makes the event fire twice from a parent template such
55
+ * as `<ks-button (clicked)="log($event)">`:
56
+ *
57
+ * For *any* event binding in a template, Ivy's `ɵɵlistener` instruction
58
+ * does two things whenever the bound name also happens to match a
59
+ * directive/component **output** on that element (`tNode.outputs[name]`):
60
+ * 1. It subscribes the template's handler to that output (`output.emit()`
61
+ * calls it).
62
+ * 2. It **also** attaches a genuine `addEventListener(name, …)` straight
63
+ * onto the host DOM node for that same literal name, independent of
64
+ * step 1 — Ivy can't know whether the name is "just" an output or also
65
+ * a real DOM event some element in the tree might dispatch, so it wires
66
+ * both to be safe.
67
+ *
68
+ * If the output were named `clicked`, both of those fire for the *same*
69
+ * underlying interaction here: (1) our own `this.ksClick.emit(...)` call
70
+ * below invokes the template handler once, and (2) the raw native
71
+ * `clicked` `CustomEvent` that `<ks-button>` dispatches on the host node
72
+ * is picked up by Ivy's own native listener from step 2 and invokes the
73
+ * *same* template handler a second time — with a different, unwrapped
74
+ * event object. (An earlier attempt at fixing this used
75
+ * `@HostListener('clicked', ['$event'])`, which hits the same
76
+ * `tNode.outputs['clicked']` check and instead causes *infinite emit
77
+ * recursion*, since the host listener itself becomes a subscriber of the
78
+ * output it emits on.)
79
+ *
80
+ * Naming the output `ksClick` (no relation to any real DOM event name)
81
+ * removes the collision entirely: a parent writes
82
+ * `<ks-button (ksClick)="log($event)">`, Ivy subscribes that to our
83
+ * output, and the phantom native `addEventListener('ksClick', …)` it also
84
+ * attaches never fires because no such DOM event exists. Only the manual
85
+ * `addEventListener('clicked', …)` below — on the *internal* native event
86
+ * name, never referenced in any template — observes the real click and
87
+ * re-emits it once through `ksClick`. (This mirrors how libraries like
88
+ * Ionic prefix outputs, e.g. `ionChange` instead of `change`, for exactly
89
+ * this reason.)
90
+ */
91
+ @Component({
92
+ selector: 'ks-button',
93
+ standalone: true,
94
+ template: '',
95
+ changeDetection: ChangeDetectionStrategy.OnPush,
96
+ })
97
+ export class KsButtonComponent {
98
+ private readonly el: KsButtonElement;
99
+
100
+ /** Icon class string (e.g. Material Icon or Font Awesome class) */
101
+ readonly icon = input('');
102
+
103
+ /** Button label text */
104
+ readonly name = input('');
105
+
106
+ /** Visual style variant */
107
+ readonly variant = input<KsVariant>('brand');
108
+
109
+ /** Size */
110
+ readonly size = input<KsBaseSize>('md');
111
+
112
+ /** Renders the button with fully-rounded pill corners */
113
+ readonly isRounded = input(false);
114
+
115
+ /** Disables the button */
116
+ readonly disabled = input(false);
117
+
118
+ /**
119
+ * Emits the `MouseEvent` from the native `clicked` `CustomEvent`
120
+ * dispatched by `<ks-button>`. Deliberately NOT named `clicked` — see
121
+ * the class doc comment above for why that would fire this event twice.
122
+ */
123
+ readonly ksClick = output<MouseEvent>();
124
+
125
+ constructor(ref: ElementRef<KsButtonElement>) {
126
+ this.el = ref.nativeElement;
127
+
128
+ // Each signal input gets its own effect so only the properties that
129
+ // actually changed are written back to the DOM. Stencil's own
130
+ // `@Prop()` watchers take it from there and re-render the button.
131
+ effect(() => (this.el.icon = this.icon()));
132
+ effect(() => (this.el.name = this.name()));
133
+ effect(() => (this.el.variant = this.variant()));
134
+ effect(() => (this.el.size = this.size()));
135
+ effect(() => (this.el.isRounded = this.isRounded()));
136
+ effect(() => (this.el.disabled = this.disabled()));
137
+
138
+ // Attached manually (NOT `@HostListener`) to the *internal* native
139
+ // event name `clicked`, which is never referenced by name in any
140
+ // Angular template (`ksClick` is what templates bind to instead) — so
141
+ // there is no `tNode.outputs` name collision for Ivy to double-wire.
142
+ // zone.js's patched `addEventListener` means this still runs inside
143
+ // the Angular zone, so no manual `NgZone.run()` is needed, and change
144
+ // detection still runs after `emit()` below.
145
+ const onNativeClicked = (event: Event): void => {
146
+ this.ksClick.emit((event as CustomEvent<MouseEvent>).detail);
147
+ event.preventDefault();
148
+ };
149
+ this.el.addEventListener('clicked', onNativeClicked);
150
+ inject(DestroyRef).onDestroy(() => this.el.removeEventListener('clicked', onNativeClicked));
151
+ }
152
+ }
@@ -0,0 +1,129 @@
1
+ // Side-effect import: registers the `ks-button-group` tag in the browser's
2
+ // CustomElementRegistry — see `button/button.ts` for the full explanation of
3
+ // this pattern (Stencil owns rendering, this class only translates
4
+ // Angular-idiomatic Input/Output into DOM property writes + event listeners).
5
+ import '@kraft.stitch/core/dist/components/ks-button-group.js';
6
+
7
+ import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, effect, inject, input, output } from '@angular/core';
8
+ import type { KsBaseSize, KsBaseVariant } from '@kraft.stitch/core';
9
+
10
+ /**
11
+ * Mirrors `KsButtonGroupItem` exported by
12
+ * `core/src/components/Buttons/button-group/button-group.tsx`.
13
+ *
14
+ * Duplicated here (instead of importing it from `@kraft.stitch/core`) because that
15
+ * package's root barrel (`core/src/index.ts`) is still being assembled and
16
+ * does not yet reliably re-export every per-component type — only the
17
+ * shared enums in `ks-ui.types` (`KsBaseSize`, `KsBaseVariant`, `KsVariant`,
18
+ * imported below) are stable to depend on today. Keep this in sync with the
19
+ * Stencil source if its shape changes.
20
+ */
21
+ export interface KsButtonGroupItem {
22
+ key: string;
23
+ label?: string;
24
+ icon?: string;
25
+ disabled?: boolean;
26
+ }
27
+
28
+ export type KsButtonGroupDisplayStyle = 'default' | 'switch';
29
+
30
+ /** DOM shape of the real `<ks-button-group>` custom element, typed for the wrapper. */
31
+ type KsButtonGroupElement = HTMLElement & {
32
+ buttons: KsButtonGroupItem[];
33
+ variant: KsBaseVariant;
34
+ size: KsBaseSize;
35
+ displayStyle: KsButtonGroupDisplayStyle;
36
+ multi: boolean;
37
+ selectable: boolean;
38
+ disabled: boolean;
39
+ };
40
+
41
+ /**
42
+ * Angular wrapper around the `<ks-button-group>` Stencil custom element.
43
+ *
44
+ * `template: ''` — the Stencil `render()` for this element has no `<slot>`,
45
+ * so there is nothing for this wrapper to project; every button is
46
+ * generated from the `buttons` input.
47
+ *
48
+ * ## Output naming
49
+ * The native element dispatches DOM `CustomEvent`s named `clicked` and
50
+ * `selectionChanged`. Both outputs below are renamed with a `ks` prefix
51
+ * (`ksClick`, `ksSelectionChange`) rather than reusing those exact strings.
52
+ * Doing so is required, not just cosmetic: for a template binding such as
53
+ * `<ks-button-group (clicked)="...">`, Angular's `ɵɵlistener` instruction
54
+ * *always* attaches a raw `addEventListener('clicked', …)` to the host
55
+ * element in addition to subscribing to a same-named component output —
56
+ * this is unconditional for element nodes (see
57
+ * `packages/core/src/render3/instructions/listener.ts` →
58
+ * `listenerInternal`), not something a custom-elements schema can opt out
59
+ * of. With a same-named output, the parent handler would therefore run
60
+ * *twice* per interaction — once via our own unwrapped `.emit()` call, once
61
+ * via Angular's own raw listener receiving the untouched `CustomEvent`
62
+ * (defeating the "no `event.detail`" requirement). Prefixing every output
63
+ * with `ks` guarantees the literal name never matches a real DOM event, so
64
+ * only our manual listener (below) ever observes it. See `button/button.ts`
65
+ * for the original write-up of this same issue.
66
+ */
67
+ @Component({
68
+ selector: 'ks-button-group',
69
+ standalone: true,
70
+ template: '',
71
+ changeDetection: ChangeDetectionStrategy.OnPush,
72
+ })
73
+ export class KsButtonGroupComponent {
74
+ private readonly el: KsButtonGroupElement;
75
+
76
+ /** The buttons to render */
77
+ readonly buttons = input<KsButtonGroupItem[]>([]);
78
+
79
+ /** Visual variant */
80
+ readonly variant = input<KsBaseVariant>('brand');
81
+
82
+ /** Size */
83
+ readonly size = input<KsBaseSize>('md');
84
+
85
+ /** Display style: 'default' (filled/outlined) or 'switch' (pill toggle) */
86
+ readonly displayStyle = input<KsButtonGroupDisplayStyle>('default');
87
+
88
+ /** Allow multiple selected buttons (toggle mode) */
89
+ readonly multi = input(false);
90
+
91
+ /** Toggle/selection mode — shows active state on click */
92
+ readonly selectable = input(false);
93
+
94
+ /** Disables the entire group */
95
+ readonly disabled = input(false);
96
+
97
+ /** Emits the clicked button's `key`. From the native `clicked` event. */
98
+ readonly ksClick = output<string>();
99
+
100
+ /** Emits the current selection when `selectable`. From the native `selectionChanged` event. */
101
+ readonly ksSelectionChange = output<string[]>();
102
+
103
+ constructor(ref: ElementRef<KsButtonGroupElement>) {
104
+ this.el = ref.nativeElement;
105
+
106
+ effect(() => (this.el.buttons = this.buttons()));
107
+ effect(() => (this.el.variant = this.variant()));
108
+ effect(() => (this.el.size = this.size()));
109
+ effect(() => (this.el.displayStyle = this.displayStyle()));
110
+ effect(() => (this.el.multi = this.multi()));
111
+ effect(() => (this.el.selectable = this.selectable()));
112
+ effect(() => (this.el.disabled = this.disabled()));
113
+
114
+ const onNativeClicked = (event: Event): void => {
115
+ this.ksClick.emit((event as CustomEvent<string>).detail);
116
+ };
117
+ const onNativeSelectionChanged = (event: Event): void => {
118
+ this.ksSelectionChange.emit((event as CustomEvent<string[]>).detail);
119
+ };
120
+
121
+ this.el.addEventListener('clicked', onNativeClicked);
122
+ this.el.addEventListener('selectionChanged', onNativeSelectionChanged);
123
+
124
+ inject(DestroyRef).onDestroy(() => {
125
+ this.el.removeEventListener('clicked', onNativeClicked);
126
+ this.el.removeEventListener('selectionChanged', onNativeSelectionChanged);
127
+ });
128
+ }
129
+ }
@@ -0,0 +1,131 @@
1
+ // Side-effect import: registers the `ks-chips` tag — see `button/button.ts`
2
+ // for the full explanation of this pattern.
3
+ import '@kraft.stitch/core/dist/components/ks-chips.js';
4
+
5
+ import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, effect, inject, input, output } from '@angular/core';
6
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
7
+ import type { KsBaseSize } from '@kraft.stitch/core';
8
+
9
+ /**
10
+ * Mirrors `KsChipOption` exported by
11
+ * `core/src/components/Buttons/chips/chips.tsx`. Duplicated locally — see
12
+ * the note in `button-group/button-group.ts` on why component-specific
13
+ * shapes aren't imported from `@kraft.stitch/core`'s root barrel yet.
14
+ */
15
+ export interface KsChipOption {
16
+ value: string | number;
17
+ label: string;
18
+ icon?: string;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export type KsChipsValue = (string | number) | (string | number)[] | null;
23
+
24
+ /** DOM shape of the real `<ks-chips>` custom element, typed for the wrapper. */
25
+ type KsChipsElement = HTMLElement & {
26
+ options: KsChipOption[];
27
+ multi: boolean;
28
+ label: string;
29
+ size: KsBaseSize;
30
+ isRounded: boolean;
31
+ disabled: boolean;
32
+ value: KsChipsValue;
33
+ writeValue(val: KsChipsValue): Promise<void>;
34
+ };
35
+
36
+ /**
37
+ * Angular wrapper around the `<ks-chips>` Stencil custom element.
38
+ *
39
+ * ## Two-way binding
40
+ * The Stencil source's own doc comment says Angular forms integration
41
+ * (`ngModel`/`formControl`) would be "provided later by the generated
42
+ * output-target wrappers" — this is that wrapper. Rather than trying to
43
+ * reuse Angular's `[(value)]` banana-box sugar (which would require an
44
+ * output literally named `valueChange`, colliding with the native
45
+ * `valueChange` `CustomEvent` the element already dispatches — see
46
+ * `button/button.ts` for why that double-fires), two-way binding is
47
+ * implemented the idiomatic Angular way: `ControlValueAccessor`. Bind with
48
+ * `[(ngModel)]="fruit"` or `formControlName="fruit"` and it Just Works,
49
+ * because `NgModel`/`FormControlDirective` talk to `writeValue` /
50
+ * `registerOnChange` directly through DI — no template event-name matching
51
+ * involved, so there's no collision to worry about.
52
+ *
53
+ * For plain (non-forms) two-way use, bind explicitly instead:
54
+ * `[value]="fruit" (ksValueChange)="fruit = $event"`.
55
+ *
56
+ * `template: ''` — no `<slot>` in the wrapped Stencil render, nothing to project.
57
+ */
58
+ @Component({
59
+ selector: 'ks-chips',
60
+ standalone: true,
61
+ template: '',
62
+ changeDetection: ChangeDetectionStrategy.OnPush,
63
+ providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: KsChipsComponent, multi: true }],
64
+ })
65
+ export class KsChipsComponent implements ControlValueAccessor {
66
+ private readonly el: KsChipsElement;
67
+ private onChange: (value: KsChipsValue) => void = () => {};
68
+ private onTouched: () => void = () => {};
69
+
70
+ /** Full list of chip options */
71
+ readonly options = input<KsChipOption[]>([]);
72
+
73
+ /** Allow selecting multiple chips */
74
+ readonly multi = input(false);
75
+
76
+ /** Field label */
77
+ readonly label = input('');
78
+
79
+ /** Size */
80
+ readonly size = input<KsBaseSize>('md');
81
+
82
+ /** Renders chips with fully-rounded pill corners */
83
+ readonly isRounded = input(true);
84
+
85
+ /** Disabled state */
86
+ readonly disabled = input(false);
87
+
88
+ /** Initial/controlled value for plain (non-forms) usage. Prefer `[(ngModel)]` / `formControlName` when inside a form. */
89
+ readonly value = input<KsChipsValue>(null);
90
+
91
+ /** Emits the newly-selected value(s). From the native `valueChange` event. */
92
+ readonly ksValueChange = output<KsChipsValue>();
93
+
94
+ constructor(ref: ElementRef<KsChipsElement>) {
95
+ this.el = ref.nativeElement;
96
+
97
+ effect(() => (this.el.options = this.options()));
98
+ effect(() => (this.el.multi = this.multi()));
99
+ effect(() => (this.el.label = this.label()));
100
+ effect(() => (this.el.size = this.size()));
101
+ effect(() => (this.el.isRounded = this.isRounded()));
102
+ effect(() => (this.el.disabled = this.disabled()));
103
+ effect(() => void this.el.writeValue(this.value()));
104
+
105
+ const onNativeValueChange = (event: Event): void => {
106
+ const detail = (event as CustomEvent<KsChipsValue>).detail;
107
+ this.ksValueChange.emit(detail);
108
+ this.onChange(detail);
109
+ this.onTouched();
110
+ };
111
+ this.el.addEventListener('valueChange', onNativeValueChange);
112
+ inject(DestroyRef).onDestroy(() => this.el.removeEventListener('valueChange', onNativeValueChange));
113
+ }
114
+
115
+ /** `ControlValueAccessor` — called by `ngModel`/`formControl` to push a value in. */
116
+ writeValue(value: KsChipsValue): void {
117
+ void this.el.writeValue(value ?? null);
118
+ }
119
+
120
+ registerOnChange(fn: (value: KsChipsValue) => void): void {
121
+ this.onChange = fn;
122
+ }
123
+
124
+ registerOnTouched(fn: () => void): void {
125
+ this.onTouched = fn;
126
+ }
127
+
128
+ setDisabledState(isDisabled: boolean): void {
129
+ this.el.disabled = isDisabled;
130
+ }
131
+ }
@@ -0,0 +1,96 @@
1
+ // Side-effect import: registers the `ks-fab` tag — see `button/button.ts`
2
+ // for the full explanation of this pattern.
3
+ import '@kraft.stitch/core/dist/components/ks-fab.js';
4
+
5
+ import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, effect, inject, input, output } from '@angular/core';
6
+ import type { KsBaseSize, KsBaseVariant } from '@kraft.stitch/core';
7
+
8
+ /**
9
+ * Mirrors `KsFabAction` / `KsFabPosition` exported by
10
+ * `core/src/components/Buttons/fab/fab.tsx`. Duplicated locally — see the
11
+ * note in `button-group/button-group.ts` for why.
12
+ */
13
+ export interface KsFabAction {
14
+ key: string;
15
+ icon: string;
16
+ label?: string;
17
+ variant?: KsBaseVariant;
18
+ }
19
+
20
+ export type KsFabPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
21
+
22
+ /** DOM shape of the real `<ks-fab>` custom element, typed for the wrapper. */
23
+ type KsFabElement = HTMLElement & {
24
+ icon: string;
25
+ label: string;
26
+ variant: KsBaseVariant;
27
+ size: KsBaseSize;
28
+ actions: KsFabAction[];
29
+ disabled: boolean;
30
+ position: KsFabPosition;
31
+ appendToParent: boolean;
32
+ };
33
+
34
+ /**
35
+ * Angular wrapper around the `<ks-fab>` Stencil custom element.
36
+ *
37
+ * `ksClick` is renamed away from the native `clicked` event name for the
38
+ * same double-fire reason documented in `button/button.ts` and
39
+ * `button-group/button-group.ts`. `template: ''` — no `<slot>` in the
40
+ * wrapped Stencil render.
41
+ */
42
+ @Component({
43
+ selector: 'ks-fab',
44
+ standalone: true,
45
+ template: '',
46
+ changeDetection: ChangeDetectionStrategy.OnPush,
47
+ })
48
+ export class KsFabComponent {
49
+ private readonly el: KsFabElement;
50
+
51
+ /** Icon for the main FAB button */
52
+ readonly icon = input('');
53
+
54
+ /** Accessible label / tooltip */
55
+ readonly label = input('');
56
+
57
+ /** Variant */
58
+ readonly variant = input<KsBaseVariant>('brand');
59
+
60
+ /** Size */
61
+ readonly size = input<KsBaseSize>('md');
62
+
63
+ /** Speed-dial actions shown on expand */
64
+ readonly actions = input<KsFabAction[]>([]);
65
+
66
+ /** Disabled */
67
+ readonly disabled = input(false);
68
+
69
+ /** Corner to anchor the FAB inside its nearest positioned container */
70
+ readonly position = input<KsFabPosition>('bottom-right');
71
+
72
+ /** When true (default), positions `absolute` inside the nearest positioned ancestor; when false, `fixed` to the viewport */
73
+ readonly appendToParent = input(true);
74
+
75
+ /** Emits `'main'` for the main button, or a speed-dial action's `key`. From the native `clicked` event. */
76
+ readonly ksClick = output<string>();
77
+
78
+ constructor(ref: ElementRef<KsFabElement>) {
79
+ this.el = ref.nativeElement;
80
+
81
+ effect(() => (this.el.icon = this.icon()));
82
+ effect(() => (this.el.label = this.label()));
83
+ effect(() => (this.el.variant = this.variant()));
84
+ effect(() => (this.el.size = this.size()));
85
+ effect(() => (this.el.actions = this.actions()));
86
+ effect(() => (this.el.disabled = this.disabled()));
87
+ effect(() => (this.el.position = this.position()));
88
+ effect(() => (this.el.appendToParent = this.appendToParent()));
89
+
90
+ const onNativeClicked = (event: Event): void => {
91
+ this.ksClick.emit((event as CustomEvent<string>).detail);
92
+ };
93
+ this.el.addEventListener('clicked', onNativeClicked);
94
+ inject(DestroyRef).onDestroy(() => this.el.removeEventListener('clicked', onNativeClicked));
95
+ }
96
+ }