From 57d3ca21154709a7baef4887b739e81f3601e7cb Mon Sep 17 00:00:00 2001 From: chodaict <5516582+chodaict@users.noreply.github.com> Date: Tue, 26 May 2026 18:19:32 +0900 Subject: [PATCH 01/75] =?UTF-8?q?feat!:=20v2.0.0=20=E2=80=94=20zero-config?= =?UTF-8?q?=20chrome=E6=9F=93=E8=89=B2=20with=20smart=20edge=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four-day surgical rewrite. v1's manual `trackScrollColors(stops, options)` is replaced by `bleed/auto` (zero-config) and `createBleedAuto()` (with options). The library now auto-detects body::before gradients, opaque section colors, and the page-end section, and染色 chrome to match the content at each viewport edge. BREAKING CHANGES: - `trackScrollColors`, `setupBleedMeta`, `setThemeColor`, `getSafeAreaInsets` removed. Replaced by `createBleedAuto()` and `import 'bleed/auto'`. - React, Vue, Svelte, UnoCSS framework wrappers removed. Zero-config import works from any framework now. NEW: - `bleed/auto` — side-effect import that wires everything up. - `bleed/utils` (rewritten) — exports `createBleedAuto()` + helpers (parseColor, parseGradient, sampleColorAt, findLastOpaqueSection, etc.). - Smart state machine per edge: STICKY_OWNED / SAFE_NATURAL / BLEED_OVERRIDE. - Detects `boundary.source` ('gradient' vs 'section') for smart decisions. - Page-end overscroll handling: overwrites , , AND body::before bg via injected + - -
- - -
-
-

- - iOS WebKit Clipping Bug -

-

When backdrop-filter is applied to a fixed element at top: 0, Safari clips the blur region right at the status bar boundary. Scroll down to see the scrollable cards below the status bar.

-
-
-

- - Try the Controls -

-

Switch between "Buggy Blur" and "Bleed Fix" to see how the status bar color transitions. In Buggy Blur mode, you will see a sharp line where the status bar refuses to blend, exposing the background.

-
-
-

- - Custom Colors -

-

Change themes to test solid red, emerald green, midnight blue, or neon gradients. The status bar inherits the header color seamlessly in Bleed mode.

-
-
-

- - Dynamic Safe Areas -

-

Use the sliders to simulate different notches, from iPhone 12 to newer iOS 26 Liquid Glass dynamic island heights. The layout automatically recalculates using CSS env values.

-
-
-

- - Bottom indicator Bleed -

-

Enable the bottom action mode. The bottom safe indicator gets cleanly dyed to avoid showing the page base layout under the indicator bar.

-
-
-

- - Zero Gaps -

-

Notice how our Bleed Top solution stretches all the way to the screen edges, maintaining a premium native iOS app feel.

-
-
+ +
+ iOS Safari needed to see chrome染色 in action — open this URL on an iPhone, or run from + npm install bleed in your project. +
+
+
+

bleed

+ v2.0.1 +

+ Zero-config iOS Safari chrome染色. The status bar and URL bar follow your page + content at each viewport edge — gradients, sections, rubber-band overscroll, all + automatic. +

+
$ npm install bleed
+> import 'bleed/auto';
+

↓ scroll to see染色 follow ↓

+
+ +
+

The four-day rabbit hole

+

+ iOS Safari染色 the chrome with whatever sits at the viewport edge — but the rules + are quirky, undocumented, and shift between versions. You ship a page with a + gradient hero, and: +

+
    +
  • + The viewport bottom染色 mint while your belt section is dark teal — visible + seam at the chrome boundary. +
  • +
  • + Compact tab bar appears; chrome染色 "shifts" 30px because + 100lvh − 100svh doesn't match the current chrome height. +
  • +
  • + User pulls past the footer — rubber-band overscroll exposes the html + background-color, which is the wrong color. +
  • +
  • + theme-color meta is ignored on iOS 26. +
  • +
  • + body::before with fixed gradient + stretches into the overscroll exposed area and overrides + whatever you set on <html>. +
  • +
+

+ bleed walks the rabbit hole for you. One import. Done. +

+
+ +
+
+

What it handles

+

Smart per-edge染色

+
+

Gradient territory

+

Probe the body::before gradient at the chrome boundary and染色 chrome to match.

- - -
- - - +
+

Mid-page sections

+

退場 — let Safari render its native translucent chrome over the section.

