feat(web): use shared accessibility helpers
This commit is contained in:
parent
8d8cf04835
commit
bfc4670fc3
@ -18,7 +18,7 @@
|
|||||||
|------|----------|-------|---------|---------|
|
|------|----------|-------|---------|---------|
|
||||||
| **TODO-001** | medium | 0 | `web/src/app/providers.tsx` | Kill switch maintenance banner — create `<MaintenanceBanner />`, set React state when `checkKillSwitch()` returns `disabled=true`, disable timer creation buttons |
|
| **TODO-001** | medium | 0 | `web/src/app/providers.tsx` | Kill switch maintenance banner — create `<MaintenanceBanner />`, set React state when `checkKillSwitch()` returns `disabled=true`, disable timer creation buttons |
|
||||||
| ✅ **TODO-002** | medium | 0 | `web/src/app/settings/page.tsx` | ~~Wire feedback button into settings page using `feedbackClient`~~ — settings feedback form submits through `@bytelyst/feedback-client`; verification blocked locally by missing `GITEA_NPM_TOKEN` for private `@bytelyst/*` install (d38b974) |
|
| ✅ **TODO-002** | medium | 0 | `web/src/app/settings/page.tsx` | ~~Wire feedback button into settings page using `feedbackClient`~~ — settings feedback form submits through `@bytelyst/feedback-client`; verification blocked locally by missing `GITEA_NPM_TOKEN` for private `@bytelyst/*` install (d38b974) |
|
||||||
| **TODO-003** | medium | 0 | `web/src/components/CreateTimerModal.tsx`, `web/src/components/AlarmOverlay.tsx` | Apply `@bytelyst/accessibility` focus trap + screen reader announcements. Ensure `--cm-*` color tokens meet WCAG AA contrast |
|
| ✅ **TODO-003** | medium | 0 | `web/src/components/CreateTimerModal.tsx`, `web/src/components/AlarmOverlay.tsx` | ~~Apply `@bytelyst/accessibility` focus trap + screen reader announcements~~ — shared helpers added in common-plat `42f60cb`, consumed by ChronoMind modals (pending commit) |
|
||||||
| ✅ **TODO-004** | medium | A.1 | `backend/src/modules/routines/routes.ts` | ~~Clone template~~ — templates now cloned via `crypto.randomUUID()`, original stays reusable (0e7c1ae) |
|
| ✅ **TODO-004** | medium | A.1 | `backend/src/modules/routines/routes.ts` | ~~Clone template~~ — templates now cloned via `crypto.randomUUID()`, original stays reusable (0e7c1ae) |
|
||||||
| ✅ **TODO-005** | high | A.4 | `backend/src/lib/ai-context.ts` | ~~Wire LLM~~ — dual-path: extraction-service → Ollama `/api/generate` (5s timeout, feature-gated) (229ce4f) |
|
| ✅ **TODO-005** | high | A.4 | `backend/src/lib/ai-context.ts` | ~~Wire LLM~~ — dual-path: extraction-service → Ollama `/api/generate` (5s timeout, feature-gated) (229ce4f) |
|
||||||
| ✅ **TODO-006** | low | A.4 | `web/src/lib/context-messages.ts` | ~~Centralize backend URL~~ — uses `getBackendBaseURL()` from `product-config.ts` (5dafcc2) |
|
| ✅ **TODO-006** | low | A.4 | `web/src/lib/context-messages.ts` | ~~Centralize backend URL~~ — uses `getBackendBaseURL()` from `product-config.ts` (5dafcc2) |
|
||||||
@ -130,7 +130,7 @@ These proxy to `chronomind-backend` (port 4011) via `chronomind-client.ts`.
|
|||||||
### 0.3 — Accessibility Package (Web)
|
### 0.3 — Accessibility Package (Web)
|
||||||
|
|
||||||
- [x] Add `@bytelyst/accessibility` to web/package.json (commit: f3e14e2)
|
- [x] Add `@bytelyst/accessibility` to web/package.json (commit: f3e14e2)
|
||||||
- [ ] Integrate helpers into existing modals (commit: )
|
- [x] Integrate helpers into existing modals (commit: pending; shared package: `learning_ai_common_plat` 42f60cb; status: `@bytelyst/accessibility` build passed, web typecheck/test/build passed, modal small text promoted from tertiary to secondary for AA contrast)
|
||||||
- TODO-003: Apply `@bytelyst/accessibility` focus trap + screen reader to CreateTimerModal, AlarmOverlay
|
- TODO-003: Apply `@bytelyst/accessibility` focus trap + screen reader to CreateTimerModal, AlarmOverlay
|
||||||
- Ensure all `--cm-*` color tokens meet WCAG AA contrast
|
- Ensure all `--cm-*` color tokens meet WCAG AA contrast
|
||||||
|
|
||||||
@ -643,7 +643,7 @@ Systematic audit against `learning_ai_common_plat/packages/` (58 packages) revea
|
|||||||
|-----|----------|----------|
|
|-----|----------|----------|
|
||||||
| No `@bytelyst/kill-switch-client` | **Critical** | Phase 0.1 |
|
| No `@bytelyst/kill-switch-client` | **Critical** | Phase 0.1 |
|
||||||
| No settings feedback entry point using `@bytelyst/feedback-client` | Medium | Phase 0.2 — complete (d38b974) |
|
| No settings feedback entry point using `@bytelyst/feedback-client` | Medium | Phase 0.2 — complete (d38b974) |
|
||||||
| No `@bytelyst/accessibility` helpers | Medium | Phase 0.3 |
|
| No `@bytelyst/accessibility` focus trap and screen reader helpers | Medium | Phase 0.3 — complete pending commit |
|
||||||
| No feature flags for new features | **Critical** | Phase 0.4 |
|
| No feature flags for new features | **Critical** | Phase 0.4 |
|
||||||
| No telemetry events for new features | Medium | Phase 0.5 |
|
| No telemetry events for new features | Medium | Phase 0.5 |
|
||||||
| MCP tools built in product backend (wrong arch) | **Critical** | Phase A (dual-layer: backend REST + mcp-server proxy) |
|
| MCP tools built in product backend (wrong arch) | **Critical** | Phase A (dual-layer: backend REST + mcp-server proxy) |
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useEffect, useRef, useCallback } from 'react';
|
|||||||
import { useTimerStore } from '@/lib/store';
|
import { useTimerStore } from '@/lib/store';
|
||||||
import { getUrgencyConfig } from '@/lib/urgency';
|
import { getUrgencyConfig } from '@/lib/urgency';
|
||||||
import { formatTime } from '@/lib/format';
|
import { formatTime } from '@/lib/format';
|
||||||
import { alertLabel } from '@bytelyst/accessibility';
|
import { alertLabel, announceToScreenReader, focusFirstElement, trapFocusKeydown } from '@bytelyst/accessibility';
|
||||||
import { Bell, BellOff } from 'lucide-react';
|
import { Bell, BellOff } from 'lucide-react';
|
||||||
|
|
||||||
export function AlarmOverlay() {
|
export function AlarmOverlay() {
|
||||||
@ -16,28 +16,16 @@ export function AlarmOverlay() {
|
|||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const trapFocus = useCallback((e: KeyboardEvent) => {
|
const trapFocus = useCallback((e: KeyboardEvent) => {
|
||||||
if (e.key !== 'Tab' || !overlayRef.current) return;
|
if (overlayRef.current) trapFocusKeydown(e, overlayRef.current);
|
||||||
const focusable = overlayRef.current.querySelectorAll<HTMLElement>(
|
|
||||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
||||||
);
|
|
||||||
if (focusable.length === 0) return;
|
|
||||||
const first = focusable[0];
|
|
||||||
const last = focusable[focusable.length - 1];
|
|
||||||
if (e.shiftKey && document.activeElement === first) {
|
|
||||||
e.preventDefault();
|
|
||||||
last.focus();
|
|
||||||
} else if (!e.shiftKey && document.activeElement === last) {
|
|
||||||
e.preventDefault();
|
|
||||||
first.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (firingTimers.length === 0) return;
|
if (firingTimers.length === 0) return;
|
||||||
document.addEventListener('keydown', trapFocus);
|
document.addEventListener('keydown', trapFocus);
|
||||||
// Auto-focus dismiss button
|
if (overlayRef.current) {
|
||||||
const el = overlayRef.current?.querySelector<HTMLElement>('button');
|
focusFirstElement(overlayRef.current, 'button');
|
||||||
el?.focus();
|
}
|
||||||
|
announceToScreenReader(`Timer fired: ${firingTimers[0]?.label ?? 'timer'}`, 'assertive');
|
||||||
return () => document.removeEventListener('keydown', trapFocus);
|
return () => document.removeEventListener('keydown', trapFocus);
|
||||||
}, [firingTimers.length, trapFocus]);
|
}, [firingTimers.length, trapFocus]);
|
||||||
|
|
||||||
@ -87,7 +75,7 @@ export function AlarmOverlay() {
|
|||||||
: 'Timer fired!'}
|
: 'Timer fired!'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-sm mb-8" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<p className="text-sm mb-8" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
{formatTime(timer.firedAt ?? Date.now())}
|
{formatTime(timer.firedAt ?? Date.now())}
|
||||||
{timer.snoozeCount > 0 && ` · Snoozed ${timer.snoozeCount}x`}
|
{timer.snoozeCount > 0 && ` · Snoozed ${timer.snoozeCount}x`}
|
||||||
</p>
|
</p>
|
||||||
@ -146,7 +134,7 @@ export function AlarmOverlay() {
|
|||||||
|
|
||||||
{/* Multiple firing indicator */}
|
{/* Multiple firing indicator */}
|
||||||
{firingTimers.length > 1 && (
|
{firingTimers.length > 1 && (
|
||||||
<p className="mt-4 text-xs" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<p className="mt-4 text-xs" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
+{firingTimers.length - 1} more timer{firingTimers.length > 2 ? 's' : ''} firing
|
+{firingTimers.length - 1} more timer{firingTimers.length > 2 ? 's' : ''} firing
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { BUILT_IN_CATEGORIES, getCategoryById } from '@/lib/categories';
|
|||||||
import { parseNaturalLanguage } from '@/lib/nl-parser';
|
import { parseNaturalLanguage } from '@/lib/nl-parser';
|
||||||
import type { ParseResult } from '@/lib/nl-parser';
|
import type { ParseResult } from '@/lib/nl-parser';
|
||||||
import { alarmSchema, countdownSchema, pomodoroSchema, eventSchema } from '@/lib/schemas';
|
import { alarmSchema, countdownSchema, pomodoroSchema, eventSchema } from '@/lib/schemas';
|
||||||
|
import { announceToScreenReader, focusFirstElement, trapFocusKeydown } from '@bytelyst/accessibility';
|
||||||
|
|
||||||
type TabType = 'alarm' | 'countdown' | 'pomodoro' | 'event';
|
type TabType = 'alarm' | 'countdown' | 'pomodoro' | 'event';
|
||||||
|
|
||||||
@ -27,27 +28,16 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
|
|
||||||
const trapFocus = useCallback((e: KeyboardEvent) => {
|
const trapFocus = useCallback((e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') { onClose(); return; }
|
if (e.key === 'Escape') { onClose(); return; }
|
||||||
if (e.key !== 'Tab' || !dialogRef.current) return;
|
if (dialogRef.current) trapFocusKeydown(e, dialogRef.current);
|
||||||
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
|
|
||||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
||||||
);
|
|
||||||
if (focusable.length === 0) return;
|
|
||||||
const first = focusable[0];
|
|
||||||
const last = focusable[focusable.length - 1];
|
|
||||||
if (e.shiftKey && document.activeElement === first) {
|
|
||||||
e.preventDefault();
|
|
||||||
last.focus();
|
|
||||||
} else if (!e.shiftKey && document.activeElement === last) {
|
|
||||||
e.preventDefault();
|
|
||||||
first.focus();
|
|
||||||
}
|
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
document.addEventListener('keydown', trapFocus);
|
document.addEventListener('keydown', trapFocus);
|
||||||
const el = dialogRef.current?.querySelector<HTMLElement>('input, button');
|
if (dialogRef.current) {
|
||||||
el?.focus();
|
focusFirstElement(dialogRef.current, 'input, button');
|
||||||
|
}
|
||||||
|
announceToScreenReader('Create timer dialog opened', 'polite');
|
||||||
return () => document.removeEventListener('keydown', trapFocus);
|
return () => document.removeEventListener('keydown', trapFocus);
|
||||||
}, [isOpen, trapFocus]);
|
}, [isOpen, trapFocus]);
|
||||||
|
|
||||||
@ -260,7 +250,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="p-1 rounded-lg transition-colors cursor-pointer"
|
className="p-1 rounded-lg transition-colors cursor-pointer"
|
||||||
style={{ color: 'var(--cm-text-tertiary)' }}
|
style={{ color: 'var(--cm-text-secondary)' }}
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={20} />
|
||||||
@ -271,7 +261,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
<div className="p-4 border-b" style={{ borderColor: 'var(--cm-border)' }}>
|
<div className="p-4 border-b" style={{ borderColor: 'var(--cm-border)' }}>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Sparkles size={14} style={{ color: 'var(--cm-accent-secondary)' }} />
|
<Sparkles size={14} style={{ color: 'var(--cm-accent-secondary)' }} />
|
||||||
<label className="text-xs font-medium" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<label className="text-xs font-medium" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
Quick create — type naturally
|
Quick create — type naturally
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -300,7 +290,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{nlResult && nlInput.trim() && (
|
{nlResult && nlInput.trim() && (
|
||||||
<div className="mt-2 text-xs" style={{ color: nlResult.success ? 'var(--cm-accent-secondary)' : 'var(--cm-text-tertiary)' }}>
|
<div className="mt-2 text-xs" style={{ color: nlResult.success ? 'var(--cm-accent-secondary)' : 'var(--cm-text-secondary)' }}>
|
||||||
{nlResult.success && nlResult.timer ? (
|
{nlResult.success && nlResult.timer ? (
|
||||||
<span>
|
<span>
|
||||||
{nlResult.timer.type === 'pomodoro' ? 'Pomodoro' : nlResult.timer.type === 'alarm' ? 'Alarm' : 'Countdown'}
|
{nlResult.timer.type === 'pomodoro' ? 'Pomodoro' : nlResult.timer.type === 'alarm' ? 'Alarm' : 'Countdown'}
|
||||||
@ -309,7 +299,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
{nlResult.timer.urgency !== 'standard' ? ` [${nlResult.timer.urgency}]` : ''}
|
{nlResult.timer.urgency !== 'standard' ? ` [${nlResult.timer.urgency}]` : ''}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: 'var(--cm-text-tertiary)' }}>{nlResult.error}</span>
|
<span style={{ color: 'var(--cm-text-secondary)' }}>{nlResult.error}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -323,7 +313,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
onClick={() => setTab(t.key)}
|
onClick={() => setTab(t.key)}
|
||||||
className="flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors cursor-pointer"
|
className="flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
color: tab === t.key ? 'var(--cm-accent)' : 'var(--cm-text-tertiary)',
|
color: tab === t.key ? 'var(--cm-accent)' : 'var(--cm-text-secondary)',
|
||||||
borderBottom: tab === t.key ? '2px solid var(--cm-accent)' : '2px solid transparent',
|
borderBottom: tab === t.key ? '2px solid var(--cm-accent)' : '2px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -400,7 +390,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
color: 'var(--cm-text-primary)',
|
color: 'var(--cm-text-primary)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="block text-center text-xs mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<span className="block text-center text-xs mt-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
{field.label}
|
{field.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -441,7 +431,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
{ label: 'Rounds', value: rounds, setter: setRounds },
|
{ label: 'Rounds', value: rounds, setter: setRounds },
|
||||||
].map((field) => (
|
].map((field) => (
|
||||||
<div key={field.label}>
|
<div key={field.label}>
|
||||||
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
{field.label}
|
{field.label}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -481,7 +471,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
/>
|
/>
|
||||||
{errors.eventDate && <p className="text-xs mt-1" style={{ color: 'var(--cm-danger)' }}>{errors.eventDate}</p>}
|
{errors.eventDate && <p className="text-xs mt-1" style={{ color: 'var(--cm-danger)' }}>{errors.eventDate}</p>}
|
||||||
{eventDate && new Date(eventDate).getTime() > Date.now() && (
|
{eventDate && new Date(eventDate).getTime() > Date.now() && (
|
||||||
<p className="text-xs mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<p className="text-xs mt-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
{Math.ceil((new Date(eventDate).getTime() - Date.now()) / 86_400_000)} days from now · Milestone warnings at 30, 7, 3, 1 days
|
{Math.ceil((new Date(eventDate).getTime() - Date.now()) / 86_400_000)} days from now · Milestone warnings at 30, 7, 3, 1 days
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -500,7 +490,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
className="px-2.5 py-1 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
className="px-2.5 py-1 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: !category ? 'var(--cm-accent)' : 'var(--cm-surface-muted)',
|
backgroundColor: !category ? 'var(--cm-accent)' : 'var(--cm-surface-muted)',
|
||||||
color: !category ? 'var(--cm-white)' : 'var(--cm-text-tertiary)',
|
color: !category ? 'var(--cm-white)' : 'var(--cm-text-secondary)',
|
||||||
border: !category ? '1px solid var(--cm-accent)' : '1px solid transparent',
|
border: !category ? '1px solid var(--cm-accent)' : '1px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -513,7 +503,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
className="px-2.5 py-1 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
className="px-2.5 py-1 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: category === cat.id ? `${cat.color}20` : 'var(--cm-surface-muted)',
|
backgroundColor: category === cat.id ? `${cat.color}20` : 'var(--cm-surface-muted)',
|
||||||
color: category === cat.id ? cat.color : 'var(--cm-text-tertiary)',
|
color: category === cat.id ? cat.color : 'var(--cm-text-secondary)',
|
||||||
border: category === cat.id ? `1px solid ${cat.color}60` : '1px solid transparent',
|
border: category === cat.id ? `1px solid ${cat.color}60` : '1px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -540,7 +530,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
className="flex-1 py-1.5 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
className="flex-1 py-1.5 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: urgency === level ? config.bgColor : 'var(--cm-surface-muted)',
|
backgroundColor: urgency === level ? config.bgColor : 'var(--cm-surface-muted)',
|
||||||
color: urgency === level ? config.color : 'var(--cm-text-tertiary)',
|
color: urgency === level ? config.color : 'var(--cm-text-secondary)',
|
||||||
border: urgency === level ? `1px solid ${config.borderColor}` : '1px solid transparent',
|
border: urgency === level ? `1px solid ${config.borderColor}` : '1px solid transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -556,7 +546,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
{tab !== 'pomodoro' && (
|
{tab !== 'pomodoro' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
Custom Warning Message <span className="text-xs font-normal" style={{ color: 'var(--cm-text-tertiary)' }}>(optional)</span>
|
Custom Warning Message <span className="text-xs font-normal" style={{ color: 'var(--cm-text-secondary)' }}>(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -571,7 +561,7 @@ export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|||||||
color: 'var(--cm-text-primary)',
|
color: 'var(--cm-text-primary)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<p className="text-xs mt-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
||||||
Shown in pre-warning notifications instead of auto-generated tips
|
Shown in pre-warning notifications instead of auto-generated tips
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user