Skip to content

Commit effe2f1

Browse files
committed
fix(platform): sentence-case the Remove from skills action and hold every *Label prop to copy-case
1 parent ed4445e commit effe2f1

6 files changed

Lines changed: 16 additions & 11 deletions

File tree

‎.claude/rules/sim-ui-copy.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This applies to app UI outside the Platform: settings rows, modals, panels, card
1919

2020
Platform views also share one vocabulary, documented in `apps/sim/platform/AGENTS.md` (Code-first product design) and enforced by `check:platform-design`:
2121

22-
- **Sentence case** for every literal `label`, `title`, `confirmLabel`, `pendingLabel` and `cancelLabel`, for `label`s in menu, column, sort and command data, and for button, menu item and chip text (`copy-case`). Product, plan and brand names keep their capitals through `PROPER_NOUNS` / `PROPER_PHRASES` in `scripts/platform-source-rules.ts`; a page or tab name inside a label is a common noun ("Open logs", "Switch to runs").
22+
- **Sentence case** for every literal `title`, `label` and `*Label` prop (`confirmLabel`, `createLabel`, …), for `label`s and `*Label`s in menu, column, sort, command and action data, and for button, menu item and chip text (`copy-case`). Product, plan and brand names keep their capitals through `PROPER_NOUNS` / `PROPER_PHRASES` in `scripts/platform-source-rules.ts`; a page or tab name inside a label is a common noun ("Open logs", "Switch to runs").
2323
- **The `…` glyph**, never three periods, in rendered copy and in object or record values such as status labels (`copy-ellipsis`).
2424
- **Confirmations** go through `ConfirmDialog` with a real `pendingLabel` ("Deleting…"), `safe` when nothing is lost (`confirm-owner`). Titles are "{Verb} {noun}" with no question mark, identical for one object everywhere; unsaved changes read "Unsaved changes" / "Discard".
2525
- **Verbs**: editing saves with "Save", creating says "Create {noun}", adding a connection or credential says "Add {noun}", OAuth keeps "Connect".

‎.cursor/rules/sim-ui-copy.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This applies to app UI outside the Platform: settings rows, modals, panels, card
1515

1616
Platform views also share one vocabulary, documented in `apps/sim/platform/AGENTS.md` (Code-first product design) and enforced by `check:platform-design`:
1717

18-
- **Sentence case** for every literal `label`, `title`, `confirmLabel`, `pendingLabel` and `cancelLabel`, for `label`s in menu, column, sort and command data, and for button, menu item and chip text (`copy-case`). Product, plan and brand names keep their capitals through `PROPER_NOUNS` / `PROPER_PHRASES` in `scripts/platform-source-rules.ts`; a page or tab name inside a label is a common noun ("Open logs", "Switch to runs").
18+
- **Sentence case** for every literal `title`, `label` and `*Label` prop (`confirmLabel`, `createLabel`, …), for `label`s and `*Label`s in menu, column, sort, command and action data, and for button, menu item and chip text (`copy-case`). Product, plan and brand names keep their capitals through `PROPER_NOUNS` / `PROPER_PHRASES` in `scripts/platform-source-rules.ts`; a page or tab name inside a label is a common noun ("Open logs", "Switch to runs").
1919
- **The `…` glyph**, never three periods, in rendered copy and in object or record values such as status labels (`copy-ellipsis`).
2020
- **Confirmations** go through `ConfirmDialog` with a real `pendingLabel` ("Deleting…"), `safe` when nothing is lost (`confirm-owner`). Titles are "{Verb} {noun}" with no question mark, identical for one object everywhere; unsaved changes read "Unsaved changes" / "Discard".
2121
- **Verbs**: editing saves with "Save", creating says "Create {noun}", adding a connection or credential says "Add {noun}", OAuth keeps "Connect".

‎apps/sim/app/(product)/o/[organizationId]/integrations/_components/integration-capabilities/integration-capabilities.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function IntegrationCapabilities({
7373
source: { providerId, name: skill.name },
7474
}}
7575
createLabel='Add to skills'
76-
deleteLabel='Remove from Skills'
76+
deleteLabel='Remove from skills'
7777
onSave={onSave}
7878
onClose={() => setSkill(null)}
7979
onDelete={selected ? () => onRemove(selected.id) : undefined}

‎apps/sim/platform/AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Measure the actual scroll viewport with `ScrollArea`, or attach `useScrollEdges`
4949

5050
Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A dialog with tabs keeps its top edge and grows downward, so switching tabs never moves them; its fields keep the dialog's field gap. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. Returning `false` keeps it open at the dialog's next stage, and `onBack` turns the quiet footer action into a step back. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. An optional footerStartAction accepts a standard button label, handler, intent and availability at the logical start; Cancel and confirmation remain at the end. Destructive editor actions require their own confirmation and never become the default focus target. Pending submission disables all footer actions. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Modal and panel content uses the shared field family: TextField, TextAreaField, SelectField, SwitchField (a setting that applies with its switch, captioned on the leading edge), SecretField, RecipientField, FileField, DetailsField and ListField. Product dialogs provide values, labels and callbacks; they do not recreate field chrome. Full-width FileField centers its prompt without an upload glyph; its image kind supplies a compact preview and independent removal. DetailsField labels read-only information, and ListField owns grouped bullets and bounded scrolling. Use DataTable inside a dialog only for actual tabular data or selection, not static skill instructions. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag.
5151

52-
A question before an action is `ConfirmDialog` (`@sim/emcn/modal/confirm-dialog`), never a hand-built `Modal variant='confirm'` (`confirm-owner`): it holds the pending label (`Deleting…`), closes only on success and shows a failure. Pass `safe` for a confirm that loses nothing. Its title is the verb and the object in sentence case with no question mark (Delete link, Remove connection, Unsaved changes), the same for one object on every surface; Unsaved changes confirms with Discard. Editing saves with Save, creating says Create {noun}, adding a connection or credential says Add {noun}, and OAuth keeps Connect. Renames use the shared `RenameDialog`. Interface copy is sentence case: a literal `label`, `title`, `confirmLabel`, `pendingLabel` or `cancelLabel`, a `label` in menu, column, sort or command data, and a button, menu item or chip's text (`copy-case`). Product names (Sim, Chat, Assistant, Search), plans, the Agent block, brands and key names keep their capitals; they are listed, with multi-word names such as Slack Search and Compute Units, in `PROPER_NOUNS` and `PROPER_PHRASES` in `scripts/platform-source-rules.ts`. A page or tab name inside a label is a common noun (Open logs, Switch to runs). `copy-ellipsis` also reads an object or record's string values, so a status or column label writes `…` too. Statuses use Logs' vocabulary: a failed run reads Failed. A page's way back is `DirectoryBack`, and a Settings sub-view's is `DetailHeader`, each labelled with a bare noun (Skills, not Back to Skills); a hand-built text button led by the back glyph is reported (`back-owner`). Tabs are `ContentTabs` (`tab-owner`), and a message announced as an alert is `InlineNotice` with `intent='danger'` for a failed action, `FailureNotice` with Retry for a failed read, or the field's `error` (`alert-owner`): no hand-written `role='tab'`, `'tablist'` or `'alert'`. A full value on hover is `Tooltip` or `OverflowText`, never a native `title=` on a DOM element (`native-title`); an iframe's `title` is its accessible name and stays. An empty list says so with `EmptyState` (or `WorkspaceEmpty` in a workspace tab), and a narrow rail or a dialog's picker with `Muted`; a search that matches nothing reads No matching {noun}, with the query in curly quotes, and no period. Within one control bar, an action a viewer cannot take is hidden, or disabled with a real `disabledReason`. A justified exception is named, with its reason, in `CONFIRM_OWNERS`, `BACK_OWNERS`, `TAB_OWNERS` or `ALERT_OWNERS`.
52+
A question before an action is `ConfirmDialog` (`@sim/emcn/modal/confirm-dialog`), never a hand-built `Modal variant='confirm'` (`confirm-owner`): it holds the pending label (`Deleting…`), closes only on success and shows a failure. Pass `safe` for a confirm that loses nothing. Its title is the verb and the object in sentence case with no question mark (Delete link, Remove connection, Unsaved changes), the same for one object on every surface; Unsaved changes confirms with Discard. Editing saves with Save, creating says Create {noun}, adding a connection or credential says Add {noun}, and OAuth keeps Connect. Renames use the shared `RenameDialog`. Interface copy is sentence case: a literal `title`, `label` or any `*Label` prop (`confirmLabel`, `createLabel`, …), a `label` or `*Label` in menu, column, sort, command or action data, and a button, menu item or chip's text (`copy-case`). Product names (Sim, Chat, Assistant, Search), plans, the Agent block, brands and key names keep their capitals; they are listed, with multi-word names such as Slack Search and Compute Units, in `PROPER_NOUNS` and `PROPER_PHRASES` in `scripts/platform-source-rules.ts`. A page or tab name inside a label is a common noun (Open logs, Switch to runs). `copy-ellipsis` also reads an object or record's string values, so a status or column label writes `…` too. Statuses use Logs' vocabulary: a failed run reads Failed. A page's way back is `DirectoryBack`, and a Settings sub-view's is `DetailHeader`, each labelled with a bare noun (Skills, not Back to Skills); a hand-built text button led by the back glyph is reported (`back-owner`). Tabs are `ContentTabs` (`tab-owner`), and a message announced as an alert is `InlineNotice` with `intent='danger'` for a failed action, `FailureNotice` with Retry for a failed read, or the field's `error` (`alert-owner`): no hand-written `role='tab'`, `'tablist'` or `'alert'`. A full value on hover is `Tooltip` or `OverflowText`, never a native `title=` on a DOM element (`native-title`); an iframe's `title` is its accessible name and stays. An empty list says so with `EmptyState` (or `WorkspaceEmpty` in a workspace tab), and a narrow rail or a dialog's picker with `Muted`; a search that matches nothing reads No matching {noun}, with the query in curly quotes, and no period. Within one control bar, an action a viewer cannot take is hidden, or disabled with a real `disabledReason`. A justified exception is named, with its reason, in `CONFIRM_OWNERS`, `BACK_OWNERS`, `TAB_OWNERS` or `ALERT_OWNERS`.
5353

5454
ContentTabs uses Base UI tab behavior with the same ControlRow lane, spacing and paint as DestinationTabs. Its optional actions sit beside the tab navigation, outside its scroll viewport and arrow-key semantics. DataTable composes those tabs, native table semantics and shared checkboxes for controlled row/select-all operations. Its persistent toolbar places search at the start, filters at the end and sorting farthest right. A reserved selection count and caller-provided selection actions appear before the trailing filters and sort only when rows are selected. At zero selection the group is invisible, inert and excluded from accessibility, while preserving its layout space so neither sorting nor the table moves. Visible actions become disabled when inapplicable. There is no default Clear selection or mutation action. Passive table rows have no whole-row hover or click target; their controls remain independent. Consumers own filtering, sorting and mutation policy. Ordinary table values use regular-weight control text and normal ink; email and supporting values use secondary ink, not a heavier font. People tables use separate name and email columns where space permits, regrouping the identity on compact layouts and placing role controls beneath it at enlarged-text widths. Keep inline avatars, checkboxes and controls centered, and select-all visible in every layout. Column positions remain stable when filtering to zero rows, restoring results or changing selection. Use fluid column allocation and natural text wrapping when space is tight; do not force horizontal scrolling with minimum column widths. Preserve the action column when the table is empty. The table fits its reading column with the shared 8px surface overhang. PropertyList uses the same inline person mark and normal value ink, with intrinsic label tracks and a stacked layout only when space requires it.
5555