- - - - +
- + diff --git a/demo/simulator.css b/demo/simulator.css new file mode 100644 index 0000000..fa1dcc6 --- /dev/null +++ b/demo/simulator.css @@ -0,0 +1,549 @@ +:root { + /* Simulated safe areas inside the phone mock (iOS 26 Liquid Glass Dynamic Island is typically 59px) */ + --sim-safe-top: 59px; + --sim-safe-bottom: 34px; +} + +/* Phone Simulator Column */ +.simulator-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* Phone Frame */ +.phone-frame { + width: 375px; + height: 812px; + background: #000; + border-radius: 48px; + border: 12px solid #27272a; + box-shadow: 0 30px 100px rgba(0, 0, 0, 0.8), + inset 0 0 10px rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* iPhone Screen Inner */ +.phone-screen { + flex: 1; + width: 100%; + height: 100%; + /* Set per-theme via JS: --demo-grad-top / --demo-grad-bot / --demo-belt / --demo-footer */ + background-color: var(--demo-grad-top, #aceace); + position: relative; + overflow-y: scroll; + scrollbar-width: none; + transition: background-color 400ms ease; +} + +.phone-screen::-webkit-scrollbar { display: none; } + +/* The demo page inside the phone — gradient backdrop (mimics body::before) + + hero / belt / footer sections. Same shape as cver.net so the simulator + behaves exactly like the production page. */ +.demo-page { + position: relative; + min-height: 100%; + display: flex; + flex-direction: column; + color: #fff; + font-family: 'Nunito', system-ui, -apple-system, sans-serif; +} +.demo-page::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + var(--demo-grad-top, #aceace) 10%, + var(--demo-grad-bot, #0a8c8e) 70% + ); + z-index: 0; + pointer-events: none; +} + +.demo-hero { + position: relative; + z-index: 1; + min-height: 100%; + padding: 80px 24px 60px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} +.demo-logo { + width: 90px; + height: 90px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.92); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.2rem; + font-family: 'Comfortaa', sans-serif; + font-size: 2rem; + font-weight: 700; + color: var(--demo-belt, #084a4c); + letter-spacing: -0.05em; +} +.demo-title { + font-family: 'Comfortaa', sans-serif; + font-size: 1.35rem; + font-weight: 700; + color: #fff; + margin: 0 0 0.3rem; + line-height: 1.2; +} +.demo-tagline { + color: rgba(255, 255, 255, 0.92); + font-size: 0.9rem; + line-height: 1.5; + margin: 0; +} +.demo-scroll-hint { + margin-top: 1.6rem; + font-size: 0.72rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.65); +} + +.demo-belt { + position: relative; + z-index: 1; + background-color: var(--demo-belt, #084a4c); + padding: 40px 22px; + color: #fff; +} +.demo-belt-tag { + font-size: 0.62rem; + letter-spacing: 0.25em; + color: var(--demo-grad-top, #aceace); + margin: 0 0 0.8rem; + font-weight: 600; +} +.demo-belt-title { + font-family: 'Comfortaa', sans-serif; + font-size: 1.05rem; + font-weight: 700; + margin: 0 0 1rem; +} +.demo-card { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 12px 14px; + margin-bottom: 8px; +} +.demo-card h4 { + font-weight: 700; + font-size: 0.9rem; + margin: 0 0 4px; + color: #fff; +} +.demo-card p { + font-size: 0.78rem; + color: rgba(255, 255, 255, 0.82); + margin: 0; + line-height: 1.4; +} + +.demo-footer { + position: relative; + z-index: 1; + background-color: var(--demo-footer, #0a8c8e); + padding: 30px 22px 50px; + text-align: center; + color: rgba(255, 255, 255, 0.9); +} +.demo-footer-links { + display: flex; + justify-content: center; + gap: 0.9rem; + font-size: 0.78rem; + font-weight: 600; + margin-bottom: 0.8rem; +} +.demo-footer-copy { + font-size: 0.68rem; + color: rgba(255, 255, 255, 0.55); +} + +/* Phone Notch Simulator (Dynamic Island Style) */ +.phone-notch { + position: absolute; + top: 11px; + left: 50%; + transform: translateX(-50%); + width: 110px; + height: 30px; + background: #000; + border-radius: 9999px; + z-index: 9999; + display: flex; + justify-content: center; + align-items: center; + box-shadow: inset 0 0 4px rgba(255, 255, 255, 0.2); +} + +.phone-notch::before { + content: ''; + position: absolute; + left: 15px; + width: 10px; + height: 10px; + background: #111; + border-radius: 50%; + opacity: 0.8; + box-shadow: inset 0 0 2px rgba(255, 255, 255, 0.4); +} + +.phone-notch::after { + content: ''; + position: absolute; + right: 15px; + width: 12px; + height: 12px; + background: radial-gradient(circle, #0c2b45 10%, #000 70%); + border-radius: 50%; + opacity: 0.7; +} + +/* Status Bar Info */ +.status-bar-info { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: var(--sim-safe-top); + padding: 0 2rem; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.92); + z-index: 1000; + pointer-events: none; + /* Background dynamically染色 by JS — Safari chrome染色 in real life. */ + background-color: var(--chrome-top, transparent); + transition: background-color 400ms ease; +} + +/* Bottom Home Indicator */ +.home-indicator-area { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: var(--sim-safe-bottom); + z-index: 999; + display: flex; + justify-content: center; + align-items: flex-end; + padding-bottom: 8px; + pointer-events: none; + /* The home-indicator area is "below" the Safari bottom bar;染色 it too + so the rubber-band-overscroll equivalent is visually consistent. */ + background-color: var(--chrome-bot, transparent); + transition: background-color 400ms ease; +} + +.home-indicator { + width: 120px; + height: 5px; + background: white; + border-radius: 99px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +/* Simulated Headers/Footers */ +.sim-header { + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 900; + color: white; + font-size: 0.85rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.sim-footer { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + z-index: 900; + color: white; + font-size: 0.85rem; + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* HEADER MODES */ +/* Mode: Buggy backdrop-filter */ +.header-buggy { + position: absolute; + top: 0; + left: 0; + width: 100%; + /* WebKit Bug: Padding is correct, but backdrop-filter clips at safe area boundary! */ + padding-top: var(--sim-safe-top); + -webkit-backdrop-filter: blur(12px) saturate(140%) !important; + backdrop-filter: blur(12px) saturate(140%) !important; + background: rgba(239, 68, 68, 0.5); /* translucent red */ + clip-path: inset(var(--sim-safe-top) 0 0 0); /* Simulating the WebKit Safe Area clipping glitch! */ +} + +/* Mode: Bleed Fixed */ +.header-bleed { + position: absolute; + top: 0; + left: 0; + width: 100%; + padding-top: calc(8px + var(--sim-safe-top)); + background: var(--active-header-bg, var(--active-bg)); + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; /* Disables outer blur to paint safe area */ +} + +/* Mode: None */ +.header-none { + display: none; +} + +/* FOOTER MODES */ +/* Mode: Buggy backdrop-filter */ +.footer-buggy { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding-bottom: var(--sim-safe-bottom); + -webkit-backdrop-filter: blur(12px) saturate(140%) !important; + backdrop-filter: blur(12px) saturate(140%) !important; + background: rgba(239, 68, 68, 0.5); + clip-path: inset(0 0 var(--sim-safe-bottom) 0); /* Simulating the WebKit Safe Area clipping glitch! */ +} + +/* Mode: Bleed Fixed */ +.footer-bleed { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding-bottom: calc(8px + var(--sim-safe-bottom)); + background: var(--active-footer-bg, var(--active-bg)); + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} + +/* Mode: None */ +.footer-none { + display: none; +} + +/* Outer/Inner Blur Layout for Bleed Demo */ +.bleed-inner-blur-demo { + -webkit-backdrop-filter: blur(10px) saturate(140%); + backdrop-filter: blur(10px) saturate(140%); + background: rgba(255, 255, 255, 0.1); + border-radius: 8px; + margin: 8px; + padding: 8px; + font-size: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Header/Footer Internal padding */ +.nav-content { + padding: 0.75rem 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; +} + +/* Dummy Screen Content */ +.dummy-content { + padding: calc(80px + var(--sim-safe-top)) 1.5rem calc(80px + var(--sim-safe-bottom)); + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.dummy-card { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 16px; + padding: 1.25rem; +} + +.dummy-card h4 { + font-size: 0.95rem; + margin-bottom: 0.5rem; + color: white; + display: flex; + align-items: center; + gap: 0.4rem; +} + +.dummy-card h4 svg { + color: var(--accent); +} + +.dummy-card p { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.6); + line-height: 1.5; +} + +/* Simulated Safari Shell Elements */ +.sim-safari-top-capsule { + position: absolute; + top: 67px; /* Sits below the notch/status bar */ + left: 16px; + right: 16px; + height: 44px; + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(25px) saturate(180%); + -webkit-backdrop-filter: blur(25px) saturate(180%); + border-radius: 99px; + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + z-index: 1001; + display: flex; + align-items: center; + padding: 0 14px; + box-sizing: border-box; +} + +.safari-capsule-address-box { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + color: white; +} + +.safari-icon-page-menu { + display: flex; + align-items: center; + justify-content: center; + opacity: 0.9; +} + +.safari-icon-page-menu svg { + display: block; +} + +.safari-icon-reload { + display: flex; + align-items: center; + justify-content: center; + opacity: 0.8; +} + +.safari-icon-reload svg { + display: block; +} + +.safari-url { + font-family: -apple-system, system-ui, sans-serif; + font-size: 0.9rem; + font-weight: 500; + letter-spacing: -0.01em; +} + +/* Unified Bottom Liquid Bar (iOS 26 Liquid Glass) */ +.sim-safari-bottom-liquid-bar { + position: absolute; + bottom: calc(var(--sim-safe-bottom) + 8px); + left: 16px; + right: 16px; + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(25px) saturate(180%); + -webkit-backdrop-filter: blur(25px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + z-index: 1001; + display: flex; + flex-direction: column; + box-sizing: border-box; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: auto; +} + +/* Compact Mode Bottom Bar */ +.sim-safari-bottom-liquid-bar.mode-compact { + height: 48px; + border-radius: 99px; + padding: 0 14px; + justify-content: center; +} + +/* Bottom Mode Bottom Bar (Double Row) */ +.sim-safari-bottom-liquid-bar.mode-bottom { + height: 98px; + border-radius: 24px; + padding: 10px 14px; + justify-content: space-between; +} + +/* Top Mode Bottom Bar (Buttons Only) */ +.sim-safari-bottom-liquid-bar.mode-top { + height: 48px; + border-radius: 99px; + padding: 0 14px; + justify-content: center; +} + +/* Sub rows inside Bottom Liquid Bar */ +.safari-liquid-address-row { + width: 100%; +} + +.mode-bottom .safari-liquid-address-row .safari-capsule-address-box { + background: rgba(0, 0, 0, 0.25); + border-radius: 99px; + padding: 0 12px; + height: 36px; + box-sizing: border-box; +} + +.safari-liquid-buttons-row { + width: 100%; + height: 36px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 8px; + box-sizing: border-box; +} + +.safari-btn { + display: flex; + align-items: center; + justify-content: center; + color: rgba(255, 255, 255, 0.9); + opacity: 0.9; + cursor: default; + transition: opacity 0.1s ease; +} + +/* Grey out the forward button inside mockup for realism (like in screenshots) */ +.safari-liquid-buttons-row .safari-btn:nth-child(2) { + color: rgba(255, 255, 255, 0.35); +} + +.safari-btn svg { + display: block; +} From 30047181f24d8553214cd30cc616e17f45aa4792 Mon Sep 17 00:00:00 2001 From: chodaict <5516582+chodaict@users.noreply.github.com> Date: Tue, 26 May 2026 19:33:56 +0900 Subject: [PATCH 06/75] fix(demo): translate UI to English + inline utils so deployed demo works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues with the deployed demo at oss.cver.net/bleed/: 1. Buttons did nothing because the script imported from ../src/utils.mjs, but gh-pages only serves /demo — the src/ folder is not deployed. The import failed silently and the script never registered event listeners. Fix: inline the few helpers we actually use (parseColor, parseColorWithAlpha, colorToRgb, colorToHex, colorsClose) directly in the demo script. Source of truth still lives in src/utils.mjs. 2. UI text was mixed Chinese / English. Translated everything visible to English (state machine labels, scroll hints, theme picker copy). Code comments inside the simulator script are also English now. Co-Authored-By: Claude Opus 4.7 --- demo/index.html | 97 ++++++++++++++++++++++++++++++++++------------ demo/simulator.css | 4 +- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/demo/index.html b/demo/index.html index 4dccd0f..c924e74 100644 --- a/demo/index.html +++ b/demo/index.html @@ -18,7 +18,7 @@

bleed

- v2.0.1 — Smart染色 by CVER.NET + v2.0.1 — Smart tinting by CVER.NET
@@ -36,13 +36,13 @@

bleed

-

bleed染色 mode

+

bleed tinting mode

- Toggle bleed染色 on the simulator chrome (status bar + Safari capsule). + Toggle bleed on the simulator chrome (status bar + Safari capsule).

- +
@@ -82,7 +82,7 @@

Page Theme

- Pick a brand palette — bleed will染色 chrome to match every viewport edge. + Pick a brand palette — bleed will tint chrome to match every viewport edge.

@@ -118,7 +118,7 @@

- +
9:41
@@ -164,7 +164,7 @@

- +