+
+
+
+ @if (getSubtypeMap(); as subtypeMap) {
+
+
+ {{ 'iot-hub.type-filter-label' | translate }}
+
+ @for (entry of subtypeMap | keyvalue; track entry.key) {
+
+ {{ entry.value | translate }}
+
+ }
+
}
-
-
-
-
-
- @if (getSubtypeMap(); as subtypeMap) {
-
-
- @for (entry of subtypeMap | keyvalue; track entry.key) {
- {{ entry.value | translate }}
- }
-
-
- }
- @if (categories.size > 0) {
-
-
- @for (entry of categories | keyvalue; track entry.key) {
- {{ entry.value | translate }}
- }
-
-
- }
-
-
- @for (entry of useCases | keyvalue; track entry.key) {
- {{ entry.value | translate }}
+ @if (categories.size > 0) {
+
+
+ {{ 'iot-hub.category' | translate }}
+
+ @for (entry of categories | keyvalue; track entry.key) {
+
+ {{ entry.value | translate }}
+
+ }
+
}
-
-
-
-
-
- @if (hasActiveDropdownFilters()) {
-
- @for (key of getActiveSubtypesArray(); track key) {
-
- {{ getSubtypeLabel(key) }}
- close
-
- }
- @for (key of getActiveCategoriesArray(); track key) {
-
- {{ getCategoryLabel(key) }}
- close
-
- }
- @for (key of getActiveUseCasesArray(); track key) {
-
- {{ getUseCaseLabel(key) }}
- close
-
- }
-
-
- }
-
-
- @if (!isLoading && !hasError) {
-
- {{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
-
- }
-
- @if (isLoading) {
-
-
+
+
+ {{ 'iot-hub.use-case' | translate }}
+
+ @for (entry of useCases | keyvalue; track entry.key) {
+
+ {{ entry.value | translate }}
+
+ }
+
+
- }
-
- @if (hasError && !isLoading) {
-
-
error_outline
-
{{ 'iot-hub.unable-to-load' | translate }}
-
{{ 'iot-hub.unable-to-load-text' | translate }}
-
-
- }
+
+
+
+
+
+ {{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
+
+
+ search
+
+ @if (textSearch) {
+
+ }
+
+
-
- @if (!isLoading && !hasError && items.length === 0) {
-
-
search_off
-
{{ 'iot-hub.no-items-found' | translate }}
-
{{ 'iot-hub.no-items-found-text' | translate }}
- @if (hasActiveFilters()) {
-
- }
-
- @if (!isLoading && !hasError && items.length > 0) {
-
- @for (item of items; track item.id) {
-
-
+
+ @if (hasError && !isLoading) {
+
+
error_outline
+
{{ 'iot-hub.unable-to-load' | translate }}
+
{{ 'iot-hub.unable-to-load-text' | translate }}
+
+ {{ 'iot-hub.retry' | translate }}
+
+
}
-
-
-
-
- }
+
+ @if (!isLoading && !hasError && items.length === 0) {
+
+
search_off
+
{{ 'iot-hub.no-items-found' | translate }}
+
{{ 'iot-hub.no-items-found-text' | translate }}
+ @if (hasActiveFilters()) {
+
+ {{ 'iot-hub.clear-all-filters' | translate }}
+
+ }
+
+ }
-
- @if (!creatorId) {
-
- }
+
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss
index deef2d98bc..d228a5f2e6 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss
@@ -18,249 +18,147 @@
display: flex;
flex-direction: column;
height: 100%;
- padding: 16px 24px;
- overflow: auto;
+}
- &.embedded {
- padding: 0;
- overflow: visible;
- height: auto;
- }
+.tb-iot-hub-browse {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
-.tb-iot-hub-browse-container {
- max-width: 1400px;
- margin: 0 auto;
- width: 100%;
- background: white;
- padding: 24px;
- border-radius: 4px;
- box-shadow: 0 0 1px rgba(0, 0, 0, 0.14), 0 1px 1px rgba(0, 0, 0, 0.12);
-
- &.embedded {
- max-width: none;
- padding: 0;
- border-radius: 0;
- box-shadow: none;
- background: transparent;
- }
+// Tabs — Design: px-40, border-bottom
+.tb-iot-hub-tabs {
+ padding: 0 40px;
+ flex-shrink: 0;
}
-.tb-iot-hub-browse-header {
+// Two-column layout below tabs
+.tb-iot-hub-content {
display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 24px;
-
- h1 {
- font-size: 28px;
- font-weight: 700;
- margin: 0 0 4px;
- color: rgba(0, 0, 0, 0.87);
- }
-
- p {
- font-size: 14px;
- color: rgba(0, 0, 0, 0.54);
- margin: 0;
- }
+ flex: 1;
+ min-height: 0;
}
-.tb-iot-hub-type-tabs {
- margin-bottom: 16px;
- border-bottom: 1px solid rgba(0, 0, 0, 0.12);
-
- ::ng-deep {
- .mat-mdc-tab-link-container {
- flex-grow: 0;
- }
-
- .mat-mdc-tab-link {
- text-transform: none;
- font-weight: 400;
- min-width: auto;
- padding: 0 16px;
- letter-spacing: normal;
- height: 40px;
- flex: 0 0 auto;
- }
-
- --mat-tab-active-indicator-color: #1976d2;
-
- .mdc-tab__text-label {
- color: rgba(0, 0, 0, 0.6);
- }
-
- .mdc-tab--active .mdc-tab__text-label {
- font-weight: 500;
- }
+// Left sidebar filters — Design: 280px wide, right border, own scroll
+.tb-iot-hub-filters {
+ width: 280px;
+ flex-shrink: 0;
+ border-right: 1px solid rgba(0, 0, 0, 0.12);
+ overflow-y: auto;
+}
- --mat-tab-active-label-text-color: #1976d2;
- --mat-tab-active-focus-label-text-color: #1976d2;
- --mat-tab-active-hover-label-text-color: #1976d2;
- --mat-tab-active-ripple-color: #1976d2;
- --mat-tab-inactive-ripple-color: #1976d2;
- --mat-tab-active-focus-indicator-color: #1976d2;
- }
+.tb-iot-hub-filters-inner {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 24px 0;
}
-// Shared field styles for search/filter/sort dropdowns
-%tb-iot-hub-field-base {
+// Filter expansion panels — Design: no border, no shadow, pl-40 for content
+.tb-iot-hub-filter-panel {
::ng-deep {
- .mat-mdc-text-field-wrapper {
- height: 42px;
- padding: 0 12px;
+ &.mat-expansion-panel {
+ box-shadow: none !important;
+ border-radius: 0 !important;
+ background: transparent;
}
- .mat-mdc-form-field-flex {
- height: 42px;
- align-items: center;
+ .mat-expansion-panel-header {
+ padding: 8px 16px 8px 40px;
}
- .mdc-notched-outline__leading,
- .mdc-notched-outline__notch,
- .mdc-notched-outline__trailing {
- border-color: #d5dbe3;
- }
-
- .mdc-notched-outline__leading {
- border-radius: 8px 0 0 8px;
- }
-
- .mdc-notched-outline__trailing {
- border-radius: 0 8px 8px 0;
- }
-
- .mat-mdc-form-field-infix {
- padding-top: 0;
- padding-bottom: 0;
- min-height: unset;
- }
-
- .mdc-notched-outline__notch {
- display: none;
- }
-
- .mat-mdc-floating-label {
- display: none;
- }
-
- .mat-mdc-form-field-subscript-wrapper {
- display: none;
- }
-
- input.mat-mdc-input-element {
+ .mat-expansion-panel-header-title {
font-size: 14px;
- color: #3e4a59;
-
- &::placeholder {
- color: #8b95a2;
- opacity: 1;
- }
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.87);
}
- .mat-mdc-select-value-text {
- font-size: 14px;
- color: #3e4a59;
+ .mat-expansion-panel-body {
+ padding: 0;
}
+ }
- .mat-mdc-select-arrow-wrapper {
- padding-left: 4px;
- }
+ mat-checkbox {
+ display: block;
+ padding: 8px 16px 8px 40px;
- .mat-icon {
- color: #8b95a2;
- font-size: 20px;
- width: 20px;
- height: 20px;
+ &:hover {
+ background: rgba(0, 0, 0, 0.04);
}
}
+}
- &.mat-focused ::ng-deep {
- .mdc-notched-outline__leading,
- .mdc-notched-outline__notch,
- .mdc-notched-outline__trailing {
- border-color: #8b95a2;
- border-width: 1px;
- }
- }
+// Right content area — Design: flex-1, pr-40, scrollable
+.tb-iot-hub-results {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ padding: 0 40px 0 20px;
}
-.tb-iot-hub-search-row {
+// Top bar: results count + search
+.tb-iot-hub-results-top {
display: flex;
- gap: 12px;
- margin-bottom: 12px;
align-items: center;
+ justify-content: space-between;
+ padding: 16px 0 0;
+ flex-shrink: 0;
+}
- .tb-iot-hub-search-field,
- .tb-iot-hub-sort-field {
- @extend %tb-iot-hub-field-base;
- }
-
- .tb-iot-hub-search-field {
- flex: 1;
-
- ::ng-deep {
- .mat-mdc-form-field-icon-prefix {
- padding: 0 8px 0 0;
-
- > .mat-icon {
- padding: 0;
- }
- }
-
- .mat-mdc-form-field-icon-suffix {
- padding: 0;
+.tb-iot-hub-results-count {
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.54);
+}
- .mat-mdc-icon-button {
- width: 32px;
- height: 32px;
- padding: 4px;
- }
- }
- }
- }
+.tb-iot-hub-search-field {
+ width: 240px;
- .tb-iot-hub-sort-field {
- width: 140px;
- flex-shrink: 0;
+ .mat-mdc-form-field-subscript-wrapper {
+ display: none;
}
}
-.tb-iot-hub-filter-row {
+// Sort row + active filters
+.tb-iot-hub-sort-row {
display: flex;
- gap: 12px;
- margin-bottom: 16px;
- padding-right: 152px;
-
- .tb-iot-hub-filter-select {
- @extend %tb-iot-hub-field-base;
- flex: 1;
- min-width: 0;
- }
+ align-items: center;
+ gap: 8px;
+ padding: 8px 0;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+.tb-iot-hub-sort-spacer {
+ flex: 1;
}
+.tb-iot-hub-sort-btn {
+ color: rgba(0, 0, 0, 0.54);
+ flex-shrink: 0;
+}
+
+// Active filter chips
.tb-iot-hub-active-filters {
display: flex;
align-items: center;
- gap: 6px;
+ gap: 8px;
flex-wrap: wrap;
- margin-bottom: 12px;
}
.tb-iot-hub-active-chip {
display: inline-flex;
align-items: center;
gap: 4px;
- padding: 4px 8px 4px 12px;
- border-radius: 16px;
+ padding: 4px 8px;
+ border-radius: 4px;
font-size: 12px;
font-weight: 500;
- background: #eef4ff;
- color: #305ECA;
+ background: rgba(0, 0, 0, 0.06);
+ color: rgba(0, 0, 0, 0.76);
cursor: pointer;
white-space: nowrap;
- transition: background-color 0.15s;
mat-icon {
font-size: 14px;
@@ -269,135 +167,85 @@
}
&:hover {
- background: #dde8fc;
+ background: rgba(0, 0, 0, 0.1);
}
}
-.tb-iot-hub-clear-filters {
- border: none;
- background: none;
+.tb-iot-hub-clear-btn {
font-size: 12px;
- color: #8b95a2;
+ color: rgba(0, 0, 0, 0.54);
cursor: pointer;
+ background: none;
+ border: none;
padding: 4px 8px;
- white-space: nowrap;
+ font-family: inherit;
&:hover {
- color: #3e4a59;
- text-decoration: underline;
+ color: rgba(0, 0, 0, 0.87);
}
}
-.tb-iot-hub-results-count {
- font-size: 14px;
- color: rgba(0, 0, 0, 0.54);
- margin-bottom: 16px;
+// Card grid — Design: 4 columns, gap 20px
+.tb-iot-hub-card-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ padding: 8px 0 24px;
}
+// Loading / Empty states
.tb-iot-hub-loading {
display: flex;
justify-content: center;
- align-items: center;
- min-height: 200px;
+ padding: 80px 0;
}
.tb-iot-hub-empty-state {
display: flex;
flex-direction: column;
align-items: center;
- justify-content: center;
- min-height: 300px;
+ padding: 80px 0;
text-align: center;
- color: rgba(0, 0, 0, 0.54);
-
- .tb-iot-hub-empty-icon {
- font-size: 64px;
- width: 64px;
- height: 64px;
- margin-bottom: 16px;
- opacity: 0.4;
- }
h3 {
- margin: 0 0 8px 0;
+ font-size: 18px;
+ font-weight: 500;
color: rgba(0, 0, 0, 0.87);
+ margin: 16px 0 8px;
}
p {
- margin: 0 0 16px 0;
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.54);
+ margin: 0 0 24px;
}
}
-.tb-iot-hub-card-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 16px;
- margin-bottom: 16px;
+.tb-iot-hub-empty-icon {
+ font-size: 48px;
+ width: 48px;
+ height: 48px;
+ color: rgba(0, 0, 0, 0.2);
}
-::ng-deep mat-paginator {
- .mat-mdc-form-field {
- .mat-mdc-text-field-wrapper {
- height: 42px;
- padding: 0 12px;
- border: 1px solid #d5dbe3;
- border-radius: 8px;
- background: white;
- }
-
- .mat-mdc-form-field-flex {
- height: 42px;
- align-items: center;
- }
-
- .mdc-notched-outline {
- display: none;
- }
-
- .mat-mdc-form-field-infix {
- padding-top: 0;
- padding-bottom: 0;
- min-height: unset;
- border-top: none;
- }
-
- .mat-mdc-floating-label {
- display: none;
- }
-
- .mat-mdc-form-field-subscript-wrapper {
- display: none;
- }
-
- .mat-mdc-select-value-text {
- font-size: 14px;
- color: #3e4a59;
- }
-
- .mat-mdc-select-arrow-wrapper {
- padding-left: 4px;
- }
+// Pagination
+mat-paginator {
+ flex-shrink: 0;
+}
- &.mat-focused .mat-mdc-text-field-wrapper {
- border-color: #8b95a2;
- }
+// Responsive
+@media (max-width: 1200px) {
+ .tb-iot-hub-card-grid {
+ grid-template-columns: repeat(3, 1fr);
}
}
-.tb-iot-hub-browse-footer {
- text-align: center;
- padding: 24px 0;
- border-top: 1px solid rgba(0, 0, 0, 0.08);
- margin-top: 16px;
-
- .tb-iot-hub-become-creator {
- color: rgba(0, 0, 0, 0.38);
- font-size: 13px;
- text-decoration: none;
+@media (max-width: 900px) {
+ .tb-iot-hub-filters {
+ width: 220px;
+ }
- &:hover {
- color: rgba(0, 0, 0, 0.54);
- text-decoration: underline;
- }
+ .tb-iot-hub-card-grid {
+ grid-template-columns: repeat(2, 1fr);
}
}
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
index 083b858c70..f753f0da27 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.ts
@@ -159,6 +159,18 @@ export class TbIotHubBrowseComponent implements OnInit, OnDestroy {
this.loadItems();
}
+ isSubtypeActive(key: string): boolean {
+ return this.getActiveSubtypes().has(key);
+ }
+
+ isCategoryActive(key: string): boolean {
+ return this.activeCategories.has(key);
+ }
+
+ isUseCaseActive(key: string): boolean {
+ return this.activeUseCases.has(key);
+ }
+
onCategoryToggle(category: string): void {
if (this.activeCategories.has(category)) {
this.activeCategories.delete(category);
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
index cbebb07387..532e9a2770 100644
--- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
+++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-card.component.html
@@ -54,6 +54,9 @@
}