Skip to content
Frontend Feeds
  • Today
  • Archive
  • Sources
  • Categories
    • The Giants6 Sources
    • Multi Author Blogs18 Sources
    • Top Front-end Bloggers25 Sources
    • More Front-end Bloggers77 Sources
    • Browsers, engines, etc.11 Sources
    • Libraries, Frameworks, etc.12 Sources
    • Company/Startup Blogs12 Sources
    • Developer/Designer News4 Sources
    • YouTube Channels13 Sources
    • Podcasts7 Sources

Today

Front-end, in one read.

The latest from the sources worth following, newest first.

Saturday, 7 November 2026

  • DEV CommunityThe Giantsread at source

    Slow-Breath Coach: your webcam scores how well you breathe along

    Challenge entry for the Presage SmartSpectra SDK Challenge. Code: https://github.com/474443481/slow-breath-coach 💡 The idea Most breathing apps count seconds and assume you followed along. Slow-Breath Coach does not assume anything: it watches your chest through a plain webcam with the Presage SmartSpectra SDK , measures your actual breathing waveform, and scores how well you tracked the pacer after each session. The target is 5.5 breaths per minute — the resonance pace where six seconds in and six seconds out line up with heart-rate rhythms. The pacer circle expands as you inhale, contracts as you exhale, and the score tells you how close your real chest motion came to that rhythm. 🎬 Demo ▶ Watch the 30-second demo: demo.webm — 10 MB, webm, direct play (Also committed at github.com/474443481/slow-breath-coach — pacer circle, live waveform and the follow score, recorded straight from the app's own /demo canvas.) 30 seconds, canvas-rendered, self-recorded by the page itself ( /demo?record=1 ). The demo replays a synthetic breathing signal through the exact same scoring function used on live camera captures, so the UI is honest even without a face in frame. 🧠 How it works Three metric channels from the SDK ( requestedMetrics: [0, 2, 15] ): the chest breathing trace, breathing rate, and pulse. Frames go from the browser (JPEG, 15 fps) to a local Node server, which decodes them and feeds the SDK through a forked worker process over IPC. Two SDK quirks ate most of my debugging time — worth documenting for the next person: Timestamps must be epoch microseconds, and they must be gap-free. I first passed process uptime. The SDK silently treated the frames as days old, stalled the pipeline, and the session died at exactly 151 frames — every single time. Then real-world browser capture jitter (a dropped frame here, a 40 ms stall there) trips the SDK's gap detector: SmartSpectra detected a gap between camera frame timestamps. The fix: a virtual 15 fps clock, +16 667 ”s per frame, re-anchored to Date.now() * 1000 on every new worker. IPC must use serialization: "advanced" . A plain child_process.fork turns Uint8Array frame buffers into plain objects on the other side, and sendFrame rejects them with a confusing type error. The score. Every second the server takes the last 30 s of the smoothed upper-chest trace and computes a normalized cross-correlation against the pacer's cosine reference, maximized over lags of ±half a period (a steady lag is fine — you may breathe slightly behind the circle — but the rhythm must match). That blends with a rate gate from zero-crossings: score = 100 × (0.6·corr + 0.4·rateGate) . A noise-only trace can fake a high correlation, but the rate gate kills it. Validated on synthetic signals from perfect following (1.00) down to pure noise (0.12). Survivability. The native SDK pipeline wedges in long real sessions, so the HTTP server never loads the SDK itself. The worker is a disposable fork that self-recycles (no face for 140 frames, no metrics for 400, 9 s of silence) and a tiny supervisor respawns it — the dashboard websocket never dies with the SDK. 🛠 Run it git clone https://github.com/474443481/slow-breath-coach cd slow-breath-coach npm install @smartspectra/node-sdk jpeg-js PRESAGE_API_KEY = " " node coach_server.mjs http://localhost:8815/ — camera capture page (consent gate, then keep the tab in the foreground) http://localhost:8815/dashboard — pacer circle + live waveform + score http://127.0.0.1:8815/demo?record=1&secs=30 — the canvas demo above, and it records itself Free API key: physiology.presagetech.com (100 credits on the free tier). 🔐 Consent and Data Handling Consent first. The capture page shows a blocking consent notice explaining what is captured and where it goes; nothing starts until you press I agree . What leaves your machine. JPEG frames go to the local Node process and are forwarded to the Presage cloud API for physiological inference — that is the SDK's architecture (see Presage's Telemetry and Privacy documentation ). The SDK also reports anonymous usage telemetry to Presage. What is stored. Nothing, anywhere. The server keeps a bounded in-memory ring buffer for the live chart and discards it on restart. No frames, no video, no waveforms are written to disk. Closing the capture page stops the stream. No medical claims. This is a breathing-pacing toy, not a diagnostic device. Its numbers are not medical measurements and it makes no health claims. 🎯 What I learned Camera pipelines are 20% vision and 80% plumbing. This week a webcam was held hostage by a QQ video call, then by the Windows Camera app, then by a quietly stopped Frame Server service, then by zombie device handles inside Edge — and underneath all of that, the SDK wanted a frame clock more regular than any real camera delivers. The virtual clock fixed the SDK; the process firewall fixed everything the SDK still throws at you. The nicest surprise: once the timestamps were honest, the correlation between "am I following the circle" and "what the SDK sees in my chest" was immediately visible on the waveform — no calibration, no tuning.

  • DEV CommunityThe Giantsread at source

    set the title How Hindsight Lets a Support Chat Continue Across Sessions

    How Hindsight Lets a Support Chat Continue Across Sessions A support assistant that remembers a customer’s laptop model but forgets which conversation contained it is only halfway useful. I built RecallCare around a different boundary: conversation history belongs to a thread, while customer knowledge should follow the customer. That sounds like a storage choice. In practice, it changes retrieval, prompting, failure handling, and even what I show in the interface. The central design decision was to make Hindsight’s long-term agent memory a separate system from the chat transcript, and to treat the two as complementary sources of context rather than competing databases. Two kinds of context, with different jobs RecallCare is a customer-support chat application. The frontend is a React app with a dashboard and conversation view. The backend is a FastAPI service: SQLAlchemy persists customers, conversations, and messages; authentication identifies the current customer; an LLM generates support replies; and Hindsight retains and recalls customer information across threads. Figure 1: RecallCare AI dashboard displaying retrieved customer memory insights. The distinction between transcript and memory matters. A transcript answers, “What did we say in this conversation?” Customer memory answers, “What relevant facts should I know before responding now?” A conversation can be long, but its context window is necessarily bounded. A customer’s support history can span many conversations, and copying every old message into every prompt would be wasteful and noisy. The request path makes that boundary explicit: Authenticate the customer and verify access to the requested conversation. Ask Hindsight for memories relevant to the incoming support question. Load a bounded set of recent messages from that conversation. Pass both kinds of context to the response generator. Persist the user and assistant messages to the conversation. Retain the customer’s actual message in Hindsight. The backend’s recall query is grounded in the current request while still describing the kinds of facts support tends to need: customer_memories = await recall_memory ( customer_id , ( f " { message } . Retrieve relevant information about this " " customer from all previous conversations, including " " devices, preferences, contact methods, reported issues, " " previous support interactions and resolutions. " ) ) I like this approach because the retrieval request has two inputs: what the customer is asking now and the support-specific context worth recovering. The question makes recall contextual; the additional terms make it less likely that a useful device detail or prior resolution is overlooked just because it is not repeated verbatim in the new message. A memory bank is not a conversation ID Each customer gets a stable Hindsight bank identifier derived from the customer ID. The identifier is a SHA-256 digest truncated to 24 hexadecimal characters, prefixed with the application name. It avoids putting the raw database ID in the bank name, and it gives every conversation for a customer the same place to retain and recall information. def get_customer_bank_id ( customer_id ): customer_hash = hashlib . sha256 ( str ( customer_id ). encode ( " utf-8 " ) ). hexdigest ()[: 24 ] return f " recallcare- { customer_hash } " That stable mapping is the connective tissue across sessions. A new conversation still has its own ID and transcript, but Hindsight lookup uses the customer identity. A customer who starts a fresh thread about a laptop problem can therefore get relevant history from an earlier thread without loading that entire thread into the prompt. Figure 4: In a separate customer session, the assistant asks the customer to provide their laptop model and preferred support channel instead of recalling the previous customer's details. The implementation wraps Hindsight operations behind a small module rather than sprinkling client calls through route handlers. That module initializes the client from environment configuration, creates or ensures the customer bank, and exposes focused operations for retain, recall, and listing valid memories. The application lifecycle initializes and closes the client with FastAPI’s lifespan. These boundaries make it easier to reason about which code owns the external service connection and which code owns the support workflow. The Hindsight documentation explains the memory model in more detail. The application delegates retaining and retrieving durable memory to a purpose-built service, while keeping messages and conversation metadata in its relational store. You can also explore the Hindsight GitHub repository to understand its implementation. Why I keep recent messages anyway Once I had long-term memory, it was tempting to treat it as the whole context layer. That would have been a mistake. Hindsight is not a replacement for the current thread. The recent transcript contains details of the ongoing exchange—what the assistant just suggested, which troubleshooting step failed, and what the customer is responding to. Long-term memory should add durable history, not reconstruct every turn. RecallCare loads the latest 20 messages for the selected conversation, orders them chronologically, and labels them separately from customer memories before sending them to the model: memories_for_ai = [ " LONG-TERM CUSTOMER MEMORY: " + item for item in customer_memories ] memories_for_ai . extend ([ " CURRENT CONVERSATION CONTEXT: " + item for item in conversation_context ]) The labels are intentionally plain. They communicate provenance and scope: one set represents recalled facts about the customer; the other is recent dialogue from this conversation. The response instructions then tell the model to use supplied memories, pay attention to dates, prefer newer explicitly dated information when facts conflict, and ask for confirmation when the order cannot be established. This separation also gives the system a graceful behavior when recall is unavailable. The chat endpoint catches recall failure, logs it, and continues with an empty set of long-term memories. The assistant can still answer using the current message and recent thread, rather than making an external memory outage a hard failure for every support interaction. That is a deliberate availability trade-off: a reply may be less personalized, but the support flow remains usable. In a production deployment I would make this degradation visible in operational telemetry, not silently treat it as equivalent to a successful recall. Retaining messages is not the same as retaining truth RecallCare retains the customer’s message with a timestamp after the assistant response has been saved. The timestamp gives later reasoning a way to distinguish a newer statement from an older one. The prompt makes that temporal rule explicit: an explicitly dated correction should take priority over older undated information about the same preference. This matters for ordinary support facts. Suppose a customer once asked for email updates, then later says, “Please call me instead.” A memory system that only stores both statements has preserved data but has not resolved the useful question: which preference should the next support interaction follow? The dashboard adds a lightweight presentation layer for that problem. It cleans formatting artifacts from memory text, removes exact and near-duplicate records, groups facts into device information, support preferences, previous support activity, and other information, and tries to identify explicit preference updates. When it sees a newer dated update, it can show the current preference and hide older conflicting preference records from the default view. If it cannot confidently resolve email-versus-phone records, it displays a conflict instead of guessing. That last behavior is important. A UI that confidently renders stale information is worse than one that asks a human to confirm. The dashboard shows both the current preference when it can infer it and the records that informed the view; the user can also expand the list to inspect all memory records. The cleanup logic is not a substitute for Hindsight’s retrieval. It is a read-side aid for making retained information understandable to a person. There is a limitation worth being clear about: the system retains the customer’s messages, not a fully verified profile fact with an authoritative source and resolution workflow. A timestamp helps order statements, but it does not prove that the newest statement is true or that two differently worded memories refer to the same preference. That distinction should shape both the product behavior and any production rollout. I would rather surface uncertainty than imply that memory is infallible. The prompt is part of the interface contract Memory quality is not only about retrieval. The model needs rules for how to use what it receives. RecallCare’s prompt says not to invent customer details, not to claim information is missing when it is present in memory, and not to claim a memory was saved unless the application confirms it. It also distinguishes a dated correction from older undated information. Those rules are small, but they make the memory boundary legible. Retrieval supplies candidate facts; the prompt sets expectations for how they influence an answer. For example, if the customer asks, “Can you send the warranty steps to my preferred contact method?” and Hindsight returns an older email preference plus a later dated phone correction, the assistant should use phone as the current preference. If the records conflict without reliable dates, it should ask rather than select one arbitrarily. The generator also keeps response behavior separate from memory mechanics. It receives a list of prepared context strings and builds the system prompt around them: if memories : memory_context = " \n " . join ( f " - { memory } " for memory in memories ) else : memory_context = ( " No relevant previous customer information " " was retrieved. " ) That lets the chat route decide what context to supply while the generator focuses on producing a concise support answer. It is a modest seam, not a grand abstraction, but it prevents the model integration from having to know how Hindsight banks are named or how SQL conversations are queried. What this changes in an interaction Consider a customer who has previously discussed a particular laptop and reported intermittent charging trouble. In a later, separate conversation, they write: “It stopped charging again after the update.” The new thread begins with no transcript of the earlier support exchange. Recall retrieves relevant history from the customer’s bank; the backend also loads the recent messages from the new thread, which may be empty. The assistant can respond with context about the known device and prior issue, then ask a focused follow-up rather than starting with “What model is your laptop?” Figure 2: A customer shares their Dell Inspiron 15 laptop model and email support preference. If the customer corrects a support preference in that same exchange, the message is retained with a timestamp. Later interactions can retrieve the correction alongside older information. The prompt tells the assistant how to treat dated changes, and the dashboard gives a person a way to inspect what has been retained and see when records disagree. Figure 3: Hindsight retrieves the customer's previously shared laptop model and preferred support channel in a new conversation. That is the practical payoff: not that the assistant remembers everything, but that it can use relevant history without confusing the history of one thread with the identity and ongoing needs of a customer. Lessons I would carry into another system Give transcripts and memory different responsibilities. A transcript is ordered dialogue. Memory is selectively retrieved customer context. Mixing the two creates oversized prompts and unclear data ownership. Make identity stable across conversations. A customer-level memory key is what turns separate threads into continuity. Keep the mapping centralized and avoid deriving it ad hoc in every route. Treat time and disagreement as first-class problems. Storing two preferences is easy; knowing which one to use is not. Preserve dates, define how corrections affect older facts, and surface unresolved conflicts. Design for partial availability. Memory recall should not automatically become a total chat outage. Decide what degraded behavior looks like, and make it observable so an outage is not mistaken for a personalized answer. Show people what the system remembers. A memory dashboard is not just a debugging convenience. It gives operators and customers a way to inspect, question, and correct the context that shapes future replies. The most useful decision in RecallCare was not adding another context source. It was deciding that each source has a different lifetime and purpose. Hindsight gives the application a customer-level memory boundary; the conversation store preserves what happened in a particular thread. Once those roles are explicit, cross-session support becomes a tractable engineering problem—with real trade-offs around relevance, stale facts, service failures, and trust, rather than a vague promise that the assistant will “remember.”

  • DEV CommunityThe Giantsread at source

    Docker tag and digest: which one Kubernetes runs

    An image reference can carry a tag and a digest at once, as in busybox:1.37.0@sha256:73aaf090
 . Kubernetes runs the image the digest names. The tag is removed before the pull and never compared with the digest. If the two disagree, the pod runs the digest's image while the spec shows the tag's version. The recipes in kadet-hub/recipes could produce exactly that mismatch. They kept the tag in one file and the digest in another, until kadet-hub/recipes#50 put both into one literal line. Everything below ran on 2026-11-02 with kind v0.33.0 and its default node image kindest/node:v1.37.0@sha256:a1ed56cf
 (Kubernetes v1.37.0, containerd v2.3.4), kubectl v1.37.2 and oras 1.3.6. A tag that disagrees with its digest The two busybox tags resolved to these digests: $ oras manifest fetch --descriptor docker.io/library/busybox:1.36.1 {"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662","size":9535} $ oras manifest fetch --descriptor docker.io/library/busybox:1.37.0 {"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec","size":9535} Each is the digest of an OCI image index, the list of per-platform images behind the tag. Tags can move, so a later run may resolve other digests. The pod takes the tag 1.37.0 and the digest of 1.36.1. That is the state a version bump leaves behind when the digest stays: apiVersion: v1 kind: Pod metadata: name: tag-and-digest spec: restartPolicy: Never containers: - name: busybox image: busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 command: [sh, -c, "busybox | head -1"] The API server accepts it, and the kubelet pulls and starts it: $ kubectl get events --field-selector involvedObject.name=tag-and-digest -o jsonpath='{range .items[*]}{.reason}{": "}{.message}{"\n"}{end}' Scheduled: Successfully assigned default/tag-and-digest to post-digest-24c921-control-plane Pulling: Pulling image "busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662" Pulled: Successfully pulled image "busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662" in 2.15s (2.15s including waiting). Image size: 2217006 bytes. Created: Container created Started: Container started The container prints the version of the binary it runs: $ kubectl logs tag-and-digest BusyBox v1.36.1 (2023-05-18 22:34:17 UTC) multi-call binary. The pod's image fields: $ kubectl get pod tag-and-digest -o jsonpath='{.spec.containers[0].image}{"\n"}{.spec.containers[0].imagePullPolicy}{"\n"}{.status.containerStatuses[0].image}{"\n"}{.status.containerStatuses[0].imageID}{"\n"}' busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 IfNotPresent sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d docker.io/library/busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 .spec.containers[].image keeps the string as written, tag included. .status.containerStatuses[].imageID holds the repository and the digest without a tag. It is the field that tells you which image runs. Its digest is the one oras resolved for 1.36.1. .status.containerStatuses[].image carries no tag either. containerd reports the image's first tag there, and the node-local image ID when the image has none ( container_status.go ). That ID, sha256:b116e155
 , is the digest of the image's config. The node stored no tag for the image it pulled by digest: $ docker exec post-digest-24c921-control-plane crictl images --digests | grep -E 'IMAGE|busybox' IMAGE TAG DIGEST IMAGE ID SIZE docker.io/library/busybox 73aaf090f3d85 b116e15507444 2.22MB A second pod with busybox:1.37.0 and no digest prints BusyBox v1.37.0 . Its imageID ends in a7dbc319
 , the digest of 1.37.0. Its .status.containerStatuses[].image is docker.io/library/busybox:1.37.0 . The tag is never looked up The tag no-such-tag does not exist: $ oras manifest fetch --descriptor docker.io/library/busybox:no-such-tag Error response from registry: failed to find "docker.io/library/busybox:no-such-tag": docker.io/library/busybox:no-such-tag: not found A third pod uses it in front of the 1.36.1 digest, with imagePullPolicy: Always so that the kubelet asks for a pull: apiVersion: v1 kind: Pod metadata: name: no-such-tag spec: restartPolicy: Never containers: - name: busybox image: busybox:no-such-tag@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 imagePullPolicy: Always command: [sh, -c, "busybox | head -1"] Its events report Successfully pulled image "busybox:no-such-tag@sha256:73aaf090
