mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
Error toasts now stay on screen until the user closes them, to meet WCAG 2.2.1 Timing Adjustable. Success, info, and warning toasts keep Sonner's default timer. Fixes [DEQ-463](https://linear.app/codercom/issue/DEQ-463/timed-toast-without-user-control) (Open Cursor Desktop failure) and [DEQ-310](https://linear.app/codercom/issue/DEQ-310/timed-toast-without-user-control) (adding a user who is already in a group). Sonner has no default duration per toast type, so this adds `site/src/components/Toaster/toast.ts`, a thin wrapper around Sonner's `toast`: - `toast.error` defaults to `duration: Infinity`. A caller can still set its own `duration`. - `toast.promise` failures get the same default. Sonner renders a rejected promise without calling `toast.error`, and the group member flow (DEQ-310) uses `toast.promise`. - All other methods pass straight through. All `toast` imports now go through the wrapper. A Biome `noRestrictedImports` rule blocks importing `toast` directly from `sonner`, following the existing `@pierre/diffs` rule. Tests that mocked `sonner` now mock the wrapper. Most of the 130-file diff is the import swap. ### Proof Error toast on the create token page (duplicate name), with the pointer off the toast so Sonner doesn't pause the timer. Shots are taken once the enter animation has finished: | | 1 second after the error | 11 seconds after the error | |---|---|---| | Before |  |  | | After |  |  | Also checked in the running app: the close button dismisses the error toast, and a "Token created" success toast still disappears on its own. `toast.test.tsx` covers the error and rejected `toast.promise` cases staying visible, dismissing them with the close button, and success toasts still timing out. With the persistent duration removed, the two error cases fail. <details> <summary>Decision log</summary> - A first version set `duration: Infinity` on the shared `<Toaster>` for every toast type. Product feedback: only error toasts were reported, so success and info toasts should keep their timer. - The wrapper sets the duration when each toast is created. The alternative was to watch toasts from `<Toaster>` with `useSonner` and change them afterward. That would add subscription and timer-ordering issues. - I didn't change Sonner's exported `toast` object in place. The wrapper plus a lint rule is explicit and enforced. - The experimental chat board undo toast (`runPlan.ts`) uses `toast()` with a timer that matches its undo window, so it isn't affected. - Not reproduced end to end: the Groups flow needs a Premium license, and the Cursor flow needs the app installed. Both use the `toast.promise` and `toast.error` paths the tests cover. </details> --- _Generated by Coder Agents on behalf of @chrifro._
137 lines
4.0 KiB
JSON
137 lines
4.0 KiB
JSON
{
|
|
"vcs": {
|
|
"enabled": true,
|
|
"clientKind": "git",
|
|
"useIgnoreFile": true,
|
|
"defaultBranch": "main"
|
|
},
|
|
"files": {
|
|
// static/*.html are Go templates with {{ }} directives that
|
|
// Biome's HTML parser does not support.
|
|
"includes": ["**", "!**/pnpm-lock.yaml", "!**/static/*.html"],
|
|
"ignoreUnknown": true,
|
|
"maxSize": 2097152
|
|
},
|
|
"linter": {
|
|
"rules": {
|
|
"a11y": {
|
|
"useKeyWithClickEvents": "off", // oxlint owns click-events-have-key-events
|
|
"useMediaCaption": "off", // oxlint owns media-has-caption
|
|
"noSvgWithoutTitle": "off",
|
|
"useButtonType": "off",
|
|
"useSemanticElements": "off",
|
|
"noStaticElementInteractions": "off"
|
|
},
|
|
"correctness": {
|
|
"noInvalidBuiltinInstantiation": "off", // oxlint owns new-for-builtins
|
|
"useExhaustiveDependencies": "off", // oxlint owns exhaustive-deps
|
|
"noUnusedImports": "warn",
|
|
"useUniqueElementIds": "off", // TODO: This is new but we want to fix it
|
|
"noNestedComponentDefinitions": "off", // TODO: Investigate, since it is used by shadcn components
|
|
"noUnusedVariables": {
|
|
"level": "warn",
|
|
"options": {
|
|
"ignoreRestSiblings": true
|
|
}
|
|
}
|
|
},
|
|
"security": {
|
|
"noDangerouslySetInnerHtml": "off" // oxlint owns no-danger
|
|
},
|
|
"style": {
|
|
"noNonNullAssertion": "off",
|
|
"noParameterAssign": "off",
|
|
"useDefaultParameterLast": "off",
|
|
"useSelfClosingElements": "off",
|
|
"useAsConstAssertion": "error",
|
|
"useEnumInitializers": "error",
|
|
"useSingleVarDeclarator": "error",
|
|
"useConsistentCurlyBraces": "off", // oxlint owns jsx-curly-brace-presence
|
|
"noUnusedTemplateLiteral": "error",
|
|
"useNumberNamespace": "error",
|
|
"noInferrableTypes": "off", // oxlint owns no-inferrable-types
|
|
"noUselessElse": "error",
|
|
"noRestrictedImports": {
|
|
"level": "error",
|
|
"options": {
|
|
"paths": {
|
|
"react": {
|
|
"message": "React 19 no longer requires forwardRef. Use ref as a prop instead.",
|
|
"importNames": ["forwardRef"]
|
|
},
|
|
"@pierre/diffs": {
|
|
"message": "Parse diffs through parseDiffString so every rendered file carries a content-derived cache key (pierrecomputer/pierre#1052).",
|
|
"importNames": [
|
|
"parsePatchFiles",
|
|
"processPatch",
|
|
"processFile",
|
|
"parseDiffFromFile"
|
|
]
|
|
},
|
|
// "#/components/Stack/Stack": "Use Tailwind flex utilities instead (e.g., <div className='flex flex-col gap-4'>).",
|
|
"sonner": {
|
|
"message": "Import toast from #/components/Toaster/toast.",
|
|
"importNames": ["toast"]
|
|
},
|
|
"lodash": "Use lodash/<name> instead."
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"suspicious": {
|
|
"noFallthroughSwitchClause": "off", // oxlint owns no-fallthrough
|
|
"noConfusingVoidType": "off", // oxlint owns no-invalid-void-type
|
|
"noExplicitAny": "off", // oxlint owns no-explicit-any
|
|
"noArrayIndexKey": "off",
|
|
"noThenProperty": "off",
|
|
"noTemplateCurlyInString": "off",
|
|
"useIterableCallbackReturn": "off",
|
|
"noUnknownAtRules": "off", // Allow Tailwind directives
|
|
"noEmptyInterface": "off", // oxlint owns no-empty-interface
|
|
"noConsole": "off" // oxlint owns no-console
|
|
},
|
|
"complexity": {
|
|
"noImportantStyles": "off", // TODO: check and fix !important styles
|
|
"noUselessThisAlias": "off",
|
|
"useLiteralKeys": "off"
|
|
}
|
|
}
|
|
},
|
|
"css": {
|
|
"parser": {
|
|
// Biome 2.3+ requires opt-in for @apply and other
|
|
// Tailwind directives.
|
|
"tailwindDirectives": true
|
|
}
|
|
},
|
|
"overrides": [
|
|
{
|
|
// parseDiff.ts is the single choke point allowed to import
|
|
// the raw @pierre/diffs parsers.
|
|
"includes": [
|
|
"**/DiffViewer/parseDiff.ts",
|
|
"**/DiffViewer/parseDiff.test.ts"
|
|
],
|
|
"linter": {
|
|
"rules": {
|
|
"style": {
|
|
"noRestrictedImports": "off"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
// toast.ts is the single choke point allowed to import Sonner's toast.
|
|
"includes": ["**/components/Toaster/toast.ts"],
|
|
"linter": {
|
|
"rules": {
|
|
"style": {
|
|
"noRestrictedImports": "off"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json"
|
|
}
|