diff --git a/packages/web/app/foundry.config.ts b/packages/web/app/foundry.config.ts
index b78920a53c3..8977cc4ecf8 100644
--- a/packages/web/app/foundry.config.ts
+++ b/packages/web/app/foundry.config.ts
@@ -47,7 +47,7 @@ export default defineConfig({
{
label: 'Primitives',
children: [
- { label: 'Accordion' },
+ { label: 'Accordion', children: [{ label: 'Component Examples' }] },
{ label: 'Avatar', children: [{ label: 'Component Examples' }] },
{ label: 'Badge', children: [{ label: 'Component Examples' }] },
{ label: 'StatusDot' },
diff --git a/packages/web/app/package.json b/packages/web/app/package.json
index a1d7138c13a..cfe11b897ed 100644
--- a/packages/web/app/package.json
+++ b/packages/web/app/package.json
@@ -33,7 +33,6 @@
"@hookform/resolvers": "5.9.1",
"@monaco-editor/react": "4.8.0-rc.2",
"@pierre/diffs": "1.3.5",
- "@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.1",
"@radix-ui/react-slot": "1.1.1",
diff --git a/packages/web/app/src/components/base/accordion/accordion.call-sites.preview.tsx b/packages/web/app/src/components/base/accordion/accordion.call-sites.preview.tsx
new file mode 100644
index 00000000000..db9c80f6029
--- /dev/null
+++ b/packages/web/app/src/components/base/accordion/accordion.call-sites.preview.tsx
@@ -0,0 +1,644 @@
+import { useState } from 'react';
+import { FolderIcon, FolderOpenIcon } from 'lucide-react';
+import { createPreview, type NavPath } from 'react-foundry';
+import { Badge } from '@/components/base/badge/badge';
+import { Card } from '@/components/base/card/card';
+import { Menu } from '@/components/base/floating/menu/menu';
+import { Select } from '@/components/base/floating/select/select';
+import { CallSite, InventoryList } from '@/components/inventory/shared';
+import { PermissionTable } from '@/components/organization/permission-table';
+import { PulseIcon } from '@/components/ui/icon';
+import {
+ CheckIcon,
+ DotsHorizontalIcon,
+ ExclamationTriangleIcon,
+ PlusIcon,
+} from '@radix-ui/react-icons';
+import { Accordion } from './accordion';
+
+export const nav: NavPath = 'Base/Primitives/Accordion/Component Examples';
+
+/**
+ * Every accordion in the app, transcribed as it ships on base Accordion. Nine mounts in eight
+ * files. The pages around them run queries and mutations, so the content inside each item is stood
+ * in where it would need one: the schema highlighter is a `
`, the change details a paragraph,
+ * the approval badge a Badge.
+ *
+ * History: until round 7 six of these were on `ui/accordion` (Radix, shadcn parts) and three on
+ * `v2/accordion` (Radix, compound parts, always collapsible), one site mixing the two. A single
+ * service on the target page was an accordion locked open with its trigger disabled; it is a Card
+ * now. Both old components and `@radix-ui/react-accordion` are deleted.
+ */
+
+const ENTRIES = [
+ {
+ source: 'components/organization/members/permission-selector.tsx:93',
+ origin: 'base',
+ what: 'Permission groups on the role form: multiple open, controlled, panels kept mounted for the dependency jump, "N selected" trailing',
+ coveredBy: 'Permission selector',
+ },
+ {
+ source: 'components/organization/members/selected-permission-overview.tsx:146',
+ origin: 'base',
+ what: 'A role\'s granted permissions: one item, open by default when anything is granted, "N allowed" trailing',
+ coveredBy: 'Permission overview',
+ },
+ {
+ source: 'components/organization/settings/access-tokens/permission-detail-view.tsx:45',
+ origin: 'base',
+ what: "An access token's permissions per level, the same shape plus the resources it was granted on",
+ coveredBy: 'Permission overview',
+ },
+ {
+ source: 'components/target/history/errors-and-changes.tsx:227',
+ origin: 'base',
+ what: 'One accordion per schema change on the check, version and proposal pages: a rich label, details below',
+ coveredBy: 'Schema changes',
+ },
+ {
+ source: 'components/target/proposals/change-detail.tsx:20',
+ origin: 'base',
+ what: 'A proposal change: dimmed message and an icon in the label',
+ coveredBy: 'Proposal change',
+ },
+ {
+ source: 'components/target/settings/registry-access-token.tsx:231',
+ origin: 'base',
+ what: "The registry token form's permission section, plain and open by default",
+ coveredBy: 'Registry token',
+ },
+ {
+ source: 'pages/target.tsx:67',
+ origin: 'base',
+ what: 'The services of a federation target, boxed, a two-line header and the SDL inside',
+ coveredBy: 'Target services',
+ },
+ {
+ source: 'pages/target.tsx:86',
+ origin: 'base',
+ what: 'A single service: a Card with the same header and SDL, since there is nothing to collapse',
+ coveredBy: 'Target services',
+ },
+ {
+ source: 'lib/hooks/laboratory/use-operation-collections-plugin.tsx:301',
+ origin: 'base',
+ what: 'Laboratory collections: plain, no chevron, folder icons in the label, a menu through action, e2e hooks on item and trigger',
+ coveredBy: 'Laboratory collections',
+ },
+] as const;
+
+export const Inventory = createPreview({
+ label: 'Inventory',
+ render: () => (
+
+ Nine mounts, all on base Accordion. Three of the four variants ship: list on the
+ permission views and the change rows, boxed on the target services, plain on the alert
+ form's disclosure, the registry token section and the collections sidebar.
+ >
+ }
+ entries={ENTRIES}
+ />
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// components/organization/members/permission-selector.tsx:93
+// ---------------------------------------------------------------------------
+
+const PERMISSION_GROUPS = [
+ {
+ title: 'Organization',
+ permissions: [
+ {
+ id: 'organization:describe',
+ title: 'Describe organization',
+ description: 'Fetch information about the organization.',
+ },
+ {
+ id: 'member:modify',
+ title: 'Modify members',
+ description: 'Invite, remove and assign roles.',
+ },
+ ],
+ },
+ {
+ title: 'Project',
+ permissions: [
+ {
+ id: 'project:describe',
+ title: 'Describe project',
+ description: 'Fetch information about projects.',
+ },
+ { id: 'project:delete', title: 'Delete project', description: 'Delete projects.' },
+ ],
+ },
+ {
+ title: 'Schema Registry',
+ permissions: [
+ { id: 'schemaCheck:create', title: 'Check schema', description: 'Run schema checks.' },
+ {
+ id: 'schemaVersion:publish',
+ title: 'Publish schema',
+ description: 'Publish schema versions.',
+ },
+ {
+ id: 'schemaCheck:approve',
+ title: 'Approve failed check',
+ description: 'Approve failed schema checks.',
+ },
+ ],
+ },
+];
+
+function PermissionSelector() {
+ const [openAccordions, setOpenAccordions] = useState([] as Array);
+ const [selected, setSelected] = useState(new Set(['organization:describe', 'project:describe']));
+ return (
+ {
+ const count = group.permissions.filter(p => selected.has(p.id)).length;
+ return {
+ value: group.title,
+ label: group.title,
+ trailing: count > 0 ? {count} selected : undefined,
+ content: (
+
+ {group.permissions.map(permission => (
+
+
+
{permission.title}
+
{permission.description}
+
+
{
+ const next = new Set(selected);
+ if (value === 'allow') next.add(permission.id);
+ else next.delete(permission.id);
+ setSelected(next);
+ }}
+ />
+
+ ))}
+
+ ),
+ };
+ })}
+ />
+ );
+}
+
+export const PermissionSelectorPreview = createPreview({
+ label: 'Permission selector',
+ render: () => (
+
+
+
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// components/organization/members/selected-permission-overview.tsx:146
+// components/organization/settings/access-tokens/permission-detail-view.tsx:45
+// ---------------------------------------------------------------------------
+
+const GRANTED_ROWS = [
+ { id: 'schemaCheck:create', title: 'Check schema', granted: true },
+ { id: 'schemaVersion:publish', title: 'Publish schema', granted: true },
+ { id: 'schemaCheck:approve', title: 'Approve failed check', granted: false },
+];
+
+function PermissionOverview(props: { withResources?: boolean }) {
+ const allowed = GRANTED_ROWS.filter(row => row.granted).length;
+ return (
+ 0 ? ['Target'] : undefined}
+ items={[
+ {
+ value: 'Target',
+ label: 'Target',
+ trailing: {allowed} allowed ,
+ content: (
+
+
+
+ {props.withResources ? (
+
+
Granted on targets:
+
+ {['the-guild/hive/production', 'the-guild/hive/staging'].map(id => (
+
+
+
+ ))}
+
+
+ ) : null}
+
+ ),
+ },
+ ]}
+ />
+ );
+}
+
+export const PermissionOverviewPreview = createPreview({
+ label: 'Permission overview',
+ render: () => (
+
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// components/target/history/errors-and-changes.tsx:227
+// ---------------------------------------------------------------------------
+
+const CHANGES = [
+ {
+ message: "Field 'Query.invoices' was removed",
+ severity: 'text-critical',
+ usage: { operations: 12, clients: 3 },
+ reason:
+ 'Removing a field is a breaking change. It is preferable to deprecate the field before removing it.',
+ },
+ {
+ message: "Field 'User.avatarUrl' was added to object type 'User'",
+ severity: 'text-emerald-400',
+ reason: 'No details available for this change.',
+ },
+ {
+ message: "Field 'Product.price' changed type from 'Int!' to 'Float!'",
+ severity: 'text-accent',
+ approved: true,
+ reason: 'Changing the type of a field may break existing queries.',
+ },
+];
+
+function ChangeRow(props: { change: (typeof CHANGES)[number] }) {
+ const { change } = props;
+ return (
+
+
+
{change.message}
+ {change.usage ? (
+ <>
+ {' '}
+
+
+
+ {change.usage.operations} operations by {change.usage.clients} clients
+ affected
+
+
+ >
+ ) : null}
+ {change.approved ? (
+ <>
+ {' '}
+
+ >
+ ) : null}
+
+
+ ),
+ content: {change.reason}
,
+ },
+ ]}
+ />
+ );
+}
+
+export const SchemaChangesPreview = createPreview({
+ label: 'Schema changes',
+ render: () => (
+
+
+ {CHANGES.map(change => (
+
+ ))}
+
+
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// components/target/proposals/change-detail.tsx:20
+// ---------------------------------------------------------------------------
+
+export const ProposalChangePreview = createPreview({
+ label: 'Proposal change',
+ render: () => (
+
+
+
+ Field 'Query.invoices' was removed
+
+
+
+
+ ),
+ content: 'No details available for this change.',
+ },
+ ]}
+ />
+
+
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// components/target/settings/registry-access-token.tsx:231
+// ---------------------------------------------------------------------------
+
+export const RegistryTokenPreview = createPreview({
+ label: 'Registry token',
+ render: () => (
+
+
+
+
+
Registry & Usage
+
+ Manage access to schema registry and usage reporting.
+
+
+
+
+ ),
+ },
+ ]}
+ />
+
+
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// pages/target.tsx:67 and :86
+// ---------------------------------------------------------------------------
+
+const SERVICES = [
+ {
+ id: 's1',
+ service: 'users',
+ url: 'https://users.storefront.local/graphql',
+ source:
+ 'type Query {\n me: User\n user(id: ID!): User\n}\n\ntype User @key(fields: "id") {\n id: ID!\n fullName: String!\n}',
+ },
+ {
+ id: 's2',
+ service: 'products',
+ url: 'https://products.storefront.local/graphql',
+ source:
+ 'type Query {\n products(first: Int = 20): [Product!]!\n}\n\ntype Product @key(fields: "id") {\n id: ID!\n name: String!\n}',
+ },
+];
+
+function serviceHeader(schema: (typeof SERVICES)[number]) {
+ return (
+
+
{schema.service}
+
{schema.url}
+
+ );
+}
+
+/** GraphQLHighlight is a Monaco editor; a code block stands in for it. */
+function serviceSchema(schema: (typeof SERVICES)[number]) {
+ return (
+
+ );
+}
+
+export const TargetServicesPreview = createPreview({
+ label: 'Target services',
+ render: () => (
+
+
+
+
({
+ value: schema.id,
+ label: serviceHeader(schema),
+ content: serviceSchema(schema),
+ }))}
+ />
+
+
+
+
+
+ {serviceHeader(SERVICES[0])}
+ {serviceSchema(SERVICES[0])}
+
+
+
+
+ ),
+});
+
+// ---------------------------------------------------------------------------
+// lib/hooks/laboratory/use-operation-collections-plugin.tsx:301
+// ---------------------------------------------------------------------------
+
+const COLLECTIONS = [
+ {
+ id: 'c1',
+ name: 'Onboarding',
+ operations: ['GetViewer', 'ListProjects', 'CreateTarget'],
+ },
+ { id: 'c2', name: 'Regression', operations: ['PublishSchema'] },
+ { id: 'c3', name: 'Scratch', operations: [] },
+];
+
+function Collections() {
+ const [value, setValue] = useState(['c1']);
+ return (
+ ({
+ value: collection.id,
+ label: (
+
+ {value.includes(collection.id) ? (
+
+ ) : (
+
+ )}
+ {collection.name}
+
+ ),
+ attrs: { 'data-cy': 'collection-item' },
+ triggerAttrs: { 'data-cy': 'collection-item-trigger' },
+ action: (
+
+
+
+ }
+ sections={[
+ [{ label: 'Add operation', trailingIcon: PlusIcon, onClick: () => {} }],
+ [
+ { label: 'Edit', onClick: () => {} },
+ { label: 'Delete', variant: 'destructiveAction', onClick: () => {} },
+ ],
+ ]}
+ />
+ ),
+ content: (
+
+ {collection.operations.length ? (
+ collection.operations.map(name => (
+
+ ))
+ ) : (
+
+ Add Operation
+
+ )}
+
+ ),
+ }))}
+ attrs={{ id: 'laboratory-collections' }}
+ />
+ );
+}
+
+export const LaboratoryCollectionsPreview = createPreview({
+ label: 'Laboratory collections',
+ render: () => (
+
+
+
+
+
+ ),
+});
diff --git a/packages/web/app/src/components/base/accordion/accordion.preview.tsx b/packages/web/app/src/components/base/accordion/accordion.preview.tsx
index 4ac13112b89..f6efd3d030a 100644
--- a/packages/web/app/src/components/base/accordion/accordion.preview.tsx
+++ b/packages/web/app/src/components/base/accordion/accordion.preview.tsx
@@ -1,71 +1,279 @@
-import { createPreview, type NavPath } from 'react-foundry';
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './accordion';
+import { useState, type ReactNode } from 'react';
+import { FolderIcon, FolderOpenIcon, MoreHorizontal, Plus } from 'lucide-react';
+import { controlsFor, createPreview, type NavPath } from 'react-foundry';
+import { Badge } from '../badge/badge';
+import { Menu } from '../floating/menu/menu';
+import { Accordion } from './accordion';
export const nav: NavPath = 'Base/Primitives/Accordion';
+/**
+ * Items in, an accordion out, on Base UI. One trigger look: medium text, the chevron at the end,
+ * no underline on hover. Each example below is drawn at the call site it will ship at, so the
+ * Component Examples beside it, which still render the old components, can be compared like for
+ * like.
+ */
+
+function Copy({ children }: { children: ReactNode }) {
+ return {children}
;
+}
+
+function Labelled(props: { label: string; children: ReactNode }) {
+ return (
+
+ {props.label}
+ {props.children}
+
+ );
+}
+
+const CHANGES = [
+ {
+ value: 'breaking',
+ label: 'Breaking changes',
+ body: 'Field User.email was removed from type User.',
+ },
+ {
+ value: 'dangerous',
+ label: 'Dangerous changes',
+ body: 'Enum value PENDING was added to Status.',
+ },
+ { value: 'safe', label: 'Safe changes', body: 'Description on type Query has changed.' },
+];
+
export const Default = createPreview(() => (
-
-
-
-
-
- Field User.email was removed from type User.
-
-
-
-
-
-
-
- Enum value PENDING was added to Status.
-
-
-
-
-
-
- Description on type Query has changed.
-
-
-
+
({
+ value: change.value,
+ label: change.label,
+ content: {change.body} ,
+ }))}
+ />
));
+/** `defaultValue` opens items on mount; `multiple` lets more than one stay open. */
export const DefaultOpen = createPreview(() => (
-
-
-
-
-
- Driven by the `defaultValue` prop.
-
-
-
-
-
- Not listed in `defaultValue`.
-
-
-
+
+
+ ({
+ value: change.value,
+ label: change.label,
+ content: {change.body} ,
+ }))}
+ />
+
+
+ ({
+ value: change.value,
+ label: change.label,
+ content: {change.body} ,
+ }))}
+ />
+
));
-export const AccentVariant = createPreview(() => (
+const PERMISSION_GROUPS = [
+ { value: 'Organization', selected: 2, total: 4 },
+ { value: 'Project', selected: 0, total: 3 },
+ { value: 'Schema Registry', selected: 3, total: 3 },
+];
+
+/**
+ * members/permission-selector.tsx: the open set held by the form, a count at the trigger's far
+ * end. `trailing` sits before the chevron.
+ */
+export const Controlled = createPreview(() => {
+ const [open, setOpen] = useState
([]);
+ return (
+
+
({
+ value: group.value,
+ label: group.value,
+ trailing: group.selected ? (
+ {group.selected} selected
+ ) : undefined,
+ content: {group.total} permissions in this group. ,
+ }))}
+ />
+ Open: {open.join(', ') || 'none'}
+
+ );
+});
+
+const SERVICES = [
+ { id: 's1', service: 'users', url: 'https://users.storefront.local/graphql' },
+ { id: 's2', service: 'products', url: 'https://products.storefront.local/graphql' },
+];
+
+/** pages/target.tsx: `boxed`, each service a card with a two-line header and its SDL inside. */
+export const Boxed = createPreview(() => (
+
+
({
+ value: schema.id,
+ label: (
+
+
{schema.service}
+
{schema.url}
+
+ ),
+ content: (
+
+ {'type Query {\n me: User\n}'}
+
+ ),
+ }))}
+ />
+
+));
+
+/**
+ * alerts/alert-form.tsx, Advanced settings: `plain`, so no hairline under a lone disclosure;
+ * small, the chevron at the start, and the accent tone to call it out.
+ */
+export const SmallAccentDisclosure = createPreview(() => (
+
+
On filter, hold minutes.,
+ },
+ ]}
+ />
+
+));
+
+const COLLECTIONS = [
+ { id: 'c1', name: 'Onboarding', operations: ['GetViewer', 'ListProjects'] },
+ { id: 'c2', name: 'Regression', operations: ['PublishSchema'] },
+];
+
+/**
+ * laboratory collections: no chevron, folder icons in the label that swap on open, and a menu
+ * beside the trigger through `action`, which stays outside the trigger button.
+ */
+export const WithAction = createPreview(() => {
+ const [open, setOpen] = useState(['c1']);
+ return (
+
+
({
+ value: collection.id,
+ label: (
+
+ {open.includes(collection.id) ? (
+
+ ) : (
+
+ )}
+ {collection.name}
+
+ ),
+ action: (
+
+
+
+ }
+ sections={[
+ [{ label: 'Add operation', trailingIcon: Plus, onClick: () => {} }],
+ [
+ { label: 'Edit', onClick: () => {} },
+ { label: 'Delete', variant: 'destructiveAction', onClick: () => {} },
+ ],
+ ]}
+ />
+ ),
+ content: (
+
+ {collection.operations.map(name => (
+ {name}
+ ))}
+
+ ),
+ attrs: { 'data-cy': 'collection-item' },
+ }))}
+ />
+
+ );
+});
+
+/** A disabled item keeps its place and its label, and cannot open. */
+export const Disabled = createPreview(() => (
-
-
-
-
- Used where the section needs emphasis.
-
-
-
-
-
- The standard neutral treatment.
-
-
-
+
None. },
+ {
+ value: 'b',
+ label: (
+
+ Breaking changes
+
+
+ ),
+ content: Two, both approved. ,
+ disabled: true,
+ },
+ ]}
+ />
));
+
+export const Playground = createPreview({
+ controls: controlsFor(Accordion, {
+ variant: { type: 'radio', options: ['list', 'boxed', 'plain'], default: 'list' },
+ size: { type: 'radio', options: ['default', 'sm'], default: 'default' },
+ chevron: { type: 'radio', options: ['end', 'start', 'none'], default: 'end' },
+ tone: { type: 'radio', options: ['default', 'accent'], default: 'default' },
+ multiple: { type: 'boolean', default: false },
+ keepMounted: { type: 'boolean', default: false },
+ }),
+ render: v => (
+
+
({
+ value: change.value,
+ label: change.label,
+ trailing: index === 0 ? 2 : undefined,
+ content: {change.body} ,
+ }))}
+ />
+
+ ),
+});
diff --git a/packages/web/app/src/components/base/accordion/accordion.spec.tsx b/packages/web/app/src/components/base/accordion/accordion.spec.tsx
new file mode 100644
index 00000000000..615856354bb
--- /dev/null
+++ b/packages/web/app/src/components/base/accordion/accordion.spec.tsx
@@ -0,0 +1,68 @@
+// @vitest-environment jsdom
+import { fireEvent, render, screen } from '@testing-library/react';
+import { Accordion } from './accordion';
+
+const ITEMS = [
+ { value: 'a', label: 'Breaking', content: Breaking panel
, attrs: { 'data-cy': 'row-a' } },
+ { value: 'b', label: 'Safe', content: Safe panel
},
+ { value: 'c', label: 'Locked', content: Locked panel
, disabled: true },
+];
+
+describe('Accordion', () => {
+ it('opens one item at a time by default, and reports the open set', () => {
+ const onValueChange = vi.fn();
+ render( );
+ expect(screen.queryByText('Breaking panel')).toBeNull();
+
+ fireEvent.click(screen.getByRole('button', { name: 'Breaking' }));
+ expect(screen.getByText('Breaking panel')).toBeTruthy();
+ expect(onValueChange).toHaveBeenLastCalledWith(['a']);
+
+ fireEvent.click(screen.getByRole('button', { name: 'Safe' }));
+ expect(screen.queryByText('Breaking panel')).toBeNull();
+ expect(screen.getByText('Safe panel')).toBeTruthy();
+ expect(onValueChange).toHaveBeenLastCalledWith(['b']);
+ });
+
+ it('keeps several open with multiple, and follows a controlled value', () => {
+ const { rerender } = render(
+ {}} />,
+ );
+ expect(screen.getByText('Breaking panel')).toBeTruthy();
+ expect(screen.getByText('Safe panel')).toBeTruthy();
+
+ rerender( {}} />);
+ expect(screen.queryByText('Breaking panel')).toBeNull();
+ });
+
+ it('puts trailing inside the trigger and action beside it, and lands attrs on item and trigger', () => {
+ render(
+ 3 selected,
+ action: More ,
+ triggerAttrs: { 'data-cy': 'row-a-trigger' },
+ },
+ ]}
+ />,
+ );
+ const trigger = screen.getByRole('button', { name: /Breaking/ });
+ expect(trigger.textContent).toContain('3 selected');
+ expect(trigger.getAttribute('data-cy')).toBe('row-a-trigger');
+ const more = screen.getByRole('button', { name: 'More' });
+ expect(trigger.contains(more)).toBe(false);
+ expect(document.querySelector('[data-cy="row-a"]')!.contains(more)).toBe(true);
+ });
+
+ it('will not open a disabled item', () => {
+ render( );
+ const locked = screen.getByRole('button', { name: 'Locked' });
+ expect(locked.hasAttribute('disabled') || locked.getAttribute('aria-disabled') === 'true').toBe(
+ true,
+ );
+ fireEvent.click(locked);
+ expect(screen.queryByText('Locked panel')).toBeNull();
+ });
+});
diff --git a/packages/web/app/src/components/base/accordion/accordion.tsx b/packages/web/app/src/components/base/accordion/accordion.tsx
index 27dfe0d69d8..9dc3e8fc5cb 100644
--- a/packages/web/app/src/components/base/accordion/accordion.tsx
+++ b/packages/web/app/src/components/base/accordion/accordion.tsx
@@ -1,51 +1,194 @@
import { type ReactNode } from 'react';
+import { cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react';
+import { cn } from '@/lib/utils';
import { Accordion as BaseAccordion } from '@base-ui/react/accordion';
+import { focusRingQuiet } from '../shared-styles';
-export function Accordion({
- children,
- defaultValue,
-}: {
- children: ReactNode;
- defaultValue?: number[];
-}) {
- return {children} ;
-}
+export type AccordionItem = {
+ value: string;
+ /** Text, or a two-line header, badges, icons. */
+ label: ReactNode;
+ /** At the far end of the trigger, before the chevron: a count, a status. */
+ trailing?: ReactNode;
+ /** Beside the trigger, outside it: a menu button. A button cannot nest in a button. */
+ action?: ReactNode;
+ content: ReactNode;
+ disabled?: boolean;
+ /** Test hooks and the like, landed on the item. */
+ attrs?: Record;
+ /** Test hooks on the trigger button itself. */
+ triggerAttrs?: Record;
+};
-export function AccordionItem({ children, value }: { children: ReactNode; value: number }) {
- return {children} ;
-}
+export type AccordionVariant = 'list' | 'boxed' | 'plain';
+export type AccordionSize = 'default' | 'sm';
+export type AccordionChevron = 'end' | 'start' | 'none';
+export type AccordionTone = 'default' | 'accent';
-export function AccordionTrigger({
- label,
- variant = 'default',
-}: {
- label: string;
- variant?: 'default' | 'accent';
-}) {
- const colorClass =
- variant === 'accent'
- ? 'text-accent hover:text-accent/80'
- : 'text-neutral-12 hover:text-neutral-11';
+type AccordionProps = {
+ items: readonly AccordionItem[];
+ value?: string[];
+ defaultValue?: string[];
+ onValueChange?: (value: string[]) => void;
+ /** More than one item open at once. */
+ multiple?: boolean;
+ /**
+ * `list` draws a hairline under each item, so rows read as a list even one at a time; `boxed`
+ * gives each item a card border, spaced apart; `plain` draws nothing, for a lone disclosure.
+ */
+ variant?: AccordionVariant;
+ size?: AccordionSize;
+ /** Where the chevron sits. `start` for a disclosure under a form; `none` when the label carries its own icon. */
+ chevron?: AccordionChevron;
+ /** `accent` calls the trigger out: a disclosure that invites, like Advanced settings, rather than a section. */
+ tone?: AccordionTone;
+ /** Closed panels stay in the DOM. */
+ keepMounted?: boolean;
+ attrs?: Record;
+};
- const chevronColor = variant === 'accent' ? 'text-accent' : 'text-neutral-8';
+const rootVariants = cva('', {
+ variants: {
+ variant: {
+ list: '',
+ boxed: 'flex flex-col gap-4',
+ plain: '',
+ },
+ },
+});
- return (
- svg]:rotate-0`}
- >
-
- {label}
-
- );
-}
+const itemVariants = cva('', {
+ variants: {
+ variant: {
+ list: 'border-neutral-5 border-b',
+ boxed: 'border-neutral-5 rounded-md border',
+ plain: '',
+ },
+ },
+});
-export function AccordionContent({ children }: { children: ReactNode }) {
+const headerRowVariants = cva('flex items-center', {
+ variants: {
+ variant: {
+ list: '',
+ boxed: 'px-4',
+ plain: '',
+ },
+ },
+});
+
+const triggerVariants = cva(
+ cn(
+ 'group flex w-full items-center gap-2 text-left font-medium transition-colors',
+ focusRingQuiet,
+ 'focus-visible:-outline-offset-2',
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
+ ),
+ {
+ variants: {
+ size: {
+ default: 'py-3 text-sm',
+ sm: 'py-2 text-xs',
+ },
+ tone: {
+ default: 'text-neutral-12 hover:text-neutral-11',
+ accent: 'text-accent hover:text-accent/80',
+ },
+ },
+ },
+);
+
+const chevronVariants = cva('shrink-0 transition-transform duration-200', {
+ variants: {
+ size: {
+ default: 'size-4',
+ sm: 'size-3',
+ },
+ tone: {
+ default: 'text-neutral-8',
+ accent: 'text-accent',
+ },
+ chevron: {
+ end: 'ml-auto group-data-[panel-open]:rotate-180',
+ start: '-rotate-90 group-data-[panel-open]:rotate-0',
+ none: 'hidden',
+ },
+ },
+});
+
+// Base UI measures the panel into a variable, which is what lets the height animate.
+const panelVariants = cva(
+ 'h-[var(--accordion-panel-height)] overflow-hidden text-sm transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0',
+);
+
+const panelInnerVariants = cva('', {
+ variants: {
+ variant: {
+ list: '',
+ boxed: 'px-4',
+ plain: '',
+ },
+ size: {
+ default: 'pb-4',
+ sm: 'pb-2',
+ },
+ },
+});
+
+export function Accordion({
+ items,
+ value,
+ defaultValue,
+ onValueChange,
+ multiple = false,
+ variant = 'list',
+ size = 'default',
+ chevron = 'end',
+ tone = 'default',
+ keepMounted,
+ attrs,
+}: AccordionProps) {
+ const chevronIcon = ;
return (
-
- {children}
-
+ onValueChange?.(next.map(String))}
+ multiple={multiple}
+ keepMounted={keepMounted}
+ className={rootVariants({ variant })}
+ {...attrs}
+ >
+ {items.map(item => (
+
+
+
+
+ {chevron === 'start' ? chevronIcon : null}
+ {item.label}
+ {item.trailing != null ? (
+ {item.trailing}
+ ) : null}
+ {chevron === 'end' ? chevronIcon : null}
+
+
+ {item.action != null ?
{item.action}
: null}
+
+
+ {item.content}
+
+
+ ))}
+
);
}
diff --git a/packages/web/app/src/components/organization/members/permission-selector.tsx b/packages/web/app/src/components/organization/members/permission-selector.tsx
index 78c0df43606..ff7c012aa96 100644
--- a/packages/web/app/src/components/organization/members/permission-selector.tsx
+++ b/packages/web/app/src/components/organization/members/permission-selector.tsx
@@ -1,14 +1,9 @@
import { useMemo, useRef, useState } from 'react';
import { InfoIcon, TriangleAlert } from 'lucide-react';
+import { Accordion } from '@/components/base/accordion/accordion';
import { Popover } from '@/components/base/floating/popover/popover';
import { Select } from '@/components/base/floating/select/select';
import type { OnSurface } from '@/components/base/shared-styles';
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from '@/components/ui/accordion';
import { Button } from '@/components/ui/button';
import { FragmentType, graphql, useFragment } from '@/gql';
import { cn } from '@/lib/utils';
@@ -96,184 +91,173 @@ export function PermissionSelector(props: PermissionSelectorProps) {
return (
setOpenAccordions(values)}
- >
- {groups.map(group => {
- return (
-
-
- {group.title}{' '}
-
- {group.selectedPermissionCount > 0 && (
-
- {group.selectedPermissionCount} selected
-
- )}
-
-
-
- {group.permissions.map(permission => {
- const needsDependency =
- !!permission.dependsOnId &&
- !props.selectedPermissionIds.has(permission.dependsOnId);
+ onValueChange={setOpenAccordions}
+ items={groups.map(group => ({
+ value: group.title,
+ label: group.title,
+ trailing:
+ group.selectedPermissionCount > 0 ? (
+ {group.selectedPermissionCount} selected
+ ) : undefined,
+ content: (
+
+ {group.permissions.map(permission => {
+ const needsDependency =
+ !!permission.dependsOnId &&
+ !props.selectedPermissionIds.has(permission.dependsOnId);
- return (
-
+ return (
+
+
{
+ if (ref) {
+ permissionRefs.current.set(permission.id, ref);
+ }
+ }}
+ >
{
- if (ref) {
- permissionRefs.current.set(permission.id, ref);
- }
- }}
>
-
-
{permission.title}
-
{permission.description}
+
{permission.title}
+
{permission.description}
+
+ {permission.isAssignableByViewer === false ? (
+
+
+
+
+ }
+ openOnHover
+ content={
+
+ Your membership has insufficient authority for assigning this
+ permission.
+
+ }
+ />
+
+ ) : permission.warning && props.selectedPermissionIds.has(permission.id) ? (
+
+
+
+
+ }
+ openOnHover
+ content={{permission.warning}
}
+ />
- {permission.isAssignableByViewer === false ? (
+ ) : (
+ !!permission.dependsOnId &&
+ permissionToGroupTitleMapping.has(permission.dependsOnId) && (
+
}
openOnHover
content={
- Your membership has insufficient authority for assigning this
- permission.
+ This permission depends on another permission.{' '}
+ {
+ const dependencyPermission = permission.dependsOnId;
+ if (!dependencyPermission) {
+ return;
+ }
+ const element =
+ permissionRefs.current.get(dependencyPermission);
+
+ if (!element) {
+ return;
+ }
+ setOpenAccordions(values => {
+ const groupName =
+ permissionToGroupTitleMapping.get(dependencyPermission);
+
+ if (groupName && values.includes(groupName) === false) {
+ return [...values, groupName];
+ }
+ return values;
+ });
+ setFocusedPermission(dependencyPermission);
+ element.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ }}
+ >
+ View permission.
+
}
/>
- ) : permission.warning && props.selectedPermissionIds.has(permission.id) ? (
-
-
-
-
- }
- openOnHover
- content={
- {permission.warning}
- }
- />
-
- ) : (
- !!permission.dependsOnId &&
- permissionToGroupTitleMapping.has(permission.dependsOnId) && (
-
-
-
-
- }
- openOnHover
- content={
-
- This permission depends on another permission.{' '}
- {
- const dependencyPermission = permission.dependsOnId;
- if (!dependencyPermission) {
- return;
- }
- const element =
- permissionRefs.current.get(dependencyPermission);
-
- if (!element) {
- return;
- }
- setOpenAccordions(values => {
- const groupName =
- permissionToGroupTitleMapping.get(dependencyPermission);
-
- if (groupName && values.includes(groupName) === false) {
- return [...values, groupName];
- }
- return values;
- });
- setFocusedPermission(dependencyPermission);
- element.scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- });
- }}
- >
- View permission.
-
-
- }
- />
-
- )
- )}
-
{
- const dependents = dependencyGraph.get(permission.id) ?? [];
- if (value === 'allow') {
- props.onSelectedPermissionsChange(
- new Set([...props.selectedPermissionIds, permission.id]),
- );
- } else if (value === 'not-selected') {
- const selectedPermissionIds = new Set(props.selectedPermissionIds);
- selectedPermissionIds.delete(permission.id);
- for (const dependent of dependents) {
- selectedPermissionIds.delete(dependent);
- }
- props.onSelectedPermissionsChange(selectedPermissionIds);
- }
- setFocusedPermission(null);
- }}
- width="sm"
- />
-
- {focusedPermission === permission.id && (
-
+ )
)}
+
{
+ const dependents = dependencyGraph.get(permission.id) ?? [];
+ if (value === 'allow') {
+ props.onSelectedPermissionsChange(
+ new Set([...props.selectedPermissionIds, permission.id]),
+ );
+ } else if (value === 'not-selected') {
+ const selectedPermissionIds = new Set(props.selectedPermissionIds);
+ selectedPermissionIds.delete(permission.id);
+ for (const dependent of dependents) {
+ selectedPermissionIds.delete(dependent);
+ }
+ props.onSelectedPermissionsChange(selectedPermissionIds);
+ }
+ setFocusedPermission(null);
+ }}
+ width="sm"
+ />
- );
- })}
-
-
- );
- })}
-
+ {focusedPermission === permission.id && (
+
+ )}
+
+ );
+ })}
+
+ ),
+ }))}
+ />
);
}
diff --git a/packages/web/app/src/components/organization/members/selected-permission-overview.tsx b/packages/web/app/src/components/organization/members/selected-permission-overview.tsx
index c9e1e7c7b4f..f93b315fcdb 100644
--- a/packages/web/app/src/components/organization/members/selected-permission-overview.tsx
+++ b/packages/web/app/src/components/organization/members/selected-permission-overview.tsx
@@ -1,11 +1,6 @@
import { useMemo } from 'react';
+import { Accordion } from '@/components/base/accordion/accordion';
import { PermissionTable } from '@/components/organization/permission-table';
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from '@/components/ui/accordion';
import { FragmentType, graphql, useFragment } from '@/gql';
import { PermissionLevelType } from '@/gql/graphql';
import { ResultOf } from '@graphql-typed-document-node/core';
@@ -149,43 +144,43 @@ function PermissionLevelGroup(props: {
return (
0 && props.isExpanded ? props.title : undefined}
- collapsible
- >
-
-
- {props.title}
- {totalAllowedCount} allowed
-
-
- {filteredGroups.map(group =>
- props.showOnlyAllowedPermissions && group.totalAllowedCount === 0 ? null : (
-
-
{
- const granted =
- props.activePermissionIds.has(permission.id) || permission.isReadOnly;
- if (props.showOnlyAllowedPermissions && !granted) {
- return [];
- }
- return [
- {
- id: permission.id,
- title: permission.title,
- granted,
- warning: permission.warning,
- },
- ];
- })}
- />
-
- ),
- )}
- {props.additionalContent}
-
-
-
+ defaultValue={totalAllowedCount > 0 && props.isExpanded ? [props.title] : undefined}
+ items={[
+ {
+ value: props.title,
+ label: props.title,
+ trailing:
{totalAllowedCount} allowed ,
+ content: (
+
+ {filteredGroups.map(group =>
+ props.showOnlyAllowedPermissions && group.totalAllowedCount === 0 ? null : (
+
+
{
+ const granted =
+ props.activePermissionIds.has(permission.id) || permission.isReadOnly;
+ if (props.showOnlyAllowedPermissions && !granted) {
+ return [];
+ }
+ return [
+ {
+ id: permission.id,
+ title: permission.title,
+ granted,
+ warning: permission.warning,
+ },
+ ];
+ })}
+ />
+
+ ),
+ )}
+ {props.additionalContent}
+
+ ),
+ },
+ ]}
+ />
);
}
diff --git a/packages/web/app/src/components/organization/settings/access-tokens/permission-detail-view.tsx b/packages/web/app/src/components/organization/settings/access-tokens/permission-detail-view.tsx
index 34337037476..a954eb1e320 100644
--- a/packages/web/app/src/components/organization/settings/access-tokens/permission-detail-view.tsx
+++ b/packages/web/app/src/components/organization/settings/access-tokens/permission-detail-view.tsx
@@ -1,6 +1,6 @@
+import { Accordion } from '@/components/base/accordion/accordion';
import { Badge } from '@/components/base/badge/badge';
import { PermissionTable } from '@/components/organization/permission-table';
-import * as Accordion from '@/components/ui/accordion';
import { FragmentType, graphql, useFragment } from '@/gql';
import { permissionLevelToResourceName } from './shared-helpers';
@@ -42,52 +42,52 @@ export function PermissionDetailView(props: {
);
return (
-
0 ? group.title : undefined}
- collapsible
- >
-
-
- {group.title}
- {totalAllowedCount} allowed
-
-
- {group.resolvedPermissionGroups.map(group => (
-
-
({
- id: permission.permission.id,
- title: permission.permission.title,
- granted: permission.isGranted,
- warning: permission.permission.warning,
- }))}
- />
+ 0 ? [group.title] : undefined}
+ items={[
+ {
+ value: group.title,
+ label: group.title,
+ trailing: {totalAllowedCount} allowed ,
+ content: (
+
+ {group.resolvedPermissionGroups.map(group => (
+
+
({
+ id: permission.permission.id,
+ title: permission.permission.title,
+ granted: permission.isGranted,
+ warning: permission.permission.warning,
+ }))}
+ />
+
+ ))}
+
+ {group.resolvedResourceIds == null ? (
+
+ Not granted on any {permissionLevelToResourceName(group.level).slice(0, -1)}.
+
+ ) : (
+ <>
+
+ Granted on {permissionLevelToResourceName(group.level)}:
+
+
+ {group.resolvedResourceIds.map(id => (
+
+
+
+ ))}
+
+ >
+ )}
+
- ))}
-
- {group.resolvedResourceIds == null ? (
-
- Not granted on any {permissionLevelToResourceName(group.level).slice(0, -1)}.
-
- ) : (
- <>
-
- Granted on {permissionLevelToResourceName(group.level)}:
-
-
- {group.resolvedResourceIds.map(id => (
-
-
-
- ))}
-
- >
- )}
-
-
-
-
+ ),
+ },
+ ]}
+ />
);
}
diff --git a/packages/web/app/src/components/target/alerts/alert-form.tsx b/packages/web/app/src/components/target/alerts/alert-form.tsx
index f6eb7fc719c..2b3e31ef884 100644
--- a/packages/web/app/src/components/target/alerts/alert-form.tsx
+++ b/packages/web/app/src/components/target/alerts/alert-form.tsx
@@ -4,12 +4,7 @@ import { Plus, X } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useMutation, useQuery } from 'urql';
import { z } from 'zod';
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from '@/components/base/accordion/accordion';
+import { Accordion } from '@/components/base/accordion/accordion';
import { Button } from '@/components/base/button/button';
import { Card } from '@/components/base/card/card';
import { Select } from '@/components/base/floating/select/select';
@@ -870,77 +865,87 @@ export function AlertForm(props: AlertFormProps) {
clipToCurrentWindow
/>
-
-
-
-
-
-
{
- const isLoading = savedFiltersQuery.fetching;
- const savedFilterOptions = [
- {
- value: '',
- label: isLoading
- ? 'Loading filters...'
- : 'No filter (all operations)',
- },
- ...(savedFiltersQuery.data?.target?.savedFilters?.edges?.map(edge => ({
- value: edge.node.id,
- label: edge.node.name,
- })) ?? []),
- ];
-
- return (
+
+ {
+ const isLoading = savedFiltersQuery.fetching;
+ const savedFilterOptions = [
+ {
+ value: '',
+ label: isLoading
+ ? 'Loading filters...'
+ : 'No filter (all operations)',
+ },
+ ...(savedFiltersQuery.data?.target?.savedFilters?.edges?.map(
+ edge => ({
+ value: edge.node.id,
+ label: edge.node.name,
+ }),
+ ) ?? []),
+ ];
+
+ return (
+
+
+
+ 10}
+ onSurface="raised"
+ />
+
+
+
+ );
+ }}
+ />
+ (
-
+
- 10}
- onSurface="raised"
- />
+
-
+
+ Wait for the condition to exist for{' '}
+
+ {field.value || '0'}
+ {' '}
+ minutes before firing. Helps prevent false alarms from brief
+ spikes. Leave at 0 to fire as soon as the condition holds for
+ two consecutive evaluations (recommended for alert ranges
+ greater than 1 day).
+ >
+ }
+ />
- );
- }}
- />
- (
-
-
-
-
-
-
- Wait for the condition to exist for{' '}
-
- {field.value || '0'}
- {' '}
- minutes before firing. Helps prevent false alarms from brief
- spikes. Leave at 0 to fire as soon as the condition holds for two
- consecutive evaluations (recommended for alert ranges greater than
- 1 day).
- >
- }
- />
-
- )}
- />
-
-
-
-
+ )}
+ />
+
+ ),
+ },
+ ]}
+ />
diff --git a/packages/web/app/src/components/target/history/errors-and-changes.tsx b/packages/web/app/src/components/target/history/errors-and-changes.tsx
index 27ddeb47715..969448fb983 100644
--- a/packages/web/app/src/components/target/history/errors-and-changes.tsx
+++ b/packages/web/app/src/components/target/history/errors-and-changes.tsx
@@ -3,6 +3,7 @@ import { clsx } from 'clsx';
import { format } from 'date-fns';
import { BoxIcon, CheckIcon } from 'lucide-react';
import reactStringReplace from 'react-string-replace';
+import { Accordion } from '@/components/base/accordion/accordion';
import { DataTable } from '@/components/base/data-table/data-table';
import { DataTableCell } from '@/components/base/data-table/data-table-cell';
import { Popover } from '@/components/base/floating/popover/popover';
@@ -10,13 +11,6 @@ import { Tooltip } from '@/components/base/floating/tooltip/tooltip';
import { ScrollArea } from '@/components/base/scroll-area/scroll-area';
import { Label, Label as LegacyLabel } from '@/components/common';
import { CompositionErrorsPopover } from '@/components/target/history/composition-errors-popover';
-import {
- Accordion,
- AccordionContent,
- AccordionHeader,
- AccordionItem,
- AccordionTrigger,
-} from '@/components/ui/accordion';
import { Button } from '@/components/ui/button';
import { Heading } from '@/components/ui/heading';
import { PulseIcon } from '@/components/ui/icon';
@@ -230,10 +224,11 @@ function ChangeItem(
);
return (
-
-
-
-
+
-
-
-
- {change.approval && (
-
- )}
- {'usageStatistics' in change && change.usageStatistics && metadata ? (
-
-
- Affected Operations (based on usage)
-
-
-
- Top 10 operations and clients affected by this change based on usage data.
-
- {metadata && (
-
- See{' '}
- {metadata.settings.targets.map((target, index, arr) => (
- <>
- {!target.target ? (
-
- ) : (
-
- {target.slug}
-
- )}
- {index === arr.length - 1
- ? null
- : index === arr.length - 2
- ? ' and '
- : ', '}
- >
- ))}{' '}
- target insights for live usage data.
-
- )}
-
-
- {'affectedAppDeployments' in change &&
- change.affectedAppDeployments?.edges?.length ? (
-
-
+ {change.approval && (
+
+ )}
+ {'usageStatistics' in change && change.usageStatistics && metadata ? (
+
+
+ Affected Operations (based on usage)
+
+
+
+ Top 10 operations and clients affected by this change based on usage data.
+
+ {metadata && (
+
+ See{' '}
+ {metadata.settings.targets.map((target, index, arr) => (
+ <>
+ {!target.target ? (
+
+ ) : (
+
+ {target.slug}
+
+ )}
+ {index === arr.length - 1
+ ? null
+ : index === arr.length - 2
+ ? ' and '
+ : ', '}
+ >
+ ))}{' '}
+ target insights for live usage data.
+
+ )}
+
+
+ {'affectedAppDeployments' in change &&
+ change.affectedAppDeployments?.edges?.length ? (
+
+ ) : null}
- ) : null}
+ ) : 'affectedAppDeployments' in change &&
+ change.affectedAppDeployments?.edges?.length ? (
+
+ ) : (
+ <>
+ {change.severityReason ??
+ `No details available for this ${
+ change.severityLevel === SeverityLevelType.Breaking ? 'breaking ' : ''
+ }change.`}
+ >
+ )}
- ) : 'affectedAppDeployments' in change && change.affectedAppDeployments?.edges?.length ? (
-
- ) : (
- <>
- {change.severityReason ??
- `No details available for this ${
- change.severityLevel === SeverityLevelType.Breaking ? 'breaking ' : ''
- }change.`}
- >
- )}
-
-
-
+ ),
+ },
+ ]}
+ />
);
}
diff --git a/packages/web/app/src/components/target/proposals/change-detail.tsx b/packages/web/app/src/components/target/proposals/change-detail.tsx
index fefb63431c7..dde72ca3ead 100644
--- a/packages/web/app/src/components/target/proposals/change-detail.tsx
+++ b/packages/web/app/src/components/target/proposals/change-detail.tsx
@@ -1,13 +1,7 @@
import { ReactNode } from 'react';
+import { Accordion } from '@/components/base/accordion/accordion';
import { Popover } from '@/components/base/floating/popover/popover';
-import {
- AccordionContent,
- AccordionHeader,
- AccordionItem,
- AccordionTrigger,
-} from '@/components/ui/accordion';
import { Button } from '@/components/ui/button';
-import { Accordion } from '@/components/v2';
import { MergeStatus } from '@/pages/target-proposal-details';
import type { Change } from '@graphql-inspector/core';
import { ComponentNoneIcon, ExclamationTriangleIcon, InfoCircledIcon } from '@radix-ui/react-icons';
@@ -19,21 +13,20 @@ export function ProposalChangeDetail(props: {
icon?: ReactNode;
}) {
return (
-
-
-
-
-
+
{labelize(props.change.message)}
{props.icon}
-
-
-
- {props.error?.message ?? <>No details available for this change.>}
-
-
-
+ ),
+ content: props.error?.message ?? 'No details available for this change.',
+ },
+ ]}
+ />
);
}
diff --git a/packages/web/app/src/components/target/settings/registry-access-token.tsx b/packages/web/app/src/components/target/settings/registry-access-token.tsx
index 73dfd56828d..9b52b411f56 100644
--- a/packages/web/app/src/components/target/settings/registry-access-token.tsx
+++ b/packages/web/app/src/components/target/settings/registry-access-token.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useForm, UseFormReturn } from 'react-hook-form';
import { useMutation } from 'urql';
import { z } from 'zod';
+import { Accordion } from '@/components/base/accordion/accordion';
import { Input } from '@/components/base/input/input';
import { Dialog } from '@/components/base/overlays/dialog/dialog';
import { useToast } from '@/components/base/toast/toast';
@@ -10,7 +11,6 @@ import { Button } from '@/components/ui/button';
import { Callout } from '@/components/ui/callout';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
import { InputCopy } from '@/components/ui/input-copy';
-import { Accordion } from '@/components/v2/accordion';
import { graphql } from '@/gql';
import { TargetAccessScope } from '@/gql/graphql';
import { RegistryAccessScope } from '@/lib/access/common';
@@ -228,31 +228,36 @@ export function GenerateTokenContent(props: {
)}
/>
-
-
- Registry & Usage
-
- true}
- onChange={value => {
- if (value === 'no-access') {
- props.setSelectedScope('no-access');
- return;
- }
- props.setSelectedScope(value);
- }}
- possibleScope={Object.values(RegistryAccessScope.mapping)}
- initialScope={props.selectedScope}
- selectedScope={props.selectedScope}
- />
-
-
-
+
true}
+ onChange={value => {
+ if (value === 'no-access') {
+ props.setSelectedScope('no-access');
+ return;
+ }
+ props.setSelectedScope(value);
+ }}
+ possibleScope={Object.values(RegistryAccessScope.mapping)}
+ initialScope={props.selectedScope}
+ selectedScope={props.selectedScope}
+ />
+ ),
+ },
+ ]}
+ />
Cancel
diff --git a/packages/web/app/src/components/ui/accordion.tsx b/packages/web/app/src/components/ui/accordion.tsx
deleted file mode 100644
index 585557ccfbb..00000000000
--- a/packages/web/app/src/components/ui/accordion.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import { ChevronDown } from 'lucide-react';
-import { cn } from '@/lib/utils';
-import * as AccordionPrimitive from '@radix-ui/react-accordion';
-
-const Accordion = AccordionPrimitive.Root;
-const AccordionHeader = AccordionPrimitive.Header;
-
-const AccordionItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-AccordionItem.displayName = 'AccordionItem';
-
-const AccordionTriggerPrimitive = AccordionPrimitive.Trigger;
-
-const AccordionTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
- svg]:rotate-180',
- className,
- )}
- {...props}
- >
- {children}
-
-
-));
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
-
-const AccordionContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
- {children}
-
-));
-
-AccordionContent.displayName = AccordionPrimitive.Content.displayName;
-
-export {
- Accordion,
- AccordionItem,
- AccordionTrigger,
- AccordionTriggerPrimitive,
- AccordionContent,
- AccordionHeader,
-};
diff --git a/packages/web/app/src/components/v2/accordion.tsx b/packages/web/app/src/components/v2/accordion.tsx
deleted file mode 100644
index a5d8ffcf5a6..00000000000
--- a/packages/web/app/src/components/v2/accordion.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { ComponentProps, ReactElement, ReactNode } from 'react';
-import { cx } from 'class-variance-authority';
-import { clsx } from 'clsx';
-import * as A from '@radix-ui/react-accordion';
-import { ChevronDownIcon } from '@radix-ui/react-icons';
-
-type AccordionProps = Partial<
- Pick, 'disabled' | 'value' | 'onValueChange'>
->;
-
-function Wrapper({
- children,
- type = 'single',
- defaultValue,
- ...props
-}: {
- defaultValue?: string | Array;
- children: ReactNode;
- className?: string;
-} & AccordionProps &
- (
- | { type?: 'single'; defaultValue?: string }
- | { type: 'multiple'; defaultValue?: Array }
- )): ReactElement {
- return (
- // @ts-expect-error docs say array is okay
-
- {children}
-
- );
-}
-
-function Item({
- value,
- className,
- children,
- ...props
-}: {
- value: string;
- children: ReactNode;
- className?: string;
-}): ReactElement {
- return (
-
- {children}
-
- );
-}
-
-function Header({
- children,
- className,
- triggerClassName,
-}: {
- children: ReactNode;
- className?: string;
- triggerClassName?: string;
-}): ReactElement {
- return (
-
-
- {children}
-
-
-
- );
-}
-
-function Content({
- children,
- className,
-}: {
- children: ReactNode;
- className?: string;
-}): ReactElement {
- return (
-
- {children}
-
- );
-}
-
-export const Accordion = Object.assign(Wrapper, { Item, Header, Content });
diff --git a/packages/web/app/src/components/v2/index.ts b/packages/web/app/src/components/v2/index.ts
index e6a7624ba3f..7e804b3758d 100644
--- a/packages/web/app/src/components/v2/index.ts
+++ b/packages/web/app/src/components/v2/index.ts
@@ -1,4 +1,3 @@
-export { Accordion } from '@/components/v2/accordion';
export { DataWrapper } from '@/components/v2/data-wrapper';
export { DiffEditor } from '@/components/v2/diff-editor';
export { default as Stat } from '@/components/v2/stat';
diff --git a/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx b/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx
index 821b53ff0af..f2f52970a9e 100644
--- a/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx
+++ b/packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx
@@ -1,7 +1,8 @@
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { FolderIcon, FolderOpenIcon, SquareTerminalIcon } from 'lucide-react';
import { useMutation, useQuery } from 'urql';
+import { Accordion } from '@/components/base/accordion/accordion';
import { Menu } from '@/components/base/floating/menu/menu';
import { Tooltip } from '@/components/base/floating/tooltip/tooltip';
import { useToast } from '@/components/base/toast/toast';
@@ -9,13 +10,6 @@ import { CreateCollectionModal } from '@/components/target/laboratory/create-col
import { DeleteCollectionModal } from '@/components/target/laboratory/delete-collection-modal';
import { DeleteOperationModal } from '@/components/target/laboratory/delete-operation-modal';
import { EditOperationModal } from '@/components/target/laboratory/edit-operation-modal';
-import {
- Accordion,
- AccordionContent,
- AccordionHeader,
- AccordionItem,
- AccordionTriggerPrimitive,
-} from '@/components/ui/accordion';
import { Button } from '@/components/ui/button';
import { PlusIcon } from '@/components/ui/icon';
import { Link } from '@/components/ui/link';
@@ -31,6 +25,9 @@ import { useCollections } from './use-collections';
import { useCurrentOperation } from './use-current-operation';
import { useSyncOperationState } from './use-sync-operation-state';
+// The accordion hands out no ref, so the operation opened from the URL is found by id instead.
+const COLLECTIONS_ID = 'laboratory-collections';
+
const CreateOperationMutation = graphql(`
mutation CreateOperation(
$selector: TargetSelectorInput!
@@ -144,7 +141,6 @@ export function Content() {
});
const router = useRouter();
const [accordionValue, setAccordionValue] = useState([]);
- const containerRef = useRef(null);
const [isScrolled, setIsScrolled] = useState(false);
const copyToClipboard = useClipboard();
@@ -289,7 +285,9 @@ export function Content() {
setAccordionValue([initialSelectedCollection]);
setTimeout(() => {
- const link = containerRef.current!.querySelector(`a[href$="${queryParamsOperationId}"]`);
+ const link = document
+ .getElementById(COLLECTIONS_ID)
+ ?.querySelector(`a[href$="${queryParamsOperationId}"]`);
if (link) {
link.scrollIntoView();
@@ -298,64 +296,67 @@ export function Content() {
}, 150);
}, [initialSelectedCollection]);
- const renderedCollections = collections.map(collection => (
-
-
-
-
-
- {collection.name}
-
- {shouldShowMenu && (
-
-
-
- }
- sections={[
- [
- {
- label: 'Add operation',
- trailingIcon: PlusIcon,
- onClick: () => void addOperation(collection.id),
- disabled: createOperationState.fetching,
- attrs: { 'data-cy': 'add-operation-to-collection' },
- },
- ],
- [
- {
- label: 'Edit',
- onClick: () => {
- setCollectionId(collection.id);
- toggleCollectionModal();
- },
- attrs: { 'data-cy': 'edit-collection' },
- },
- {
- label: 'Delete',
- variant: 'destructiveAction',
- onClick: () => {
- setCollectionId(collection.id);
- toggleDeleteCollectionModalOpen();
- },
- attrs: { 'data-cy': 'delete-collection' },
- },
- ],
- ]}
- />
+ const renderedCollections = collections.map(collection => ({
+ value: collection.id,
+ label: (
+
+ {accordionValue.includes(collection.id) ? (
+
+ ) : (
+
)}
-
-
+ {collection.name}
+
+ ),
+ attrs: { 'data-cy': 'collection-item' },
+ triggerAttrs: { 'data-cy': 'collection-item-trigger' },
+ action: shouldShowMenu ? (
+
+
+
+ }
+ sections={[
+ [
+ {
+ label: 'Add operation',
+ trailingIcon: PlusIcon,
+ onClick: () => void addOperation(collection.id),
+ disabled: createOperationState.fetching,
+ attrs: { 'data-cy': 'add-operation-to-collection' },
+ },
+ ],
+ [
+ {
+ label: 'Edit',
+ onClick: () => {
+ setCollectionId(collection.id);
+ toggleCollectionModal();
+ },
+ attrs: { 'data-cy': 'edit-collection' },
+ },
+ {
+ label: 'Delete',
+ variant: 'destructiveAction',
+ onClick: () => {
+ setCollectionId(collection.id);
+ toggleDeleteCollectionModalOpen();
+ },
+ attrs: { 'data-cy': 'delete-collection' },
+ },
+ ],
+ ]}
+ />
+ ) : undefined,
+ content: (
+
{collection.operations.edges.length ? (
collection.operations.edges.map(({ node }) => (
@@ -433,9 +434,9 @@ export function Content() {
Add Operation
)}
-
-
- ));
+
+ ),
+ }));
const target = query.data?.target;
@@ -476,13 +477,14 @@ export function Content() {
) : collections.length ? (
- {renderedCollections}
-
+ items={renderedCollections}
+ attrs={{ id: COLLECTIONS_ID }}
+ />
) : (
diff --git a/packages/web/app/src/pages/target.tsx b/packages/web/app/src/pages/target.tsx
index 8be88327e65..460e7e05573 100644
--- a/packages/web/app/src/pages/target.tsx
+++ b/packages/web/app/src/pages/target.tsx
@@ -1,6 +1,8 @@
import { ReactElement } from 'react';
import { XIcon } from 'lucide-react';
import { useQuery } from 'urql';
+import { Accordion } from '@/components/base/accordion/accordion';
+import { Card } from '@/components/base/card/card';
import { Select } from '@/components/base/floating/select/select';
import { Page, TargetLayout } from '@/components/layouts/target';
import { Button } from '@/components/ui/button';
@@ -8,7 +10,6 @@ import { EmptyList, noSchema, NoSchemaVersion } from '@/components/ui/empty-list
import { Meta } from '@/components/ui/meta';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
-import { Accordion } from '@/components/v2/accordion';
import { GraphQLBlock, GraphQLHighlight } from '@/components/v2/graphql-block';
import { DocumentType, FragmentType, graphql, useFragment } from '@/gql';
import { ProjectType } from '@/gql/graphql';
@@ -34,23 +35,23 @@ function isCompositeSchema(
return schema.__typename === 'CompositeSchema';
}
-function SchemaBlock({ schema }: { schema: CompositeSchema }) {
+/** The service's name, an anchor for deep links, and its URL. */
+function serviceHeader(schema: CompositeSchema) {
return (
-
-
-
-
- {schema.service ?? 'SDL'}
-
- {schema.url ?
{schema.url}
: null}
-
-
-
-
-
-
-
-
+
+
+ {schema.service ?? 'SDL'}
+
+ {schema.url ?
{schema.url}
: null}
+
+ );
+}
+
+function serviceSchema(schema: CompositeSchema) {
+ return (
+
+
+
);
}
@@ -71,11 +72,14 @@ function Schemas(props: { schemas?: readonly CompositeSchema[]; schema?: SingleS
if (props.schemas.length > 1) {
return (
-
- {props.schemas.map(schema => (
-
- ))}
-
+
({
+ value: schema.id,
+ label: serviceHeader(schema),
+ content: serviceSchema(schema),
+ }))}
+ />
);
}
@@ -90,10 +94,12 @@ function Schemas(props: { schemas?: readonly CompositeSchema[]; schema?: SingleS
);
}
+ // One service has nothing to collapse, so it is a card, not an accordion locked open.
return (
-
-
-
+
+ {serviceHeader(schema)}
+ {serviceSchema(schema)}
+
);
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b1cd19b3a6..5337725a7f7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1769,7 +1769,7 @@ importers:
version: 1.1.0(pino@10.3.0)
'@graphql-hive/plugin-opentelemetry':
specifier: 1.4.35
- version: 1.4.35(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)(ws@8.21.0)
+ version: 1.4.35(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)
'@opentelemetry/api':
specifier: 1.9.1
version: 1.9.1
@@ -2139,9 +2139,6 @@ importers:
'@pierre/diffs':
specifier: 1.3.5
version: 1.3.5(@shikijs/themes@3.23.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-accordion':
- specifier: 1.2.2
- version: 1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-icons':
specifier: 1.3.2
version: 1.3.2(react@18.3.1)
@@ -7453,9 +7450,6 @@ packages:
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
- '@radix-ui/primitive@1.1.1':
- resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
-
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
@@ -7485,19 +7479,6 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-accordion@1.2.2':
- resolution: {integrity: sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==}
- peerDependencies:
- '@types/react': '*'
- '@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
- '@types/react-dom':
- optional: true
-
'@radix-ui/react-alert-dialog@1.1.15':
resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
peerDependencies:
@@ -7576,32 +7557,6 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-collapsible@1.1.2':
- resolution: {integrity: sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==}
- peerDependencies:
- '@types/react': '*'
- '@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
- '@types/react-dom':
- optional: true
-
- '@radix-ui/react-collection@1.1.1':
- resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==}
- peerDependencies:
- '@types/react': '*'
- '@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
- '@types/react-dom':
- optional: true
-
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -7646,15 +7601,6 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-context@1.1.1':
- resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
@@ -7677,15 +7623,6 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-direction@1.1.0':
- resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
'@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies:
@@ -7774,15 +7711,6 @@ packages:
peerDependencies:
react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc
- '@radix-ui/react-id@1.1.0':
- resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
@@ -7935,19 +7863,6 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-presence@1.1.2':
- resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==}
- peerDependencies:
- '@types/react': '*'
- '@types/react-dom': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
- '@types/react-dom':
- optional: true
-
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
peerDependencies:
@@ -8222,15 +8137,6 @@ packages:
'@types/react-dom':
optional: true
- '@radix-ui/react-use-callback-ref@1.1.0':
- resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@@ -8240,15 +8146,6 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-use-controllable-state@1.1.0':
- resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
'@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
@@ -8285,15 +8182,6 @@ packages:
'@types/react':
optional: true
- '@radix-ui/react-use-layout-effect@1.1.0':
- resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
- peerDependencies:
- '@types/react': '*'
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- '@types/react':
- optional: true
-
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
@@ -21901,6 +21789,57 @@ snapshots:
- pino
- winston
+ '@graphql-hive/gateway-runtime@2.9.10(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)':
+ dependencies:
+ '@envelop/core': 5.5.1
+ '@envelop/disable-introspection': 9.0.0(@envelop/core@5.5.1)(graphql@16.14.2)
+ '@envelop/generic-auth': 11.0.0(@envelop/core@5.5.1)(graphql@16.14.2)
+ '@envelop/instrumentation': 1.0.0
+ '@graphql-hive/core': 0.21.1(graphql@16.14.2)(pino@10.3.0)
+ '@graphql-hive/logger': 1.1.1(pino@10.3.0)
+ '@graphql-hive/pubsub': 2.1.1(ioredis@5.10.1)
+ '@graphql-hive/signal': 2.0.0
+ '@graphql-hive/yoga': 0.48.2(graphql-yoga@5.22.0(graphql@16.14.2))(graphql@16.14.2)(pino@10.3.0)
+ '@graphql-mesh/cross-helpers': 0.4.14(graphql@16.14.2)
+ '@graphql-mesh/fusion-runtime': 1.10.10(@types/node@25.5.0)(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)
+ '@graphql-mesh/hmac-upstream-signature': 2.0.12(graphql@16.14.2)(ioredis@5.10.1)
+ '@graphql-mesh/plugin-response-cache': 0.104.43(graphql@16.14.2)(ioredis@5.10.1)
+ '@graphql-mesh/transport-common': 1.0.17(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)
+ '@graphql-mesh/types': 0.104.28(graphql@16.14.2)(ioredis@5.10.1)
+ '@graphql-mesh/utils': 0.104.36(graphql@16.14.2)(ioredis@5.10.1)
+ '@graphql-tools/batch-delegate': 10.0.26(graphql@16.14.2)
+ '@graphql-tools/delegate': 12.0.19(graphql@16.14.2)
+ '@graphql-tools/executor-common': 1.0.6(graphql@16.14.2)
+ '@graphql-tools/executor-http': 3.3.0(@types/node@25.5.0)(graphql@16.14.2)
+ '@graphql-tools/federation': 4.4.8(@types/node@25.5.0)(graphql@16.14.2)
+ '@graphql-tools/stitch': 10.2.1(graphql@16.14.2)
+ '@graphql-tools/utils': 11.1.0(graphql@16.14.2)
+ '@graphql-tools/wrap': 11.1.18(graphql@16.14.2)
+ '@graphql-yoga/plugin-apollo-usage-report': 0.16.0(@envelop/core@5.5.1)(graphql-yoga@5.22.0(graphql@16.14.2))(graphql@16.14.2)
+ '@graphql-yoga/plugin-csrf-prevention': 3.16.2(graphql-yoga@5.22.0(graphql@16.14.2))
+ '@graphql-yoga/plugin-defer-stream': 3.16.2(graphql-yoga@5.22.0(graphql@16.14.2))(graphql@16.14.2)
+ '@graphql-yoga/plugin-persisted-operations': 3.16.2(graphql-yoga@5.22.0(graphql@16.14.2))(graphql@16.14.2)
+ '@types/node': 25.5.0
+ '@whatwg-node/disposablestack': 0.0.6
+ '@whatwg-node/fetch': 0.10.13
+ '@whatwg-node/promise-helpers': 1.3.2
+ '@whatwg-node/server': 0.11.0
+ '@whatwg-node/server-plugin-cookies': 1.0.7(@whatwg-node/server@0.11.0)
+ graphql: 16.14.2
+ graphql-ws: 6.0.6(graphql@16.14.2)(ws@8.21.0)
+ graphql-yoga: 5.22.0(graphql@16.14.2)
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - '@fastify/websocket'
+ - '@logtape/logtape'
+ - '@nats-io/nats-core'
+ - crossws
+ - ioredis
+ - pino
+ - uWebSockets.js
+ - winston
+ - ws
+
'@graphql-hive/gateway-runtime@2.9.10(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)(ws@8.21.0)':
dependencies:
'@envelop/core': 5.5.1
@@ -21952,6 +21891,57 @@ snapshots:
- winston
- ws
+ '@graphql-hive/gateway-runtime@2.9.10(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)':
+ dependencies:
+ '@envelop/core': 5.5.1
+ '@envelop/disable-introspection': 9.0.0(@envelop/core@5.5.1)(graphql@16.9.0)
+ '@envelop/generic-auth': 11.0.0(@envelop/core@5.5.1)(graphql@16.9.0)
+ '@envelop/instrumentation': 1.0.0
+ '@graphql-hive/core': 0.21.1(graphql@16.9.0)(pino@10.3.0)
+ '@graphql-hive/logger': 1.1.1(pino@10.3.0)
+ '@graphql-hive/pubsub': 2.1.1(ioredis@5.10.1)
+ '@graphql-hive/signal': 2.0.0
+ '@graphql-hive/yoga': 0.48.2(graphql-yoga@5.22.0(graphql@16.9.0))(graphql@16.9.0)(pino@10.3.0)
+ '@graphql-mesh/cross-helpers': 0.4.14(graphql@16.9.0)
+ '@graphql-mesh/fusion-runtime': 1.10.10(@types/node@25.5.0)(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)
+ '@graphql-mesh/hmac-upstream-signature': 2.0.12(graphql@16.9.0)(ioredis@5.10.1)
+ '@graphql-mesh/plugin-response-cache': 0.104.43(graphql@16.9.0)(ioredis@5.10.1)
+ '@graphql-mesh/transport-common': 1.0.17(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)
+ '@graphql-mesh/types': 0.104.28(graphql@16.9.0)(ioredis@5.10.1)
+ '@graphql-mesh/utils': 0.104.36(graphql@16.9.0)(ioredis@5.10.1)
+ '@graphql-tools/batch-delegate': 10.0.26(graphql@16.9.0)
+ '@graphql-tools/delegate': 12.0.19(graphql@16.9.0)
+ '@graphql-tools/executor-common': 1.0.6(graphql@16.9.0)
+ '@graphql-tools/executor-http': 3.3.0(@types/node@25.5.0)(graphql@16.9.0)
+ '@graphql-tools/federation': 4.4.8(@types/node@25.5.0)(graphql@16.9.0)
+ '@graphql-tools/stitch': 10.2.1(graphql@16.9.0)
+ '@graphql-tools/utils': 11.1.0(graphql@16.9.0)
+ '@graphql-tools/wrap': 11.1.18(graphql@16.9.0)
+ '@graphql-yoga/plugin-apollo-usage-report': 0.16.0(@envelop/core@5.5.1)(graphql-yoga@5.22.0(graphql@16.9.0))(graphql@16.9.0)
+ '@graphql-yoga/plugin-csrf-prevention': 3.16.2(graphql-yoga@5.22.0(graphql@16.9.0))
+ '@graphql-yoga/plugin-defer-stream': 3.16.2(graphql-yoga@5.22.0(graphql@16.9.0))(graphql@16.9.0)
+ '@graphql-yoga/plugin-persisted-operations': 3.16.2(graphql-yoga@5.22.0(graphql@16.9.0))(graphql@16.9.0)
+ '@types/node': 25.5.0
+ '@whatwg-node/disposablestack': 0.0.6
+ '@whatwg-node/fetch': 0.10.13
+ '@whatwg-node/promise-helpers': 1.3.2
+ '@whatwg-node/server': 0.11.0
+ '@whatwg-node/server-plugin-cookies': 1.0.7(@whatwg-node/server@0.11.0)
+ graphql: 16.9.0
+ graphql-ws: 6.0.6(graphql@16.9.0)(ws@8.21.0)
+ graphql-yoga: 5.22.0(graphql@16.9.0)
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - '@fastify/websocket'
+ - '@logtape/logtape'
+ - '@nats-io/nats-core'
+ - crossws
+ - ioredis
+ - pino
+ - uWebSockets.js
+ - winston
+ - ws
+
'@graphql-hive/gateway-runtime@2.9.10(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)(ws@8.21.3)':
dependencies:
'@envelop/core': 5.5.1
@@ -22143,6 +22133,44 @@ snapshots:
- pino
- winston
+ '@graphql-hive/plugin-opentelemetry@1.4.35(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)':
+ dependencies:
+ '@graphql-hive/core': 0.21.1(graphql@16.14.2)(pino@10.3.0)
+ '@graphql-hive/gateway-runtime': 2.9.10(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)
+ '@graphql-hive/logger': 1.1.1(pino@10.3.0)
+ '@graphql-mesh/cross-helpers': 0.4.14(graphql@16.14.2)
+ '@graphql-mesh/transport-common': 1.0.17(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)
+ '@graphql-mesh/types': 0.104.28(graphql@16.14.2)(ioredis@5.10.1)
+ '@graphql-mesh/utils': 0.104.36(graphql@16.14.2)(ioredis@5.10.1)
+ '@graphql-tools/utils': 11.2.2(graphql@16.14.2)
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.219.0
+ '@opentelemetry/auto-instrumentations-node': 0.77.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))
+ '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/exporter-trace-otlp-grpc': 0.219.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/exporter-trace-otlp-http': 0.219.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/instrumentation': 0.219.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-logs': 0.219.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-node': 0.219.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.43.0
+ '@whatwg-node/promise-helpers': 1.3.2
+ graphql: 16.14.2
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - '@fastify/websocket'
+ - '@logtape/logtape'
+ - '@nats-io/nats-core'
+ - crossws
+ - ioredis
+ - pino
+ - supports-color
+ - uWebSockets.js
+ - winston
+ - ws
+
'@graphql-hive/plugin-opentelemetry@1.4.35(graphql@16.14.2)(ioredis@5.10.1)(pino@10.3.0)(ws@8.21.0)':
dependencies:
'@graphql-hive/core': 0.21.1(graphql@16.14.2)(pino@10.3.0)
@@ -22184,7 +22212,7 @@ snapshots:
'@graphql-hive/plugin-opentelemetry@1.4.35(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)':
dependencies:
'@graphql-hive/core': 0.21.1(graphql@16.9.0)(pino@10.3.0)
- '@graphql-hive/gateway-runtime': 2.9.10(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)(ws@8.21.3)
+ '@graphql-hive/gateway-runtime': 2.9.10(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)
'@graphql-hive/logger': 1.1.1(pino@10.3.0)
'@graphql-mesh/cross-helpers': 0.4.14(graphql@16.9.0)
'@graphql-mesh/transport-common': 1.0.17(graphql@16.9.0)(ioredis@5.10.1)(pino@10.3.0)
@@ -27544,8 +27572,6 @@ snapshots:
'@radix-ui/number@1.1.1': {}
- '@radix-ui/primitive@1.1.1': {}
-
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
@@ -27574,23 +27600,6 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
- '@radix-ui/react-accordion@1.2.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
- dependencies:
- '@radix-ui/primitive': 1.1.1
- '@radix-ui/react-collapsible': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-direction': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
- optionalDependencies:
- '@types/react': 18.3.18
- '@types/react-dom': 18.3.5(@types/react@18.3.18)
-
'@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -27668,34 +27677,6 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
- '@radix-ui/react-collapsible@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
- dependencies:
- '@radix-ui/primitive': 1.1.1
- '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-id': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
- optionalDependencies:
- '@types/react': 18.3.18
- '@types/react-dom': 18.3.5(@types/react@18.3.18)
-
- '@radix-ui/react-collection@1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
- dependencies:
- '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- '@radix-ui/react-slot': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
- optionalDependencies:
- '@types/react': 18.3.18
- '@types/react-dom': 18.3.5(@types/react@18.3.18)
-
'@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
@@ -27734,12 +27715,6 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
- '@radix-ui/react-context@1.1.1(@types/react@18.3.18)(react@18.3.1)':
- dependencies:
- react: 18.3.1
- optionalDependencies:
- '@types/react': 18.3.18
-
'@radix-ui/react-context@1.1.2(@types/react@18.3.18)(react@18.3.1)':
dependencies:
react: 18.3.1
@@ -27768,12 +27743,6 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
- '@radix-ui/react-direction@1.1.0(@types/react@18.3.18)(react@18.3.1)':
- dependencies:
- react: 18.3.1
- optionalDependencies:
- '@types/react': 18.3.18
-
'@radix-ui/react-direction@1.1.1(@types/react@18.3.18)(react@18.3.1)':
dependencies:
react: 18.3.1
@@ -27860,13 +27829,6 @@ snapshots:
dependencies:
react: 18.3.1
- '@radix-ui/react-id@1.1.0(@types/react@18.3.18)(react@18.3.1)':
- dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- react: 18.3.1
- optionalDependencies:
- '@types/react': 18.3.18
-
'@radix-ui/react-id@1.1.1(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.18)(react@18.3.1)
@@ -28054,16 +28016,6 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
- '@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
- dependencies:
- '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.18)(react@18.3.1)
- '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
- optionalDependencies:
- '@types/react': 18.3.18
- '@types/react-dom': 18.3.5(@types/react@18.3.18)
-
'@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.18)(react@18.3.1)
@@ -28362,25 +28314,12 @@ snapshots:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
- '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.18)(react@18.3.1)':
- dependencies:
- react: 18.3.1
- optionalDependencies:
- '@types/react': 18.3.18
-
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.18)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.18
- '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.18)(react@18.3.1)':
- dependencies:
- '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.18)(react@18.3.1)
- react: 18.3.1
- optionalDependencies:
- '@types/react': 18.3.18
-
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.18)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.18)(react@18.3.1)
@@ -28410,12 +28349,6 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.18
- '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.18)(react@18.3.1)':
- dependencies:
- react: 18.3.1
- optionalDependencies:
- '@types/react': 18.3.18
-
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.18)(react@18.3.1)':
dependencies:
react: 18.3.1