Browse Source

refactor(iot-hub): unify dot-pattern previews + rename search results container

- Drop the DEVICE-only `tb-iot-hub-card-preview-device` /
  `dlg-preview-device` variants in the item card and item detail
  dialog. The previously-special white-background-with-border treatment
  is gone — device previews now reuse the same dot-pattern background
  as widgets / solution templates for a consistent look.
- In the home-page iot-hub widget, fold the
  `tb-iot-hub-widget-card-image-pattern` modifier into the base
  `.tb-iot-hub-widget-card-image` so every preview card already paints
  the dot pattern (no opt-in class required).
- Rename `.tb-search-categories` to `.tb-results-container` across
  the iot-hub-search component / search page / creator profile pages
  so the wrapper class better reflects what it actually contains
  (browse / search results) and stays consistent between the search
  component and the screens that embed it.
- Pick up incidental browse / search component html / scss tweaks
  touched alongside the rename.
pull/15772/head
Igor Kulikov 4 months ago
parent
commit
e54e94e469
  1. 153
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html
  2. 13
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss
  3. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html
  4. 5
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss
  5. 3
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  6. 5
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.scss
  7. 165
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.html
  8. 25
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss
  9. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/iot-hub-widget.component.html
  10. 9
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/iot-hub-widget.component.scss
  11. 9
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.scss
  12. 1
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss
  13. 8
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.scss

153
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html

@ -405,87 +405,92 @@
</button>
</div>
<!-- Loading -->
@if (isLoading) {
<div class="flex justify-center py-20">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
<div class="relative">
<!-- Error -->
@if (hasError && !isLoading) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.unable-to-load' | translate }}</h3>
<p>{{ 'iot-hub.unable-to-load-text' | translate }}</p>
<button mat-flat-button color="primary" (click)="loadItems()">
{{ 'iot-hub.retry' | translate }}
</button>
</div>
}
<!-- Empty state -->
@if (!isLoading && !hasError && items.length === 0) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.no-items-found-text' | translate }}</p>
@if (hasActiveFilters()) {
<button mat-flat-button color="primary" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all-filters' | translate }}
</button>
<!-- Error -->
@if (hasError) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.unable-to-load' | translate }}</h3>
<p>{{ 'iot-hub.unable-to-load-text' | translate }}</p>
<button mat-flat-button color="primary" (click)="loadItems()">
{{ 'iot-hub.retry' | translate }}
</button>
</div>
}
</div>
}
<!-- Card grid -->
@if (!isLoading && !hasError && items.length > 0) {
<div class="tb-iot-hub-card-grid" [class.tb-iot-hub-card-grid-compact]="isCompactType">
@for (item of items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="!fixedSubType"
[showCreator]="!creatorId"
[mode]="mode"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)"
(addClick)="onItemAdd($event)"
(guideClick)="openInstallGuide($event)">
</tb-iot-hub-item-card>
<!-- Empty state -->
@if (!hasError && items.length === 0) {
<div class="tb-iot-hub-empty-state flex flex-col items-center py-20 text-center">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.no-items-found-text' | translate }}</p>
@if (hasActiveFilters()) {
<button mat-flat-button color="primary" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all-filters' | translate }}
</button>
}
</div>
}
</div>
<!-- Pagination -->
<div class="tb-iot-hub-pagination flex shrink-0 items-center pt-6">
<div class="tb-iot-hub-pagination-pages flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-iot-hub-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
}
<button mat-icon-button [disabled]="pageIndex >= getTotalPages() - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-iot-hub-pagination-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-iot-hub-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
<!-- Card grid -->
@if (!hasError && items.length > 0) {
<div class="tb-iot-hub-card-grid" [class.tb-iot-hub-card-grid-compact]="isCompactType">
@for (item of items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="!fixedSubType"
[showCreator]="!creatorId"
[mode]="mode"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)"
(addClick)="onItemAdd($event)"
(guideClick)="openInstallGuide($event)">
</tb-iot-hub-item-card>
}
</mat-select>
</div>
</div>
<!-- Pagination -->
<div class="tb-iot-hub-pagination flex shrink-0 items-center pt-6">
<div class="tb-iot-hub-pagination-pages flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-iot-hub-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
}
<button mat-icon-button [disabled]="pageIndex >= getTotalPages() - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-iot-hub-pagination-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-iot-hub-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
}
</mat-select>
</div>
</div>
}
<!-- Loading -->
@if (isLoading) {
<div class="absolute inset-0 backdrop-grayscale" style="background: rgba(255, 255, 255, 0.72)">
<div class="flex justify-center py-20">
<mat-spinner diameter="64"></mat-spinner>
</div>
</div>
}
</div>
}
</div>
</mat-drawer-content>
</mat-drawer-container>

