@kraft.stitch/angular 0.0.3 → 0.0.4-beta.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.
- package/.github/workflows/npm-publish.yml +41 -0
- package/README.md +0 -4
- package/ng-package.json +10 -0
- package/package.json +28 -19
- package/src/lib/Buttons/button/button.ts +152 -0
- package/src/lib/Buttons/button-group/button-group.ts +129 -0
- package/src/lib/Buttons/chips/chips.ts +131 -0
- package/src/lib/Buttons/fab/fab.ts +96 -0
- package/src/lib/Buttons/split-button/split-button.ts +111 -0
- package/src/lib/Calendars/datepicker/datepicker.ts +105 -0
- package/src/lib/Calendars/daterange/daterange.ts +112 -0
- package/src/lib/Calendars/timepicker/timepicker.ts +117 -0
- package/src/lib/Dropdowns/dropdown/dropdown.ts +121 -0
- package/src/lib/Dropdowns/listbox/listbox.ts +116 -0
- package/src/lib/Inputs/checkbox/checkbox.ts +126 -0
- package/src/lib/Inputs/colour-picker/colour-picker.ts +101 -0
- package/src/lib/Inputs/input/input.ts +139 -0
- package/src/lib/Inputs/otp-input/otp-input.ts +113 -0
- package/src/lib/Inputs/radio/radio.ts +115 -0
- package/src/lib/Inputs/rating/rating.ts +104 -0
- package/src/lib/Inputs/rolling-number/rolling-number.ts +136 -0
- package/src/lib/Inputs/slider/slider.ts +121 -0
- package/src/lib/Inputs/switch/switch.ts +100 -0
- package/src/lib/Inputs/textarea/textarea.ts +129 -0
- package/src/lib/Inputs/theme-switch/theme-switch.ts +61 -0
- package/src/lib/Inputs/uploader/uploader.ts +97 -0
- package/src/lib/Layout/avatar/avatar.ts +66 -0
- package/src/lib/Layout/card/card.ts +88 -0
- package/src/lib/Layout/code-view/code-view.ts +77 -0
- package/src/lib/Layout/dialog/dialog.ts +80 -0
- package/src/lib/Layout/not-found/not-found.ts +73 -0
- package/src/lib/Layout/sidebar/sidebar.ts +97 -0
- package/src/lib/Layout/table/table.ts +81 -0
- package/src/lib/Layout/tooltip/tooltip.ts +48 -0
- package/src/lib/Navigation/accordion/accordion.ts +80 -0
- package/src/lib/Navigation/breadcrumb/breadcrumb.ts +58 -0
- package/src/lib/Navigation/carousel/carousel.ts +70 -0
- package/src/lib/Navigation/context-menu/context-menu.ts +79 -0
- package/src/lib/Navigation/nav-bar/nav-bar.ts +72 -0
- package/src/lib/Navigation/pagination/pagination.ts +82 -0
- package/src/lib/Navigation/stepper/stepper.ts +77 -0
- package/src/lib/Navigation/tab/tab.ts +71 -0
- package/src/lib/Navigation/toolbar/toolbar.ts +60 -0
- package/src/lib/Navigation/top-nav-bar/top-nav-bar.ts +71 -0
- package/src/lib/Navigation/tree-view/tree-view.ts +75 -0
- package/src/lib/Notification/badge/badge.ts +55 -0
- package/src/lib/Notification/loader/loader.ts +41 -0
- package/src/lib/Notification/message/message.ts +60 -0
- package/src/lib/Notification/progress/progress.ts +65 -0
- package/src/lib/Notification/spinner/spinner.ts +50 -0
- package/src/lib/Notification/toast/toast.ts +56 -0
- package/src/public-api.ts +68 -0
- package/src/utils/theme-service.ts +11 -0
- package/tsconfig.json +23 -0
- package/fesm2022/kraft.stitch-angular.mjs +0 -2873
- package/fesm2022/kraft.stitch-angular.mjs.map +0 -1
- package/types/kraft.stitch-angular.d.ts +0 -2247
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
permissions:
|
|
24
|
+
id-token: write
|
|
25
|
+
contents: read
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
|
+
- uses: actions/setup-node@v5
|
|
29
|
+
with:
|
|
30
|
+
node-version: 24
|
|
31
|
+
registry-url: https://registry.npmjs.org/
|
|
32
|
+
- run: npm ci
|
|
33
|
+
- run: npm run build
|
|
34
|
+
- name: Publish stable release
|
|
35
|
+
if: ${{ !github.event.release.prerelease }}
|
|
36
|
+
run: npm publish --access public
|
|
37
|
+
|
|
38
|
+
- name: Publish pre-release
|
|
39
|
+
if: ${{ github.event.release.prerelease }}
|
|
40
|
+
run: npm publish --access public --tag beta
|
|
41
|
+
# - run: npm publish ./dist --access public
|
package/README.md
CHANGED
|
@@ -94,8 +94,4 @@ Your feedback, suggestions, and contributions help improve KSPack.
|
|
|
94
94
|
|
|
95
95
|
**Made with ❤️ by @kraft.stitch**
|
|
96
96
|
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Keywords
|
|
100
97
|
|
|
101
|
-
Angular, React, UI Components, Design System, Component Library, Forms, Navigation, Layout, Notifications, Inputs, Dropdowns, Calendars, TypeScript,
|
package/ng-package.json
ADDED
package/package.json
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kraft.stitch/angular",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4-beta.1",
|
|
4
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
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ks-ui",
|
|
8
|
+
"kraft-stitch",
|
|
9
|
+
"ui",
|
|
10
|
+
"ui-components",
|
|
11
|
+
"component-library",
|
|
12
|
+
"design-system",
|
|
13
|
+
"web-components",
|
|
14
|
+
"stencil",
|
|
15
|
+
"angular",
|
|
16
|
+
"react"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "ng-packagr -p ng-package.json",
|
|
20
|
+
"pack": "npm run build && cd dist && npm pack"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
9
23
|
"@angular/core": "^21.2.0 || ^22.0.0",
|
|
10
24
|
"@angular/common": "^21.2.0 || ^22.0.0",
|
|
11
25
|
"@angular/forms": "^21.2.0 || ^22.0.0"
|
|
12
26
|
},
|
|
13
27
|
"dependencies": {
|
|
14
|
-
"@kraft.stitch/core": "^0.0.
|
|
15
|
-
"tslib": "^2.3.0"
|
|
28
|
+
"@kraft.stitch/core": "^0.0.3"
|
|
16
29
|
},
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
".": {
|
|
25
|
-
"types": "./types/kraft.stitch-angular.d.ts",
|
|
26
|
-
"default": "./fesm2022/kraft.stitch-angular.mjs"
|
|
27
|
-
}
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@angular/common": "^21.2.0",
|
|
32
|
+
"@angular/compiler-cli": "^21.2.0",
|
|
33
|
+
"@angular/core": "^21.2.0",
|
|
34
|
+
"@angular/forms": "^21.2.0",
|
|
35
|
+
"ng-packagr": "^21.2.0",
|
|
36
|
+
"typescript": "~5.9.2"
|
|
28
37
|
},
|
|
29
|
-
"
|
|
30
|
-
}
|
|
38
|
+
"sideEffects": false
|
|
39
|
+
}
|
|
@@ -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
|
+
}
|