diff --git a/packages/boxel-ui/addon/src/components.ts b/packages/boxel-ui/addon/src/components.ts index f3e343eec82..d23ec18c805 100644 --- a/packages/boxel-ui/addon/src/components.ts +++ b/packages/boxel-ui/addon/src/components.ts @@ -26,6 +26,7 @@ import EntityDisplayWithIcon from './components/entity-icon-display/index.gts'; import EntityDisplayWithThumbnail from './components/entity-thumbnail-display/index.gts'; import FieldContainer from './components/field-container/index.gts'; import FilterList, { type Filter } from './components/filter-list/index.gts'; +import FittedCardContainer from './components/fitted-card-container/index.gts'; import GridContainer from './components/grid-container/index.gts'; import BoxelHeader from './components/header/index.gts'; import Header from './components/header/index.gts'; @@ -115,6 +116,7 @@ export { EntityDisplayWithThumbnail, FieldContainer, FilterList, + FittedCardContainer, GridContainer, Header, IconButton, diff --git a/packages/boxel-ui/addon/src/components/basic-fitted/usage.gts b/packages/boxel-ui/addon/src/components/basic-fitted/usage.gts index 1933062ee52..8bea15cc159 100644 --- a/packages/boxel-ui/addon/src/components/basic-fitted/usage.gts +++ b/packages/boxel-ui/addon/src/components/basic-fitted/usage.gts @@ -5,134 +5,18 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import FreestyleUsage from 'ember-freestyle/components/freestyle/usage'; -import { cn, gt, gte } from '../../helpers.ts'; +import { + cn, + gt, + gte, + FITTED_FORMATS, + type FittedFormatSpec, +} from '../../helpers.ts'; import type { Icon } from '../../icons.ts'; import CardContainer from '../card-container/index.gts'; import BasicFitted from './index.gts'; -type Spec = { height: number; title?: string; width: number }; - -// These can be imported from the @cardstack/runtime-common package: -// `import { FITTED_FORMATS } from '@cardstack/runtime-common'` -// For various build problems, could not do that here. -const FITTED_FORMATS = [ - { - name: 'Badges', - specs: [ - { - id: 'small-badge', - title: 'Small Badge', - width: 150, - height: 40, - }, - { - id: 'medium-badge', - title: 'Medium Badge', - width: 150, - height: 65, - }, - { - id: 'large-badge', - title: 'Large Badge', - width: 150, - height: 105, - }, - ], - }, - { - name: 'Strips', - specs: [ - { - id: 'single-strip', - title: 'Single Strip', - width: 250, - height: 40, - }, - { - id: 'double-strip', - title: 'Double Strip', - width: 250, - height: 65, - }, - { - id: 'triple-strip', - title: 'Triple Strip', - width: 250, - height: 105, - }, - { - id: 'double-wide-strip', - title: 'Double Wide Strip', - width: 400, - height: 65, - }, - { - id: 'triple-wide-strip', - title: 'Triple Wide Strip', - width: 400, - height: 105, - }, - ], - }, - { - name: 'Tiles', - specs: [ - { - id: 'small-tile', - title: 'Small Tile', - width: 150, - height: 170, - }, - { - id: 'regular-tile', - title: 'Regular Tile', - width: 250, - height: 170, - }, - { - id: 'cardsgrid-tile', - title: 'CardsGrid Tile', - width: 170, - height: 250, - }, - { - id: 'tall-tile', - title: 'Tall Tile', - width: 150, - height: 275, - }, - { - id: 'large-tile', - title: 'Large Tile', - width: 250, - height: 275, - }, - ], - }, - { - name: 'Cards', - specs: [ - { - id: 'compact-card', - title: 'Compact Card', - width: 400, - height: 170, - }, - { - id: 'full-card', - title: 'Full Card', - width: 400, - height: 275, - }, - { - id: 'expanded-card', - title: 'Expanded Card', - width: 400, - height: 445, - }, - ], - }, -]; +type Spec = Partial & { width: number; height: number }; const OTHER_SIZES: Spec[] = [ { width: 226, height: 226 }, diff --git a/packages/boxel-ui/addon/src/components/card-container/index.gts b/packages/boxel-ui/addon/src/components/card-container/index.gts index 8d5d6f4138e..57545f4ca5d 100644 --- a/packages/boxel-ui/addon/src/components/card-container/index.gts +++ b/packages/boxel-ui/addon/src/components/card-container/index.gts @@ -58,12 +58,9 @@ const CardContainer: TemplateOnlyComponent = ; -export default GridContainer; + return fittedFormatById.get(size) ?? null; + } + + get containerStyle() { + let formatSpec = this.formatSpec; + + if (!formatSpec) { + return sanitizeHtmlSafe(''); + } + + if (this.args.items) { + if (this.args.viewFormat === 'list') { + return sanitizeHtmlSafe('grid-template-columns: 1fr;'); + } + return sanitizeHtmlSafe( + `grid-template-columns: repeat(auto-fill, ${formatSpec.width}px);`, + ); + } else { + if (this.args.viewFormat === 'list') { + return sanitizeHtmlSafe( + `grid-template-columns: 1fr; grid-auto-rows: ${formatSpec.height}px`, + ); + } + return sanitizeHtmlSafe( + `grid-template-columns: repeat(auto-fill, ${formatSpec.width}px); grid-auto-rows: ${formatSpec.height}px`, + ); + } + } +} diff --git a/packages/boxel-ui/addon/src/helpers.ts b/packages/boxel-ui/addon/src/helpers.ts index b63211d3813..ac355f81ef4 100644 --- a/packages/boxel-ui/addon/src/helpers.ts +++ b/packages/boxel-ui/addon/src/helpers.ts @@ -63,6 +63,8 @@ import type { NormalizePhoneFormatResult } from './helpers/validate-phone-format export * from './helpers/color-tools.ts'; +export * from './utils/fitted-formats.ts'; + export { add, addClassToSVG, diff --git a/packages/boxel-ui/addon/src/usage.ts b/packages/boxel-ui/addon/src/usage.ts index 0d2f7453846..460952182a8 100644 --- a/packages/boxel-ui/addon/src/usage.ts +++ b/packages/boxel-ui/addon/src/usage.ts @@ -24,6 +24,7 @@ import EntityIconDisplayUsage from './components/entity-icon-display/usage.gts'; import EntityThumbnailDisplayUsage from './components/entity-thumbnail-display/usage.gts'; import FieldContainerUsage from './components/field-container/usage.gts'; import FilterListUsage from './components/filter-list/usage.gts'; +import FittedCardContainerUsage from './components/fitted-card-container/usage.gts'; import GridContainerUsage from './components/grid-container/usage.gts'; import HeaderUsage from './components/header/usage.gts'; import IconButtonUsage from './components/icon-button/usage.gts'; @@ -77,6 +78,7 @@ export const ALL_USAGE_COMPONENTS = [ ['EntityThumbnailDisplay', EntityThumbnailDisplayUsage], ['FieldContainer', FieldContainerUsage], ['FilterList', FilterListUsage], + ['FittedCardContainer', FittedCardContainerUsage], ['GridContainer', GridContainerUsage], ['Header', HeaderUsage], ['IconButton', IconButtonUsage], diff --git a/packages/boxel-ui/addon/src/utils/fitted-formats.ts b/packages/boxel-ui/addon/src/utils/fitted-formats.ts new file mode 100644 index 00000000000..5112e7fd010 --- /dev/null +++ b/packages/boxel-ui/addon/src/utils/fitted-formats.ts @@ -0,0 +1,161 @@ +export type FittedFormatId = + | 'cardsgrid-tile' + | 'compact-card' + | 'double-strip' + | 'double-wide-strip' + | 'expanded-card' + | 'full-card' + | 'large-badge' + | 'large-tile' + | 'medium-badge' + | 'regular-tile' + | 'single-strip' + | 'small-badge' + | 'small-tile' + | 'tall-tile' + | 'triple-strip' + | 'triple-wide-strip'; + +export type FittedFormatSpec = { + id: FittedFormatId; + title: string; + width: number; + height: number; +}; + +type FittedFormatGallery = ReadonlyArray<{ + name: string; + specs: FittedFormatSpec[]; +}>; + +export const FITTED_FORMATS: FittedFormatGallery = [ + { + name: 'Badges', + specs: [ + { + id: 'small-badge', + title: 'Small Badge', + width: 150, + height: 40, + }, + { + id: 'medium-badge', + title: 'Medium Badge', + width: 150, + height: 65, + }, + { + id: 'large-badge', + title: 'Large Badge', + width: 150, + height: 105, + }, + ], + }, + { + name: 'Strips', + specs: [ + { + id: 'single-strip', + title: 'Single Strip', + width: 250, + height: 40, + }, + { + id: 'double-strip', + title: 'Double Strip', + width: 250, + height: 65, + }, + { + id: 'triple-strip', + title: 'Triple Strip', + width: 250, + height: 105, + }, + { + id: 'double-wide-strip', + title: 'Double Wide Strip', + width: 400, + height: 65, + }, + { + id: 'triple-wide-strip', + title: 'Triple Wide Strip', + width: 400, + height: 105, + }, + ], + }, + { + name: 'Tiles', + specs: [ + { + id: 'small-tile', + title: 'Small Tile', + width: 150, + height: 170, + }, + { + id: 'regular-tile', + title: 'Regular Tile', + width: 250, + height: 170, + }, + { + id: 'cardsgrid-tile', + title: 'CardsGrid Tile', + width: 170, + height: 250, + }, + { + id: 'tall-tile', + title: 'Tall Tile', + width: 150, + height: 275, + }, + { + id: 'large-tile', + title: 'Large Tile', + width: 250, + height: 275, + }, + ], + }, + { + name: 'Cards', + specs: [ + { + id: 'compact-card', + title: 'Compact Card', + width: 400, + height: 170, + }, + { + id: 'full-card', + title: 'Full Card', + width: 400, + height: 275, + }, + { + id: 'expanded-card', + title: 'Expanded Card', + width: 400, + height: 445, + }, + ], + }, +]; + +export const FITTED_FORMAT_SIZES = FITTED_FORMATS.flatMap( + (group) => group.specs, +); + +export const fittedFormatIds = FITTED_FORMAT_SIZES.flatMap( + (formatSpec) => formatSpec.id, +); + +export const fittedFormatById: ReadonlyMap = + new Map( + FITTED_FORMAT_SIZES.map((format) => [format.id, format]), + ); diff --git a/packages/host/app/components/card-catalog/modal.gts b/packages/host/app/components/card-catalog/modal.gts index 9c967dbf588..39a0dd39b19 100644 --- a/packages/host/app/components/card-catalog/modal.gts +++ b/packages/host/app/components/card-catalog/modal.gts @@ -120,7 +120,6 @@ export default class CardCatalogModal extends Component { @selectedCard={{this.state.selectedCard}} @baseFilter={{this.state.baseFilter}} @offerToCreate={{this.offerToCreateArg}} - @showRecents={{false}} /> <:footer> diff --git a/packages/host/app/components/card-search/item-button.gts b/packages/host/app/components/card-search/item-button.gts index f8881e46ba0..c3ec1e2ba50 100644 --- a/packages/host/app/components/card-search/item-button.gts +++ b/packages/host/app/components/card-search/item-button.gts @@ -9,7 +9,6 @@ import { IconPlus } from '@cardstack/boxel-ui/icons'; import { isCardInstance } from '@cardstack/runtime-common'; -import { urlForRealmLookup } from '@cardstack/host/lib/utils'; import type RealmService from '@cardstack/host/services/realm'; import type { CardDef } from 'https://cardstack.com/base/card-api'; @@ -29,8 +28,6 @@ interface Signature { item: ItemType; itemId?: string; isSelected: boolean; - isCompact: boolean; - displayRealmName?: boolean; onSelect: (selection: string | NewCardArgs) => void; onSubmit?: (selection: string | NewCardArgs) => void; }; @@ -96,13 +93,6 @@ export default class ItemButton extends Component { return this.args.itemId ?? this.cardItem?.id; } - private get urlForRealmLookup(): string | undefined { - if (!this.args.displayRealmName) { - return undefined; - } - return this.cardItem ? urlForRealmLookup(this.cardItem) : this.args.itemId; - } - @action handleClick() { this.args.onSelect(this.selectPayload); } @@ -120,16 +110,22 @@ export default class ItemButton extends Component { } } diff --git a/packages/host/app/components/card-search/search-content.gts b/packages/host/app/components/card-search/search-content.gts index f91d4f1e66e..1a316ec5f3b 100644 --- a/packages/host/app/components/card-search/search-content.gts +++ b/packages/host/app/components/card-search/search-content.gts @@ -96,7 +96,6 @@ interface Signature { relativeTo: URL | undefined; }; onSubmit?: (selection: string | NewCardArgs) => void; - showRecents?: boolean; showHeader?: boolean; }; Blocks: {}; @@ -157,10 +156,6 @@ export default class SearchContent extends Component { return this.isSearchKeyEmpty || this.searchKeyIsURL; } - private get showRecents() { - return this.args.showRecents !== false; - } - private get showHeader() { return this.args.showHeader !== false; } @@ -291,10 +286,7 @@ export default class SearchContent extends Component { } // Recents view (empty search or focused on recents) - if ( - (this.focusedSection === 'recents' || this.isSearchKeyEmpty) && - this.showRecents - ) { + if (this.focusedSection === 'recents' || this.isSearchKeyEmpty) { const count = this.recentCardsSection?.totalCount ?? 0; return `${count} in ${pluralize('Recent', count)}`; } @@ -326,6 +318,9 @@ export default class SearchContent extends Component { if (!cards) { return []; } + if (this.args.isCompact) { + return cards; + } let filtered = cards; const term = this.searchTerm; if (term) { @@ -529,6 +524,11 @@ export default class SearchContent extends Component { private get sections(): SearchSheetSection[] { const sections: SearchSheetSection[] = []; + // Add recents section if present + if (this.recentCardsSection) { + sections.push(this.recentCardsSection); + } + // Add URL section if present if (this.cardByUrlSection) { sections.push(this.cardByUrlSection); @@ -539,11 +539,6 @@ export default class SearchContent extends Component { sections.push(...this.cardsByQuerySection); } - // Add recents section if enabled - if (this.showRecents && this.recentCardsSection) { - sections.push(this.recentCardsSection); - } - return sections; } @@ -621,24 +616,34 @@ export default class SearchContent extends Component { {{/if}} {{/if}} - {{! Render all sections }} - {{#each this.sections as |section i|}} - - {{/each}} + {{#if @isCompact}} + {{#if this.recentCardsSection}} + + {{/if}} + {{else}} + {{! Render all sections }} + {{#each this.sections as |section i|}} + + {{/each}} + {{/if}} {{#if this.hasNoResults}}
diff --git a/packages/host/app/components/card-search/search-result-section.gts b/packages/host/app/components/card-search/search-result-section.gts index b72a5394933..feddb95a8df 100644 --- a/packages/host/app/components/card-search/search-result-section.gts +++ b/packages/host/app/components/card-search/search-result-section.gts @@ -7,11 +7,12 @@ import Component from '@glimmer/component'; import HistoryIcon from '@cardstack/boxel-icons/history'; import pluralize from 'pluralize'; -import { Button } from '@cardstack/boxel-ui/components'; -import { eq } from '@cardstack/boxel-ui/helpers'; +import { Button, GridContainer } from '@cardstack/boxel-ui/components'; +import { cn, eq, type FittedFormatId } from '@cardstack/boxel-ui/helpers'; import type { CodeRef } from '@cardstack/runtime-common'; +import { urlForRealmLookup } from '@cardstack/host/lib/utils'; import type RealmService from '@cardstack/host/services/realm'; import { SECTION_SHOW_MORE_INCREMENT } from './constants'; @@ -30,12 +31,12 @@ interface Signature { Element: HTMLElement; Args: { section: SearchSheetSection; - viewOption: string; - isCompact: boolean; + viewOption?: string; + isCompact?: boolean; handleSelect: (selection: string | NewCardArgs) => void; - isFocused: boolean; - isCollapsed: boolean; - onFocusSection: (sectionId: string | null) => void; + isFocused?: boolean; + isCollapsed?: boolean; + onFocusSection?: (sectionId: string | null) => void; getDisplayedCount?: (sectionId: string, totalCount: number) => number; onShowMore?: (sectionId: string, totalCount: number) => void; selectedCard?: string | NewCardArgs; @@ -110,6 +111,10 @@ export default class SearchResultSection extends Component { get displayedRecentsCards() { const section = this.recentsSection; if (!section) return []; + if (this.args.isCompact) { + // do not limit the cards in the quick menu and keep last-updated sort order + return section.cards; + } const sid = this.args.section.sid; const getDisplayedCount = this.args.getDisplayedCount; if (!sid || !getDisplayedCount) return section.cards; @@ -153,10 +158,13 @@ export default class SearchResultSection extends Component { this.urlSection) ) { return 'grid-view'; - } else if (this.args.viewOption === 'strip') { + } else { return 'strip-view'; } - return ''; + } + + get viewFormat() { + return this.viewClass === 'grid-view' ? 'grid' : 'list'; } get displayShowMore() { @@ -194,29 +202,44 @@ export default class SearchResultSection extends Component { }; }; + private get cardSize(): FittedFormatId { + if (this.viewClass === 'compact-view') { + return 'single-strip'; + } else if (this.viewClass === 'strip-view') { + return 'double-wide-strip'; + } else { + return 'cardsgrid-tile'; + } + } +