Skip to content

Commit a158a24

Browse files
authored
Homescreen search (#11262)
* app / header portion * add search to service * add to projects view * add a searchTerms field for hidden search terms (e.g. platformer for that type of game) * bring all back scoped to projects.tsx for search * make it more of a mode * search 'mode' * prewarm / fix bug with space * include user projects in search results * fix detail view in search * typing annoyance and show description from pxt.json * less tall start typing instructions * remove import from search view * header * still show footer at bottom, and maintain scroll bar so it doesn't pop in / out and rearrange * fix first result card being mildly higher, and add a targetconfig field for additional searchable galleries * react common buttons / links / search * spacing * fix issue with less evaling numbers
1 parent ed81728 commit a158a24

10 files changed

Lines changed: 633 additions & 84 deletions

File tree

localtypings/pxtarget.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ declare namespace pxt {
2424
multiplayer?: MultiplayerConfig;
2525
// common galleries
2626
galleries?: pxt.Map<string | GalleryProps>;
27+
// additional galleries included in projects search but not shown on the homescreen
28+
searchGalleries?: pxt.Map<string | GalleryProps>;
2729
// localized galleries
2830
localizedGalleries?: pxt.Map<pxt.Map<string>>;
2931
windowsStoreLink?: string;

localtypings/pxtpackage.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ declare namespace pxt {
158158
labelIcon?: string;
159159
labelClass?: string;
160160
tags?: string[]; // tags shown in home screen, colors specified in theme
161+
searchTerms?: string[]; // extra terms used to improve home screen search matching
161162
tabIndex?: number;
162163
style?: string; // "card" | "item" | undefined;
163164

pxtcompiler/emitter/service.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,8 @@ namespace ts.pxtc.service {
723723
// don't export, fuse is internal only
724724
let lastFuse: Fuse<SearchInfo>;
725725
let lastProjectFuse: Fuse<ProjectSearchInfo>;
726+
let lastHomeFuse: Fuse<HomeSearchInfo>;
727+
let lastHomeFuseKey: string;
726728
export let builtinItems: SearchInfo[];
727729
export let blockDefinitions: pxt.Map<pxt.blocks.BlockDefinition>;
728730
export let tbSubset: pxt.Map<boolean | string>;
@@ -771,6 +773,23 @@ namespace ts.pxtc.service {
771773
return newOpts
772774
}
773775

776+
export interface HomeSearchInfo {
777+
id: string;
778+
name: string;
779+
description?: string;
780+
tags?: string;
781+
searchTerms?: string;
782+
}
783+
784+
export interface HomeSearchOptions {
785+
term: string;
786+
entries: HomeSearchInfo[];
787+
}
788+
789+
export interface OpArg {
790+
homeSearch?: HomeSearchOptions;
791+
}
792+
774793
export interface ServiceOps {
775794
reset: () => void;
776795
setOptions: (v: OpArg) => void;
@@ -796,6 +815,8 @@ namespace ts.pxtc.service {
796815
apiSearch: (v: OpArg) => SearchInfo[];
797816
projectSearch: (v: OpArg) => ProjectSearchInfo[];
798817
projectSearchClear: () => void;
818+
homeSearch: (v: OpArg) => HomeSearchInfo[];
819+
homeSearchClear: () => void;
799820
};
800821

801822
export type OpRes =
@@ -804,7 +825,7 @@ namespace ts.pxtc.service {
804825
| { words: number[]; }
805826
| KsDiagnostic[]
806827
| { formatted: string; pos: number; }
807-
| ApisInfo | BlocksInfo | ProjectSearchInfo[]
828+
| ApisInfo | BlocksInfo | ProjectSearchInfo[] | HomeSearchInfo[]
808829
| {};
809830

810831
export type OpError = { errorMessage: string };
@@ -1362,6 +1383,38 @@ namespace ts.pxtc.service {
13621383
},
13631384
projectSearchClear: () => {
13641385
lastProjectFuse = undefined;
1386+
},
1387+
homeSearch: v => {
1388+
const search = v.homeSearch;
1389+
const searchSet = search.entries;
1390+
const searchKey = searchSet.map((h: HomeSearchInfo) => h.id).join("\n");
1391+
1392+
if (!lastHomeFuse || lastHomeFuseKey !== searchKey) {
1393+
const fuseOptions = {
1394+
shouldSort: true,
1395+
threshold: 0.4,
1396+
location: 0,
1397+
distance: 1000,
1398+
maxPatternLength: 16,
1399+
minMatchCharLength: 2,
1400+
findAllMatches: false,
1401+
caseSensitive: false,
1402+
keys: [
1403+
{ name: 'name', weight: 0.45 },
1404+
{ name: 'description', weight: 0.35 },
1405+
{ name: 'searchTerms', weight: 0.35 },
1406+
{ name: 'tags', weight: 0.15 },
1407+
]
1408+
};
1409+
lastHomeFuse = new Fuse(searchSet, fuseOptions);
1410+
lastHomeFuseKey = searchKey;
1411+
}
1412+
1413+
return lastHomeFuse.search(search.term);
1414+
},
1415+
homeSearchClear: () => {
1416+
lastHomeFuse = undefined;
1417+
lastHomeFuseKey = undefined;
13651418
}
13661419
}
13671420

pxtlib/service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,7 @@ namespace ts.pxtc.service {
17401740
blocks?: BlocksOptions;
17411741
extensions?: ExtensionsOptions;
17421742
projectSearch?: ProjectSearchOptions;
1743+
homeSearch?: HomeSearchOptions;
17431744
snippet?: SnippetOptions;
17441745
runtime?: pxt.RuntimeOptions;
17451746
light?: boolean; // in light mode?
@@ -1807,6 +1808,19 @@ namespace ts.pxtc.service {
18071808
id?: string;
18081809
}
18091810

1811+
export interface HomeSearchOptions {
1812+
term: string;
1813+
entries: HomeSearchInfo[];
1814+
}
1815+
1816+
export interface HomeSearchInfo {
1817+
id: string;
1818+
name: string;
1819+
description?: string;
1820+
tags?: string;
1821+
searchTerms?: string;
1822+
}
1823+
18101824
export interface BlocksOptions {
18111825
bannedCategories?: string[];
18121826
}

react-common/components/controls/Button.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ButtonViewProps extends ContainerProps {
88
labelClassName?: string;
99
leftIcon?: string;
1010
rightIcon?: string;
11+
autoFocus?: boolean;
1112
disabled?: boolean; // Disables the button in an accessible-friendly way.
1213
hardDisabled?: boolean; // Disables the button and prevents clicks. Not recommended. Use `disabled` instead.
1314
href?: string;
@@ -97,6 +98,7 @@ export function inflateButtonProps(props: ButtonProps) {
9798
href,
9899
target,
99100
tabIndex,
101+
autoFocus,
100102
} = props;
101103

102104
let {
@@ -140,6 +142,7 @@ export function inflateButtonProps(props: ButtonProps) {
140142
"onFocus": onFocus,
141143
"role": role || "button",
142144
"tabIndex": tabIndex || (disabled ? -1 : 0),
145+
"autoFocus": autoFocus,
143146
"disabled": hardDisabled,
144147
"aria-label": ariaLabel,
145148
"aria-hidden": ariaHidden,

react-common/components/controls/Input.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface InputProps extends ControlProps {
1616
disabled?: boolean;
1717
type?: string;
1818
readOnly?: boolean;
19+
autoFocus?: boolean;
1920
autoComplete?: boolean;
2021
selectOnClick?: boolean;
2122
treatSpaceAsEnter?: boolean;
@@ -59,6 +60,7 @@ export const Input = (props: InputProps) => {
5960
disabled,
6061
type,
6162
readOnly,
63+
autoFocus,
6264
autoComplete,
6365
selectOnClick,
6466
onChange,
@@ -217,6 +219,7 @@ export const Input = (props: InputProps) => {
217219
placeholder={placeholder}
218220
value={value}
219221
readOnly={!!readOnly}
222+
autoFocus={autoFocus}
220223
onClick={clickHandler}
221224
onChange={changeHandler}
222225
onKeyDown={keyDownHandler}

theme/home.less

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
.projectsdialog {
3232
position: relative;
3333
height: 100%;
34-
overflow: auto;
34+
overflow-y: scroll;
35+
overflow-x: hidden;
36+
scrollbar-gutter: stable;
3537
z-index: @homeScreenZIndex+1;
3638
.accessibleMenu {
3739
z-index: @homeScreenZIndex+3 !important;
@@ -144,18 +146,97 @@
144146
padding-top: calc(@mainMenuHeight + 2rem) !important;
145147
margin: 0;
146148
border: 0;
149+
display: flex;
150+
flex-direction: column;
151+
box-sizing: border-box;
147152
width: 100%;
148-
min-height: 100%;
153+
min-height: 100vh;
149154
background: var(--pxt-target-background1);
150155
color: var(--pxt-target-foreground1);
151156
}
157+
158+
.button.large {
159+
font-weight: 700;
160+
}
161+
152162
.ui.segment.gallerysegment {
153163
background: none;
154164
border: none;
155165
box-shadow: none;
156166
padding: 0rem;
157167
margin: 0;
158168

169+
&.mystuff-segment {
170+
.gallery-actions button,
171+
&.search-mode .heading button {
172+
padding: .75em;
173+
}
174+
}
175+
176+
&.mystuff-segment.search-mode {
177+
.gallery-heading-column {
178+
padding-left: @carouselArrowSize !important;
179+
}
180+
181+
.heading {
182+
position: relative;
183+
align-items: center;
184+
}
185+
186+
.ui.header.search-mode-title {
187+
position: absolute;
188+
left: 50%;
189+
transform: translateX(-50%);
190+
padding-left: 0;
191+
margin: 0;
192+
text-align: center;
193+
pointer-events: none;
194+
}
195+
196+
.search-input {
197+
height: 3em;
198+
input {
199+
padding: .75em 1em;
200+
}
201+
i {
202+
bottom: 1em;
203+
};
204+
}
205+
}
206+
207+
&.search-segment {
208+
padding: 1rem @carouselArrowSize;
209+
210+
.search-results-grid {
211+
display: grid;
212+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
213+
gap: 1rem;
214+
align-items: start;
215+
grid-auto-flow: dense;
216+
217+
// override default that's bumping it up
218+
> .ui.card:first-child {
219+
margin: 1em 0;
220+
}
221+
}
222+
223+
.search-detailview.detailview {
224+
grid-column-start: 1;
225+
grid-column-end: -1;
226+
min-width: 0;
227+
width: 100%;
228+
}
229+
230+
.search-empty-state {
231+
margin: 0;
232+
width: 100%;
233+
box-sizing: border-box;
234+
display: flex;
235+
align-items: center;
236+
align-self: stretch;
237+
}
238+
}
239+
159240
.ui.header {
160241
margin: 0;
161242
padding-left: @carouselArrowSize;
@@ -192,13 +273,27 @@
192273
margin-bottom: -1rem !important;
193274
margin-top: 1rem !important;
194275
}
276+
.gallery-heading-column,
277+
.gallery-actions {
278+
z-index: 1;
279+
}
280+
.gallery-actions {
281+
display: flex;
282+
justify-content: flex-end;
283+
gap: 0.5rem;
284+
flex-wrap: wrap;
285+
}
195286
.column {
196287
padding: 0 !important;
197288
}
198289
.column.right.aligned {
199290
padding-right: @carouselArrowSize !important;
200291
}
201292
}
293+
.homescreen-search-box {
294+
padding-top: 1em;
295+
margin: 1em @carouselArrowSize;
296+
}
202297
.import-dialog-btn {
203298
position: relative;
204299
z-index: 1; /* Move up so it's above the carousel container that has an offset margin */
@@ -216,8 +311,9 @@
216311
width: 100%;
217312
text-align: center;
218313
padding: 5px !important;
314+
margin-top: auto !important;
219315
z-index: @homeFooterZIndex;
220-
position: absolute;
316+
position: relative;
221317
.item {
222318
font-size: 0.8rem !important;
223319
color: var(--pxt-neutral-foreground1) !important;
@@ -441,6 +537,19 @@
441537
display: inline-block;
442538
width: 100%;
443539
}
540+
.card-action .card-action-button-link.common-link {
541+
text-decoration: none;
542+
}
543+
.card-action .card-action-button-link.common-link:hover,
544+
.card-action .card-action-button-link.common-link:focus,
545+
.card-action .card-action-button-link.common-link:visited {
546+
text-decoration: none;
547+
color: var(--pxt-neutral-foreground3);
548+
}
549+
.card-action .card-action-button-link .ui.text {
550+
display: inline-block;
551+
width: 100%;
552+
}
444553
.card-action .button {
445554
border: none!important;
446555
outline: none!important;
@@ -760,6 +869,17 @@
760869

761870
@media only screen and (max-width: @largestTabletScreen) {
762871
/* Carousel */
872+
.gallerysegment.search-segment {
873+
padding: 1rem @carouselArrowSizeTablet;
874+
}
875+
.ui.segment.gallerysegment.mystuff-segment.search-mode {
876+
.gallery-heading-column {
877+
padding-left: @carouselArrowSizeTablet !important;
878+
}
879+
}
880+
.homescreen-search-box {
881+
margin: 1em @carouselArrowSizeTablet;
882+
}
763883
.projectsdialog {
764884
.ui.segment.getting-started-segment {
765885
background-position: left center;
@@ -833,6 +953,21 @@
833953

834954
@media only screen and (max-width: @largestMobileScreen) {
835955
/* Carousel */
956+
.gallerysegment.search-segment {
957+
padding: 1rem @carouselArrowSizeMobile;
958+
}
959+
.ui.segment.gallerysegment.mystuff-segment.search-mode {
960+
.gallery-heading-column {
961+
padding-left: @carouselArrowSizeMobile !important;
962+
}
963+
964+
.ui.header.search-mode-title {
965+
display: none;
966+
}
967+
}
968+
.homescreen-search-box {
969+
margin: 1em @carouselArrowSizeMobile;
970+
}
836971
.projectsdialog {
837972
.ui.segment.getting-started-segment {
838973
height: 10rem;

0 commit comments

Comments
 (0)