ReviewOS

also looking at this

loghqorg/loghq

refactor(ui): use shared auth submit buttons

#13
Merged glennmichael123 wants to merge refactor/loghq-shared-controls into main
4 files +74 -3
config/crosswind.tsmodified+8-0
Changes to config/crosswind.ts
@@ -197,6 +197,14 @@ export default {
197197 + 'hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] '
198198 + 'active:translate-y-px disabled:opacity-60 disabled:cursor-default',
199199
200 // Shared Button owns semantics and reactive state. These explicit
201 // overrides preserve the existing auth controls without depending on
202 // utility order or copying the component into the app.
203 'auth-submit': '!py-2.5 !shadow-none '
204 + '![transition:transform_0.12s_ease,opacity_0.15s_ease] '
205 + 'hover:!bg-[color-mix(in_srgb,var(--accent)_88%,#000)] '
206 + 'active:translate-y-px disabled:!opacity-60 disabled:!cursor-default',
207
200208 'oauth-btn': 'bg-panel border border-line rounded-[10px] text-ink '
201209 + '[transition:border-color_0.15s_ease,transform_0.12s_ease] '
202210 + 'hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))] '
resources/views/login.stxmodified+2-2
Changes to resources/views/login.stx
@@ -163,7 +163,7 @@ async function onVerify(e: Event): Promise<void> {
163163 <span>Keep me signed in for 30 days</span>
164164 </label>
165165
166 <button type="submit" :disabled="loading()" :text="loading() ? 'Signing in…' : 'Sign in'" class="py-2.5 w-full font-semibold text-sm btn-solid">Sign in</button>
166 <Button type="submit" size="sm" fullWidth :disabled="loading()" :label="loading() ? 'Signing in…' : 'Sign in'" className="auth-submit">Sign in</Button>
167167 </form>
168168
169169 {{-- Step 2: TOTP, shown only when the account has 2FA enabled. --}}
@@ -173,7 +173,7 @@ async function onVerify(e: Event): Promise<void> {
173173 <label for="login-code" class="block mb-1.5 font-medium text-muted text-xs">Authentication code</label>
174174 <input id="login-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="123456" :model="code" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'login-error' : ''">
175175 </div>
176 <button type="submit" :disabled="loading()" :text="loading() ? 'Verifying…' : 'Verify and sign in'" class="py-2.5 w-full font-semibold text-sm btn-solid">Verify and sign in</button>
176 <Button type="submit" size="sm" fullWidth :disabled="loading()" :label="loading() ? 'Verifying…' : 'Verify and sign in'" className="auth-submit">Verify and sign in</Button>
177177 </form>
178178
179179 @if (oauthError)
resources/views/register.stxmodified+1-1
Changes to resources/views/register.stx
@@ -112,7 +112,7 @@ async function onRegister(e: Event): Promise<void> {
112112 <input id="register-password" name="password" type="password" required autocomplete="new-password" placeholder="At least 8 characters" :model="password" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'form-error' : ''">
113113 </div>
114114
115 <button type="submit" :disabled="loading()" :text="loading() ? 'Creating account…' : 'Create account'" class="py-2.5 w-full font-semibold text-sm btn-solid">Create account</button>
115 <Button type="submit" size="sm" fullWidth :disabled="loading()" :label="loading() ? 'Creating account…' : 'Create account'" className="auth-submit">Create account</Button>
116116 </form>
117117
118118 @if (anyOauth)
tests/unit/shared-controls.test.tsadded+63-0
Changes to tests/unit/shared-controls.test.ts
@@ -0,0 +1,63 @@
1import { describe, expect, test } from 'bun:test'
2import { readFileSync } from 'node:fs'
3import { resolve } from 'node:path'
4import theme from '../../config/crosswind'
5import { processComponents } from '../../node_modules/@stacksjs/stx/dist/component-renderer.js'
6import { loadCssEngine } from '../../node_modules/@stacksjs/stx/dist/dev-server/ts-css.js'
7import { mergeCssConfig } from '../../node_modules/@stacksjs/stx/dist/ts-css-config.js'
8
9const root = resolve(import.meta.dir, '../..')
10
11describe('shared auth submit controls', () => {
12 for (const [page, labels] of [
13 ['login', ['Sign in', 'Verify and sign in']],
14 ['register', ['Create account']],
15 ] as const) {
16 test(`${page} renders real submit buttons with initial names and live parent bindings`, async () => {
17 const file = resolve(root, `resources/views/${page}.stx`)
18 const source = readFileSync(file, 'utf8')
19 const controls = source.match(/<Button\b[^]*?<\/Button>/g) || []
20 expect(controls).toHaveLength(labels.length)
21 expect(source).not.toMatch(/<button\b[^>]*type="submit"/)
22
23 for (const [index, control] of controls.entries()) {
24 const dependencies = new Set<string>()
25 const html = await processComponents(control, {
26 __stx_client_signal_names: ['loading'],
27 __importedComponents: new Map([['button', resolve(root, 'node_modules/@stacksjs/components/src/ui/button/Button.stx')]]),
28 }, file, {
29 componentsDir: resolve(root, 'resources/components'),
30 }, dependencies)
31 expect([...dependencies].some(path => path.endsWith('/Button.stx'))).toBe(true)
32 expect(html).not.toContain('<Button')
33 expect(html).not.toContain('[Component Error')
34 expect(html).toContain('type="submit"')
35 expect(html).toContain(labels[index])
36 expect(html).toContain('auth-submit')
37 expect(html).toContain('w-full')
38 expect(html).toContain('text-sm')
39 expect(html).toContain('data-stx-parent-bindings')
40 expect(html).toContain('loading()')
41 // A label binding belongs to Button, not :text on its root, which
42 // would destroy its accessible slot and internal state elements.
43 expect(control).toContain(':label=')
44 expect(control).toContain(':disabled="loading()"')
45 expect(control).not.toContain(':text=')
46 }
47 })
48 }
49
50 test('the auth sizing and appearance overrides compile with explicit precedence', async () => {
51 const engine = await loadCssEngine()
52 if (!engine) throw new Error('The installed STX CSS engine is required')
53 const generator = new engine.CSSGenerator(mergeCssConfig(engine.defaultConfig || engine.config, theme).config)
54 generator.generate('auth-submit')
55 const css = generator.toCSS(false, false)
56 expect(css).toContain('.auth-submit')
57 expect(css).toContain('!important')
58 expect(css).toContain('0.625rem')
59 expect(css).toContain('color-mix(in srgb,var(--accent) 88%,#000)')
60 expect(css).toContain('opacity: 0.6 !important')
61 expect(css).toContain('cursor: default !important')
62 })
63})