@keepui/ui 0.1.0
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 +267 -0
- package/capacitor/index.d.ts +6 -0
- package/capacitor/keepui-ui-capacitor.d.ts.map +1 -0
- package/capacitor/lib/providers/provide-keep-ui-capacitor.d.ts +22 -0
- package/capacitor/lib/providers/provide-keep-ui-capacitor.d.ts.map +1 -0
- package/capacitor/lib/services/capacitor-file.service.d.ts +23 -0
- package/capacitor/lib/services/capacitor-file.service.d.ts.map +1 -0
- package/capacitor/public-api.d.ts +3 -0
- package/capacitor/public-api.d.ts.map +1 -0
- package/fesm2022/keepui-ui-capacitor.mjs +79 -0
- package/fesm2022/keepui-ui-capacitor.mjs.map +1 -0
- package/fesm2022/keepui-ui.mjs +656 -0
- package/fesm2022/keepui-ui.mjs.map +1 -0
- package/index.d.ts +6 -0
- package/keepui-ui.d.ts.map +1 -0
- package/lib/components/button/button.component.d.ts +72 -0
- package/lib/components/button/button.component.d.ts.map +1 -0
- package/lib/components/button/button.types.d.ts +17 -0
- package/lib/components/button/button.types.d.ts.map +1 -0
- package/lib/components/card/card.component.d.ts +23 -0
- package/lib/components/card/card.component.d.ts.map +1 -0
- package/lib/components/image-preview/image-preview.component.d.ts +40 -0
- package/lib/components/image-preview/image-preview.component.d.ts.map +1 -0
- package/lib/i18n/keep-ui-translations.d.ts +21 -0
- package/lib/i18n/keep-ui-translations.d.ts.map +1 -0
- package/lib/i18n/translation-keys.d.ts +26 -0
- package/lib/i18n/translation-keys.d.ts.map +1 -0
- package/lib/models/file-result.model.d.ts +11 -0
- package/lib/models/file-result.model.d.ts.map +1 -0
- package/lib/ports/file.port.d.ts +17 -0
- package/lib/ports/file.port.d.ts.map +1 -0
- package/lib/providers/keep-ui-i18n.provider.d.ts +43 -0
- package/lib/providers/keep-ui-i18n.provider.d.ts.map +1 -0
- package/lib/providers/provide-keep-ui.d.ts +16 -0
- package/lib/providers/provide-keep-ui.d.ts.map +1 -0
- package/lib/services/keep-ui-language.service.d.ts +36 -0
- package/lib/services/keep-ui-language.service.d.ts.map +1 -0
- package/lib/services/keep-ui-transloco-loader.service.d.ts +18 -0
- package/lib/services/keep-ui-transloco-loader.service.d.ts.map +1 -0
- package/lib/services/web-file.service.d.ts +17 -0
- package/lib/services/web-file.service.d.ts.map +1 -0
- package/lib/testing/mock-file.service.d.ts +21 -0
- package/lib/testing/mock-file.service.d.ts.map +1 -0
- package/lib/tokens/file.token.d.ts +11 -0
- package/lib/tokens/file.token.d.ts.map +1 -0
- package/package.json +40 -0
- package/public-api.d.ts +15 -0
- package/public-api.d.ts.map +1 -0
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.d.ts +12 -0
- package/schematics/ng-add/index.js +34 -0
- package/schematics/ng-add/index.js.map +1 -0
- package/schematics/ng-add/index.ts +42 -0
- package/schematics/ng-add/schema.json +16 -0
- package/styles/_index.css +73 -0
- package/styles/themes.css +130 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, input, output, computed, ChangeDetectionStrategy, Component, inject, signal, makeEnvironmentProviders } from '@angular/core';
|
|
3
|
+
import { TranslocoPipe, TRANSLOCO_SCOPE, TranslocoService, provideTransloco } from '@jsverse/transloco';
|
|
4
|
+
import { of } from 'rxjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Injection token for the platform file/image adapter.
|
|
8
|
+
*
|
|
9
|
+
* Register a concrete implementation with:
|
|
10
|
+
* - `provideKeepUi()` for web projects
|
|
11
|
+
* - `provideKeepUiCapacitor()` for Angular + Capacitor projects
|
|
12
|
+
*/
|
|
13
|
+
const FILE_PORT = new InjectionToken('FILE_PORT');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Web implementation of `FilePort`.
|
|
17
|
+
*
|
|
18
|
+
* Uses a hidden `<input type="file">` and the `FileReader` API to let the user
|
|
19
|
+
* pick an image from the file system in a browser environment.
|
|
20
|
+
*
|
|
21
|
+
* This implementation has no dependency on Capacitor or any native plugin.
|
|
22
|
+
*/
|
|
23
|
+
class WebFileService {
|
|
24
|
+
pickImage() {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const input = document.createElement('input');
|
|
27
|
+
input.type = 'file';
|
|
28
|
+
input.accept = 'image/*';
|
|
29
|
+
input.onchange = () => {
|
|
30
|
+
const file = input.files?.[0];
|
|
31
|
+
if (!file) {
|
|
32
|
+
reject(new Error('No file selected'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const reader = new FileReader();
|
|
36
|
+
reader.onload = () => {
|
|
37
|
+
resolve({
|
|
38
|
+
dataUrl: reader.result,
|
|
39
|
+
mimeType: file.type,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
reader.onerror = () => reject(reader.error ?? new Error('FileReader error'));
|
|
43
|
+
reader.readAsDataURL(file);
|
|
44
|
+
};
|
|
45
|
+
input.click();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: WebFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
49
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: WebFileService }); }
|
|
50
|
+
}
|
|
51
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: WebFileService, decorators: [{
|
|
52
|
+
type: Injectable
|
|
53
|
+
}] });
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Accessible, themed action button with support for variants, shapes, sizes,
|
|
57
|
+
* loading state, full-width layout, and named icon slots.
|
|
58
|
+
*
|
|
59
|
+
* Works identically on **web** and **Angular + Capacitor** (no native API usage).
|
|
60
|
+
*
|
|
61
|
+
* ```html
|
|
62
|
+
* <!-- Basic usage -->
|
|
63
|
+
* <keepui-button (clicked)="save()">Save</keepui-button>
|
|
64
|
+
*
|
|
65
|
+
* <!-- Primary, pill-shaped, fixed width -->
|
|
66
|
+
* <keepui-button variant="primary" shape="pill">Confirm</keepui-button>
|
|
67
|
+
*
|
|
68
|
+
* <!-- With leading icon (any inline element) -->
|
|
69
|
+
* <keepui-button variant="outline" size="auto">
|
|
70
|
+
* <svg slot="leading" width="16" height="16" aria-hidden="true">…</svg>
|
|
71
|
+
* Upload
|
|
72
|
+
* </keepui-button>
|
|
73
|
+
*
|
|
74
|
+
* <!-- Loading state -->
|
|
75
|
+
* <keepui-button [loading]="isSaving()">Saving…</keepui-button>
|
|
76
|
+
*
|
|
77
|
+
* <!-- Full-width, danger -->
|
|
78
|
+
* <keepui-button variant="danger" [fullWidth]="true">Delete account</keepui-button>
|
|
79
|
+
*
|
|
80
|
+
* <!-- Icon-only (requires ariaLabel for accessibility) -->
|
|
81
|
+
* <keepui-button variant="ghost" size="auto" ariaLabel="Close dialog">
|
|
82
|
+
* <svg slot="leading" …>…</svg>
|
|
83
|
+
* </keepui-button>
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
class ButtonComponent {
|
|
87
|
+
constructor() {
|
|
88
|
+
/** Visual style of the button. @default 'primary' */
|
|
89
|
+
this.variant = input('primary');
|
|
90
|
+
/**
|
|
91
|
+
* Size mode.
|
|
92
|
+
* - `md` → fixed 160 px wide, 40 px tall.
|
|
93
|
+
* - `auto` → padding-driven width, 40 px tall.
|
|
94
|
+
* @default 'md'
|
|
95
|
+
*/
|
|
96
|
+
this.size = input('md');
|
|
97
|
+
/** Border-radius style. @default 'pill' */
|
|
98
|
+
this.shape = input('pill');
|
|
99
|
+
/** HTML `type` attribute of the inner `<button>`. @default 'button' */
|
|
100
|
+
this.type = input('button');
|
|
101
|
+
/** Disables the button when `true`. @default false */
|
|
102
|
+
this.disabled = input(false);
|
|
103
|
+
/**
|
|
104
|
+
* Replaces the content with an animated spinner and sets `aria-busy`.
|
|
105
|
+
* Also disables the button until the operation completes.
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
this.loading = input(false);
|
|
109
|
+
/** Expands the button to fill its container width. @default false */
|
|
110
|
+
this.fullWidth = input(false);
|
|
111
|
+
/**
|
|
112
|
+
* Accessible label for icon-only buttons.
|
|
113
|
+
* When provided, sets the `aria-label` attribute on the inner `<button>`.
|
|
114
|
+
* @default ''
|
|
115
|
+
*/
|
|
116
|
+
this.ariaLabel = input('');
|
|
117
|
+
/** Emitted when the button is clicked and is not disabled or loading. */
|
|
118
|
+
this.clicked = output();
|
|
119
|
+
this.isDisabled = computed(() => this.disabled() || this.loading());
|
|
120
|
+
this.buttonClass = computed(() => {
|
|
121
|
+
const base = [
|
|
122
|
+
'inline-flex items-center justify-center gap-2',
|
|
123
|
+
'min-h-[2.75rem] text-sm font-medium cursor-pointer select-none',
|
|
124
|
+
'transition-colors duration-200',
|
|
125
|
+
'focus-visible:outline-none focus-visible:ring-2',
|
|
126
|
+
'focus-visible:ring-keepui-primary focus-visible:ring-offset-2',
|
|
127
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
128
|
+
].join(' ');
|
|
129
|
+
const sizeClass = this.fullWidth()
|
|
130
|
+
? 'w-full h-10 px-4'
|
|
131
|
+
: this.size() === 'md'
|
|
132
|
+
? 'w-40 h-10'
|
|
133
|
+
: 'px-6 h-10';
|
|
134
|
+
const shapeMap = {
|
|
135
|
+
pill: 'rounded-full',
|
|
136
|
+
rounded: 'rounded-2xl',
|
|
137
|
+
};
|
|
138
|
+
const variantMap = {
|
|
139
|
+
primary: [
|
|
140
|
+
'bg-keepui-primary text-keepui-primary-fg border border-keepui-primary',
|
|
141
|
+
'enabled:hover:bg-keepui-primary-hover enabled:hover:border-keepui-primary-hover',
|
|
142
|
+
'enabled:active:bg-keepui-primary-active enabled:active:border-keepui-primary-active',
|
|
143
|
+
].join(' '),
|
|
144
|
+
secondary: [
|
|
145
|
+
'bg-keepui-surface text-keepui-text border border-keepui-border',
|
|
146
|
+
'enabled:hover:bg-keepui-surface-hover enabled:hover:border-keepui-border-strong',
|
|
147
|
+
'disabled:text-keepui-text-disabled',
|
|
148
|
+
].join(' '),
|
|
149
|
+
outline: [
|
|
150
|
+
'bg-transparent border border-keepui-border text-keepui-text',
|
|
151
|
+
'enabled:hover:border-keepui-primary enabled:hover:text-keepui-primary',
|
|
152
|
+
].join(' '),
|
|
153
|
+
ghost: [
|
|
154
|
+
'bg-transparent border border-keepui-primary text-keepui-primary',
|
|
155
|
+
'enabled:hover:bg-keepui-primary/10',
|
|
156
|
+
].join(' '),
|
|
157
|
+
danger: [
|
|
158
|
+
'bg-keepui-error text-keepui-error-fg border border-keepui-error',
|
|
159
|
+
'enabled:hover:opacity-90',
|
|
160
|
+
'enabled:active:opacity-80',
|
|
161
|
+
].join(' '),
|
|
162
|
+
};
|
|
163
|
+
return [base, sizeClass, shapeMap[this.shape()], variantMap[this.variant()]].join(' ');
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
handleClick() {
|
|
167
|
+
if (!this.isDisabled()) {
|
|
168
|
+
this.clicked.emit();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
172
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: ButtonComponent, isStandalone: true, selector: "keepui-button", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, host: { properties: { "style.display": "fullWidth() ? \"block\" : \"inline-block\"", "style.width": "fullWidth() ? \"100%\" : null" } }, ngImport: i0, template: `
|
|
173
|
+
<button
|
|
174
|
+
[attr.type]="type()"
|
|
175
|
+
[disabled]="isDisabled()"
|
|
176
|
+
[attr.aria-disabled]="isDisabled() ? true : null"
|
|
177
|
+
[attr.aria-busy]="loading() ? true : null"
|
|
178
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
179
|
+
(click)="handleClick()"
|
|
180
|
+
[class]="buttonClass()"
|
|
181
|
+
>
|
|
182
|
+
@if (loading()) {
|
|
183
|
+
<span class="keepui-btn-spinner" aria-hidden="true"></span>
|
|
184
|
+
} @else {
|
|
185
|
+
<ng-content select="[slot='leading']" />
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
<ng-content />
|
|
189
|
+
|
|
190
|
+
@if (!loading()) {
|
|
191
|
+
<ng-content select="[slot='trailing']" />
|
|
192
|
+
}
|
|
193
|
+
</button>
|
|
194
|
+
`, isInline: true, styles: [".keepui-btn-spinner{display:inline-block;width:1em;height:1em;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:keepui-spin .65s linear infinite;flex-shrink:0}@keyframes keepui-spin{to{transform:rotate(360deg)}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
195
|
+
}
|
|
196
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
|
|
197
|
+
type: Component,
|
|
198
|
+
args: [{ selector: 'keepui-button', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
199
|
+
'[style.display]': 'fullWidth() ? "block" : "inline-block"',
|
|
200
|
+
'[style.width]': 'fullWidth() ? "100%" : null',
|
|
201
|
+
}, template: `
|
|
202
|
+
<button
|
|
203
|
+
[attr.type]="type()"
|
|
204
|
+
[disabled]="isDisabled()"
|
|
205
|
+
[attr.aria-disabled]="isDisabled() ? true : null"
|
|
206
|
+
[attr.aria-busy]="loading() ? true : null"
|
|
207
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
208
|
+
(click)="handleClick()"
|
|
209
|
+
[class]="buttonClass()"
|
|
210
|
+
>
|
|
211
|
+
@if (loading()) {
|
|
212
|
+
<span class="keepui-btn-spinner" aria-hidden="true"></span>
|
|
213
|
+
} @else {
|
|
214
|
+
<ng-content select="[slot='leading']" />
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
<ng-content />
|
|
218
|
+
|
|
219
|
+
@if (!loading()) {
|
|
220
|
+
<ng-content select="[slot='trailing']" />
|
|
221
|
+
}
|
|
222
|
+
</button>
|
|
223
|
+
`, styles: [".keepui-btn-spinner{display:inline-block;width:1em;height:1em;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:keepui-spin .65s linear infinite;flex-shrink:0}@keyframes keepui-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
224
|
+
}] });
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A versatile card container component.
|
|
228
|
+
*
|
|
229
|
+
* ```html
|
|
230
|
+
* <keepui-card>
|
|
231
|
+
* <h2>Title</h2>
|
|
232
|
+
* <p>Some content</p>
|
|
233
|
+
* </keepui-card>
|
|
234
|
+
*
|
|
235
|
+
* <keepui-card [elevation]="2">
|
|
236
|
+
* Elevated card
|
|
237
|
+
* </keepui-card>
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
class CardComponent {
|
|
241
|
+
constructor() {
|
|
242
|
+
/** Shadow elevation level (0–3). */
|
|
243
|
+
this.elevation = input(1);
|
|
244
|
+
this.cardClass = computed(() => {
|
|
245
|
+
const base = [
|
|
246
|
+
'rounded-lg p-4',
|
|
247
|
+
'bg-keepui-surface border border-keepui-border text-keepui-text',
|
|
248
|
+
'transition-all duration-200',
|
|
249
|
+
].join(' ');
|
|
250
|
+
const shadowMap = {
|
|
251
|
+
0: '',
|
|
252
|
+
1: 'shadow-keepui-sm',
|
|
253
|
+
2: 'shadow-keepui-md',
|
|
254
|
+
3: 'shadow-keepui-lg',
|
|
255
|
+
};
|
|
256
|
+
const shadow = shadowMap[this.elevation()];
|
|
257
|
+
return shadow ? `${base} ${shadow}` : base;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
261
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.20", type: CardComponent, isStandalone: true, selector: "keepui-card", inputs: { elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
262
|
+
<div [class]="cardClass()">
|
|
263
|
+
<ng-content />
|
|
264
|
+
</div>
|
|
265
|
+
`, isInline: true }); }
|
|
266
|
+
}
|
|
267
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CardComponent, decorators: [{
|
|
268
|
+
type: Component,
|
|
269
|
+
args: [{
|
|
270
|
+
selector: 'keepui-card',
|
|
271
|
+
standalone: true,
|
|
272
|
+
host: { class: 'block' },
|
|
273
|
+
template: `
|
|
274
|
+
<div [class]="cardClass()">
|
|
275
|
+
<ng-content />
|
|
276
|
+
</div>
|
|
277
|
+
`,
|
|
278
|
+
}]
|
|
279
|
+
}] });
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Typed constants for every translation key used in @keepui/ui.
|
|
283
|
+
*
|
|
284
|
+
* Use these instead of raw strings so that renaming a key is caught by
|
|
285
|
+
* the TypeScript compiler across all consumers.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* import { KEEPUI_TRANSLATION_KEYS as T } from '@keepui/ui';
|
|
290
|
+
*
|
|
291
|
+
* const label = translocoService.translate(T.IMAGE_PREVIEW.SELECT_IMAGE);
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
const KEEPUI_TRANSLATION_KEYS = {
|
|
295
|
+
IMAGE_PREVIEW: {
|
|
296
|
+
SELECT_IMAGE: 'imagePreview.selectImage',
|
|
297
|
+
LOADING: 'imagePreview.loading',
|
|
298
|
+
PREVIEW_ALT: 'imagePreview.previewAlt',
|
|
299
|
+
ERROR_UNEXPECTED: 'imagePreview.errorUnexpected',
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
/** Ordered list of languages available in KeepUI. */
|
|
303
|
+
const KEEPUI_AVAILABLE_LANGUAGES = [
|
|
304
|
+
'en',
|
|
305
|
+
'es',
|
|
306
|
+
'de',
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Standalone component that allows the user to pick and preview an image.
|
|
311
|
+
*
|
|
312
|
+
* This component is fully platform-agnostic. It delegates file picking to
|
|
313
|
+
* whatever `FilePort` implementation is provided via `FILE_PORT`.
|
|
314
|
+
*
|
|
315
|
+
* UI strings are fully internationalised via Transloco (scope `'keepui'`).
|
|
316
|
+
* Call `provideKeepUiI18n()` in your `app.config.ts` and use
|
|
317
|
+
* `KeepUiLanguageService.setLanguage(lang)` to change locale at runtime.
|
|
318
|
+
*
|
|
319
|
+
* Usage:
|
|
320
|
+
* ```html
|
|
321
|
+
* <keepui-image-preview />
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* Prerequisites — register providers in `app.config.ts`:
|
|
325
|
+
* - Web: `provideKeepUi()` + `provideKeepUiI18n()`
|
|
326
|
+
* - Capacitor: `provideKeepUiCapacitor()` + `provideKeepUiI18n()`
|
|
327
|
+
*/
|
|
328
|
+
class ImagePreviewComponent {
|
|
329
|
+
constructor() {
|
|
330
|
+
this.filePort = inject(FILE_PORT);
|
|
331
|
+
/** Translation key references (typed via KEEPUI_TRANSLATION_KEYS). */
|
|
332
|
+
this.keys = KEEPUI_TRANSLATION_KEYS.IMAGE_PREVIEW;
|
|
333
|
+
/** URL of the selected image, ready to bind to `[src]`. */
|
|
334
|
+
this.imageUrl = signal(null);
|
|
335
|
+
/** Error message if the last pick operation failed. */
|
|
336
|
+
this.error = signal(null);
|
|
337
|
+
/** True while the pick operation is in progress. */
|
|
338
|
+
this.loading = signal(false);
|
|
339
|
+
}
|
|
340
|
+
async pickImage() {
|
|
341
|
+
this.error.set(null);
|
|
342
|
+
this.loading.set(true);
|
|
343
|
+
try {
|
|
344
|
+
const result = await this.filePort.pickImage();
|
|
345
|
+
this.imageUrl.set(result.dataUrl);
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
this.error.set(err instanceof Error ? err.message : KEEPUI_TRANSLATION_KEYS.IMAGE_PREVIEW.ERROR_UNEXPECTED);
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
this.loading.set(false);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ImagePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
355
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: ImagePreviewComponent, isStandalone: true, selector: "keepui-image-preview", host: { classAttribute: "block" }, providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'keepui' }], ngImport: i0, template: `
|
|
356
|
+
<div class="flex flex-col gap-4">
|
|
357
|
+
|
|
358
|
+
<keepui-button
|
|
359
|
+
type="button"
|
|
360
|
+
variant="secondary"
|
|
361
|
+
size="auto"
|
|
362
|
+
shape="rounded"
|
|
363
|
+
[loading]="loading()"
|
|
364
|
+
(clicked)="pickImage()"
|
|
365
|
+
>
|
|
366
|
+
{{ (loading() ? keys.LOADING : keys.SELECT_IMAGE) | transloco }}
|
|
367
|
+
</keepui-button>
|
|
368
|
+
|
|
369
|
+
<span role="status" class="sr-only">
|
|
370
|
+
@if (imageUrl()) {
|
|
371
|
+
{{ keys.PREVIEW_ALT | transloco }}
|
|
372
|
+
}
|
|
373
|
+
</span>
|
|
374
|
+
|
|
375
|
+
@if (imageUrl()) {
|
|
376
|
+
<div class="max-w-full">
|
|
377
|
+
<img
|
|
378
|
+
class="max-w-full h-auto rounded border border-keepui-border"
|
|
379
|
+
[src]="imageUrl()"
|
|
380
|
+
[attr.alt]="keys.PREVIEW_ALT | transloco"
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@if (error()) {
|
|
386
|
+
<p class="text-keepui-error text-sm m-0" role="alert">{{ error() }}</p>
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
</div>
|
|
390
|
+
`, isInline: true, dependencies: [{ kind: "pipe", type: TranslocoPipe, name: "transloco" }, { kind: "component", type: ButtonComponent, selector: "keepui-button", inputs: ["variant", "size", "shape", "type", "disabled", "loading", "fullWidth", "ariaLabel"], outputs: ["clicked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
391
|
+
}
|
|
392
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ImagePreviewComponent, decorators: [{
|
|
393
|
+
type: Component,
|
|
394
|
+
args: [{
|
|
395
|
+
selector: 'keepui-image-preview',
|
|
396
|
+
standalone: true,
|
|
397
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
398
|
+
imports: [TranslocoPipe, ButtonComponent],
|
|
399
|
+
providers: [{ provide: TRANSLOCO_SCOPE, useValue: 'keepui' }],
|
|
400
|
+
host: { class: 'block' },
|
|
401
|
+
template: `
|
|
402
|
+
<div class="flex flex-col gap-4">
|
|
403
|
+
|
|
404
|
+
<keepui-button
|
|
405
|
+
type="button"
|
|
406
|
+
variant="secondary"
|
|
407
|
+
size="auto"
|
|
408
|
+
shape="rounded"
|
|
409
|
+
[loading]="loading()"
|
|
410
|
+
(clicked)="pickImage()"
|
|
411
|
+
>
|
|
412
|
+
{{ (loading() ? keys.LOADING : keys.SELECT_IMAGE) | transloco }}
|
|
413
|
+
</keepui-button>
|
|
414
|
+
|
|
415
|
+
<span role="status" class="sr-only">
|
|
416
|
+
@if (imageUrl()) {
|
|
417
|
+
{{ keys.PREVIEW_ALT | transloco }}
|
|
418
|
+
}
|
|
419
|
+
</span>
|
|
420
|
+
|
|
421
|
+
@if (imageUrl()) {
|
|
422
|
+
<div class="max-w-full">
|
|
423
|
+
<img
|
|
424
|
+
class="max-w-full h-auto rounded border border-keepui-border"
|
|
425
|
+
[src]="imageUrl()"
|
|
426
|
+
[attr.alt]="keys.PREVIEW_ALT | transloco"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
@if (error()) {
|
|
432
|
+
<p class="text-keepui-error text-sm m-0" role="alert">{{ error() }}</p>
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
</div>
|
|
436
|
+
`,
|
|
437
|
+
}]
|
|
438
|
+
}] });
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Registers KeepUI core providers for a **web** Angular application.
|
|
442
|
+
*
|
|
443
|
+
* Registers `WebFileService` as the implementation of `FILE_PORT`, enabling
|
|
444
|
+
* all KeepUI components to use the browser's native file picker.
|
|
445
|
+
*
|
|
446
|
+
* Add to `app.config.ts`:
|
|
447
|
+
* ```ts
|
|
448
|
+
* export const appConfig: ApplicationConfig = {
|
|
449
|
+
* providers: [provideKeepUi()],
|
|
450
|
+
* };
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
function provideKeepUi() {
|
|
454
|
+
return makeEnvironmentProviders([
|
|
455
|
+
{ provide: FILE_PORT, useClass: WebFileService },
|
|
456
|
+
]);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Inline translation objects for KeepUI.
|
|
461
|
+
*
|
|
462
|
+
* These mirror `en.json`, `es.json` and `de.json` and are bundled directly
|
|
463
|
+
* into the library so that no HTTP request is needed at runtime.
|
|
464
|
+
*
|
|
465
|
+
* > **Do not edit this file manually.**
|
|
466
|
+
* > Keep it in sync with the JSON files inside `/i18n/`.
|
|
467
|
+
*/
|
|
468
|
+
const EN = {
|
|
469
|
+
imagePreview: {
|
|
470
|
+
selectImage: 'Select image',
|
|
471
|
+
loading: 'Loading…',
|
|
472
|
+
previewAlt: 'Selected image preview',
|
|
473
|
+
errorUnexpected: 'An unexpected error occurred',
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
const ES = {
|
|
477
|
+
imagePreview: {
|
|
478
|
+
selectImage: 'Seleccionar imagen',
|
|
479
|
+
loading: 'Cargando…',
|
|
480
|
+
previewAlt: 'Vista previa de imagen seleccionada',
|
|
481
|
+
errorUnexpected: 'Ha ocurrido un error inesperado',
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
const DE = {
|
|
485
|
+
imagePreview: {
|
|
486
|
+
selectImage: 'Bild auswählen',
|
|
487
|
+
loading: 'Laden…',
|
|
488
|
+
previewAlt: 'Vorschau des ausgewählten Bildes',
|
|
489
|
+
errorUnexpected: 'Ein unerwarteter Fehler ist aufgetreten',
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
/** Map from locale code to its translation object. */
|
|
493
|
+
const KEEPUI_TRANSLATIONS = {
|
|
494
|
+
en: EN,
|
|
495
|
+
es: ES,
|
|
496
|
+
de: DE,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Transloco loader for KeepUI.
|
|
501
|
+
*
|
|
502
|
+
* Serves translations **inline** (bundled in the library JS) so that no HTTP
|
|
503
|
+
* request is required. It handles both plain lang paths (`'en'`, `'es'`, …)
|
|
504
|
+
* and scope-prefixed paths (`'keepui/en'`, `'keepui/es'`, …).
|
|
505
|
+
*
|
|
506
|
+
* @internal
|
|
507
|
+
*/
|
|
508
|
+
class KeepUiTranslocoLoader {
|
|
509
|
+
getTranslation(langPath) {
|
|
510
|
+
// Transloco calls this with 'keepui/en', 'keepui/es', etc.
|
|
511
|
+
// when a component has TRANSLOCO_SCOPE = 'keepui'.
|
|
512
|
+
const lang = langPath.includes('/') ? langPath.split('/')[1] : langPath;
|
|
513
|
+
const isValid = (l) => l === 'en' || l === 'es' || l === 'de';
|
|
514
|
+
const translations = isValid(lang)
|
|
515
|
+
? KEEPUI_TRANSLATIONS[lang]
|
|
516
|
+
: KEEPUI_TRANSLATIONS['en'];
|
|
517
|
+
return of(translations);
|
|
518
|
+
}
|
|
519
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: KeepUiTranslocoLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
520
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: KeepUiTranslocoLoader }); }
|
|
521
|
+
}
|
|
522
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: KeepUiTranslocoLoader, decorators: [{
|
|
523
|
+
type: Injectable
|
|
524
|
+
}] });
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Service that exposes a public API for changing the active language of all
|
|
528
|
+
* KeepUI components at runtime.
|
|
529
|
+
*
|
|
530
|
+
* ### Usage
|
|
531
|
+
* ```ts
|
|
532
|
+
* // Inject anywhere in the host application
|
|
533
|
+
* const lang = inject(KeepUiLanguageService);
|
|
534
|
+
*
|
|
535
|
+
* // Switch to Spanish
|
|
536
|
+
* lang.setLanguage('es');
|
|
537
|
+
*
|
|
538
|
+
* // Read the active language
|
|
539
|
+
* console.log(lang.activeLanguage()); // 'es'
|
|
540
|
+
* ```
|
|
541
|
+
*
|
|
542
|
+
* Register via `provideKeepUiI18n()` in `app.config.ts`.
|
|
543
|
+
*/
|
|
544
|
+
class KeepUiLanguageService {
|
|
545
|
+
constructor() {
|
|
546
|
+
this.transloco = inject(TranslocoService);
|
|
547
|
+
/** Signal that reflects the currently active KeepUI locale. */
|
|
548
|
+
this.activeLanguage = signal(this.transloco.getActiveLang());
|
|
549
|
+
/** Ordered list of all supported locales. */
|
|
550
|
+
this.availableLanguages = KEEPUI_AVAILABLE_LANGUAGES;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Changes the active language for all KeepUI components.
|
|
554
|
+
*
|
|
555
|
+
* @param lang - One of `'en'`, `'es'` or `'de'`.
|
|
556
|
+
*/
|
|
557
|
+
setLanguage(lang) {
|
|
558
|
+
this.transloco.setActiveLang(lang);
|
|
559
|
+
this.activeLanguage.set(lang);
|
|
560
|
+
}
|
|
561
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: KeepUiLanguageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
562
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: KeepUiLanguageService }); }
|
|
563
|
+
}
|
|
564
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: KeepUiLanguageService, decorators: [{
|
|
565
|
+
type: Injectable
|
|
566
|
+
}] });
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Registers all providers required for KeepUI internationalisation.
|
|
570
|
+
*
|
|
571
|
+
* - Configures a self-contained Transloco instance scoped to `'keepui'`.
|
|
572
|
+
* - Bundles all translations **inline** — no HTTP assets required.
|
|
573
|
+
* - Provides {@link KeepUiLanguageService} so the host app can switch locale.
|
|
574
|
+
*
|
|
575
|
+
* ### Usage in `app.config.ts`
|
|
576
|
+
* ```ts
|
|
577
|
+
* export const appConfig: ApplicationConfig = {
|
|
578
|
+
* providers: [
|
|
579
|
+
* provideKeepUi(),
|
|
580
|
+
* provideKeepUiI18n(), // default language: 'en'
|
|
581
|
+
* provideKeepUiI18n({ defaultLang: 'es' }), // or start in Spanish
|
|
582
|
+
* ],
|
|
583
|
+
* };
|
|
584
|
+
* ```
|
|
585
|
+
*
|
|
586
|
+
* ### Changing language at runtime
|
|
587
|
+
* ```ts
|
|
588
|
+
* const lang = inject(KeepUiLanguageService);
|
|
589
|
+
* lang.setLanguage('de');
|
|
590
|
+
* ```
|
|
591
|
+
*
|
|
592
|
+
* > **Note:** If your application already calls `provideTransloco()`, do NOT
|
|
593
|
+
* > call `provideKeepUiI18n()` — instead configure your Transloco loader to
|
|
594
|
+
* > handle the `'keepui/{lang}'` scope paths, and provide `KeepUiLanguageService`
|
|
595
|
+
* > manually.
|
|
596
|
+
*/
|
|
597
|
+
function provideKeepUiI18n(options = {}) {
|
|
598
|
+
const defaultLang = options.defaultLang ?? 'en';
|
|
599
|
+
return makeEnvironmentProviders([
|
|
600
|
+
provideTransloco({
|
|
601
|
+
config: {
|
|
602
|
+
defaultLang,
|
|
603
|
+
availableLangs: [...KEEPUI_AVAILABLE_LANGUAGES],
|
|
604
|
+
reRenderOnLangChange: true,
|
|
605
|
+
failedRetries: 0,
|
|
606
|
+
missingHandler: { useFallbackTranslation: true },
|
|
607
|
+
fallbackLang: 'en',
|
|
608
|
+
},
|
|
609
|
+
loader: KeepUiTranslocoLoader,
|
|
610
|
+
}),
|
|
611
|
+
KeepUiLanguageService,
|
|
612
|
+
]);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* A controllable mock implementation of `FilePort` for use in unit tests.
|
|
617
|
+
*
|
|
618
|
+
* By default, `pickImage()` resolves successfully. Set `nextError` to make the
|
|
619
|
+
* next call reject with that error.
|
|
620
|
+
*
|
|
621
|
+
* ```ts
|
|
622
|
+
* providers: [{ provide: FILE_PORT, useClass: MockFileService }]
|
|
623
|
+
* ```
|
|
624
|
+
*/
|
|
625
|
+
class MockFileService {
|
|
626
|
+
constructor() {
|
|
627
|
+
/** Set this to make the next `pickImage()` call reject with this error. */
|
|
628
|
+
this.nextError = null;
|
|
629
|
+
}
|
|
630
|
+
pickImage() {
|
|
631
|
+
if (this.nextError) {
|
|
632
|
+
const err = this.nextError;
|
|
633
|
+
this.nextError = null;
|
|
634
|
+
return Promise.reject(err);
|
|
635
|
+
}
|
|
636
|
+
return Promise.resolve({
|
|
637
|
+
dataUrl: 'data:image/jpeg;base64,mockImageData==',
|
|
638
|
+
mimeType: 'image/jpeg',
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MockFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
642
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MockFileService }); }
|
|
643
|
+
}
|
|
644
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MockFileService, decorators: [{
|
|
645
|
+
type: Injectable
|
|
646
|
+
}] });
|
|
647
|
+
|
|
648
|
+
// Public API Surface of @keepui/ui
|
|
649
|
+
// Models
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Generated bundle index. Do not edit.
|
|
653
|
+
*/
|
|
654
|
+
|
|
655
|
+
export { ButtonComponent, CardComponent, FILE_PORT, ImagePreviewComponent, KEEPUI_AVAILABLE_LANGUAGES, KEEPUI_TRANSLATIONS, KEEPUI_TRANSLATION_KEYS, KeepUiLanguageService, MockFileService, WebFileService, provideKeepUi, provideKeepUiI18n };
|
|
656
|
+
//# sourceMappingURL=keepui-ui.mjs.map
|