Skip to content

Commit c725e3f

Browse files
committed
Merge branch 'wip/fx23-copy' into wip/platform-org-wiring
2 parents f1d5974 + d5c5ea3 commit c725e3f

100 files changed

Lines changed: 589 additions & 609 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/rules/sim-platform-views.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ like that" is not one.
1919

2020
| Job | Use | Never |
2121
| --- | --- | --- |
22+
| Explain a field, dialog or row | Its label, placeholder and options (`Access keys (long-lived)`); `error` for validation, `disabledReason` for a locked control, the dialog's Setup guide for steps | A `hint`, form `Modal` `description`, written `SettingsRow`/`DetailHeader` subtitle or data `hint` (`check:platform-design` `helper-copy`; a line that must stay is named in `HELPER_COPY_OWNERS`) |
2223
| First read of a view | Render nothing for data-backed content while pending (static chrome renders at once), `FailureNotice` when the read fails, `EmptyState` when it settles empty. Read the query's `status`, not an empty list. An action's own pending label (`pendingLabel`, a Load action's `Loading…`) is feedback, not loading copy | A skeleton, shimmer, placeholder rows or loading copy (`check:platform-design` `loading-copy`); a pending hint, placeholder or detail line; treating empty as loading |
2324
| Failed read | `FailureNotice`, labelled Retry. `intent='warning'` where the read partly answered | A hand-built retry button or `InlineNotice` + `Button` |
2425
| Failed background refresh | `FailureNotice` above the content it keeps | Replacing content already on screen |

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ paths:
1313

1414
**Do not add subtitles, helper text, or descriptive copy beneath headings, labels, cards, or settings by default.** Prefer one concise, self-explanatory heading or label. Only add supporting copy when the user explicitly asks for it, or when it is necessary to prevent misunderstanding or error — and never use it to restate the heading.
1515

16-
This applies to app UI outside the Platform: settings rows, modals, panels, cards, list rows, empty states, form fields, and section headers. Platform views (`app/(product)`, `platform`) follow `apps/sim/platform/AGENTS.md`; marketing surfaces (`app/(landing)`, docs) follow `constitution.md`.
16+
This applies to app UI outside the Platform: settings rows, modals, panels, cards, list rows, empty states, form fields, and section headers. Platform views (`app/(product)`, `platform`) follow the stricter one-line-per-control rule in `apps/sim/platform/AGENTS.md`, which `check:platform-design` enforces (`helper-copy`); marketing surfaces (`app/(landing)`, docs) follow `constitution.md`.
1717

1818
**Carve-out — settings section metadata.** `SettingsNavigationItem.description` in `components/settings/navigation.ts` stays required, and `SettingsPanel` keeps rendering it as the page subtitle. Settings sections are reached through a nav list where the description is the only thing distinguishing adjacent sections, so it earns its place by the "prevents misunderstanding" test. Keep those descriptions verb-first and one line, per `sim-settings-pages.md`. Everything else on a settings page — inline `<p>` blurbs under section headings, field hints, modal bodies, row subtitles — follows the default rule above.
1919

@@ -32,14 +32,14 @@ This applies to app UI outside the Platform: settings rows, modals, panels, card
3232
<ChipModalField title='Workspace name' />
3333
```
3434

35-
If a heading needs a subtitle to be understood, the heading is wrong. Fix the heading — don't append a second line.
35+
If a heading needs a subtitle to be understood, the heading is wrong. Fix the heading — don't append a second line. For a field, bake the meaning into the line itself first: a format into the placeholder (`us-gov-west-1`), a bound or scope into the label (`Idle timeout (hours, min 1)`), a trade-off into the option (`Access keys (long-lived)`), or point to the dialog's setup guide.
3636

3737
## When supporting copy earns its place
3838

3939
Keep (or add) a description only when it carries information the label cannot, and its absence would cause a mistake:
4040

4141
- **Irreversible or destructive consequences** — "Deleting this workspace removes every workflow and log. This cannot be undone."
42-
- **A non-obvious format, unit, or bound** — "Comma-separated. Max 50 domains.", "Cost per 1M input tokens."
42+
- **A non-obvious format, unit, or bound the label, placeholder and options cannot hold** — "Comma-separated. Max 50 domains.", "Cost per 1M input tokens."
4343
- **A security or access implication** — "This key is shown once and grants full workspace access."
4444
- **A state the user cannot otherwise see** — "Inherited from your organization's policy."
4545
- **Instructional copy that advances a flow** — "We sent a 6-digit code to you@example.com."

‎.cursor/rules/sim-platform-views.mdc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ like that" is not one.
1717

1818
| Job | Use | Never |
1919
| --- | --- | --- |
20+
| Explain a field, dialog or row | Its label, placeholder and options (`Access keys (long-lived)`); `error` for validation, `disabledReason` for a locked control, the dialog's Setup guide for steps | A `hint`, form `Modal` `description`, written `SettingsRow`/`DetailHeader` subtitle or data `hint` (`check:platform-design` `helper-copy`; a line that must stay is named in `HELPER_COPY_OWNERS`) |
2021
| First read of a view | Render nothing for data-backed content while pending (static chrome renders at once), `FailureNotice` when the read fails, `EmptyState` when it settles empty. Read the query's `status`, not an empty list. An action's own pending label (`pendingLabel`, a Load action's `Loading…`) is feedback, not loading copy | A skeleton, shimmer, placeholder rows or loading copy (`check:platform-design` `loading-copy`); a pending hint, placeholder or detail line; treating empty as loading |
2122
| Failed read | `FailureNotice`, labelled Retry. `intent='warning'` where the read partly answered | A hand-built retry button or `InlineNotice` + `Button` |
2223
| Failed background refresh | `FailureNotice` above the content it keeps | Replacing content already on screen |

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ globs: ["apps/sim/components/**/*.tsx","apps/sim/ee/**/*.tsx","apps/sim/app/_she
99

1010
**Do not add subtitles, helper text, or descriptive copy beneath headings, labels, cards, or settings by default.** Prefer one concise, self-explanatory heading or label. Only add supporting copy when the user explicitly asks for it, or when it is necessary to prevent misunderstanding or error — and never use it to restate the heading.
1111

12-
This applies to app UI outside the Platform: settings rows, modals, panels, cards, list rows, empty states, form fields, and section headers. Platform views (`app/(product)`, `platform`) follow `apps/sim/platform/AGENTS.md`; marketing surfaces (`app/(landing)`, docs) follow `constitution.md`.
12+
This applies to app UI outside the Platform: settings rows, modals, panels, cards, list rows, empty states, form fields, and section headers. Platform views (`app/(product)`, `platform`) follow the stricter one-line-per-control rule in `apps/sim/platform/AGENTS.md`, which `check:platform-design` enforces (`helper-copy`); marketing surfaces (`app/(landing)`, docs) follow `constitution.md`.
1313

1414
**Carve-out — settings section metadata.** `SettingsNavigationItem.description` in `components/settings/navigation.ts` stays required, and `SettingsPanel` keeps rendering it as the page subtitle. Settings sections are reached through a nav list where the description is the only thing distinguishing adjacent sections, so it earns its place by the "prevents misunderstanding" test. Keep those descriptions verb-first and one line, per `sim-settings-pages.md`. Everything else on a settings page — inline `<p>` blurbs under section headings, field hints, modal bodies, row subtitles — follows the default rule above.
1515

@@ -28,14 +28,14 @@ This applies to app UI outside the Platform: settings rows, modals, panels, card
2828
<ChipModalField title='Workspace name' />
2929
```
3030

31-
If a heading needs a subtitle to be understood, the heading is wrong. Fix the heading — don't append a second line.
31+
If a heading needs a subtitle to be understood, the heading is wrong. Fix the heading — don't append a second line. For a field, bake the meaning into the line itself first: a format into the placeholder (`us-gov-west-1`), a bound or scope into the label (`Idle timeout (hours, min 1)`), a trade-off into the option (`Access keys (long-lived)`), or point to the dialog's setup guide.
3232

3333
## When supporting copy earns its place
3434

3535
Keep (or add) a description only when it carries information the label cannot, and its absence would cause a mistake:
3636

3737
- **Irreversible or destructive consequences** — "Deleting this workspace removes every workflow and log. This cannot be undone."
38-
- **A non-obvious format, unit, or bound** — "Comma-separated. Max 50 domains.", "Cost per 1M input tokens."
38+
- **A non-obvious format, unit, or bound the label, placeholder and options cannot hold** — "Comma-separated. Max 50 domains.", "Cost per 1M input tokens."
3939
- **A security or access implication** — "This key is shown once and grants full workspace access."
4040
- **A state the user cannot otherwise see** — "Inherited from your organization's policy."
4141
- **Instructional copy that advances a flow** — "We sent a 6-digit code to you@example.com."

‎apps/sim/app/(product)/_components/access-requests/review-modal.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ export function ReviewModal({ organizationId, requestId, onClose }: ReviewModalP
138138
inputMode='numeric'
139139
value={newLimit}
140140
onChange={setNewLimit}
141-
hint='Enter a whole number above the current limit.'
141+
error={
142+
newLimit.trim() && !review.isValidLimit(parsedLimit)
143+
? 'Enter a whole number above the current limit.'
144+
: undefined
145+
}
142146
/>
143147
)}
144148
</>

