mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## What kind of change does this PR introduce? Bug fix and design-system update. Resolves DEPR-316. Follow-up rename (`text-brand` → `text-primary`) is in a dedicated PR (https://github.com/supabase/supabase/pull/50564) stacked on this one. ## What is the current behavior? `text-brand` resolves to the canonical bright brand green in places that need readable text, which fails WCAG AA on light surfaces. A separate `text-brand-display` token adds another green for large type. ## What is the new behavior? - `text-brand` maps to accessible `--primary` (light mode darkened to meet ~4.5:1 AA) - `--hue` / `--primary-hue` aligned to 157.5 - `text-brand-display` removed; former display callsites use `text-brand` - Bright fills/borders stay on `brand-default` - Design-system colour and typography docs updated | Before | After | | --- | --- | | <img width="514" height="112" alt="CleanShot 2026-09-02 at 11 13 09@2x" src="https://github.com/user-attachments/assets/4e0138a9-a32d-4e4c-a426-90736706e1e7" /> | <img width="512" height="138" alt="CleanShot 2026-09-21 at 11 42 05@2x" src="https://github.com/user-attachments/assets/164cc5b1-a0c5-4e93-95f1-80016641a114" /> | | <img width="864" height="266" alt="CleanShot 2026-09-02 at 11 13 53@2x" src="https://github.com/user-attachments/assets/3c1ca53f-bf9e-431e-bc15-816b4a275b8e" /> | <img width="882" height="248" alt="CleanShot 2026-09-21 at 11 41 37@2x" src="https://github.com/user-attachments/assets/24828e7b-ed6b-44cb-b9dc-becc3398bdfc" /> | | <img width="782" height="692" alt="CleanShot 2026-09-02 at 11 16 30@2x" src="https://github.com/user-attachments/assets/fc871977-77bc-47fb-9e0e-9284e0ecd5cc" /> | <img width="730" height="690" alt="CleanShot 2026-09-21 at 11 42 52@2x" src="https://github.com/user-attachments/assets/bf479515-d5f9-471e-b82d-f097c0f4b56c" /> | | <img width="480" height="306" alt="CleanShot 2026-09-02 at 11 18 53@2x" src="https://github.com/user-attachments/assets/03f341f4-f02e-44f8-a2b2-8c31670d0427" /> | <img width="470" height="300" alt="CleanShot 2026-09-21 at 11 43 19@2x" src="https://github.com/user-attachments/assets/9df18217-d5e6-48b8-ba0b-579d2664b94b" /> | | <img width="960" height="300" alt="CleanShot 2026-09-02 at 11 32 04@2x" src="https://github.com/user-attachments/assets/6b1d9373-7a71-4247-81ff-26441604b09d" /> | <img width="980" height="306" alt="CleanShot 2026-09-21 at 11 44 13@2x" src="https://github.com/user-attachments/assets/41ad4784-02ec-4b29-b860-32af9fa79aa8" /> | | <img width="924" height="214" alt="CleanShot 2026-09-02 at 11 34 44@2x" src="https://github.com/user-attachments/assets/1de661fe-c7b6-499b-a94f-e4737436ec79" /> | <img width="752" height="162" alt="CleanShot 2026-09-21 at 11 44 56@2x" src="https://github.com/user-attachments/assets/1811890f-0660-4445-84e9-447720954fa1" /> | ## To test Test each callsite **in light mode** (dark mode is largely unchanged). ### WWW - [Homepage](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/): “Scale to millions” uses readable brand text (display token is gone) - [Careers](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/careers): small “Careers” eyebrow readable; green dividers stay bright `brand-default` - [Contact](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/contact-us): email / policy links use readable brand text - [Regions](https://zone-www-dot-com-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/regions): “Ask about early access to BYOC” readable ### Docs - [Docs homepage](https://docs-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs): “DOCS” wordmark and resource links readable - [Database overview](https://docs-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/guides/database/overview): nav / footer brand links readable - [JavaScript reference](https://docs-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/reference/javascript/introduction): active sidebar treatment readable ### Design system - [Typography](https://design-system-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/typography): documents `text-brand` only (no display) - [Colour usage](https://design-system-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/docs/color-usage): `text-brand` vs `bg-brand-default` - [Design-system homepage](https://design-system-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/): brand text examples across themes ### Studio - [Auth providers](https://studio-staging-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/dashboard/project/_/auth/providers): enabled provider badge text readable; status dot stays bright - [Database policies](https://studio-staging-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/dashboard/project/_/database/policies?new=true): template hover text more legible - [Database connections](https://studio-staging-git-dnywh-depr-316-brand-text-tokens-supabase.vercel.app/dashboard/project/_/observability/connections): “Live” status readable; animated dot stays bright green --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
236 lines
7.8 KiB
TypeScript
236 lines
7.8 KiB
TypeScript
'use client'
|
|
|
|
import { createClient } from '@supabase/supabase-js'
|
|
import { IS_PLATFORM } from '~/lib/constants'
|
|
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
|
import { gotrueClient, useConstant, useIsLoggedIn, type Database } from 'common'
|
|
import { Check, MessageSquareQuote, X } from 'lucide-react'
|
|
import { usePathname } from 'next/navigation'
|
|
import {
|
|
forwardRef,
|
|
useReducer,
|
|
useRef,
|
|
useState,
|
|
type CSSProperties,
|
|
type MouseEventHandler,
|
|
} from 'react'
|
|
import { Button, cn } from 'ui'
|
|
|
|
import { getSanitizedTabParams } from './Feedback.utils'
|
|
import { FeedbackModal, type FeedbackFields } from './FeedbackModal'
|
|
|
|
const FeedbackButton = forwardRef<
|
|
HTMLButtonElement,
|
|
{ isYes: boolean; onClick: MouseEventHandler; visible: boolean }
|
|
>(({ isYes, onClick, visible }, ref) => {
|
|
const isLoggedIn = useIsLoggedIn()
|
|
if (!isLoggedIn) return null
|
|
|
|
return (
|
|
<button
|
|
tabIndex={0}
|
|
ref={ref}
|
|
className={cn(
|
|
'mt-0',
|
|
'flex items-center gap-1',
|
|
'text-xs text-foreground-lighter',
|
|
'hover:text-foreground text-left',
|
|
!visible && 'opacity-0 invisible',
|
|
'[transition-property:opacity,color]',
|
|
'[transition-delay:700ms,0ms]'
|
|
)}
|
|
onClick={onClick}
|
|
>
|
|
{isYes ? <>What went well?</> : <>How can we improve?</>}
|
|
<MessageSquareQuote size={14} strokeWidth={1.5} />
|
|
</button>
|
|
)
|
|
})
|
|
FeedbackButton.displayName = 'FeedbackButton'
|
|
|
|
type Response = 'yes' | 'no'
|
|
|
|
enum StateType {
|
|
Unanswered = 'unanswered',
|
|
Followup = 'followup',
|
|
}
|
|
|
|
type State = { type: StateType.Unanswered } | { type: StateType.Followup; response: Response }
|
|
|
|
type Action = { event: 'VOTED'; response: Response }
|
|
|
|
const initialState = { type: StateType.Unanswered } satisfies State
|
|
|
|
function reducer(state: State, action: Action) {
|
|
switch (action.event) {
|
|
case 'VOTED':
|
|
if (state.type === StateType.Unanswered)
|
|
return { type: StateType.Followup, response: action.response }
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
function Feedback({ className }: { className?: string }) {
|
|
const [state, dispatch] = useReducer(reducer, initialState)
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const feedbackButtonRef = useRef<HTMLButtonElement>(null)
|
|
|
|
const pathname = usePathname() ?? ''
|
|
const sendTelemetryEvent = useSendTelemetryEvent()
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
|
|
const supabase = useConstant(() =>
|
|
IS_PLATFORM && supabaseUrl && supabaseAnonKey
|
|
? createClient<Database>(supabaseUrl, supabaseAnonKey)
|
|
: undefined
|
|
)
|
|
|
|
const unanswered = state.type === 'unanswered'
|
|
const isYes = 'response' in state && state.response === 'yes'
|
|
const isNo = 'response' in state && state.response === 'no'
|
|
const showYes = unanswered || isYes
|
|
const showNo = unanswered || isNo
|
|
|
|
async function sendFeedbackVote(response: Response) {
|
|
if (!supabase) return
|
|
const { error } = await supabase
|
|
.from('feedback')
|
|
.insert({ vote: response, page: pathname, metadata: { query: getSanitizedTabParams() } })
|
|
if (error) console.error(error)
|
|
}
|
|
|
|
function handleVote(response: Response) {
|
|
sendTelemetryEvent({
|
|
action: 'docs_feedback_clicked',
|
|
properties: { response },
|
|
})
|
|
sendFeedbackVote(response)
|
|
dispatch({ event: 'VOTED', response })
|
|
// Focus so screen reader users are aware of the new element
|
|
setTimeout(() => {
|
|
feedbackButtonRef.current?.focus()
|
|
// Wait for element to show up first
|
|
}, 700)
|
|
}
|
|
|
|
function refocusButton() {
|
|
setTimeout(() => {
|
|
feedbackButtonRef.current?.focus()
|
|
// Wait for modal to disappear and button to become focusable again
|
|
}, 100)
|
|
}
|
|
|
|
async function handleSubmit({ comment, title }: FeedbackFields) {
|
|
if (supabase) {
|
|
const userId = (await gotrueClient.getSession()).data.session?.user?.id ?? null
|
|
const { error } = await supabase.from('feedback_comments').insert({
|
|
page: pathname,
|
|
// @ts-expect-error -- the comment modal only opens after a vote, so state.response is set
|
|
vote: state.response,
|
|
title,
|
|
comment,
|
|
user_id: userId,
|
|
metadata: { query: getSanitizedTabParams() },
|
|
})
|
|
if (error) console.error(error)
|
|
}
|
|
setModalOpen(false)
|
|
refocusButton()
|
|
}
|
|
|
|
return (
|
|
<section className={cn('@container', className)} aria-labelledby="feedback-title">
|
|
<h2 id="feedback-title" className="block font-mono text-xs text-foreground-light mb-3">
|
|
Is this helpful?
|
|
</h2>
|
|
<div className="relative flex flex-col gap-2 @[12rem]:gap-4 @[12rem]:flex-row @[12rem]:items-center">
|
|
<div
|
|
style={{ '--container-flex-gap': '0.5rem' } as CSSProperties}
|
|
className="relative flex gap-2 items-center"
|
|
>
|
|
<Button
|
|
variant="outline"
|
|
rounded
|
|
className={cn(
|
|
'px-1 w-7 h-7',
|
|
'text-foreground-light',
|
|
'[transition-property:opacity,transform,color] [transition-duration:150ms,250ms,250ms]',
|
|
'motion-reduce:[transition-duration:150ms,1ms,300ms]',
|
|
'[transition-timing-function:cubic-bezier(.76,0,.23,1)]',
|
|
!isNo && 'hover:text-warning hover:border-warning-500',
|
|
isNo && `bg-warning text-warning-200 border-warning! disabled:opacity-100`,
|
|
!showNo && 'opacity-0 invisible'
|
|
)}
|
|
onClick={() => handleVote('no')}
|
|
disabled={state.type === StateType.Followup}
|
|
>
|
|
<X size={14} strokeWidth={2} className="text-current" />
|
|
<span className="sr-only">No</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
rounded
|
|
className={cn(
|
|
'px-1 w-7 h-7',
|
|
'text-foreground-light',
|
|
'[transition-property:opacity,transform,color] [transition-duration:150ms,250ms,250ms]',
|
|
'motion-reduce:[transition-duration:150ms,1ms,300ms]',
|
|
'[transition-timing-function:cubic-bezier(.76,0,.23,1)]',
|
|
!isYes && 'hover:text-brand-600 hover:border-brand-500',
|
|
isYes &&
|
|
'bg-brand-default text-brand-200 border-brand-default! disabled:opacity-100 -translate-x-[calc(100%+var(--container-inline-flex-gap,0.5rem))]',
|
|
!showYes && 'opacity-0 invisible'
|
|
)}
|
|
onClick={() => handleVote('yes')}
|
|
disabled={state.type === StateType.Followup}
|
|
>
|
|
<Check size={14} strokeWidth={2} />
|
|
<span className="sr-only">Yes</span>
|
|
</Button>
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
'flex flex-col gap-0.5',
|
|
'@[12rem]:absolute @[12rem]:left-9',
|
|
'text-xs',
|
|
'opacity-0 invisible',
|
|
'text-left',
|
|
'-translate-x-2',
|
|
'[transition-property:opacity,transform]',
|
|
'[transition-duration:450ms,300ms]',
|
|
'[transition-delay:200ms,0ms]',
|
|
'[transition-timing-function:cubic-bezier(.76,0,.23,1)]',
|
|
'motion-reduce:[transition-duration:150ms,1ms]',
|
|
'ease-out!',
|
|
state.type === StateType.Followup && 'opacity-100 visible translate-x-0'
|
|
)}
|
|
>
|
|
{state.type === StateType.Followup && (
|
|
<>
|
|
<span className="text-foreground-light">Thanks for your feedback!</span>
|
|
<FeedbackButton
|
|
ref={feedbackButtonRef}
|
|
onClick={() => setModalOpen(true)}
|
|
isYes={isYes}
|
|
visible={true}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<FeedbackModal
|
|
visible={modalOpen}
|
|
page={pathname}
|
|
onCancel={() => {
|
|
setModalOpen(false)
|
|
refocusButton()
|
|
}}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export { Feedback }
|