From 8f9aeed330796a953d9bc21528856fe12782a1cb Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 19:04:23 -0700 Subject: [PATCH] fix(settings): chip-consistency + shared credential-style resource row - chip: move default/filled hover into active-keyed compound variants so raw chipVariants({...}) renders identically to cn(chipVariants({...})); fixes sidebar/settings-sidebar active-hover divergence (no change to cn-wrapped consumers) - integrations: 'Explore in chat' uses active chip (darkens on hover) instead of floating text - data-retention: fold the retention-policies helper into the page description; drop the redundant wrapper - settings: extract shared SettingsResourceRow (rounded icon tile + title/desc + trailing, icons normalized to 20px); migrate recently-deleted, byok, and credential-sets onto it; recently-deleted actions are now Chips --- .../showcase-with-explore.tsx | 1 + .../components/byok/byok-key-manager.tsx | 23 ++-- .../credential-sets/credential-sets.tsx | 111 +++++++----------- .../recently-deleted/recently-deleted.tsx | 70 ++++++----- .../components/settings-resource-row/index.ts | 1 + .../settings-resource-row.tsx | 47 ++++++++ .../[workspaceId]/settings/navigation.ts | 3 +- .../components/data-retention-settings.tsx | 63 +++++----- packages/emcn/src/components/chip/chip.tsx | 20 +++- 9 files changed, 179 insertions(+), 160 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx index 892ccb598b2..23e99adca94 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/showcase-with-explore/showcase-with-explore.tsx @@ -30,6 +30,7 @@ export function ShowcaseWithExplore({ prompt }: ShowcaseWithExploreProps) {
{ storeCuratedPrompt(prompt) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx index 8751e926383..20fa47d396a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx @@ -23,6 +23,7 @@ import { import { BYOKProviderKeysModal } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-provider-keys-modal' import { BYOKKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' +import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' const logger = createLogger('BYOKKeyManager') @@ -278,21 +279,13 @@ export function BYOKKeyManager(props: BYOKKeyManagerProps) { const Icon = provider.icon return ( -
-
-
- -
-
- {provider.name} - - {provider.description} - -
-
- - {renderActions(provider)} -
+ } + title={provider.name} + description={provider.description} + trailing={renderActions(provider)} + /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx index acdcc5eb116..bb285ffdd02 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets.tsx @@ -34,6 +34,7 @@ import { getUserRole } from '@/lib/workspaces/organization' import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { type CredentialSet, @@ -632,33 +633,23 @@ export function CredentialSets() {
{filteredInvitations.length > 0 && ( -
+
{filteredInvitations.map((invitation) => ( -
-
-
- {getProviderIcon(invitation.providerId)} -
-
- - {invitation.credentialSetName} - - - {invitation.organizationName} - -
-
- handleAcceptInvitation(invitation.token)} - disabled={acceptInvitation.isPending} - > - {acceptInvitation.isPending ? 'Accepting...' : 'Accept'} - -
+ icon={getProviderIcon(invitation.providerId)} + title={invitation.credentialSetName} + description={invitation.organizationName} + trailing={ + handleAcceptInvitation(invitation.token)} + disabled={acceptInvitation.isPending} + > + {acceptInvitation.isPending ? 'Accepting...' : 'Accept'} + + } + /> ))}
@@ -666,34 +657,24 @@ export function CredentialSets() { {filteredMemberships.length > 0 && ( -
+
{filteredMemberships.map((membership) => ( -
-
-
- {getProviderIcon(membership.providerId)} -
-
- - {membership.credentialSetName} - - - {membership.organizationName} - -
-
- - handleLeave(membership.credentialSetId, membership.credentialSetName) - } - disabled={leaveCredentialSet.isPending} - > - Leave - -
+ icon={getProviderIcon(membership.providerId)} + title={membership.credentialSetName} + description={membership.organizationName} + trailing={ + + handleLeave(membership.credentialSetId, membership.credentialSetName) + } + disabled={leaveCredentialSet.isPending} + > + Leave + + } + /> ))}
@@ -709,26 +690,14 @@ export function CredentialSets() { No polling groups created yet
) : ( -
+
{filteredOwnedSets.map((set) => ( -
-
-
- {getProviderIcon(set.providerId)} -
-
- - {set.name} - - - {set.memberCount} member{set.memberCount !== 1 ? 's' : ''} - -
-
-
+ icon={getProviderIcon(set.providerId)} + title={set.name} + description={`${set.memberCount} member${set.memberCount !== 1 ? 's' : ''}`} + trailing={ -
-
+ } + /> ))}
)} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx index b7f4661baaa..78f91e8510e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useMemo, useState } from 'react' -import { Button, ChipInput, ChipModalTabs } from '@sim/emcn' +import { Chip, ChipInput, ChipModalTabs } from '@sim/emcn' import { Folder, Search, Workflow } from '@sim/emcn/icons' import { toError } from '@sim/utils/errors' import { formatDate } from '@sim/utils/formatting' @@ -21,6 +21,7 @@ import { } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/search-params' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { useFolders, useRestoreFolder } from '@/hooks/queries/folders' import { useKnowledgeBasesQuery, useRestoreKnowledgeBase } from '@/hooks/queries/kb/knowledge' import { useRestoreTable, useTablesList } from '@/hooks/queries/tables' @@ -82,7 +83,7 @@ const SORT_OPTIONS: ColumnOption[] = [ { id: 'type', label: 'Type' }, ] -const ICON_CLASS = 'size-[14px]' +const ICON_CLASS = 'size-5' const RESOURCE_TYPE_TO_MOTHERSHIP: Partial< Record, MothershipResourceType> @@ -464,45 +465,40 @@ export function RecentlyDeleted() { const isRestored = restoredItems.has(resource.id) return ( -
- - -
- - {resource.name} - - + icon={} + title={resource.name} + description={ + <> {TYPE_LABEL[resource.type]} {' \u00b7 '} Deleted {formatDate(resource.deletedAt)} - -
- - {isRestoring ? ( - - ) : isRestored ? ( -
- Restored - -
- ) : ( - - )} -
+ + } + trailing={ + isRestoring ? ( + + Restoring... + + ) : isRestored ? ( +
+ Restored + handleView(resource)}> + View + +
+ ) : ( + void handleRestore(resource)} + className='shrink-0' + > + Restore + + ) + } + /> ) })}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/index.ts b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/index.ts new file mode 100644 index 00000000000..da29f86f11c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/index.ts @@ -0,0 +1 @@ +export { SettingsResourceRow } from './settings-resource-row' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx new file mode 100644 index 00000000000..d01bfe131d6 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from 'react' + +/** + * The canonical settings "resource row": a rounded-bordered icon tile, a + * title + muted description text block, and an optional trailing slot + * (action chips, a {@link RowActionsMenu}, a status label, etc.). + * + * Single source of truth for the credential-style row shared by the BYOK key + * manager, credential sets, and recently-deleted lists — never re-derive the + * tile/text chrome per consumer. The tile force-sizes any ``/`` it + * contains to 20px, so callers pass their raw icon node without pre-sizing it. + */ +interface SettingsResourceRowProps { + /** Icon node centered in the tile; any ``/`` is normalized to 20px. */ + icon: ReactNode + /** Primary line — truncates. */ + title: ReactNode + /** Secondary muted line — truncates. */ + description?: ReactNode + /** Trailing element pinned to the row's end (chips, actions menu, status). */ + trailing?: ReactNode +} + +const TILE_CLASS = + 'flex size-9 flex-shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border-1)] bg-[var(--bg)] [&_img]:size-5 [&_svg]:size-5' + +export function SettingsResourceRow({ + icon, + title, + description, + trailing, +}: SettingsResourceRowProps) { + return ( +
+
+
{icon}
+
+ {title} + {description != null && ( + {description} + )} +
+
+ {trailing} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index d2d9ff3e7b9..538e7182899 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -248,7 +248,8 @@ export const allNavigationItems: NavigationItem[] = [ { id: 'data-retention', label: 'Data retention', - description: 'Control data retention windows and PII redaction.', + description: + 'Control data retention windows and PII redaction. Workspaces without an override inherit the organization defaults.', icon: Database, section: 'enterprise', requiresHosted: true, diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index b9b08e656d9..d676406833d 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -787,50 +787,45 @@ export function DataRetentionSettings() { ]} > -
- - Workspaces without an override inherit the organization defaults. - -
+
+ + {overrideWorkspaceIds.map((workspaceId) => ( - {overrideWorkspaceIds.map((workspaceId) => ( - - ))} -
+ ))}
diff --git a/packages/emcn/src/components/chip/chip.tsx b/packages/emcn/src/components/chip/chip.tsx index 764fd2f6039..fb868dd8b18 100644 --- a/packages/emcn/src/components/chip/chip.tsx +++ b/packages/emcn/src/components/chip/chip.tsx @@ -38,14 +38,20 @@ import { * `active` renders the default/filled chip in its selected state — `--surface-active` at rest, one surface darker * (`--surface-6`) on hover. `fullWidth` swaps `inline-flex` for block-level `flex`. `flush` removes the default * `mx-0.5` cluster margin — use when a single chip sits in its own layout slot (grid/table cell). + * + * The default/filled hover lives in `active`-keyed compound variants (not the base variant string) so the + * rest/hover classes are mutually exclusive — a chip renders exactly ONE `hover-hover:bg-*`. This keeps raw + * `chipVariants({...})` consumers identical to `cn(chipVariants({...}))` ones; folding the non-active hover back + * into the variant string would emit two conflicting hover classes that only `cn`'s tailwind-merge resolves, + * silently diverging raw consumers (e.g. an active row that darkens with `Chip` but not with raw `chipVariants`). */ const chipVariants = cva( `group cursor-pointer ${chipGeometryClass} transition-colors disabled:cursor-not-allowed disabled:opacity-60`, { variants: { variant: { - default: 'hover-hover:bg-[var(--surface-active)]', - filled: `${chipFilledFillTokens} hover-hover:bg-[var(--surface-active)]`, + default: '', + filled: chipFilledFillTokens, primary: `${chipPrimaryFillTokens} hover-hover:bg-[var(--text-body)] hover-hover:text-[var(--text-inverse)] dark:hover-hover:bg-[var(--text-secondary)] dark:hover-hover:text-[var(--bg)]`, destructive: 'bg-[var(--text-error)] text-white hover-hover:text-white hover-hover:brightness-106', @@ -59,11 +65,21 @@ const chipVariants = cva( flush: { true: 'mx-0', false: 'mx-0.5' }, }, compoundVariants: [ + { + variant: 'default', + active: false, + className: 'hover-hover:bg-[var(--surface-active)]', + }, { variant: 'default', active: true, className: 'bg-[var(--surface-active)] hover-hover:bg-[var(--surface-6)]', }, + { + variant: 'filled', + active: false, + className: 'hover-hover:bg-[var(--surface-active)]', + }, { variant: 'filled', active: true,