feat(picker): online-only COMPONENT ad cards + overlay datasheet seam
Components that only exist in the hosted editor now advertise themselves in the picker exactly like the online-only boards do: ONLINE_ONLY_COMPONENT_ADS (same module as the board ads) renders an ONLINE-badged card that links to velxio.com, auto-hidden in any build whose ComponentRegistry has the real component (the hosted overlay merges it in — no per-build switches). First entries: the M5Stack Chain RGB/Mono 8x8 matrices. componentDocs gains registerComponentDoc(id, raw) so an overlay can supply the hover datasheet for components it injects at runtime.
This commit is contained in:
parent
c4ef03b6df
commit
9e8381e032
|
|
@ -35,8 +35,10 @@ import { BOARD_KIND_LABELS } from '../types/board';
|
|||
import { isProBoardKind } from '../lib/proBoardGate';
|
||||
import {
|
||||
ONLINE_ONLY_BOARD_ADS,
|
||||
ONLINE_ONLY_COMPONENT_ADS,
|
||||
ONLINE_EDITOR_URL,
|
||||
type OnlineOnlyBoardAd,
|
||||
type OnlineOnlyComponentAd,
|
||||
} from '../lib/onlineOnlyBoards';
|
||||
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
|
||||
import raspberryPi4Png from '../assets/raspberry-pi-4-board.png';
|
||||
|
|
@ -203,6 +205,20 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|||
return components;
|
||||
}, [searchQuery, selectedCategory, registry, isLoading]);
|
||||
|
||||
// Online-only component ads: shown where the real component would sit, and
|
||||
// hidden automatically in any build whose registry has the real component
|
||||
// (the hosted overlay merges it in) — same contract as VISIBLE_BOARD_ADS.
|
||||
const visibleComponentAds = useMemo(() => {
|
||||
if (isLoading) return [];
|
||||
const q = searchQuery.toLowerCase();
|
||||
return ONLINE_ONLY_COMPONENT_ADS.filter(
|
||||
(ad) =>
|
||||
!registry.getById(ad.id) &&
|
||||
(selectedCategory === 'all' || ad.category === selectedCategory) &&
|
||||
(!q || ad.label.toLowerCase().includes(q)),
|
||||
);
|
||||
}, [registry, isLoading, searchQuery, selectedCategory]);
|
||||
|
||||
// Get available categories
|
||||
const categories = useMemo(() => {
|
||||
if (isLoading) return [];
|
||||
|
|
@ -357,7 +373,7 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|||
<div className="spinner"></div>
|
||||
<p>{t('editor.componentPicker.loading')}</p>
|
||||
</div>
|
||||
) : filteredComponents.length === 0 ? (
|
||||
) : filteredComponents.length === 0 && visibleComponentAds.length === 0 ? (
|
||||
<div className="no-results">
|
||||
<p>{t('editor.componentPicker.noResults')}</p>
|
||||
{searchQuery && (
|
||||
|
|
@ -393,6 +409,10 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|||
/>
|
||||
))
|
||||
)}
|
||||
{!isLoading &&
|
||||
visibleComponentAds.map((ad) => (
|
||||
<OnlineOnlyComponentCard key={ad.id} ad={ad} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -764,6 +784,28 @@ const OnlineBadge: React.FC = () => (
|
|||
</span>
|
||||
);
|
||||
|
||||
/** Advertisement card for a component only available in the hosted editor. */
|
||||
const OnlineOnlyComponentCard: React.FC<{ ad: OnlineOnlyComponentAd }> = ({ ad }) => (
|
||||
<button
|
||||
className="component-card"
|
||||
style={{ position: 'relative' }}
|
||||
title={`${ad.label} — available in the online editor at velxio.com`}
|
||||
onClick={() => window.open(ONLINE_EDITOR_URL, '_blank', 'noopener')}
|
||||
>
|
||||
<OnlineBadge />
|
||||
<div className="card-thumbnail">
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
dangerouslySetInnerHTML={{ __html: ad.thumbnailSvg }}
|
||||
/>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="card-name">{ad.label}</div>
|
||||
<div className="card-description">{ad.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
/** Advertisement card for a board only available in the hosted editor. */
|
||||
const OnlineOnlyBoardCard: React.FC<{ ad: OnlineOnlyBoardAd }> = ({ ad }) => (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@ for (const path in modules) {
|
|||
|
||||
const cache = new Map<string, ComponentDoc | null>();
|
||||
|
||||
/**
|
||||
* Overlay seam: a private build (velxio.com) can register the datasheet
|
||||
* markdown for a component it injects at runtime — the import.meta.glob above
|
||||
* only sees files committed in this tree. Same raw format as the .md files
|
||||
* (optional front-matter + body). Last write wins; the parsed cache for the
|
||||
* id is invalidated so a re-render picks the new doc up.
|
||||
*/
|
||||
export function registerComponentDoc(id: string, raw: string): void {
|
||||
byId[id] = () => Promise.resolve(raw);
|
||||
cache.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split an optional `---`-delimited front-matter block off the top of a
|
||||
* Markdown string. Only a leading block counts, so table separators
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue