Converge API token scope pills on shared selectable primitive

This commit is contained in:
rcourtman
2026-06-13 21:52:18 +01:00
parent 9578cd7368
commit 55e058d1e6
11 changed files with 312 additions and 13 deletions
@@ -381,6 +381,10 @@ payload shape change when the portal presents compact client rows.
Token refresh/loading state remains API contract data only while the
visible spinner shell routes through frontend-primitives-owned
`LoadingSpinner` instead of an API-token-local animate-spin SVG.
Token scope selector semantics stay API-contract owned, but the visible
pressed/unpressed selector pill shell routes through frontend-primitives
`SelectablePillButton` instead of API-token-local rounded-full selector
classes.
11. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` shared with `security-privacy`: the pure API token settings model is both a security/privacy control surface and a canonical API payload contract boundary.
12. `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx` shared with `agent-lifecycle`: the inline node credential slot is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.
13. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` shared with `agent-lifecycle`: the pure infrastructure operations inventory/install model is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary.
@@ -2576,6 +2576,13 @@ owns variant resolution plus disabled selection/change runtime, and
variant class catalog, compact-label policy, and segmented button class
selection. Future filter-button-group work should extend those owners instead
of pushing label truncation or segmented variant policy back into the shell.
Pressed/unpressed selector pills follow the same primitive rule.
`frontend-modern/src/components/shared/SelectablePillButton.tsx` owns the
pressed button shell and `aria-pressed` wiring, while
`frontend-modern/src/components/shared/selectablePillModel.ts` owns the active
and inactive pill class catalog. API token scope surfaces may own the security
scope labels and click handlers, but must not recreate rounded-full selector
pill class strings locally.
Filter-toolbar segmented controls must delegate to this primitive rather than
calling `segmentedButtonClass` directly, and icon+text labels must render as
one inline-flex button label so compact bars keep the v5 single-line control
@@ -2884,6 +2891,10 @@ active-button tone, disabled-option behavior, pressed-state semantics,
compact labels, and horizontal scroll treatment through the shared
shell/state/model split. Settings and compact feature surfaces must compose
that primitive instead of copying active-button selector styling locally.
Selectable pill buttons are registry-backed too. `SelectablePillButton` owns
rounded pressed/unpressed selector pills, including active tone, disabled
treatment, focus ring, and `aria-pressed`; settings and security surfaces must
compose that primitive instead of copying rounded-full active selector styling.
`ResourcePicker` report-domain filters are part of that boundary: the picker
owns the reportable resource categories and labels, but the type selector shell
must come from `FilterButtonGroup`.
@@ -84,6 +84,9 @@ controls as normal product settings.
negative-margin wrappers around the inventory grid. Scope-reference
documentation links compose `ExternalTextLink` for shared rel/target safety
and link chrome.
API token scope selectors follow the same split: security/privacy owns
the wildcard, preset, and custom scope semantics, while frontend-primitives
owns the pressed selector pill chrome through `SelectablePillButton`.
4. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` shared with `api-contracts`: the pure API token settings model is both a security/privacy control surface and a canonical API payload contract boundary.
5. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` shared with `frontend-primitives`: the data-handling settings surface is both a security/privacy trust surface and a canonical settings-shell presentation boundary.
6. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` shared with `frontend-primitives`: the data-handling settings model is both a security/privacy posture projection and a canonical settings-shell presentation boundary.
@@ -214,6 +217,10 @@ controls as normal product settings.
primitive. Security/privacy owns the token-management trust copy and
refresh semantics; frontend-primitives owns spinner shell, tone, and
accessible status behavior.
6b. Keep API token scope selector pills on the shared `SelectablePillButton`
primitive. Security/privacy owns scope authority, wildcard behavior, preset
membership, and custom scope toggles; frontend-primitives owns active and
inactive pill tone, focus, disabled treatment, and pressed-state wiring.
6. Keep the shared storage-directory and secure storage-file hardening helper aligned with the crypto manager plus control-plane magic-link key and store handling whenever runtime data-root ownership assumptions change.
7. Keep auth-env ingestion, hosted commercial base URL validation, and shared
fingerprint-verifier TLS defaults aligned whenever runtime auth loading,
@@ -2941,6 +2941,32 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "selectable-pill-button-shell",
"category": "selectable-pill-button",
"summary": "Pressed/unpressed rounded pill selectors must compose SelectablePillButton so active tone, disabled treatment, focus ring, and aria-pressed semantics stay shared instead of being recreated inside settings surfaces.",
"canonical": {
"path": "src/components/shared/SelectablePillButton.tsx",
"export": "SelectablePillButton"
},
"requiredConsumers": [{ "path": "src/components/Settings/APITokenManager.tsx" }],
"forbiddenPatterns": [
{
"path": "src/components/Settings/APITokenManager.tsx",
"patterns": [
"inline-flex min-h-10 sm:min-h-10 items-center rounded-full border px-3 py-2 text-sm font-semibold transition",
"min-h-10 sm:min-h-10 rounded-full border px-3 py-2 text-sm font-semibold transition",
"border-blue-500 bg-blue-600 text-white shadow-sm",
"hover:border-blue-400 hover:text-blue-600 dark:hover:border-blue-400 dark:hover:text-blue-200"
]
}
],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/components/shared/SelectablePillButton.test.tsx",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "chart-visibility-toggle-button",
"category": "display-toggle",
@@ -4854,6 +4880,33 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "selectable-pill-button-local-scope-selector-styles",
"category": "selectable-pill-button",
"summary": "Settings and feature surfaces must not copy the rounded active/inactive selector pill styling owned by SelectablePillButton.",
"canonical": {
"path": "src/components/shared/selectablePillModel.ts",
"export": "getSelectablePillButtonClass"
},
"scopes": ["src/components/Settings", "src/features", "src/pages"],
"extensions": [".tsx"],
"allPatterns": [
"rounded-full border px-3 py-2 text-sm font-semibold transition",
"border-blue-500 bg-blue-600 text-white shadow-sm",
"hover:border-blue-400 hover:text-blue-600"
],
"legacyReason": "Retired migration debt. Pressed/unpressed pill selectors belong to SelectablePillButton.",
"allowedPaths": [],
"ignoredPaths": [
"src/components/Settings/__tests__/APITokenManager.test.tsx",
"src/components/shared/SelectablePillButton.test.tsx"
],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/components/shared/SelectablePillButton.test.tsx",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "button-secondary-command-local-shell",
"category": "action-button",
@@ -5,6 +5,7 @@ import { ExternalTextLink } from '@/components/shared/ExternalTextLink';
import { SectionHeader } from '@/components/shared/SectionHeader';
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
import { PulseDataGrid } from '@/components/shared/PulseDataGrid';
import { SelectablePillButton } from '@/components/shared/SelectablePillButton';
import BadgeCheck from 'lucide-solid/icons/badge-check';
import { MONITORING_READ_SCOPE } from '@/constants/apiScopes';
import {
@@ -460,27 +461,25 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
</div>
<div class="flex flex-wrap gap-2">
<button
type="button"
class={`inline-flex min-h-10 sm:min-h-10 items-center rounded-full border px-3 py-2 text-sm font-semibold transition ${isFullAccessSelected() ? 'border-blue-500 bg-blue-600 text-white shadow-sm' : 'border-border bg-surface text-base-content hover:border-blue-400 hover:text-blue-600 dark:hover:border-blue-400 dark:hover:text-blue-200'}`}
<SelectablePillButton
active={isFullAccessSelected()}
onClick={clearScopes}
disabled={!canManage()}
title="Legacy wildcard all permissions"
>
Full access
</button>
</SelectablePillButton>
<For each={scopePresets}>
{(preset) => (
<button
type="button"
class={`inline-flex min-h-10 sm:min-h-10 items-center rounded-full border px-3 py-2 text-sm font-semibold transition ${presetMatchesSelection(preset.scopes) ? 'border-blue-500 bg-blue-600 text-white shadow-sm' : 'border-border bg-surface text-base-content hover:border-blue-400 hover:text-blue-600 dark:hover:border-blue-400 dark:hover:text-blue-200'}`}
<SelectablePillButton
active={presetMatchesSelection(preset.scopes)}
onClick={() => applyScopePreset(preset.scopes)}
disabled={!canManage()}
title={preset.description}
>
{preset.label}
</button>
</SelectablePillButton>
)}
</For>
</div>
@@ -502,15 +501,14 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
{(option) => {
const isActive = () => selectedScopes().includes(option.value);
return (
<button
type="button"
class={`min-h-10 sm:min-h-10 rounded-full border px-3 py-2 text-sm font-semibold transition ${isActive() ? 'border-blue-500 bg-blue-600 text-white shadow-sm' : 'border-border bg-surface text-base-content hover:border-blue-400 hover:text-blue-600 dark:hover:border-blue-400 dark:hover:text-blue-200'}`}
<SelectablePillButton
active={isActive()}
onClick={() => toggleScope(option.value)}
disabled={!canManage()}
title={option.description}
>
{option.label}
</button>
</SelectablePillButton>
);
}}
</For>
@@ -10,6 +10,7 @@ import {
DOCKER_REPORT_SCOPE,
} from '@/constants/apiScopes';
import apiAccessPanelSource from '../APIAccessPanel.tsx?raw';
import apiTokenManagerSource from '../APITokenManager.tsx?raw';
import { APITokenManager } from '../APITokenManager';
const listTokensMock = vi.fn();
@@ -147,6 +148,17 @@ describe('APITokenManager', () => {
});
it('creates scoped tokens from the canonical preset path', async () => {
expect(apiTokenManagerSource).toContain('@/components/shared/SelectablePillButton');
expect(apiTokenManagerSource.match(/<SelectablePillButton/g) ?? []).toHaveLength(3);
expect(apiTokenManagerSource).not.toContain(
'inline-flex min-h-10 sm:min-h-10 items-center rounded-full border px-3 py-2 text-sm font-semibold transition',
);
expect(apiTokenManagerSource).not.toContain(
'min-h-10 sm:min-h-10 rounded-full border px-3 py-2 text-sm font-semibold transition',
);
expect(apiTokenManagerSource).not.toContain('border-blue-500 bg-blue-600 text-white shadow-sm');
expect(apiTokenManagerSource).not.toContain('hover:border-blue-400 hover:text-blue-600');
render(() => <APITokenManager onTokensChanged={vi.fn()} canManage />);
await waitFor(() => {
@@ -0,0 +1,69 @@
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { SelectablePillButton } from './SelectablePillButton';
import selectablePillButtonSource from './SelectablePillButton.tsx?raw';
import selectablePillModelSource from './selectablePillModel.ts?raw';
describe('SelectablePillButton', () => {
afterEach(() => {
cleanup();
});
it('keeps pressed semantics and class ownership in the shared primitive', () => {
expect(selectablePillButtonSource).toContain('getSelectablePillButtonClass');
expect(selectablePillButtonSource).toContain("aria-pressed={local.active ? 'true' : 'false'}");
expect(selectablePillButtonSource).not.toContain('border-blue-500 bg-blue-600');
expect(selectablePillButtonSource).not.toContain('hover:border-blue-400');
expect(selectablePillModelSource).toContain('SELECTABLE_PILL_BUTTON_BASE_CLASS');
expect(selectablePillModelSource).toContain('SELECTABLE_PILL_BUTTON_ACTIVE_CLASS');
expect(selectablePillModelSource).toContain('SELECTABLE_PILL_BUTTON_INACTIVE_CLASS');
expect(selectablePillModelSource).toContain('getSelectablePillButtonClass');
});
it('renders active and inactive pill states consistently', () => {
const onActiveClick = vi.fn();
const onInactiveClick = vi.fn();
render(() => (
<div>
<SelectablePillButton active onClick={onActiveClick}>
Active
</SelectablePillButton>
<SelectablePillButton active={false} onClick={onInactiveClick}>
Inactive
</SelectablePillButton>
</div>
));
const active = screen.getByRole('button', { name: 'Active' });
const inactive = screen.getByRole('button', { name: 'Inactive' });
expect(active).toHaveAttribute('aria-pressed', 'true');
expect(active.className).toContain('border-blue-500');
expect(active.className).toContain('bg-blue-600');
expect(inactive).toHaveAttribute('aria-pressed', 'false');
expect(inactive.className).toContain('border-border');
expect(inactive.className).toContain('bg-surface');
fireEvent.click(active);
fireEvent.click(inactive);
expect(onActiveClick).toHaveBeenCalledTimes(1);
expect(onInactiveClick).toHaveBeenCalledTimes(1);
});
it('preserves disabled behavior through the native button contract', () => {
const onClick = vi.fn();
render(() => (
<SelectablePillButton active={false} disabled onClick={onClick}>
Disabled
</SelectablePillButton>
));
const disabled = screen.getByRole('button', { name: 'Disabled' });
expect(disabled).toBeDisabled();
fireEvent.click(disabled);
expect(onClick).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,34 @@
import { JSX, mergeProps, splitProps } from 'solid-js';
import { getSelectablePillButtonClass, type SelectablePillButtonSize } from './selectablePillModel';
export interface SelectablePillButtonProps extends Omit<
JSX.ButtonHTMLAttributes<HTMLButtonElement>,
'aria-pressed'
> {
active: boolean;
size?: SelectablePillButtonSize;
class?: string;
children?: JSX.Element;
}
export function SelectablePillButton(props: SelectablePillButtonProps) {
const merged = mergeProps({ type: 'button' as const, size: 'md' as const }, props);
const [local, rest] = splitProps(merged, ['active', 'size', 'class', 'children', 'disabled']);
return (
<button
{...rest}
class={getSelectablePillButtonClass({
active: local.active,
size: local.size,
class: local.class,
})}
aria-pressed={local.active ? 'true' : 'false'}
disabled={local.disabled}
>
{local.children}
</button>
);
}
export default SelectablePillButton;
@@ -30,6 +30,8 @@ import dialogSource from '@/components/shared/Dialog.tsx?raw';
import dialogModelSource from '@/components/shared/dialogModel.ts?raw';
import filterButtonGroupSource from '@/components/shared/FilterButtonGroup.tsx?raw';
import filterButtonGroupModelSource from '@/components/shared/filterButtonGroupModel.ts?raw';
import selectablePillButtonSource from '@/components/shared/SelectablePillButton.tsx?raw';
import selectablePillModelSource from '@/components/shared/selectablePillModel.ts?raw';
import filterToolbarSource from '@/components/shared/FilterToolbar.tsx?raw';
import filterOptionPresentationSource from '@/components/shared/filterOptionPresentation.ts?raw';
import formSelectSource from '@/components/shared/FormSelect.tsx?raw';
@@ -471,6 +473,83 @@ describe('shared primitive guardrails', () => {
);
});
it('routes selectable scope pill buttons through SelectablePillButton', () => {
const registry = JSON.parse(sharedTemplateRegistrySource) as {
rules?: Array<{
id: string;
canonical?: { path?: string; export?: string };
requiredConsumers?: Array<{ path?: string }>;
forbiddenPatterns?: Array<{ path?: string; patterns?: string[] }>;
}>;
patternGuards?: Array<{
id: string;
canonical?: { path?: string; export?: string };
allPatterns?: string[];
scopes?: string[];
allowedPaths?: string[];
ignoredPaths?: string[];
}>;
};
const registeredRule = registry.rules?.find(
(rule) => rule.id === 'selectable-pill-button-shell',
);
const registeredGuard = registry.patternGuards?.find(
(guard) => guard.id === 'selectable-pill-button-local-scope-selector-styles',
);
expect(registeredRule?.canonical?.path).toBe('src/components/shared/SelectablePillButton.tsx');
expect(registeredRule?.canonical?.export).toBe('SelectablePillButton');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
'src/components/Settings/APITokenManager.tsx',
]);
expect(registeredRule?.forbiddenPatterns).toEqual([
{
path: 'src/components/Settings/APITokenManager.tsx',
patterns: [
'inline-flex min-h-10 sm:min-h-10 items-center rounded-full border px-3 py-2 text-sm font-semibold transition',
'min-h-10 sm:min-h-10 rounded-full border px-3 py-2 text-sm font-semibold transition',
'border-blue-500 bg-blue-600 text-white shadow-sm',
'hover:border-blue-400 hover:text-blue-600 dark:hover:border-blue-400 dark:hover:text-blue-200',
],
},
]);
expect(registeredGuard?.canonical?.path).toBe('src/components/shared/selectablePillModel.ts');
expect(registeredGuard?.canonical?.export).toBe('getSelectablePillButtonClass');
expect(registeredGuard?.allPatterns).toEqual([
'rounded-full border px-3 py-2 text-sm font-semibold transition',
'border-blue-500 bg-blue-600 text-white shadow-sm',
'hover:border-blue-400 hover:text-blue-600',
]);
expect(registeredGuard?.scopes).toEqual([
'src/components/Settings',
'src/features',
'src/pages',
]);
expect(registeredGuard?.allowedPaths ?? []).toHaveLength(0);
expect(registeredGuard?.ignoredPaths).toEqual([
'src/components/Settings/__tests__/APITokenManager.test.tsx',
'src/components/shared/SelectablePillButton.test.tsx',
]);
expect(selectablePillButtonSource).toContain('getSelectablePillButtonClass');
expect(selectablePillButtonSource).toContain("aria-pressed={local.active ? 'true' : 'false'}");
expect(selectablePillButtonSource).not.toContain('border-blue-500 bg-blue-600');
expect(selectablePillModelSource).toContain('SELECTABLE_PILL_BUTTON_BASE_CLASS');
expect(selectablePillModelSource).toContain('SELECTABLE_PILL_BUTTON_ACTIVE_CLASS');
expect(selectablePillModelSource).toContain('SELECTABLE_PILL_BUTTON_INACTIVE_CLASS');
expect(selectablePillModelSource).toContain('getSelectablePillButtonClass');
expect(apiTokenManagerSource).toContain('SelectablePillButton');
expect(apiTokenManagerSource.match(/<SelectablePillButton/g) ?? []).toHaveLength(3);
expect(apiTokenManagerSource).not.toContain(
'inline-flex min-h-10 sm:min-h-10 items-center rounded-full border px-3 py-2 text-sm font-semibold transition',
);
expect(apiTokenManagerSource).not.toContain(
'min-h-10 sm:min-h-10 rounded-full border px-3 py-2 text-sm font-semibold transition',
);
expect(apiTokenManagerSource).not.toContain('border-blue-500 bg-blue-600 text-white shadow-sm');
expect(apiTokenManagerSource).not.toContain('hover:border-blue-400 hover:text-blue-600');
});
it('keeps AI model picker labels route-aware for gateway providers', () => {
expect(aiModelPickerSource).toContain('formatAIModelRouteLabel(match)');
expect(aiModelPickerSource).toContain('formatAIModelRouteLabel(model)');
@@ -0,0 +1,32 @@
export type SelectablePillButtonSize = 'md';
export const SELECTABLE_PILL_BUTTON_BASE_CLASS =
'inline-flex items-center justify-center rounded-full border font-semibold transition whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60';
export const SELECTABLE_PILL_BUTTON_SIZE_CLASSES: Record<SelectablePillButtonSize, string> = {
md: 'min-h-10 px-3 py-2 text-sm sm:min-h-10',
};
export const SELECTABLE_PILL_BUTTON_ACTIVE_CLASS =
'border-blue-500 bg-blue-600 text-white shadow-sm';
export const SELECTABLE_PILL_BUTTON_INACTIVE_CLASS =
'border-border bg-surface text-base-content hover:border-blue-400 hover:text-blue-600 dark:hover:border-blue-400 dark:hover:text-blue-200';
export type SelectablePillButtonClassOptions = {
active?: boolean;
size?: SelectablePillButtonSize;
class?: string;
};
export const getSelectablePillButtonClass = (
options: SelectablePillButtonClassOptions = {},
): string =>
[
SELECTABLE_PILL_BUTTON_BASE_CLASS,
SELECTABLE_PILL_BUTTON_SIZE_CLASSES[options.size ?? 'md'],
options.active ? SELECTABLE_PILL_BUTTON_ACTIVE_CLASS : SELECTABLE_PILL_BUTTON_INACTIVE_CLASS,
options.class,
]
.filter(Boolean)
.join(' ');
@@ -2860,7 +2860,7 @@ class SubsystemLookupTest(unittest.TestCase):
{
"heading": "## Shared Boundaries",
"path": "internal/api/access_control_handlers.go",
"line": 472,
"line": 476,
"heading_line": 123,
}
],