@voltro/ui-shadcn 0.66.0 → 0.67.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.
@@ -106,6 +106,7 @@ export const SearchModal = <TItem,>({
106
106
  const [query, setQuery] = useState('')
107
107
  const [activeIdx, setActiveIdx] = useState(0)
108
108
  const inputRef = useRef<HTMLInputElement>(null)
109
+ const dialogRef = useRef<HTMLDivElement>(null)
109
110
  const listboxId = useId()
110
111
 
111
112
  // Re-run search on every query change. Cheap for our doc-sized
@@ -132,18 +133,21 @@ export const SearchModal = <TItem,>({
132
133
  // Focus the input + reset state on open.
133
134
  useEffect(() => {
134
135
  if (!open) return
136
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null
135
137
  setQuery('')
136
138
  setActiveIdx(0)
137
139
  // RAF lets the modal mount before we grab focus, so the autofocus
138
140
  // ring doesn't paint twice.
139
141
  const id = requestAnimationFrame(() => inputRef.current?.focus())
140
- return () => cancelAnimationFrame(id)
142
+ return () => {
143
+ cancelAnimationFrame(id)
144
+ if (previous?.isConnected) previous.focus()
145
+ }
141
146
  }, [open])
142
147
 
143
148
  const close = useCallback((): void => setOpen(false), [setOpen])
144
149
 
145
150
  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
146
- if (e.key === 'Escape') { e.preventDefault(); close(); return }
147
151
  if (e.key === 'ArrowDown') {
148
152
  e.preventDefault()
149
153
  setActiveIdx((i) => Math.min(i + 1, Math.max(hits.length - 1, 0)))
@@ -164,10 +168,20 @@ export const SearchModal = <TItem,>({
164
168
 
165
169
  return (
166
170
  <div
171
+ ref={dialogRef}
167
172
  role="dialog"
168
173
  aria-modal="true"
169
174
  aria-label={t.dialog}
170
175
  className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh] px-4"
176
+ onKeyDown={event => {
177
+ if (event.key === 'Escape') { event.preventDefault(); close(); return }
178
+ if (event.key !== 'Tab') return
179
+ const controls = [...(dialogRef.current?.querySelectorAll<HTMLElement>('button:not([disabled]), input:not([disabled]), a[href], [tabindex="0"]') ?? [])]
180
+ const first = controls[0]
181
+ const last = controls.at(-1)
182
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus() }
183
+ if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus() }
184
+ }}
171
185
  >
172
186
  {/* Backdrop — click closes */}
173
187
  <button
package/src/signal.css ADDED
@@ -0,0 +1,223 @@
1
+ /* Voltro Signal: opt-in brand theme. Import after tokens.css. */
2
+ :root {
3
+ --signal-violet: #7c3aed;
4
+ --signal-header-height: 112px;
5
+ --signal-light: #6d28d9;
6
+ --signal-ease: cubic-bezier(.16,1,.3,1);
7
+ --background: #faf9fc;
8
+ --foreground: #21192d;
9
+ --card: #ffffff;
10
+ --card-foreground: #21192d;
11
+ --popover: #ffffff;
12
+ --popover-foreground: #21192d;
13
+ --primary: #7c3aed;
14
+ --primary-foreground: #ffffff;
15
+ --secondary: #eee9f4;
16
+ --secondary-foreground: #38264e;
17
+ --muted: #f2eef7;
18
+ --muted-foreground: #6b5c7c;
19
+ --accent: #eee6fc;
20
+ --accent-foreground: #5b21b6;
21
+ --border: #ded6e9;
22
+ --input: #d0c4df;
23
+ --ring: #7c3aed;
24
+ --sidebar: #f3eff8;
25
+ --sidebar-foreground: #38264e;
26
+ --sidebar-primary: #7c3aed;
27
+ --sidebar-primary-foreground: #fff;
28
+ --sidebar-accent: #e7ddf5;
29
+ --sidebar-accent-foreground: #5b21b6;
30
+ --sidebar-border: #ded6e9;
31
+ --sidebar-ring: #7c3aed;
32
+ --radius: .375rem;
33
+ --signal-accent-ink: #6d28d9;
34
+ --signal-ground: #f4effa;
35
+ --signal-ink: #21192d;
36
+ --signal-muted: #6b5c7c;
37
+ --signal-line: #d4c7e5;
38
+ --signal-detail: #735094;
39
+ --signal-field-a: #e4d5f5;
40
+ --signal-field-b: #efe5fb;
41
+ --signal-field-base: #faf9fc;
42
+ --signal-field-floor: #faf9fc;
43
+ }
44
+ .dark {
45
+ --signal-light: #a874ff;
46
+ --signal-ground: #0b0911;
47
+ --signal-ink: #f5f3fa;
48
+ --signal-muted: #b9afc9;
49
+ --signal-line: #342b43;
50
+ --signal-detail: #c2a1e8;
51
+ --signal-field-a: #29193d;
52
+ --signal-field-b: #1b1430;
53
+ --signal-field-base: #100c19;
54
+ --signal-field-floor: linear-gradient(145deg,#100c18 12%,#110c1e 55%,#0e0b15 100%);
55
+ --background: #100c19;
56
+ --foreground: #f5f3fa;
57
+ --card: #17111f;
58
+ --card-foreground: #f5f3fa;
59
+ --popover: #1c1427;
60
+ --popover-foreground: #f5f3fa;
61
+ --primary: #7c3aed;
62
+ --primary-foreground: #ffffff;
63
+ --secondary: #251b33;
64
+ --secondary-foreground: #e7dbf7;
65
+ --muted: #201828;
66
+ --muted-foreground: #b9afc9;
67
+ --accent: #2c1d40;
68
+ --accent-foreground: #d9bcff;
69
+ --border: #342b43;
70
+ --input: #584566;
71
+ --ring: #b995ff;
72
+ --sidebar: #140f1d;
73
+ --sidebar-foreground: #d5c9e4;
74
+ --sidebar-primary: #a874ff;
75
+ --sidebar-primary-foreground: #170c29;
76
+ --sidebar-accent: #2b1e3c;
77
+ --sidebar-accent-foreground: #e2ccff;
78
+ --sidebar-border: #342b43;
79
+ --sidebar-ring: #b995ff;
80
+ --signal-accent-ink: #c19aff;
81
+ }
82
+ @layer components {
83
+ :where(a, button, input, textarea, select, summary):focus-visible {
84
+ outline: 2px solid var(--ring);
85
+ outline-offset: 4px;
86
+ }
87
+ :where(input, textarea) { caret-color: var(--signal-accent-ink); }
88
+ ::selection { background: #6530ba; color: #fff; }
89
+ :where(table, [role="table"]) { font-variant-numeric: tabular-nums; }
90
+ }
91
+ .signal-stage { position: relative; isolation: isolate; }
92
+ .signal-atmosphere{position:absolute;inset:0;z-index:-1;overflow:hidden;pointer-events:none;background:radial-gradient(ellipse at 89% 22%,var(--signal-field-a) 0%,transparent 48%),radial-gradient(ellipse at 8% 76%,var(--signal-field-b) 0%,transparent 44%),var(--signal-field-floor)}
93
+ .field-contours{position:absolute;width:max(100%,1400px);height:auto;top:0;right:0;max-width:none}
94
+ .field-light{position:absolute;inset:20% 0 8% 0;background:radial-gradient(ellipse at 66% 49%,#7c3aed26 0%,#a874ff0b 31%,transparent 65%);opacity:.45;transition:opacity 650ms ease-out}
95
+ .signal-stage:has(.signal-demo[data-state=running]) .field-light{opacity:1}
96
+ .field-highlight{stroke-dasharray:7 93;stroke-dashoffset:-34;opacity:.22}
97
+ .signal-stage:has(.signal-demo[data-state=running]) .field-highlight{animation:field-response 2900ms ease-out 1}
98
+ @keyframes field-response{0%{stroke-dashoffset:-34;opacity:.22}40%{opacity:.55}100%{stroke-dashoffset:-78;opacity:0}}
99
+ .field-ambient-trace{stroke-dasharray:7 93;opacity:0;animation:field-travel 14s linear infinite;animation-play-state:paused}
100
+ .trace-1{animation-delay:-3s}.trace-2{animation-delay:-6s}.trace-3{animation-delay:-9s}.trace-4{animation-delay:-11s;animation-duration:17s}
101
+ .signal-atmosphere[data-motion=running] .field-ambient-trace{animation-play-state:running}
102
+ @keyframes field-travel{0%{stroke-dashoffset:3;opacity:0}10%{opacity:.32}78%{opacity:.32}100%{stroke-dashoffset:-100;opacity:0}}
103
+
104
+ .signal-stage .signal-atmosphere { mask-image: linear-gradient(#000 0%, #000 85%, transparent 100%); }
105
+ .signal-shell { width: min(100% - 88px, 1536px); margin-inline: auto; }
106
+ .signal-primary {
107
+ display: inline-flex; align-items: stretch; min-height: 54px; width: fit-content;
108
+ background: #7c3aed linear-gradient(115deg, #6d28d9, #7c3aed 48%, #8a49ed);
109
+ background-size: 140% 100%; color: #fff; border-radius: 4px;
110
+ box-shadow: 0 5px 20px -9px #7c3aed66; overflow: hidden;
111
+ font-size: 1rem; font-weight: 500; text-decoration: none;
112
+ transition: background-position 280ms var(--signal-ease), box-shadow 240ms var(--signal-ease);
113
+ }
114
+ .signal-primary > span:first-child { padding: 14px 23px; display: flex; align-items: center; }
115
+ .signal-primary .signal-arrow { width: 55px; border-left: 1px solid #a57bdd; display: grid; place-items: center; }
116
+ .signal-primary svg { width: 22px; height: 22px; transition: transform 220ms var(--signal-ease); }
117
+ .signal-primary:hover, .signal-primary:focus-visible { background-position: 100% 50%; box-shadow: 0 8px 30px -8px #7c3aed7a; }
118
+ .signal-primary:hover svg, .signal-primary:focus-visible svg { transform: translateX(4px); }
119
+ .signal-text-link { display: inline-flex; gap: 10px; align-items: center; min-height: 44px; text-underline-offset: 8px; text-decoration-color: var(--signal-light); }
120
+ .signal-text-link:hover, .signal-text-link:focus-visible { text-decoration-line: underline; }
121
+ .signal-text-link svg { width: 16px; height: 16px; color: var(--signal-accent-ink); }
122
+ .signal-motion-toggle { display: inline-flex; align-items: center; gap: 7px; min-height: 44px; color: var(--muted-foreground); font-size: 12px; background: none; border: 0; cursor: pointer; }
123
+ .signal-motion-toggle svg { width: 16px; height: 16px; }
124
+ .signal-motion-toggle:hover { color: var(--signal-accent-ink); }
125
+ @media (max-width: 1150px) { .signal-shell { width: calc(100% - 56px); } }
126
+ @media (max-width: 760px) {
127
+ .signal-shell { width: calc(100% - 40px); }
128
+ .field-contours { width: 1000px; right: -390px; top: 50px; }
129
+ .signal-atmosphere { background: radial-gradient(ellipse at 100% 24%, var(--signal-field-a), transparent 55%), radial-gradient(ellipse at 0% 80%, var(--signal-field-b), transparent 48%), var(--signal-field-base); }
130
+ .signal-primary { min-height: 50px; font-size: 14px; }
131
+ .signal-primary > span:first-child { padding-inline: 18px; }
132
+ .signal-primary .signal-arrow { width: 44px; }
133
+ }
134
+ @media (prefers-reduced-motion: reduce) {
135
+ .field-ambient-trace { animation: none; }
136
+ .signal-primary, .signal-primary svg { transition: none; }
137
+ .signal-motion-toggle { display: none; }
138
+ html { scroll-behavior: auto; }
139
+ }
140
+ @media (forced-colors: active) {
141
+ .signal-atmosphere { display: none; }
142
+ .signal-primary { border: 1px solid ButtonText; }
143
+ }
144
+ .signal-masthead { position: relative; z-index: 40; color: var(--foreground); background: transparent; transition: background-color 180ms var(--signal-ease); }
145
+ .signal-masthead-sticky { position: sticky; top: 0; }
146
+ .signal-masthead[data-scrolled=true] { background: var(--background); }
147
+ .signal-header { min-height: var(--signal-header-height); display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 20; gap: 24px; }
148
+ .signal-brand { flex: none; }
149
+ .signal-brand .voltro-wordmark { --voltro-wordmark-size: 42px; }
150
+ .signal-brand > a { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; }
151
+ .signal-brand svg { flex: none; }
152
+ .signal-navigation-area { display: flex; align-items: center; gap: 24px; }
153
+ .signal-navigation { position: relative; display: flex; align-items: center; padding: 5px; gap: 5px; border: 1px solid var(--border); border-radius: 7px; background: var(--background); }
154
+ .signal-nav-marker { position: absolute; inset-block: 5px; left: 5px; width: 100px; border-radius: 2px; background: var(--secondary); transform-origin: left; transition: transform 240ms var(--signal-ease); pointer-events: none; }
155
+ .signal-nav-item { border: 0; background: none; position: relative; display: flex; align-items: center; gap: 10px; padding: 11px 17px; font-size: 14px; font-weight: 500; min-height: 44px; white-space: nowrap; color: var(--foreground); }
156
+ .signal-nav-dot { width: 5px; height: 5px; background: var(--signal-light); flex: none; }
157
+ .signal-header svg:not(.voltro-logo) { stroke-linecap: round; stroke-linejoin: round; }
158
+ .signal-nav-item > svg, .signal-menu-links svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; color: var(--muted-foreground); transition: transform 180ms var(--signal-ease); }
159
+ .signal-nav-item[aria-expanded=true] > svg { transform: rotate(180deg); }
160
+ .signal-nav-item[href]:hover > svg { transform: translate(2px, -2px); }
161
+ .signal-header-utilities { display: flex; align-items: center; gap: 12px; }
162
+ .signal-control { display: inline-flex; flex: none; align-items: center; justify-content: center; gap: 10px; min-height: 56px; padding: 0 16px; border: 1px solid var(--border); border-radius: 7px; background: var(--background); color: var(--foreground); font-size: 14px; font-weight: 500; line-height: 1.4; white-space: nowrap; cursor: pointer; text-decoration: none; transition: color 180ms var(--signal-ease), border-color 180ms var(--signal-ease), background-color 180ms var(--signal-ease); }
163
+ .signal-control > svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
164
+ .signal-control:hover, .signal-control[data-state=open] { border-color: var(--input); background: var(--secondary); }
165
+ .signal-preferences { padding-inline: 12px; border-color: transparent; background: transparent; color: var(--muted-foreground); font-size: 12px; }
166
+ .signal-preferences:hover, .signal-preferences[data-state=open] { color: var(--foreground); }
167
+ .signal-header-action { border-color: var(--input); }
168
+ .signal-header-action > svg { color: var(--signal-accent-ink); transition: transform 180ms var(--signal-ease); }
169
+ .signal-header-action:hover > svg { transform: translateX(3px); }
170
+ .signal-search { width: 224px; justify-content: flex-start; color: var(--muted-foreground); font-weight: 400; }
171
+ .signal-search kbd { margin-left: auto; padding: 2px 5px; border: 1px solid var(--border); border-radius: 3px; font-family: inherit; font-size: 11px; line-height: 1.4; }
172
+ .signal-search:hover { color: var(--foreground); }
173
+ @media(max-width:1250px) { .signal-search { width: 56px; padding: 0; justify-content: center; } .signal-search > span, .signal-search kbd { display: none; } }
174
+ .signal-menu { position: absolute; right: 100px; top: calc(100% - 16px); width: 570px; padding: 26px; display: grid; grid-template-columns: 1fr 1.4fr; gap: 28px; background: var(--popover); color: var(--popover-foreground); border: 1px solid var(--input); border-radius: 7px; }
175
+ .signal-menu[hidden] { display: none; }
176
+ .signal-menu-intro { font-size: 22px; font-weight: 500; line-height: 1.25; letter-spacing: -.025em; }
177
+ .signal-menu-links { display: flex; flex-direction: column; }
178
+ .signal-menu-links a { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
179
+ .signal-menu-links a:first-child { padding-top: 0; }
180
+ .signal-menu-links a:last-child { border: 0; padding-bottom: 0; }
181
+ .signal-menu-links a:hover { color: var(--signal-accent-ink); }
182
+ .signal-menu-links small { display: block; font-size: 12px; color: var(--muted-foreground); margin-top: 5px; }
183
+ @media(max-width:1150px) { :root { --signal-header-height: 96px; } .signal-navigation-area { gap: 24px; } .signal-nav-item { padding-inline: 12px; } }
184
+ @media(max-width:900px) {
185
+ :root { --signal-header-height: 158px; }
186
+ .signal-header { padding-block: 18px; align-items: center; flex-wrap: wrap; gap: 16px; }
187
+ .signal-brand .voltro-wordmark { --voltro-wordmark-size: 32px; display: grid; grid-template-columns: auto auto; row-gap: 5px; }
188
+ .signal-brand .voltro-wordmark > svg { grid-row: 1 / 3; }
189
+ .signal-brand .voltro-wordmark-label, .signal-brand .voltro-product-badge { grid-column: 2; justify-self: start; }
190
+ .signal-navigation-area { display: contents; }
191
+ .signal-navigation { order: 3; width: 100%; gap: 3px; }
192
+ .signal-nav-item { flex: 1; justify-content: center; padding: 7px; font-size: 12px; gap: 7px; }
193
+ .signal-header-utilities { margin-left: auto; }
194
+ .signal-control { min-height: 44px; font-size: 12px; padding-inline: 12px; border-radius: 5px; }
195
+ .signal-search, .signal-preferences { width: 44px; padding: 0; }
196
+ .signal-preferences > span { display: none; }
197
+ .signal-menu { top: 100%; right: 0; width: 100%; grid-template-columns: 1fr; padding: 20px; gap: 21px; }
198
+ .signal-menu-intro { font-size: 20px; }
199
+ .signal-menu-links a { min-height: 55px; }
200
+ }
201
+ @media(prefers-reduced-motion:reduce) { .signal-masthead, .signal-nav-marker, .signal-nav-item > svg, .signal-control, .signal-header-action > svg { transition: none; } }
202
+
203
+ .signal-skip { position: fixed; top: 10px; left: 20px; padding: 12px 20px; background: var(--foreground); color: var(--background); z-index: 100; transform: translateY(-160%); }
204
+ .signal-skip:focus { transform: none; }
205
+
206
+ .signal-operate h1 { letter-spacing: -.03em; }
207
+ .signal-operate h2 { letter-spacing: -.02em; }
208
+ .signal-operate [data-sidebar="sidebar"], .signal-operate aside { background-color: var(--sidebar); }
209
+ .signal-operate [aria-current="page"], .signal-operate [data-active="true"] { color: var(--sidebar-accent-foreground); background-color: var(--sidebar-accent); }
210
+ .signal-operate [role="tab"][aria-selected="true"] { color: var(--signal-accent-ink); }
211
+ .signal-operate :where(td, th) { padding-block: .8rem; }
212
+ .signal-operate :where(tbody tr) { transition: background-color 140ms; }
213
+ .signal-operate :where(tbody tr):hover { background-color: var(--muted); }
214
+ .signal-operate :where(input, select, textarea) { border-radius: 4px; }
215
+ @media(prefers-reduced-motion:reduce) { .signal-operate :where(tbody tr) { transition: none; } }
216
+
217
+ .text-primary { color: var(--signal-accent-ink); }
218
+
219
+ /* The marketing close repeats the hero's violet action and plum surface. */
220
+ .signal-marketing-close { background: var(--signal-ground); backdrop-filter: none; }
221
+ .signal-marketing-close::before { background: radial-gradient(ellipse at 20% 30%, #7c3aed29, transparent 65%); }
222
+ .signal-marketing-close .group { background: linear-gradient(115deg, #6d28d9, #7c3aed 48%, #8a49ed); color: #fff; border-radius: 4px; box-shadow: 0 5px 20px -9px #7c3aed66; }
223
+ @media(max-width:760px) { .signal-marketing-close { padding: 40px 24px; } }