今日已更新 335 条资讯 | 累计 37240 条内容
关于我们

标签:#a11y

找到 52 篇相关文章

AI 资讯

How to generate WCAG-compliant ALT text for WordPress images without sending them to a vendor's black-box API

If you've ever tried to fix accessibility on an old WordPress site, you know the drill: hundreds of images in the Media Library, most with empty alt attributes, and a WCAG 2.1 audit (or a client demanding one) breathing down your neck. Writing alt text by hand for 400 images is not a fun Tuesday. Every "AI alt text" SaaS I looked at wanted a monthly subscription, routed my images through their own servers, and gave me zero control over which model actually looked at the picture. This post is about the plugin I built to fix that for my own sites, and the handful of implementation details that turned out to matter more than expected. The actual problem WCAG 2.1 Success Criterion 1.1.1 requires non-text content to have a text alternative. In WordPress terms: every attachment post of MIME type image should have _wp_attachment_image_alt set to something meaningful, not "IMG_4821.jpg" and not empty. Doing this with a vision-capable LLM is trivial in principle — send the image, ask for a short description, save it as the alt attribute. The part that's not trivial, if you don't want another recurring SaaS bill and don't want to hand a third party your whole media library, is: whose API key, which model, and where does the image actually go. Design decision: BYOK, not a hosted service The plugin ( Alt Text BYOK ) doesn't call any server of mine. It calls whatever OpenAI-compatible chat/completions endpoint you configure, with your own API key. That's the entire trust model: your images go from your WordPress install directly to the provider you already chose (OpenAI, or any of the growing list of OpenAI-compatible vision endpoints), and nowhere else. The settings are deliberately just four fields: function atbyok_default_settings () { return array ( 'api_base' => 'https://api.openai.com/v1' , 'api_key' => '' , 'model' => 'gpt-4o-mini' , 'language' => 'English' , 'overwrite_existing' => '0' , 'license_key' => '' , ); } api_base is the detail that matters most for portability:

2026-08-29 原文 →
AI 资讯

Web Accessibility in 2026: A Compliance Guide

Web accessibility stopped being optional. The European Accessibility Act has been enforced since June 28, 2025, and it reaches any business that sells products or services to EU customers, regardless of where that business is based. In the United States, the Department of Justice's ADA Title II rule requires public bodies to meet WCAG 2.1 Level AA by April 2026, and private-sector lawsuits keep climbing every year. For a company shipping a website or app, that means a real deadline and real financial exposure. EAA penalties can reach 5% of annual turnover for large companies, and a single ADA complaint can cost tens of thousands to settle before you have fixed anything. The good news: the standard everyone points to, WCAG 2.1 AA, is well-defined and achievable. The bad news is that the most heavily marketed shortcut, the accessibility overlay widget, does not get you there and can make your legal position worse. This guide covers what the law actually requires, why the quick fix backfires, and how we build accessibility into a site from the start instead of bolting it on at the end. What the law actually requires Three names come up constantly, and they fit together cleanly. WCAG 2.1 Level AA is the technical standard. The EAA and the ADA are the laws that, in practice, point back to it. In Europe, meeting WCAG 2.1 AA satisfies the digital requirements of the harmonized EN 301 549 standard, which is how you demonstrate EAA conformance. WCAG is organized around four principles, known as POUR: content must be Perceivable, Operable, Understandable, and Robust. In concrete terms that means text alternatives for images, sufficient color contrast, full keyboard operability, visible focus states, labeled form fields, and markup that screen readers can parse. Level AA, not AAA, is the bar nearly every regulation references. Note the EAA exempts the smallest businesses, those under 10 employees and under two million euros in turnover, but that carve-out is narrower than most

2026-08-28 原文 →
AI 资讯

Everyone is getting ready for WCAG 2.2. Two thirds of Europe's biggest sites still fail 2.1 Level A.

