my-km-components 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.
- package/README.md +117 -0
- package/assets/sass/_corner-focus.scss +50 -0
- package/my-km-components-0.0.1.tgz +0 -0
- package/ng-package.json +7 -0
- package/package.json +20 -0
- package/services/km.service.ts +14 -0
- package/src/directives/km-block.directive.ts +39 -0
- package/src/directives/km-block.style.scss +41 -0
- package/src/lib/km-color-combo/km-color-combo.component.html +28 -0
- package/src/lib/km-color-combo/km-color-combo.component.scss +0 -0
- package/src/lib/km-color-combo/km-color-combo.component.ts +52 -0
- package/src/lib/km-generic-card/km-generic-card.html +49 -0
- package/src/lib/km-generic-card/km-generic-card.interface.ts +16 -0
- package/src/lib/km-generic-card/km-generic-card.scss +15 -0
- package/src/lib/km-generic-card/km-generic-card.ts +43 -0
- package/src/lib/km-input/km-input.component.html +30 -0
- package/src/lib/km-input/km-input.component.scss +5 -0
- package/src/lib/km-input/km-input.component.ts +30 -0
- package/src/lib/km-input-group/km-input-group.component.html +24 -0
- package/src/lib/km-input-group/km-input-group.component.scss +0 -0
- package/src/lib/km-input-group/km-input-group.component.ts +44 -0
- package/src/lib/km-modal/km-modal.html +38 -0
- package/src/lib/km-modal/km-modal.scss +26 -0
- package/src/lib/km-modal/km-modal.ts +40 -0
- package/src/lib/km-select/km-select.component.html +40 -0
- package/src/lib/km-select/km-select.component.scss +0 -0
- package/src/lib/km-select/km-select.component.ts +49 -0
- package/src/public-api.ts +10 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# My KM Components Library
|
|
2
|
+
|
|
3
|
+
A library of reusable UI components for Angular applications.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
**Tip:** In your `styles.scss` (or `styles.css`), make sure to include the `ng-select` theme:
|
|
8
|
+
|
|
9
|
+
```scss
|
|
10
|
+
@use '@ng-select/ng-select/themes/default.theme.css' as ng-select-theme;
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Component Usage
|
|
14
|
+
|
|
15
|
+
### `<km-select>`
|
|
16
|
+
|
|
17
|
+
A wrapper around `ng-select` providing integration with signal-based form controls.
|
|
18
|
+
|
|
19
|
+
**Inputs / Models:**
|
|
20
|
+
|
|
21
|
+
- `value` (`ModelSignal<string | number | null>`)
|
|
22
|
+
- `items` (`required`): Array of options to select from.
|
|
23
|
+
- `bindLabel` (default: `'label'`): Object property to use for label.
|
|
24
|
+
- `bindValue` (default: `'value'`): Object property to use for value.
|
|
25
|
+
- `multiple` (default: `true`): Allows multiple selection.
|
|
26
|
+
- `searchable` (default: `true`): Enables searching within the dropdown.
|
|
27
|
+
- `clearable` (default: `true`): Enables clearing the value.
|
|
28
|
+
- `placeholder` (default: `'Select...'`)
|
|
29
|
+
- `label` (default: `'Label'`)
|
|
30
|
+
- `limit` (default: `3`): Max visible tags.
|
|
31
|
+
- `invalid`, `touched`, `disabled`, `readonly`, `errors`: Control state and validation flags.
|
|
32
|
+
|
|
33
|
+
### `<km-input>`
|
|
34
|
+
|
|
35
|
+
A standard input component with label and tooltip support.
|
|
36
|
+
|
|
37
|
+
**Inputs / Models:**
|
|
38
|
+
|
|
39
|
+
- `value` (`ModelSignal<string | null>`)
|
|
40
|
+
- `inputType` (default: `'text'`): HTML input type.
|
|
41
|
+
- `placeholder` (default: `'Εισάγετε μια τιμή'`)
|
|
42
|
+
- `label` (default: `'Label'`)
|
|
43
|
+
- `showLabel` (default: `true`): Whether to display the label.
|
|
44
|
+
- `formControlSize` (default: `'sm'`): Sizing class suffix for `ng-bootstrap`.
|
|
45
|
+
- `invalid`, `touched`, `disabled`, `required`, `errors`: Control state flags.
|
|
46
|
+
|
|
47
|
+
### `<km-color-combo>`
|
|
48
|
+
|
|
49
|
+
A color picker and input combo that automatically adjusts text contrast based on the background color.
|
|
50
|
+
|
|
51
|
+
**Inputs / Models:**
|
|
52
|
+
|
|
53
|
+
- `value` (`ModelSignal<string | null>`)
|
|
54
|
+
- `label` (default: `'Label'`)
|
|
55
|
+
- `showLabel` (default: `true`): Whether to display the label.
|
|
56
|
+
- `invalid`, `touched`, `disabled`, `required`, `errors`: Control state flags.
|
|
57
|
+
|
|
58
|
+
### `<km-input-group>`
|
|
59
|
+
|
|
60
|
+
An input group component with an optional action button and icons.
|
|
61
|
+
|
|
62
|
+
**Inputs / Models:**
|
|
63
|
+
|
|
64
|
+
- `value` (`ModelSignal<number | null>`)
|
|
65
|
+
- `type` (default: `'text'`): Input type.
|
|
66
|
+
- `placeholder` (default: `'Εισάγετε τιμή'`)
|
|
67
|
+
- `label` (default: `'Label'`)
|
|
68
|
+
- `addonText`: Text for the input group addon label.
|
|
69
|
+
- `icon`: Icon class for the action button.
|
|
70
|
+
- `buttonClass`: Class name for the action button styling.
|
|
71
|
+
- `invalid`, `required`, `touched`, `disabled`, `readOnly`, `errors`: Control state flags.
|
|
72
|
+
|
|
73
|
+
**Outputs:**
|
|
74
|
+
|
|
75
|
+
- `actionClicked`: Emitted when the action button is clicked.
|
|
76
|
+
|
|
77
|
+
### `<km-modal>`
|
|
78
|
+
|
|
79
|
+
A customizable modal layout wrapper component with standard actions (Save, Cancel, Close, Reset).
|
|
80
|
+
|
|
81
|
+
**Inputs:**
|
|
82
|
+
|
|
83
|
+
- `title` (default: `'Τίτλος παραθύρου'`)
|
|
84
|
+
- `showSave` (default: `true`): Shows the Save button.
|
|
85
|
+
- `showReset` (default: `false`): Shows the Reset button.
|
|
86
|
+
- `showClose` (default: `'Κλείσιμο'`): Acts as standard close label toggle.
|
|
87
|
+
- `saveLabel` (default: `'Αποθήκευση'`)
|
|
88
|
+
- `cancelLabel` (default: `'Ακύρωση'`)
|
|
89
|
+
- `closeLabel` (default: `'Κλείσιμο'`)
|
|
90
|
+
- `resetLabel` (default: `'Επαναφορά'`)
|
|
91
|
+
- `saveDisabled` (default: `false`): Disables the Save button when true.
|
|
92
|
+
|
|
93
|
+
**Outputs:**
|
|
94
|
+
|
|
95
|
+
- `save`: Emitted on Save button click.
|
|
96
|
+
- `cancel`: Emitted on Cancel button click.
|
|
97
|
+
- `reset`: Emitted on Reset button click.
|
|
98
|
+
- `closeClicked`: Emitted on Close button click.
|
|
99
|
+
|
|
100
|
+
### `<km-generic-card>`
|
|
101
|
+
|
|
102
|
+
A card component that is collapsible, supports custom action buttons, and can emit events back.
|
|
103
|
+
|
|
104
|
+
**Inputs / Signals:**
|
|
105
|
+
|
|
106
|
+
- `panelTitle`: The title text of the panel.
|
|
107
|
+
- `subtitle`: The subtitle text.
|
|
108
|
+
- `visible` (default: `true`): Initial visibility flag.
|
|
109
|
+
- `isCollapsed` (`Signal<boolean>`): Signal controlling the collapse state.
|
|
110
|
+
- `isVisible` (`Signal<boolean>`): Signal controlling the modal visibility state.
|
|
111
|
+
- `showBackButton` (default: `false`): Shows a back button if true.
|
|
112
|
+
- `buttons`: Card buttons model implementing `KmGenericCardInterface`.
|
|
113
|
+
|
|
114
|
+
**Outputs:**
|
|
115
|
+
|
|
116
|
+
- `clicked`: Emitted on the main action button click.
|
|
117
|
+
- `backClicked`: Emitted on the back button click.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
$corner-color: var(--bs-primary, #0d6efd) !default;
|
|
2
|
+
$corner-size: 10px !default;
|
|
3
|
+
$corner-border: 2px solid $corner-color !default;
|
|
4
|
+
|
|
5
|
+
@mixin corner-focus-indicators {
|
|
6
|
+
position: relative;
|
|
7
|
+
|
|
8
|
+
.corner {
|
|
9
|
+
position: absolute;
|
|
10
|
+
width: $corner-size;
|
|
11
|
+
height: $corner-size;
|
|
12
|
+
opacity: 0;
|
|
13
|
+
transition: opacity 0.2s ease;
|
|
14
|
+
pointer-events: none;
|
|
15
|
+
z-index: 1;
|
|
16
|
+
|
|
17
|
+
&.top-left {
|
|
18
|
+
top: 0;
|
|
19
|
+
left: 0;
|
|
20
|
+
border-top: $corner-border;
|
|
21
|
+
border-left: $corner-border;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&.top-right {
|
|
25
|
+
top: 0;
|
|
26
|
+
right: 0;
|
|
27
|
+
border-top: $corner-border;
|
|
28
|
+
border-right: $corner-border;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&.bottom-left {
|
|
32
|
+
bottom: 0;
|
|
33
|
+
left: 0;
|
|
34
|
+
border-bottom: $corner-border;
|
|
35
|
+
border-left: $corner-border;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&.bottom-right {
|
|
39
|
+
bottom: 0;
|
|
40
|
+
right: 0;
|
|
41
|
+
border-bottom: $corner-border;
|
|
42
|
+
border-right: $corner-border;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
input:focus~.corner,
|
|
47
|
+
textarea:focus~.corner {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
Binary file
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-km-components",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@angular/common": "^21.2.0",
|
|
6
|
+
"@angular/core": "^21.2.0",
|
|
7
|
+
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
|
|
8
|
+
"@ng-select/ng-select": "^21.7.0"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/viverosimo/km-components.git"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"tslib": "^2.3.0",
|
|
16
|
+
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
|
|
17
|
+
"@ng-select/ng-select": "^21.7.0"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Injectable, signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable()
|
|
4
|
+
export class KmService {
|
|
5
|
+
private loading = signal<boolean>(false);
|
|
6
|
+
|
|
7
|
+
getLoading(): boolean {
|
|
8
|
+
return this.loading();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setLoading(value: boolean): void {
|
|
12
|
+
this.loading.set(value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Directive, ElementRef, effect, inject, input, Renderer2 } from '@angular/core'
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[KmBlockUI]',
|
|
5
|
+
})
|
|
6
|
+
export class KmBlockDirective {
|
|
7
|
+
KmBlockUI = input<boolean>(false)
|
|
8
|
+
private el = inject(ElementRef)
|
|
9
|
+
private renderer = inject(Renderer2)
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.renderer.setStyle(this.el.nativeElement, 'position', 'relative')
|
|
13
|
+
effect(() => {
|
|
14
|
+
this.KmBlockUI() ? this.block() : this.unblock()
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private block() {
|
|
19
|
+
this.renderer.addClass(this.el.nativeElement, 'block-loading')
|
|
20
|
+
|
|
21
|
+
// Create the overlay div
|
|
22
|
+
const overlay = this.renderer.createElement('div')
|
|
23
|
+
this.renderer.addClass(overlay, 'block-overlay')
|
|
24
|
+
this.renderer.setAttribute(overlay, 'id', 'block-overlay-node')
|
|
25
|
+
|
|
26
|
+
// Optional: Add a spinner icon or text
|
|
27
|
+
overlay.innerHTML = '<div class="spinner"></div>'
|
|
28
|
+
|
|
29
|
+
this.renderer.appendChild(this.el.nativeElement, overlay)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private unblock() {
|
|
33
|
+
this.renderer.removeClass(this.el.nativeElement, 'block-loading')
|
|
34
|
+
const overlay = this.el.nativeElement.querySelector('#block-overlay-node')
|
|
35
|
+
if (overlay) {
|
|
36
|
+
this.renderer.removeChild(this.el.nativeElement, overlay)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* The container gets a slight blur/grayscale effect */
|
|
2
|
+
.block-loading {
|
|
3
|
+
pointer-events: none;
|
|
4
|
+
/* Prevents clicks on buttons/inputs inside */
|
|
5
|
+
user-select: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.block-overlay {
|
|
9
|
+
position: absolute;
|
|
10
|
+
top: 0;
|
|
11
|
+
left: 0;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
background: rgba(255, 255, 255, 0.7);
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
z-index: 10;
|
|
19
|
+
border-radius: inherit;
|
|
20
|
+
/* Matches the parent's corners */
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* A simple CSS spinner */
|
|
24
|
+
.spinner {
|
|
25
|
+
width: 30px;
|
|
26
|
+
height: 30px;
|
|
27
|
+
border: 4px solid #f3f3f3;
|
|
28
|
+
border-top: 4px solid #3498db;
|
|
29
|
+
border-radius: 50%;
|
|
30
|
+
animation: spin 1s linear infinite;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@keyframes spin {
|
|
34
|
+
0% {
|
|
35
|
+
transform: rotate(0deg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
100% {
|
|
39
|
+
transform: rotate(360deg);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div class="mb-2">
|
|
2
|
+
@if (showLabel()) {
|
|
3
|
+
<label class="form-label fw-700"
|
|
4
|
+
>{{ label() }}
|
|
5
|
+
@if (required()) {
|
|
6
|
+
<span class="text-danger" ngbTooltip="Είναι υποχρεωτικό" triggers="hover">*</span>
|
|
7
|
+
}
|
|
8
|
+
</label>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
<div class="input-group input-group-sm">
|
|
12
|
+
<input
|
|
13
|
+
type="color"
|
|
14
|
+
class="form-control form-control-color"
|
|
15
|
+
(input)="change($event)"
|
|
16
|
+
[value]="value()"
|
|
17
|
+
title="Επιλέξτε χρώμα"
|
|
18
|
+
(blur)="touched.set(true)"
|
|
19
|
+
/>
|
|
20
|
+
<span
|
|
21
|
+
class="input-group-text font-monospace"
|
|
22
|
+
[style.backgroundColor]="value()"
|
|
23
|
+
[style.color]="getTextColor(value() ?? '')"
|
|
24
|
+
>
|
|
25
|
+
{{ value() }}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Component, computed, input, model } from '@angular/core'
|
|
2
|
+
import { FormValueControl, ValidationError } from '@angular/forms/signals'
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'km-color-combo',
|
|
6
|
+
imports: [],
|
|
7
|
+
templateUrl: './km-color-combo.component.html',
|
|
8
|
+
styleUrl: './km-color-combo.component.scss',
|
|
9
|
+
})
|
|
10
|
+
export class KmColorComboComponent implements FormValueControl<string | null> {
|
|
11
|
+
readonly value = model<string | null>(null)
|
|
12
|
+
|
|
13
|
+
readonly invalid = input<boolean>(false)
|
|
14
|
+
readonly touched = model<boolean>(false)
|
|
15
|
+
readonly disabled = input<boolean>(false)
|
|
16
|
+
readonly required = input<boolean>(false)
|
|
17
|
+
readonly errors = input<readonly ValidationError[]>([])
|
|
18
|
+
|
|
19
|
+
showLabel = input<boolean>(true)
|
|
20
|
+
label = input<string>('Label')
|
|
21
|
+
|
|
22
|
+
getTextColor(hexColor: string): string {
|
|
23
|
+
if (!hexColor) return '#000000' // Default to black if no color
|
|
24
|
+
|
|
25
|
+
// Remove the '#' if it exists
|
|
26
|
+
const hex = hexColor.replace('#', '')
|
|
27
|
+
|
|
28
|
+
// Convert 3-char hex to 6-char if needed (e.g., #abc to #aabbcc)
|
|
29
|
+
const fullHex =
|
|
30
|
+
hex.length === 3
|
|
31
|
+
? hex
|
|
32
|
+
.split('')
|
|
33
|
+
.map((c) => c + c)
|
|
34
|
+
.join('')
|
|
35
|
+
: hex
|
|
36
|
+
|
|
37
|
+
// Parse the RGB values
|
|
38
|
+
const r = parseInt(fullHex.substring(0, 2), 16)
|
|
39
|
+
const g = parseInt(fullHex.substring(2, 4), 16)
|
|
40
|
+
const b = parseInt(fullHex.substring(4, 6), 16)
|
|
41
|
+
|
|
42
|
+
// Calculate YIQ contrast (Standard formula for human eye brightness perception)
|
|
43
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000
|
|
44
|
+
|
|
45
|
+
// If the background is light (yiq >= 128), return black text. Otherwise, white text.
|
|
46
|
+
return yiq >= 128 ? '#000000' : '#ffffff'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
change(event: any) {
|
|
50
|
+
this.value.set(event.target.value)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div class="panel" [KmBlockUI]="service.getLoading()">
|
|
2
|
+
<div class="panel-hdr">
|
|
3
|
+
<h2>
|
|
4
|
+
{{ panelTitle() }}
|
|
5
|
+
<span class="fw-300"
|
|
6
|
+
><i>{{ subtitle() }}</i></span
|
|
7
|
+
>
|
|
8
|
+
</h2>
|
|
9
|
+
<div class="panel-toolbar">
|
|
10
|
+
@for (button of buttons()?.buttons; track $index) {
|
|
11
|
+
<button
|
|
12
|
+
(click)="button.clicked()"
|
|
13
|
+
[disabled]="button.disabled"
|
|
14
|
+
[ngbTooltip]="button.tooltip"
|
|
15
|
+
triggers="hover"
|
|
16
|
+
class="btn {{
|
|
17
|
+
button.color
|
|
18
|
+
}} btn-sm btn-icon rounded-circle waves-effect waves-themed gap-5"
|
|
19
|
+
>
|
|
20
|
+
<i [class]="button.icon"></i>
|
|
21
|
+
</button>
|
|
22
|
+
}
|
|
23
|
+
@if (showBackButton()) {
|
|
24
|
+
<button
|
|
25
|
+
(click)="backClicked.emit()"
|
|
26
|
+
ngbTooltip="Επιστροφή"
|
|
27
|
+
triggers="hover"
|
|
28
|
+
class="btn btn-dark btn-sm btn-icon rounded-circle waves-effect waves-themed"
|
|
29
|
+
>
|
|
30
|
+
<i class="fa-light fa-arrow-left"></i>
|
|
31
|
+
</button>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
(click)="toggleCollapse()"
|
|
36
|
+
ngbTooltip="{{ isCollapsed() ? 'Ανοιγμα' : 'Κλεισιμο' }}"
|
|
37
|
+
class="btn btn-outline-dark btn-sm btn-icon rounded-circle waves-effect waves-themed"
|
|
38
|
+
>
|
|
39
|
+
<i [class]="isCollapsed() ? 'fa-light fa-plus' : 'fa-light fa-minus'"></i>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="panel-container" [class.show]="!isCollapsed()" [hidden]="isCollapsed()">
|
|
45
|
+
<div class="panel-content">
|
|
46
|
+
<ng-content></ng-content>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface KmGenericCardInterface {
|
|
2
|
+
title: string
|
|
3
|
+
subtitle: string
|
|
4
|
+
isCollapsed: boolean
|
|
5
|
+
isVisible: boolean
|
|
6
|
+
buttons: KmGenericCardButtonInterface[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface KmGenericCardButtonInterface {
|
|
10
|
+
label: string
|
|
11
|
+
tooltip: string
|
|
12
|
+
icon: string
|
|
13
|
+
color: string
|
|
14
|
+
clicked: () => void
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
.panel-toolbar {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: row;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
padding: 0;
|
|
7
|
+
align-items: center; /* Centers items vertically in the container */
|
|
8
|
+
justify-content: start; /* Optional: centers the group horizontally in the cell */
|
|
9
|
+
margin-right: 15px;
|
|
10
|
+
}
|
|
11
|
+
.panel-content {
|
|
12
|
+
overflow-x: scroll;
|
|
13
|
+
max-height: calc(100vh - 200px);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Component, EventEmitter, inject, input, output, signal } from '@angular/core';
|
|
2
|
+
import { KmGenericCardButtonInterface, KmGenericCardInterface } from './km-generic-card.interface';
|
|
3
|
+
import { CommonModule } from '@angular/common';
|
|
4
|
+
import { KmService } from '../../../services/km.service';
|
|
5
|
+
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
|
6
|
+
import { KmBlockDirective } from '../../directives/km-block.directive';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'km-generic-card',
|
|
10
|
+
imports: [CommonModule, KmBlockDirective, NgbTooltip],
|
|
11
|
+
templateUrl: './km-generic-card.html',
|
|
12
|
+
styleUrl: './km-generic-card.scss',
|
|
13
|
+
providers: [KmService],
|
|
14
|
+
})
|
|
15
|
+
export class KmGenericCard {
|
|
16
|
+
panelTitle = input<string>('');
|
|
17
|
+
subtitle = input<string>('');
|
|
18
|
+
visible = input<boolean>(true);
|
|
19
|
+
isCollapsed = signal<boolean>(false);
|
|
20
|
+
isVisible = signal<boolean>(this.visible());
|
|
21
|
+
showBackButton = input<boolean>(false);
|
|
22
|
+
buttons = input<KmGenericCardInterface>();
|
|
23
|
+
formValid = input<boolean | undefined>(undefined);
|
|
24
|
+
backClicked = output<void>();
|
|
25
|
+
service = inject(KmService);
|
|
26
|
+
|
|
27
|
+
clicked = output<void>();
|
|
28
|
+
|
|
29
|
+
toggleCollapse() {
|
|
30
|
+
this.isCollapsed.set(!this.isCollapsed());
|
|
31
|
+
}
|
|
32
|
+
closePanel() {
|
|
33
|
+
this.isVisible.set(false);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onButtonClicked() {
|
|
37
|
+
this.clicked.emit();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onBackClicked() {
|
|
41
|
+
this.backClicked.emit();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="mb-2">
|
|
2
|
+
@if (showLabel()) {
|
|
3
|
+
<label class="form-label fw-700"
|
|
4
|
+
>{{ label() }}
|
|
5
|
+
@if (required()) {
|
|
6
|
+
<span class="text-danger" ngbTooltip="Είναι υποχρεωτικό" triggers="hover">*</span>
|
|
7
|
+
}
|
|
8
|
+
</label>
|
|
9
|
+
}
|
|
10
|
+
<div class="input-wrapper">
|
|
11
|
+
<input
|
|
12
|
+
class="form-control form-control-{{ formControlSize() }}"
|
|
13
|
+
[type]="inputType()"
|
|
14
|
+
[placeholder]="placeholder()"
|
|
15
|
+
(input)="change($event)"
|
|
16
|
+
[value]="value()"
|
|
17
|
+
[class.is-invalid]="invalid() && touched()"
|
|
18
|
+
(blur)="touched.set(true)"
|
|
19
|
+
/>
|
|
20
|
+
<span class="corner top-left"></span>
|
|
21
|
+
<span class="corner top-right"></span>
|
|
22
|
+
<span class="corner bottom-left"></span>
|
|
23
|
+
<span class="corner bottom-right"></span>
|
|
24
|
+
</div>
|
|
25
|
+
@if (invalid() && touched() && errors().length > 0) {
|
|
26
|
+
@for (error of errors(); track error.kind) {
|
|
27
|
+
<small class="invalid-feedback">{{ error.message }}</small>
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Component, inject, input, InputSignal, InputSignalWithTransform, model, output } from '@angular/core'
|
|
2
|
+
import { FormValueControl, ValidationError } from '@angular/forms/signals'
|
|
3
|
+
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'km-input',
|
|
7
|
+
imports: [NgbTooltip],
|
|
8
|
+
templateUrl: './km-input.component.html',
|
|
9
|
+
styleUrl: './km-input.component.scss',
|
|
10
|
+
})
|
|
11
|
+
export class KmInputComponent implements FormValueControl<string | null> {
|
|
12
|
+
readonly value = model<string | null>(null)
|
|
13
|
+
|
|
14
|
+
readonly invalid = input<boolean>(false)
|
|
15
|
+
readonly touched = model<boolean>(false)
|
|
16
|
+
readonly disabled = input<boolean>(false)
|
|
17
|
+
readonly required = input<boolean>(false)
|
|
18
|
+
readonly errors = input<readonly ValidationError[]>([])
|
|
19
|
+
|
|
20
|
+
inputType = input<string>('text')
|
|
21
|
+
placeholder = input<string>('Εισάγετε μια τιμή')
|
|
22
|
+
label = input<string>('Label')
|
|
23
|
+
showLabel = input<boolean>(true)
|
|
24
|
+
|
|
25
|
+
formControlSize = input<string>('sm')
|
|
26
|
+
|
|
27
|
+
change(event: any) {
|
|
28
|
+
this.value.set(event.target.value)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div class="mb-2">
|
|
2
|
+
<label class="form-label fw-700">
|
|
3
|
+
{{ label() }}
|
|
4
|
+
@if (required()) {
|
|
5
|
+
<span class="text-danger">*</span>
|
|
6
|
+
}
|
|
7
|
+
</label>
|
|
8
|
+
<div class="input-group">
|
|
9
|
+
<input
|
|
10
|
+
[type]="type()"
|
|
11
|
+
[placeholder]="placeholder()"
|
|
12
|
+
(change)="change($event)"
|
|
13
|
+
[value]="value()"
|
|
14
|
+
[class.is-invalid]="invalid() && touched()"
|
|
15
|
+
(blur)="touched.set(true)"
|
|
16
|
+
[disabled]="disabled()"
|
|
17
|
+
class="form-control form-control-sm"
|
|
18
|
+
[readOnly]="readOnly()"
|
|
19
|
+
/>
|
|
20
|
+
<button type="button" [class]="buttonClass()" (click)="onActionClicked()">
|
|
21
|
+
<i [class]="icon()"></i>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component, input, InputSignal, InputSignalWithTransform, model, output } from '@angular/core';
|
|
2
|
+
import { FormValueControl } from '@angular/forms/signals';
|
|
3
|
+
import { KmInputComponent } from "../km-input/km-input.component";
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'km-input-group',
|
|
7
|
+
imports: [],
|
|
8
|
+
templateUrl: './km-input-group.component.html',
|
|
9
|
+
styleUrl: './km-input-group.component.scss',
|
|
10
|
+
})
|
|
11
|
+
export class KmInputGroupComponent implements FormValueControl<number | null> {
|
|
12
|
+
|
|
13
|
+
readonly value = model<number | null>(null);
|
|
14
|
+
|
|
15
|
+
readonly invalid = input<boolean>(false);
|
|
16
|
+
readonly required = input<boolean>(false);
|
|
17
|
+
readonly touched = model<boolean>(false);
|
|
18
|
+
readonly disabled = input<boolean>(false);
|
|
19
|
+
readonly errors = input<readonly any[]>([]);
|
|
20
|
+
readonly readOnly = input<boolean>(false, { alias: 'readonly' });
|
|
21
|
+
actionClicked = output<void>();
|
|
22
|
+
|
|
23
|
+
placeholder = input<string>('Εισάγετε τιμή');
|
|
24
|
+
label = input<string>('Label');
|
|
25
|
+
addonText = input<string>('');
|
|
26
|
+
type = input<string>('text');
|
|
27
|
+
icon = input<string>('');
|
|
28
|
+
buttonClass = input<string>('');
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
onActionClicked() {
|
|
32
|
+
this.actionClicked.emit();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
change(event: any) {
|
|
37
|
+
if (this.disabled()) return;
|
|
38
|
+
if (this.readOnly()) return;
|
|
39
|
+
this.touched.set(true);
|
|
40
|
+
this.value.set(event.target.value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<div class="compact-modal">
|
|
2
|
+
<div class="modal-header d-flex align-items-center bg-light border-bottom-1">
|
|
3
|
+
<h6 class="modal-title text-truncate me-3">{{ title() }}</h6>
|
|
4
|
+
|
|
5
|
+
<div class="ms-auto d-flex gap-1">
|
|
6
|
+
<ng-content select="[header-actions]"></ng-content>
|
|
7
|
+
|
|
8
|
+
<button type="button" class="btn-close ms-2" (click)="cancelClicked()" aria-label="Close"></button>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="modal-body">
|
|
13
|
+
<ng-content></ng-content>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="modal-footer border-top-0">
|
|
17
|
+
@if(showReset()){
|
|
18
|
+
<button type="button" class="btn btn-sm-custom btn-link text-muted me-auto px-0" (click)="reset.emit()">
|
|
19
|
+
{{ resetLabel() }}
|
|
20
|
+
</button>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
<button type="button" class="btn btn-sm-custom btn-secondary px-3" (click)="cancelClicked()">
|
|
24
|
+
{{ cancelLabel() }}
|
|
25
|
+
</button>
|
|
26
|
+
|
|
27
|
+
@if(showSave()){
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="btn btn-sm-custom btn-primary px-4"
|
|
31
|
+
[disabled]="saveDisabled()"
|
|
32
|
+
(click)="save.emit()"
|
|
33
|
+
>
|
|
34
|
+
{{ saveLabel() }}
|
|
35
|
+
</button>
|
|
36
|
+
}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Target the modal specifically to save space */
|
|
2
|
+
.compact-modal {
|
|
3
|
+
.modal-header {
|
|
4
|
+
padding: 0.3rem 1rem; // Half of default bootstrap padding
|
|
5
|
+
min-height: 32px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.modal-body {
|
|
9
|
+
padding: 0.75rem 1rem; // Reduces vertical scrolling
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.modal-footer {
|
|
13
|
+
padding: 0.4rem 1rem; // Slim footer
|
|
14
|
+
background-color: #f8f9fa; // Subtle distinction
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.modal-title {
|
|
18
|
+
font-size: 1rem; // Smaller heading
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.btn-sm-custom {
|
|
23
|
+
padding: 0.2rem 0.5rem;
|
|
24
|
+
font-size: 0.85rem;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Component, input, output, outputBinding } from '@angular/core'
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'km-modal',
|
|
5
|
+
imports: [],
|
|
6
|
+
templateUrl: './km-modal.html',
|
|
7
|
+
styleUrl: './km-modal.scss',
|
|
8
|
+
})
|
|
9
|
+
export class KmModal {
|
|
10
|
+
title = input('Τίτλος παραθύρου')
|
|
11
|
+
saveLabel = input('Αποθήκευση')
|
|
12
|
+
cancelLabel = input('Ακύρωση')
|
|
13
|
+
closeLabel = input('Κλείσιμο')
|
|
14
|
+
resetLabel = input('Επαναφορά')
|
|
15
|
+
showSave = input<boolean>(true)
|
|
16
|
+
showReset = input<boolean>(false)
|
|
17
|
+
showClose = input('Κλείσιμο')
|
|
18
|
+
saveDisabled = input<boolean>(false)
|
|
19
|
+
|
|
20
|
+
save = output()
|
|
21
|
+
cancel = output()
|
|
22
|
+
closeClicked = output()
|
|
23
|
+
reset = output()
|
|
24
|
+
|
|
25
|
+
saveClicked() {
|
|
26
|
+
this.save.emit()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
cancelClicked() {
|
|
30
|
+
this.cancel.emit()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
resetClicked() {
|
|
34
|
+
this.reset.emit()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
close() {
|
|
38
|
+
this.closeClicked.emit()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<label class="form-label fw-700">{{ label() }}</label>
|
|
2
|
+
<ng-select
|
|
3
|
+
[ngModel]="value()"
|
|
4
|
+
[class]="class()"
|
|
5
|
+
[class.is-invalid]="invalid() && touched()"
|
|
6
|
+
[items]="items()"
|
|
7
|
+
[multiple]="multiple()"
|
|
8
|
+
[bindLabel]="bindLabel()"
|
|
9
|
+
appendTo="body"
|
|
10
|
+
[bindValue]="bindValue()"
|
|
11
|
+
[placeholder]="placeholder()"
|
|
12
|
+
[searchable]="searchable()"
|
|
13
|
+
[clearable]="clearable()"
|
|
14
|
+
[disabled]="disabled()"
|
|
15
|
+
(change)="change($event)"
|
|
16
|
+
(clear)="value.set(null)"
|
|
17
|
+
[readonly]="readOnly()"
|
|
18
|
+
[closeOnSelect]="!multiple()"
|
|
19
|
+
>
|
|
20
|
+
@if (multiple()) {
|
|
21
|
+
<ng-template ng-multi-label-tmp let-items="items" let-clear="clear">
|
|
22
|
+
@for (item of items | slice: 0 : 2; track item) {
|
|
23
|
+
<div class="ng-value">
|
|
24
|
+
<span class="ng-value-label">{{ $any(item)[bindLabel()].value }}</span>
|
|
25
|
+
<span class="ng-value-icon right" (click)="clear(item)" aria-hidden="true">×</span>
|
|
26
|
+
</div>
|
|
27
|
+
}
|
|
28
|
+
@if (items.length > 2) {
|
|
29
|
+
<div class="ng-value">
|
|
30
|
+
<span class="ng-value-label">{{ items.length - 2 }} more...</span>
|
|
31
|
+
</div>
|
|
32
|
+
}
|
|
33
|
+
</ng-template>
|
|
34
|
+
}
|
|
35
|
+
</ng-select>
|
|
36
|
+
@if (invalid() && touched() && errors().length > 0) {
|
|
37
|
+
@for (error of errors(); track error.kind) {
|
|
38
|
+
<small class="invalid-feedback">{{ error.message }}</small>
|
|
39
|
+
}
|
|
40
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { Component, computed, input, model, ViewEncapsulation } from '@angular/core';
|
|
3
|
+
import { FormsModule } from '@angular/forms';
|
|
4
|
+
import { FormValueControl, ValidationError } from '@angular/forms/signals';
|
|
5
|
+
import { NgSelectModule } from '@ng-select/ng-select';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'km-select',
|
|
9
|
+
imports: [FormsModule, NgSelectModule, CommonModule],
|
|
10
|
+
templateUrl: './km-select.component.html',
|
|
11
|
+
styleUrl: './km-select.component.scss',
|
|
12
|
+
})
|
|
13
|
+
export class KmSelectComponent implements FormValueControl<string | number | null> {
|
|
14
|
+
value = model<string | number | null>(null);
|
|
15
|
+
readonly invalid = input<boolean>(false);
|
|
16
|
+
readonly touched = model<boolean>(false);
|
|
17
|
+
readonly disabled = input<boolean>(false);
|
|
18
|
+
readonly readOnly = input<boolean>(false, { alias: 'readonly' });
|
|
19
|
+
readonly errors = input<readonly ValidationError[]>([]);
|
|
20
|
+
|
|
21
|
+
items = input.required<any[]>();
|
|
22
|
+
multiple = input<boolean>(true);
|
|
23
|
+
bindLabel = input<string>('label');
|
|
24
|
+
bindValue = input<string>('value');
|
|
25
|
+
placeholder = input<string>('Select...');
|
|
26
|
+
class = input<string>('custom');
|
|
27
|
+
limit = input<number>(3);
|
|
28
|
+
label = input<string>('Label');
|
|
29
|
+
|
|
30
|
+
searchable = input<boolean>(true);
|
|
31
|
+
clearable = input<boolean>(true);
|
|
32
|
+
|
|
33
|
+
displayArray = computed(() => {
|
|
34
|
+
const val = this.value();
|
|
35
|
+
if (!val) return [];
|
|
36
|
+
return Array.isArray(val) ? val : [val];
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
change = (event: any) => {
|
|
40
|
+
this.touched.set(true);
|
|
41
|
+
this.value.set(event[this.bindValue()]);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
clear = () => {
|
|
45
|
+
+this.value.set(null);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
remainingCount = computed(() => Math.max(0, this.displayArray().length - this.limit()));
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public API Surface of my-km-components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export * from './lib/km-select/km-select.component';
|
|
6
|
+
export * from './lib/km-input/km-input.component';
|
|
7
|
+
export * from './lib/km-color-combo/km-color-combo.component';
|
|
8
|
+
export * from './lib/km-input-group/km-input-group.component';
|
|
9
|
+
export * from './lib/km-modal/km-modal';
|
|
10
|
+
export * from './lib/km-generic-card/km-generic-card';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/lib",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"types": []
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.ts"
|
|
13
|
+
],
|
|
14
|
+
"exclude": [
|
|
15
|
+
"**/*.spec.ts"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.lib.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"declarationMap": false
|
|
7
|
+
},
|
|
8
|
+
"angularCompilerOptions": {
|
|
9
|
+
"compilationMode": "partial"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/spec",
|
|
7
|
+
"types": [
|
|
8
|
+
"vitest/globals"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.d.ts",
|
|
13
|
+
"src/**/*.spec.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|