2 Commits

Author SHA1 Message Date
google-labs-jules[bot]
b7a0cb589b 🎨 Palette: Improve accessibility for password visibility toggles
- Added `aria-label` and `aria-pressed` to password visibility toggle buttons in Login, Register, and Reset Password pages.
- Added missing `showPassword` and `hidePassword` translations in English and Chinese.
- This ensures screen reader users can understand and interact with the password toggle functionality.

Co-authored-by: tinkle-community <240652709+tinkle-community@users.noreply.github.com>
2026-01-29 15:53:25 +00:00
wqqqqqq
9dbc861cdf feat: add depth websocket from coinank (#1362) 2026-01-27 22:07:38 +08:00
8 changed files with 187 additions and 0 deletions

3
.Jules/palette.md Normal file
View File

@@ -0,0 +1,3 @@
## 2025-05-14 - Password Visibility Toggle Accessibility
**Learning:** Icon-only buttons for toggling password visibility are often missing `aria-label` and `aria-pressed` states, making them unusable for screen reader users who need to verify their input.
**Action:** Always include dynamic `aria-label` (Switching between "Show password" and "Hide password") and `aria-pressed` state for password toggles.

View File

@@ -0,0 +1,108 @@
package coinank_api
import (
"context"
"encoding/json"
"nofx/provider/coinank/coinank_enum"
"golang.org/x/net/websocket"
)
const MainDepthWsUrl = "wss://ws.coinank.com/wsDepth/wsKline"
type DepthWs struct {
conn *websocket.Conn
DepthV3Ch <-chan *WsResult[DepthV3]
}
// DepthWsConn connect ws , read data from DepthV3Ch
func DepthWsConn(ctx context.Context) (*DepthWs, error) {
conn, ch, err := depth_ws(ctx)
if err != nil {
return nil, err
}
ws := &DepthWs{
conn: conn,
DepthV3Ch: ch,
}
return ws, nil
}
// Subscribe subscribe depth
func (ws *DepthWs) Subscribe(symbol string, exchange coinank_enum.Exchange, step string) error {
var args = "depthV3@" + symbol + "@" + string(exchange) + "@SWAP@" + step
info := SubscribeInfo{
Op: "subscribe",
Args: args,
}
json, err := json.Marshal(info)
if err != nil {
return err
}
err = websocket.Message.Send(ws.conn, json)
if err != nil {
return err
}
return nil
}
// UnSubscribe unsubscribe depth
func (ws *DepthWs) UnSubscribe(symbol string, exchange coinank_enum.Exchange, step string) error {
var args = "depthV3@" + symbol + "@" + string(exchange) + "@SWAP@" + step
info := SubscribeInfo{
Op: "unsubscribe",
Args: args,
}
json, err := json.Marshal(info)
if err != nil {
return err
}
err = websocket.Message.Send(ws.conn, json)
if err != nil {
return err
}
return nil
}
// Close websocket
func (ws *DepthWs) Close() error {
return ws.conn.Close()
}
func depth_ws(ctx context.Context) (*websocket.Conn, <-chan *WsResult[DepthV3], error) {
config, err := websocket.NewConfig(MainDepthWsUrl, "http://localhost")
if err != nil {
return nil, nil, err
}
conn, err := config.DialContext(ctx)
if err != nil {
return nil, nil, err
}
ch := make(chan *WsResult[DepthV3], 1024)
go depth_read(conn, ch)
return conn, ch, nil
}
func depth_read(conn *websocket.Conn, ch chan *WsResult[DepthV3]) {
defer conn.Close()
defer close(ch)
var msg string
for {
err := websocket.Message.Receive(conn, &msg)
if err != nil {
return
}
var depth WsResult[DepthV3]
err = json.Unmarshal([]byte(msg), &depth)
if err == nil {
ch <- &depth
}
}
}
type DepthV3 struct {
Type string `json:"type"`
Ts uint64 `json:"ts"`
Asks [][]string `json:"asks"`
Bids [][]string `json:"bids"`
}

View File

@@ -0,0 +1,42 @@
package coinank_api
import (
"context"
"encoding/json"
"fmt"
"nofx/provider/coinank/coinank_enum"
"testing"
"time"
)
func TestDepthWs(t *testing.T) {
ctx := context.TODO()
ws, err := DepthWsConn(ctx)
if err != nil {
t.Fatal(err)
}
go func() {
for tickers := range ws.DepthV3Ch {
msg, err := json.Marshal(tickers)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(msg))
}
fmt.Println("DepthV3Ch closed")
}()
err = ws.Subscribe("BTCUSDT", coinank_enum.Binance, "0.1")
if err != nil {
t.Fatal(err)
}
fmt.Println("sub success")
time.Sleep(10 * time.Second)
err = ws.UnSubscribe("BTCUSDT", coinank_enum.Binance, "0.1")
if err != nil {
t.Fatal(err)
}
fmt.Println("unsub success")
time.Sleep(10 * time.Second)
ws.Close()
fmt.Println("cancel success")
}

View File

@@ -334,6 +334,12 @@ export function LoginPage() {
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
aria-label={
showPassword
? t('hidePassword', language)
: t('showPassword', language)
}
aria-pressed={showPassword}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>

View File

@@ -248,6 +248,12 @@ export function RegisterPage() {
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
aria-label={
showPassword
? t('hidePassword', language)
: t('showPassword', language)
}
aria-pressed={showPassword}
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
@@ -269,6 +275,12 @@ export function RegisterPage() {
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
aria-label={
showConfirmPassword
? t('hidePassword', language)
: t('showPassword', language)
}
aria-pressed={showConfirmPassword}
>
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>

View File

@@ -150,6 +150,12 @@ export function ResetPasswordPage() {
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
style={{ color: 'var(--text-secondary)' }}
aria-label={
showPassword
? t('hidePassword', language)
: t('showPassword', language)
}
aria-pressed={showPassword}
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
@@ -184,6 +190,12 @@ export function ResetPasswordPage() {
}
className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
style={{ color: 'var(--text-secondary)' }}
aria-label={
showConfirmPassword
? t('hidePassword', language)
: t('showPassword', language)
}
aria-pressed={showConfirmPassword}
>
{showConfirmPassword ? (
<EyeOff className="w-5 h-5" />

View File

@@ -667,6 +667,8 @@ export const translations = {
passwordRequired: 'Password is required',
invalidEmail: 'Invalid email format',
passwordTooShort: 'Password must be at least 6 characters',
showPassword: 'Show password',
hidePassword: 'Hide password',
// Landing Page
features: 'Features',
@@ -1834,6 +1836,8 @@ export const translations = {
passwordRequired: '请输入密码',
invalidEmail: '邮箱格式不正确',
passwordTooShort: '密码至少需要6个字符',
showPassword: '显示密码',
hidePassword: '隐藏密码',
// Landing Page
features: '功能',