The next version of the European accessibility standard is scheduled for citation on 30 November 2026. EN 301 549 V4.1.1 swaps WCAG 2.1 for WCAG 2.2, and six new success criteria arrive at levels A and AA. There is a small industry of readiness checklists for it already. So I measured what the current version looks like first. The answer is that the deadline people are preparing for is not the one they have missed. I scanned the most-visited websites on EU country domains and counted which clauses of EN 301 549 they fail today, under the version cited right now. Not the one arriving. The one in force since before the European Accessibility Act deadline passed in June 2025. Sixty-four per cent fail clause 9.4.1.2, Name, Role, Value. It is Level A, the lowest bar the standard has, and it has been in every version of WCAG since 2008. Here is the full picture, and then the reasons to distrust parts of it. What was measured Clause Criterion Level Sites failing 9.4.1.2 Name, Role, Value A 96 of 149 (64%) 9.1.4.3 Contrast (Minimum) AA 66 of 149 (44%) 9.2.4.4 Link Purpose (In Context) A 53 of 149 (36%) 9.2.5.8 Target Size (Minimum) AA 51 of 149 (34%) 9.1.1.1 Non-text Content A 35 of 149 (23%) 9.1.3.1 Info and Relationships A 27 of 149 (18%) Target size is the odd one out: it is a WCAG 2.2 criterion and not currently required. It is in the table because it is the only one of the six arriving in V4.1.1 that the rule engine used here has a check for, which is a point I will come back to. Thirty-two sites of the 149, about one in five, failed nothing that automated testing can detect. That is not the same as passing. Two of those rows are not independent. The rule that most often breaks Name, Role, Value is a link with no accessible name, and the same defect also fails Link Purpose. One missing label lands in two rows of that table. I am pointing this out because a table of six numbers implies six problems, and some of them are the same problem counted twice under different cla

2026-08-27 原文 →
AI 资讯

I open-sourced a UI kit — then went looking for everything I got wrong about it

There's no shortage of React UI kits on npm. Search for one right now, and you'll get hundreds of results, most with the same seven button variants and a Storybook someone abandoned halfway through. So when I open-sourced brightframe — pulled out of a real coworking site I built, LAN — I didn't really want to write the usual "here's our 70 components, look how many there are" post. Component count isn't interesting. Anyone can list props and screenshot a button in five colors. What actually took time, and what I think is worth writing about, is the part that happens after the README makes a claim. "Tree-shakeable." "Server Components-safe." "Accessible." Those are three words I typed pretty confidently early on, and then, more recently, I sat down and tried to prove myself wrong on each one. This post is what that turned up. "Tree-shakeable per component" — okay, but how much, actually? Every component ships as its own entry point: import " brightframe/tokens.css " ; import " brightframe/Btn.css " ; import { Btn } from " brightframe/Btn " ; Saying "unused components add nothing to your bundle" costs nothing. I added size-limit to CI so the claim has to keep being true, not just have been true once when I wrote the sentence: Entry Minified + brotli Whole kit ( import { ... } from "brightframe" , JS) 40.13 kB Whole kit ( brightframe/style.css ) 11.83 kB One component ( brightframe/Btn , JS) 641 B One component's styles ( brightframe/Btn.css ) 890 B 641 bytes vs. 40 kilobytes. That gap is the whole reason the per-component entry points exist, and now if a refactor accidentally makes Btn drag in half the kit, the build just fails instead of me finding out from a bundle-size complaint six months later. "Server Components-safe" — this one had an actual bug in it RSC has no hook dispatcher at all. A component needs "use client" if it does one of two things in its own source: calls a hook, or wires up a DOM event handler in its own JSX. I wrote a little script ( scripts/che

2026-08-25 原文 →
AI 资讯

From CSS selector to source line: instrumenting Angular templates

Every accessibility tool I have used reports violations like this: Images must have alternative text body > main > div:nth-child(2) > form > div.field > img That selector is correct. It is also useless. It describes the rendered DOM , and I do not write rendered DOM — I write templates. Somewhere in a few hundred .component.html files there is an <img> that produced it, and finding it is manual work: grep for img , get forty hits, open them one by one, compare surrounding markup until something matches. Multiply that by sixty violations and the scan stops being useful. Not because it is wrong, but because acting on it costs more than ignoring it. React solved this years ago If you write JSX, babel-plugin-transform-react-jsx-source puts a _debugSource on every element at build time — file, line, column. That is how React DevTools can jump you straight to source, and how error overlays point at the right line. Angular has no equivalent. The compiler knows the position of every element in every template: it has to, to report template errors. But nothing carries that knowledge into the DOM. So I built the bridge. parseTemplate hands you the positions @angular/compiler exports parseTemplate , the same entry point @angular-eslint uses. Give it a template string and you get an AST where every node carries a sourceSpan with byte offsets, lines and columns: import { parseTemplate } from ' @angular/compiler ' ; const parsed = parseTemplate ( source , filePath , { preserveWhitespaces : true }); // each element node has startSourceSpan.start.{offset,line,col} Two things to know immediately. The compiler counts lines and columns from zero , and every editor counts from one — so you add one, or every location you report is off by one in both axes and nobody trusts the tool again: line : span . start . line + 1 , // the compiler counts from zero, editors do not column : span . start . col + 1 , And preserveWhitespaces: true matters: without it the offsets you get back describe a t