‎scripts/platform-source-rules.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ describe('titleCaseWords', () => {
178178
[`const view = <Tab label={open ? 'Switch to Runs' : 'Runs'} />`, ['Runs']],
179179
[`const view = <Modal title='Single Sign-On is available' />`, ['Sign', 'On']],
180180
[`const view = <Row label='Open in Logs' />`, ['Logs']],
181+
[`const view = <Capabilities deleteLabel='Remove from Skills' />`, ['Skills']],
182+
[`const actions = { createLabel: 'Add to Skills' }`, ['Skills']],
181183
])('flags %s', (source, words) => {
182184
expect(found(source)).toEqual(words)
183185
})
@@ -193,6 +195,7 @@ describe('titleCaseWords', () => {
193195
`const view = <Modal title={name} />`,
194196
`const view = <Row label='Search: Recent' />`,
195197
`const view = <Row label='Install the GitHub App' />`,
198+
`const view = <Import helpLabel='Open Graph Explorer' />`,
196199
`const view = <Row label={\`\${name} · Admin\`} />`,
197200
`const view = <Button>Upgrade to Max</Button>`,
198201
])('passes %s', (source) => {

‎scripts/platform-source-rules.ts‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,10 @@ export function isNativeTitle(node: ts.Node): boolean {
255255
return /^[a-z][\w-]*$/.test(tag) && !TITLED_EMBEDS.has(tag)
256256
}
257257

258-
/** Attributes and data properties whose value is a control's or dialog's name. */
259-
const NAMING_ATTRIBUTES = new Set(['label', 'title', 'confirmLabel', 'pendingLabel', 'cancelLabel'])
260-
const NAMING_PROPERTIES = new Set(['label', 'confirmLabel', 'pendingLabel', 'cancelLabel'])
258+
/** A data property whose value names a control: `label` or any `*Label` (`confirmLabel`, `createLabel`, …). */
259+
const isNamingProperty = (name: string) => name === 'label' || name.endsWith('Label')
260+
/** An attribute whose value names a control or dialog: a naming property, or `title`. */
261+
const isNamingAttribute = (name: string) => name === 'title' || isNamingProperty(name)
261262
/** Controls whose text is their name: a button, a menu or list item, a chip. */
262263
const NAMED_CONTROL = /^(?:button|\w*Button|\w*Item|Chip)$/
263264

@@ -363,6 +364,7 @@ export const PROPER_PHRASES: readonly string[] = [
363364
'Credential Groups',
364365
'Generic Secrets',
365366
'GitHub App',
367+
'Graph Explorer',
366368
'Google Calendar',
367369
'Google Docs',
368370
'Google Drive',
@@ -430,16 +432,16 @@ function titleCaseIn(text: string): string[] {
430432

431433
/**
432434
* The Title Case words in a control's or dialog's name, which Platform copy writes in sentence case:
433-
* a literal `label`, `title`, `confirmLabel`, `pendingLabel` or `cancelLabel` attribute, a `label`
434-
* in menu, column or option data, or the text of a button, menu item or chip.
435+
* a literal `title`, `label` or any `*Label` attribute (`confirmLabel`, `createLabel`, …), a `label`
436+
* or `*Label` in menu, column, option or action data, or the text of a button, menu item or chip.
435437
*/
436438
export function titleCaseWords(node: ts.Node): string[] {
437439
let copy: string[] = []
438-
if (ts.isJsxAttribute(node) && NAMING_ATTRIBUTES.has(node.name.getText()) && node.initializer)
440+
if (ts.isJsxAttribute(node) && isNamingAttribute(node.name.getText()) && node.initializer)
439441
copy = namingCopy(node.initializer)
440442
else if (
441443
ts.isPropertyAssignment(node) &&
442-
NAMING_PROPERTIES.has(node.name.getText()) &&
444+
isNamingProperty(node.name.getText()) &&
443445
ts.isObjectLiteralExpression(node.parent)
444446
)
445447
copy = namingCopy(node.initializer)

0 commit comments

Comments
 (0)