mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Converge API token scope pills on shared selectable primitive
This commit is contained in:
@@ -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,
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user