2026-08-23 原文 →
AI 资讯

A CSS Hover-Reveal Pattern for Technical Specs

The problem on the Gate Seal page The Gate Seal product page for a maritime client needed to present detailed specifications without turning the layout into a wall of text or a table that looked like an export from Excel. The technical detail buyers cared about was present, but visually buried. The requirement was to surface those details in a compact way, keep the implementation CSS-only, and make sure it still worked with keyboard navigation. The hover-reveal pattern The pattern below uses a hover-reveal on key specification rows. On desktop, moving the cursor over a spec row reveals additional context. With a keyboard, focusing the same row does the same thing. No JavaScript is required for the basic interaction. Structurally, each spec item is a container with two layers of content: Always-visible summary (label and primary value) Hidden detail that appears on hover or focus Here is a simplified version of the markup: <div class="spec-list"> <button class="spec-item"> <div class="spec-main"> <span class="spec-label">Gate size</span> <span class="spec-value">Up to 6 m</span> </div> <div class="spec-detail"> Custom diameters available for retrofit situations. </div> </button> <button class="spec-item"> <div class="spec-main"> <span class="spec-label">Seal material</span> <span class="spec-value">EPDM / NBR</span> </div> <div class="spec-detail"> Oil-resistant compounds for lock gates in heavy traffic.</div> </button> </div> The choice of <button> here is deliberate: it is naturally focusable, works with keyboard navigation, and is announced as an interactive element by assistive technology. In a production implementation, the button semantics can be adapted depending on whether you need a true button or a different element with role="button" . The CSS-only interaction The interaction is controlled through :hover and :focus-visible , with a basic transition for a smoother reveal. .spec-list { display: grid; gap: 0.75rem; } .spec-item { width: 100%; text-align: left

2026-08-21 原文 →
开发者

Stop Writing Media Queries for Font Size

A teammate opened a PR titled "fix hero heading on small screens." The diff added a media query. Mine, reviewing it, found four more already in that file — one per breakpoint, added over eighteen months by four different people, each one patching the width the last person didn't think of: .hero-heading { font-size : 3rem ; } @media ( max-width : 1200px ) { .hero-heading { font-size : 2.5rem ; } } @media ( max-width : 992px ) { .hero-heading { font-size : 2.25rem ; } } @media ( max-width : 768px ) { .hero-heading { font-size : 1.75rem ; } } @media ( max-width : 480px ) { .hero-heading { font-size : 1.5rem ; } } Five rules to make one number — the font size of one heading — track the width of the screen it's on. And it still didn't work everywhere: resize the window to 850px and the heading is stuck at the 992px value, a little too big for the space it actually has. Every gap between breakpoints is a size nobody chose, it's just whatever the nearest rule left behind. Here's the part that stings: none of this has been necessary since 2020. The fix that isn't a breakpoint at all clamp() takes three values — a minimum, a preferred value, and a maximum — and returns whichever one the situation calls for: .hero-heading { font-size : clamp ( 1.5rem , 1rem + 2vw , 3rem ); } Read it as a sentence: never smaller than 1.5rem, never bigger than 3rem, and in between, scale with the viewport. The five media queries above collapse into that one line — and unlike them, it doesn't have gaps. clamp() recalculates the size continuously, every pixel the viewport moves, so there's no "850px value" that got left behind. It's a formula, not a lookup table. The middle value is where the "preferred" size lives, and it's 1rem + 2vw — a fixed part plus a viewport-relative part — not just 4vw on its own. That's not decoration. It's the one part of this pattern worth getting right, because the shortcut version quietly breaks something. The version that looks fine and isn't The formula you'll see

2026-08-19 原文 →
AI 资讯

I set the font to the largest size and found the same bug eleven times

I was cleaning up the UI on a side-project iOS app and did one thing: set Dynamic Type to XXXL and screenshot every screen. Reading the code had turned up nothing. The screenshots showed problems immediately. Eleven of them, in the end. All the same cause. Here's the conclusion first. "A parent that pins things side by side" × "text that grows" is not a bug, it's a pattern. And in Japanese it breaks reliably worse than in English. English truncates. Japanese stacks vertically. Same layout, different failure depending on language. English wraps at word boundaries and, failing that, ends in … . You can't read it, but you can tell something was cut. Japanese doesn't do that. It can break between almost any two characters, so once a column is squeezed to one character wide, you get one character per line, stacked downward. Actual output: Rendered Intended Paire / d / Macs Paired Macs ( broken mid-word ) Claud / e / Code Claude Code One character per line "実行中のセッション" (Running sessions) Three step labels stacked vertically A three-step horizontal stepper De / mo , turning the capsule into a circle A "Demo" badge Paire / d / Macs is English breaking mid-word. Once the column has only a few characters left, even English gets there. Japanese gets there much earlier. The cause had the same shape every time Nearly all eleven were this: HStack { Image ( systemName : icon ) . frame ( width : 44 ) // fixed Text ( label ) Spacer () Text ( value ) // pinned right } The 44pt icon and the trailing value claim their width first, leaving the label column a few characters. The fix: rows that carry a value drop the value to the line below — but affordances like chevrons and toggles stay on the right. That row component was shared across the whole settings tree, so fixing one place fixed the entire settings screen. Which also means one decision inside a shared component was breaking eleven screens. ViewThatFits is not a general answer I used ViewThatFits to switch to a stacked layout. It

2026-08-16 原文 →
AI 资讯

جعلنا موقعنا غير قابل للضغط مرتين، ولم يكن الخطأ في الكود

مرتين خلال أسابيع صار موقعنا يبدو سليمًا تمامًا ولا يستجيب للضغط. الصفحة تُحمَّل، والتصميم في مكانه، والكونسول نظيف، والزوار لا يستطيعون فتح أي رابط. في المرتين لم يكن السبب خطأً برمجيًا بالمعنى المعتاد. كان سلوكًا موثّقًا في المتصفح يعمل كما صُمّم تمامًا، لكنه انطبق على نطاق أوسع مما توقّعنا. والأخطر أن اختباراتنا الآلية مرّت بنجاح في الحالتين. الحادثة الأولى: إعداد واحد عطّل سبعين عنصرًا أضفنا ويدجت مساعد ذكي للموقع، وفيه إعداد يفتح نافذة المحادثة تلقائيًا عند دخول الزائر. فعّلناه. بعدها صارت الصفحة ميتة. الروابط لا تُفتح، والأزرار لا تستجيب، وحقول البحث لا تستقبل كتابة. السبب أن الويدجت يعتمد نمطًا شائعًا في نوافذ الحوار: عند فتح النافذة، يضع السمة inert على كل ما عداها حتى لا يتشتت التركيز ولا يهرب مؤشر لوحة المفاتيح خارجها. سلوك صحيح ومطلوب في الحوارات. المشكلة أن الفتح التلقائي يجعل هذه الحالة هي حالة الصفحة الافتراضية عند كل زيارة . سبعون عنصرًا في الصفحة ورثوا inert ، وبقوا كذلك حتى يغلق الزائر نافذة لم يطلب فتحها أصلًا. // ما يفعله الويدجت عند الفتح document . querySelectorAll ( ' body > *:not(.assistant-root) ' ) . forEach (( el ) => el . setAttribute ( ' inert ' , '' )); و inert ليست سمة تجميلية. الفحص السريع يوضح مداها: const el = document . querySelector ( ' a.main-cta ' ); el . offsetParent !== null ; // true — العنصر مرئي getComputedStyle ( el ). pointerEvents ; // 'auto' — لا شيء يمنع المؤشر el . getBoundingClientRect (). width ; // 180 — له مساحة حقيقية el . matches ( ' :disabled ' ); // false — ليس معطّلًا el . closest ( ' [inert] ' ) !== null ; // true ← هنا الجواب كل فحص اعتدنا عليه يقول إن العنصر سليم. inert تعمل في طبقة أخرى: تُخرج العنصر وكل أبنائه من شجرة الوصول، وتلغي استقباله لأحداث المؤشر والتركيز، بلا أي أثر في الأنماط المحسوبة . لماذا مرّت الاختبارات اختباراتنا كانت تسأل الأسئلة المعتادة: هل العنصر موجود في الـDOM؟ هل هو مرئي؟ هل نصّه صحيح؟ الإجابات كلها نعم. ما كشف العطل كان لقطة شاشة نظر إليها إنسان ، ثم محاولة ضغط واحدة. الفحوص البرمجية كانت تصف صفحة سليمة بينما الزائر يرى صفحة جامدة. إن كنت تستخدم أي مكوّن يطبّق inert ، أضف هذا التأك

2026-08-15 原文 →
开发者

CSS Anchor Positioning: Building Tooltips Without JavaScript Positioning Hacks

Introduction Positioning a tooltip sounds simple. Put a small box next to a button. Done. But anyone who has built one knows that it can quickly turn into: position: absolute calculating coordinates listening for resize events handling scrolling checking whether the tooltip fits on screen and sometimes pulling in an entire positioning library Modern CSS is starting to change that. CSS Anchor Positioning lets us position one element relative to another directly in CSS. Let's look at what that means with a very simple tooltip. What Is CSS Anchor Positioning? CSS Anchor Positioning allows one element to act as an anchor and another element to position itself relative to that anchor. Think about UI components such as: Tooltips Dropdown menus Popovers Context menus Floating labels These elements usually need to appear next to another element. Instead of calculating where they belong with JavaScript, we can now describe that relationship in CSS. Conceptually, we're saying: "This button is my anchor. Position this tooltip relative to it." A Simple Example Imagine we have a button: <button class= "info-button" > More info </button> <div class= "tooltip" > Your changes are saved automatically. </div> We want the tooltip to appear directly below the button. First, let's make the button an anchor. .info-button { anchor-name : --info-button ; } We've now given the button an anchor name. Next, connect our tooltip to it. .tooltip { position : absolute ; position-anchor : --info-button ; top : anchor ( bottom ); left : anchor ( left ); margin-top : 8px ; } That's the interesting part. top : anchor ( bottom ); tells the browser: Position the top of the tooltip at the bottom of the anchor. And: left : anchor ( left ); aligns its left side with the button. No getBoundingClientRect() . No coordinate calculations. No resize listener just to figure out where the tooltip belongs. Why Is This Useful? Before Anchor Positioning, we often had to manage positioning ourselves. A simplified Jav

2026-08-14 原文 →
AI 资讯

10 Website Performance and UX Problems That Cost Small Businesses Customers

Small business websites rarely fail because of one catastrophic bug. They fail from an accumulation of small, fixable problems — a slow hero image here, an unlabeled form field there, a broken tab order that quietly locks out keyboard users. None of it looks dramatic in a screenshot. All of it adds up to lost conversions. Working across client rebuilds and audits at Alynox, the same handful of issues show up repeatedly, regardless of industry. Here are ten of the most common, with the practical, mostly low-effort fixes that address them. Unoptimized Images Dragging Down Load Time The single most common performance killer on small business sites is still oversized images — a 4MB PNG hero banner exported straight from a design tool, served at full resolution to a phone screen 400px wide. Fix: html src="hero-800.webp" srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w" sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1600px" alt="Interior of the workshop showing custom furniture in progress" loading="lazy" width="1600" height="900" /> Convert to WebP or AVIF, generate a handful of responsive sizes, lazy-load anything below the fold, and always set explicit width/height to reserve space and avoid layout shift. No Real Mobile-First Design A lot of "responsive" small business sites are really desktop layouts that get squeezed with media queries until they technically fit a phone screen. Buttons end up too small to tap accurately, text wraps awkwardly, and nav menus overlap content. Fix: Design and build mobile-first — base styles for small screens, then progressively enhance with min-width media queries for larger viewports: css .card { padding: 1rem; } @media (min-width: 768px) { .card { padding: 2rem; } } Tap targets should be at least 44×44px (per WCAG and Apple/Google HIG guidance), with enough spacing between interactive elements to prevent mis-taps on smaller screens. Accessibility Treated as an Afterthought Missing alt text, low-contras

2026-08-13 原文 →
AI 资讯

Debugging is also clicking 🖱️

In the last couple of posts I let agents debug over DAP — breakpoints, step over, continue. That's real debugging. But it's only half of it. When I debug something for real, I also click : I press the button and watch what happens, read the dialog, notice the toggle is greyed out. No backtrace ever tells you the Save button never enabled. So — can the agent do that half too? The web is the easy case Browsers are automatable by design. Most agent tools ship their own browser or drive an external one; point Playwright at a page and every element has a stable, queryable handle. The DOM is an accessibility tree wearing a different hat — roles, labels, structure, all there for the reading. For the web, this half of debugging is close to solved. Native apps are another game There's no DOM. When the agent has nothing to go on, it falls back to the eyeball approach: take a screenshot, let the model look, maybe run OCR or a pre-analysis pass to label what's on screen. It works — and sometimes it's the only option — but it's brittle (a few pixels off and the click misses) and it burns tokens describing pictures. I ran into this by accident. I once wrote a tiny skill whose only job was to screenshot a running 4D form and stitch an animated GIF for a README — 4d-capture-gif . Then I noticed Claude Code reaching for it to debug : the skill also reports a bit of the form's structure — where the buttons are — so the agent knows where to click. For simple cases it genuinely works. But screenshots-plus-coordinates is not the thing I want to build on. The cleaner path: read the tree, don't look at pixels Instead of staring at the screen, read the UI tree directly. On macOS you can script the Accessibility API from Python (pyobjc), and there are automation libraries to help. Now you're clicking element #37, the "Save" button instead of coordinate (412, 260), and hope . A couple of open-source tools are pushing exactly here: agent-desktop — a native CLI that exposes any app's accessibi

2026-08-10 原文 →
AI 资讯

Your axe run is green and your dark mode has 1.04:1 contrast

I shipped a page that reported zero axe violations . It had button text at a contrast ratio of 1.04:1 — which is, for practical purposes, invisible text. The scan wasn't broken. It was answering a narrower question than I thought I was asking. The bug I had a theme system built the ordinary way. Tokens on :root , overridden in a prefers-color-scheme media query, and overridden again by an explicit [data-theme] attribute so a manual toggle wins in both directions. Buttons came in two flavours: a solid primary and a bordered secondary. .btn { background : var ( --accent ); color : var ( --panel ); } .btn.sec { background : transparent ; color : var ( --ink ); } In dark mode the accent goes light green, so white-on-accent stops working. I patched it the way you patch things at 1am: :root [ data-theme = dark ] .btn { color : #10241b } @media ( prefers-color-scheme : dark ) { :root:not ([ data-theme = light ]) .btn { color : #10241b } } Now count the specificity. Selector Specificity .btn.sec 0,2,0 :root[data-theme=dark] .btn 0,3,0 :root:not([data-theme=light]) .btn 0,3,0 :not() doesn't add specificity of its own, but its argument does. So :root (0,1,0) + [data-theme=light] (0,1,0) + .btn (0,1,0) lands at 0,3,0. My theme patch outranks the component modifier. In dark mode, every secondary button — transparent background, sitting on a #1a1c1f panel — got painted #10241b . Dark green on near-black. 1.04:1. The nasty part is that this class of bug is invisible in review. The rule looks correct. It is correct, for the buttons it was written for. It just also matched buttons it was never meant to touch, in one theme only. Why the scan didn't catch it axe-core evaluates the DOM as currently rendered . It reads computed styles, and computed styles resolve exactly one colour scheme: whichever one the browser is in right now. So npx axe https://example.com is not "does this page pass contrast." It's "does this page pass contrast in the scheme this headless browser happened to boo

2026-08-10 原文 →
AI 资讯

I find reading hard, so I built a text-to-speech reader for Android — here's how

I've always found reading hard. Long documents slide off my attention, and I lose my place constantly. What I really wanted was something that would read to me and show me the words as it went — so my eyes and ears stayed in sync. Nothing did exactly that, so I built it. It's called ReadAloud , it's on Google Play, and this post is the "why" and the interesting bits of the "how." The moment it became real The first person I showed a rough build to was my Sister, Praise . She'd come to town to officiate a Women's Premier League match at Auntie Aku Astro Turf Park, and I pulled out my phone between everything else. She watched a paragraph read itself aloud with each word lighting up and got genuinely excited — that was the push I needed. She became tester #1. My colleague Reggie became tester #2. Between them they found the rough edges I'd stopped seeing, and the app settled into something stable. What it is A text-to-speech reader for PDFs, EPUB, DOCX, plain text and web articles . It reads aloud in natural voices, highlights each word as it speaks , and auto-scrolls to follow along. There's offline listening, English/French/Spanish, speed-reading (RSVP), a vocabulary builder, and reading stats. The stack: Kotlin, Jetpack Compose + Material 3, MVVM + Clean Architecture, Hilt, Room, DataStore, WorkManager , minSdk 26 . Now the parts that were actually interesting to build. 1. Word-by-word highlighting This is the whole product, so it had to be right. On-device voices are easy — Android's TextToSpeech gives you onRangeStart (API 26+), which fires per spoken range: override fun onRangeStart ( utteranceId : String , start : Int , end : Int , frame : Int ) { // highlight the substring [start, end) in the reader _currentRange . value = start to end } The catch: the natural cloud voices people actually want don't emit onRangeStart . So for cloud synthesis I wrap each word in an SSML <mark> and ask Google Cloud TTS to return timepoints : <speak><mark name= "w0" /> Every <mar

2026-08-08 原文 →
AI 资讯

Sobremesa: Six meals in Mexico, heritage without an address.

This is a submission for Frontend Challenge - Comfort Food Edition, Perfect Landing Mexico is our heritage. Yet, we have no family there to visit. That sounds sadder than it is. What it actually meant, for the years before my wife and I were married and most of our time off since, is that we had to go find it ourselves. No family kitchen waiting. No grandmother's recipe with an address attached. Just the two of us and a country that is ours and that we did not know. So we did what every hungry person in a new city does...we ate. Six cities, six completely different cuisines, and somewhere in there it stopped feeling like traveling. A tlayuda from a stand outside Santo Domingo in Oaxaca. An hour in line at El Yaqui with a michelada in Rosarito. Different food every time. Same feeling every time, and there is no English word for that feeling. There is a Spanish one. What I Built Sobremesa is the time you stay at the table after the food is gone, still talking. Not the meal. The part after the meal. That is the whole site. Six meals across six Mexican cities, and the thing it measures is not how good the food was. It is how long we stayed. Tijuana, one hour. Rosarito, two. Ensenada, one. Guadalajara, ninety minutes. Mexico City, two hours. Oaxaca, two. The page adds them up at the end. Nine hours and thirty minutes at six tables. Comfort food usually means a kitchen you can go back to. We do not have one over there. So the six tables became it. The stand at Plaza Santo Domingo is the family table. The hour in line at Tacos El Yaqui is the Sunday afternoon table. Each entry has the dish, where we ate it, one verified fact about the food, and one line that is just ours, from our experience. There is a form at the bottom where you add your own table and download a card of it, generated in your browser. Nothing gets sent anywhere. One static HTML file. No framework, no build step, no tracking, no cookies, no storage. Two fonts off Google Fonts and nothing else. Designed an

2026-08-07 原文 →
AI 资讯

Building a low-friction DBT skills companion for the web

I built DBT Companion, a free, mobile-friendly web app for exploring skills commonly taught in dialectical behavior therapy: https://dbt-companion.org The main product constraint was brevity without making the material vague. Each skill combines a short explanation with steps someone can follow. Users can also save favourites and use a browser-based diary card. A few implementation priorities were: making the core content usable on small screens keeping navigation predictable making privacy language visible rather than burying it keeping the stack simple with Rails, Hotwire, and Tailwind Diary cards are saved only in this browser's cookies. If cookies are cleared, saved diary cards will be lost. The app is educational, not a replacement for therapy, crisis care, or professional support. I'd welcome feedback on the accessibility, mobile interface, or anything in the privacy wording that could be clearer.

2026-08-02 原文 →
AI 资讯

How I Built a Privacy-First Browser Game Portal with Click-to-Load Iframes

Embedding a browser game looks simple: <iframe src= "https://games.example.net/my-game" ></iframe> That line lets a third party join the page lifecycle immediately. It can download a large bundle, establish connections, run scripts, request storage, display advertising, or fail before the visitor decides to play. AI-assistance disclosure: I used AI to help draft and edit this article, then reviewed its architecture, code, claims, and limitations before publication. For a game directory, that default is both expensive and surprising. A visitor may have opened the page to read the controls, compare games, or check whether the game works on a phone. Loading the player before that intent is known wastes bandwidth and collapses two separate decisions—visiting the guide and opening the third-party game—into one. While working on a browser-game portal, I treated the site and the embedded player as two different trust and performance boundaries. The page renders first-party information immediately. The third-party frame is created only after an explicit Play action. This article explains that pattern and the engineering details that made it useful rather than merely decorative. Start with a two-layer model The outer page should be a complete page without the game: A descriptive heading and summary Controls and gameplay tips Developer and platform information Related games and category navigation A poster or cover image A real button that starts the player The inner layer is a small launcher responsible for the game lifecycle: Validate the requested game. Wait for an intentional Play action. Create the provider iframe. Report loading state. Offer recovery when loading is slow or blocked. Remove the frame when the player resets it. Do not put the remote URL in the initial markup Native iframe lazy loading is helpful below the fold, but it is not an intent gate. Browsers decide when a loading="lazy" frame is close enough to fetch. If the goal is “no third-party game request be

2026-07-30 原文 →
AI 资讯

Show the Evidence That an AI Action Approval Actually Covered

A reviewer approves “update dependencies,” but the system later interprets that as publishing a package. The human was present; meaningful approval was not. The missing artifact is evidence connecting the reviewed plan, its authority, and its consequences to the exact action that ran. What is verified According to OpenAI's July 21 disclosure, a combination of models operating in an internal benchmark with reduced cyber refusals compromised Hugging Face infrastructure. The primary statement is https://openai.com/index/hugging-face-model-evaluation-security-incident/ . July 24 coverage separately reports US discussion of independent audits and emergency-shutdown rules; it should be read as policy reporting and proposals, not as established incident detail or enacted law. Nothing public there establishes the exact attack path, full asset set, or complete response. The approval evidence card Before asking for approval, show: Field Question it answers Stop condition immutable plan version is this still the reviewed plan? version changed actions and arguments what will happen? hidden or wildcard action destinations where will effects land? destination unresolved credential scope/expiry what authority is granted? broad or persistent grant reversibility what can be undone? irreversible effect unexplained independent checks what constrained the plan? required check missing stop receipt did revocation complete? receipt unconfirmed Flow: draft plan -> automated checks -> human review -> version-bound approval -> execution receipts -> completion or emergency stop -> post-action summary. Any plan mutation loops back to review. “Approve all future actions” is not a shortcut; it changes the authority being requested. Research protocol Give participants three scenarios: a harmless wording change, a destination change, and an irreversible action inserted after review. Ask them to identify what they authorize, what would make them refuse, and where they expect emergency stop. Success

2026-07-24 原文 →
AI 资讯

Keep an Accessible Combobox Stable When Search Results Arrive Out of Order

An accessible combobox can follow the correct ARIA pattern and still become unusable when two search responses arrive in the wrong order. Reproduce this sequence: Type ca , then quickly type cat . The cat response arrives first and highlights cat facts . The slower ca response arrives and replaces the list. aria-activedescendant now points to an option that no longer exists. IME input adds another boundary: searching during composition can send partial text the user has not committed. I would model the request generation explicitly: let generation = 0 ; let composing = false ; async function search ( query : string ) { const mine = ++ generation ; const options = await fetchOptions ( query ); if ( mine !== generation || composing ) return ; render ( options ); restoreActiveOptionByKey (); } The stable key matters. An array index cannot preserve the active option when ranking changes. Regression matrix Input Injected failure Expected evidence ca → cat first request delayed only cat results render Arrow Down result refresh active key survives or resets visibly Escape response arrives afterward popup stays closed IME composition network is fast no request until compositionend A Playwright test should assert focus remains on the input, every aria-activedescendant resolves to a live element, and Escape invalidates outstanding generations. A manual screen-reader pass should confirm result-count announcements are not emitted for discarded responses. The WAI-ARIA Authoring Practices combobox pattern defines the keyboard contract. The missing production step is testing that contract under asynchronous replacement, not only against static example data. Which stale-response failure has been hardest to reproduce in your search UI?

2026-07-20 原文 →