mirror of
https://github.com/cosscom/coss.git
synced 2026-09-14 20:06:50 +08:00
682caddcb2
* feat(ui): add 19 new input/group/field particles - p-input-13: Input with label and required indicator - p-input-14: Input with optional label - p-input-15: Input with custom border and background - p-field-19: Field with error state - p-group-16: Group with copy button and input - p-group-17: Group with input and currency text - p-group-18: Group with select and input - p-group-19: Group with input and select - p-group-20: Group with input and text button - p-group-21: Group with separate input and button - p-group-22: Group with two inputs for range - p-input-group-20: Input group with search icon - p-input-group-21: Input group with start tooltip - p-input-group-22: Input group with clear button - p-input-group-23: Search input group with loader and voice button - p-input-group-24: Input group with character counter - p-input-group-25: Input with characters remaining counter - p-input-group-26: Password input with strength indicator - p-button-40: Read-only disabled button Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): use match prop instead of forceShow on FieldError Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): address feedback on particles - p-group-16: replace copy button with add button (real use-case) - p-field-19: use match={true} to force show FieldError - p-group-18, p-group-19: add min-w-none to reduce select trigger width - p-input-7: refactor to use Group with div wrapper around button - p-input-14: add w-full and inline-flex to wrapper div - p-input-15: add before:hidden to hide inset shadow - p-input-group-24, p-input-group-25: set chars limit to 14 - p-input-16: new particle with loader on the right (variant of p-input-12) - Remove p-group-21 (duplicate of refactored p-input-7) Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): address additional feedback on particles - p-group-17: use GroupText with Label render prop - p-input-group-25: use Field and FieldDescription instead of manual markup Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): p-field-19 use typeMismatch for proper error behavior - Use match='typeMismatch' instead of match={true} so error clears when valid email is entered - Change defaultValue to 'invalid-email' (no @) to trigger typeMismatch initially - Remove aria-invalid since Field handles this automatically Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): remove div wrapper around button in p-group-16 Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): p-group-22 use NumberField instead of Input Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): p-field-19 show error on page load and clear on valid input - Use controlled state with invalid prop to show error initially - Update isInvalid state on change based on input validity - Use validationMode='onChange' for immediate validation feedback Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): p-input-14 use Label with render prop for Optional text Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): remove p-field-19 particle Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): remove labels from p-input-group-24 and p-input-group-25 Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * refactor(ui): reorganize registry-particles to show basic inputs first Move autocomplete and combobox sections to come after p-input-* and p-input-group-* sections. This ensures that when filtering by 'input' tag, basic input components appear first, followed by input groups, and then more complex patterns like autocomplete and combobox. Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * refactor(ui): move field/group/form particles with input tag after p-input-group-* Move specific particles that have 'input' as a secondary tag to come after the p-input-* and p-input-group-* sections: - p-field-6, p-field-7, p-field-8, p-field-9, p-field-17 - p-form-1 - p-group-2, p-group-16, p-group-17, p-group-18, p-group-19, p-group-20 This ensures that when filtering by 'input' tag, basic input components appear first, followed by input groups, then field/group compositions, and finally autocomplete/combobox patterns. Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * refactor(ui): reorder registry - field/form particles after combobox Move field and form particles with 'input' tag to come after combobox section. New order for input-tagged particles: 1. p-input-* 2. p-input-group-* 3. p-group-* 4. p-autocomplete-* 5. p-combobox-* 6. p-field-* (with input tag) 7. p-form-* (at the bottom) Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * revert: restore registry to state before reorganization Reverts the registry reorganization commits to restore the original ordering. The reorganization approach needs to be reconsidered. Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * chore: rebuild registry after restore Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * feat(ui): add weighted relevance sorting for particles page Implement smart sorting for particles based on relevance to search terms: - Name prefix match (+30): p-input-* ranks highest for 'input' search - registryDependency match (+20): components using @coss/input get boosted - Primary category match (+10): first category match gets bonus This ensures basic inputs appear first when searching 'input', followed by input groups, then autocomplete/combobox patterns. Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * refactor(ui): unify categories and use single tag icon - Merge componentCategories and tagCategories into single registryCategories array - Remove isComponentCategory and isTagCategory helper functions - Use Label icon for all categories in search UI (no visual distinction) - Simplify types by removing isComponent property Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): exclude field/fieldset from sorting weight + add input to number fields - Exclude 'field' and 'fieldset' from primary category weight bonus to avoid visually separating similar components when searching - Add 'input' category to all number field particles (p-number-field-1 to 10) so they appear when filtering by 'input' Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * chore: rebuild registry with updated number field categories Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * fix(ui): replace p-button-40 with p-input-17 read-only input User wanted a read-only input, not a disabled button. Created p-input-17 with readOnly prop and read-only:bg-muted styling. Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * chore: update registry.json with p-input-17 Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * revert: remove field/fieldset exclusion from weight calculation Reverts the EXCLUDED_PRIMARY_CATEGORIES logic that was preventing field and fieldset from contributing to the primary category weight bonus. Co-Authored-By: pasquale@cal.com <pasquale@cal.com> * mc: remove label from p-input-17 * improve group component to allow number field * mc --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: pasquale@cal.com <pasquale@cal.com> Co-authored-by: pasqualevitiello <pasqualevitiello@gmail.com>
122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
import { cache, Suspense } from "react";
|
|
|
|
import { Index } from "@/registry/__index__";
|
|
import { Skeleton } from "@/registry/default/ui/skeleton";
|
|
import type { RegistryCategory } from "@/registry/registry-categories";
|
|
|
|
import { ParticleCard } from "./particle-card";
|
|
import { ParticleCardContainer } from "./particle-card-container";
|
|
|
|
type Particle = {
|
|
name: string;
|
|
categories?: RegistryCategory[];
|
|
registryDependencies?: string[];
|
|
meta?: { className?: string; colSpan?: number };
|
|
};
|
|
|
|
function calculateRelevanceWeight(
|
|
particle: Particle,
|
|
searchTerms: RegistryCategory[],
|
|
): number {
|
|
let weight = 0;
|
|
|
|
for (const term of searchTerms) {
|
|
const normalizedTerm = term.replace(/\s+/g, "-");
|
|
|
|
if (particle.name.startsWith(`p-${normalizedTerm}-`)) {
|
|
weight += 30;
|
|
}
|
|
|
|
const deps = particle.registryDependencies ?? [];
|
|
if (deps.some((dep) => dep === `@coss/${normalizedTerm}`)) {
|
|
weight += 20;
|
|
}
|
|
|
|
const categories = particle.categories ?? [];
|
|
if (categories[0] === term) {
|
|
weight += 10;
|
|
}
|
|
}
|
|
|
|
return weight;
|
|
}
|
|
|
|
function ParticleCardSkeleton({ className }: { className?: string }) {
|
|
return (
|
|
<ParticleCardContainer
|
|
className={className}
|
|
footer={
|
|
<>
|
|
<div className="flex flex-1 gap-1">
|
|
<Skeleton className="size-4 shrink-0" />
|
|
<Skeleton className="h-4 w-32" />
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<Skeleton className="size-7" />
|
|
<Skeleton className="h-7 w-20" />
|
|
</div>
|
|
</>
|
|
}
|
|
>
|
|
<Skeleton className="h-7 w-64" />
|
|
</ParticleCardContainer>
|
|
);
|
|
}
|
|
|
|
// Cache the particles array to avoid recomputing on every render
|
|
const getParticles = cache(() => {
|
|
return Object.values(Index).filter(
|
|
(item) => item.type === "registry:block",
|
|
) as Particle[];
|
|
});
|
|
|
|
export async function ParticlesDisplay({
|
|
selectedCategories,
|
|
}: {
|
|
selectedCategories: RegistryCategory[];
|
|
}) {
|
|
const particles = getParticles();
|
|
|
|
const filteredParticles = particles
|
|
.filter((particle) => {
|
|
const categories = particle.categories ?? [];
|
|
return selectedCategories.every((value) => categories.includes(value));
|
|
})
|
|
.sort((a, b) => {
|
|
const weightA = calculateRelevanceWeight(a, selectedCategories);
|
|
const weightB = calculateRelevanceWeight(b, selectedCategories);
|
|
return weightB - weightA;
|
|
});
|
|
|
|
if (filteredParticles.length === 0) {
|
|
return (
|
|
<div className="text-center">
|
|
<p className="text-muted-foreground">
|
|
No particles found for the selected filters
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="grid flex-1 items-stretch gap-9 pb-12 lg:grid-cols-2 lg:gap-6 xl:gap-9">
|
|
{filteredParticles.map((particle) => {
|
|
const className = particle.meta?.className as string | undefined;
|
|
const colSpan = particle.meta?.colSpan as number | undefined;
|
|
return (
|
|
<Suspense
|
|
fallback={<ParticleCardSkeleton className={className} />}
|
|
key={particle.name}
|
|
>
|
|
<ParticleCard
|
|
className={className}
|
|
colSpan={colSpan}
|
|
name={particle.name}
|
|
/>
|
|
</Suspense>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|