@foxeltech/angular-ui 0.0.6 → 0.0.8
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/fesm2022/foxeltech-angular-ui.mjs +102 -17
- package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
- package/index.d.ts +18 -4
- package/package.json +1 -1
- package/src/lib/styles/theme.css +1 -1
- package/src/lib/ui/components/chart/chart.component.html +18 -9
- package/src/lib/ui/components/dnd-upload/dnd-upload.component.html +20 -11
- package/src/lib/ui/components/tag/tag.component.html +1 -0
- package/theme.css +1 -1
|
@@ -156,6 +156,7 @@ class FxToastrService {
|
|
|
156
156
|
appRef;
|
|
157
157
|
containerRef;
|
|
158
158
|
idCounter = 0;
|
|
159
|
+
recentMessages = new Set();
|
|
159
160
|
constructor(appRef) {
|
|
160
161
|
this.appRef = appRef;
|
|
161
162
|
this.ensureContainerExists();
|
|
@@ -170,6 +171,12 @@ class FxToastrService {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
show(message, type, title) {
|
|
174
|
+
const dedupeKey = `${type}:${message}`;
|
|
175
|
+
if (this.recentMessages.has(dedupeKey)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.recentMessages.add(dedupeKey);
|
|
179
|
+
setTimeout(() => this.recentMessages.delete(dedupeKey), 2000);
|
|
173
180
|
const toast = {
|
|
174
181
|
id: ++this.idCounter,
|
|
175
182
|
message,
|
|
@@ -306,8 +313,30 @@ class FxUtils {
|
|
|
306
313
|
return _.isEqual(model, compareModel);
|
|
307
314
|
}
|
|
308
315
|
static parseError(err) {
|
|
309
|
-
|
|
310
|
-
|
|
316
|
+
const status = _.get(err, 'status');
|
|
317
|
+
switch (status) {
|
|
318
|
+
case 401:
|
|
319
|
+
return 'Session timeout. Please login again.';
|
|
320
|
+
case 403:
|
|
321
|
+
return 'You do not have permission to perform this action.';
|
|
322
|
+
case 404:
|
|
323
|
+
return 'The requested resource was not found.';
|
|
324
|
+
case 408:
|
|
325
|
+
return 'The request timed out. Please try again.';
|
|
326
|
+
case 409:
|
|
327
|
+
return 'A conflict occurred. Please refresh and try again.';
|
|
328
|
+
case 413:
|
|
329
|
+
return 'The uploaded data is too large.';
|
|
330
|
+
case 429:
|
|
331
|
+
return 'Too many requests. Please wait a moment and try again.';
|
|
332
|
+
case 500:
|
|
333
|
+
return 'An internal server error occurred. Please try again later.';
|
|
334
|
+
case 502:
|
|
335
|
+
return 'The server is temporarily unavailable. Please try again later.';
|
|
336
|
+
case 503:
|
|
337
|
+
return 'The service is currently unavailable. Please try again later.';
|
|
338
|
+
case 504:
|
|
339
|
+
return 'The server took too long to respond. Please try again later.';
|
|
311
340
|
}
|
|
312
341
|
if (this.isStringEmpty(_.get(err, 'error.code'))) {
|
|
313
342
|
return _.get(err, 'error.errors', 'Unknown Error');
|
|
@@ -700,7 +729,7 @@ class BaseTableComponent extends BaseComponent {
|
|
|
700
729
|
capitalize(label) {
|
|
701
730
|
return _.capitalize(label);
|
|
702
731
|
}
|
|
703
|
-
value(model, key, defaultValue) {
|
|
732
|
+
value(model, key, defaultValue = '---') {
|
|
704
733
|
return _.get(model, key, defaultValue);
|
|
705
734
|
}
|
|
706
735
|
toggleExpand(element) {
|
|
@@ -1249,14 +1278,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
1249
1278
|
|
|
1250
1279
|
class AuthInterceptor {
|
|
1251
1280
|
authState;
|
|
1281
|
+
handlingUnauthorized = false;
|
|
1252
1282
|
constructor(authState) {
|
|
1253
1283
|
this.authState = authState;
|
|
1254
1284
|
}
|
|
1255
1285
|
intercept(req, next) {
|
|
1256
1286
|
const cloned = req.clone({ withCredentials: true });
|
|
1257
1287
|
return next.handle(cloned).pipe(catchError((error) => {
|
|
1258
|
-
if (error.status === 401) {
|
|
1288
|
+
if (error.status === 401 && !this.handlingUnauthorized) {
|
|
1289
|
+
this.handlingUnauthorized = true;
|
|
1259
1290
|
this.authState.handleUnauthorized();
|
|
1291
|
+
setTimeout(() => {
|
|
1292
|
+
this.handlingUnauthorized = false;
|
|
1293
|
+
}, 2000);
|
|
1260
1294
|
}
|
|
1261
1295
|
return throwError(() => error);
|
|
1262
1296
|
}));
|
|
@@ -1614,11 +1648,13 @@ class ChartComponent {
|
|
|
1614
1648
|
showPieLabel = true;
|
|
1615
1649
|
mobileWidth = 350;
|
|
1616
1650
|
labelFontFamily = 'DMSans';
|
|
1651
|
+
emptyText = 'No data available';
|
|
1617
1652
|
/* ================= OUTPUTS ================= */
|
|
1618
1653
|
chartClick = new EventEmitter();
|
|
1619
1654
|
chartInit = new EventEmitter();
|
|
1620
1655
|
/* ================= INTERNAL ================= */
|
|
1621
1656
|
chartOptions = {};
|
|
1657
|
+
isEmpty = false;
|
|
1622
1658
|
labelFontSize = 12;
|
|
1623
1659
|
chartInstance;
|
|
1624
1660
|
resizeObserver;
|
|
@@ -1695,12 +1731,34 @@ class ChartComponent {
|
|
|
1695
1731
|
this.chartInstance.resize();
|
|
1696
1732
|
}
|
|
1697
1733
|
rebuildChart() {
|
|
1734
|
+
this.isEmpty = this.checkEmpty();
|
|
1735
|
+
if (this.isEmpty) {
|
|
1736
|
+
if (this.chartInstance) {
|
|
1737
|
+
this.chartInstance.clear();
|
|
1738
|
+
}
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1698
1741
|
this.chartOptions = this.buildOptions();
|
|
1699
1742
|
if (this.chartInstance) {
|
|
1700
1743
|
this.chartInstance.clear();
|
|
1701
1744
|
this.chartInstance.setOption(this.chartOptions, true);
|
|
1702
1745
|
}
|
|
1703
1746
|
}
|
|
1747
|
+
checkEmpty() {
|
|
1748
|
+
const type = this.detectType();
|
|
1749
|
+
if (type === 'bar') {
|
|
1750
|
+
const categories = this.data?.categories;
|
|
1751
|
+
const values = this.data?.values;
|
|
1752
|
+
return !categories?.length || !values?.length;
|
|
1753
|
+
}
|
|
1754
|
+
if (type === 'pie') {
|
|
1755
|
+
if (!Array.isArray(this.data) || this.data.length === 0)
|
|
1756
|
+
return true;
|
|
1757
|
+
const total = this.data.reduce((sum, d) => sum + (d.value || 0), 0);
|
|
1758
|
+
return total === 0;
|
|
1759
|
+
}
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1704
1762
|
/* ================= REGISTRY ================= */
|
|
1705
1763
|
registry = {
|
|
1706
1764
|
bar: (data, colors) => ({
|
|
@@ -1940,11 +1998,11 @@ class ChartComponent {
|
|
|
1940
1998
|
});
|
|
1941
1999
|
}
|
|
1942
2000
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: ChartComponent, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1943
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
2001
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: ChartComponent, isStandalone: false, selector: "fx-ui-chart", inputs: { type: "type", data: "data", options: "options", palette: "palette", height: "height", loading: "loading", pieLegendAlign: "pieLegendAlign", pieLegendPosition: "pieLegendPosition", showPieLabel: "showPieLabel", mobileWidth: "mobileWidth", labelFontFamily: "labelFontFamily", emptyText: "emptyText" }, outputs: { chartClick: "chartClick", chartInit: "chartInit" }, host: { listeners: { "window:theme-changed": "onThemeChange()" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"chart-wrapper\" [style.height]=\"height\">\n @if (isEmpty) {\n <div class=\"chart-empty\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"chart-empty-icon\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z\" />\n </svg>\n <span class=\"chart-empty-text\">{{ emptyText }}</span>\n </div>\n } @else {\n <div\n echarts\n [options]=\"chartOptions\"\n [loading]=\"loading\"\n (chartInit)=\"onChartInit($event)\"\n (chartClick)=\"onChartClick($event)\"\n class=\"fx-chart\"\n [style.height]=\"height\"\n ></div>\n }\n</div>\n", styles: [":host{display:block;width:100%}.chart-wrapper{position:relative;width:100%;height:100%}.chart-container{width:100%;height:100%}.chart-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--bg-primary-hover)/.6);font-weight:500;font-size:1.1rem;z-index:2;border-radius:4px}.chart-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:rgba(var(--text-secondary)/1)}.chart-empty-icon{width:48px;height:48px;opacity:.4}.chart-empty-text{font-size:14px;font-weight:500}\n"], dependencies: [{ kind: "directive", type: i1$5.NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }] });
|
|
1944
2002
|
}
|
|
1945
2003
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: ChartComponent, decorators: [{
|
|
1946
2004
|
type: Component,
|
|
1947
|
-
args: [{ selector: 'fx-ui-chart', standalone: false, template: "<div class=\"chart-wrapper\" [style.height]=\"height\">\n <div\n echarts\n
|
|
2005
|
+
args: [{ selector: 'fx-ui-chart', standalone: false, template: "<div class=\"chart-wrapper\" [style.height]=\"height\">\n @if (isEmpty) {\n <div class=\"chart-empty\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"chart-empty-icon\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z\" />\n </svg>\n <span class=\"chart-empty-text\">{{ emptyText }}</span>\n </div>\n } @else {\n <div\n echarts\n [options]=\"chartOptions\"\n [loading]=\"loading\"\n (chartInit)=\"onChartInit($event)\"\n (chartClick)=\"onChartClick($event)\"\n class=\"fx-chart\"\n [style.height]=\"height\"\n ></div>\n }\n</div>\n", styles: [":host{display:block;width:100%}.chart-wrapper{position:relative;width:100%;height:100%}.chart-container{width:100%;height:100%}.chart-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--bg-primary-hover)/.6);font-weight:500;font-size:1.1rem;z-index:2;border-radius:4px}.chart-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:rgba(var(--text-secondary)/1)}.chart-empty-icon{width:48px;height:48px;opacity:.4}.chart-empty-text{font-size:14px;font-weight:500}\n"] }]
|
|
1948
2006
|
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { type: [{
|
|
1949
2007
|
type: Input
|
|
1950
2008
|
}], data: [{
|
|
@@ -1967,6 +2025,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
1967
2025
|
type: Input
|
|
1968
2026
|
}], labelFontFamily: [{
|
|
1969
2027
|
type: Input
|
|
2028
|
+
}], emptyText: [{
|
|
2029
|
+
type: Input
|
|
1970
2030
|
}], chartClick: [{
|
|
1971
2031
|
type: Output
|
|
1972
2032
|
}], chartInit: [{
|
|
@@ -2202,12 +2262,21 @@ class DndUploadComponent {
|
|
|
2202
2262
|
accept = '';
|
|
2203
2263
|
maxSizeMB = 10;
|
|
2204
2264
|
strictSize = true;
|
|
2265
|
+
/** Icon name from heroicons (outline). Default: 'arrow-up-tray' */
|
|
2266
|
+
icon = 'arrow-up-tray';
|
|
2267
|
+
/** CSS color value for accent (border, icon, link). Default uses --gradient-primary-start */
|
|
2268
|
+
accentColor = '';
|
|
2205
2269
|
fileDrop = new EventEmitter();
|
|
2206
2270
|
fileClick = new EventEmitter();
|
|
2207
2271
|
uploadComplete = new EventEmitter();
|
|
2208
2272
|
uploadError = new EventEmitter();
|
|
2209
2273
|
isDragOver = false;
|
|
2210
2274
|
isOpening = false;
|
|
2275
|
+
get accentStyle() {
|
|
2276
|
+
if (!this.accentColor)
|
|
2277
|
+
return {};
|
|
2278
|
+
return { '--dnd-accent': this.accentColor };
|
|
2279
|
+
}
|
|
2211
2280
|
onDragOver(event) {
|
|
2212
2281
|
event.preventDefault();
|
|
2213
2282
|
this.isDragOver = true;
|
|
@@ -2247,21 +2316,21 @@ class DndUploadComponent {
|
|
|
2247
2316
|
const valid = [];
|
|
2248
2317
|
const acceptedTypes = this.accept
|
|
2249
2318
|
.split(',')
|
|
2250
|
-
.map(t => t.trim().toLowerCase())
|
|
2251
|
-
.filter(t => t);
|
|
2319
|
+
.map((t) => t.trim().toLowerCase())
|
|
2320
|
+
.filter((t) => t);
|
|
2252
2321
|
for (const file of files) {
|
|
2253
2322
|
const sizeMB = file.size / 1024 / 1024;
|
|
2254
2323
|
if (this.strictSize && sizeMB > this.maxSizeMB) {
|
|
2255
2324
|
this.uploadError.emit({
|
|
2256
2325
|
file,
|
|
2257
|
-
reason: `File too large: ${sizeMB.toFixed(2)} MB > ${this.maxSizeMB} MB
|
|
2326
|
+
reason: `File too large: ${sizeMB.toFixed(2)} MB > ${this.maxSizeMB} MB`,
|
|
2258
2327
|
});
|
|
2259
2328
|
continue;
|
|
2260
2329
|
}
|
|
2261
2330
|
if (acceptedTypes.length && !this.isAccepted(file, acceptedTypes)) {
|
|
2262
2331
|
this.uploadError.emit({
|
|
2263
2332
|
file,
|
|
2264
|
-
reason: `File type not allowed: "${file.name}"
|
|
2333
|
+
reason: `File type not allowed: "${file.name}"`,
|
|
2265
2334
|
});
|
|
2266
2335
|
continue;
|
|
2267
2336
|
}
|
|
@@ -2273,13 +2342,12 @@ class DndUploadComponent {
|
|
|
2273
2342
|
}
|
|
2274
2343
|
}
|
|
2275
2344
|
onUpload(files) {
|
|
2276
|
-
console.log('Uploading files:', files);
|
|
2277
2345
|
this.uploadComplete.emit(files);
|
|
2278
2346
|
}
|
|
2279
2347
|
isAccepted(file, accepted) {
|
|
2280
2348
|
const fileType = file.type.toLowerCase();
|
|
2281
2349
|
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
|
2282
|
-
return accepted.some(a => {
|
|
2350
|
+
return accepted.some((a) => {
|
|
2283
2351
|
if (a.startsWith('.')) {
|
|
2284
2352
|
return a === fileExt;
|
|
2285
2353
|
}
|
|
@@ -2290,11 +2358,11 @@ class DndUploadComponent {
|
|
|
2290
2358
|
});
|
|
2291
2359
|
}
|
|
2292
2360
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: DndUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2293
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
2361
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: DndUploadComponent, isStandalone: false, selector: "fx-ui-dnd-upload", inputs: { multiple: "multiple", accept: "accept", maxSizeMB: "maxSizeMB", strictSize: "strictSize", icon: "icon", accentColor: "accentColor" }, outputs: { fileDrop: "fileDrop", fileClick: "fileClick", uploadComplete: "uploadComplete", uploadError: "uploadError" }, ngImport: i0, template: "<div\n class=\"dnd-wrapper\"\n [ngStyle]=\"accentStyle\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"dnd-area\"\n [class.drag-over]=\"isDragOver\"\n (click)=\"onAreaClick(fileInput, $event)\"\n (keydown.enter)=\"onAreaClick(fileInput, $event)\"\n >\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"40\"\n class=\"flex dnd-icon\"\n ></fx-ui-hero-icon>\n <div class=\"dnd-text\">\n <span class=\"dnd-link\">Click to upload</span>\n <span class=\"dnd-or\">or drag and drop</span>\n </div>\n <div class=\"dnd-hint\">\n @if (accept) {\n Accepted: {{ accept }}\n } @else {\n Any file type\n }\n \u2014 Max {{ maxSizeMB }} MB\n </div>\n\n <input\n #fileInput\n type=\"file\"\n class=\"dnd-input\"\n [attr.multiple]=\"multiple ? '' : null\"\n [attr.accept]=\"accept\"\n (change)=\"onFileSelected($event)\"\n />\n </div>\n</div>\n", styles: [":host{--dnd-accent: rgba(var(--gradient-primary-start) / 1)}.dnd-wrapper{display:block;width:100%}.dnd-area{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;min-height:160px;padding:32px 24px;border:2px dashed color-mix(in srgb,var(--dnd-accent) 40%,transparent);border-radius:12px;background:color-mix(in srgb,var(--dnd-accent) 4%,transparent);cursor:pointer;outline:none;transition:all .2s ease;position:relative}.dnd-area:hover{border-color:color-mix(in srgb,var(--dnd-accent) 70%,transparent);background:color-mix(in srgb,var(--dnd-accent) 8%,transparent)}.dnd-area:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--dnd-accent) 20%,transparent)}.dnd-area.drag-over{border-color:var(--dnd-accent);background:color-mix(in srgb,var(--dnd-accent) 10%,transparent)}.dnd-icon{color:var(--dnd-accent)}.dnd-text{display:flex;align-items:center;gap:4px;font-size:14px;flex-wrap:wrap;justify-content:center}.dnd-link{color:var(--dnd-accent);font-weight:600;text-decoration:underline}.dnd-or{color:rgba(var(--text-secondary)/1)}.dnd-hint{font-size:12px;color:rgba(var(--text-secondary)/.7)}.dnd-input{position:absolute;width:0;height:0;overflow:hidden;opacity:0}@media(max-width:640px){.dnd-area{min-height:120px;padding:24px 16px}.dnd-text{font-size:13px}}\n"], dependencies: [{ kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }] });
|
|
2294
2362
|
}
|
|
2295
2363
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: DndUploadComponent, decorators: [{
|
|
2296
2364
|
type: Component,
|
|
2297
|
-
args: [{ selector: 'fx-ui-dnd-upload', standalone: false, template: "<div\n class=\"
|
|
2365
|
+
args: [{ selector: 'fx-ui-dnd-upload', standalone: false, template: "<div\n class=\"dnd-wrapper\"\n [ngStyle]=\"accentStyle\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n>\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"dnd-area\"\n [class.drag-over]=\"isDragOver\"\n (click)=\"onAreaClick(fileInput, $event)\"\n (keydown.enter)=\"onAreaClick(fileInput, $event)\"\n >\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"40\"\n class=\"flex dnd-icon\"\n ></fx-ui-hero-icon>\n <div class=\"dnd-text\">\n <span class=\"dnd-link\">Click to upload</span>\n <span class=\"dnd-or\">or drag and drop</span>\n </div>\n <div class=\"dnd-hint\">\n @if (accept) {\n Accepted: {{ accept }}\n } @else {\n Any file type\n }\n \u2014 Max {{ maxSizeMB }} MB\n </div>\n\n <input\n #fileInput\n type=\"file\"\n class=\"dnd-input\"\n [attr.multiple]=\"multiple ? '' : null\"\n [attr.accept]=\"accept\"\n (change)=\"onFileSelected($event)\"\n />\n </div>\n</div>\n", styles: [":host{--dnd-accent: rgba(var(--gradient-primary-start) / 1)}.dnd-wrapper{display:block;width:100%}.dnd-area{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;min-height:160px;padding:32px 24px;border:2px dashed color-mix(in srgb,var(--dnd-accent) 40%,transparent);border-radius:12px;background:color-mix(in srgb,var(--dnd-accent) 4%,transparent);cursor:pointer;outline:none;transition:all .2s ease;position:relative}.dnd-area:hover{border-color:color-mix(in srgb,var(--dnd-accent) 70%,transparent);background:color-mix(in srgb,var(--dnd-accent) 8%,transparent)}.dnd-area:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--dnd-accent) 20%,transparent)}.dnd-area.drag-over{border-color:var(--dnd-accent);background:color-mix(in srgb,var(--dnd-accent) 10%,transparent)}.dnd-icon{color:var(--dnd-accent)}.dnd-text{display:flex;align-items:center;gap:4px;font-size:14px;flex-wrap:wrap;justify-content:center}.dnd-link{color:var(--dnd-accent);font-weight:600;text-decoration:underline}.dnd-or{color:rgba(var(--text-secondary)/1)}.dnd-hint{font-size:12px;color:rgba(var(--text-secondary)/.7)}.dnd-input{position:absolute;width:0;height:0;overflow:hidden;opacity:0}@media(max-width:640px){.dnd-area{min-height:120px;padding:24px 16px}.dnd-text{font-size:13px}}\n"] }]
|
|
2298
2366
|
}], propDecorators: { multiple: [{
|
|
2299
2367
|
type: Input
|
|
2300
2368
|
}], accept: [{
|
|
@@ -2303,6 +2371,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
2303
2371
|
type: Input
|
|
2304
2372
|
}], strictSize: [{
|
|
2305
2373
|
type: Input
|
|
2374
|
+
}], icon: [{
|
|
2375
|
+
type: Input
|
|
2376
|
+
}], accentColor: [{
|
|
2377
|
+
type: Input
|
|
2306
2378
|
}], fileDrop: [{
|
|
2307
2379
|
type: Output
|
|
2308
2380
|
}], fileClick: [{
|
|
@@ -2698,6 +2770,8 @@ class SelectComponent extends FxComponent {
|
|
|
2698
2770
|
class;
|
|
2699
2771
|
enableSearch = false;
|
|
2700
2772
|
searchFn;
|
|
2773
|
+
/** Emits the current value when the select panel closes (useful for multiple mode). */
|
|
2774
|
+
closed = new EventEmitter();
|
|
2701
2775
|
searchTerm = '';
|
|
2702
2776
|
filteredOptions = [];
|
|
2703
2777
|
search$ = new Subject();
|
|
@@ -2720,6 +2794,9 @@ class SelectComponent extends FxComponent {
|
|
|
2720
2794
|
if (this.enableSearch)
|
|
2721
2795
|
this.searchTerm = '';
|
|
2722
2796
|
}
|
|
2797
|
+
else {
|
|
2798
|
+
this.closed.emit(this.value);
|
|
2799
|
+
}
|
|
2723
2800
|
}
|
|
2724
2801
|
onSearchChange(value) {
|
|
2725
2802
|
if (!this.enableSearch)
|
|
@@ -2750,7 +2827,7 @@ class SelectComponent extends FxComponent {
|
|
|
2750
2827
|
return this.value === value;
|
|
2751
2828
|
}
|
|
2752
2829
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: SelectComponent, deps: [{ token: i1$3.NgControl }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2753
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: SelectComponent, isStandalone: false, selector: "fx-ui-select", inputs: { label: "label", placeholder: "placeholder", options: "options", multiple: "multiple", disabled: "disabled", errorMessages: "errorMessages", required: "required", class: "class", enableSearch: "enableSearch", searchFn: "searchFn" }, usesInheritance: true, ngImport: i0, template: "<div class=\"mb-4 text-left w-full\" [class]=\"class\">\n @if (label) {\n <label class=\"block txt-field-label\">\n {{ label }}\n @if (required) {\n <span class=\"txt-required\">*</span>\n }\n </label>\n }\n\n <mat-form-field\n [attr.invalid]=\"invalid || null\"\n class=\"fx-select-field w-full shadow-sm\"\n appearance=\"fill\"\n >\n <mat-select\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [multiple]=\"multiple\"\n [value]=\"value\"\n (selectionChange)=\"onSelectionChange($event.value)\"\n (openedChange)=\"onOpened($event)\"\n (blur)=\"onTouched()\"\n >\n @if (enableSearch) {\n <div class=\"px-semi w-full\">\n <input\n type=\"text\"\n [placeholder]=\"'Search'\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n class=\"text-text-primary placeholder:text-text-placeholder w-full bg-bg-primary border-b px-semi py-normal border-border-default focus:outline-none focus:border-gradient-primary-start text-sm text-text-primary tracking-normal ring-0 transition-colors duration-500 ease-in-out\"\n />\n </div>\n }\n <mat-option *ngFor=\"let option of filteredOptions\" [value]=\"option.value\">\n <span class=\"txt-default\">{{ option.label }}</span>\n </mat-option>\n </mat-select>\n </mat-form-field>\n\n @if (invalid) {\n <div class=\"txt-invalid text-left\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n", styles: ["::ng-deep .fx-select-field{display:block!important;width:100%!important;margin:0!important;padding:0!important;border-radius:.75rem;box-shadow:0 1px 1px rgb(var(--border-default))}::ng-deep .fx-select-field .mat-mdc-form-field-wrapper,::ng-deep .fx-select-field .mat-mdc-text-field-wrapper{padding:0!important;margin:0!important;width:100%!important}::ng-deep .fx-select-field .mdc-text-field{width:100%!important;margin:0!important;padding:0!important;--mdc-shape-small: 0 !important}::ng-deep .fx-select-field .mat-mdc-form-field-flex{width:100%!important;margin:0!important;padding:0 .75rem!important;border:1px solid rgb(var(--border-default));border-radius:.375rem;height:36px!important;align-items:center;background-color:rgb(var(--bg-primary))}::ng-deep .fx-select-field .mdc-notched-outline,::ng-deep .fx-select-field .mat-mdc-form-field-subscript-wrapper{display:none!important}::ng-deep .fx-select-field.mat-focused .mat-mdc-form-field-flex{border:2px solid rgb(var(--gradient-primary-start))!important;outline:none!important;border-radius:.375rem}::ng-deep .fx-select-field.mat-form-field-invalid .mat-mdc-form-field-flex{border:1px solid rgb(var(--bg-error-bold))!important;box-shadow:none!important}::ng-deep mat-form-field[invalid] .mat-mdc-form-field-flex{border:1px solid rgb(var(--bg-error-bold))!important;box-shadow:none!important}::ng-deep .fx-select-field .mat-mdc-select-trigger{padding:0!important;margin:0!important}::ng-deep .fx-select-field .mat-mdc-select-arrow-wrapper{margin-right:0!important}::ng-deep .fx-select-field .mat-mdc-select-arrow-wrapper{color:rgb(var(--text-primary))}::ng-deep .mat-mdc-select-panel{background-color:rgb(var(--bg-primary))!important;border:1px solid rgb(var(--border-default))!important;border-radius:.375rem!important;box-shadow:0 2px 4px #0000001a!important}::ng-deep .fx-select-field .mat-mdc-select-placeholder{color:rgb(var(--text-placeholder))!important;font-size:14px}::ng-deep .fx-select-field .mat-mdc-select-value{color:rgb(var(--text-primary))!important;font-size:14px;font-weight:400}.mat-select-panel .search-option-full-width{padding-left:0!important;padding-right:0!important;display:block;pointer-events:none;line-height:initial;height:auto}.mat-select-panel .search-option-full-width .search-bar{width:100%;padding:8px 16px;border:none;border-bottom:1px solid #ddd;box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "component", type: i3$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }] });
|
|
2830
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: SelectComponent, isStandalone: false, selector: "fx-ui-select", inputs: { label: "label", placeholder: "placeholder", options: "options", multiple: "multiple", disabled: "disabled", errorMessages: "errorMessages", required: "required", class: "class", enableSearch: "enableSearch", searchFn: "searchFn" }, outputs: { closed: "closed" }, usesInheritance: true, ngImport: i0, template: "<div class=\"mb-4 text-left w-full\" [class]=\"class\">\n @if (label) {\n <label class=\"block txt-field-label\">\n {{ label }}\n @if (required) {\n <span class=\"txt-required\">*</span>\n }\n </label>\n }\n\n <mat-form-field\n [attr.invalid]=\"invalid || null\"\n class=\"fx-select-field w-full shadow-sm\"\n appearance=\"fill\"\n >\n <mat-select\n [placeholder]=\"placeholder\"\n [disabled]=\"disabled\"\n [multiple]=\"multiple\"\n [value]=\"value\"\n (selectionChange)=\"onSelectionChange($event.value)\"\n (openedChange)=\"onOpened($event)\"\n (blur)=\"onTouched()\"\n >\n @if (enableSearch) {\n <div class=\"px-semi w-full\">\n <input\n type=\"text\"\n [placeholder]=\"'Search'\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n class=\"text-text-primary placeholder:text-text-placeholder w-full bg-bg-primary border-b px-semi py-normal border-border-default focus:outline-none focus:border-gradient-primary-start text-sm text-text-primary tracking-normal ring-0 transition-colors duration-500 ease-in-out\"\n />\n </div>\n }\n <mat-option *ngFor=\"let option of filteredOptions\" [value]=\"option.value\">\n <span class=\"txt-default\">{{ option.label }}</span>\n </mat-option>\n </mat-select>\n </mat-form-field>\n\n @if (invalid) {\n <div class=\"txt-invalid text-left\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n", styles: ["::ng-deep .fx-select-field{display:block!important;width:100%!important;margin:0!important;padding:0!important;border-radius:.75rem;box-shadow:0 1px 1px rgb(var(--border-default))}::ng-deep .fx-select-field .mat-mdc-form-field-wrapper,::ng-deep .fx-select-field .mat-mdc-text-field-wrapper{padding:0!important;margin:0!important;width:100%!important}::ng-deep .fx-select-field .mdc-text-field{width:100%!important;margin:0!important;padding:0!important;--mdc-shape-small: 0 !important}::ng-deep .fx-select-field .mat-mdc-form-field-flex{width:100%!important;margin:0!important;padding:0 .75rem!important;border:1px solid rgb(var(--border-default));border-radius:.375rem;height:36px!important;align-items:center;background-color:rgb(var(--bg-primary))}::ng-deep .fx-select-field .mdc-notched-outline,::ng-deep .fx-select-field .mat-mdc-form-field-subscript-wrapper{display:none!important}::ng-deep .fx-select-field.mat-focused .mat-mdc-form-field-flex{border:2px solid rgb(var(--gradient-primary-start))!important;outline:none!important;border-radius:.375rem}::ng-deep .fx-select-field.mat-form-field-invalid .mat-mdc-form-field-flex{border:1px solid rgb(var(--bg-error-bold))!important;box-shadow:none!important}::ng-deep mat-form-field[invalid] .mat-mdc-form-field-flex{border:1px solid rgb(var(--bg-error-bold))!important;box-shadow:none!important}::ng-deep .fx-select-field .mat-mdc-select-trigger{padding:0!important;margin:0!important}::ng-deep .fx-select-field .mat-mdc-select-arrow-wrapper{margin-right:0!important}::ng-deep .fx-select-field .mat-mdc-select-arrow-wrapper{color:rgb(var(--text-primary))}::ng-deep .mat-mdc-select-panel{background-color:rgb(var(--bg-primary))!important;border:1px solid rgb(var(--border-default))!important;border-radius:.375rem!important;box-shadow:0 2px 4px #0000001a!important}::ng-deep .fx-select-field .mat-mdc-select-placeholder{color:rgb(var(--text-placeholder))!important;font-size:14px}::ng-deep .fx-select-field .mat-mdc-select-value{color:rgb(var(--text-primary))!important;font-size:14px;font-weight:400}.mat-select-panel .search-option-full-width{padding-left:0!important;padding-right:0!important;display:block;pointer-events:none;line-height:initial;height:auto}.mat-select-panel .search-option-full-width .search-bar{width:100%;padding:8px 16px;border:none;border-bottom:1px solid #ddd;box-sizing:border-box}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "component", type: i3$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }] });
|
|
2754
2831
|
}
|
|
2755
2832
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: SelectComponent, decorators: [{
|
|
2756
2833
|
type: Component,
|
|
@@ -2775,6 +2852,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
2775
2852
|
type: Input
|
|
2776
2853
|
}], searchFn: [{
|
|
2777
2854
|
type: Input
|
|
2855
|
+
}], closed: [{
|
|
2856
|
+
type: Output
|
|
2778
2857
|
}] } });
|
|
2779
2858
|
|
|
2780
2859
|
class SkeletonTableLoadingComponent {
|
|
@@ -3128,12 +3207,16 @@ class TagComponent {
|
|
|
3128
3207
|
rounded = true;
|
|
3129
3208
|
icon;
|
|
3130
3209
|
solid = false;
|
|
3210
|
+
click = new EventEmitter();
|
|
3211
|
+
onClick() {
|
|
3212
|
+
this.click.emit();
|
|
3213
|
+
}
|
|
3131
3214
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: TagComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3132
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: TagComponent, isStandalone: false, selector: "fx-ui-tag", inputs: { label: "label", type: "type", rounded: "rounded", icon: "icon", solid: "solid" }, ngImport: i0, template: "<div\n [class]=\"\n `flex flex-row gap-small ${rounded ? 'tag-round' : 'tag-square'} tag-${type}${solid ? '-solid' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type} ${solid && type !== 'default' ? 'text-white' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type} ${solid && type !== 'default' ? 'text-white font-medium' : ''}`\">\n {{ label }}\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }] });
|
|
3215
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.4", type: TagComponent, isStandalone: false, selector: "fx-ui-tag", inputs: { label: "label", type: "type", rounded: "rounded", icon: "icon", solid: "solid" }, outputs: { click: "click" }, ngImport: i0, template: "<div\n (click)=\"onClick()\"\n [class]=\"\n `flex flex-row gap-small ${rounded ? 'tag-round' : 'tag-square'} tag-${type}${solid ? '-solid' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type} ${solid && type !== 'default' ? 'text-white' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type} ${solid && type !== 'default' ? 'text-white font-medium' : ''}`\">\n {{ label }}\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }] });
|
|
3133
3216
|
}
|
|
3134
3217
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImport: i0, type: TagComponent, decorators: [{
|
|
3135
3218
|
type: Component,
|
|
3136
|
-
args: [{ selector: 'fx-ui-tag', standalone: false, template: "<div\n [class]=\"\n `flex flex-row gap-small ${rounded ? 'tag-round' : 'tag-square'} tag-${type}${solid ? '-solid' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type} ${solid && type !== 'default' ? 'text-white' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type} ${solid && type !== 'default' ? 'text-white font-medium' : ''}`\">\n {{ label }}\n </div>\n</div>\n" }]
|
|
3219
|
+
args: [{ selector: 'fx-ui-tag', standalone: false, template: "<div\n (click)=\"onClick()\"\n [class]=\"\n `flex flex-row gap-small ${rounded ? 'tag-round' : 'tag-square'} tag-${type}${solid ? '-solid' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type} ${solid && type !== 'default' ? 'text-white' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type} ${solid && type !== 'default' ? 'text-white font-medium' : ''}`\">\n {{ label }}\n </div>\n</div>\n" }]
|
|
3137
3220
|
}], propDecorators: { label: [{
|
|
3138
3221
|
type: Input
|
|
3139
3222
|
}], type: [{
|
|
@@ -3144,6 +3227,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.4", ngImpor
|
|
|
3144
3227
|
type: Input
|
|
3145
3228
|
}], solid: [{
|
|
3146
3229
|
type: Input
|
|
3230
|
+
}], click: [{
|
|
3231
|
+
type: Output
|
|
3147
3232
|
}] } });
|
|
3148
3233
|
|
|
3149
3234
|
class TreeDiagram {
|