Browse Source

Update item detail dialog, card installed label, and home page installed items

- Rewrite item detail dialog to match design: new header with subtitle row,
  side-by-side image+description layout, category/use-case chips section,
  markdown readme with style overrides, Close+Install footer buttons
- Description uses tb-markdown component with matching style overrides
- Installed label: check icon + text, no background, TB CE primary color
- Home page fetches installed widgets and solution templates, passes
  installedItem to cards and detail dialog
- Replace "Figma" with "Design" in all source code comments
pull/15347/head
Igor Kulikov 6 months ago
parent
commit
810aa4342f
  1. 2
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html
  2. 26
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss
  3. 36
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
  4. 6
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
  5. 45
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
  6. 290
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html
  7. 690
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss

2
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.html

@ -83,6 +83,7 @@
@for (item of popularWidgets; track item.id) { @for (item of popularWidgets; track item.id) {
<tb-iot-hub-item-card <tb-iot-hub-item-card
[item]="item" [item]="item"
[installedItem]="getInstalledWidget(item)"
[showTypeChip]="false" [showTypeChip]="false"
[showCreator]="true" [showCreator]="true"
(cardClick)="openItemDetail($event)" (cardClick)="openItemDetail($event)"
@ -127,6 +128,7 @@
@for (item of popularSolutionTemplates; track item.id) { @for (item of popularSolutionTemplates; track item.id) {
<tb-iot-hub-item-card <tb-iot-hub-item-card
[item]="item" [item]="item"
[installedItem]="getInstalledSolutionTemplate(item)"
[showTypeChip]="false" [showTypeChip]="false"
[showCreator]="true" [showCreator]="true"
(cardClick)="openItemDetail($event)" (cardClick)="openItemDetail($event)"

26
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss

@ -20,7 +20,7 @@
padding: 16px; padding: 16px;
} }
// Page container — white card with 8px radius, matches Figma "page-container" // Page container — white card with 8px radius, matches Design "page-container"
.tb-iot-hub-home { .tb-iot-hub-home {
position: relative; position: relative;
background: white; background: white;
@ -202,7 +202,7 @@
} }
} }
// Category cards — Figma: centered 1200px container, flex-col gap=20, pb=48, 3-col rows // Category cards — Design: centered 1200px container, flex-col gap=20, pb=48, 3-col rows
.tb-iot-hub-categories { .tb-iot-hub-categories {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@ -212,7 +212,7 @@
padding-bottom: 48px; padding-bottom: 48px;
} }
// Card — Figma: flex-col, gap=24, pt=24, rounded-8, border, overflow-clip, h=220 // Card — Design: flex-col, gap=24, pt=24, rounded-8, border, overflow-clip, h=220
.tb-iot-hub-category-card { .tb-iot-hub-category-card {
height: 220px; height: 220px;
border-radius: 8px; border-radius: 8px;
@ -232,7 +232,7 @@
} }
} }
// Title — Figma: 18px Medium, centered, tracking=0.15, flex child // Title — Design: 18px Medium, centered, tracking=0.15, flex child
.tb-iot-hub-category-title { .tb-iot-hub-category-title {
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
@ -244,7 +244,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
// Card image — Figma: h=148, w=full, overflow-clip, fills remaining space // Card image — Design: h=148, w=full, overflow-clip, fills remaining space
.tb-iot-hub-category-img { .tb-iot-hub-category-img {
width: 100%; width: 100%;
height: 148px; height: 148px;
@ -252,7 +252,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
// Exact Figma gradients (104.75deg angle) // Exact Design gradients (104.75deg angle)
.category-widgets { .category-widgets {
background: linear-gradient(104.75deg, rgb(240, 255, 245) 0%, rgb(147, 246, 182) 100%); background: linear-gradient(104.75deg, rgb(240, 255, 245) 0%, rgb(147, 246, 182) 100%);
} }
@ -277,13 +277,13 @@
background: linear-gradient(104.75deg, rgb(245, 255, 252) 0%, rgb(147, 240, 213) 100%); background: linear-gradient(104.75deg, rgb(245, 255, 252) 0%, rgb(147, 240, 213) 100%);
} }
// Sections — matches Figma "items-section" (px=40, gap between sections ~48px) // Sections — matches Design "items-section" (px=40, gap between sections ~48px)
.tb-iot-hub-section { .tb-iot-hub-section {
padding: 0 40px; padding: 0 40px;
margin-top: 48px; margin-top: 48px;
} }
// Section header — mat-button, Figma: height=40, font-size=20, font-weight=500 // Section header — mat-button, Design: height=40, font-size=20, font-weight=500
.tb-iot-hub-section-header { .tb-iot-hub-section-header {
font-size: 20px; font-size: 20px;
font-weight: 500; font-weight: 500;
@ -298,7 +298,7 @@
} }
// Big card row — matches Figma "cards-row" (5 cards, gap=20, mt=12 from header) // Big card row — matches Design "cards-row" (5 cards, gap=20, mt=12 from header)
.tb-iot-hub-big-cards-row { .tb-iot-hub-big-cards-row {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
@ -306,7 +306,7 @@
margin-top: 12px; margin-top: 12px;
} }
// Compact card grid — matches Figma "cards" (3 columns, row gap=20, col gap=20) // Compact card grid — matches Design "cards" (3 columns, row gap=20, col gap=20)
.tb-iot-hub-compact-cards-grid { .tb-iot-hub-compact-cards-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@ -314,14 +314,14 @@
margin-top: 12px; margin-top: 12px;
} }
// Divider — matches Figma "divider" (1px, rgba(0,0,0,0.12)) // Divider — matches Design "divider" (1px, rgba(0,0,0,0.12))
.tb-iot-hub-divider { .tb-iot-hub-divider {
height: 1px; height: 1px;
background: rgba(0, 0, 0, 0.12); background: rgba(0, 0, 0, 0.12);
margin: 48px 0 0; margin: 48px 0 0;
} }
// Become a Creator — matches Figma (pt=64, text centered, button at y=156) // Become a Creator — matches Design (pt=64, text centered, button at y=156)
.tb-iot-hub-become-creator { .tb-iot-hub-become-creator {
text-align: center; text-align: center;
padding: 64px 40px; padding: 64px 40px;
@ -349,7 +349,7 @@
padding: 80px 0; padding: 80px 0;
} }
// Page load animation — Figma: Default→appear-first, Smart Animate 1.02s, Gentle easing // Page load animation — Design: Default→appear-first, Smart Animate 1.02s, Gentle easing
// Hero elements fade in + slide DOWN 20px; categories fade in + slide UP 20px // Hero elements fade in + slide DOWN 20px; categories fade in + slide UP 20px
@keyframes iot-hub-fade-slide-down { @keyframes iot-hub-fade-slide-down {
from { from {

36
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts

@ -22,6 +22,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models'; import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service'; import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component'; import { TbIotHubItemDetailDialogComponent, IotHubItemDetailDialogData } from './iot-hub-item-detail-dialog.component';
import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component'; import { TbIotHubInstallDialogComponent, IotHubInstallDialogData } from './iot-hub-install-dialog.component';
@ -102,6 +103,9 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
popularCalcFields: MpItemVersionView[] = []; popularCalcFields: MpItemVersionView[] = [];
popularRuleChains: MpItemVersionView[] = []; popularRuleChains: MpItemVersionView[] = [];
installedWidgets: IotHubInstalledItem[] = [];
installedSolutionTemplates: IotHubInstalledItem[] = [];
isLoading = true; isLoading = true;
constructor( constructor(
@ -162,19 +166,32 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
} }
openItemDetail(item: MpItemVersionView): void { openItemDetail(item: MpItemVersionView): void {
const installedItem = this.findInstalledItem(item);
this.dialog.open(TbIotHubItemDetailDialogComponent, { this.dialog.open(TbIotHubItemDetailDialogComponent, {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
autoFocus: false, autoFocus: false,
data: { data: {
item, item,
iotHubApiService: this.iotHubApiService iotHubApiService: this.iotHubApiService,
installedItem
} as IotHubItemDetailDialogData } as IotHubItemDetailDialogData
}); });
} }
private findInstalledItem(item: MpItemVersionView): IotHubInstalledItem | undefined {
switch (item.type) {
case ItemType.WIDGET:
return this.installedWidgets.find(i => i.itemId === item.itemId);
case ItemType.SOLUTION_TEMPLATE:
return this.installedSolutionTemplates.find(i => i.itemId === item.itemId);
default:
return undefined;
}
}
installItem(item: MpItemVersionView): void { installItem(item: MpItemVersionView): void {
this.dialog.open(TbIotHubInstallDialogComponent, { this.dialog.open(TbIotHubInstallDialogComponent, {
panelClass: ['tb-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
item, item,
iotHubApiService: this.iotHubApiService iotHubApiService: this.iotHubApiService
@ -186,6 +203,14 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
this.router.navigate(['/iot-hub/creator', creatorId]); this.router.navigate(['/iot-hub/creator', creatorId]);
} }
getInstalledWidget(item: MpItemVersionView): IotHubInstalledItem | undefined {
return this.installedWidgets.find(i => i.itemId === item.itemId);
}
getInstalledSolutionTemplate(item: MpItemVersionView): IotHubInstalledItem | undefined {
return this.installedSolutionTemplates.find(i => i.itemId === item.itemId);
}
openSignup(): void { openSignup(): void {
window.open('https://iothub.thingsboard.io/signup', '_blank'); window.open('https://iothub.thingsboard.io/signup', '_blank');
} }
@ -193,6 +218,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
private loadPopularItems(): void { private loadPopularItems(): void {
const sortOrder: SortOrder = { property: 'totalInstallCount', direction: Direction.DESC }; const sortOrder: SortOrder = { property: 'totalInstallCount', direction: Direction.DESC };
const config = { ignoreLoading: true }; const config = { ignoreLoading: true };
const installedPageLink = new PageLink(10000, 0);
const buildQuery = (type: ItemType, size: number): MpItemVersionQuery => { const buildQuery = (type: ItemType, size: number): MpItemVersionQuery => {
const pageLink = new PageLink(size, 0, null, sortOrder); const pageLink = new PageLink(size, 0, null, sortOrder);
@ -204,7 +230,9 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, 5), config), dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, 5), config),
solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, 5), config), solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, 5), config),
calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, 6), config), calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, 6), config),
ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, 6), config) ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, 6), config),
installedWidgets: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.WIDGET, config),
installedSolutionTemplates: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.SOLUTION_TEMPLATE, config)
}).subscribe({ }).subscribe({
next: (results) => { next: (results) => {
this.popularWidgets = results.widgets.data; this.popularWidgets = results.widgets.data;
@ -212,6 +240,8 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
this.popularSolutionTemplates = results.solutionTemplates.data; this.popularSolutionTemplates = results.solutionTemplates.data;
this.popularCalcFields = results.calcFields.data; this.popularCalcFields = results.calcFields.data;
this.popularRuleChains = results.ruleChains.data; this.popularRuleChains = results.ruleChains.data;
this.installedWidgets = results.installedWidgets.data;
this.installedSolutionTemplates = results.installedSolutionTemplates.data;
this.isLoading = false; this.isLoading = false;
}, },
error: () => { error: () => {

6
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html

@ -16,7 +16,7 @@
--> -->
@if (isCompactLayout()) { @if (isCompactLayout()) {
<!-- Compact card (CF / RC) — Figma: horizontal row, icon | content | button --> <!-- Compact card (CF / RC) — Design: horizontal row, icon | content | button -->
<div class="tb-iot-hub-card-compact" (click)="onCardClick()"> <div class="tb-iot-hub-card-compact" (click)="onCardClick()">
<div class="tb-iot-hub-compact-icon" [style.background]="item.color || '#4caf50'"> <div class="tb-iot-hub-compact-icon" [style.background]="item.color || '#4caf50'">
<tb-icon>{{ getPlaceholderIcon() }}</tb-icon> <tb-icon>{{ getPlaceholderIcon() }}</tb-icon>
@ -33,7 +33,7 @@
</div> </div>
@if (isInstalled()) { @if (isInstalled()) {
@if (isSameVersion()) { @if (isSameVersion()) {
<span class="tb-iot-hub-card-installed-label">{{ 'iot-hub.installed' | translate }}</span> <span class="tb-iot-hub-card-installed-label"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</span>
} @else { } @else {
<button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button> <button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button>
} }
@ -66,7 +66,7 @@
<span class="tb-iot-hub-card-stats"> <span class="tb-iot-hub-card-stats">
@if (isInstalled()) { @if (isInstalled()) {
@if (isSameVersion()) { @if (isSameVersion()) {
<span class="tb-iot-hub-card-installed-label">{{ 'iot-hub.installed' | translate }}</span> <span class="tb-iot-hub-card-installed-label"><tb-icon>check</tb-icon>{{ 'iot-hub.installed' | translate }}</span>
} @else { } @else {
<button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button> <button class="tb-iot-hub-card-update-btn" (click)="onUpdateClick($event)">{{ 'iot-hub.update' | translate }}</button>
} }

45
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss

@ -19,7 +19,7 @@
height: 100%; height: 100%;
} }
// Card — Figma: white bg, border rgba(0,0,0,0.12), rounded-8, overflow-clip, flex-col, gap-16, pb-16 // Card — Design: white bg, border rgba(0,0,0,0.12), rounded-8, overflow-clip, flex-col, gap-16, pb-16
.tb-iot-hub-item-card { .tb-iot-hub-item-card {
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 8px; border-radius: 8px;
@ -39,7 +39,7 @@
} }
// Preview area — Figma: aspect ~305:172, bg rgba(0,0,0,0.04), overflow-clip, p-12, dot pattern bg // Preview area — Design: aspect ~305:172, bg rgba(0,0,0,0.04), overflow-clip, p-12, dot pattern bg
.tb-iot-hub-card-preview { .tb-iot-hub-card-preview {
aspect-ratio: 305 / 172; aspect-ratio: 305 / 172;
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
@ -88,7 +88,7 @@
font-weight: 600; font-weight: 600;
} }
// Content area — Figma: px-16, gap-8, flex-col // Content area — Design: px-16, gap-8, flex-col
.tb-iot-hub-card-content { .tb-iot-hub-card-content {
padding: 0 16px 16px; padding: 0 16px 16px;
flex: 1; flex: 1;
@ -97,7 +97,7 @@
gap: 8px; gap: 8px;
} }
// Compact card (CF / RC) — Figma: horizontal flex, gap-16, p-16, rounded-8 // Compact card (CF / RC) — Design: horizontal flex, gap-16, p-16, rounded-8
.tb-iot-hub-card-compact { .tb-iot-hub-card-compact {
display: flex; display: flex;
align-items: center; align-items: center;
@ -116,7 +116,7 @@
} }
} }
// Compact icon — Figma: 72x72, rounded-6, colored bg, white icon 36px centered // Compact icon — Design: 72x72, rounded-6, colored bg, white icon 36px centered
.tb-iot-hub-compact-icon { .tb-iot-hub-compact-icon {
width: 72px; width: 72px;
height: 72px; height: 72px;
@ -134,7 +134,7 @@
} }
} }
// Compact content — Figma: flex-1, flex-col, gap-8 // Compact content — Design: flex-1, flex-col, gap-8
.tb-iot-hub-compact-content { .tb-iot-hub-compact-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -143,7 +143,7 @@
gap: 8px; gap: 8px;
} }
// Compact name — Figma: 16px Medium, line-height 24, tracking 0.25, ellipsis // Compact name — Design: 16px Medium, line-height 24, tracking 0.25, ellipsis
.tb-iot-hub-compact-name { .tb-iot-hub-compact-name {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
@ -156,7 +156,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
// Compact meta row — Figma: flex row, gap-8, items-center // Compact meta row — Design: flex row, gap-8, items-center
.tb-iot-hub-compact-meta { .tb-iot-hub-compact-meta {
display: flex; display: flex;
align-items: center; align-items: center;
@ -203,7 +203,7 @@
text-transform: uppercase; text-transform: uppercase;
} }
// Name — Figma: 16px Medium, line-height 24, tracking 0.25, ellipsis // Name — Design: 16px Medium, line-height 24, tracking 0.25, ellipsis
.tb-iot-hub-card-name { .tb-iot-hub-card-name {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
@ -217,7 +217,7 @@
} }
// Footer — Figma: flex, justify-between, no border-top, inline with content // Footer — Design: flex, justify-between, no border-top, inline with content
.tb-iot-hub-card-footer { .tb-iot-hub-card-footer {
display: flex; display: flex;
align-items: center; align-items: center;
@ -230,7 +230,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
margin-top: auto; margin-top: auto;
// Author icon — Figma: person icon, 16px // Author icon — Design: person icon, 16px
.tb-iot-hub-card-author-icon { .tb-iot-hub-card-author-icon {
font-size: 16px; font-size: 16px;
width: 16px; width: 16px;
@ -250,7 +250,7 @@
} }
} }
// Dot separator — Figma: 3px circle, rgba(0,0,0,0.38) // Dot separator — Design: 3px circle, rgba(0,0,0,0.38)
.tb-iot-hub-card-dot { .tb-iot-hub-card-dot {
margin: 0 8px; margin: 0 8px;
flex-shrink: 0; flex-shrink: 0;
@ -268,7 +268,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
// Download stat — Figma: download icon 12px, count 12px Medium uppercase // Download stat — Design: download icon 12px, count 12px Medium uppercase
.tb-iot-hub-card-stat { .tb-iot-hub-card-stat {
display: flex; display: flex;
align-items: center; align-items: center;
@ -287,7 +287,7 @@
} }
// Install button — Figma: border rgba(0,0,0,0.12), rounded-4, px-16 py-6, text #00695c 14px Medium // Install button — Design: border rgba(0,0,0,0.12), rounded-4, px-16 py-6, text #00695c 14px Medium
.tb-iot-hub-card-install-btn { .tb-iot-hub-card-install-btn {
padding: 6px 16px; padding: 6px 16px;
border-radius: 4px; border-radius: 4px;
@ -308,16 +308,27 @@
} }
} }
// Installed label — Design: check icon + text, no bg, #305680, h-32, pl-8 pr-12 py-6, rounded-4, gap-4
.tb-iot-hub-card-installed-label { .tb-iot-hub-card-installed-label {
padding: 6px 16px; display: inline-flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 6px 12px 6px 8px;
border-radius: 4px; border-radius: 4px;
background: #e8f5e9; color: #305680;
color: #2e7d32;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
line-height: 20px; line-height: 20px;
letter-spacing: 0.25px;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
tb-icon {
font-size: 24px;
width: 24px;
height: 24px;
}
} }
.tb-iot-hub-card-update-btn { .tb-iot-hub-card-update-btn {

290
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.html

@ -15,186 +15,106 @@
limitations under the License. limitations under the License.
--> -->
<div mat-dialog-content class="tb-iot-hub-detail-dialog"> <div class="tb-iot-hub-detail-dialog">
<!-- Header (sticky) --> <!-- Header -->
<div class="dlg-header"> <div class="dlg-header">
<div class="dlg-header-top"> <div class="dlg-header-content">
@if (item.type === ItemType.CALCULATED_FIELD || item.type === ItemType.RULE_CHAIN) { <span class="dlg-title">{{ item.name }}</span>
<div class="dlg-icon" [style.background]="item.color || '#6366f1'"> <div class="dlg-subtitle">
<tb-icon>{{ getCompactIcon() }}</tb-icon> <tb-icon class="dlg-subtitle-icon">{{ getTypeIcon() }}</tb-icon>
</div> <span>{{ typeTranslations.get(item.type) | translate }}</span>
} <span class="dlg-dot"></span>
<div class="dlg-header-info"> <tb-icon class="dlg-subtitle-icon">download</tb-icon>
<div class="dlg-title-row"> <span>{{ item.totalInstallCount | shortNumber }} {{ 'iot-hub.installs' | translate }}</span>
<span class="dlg-title">{{ item.name }}</span> <span class="dlg-dot"></span>
<span class="dlg-version">v{{ item.version }}</span> <span>Latest</span>
@if (getSubtypeLabel(); as subtypeLabel) { <span class="dlg-dot"></span>
<span class="dlg-type-badge" [ngClass]="getSubtypeColorClass()">{{ subtypeLabel }}</span> <span>v {{ item.version }}</span>
} @if (item.publishedTime) {
</div> <span class="dlg-dot"></span>
<div class="dlg-subtitle"> <span>{{ item.publishedTime | date:'mediumDate' }}</span>
<span>{{ typeTranslations.get(item.type) | translate }}</span> }
<span class="dlg-sep">&middot;</span>
<span>{{ 'iot-hub.by-creator' | translate:{ name: '' } }}</span>
<span class="dlg-creator-link" (click)="navigateToCreator()">{{ item.creatorDisplayName }}</span>
@if (item.publishedTime) {
<span class="dlg-sep">&middot;</span>
<span>{{ item.publishedTime | date:'mediumDate' }}</span>
}
</div>
</div> </div>
<button mat-icon-button class="dlg-close" (click)="close()" type="button">
<tb-icon>close</tb-icon>
</button>
</div> </div>
<button mat-icon-button class="dlg-close" (click)="close()" type="button">
<tb-icon>close</tb-icon>
</button>
</div> </div>
<!-- Scrollable body --> <!-- Scrollable content -->
<div class="dlg-body"> <div class="dlg-body">
<!-- Widget: image + description side by side --> <!-- Row 1: Image + Description + Author -->
@if (item.type === ItemType.WIDGET) { <div class="dlg-main">
<div class="dlg-hero-split"> <div class="dlg-row-1">
<div class="dlg-hero-image"> <div class="dlg-preview">
@if (getPreviewUrl(); as url) { @if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {
<img [src]="url" alt=""> <div class="dlg-hero-carousel">
} @else { <ngx-hm-carousel
<div class="tb-iot-hub-preview-placeholder"> [(ngModel)]="carouselIndex"
<tb-icon>widgets</tb-icon> [data]="carouselImages"
[infinite]="true"
[autoplay-speed]="3000"
[between-delay]="0"
[autoplay]="true"
[mouse-enable]="true"
class="dlg-carousel">
<section ngx-hm-carousel-container class="dlg-carousel-container">
<article class="dlg-carousel-item"
ngx-hm-carousel-item
*ngFor="let imageUrl of carouselImages">
<img [src]="imageUrl" alt="">
</article>
</section>
<ng-template #carouselPrev>
<button mat-icon-button class="dlg-carousel-nav">
<mat-icon>chevron_left</mat-icon>
</button>
</ng-template>
<ng-template #carouselNext>
<button mat-icon-button class="dlg-carousel-nav">
<mat-icon>chevron_right</mat-icon>
</button>
</ng-template>
<ng-template #carouselDot let-model>
<div class="dlg-carousel-dot"
[class.active]="model.index === model.currentIndex"></div>
</ng-template>
</ngx-hm-carousel>
</div> </div>
}
</div>
@if (item.description) {
<div class="dlg-hero-desc">{{ item.description }}</div>
}
</div>
}
<!-- Dashboard: full-width image + description below -->
@if (item.type === ItemType.DASHBOARD) {
<div class="dlg-hero-full">
<div class="dlg-hero-full-image">
@if (getPreviewUrl(); as url) {
<img [src]="url" alt="">
} @else { } @else {
<div class="tb-iot-hub-preview-placeholder">
<tb-icon>{{ getTypeIcon() }}</tb-icon>
</div>
}
</div>
@if (item.description) {
<div class="dlg-hero-full-desc">{{ item.description }}</div>
}
</div>
}
<!-- Solution Template: image carousel + description below -->
@if (item.type === ItemType.SOLUTION_TEMPLATE) {
<div class="dlg-hero-full">
@if (carouselImages.length > 1) {
<div class="dlg-hero-carousel">
<ngx-hm-carousel
[(ngModel)]="carouselIndex"
[data]="carouselImages"
[infinite]="true"
[autoplay-speed]="3000"
[between-delay]="0"
[autoplay]="true"
[mouse-enable]="true"
class="dlg-carousel">
<section ngx-hm-carousel-container class="dlg-carousel-container">
<article class="dlg-carousel-item"
ngx-hm-carousel-item
*ngFor="let imageUrl of carouselImages">
<img [src]="imageUrl" alt="">
</article>
</section>
<ng-template #carouselPrev>
<button mat-icon-button class="dlg-carousel-nav dlg-carousel-prev">
<mat-icon>chevron_left</mat-icon>
</button>
</ng-template>
<ng-template #carouselNext>
<button mat-icon-button class="dlg-carousel-nav dlg-carousel-next">
<mat-icon>chevron_right</mat-icon>
</button>
</ng-template>
<ng-template #carouselDot let-model>
<div class="dlg-carousel-dot"
[class.active]="model.index === model.currentIndex"></div>
</ng-template>
</ngx-hm-carousel>
</div>
} @else {
<div class="dlg-hero-full-image">
@if (getPreviewUrl(); as url) { @if (getPreviewUrl(); as url) {
<img [src]="url" alt=""> <img [src]="url" alt="" class="dlg-preview-img">
} @else { } @else {
<div class="tb-iot-hub-preview-placeholder"> <div class="dlg-preview-placeholder">
<tb-icon>{{ getTypeIcon() }}</tb-icon> <tb-icon>{{ getTypeIcon() }}</tb-icon>
</div> </div>
} }
}
</div>
<div class="dlg-info">
@if (item.description) {
<div class="dlg-description">
<tb-markdown [data]="item.description"></tb-markdown>
</div>
}
<div class="dlg-author" (click)="navigateToCreator()">
<tb-icon>person</tb-icon>
<span>{{ item.creatorDisplayName }}</span>
</div> </div>
} </div>
@if (item.description) {
<div class="dlg-hero-full-desc">{{ item.description }}</div>
}
</div> </div>
}
<!-- Stats bar -->
<div class="dlg-stats-bar">
<span class="dlg-stat">
<tb-icon>download</tb-icon>
<strong>{{ item.totalInstallCount | shortNumber }}</strong> {{ 'iot-hub.installs' | translate }}
</span>
<span class="dlg-stat">
<tb-icon>{{ getTypeIcon() }}</tb-icon>
{{ typeTranslations.get(item.type) | translate }}
</span>
@if (item.peOnly) {
<span class="dlg-pe-label">PE</span>
}
</div> </div>
<!-- Device preview -->
@if (item.type === ItemType.DEVICE) {
<div class="dlg-device-preview">
@if (getPreviewUrl(); as url) {
<img [src]="url" alt="">
} @else {
<div class="tb-iot-hub-preview-placeholder">
<tb-icon>memory</tb-icon>
</div>
}
</div>
}
<!-- CF/RC: description block -->
@if (item.description && item.type !== ItemType.WIDGET && item.type !== ItemType.DASHBOARD && item.type !== ItemType.SOLUTION_TEMPLATE) {
<div class="dlg-desc-block">{{ item.description }}</div>
}
<!-- Node chips (Rule Chain) -->
@if (item.type === ItemType.RULE_CHAIN && getNodes().length) {
<div class="dlg-node-chips">
@for (node of getNodes(); track node.name) {
<span class="dlg-node-chip" [ngClass]="'dlg-node-' + node.type.toLowerCase()" [title]="getNodeLabel(node)">{{ node.name }}</span>
}
@if (getNodeCount() > getNodes().length) {
<span class="dlg-node-more">+{{ getNodeCount() - getNodes().length }} {{ 'iot-hub.more' | translate }}</span>
}
</div>
}
<!-- Categories & Use Cases --> <!-- Categories & Use Cases -->
@if (item.categories?.length || item.useCases?.length) { @if (item.categories?.length || item.useCases?.length) {
<div class="dlg-meta-section"> <div class="dlg-divider"></div>
<div class="dlg-meta">
@if (item.categories?.length) { @if (item.categories?.length) {
<div class="dlg-meta-group"> <div class="dlg-meta-group">
<span class="dlg-meta-label">{{ 'iot-hub.categories' | translate }}</span> <span class="dlg-meta-label">{{ 'iot-hub.category' | translate }}</span>
<div class="dlg-meta-chips"> <div class="dlg-meta-chips">
@for (cat of item.categories; track cat) { @for (cat of item.categories; track cat) {
<span class="dlg-meta-chip dlg-meta-chip-category">{{ getCategoryLabel(cat) }}</span> <span class="dlg-chip dlg-chip-category">{{ getCategoryLabel(cat) }}</span>
} }
</div> </div>
</div> </div>
@ -204,7 +124,7 @@
<span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span> <span class="dlg-meta-label">{{ 'iot-hub.use-cases' | translate }}</span>
<div class="dlg-meta-chips"> <div class="dlg-meta-chips">
@for (uc of item.useCases; track uc) { @for (uc of item.useCases; track uc) {
<span class="dlg-meta-chip dlg-meta-chip-usecase">{{ getUseCaseLabel(uc) }}</span> <span class="dlg-chip dlg-chip-usecase">{{ getUseCaseLabel(uc) }}</span>
} }
</div> </div>
</div> </div>
@ -214,7 +134,8 @@
<!-- Readme / Changelog --> <!-- Readme / Changelog -->
@if (readmeContent || item.changelog) { @if (readmeContent || item.changelog) {
<div class="dlg-content"> <div class="dlg-divider"></div>
<div class="dlg-readme">
@if (readmeContent) { @if (readmeContent) {
<tb-markdown [data]="readmeContent"></tb-markdown> <tb-markdown [data]="readmeContent"></tb-markdown>
} }
@ -228,38 +149,31 @@
<!-- Footer --> <!-- Footer -->
<div class="dlg-footer"> <div class="dlg-footer">
<div class="dlg-footer-actions"> @if (isSameVersion() && item.type === ItemType.SOLUTION_TEMPLATE) {
@if (isSameVersion() && item.type === ItemType.SOLUTION_TEMPLATE) { <button mat-button (click)="openSolutionInstructions()">
<button class="dlg-outlined-btn" (click)="openSolutionInstructions()"> {{ 'iot-hub.solution-instructions' | translate }}
{{ 'iot-hub.solution-instructions' | translate }} </button>
</button> }
} <span class="dlg-footer-spacer"></span>
@if (isSameVersion()) { <button mat-button (click)="close()">{{ 'action.close' | translate }}</button>
@if (item.type === ItemType.SOLUTION_TEMPLATE) { @if (isSameVersion()) {
<button class="dlg-install-btn" (click)="openEntityDetails()"> @if (item.type === ItemType.SOLUTION_TEMPLATE) {
{{ 'iot-hub.goto-main-dashboard' | translate }} <button mat-flat-button color="primary" (click)="openEntityDetails()">
</button> {{ 'iot-hub.goto-main-dashboard' | translate }}
} @else {
<button class="dlg-install-btn" (click)="openEntityDetails()">
{{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</button>
}
} @else if (hasUpdate()) {
<button class="dlg-update-btn" (click)="updateItem()">
{{ 'iot-hub.update' | translate }}
</button> </button>
} @else { } @else {
<button class="dlg-install-btn" (click)="install()"> <button mat-flat-button color="primary" (click)="openEntityDetails()">
{{ 'iot-hub.install-item-type' | translate:{ type: getTypeLabel() } }} {{ 'iot-hub.open-item-type-details' | translate:{ type: getTypeLabel() } }}
</button> </button>
} }
</div> } @else if (hasUpdate()) {
<div class="dlg-creator-info"> <button mat-flat-button color="primary" (click)="updateItem()">
<span>{{ 'iot-hub.created-by' | translate:{ name: item.creatorDisplayName } }}</span> {{ 'iot-hub.update' | translate }}
<span class="dlg-sep">&middot;</span> </button>
<span class="dlg-become-creator" (click)="openSignup()"> } @else {
{{ 'iot-hub.become-a-creator' | translate }} <button mat-flat-button color="primary" (click)="install()">
</span> {{ 'iot-hub.install' | translate }}
</div> </button>
}
</div> </div>
</div> </div>

690
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-detail-dialog.component.scss

@ -14,235 +14,361 @@
* limitations under the License. * limitations under the License.
*/ */
:host-context(.tb-default .tb-dialog .mat-mdc-dialog-component-host) { // Dialog container
@media (min-width: 600px) {
.mat-mdc-dialog-content {
width: 800px;
max-height: 80vh;
}
}
}
.tb-iot-hub-detail-dialog { .tb-iot-hub-detail-dialog {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0 !important; background: white;
--mat-dialog-supporting-text-line-height: 1.5; border-radius: 8px;
overflow: hidden;
max-height: 80vh;
width: 900px;
} }
// Header // Header — Design: pl-24, pr-12, py-16, gap-8
.dlg-header { .dlg-header {
padding: 20px 24px 16px;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
}
.dlg-header-top {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 14px; gap: 8px;
} padding: 16px 12px 16px 24px;
.dlg-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0; flex-shrink: 0;
tb-icon {
font-size: 26px;
width: 26px;
height: 26px;
color: #fff;
}
} }
.dlg-header-info { .dlg-header-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
}
.dlg-title-row {
display: flex; display: flex;
align-items: baseline; flex-direction: column;
gap: 8px; gap: 8px;
flex-wrap: wrap;
} }
// Title — Design: 24px Medium, rgba(0,0,0,0.87)
.dlg-title { .dlg-title {
font-size: 18px; font-size: 24px;
font-weight: 700;
color: #1a1a2e;
}
.dlg-version {
font-size: 12px;
font-weight: 500; font-weight: 500;
color: #9ca3af; line-height: 32px;
} color: rgba(0, 0, 0, 0.87);
.dlg-type-badge {
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
&.wt-timeseries { background: #e3f2fd; color: #1565c0; }
&.wt-latest { background: #e8f5e9; color: #2e7d32; }
&.wt-rpc { background: #f3e5f5; color: #7b1fa2; }
&.wt-alarm { background: #ffebee; color: #c62828; }
&.wt-static { background: #eceff1; color: #455a64; }
&.cf-simple { background: #e3f2fd; color: #1565c0; }
&.cf-script { background: #f3e5f5; color: #6a1b9a; }
&.cf-geofencing { background: #e8f5e9; color: #2e7d32; }
&.cf-alarm { background: #ffebee; color: #c62828; }
&.cf-propagation { background: #e0f2f1; color: #00695c; }
&.cf-related-agg { background: #e8eaf6; color: #283593; }
&.cf-entity-agg { background: #fff3e0; color: #e65100; }
&.rc-core { background: #f3e5f5; color: #7b1fa2; }
&.rc-edge { background: #e0f2f1; color: #00695c; }
} }
// Subtitle row — Design: 13px Regular, rgba(0,0,0,0.54), gap-12 between groups, gap-4 within
.dlg-subtitle { .dlg-subtitle {
font-size: 12px; display: flex;
color: #6b7280; align-items: center;
margin-top: 2px; gap: 4px;
font-size: 13px;
line-height: 16px;
letter-spacing: 0.4px;
color: rgba(0, 0, 0, 0.54);
flex-wrap: wrap;
} }
.dlg-creator-link { .dlg-subtitle-icon {
color: #1976d2; font-size: 16px;
text-decoration: none; width: 16px;
cursor: pointer; height: 16px;
}
&:hover { // Dot separator — Design: 4px circle, rgba(0,0,0,0.54)
text-decoration: underline; .dlg-dot {
} width: 4px;
height: 4px;
border-radius: 2px;
background: rgba(0, 0, 0, 0.54);
flex-shrink: 0;
margin: 0 4px;
} }
.dlg-close { .dlg-close {
flex-shrink: 0; flex-shrink: 0;
margin: -8px -8px 0 0;
} }
.dlg-sep { // Scrollable body
color: #9ca3af; .dlg-body {
padding: 0 2px; overflow-y: auto;
overflow-x: hidden;
flex: 1;
min-height: 0;
}
// Main content — Design: px-24, pb-16
.dlg-main {
padding: 0 24px 16px;
} }
// Hero split (widget: image + description side by side) // Row 1 — Design: flex row, gap-24, items-center
.dlg-hero-split { .dlg-row-1 {
display: flex; display: flex;
gap: 20px; gap: 24px;
padding: 20px 24px; align-items: center;
border-bottom: 1px solid #f3f4f6;
} }
.dlg-hero-image { // Preview image — Design: flex-1, aspect 305:172, bg rgba(0,0,0,0.06), rounded-8, p-16, dot pattern
flex: 0 0 48%; .dlg-preview {
background: #f5f7fa; flex: 1;
min-width: 0;
aspect-ratio: 305 / 172;
background: rgba(0, 0, 0, 0.06);
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.06) 1px, transparent 1px);
background-size: 20px 20px;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 140px; padding: 16px;
}
img { .dlg-preview-img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
padding: 8px; }
}
.tb-iot-hub-preview-placeholder tb-icon { .dlg-preview-placeholder {
font-size: 64px; tb-icon {
width: 64px; font-size: 48px;
height: 64px; width: 48px;
opacity: 0.3; height: 48px;
color: rgba(0, 0, 0, 0.2);
} }
} }
.dlg-hero-desc { // Info panel (right side) — Design: flex-1, flex-col, gap-16
.dlg-info {
flex: 1; flex: 1;
font-size: 13px; min-width: 0;
color: #4b5563; display: flex;
line-height: 1.6; flex-direction: column;
gap: 16px;
p { justify-content: center;
margin: 0;
}
} }
// Dashboard: full-width image + description below // Description — rendered via tb-markdown, same overrides as dlg-readme
.dlg-hero-full { .dlg-description {
padding: 20px 24px; ::ng-deep tb-markdown .tb-markdown-view {
border-bottom: 1px solid #f3f4f6; padding: 0;
> p, > div {
padding: 0;
}
p {
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
}
}
} }
.dlg-hero-full-image { // Author — Design: person icon 20px + name 14px Medium #305680
background: #f5f7fa; .dlg-author {
border-radius: 8px;
overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 4px;
min-height: 200px; cursor: pointer;
max-height: 300px;
img { tb-icon {
width: 100%; font-size: 20px;
height: 100%; width: 20px;
object-fit: contain; height: 20px;
padding: 8px; color: rgba(0, 0, 0, 0.54);
} }
.tb-iot-hub-preview-placeholder tb-icon { span {
font-size: 64px; font-size: 14px;
width: 64px; font-weight: 500;
height: 64px; line-height: 20px;
opacity: 0.3; letter-spacing: 0.25px;
color: #305680;
}
&:hover span {
text-decoration: underline;
} }
} }
.dlg-hero-full-desc { // Divider
margin-top: 12px; .dlg-divider {
font-size: 13px; height: 1px;
color: #4b5563; background: rgba(0, 0, 0, 0.12);
line-height: 1.6; }
// Meta section (categories + use cases) — Design: px-24, py-16, gap-24, flex row
.dlg-meta {
display: flex;
gap: 24px;
padding: 16px 24px;
}
.dlg-meta-group {
display: flex;
flex-direction: column;
gap: 8px;
}
// Meta label — Design: 12px Medium, rgba(0,0,0,0.54), tracking 0.25
.dlg-meta-label {
font-size: 12px;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
}
.dlg-meta-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
// Chip — Design: 12px Medium, h-24, p-4 px-8, rounded-4
.dlg-chip {
display: inline-flex;
align-items: center;
height: 24px;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.25px;
white-space: nowrap;
}
// Category chip — Design: bg rgba(16,108,200,0.06), color #106cc8
.dlg-chip-category {
background: rgba(16, 108, 200, 0.06);
color: #106cc8;
}
// Use case chip — Design: bg rgba(25,128,56,0.06), color #198038
.dlg-chip-usecase {
background: rgba(25, 128, 56, 0.06);
color: #198038;
}
// Readme content — Design: px-24, pt-16, pb-24, gap-20
.dlg-readme {
font-size: 14px;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
// Override markdown-view styles to match Design: px-24, pt-16, pb-24, gap-20
::ng-deep tb-markdown .tb-markdown-view {
padding: 16px 24px 24px;
// Headings: Design:20px SemiBold, line-height 24, tracking 0.1
// Override markdown-view's padding: 30px 32px 10px on direct headings
h1, h2, h3, h4, h5, h6 {
font-size: 20px;
font-weight: 600;
line-height: 24px;
letter-spacing: 0.1px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
padding: 0;
}
// Direct child headings: override markdown-view's > h padding
> h1, > h2, > h3, > h4, > h5, > h6 {
padding: 0;
margin-top: 20px;
}
// First heading: no top margin
> :first-child {
margin-top: 0;
}
h1 {
font-size: 24px;
line-height: 32px;
padding-right: 0;
}
// Paragraphs: Design:14px Regular, tracking 0.2, line-height 24
// Override markdown-view's font-size: 16px and padding
p {
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
}
> p, > div {
padding-left: 0;
padding-right: 0;
}
p + p {
margin-top: 8px;
}
// Heading followed by content: 8px gap
h1 + p, h2 + p, h3 + p,
h1 + ul, h2 + ul, h3 + ul,
h1 + ol, h2 + ol, h3 + ol {
margin-top: 8px;
}
// Lists: Design:ms-21 per li, 14px, line-height 24, no extra margin/padding
// Override markdown-view's padding-left: 62px, margin: 16px
ul, ol {
padding-left: 21px;
padding-right: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
// Override markdown-view's heading-after-list padding
+ h1, + h2, + h3, + h4, + h5, + h6 {
padding-top: 0;
margin-top: 20px;
}
}
// List items: override markdown-view's padding-bottom: .75em
li {
padding-bottom: 0;
margin-bottom: 0;
line-height: 24px;
}
// Code blocks: keep smaller font
code:not([class*=language-]) {
font-size: 14px;
}
}
} }
// Image carousel // Carousel (solution templates)
.dlg-hero-carousel { .dlg-hero-carousel {
background: #f5f7fa; width: 100%;
border-radius: 8px; height: 100%;
overflow: hidden;
position: relative;
height: 340px;
animation: dlg-carousel-fade-in 0s 0.3s forwards; animation: dlg-carousel-fade-in 0s 0.3s forwards;
visibility: hidden; visibility: hidden;
} }
@keyframes dlg-carousel-fade-in { @keyframes dlg-carousel-fade-in {
to { to { visibility: visible; }
visibility: visible;
}
} }
.dlg-carousel { .dlg-carousel {
height: 340px; height: 100%;
::ng-deep .carousel { ::ng-deep .carousel {
display: flex !important; display: flex !important;
flex-direction: column !important; flex-direction: column !important;
> section { > section { flex: 1; min-height: 0; }
flex: 1;
min-height: 0;
}
ul.indicators { ul.indicators {
position: relative !important; position: relative !important;
@ -251,9 +377,7 @@
padding: 8px 0; padding: 8px 0;
} }
.direction { .direction { height: calc(100% - 40px) !important; }
height: calc(100% - 40px) !important;
}
} }
.dlg-carousel-container { .dlg-carousel-container {
@ -266,8 +390,7 @@
img { img {
position: absolute; position: absolute;
top: 0; inset: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
@ -281,9 +404,7 @@
color: rgba(0, 0, 0, 0.6); color: rgba(0, 0, 0, 0.6);
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
&:hover { &:hover { background: rgba(255, 255, 255, 0.95); }
background: rgba(255, 255, 255, 0.95);
}
} }
.dlg-carousel-dot { .dlg-carousel-dot {
@ -294,250 +415,19 @@
margin: 0 3px; margin: 0 3px;
transition: background 0.2s; transition: background 0.2s;
&.active { &.active { background: rgba(0, 0, 0, 0.6); }
background: rgba(0, 0, 0, 0.6);
}
}
// Stats bar
.dlg-stats-bar {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 24px;
background: #f9fafb;
font-size: 12px;
color: #6b7280;
border-bottom: 1px solid #f3f4f6;
}
.dlg-stat {
display: flex;
align-items: center;
gap: 4px;
tb-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
strong {
color: #1a1a2e;
}
}
.dlg-pe-label {
display: inline-block;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
// Device preview
.dlg-device-preview {
display: flex;
align-items: center;
justify-content: center;
min-height: 160px;
background: #f5f7fa;
border-bottom: 1px solid #f3f4f6;
img {
max-width: 100%;
max-height: 160px;
object-fit: contain;
padding: 8px;
}
.tb-iot-hub-preview-placeholder tb-icon {
font-size: 64px;
width: 64px;
height: 64px;
opacity: 0.3;
}
}
// Description block (CF/RC)
.dlg-desc-block {
padding: 16px 24px;
font-size: 13px;
color: #4b5563;
line-height: 1.6;
border-bottom: 1px solid #f3f4f6;
}
// Node chips (Rule Chain)
.dlg-node-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 12px 24px;
border-bottom: 1px solid #f3f4f6;
}
.dlg-node-chip {
padding: 4px 12px;
border-radius: 14px;
font-size: 12px;
font-weight: 500;
border: 1px solid;
&.dlg-node-enrichment { background: #f5fcd5; color: #4a7a00; border-color: #d4e6a0; }
&.dlg-node-filter { background: #fcfadb; color: #7a7200; border-color: #e8e4a0; }
&.dlg-node-transformation { background: #ddf2fc; color: #1a6d9e; border-color: #a8d8f0; }
&.dlg-node-action { background: #fde3e2; color: #9e2a27; border-color: #f0b8b6; }
&.dlg-node-analytics { background: #d5f6f4; color: #0e7a74; border-color: #a0e0dc; }
&.dlg-node-external { background: #fef1d8; color: #8a5d00; border-color: #f0d8a0; }
&.dlg-node-flow { background: #f3edfc; color: #5b3a9e; border-color: #d0c4f0; }
&.dlg-node-unknown { background: #f5f5f5; color: rgba(0, 0, 0, 0.54); border-color: #e0e0e0; }
}
.dlg-node-more {
font-size: 11px;
font-weight: 500;
color: rgba(0, 0, 0, 0.54);
align-self: center;
}
// Categories & Use Cases
.dlg-meta-section {
display: flex;
gap: 40px;
padding: 14px 24px;
border-bottom: 1px solid #e5e7eb;
}
.dlg-meta-group {
display: flex;
flex-direction: column;
gap: 6px;
} }
.dlg-meta-label { // Footer — Design: border-top rgba(0,0,0,0.12), p-8, flex row, justify-end, gap-8
font-size: 10px;
font-weight: 600;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.dlg-meta-chips {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.dlg-meta-chip {
padding: 3px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
border: 1px solid #e5e7eb;
color: #4b5563;
background: #fff;
}
// Scrollable body
.dlg-body {
overflow-y: auto;
flex: 1;
min-height: 0;
}
.dlg-content {
padding: 0;
h3 {
font-size: 16px;
font-weight: 600;
color: #1a1a2e;
margin: 20px 0 8px;
}
}
// Footer
.dlg-footer { .dlg-footer {
padding: 16px 24px;
border-top: 1px solid #e5e7eb;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
padding: 8px;
border-top: 1px solid rgba(0, 0, 0, 0.12);
flex-shrink: 0; flex-shrink: 0;
} }
.dlg-footer-actions { .dlg-footer-spacer {
display: flex; flex: 1;
justify-content: flex-end;
gap: 12px;
width: 100%;
}
.dlg-outlined-btn {
padding: 12px 24px;
border: 1px solid #305680;
border-radius: 8px;
background: transparent;
color: #305680;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
margin-right: auto;
&:hover {
background: #305680;
color: #fff;
}
}
.dlg-install-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
background: #305680;
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
&:hover {
background: #264466;
}
}
.dlg-update-btn {
padding: 12px 24px;
border: 1px solid #e65100;
border-radius: 8px;
background: transparent;
color: #e65100;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
&:hover {
background: #e65100;
color: #fff;
}
}
.dlg-creator-info {
font-size: 12px;
color: #9ca3af;
}
.dlg-become-creator {
color: #305680;
cursor: pointer;
&:hover {
text-decoration: underline;
}
} }

Loading…
Cancel
Save