create-top-secret-starter 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/backend-overlay/web/src/components/sidebar-user.tsx +33 -0
  2. package/backend-overlay/web-router/src/routes/sign-in.tsx +16 -2
  3. package/backend-overlay/web-router/src/routes/sign-up.tsx +17 -3
  4. package/backend-overlay/web-start/src/routes/sign-in.tsx +16 -2
  5. package/backend-overlay/web-start/src/routes/sign-up.tsx +17 -3
  6. package/features.json +1 -1
  7. package/index.js +5 -3
  8. package/package.json +1 -1
  9. package/templates/template-router/.claude/skills/better-accessibility/SKILL.md +111 -0
  10. package/templates/template-router/.claude/skills/better-accessibility/agents/openai.yaml +3 -0
  11. package/templates/template-router/.claude/skills/better-accessibility/focus-and-keyboard.md +131 -0
  12. package/templates/template-router/.claude/skills/better-accessibility/forms.md +84 -0
  13. package/templates/template-router/.claude/skills/better-accessibility/hit-areas.md +94 -0
  14. package/templates/template-router/.claude/skills/better-accessibility/motion-and-zoom.md +79 -0
  15. package/templates/template-router/.claude/skills/better-accessibility/screen-readers.md +101 -0
  16. package/templates/template-router/.claude/skills/better-accessibility/semantics-and-aria.md +84 -0
  17. package/templates/template-router/.claude/skills/better-colors/SKILL.md +105 -0
  18. package/templates/template-router/.claude/skills/better-colors/agents/openai.yaml +3 -0
  19. package/templates/template-router/.claude/skills/better-colors/color-formats.md +90 -0
  20. package/templates/template-router/.claude/skills/better-colors/color-usage.md +118 -0
  21. package/templates/template-router/.claude/skills/better-colors/contrast.md +79 -0
  22. package/templates/template-router/.claude/skills/better-colors/palette-generation.md +104 -0
  23. package/templates/template-router/.claude/skills/better-colors/palette-structure.md +76 -0
  24. package/templates/template-router/.claude/skills/better-colors/token-naming.md +97 -0
  25. package/templates/template-router/.claude/skills/better-interface/SKILL.md +137 -0
  26. package/templates/template-router/.claude/skills/better-interface/agents/openai.yaml +3 -0
  27. package/templates/template-router/.claude/skills/better-interface/review-format.md +46 -0
  28. package/templates/template-router/.claude/skills/better-layout/SKILL.md +81 -0
  29. package/templates/template-router/.claude/skills/better-layout/agents/openai.yaml +3 -0
  30. package/templates/template-router/.claude/skills/better-layout/grouping-and-alignment.md +123 -0
  31. package/templates/template-router/.claude/skills/better-layout/spacing-and-adaptivity.md +159 -0
  32. package/templates/template-router/.claude/skills/better-typography/SKILL.md +162 -0
  33. package/templates/template-router/.claude/skills/better-typography/agents/openai.yaml +3 -0
  34. package/templates/template-router/.claude/skills/better-typography/choosing-fonts.md +64 -0
  35. package/templates/template-router/.claude/skills/better-typography/css-cheat-sheet.md +65 -0
  36. package/templates/template-router/.claude/skills/better-typography/details-and-accessibility.md +119 -0
  37. package/templates/template-router/.claude/skills/better-typography/spacing-and-sizing.md +121 -0
  38. package/templates/template-router/.claude/skills/better-typography/variable-fonts-and-opentype.md +105 -0
  39. package/templates/template-router/.claude/skills/better-typography/wrapping-and-punctuation.md +55 -0
  40. package/templates/template-router/.claude/skills/better-ui/SKILL.md +112 -0
  41. package/templates/template-router/.claude/skills/better-ui/agents/openai.yaml +3 -0
  42. package/templates/template-router/.claude/skills/better-ui/animations.md +205 -0
  43. package/templates/template-router/.claude/skills/better-ui/enter-exit.md +147 -0
  44. package/templates/template-router/.claude/skills/better-ui/icon-transitions.md +102 -0
  45. package/templates/template-router/.claude/skills/better-ui/icons.md +110 -0
  46. package/templates/template-router/.claude/skills/better-ui/performance.md +88 -0
  47. package/templates/template-router/.claude/skills/better-ui/surfaces.md +219 -0
  48. package/templates/template-router/.claude/skills/better-writing/SKILL.md +114 -0
  49. package/templates/template-router/.claude/skills/better-writing/agents/openai.yaml +3 -0
  50. package/templates/template-router/.claude/skills/break/SKILL.md +85 -0
  51. package/templates/template-router/.claude/skills/break/agents/openai.yaml +5 -0
  52. package/templates/template-router/.claude/skills/break/scenarios.md +78 -0
  53. package/templates/template-router/.claude/skills/explain-interface/SKILL.md +132 -0
  54. package/templates/template-router/.claude/skills/explain-interface/agents/openai.yaml +5 -0
  55. package/templates/template-router/.claude/skills/explain-interface/find-the-effect.md +94 -0
  56. package/templates/template-router/.claude/skills/explain-interface/from-an-image.md +55 -0
  57. package/templates/template-router/.claude/skills/explain-interface/no-browser.md +73 -0
  58. package/templates/template-router/.claude/skills/explain-interface/read-the-system.md +178 -0
  59. package/templates/template-router/.claude/skills/interface-review/SKILL.md +154 -0
  60. package/templates/template-router/.claude/skills/interface-review/agents/openai.yaml +5 -0
  61. package/templates/template-router/.claude/skills/interface-review/removed-signals.md +38 -0
  62. package/templates/template-router/.claude/skills/interface-review/scope-resolution.md +88 -0
  63. package/templates/template-router/.claude/skills/variant/SKILL.md +110 -0
  64. package/templates/template-router/.claude/skills/variant/agents/openai.yaml +5 -0
  65. package/templates/template-router/.claude/skills/variant/picker.md +76 -0
  66. package/templates/template-router/.oxfmtrc.json +7 -1
  67. package/templates/template-router/.oxlintrc.json +8 -18
  68. package/templates/template-router/e2e/keyboard.spec.ts +3 -3
  69. package/templates/template-router/e2e/orders.spec.ts +36 -4
  70. package/templates/template-router/src/components/app-shell.plain.tsx +5 -1
  71. package/templates/template-router/src/components/app-shell.tsx +6 -2
  72. package/templates/template-router/src/components/app-sidebar.tsx +64 -5
  73. package/templates/template-router/src/components/orders/create-order-dialog.tsx +36 -7
  74. package/templates/template-router/src/components/orders/orders-table-store.ts +50 -0
  75. package/templates/template-router/src/components/orders/orders-table.tsx +296 -41
  76. package/templates/template-router/src/components/sidebar-user.tsx +34 -0
  77. package/templates/template-router/src/components/skip-link.tsx +11 -0
  78. package/templates/template-router/src/components/theme-toggle.tsx +1 -1
  79. package/templates/template-router/src/components/ui/table.tsx +17 -2
  80. package/templates/template-router/src/components/ui/toast.tsx +24 -2
  81. package/templates/template-router/src/index.css +27 -3
  82. package/templates/template-router/src/routes/_authenticated/index.tsx +1 -1
  83. package/templates/template-router/src/routes/_authenticated/notes/$noteId.tsx +2 -1
  84. package/templates/template-router/src/routes/_authenticated/notes/index.tsx +43 -3
  85. package/templates/template-router/src/routes/_authenticated/orders/index.tsx +34 -1
  86. package/templates/template-router/src/routes/sign-in.tsx +2 -0
  87. package/templates/template-start/.claude/skills/better-accessibility/SKILL.md +111 -0
  88. package/templates/template-start/.claude/skills/better-accessibility/agents/openai.yaml +3 -0
  89. package/templates/template-start/.claude/skills/better-accessibility/focus-and-keyboard.md +131 -0
  90. package/templates/template-start/.claude/skills/better-accessibility/forms.md +84 -0
  91. package/templates/template-start/.claude/skills/better-accessibility/hit-areas.md +94 -0
  92. package/templates/template-start/.claude/skills/better-accessibility/motion-and-zoom.md +79 -0
  93. package/templates/template-start/.claude/skills/better-accessibility/screen-readers.md +101 -0
  94. package/templates/template-start/.claude/skills/better-accessibility/semantics-and-aria.md +84 -0
  95. package/templates/template-start/.claude/skills/better-colors/SKILL.md +105 -0
  96. package/templates/template-start/.claude/skills/better-colors/agents/openai.yaml +3 -0
  97. package/templates/template-start/.claude/skills/better-colors/color-formats.md +90 -0
  98. package/templates/template-start/.claude/skills/better-colors/color-usage.md +118 -0
  99. package/templates/template-start/.claude/skills/better-colors/contrast.md +79 -0
  100. package/templates/template-start/.claude/skills/better-colors/palette-generation.md +104 -0
  101. package/templates/template-start/.claude/skills/better-colors/palette-structure.md +76 -0
  102. package/templates/template-start/.claude/skills/better-colors/token-naming.md +97 -0
  103. package/templates/template-start/.claude/skills/better-interface/SKILL.md +137 -0
  104. package/templates/template-start/.claude/skills/better-interface/agents/openai.yaml +3 -0
  105. package/templates/template-start/.claude/skills/better-interface/review-format.md +46 -0
  106. package/templates/template-start/.claude/skills/better-layout/SKILL.md +81 -0
  107. package/templates/template-start/.claude/skills/better-layout/agents/openai.yaml +3 -0
  108. package/templates/template-start/.claude/skills/better-layout/grouping-and-alignment.md +123 -0
  109. package/templates/template-start/.claude/skills/better-layout/spacing-and-adaptivity.md +159 -0
  110. package/templates/template-start/.claude/skills/better-typography/SKILL.md +162 -0
  111. package/templates/template-start/.claude/skills/better-typography/agents/openai.yaml +3 -0
  112. package/templates/template-start/.claude/skills/better-typography/choosing-fonts.md +64 -0
  113. package/templates/template-start/.claude/skills/better-typography/css-cheat-sheet.md +65 -0
  114. package/templates/template-start/.claude/skills/better-typography/details-and-accessibility.md +119 -0
  115. package/templates/template-start/.claude/skills/better-typography/spacing-and-sizing.md +121 -0
  116. package/templates/template-start/.claude/skills/better-typography/variable-fonts-and-opentype.md +105 -0
  117. package/templates/template-start/.claude/skills/better-typography/wrapping-and-punctuation.md +55 -0
  118. package/templates/template-start/.claude/skills/better-ui/SKILL.md +112 -0
  119. package/templates/template-start/.claude/skills/better-ui/agents/openai.yaml +3 -0
  120. package/templates/template-start/.claude/skills/better-ui/animations.md +205 -0
  121. package/templates/template-start/.claude/skills/better-ui/enter-exit.md +147 -0
  122. package/templates/template-start/.claude/skills/better-ui/icon-transitions.md +102 -0
  123. package/templates/template-start/.claude/skills/better-ui/icons.md +110 -0
  124. package/templates/template-start/.claude/skills/better-ui/performance.md +88 -0
  125. package/templates/template-start/.claude/skills/better-ui/surfaces.md +219 -0
  126. package/templates/template-start/.claude/skills/better-writing/SKILL.md +114 -0
  127. package/templates/template-start/.claude/skills/better-writing/agents/openai.yaml +3 -0
  128. package/templates/template-start/.claude/skills/break/SKILL.md +85 -0
  129. package/templates/template-start/.claude/skills/break/agents/openai.yaml +5 -0
  130. package/templates/template-start/.claude/skills/break/scenarios.md +78 -0
  131. package/templates/template-start/.claude/skills/explain-interface/SKILL.md +132 -0
  132. package/templates/template-start/.claude/skills/explain-interface/agents/openai.yaml +5 -0
  133. package/templates/template-start/.claude/skills/explain-interface/find-the-effect.md +94 -0
  134. package/templates/template-start/.claude/skills/explain-interface/from-an-image.md +55 -0
  135. package/templates/template-start/.claude/skills/explain-interface/no-browser.md +73 -0
  136. package/templates/template-start/.claude/skills/explain-interface/read-the-system.md +178 -0
  137. package/templates/template-start/.claude/skills/interface-review/SKILL.md +154 -0
  138. package/templates/template-start/.claude/skills/interface-review/agents/openai.yaml +5 -0
  139. package/templates/template-start/.claude/skills/interface-review/removed-signals.md +38 -0
  140. package/templates/template-start/.claude/skills/interface-review/scope-resolution.md +88 -0
  141. package/templates/template-start/.claude/skills/variant/SKILL.md +110 -0
  142. package/templates/template-start/.claude/skills/variant/agents/openai.yaml +5 -0
  143. package/templates/template-start/.claude/skills/variant/picker.md +76 -0
  144. package/templates/template-start/.oxfmtrc.json +7 -1
  145. package/templates/template-start/.oxlintrc.json +8 -18
  146. package/templates/template-start/e2e/keyboard.spec.ts +3 -3
  147. package/templates/template-start/e2e/orders.spec.ts +36 -4
  148. package/templates/template-start/src/components/app-shell.plain.tsx +5 -1
  149. package/templates/template-start/src/components/app-shell.tsx +6 -2
  150. package/templates/template-start/src/components/app-sidebar.tsx +64 -5
  151. package/templates/template-start/src/components/orders/create-order-dialog.tsx +36 -7
  152. package/templates/template-start/src/components/orders/orders-table-store.ts +50 -0
  153. package/templates/template-start/src/components/orders/orders-table.tsx +296 -41
  154. package/templates/template-start/src/components/sidebar-user.tsx +34 -0
  155. package/templates/template-start/src/components/skip-link.tsx +11 -0
  156. package/templates/template-start/src/components/theme-toggle.tsx +1 -1
  157. package/templates/template-start/src/components/ui/table.tsx +17 -2
  158. package/templates/template-start/src/components/ui/toast.tsx +24 -2
  159. package/templates/template-start/src/index.css +27 -3
  160. package/templates/template-start/src/routes/_authenticated/index.tsx +1 -1
  161. package/templates/template-start/src/routes/_authenticated/notes/$noteId.tsx +2 -1
  162. package/templates/template-start/src/routes/_authenticated/notes/index.tsx +43 -3
  163. package/templates/template-start/src/routes/_authenticated/orders/index.tsx +34 -1
  164. package/templates/template-start/src/routes/sign-in.tsx +2 -0
@@ -0,0 +1,33 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ import { useSession } from '@/api/auth'
4
+ import { SidebarMenuButton, useSidebar } from '@/components/ui/sidebar'
5
+
6
+ const initialsOf = (email: string) => email.slice(0, 2).toUpperCase()
7
+
8
+ // the account row in the sidebar footer, reading the Better Auth session
9
+ export const SidebarUser = () => {
10
+ const { data: session } = useSession()
11
+ const { isMobile, setOpenMobile } = useSidebar()
12
+ if (!session) return null
13
+
14
+ const { email } = session.user
15
+ return (
16
+ <SidebarMenuButton
17
+ size='lg'
18
+ tooltip={email}
19
+ render={<Link to='/' onClick={() => isMobile && setOpenMobile(false)} />}
20
+ >
21
+ <span
22
+ aria-hidden='true'
23
+ className='flex size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-accent text-xs font-medium text-sidebar-accent-foreground'
24
+ >
25
+ {initialsOf(email)}
26
+ </span>
27
+ <span className='flex min-w-0 flex-col leading-tight'>
28
+ <span className='truncate text-xs text-muted-foreground'>Signed in as</span>
29
+ <span className='truncate font-medium'>{email}</span>
30
+ </span>
31
+ </SidebarMenuButton>
32
+ )
33
+ }
@@ -16,13 +16,26 @@ const SignInFormSchema = z.object({
16
16
  })
