Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions samples/order.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Curated display order for the /samples hub.
#
# Each entry is a sample id — the file name without its .mdx extension
# (e.g. "verity" for samples/verity.mdx). Samples are rendered top-to-bottom
# in this order. Any sample not listed here is shown afterwards, sorted
# alphabetically by title. Reorder freely; this is the single source of truth
# for the gallery order.
- verity
- human-resources-assistant
- fit-assistant
- the-ravens-library
- brain-slop
- hugin
- yabt
38 changes: 3 additions & 35 deletions src/components/Common/CardWithImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import Badge from "@site/src/components/Common/Badge";
import LazyImage from "@site/src/components/Common/LazyImage";
import isInternalUrl from "@docusaurus/isInternalUrl";
import clsx from "clsx";
import { useTagLimit } from "@site/src/hooks/useTagLimit";
import Tag from "@site/src/theme/Tag";
import OverflowTagRow from "@site/src/components/Common/OverflowTagRow";

export interface CardWithImageProps {
title: string;
Expand Down Expand Up @@ -39,10 +38,6 @@ export default function CardWithImage({
const hasTags = tags.length > 0;
const hasDate = date !== undefined;

const { visibleTags, hiddenCount, isExpanded, expandTags } = useTagLimit({
tags,
});

return (
<article className="card-wrapper group relative">
<div
Expand Down Expand Up @@ -100,35 +95,8 @@ export default function CardWithImage({
<p className="!mb-0 text-sm pt-2">{description}</p>
<div className="flex-grow" />
{(hasTags || hasDate) && (
<div className="flex flex-wrap flex-col 2xl:flex-row xl:flex-nowrap justify-between pt-2 gap-3 z-2 relative">
{hasTags && (
<div className="flex gap-1 items-center flex-wrap">
{visibleTags.map((tag) => (
<Tag
key={tag.label}
size="xs"
permalink={tag.permalink}
className="pointer-events-auto"
>
{tag.label}
</Tag>
))}
{!isExpanded && hiddenCount > 0 && (
<Tag
size="xs"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
expandTags();
}}
title="Show all tags"
className="pointer-events-auto"
>
+{hiddenCount} more
</Tag>
)}
</div>
)}
<div className="flex flex-col 2xl:flex-row 2xl:items-center justify-between pt-2 gap-x-3 gap-y-1 z-2 relative">
{hasTags && <OverflowTagRow tags={tags} className="min-w-0 2xl:flex-1" />}
{hasDate && <p className="!mb-0 text-xs flex-shrink-0 leading-[20px]">{date}</p>}
</div>
)}
Expand Down
168 changes: 168 additions & 0 deletions src/components/Common/OverflowTagRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import clsx from "clsx";
import Tag from "@site/src/theme/Tag";

const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;

export interface OverflowTagItem {
label: string;
key?: string;
permalink?: string;
category?: string;
}

interface OverflowTagRowProps {
tags: OverflowTagItem[];
onTagClick?: (e: React.MouseEvent, tag: OverflowTagItem) => void;
isTagSelected?: (tag: OverflowTagItem) => boolean;
className?: string;
}

const GAP_PX = 4;

const tagKey = (tag: OverflowTagItem) => tag.key ?? tag.permalink ?? tag.label;

export default function OverflowTagRow({ tags, onTagClick, isTagSelected, className }: OverflowTagRowProps) {
const measureRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(tags.length);
const [expanded, setExpanded] = useState(false);

const recompute = useCallback(() => {
const el = measureRef.current;
if (!el) {
return;
}

const containerWidth = el.clientWidth;
if (containerWidth === 0) {
return;
}

const nodes = Array.from(el.children) as HTMLElement[];
const pill = nodes[nodes.length - 1];
const tagNodes = nodes.slice(0, tags.length);
const widths = tagNodes.map((node) => node.offsetWidth);

const totalWithGaps = widths.reduce((sum, w, i) => sum + w + (i > 0 ? GAP_PX : 0), 0);

if (totalWithGaps <= containerWidth) {
setVisibleCount(tags.length);
return;
}

const available = containerWidth - pill.offsetWidth - GAP_PX;
let used = 0;
let count = 0;
for (let i = 0; i < widths.length; i++) {
const next = widths[i] + (count > 0 ? GAP_PX : 0);
if (used + next <= available) {
used += next;
count += 1;
} else {
break;
}
}

setVisibleCount(Math.max(count, 1));
}, [tags.length]);

useIsomorphicLayoutEffect(() => {
const el = measureRef.current;
if (!el) {
return undefined;
}

let cancelled = false;
const run = () => {
if (!cancelled) {
recompute();
}
};

// The mirror's box width tracks the container, but its contents reflow
// when fonts finish loading after hydration — and that reflow doesn't
// resize the mirror, so recompute now, next frame, and once fonts settle.
const observer = new ResizeObserver(run);
observer.observe(el);
run();
const raf = requestAnimationFrame(run);
if (typeof document !== "undefined" && document.fonts?.ready) {
document.fonts.ready.then(run).catch(() => {});
}

return () => {
cancelled = true;
cancelAnimationFrame(raf);
observer.disconnect();
};
}, [recompute, tags]);

useEffect(() => {
setExpanded(false);
}, [tags]);

const renderTag = (tag: OverflowTagItem) => (
<Tag
key={tagKey(tag)}
size="xs"
permalink={tag.permalink}
onClick={onTagClick ? (e) => onTagClick(e, tag) : undefined}
className={clsx(
"shrink-0 whitespace-nowrap pointer-events-auto",
onTagClick && "cursor-pointer",
isTagSelected && !isTagSelected(tag) && "opacity-50"
)}
>
{tag.label}
</Tag>
);

const hiddenCount = tags.length - visibleCount;
const visibleTags = expanded ? tags : tags.slice(0, visibleCount);
const hiddenTags = tags.slice(visibleCount);

return (
<div className={clsx("relative", className)}>
{/* Off-flow mirror used only for measurement: every tag + a worst-case pill. */}
<div
ref={measureRef}
aria-hidden="true"
className="invisible pointer-events-none absolute inset-x-0 top-0 flex flex-nowrap gap-1 overflow-hidden"
>
{tags.map((tag) => (
<Tag key={tagKey(tag)} size="xs" className="shrink-0 whitespace-nowrap">
{tag.label}
</Tag>
))}
<Tag size="xs" className="shrink-0 whitespace-nowrap">
+{tags.length} more
</Tag>
</div>

<div className={clsx("flex gap-1", expanded ? "flex-wrap" : "flex-nowrap overflow-hidden")}>
{visibleTags.map(renderTag)}

{!expanded && hiddenCount > 0 && (
<button
type="button"
title={hiddenTags.map((t) => t.label).join(", ")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setExpanded(true);
}}
className={clsx(
"relative z-10 inline-flex items-center shrink-0 select-none cursor-pointer rounded-full border pointer-events-auto",
"h-5 px-2 text-[11px] font-medium leading-4 whitespace-nowrap",
"bg-black/5 dark:bg-white/5 border-black/10 dark:border-white/10",
"text-black/60 dark:text-white/60",
"hover:bg-black/10 dark:hover:bg-white/10 hover:!no-underline"
)}
>
+{hiddenCount} more
</button>
)}
</div>
</div>
);
}
56 changes: 23 additions & 33 deletions src/components/Samples/Hub/Partials/SampleCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Link from "@docusaurus/Link";
import Heading from "@theme/Heading";
import LazyImage from "@site/src/components/Common/LazyImage";
import clsx from "clsx";
import Tag from "@site/src/theme/Tag";
import LanguageTag from "@site/src/components/Samples/Hub/Partials/LanguageTag";
import OverflowTagRow from "@site/src/components/Common/OverflowTagRow";

interface TagWithCategory {
label: string;
Expand Down Expand Up @@ -56,6 +56,16 @@ export default function SampleCard({
return selectedTags.has(tag.key);
};

const selectedFirst = (list: TagWithCategory[]) => {
if (!selectedTags || selectedTags.size === 0) {
return list;
}
return [...list].sort((a, b) => Number(selectedTags.has(b.key)) - Number(selectedTags.has(a.key)));
};

const orderedChallengesSolutionsTags = selectedFirst(challengesSolutionsTags);
const orderedFeatureTags = selectedFirst(featureTags);

return (
<div
className={clsx(
Expand Down Expand Up @@ -117,45 +127,25 @@ export default function SampleCard({
<p className="!mb-0 text-sm pt-2 flex-grow">{description}</p>

<div className="flex flex-col gap-2 z-2 relative">
{challengesSolutionsTags.length > 0 && (
{orderedChallengesSolutionsTags.length > 0 && (
<div>
<span className="text-xs">Challenges & Solutions</span>
<div className="flex flex-wrap gap-1">
{challengesSolutionsTags.map((tag) => (
<Tag
key={tag.label}
size="xs"
onClick={onTagClick ? (e) => handleTagClick(e, tag) : undefined}
className={clsx(
onTagClick && "cursor-pointer",
!isTagSelected(tag) && "opacity-50"
)}
>
{tag.label}
</Tag>
))}
</div>
<OverflowTagRow
tags={orderedChallengesSolutionsTags}
onTagClick={onTagClick ? handleTagClick : undefined}
isTagSelected={isTagSelected}
/>
</div>
)}

{featureTags.length > 0 && (
{orderedFeatureTags.length > 0 && (
<div>
<span className="text-xs">Features</span>
<div className="flex flex-wrap gap-1">
{featureTags.map((tag) => (
<Tag
key={tag.label}
size="xs"
onClick={onTagClick ? (e) => handleTagClick(e, tag) : undefined}
className={clsx(
onTagClick && "cursor-pointer",
!isTagSelected(tag) && "opacity-50"
)}
>
{tag.label}
</Tag>
))}
</div>
<OverflowTagRow
tags={orderedFeatureTags}
onTagClick={onTagClick ? handleTagClick : undefined}
isTagSelected={isTagSelected}
/>
</div>
)}
</div>
Expand Down
22 changes: 22 additions & 0 deletions src/plugins/recent-samples-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,28 @@ export default function recentSamplesPlugin(context, _options): Plugin {
};
});

let orderList: string[] = [];
const orderFilePath = path.join(samplesDir, "order.yml");
if (fs.existsSync(orderFilePath)) {
try {
const loaded = yaml.load(fs.readFileSync(orderFilePath, "utf8"));
if (Array.isArray(loaded)) {
orderList = loaded.map((id) => String(id));
}
} catch (e) {
console.error("Failed to load samples/order.yml", e);
}
}

const orderIndex = new Map<string, number>();
orderList.forEach((id, index) => orderIndex.set(id, index));
const rankOf = (id: string) => (orderIndex.has(id) ? orderIndex.get(id)! : Number.MAX_SAFE_INTEGER);

samples.sort((a, b) => {
const rankDiff = rankOf(a.id) - rankOf(b.id);
return rankDiff !== 0 ? rankDiff : (a.title || "").localeCompare(b.title || "");
});

const allTags: Array<{
label: string;
key: string;
Expand Down
Loading