‎apps/sim/app/(product)/_components/access-requests/review-requests.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function ReviewRequests({ organizationId }: ReviewRequestsProps) {
5656
label='Allow requests'
5757
description={
5858
settings.allowed
59-
? 'People can send requests for administrators to review.'
59+
? undefined
6060
: 'New requests and approvals are paused. You can still view history and decline pending requests.'
6161
}
6262
>

‎apps/sim/app/(product)/_components/chat/panel/browser-session/page-prompts.tsx‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ interface ChoicePromptProps {
130130
fieldLabel: string
131131
placeholder: string
132132
options: readonly { value: string; label: string }[]
133-
hintFor?: (choiceId: string) => string | undefined
134133
/** A refusal the page should remember, offered beside Cancel. */
135134
refuse?: { label: string; onSelect: () => void }
136135
onCancel: () => void
@@ -147,7 +146,6 @@ function ChoicePrompt({
147146
fieldLabel,
148147
placeholder,
149148
options,
150-
hintFor,
151149
refuse,
152150
onCancel,
153151
onChoose,
@@ -191,7 +189,6 @@ function ChoicePrompt({
191189
options={options}
192190
value={choiceId || undefined}
193191
onValueChange={setChoiceId}
194-
hint={choiceId ? hintFor?.(choiceId) : undefined}
195192
/>
196193
</div>
197194
</Modal>
@@ -227,14 +224,8 @@ function ClientCertificatePrompt({ prompt, respond }: PromptModalProps<'client-c
227224
placeholder='Choose a certificate'
228225
options={prompt.certificates.map((certificate) => ({
229226
value: certificate.id,
230-
label: certificate.subject || certificate.id,
227+
label: `${certificate.subject || certificate.id} · ${certificate.issuer || 'Unknown issuer'} · Expires ${formatInstantDate(certificate.expiresAt)}`,
231228
}))}
232-
hintFor={(choiceId) => {
233-
const chosen = prompt.certificates.find((certificate) => certificate.id === choiceId)
234-
return chosen
235-
? `Issued by ${chosen.issuer || 'unknown issuer'} · Expires ${formatInstantDate(chosen.expiresAt)}`
236-
: undefined
237-
}}
238229
refuse={{
239230
label: "Don't send",
240231
onSelect: () => respond({ allowed: false, remember: true }),

‎apps/sim/app/(product)/_components/links-page/link-editor.tsx‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export function LinkEditor({
3636
}: LinkEditorProps) {
3737
const editor = useLinkEditor(organizationId, link, initialName, initialUrl)
3838
const { draft, errors, pending } = editor
39+
const accessReason = editor.canManage
40+
? undefined
41+
: 'Only the creator or an organization admin can change access.'
3942
return (
4043
<Modal
4144
open
@@ -65,7 +68,6 @@ export function LinkEditor({
6568
value={draft.defaultUrl ?? ''}
6669
onChange={(value) => editor.change('defaultUrl', value || null)}
6770
error={errors.defaultUrl}
68-
hint={`Opens when no value fills ${LINK_LIMITS.variable}.`}
6971
/>
7072
)}
7173
<TextField
@@ -87,25 +89,20 @@ export function LinkEditor({
8789
onChange={(value) => editor.change('description', value)}
8890
error={errors.description}
8991
/>
90-
<FieldFrame
91-
label='Access'
92-
hint={
93-
editor.canManage
94-
? undefined
95-
: 'Only the creator or an organization admin can change access.'
96-
}
97-
>
92+
<FieldFrame label='Access'>
9893
<div className={styles.access}>
9994
<Checkbox
10095
label='Hide from the Links page'
10196
checked={draft.unlisted}
10297
disabled={!editor.canManage || pending}
98+
disabledReason={accessReason}
10399
onCheckedChange={(checked) => editor.change('unlisted', checked)}
104100
/>
105101
<Checkbox
106102
label='Only the creator and admins can edit'
107103
checked={draft.restrictEdit}
108104
disabled={!editor.canManage || pending}
105+
disabledReason={accessReason}
109106
onCheckedChange={(checked) => editor.change('restrictEdit', checked)}
110107
/>
111108
</div>

‎apps/sim/app/(product)/_components/oauth-connect-modal/oauth-connect-modal.tsx‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ export function OAuthConnectModal({
112112
value={connect.clientFieldValues[field.id] ?? ''}
113113
onValueChange={(value) => connect.setClientField(field.id, value)}
114114
options={field.options}
115-
hint={field.hint}
116115
/>
117116
) : field.secret ? (
118117
<SecretField
@@ -123,7 +122,6 @@ export function OAuthConnectModal({
123122
placeholder={field.placeholder}
124123
autoComplete='new-password'
125124
required
126-
hint={field.hint}
127125
/>
128126
) : (
129127
<TextField
@@ -134,7 +132,6 @@ export function OAuthConnectModal({
134132
placeholder={field.placeholder}
135133
autoComplete='off'
136134
required
137-
hint={field.hint}
138135
/>
139136
)
140137
)}

‎apps/sim/app/(product)/_components/service-account-modal/service-account-modal.dom.test.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ it('adds a keyless AWS role: shows what to paste into AWS and submits the signed
268268

269269
it('adds AWS access keys: submits only the keys, never a subject, and requires both', async () => {
270270
const dialog = await open('aws-service-account', 'AWS')
271-
await chooseAuthType(dialog, 'Access keys')
271+
await chooseAuthType(dialog, 'Access keys (long-lived)')
272272

273273
expect(labelled(dialog, 'Issuer')).toBeNull()
274274
expect(labelled(dialog, 'Role ARN')).toBeNull()
@@ -287,12 +287,12 @@ it('adds AWS access keys: submits only the keys, never a subject, and requires b
287287

288288
it('drops a secret when the auth type that used it is left', async () => {
289289
const dialog = await open('aws-service-account', 'AWS')
290-
await chooseAuthType(dialog, 'Access keys')
290+
await chooseAuthType(dialog, 'Access keys (long-lived)')
291291
type(input(dialog, 'Access key ID'), 'AKIAEXAMPLEKEY12345')
292292
type(input(dialog, 'Secret access key'), 'first-secret')
293293

294294
await chooseAuthType(dialog, 'IAM role (keyless, via Sim OIDC)')
295-
await chooseAuthType(dialog, 'Access keys')
295+
await chooseAuthType(dialog, 'Access keys (long-lived)')
296296

297297
expect(input(dialog, 'Secret access key').value).toBe('')
298298
expect(input(dialog, 'Access key ID').value).toBe('AKIAEXAMPLEKEY12345')
@@ -322,7 +322,7 @@ it('offers keyless sign-in disabled, with no instructions, where the deployment
322322
keyless = { available: false }
323323
const dialog = await open('aws-service-account', 'AWS')
324324

325-
expect(authTypeTrigger(dialog).textContent).toContain('Access keys')
325+
expect(authTypeTrigger(dialog).textContent).toContain('Access keys (long-lived)')
326326
expect(labelled(dialog, 'Issuer')).toBeNull()
327327
act(() => authTypeTrigger(dialog).click())
328328
await settle()
@@ -336,7 +336,7 @@ it('never offers keyless sign-in to an organization credential, and never mints
336336
scope: { kind: 'organization', organizationId: 'org-1' },
337337
})
338338

339-
expect(authTypeTrigger(dialog).textContent).toContain('Access keys')
339+
expect(authTypeTrigger(dialog).textContent).toContain('Access keys (long-lived)')
340340
expect(requests.some((request) => request.path === '/api/credentials/workload-identity')).toBe(
341341
false
342342
)

0 commit comments

Comments
 (0)