+ @for (card of categoryCards; track card.type) {
+
- @if (!isLoading) {
-
- @if (popularWidgets.length) {
-
+ {{ card.titleKey | translate }}
+
+
+ }
+
-
-
- @for (item of popularWidgets; track item.id) {
-
-
- }
+ @if (!isLoading) {
+
+ @if (popularWidgets.length) {
+
- }
+ }
-
- @if (popularDashboards.length) {
-
+
+
-
+ @for (item of popularWidgets; track item.id) {
+
+
+ }
+
-
-
- @for (item of popularDashboards; track item.id) {
-
-
- }
+
+ @if (popularDashboards.length) {
+
- }
+ }
-
- @if (popularSolutionTemplates.length) {
-
+
+
-
+ @for (item of popularDashboards; track item.id) {
+
+
+ }
+
-
-
- @for (item of popularSolutionTemplates; track item.id) {
-
-
- }
+
+ @if (popularSolutionTemplates.length) {
+
- }
+ }
-
- @if (popularCalcFields.length) {
-
+
+
-
+ @for (item of popularSolutionTemplates; track item.id) {
+
+
+ }
+
-
-
- @for (item of popularCalcFields; track item.id) {
-
-
- }
+
+ @if (popularCalcFields.length) {
+
- }
+ }
-
- @if (popularRuleChains.length) {
-
+
+
-
+ @for (item of popularCalcFields; track item.id) {
+
+
+ }
+
-
-
}
-
- @if (isLoading) {
-
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss
index b81f104aee..7251f41187 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.scss
@@ -102,6 +102,7 @@
}
// Individual floating icon — SVG has rotation/bg/shadow baked in, CSS handles position + animation
+// Base: ≥1600px — 78px top icons (top: 40px), 87px bottom icons (top: 216px, bottom edge: 303px)
.tb-iot-hub-hero-float-icon {
position: absolute;
width: 78px;
@@ -116,35 +117,37 @@
// Top-left
&.icon-pos-1 {
top: 40px;
- left: 23%;
- --icon-rotation: ;
+ left: calc(50% - 404px);
--start-x: 40px;
--start-y: 12px;
}
- // Top-right
+ // Top-right — mirrors icon-pos-1
&.icon-pos-2 {
top: 40px;
- right: 23%;
- --icon-rotation: ;
+ left: auto;
+ right: calc(50% - 404px);
--start-x: -40px;
--start-y: 12px;
}
- // Bottom-left
+ // Bottom-left — 87px bounding box
&.icon-pos-3 {
- bottom: 100px;
- left: 18%;
- --icon-rotation: ;
+ top: 216px;
+ left: calc(50% - 487px);
+ width: 87px;
+ height: 87px;
--start-x: 50px;
--start-y: -8px;
}
- // Bottom-right
+ // Bottom-right — 87px bounding box, mirrors icon-pos-3
&.icon-pos-4 {
- bottom: 100px;
- right: 18%;
- --icon-rotation: ;
+ top: 216px;
+ left: auto;
+ right: calc(50% - 487px);
+ width: 87px;
+ height: 87px;
--start-x: -45px;
--start-y: -4px;
}
@@ -178,6 +181,11 @@
gap: 6px;
}
+.tb-iot-hub-hero-subtitle-break {
+ flex-basis: 100%;
+ height: 0;
+}
+
.tb-iot-hub-hero-prefix {
font-weight: 400;
color: rgba(0, 0, 0, 0.76);
@@ -425,19 +433,26 @@
}
}
-// Category cards — Design: centered 1200px container, flex-col gap=20, pb=48, 3-col rows
+// Main content — wraps categories + item sections
+.tb-iot-hub-main-content {
+ display: flex;
+ flex-direction: column;
+ gap: 48px;
+ padding: 0 40px 64px;
+}
+
+// Category cards — Design: centered 1200px container, 3-col rows
.tb-iot-hub-categories {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 1200px;
margin: 0 auto;
- padding-bottom: 48px;
+ width: 100%;
}
// Card — Design: flex-col, gap=24, pt=24, rounded-8, border, overflow-clip, h=220
.tb-iot-hub-category-card {
- height: 220px;
border-radius: 8px;
cursor: pointer;
display: flex;
@@ -470,8 +485,7 @@
// Card image — Design: h=148, w=full, overflow-clip, fills remaining space
.tb-iot-hub-category-img {
width: 100%;
- height: 148px;
- object-fit: cover;
+ object-fit: contain;
flex-shrink: 0;
}
@@ -500,10 +514,8 @@
background: linear-gradient(104.75deg, rgb(245, 255, 252) 0%, rgb(147, 240, 213) 100%);
}
-// Sections — matches Design "items-section" (px=40, gap between sections ~48px)
+// Sections — inside main-content, no individual padding needed
.tb-iot-hub-section {
- padding: 0 40px;
- margin-top: 48px;
}
// Section header — mat-button, Design: height=40, font-size=20, font-weight=500
@@ -541,7 +553,6 @@
.tb-iot-hub-divider {
height: 1px;
background: rgba(0, 0, 0, 0.12);
- margin: 48px 0 0;
}
// Become a Creator — matches Design (pt=64, text centered, button at y=156)
@@ -606,23 +617,41 @@
animation: iot-hub-fade-slide-up 1s cubic-bezier(0.19, 1, 0.22, 1) 0.2s both;
}
-// Responsive — breakpoints from design: LG=1280, MD=960, SM=600, XS=375
+// Responsive — breakpoints from design: XXXL=2448, XXL=1920, XL=1600, LG=1280, MD=960, SM=600, XS=375
-// LG: 1280px content area (sidebar present)
-@media (max-width: 1600px) {
+// ≥2448px: 6-col big cards, 4-col compact
+@media #{$mat-gt-xxl} {
.tb-iot-hub-big-cards-row {
+ grid-template-columns: repeat(6, 1fr);
+ }
+
+ .tb-iot-hub-compact-cards-grid {
grid-template-columns: repeat(4, 1fr);
}
+}
+
+// ≤1919px: 4-col big cards, 3-col compact
+@media #{$mat-lt-xl} {
+ .tb-iot-hub-big-cards-row {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
+// ≤1599px: 3-col big cards, 2-col compact
+@media #{$mat-lt-xmd} {
+ .tb-iot-hub-big-cards-row {
+ grid-template-columns: repeat(3, 1fr);
+ }
.tb-iot-hub-compact-cards-grid {
grid-template-columns: repeat(2, 1fr);
}
}
-// MD: 960px viewport — sidebar present, narrower content
-@media (max-width: 1280px) {
+// ≤1279px: 2-col big cards, 1-col compact, 2-col categories, smaller icons
+@media #{$mat-lt-lg} {
.tb-iot-hub-big-cards-row {
- grid-template-columns: repeat(3, 1fr);
+ grid-template-columns: repeat(2, 1fr);
}
.tb-iot-hub-compact-cards-grid {
@@ -631,14 +660,10 @@
.tb-iot-hub-categories {
grid-template-columns: repeat(2, 1fr);
-
- .tb-iot-hub-category-card {
- height: 189px;
- }
}
.tb-iot-hub-hero {
- padding: 100px 24px 60px;
+ padding: 140px 24px 60px;
}
.tb-iot-hub-hero-title {
@@ -650,75 +675,102 @@
line-height: 22px;
}
- .tb-iot-hub-section {
- padding: 0 24px;
+ .tb-iot-hub-main-content {
+ padding: 0 24px 64px;
}
.tb-iot-hub-become-creator {
padding: 64px 24px;
}
-}
-// SM: 600px viewport — sidebar hidden, full width
-@media (max-width: 960px) {
- .tb-iot-hub-big-cards-row {
- grid-template-columns: repeat(2, 1fr);
- }
+ // Icons: ≥1280px — 69px top (top: 90px), 77px bottom (top: 246px, bottom edge: 323px)
+ .tb-iot-hub-hero-float-icon {
+ width: 69px;
+ height: 69px;
- .tb-iot-hub-categories {
- .tb-iot-hub-category-card {
- height: 168px;
+ &.icon-pos-1 {
+ top: 90px;
+ left: 106px;
+ }
+
+ &.icon-pos-2 {
+ top: 90px;
+ left: auto;
+ right: 106px;
+ }
+
+ &.icon-pos-3 {
+ top: 246px;
+ left: 28px;
+ width: 77px;
+ height: 77px;
+ }
+
+ &.icon-pos-4 {
+ top: 246px;
+ left: auto;
+ right: 28px;
+ width: 77px;
+ height: 77px;
}
}
+}
+// ≤959px: only top pair, area width ≤160px (icon 51px + gap + icon 51px)
+@media #{$mat-lt-md} {
.tb-iot-hub-hero-float-icon {
width: 51px;
height: 51px;
- img {
- width: 51px;
- height: 51px;
+ &.icon-pos-1 {
+ top: 80px;
+ left: calc(50% - 80px);
+ }
+
+ &.icon-pos-2 {
+ top: 80px;
+ left: auto;
+ right: calc(50% - 80px);
+ }
+
+ &.icon-pos-3,
+ &.icon-pos-4 {
+ display: none;
}
}
}
-// XS: 375px viewport — single column, stacked layout
-@media (max-width: 600px) {
+// ≤599px: only top pair visible (51px), bottom pair hidden
+@media #{$mat-lt-sm} {
:host {
padding: 8px;
}
.tb-iot-hub-hero {
- padding: 80px 16px 40px;
+ padding: 140px 16px 40px;
}
.tb-iot-hub-categories {
grid-template-columns: 1fr;
- padding-bottom: 32px;
-
- .tb-iot-hub-category-card {
- height: 197px;
- }
}
.tb-iot-hub-big-cards-row {
- grid-template-columns: repeat(2, 1fr);
+ grid-template-columns: 1fr;
}
- .tb-iot-hub-section {
- padding: 0 16px;
+ .tb-iot-hub-main-content {
+ padding: 0 16px 48px;
}
.tb-iot-hub-become-creator {
padding: 48px 16px;
}
- .tb-iot-hub-hero-float-icon {
- display: none;
- }
-
.tb-iot-hub-installed-btn {
top: 12px;
- right: 12px;
+ right: auto;
+ left: 50%;
+ transform: translateX(-50%);
}
+
}
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
index 6cb6777a49..00f49903b4 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-home.component.ts
@@ -19,8 +19,10 @@ import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
+import { BreakpointObserver } from '@angular/cdk/layout';
import { forkJoin, Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
+import { MediaBreakpoints } from '@shared/models/constants';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MpItemVersionQuery, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
@@ -134,15 +136,35 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
isLoading = true;
+ bigCardCount = 5;
+ compactCardCount = 6;
+ private breakpointSubscription: Subscription;
+
constructor(
private router: Router,
private dialog: MatDialog,
private iotHubApiService: IotHubApiService,
- private translate: TranslateService
+ private translate: TranslateService,
+ private breakpointObserver: BreakpointObserver
) {}
ngOnInit(): void {
+ this.updateCardCounts();
this.loadPopularItems();
+ this.breakpointSubscription = this.breakpointObserver.observe([
+ MediaBreakpoints['lt-sm'],
+ MediaBreakpoints['lt-md'],
+ MediaBreakpoints['lt-lg'],
+ MediaBreakpoints['lt-xmd'],
+ MediaBreakpoints['lt-xl'],
+ MediaBreakpoints['gt-xxl']
+ ]).subscribe(() => {
+ const prev = { big: this.bigCardCount, compact: this.compactCardCount };
+ this.updateCardCounts();
+ if (this.bigCardCount !== prev.big || this.compactCardCount !== prev.compact) {
+ this.loadPopularItems();
+ }
+ });
this.searchSubscription = this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
@@ -169,6 +191,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.stopHeroCycle();
this.searchSubscription?.unsubscribe();
+ this.breakpointSubscription?.unsubscribe();
}
onHeroTypeHover(config: HeroTypeConfig): void {
@@ -236,7 +259,7 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
}
isCompactType(type: ItemType): boolean {
- return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
+ return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN || type === ItemType.DEVICE;
}
getCompactIcon(item: MpItemVersionView): string {
@@ -438,6 +461,38 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
window.open('https://iothub.thingsboard.io/signup', '_blank');
}
+ private updateCardCounts(): void {
+ if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-sm'])) {
+ // ≤599px: 1-col big cards, 1-col compact
+ this.bigCardCount = 2;
+ this.compactCardCount = 4;
+ } else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-md'])) {
+ // ≤959px: 2-col big cards, 1-col compact
+ this.bigCardCount = 4;
+ this.compactCardCount = 4;
+ } else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-lg'])) {
+ // ≤1279px: 2-col big cards, 1-col compact
+ this.bigCardCount = 4;
+ this.compactCardCount = 4;
+ } else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-xmd'])) {
+ // ≤1599px: 3-col big cards, 2-col compact
+ this.bigCardCount = 3;
+ this.compactCardCount = 4;
+ } else if (this.breakpointObserver.isMatched(MediaBreakpoints['lt-xl'])) {
+ // ≤1919px: 4-col big cards, 3-col compact
+ this.bigCardCount = 4;
+ this.compactCardCount = 6;
+ } else if (this.breakpointObserver.isMatched(MediaBreakpoints['gt-xxl'])) {
+ // ≥2448px: 6-col big cards, 4-col compact
+ this.bigCardCount = 6;
+ this.compactCardCount = 8;
+ } else {
+ // ≥1920px: 5-col big cards, 3-col compact
+ this.bigCardCount = 5;
+ this.compactCardCount = 6;
+ }
+ }
+
private loadPopularItems(): void {
const sortOrder: SortOrder = { property: 'totalInstallCount', direction: Direction.DESC };
const config = { ignoreLoading: true };
@@ -449,11 +504,11 @@ export class TbIotHubHomeComponent implements OnInit, OnDestroy {
};
forkJoin({
- widgets: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.WIDGET, 5), config),
- dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, 5), config),
- solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, 5), config),
- calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, 6), config),
- ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, 6), config),
+ widgets: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.WIDGET, this.bigCardCount), config),
+ dashboards: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.DASHBOARD, this.bigCardCount), config),
+ solutionTemplates: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.SOLUTION_TEMPLATE, this.bigCardCount), config),
+ calcFields: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.CALCULATED_FIELD, this.compactCardCount), config),
+ ruleChains: this.iotHubApiService.getPublishedVersions(buildQuery(ItemType.RULE_CHAIN, this.compactCardCount), config),
installedWidgets: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.WIDGET, config),
installedSolutionTemplates: this.iotHubApiService.getInstalledItems(installedPageLink, ItemType.SOLUTION_TEMPLATE, config),
installedCount: this.iotHubApiService.getInstalledItemsCount(null, config)
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
index 3e7d038e44..13bbcec662 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.scss
@@ -19,6 +19,7 @@
:host {
display: block;
height: 100%;
+ min-width: 0;
}
// Card — Design: white bg, border rgba(0,0,0,0.12), rounded-8, overflow-clip, flex-col, gap-16, pb-16
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss
index 4dbed2491e..24e5ab5fb7 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.scss
@@ -214,7 +214,7 @@
}
}
-// Card grid — Design: 5-column for big cards, 3-column for compact
+// Card grid — base: ≥1920 5-col big, 3-col compact
.tb-search-card-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
@@ -225,18 +225,29 @@
}
}
-// Responsive
-@media (max-width: 1400px) {
+// Responsive — same columns as iot-hub home
+
+@media #{$mat-gt-xxl} {
+ .tb-search-card-grid {
+ grid-template-columns: repeat(6, 1fr);
+
+ &.tb-search-card-grid-compact {
+ grid-template-columns: repeat(4, 1fr);
+ }
+ }
+}
+
+@media #{$mat-lt-xl} {
.tb-search-card-grid {
grid-template-columns: repeat(4, 1fr);
&.tb-search-card-grid-compact {
- grid-template-columns: repeat(2, 1fr);
+ grid-template-columns: repeat(3, 1fr);
}
}
}
-@media (max-width: 1100px) {
+@media #{$mat-lt-xmd} {
.tb-search-card-grid {
grid-template-columns: repeat(3, 1fr);
@@ -246,7 +257,7 @@
}
}
-@media (max-width: 900px) {
+@media #{$mat-lt-lg} {
.tb-search-card-grid {
grid-template-columns: repeat(2, 1fr);
@@ -254,7 +265,9 @@
grid-template-columns: repeat(1, 1fr);
}
}
+}
+@media #{$mat-lt-md} {
.tb-search-input-field {
width: 100%;
flex: 1;
@@ -264,3 +277,9 @@
padding: 24px;
}
}
+
+@media #{$mat-lt-sm} {
+ .tb-search-card-grid {
+ grid-template-columns: repeat(1, 1fr);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts
index 039c1f25c8..b1b3252b7b 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search.component.ts
@@ -173,7 +173,7 @@ export class TbIotHubSearchComponent implements OnInit, OnDestroy {
// Type helpers
isCompactType(type: ItemType): boolean {
- return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN;
+ return type === ItemType.CALCULATED_FIELD || type === ItemType.RULE_CHAIN || type === ItemType.DEVICE;
}
getTypeLabel(type: ItemType): string {
diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts
index 429bfe217a..1117f032ed 100644
--- a/ui-ngx/src/app/shared/models/constants.ts
+++ b/ui-ngx/src/app/shared/models/constants.ts
@@ -69,11 +69,14 @@ export const MediaBreakpoints = {
'lt-sm': 'screen and (max-width: 599px)',
'lt-md': 'screen and (max-width: 959px)',
'lt-lg': 'screen and (max-width: 1279px)',
+ 'lt-xmd': 'screen and (max-width: 1599px)',
'lt-xl': 'screen and (max-width: 1919px)',
'gt-xs': 'screen and (min-width: 600px)',
'gt-sm': 'screen and (min-width: 960px)',
'gt-md': 'screen and (min-width: 1280px)',
+ 'gt-xmd': 'screen and (min-width: 1600px)',
'gt-lg': 'screen and (min-width: 1920px)',
+ 'gt-xxl': 'screen and (min-width: 2448px)',
'gt-xl': 'screen and (min-width: 5001px)',
'md-lg': 'screen and (min-width: 960px) and (max-width: 1819px)'
};
diff --git a/ui-ngx/src/scss/constants.scss b/ui-ngx/src/scss/constants.scss
index aca02be591..83c7114f01 100644
--- a/ui-ngx/src/scss/constants.scss
+++ b/ui-ngx/src/scss/constants.scss
@@ -25,7 +25,9 @@ $mat-lt-xl: "screen and (max-width: 1919px)";
$mat-gt-xs: "screen and (min-width: 600px)";
$mat-gt-sm: "screen and (min-width: 960px)";
$mat-gt-md: "screen and (min-width: 1280px)";
+$mat-lt-xmd: "screen and (max-width: 1599px)";
$mat-gt-xmd: "screen and (min-width: 1600px)";
+$mat-gt-xxl: "screen and (min-width: 2448px)";
$mat-gt-xl: "screen and (min-width: 1920px)";
$mat-md-lg: "screen and (min-width: 960px) and (max-width: 1819px)";
- @for (item of popularRuleChains; track item.id) {
-
-
- }
+
+ @if (popularRuleChains.length) {
+
-
+
+ @if (!isLoading) {
+
+
+ }
+ }
+
+ @if (isLoading) {
+
+ @for (item of popularRuleChains; track item.id) {
+
+
+ }
+
+
}
+ {{ 'iot-hub.become-a-creator' | translate }}
@@ -268,10 +282,4 @@
-
-
- }