17
17
 
18
18
  const FIELDS = [
19
- { name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
20
- { name: 'password', label: 'Password', type: 'password', autoComplete: 'current-password' }
19
+ {
20
+ name: 'email',
21
+ label: 'Email',
22
+ type: 'email',
23
+ autoComplete: 'email',
24
+ placeholder: 'you@example.com'
25
+ },
26
+ {
27
+ name: 'password',
28
+ label: 'Password',
29
+ type: 'password',
30
+ autoComplete: 'current-password',
31
+ placeholder: '••••••••'
32
+ }
21
33
  ] as const satisfies ReadonlyArray<{
22
34
  name: keyof SignInInput
23
35
  label: string
24
36
  type: string
25
37
  autoComplete: string
38
+ placeholder: string
26
39
  }>
27
40
 
28
41
  export const Route = createFileRoute('/sign-in')({
@@ -61,6 +74,7 @@ function SignInPage() {
61
74
  id={field.name}
62
75
  type={field.type}
63
76
  autoComplete={field.autoComplete}
77
+ placeholder={field.placeholder}
64
78
  aria-invalid={Boolean(error)}
65
79
  aria-describedby={error ? `${field.name}-error` : undefined}
66
80
  {...register(field.name)}
@@ -18,14 +18,27 @@ const SignUpFormSchema = z.object({
18
18
  })
19
19
 
20
20
  const FIELDS = [
21
- { name: 'name', label: 'Name', type: 'text', autoComplete: 'name' },
22
- { name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
23
- { name: 'password', label: 'Password', type: 'password', autoComplete: 'new-password' }
21
+ { name: 'name', label: 'Name', type: 'text', autoComplete: 'name', placeholder: 'Ada Lovelace' },
22
+ {
23
+ name: 'email',
24
+ label: 'Email',
25
+ type: 'email',
26
+ autoComplete: 'email',
27
+ placeholder: 'you@example.com'
28
+ },
29
+ {
30
+ name: 'password',
31
+ label: 'Password',
32
+ type: 'password',
33
+ autoComplete: 'new-password',
34
+ placeholder: 'At least 8 characters'
35
+ }
24
36
  ] as const satisfies ReadonlyArray<{
25
37
  name: keyof SignUpInput
26
38
  label: string
27
39
  type: string
28
40
  autoComplete: string
41
+ placeholder: string
29
42
  }>
30
43
 
31
44
  export const Route = createFileRoute('/sign-up')({
@@ -64,6 +77,7 @@ function SignUpPage() {
64
77
  id={field.name}
65
78
  type={field.type}
66
79
  autoComplete={field.autoComplete}
80
+ placeholder={field.placeholder}
67
81
  aria-invalid={Boolean(error)}
68
82
  aria-describedby={error ? `${field.name}-error` : undefined}
69
83
  {...register(field.name)}
@@ -17,13 +17,26 @@ const SignInFormSchema = z.object({
17
17
  })
18
18
 
19
19
  const FIELDS = [
20
- { name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
21
- { name: 'password', label: 'Password', type: 'password', autoComplete: 'current-password' }
20
+ {
21
+ name: 'email',
22
+ label: 'Email',
23
+ type: 'email',
24
+ autoComplete: 'email',
25
+ placeholder: 'you@example.com'
26
+ },
27
+ {
28
+ name: 'password',
29
+ label: 'Password',
30
+ type: 'password',
31
+ autoComplete: 'current-password',
32
+ placeholder: '••••••••'
33
+ }
22
34
  ] as const satisfies ReadonlyArray<{
23
35
  name: keyof SignInInput
24
36
  label: string
25
37
  type: string
26
38
  autoComplete: string
39
+ placeholder: string
27
40
  }>
28
41
 
29
42
  export const Route = createFileRoute('/sign-in')({
@@ -65,6 +78,7 @@ function SignInPage() {
65
78
  id={field.name}
66
79
  type={field.type}
67
80
  autoComplete={field.autoComplete}
81
+ placeholder={field.placeholder}
68
82
  aria-invalid={Boolean(error)}
69
83
  aria-describedby={error ? `${field.name}-error` : undefined}
70
84
  {...register(field.name)}
@@ -19,14 +19,27 @@ const SignUpFormSchema = z.object({
19
19
  })
20
20
 
21
21
  const FIELDS = [
22
- { name: 'name', label: 'Name', type: 'text', autoComplete: 'name' },
23
- { name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
24
- { name: 'password', label: 'Password', type: 'password', autoComplete: 'new-password' }
22
+ { name: 'name', label: 'Name', type: 'text', autoComplete: 'name', placeholder: 'Ada Lovelace' },
23
+ {
24
+ name: 'email',
25
+ label: 'Email',
26
+ type: 'email',
27
+ autoComplete: 'email',
28
+ placeholder: 'you@example.com'
29
+ },
30
+ {
31
+ name: 'password',
32
+ label: 'Password',
33
+ type: 'password',
34
+ autoComplete: 'new-password',
35
+ placeholder: 'At least 8 characters'
36
+ }
25
37
  ] as const satisfies ReadonlyArray<{
26
38
  name: keyof SignUpInput
27
39
  label: string
28
40
  type: string
29
41
  autoComplete: string
42
+ placeholder: string
30
43
  }>
31
44
 
32
45
  export const Route = createFileRoute('/sign-up')({
@@ -68,6 +81,7 @@ function SignUpPage() {
68
81
  id={field.name}
69
82
  type={field.type}
70
83
  autoComplete={field.autoComplete}
84
+ placeholder={field.placeholder}
71
85
  aria-invalid={Boolean(error)}
72
86
  aria-describedby={error ? `${field.name}-error` : undefined}
73
87
  {...register(field.name)}
package/features.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "off": {
7
7
  "remove": [
8
8
  "src/components/app-sidebar.tsx",
9
+ "src/components/sidebar-user.tsx",
9
10
  "src/components/app-shell.tsx",
10
11
  "src/components/ui/sidebar.tsx",
11
12
  "src/components/ui/sheet.tsx",
@@ -26,7 +27,6 @@
26
27
  "src/components/orders",
27
28
  "src/mocks/orders.ts",
28
29
  "src/components/ui/table.tsx",
29
- "src/components/ui/dialog.tsx",
30
30
  "src/components/ui/dropdown-menu.tsx",
31
31
  "src/components/ui/select.tsx",
32
32
  "e2e/orders.spec.ts"
package/index.js CHANGED
@@ -23,7 +23,10 @@ const BACKEND_WEB_DEPS = { '@elysia/eden': '^1.4.10', 'better-auth': '^1.7.3' }
23
23
  // optional example features; features.json lists what each one removes when it is off
24
24
  const FEATURES = {
25
25
  sidebar: { label: 'Sidebar shell', hint: 'collapsible nav, ⌘B, state persisted' },
26
- table: { label: 'Orders table', hint: 'TanStack Table, URL-synced filters and sorting' },
26
+ table: {
27
+ label: 'Orders table',
28
+ hint: 'TanStack Table, URL-synced sorting, pin and drag columns'
29
+ },
27
30
  realtime: { label: 'Live dashboard', hint: 'WebSocket metrics feed, recharts' }
28
31
  }
29
32
  const FEATURE_IDS = Object.keys(FEATURES)
@@ -245,11 +248,10 @@ const substitute = (file, pairs) =>
245
248
  return text.replaceAll(from, to)
246
249
  }, source)
247
250
  )
248
- // the project name lands in the document title/description and the demo home page
251
+ // the project name lands in the document title/description and the sidebar brand
249
252
  const nameTargets = base => [
250
253
  path.join(base, 'index.html'),
251
254
  path.join(base, 'src', 'site.ts'),
252
- path.join(base, 'src', 'routes', '_authenticated', 'index.tsx'),
253
255
  path.join(base, 'src', 'components', 'app-sidebar.tsx')
254
256
  ]
255
257
  const applyName = files => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-top-secret-starter",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Scaffold a Vite + React 19 + TypeScript + shadcn starter with TanStack Router",
5
5
  "keywords": [
6
6
  "create",
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: better-accessibility
3
+ description: Helps your project comply with accessibility standards and best practices.
4
+ ---
5
+
6
+ # Accessibility
7
+
8
+ Most accessibility is free if you use the platform. Native elements ship with keyboard support, real labels announce themselves and a visible focus ring is one CSS rule.
9
+
10
+ Write every fix in the project's styling system, and use the exact values below rather than familiar-looking substitutes.
11
+
12
+ Reviewing means two walks. Keyboard-only, where every flow completes without a mouse. Then screen-reader, where every control announces a name, a role and its state. When unsure, take the platform default over a custom rebuild, and remove ARIA rather than add it.
13
+
14
+ Contrast measurement and color fixes belong to `better-colors`. Text sizing and iOS input zoom belong to `better-typography`. Spatial RTL layout belongs to `better-layout`.
15
+
16
+ ## Native elements first
17
+
18
+ The first rule of ARIA: don't use ARIA when a native element exists. `<button>` for actions, `<a href>` for navigation, never `<div onClick>`. A real link must support Cmd/Ctrl/middle-click. No ARIA is better than bad ARIA. See [semantics-and-aria.md](semantics-and-aria.md) for landmarks, button-vs-link and disabled states.
19
+
20
+ ## Visible focus rings
21
+
22
+ Style `:focus-visible`, not bare `:focus`. Keyboard users get a ring and mouse users usually don't. Prefer the browser's unmodified indicator.
23
+
24
+ A custom ring needs a project focus token or another explicit color. Verify the whole indicator against every adjacent color it crosses, `currentColor` included. Use at least a `2px` solid perimeter or an equivalent visible area. Never use `outline: none` without a verified replacement, and preserve system colors in forced-colors mode. Recipes are in [focus-and-keyboard.md](focus-and-keyboard.md).
25
+
26
+ ## Full keyboard support
27
+
28
+ Every pointer interaction needs a keyboard path. Follow the ARIA APG patterns: Escape closes overlays, arrow keys move within composite widgets, Tab moves between widgets, Enter and Space activate.
29
+
30
+ Use only `tabindex="0"` to join the natural tab order and `tabindex="-1"` for programmatic focus. Positive values break that order. Composite widgets use roving tabindex, where the active item is `0` and every other is `-1`.
31
+
32
+ ## Trap and restore focus
33
+
34
+ Modals set `inert` on the background content, move focus inside on open and return focus to the trigger on close. Add `overscroll-behavior: contain` so background content doesn't scroll.
35
+
36
+ ## Minimum hit area
37
+
38
+ WCAG 2.5.8's Level AA baseline is a 24×24 CSS-pixel target, or one of its spacing, equivalent-control, inline, user-agent and essential exceptions. Aim for 44×44px on touch and 40×40px on desktop where density permits. Extend with a pseudo-element when the visible element should stay smaller.
39
+
40
+ Never let extended hit areas overlap. Give decorative layers `pointer-events: none`, so a glow never swallows the clicks meant for the control beneath it. Sizes and collision rules are in [hit-areas.md](hit-areas.md).
41
+
42
+ ## Label and type every control
43
+
44
+ Every input gets a `<label for>` or a wrapping `<label>`. A placeholder is never a label. Label and control share one hit target, with no dead zone between a checkbox and its text.
45
+
46
+ Add `autocomplete` with a meaningful `name`, plus the `type` and `inputmode` that summon the right keyboard. Never block paste; users paste passwords and one-time codes. See [forms.md](forms.md).
47
+
48
+ ## Errors that announce
49
+
50
+ Keep submit enabled until the request starts, then disable with a spinner and the original label. Validate on submit. Mark failing fields `aria-invalid="true"`, point `aria-describedby` at the inline error text and focus the first invalid field.
51
+
52
+ Use native `disabled` when a control is genuinely unavailable. Reach for `aria-disabled="true"` only when it should stay focusable, then block pointer, keyboard and form behavior in code and style the state explicitly.
53
+
54
+ ## Accessible names everywhere
55
+
56
+ Icon-only buttons need a descriptive `aria-label`. Visible label text must appear in the accessible name. Decorative elements get `aria-hidden="true"`, never on a focusable element.
57
+
58
+ ## Don't rely on color alone
59
+
60
+ Status needs a redundant cue: an icon, text, or an underline alongside the color. Work out which WCAG contrast requirement applies, then use `better-colors` to measure the rendered pair. When it fails, report the pair and the requirement it misses, and leave the colors alone unless asked.
61
+
62
+ ## Honor prefers-reduced-motion
63
+
64
+ Wrap motion in `@media (prefers-reduced-motion: no-preference)` so it is opt-in. Under reduced motion, replace slides and scales with opacity crossfades, and kill parallax and autoplay entirely.
65
+
66
+ Two rules hold regardless of the preference. Autoplaying media needs a visible pause control, and toasts carrying an action or an error stay until dismissed. See [motion-and-zoom.md](motion-and-zoom.md).
67
+
68
+ ## Announce dynamic content
69
+
70
+ Three mechanisms, three jobs. `aria-describedby` carries field-specific validation. A polite live region (`role="status"`) carries non-urgent updates not tied to a control, such as toasts and result counts. `role="alert"` carries urgent untied errors and nothing else.
71
+
72
+ Repeated polite announcements need a stable empty region rendered before its text updates. Dynamically inserted alerts vary in support, so test them on the screen readers you target. See [screen-readers.md](screen-readers.md).
73
+
74
+ ## Alt text by purpose
75
+
76
+ Decorative images get `alt=""`. Informative images describe the meaning. Functional images describe the action: a search icon button is `alt="Search"`, not `alt="magnifying glass"`.
77
+
78
+ ## Structure is navigation
79
+
80
+ Use headings that describe their sections and form a coherent outline. Give the page one `<h1>` and nest the levels below it without skipping. Expose one visible primary `<main>` landmark. When repeated navigation or chrome precedes it, make a "Skip to content" link the first focusable element. Anchored headings get `scroll-margin-top`.
81
+
82
+ ## Survive zoom and text resize
83
+
84
+ The page must work at 200% zoom and reflow at 320px width without horizontal scrolling. Use `min-height` rather than fixed `height` on text containers. Prefer `rem` breakpoints where they fit the codebase, and never let the viewport meta cap how far the reader can zoom.
85
+
86
+ ## Before you finish
87
+
88
+ | Mistake | Fix |
89
+ | --- | --- |
90
+ | Custom focus color assumed to work everywhere | Verify it against every adjacent color and in forced-colors mode |
91
+ | Repeated polite update inconsistently announced | Keep a stable empty status region and update its text |
92
+ | `assertive` live region for a routine toast | Use `polite`; reserve `assertive` for errors |
93
+ | `aria-hidden="true"` on a focusable element | Remove it or make the element non-focusable |
94
+ | Submit disabled until the form is valid | Keep it enabled; validate on submit and focus the first error |
95
+ | Hover treatment stuck after a tap on touch | Gate hover styling with `@media (hover: hover)` |
96
+ | Tooltip on a natively `disabled` control | Text beside it, or `aria-disabled` so it stays focusable |
97
+
98
+ ## Reporting
99
+
100
+ **Severity.** `HIGH` prevents a task, hides content from assistive technology, or creates a systemic failure. `MEDIUM` makes an interaction meaningfully harder. `LOW` is isolated polish.
101
+
102
+ **Verification.** Without a browser: accessible names on every interactive element, keyboard handlers on non-native controls, focus styles, `prefers-reduced-motion` guards and form labels bound to their inputs. With one: tab the flow in order, read computed names and roles from the accessibility tree, confirm a visible focus indicator at every stop and run an automated audit. Report every check you could not run as `Not verified`.
103
+
104
+ **Format.** Group findings under the principle each violates, ordered by severity, one row per root cause listing every location it appears in:
105
+
106
+ | Severity | Location | Before | After | Why |
107
+ | --- | --- | --- | --- | --- |
108
+
109
+ `Location` is `path/to/file:line`. `Why` names the principle and the user impact.
110
+
111
+ End with `Block` when any `HIGH` remains, `Approve` otherwise, leaving the rest in the table as work to do. Never `Approve` coverage you did not inspect. With nothing to report, state "No actionable accessibility findings" and report verification.
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "Better Accessibility"
3
+ short_description: "Focus, keyboard, ARIA, forms and screen readers"
@@ -0,0 +1,131 @@
1
+ # Focus and keyboard
2
+
3
+ Focus rings, skip links, tabindex, focus trapping and the APG keyboard patterns.
4
+
5
+ ## Focus rings
6
+
7
+ Style `:focus-visible`, not bare `:focus`. The browser shows it for keyboard and assistive-tech focus and suppresses it for mouse clicks, where focus is already obvious. Never write `outline: none` or `focus:outline-none` without a visible replacement, which removes keyboard navigation for sighted keyboard users.
8
+
9
+ Prefer the browser's unmodified focus indicator, which adapts to platform and forced-color settings without the author predicting every background. Adding only `outline-offset` preserves it. A custom `outline: 2px solid` with no color renders `currentColor`, which is not automatically accessible, because the outline may cross colors unlike the text's own background. The preference order:
10
+
11
+ ```css
12
+ /* Best: keep the browser ring, just give it breathing room */
13
+ :focus-visible {
14
+ outline-offset: 2px;
15
+ }
16
+
17
+ /* Custom ring when the design requires one: use the project's verified token */
18
+ :focus-visible {
19
+ outline: 2px solid var(--focus-ring);
20
+ outline-offset: 2px;
21
+ }
22
+ ```
23
+
24
+ ```tsx
25
+ // Tailwind: use the project's focus token or established focus-ring utility
26
+ <button className="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--focus-ring)]">
27
+ Save
28
+ </button>
29
+ ```
30
+
31
+ A custom focus indicator must meet the applicable project or WCAG target for visible area and change of contrast. Inspect the whole perimeter against every adjacent color it crosses: component fills, page surfaces, images, gradients, hover and selected states. A token, brand color, or `currentColor` passes only when that rendered check does.
32
+
33
+ In `forced-colors: active` (Windows High Contrast), keep the default color adjustment or name a system color such as `Highlight`. `forced-color-adjust: none` freezes the authored color, so use it only where you have checked the control stays perceivable.
34
+
35
+ Group focus styles with `:focus-within` when a wrapper should light up while an inner input has focus (e.g. a search box with an icon inside the border).
36
+
37
+ ## Skip link
38
+
39
+ Target `<main id="main">` and visually hide the link until focused:
40
+
41
+ ```css
42
+ .skip-link {
43
+ position: absolute;
44
+ inset-inline-start: -999px;
45
+ }
46
+ .skip-link:focus {
47
+ inset-inline-start: 16px;
48
+ top: 16px;
49
+ }
50
+ ```
51
+
52
+ ```html
53
+ <body>
54
+ <a class="skip-link" href="#main">Skip to content</a>
55
+ <header>…</header>
56
+ <main id="main">…</main>
57
+ </body>
58
+ ```
59
+
60
+ Give in-page anchor targets `scroll-margin-top`, such as `80px` under a sticky header, so the target isn't hidden when jumped to.
61
+
62
+ ## tabindex rules
63
+
64
+ - `tabindex="0"`: adds an element to the natural tab order. Only for custom interactive elements that aren't natively focusable.
65
+ - `tabindex="-1"`: focusable via JavaScript only (`el.focus()`). Use for headings you move focus to, modal containers and roving-tabindex members.
66
+ - Positive `tabindex`: never. It hijacks the tab order for the whole page. Fix the DOM order instead.
67
+
68
+ ### Roving tabindex
69
+
70
+ Composite widgets, meaning tabs, menus, toolbars and radio groups, occupy one Tab stop. The active item has `tabindex="0"`, all others `tabindex="-1"`, and arrow keys move both focus and the `0`:
71
+
72
+ ```tsx
73
+ <div role="tablist">
74
+ {tabs.map((tab, i) => (
75
+ <button
76
+ role="tab"
77
+ tabIndex={i === activeIndex ? 0 : -1}
78
+ aria-selected={i === activeIndex}
79
+ onKeyDown={handleArrowKeys} // ArrowLeft/ArrowRight move activeIndex, wrapping
80
+ >
81
+ {tab.label}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ ```
86
+
87
+ ## Focus trapping and restoration
88
+
89
+ Modals must trap focus. Put `inert` on everything behind the dialog, which removes background content from the tab order and from assistive tech in one move:
90
+
91
+ ```tsx
92
+ // On open
93
+ document.getElementById("app-content").inert = true;
94
+ const dialog = dialogRef.current;
95
+ (dialog.querySelector("[autofocus]") ??
96
+ dialog.querySelector("button, [href], input, select, textarea"))?.focus();
97
+
98
+ // On close
99
+ document.getElementById("app-content").inert = false;
100
+ triggerRef.current?.focus(); // always return focus to the element that opened it
101
+ ```
102
+
103
+ Prefer native `<dialog>` with `showModal()`, which gives you the trap, the `inert` background and Escape handling for free. A custom overlay that can't use it needs `role="dialog"`, `aria-modal="true"` and an accessible name via `aria-labelledby`. Either way:
104
+
105
+ - On open, focus the first focusable element. For destructive confirmations, focus the least destructive action instead.
106
+ - On close, return focus to the trigger, or to the nearest logical container if the trigger is gone.
107
+ - Add `overscroll-behavior: contain` on the dialog so scrolling inside never scrolls the page behind it.
108
+
109
+ ## Keyboard patterns (ARIA APG)
110
+
111
+ Native elements come with these behaviors; custom widgets must implement them. A role is a promise. Give something `role="tab"` and users expect the full tab keyboard model.
112
+
113
+ | Widget | Keys |
114
+ | --- | --- |
115
+ | Dialog | Tab/Shift+Tab cycle inside (wrap at ends); Escape closes |
116
+ | Tabs | Arrow keys move between tabs (wrapping); Tab exits to the panel; Home/End jump to first/last |
117
+ | Menu button | Enter/Space/ArrowDown opens and focuses first item; ArrowUp opens and focuses last; arrows navigate; Escape closes and refocuses the button |
118
+ | Disclosure / accordion | Header is a `<button aria-expanded>`; Enter and Space toggle |
119
+ | Combobox | ArrowDown opens/moves into the list; Enter accepts; Escape closes and returns to the input; typing filters |
120
+ | Listbox / radio group | Arrow keys move selection; one Tab stop for the whole group |
121
+
122
+ Universal rules:
123
+
124
+ - Escape dismisses whatever opened last: tooltip, then menu, then dialog.
125
+ - Arrow keys, not Tab, move within a composite widget; Tab moves between widgets.
126
+ - Tabs choose activation mode: automatic (panel switches on arrow focus) when panels render instantly, manual (Enter/Space to activate) when switching is expensive.
127
+ - Enter submits the focused input's form. In `<textarea>`, Enter inserts a newline and ⌘/Ctrl+Enter submits.
128
+
129
+ ## SPA route changes
130
+
131
+ Client-side navigation doesn't reset focus or announce anything. On route change, update `document.title` to match the new context, then move focus to the new view's `<h1>` (given `tabindex="-1"`) or to `<main>`. Restore scroll position on back and forward navigation, and scroll to top on forward navigation.
@@ -0,0 +1,84 @@
1
+ # Forms
2
+
3
+ Labels, autocomplete, error messaging, input types and submit behavior.
4
+
5
+ ## Labels
6
+
7
+ Every control needs a programmatic label: `<label for>` pointing at the input's `id`, or a wrapping `<label>`. A placeholder is never a label. It disappears the moment the user types and usually fails contrast.
8
+
9
+ ```html
10
+ <!-- Good: explicit association -->
11
+ <label for="email">Email</label>
12
+ <input id="email" type="email" autocomplete="email" />
13
+
14
+ <!-- Good: wrapping label, so label and control share one hit target -->
15
+ <label>
16
+ <input type="checkbox" /> Send me updates
17
+ </label>
18
+ ```
19
+
20
+ Label and control share one hit target, so clicking "Send me updates" toggles the checkbox with no dead zone between them. Mark required fields with native `required` plus a visible indicator explained once per form ("* required").
21
+
22
+ A placeholder used *in addition to* a label shows an example of the expected format: `placeholder="name@company.com"`.
23
+
24
+ ## Error messaging
25
+
26
+ The complete pattern:
27
+
28
+ ```html
29
+ <label for="email">Email</label>
30
+ <input
31
+ id="email"
32
+ type="email"
33
+ autocomplete="email"
34
+ aria-invalid="true"
35
+ aria-describedby="email-error"
36
+ />
37
+ <p id="email-error">Enter a valid email address.</p>
38
+ ```
39
+
40
+ - `aria-invalid="true"` on the failing field, removed once fixed.
41
+ - `aria-describedby` links the field to its inline error so screen readers announce it with the field.
42
+ - Errors render inline beside their fields, with an icon or text. Never a red border alone, which is a color-only cue.
43
+ - On submit, focus the first invalid field.
44
+ - Allow incomplete submission so validation can surface. Never disable submit until valid (see below).
45
+ - Accept free text and validate after. Never block typing or filter characters as the user types. Trim values before validating, because autocomplete and text expansion add trailing spaces.
46
+
47
+ ## Autocomplete and input types
48
+
49
+ `autocomplete` with a meaningful `name` fills forms in one tap and is a WCAG requirement (1.3.5) for fields about the user. The common tokens:
50
+
51
+ | Field | `autocomplete` |
52
+ | --- | --- |
53
+ | Name | `name` (or `given-name` / `family-name`) |
54
+ | Email | `email` |
55
+ | Phone | `tel` |
56
+ | Address | `street-address`, `address-line1`, `postal-code`, `country` |
57
+ | Card | `cc-number`, `cc-exp`, `cc-csc`, `cc-name` |
58
+ | Login | `username`, `current-password` |
59
+ | Signup / reset | `new-password` |
60
+ | 2FA code | `one-time-code` |
61
+
62
+ Prefix with a section where relevant: `autocomplete="shipping street-address"`.
63
+
64
+ Correct `type` and `inputmode` pick the right mobile keyboard:
65
+
66
+ | Input | Use |
67
+ | --- | --- |
68
+ | Email, URL, phone | `type="email"`, `type="url"`, `type="tel"` |
69
+ | OTP / PIN / card number | `type="text" inputmode="numeric"` (keeps text semantics, no spinner) |
70
+ | Money, decimals | `type="text" inputmode="decimal"` |
71
+ | True numeric quantity | `type="number"` |
72
+
73
+ Disable spellcheck on emails, codes and usernames: `spellcheck="false"`.
74
+
75
+ ## Never fight the user's tools
76
+
77
+ - Stay compatible with password managers and 2FA autofill: real `<form>`, correct `autocomplete`, no fake inputs.
78
+
79
+ ## Submit behavior
80
+
81
+ - Keep submit enabled until the request starts, then disable it and show a spinner *beside the original label*. "Save" with a spinner, not a bare spinner. The label is what tells assistive tech which button is busy.
82
+ - Announce results. Success goes through a polite live region. On failure, focus the first invalid field, which is itself the announcement. Reserve `role="alert"` for form-level errors not tied to a field ([screen-readers.md](screen-readers.md)).
83
+ - Warn on unsaved changes before navigation, and never lose typed input to a re-render. Hydration must preserve focus and value.
84
+ - Enter submits from any focused input; in `<textarea>`, ⌘/Ctrl+Enter submits.
@@ -0,0 +1,94 @@
1
+ # Hit areas
2
+
3
+ Target sizes, expanding hit areas without changing visual size and collision rules.
4
+
5
+ ## Target sizes
6
+
7
+ Separate the conformance baseline from larger usability targets:
8
+
9
+ | Standard | Minimum |
10
+ | --- | --- |
11
+ | WCAG 2.5.8 (AA) | 24×24px, the hard floor |
12
+ | WCAG 2.5.5 (AAA) | 44×44px |
13
+ | Apple HIG | 44×44pt |
14
+ | Material Design | 48×48dp |
15
+
16
+ WCAG 2.5.8 Level AA requires a 24×24 CSS-pixel target or one of its exceptions. Treat 44px as the recommended touch target for primary controls and 40px as a useful desktop target where density permits. Smaller controls are not automatic failures. Check the spacing, equivalent-control, inline, user-agent and essential exceptions before reporting one.
17
+
18
+ Under the spacing exception, an undersized target passes when a 24px circle centered on its bounding box intersects no other target and no other undersized target's circle. In the simple case, 20px targets need a 4px gap.
19
+
20
+ The visible element can stay small; the hit area is what must be big. Anything that looks clickable must be clickable across its whole visual extent, with no dead zones. A checkbox and its label share one hit target.
21
+
22
+ ## Expanding the hit area
23
+
24
+ Where the visible element is smaller, say a 20×20 checkbox, extend the hit area with a pseudo-element. Put it on the wrapping `<label>` or `<button>`, never on the `<input>`, because replaced elements don't render `::before`/`::after` reliably.
25
+
26
+ ### CSS example
27
+
28
+ ```css
29
+ /* Small checkbox with expanded 44px hit area, on the wrapping label */
30
+ .checkbox-label {
31
+ position: relative;
32
+ width: 20px;
33
+ height: 20px;
34
+ }
35
+
36
+ .checkbox-label::after {
37
+ content: "";
38
+ position: absolute;
39
+ top: 50%;
40
+ left: 50%; /* physical centering: direction-independent */
41
+ transform: translate(-50%, -50%);
42
+ width: 44px;
43
+ height: 44px;
44
+ }
45
+ ```
46
+
47
+ ### Tailwind example
48
+
49
+ ```tsx
50
+ <button className="relative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-1/2">
51
+ <CheckIcon />
52
+ </button>
53
+ ```
54
+
55
+ ### Layout alternative
56
+
57
+ Where the element can afford real box size, skip the pseudo-element and let the box be the target. That hands the browser real geometry for scrolling and gestures:
58
+
59
+ ```css
60
+ .icon-button {
61
+ min-width: 44px;
62
+ min-height: 44px;
63
+ display: inline-grid;
64
+ place-items: center;
65
+ }
66
+ ```
67
+
68
+ ## Collision rule
69
+
70
+ Where the extended hit area overlaps another interactive element, shrink the pseudo-element to the largest size that does not collide. Two interactive elements never have overlapping hit areas.
71
+
72
+ ## Decorative layers
73
+
74
+ A decorative layer painted over interactive content absorbs every pointer event its box covers: a gradient scrim, a glow, a blurred sheen, a full-bleed `::after`. The control underneath looks live and does nothing, and no hit-area sizing fixes it.
75
+
76
+ Give each one `pointer-events: none` (Tailwind `pointer-events-none`) so events reach the control below, plus `aria-hidden="true"` to keep it out of the accessibility tree:
77
+
78
+ ```css
79
+ .card-glow {
80
+ position: absolute;
81
+ inset: 0;
82
+ pointer-events: none;
83
+ }
84
+ ```
85
+
86
+ Keep pointer events on any layer the user is meant to hit: a modal scrim that dismisses on click is a control, not decoration.
87
+
88
+ ## Touch behavior
89
+
90
+ - Add `touch-action: manipulation` to interactive elements to remove the double-tap-to-zoom delay on mobile.
91
+ - Set `touch-action: none` on a surface implementing its own pan, zoom, or drag gestures, so the browser stops claiming them for scrolling and pinch-zoom. Scope it to that surface; at page level it removes scrolling.
92
+ - Set `-webkit-tap-highlight-color` to match the design instead of the default gray flash.
93
+ - Put hover-only styling behind `@media (hover: hover)`. On touch, `:hover` latches after a tap and holds until the user taps elsewhere, so it reads as a stuck selected state. Tailwind 4's `hover:` variant already compiles under this query.
94
+ - Prefer generous targets and clear affordances over finicky interactions such as tiny drag handles and precise hover zones.