- {isApplied ? (
-
- ) : (
-
- )}
-
-
{enhancement.label}
-
{enhancement.description}
+
+ Enhance your prompt
+
+ {ENHANCEMENTS.map((enhancement) => {
+ const isApplied = appliedIds.has(enhancement.id);
+ return (
+
-
- );
- })}
+
+ );
+ })}
+
);
diff --git a/apps/web/src/components/simulation/SimulationViewer.tsx b/apps/web/src/components/simulation/SimulationViewer.tsx
index 24393b629..c4a1acea4 100644
--- a/apps/web/src/components/simulation/SimulationViewer.tsx
+++ b/apps/web/src/components/simulation/SimulationViewer.tsx
@@ -22,6 +22,7 @@ import { useMediaQuery, useIsMobile } from "~/hooks/useMediaQuery";
import { Button } from "~/components/ui/button";
import {
Menu,
+ MenuGroup,
MenuGroupLabel,
MenuPopup,
MenuRadioGroup,
@@ -120,37 +121,39 @@ function SimulationToolbar({ onClose }: { onClose: () => void }) {
{preset ? preset.label : "Responsive"}
- Simulation Viewport
- {
- setViewportPreset(
- value === RESPONSIVE_VALUE ? null : (value as SimulationViewportPreset),
- );
- }}
- >
-
-
-
- Responsive
-
-
-
- {SIMULATION_VIEWPORT_PRESETS.map((p) => {
- const Icon = PRESET_ICONS[p.id];
- return (
-
-
-
- {p.label}
-
- {p.width}×{p.height}
+
+ Simulation Viewport
+ {
+ setViewportPreset(
+ value === RESPONSIVE_VALUE ? null : (value as SimulationViewportPreset),
+ );
+ }}
+ >
+
+
+
+ Responsive
+
+
+
+ {SIMULATION_VIEWPORT_PRESETS.map((p) => {
+ const Icon = PRESET_ICONS[p.id];
+ return (
+
+
+
+ {p.label}
+
+ {p.width}×{p.height}
+
-
-
- );
- })}
-
+
+ );
+ })}
+
+
diff --git a/apps/web/src/components/ui/baseUiInvariants.test.ts b/apps/web/src/components/ui/baseUiInvariants.test.ts
new file mode 100644
index 000000000..55602ada8
--- /dev/null
+++ b/apps/web/src/components/ui/baseUiInvariants.test.ts
@@ -0,0 +1,138 @@
+import { readdirSync, readFileSync, statSync } from "node:fs";
+import path from "node:path";
+import { describe, expect, it } from "vitest";
+
+const WEB_SRC_ROOT = path.resolve(import.meta.dirname, "../..");
+
+const REQUIRED_ANCESTORS = {
+ AutocompleteGroupLabel: "AutocompleteGroup",
+ ComboboxGroupLabel: "ComboboxGroup",
+ MenuGroupLabel: "MenuGroup",
+ MenuRadioItem: "MenuRadioGroup",
+ MenuSubPopup: "MenuSub",
+ MenuSubTrigger: "MenuSub",
+ SelectGroupLabel: "SelectGroup",
+} as const;
+
+const TARGET_COMPONENTS = new Set(Object.keys(REQUIRED_ANCESTORS));
+const JSX_TAG_PATTERN = /<(?
\/)?(?[A-Z][A-Za-z0-9]*)\b[^>]*?(?\/)?>/g;
+
+const WRAPPER_SOURCE_ASSERTIONS = [
+ {
+ filePath: path.resolve(import.meta.dirname, "./select.tsx"),
+ message: "Select item wrappers keep item indicator and text inside SelectPrimitive.Item",
+ patterns: [
+ /[\s\S]*?[\s\S]*?<\/SelectPrimitive\.Item>/,
+ ],
+ },
+ {
+ filePath: path.resolve(import.meta.dirname, "./combobox.tsx"),
+ message: "Combobox item wrappers keep item indicators inside ComboboxPrimitive.Item",
+ patterns: [
+ /[\s\S]*?<\/ComboboxPrimitive\.Item>/,
+ ],
+ },
+ {
+ filePath: path.resolve(import.meta.dirname, "./menu.tsx"),
+ message: "Menu item wrappers keep radio and checkbox indicators inside their item parents",
+ patterns: [
+ /[\s\S]*?<\/MenuPrimitive\.RadioItem>/,
+ /[\s\S]*?<\/MenuPrimitive\.CheckboxItem>/,
+ ],
+ },
+ {
+ filePath: path.resolve(import.meta.dirname, "./scroll-area.tsx"),
+ message: "ScrollArea wrappers keep thumbs inside scrollbars",
+ patterns: [
+ /[\s\S]*?<\/ScrollAreaPrimitive\.Scrollbar>/,
+ ],
+ },
+] as const;
+
+function collectTsxFiles(rootDir: string): string[] {
+ const entries = readdirSync(rootDir);
+ const files: string[] = [];
+
+ for (const entry of entries) {
+ const absolutePath = path.join(rootDir, entry);
+ const stats = statSync(absolutePath);
+
+ if (stats.isDirectory()) {
+ files.push(...collectTsxFiles(absolutePath));
+ continue;
+ }
+
+ if (
+ absolutePath.endsWith(".tsx") &&
+ !absolutePath.includes(`${path.sep}components${path.sep}ui${path.sep}`)
+ ) {
+ files.push(absolutePath);
+ }
+ }
+
+ return files;
+}
+
+function findMissingAncestorViolations(source: string): string[] {
+ const stack: string[] = [];
+ const violations: string[] = [];
+
+ for (const match of source.matchAll(JSX_TAG_PATTERN)) {
+ const name = match.groups?.name;
+ if (!name) continue;
+
+ const isClosing = Boolean(match.groups?.closing);
+ const isSelfClosing = Boolean(match.groups?.selfClosing);
+
+ if (isClosing) {
+ const lastIndex = stack.lastIndexOf(name);
+ if (lastIndex >= 0) {
+ stack.splice(lastIndex, 1);
+ }
+ continue;
+ }
+
+ if (TARGET_COMPONENTS.has(name)) {
+ const requiredAncestor = REQUIRED_ANCESTORS[name as keyof typeof REQUIRED_ANCESTORS];
+ if (!stack.includes(requiredAncestor)) {
+ violations.push(`${name} must be rendered within ${requiredAncestor}`);
+ }
+ }
+
+ if (!isSelfClosing) {
+ stack.push(name);
+ }
+ }
+
+ return violations;
+}
+
+describe("Base UI wrapper invariants", () => {
+ it("keeps label and item wrapper parts inside their required parent components", () => {
+ const failures = collectTsxFiles(WEB_SRC_ROOT).flatMap((filePath) => {
+ const source = readFileSync(filePath, "utf8");
+ const violations = findMissingAncestorViolations(source);
+
+ return violations.map(
+ (violation) => `${path.relative(WEB_SRC_ROOT, filePath)}: ${violation}`,
+ );
+ });
+
+ expect(failures).toEqual([]);
+ });
+
+ it("keeps second-tier primitive relationships intact inside shared UI wrappers", () => {
+ const failures = WRAPPER_SOURCE_ASSERTIONS.flatMap(({ filePath, message, patterns }) => {
+ const source = readFileSync(filePath, "utf8");
+ const missingPattern = patterns.some((pattern) => !pattern.test(source));
+
+ if (!missingPattern) {
+ return [];
+ }
+
+ return `${path.relative(WEB_SRC_ROOT, filePath)}: ${message}`;
+ });
+
+ expect(failures).toEqual([]);
+ });
+});