" , and it runs 1.36.1: $ kubectl logs no-such-tag BusyBox v1.36.1 (2023-05-18 22:34:17 UTC) multi-call binary. The kubelet hands the reference to the container runtime as written. Its applyDefaultImageTag appends :latest only when a reference has neither a tag nor a digest ( image_manager.go , Kubernetes v1.37.0). containerd's CRI plugin normalizes the reference with ParseDockerRef before it pulls ( image_pull.go ). The function comes from distribution/reference v0.6.0, the version containerd v2.3.4 depends on. Its documentation says what happens to the tag: For references containing both a tag and a digest, it returns a digested reference. Its example turns docker.io/library/busybox:latest@sha256:7cc4b5ae
 into docker.io/library/busybox@sha256:7cc4b5ae
 ( normalize.go ). containerd also resolves a reference against the images already on the node through ParseDockerRef ( service.go ). The presence check behind IfNotPresent therefore ignores the tag as well. The Kubernetes documentation states the outcome in its list of image name examples. It describes registry.k8s.io/pause:3.5@sha256:1ff6c18f
 as "Image name with tag and digest. Only the digest will be used for pulling." ( Images ). The tag still sets the pull policy When imagePullPolicy is omitted, the API server defaults it from the tag. SetDefaults_Container sets Always for the tag latest and IfNotPresent for any other ( defaults.go ). The tag comes from ParseImageName , which returns it even when a digest is present ( parsers.go ). A server-side dry run shows both cases: $ kubectl run latest-and-digest --restart=Never --image=busybox:latest@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 --dry-run=server -o jsonpath='{.spec.containers[0].imagePullPolicy}{"\n"}' Always $ kubectl run digest-only --restart=Never --image=busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 --dry-run=server -o jsonpath='{.spec.containers[0].imagePullPolicy}{"\n"}' IfNotPresent The documentation's list of defaults says: "if you omit the imagePullPolicy field, and you specify the digest for the container image, the imagePullPolicy is automatically set to IfNotPresent " ( default image pull policy ). With :latest in front of the digest, the tag decides and the result is Always . Under Always the runtime contacts the registry at every container start, although the digest already fixes the image ( image pull policy ). Two files, one image The recipes are Kapitan inventory classes. Until kadet-hub/recipes#50, each kept an image's tag in applications. .version in its init.yml , and the digest in the image line of another class file. In the miniflux recipe, init.yml set version: 2.3.4 and miniflux.yml had this line: image: miniflux/miniflux:${applications.miniflux.version}@sha256:72e49f513a63104506aab98ba8cfc3a409d4fde23550a583dec07ec941f1e94e The ${...} is an interpolation that Kapitan fills in from the parameter. A target that raised the version without the digest compiled to manifests with the new tag and the old digest. That is the first pod above. The spec and every diff of the compiled manifests show the new version, and the kubelet runs the old image. The fix writes each image as one literal, / : @sha256: : image: miniflux/miniflux:2.3.4@sha256:72e49f513a63104506aab98ba8cfc3a409d4fde23550a583dec07ec941f1e94e applications. .version is gone wherever it only fed image tags. check lint , the linter the recipes run in CI, now fails on a line of a class file that holds both @sha256: and ${ : for n, line in enumerate(text.splitlines(), 1): if "@sha256:" in line and "${" in line: errors.append(f"{f}:{n}: image reference takes its tag from an interpolation (RCP-66)") A reference without a digest does not match this check, and the recipes' specification leaves those to review ( RCP-66 ). Renovate reads the tag from the same line Renovate writes references in this form. Its Docker documentation says that "When pinning a digest, Renovate retains the Docker tag in the FROM line for readability", with FROM node:14.15.1@sha256:d938c176
 as the example ( Digest Pinning ). The pinDigests option pins images "by means of their SHA256 digest and not only by tag" ( pinDigests ). Renovate's updates start from the tag it finds in the file ( How It Works ): If the image tag in use "looks" like a version (e.g. myimage:1 , myimage:1.1 , myimage:1.1.0 , myimage:1-onbuild ) then Renovate checks the Docker registry for upgrades The same page describes pull requests for a new digest behind an unchanged tag ( Digest Updating ). In kadet-hub/recipes, Renovate's regex managers capture the tag as currentValue and the digest as currentDigest from the same image line. Those are the capture groups the regex manager documentation asks for. The old miniflux line had ${applications.miniflux.version} where that version belongs. Originally published at kadet-hub.org .

  • DEV CommunityThe Giantsread at source

    Renovate automerge with required reviews on GitHub

    Renovate's automerge stops at a branch that requires an approving review. Its documentation says so: "If you have mandatory Pull Request reviews then it means Renovate can't automerge its own PR until such a review has happened" ( Renovate docs ). The main ruleset of kadet-hub/registry requires one approving review and a code-owner review. Since 2 November 2026 Renovate merges its own minor, patch and digest updates there without either. The Renovate GitHub App has a ruleset bypass that works on pull requests only. GitHub's auto-merge stays off, so Renovate merges once every check on the pull request has passed, including the checks the ruleset does not require. The change is kadet-hub/registry#441 . The first pull request merged this way was #439 , a digest update of the distroless debug image that the gate 's sandbox image copies its busybox from. The GitHub docs quoted below were read on 2 November 2026, the Renovate docs and source at Renovate 44.201.0. Why an approval bot does not help Renovate's docs name two ways past a required review on GitHub. One is a bypass of the review requirement. The other is the helper apps renovate-approve and renovate-approve-2, which "will mark all automerging Pull Requests by Renovate as approved" ( Renovate docs ). An approval from an app does not satisfy a code-owner review. With code-owner review required, "any pull request that modifies content with a code owner must be approved by that code owner" ( GitHub docs ). A CODEOWNERS file lists owners as "GitHub usernames or team names" ( GitHub docs ). A workflow that approves with its GITHUB_TOKEN approves as an app too, because "The GITHUB_TOKEN secret is a GitHub App installation access token" ( GitHub docs ). Renovate's docs put it this way: "having a CODEOWNERS file could block automerging, because it means a code owner must review the PR" ( Renovate docs ). kadet-hub/registry has a code owner for .github/ , gate/ , policy/ , scan/ , sandbox/ and .clusterfuzzlite/ . Every file Renovate updates in that repository sits under one of them, apart from the registry entries. 439 changed sandbox/Dockerfile . GitHub's auto-merge is the other half of that setup, and Renovate uses it by default. "Auto-merge merges a pull request automatically after all required reviews and status checks pass" ( GitHub docs ). It does not wait for a check the branch does not require. The registry's ruleset requires two checks, gate and renders . Several checks that exercise a dependency update in this repository come from path-filtered workflows. selftest runs the gate's self tests when sandbox/ , gate/ , scan/ , policy/ or a few other paths change. sandbox-advisories fails on an advisory that has a fix for the sandbox image. GitHub's troubleshooting page says that the checks of a workflow skipped by path filtering "stay in a "Pending" state and block merging", and advises: "Avoid requiring workflows that can be skipped" ( GitHub docs ). So these checks are not required, and auto-merge would not wait for them. A bypass for pull requests only 441 added one entry to the bypass list of the main ruleset. The repository keeps a copy of the ruleset in .github/ruleset.json : - "bypass_actors": [{"actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request"}], + "bypass_actors": [ + {"actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request"}, + {"actor_id": 2740, "actor_type": "Integration", "bypass_mode": "pull_request"} + ], Integration is the actor type of a GitHub App. 2740 is the ID that gh api /apps/renovate returns for the Renovate app. The live ruleset carries the same entry: $ gh api repos/kadet-hub/registry/rulesets/24720369 --jq '.bypass_actors[] | select(.actor_type == "Integration")' {"actor_id":2740,"actor_type":"Integration","bypass_mode":"pull_request"} The REST API accepts always , pull_request and exempt for bypass_mode , and its reference says: "pull_request means that an actor can only bypass rules on pull requests" ( GitHub REST docs ). The default is always . An entry that leaves bypass_mode out lets the app push to main directly. In the ruleset editor, pull_request is the option "For pull requests only", which GitHub offers "to grant bypass to an actor without allowing them to push directly to a repository". The same page goes on: The selected actor is now required to open a pull request to make changes to a repository, creating a clear trail of their changes in the pull request and audit log. The actor can then choose to bypass any branch protections and merge that pull request. ( GitHub docs ) The bypass covers the rules of the whole ruleset, the required status checks among them. Once the app is on the list, GitHub no longer stops its merge on a failed check. That job moves to Renovate. What Renovate waits for The top of renovate.json leaves automerge off by default and switches GitHub's auto-merge off: "automerge" : false , "platformAutomerge" : false , "minimumReleaseAge" : "3 days" , "minimumReleaseAgeBehaviour" : "timestamp-required" , With platformAutomerge at its default true , Renovate hands the merge to the platform, which merges "after all (if any) branch policies have been met" ( Renovate docs ). With false , Renovate merges with its own API call. For the default automergeType of pr , Renovate "automerges the PR on a subsequent run once it detects the PR's status checks are 'green'", and "Renovate only automerges branches which are up-to-date and green" ( Renovate docs ). The entry for ignoreTests states the default: "Renovate's default behavior is to only automerge if every status check has succeeded" ( Renovate docs ). What green means on GitHub is decided in getBranchStatus ( Renovate 44.201.0 source ). It reads the commit statuses and the check runs of the branch's head commit. One check run with the conclusion failure makes the branch red. The branch is green only when every check run has the conclusion success , neutral or skipped and the remaining commit statuses are successful. A check run that is queued or still running has no conclusion yet, so the branch stays yellow and Renovate does not merge. Successful statuses that Renovate sets itself, such as renovate/stability-days , are filtered out first, so they cannot turn a branch green on their own. GitHub counts the same three conclusions as a passing required check ( GitHub docs ). Renovate sees only the checks that exist on the commit when it runs. A workflow that has not created its check run yet cannot hold the merge. Which updates merge The last three packageRules of renovate.json decide what Renovate may merge: { "description" : "REV-5: Renovate merges its own pull request once every check passed" , "matchUpdateTypes" : [ "minor" , "patch" , "digest" , "pin" , "pinDigest" ], "automerge" : true } , { "description" : "REV-5: an entry bump publishes a new version, which a maintainer approves (DEC-3)" , "matchFileNames" : [ "generators/*.yaml" , "recipes/*.yaml" ], "automerge" : false } , { "description" : "REV-5: a digest update of an action under the same version is a moved tag" , "matchManagers" : [ "github-actions" ], "matchUpdateTypes" : [ "digest" ], "automerge" : false } The descriptions cite kadet-hub's specs. REV-5 is the automerge requirement in gate.md , and DEC-3 is the decision in marketplace.md that every new version needs a maintainer's approval. Renovate applies packageRules in order, and "later rules may override configuration options from earlier ones, if they both specify the same option" ( Renovate docs ). The first rule turns automerge on for five update types. A major update matches none of the three and keeps the top-level "automerge": false . The second rule covers the registry entries. Each file under generators/ and recipes/ pins a generator or a recipe to a tag and commit in its author's repository. Merging a bump of that pin publishes the new version in the catalog, so these bumps stay with a maintainer whatever their update type. The third rule is about GitHub Actions. kadet-hub pins every action to a commit SHA with the version as a comment, through the helpers:pinGitHubActionDigests preset. A new release of an action changes the version in that comment. A digest update under the same version means that the tag now points at another commit. That is how tj-actions/changed-files was compromised in March 2025: "Attackers retroactively modified multiple version tags to reference a malicious commit" ( GHSA-mrrh-fwg8-r2c3 ). The three days of minimumReleaseAge do not hold such an update back: "If an existing tag is force-pushed to new commits, the digest update ages against the original release date, so it may pass Minimum Release Age immediately" ( Renovate docs ). Everything else still waits three days after its release when the datasource reports a release timestamp. kadet-hub's config treats git tags and registries other than Docker Hub as reporting none and lets those updates through without one ( minimumReleaseAgeBehaviour: timestamp-optional ). Such an update merges as soon as its checks pass. kadet-hub's threat model in marketplace.md records what this gives up. A malicious minor, patch or image digest release now reaches main without a maintainer once the release age allows it and every check passes. Before 441 it needed one approval by the single maintainer. kadet-hub/recipes#56 made the same change in kadet-hub/recipes and keeps more updates manual. Lock file maintenance is not on its automerge list, because "the updated packages have no release timestamp for Renovate to check" ( Renovate docs ). The pin of the next tag of kadet-hub's Kapitan image stays manual, since that image is rebuilt up to every 15 minutes and its digest updates have no release timestamp. A recipe's Source line names the chart or image the recipe is built from, and updates to it wait for a reviewer. The main ruleset there requires one approval without a code-owner review and has the same pull_request bypass for the Renovate app. The first merge 439 has no review, and GitHub still reports a review as required: $ gh pr view 439 -R kadet-hub/registry --json mergedBy,mergedAt,reviewDecision,reviews {"mergedAt":"2026-11-02T10:11:50Z","mergedBy":{"is_bot":true,"login":"app/renovate"},"reviewDecision":"REVIEW_REQUIRED","reviews":[]} The check runs on its last commit, ordered by completion time in UTC: $ gh pr view 439 -R kadet-hub/registry --json statusCheckRollup \ --jq '.statusCheckRollup | map(select(.__typename == "CheckRun")) | sort_by(.completedAt)[] | [.completedAt, .conclusion, .name] | @tsv' 2026-11-02T09:25:20Z SUCCESS workflow-lint 2026-11-02T09:25:24Z SUCCESS checksum-pins 2026-11-02T09:25:31Z SUCCESS sandbox-advisories 2026-11-02T09:25:49Z SUCCESS gate 2026-11-02T09:25:49Z SKIPPED renders 2026-11-02T09:25:59Z SUCCESS comment 2026-11-02T09:26:06Z SUCCESS consumer-selftest 2026-11-02T09:26:26Z SUCCESS CodeQL 2026-11-02T09:26:31Z SUCCESS analyze 2026-11-02T10:10:36Z SUCCESS gate-selftest The same rollup holds one commit status, renovate/stability-days , with the state SUCCESS . gate and renders are the required checks. renders checks a recipe on kind and runs only when the gate compiled one, so on #439 it was skipped. A skipped job "will not prevent a pull request from merging, even if it is a required check" ( GitHub docs ). Both required checks were done at 09:25:49. gate-selftest finished at 10:10:36, and Renovate merged at 10:11:50. GitHub keeps the rule evaluations of the merge as a rule suite, which the REST API returns ( GitHub REST docs ): $ gh api repos/kadet-hub/registry/rulesets/rule-suites/4853993537 \ --jq '{actor_name, ref, result, rules: [.rule_evaluations[] | {rule_type, result}]}' {"actor_name":"renovate[bot]","ref":"refs/heads/main","result":"bypass","rules":[{"result":"fail","rule_type":"pull_request"},{"result":"pass","rule_type":"required_status_checks"},{"result":"pass","rule_type":"non_fast_forward"},{"result":"pass","rule_type":"deletion"}]} The pull_request rule failed, and its details name the missing code-owner review. The required status checks passed on their own. The merge went through as bypass . Originally published at kadet-hub.org .

  • DEV CommunityThe Giantsread at source

    Inside the Favicon Pack: What the ZIP Actually Holds, Read Byte by Byte

    I work on OnePassApps, so this is our own tool, and every number below was read out of the ZIP that the Icon Studio produced in a real browser on 7 November 2026 - off the disk, not off the panel, and not from memory. A favicon pack is a small chore with a fixed shape: one source image in, a folder of near-duplicates out - a 16, a 32, a 48, an Apple touch icon, two Android sizes, an .ico , a web manifest, and the tags to paste. The Icon Studio does that conversion in the browser tab. This post is what actually sits inside the ZIP, file by file. What the panel says first Two stages, and the empty state says what it will take: 01 Source - "No SVG or PNG yet. Drop one on the stage or browse for it. SVG up to 5 MiB, PNG up to 24 MiB." 02 Pack details - "Name the app for the manifest once an image is open. The pack covers 16 to 512 pixels." The action button is Create favicon pack , and the result stage gives you a Download ZIP control. The ZIP, on disk The file written to disk is 13,279 bytes, and the panel calls it "13.0 KiB ZIP" with "9 files". There are indeed 9 entries in it: Entry Bytes android-chrome-192x192.png 1,593 bytes android-chrome-512x512.png 7,167 bytes apple-touch-icon.png 1,492 bytes favicon-16x16.png 141 bytes favicon-32x32.png 162 bytes favicon-48x48.png 267 bytes favicon.ico 624 bytes html-snippet.txt 381 bytes site.webmanifest 414 bytes One detail you only see on disk: every entry is stored, not compressed. All nine have a compressed size equal to their uncompressed size. The nine files add up to 12,241 bytes of content, and the archive is 13,279 bytes - 1,038 bytes more, because ZIP still writes a local header, a central directory record and an end-of-directory record for every entry. The six PNGs and their real pixel sizes Reading the IHDR header of each PNG inside the archive gives the dimensions the file names promise: File Pixels Bytes favicon-16x16.png 16 x 16 141 bytes favicon-32x32.png 32 x 32 162 bytes favicon-48x48.png 48 x 48 267 bytes apple-touch-icon.png 180 x 180 1,492 bytes android-chrome-192x192.png 192 x 192 1,593 bytes android-chrome-512x512.png 512 x 512 7,167 bytes favicon.ico is a real multi-image icon The ICO is 624 bytes and its header says type 1 with 3 images inside - it is not a single 16x16 renamed. The three images are the same three PNGs the pack also ships, byte for byte: ICO image Matching PNG Compared 16x16 favicon-16x16.png byte for byte identical 32x32 favicon-32x32.png byte for byte identical 48x48 favicon-48x48.png byte for byte identical The manifest names two of the six site.webmanifest is 414 bytes of JSON. It lists the Android sizes only: src sizes type /android-chrome-192x192.png 192x192 image/png /android-chrome-512x512.png 512x512 image/png It also carries "name": "App" , "short_name": "App" , "theme_color": "#ffffff" , "background_color": "#ffffff" and "display": "standalone" . The 16, 32, 48 and Apple touch icons are not in it - a manifest is for installable surfaces, and those four are not one. The app-name field really does reach the manifest This one is worth a run of its own. The name field is the second input on the stage, it ships with the value App , and the manifest comes out carrying "name": "App" . To check that the field is actually wired to the file and not just echoed back, I typed a different name into it, built the pack again, and read the second ZIP off the disk: the manifest then carried "name": "OnePassApps" and "short_name": "OnePassApps" , and the archive grew by exactly 16 bytes - the two longer name strings, once each. The snippet is the markup, verbatim html-snippet.txt is 381 bytes: 6 lines, 6 line feeds and 0 carriage returns - Unix endings, one tag per line, a newline after each. The tags are the same six the panel prints above its Copy snippet button: rel= "icon" href= "/favicon.ico" sizes= "any" > rel= "icon" type= "image/png" sizes= "16x16" href= "/favicon-16x16.png" > rel= "icon" type= "image/png" sizes= "32x32" href= "/favicon-32x32.png" > rel= "icon" type= "image/png" sizes= "48x48" href= "/favicon-48x48.png" > rel= "apple-touch-icon" href= "/apple-touch-icon.png" > rel= "manifest" href= "/site.webmanifest" > Nothing left the tab With the network trace on for the whole session - open the page, drop the image in, create the pack, download the ZIP, then do it all again - the browser made 172 requests, every one of them a GET, and not one carried a request body. The source image's file name appeared in no request URL. The only hosts reached were api.onepassapps.com, app.onepassapps.com, clerk.onepassapps.com, static.cloudflareinsights.com - our own site, our API host, the sign-in script host and the analytics beacon. Where it does not fit A browser tab is a good home for icon-sized art and a bad one for anything huge. The pack is still handed to you as a ZIP, so you unzip it and copy the files into your project - it replaces the resizing and the markup, not the deployment. And the size set is fixed at 16 to 512 pixels; a bespoke size still needs an image editor. What I did not test: one build, one browser, one signed-out session, 7 November 2026. I did not open the exported files in an image editor or in a browser, I did not test an SVG source, I did not check how the pack behaves in a project that already has its own icons, and I did not test an app name with a character that needs escaping in JSON. The app guides spell out what each browser tool does and where the boundary is. I work on OnePassApps, so this is our own build and our own output, measured today rather than described from memory.

  • Hacker News: Show HNLibraries, Frameworks, etc.read at source

    Show HN: Logo Deal – Logos anyone can edit with text

    Comments

  • Hacker News: Show HNLibraries, Frameworks, etc.read at source

    Show HN: An online shop where your friends can hurry the courier

    Comments

  • Hacker News Front PageThe Giantsread at source

    Every Windows 11 app should kill Electron, and Discord just showed why

  • Flavio CopesMore Front-end Bloggersread at source

    Build a private AI feature with a local model, Ollama and Node.js

    Build a Node.js feature on a local Ollama model: what's in the model file, schema output, validation, timeouts, a fallback, evals, tests and real speed.

  • Hacker News: Show HNLibraries, Frameworks, etc.read at source

    Show HN: Retex, a Faster LaTeX Engine

    Comments

  • Hacker News Front PageThe Giantsread at source

    Upload Your RAM

  • Hacker News Front PageThe Giantsread at source

    Every Mac comes with an (undocumented) family member classifier

  • Hacker News Front PageThe Giantsread at source

    Running a law firm on Free software

  • Hacker News Front PageThe Giantsread at source

    Port 0: The Port That Does Not Exist

  • Hacker News: Show HNLibraries, Frameworks, etc.read at source

    Show HN: I taught my Fujifilm X-T30 II to print on an instax Link 3 over BLE

    Comments

  • SitePointThe Giantsread at source

    Google Workspace vs. Microsoft 365 vs. Proton Workspace: Which Is Best for Developers and Freelancers?

    Google Workspace, Microsoft 365 and Proton Workspace compared on pricing, encryption and the tools developers and freelancers actually use. Continue reading Google Workspace vs. Microsoft 365 vs. Proton Workspace: Which Is Best for Developers and Freelancers? on SitePoint .

  • Flavio CopesMore Front-end Bloggersread at source

    Migrating a Ghost blog to EmDash on Cloudflare

    Export Ghost, scaffold EmDash, keep every URL, move posts, tags and images with a script, publish them with their original dates, and shut the VPS down.

  • Flavio CopesMore Front-end Bloggersread at source

    Migrating a WordPress blog to EmDash: the checklist

    The built-in WordPress importer, the exporter plugin, what moves and what doesn't, how statuses and dates come across, and a checklist to run the migration from export to cutover.

  • Hacker News: Show HNLibraries, Frameworks, etc.read at source

    Show HN: I built ivyNote, a local-first Markdown notes app

    Comments

Friday, 6 November 2026

  • AbduzeedoMulti Author Blogsread at source

    Bar RĂȘve: Belle Époque Visual Identity by Dutchscot

    London studio Dutchscot crafts a Belle Époque visual identity for Brooklyn cocktail bar Bar RĂȘve with wobbly script and a 222 monogram. Named after the French word for dream, Bar RĂȘve sits at 222 Smit...

  • AbduzeedoMulti Author Blogsread at source

    &Walsh Unveils BATCH Brand Identity

    &Walsh built the BATCH Brand Identity on a blocked matte wordmark, film grain and Pre is the new post, for film mattes in hours, not months. BATCH makes software that pulls mattes, the cutouts tha...

  • AbduzeedoMulti Author Blogsread at source

    NM Art & Design: Digital Platform UI/UX Design by ESH Gruppa

    ESH Gruppa pairs a stencil-cut monogram with calm cream-and-ochre UI/UX design for contemporary gallery platform NM Art & Design. NM Art & Design runs both a collectible contemporary gallery a...

  • Flavio CopesMore Front-end Bloggersread at source

    EmDash: Cloudflare's CMS for Astro, explained

    What EmDash is, how it's built, what the admin and the MCP server look like, where it runs and what it costs, and where it falls short of WordPress and Ghost.

  • CSS-TricksThe Giantsread at source

    RSS Club: CSS-Tricks Redesign Preview

    Hey you wonderful RSS subscribers! We just soft-pushed a site redesign and wanted to give ya'll the first heads-up. A full write-up is coming but here are the high-level details. RSS Club: CSS-Tricks Redesign Preview originally handwritten and published with love on CSS-Tricks . You should really get the newsletter as well.

  • AbduzeedoMulti Author Blogsread at source

    Gift Guide: The Outdoor Enthusiast Edit

    Trading pixel grids for backcountry ridges should never mean accepting hideous tactical gear. Here is our edit of trail objects that respect good design. Disclosure: Abduzeedo is reader-supported. Whe...

  • AbduzeedoMulti Author Blogsread at source

    Inside the Craft of Snooz Ice Cream Branding by HOW+HOW

    HOW+HOW built Snooz ice cream branding around two eclipsed moons, starry skies and flash-lit nights, for the only sweet that helps you sleep. Sixty percent of ice cream gets eaten after 6pm, and sugar...

  • Flavio CopesMore Front-end Bloggersread at source

    Tailscale vs Cloudflare Tunnel vs a cloud firewall

    I scanned one Droplet behind ufw, a cloud firewall, Tailscale and a Cloudflare Tunnel. What each one left open, what it costs, and how fast it was.

  • Dave RupertTop Front-end Bloggersread at source

    Pseudo-pseudo-classes for maximum interactive demos!

    Have you ever wanted to display all variants of a Button next to each other in your Storybook like your designer did in Figma? But then you come to a screeching halt when you realize :hover , :active , :focus don’t work without user-interaction? Blergh. Whatever can we do? I got nerd-sniped by this problem in the ShopTalk Discord when Nils said he was trying to build a simple state machine demo for his components. A handful of immediate options came to my head: Style states using .is-hover utility classes (à la SMACSS ) Create a system of [data-state="hover"] attributes and copy over styles Build some over-the-top Playwright-based screenshot contraption Each of those approaches has trade-offs for me. I don’t want to ship extra component styles or manage duplicate global styles. And if I’m building a Playwright contraption for a CSS demo
 yeesh, I should probably back away from the keyboard. When Nils mentioned he was using web components, I realized I might have an ace up my sleeve for a simple’ish solution to this problem. See the Pen Pseudo-pseudo-states Demo! by Dave Rupert ( @davatron5000 ) on CodePen . There’s a handful of Advanced Web Component Tips & Tactics happening in this demo like adoptedStyleSheets , ElementInternals , and custom state sets but the approach is pretty lightweight. I scan the custom element’s styles, clone the interactive bits, rewrite styles like :hover state to :state(hover) and add the new stylesheet inside the component. What I like about this approach compared to the other ideas I had is: Styles owned by the component - No “magic” global styles or utilities. Appended styles also owned by the component - No global conflicts. Interactive states use regular HTML/CSS - Avoid managing interactive state in JavaScript when possible. Write once, ship once - I don’t write or ship duplicate :hover and .is-hover styles to customers. Low maintenance - Beyond the initial setup, it’s automatic and I’m resilient to component style updates. Want an end-to-end walkthrough of how it all works? Please, follow me
 Adopting stylesheets in your web components In web components, we’d typically author our styles in a CSS tagged template literal: const MyButtonStyles = css` /* Change the host-element itself */ :host(:hover) { background: pink; scale: 1.1; } /* Update a child element based on the host-element interaction */ :host(:hover) button { background: pink; scale: 1.1; } /* Or put the interaction on the interactive child-element */ button:hover { background: pink; scale: 1.1; } ` Then we create a new “constructed stylesheet” and populate the stylesheet with the styles we wrote. That stylesheet then attaches to our component by adding it to the Shadow Root’s array of adoptedStyleSheets . /* Create a new constructed stylesheet */ const MyButtonStyleSheet = new CSSStyleSheet() MyButtonStyleSheet.replaceSync(MyButtonStyles) class MyButton extends HTMLElement { constructor() { /* Setup and create shadowroot */ super(); this.attachShadow({ mode: "open" }) /* Adopt the constructed stylesheet */ this.shadowRoot.adoptedStyleSheets = [MyButtonStyleSheet] /* ... etc */ } } The component will now render its styles without a tag or inline tag in the template. If we put two MyButton components on the page, they’ll share the same stylesheet in memory which is a pretty neat efficiency gain for style application. adoptedStyleSheets aren’t limited to web components, but they’re still pretty niche. But the web component ecosystem’s pattern of using adoptedStyleSheets for style application will come in handy later when we want to apply our new custom states. Attach internals to unlock custom states Before we jump to the grand finale, there’s one other unique-to-web-components feature we need to talk about first: ElementInternals . In a web component class you can call attachInternals() which allows you to dip into the element’s guts and mutate the innards of your spicy div . An example of this might be setting an ARIA role: class MyButton extends HTMLElement { constructor() { super(); this._internals = this.attachInternals() this._internals.role = "button" } } customElements.define('my-button', MyButton) The accessibility inspector will report a role of button for your custom element. Setting ARIA role is a weak example because both and Element.role = "button" both work pretty seamlessly. But ElementInternals brings other ✹superpowers✹ like adding form validation behaviors that you can’t do on a normal div . One newer capability of ElementInternals is that it allows you to create your own custom pseudo-classes and apply styles using :state() . class MyScrollingElement extends HTMLElement { constructor() { super(); this._internals = this.attachInternals() } connectedCallback() { this.addEventListener('scroll', this.handleScroll) this.addEventListener('scrollend', this.handleScrollEnd) } /* Use arrow functions to avoid `this` complications */ handleScroll = (_event) => { this._internals.states.add('scrolling') } handleScrollEnd = (_event) => { this._internals.states.delete('scrolling') } } Now you can style your element when it’s scrolling using :state(scrolling) and you’re manipulating the element’s internal state, not modifying HTML in the DOM. :host { overflow-y: scroll; transition: background 0.2s; } :host(:state(scrolling)) { background: pink; } Isn’t that cool? With great power comes great spider-mans, of course, but having your own custom pseudos is a thing you can do now with web components. Mimic native pseudo-states using custom states Now that we have a loose grasp on all the pieces we need to make our demo work, the next steps are: Find all our components and loop through them Copy their interactive rules to a new constructed stylesheet Replace :hover with :state(hover) Adopt the constructed stylesheet in the component Attach internals and imperatively set the state Here’s a simplified version of what’s happening in the CodePen version. /* A bit of pseudo-code here... */ const MyButtons = document.querySelectorAll('my-button'); const sheet = new CSSStyleSheet(); MyButtons.forEach(async (button, index) => { const isDefined = await customElements.whenDefined(button.localName); // Grab the stylesheet from the first if(isDefined && index === 0) { // Get current CSS rules const cssRules = Array.from( button.shadowRoot.adoptedStyleSheets[0].cssRules ) .filter(rule => rule.cssText.includes(':hover')) .map(rule => rule.cssText.replaceAll(':hover', ':state(hover)')); // Update the stylesheet contents sheet.replaceSync(cssRules.join('')); } // Adopt the stylesheet button.shadowRoot.adoptedStyleSheets = [...button.shadowRoot.adoptedStyleSheets, sheet] // Attach internals and set the state if(button.dataset.state == 'hover') { const _internals = button._internals ?? button.attachInternals(); _internals.states.add('hover'); } }) And that’s the gist of the trick: clone the pseudo-class states and set imposter custom states. I’m hand-waving some of the more complex bits like button:hover {} needs to be rewritten to :host(:state(hover)) button {} , but again, feel free to read the code. And my demo makes a couple assumptions I know won’t scale: All elements use the same static stylesheet (e.g. it doesn’t change per component). Each only has one adoptedStyleSheet. Hard-coded to HTMLButtonElement -specific interactions. Assumes you either don’t have ElementInternals already setup or already externally expose them and you know that @public property name. Next steps might be to make this a bit more robust and support all the other platform pseudo-classes like :popover-open , :user-invalid , or :playing . An agent could probably do this, not me though. I’m tired. Shadow DOM isn’t that bad, actually. Shadow DOM often gets a bad rap even in web component circles. I fully understand the friction there. Everyone’s first test drive of Shadow DOM styling almost always ends up in flames, leading to a poor unboxing experience that’s hard to shake off. That said, I feel like this demo captures a glimpse at some of the unique ✹superpowers✹ you get when styling with Shadow DOM. When coming from a global styles world, that loss of control can feel frustrating. But in my experience, with Shadow DOM encapsulation you’re gaining predictability and precise control by keeping everything local. You could probably figure out a way to do this with global styles. It’d be more difficult with roboclass-based scoping from something like CSS Modules, but I think it’s possible. But I imagine you’d have to parse through all the global document.styleSheets and that seems like a lot of work to add a :hover state demo to a . Style encapsulation here saves a lot of work. If you got all the way here and still hate Shadow DOM, I’d love to read a post of yours about where it broke down for you. But if you use Shadow DOM on the regular, I’d love to hear why it’s great. This has been my mantra for awhile now, but I think the web component community can do more about promoting the benefits. For example, there’s only two articles about adoptedStyleSheets on CSS-Tricks which seems low. In the meantime, while I try to convince my bosses to let me blog for a living, I’ve got my free course guidebook online and you can check it out.

  • AbduzeedoMulti Author Blogsread at source

    Industrial Design: Toki Desk Timer by Stanley Pulvirenti

    Stanley Pulvirenti pairs soft-edged industrial design with animated Pomodoro eye states for Toki, a tactile desk timer and mobile widget. Named after the Japanese word for time, Toki tackles a familia...

  • freeCodeCamp.orgYouTube ChannelsVideoWatch on YouTube

    Godot 4 Roguelike Course – Build a Complete Top-Down Action Game

  • HeyDesignerDeveloper/Designer Newsread at source

    On borrowing Apple's type scale

    How a designer/founder wins by their own rules (video), Taming typography: Building a modern CSS reset, Introducing Adaptive Dark Mode.

  • Frontend Masters BlogMore Front-end Bloggers5 min read

    true or false in CSS

    CSS doesn’t have a boolean type, but it does have the true and false keywords. Register a custom property that only accepts them, flip it with :has(), and container style queries turn messy :checked selector combinations into readable conditions.

  • Net NinjaYouTube ChannelsVideoWatch on YouTube

    GitHub Copilot App Tutorial #9 - Using the Rubber Duck Agent

  • Flavio CopesMore Front-end Bloggersread at source

    Tailscale tutorial: your Mac and a server on one private network

    Install Tailscale on a Mac and a VPS, SSH by name, serve a page only you can open, close every public port, and use the server as an exit node.

  • QuirksBlogTop Front-end Bloggersread at source

    The Valley bubble

    I expected something to happen around the AI hype after the US elections. I didn’t expect it to happen so soon . Too early to tell yaddah yaddah, and the hype could very well continue for another few months, but did any AI-related stock ever tumble before now? Silicon Valley exists in its own reality bubble. Hell, the entire US tech scene inhabits in a bubble whose main feature is relentless self-flagellation. Apparently they’re all Guilty and will all be Punished. (Then again, the US is fundamentally a Calvinist country.) Sanity prevails outside the US. Sometimes. For now I’ll stick to my idea : the Valley is creating something with the appeal of a 19th-century factory, complete with oppressed and estranged workers, without delivering the sole benefit of such factories: cheaper IT and other services. They want to get rid of the middle classes whose salaries they have to pay so badly that they forgot to create an appealing product. The Valley doesn’t understand how fed up normal people are with AI. This is the first time ever a technological advance is so cordially disliked, and the Valley is not equipped to detect that. It has no experience with being disliked by normal people. Also, so far many of the Valley’s consumer tech innovations were free . AI isn’t free, or even cheap, as we’ll see in the coming months, once credit dries up. The Valley’s position will be damaged permanently, and tech will lose its centre of gravity, its global Schwerpunkt . I don’t think there will be a replacement. I think we’re moving to a multi-hub world, and I think that’s a good development. Sure, inertia will keep the Valley important for decades to come, but it’s lost its natural dominance of the hearts and minds of the world. Permanently. None of this is a judgement on AI itself. I refuse to do anything about it until its true costs are known, after the hundreds of billions per year (month?) of free money from clueless investors dry up. All the while I acknowledge that there is something there that will change programming — just not as radically as the US-based doomsayers think. AI can copy, it cannot innovate.

  • Smashing MagazineThe Giants4 min read

    A New Community For In-House UX People

    We’ve been working on something new! We’ve teamed up for a **new small community** for people doing UX-inside organizations — with weekly calls and a shared library of the business cases, templates, and research plans that have actually worked for other folks. [Jump to details](https://uxinsiders.boagworld.com/).

  • PiccalilliMore Front-end Bloggersread at source

    The Index: Issue #205

    Poznote In our endless search for a Notion replacement, this one is showing real promise. Fixing illustrations on a dark background with CSS The drop-shadow property is so overlooked considering how useful it is. This is a great use of it! Death comes for XSLT It's so frustrating that browsers are ignoring the one main rule: don't break the web . How to quit Spotify and improve your life in the process This one weird trick to improve your life. (We recommend TIDAL or Qobuz) Introducing adaptive dark mode It's interesting to see this concept back — this time from Safari — considering how much of a dreadful job Google did half a decade ago in Chrome. We'll see how it goes! CSS nesting: use with caution Here's one from the Piccalilli archives that you might have missed to wrap up this issue. P.S. this is a good website . Sponsor message When anyone can make anything, we can make you stand out. AI technologies have given anyone the ability to bring their ideas to life, but to produce a high quality service that actually works for the people it needs to work for requires experience, specialist skills and creativity. We’ve got heaps of all of that here at Set Studio and we've got availability starting in 2027, so check us out and see what we're about. Check out our work

  • Chris HawkesYouTube ChannelsVideoWatch on YouTube

    I Made a Fake 1980s AI Commercial (Step by Step)

  • ESLint BlogLibraries, Frameworks, etc.2 min read

    ESLint now officially supports linting of shell scripts

    Back in July of 2024 we announced our plan to make ESLint a more general-purpose linter capable of linting any language. Since then, we’ve added official support for JSON, Markdown, and CSS. Today, we’re happy to add shell scripts to that list with the introduction of the


  • Hidde's blogMore Front-end Bloggers4 min read

    Reflections from TPAC 2026

    TPAC is W3C's yearly work week. A large part of the Working Groups and all sorts of committees get together to collaborate in person (mostly hybrid). This post has some things I got out of it this year. Some of my week had been slightly overbooked, as is the faith of most TPAC


  • Twilio BlogMulti Author Blogsread at source

    How to build an AI agent that remembers returning customers

    Build an AI agent that recognizes returning customers, recalls past conversations, and saves new facts, with Twilio Conversation Memory.

Thursday, 5 November 2026

  • AbduzeedoMulti Author Blogsread at source

    Inside Téléfilm Canada: Brand Identity by Caserne

    Montreal studio Caserne builds TĂ©lĂ©film Canada’s brand identity around a 30°–70° projector-beam grid, SN Skandia type, and die-cut print. Supporting Canadian and Indigenous cinema from early script de...

  • AdactioTop Front-end Bloggersread at source

    Thursday session

  • Mux BlogCompany/Startup Blogs11 min read

    How I built a video sharing site with Claude Code and Mux after years away from coding

    See how Eric built ScubaTube with Claude Code, using Video.js v10 for a dive data overlay and TV mode, Mux Robots for chapters, and Mux Data for engagement.

  • SitePointThe Giantsread at source

    A Practical Introduction to the Twitch Helix API

    null Continue reading A Practical Introduction to the Twitch Helix API on SitePoint .

  • AbduzeedoMulti Author Blogsread at source

    Editorial Design: GridType Vol. 1 by Slanted Publishers

    Slanted Publishers pairs open-spine editorial design with ten grid-type.com stencil typefaces in GridType Vol. 1: Type & Tools. On the uncoated cover of GridType Vol. 1: Type & Tools, stepped ...

  • SitePointThe Giantsread at source

    Best Static Website Hosting Providers in 2027: Free and Paid Options Compared

    null Continue reading Best Static Website Hosting Providers in 2027: Free and Paid Options Compared on SitePoint .

  • Flavio CopesMore Front-end Bloggersread at source

    GPT-Live 1 tutorial: build a voice agent you can interrupt

    How OpenAI's full-duplex voice model works, how to use it in ChatGPT, and how to build with gpt-live-1 in the API: delegation, web search, function tools, voices and real costs.

  • Microsoft Edge BlogBrowsers, engines, etc.3 min read

    Discover more of what matters, every time you open a new tab in Microsoft Edge

    A new browser tab is full of possibilities. It can be the start of something you set out to do—or a chance to discover something you didn't know. As the amount of content online has grown, Edge users have told us they want more help cutting through The post Discover more of what matters, every time you open a new tab in Microsoft Edge appeared first on Microsoft Edge Blog .

  • Adam ArgyleTop Front-end Bloggers2 min read

    Bye Deno, Hello Cloudflare

    and wassup Astro

  • Web Tools WeeklyMulti Author Blogsread at source

    Web Tools Weekly Issue #694

    React Tools, AI Tools, Text Editors

  • CSS-TricksThe Giantsread at source

    Taming Typography: Building a Modern CSS Reset

    Stu created tidy-typography.css: a style reset for taming typography with modern CSS features for everything from fluid typography and vertical rhythm, to hyphens and variable fonts. Taming Typography: Building a Modern CSS Reset originally handwritten and published with love on CSS-Tricks . You should really get the newsletter as well.

  • Level AccessCompany/Startup Blogs10 min read

    A Guide to PDF Accessibility

    PDF accessibility means people with disabilities can read, navigate, and use a PDF, including with a screen reader, keyboard, or magnification. Among other The post A Guide to PDF Accessibility appeared first on Level Access .

  • HeyDesignerDeveloper/Designer Newsread at source

    Fortune telling and speculative design

    Modern CSS might make your front-end more complex, Blossom carousel.

  • DigitalOcean BlogMulti Author Blogs4 min read

    Introducing DigitalOcean Cuttlefish: Get more done with agents on the go

    Your agent has been refactoring for two hours. It’s deep in the codebase, making real progress, and then it stops: “Can I run this migration?” Nothing happens until you answer. And you’re not at your desk. You’re in line at the grocery store, or waiting at school pickup.


  • Smashing MagazineThe Giants20 min read

    Making Immersive LLM Chat Experiences More Accessible

    As LLM chat patterns become more widespread, making them accessible is essential to ensuring everyone can access the information, tools, and opportunities they offer. Drawing on his work improving these interfaces, Eric Bailey shares practical ways to make chat-based experiences on the web more accessible and usable for people who rely on assistive technology.

  • SitePointThe Giantsread at source

    Top 6 AI YouTube Transcript Generators: Features, Accuracy, and Limits

    null Continue reading Top 6 AI YouTube Transcript Generators: Features, Accuracy, and Limits on SitePoint .

  • W3C BlogBrowsers, engines, etc.6 min read

    W3C Newsletter - October 2026

    W3C President and Interim CEO Dominique Hazaël-Massieux shares timely and notable highlights and information selected from the busy and prolific work last month at W3C such as TPAC 2026, AI @ W3C, progress on Identity and Credentials, further improvements to W3C infrastructure for standardization and more.

  • SitePointThe Giantsread at source

    7 E-commerce UX Tips for Selling Car Accessories and Home & Kitchen Products Online

    null Continue reading 7 E-commerce UX Tips for Selling Car Accessories and Home & Kitchen Products Online on SitePoint .

  • Flavio CopesMore Front-end Bloggersread at source

    Conductor alternatives after the Meta acquisition

    Conductor alternatives compared: Orca, Paradev, Paseo, T3 Code, Emdash, boxes.dev, Grok Bot and the agent apps from Anthropic, OpenAI and Cursor, with prices and how to move your workspaces.

See everything from the last 30 days →

Articles belong to their publishers. This site only collects what their feeds provide.

Updated 7 November 2026 at 13:50