Browse Source

Update browse page filters, search, and creator profile to match design

- Browse: custom search field (320px, rounded-8, border, plain input),
  sort button with primary color, results count 14px Medium
- Filter chips: use Angular Material mat-chip-set with rgba(0,0,0,0.04) bg,
  "Clear all" text button
- Filter sidebar: scroll via absolute-positioned wrapper, dividers between panels
- Creator profile: flat layout (no mat-card), avatar+name+email header,
  description, website stroked button with globe icon, social icon buttons,
  "Published Items (N)" heading, embedded browse component
pull/15508/head
Igor Kulikov 6 months ago
parent
commit
7d6d4ecb9e
  1. 48
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.html
  2. 158
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-browse.component.scss
  3. 103
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html
  4. 126
      ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.scss
  5. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -55,6 +55,7 @@
<div class="tb-iot-hub-content">
<!-- Left sidebar filters -->
<div class="tb-iot-hub-filters">
<div class="tb-iot-hub-filters-wrapper">
<div class="tb-iot-hub-filters-inner">
@if (getSubtypeMap(); as subtypeMap) {
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
@ -71,6 +72,9 @@
}
@if (categories.size > 0) {
@if (getSubtypeMap()) {
<div class="tb-iot-hub-filter-divider"></div>
}
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.category' | translate }}</mat-panel-title>
@ -84,6 +88,9 @@
</mat-expansion-panel>
}
@if (getSubtypeMap() || categories.size > 0) {
<div class="tb-iot-hub-filter-divider"></div>
}
<mat-expansion-panel [expanded]="false" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.use-case' | translate }}</mat-panel-title>
@ -96,6 +103,7 @@
}
</mat-expansion-panel>
</div>
</div>
</div>
<!-- Right content area -->
@ -105,47 +113,47 @@
<span class="tb-iot-hub-results-count">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span>
<mat-form-field appearance="outline" class="tb-iot-hub-search-field">
<mat-icon matPrefix>search</mat-icon>
<input matInput
[placeholder]="'iot-hub.search-placeholder' | translate"
<div class="tb-iot-hub-search-field">
<mat-icon class="tb-iot-hub-search-icon">search</mat-icon>
<input class="tb-iot-hub-search-input"
[placeholder]="'iot-hub.search' | translate"
[value]="textSearch"
(input)="onSearchChange($any($event.target).value)">
@if (textSearch) {
<button matSuffix mat-icon-button (click)="onSearchChange('')">
<button mat-icon-button class="tb-iot-hub-search-clear" (click)="onSearchChange('')">
<mat-icon>close</mat-icon>
</button>
}
</mat-form-field>
</div>
</div>
<!-- Sort button -->
<div class="tb-iot-hub-sort-row">
<!-- Active filter chips -->
@if (hasActiveDropdownFilters()) {
<div class="tb-iot-hub-active-filters">
<mat-chip-set class="tb-iot-hub-active-filters">
@for (key of getActiveSubtypesArray(); track key) {
<span class="tb-iot-hub-active-chip" (click)="onSubtypeToggle(key)">
<mat-chip (removed)="onSubtypeToggle(key)">
{{ getSubtypeLabel(key) }}
<mat-icon>close</mat-icon>
</span>
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveCategoriesArray(); track key) {
<span class="tb-iot-hub-active-chip" (click)="onCategoryToggle(key)">
<mat-chip (removed)="onCategoryToggle(key)">
{{ getCategoryLabel(key) }}
<mat-icon>close</mat-icon>
</span>
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
@for (key of getActiveUseCasesArray(); track key) {
<span class="tb-iot-hub-active-chip" (click)="onUseCaseToggle(key)">
<mat-chip (removed)="onUseCaseToggle(key)">
{{ getUseCaseLabel(key) }}
<mat-icon>close</mat-icon>
</span>
<mat-icon matChipRemove>close</mat-icon>
</mat-chip>
}
<button class="tb-iot-hub-clear-btn" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all-filters' | translate }}
</button>
</div>
</mat-chip-set>
<button class="tb-iot-hub-clear-btn" (click)="clearAllFilters()">
{{ 'iot-hub.clear-all' | translate }}
</button>
}
<span class="tb-iot-hub-sort-spacer"></span>
<button mat-button class="tb-iot-hub-sort-btn" [matMenuTriggerFor]="sortMenu">

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

@ -14,10 +14,10 @@
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
display: flex;
flex-direction: column;
height: 100%;
display: block;
}
.tb-iot-hub-browse {
@ -26,17 +26,17 @@
height: 100%;
}
// Tabs — Design: px-40, border-bottom
// Tabs — Design: px-40, bottom border rgba(0,0,0,0.12)
.tb-iot-hub-tabs {
padding: 0 40px;
flex-shrink: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
// Two-column layout below tabs
.tb-iot-hub-content {
display: flex;
flex: 1;
min-height: 0;
}
// Left sidebar filters — Design: 280px wide, right border, own scroll
@ -44,6 +44,12 @@
width: 280px;
flex-shrink: 0;
border-right: 1px solid rgba(0, 0, 0, 0.12);
position: relative;
}
.tb-iot-hub-filters-wrapper {
position: absolute;
inset: 0;
overflow-y: auto;
}
@ -54,33 +60,37 @@
padding: 24px 0;
}
// Filter expansion panels — Design: no border, no shadow, pl-40 for content
.tb-iot-hub-filter-panel {
::ng-deep {
&.mat-expansion-panel {
box-shadow: none !important;
border-radius: 0 !important;
background: transparent;
}
// Filter divider between sections
.tb-iot-hub-filter-divider {
height: 1px;
background: rgba(0, 0, 0, 0.12);
}
.mat-expansion-panel-header {
padding: 8px 16px 8px 40px;
}
// Filter expansion panels — Design: no shadow, pl-40 for content
.tb-iot-hub-filter-panel.mat-expansion-panel {
box-shadow: none;
border-radius: 0;
background: transparent;
.mat-expansion-panel-header-title {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
}
.mat-expansion-panel-header {
padding: 8px 16px 8px 40px;
height: 40px;
min-height: 40px;
}
.mat-expansion-panel-body {
padding: 0;
}
::ng-deep .mat-expansion-panel-header-title {
font-size: 14px;
font-weight: 500;
color: rgba(0, 0, 0, 0.87);
}
::ng-deep .mat-expansion-panel-body {
padding: 0;
}
mat-checkbox {
display: block;
padding: 8px 16px 8px 40px;
padding: 0 0 0 28px;
&:hover {
background: rgba(0, 0, 0, 0.04);
@ -107,16 +117,64 @@
flex-shrink: 0;
}
// Results count — Design: 14px Medium, rgba(0,0,0,0.76), tracking 0.25
.tb-iot-hub-results-count {
font-size: 14px;
color: rgba(0, 0, 0, 0.54);
font-weight: 500;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.76);
}
// Search field — Design: 320px, h-40, rounded-8, border rgba(0,0,0,0.12), white bg
.tb-iot-hub-search-field {
width: 240px;
width: 320px;
height: 40px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.12);
background: white;
}
.tb-iot-hub-search-icon {
font-size: 24px;
width: 24px;
height: 24px;
color: rgba(0, 0, 0, 0.38);
flex-shrink: 0;
}
.tb-iot-hub-search-input {
flex: 1;
border: none;
outline: none;
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: rgba(0, 0, 0, 0.87);
background: transparent;
font-family: inherit;
.mat-mdc-form-field-subscript-wrapper {
display: none;
&::placeholder {
color: rgba(0, 0, 0, 0.38);
}
}
.tb-iot-hub-search-clear.mat-mdc-icon-button {
width: 24px;
height: 24px;
padding: 0;
flex-shrink: 0;
color: rgba(0, 0, 0, 0.38);
--mdc-icon-button-state-layer-size: 24px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
@ -134,50 +192,32 @@
flex: 1;
}
// Sort button — Design: 14px Medium, primary color, with filter_list icon
.tb-iot-hub-sort-btn {
color: rgba(0, 0, 0, 0.54);
color: $tb-primary-color;
flex-shrink: 0;
font-size: 14px;
font-weight: 500;
}
// Active filter chips
// Active filter chips — Design: mat-chip-set, gap-8, chip bg rgba(0,0,0,0.04)
.tb-iot-hub-active-filters {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
display: inline;
.tb-iot-hub-active-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: rgba(0, 0, 0, 0.06);
color: rgba(0, 0, 0, 0.76);
cursor: pointer;
white-space: nowrap;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
&:hover {
background: rgba(0, 0, 0, 0.1);
.mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) {
--mat-chip-elevated-container-color: rgba(0, 0, 0, 0.04);
}
}
// Clear all button — Design: 14px Regular, rgba(0,0,0,0.54)
.tb-iot-hub-clear-btn {
font-size: 12px;
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.54);
cursor: pointer;
background: none;
border: none;
padding: 4px 8px;
padding: 6px 8px;
font-family: inherit;
&:hover {

103
ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-creator-profile.component.html

@ -15,64 +15,67 @@
limitations under the License.
-->
<div class="tb-iot-hub-creator-profile-container">
<button mat-button class="tb-iot-hub-back-btn" (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
<div class="tb-iot-hub-creator-page">
<!-- Back link -->
<button mat-button color="primary" class="tb-iot-hub-back-btn" (click)="goBack()">
<mat-icon>chevron_left</mat-icon>
{{ 'iot-hub.back-to-iot-hub' | translate }}
</button>
@if (creator) {
<!-- Creator header -->
<mat-card class="tb-iot-hub-creator-header" appearance="outlined">
<mat-card-content>
<div class="tb-iot-hub-creator-info">
<div class="tb-iot-hub-creator-avatar">
@if (getAvatarUrl(); as avatarUrl) {
<img [src]="avatarUrl" alt="">
} @else {
<mat-icon>account_circle</mat-icon>
}
</div>
<div class="tb-iot-hub-creator-details">
<h2>{{ creator.displayName }}</h2>
@if (creator.description) {
<p class="tb-iot-hub-creator-desc">{{ creator.description }}</p>
}
@if (creator.website) {
<a class="tb-iot-hub-creator-link" [href]="creator.website" target="_blank">
{{ creator.website }}
</a>
}
@if (creator.contactEmail) {
<div class="tb-iot-hub-creator-email">{{ creator.contactEmail }}</div>
}
<div class="tb-iot-hub-social-links">
@if (creator.githubUrl) {
<a [href]="creator.githubUrl" target="_blank" mat-icon-button matTooltip="GitHub">
<mat-icon>code</mat-icon>
</a>
}
@if (creator.linkedinUrl) {
<a [href]="creator.linkedinUrl" target="_blank" mat-icon-button matTooltip="LinkedIn">
<mat-icon>work</mat-icon>
</a>
}
@if (creator.twitterUrl) {
<a [href]="creator.twitterUrl" target="_blank" mat-icon-button matTooltip="Twitter/X">
<mat-icon>chat</mat-icon>
</a>
}
</div>
</div>
<!-- Creator profile -->
<div class="tb-iot-hub-creator-profile">
<div class="tb-iot-hub-creator-header">
<div class="tb-iot-hub-creator-avatar">
@if (getAvatarUrl(); as avatarUrl) {
<img [src]="avatarUrl" alt="">
} @else {
<mat-icon>person</mat-icon>
}
</div>
</mat-card-content>
</mat-card>
<div class="tb-iot-hub-creator-name-block">
<h2 class="tb-iot-hub-creator-name">{{ creator.displayName }}</h2>
@if (creator.contactEmail) {
<span class="tb-iot-hub-creator-email">{{ creator.contactEmail }}</span>
}
</div>
</div>
@if (creator.description) {
<p class="tb-iot-hub-creator-desc">{{ creator.description }}</p>
}
<!-- Published items -->
<div class="tb-iot-hub-creator-items-header">
<h3>{{ 'iot-hub.published-items' | translate }} ({{ creator.publishedCount }})</h3>
<div class="tb-iot-hub-creator-links">
@if (creator.website) {
<a mat-stroked-button [href]="creator.website" target="_blank" class="tb-iot-hub-creator-website">
<mat-icon>language</mat-icon>
{{ creator.website | slice:0:40 }}
</a>
}
@if (creator.githubUrl) {
<a [href]="creator.githubUrl" target="_blank" mat-icon-button>
<mat-icon svgIcon="mdi:github"></mat-icon>
</a>
}
@if (creator.linkedinUrl) {
<a [href]="creator.linkedinUrl" target="_blank" mat-icon-button>
<mat-icon svgIcon="mdi:linkedin"></mat-icon>
</a>
}
@if (creator.twitterUrl) {
<a [href]="creator.twitterUrl" target="_blank" mat-icon-button>
<mat-icon svgIcon="mdi:twitter"></mat-icon>
</a>
}
</div>
</div>
<!-- Published items heading -->
<h3 class="tb-iot-hub-published-heading">
{{ 'iot-hub.published-items' | translate }} ({{ creator.publishedCount }})
</h3>
<!-- Embedded browse -->
<tb-iot-hub-browse
#browseComponent
[creatorId]="creatorId"

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

@ -14,42 +14,40 @@
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px 24px;
overflow: auto;
display: block;
}
.tb-iot-hub-creator-profile-container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
.tb-iot-hub-creator-page {
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);
border-radius: 8px;
padding: 24px 40px;
}
// Back button
.tb-iot-hub-back-btn {
margin-bottom: 16px;
margin-bottom: 24px;
}
.tb-iot-hub-creator-header {
margin-bottom: 24px;
// Creator profile section
.tb-iot-hub-creator-profile {
margin-bottom: 32px;
}
.tb-iot-hub-creator-info {
// Header: avatar + name + email
.tb-iot-hub-creator-header {
display: flex;
gap: 24px;
align-items: flex-start;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.tb-iot-hub-creator-avatar {
width: 80px;
height: 80px;
min-width: 80px;
width: 64px;
height: 64px;
min-width: 64px;
border-radius: 50%;
overflow: hidden;
display: flex;
@ -64,56 +62,60 @@
}
mat-icon {
font-size: 64px;
width: 64px;
height: 64px;
font-size: 40px;
width: 40px;
height: 40px;
color: rgba(0, 0, 0, 0.3);
}
}
.tb-iot-hub-creator-details {
flex: 1;
h2 {
margin: 0 0 4px 0;
font-size: 22px;
}
.tb-iot-hub-creator-desc {
margin: 0 0 8px 0;
color: rgba(0, 0, 0, 0.6);
}
.tb-iot-hub-creator-name-block {
display: flex;
flex-direction: column;
gap: 2px;
}
.tb-iot-hub-creator-link {
color: #1976d2;
text-decoration: none;
font-size: 14px;
display: block;
margin-bottom: 4px;
.tb-iot-hub-creator-name {
font-size: 24px;
font-weight: 500;
line-height: 32px;
color: rgba(0, 0, 0, 0.87);
margin: 0;
}
&:hover {
text-decoration: underline;
}
}
.tb-iot-hub-creator-email {
font-size: 14px;
color: rgba(0, 0, 0, 0.54);
}
.tb-iot-hub-creator-email {
font-size: 14px;
color: rgba(0, 0, 0, 0.54);
margin-bottom: 8px;
}
// Description
.tb-iot-hub-creator-desc {
font-size: 14px;
line-height: 20px;
color: rgba(0, 0, 0, 0.76);
margin: 0 0 16px;
max-width: 600px;
}
.tb-iot-hub-social-links {
display: flex;
gap: 4px;
}
// Links: website button + social icons
.tb-iot-hub-creator-links {
display: flex;
align-items: center;
gap: 8px;
}
.tb-iot-hub-creator-items-header {
margin-bottom: 16px;
.tb-iot-hub-creator-website {
color: $tb-primary-color;
font-size: 14px;
font-weight: 500;
text-decoration: none;
}
h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
// Published items heading
.tb-iot-hub-published-heading {
font-size: 20px;
font-weight: 600;
line-height: 24px;
color: rgba(0, 0, 0, 0.87);
margin: 0 0 16px;
}

2
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3702,6 +3702,7 @@
"home-subtitle-prefix": "Discover ready-to-use",
"and-devices": "& Devices",
"search-placeholder": "Search in IoT Hub...",
"search": "Search...",
"device-library": "Device Library",
"popular-widgets": "Popular Widgets",
"popular-dashboards": "Popular Dashboards",
@ -3737,6 +3738,7 @@
"category": "Category",
"use-case": "Use case",
"clear-all-filters": "Clear all filters",
"clear-all": "Clear all",
"results-count": "{{count}} results",
"unable-to-load": "Unable to load",
"unable-to-load-text": "There was a problem loading items. Please try again.",

Loading…
Cancel
Save