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"> <div class="tb-iot-hub-content">
<!-- Left sidebar filters --> <!-- Left sidebar filters -->
<div class="tb-iot-hub-filters"> <div class="tb-iot-hub-filters">
<div class="tb-iot-hub-filters-wrapper">
<div class="tb-iot-hub-filters-inner"> <div class="tb-iot-hub-filters-inner">
@if (getSubtypeMap(); as subtypeMap) { @if (getSubtypeMap(); as subtypeMap) {
<mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel"> <mat-expansion-panel [expanded]="true" class="tb-iot-hub-filter-panel">
@ -71,6 +72,9 @@
} }
@if (categories.size > 0) { @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 [expanded]="true" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.category' | translate }}</mat-panel-title> <mat-panel-title>{{ 'iot-hub.category' | translate }}</mat-panel-title>
@ -84,6 +88,9 @@
</mat-expansion-panel> </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 [expanded]="false" class="tb-iot-hub-filter-panel">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title>{{ 'iot-hub.use-case' | translate }}</mat-panel-title> <mat-panel-title>{{ 'iot-hub.use-case' | translate }}</mat-panel-title>
@ -96,6 +103,7 @@
} }
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</div>
</div> </div>
<!-- Right content area --> <!-- Right content area -->
@ -105,47 +113,47 @@
<span class="tb-iot-hub-results-count"> <span class="tb-iot-hub-results-count">
{{ 'iot-hub.results-count' | translate:{ count: totalElements } }} {{ 'iot-hub.results-count' | translate:{ count: totalElements } }}
</span> </span>
<mat-form-field appearance="outline" class="tb-iot-hub-search-field"> <div class="tb-iot-hub-search-field">
<mat-icon matPrefix>search</mat-icon> <mat-icon class="tb-iot-hub-search-icon">search</mat-icon>
<input matInput <input class="tb-iot-hub-search-input"
[placeholder]="'iot-hub.search-placeholder' | translate" [placeholder]="'iot-hub.search' | translate"
[value]="textSearch" [value]="textSearch"
(input)="onSearchChange($any($event.target).value)"> (input)="onSearchChange($any($event.target).value)">
@if (textSearch) { @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> <mat-icon>close</mat-icon>
</button> </button>
} }
</mat-form-field> </div>
</div> </div>
<!-- Sort button --> <!-- Sort button -->
<div class="tb-iot-hub-sort-row"> <div class="tb-iot-hub-sort-row">
<!-- Active filter chips --> <!-- Active filter chips -->
@if (hasActiveDropdownFilters()) { @if (hasActiveDropdownFilters()) {
<div class="tb-iot-hub-active-filters"> <mat-chip-set class="tb-iot-hub-active-filters">
@for (key of getActiveSubtypesArray(); track key) { @for (key of getActiveSubtypesArray(); track key) {
<span class="tb-iot-hub-active-chip" (click)="onSubtypeToggle(key)"> <mat-chip (removed)="onSubtypeToggle(key)">
{{ getSubtypeLabel(key) }} {{ getSubtypeLabel(key) }}
<mat-icon>close</mat-icon> <mat-icon matChipRemove>close</mat-icon>
</span> </mat-chip>
} }
@for (key of getActiveCategoriesArray(); track key) { @for (key of getActiveCategoriesArray(); track key) {
<span class="tb-iot-hub-active-chip" (click)="onCategoryToggle(key)"> <mat-chip (removed)="onCategoryToggle(key)">
{{ getCategoryLabel(key) }} {{ getCategoryLabel(key) }}
<mat-icon>close</mat-icon> <mat-icon matChipRemove>close</mat-icon>
</span> </mat-chip>
} }
@for (key of getActiveUseCasesArray(); track key) { @for (key of getActiveUseCasesArray(); track key) {
<span class="tb-iot-hub-active-chip" (click)="onUseCaseToggle(key)"> <mat-chip (removed)="onUseCaseToggle(key)">
{{ getUseCaseLabel(key) }} {{ getUseCaseLabel(key) }}
<mat-icon>close</mat-icon> <mat-icon matChipRemove>close</mat-icon>
</span> </mat-chip>
} }
<button class="tb-iot-hub-clear-btn" (click)="clearAllFilters()"> </mat-chip-set>
{{ 'iot-hub.clear-all-filters' | translate }} <button class="tb-iot-hub-clear-btn" (click)="clearAllFilters()">
</button> {{ 'iot-hub.clear-all' | translate }}
</div> </button>
} }
<span class="tb-iot-hub-sort-spacer"></span> <span class="tb-iot-hub-sort-spacer"></span>
<button mat-button class="tb-iot-hub-sort-btn" [matMenuTriggerFor]="sortMenu"> <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. * limitations under the License.
*/ */
@import "../../../../../scss/constants";
:host { :host {
display: flex; display: block;
flex-direction: column;
height: 100%;
} }
.tb-iot-hub-browse { .tb-iot-hub-browse {
@ -26,17 +26,17 @@
height: 100%; height: 100%;
} }
// Tabs — Design: px-40, border-bottom // Tabs — Design: px-40, bottom border rgba(0,0,0,0.12)
.tb-iot-hub-tabs { .tb-iot-hub-tabs {
padding: 0 40px; padding: 0 40px;
flex-shrink: 0; flex-shrink: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
} }
// Two-column layout below tabs // Two-column layout below tabs
.tb-iot-hub-content { .tb-iot-hub-content {
display: flex; display: flex;
flex: 1; flex: 1;
min-height: 0;
} }
// Left sidebar filters — Design: 280px wide, right border, own scroll // Left sidebar filters — Design: 280px wide, right border, own scroll
@ -44,6 +44,12 @@
width: 280px; width: 280px;
flex-shrink: 0; flex-shrink: 0;
border-right: 1px solid rgba(0, 0, 0, 0.12); border-right: 1px solid rgba(0, 0, 0, 0.12);
position: relative;
}
.tb-iot-hub-filters-wrapper {
position: absolute;
inset: 0;
overflow-y: auto; overflow-y: auto;
} }
@ -54,33 +60,37 @@
padding: 24px 0; padding: 24px 0;
} }
// Filter expansion panels — Design: no border, no shadow, pl-40 for content // Filter divider between sections
.tb-iot-hub-filter-panel { .tb-iot-hub-filter-divider {
::ng-deep { height: 1px;
&.mat-expansion-panel { background: rgba(0, 0, 0, 0.12);
box-shadow: none !important; }
border-radius: 0 !important;
background: transparent;
}
.mat-expansion-panel-header { // Filter expansion panels — Design: no shadow, pl-40 for content
padding: 8px 16px 8px 40px; .tb-iot-hub-filter-panel.mat-expansion-panel {
} box-shadow: none;
border-radius: 0;
background: transparent;
.mat-expansion-panel-header-title { .mat-expansion-panel-header {
font-size: 14px; padding: 8px 16px 8px 40px;
font-weight: 500; height: 40px;
color: rgba(0, 0, 0, 0.87); min-height: 40px;
} }
.mat-expansion-panel-body { ::ng-deep .mat-expansion-panel-header-title {
padding: 0; font-size: 14px;
} font-weight: 500;
color: rgba(0, 0, 0, 0.87);
}
::ng-deep .mat-expansion-panel-body {
padding: 0;
} }
mat-checkbox { mat-checkbox {
display: block; display: block;
padding: 8px 16px 8px 40px; padding: 0 0 0 28px;
&:hover { &:hover {
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
@ -107,16 +117,64 @@
flex-shrink: 0; flex-shrink: 0;
} }
// Results count — Design: 14px Medium, rgba(0,0,0,0.76), tracking 0.25
.tb-iot-hub-results-count { .tb-iot-hub-results-count {
font-size: 14px; 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 { .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 { &::placeholder {
display: none; 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; flex: 1;
} }
// Sort button — Design: 14px Medium, primary color, with filter_list icon
.tb-iot-hub-sort-btn { .tb-iot-hub-sort-btn {
color: rgba(0, 0, 0, 0.54); color: $tb-primary-color;
flex-shrink: 0; 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 { .tb-iot-hub-active-filters {
display: flex; display: inline;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.tb-iot-hub-active-chip { .mat-mdc-standard-chip:not(.mdc-evolution-chip--disabled) {
display: inline-flex; --mat-chip-elevated-container-color: rgba(0, 0, 0, 0.04);
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);
} }
} }
// Clear all button — Design: 14px Regular, rgba(0,0,0,0.54)
.tb-iot-hub-clear-btn { .tb-iot-hub-clear-btn {
font-size: 12px; font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
cursor: pointer; cursor: pointer;
background: none; background: none;
border: none; border: none;
padding: 4px 8px; padding: 6px 8px;
font-family: inherit; font-family: inherit;
&:hover { &: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. limitations under the License.
--> -->
<div class="tb-iot-hub-creator-profile-container"> <div class="tb-iot-hub-creator-page">
<button mat-button class="tb-iot-hub-back-btn" (click)="goBack()"> <!-- Back link -->
<mat-icon>arrow_back</mat-icon> <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 }} {{ 'iot-hub.back-to-iot-hub' | translate }}
</button> </button>
@if (creator) { @if (creator) {
<!-- Creator header --> <!-- Creator profile -->
<mat-card class="tb-iot-hub-creator-header" appearance="outlined"> <div class="tb-iot-hub-creator-profile">
<mat-card-content> <div class="tb-iot-hub-creator-header">
<div class="tb-iot-hub-creator-info"> <div class="tb-iot-hub-creator-avatar">
<div class="tb-iot-hub-creator-avatar"> @if (getAvatarUrl(); as avatarUrl) {
@if (getAvatarUrl(); as avatarUrl) { <img [src]="avatarUrl" alt="">
<img [src]="avatarUrl" alt=""> } @else {
} @else { <mat-icon>person</mat-icon>
<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>
</div> </div>
</mat-card-content> <div class="tb-iot-hub-creator-name-block">
</mat-card> <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-links">
<div class="tb-iot-hub-creator-items-header"> @if (creator.website) {
<h3>{{ 'iot-hub.published-items' | translate }} ({{ creator.publishedCount }})</h3> <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> </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 <tb-iot-hub-browse
#browseComponent #browseComponent
[creatorId]="creatorId" [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. * limitations under the License.
*/ */
@import "../../../../../scss/constants";
:host { :host {
display: flex; display: block;
flex-direction: column;
height: 100%;
padding: 16px 24px;
overflow: auto;
} }
.tb-iot-hub-creator-profile-container { .tb-iot-hub-creator-page {
max-width: 1200px;
margin: 0 auto;
width: 100%;
background: white; background: white;
padding: 24px; border-radius: 8px;
border-radius: 4px; padding: 24px 40px;
box-shadow: 0 0 1px rgba(0, 0, 0, 0.14), 0 1px 1px rgba(0, 0, 0, 0.12);
} }
// Back button
.tb-iot-hub-back-btn { .tb-iot-hub-back-btn {
margin-bottom: 16px; margin-bottom: 24px;
} }
.tb-iot-hub-creator-header { // Creator profile section
margin-bottom: 24px; .tb-iot-hub-creator-profile {
margin-bottom: 32px;
} }
.tb-iot-hub-creator-info { // Header: avatar + name + email
.tb-iot-hub-creator-header {
display: flex; display: flex;
gap: 24px; align-items: center;
align-items: flex-start; gap: 16px;
margin-bottom: 16px;
} }
.tb-iot-hub-creator-avatar { .tb-iot-hub-creator-avatar {
width: 80px; width: 64px;
height: 80px; height: 64px;
min-width: 80px; min-width: 64px;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
@ -64,56 +62,60 @@
} }
mat-icon { mat-icon {
font-size: 64px; font-size: 40px;
width: 64px; width: 40px;
height: 64px; height: 40px;
color: rgba(0, 0, 0, 0.3); color: rgba(0, 0, 0, 0.3);
} }
} }
.tb-iot-hub-creator-details { .tb-iot-hub-creator-name-block {
flex: 1; display: flex;
flex-direction: column;
h2 { gap: 2px;
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-link { .tb-iot-hub-creator-name {
color: #1976d2; font-size: 24px;
text-decoration: none; font-weight: 500;
font-size: 14px; line-height: 32px;
display: block; color: rgba(0, 0, 0, 0.87);
margin-bottom: 4px; margin: 0;
}
&:hover { .tb-iot-hub-creator-email {
text-decoration: underline; font-size: 14px;
} color: rgba(0, 0, 0, 0.54);
} }
.tb-iot-hub-creator-email { // Description
font-size: 14px; .tb-iot-hub-creator-desc {
color: rgba(0, 0, 0, 0.54); font-size: 14px;
margin-bottom: 8px; line-height: 20px;
} color: rgba(0, 0, 0, 0.76);
margin: 0 0 16px;
max-width: 600px;
}
.tb-iot-hub-social-links { // Links: website button + social icons
display: flex; .tb-iot-hub-creator-links {
gap: 4px; display: flex;
} align-items: center;
gap: 8px;
} }
.tb-iot-hub-creator-items-header { .tb-iot-hub-creator-website {
margin-bottom: 16px; color: $tb-primary-color;
font-size: 14px;
font-weight: 500;
text-decoration: none;
}
h3 { // Published items heading
margin: 0; .tb-iot-hub-published-heading {
font-size: 18px; font-size: 20px;
font-weight: 600; 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", "home-subtitle-prefix": "Discover ready-to-use",
"and-devices": "& Devices", "and-devices": "& Devices",
"search-placeholder": "Search in IoT Hub...", "search-placeholder": "Search in IoT Hub...",
"search": "Search...",
"device-library": "Device Library", "device-library": "Device Library",
"popular-widgets": "Popular Widgets", "popular-widgets": "Popular Widgets",
"popular-dashboards": "Popular Dashboards", "popular-dashboards": "Popular Dashboards",
@ -3737,6 +3738,7 @@
"category": "Category", "category": "Category",
"use-case": "Use case", "use-case": "Use case",
"clear-all-filters": "Clear all filters", "clear-all-filters": "Clear all filters",
"clear-all": "Clear all",
"results-count": "{{count}} results", "results-count": "{{count}} results",
"unable-to-load": "Unable to load", "unable-to-load": "Unable to load",
"unable-to-load-text": "There was a problem loading items. Please try again.", "unable-to-load-text": "There was a problem loading items. Please try again.",

Loading…
Cancel
Save