13
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss

@ -112,7 +112,8 @@
}
.tb-iot-hub-filters-wrapper {
position: absolute;
position: sticky;
max-height: calc(100vh - var(--mat-toolbar-standard-height, 64px));
inset: 0;
overflow-y: auto;
}
@ -146,10 +147,12 @@
width: 60%;
max-width: 550px;
min-width: 300px;
margin-bottom: -48px;
.embedded & {
margin-bottom: 0;
&:not(.embedded &) {
position: fixed;
top: var(--mat-toolbar-standard-height, 64px);
@media #{$mat-xs} {
top: var(--mat-toolbar-mobile-height, 56px);
}
}
}

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html

@ -80,7 +80,7 @@
} @else {
<!-- Big card (Widget / Dashboard / Solution Template) -->
<div class="tb-iot-hub-item-card" role="button" tabindex="0" (click)="onCardClick()" (keyup.enter)="onCardClick()">
<div class="tb-iot-hub-card-preview" [class.tb-iot-hub-card-preview-device]="item.type === ItemType.DEVICE">
<div class="tb-iot-hub-card-preview">
@if (getPreviewUrl(); as previewUrl) {
<img [src]="previewUrl" alt="" class="tb-iot-hub-preview-image">
} @else {

5
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss

@ -52,11 +52,6 @@
position: relative;
overflow: hidden;
padding: 12px;
&.tb-iot-hub-card-preview-device {
background: white;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
}
.tb-iot-hub-preview-image {

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

@ -94,8 +94,7 @@
<!-- Widget/Dashboard/Solution: image + description side by side -->
<div class="flex gap-6">
<div class="dlg-preview"
[class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1"
[class.dlg-preview-device]="item.type === ItemType.DEVICE">
[class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1">
@if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {
<div class="dlg-hero-carousel">
<ngx-hm-carousel

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

@ -143,11 +143,6 @@
&.dlg-preview-carousel {
aspect-ratio: 1.5;
}
&.dlg-preview-device {
background: white;
border: 1px solid rgba(0, 0, 0, 0.12);
}
}
.dlg-preview-img {

165
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.html

@ -15,13 +15,11 @@
limitations under the License.
-->
<div class="flex items-baseline gap-4 lt-md:-mb-6">
<div class="mb-10 flex items-baseline gap-4 lt-md:mb-4">
<ng-content></ng-content>
@if (!isLoading && totalElements > 0) {
<span class="tb-search-results-count tb-search-results-count-mobile">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
}
<span class="tb-search-results-count tb-search-results-count-mobile">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
</div>
<!-- Search + Sort row -->
<div class="flex items-center gap-4">
@ -56,92 +54,93 @@
</div>
<!-- Results count + Pagination (shown when results exist) -->
@if (!isLoading && totalElements > 0) {
<div class="tb-search-pagination-row">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
<div class="tb-search-pagination-row" [class.lt-md:!hidden]="totalElements === 0">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
@if (totalElements > 0) {
<div class="tb-search-pagination">
<div class="flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
<div class="flex items-center gap-px">
<button mat-icon-button [disabled]="pageIndex === 0" (click)="goToPage(pageIndex - 1)">
<mat-icon>chevron_left</mat-icon>
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-search-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
@for (p of getPageNumbers(); track p) {
<button class="tb-search-page-btn" [class.active]="p === pageIndex" (click)="goToPage(p)">
{{ p + 1 }}
</button>
}
<button mat-icon-button [disabled]="pageIndex >= totalPages - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-search-page-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-search-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
}
<button mat-icon-button [disabled]="pageIndex >= totalPages - 1" (click)="goToPage(pageIndex + 1)">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<div class="tb-search-page-size">
<span>{{ 'iot-hub.items-per-page' | translate }}:</span>
<mat-select [value]="pageSize" (selectionChange)="onPageSizeChange($event.value)" class="tb-search-page-size-select">
@for (size of pageSizeOptions; track size) {
<mat-option [value]="size">{{ size }}</mat-option>
}
</mat-select>
</div>
</mat-select>
</div>
</div>
}
}
</div>
<!-- No results count when 0 -->
@if (!isLoading && totalElements === 0 && searchText?.trim()) {
<div class="tb-search-pagination-row">
<span class="tb-search-results-count">
{{ 'iot-hub.results-count' | translate:{ count: 0 } }}
</span>
</div>
}
<div class="tb-search-divider"></div>
<!-- Loading -->
@if (isLoading) {
<div class="tb-search-loading flex justify-center py-12">
<mat-spinner diameter="40"></mat-spinner>
</div>
}
<div class="relative min-h-0">
<!-- No results -->
@if (!isLoading && totalElements === 0 && searchText?.trim()) {
<div class="tb-search-empty flex flex-col items-center py-10">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.try-adjusting-search' | translate }}</p>
</div>
}
<div class="tb-results-container">
<!-- No results -->
@if (totalElements === 0 && searchText?.trim()) {
<div class="tb-search-empty flex flex-col items-center py-10">
<div class="tb-no-data-bg"></div>
<h3>{{ 'iot-hub.no-items-found' | translate }}</h3>
<p>{{ 'iot-hub.try-adjusting-search' | translate }}</p>
</div>
}
<!-- Results grouped by type -->
@if (!isLoading && resultGroups.length > 0) {
<div class="tb-search-divider"></div>
<div class="tb-search-categories">
@for (group of resultGroups; track group.type) {
<div class="tb-search-section">
<!-- Section header -->
<button mat-button class="tb-search-section-header" (click)="navigateToType(group.type)">
{{ getTypeLabel(group.type) }}
<mat-icon>chevron_right</mat-icon>
</button>
<!-- Card grid -->
<div class="tb-search-card-grid" [class.tb-search-card-grid-compact]="isCompactType(group.type)">
@for (item of group.items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="false"
[showCreator]="showCreator"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
<!-- Results grouped by type -->
@if (resultGroups.length > 0) {
<div class="tb-search-categories">
@for (group of resultGroups; track group.type) {
<div class="tb-search-section">
<!-- Section header -->
<button mat-button class="tb-search-section-header" (click)="navigateToType(group.type)">
{{ getTypeLabel(group.type) }}
<mat-icon>chevron_right</mat-icon>
</button>
<!-- Card grid -->
<div class="tb-search-card-grid" [class.tb-search-card-grid-compact]="isCompactType(group.type)">
@for (item of group.items; track item.id) {
<tb-iot-hub-item-card
[item]="item"
[installedItem]="getInstalledItem(item)"
[installedItemsCount]="getInstalledItemsCount(item)"
[showTypeChip]="false"
[showSubtype]="false"
[showCreator]="showCreator"
(cardClick)="openItemDetail($event)"
(creatorClick)="navigateToCreator($event)"
(installClick)="installItem($event)"
(updateClick)="updateItem($event)"
(deleteClick)="deleteInstalledItem($event)">
</tb-iot-hub-item-card>
}
</div>
</div>
}
</div>
}
</div>
}
<!-- Loading -->
@if (isLoading) {
<div class="absolute inset-0 z-10 backdrop-grayscale" style="background: rgba(255, 255, 255, 0.72)">
<div class="flex justify-center py-32">
<mat-spinner diameter="64"></mat-spinner>
</div>
</div>
}
</div>

25
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-search.component.scss

@ -20,7 +20,6 @@
:host {
display: flex;
flex-direction: column;
gap: 40px;
}
.tb-search-results-count-mobile {
@ -54,10 +53,11 @@
// Results count + Pagination row
.tb-search-pagination-row {
flex: 0 0 48px;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: -28px;
margin-top: 12px;
}
.tb-search-results-count {
@ -121,12 +121,6 @@
}
}
// Loading / Empty negative margin
.tb-search-loading,
.tb-search-empty {
margin-top: -20px;
}
// Empty state no-data-bg + text
.tb-search-empty {
.tb-no-data-bg {
@ -157,8 +151,15 @@
.tb-search-divider {
background: rgba(0, 0, 0, 0.12);
height: 1px;
margin-bottom: -20px;
margin-top: -20px;
min-height: 1px;
margin-top: 20px;
}
.tb-results-container {
position: relative;
height: 100%;
padding-top: 20px;
overflow: auto;
}
// Categories / sections
@ -166,9 +167,6 @@
display: flex;
flex-direction: column;
gap: 48px;
margin-top: -20px;
overflow: auto;
padding-top: 20px;
}
// Section header
@ -234,6 +232,7 @@
.tb-search-pagination-row {
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 12px;
}

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/iot-hub-widget.component.html

@ -29,7 +29,7 @@
<div class="tb-iot-hub-widget-card" role="button" tabindex="0"
(click)="openItemDetail(item)"
(keyup.enter)="openItemDetail(item)">
<div class="tb-iot-hub-widget-card-image tb-iot-hub-widget-card-image-pattern">
<div class="tb-iot-hub-widget-card-image">
@if (getItemImage(item); as imgUrl) {
<img [src]="imgUrl" alt="">
}

9
ui-ngx/src/app/modules/home/components/widget/lib/home-page/iot-hub-widget.component.scss

@ -131,7 +131,7 @@
padding: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 10px;
background: white;
background: radial-gradient(circle, rgba(0, 0, 0, 0.06) 1px, transparent 1px) 0 0 / 16px 16px rgba(0, 0, 0, 0.04);
overflow: hidden;
transition: border-color 0.15s, box-shadow 0.15s;
@media #{$mat-md-lg} {
@ -144,13 +144,6 @@
}
}
// Dot pattern used for solution template preview cards
.tb-iot-hub-widget-card-image-pattern {
background:
radial-gradient(circle, rgba(0, 0, 0, 0.06) 1px, transparent 1px) 0 0 / 15px 15px,
rgba(0, 0, 0, 0.04);
}
// Installed badge — green circle + check icon, top-right corner
.tb-iot-hub-widget-card-installed {
position: absolute;

9
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.scss

@ -163,7 +163,7 @@ a.tb-creator-link:hover span {
margin-right: -123px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 123px;
padding-bottom: 40px;
margin-right: -123px;
@ -203,7 +203,7 @@ a.tb-creator-link:hover span {
margin-right: -40px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 40px;
margin-right: -40px;
}
@ -251,6 +251,7 @@ a.tb-creator-link:hover span {
@media #{$mat-lt-lg} {
:host {
height: auto;
flex: 1;
}
.tb-creator-page {
@ -273,7 +274,7 @@ a.tb-creator-link:hover span {
margin-right: -24px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 24px;
margin-right: -24px;
}
@ -311,7 +312,7 @@ a.tb-creator-link:hover span {
margin-right: -16px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 16px;
margin-right: -16px;
}

1
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-items-page.component.scss

@ -25,7 +25,6 @@
background: white;
border-radius: 8px;
padding: 40px 40px 48px;
overflow: hidden;
}
.tb-iot-hub-back-btn {

8
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-search-page.component.scss

@ -56,7 +56,7 @@ tb-iot-hub-search {
margin-right: -123px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 123px;
padding-bottom: 40px;
margin-right: -123px;
@ -80,7 +80,7 @@ tb-iot-hub-search {
margin-right: -40px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 40px;
margin-right: -40px;
}
@ -104,7 +104,7 @@ tb-iot-hub-search {
margin-right: -24px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 24px;
padding-bottom: 24px;
margin-right: -24px;
@ -133,7 +133,7 @@ tb-iot-hub-search {
margin-right: -16px;
}
.tb-search-categories {
.tb-results-container {
padding-right: 16px;
padding-bottom: 16px;
margin-right: -16px;

Loading…
Cancel
Save