diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e62d46..572331e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## [1.3.1] - 2026-05-14 + +### โš ๏ธ Migration from 1.3.0 + +`hyper_render_clipboard` and `hyper_render_math` are no longer transitive dependencies of `hyper_render`. If you use either, add them explicitly: + +```yaml +dependencies: + hyper_render: ^1.3.1 + hyper_render_clipboard: ^1.3.1 # only if you use SuperClipboardHandler + hyper_render_math: ^1.3.1 # only if you use MathNodePlugin / LatexNodePlugin +``` + +### โœจ New CSS Properties + +- **`list-style-type`**: All 11 marker types โ€” `disc`, `circle`, `square`, `decimal`, `decimal-leading-zero`, `lower-alpha`, `upper-alpha`, `lower-latin`, `upper-latin`, `lower-roman`, `upper-roman`, `none` +- **`list-style-position`**: `inside` / `outside` +- **`list-style` shorthand**: parses type and position in any order +- **`background-repeat`**: `repeat`, `repeat-x`, `repeat-y`, `no-repeat`, `space`, `round` +- **`background-position`**: keyword (`center`, `top left`, etc.) and percentage values + +### ๐Ÿš€ Performance + +- **Selection rects cached**: `getSelectionRects()` now called once per drag event (was 3ร—) โ€” stored in `_selectionRects` field, eliminating redundant layout walks during selection drag +- **Auto-scroll proportional speed**: `_autoScrollIfNearEdge` scales 0โ€“20 px/frame based on finger proximity to edge (was fixed 15 px/frame) +- **`HyperTeardropHandlePainter` deduplicated**: renamed, made public, and exported from `hyper_render_core`; duplicate implementation in the virtualized overlay removed + +### ๐Ÿ› Bug Fixes + +- **Edge-to-edge images**: `width: 100%` images now truly fill their container โ€” no internal margin offset + +### ๐Ÿ—๏ธ Build Fixes + +- **Decoupled native dependencies**: `hyper_render_clipboard` and `hyper_render_math` removed from root `hyper_render` default dependencies โ€” eliminates the `compileSdk = 34` Gradle requirement for basic usage +- **Removed outdated `compileSdk` workaround** from example app's Android Gradle config + + ## [1.3.0] - 2026-05-03 ### โœจ New Features diff --git a/README.md b/README.md index a2318c6..1691386 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # HyperRender -### Fast, robust HTML rendering for Flutter. +### The only Flutter HTML renderer with CSS float layout. [![pub.dev](https://img.shields.io/pub/v/hyper_render.svg?label=pub.dev&color=0175C2)](https://pub.dev/packages/hyper_render) [![pub points](https://img.shields.io/pub/points/hyper_render?label=pub%20points&color=00b4ab)](https://pub.dev/packages/hyper_render/score) @@ -13,7 +13,7 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Flutter](https://img.shields.io/badge/Flutter-3.10+-54C5F8.svg?logo=flutter)](https://flutter.dev) -**CSS float ยท crash-free selection ยท CJK/Furigana ยท `@keyframes` ยท 80%+ Test Coverage ยท XSS-safe** +**CSS float ยท crash-free selection ยท CJK/Furigana ยท `@keyframes` ยท 1 646 tests ยท XSS-safe ยท Zero Gradle config** [**Quick Start**](#-quick-start) ยท [**Why Switch?**](#๏ธ-why-switch-the-architecture-argument) ยท [**API**](#-api-reference) ยท [**Packages**](#-packages) @@ -26,7 +26,7 @@ | CSS Float Layout | Ruby / Furigana | Crash-Free Selection | |:---:|:---:|:---:| | ![CSS Float Demo](https://raw.githubusercontent.com/brewkits/hyper_render/main/assets/float_demo.gif) | ![Ruby Demo](https://raw.githubusercontent.com/brewkits/hyper_render/main/assets/ruby_demo.gif) | ![Selection Demo](https://raw.githubusercontent.com/brewkits/hyper_render/main/assets/selection_demo.gif) | -| Text wraps around floated images โ€” no other Flutter HTML renderer does this | Furigana centered above base glyphs, full Kinsoku line-breaking | Select across headings, paragraphs, tables โ€” tested to 100 000 chars | +| Text wraps around floated images โ€” **no other Flutter HTML renderer does this** | Furigana centered above base glyphs, full Kinsoku line-breaking | Select across headings, paragraphs, tables โ€” tested to 100 000 chars | | Advanced Tables | Head-to-Head | Virtualized Mode | |:---:|:---:|:---:| @@ -39,7 +39,7 @@ ```yaml dependencies: - hyper_render: ^1.3.0 + hyper_render: ^1.3.1 ``` ```dart @@ -51,22 +51,7 @@ HyperViewer( ) ``` -Zero configuration. XSS sanitization is **on by default**. - -> **Android note:** `hyper_render` depends on `super_clipboard` which transitively pulls in `irondash_engine_context`. That library was compiled against Android SDK 31, but its `androidx.fragment:1.7.1` dependency requires `compileSdk โ‰ฅ 34`. Add this one-time workaround to your `android/build.gradle.kts`: -> -> ```kotlin -> // android/build.gradle.kts (root โ€” not app/build.gradle.kts) -> subprojects { -> afterEvaluate { -> extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply { -> compileSdk = 35 -> } -> } -> } -> ``` -> -> This overrides `compileSdk` for all library sub-projects so AGP's `checkAarMetadata` passes. Tracked in [#5](https://github.com/brewkits/hyper_render/issues/5). +Zero configuration. XSS sanitization is **on by default**. No Gradle setup required. --- @@ -74,10 +59,10 @@ Zero configuration. XSS sanitization is **on by default**. Most Flutter HTML libraries map each HTML tag to a Flutter widget. A 3 000-word article becomes **500+ nested widgets** โ€” and some layout primitives simply cannot be expressed that way: -> **CSS `float` is not possible in a widget tree.** -> Wrapping text around a floated image requires every fragment's coordinates before adjacent text can be composed. That geometry only exists when a single `RenderObject` owns the entire layout. +> **CSS `float` is architecturally impossible in a widget tree.** +> Wrapping text around a floated image requires every fragment's coordinates before adjacent text can be composed. That geometry only exists when a single `RenderObject` owns the entire layout pass. -HyperRender renders the whole document inside **one custom `RenderObject`**. Float, crash-free selection, and sub-millisecond binary-search hit-testing all follow from that single design decision. +HyperRender renders the whole document inside **one custom `RenderObject`**. CSS float, crash-free selection, O(log N) binary-search hit-testing, and `@keyframes` animations all follow directly from that single architectural decision. ### Feature Matrix @@ -85,13 +70,18 @@ HyperRender renders the whole document inside **one custom `RenderObject`**. Flo |---|:---:|:---:|:---:| | `float: left / right` | โŒ | โŒ | โœ… | | Text selection โ€” large docs | โŒ Crashes | โŒ Crashes | โœ… Crash-free | -| Ruby / Furigana | โŒ Raw text | โŒ Raw text | โœ… | -| `
` / `` | โŒ | โŒ | โœ… Interactive | +| Ruby / Furigana + Kinsoku | โŒ Raw text | โŒ Raw text | โœ… | +| RTL / BiDi (Arabic, Hebrew) | โš ๏ธ | โš ๏ธ | โœ… | | CSS Variables `var()` | โŒ | โŒ | โœ… | -| CSS `@keyframes` | โŒ | โŒ | โœ… | +| CSS `@keyframes` animation | โŒ | โŒ | โœ… | | Flexbox / Grid | โš ๏ธ Partial | โš ๏ธ Partial | โœ… Full | -| Box shadow ยท `filter` | โŒ | โŒ | โœ… | -| SVG `` | โš ๏ธ | โš ๏ธ | โœ… | +| `box-shadow` ยท `filter` | โŒ | โŒ | โœ… | +| `list-style-type` (all 11 values) | โš ๏ธ disc only | โš ๏ธ disc only | โœ… | +| `
` / `` | โŒ | โŒ | โœ… Interactive | +| Quill Delta input | โŒ | โŒ | โœ… | +| Markdown input | โŒ | โŒ | โœ… GFM | +| Modular packages | โŒ monolith | โŒ monolith | โœ… opt-in add-ons | +| Zero Gradle config | โœ… | โœ… | โœ… | ### Benchmarks @@ -368,10 +358,11 @@ HTML / Markdown / Quill Delta Kinsoku ยท O(log N) binary-search selection ``` -- **Single RenderObject** โ€” float layout and crash-free selection require one shared coordinate system +- **Single RenderObject** โ€” float layout and crash-free selection require one shared coordinate system; a widget tree cannot provide this - **O(1) CSS rule lookup** โ€” rules indexed by tag / class / ID; constant time regardless of stylesheet size -- **O(log N) hit-testing** โ€” `_lineStartOffsets[]` precomputed at layout time; each touch is a binary search -- **RepaintBoundary per chunk** โ€” unmodified chunks are composited, not repainted +- **O(log N) hit-testing** โ€” `_lineStartOffsets[]` precomputed at layout time; each touch is a binary search, not a linear scan +- **RepaintBoundary per chunk** โ€” unmodified chunks are composited, not repainted; incremental layout caches unchanged sections by content hash +- **1 646 passing tests** โ€” unit, widget, integration, fuzz (43 cases), and golden pixel tests across 3 OS platforms --- @@ -403,13 +394,65 @@ HTML / Markdown / Quill Delta | Package | pub.dev | Description | |---------|---------|-------------| -| [`hyper_render`](https://pub.dev/packages/hyper_render) | [![pub](https://img.shields.io/pub/v/hyper_render.svg)](https://pub.dev/packages/hyper_render) | Convenience wrapper โ€” one dependency, everything included | -| [`hyper_render_core`](https://pub.dev/packages/hyper_render_core) | [![pub](https://img.shields.io/pub/v/hyper_render_core.svg)](https://pub.dev/packages/hyper_render_core) | Core engine โ€” UDT model, CSS resolver, RenderObject | +| [`hyper_render`](https://pub.dev/packages/hyper_render) | [![pub](https://img.shields.io/pub/v/hyper_render.svg)](https://pub.dev/packages/hyper_render) | Convenience wrapper โ€” HTML, Markdown, Delta, syntax highlight | +| [`hyper_render_core`](https://pub.dev/packages/hyper_render_core) | [![pub](https://img.shields.io/pub/v/hyper_render_core.svg)](https://pub.dev/packages/hyper_render_core) | Core engine โ€” UDT model, CSS resolver, RenderObject; zero native deps | | [`hyper_render_html`](https://pub.dev/packages/hyper_render_html) | [![pub](https://img.shields.io/pub/v/hyper_render_html.svg)](https://pub.dev/packages/hyper_render_html) | HTML + CSS parser | | [`hyper_render_markdown`](https://pub.dev/packages/hyper_render_markdown) | [![pub](https://img.shields.io/pub/v/hyper_render_markdown.svg)](https://pub.dev/packages/hyper_render_markdown) | Markdown adapter (GFM) | | [`hyper_render_highlight`](https://pub.dev/packages/hyper_render_highlight) | [![pub](https://img.shields.io/pub/v/hyper_render_highlight.svg)](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `` / `
` blocks |
-| [`hyper_render_clipboard`](https://pub.dev/packages/hyper_render_clipboard) | [![pub](https://img.shields.io/pub/v/hyper_render_clipboard.svg)](https://pub.dev/packages/hyper_render_clipboard) | Image copy / share |
-| [`hyper_render_devtools`](https://pub.dev/packages/hyper_render_devtools) | [![pub](https://img.shields.io/pub/v/hyper_render_devtools.svg)](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools extension โ€” UDT inspector, computed styles |
+| [`hyper_render_devtools`](https://pub.dev/packages/hyper_render_devtools) | [![pub](https://img.shields.io/pub/v/hyper_render_devtools.svg)](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools extension โ€” UDT inspector, computed styles, float visualizer |
+
+### Optional add-ons
+
+These packages bring native dependencies and are **not bundled** by default. Install only what you need.
+
+| Package | pub.dev | Description |
+|---------|---------|-------------|
+| [`hyper_render_clipboard`](https://pub.dev/packages/hyper_render_clipboard) | [![pub](https://img.shields.io/pub/v/hyper_render_clipboard.svg)](https://pub.dev/packages/hyper_render_clipboard) | Native image copy / share via `super_clipboard` |
+| [`hyper_render_math`](https://pub.dev/packages/hyper_render_math) | [![pub](https://img.shields.io/pub/v/hyper_render_math.svg)](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML via `flutter_math_fork` |
+
+#### `hyper_render_clipboard` โ€” Native image copy / share
+
+```yaml
+dependencies:
+  hyper_render_clipboard: ^1.3.1
+```
+
+```dart
+import 'package:hyper_render_clipboard/hyper_render_clipboard.dart';
+
+HyperViewer(
+  html: html,
+  imageClipboardHandler: SuperClipboardHandler(),
+)
+```
+
+> **Android setup required:** `super_clipboard` transitively pulls in `irondash_engine_context`, which requires `compileSdk โ‰ฅ 34`. Add this to `android/build.gradle.kts` (root file, not `app/`):
+>
+> ```kotlin
+> subprojects {
+>     afterEvaluate {
+>         extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
+>             compileSdk = 35
+>         }
+>     }
+> }
+> ```
+>
+> Tracked in [#5](https://github.com/brewkits/hyper_render/issues/5).
+
+#### `hyper_render_math` โ€” LaTeX / MathML rendering
+
+```yaml
+dependencies:
+  hyper_render_math: ^1.3.1
+```
+
+```dart
+import 'package:hyper_render_math/hyper_render_math.dart';
+
+final registry = HyperPluginRegistry()..register(const MathPlugin());
+HyperViewer(html: html, pluginRegistry: registry)
+```
 
 ---
 
diff --git a/doc/MIGRATION_GUIDE.md b/doc/MIGRATION_GUIDE.md
index 7a902fb..e1bec01 100644
--- a/doc/MIGRATION_GUIDE.md
+++ b/doc/MIGRATION_GUIDE.md
@@ -1,17 +1,41 @@
 # Migration Guide
 
-> **Current version: v1.2.0** โ€” All v1.x releases are additive and backward-compatible. No breaking API changes.
+> **Current version: v1.3.1**
 
-## Current Version: 1.2.0
+## Upgrading to 1.3.1
 
-**No migration needed!** If you're starting fresh with HyperRender v1.2.0:
+### โš ๏ธ Breaking change โ€” clipboard and math are now opt-in
+
+`hyper_render_clipboard` and `hyper_render_math` are no longer transitive dependencies of the root `hyper_render` package. If you use either, add them explicitly:
+
+```yaml
+dependencies:
+  hyper_render: ^1.3.1
+  hyper_render_clipboard: ^1.3.1   # only if you use SuperClipboardHandler
+  hyper_render_math: ^1.3.1        # only if you use MathNodePlugin / LatexNodePlugin
+```
+
+If you don't use either feature, **no changes are needed** โ€” just bump the version and your Android build will no longer require a `compileSdk = 35` workaround.
+
+### New in 1.3.1
+
+- `list-style-type`, `list-style-position`, `list-style` shorthand CSS support
+- `background-repeat`, `background-position` CSS support
+- Edge-to-edge images: `width: 100%` now truly fills the container
+- Selection drag performance improved (rects cached, auto-scroll proportional)
+
+---
+
+## Starting fresh with 1.3.1
+
+**No migration needed!** If you're starting fresh:
 
 ```yaml
 dependencies:
-  hyper_render: ^1.2.0
-  # or use individual packages:
-  hyper_render_core: ^1.2.0
-  hyper_render_clipboard: ^1.2.0
+  hyper_render: ^1.3.1
+  # opt-in extras:
+  hyper_render_clipboard: ^1.3.1   # image copy/save/share
+  hyper_render_math: ^1.3.1        # LaTeX/MathML
 ```
 
 ```dart
@@ -156,7 +180,7 @@ These APIs are stable and will remain backward-compatible in v2.0:
 
 ## Getting Help
 
-For the current v1.3.0 release:
+For the current v1.3.1 release:
 - See [README](../README.md) for usage
 - Check [CHANGELOG](../CHANGELOG.md) for version history
 - Review [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) for extending
@@ -164,20 +188,4 @@ For the current v1.3.0 release:
 
 ---
 
-*Last Updated: April 29, 2026 for v1.3.0*
-ard support
-- Cross-platform (iOS, Android, Web, Desktop)
-
----
-
-## Getting Help
-
-For the current v1.2.0 release:
-- See [README](../README.md) for usage
-- Check [CHANGELOG](../CHANGELOG.md) for version history
-- Review [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) for extending
-- File issues at [GitHub Issues](https://github.com/your-repo/issues)
-
----
-
-*Last Updated: March 30, 2026 for v1.2.0*
+*Last Updated: May 14, 2026 for v1.3.1*
diff --git a/doc/ROADMAP.md b/doc/ROADMAP.md
index 1b15966..d5140d7 100644
--- a/doc/ROADMAP.md
+++ b/doc/ROADMAP.md
@@ -13,7 +13,7 @@ For detailed CSS property tracking, see [`internal/CSS_SUPPORT_ROADMAP.md`](inte
 
 - Single `RenderObject` pipeline (Parse โ†’ Style โ†’ Layout โ†’ Paint)
 - Float layout algorithm (`float: left/right`, `clear`) โ€” unique advantage over FWFH
-- Isolate-based HTML parsing (non-blocking UI thread)
+- Async microtask-based HTML parsing (non-blocking UI thread; uses `Future.microtask` instead of a real isolate so `FakeAsync` works in widget tests)
 - `ListView.builder` virtualization (low RAM on large documents)
 - Full Flexbox support (90% coverage: direction, wrap, gap, align, grow/shrink/basis)
 - CSS Variables `var()`, `transition`, `animation-*` parsing
@@ -79,12 +79,12 @@ jarring for large images. A workaround is to increase `chunkSize` so fewer split
 occur near floats.
 
 Scope:
-- [ ] Add `FloatCarryover` data class to `render_hyper_box_types.dart`
-- [ ] Add `danglingFloats` getter to `RenderHyperBox`
-- [ ] Add `initialFloats` parameter to `RenderHyperBox` / `HyperRenderWidget`
-- [ ] Seed initial floats in `_performLineLayout`
-- [ ] Wire `FloatCarryover` callbacks through `VirtualizedChunk` โ†’ `HyperViewer`
-- [ ] Add offset rendering support in `_paintFloatImages` for image floats
+- [x] Add `FloatCarryover` data class to `render_hyper_box_types.dart`
+- [x] Add `danglingFloats` getter to `RenderHyperBox`
+- [x] Add `initialFloats` parameter to `RenderHyperBox` / `HyperRenderWidget`
+- [x] Seed initial floats in `_performLineLayout`
+- [x] Wire `FloatCarryover` callbacks through `VirtualizedChunk` โ†’ `HyperViewer`
+- [ ] Add offset rendering support in `_paintFloatImages` for image floats (imagePixelOffset not yet read by painter)
 - [ ] Integration test: tall float at chunk boundary shows no wasted space
 
 ---
@@ -98,22 +98,8 @@ Scope:
 **Source**: Expert review recommendation
 **Priority**: High โ€” directly impacts stability on low-end devices (2 GB RAM)
 
-The image cache is currently tuned manually. `WidgetsBindingObserver` should be
-integrated so HyperRender automatically evicts caches when the OS signals memory pressure.
-
-```dart
-class HyperRenderController with WidgetsBindingObserver {
-  @override
-  void didHaveMemoryPressure() {
-    imageCache.evictAll();        // Flutter image cache
-    _internalSpanCache.clear();   // HyperRender internal span cache
-    super.didHaveMemoryPressure();
-  }
-}
-```
-
 Scope:
-- [ ] Implement `WidgetsBindingObserver` in `HyperRenderController`
+- [x] Implement `WidgetsBindingObserver` in `HyperViewer` โ€” `didHaveMemoryPressure` clears TextPainter cache, `LazyImageQueue.clearPending()`, and `PaintingBinding.imageCache.clear()`
 - [ ] Expose `onMemoryPressure` callback for host-app customization
 - [ ] Debug-mode metrics: eviction count, bytes freed
 - [ ] Smoke test on a 2 GB RAM device
@@ -122,12 +108,14 @@ Scope:
 
 Properties deferred from Phase 3 in [`internal/CSS_SUPPORT_ROADMAP.md`](internal/CSS_SUPPORT_ROADMAP.md):
 
-- [ ] `text-shadow` โ€” high visual impact, 1-day effort
-- [ ] `text-overflow: ellipsis` โ€” extremely common, 4-hour effort
-- [ ] `box-shadow` โ€” design system compatibility
-- [ ] `list-style-type`, `list-style-position` โ€” better `
    ` / `
      ` rendering -- [ ] `word-break`, `overflow-wrap` โ€” CJK and long-URL handling -- [ ] `background-repeat`, `background-position`, `background-size` +- [x] `text-shadow` โ€” parsed + applied to `TextStyle.shadows` in `ComputedStyle` +- [x] `text-overflow: ellipsis` โ€” parsed + executed in `render_hyper_box_fragments.dart` +- [x] `box-shadow` โ€” parsed + applied in `render_hyper_box_paint.dart` +- [x] `word-break`, `overflow-wrap` โ€” parsed + executed in `render_hyper_box_layout.dart` (L1339โ€“1375) +- [ ] `list-style-type`, `list-style-position` โ€” not yet in resolver or painter +- [x] `background-repeat` โ€” parsed + mapped to `ImageRepeat` in `paintImage()` +- [x] `background-position` โ€” parsed + mapped to `Alignment` in `paintImage()` (keyword values: top/center/bottom/left/right and combinations) +- [x] `background-size` โ€” parsed and applied --- diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts index 61ec193..8c589a6 100644 --- a/example/android/build.gradle.kts +++ b/example/android/build.gradle.kts @@ -19,25 +19,6 @@ subprojects { project.evaluationDependsOn(":app") } -// Workaround: irondash_engine_context 0.5.5 was compiled against android-31 but -// transitively requires androidx.fragment:1.7.1 which has minCompileSdk=34. -// AGP 8 checkAarMetadata blocks the build. Override compileSdk for all library -// subprojects so the check passes. -// Tracked: https://github.com/brewkits/hyper_render/issues/5 -subprojects { - val project = this - if (project.state.executed) { - project.extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply { - compileSdk = 35 - } - } else { - project.afterEvaluate { - project.extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply { - compileSdk = 35 - } - } - } -} tasks.register("clean") { delete(rootProject.layout.buildDirectory) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6008ca7..3d06689 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,11 +1,7 @@ PODS: - audio_session (0.0.1): - Flutter - - device_info_plus (0.0.1): - - Flutter - Flutter (1.0.0) - - irondash_engine_context (0.0.1): - - Flutter - just_audio (0.0.1): - Flutter - FlutterMacOS @@ -16,8 +12,6 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS - - super_native_extensions (0.0.1): - - Flutter - url_launcher_ios (0.0.1): - Flutter - video_player_avfoundation (0.0.1): @@ -31,14 +25,11 @@ PODS: DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) - - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - just_audio (from `.symlinks/plugins/just_audio/darwin`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) @@ -47,12 +38,8 @@ DEPENDENCIES: EXTERNAL SOURCES: audio_session: :path: ".symlinks/plugins/audio_session/ios" - device_info_plus: - :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter - irondash_engine_context: - :path: ".symlinks/plugins/irondash_engine_context/ios" just_audio: :path: ".symlinks/plugins/just_audio/darwin" package_info_plus: @@ -61,8 +48,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" sqflite: :path: ".symlinks/plugins/sqflite/darwin" - super_native_extensions: - :path: ".symlinks/plugins/super_native_extensions/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: @@ -74,14 +59,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 - device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 - super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 diff --git a/example/lib/aesthetic_demo.dart b/example/lib/aesthetic_demo.dart index 283f0da..d253924 100644 --- a/example/lib/aesthetic_demo.dart +++ b/example/lib/aesthetic_demo.dart @@ -79,6 +79,7 @@ class AestheticDemo extends StatelessWidget { _buildFeature('โœจ CSS filters and backdrop-filter (blur)'), _buildFeature( 'โœจ NEW: word-break and background-size (cover/contain)'), + _buildFeature('โœจ NEW: list-style and background (repeat/position)'), _buildFeature('โœจ NEW: Advanced borders (dashed, dotted, double)'), _buildFeature( 'โœจ NEW: Professional truncation (text-overflow: ellipsis)'), diff --git a/example/lib/cjk_languages_demo.dart b/example/lib/cjk_languages_demo.dart index b1b8073..209e151 100644 --- a/example/lib/cjk_languages_demo.dart +++ b/example/lib/cjk_languages_demo.dart @@ -72,62 +72,64 @@ class _SimplifiedChineseTab extends StatelessWidget { const _SimplifiedChineseTab(); static const _html = ''' -
      +
      -

      Flutter ่ทจๅนณๅฐๅผ€ๅ‘ๆŒ‡ๅ—

      -

      + padding: 24px 20px; border-radius: 12px; margin-bottom: 28px; box-shadow: 0 4px 12px rgba(21,101,192,0.15);"> +

      Flutter ่ทจๅนณๅฐๅผ€ๅ‘ๆŒ‡ๅ—

      +

      ๆŠ€ๆœฏๆทฑๅบฆ่งฃๆž ยท 2024ๅนด็‰ˆ

      -

      ไธ€ใ€ๆ ธๅฟƒไผ˜ๅŠฟ

      -

      +

      ไธ€ใ€ๆ ธๅฟƒไผ˜ๅŠฟ

      +

      Flutter ๆ˜ฏ็”ฑ Google ๅผ€ๅ‘็š„ๅผ€ๆบ UI ๆก†ๆžถ๏ผŒๅฏไปฅไปŽๅ•ไธ€ไปฃ็ ๅบ“ๆž„ๅปบ้€‚็”จไบŽ ็งปๅŠจ็ซฏใ€Web ๅ’ŒๆกŒ้ข็š„็ฒพ็พŽๅŽŸ็”Ÿ็ผ–่ฏ‘ๅบ”็”จใ€‚ไธŽไผ ็ปŸ็š„่ทจๅนณๅฐๆ–นๆกˆไธๅŒ๏ผŒFlutter ไฝฟ็”จ่‡ชๅทฑ็š„ ๆธฒๆŸ“ๅผ•ๆ“Ž Skia / Impeller๏ผŒๅฎŒๅ…จไธไพ่ต–ๅนณๅฐๅŽŸ็”ŸๆŽงไปถใ€‚

      -
      -

      +

      +

      "ไธ€ๆฌก็ผ–ๅ†™๏ผŒๅˆฐๅค„่ฟ่กŒโ€”โ€”่ฟ™ไธๅ†ๅชๆ˜ฏๅฃๅท๏ผŒFlutter ่ฎฉๅฎƒๆˆไธบ็Žฐๅฎžใ€‚"

      -

      โ€” Google ๅผ€ๅ‘่€…ๅคงไผš๏ผŒ2023

      +

      โ€” Google ๅผ€ๅ‘่€…ๅคงไผš๏ผŒ2023

      -

      ไบŒใ€ไธป่ฆ็‰นๆ€งๅฏนๆฏ”

      - - - - - - - - - - - - - - - - - - - - - - - - - -
      ๆก†ๆžถ็ƒญ้‡่ฝฝๆธฒๆŸ“ๅผ•ๆ“Žๆ€ง่ƒฝ
      Flutterโœ… ไบš็ง’็บง่‡ช็ ” Impellerโญโญโญโญโญ
      React Nativeโœ… Fast RefreshๅนณๅฐๅŽŸ็”Ÿโญโญโญโญ
      Xamarinโš ๏ธ ๆœ‰้™ๅนณๅฐๅŽŸ็”Ÿโญโญโญ
      - -

      ไธ‰ใ€Dart ่ฏญ่จ€็คบไพ‹

      +

      ไบŒใ€ไธป่ฆ็‰นๆ€งๅฏนๆฏ”

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + +
      ๆก†ๆžถ็ƒญ้‡่ฝฝๆธฒๆŸ“ๅผ•ๆ“Žๆ€ง่ƒฝ
      Flutterโœ… ไบš็ง’็บง่‡ช็ ” Impellerโญโญโญโญโญ
      React Nativeโœ… Fast RefreshๅนณๅฐๅŽŸ็”Ÿโญโญโญโญ
      Xamarinโš ๏ธ ๆœ‰้™ๅนณๅฐๅŽŸ็”Ÿโญโญโญ
      +
      + +

      ไธ‰ใ€Dart ่ฏญ่จ€็คบไพ‹

      // ๅผ‚ๆญฅ็ผ–็จ‹็คบไพ‹
      +              font-size: 14px; overflow: auto; line-height: 1.6; margin-bottom: 24px;">// ๅผ‚ๆญฅ็ผ–็จ‹็คบไพ‹
       Future<void> ๅŠ ่ฝฝ็”จๆˆทๆ•ฐๆฎ() async {
         try {
           final ๆ•ฐๆฎ = await ็ฝ‘็ปœ่ฏทๆฑ‚('/api/็”จๆˆท');
      @@ -135,10 +137,10 @@ Future<void> ๅŠ ่ฝฝ็”จๆˆทๆ•ฐๆฎ() async {
         } catch (้”™่ฏฏ) {
           print('ๅŠ ่ฝฝๅคฑ่ดฅ: \$้”™่ฏฏ');
         }
      -}
      +}
-

ๅ››ใ€็”Ÿๆ€็ณป็ปŸ

-
    +

    ๅ››ใ€็”Ÿๆ€็ณป็ปŸ

    +
    • pub.dev โ€” ่ถ…่ฟ‡ 30,000 ไธชๅผ€ๆบ package
    • Flutter DevTools โ€” ๅผบๅคง็š„ๆ€ง่ƒฝๅˆ†ๆžๅ’Œ่ฐƒ่ฏ•ๅทฅๅ…ท
    • Firebase โ€” ๅฎŒๆ•ด็š„ๅŽ็ซฏๆœๅŠก้›†ๆˆ
    • @@ -146,9 +148,9 @@ Future<void> ๅŠ ่ฝฝ็”จๆˆทๆ•ฐๆฎ() async {
    • Widget ๆต‹่ฏ• โ€” ๅฎŒๆ•ด็š„ UI ่‡ชๅŠจๅŒ–ๆต‹่ฏ•ๆ”ฏๆŒ
    -
    -

    +

    +

    ๐Ÿ’ก ๅฐ่ดดๅฃซ๏ผšๅœจไธญๆ–‡ๆŽ’็‰ˆไธญ๏ผŒHyperRender ไผš่‡ชๅŠจๅค„็†ๆฑ‰ๅญ—้—ด่ทใ€ ๆ ‡็‚น็ฆๅˆ™่ง„ๅˆ™๏ผˆ็ฆๆญขๅœจ่กŒ้ฆ–ๅ‡บ็Žฐๅฅๅทใ€้€—ๅท็ญ‰ๆ ‡็‚น๏ผ‰๏ผŒไปฅๅŠไธญ่‹ฑๆ–‡ๆททๆŽ’ๆ—ถ็š„้—ด่ทไผ˜ๅŒ–ใ€‚

    @@ -159,10 +161,14 @@ Future<void> ๅŠ ่ฝฝ็”จๆˆทๆ•ฐๆฎ() async { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: HyperViewer(html: _html, selectable: true), + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: HyperViewer(html: _html, selectable: true), + ), ), ); } @@ -176,86 +182,88 @@ class _TraditionalChineseTab extends StatelessWidget { const _TraditionalChineseTab(); static const _html = ''' -
    +
    -

    ๅคๅ…ธๆ–‡ๅญธ้ธ็ฒน

    -

    + padding: 24px 20px; border-radius: 12px; margin-bottom: 28px; box-shadow: 0 4px 12px rgba(136,14,79,0.15);"> +

    ๅคๅ…ธๆ–‡ๅญธ้ธ็ฒน

    +

    ๅ”่ฉฉ ยท ๅฎ‹่ฉž ยท ๅคๆ–‡ๅ็ฏ‡

    -

    +

    ใ€ˆ้œๅคœๆ€ใ€‰โ€” ๆŽ็™ฝ

    -
    -

    ๅบŠๅ‰ๆ˜Žๆœˆๅ…‰๏ผŒ
    ็–‘ๆ˜ฏๅœฐไธŠ้œœใ€‚
    ่ˆ‰้ ญๆœ›ๆ˜Žๆœˆ๏ผŒ
    ไฝŽ้ ญๆ€ๆ•…้„‰ใ€‚

    -

    +

    โ€”โ€” ๅ” ยท ๆŽ็™ฝ๏ผˆ701โ€”762ๅนด๏ผ‰

    -

    +

    ใ€ˆๆฐด่ชฟๆญŒ้ ญใ€‰โ€” ่˜‡่ปพ

    -
    -

    +

    ๆ˜Žๆœˆๅนพๆ™‚ๆœ‰๏ผŸๆŠŠ้…’ๅ•้’ๅคฉใ€‚
    ไธ็ŸฅๅคฉไธŠๅฎฎ้—•๏ผŒไปŠๅค•ๆ˜ฏไฝ•ๅนดใ€‚
    ๆˆ‘ๆฌฒไน˜้ขจๆญธๅŽป๏ผŒๅˆๆ็“Šๆจ“็މๅฎ‡๏ผŒ
    ้ซ˜่™•ไธๅ‹ๅฏ’ใ€‚
    ่ตท่ˆžๅผ„ๆธ…ๅฝฑ๏ผŒไฝ•ไผผๅœจไบบ้–“ใ€‚

    -

    +

    ่ฝ‰ๆœฑ้–ฃ๏ผŒไฝŽ็ถบๆˆถ๏ผŒ็…ง็„ก็œ ใ€‚
    ไธๆ‡‰ๆœ‰ๆจ๏ผŒไฝ•ไบ‹้•ทๅ‘ๅˆฅๆ™‚ๅœ“๏ผŸ
    ไบบๆœ‰ๆ‚ฒๆญก้›ขๅˆ๏ผŒๆœˆๆœ‰้™ฐๆ™ดๅœ“็ผบ๏ผŒ
    ๆญคไบ‹ๅค้›ฃๅ…จใ€‚
    ไฝ†้ก˜ไบบ้•ทไน…๏ผŒๅƒ้‡Œๅ…ฑๅฌ‹ๅจŸใ€‚

    -

    +

    โ€”โ€” ๅฎ‹ ยท ่˜‡่ปพ๏ผˆ1037โ€”1101ๅนด๏ผ‰

    -

    +

    ๅคๆ–‡็ถ“ๅ…ธๆณจ้‡‹

    - - - - - - - - - - - - - - - - - - - - - -
    ่ฉž่ชžๆณจ้Ÿณ้‡‹็พฉ
    ๅฌ‹ๅจŸchรกn juฤn็พŽๅฅฝใ€ๆŒ‡ๆœˆไบฎ๏ผ›ไบฆๅ–ปๆ€ๅฟตไน‹ไบบ
    ็“Šๆจ“็މๅฎ‡qiรณng lรณu yรน yว”ๅฝขๅฎนไป™ๅขƒไธญ็พŽ้บ—็š„ๅฎฎๆฎฟๆจ“้–ฃ
    ็ถบๆˆถqว hรน้›•ๅˆป็ฒพ็พŽ็š„้–€็ช—๏ผ›ๆณ›ๆŒ‡่ฏ้บ—็š„ๅฑ…ๆ‰€
    - -
    -

    +

    + + + + + + + + + + + + + + + + + + + + + +
    ่ฉž่ชžๆณจ้Ÿณ้‡‹็พฉ
    ๅฌ‹ๅจŸchรกn juฤn็พŽๅฅฝใ€ๆŒ‡ๆœˆไบฎ๏ผ›ไบฆๅ–ปๆ€ๅฟตไน‹ไบบ
    ็“Šๆจ“็މๅฎ‡qiรณng lรณu yรน yว”ๅฝขๅฎนไป™ๅขƒไธญ็พŽ้บ—็š„ๅฎฎๆฎฟๆจ“้–ฃ
    ็ถบๆˆถqว hรน้›•ๅˆป็ฒพ็พŽ็š„้–€็ช—๏ผ›ๆณ›ๆŒ‡่ฏ้บ—็š„ๅฑ…ๆ‰€
    +
    + +
    +

    ๐Ÿ“– ๆŽ’็‰ˆ่ชชๆ˜Ž๏ผšHyperRender ๆ”ฏๆด็น้ซ”ไธญๆ–‡็š„ๆญฃ็ขบๆธฒๆŸ“๏ผŒๅŒ…ๅซ ๆจ™้ปž็ฌฆ่™Ÿ็ฆๅ‰‡๏ผˆ้ฟ้ ญๅฐพ่ฆๅ‰‡๏ผ‰ใ€ๅ…จๅฝขๆจ™้ปž่ˆ‡ๅŠๅฝข่‹ฑๆ•ธๆททๆŽ’้–“่ท๏ผŒไปฅๅŠ ้ซ˜ๅฏ†ๅบฆๆผขๅญ—ๆ–‡ๆœฌ็š„็ฒพ็ขบๆ›่กŒ่จˆ็ฎ—ใ€‚ @@ -267,10 +275,14 @@ class _TraditionalChineseTab extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: HyperViewer(html: _html, selectable: true), + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: HyperViewer(html: _html, selectable: true), + ), ), ); } @@ -284,93 +296,95 @@ class _KoreanTab extends StatelessWidget { const _KoreanTab(); static const _html = ''' -

    +
    -

    ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ

    -

    + padding: 24px 20px; border-radius: 12px; margin-bottom: 28px; box-shadow: 0 4px 12px rgba(0,77,64,0.15);"> +

    ๋ชจ๋ฐ”์ผ ์•ฑ ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ

    +

    2024๋…„ ๊ธฐ์ˆ  ๋™ํ–ฅ ๋ถ„์„ ๋ฆฌํฌํŠธ

    -

    ๐Ÿ“ฑ ํฌ๋กœ์Šคํ”Œ๋žซํผ์˜ ๋ถ€์ƒ

    -

    +

    ๐Ÿ“ฑ ํฌ๋กœ์Šคํ”Œ๋žซํผ์˜ ๋ถ€์ƒ

    +

    ์ตœ๊ทผ ๋ช‡ ๋…„๊ฐ„ ํฌ๋กœ์Šคํ”Œ๋žซํผ ๊ฐœ๋ฐœ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๊ธ‰๊ฒฉํžˆ ์„ฑ์žฅํ–ˆ์Šต๋‹ˆ๋‹ค. Flutter, React Native, Kotlin Multiplatform ๋“ฑ์˜ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ iOS์™€ Android๋ฅผ ๋™์‹œ์— ์ง€์›ํ•˜๋ฉด์„œ ๊ฐœ๋ฐœ ๋น„์šฉ์„ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    -
    -

    +

    +

    "Flutter๋Š” ๋‹จ์ˆœํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์•„๋‹ˆ๋ผ, ์ƒˆ๋กœ์šด ๋ฐฉ์‹์˜ UI ํŒจ๋Ÿฌ๋‹ค์ž„์ž…๋‹ˆ๋‹ค. Dart ์–ธ์–ด์˜ ์„ฑ๋Šฅ๊ณผ Hot Reload์˜ ํŽธ์˜์„ฑ์ด ๊ฒฐํ•ฉ๋˜์–ด ์ƒ์‚ฐ์„ฑ์„ ๊ทน๋Œ€ํ™”ํ•ฉ๋‹ˆ๋‹ค."

    -

    โ€” Google I/O 2024

    +

    โ€” Google I/O 2024

    -

    ๐Ÿ“Š ์ฃผ์š” ํ”„๋ ˆ์ž„์›Œํฌ ๋น„๊ต

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    ํ”„๋ ˆ์ž„์›Œํฌ์–ธ์–ด์„ฑ๋Šฅํ•™์Šต ๋‚œ์ด๋„
    FlutterDart๋งค์šฐ ์šฐ์ˆ˜์ค‘๊ฐ„
    React NativeJavaScript์šฐ์ˆ˜๋‚ฎ์Œ
    KMPKotlin๋„ค์ดํ‹ฐ๋ธŒ ์ˆ˜์ค€๋†’์Œ
    - -

    ๐Ÿ”‘ ํ•ต์‹ฌ ์šฉ์–ด ์„ค๋ช…

    -
    -
    -
    +

    ๐Ÿ“Š ์ฃผ์š” ํ”„๋ ˆ์ž„์›Œํฌ ๋น„๊ต

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ํ”„๋ ˆ์ž„์›Œํฌ์–ธ์–ด์„ฑ๋Šฅํ•™์Šต ๋‚œ์ด๋„
    FlutterDart๋งค์šฐ ์šฐ์ˆ˜์ค‘๊ฐ„
    React NativeJavaScript์šฐ์ˆ˜๋‚ฎ์Œ
    KMPKotlin๋„ค์ดํ‹ฐ๋ธŒ ์ˆ˜์ค€๋†’์Œ
    +
    + +

    ๐Ÿ”‘ ํ•ต์‹ฌ ์šฉ์–ด ์„ค๋ช…

    +
    +
    +
    ์œ„์ ฏ ํŠธ๋ฆฌ (Widget Tree)
    -
    +
    Flutter์˜ UI ๊ตฌ์„ฑ ์š”์†Œ๋“ค์ด ๊ณ„์ธต์ ์œผ๋กœ ๋ฐฐ์น˜๋œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ฒƒ์ด ์œ„์ ฏ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋ฉฐ, ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๋กœ ํ™”๋ฉด์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    -
    -
    +
    +
    ์ƒํƒœ ๊ด€๋ฆฌ (State Management)
    -
    +
    ์•ฑ์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ธฐ์ˆ ๋กœ, Riverpod, Bloc, Provider ๋“ฑ ๋‹ค์–‘ํ•œ ์†”๋ฃจ์…˜์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๊ทœ๋ชจ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฐฉ์‹์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    -
    -
    +
    +
    ํ•ซ ๋ฆฌ๋กœ๋“œ (Hot Reload)
    -
    +
    ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์•ฑ์„ ์žฌ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ๋ฐ˜์˜ํ•˜๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ํš๊ธฐ์ ์œผ๋กœ ํ–ฅ์ƒ์‹œ์ผœ์ค๋‹ˆ๋‹ค.
    -

    โœ… ๊ฐœ๋ฐœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

    -
      +

      โœ… ๊ฐœ๋ฐœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

      +
      • ์•ฑ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐฉ์‹ ๊ฒฐ์ •
      • ๋””์ž์ธ ์‹œ์Šคํ…œ ๋ฐ ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ตฌ์ถ•
      • CI/CD ํŒŒ์ดํ”„๋ผ์ธ ์„ค์ • (GitHub Actions, Codemagic)
      • @@ -379,9 +393,9 @@ class _KoreanTab extends StatelessWidget {
      • ์•ฑ์Šคํ† ์–ด ์ œ์ถœ ๋ฐ ์‹ฌ์‚ฌ ๋Œ€์‘ ์ „๋žต
      -
      -

      +

      +

      ๐Ÿ’ก ๋ Œ๋”๋ง ์ฐธ๊ณ : HyperRender๋Š” ํ•œ๊ธ€ ์ž๋ชจ ๋ถ„๋ฆฌ ์—†์ด ์Œ์ ˆ ๋‹จ์œ„๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ค„ ๋ฐ”๊ฟˆ์„ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์˜๋ฌธยท์ˆซ์žยทํ•œ๊ธ€ ํ˜ผ์šฉ ๋ฌธ์„œ์—์„œ๋„ ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ฐ„๊ฒฉ๊ณผ ์ค„ ๋‚˜๋ˆ”์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. @@ -393,10 +407,14 @@ class _KoreanTab extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: HyperViewer(html: _html, selectable: true), + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: HyperViewer(html: _html, selectable: true), + ), ), ); } diff --git a/example/lib/css_properties_demo.dart b/example/lib/css_properties_demo.dart index 4f5bedf..0389906 100644 --- a/example/lib/css_properties_demo.dart +++ b/example/lib/css_properties_demo.dart @@ -24,6 +24,35 @@ class CssPropertiesDemo extends StatelessWidget { // NEW PROPERTIES _buildSection('๐ŸŽ‰ NEW CSS Properties'), + _buildPropertyCard( + title: 'list-style-type & position', + description: 'Advanced list markers (roman, alpha) and positioning', + html: ''' +

        +
      • First Roman
      • +
      • Second Roman
      • +
      +
        +
      1. First Alpha
      2. +
      3. Second Alpha
      4. +
      +
        +
      • No markers here
      • +
      + ''', + ), + _buildPropertyCard( + title: 'background-repeat & position', + description: 'Advanced background image rendering', + html: ''' +
      +
      repeat-x + center
      +
      +
      +
      no-repeat + right bottom
      +
      + ''', + ), _buildPropertyCard( title: 'text-shadow', description: 'Drop shadow on text (multiple shadows supported)', diff --git a/example/lib/email_demo.dart b/example/lib/email_demo.dart index c609c14..6967ee8 100644 --- a/example/lib/email_demo.dart +++ b/example/lib/email_demo.dart @@ -144,7 +144,7 @@ class _EmailTab extends StatelessWidget { // Email body โ€” scrollable, padded like a real email client viewport Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), child: HyperViewer( html: html, selectable: true, diff --git a/example/lib/main.dart b/example/lib/main.dart index b130f82..b342d25 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -36,6 +36,7 @@ import 'paged_mode_demo.dart'; import 'plugin_api_demo.dart'; import 'reader_app/library_screen.dart'; import 'float_hell_demo.dart'; +import 'zero_padding_image_demo.dart'; /// Optimized base TextStyle for better readability /// - fontSize: 16 (comfortable reading size) @@ -69,7 +70,7 @@ class HyperRenderDemoApp extends StatelessWidget { title: 'HyperRender Demo', debugShowCheckedModeBanner: false, theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A56DB)), useMaterial3: true, ), scrollBehavior: const MaterialScrollBehavior().copyWith( @@ -96,9 +97,9 @@ class DemoHomePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('HyperRender Demo'), + title: const Text('HyperRender'), centerTitle: false, - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: const Color(0xFF1A56DB), foregroundColor: Colors.white, elevation: 0, ), @@ -110,42 +111,40 @@ class DemoHomePage extends StatelessWidget { const SizedBox(height: 16), _buildWhyCard(context), const SizedBox(height: 8), - // โ”€โ”€ The Ultimate Showcase โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - _buildSectionHeader(context, 'The Ultimate Showcase'), + // โ”€โ”€ Signature Features โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + _buildSectionHeader(context, 'Signature Features'), _buildDemoCard( context, icon: Icons.auto_awesome, - title: 'Ultra Showcase 2026', + title: 'Flagship Demo', subtitle: - 'Float + CJK Typography, Giant Div Virtualization, and Interactive Plugins', - color: Colors.redAccent, + 'Float layout ยท CJK ruby ยท virtualized rendering ยท plugin injection โ€” all at once', + color: const Color(0xFF1A56DB), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const UltraShowcase2026())), ), - // โ”€โ”€ Highlights โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - _buildSectionHeader(context, 'Highlights'), _buildDemoCard( context, icon: Icons.view_quilt, - title: 'Float Layout', + title: 'CSS Float Layout', subtitle: - 'Text wraps around floated images โ€” the feature no other Flutter HTML library has', - color: DemoColors.primary, + 'Text wraps around floated images โ€” impossible in any widget-tree-based renderer', + color: const Color(0xFF1A56DB), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const FloatLayoutDemo())), ), _buildDemoCard( context, - icon: Icons.email, + icon: Icons.email_outlined, title: 'HTML Email', subtitle: - 'Render real HTML emails natively โ€” no WebView, no heavy dependencies', - color: DemoColors.primary, + 'Real HTML emails rendered natively โ€” no WebView, <1 MB overhead vs ~20 MB', + color: const Color(0xFF1A56DB), onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const EmailDemo())), ), // โ”€โ”€ Applications โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - _buildSectionHeader(context, 'Applications & Solutions'), + _buildSectionHeader(context, 'Real-World Applications'), _buildDemoCard( context, icon: Icons.auto_stories, @@ -264,6 +263,16 @@ class DemoHomePage extends StatelessWidget { onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const _MediaHubPage())), ), + _buildDemoCard( + context, + icon: Icons.fullscreen, + title: 'Zero Padding Images', + subtitle: + 'Edge-to-edge images with no padding relative to the device edges', + color: DemoColors.warning, + onTap: () => Navigator.push(context, + MaterialPageRoute(builder: (_) => const ZeroPaddingImageDemo())), + ), _buildDemoCard( context, icon: Icons.widgets, @@ -298,24 +307,24 @@ class DemoHomePage extends StatelessWidget { onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const FormulaDemo())), ), - // โ”€โ”€ Advanced & Quality โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - _buildSectionHeader(context, 'Advanced & Quality'), + // โ”€โ”€ Engineering & Quality โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + _buildSectionHeader(context, 'Engineering & Quality'), _buildDemoCard( context, - icon: Icons.whatshot, - title: 'Sprint 1: Float Hell Stress Test', + icon: Icons.speed, + title: 'Float Layout Stress Test', subtitle: - '2000 blocks, randomized left/right floats, width animation, virtualization test.', - color: Colors.deepPurple, + '2 000 blocks, randomised left/right floats, live width animation โ€” 60 FPS', + color: DemoColors.success, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const FloatHellDemo())), ), _buildDemoCard( context, - icon: Icons.compare, + icon: Icons.compare_arrows_rounded, title: 'Comparison & Performance', subtitle: - 'Side-by-side vs other libraries, unique features, stress test, render pipeline', + 'Side-by-side vs other libraries, render pipeline deep-dive, benchmark numbers', color: DemoColors.success, onTap: () => Navigator.push( context, @@ -325,30 +334,30 @@ class DemoHomePage extends StatelessWidget { _buildDemoCard( context, icon: Icons.dark_mode, - title: 'Dark Mode, Skeletons & Visual Quality', + title: 'Dark Mode & Visual Quality', subtitle: - 'Theme switching, skeleton loading, error boundaries, crisp rendering', + 'Theme switching, skeleton loading, error boundaries, crisp retina rendering', color: DemoColors.success, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const V21Showcase())), ), _buildDemoCard( context, - icon: Icons.security, + icon: Icons.shield_outlined, title: 'Security & Accessibility', subtitle: - 'XSS protection, screen reader support, WebView fallback detection', + 'XSS sanitization, WCAG 2.1 AA screen reader support, WebView fallback', color: DemoColors.success, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const _QualityHubPage())), ), _buildDemoCard( context, - icon: Icons.business_center, + icon: Icons.business_center_outlined, title: 'Enterprise Features', subtitle: - 'GPU resource safety, error routing, device tuning, deeplink security, zoom modes', - color: const Color(0xFF1A237E), + 'GPU safety, error routing, memory pressure handling, deeplink security, zoom', + color: DemoColors.success, onTap: () => Navigator.push( context, MaterialPageRoute( @@ -362,15 +371,15 @@ class DemoHomePage extends StatelessWidget { Widget _buildWhyCard(BuildContext context) { return Container( decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.amber.shade700, Colors.orange.shade600], + gradient: const LinearGradient( + colors: [Color(0xFF0D3B8E), Color(0xFF1A56DB)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( - color: Colors.orange.withValues(alpha: 0.35), + color: const Color(0xFF1A56DB).withValues(alpha: 0.35), blurRadius: 16, offset: const Offset(0, 4), ), @@ -384,11 +393,19 @@ class DemoHomePage extends StatelessWidget { MaterialPageRoute(builder: (_) => const WhyHyperRenderDemo())), borderRadius: BorderRadius.circular(14), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), child: Row( children: [ - const Icon(Icons.emoji_events, - color: Color(0xFF4E2600), size: 32), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon(Icons.compare_arrows_rounded, + color: Colors.white, size: 22), + ), const SizedBox(width: 14), const Expanded( child: Column( @@ -397,17 +414,17 @@ class DemoHomePage extends StatelessWidget { Text( 'Why HyperRender?', style: TextStyle( - color: Color(0xFF4E2600), - fontSize: 16, + color: Colors.white, + fontSize: 15, fontWeight: FontWeight.w800, letterSpacing: -0.2, ), ), - SizedBox(height: 2), + SizedBox(height: 3), Text( - 'Live demos ยท Feature matrix ยท 16/16 score vs other libraries', + 'Live feature demos ยท side-by-side comparison ยท 16 vs 3 exclusive features', style: TextStyle( - color: Color(0xFF7A3E00), + color: Color(0xFFB8CEFC), fontSize: 12, height: 1.4), ), @@ -417,15 +434,17 @@ class DemoHomePage extends StatelessWidget { const SizedBox(width: 8), Container( padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + const EdgeInsets.symmetric(horizontal: 9, vertical: 5), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.12), + color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), width: 1), ), child: const Text( - '16/16', + '16 / 16', style: TextStyle( - color: Color(0xFF4E2600), + color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13), ), @@ -439,21 +458,20 @@ class DemoHomePage extends StatelessWidget { } Widget _buildHeader(BuildContext context) { - final primary = Theme.of(context).colorScheme.primary; return Container( padding: const EdgeInsets.all(24), - decoration: BoxDecoration( + decoration: const BoxDecoration( gradient: LinearGradient( - colors: [primary, Color.lerp(primary, Colors.purple.shade800, 0.55)!], + colors: [Color(0xFF0D3B8E), Color(0xFF1A56DB)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.all(Radius.circular(20)), boxShadow: [ BoxShadow( - color: primary.withValues(alpha: 0.35), + color: Color(0x591A56DB), blurRadius: 24, - offset: const Offset(0, 8), + offset: Offset(0, 8), ), ], ), @@ -467,11 +485,22 @@ class DemoHomePage extends StatelessWidget { width: 52, height: 52, decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.25), + color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(14), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), width: 1), + ), + child: const Center( + child: Text( + 'HR', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w900, + letterSpacing: 1, + ), + ), ), - child: const Icon(Icons.rocket_launch, - color: Colors.white, size: 30), ), const SizedBox(width: 16), const Expanded( @@ -487,13 +516,13 @@ class DemoHomePage extends StatelessWidget { letterSpacing: -0.5, ), ), - SizedBox(height: 2), + SizedBox(height: 3), Text( - 'Universal Content Engine for Flutter', + 'The only Flutter HTML renderer\nwith CSS float layout.', style: TextStyle( fontSize: 13, - color: Colors.white, - letterSpacing: 0.1, + color: Color(0xFFB8CEFC), + height: 1.45, ), ), ], @@ -502,15 +531,30 @@ class DemoHomePage extends StatelessWidget { ], ), const SizedBox(height: 20), + // โ”€โ”€ Stat row โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Row( + children: [ + _buildStatChip('1 646', 'tests'), + const SizedBox(width: 8), + _buildStatChip('60 FPS', 'scroll'), + const SizedBox(width: 8), + _buildStatChip('<100 ms', 'parse'), + const SizedBox(width: 8), + _buildStatChip('0', 'Gradle config'), + ], + ), + const SizedBox(height: 14), + // โ”€โ”€ Feature chips โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Wrap( spacing: 8, runSpacing: 8, children: [ - _buildChip('Float Layout', Icons.view_quilt_rounded), - _buildChip('Text Selection', Icons.select_all_rounded), - _buildChip('Ruby', Icons.translate_rounded), - _buildChip('Flexbox', Icons.view_column_rounded), - _buildChip('Widget Injection', Icons.widgets_rounded), + _buildChip('CSS Float', Icons.view_quilt_rounded), + _buildChip('Ruby / CJK', Icons.translate_rounded), + _buildChip('@keyframes', Icons.animation_rounded), + _buildChip('CSS Grid', Icons.grid_view_rounded), + _buildChip('Selection', Icons.select_all_rounded), + _buildChip('Markdown', Icons.description_outlined), ], ), ], @@ -518,14 +562,49 @@ class DemoHomePage extends StatelessWidget { ); } + Widget _buildStatChip(String value, String label) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), width: 1), + ), + child: Column( + children: [ + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w800, + letterSpacing: -0.3, + ), + ), + const SizedBox(height: 1), + Text( + label, + style: const TextStyle( + color: Color(0xFFB8CEFC), + fontSize: 10, + ), + ), + ], + ), + ), + ); + } + Widget _buildChip(String label, IconData icon) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.25), + color: Colors.white.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(20), border: - Border.all(color: Colors.white.withValues(alpha: 0.4), width: 1), + Border.all(color: Colors.white.withValues(alpha: 0.25), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -586,61 +665,78 @@ class DemoHomePage extends StatelessWidget { borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 8, + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 10, offset: const Offset(0, 2), ), ], ), - child: Material( - color: Colors.transparent, + child: ClipRRect( borderRadius: BorderRadius.circular(14), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(14), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: color.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(12), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Left accent bar + Container( + width: 4, + color: color, ), - child: Icon(icon, color: color, size: 24), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Color(0xFF1A1A2E), - letterSpacing: -0.1, - ), - ), - const SizedBox(height: 3), - Text( - subtitle, - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade500, - height: 1.3, - ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 14), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(11), + ), + child: Icon(icon, color: color, size: 22), + ), + const SizedBox(width: 13), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF0D1B3E), + letterSpacing: -0.1, + ), + ), + const SizedBox(height: 3), + Text( + subtitle, + style: TextStyle( + fontSize: 12.5, + color: Colors.grey.shade500, + height: 1.35, + ), + ), + ], + ), + ), + const SizedBox(width: 6), + Icon(Icons.chevron_right_rounded, + color: Colors.grey.shade300, size: 20), + ], ), - ], + ), ), - ), - const SizedBox(width: 8), - Icon(Icons.chevron_right_rounded, - color: Colors.grey.shade400, size: 22), - ], + ], + ), ), ), ), diff --git a/example/lib/manga_demo.dart b/example/lib/manga_demo.dart index 53f7134..f261bc6 100644 --- a/example/lib/manga_demo.dart +++ b/example/lib/manga_demo.dart @@ -80,86 +80,89 @@ class _CharacterTab extends StatelessWidget { // gap replaced by margin-right on children for compatibility. static const _html = ''' @@ -267,9 +270,17 @@ class _CharacterTab extends StatelessWidget { @override Widget build(BuildContext context) { - return HyperViewer( - html: _html, - selectable: true, + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 16, + ), + child: HyperViewer(html: _html, selectable: true), + ), + ), ); } } @@ -283,42 +294,44 @@ class _SynopsisTab extends StatelessWidget { static const _html = '''
      @@ -423,9 +436,17 @@ class _SynopsisTab extends StatelessWidget { @override Widget build(BuildContext context) { - return HyperViewer( - html: _html, - selectable: true, + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 16, + ), + child: HyperViewer(html: _html, selectable: true), + ), + ), ); } } @@ -441,43 +462,43 @@ class _PanelsTab extends StatelessWidget { // rock-solid. Flex with nested children collapses to zero-height panels. // Dark background (#212121) is inside the HTML wrapper, not Flutter-level. static const _html = ''' -
      +
      -
      - +
      + ๅฝฑ็•Œๅนดไปฃ่จ˜ โ–ธ Ch.12 โ–ธ P.8
      -
      +
      -
      -
      ๐Ÿฏ
      -
      +
      +
      ๐Ÿฏ
      +
      ้ฌผๆฎบ้šŠใใ•ใคใŸใ„ๆœฌ้ƒจโ€”โ€”ๆทฑๅคœ
      -
      +
      -
      -
      ๐Ÿ˜ค
      -
      +
      +
      ๐Ÿ˜ค
      +
      ใ€Œๆฅใใ‚‹ใŒใ„ใ„โ€ฆโ€ฆไธŠๅผฆใ˜ใ‚‡ใ†ใ’ใ‚“ใ‚ˆใ€
      -
      -
      ใƒ‰ใƒ‰ใƒ‰ใ‚ฉ๏ผ๏ผ
      +
      +
      ใƒ‰ใƒ‰ใƒ‰ใ‚ฉ๏ผ๏ผ
      -
      -
      ๐Ÿ˜ˆ
      -
      +
      +
      ๐Ÿ˜ˆ
      +
      ใ€Œ้ข็™ฝใŠใ‚‚ใ—ใ‚ใ„โ€ฆโ€ฆ๏ผใ€
      @@ -486,34 +507,34 @@ class _PanelsTab extends StatelessWidget {
      -
      -
      - โš”๏ธ - ใ‚บใƒใƒƒ๏ผ๏ผ - โš”๏ธ +
      +
      + โš”๏ธ + ใ‚บใƒใƒƒ๏ผ๏ผ + โš”๏ธ
      -
      +
      ๅฝฑใ‹ใ’ใƒŽๅž‹ใ‹ใŸโ€”โ€” - ๆผ†้ป’ใ—ใฃใ“ใใฎๆ–ฌ้–ƒใ–ใ‚“ใ›ใ‚“๏ผ๏ผ + ๆผ†้ป’ใ—ใฃใ“ใใฎๆ–ฌ้–ƒใ–ใ‚“ใ›ใ‚“๏ผ๏ผ
      -
      +
      -
      -
      -
      ๐Ÿ˜ฑ
      -
      ใ€Œใพใ•ใ‹โ€”โ€”๏ผใ€
      +
      +
      +
      ๐Ÿ˜ฑ
      +
      ใ€Œใพใ•ใ‹โ€”โ€”๏ผใ€
      -
      -
      ใƒใ‚ญใƒƒ๏ผ
      +
      +
      ใƒใ‚ญใƒƒ๏ผ
      -
      -
      +
      +
      ๏ผˆ็ˆถใกใกใ•ใ‚“โ€ฆโ€ฆ๏ผ‰
      @@ -521,8 +542,8 @@ class _PanelsTab extends StatelessWidget {
      -
      - ใคใƒปใฅใƒปใโ€ฆโ€ฆ +
      + ใคใƒปใฅใƒปใโ€ฆโ€ฆ
      @@ -532,9 +553,17 @@ class _PanelsTab extends StatelessWidget { @override Widget build(BuildContext context) { - return HyperViewer( - html: _html, - selectable: false, + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + child: HyperViewer( + html: _html, + selectable: false, + ), + ), + ), ); } } @@ -548,32 +577,35 @@ class _FuriganaTab extends StatelessWidget { static const _html = '''
      @@ -679,9 +711,17 @@ class _FuriganaTab extends StatelessWidget { ), ), Expanded( - child: HyperViewer( - html: _html, - selectable: true, + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + child: HyperViewer( + html: _html, + selectable: true, + ), + ), + ), ), ), ], diff --git a/example/lib/stress_test_demo.dart b/example/lib/stress_test_demo.dart index 93dd0b8..dfc5052 100644 --- a/example/lib/stress_test_demo.dart +++ b/example/lib/stress_test_demo.dart @@ -255,7 +255,7 @@ class _StressTestDemoState extends State { .tale-section { padding:0 16px 28px 16px; } .tale-heading { color:#6B2D0C; font-size:20px; margin:0 0 14px 0; padding-bottom:8px; border-bottom:2px solid #D4B896; font-style:italic; } .tale-emoji { color:#C19A6B; padding-right:6px; } - .tale-img { display:block; margin:0 auto 18px auto; border-radius:10px; border:3px solid #D4B896; } + .tale-img { display:block; max-width:100%; margin:0 auto 18px auto; border-radius:10px; border:3px solid #D4B896; } .tale-caption { text-align:center; color:#C19A6B; font-size:12px; margin:-12px 0 16px 0; font-style:italic; } .tale-para { margin:0 0 14px 0; font-size:15px; text-align:justify; color:#3a2010; } .tale-quote { border-left:4px solid #C19A6B; margin:16px 0; padding:12px 16px; background:#FEF9F0; border-radius:0 8px 8px 0; } @@ -328,11 +328,11 @@ class _StressTestDemoState extends State { .novel-drop { font-size:48px; font-weight:bold; color:#1565C0; float:left; line-height:0.85; margin:6px 8px 0 0; } .novel-highlight { background:#E3F2FD; padding:12px 16px; border-radius:8px; margin:16px 0; border-left:4px solid #1976D2; } .novel-highlight strong { color:#1565C0; } - .novel-img { display:block; margin:0 auto 16px auto; border-radius:10px; } + .novel-img { display:block; max-width:100%; margin:0 auto 16px auto; border-radius:10px; } .novel-hr { margin:28px 0; border:none; border-top:1px solid #E0E0E0; } .novel-tag { display:inline-block; background:#E3F2FD; color:#1565C0; border-radius:12px; padding:2px 8px; font-size:11px; margin:0 4px 4px 0; } -
      +

      ๐Ÿ“– The Infinite Archive

      $pages chapters ยท Performance & rendering showcase

      @@ -978,12 +978,17 @@ class _StressTestDemoState extends State { switch (_selectedLibrary) { case 'HyperRender': - return HyperViewer( - html: content, - mode: HyperRenderMode.auto, - selectable: true, - placeholderBuilder: (context) => - _buildLoadingPlaceholder(content.length), + // Manga is intentionally edge-to-edge; other content types need margin. + final hPad = _contentType == _ContentType.manga ? 0.0 : 16.0; + return Padding( + padding: EdgeInsets.symmetric(horizontal: hPad), + child: HyperViewer( + html: content, + mode: HyperRenderMode.auto, + selectable: true, + placeholderBuilder: (context) => + _buildLoadingPlaceholder(content.length), + ), ); case 'flutter_html': return ClipRect( diff --git a/example/lib/why_hyper_render_demo.dart b/example/lib/why_hyper_render_demo.dart index e50beb9..19bdf32 100644 --- a/example/lib/why_hyper_render_demo.dart +++ b/example/lib/why_hyper_render_demo.dart @@ -347,15 +347,15 @@ class WhyHyperRenderDemo extends StatelessWidget { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( - gradient: LinearGradient( - colors: [DemoColors.primary, const Color(0xFF6A1B9A)], + gradient: const LinearGradient( + colors: [Color(0xFF0D3B8E), Color(0xFF1A56DB)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: DemoColors.primary.withValues(alpha: 0.4), + color: const Color(0xFF1A56DB).withValues(alpha: 0.4), blurRadius: 24, offset: const Offset(0, 8), ), @@ -367,13 +367,25 @@ class WhyHyperRenderDemo extends StatelessWidget { Row( children: [ Container( - padding: const EdgeInsets.all(10), + width: 50, + height: 50, decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.25), + color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), width: 1), + ), + child: const Center( + child: Text( + 'HR', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w900, + letterSpacing: 1, + ), + ), ), - child: const Icon(Icons.emoji_events, - color: Colors.white, size: 28), ), const SizedBox(width: 14), const Expanded( @@ -381,19 +393,22 @@ class WhyHyperRenderDemo extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'The Best Flutter HTML Renderer', + 'CSS float. No other Flutter\nHTML library can do this.', style: TextStyle( color: Colors.white, - fontSize: 18, + fontSize: 17, fontWeight: FontWeight.w800, - letterSpacing: -0.3, + letterSpacing: -0.4, + height: 1.3, ), ), + SizedBox(height: 4), Text( - 'Custom RenderObject ยท Zero WebView ยท Zero Deps', + 'Single RenderObject ยท Zero WebView ยท XSS-safe by default', style: TextStyle( - color: Colors.white, + color: Color(0xFFB8CEFC), fontSize: 12, + height: 1.4, ), ), ], @@ -518,20 +533,20 @@ class WhyHyperRenderDemo extends StatelessWidget { return Row( children: [ Expanded( - child: _buildStatCard( - '16 / 16', 'Features', Icons.check_circle, Colors.green)), + child: _buildStatCard('1 646', 'Tests passing', + Icons.check_circle_outline, const Color(0xFF2E7D32))), const SizedBox(width: 10), Expanded( - child: - _buildStatCard('60+', 'CSS Props', Icons.style, Colors.indigo)), + child: _buildStatCard( + '60 FPS', 'Scroll', Icons.speed, Colors.indigo)), const SizedBox(width: 10), Expanded( - child: _buildStatCard( - 'Zero', 'WebViews', Icons.no_encryption, Colors.red)), + child: _buildStatCard('<100ms', 'Parse', Icons.timer_outlined, + const Color(0xFFBF360C))), const SizedBox(width: 10), Expanded( child: _buildStatCard( - '3+', 'Formats', Icons.data_object, Colors.teal)), + '8 MB', 'RAM (25k)', Icons.memory, const Color(0xFF00695C))), ], ); } diff --git a/example/lib/zero_padding_image_demo.dart b/example/lib/zero_padding_image_demo.dart new file mode 100644 index 0000000..77f6f94 --- /dev/null +++ b/example/lib/zero_padding_image_demo.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:hyper_render/hyper_render.dart'; + +class ZeroPaddingImageDemo extends StatelessWidget { + const ZeroPaddingImageDemo({super.key}); + + static const html = ''' + + +
      +
      +

      Edge-to-Edge Typography

      + +
      + + + Main hero + +
      +

      + HyperRender 2.0 now supports true edge-to-edge images. + By setting _kImageMargin to 0.0 in the core engine, + width: 100% images now fill the entire available width of their container. +

      + +
      + Developer Tip: To create the "Modern Reader" look, wrap your HyperViewer + without any Flutter-level padding, and handle horizontal spacing entirely within your CSS. +
      + +

      + In this demo, the header and this text section have 20px of horizontal padding defined in CSS, + while the hero image above and the secondary image below span the full width of your device. +

      +
      + + + Mid article image + +
      +

      + Below is a standard image. It is placed inside a container with padding, + so it respects the margins. We've also added a border-radius and box-shadow + to demonstrate high-end visual styling. +

      + + + Standard image +
      Figure 1: A standard image respecting container padding.
      + +

      + The combination of immersive full-bleed imagery and perfectly aligned typography + makes HyperRender the choice for premium publishing applications. +

      + +
      +
      +
      +'''; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBar( + title: const Text('Edge-to-Edge Layout'), + backgroundColor: const Color(0xFF1A56DB), + foregroundColor: Colors.white, + elevation: 0, + ), + body: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: const HyperViewer( + html: html, + selectable: true, + ), + ), + ), + ); + } +} diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index fcafa49..f6f23bf 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,17 +6,9 @@ #include "generated_plugin_registrant.h" -#include -#include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); - irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); - g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); - super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 8a5dc51..df8d2f7 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,8 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - irondash_engine_context - super_native_extensions url_launcher_linux ) diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index e481378..e7a0ba1 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,10 @@ import FlutterMacOS import Foundation import audio_session -import device_info_plus -import irondash_engine_context import just_audio import package_info_plus import share_plus import sqflite -import super_native_extensions import url_launcher_macos import video_player_avfoundation import wakelock_plus @@ -20,13 +17,10 @@ import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) - DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) - IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) - SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) diff --git a/example/pubspec.lock b/example/pubspec.lock index 0b9e861..6646d1e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -137,22 +137,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.12" - device_info_plus: - dependency: transitive - description: - name: device_info_plus - sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" - url: "https://pub.dev" - source: hosted - version: "11.5.0" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.dev" - source: hosted - version: "7.0.3" fake_async: dependency: transitive description: @@ -222,14 +206,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" - flutter_math_fork: - dependency: transitive - description: - name: flutter_math_fork - sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" - url: "https://pub.dev" - source: hosted - version: "0.7.4" flutter_svg: dependency: transitive description: @@ -366,65 +342,35 @@ packages: path: ".." relative: true source: path - version: "1.3.0" - hyper_render_clipboard: - dependency: "direct main" - description: - path: "../packages/hyper_render_clipboard" - relative: true - source: path - version: "1.3.0" + version: "1.3.1" hyper_render_core: dependency: "direct main" description: path: "../packages/hyper_render_core" relative: true source: path - version: "1.3.0" + version: "1.3.1" hyper_render_highlight: dependency: transitive description: path: "../packages/hyper_render_highlight" relative: true source: path - version: "1.3.0" + version: "1.3.1" hyper_render_html: dependency: transitive description: path: "../packages/hyper_render_html" relative: true source: path - version: "1.3.0" + version: "1.3.1" hyper_render_markdown: dependency: transitive description: path: "../packages/hyper_render_markdown" relative: true source: path - version: "1.3.0" - hyper_render_math: - dependency: transitive - description: - path: "../packages/hyper_render_math" - relative: true - source: path - version: "1.3.0" - irondash_engine_context: - dependency: transitive - description: - name: irondash_engine_context - sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7" - url: "https://pub.dev" - source: hosted - version: "0.5.5" - irondash_message_channel: - dependency: transitive - description: - name: irondash_message_channel - sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 - url: "https://pub.dev" - source: hosted - version: "0.7.0" + version: "1.3.1" jni: dependency: transitive description: @@ -681,14 +627,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.2" - pixel_snap: - dependency: transitive - description: - name: pixel_snap - sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" - url: "https://pub.dev" - source: hosted - version: "0.1.5" platform: dependency: transitive description: @@ -806,22 +744,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - super_clipboard: - dependency: transitive - description: - name: super_clipboard - sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16 - url: "https://pub.dev" - source: hosted - version: "0.9.1" - super_native_extensions: - dependency: transitive - description: - name: super_native_extensions - sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 - url: "https://pub.dev" - source: hosted - version: "0.9.1" synchronized: dependency: transitive description: @@ -846,14 +768,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: @@ -1078,14 +992,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" - url: "https://pub.dev" - source: hosted - version: "2.1.0" xdg_directories: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 472f3c1..32bfdf7 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -22,8 +22,6 @@ dependencies: path: ../packages/hyper_render_core # Clipboard/sharing support - hyper_render_clipboard: - path: ../packages/hyper_render_clipboard # Competitor libraries for comparison flutter_html: ^3.0.0-beta.2 @@ -46,8 +44,6 @@ dependency_overrides: path: ../ hyper_render_core: path: ../packages/hyper_render_core - hyper_render_clipboard: - path: ../packages/hyper_render_clipboard dev_dependencies: flutter_test: diff --git a/lib/hyper_render.dart b/lib/hyper_render.dart index 75b4a66..e0e021d 100644 --- a/lib/hyper_render.dart +++ b/lib/hyper_render.dart @@ -194,9 +194,5 @@ export 'src/plugins/default_css_parser.dart' show DefaultCssParser; export 'src/plugins/default_code_highlighter.dart' show DefaultCodeHighlighter, HighlightTheme; -// ============================================ -// Clipboard plugin -// ============================================ - -export 'package:hyper_render_clipboard/hyper_render_clipboard.dart' - show SuperClipboardHandler; +// SuperClipboardHandler is available in the optional hyper_render_clipboard package. +// Add it to your pubspec if you need native image copy/share. diff --git a/lib/src/widgets/virtualized_selection_overlay.dart b/lib/src/widgets/virtualized_selection_overlay.dart index 0501e15..3a73cce 100644 --- a/lib/src/widgets/virtualized_selection_overlay.dart +++ b/lib/src/widgets/virtualized_selection_overlay.dart @@ -356,13 +356,14 @@ class _VirtualizedSelectionOverlayState final localPosition = scrollableBox.globalToLocal(globalPosition); final size = scrollableBox.size; - const threshold = 50.0; + const threshold = 60.0; + const maxStep = 20.0; double dy = 0.0; if (localPosition.dy < threshold) { - dy = -15.0; + dy = -maxStep * (1.0 - localPosition.dy / threshold); } else if (localPosition.dy > size.height - threshold) { - dy = 15.0; + dy = maxStep * (1.0 - (size.height - localPosition.dy) / threshold); } if (dy != 0.0) { @@ -415,7 +416,7 @@ class _VirtualizedSelectionOverlayState }, child: CustomPaint( size: const Size(22, 22), - painter: _TeardropHandlePainter( + painter: HyperTeardropHandlePainter( color: widget.handleColor, isStart: isStart, ), @@ -522,43 +523,5 @@ class _MenuButton extends StatelessWidget { } } -/// Teardrop handle painter (mirrors the one in hyper_selection_overlay.dart). -class _TeardropHandlePainter extends CustomPainter { - const _TeardropHandlePainter({required this.color, required this.isStart}); - final Color color; - final bool isStart; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - final shadow = Paint() - ..color = const Color(0x33000000) - ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2); - final path = Path(); - final w = size.width; - final h = size.height; - - if (isStart) { - path.moveTo(w / 2, 0); - path.quadraticBezierTo(w, h * 0.4, w * 0.85, h * 0.7); - path.arcToPoint(Offset(w * 0.15, h * 0.7), - radius: Radius.circular(w * 0.35), clockwise: false); - path.quadraticBezierTo(0, h * 0.4, w / 2, 0); - } else { - path.moveTo(w / 2, h); - path.quadraticBezierTo(w, h * 0.6, w * 0.85, h * 0.3); - path.arcToPoint(Offset(w * 0.15, h * 0.3), - radius: Radius.circular(w * 0.35), clockwise: true); - path.quadraticBezierTo(0, h * 0.6, w / 2, h); - } - - canvas.drawPath(path, shadow); - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(_TeardropHandlePainter old) => - old.color != color || old.isStart != isStart; -} +// HyperTeardropHandlePainter is defined in hyper_selection_overlay.dart +// and exported from hyper_render_core โ€” no local copy needed. diff --git a/packages/hyper_render_clipboard/CHANGELOG.md b/packages/hyper_render_clipboard/CHANGELOG.md index 9c66ad4..df6ee74 100644 --- a/packages/hyper_render_clipboard/CHANGELOG.md +++ b/packages/hyper_render_clipboard/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog โ€” hyper_render_clipboard +## [1.3.1] - 2026-05-14 + +### ๐Ÿ—๏ธ Packaging +- **Opt-in add-on**: `hyper_render_clipboard` is no longer bundled with the root `hyper_render` package. Add it explicitly to your `pubspec.yaml` if you use `SuperClipboardHandler`. This removes the `compileSdk = 34` requirement from default `hyper_render` users. + ## [1.3.0] - 2026-05-03 ### โœจ New Features diff --git a/packages/hyper_render_clipboard/README.md b/packages/hyper_render_clipboard/README.md index 015bffc..dabcbfc 100644 --- a/packages/hyper_render_clipboard/README.md +++ b/packages/hyper_render_clipboard/README.md @@ -8,7 +8,7 @@ Image clipboard operations for [HyperRender](https://pub.dev/packages/hyper_rend ```yaml dependencies: - hyper_render_clipboard: ^1.3.0 + hyper_render_clipboard: ^1.3.1 ``` --- @@ -78,6 +78,23 @@ PNG, JPEG, GIF, WebP, BMP, TIFF --- +## HyperRender Ecosystem + +| Package | Description | +|---------|-------------| +| [hyper_render](https://pub.dev/packages/hyper_render) | Main package โ€” `HyperViewer` widget, HTML + Markdown rendering | +| [hyper_render_core](https://pub.dev/packages/hyper_render_core) | Core engine: UDT model, `RenderHyperBox`, plugin API | +| [hyper_render_html](https://pub.dev/packages/hyper_render_html) | HTML + CSS โ†’ UDT parser | +| [hyper_render_markdown](https://pub.dev/packages/hyper_render_markdown) | Markdown (GFM) โ†’ UDT parser | +| [hyper_render_highlight](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `` / `
      ` blocks |
      +| **[hyper_render_clipboard](https://pub.dev/packages/hyper_render_clipboard)** | **Image copy / save / share** โ† you are here |
      +| [hyper_render_math](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML rendering *(opt-in)* |
      +| [hyper_render_devtools](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools inspector |
      +
      +[Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_clipboard) ยท [Issues](https://github.com/brewkits/hyper_render/issues) ยท [Changelog](CHANGELOG.md)
      +
      +---
      +
       ## License
       
       MIT โ€” see [LICENSE](LICENSE).
      diff --git a/packages/hyper_render_clipboard/pubspec.yaml b/packages/hyper_render_clipboard/pubspec.yaml
      index e374bc0..d3c134f 100644
      --- a/packages/hyper_render_clipboard/pubspec.yaml
      +++ b/packages/hyper_render_clipboard/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render_clipboard
       description: Image clipboard support for HyperRender using super_clipboard. Enables copying, saving, and sharing images.
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_clipboard
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      diff --git a/packages/hyper_render_core/CHANGELOG.md b/packages/hyper_render_core/CHANGELOG.md
      index e8afea7..b9d3a12 100644
      --- a/packages/hyper_render_core/CHANGELOG.md
      +++ b/packages/hyper_render_core/CHANGELOG.md
      @@ -1,5 +1,22 @@
       # Changelog โ€” hyper_render_core
       
      +## [1.3.1] - 2026-05-14
      +
      +### โœจ New CSS Properties
      +- **`list-style-type`**: All 11 values โ€” `disc`, `circle`, `square`, `decimal`, `decimal-leading-zero`, `lower-alpha`, `upper-alpha`, `lower-latin`, `upper-latin`, `lower-roman`, `upper-roman`, `none`
      +- **`list-style-position`**: `inside` / `outside` (default)
      +- **`list-style` shorthand**: parses ` ` in any order
      +- **`background-repeat`**: `repeat`, `repeat-x`, `repeat-y`, `no-repeat`, `space`, `round`
      +- **`background-position`**: keyword (`center`, `top left`, etc.) and percentage values
      +
      +### ๐Ÿš€ Performance
      +- **Selection rects cached**: `getSelectionRects()` called once per drag event (was 3ร—); stored in `_selectionRects` field โ€” eliminates redundant layout walks during selection drag
      +- **Auto-scroll proportional speed**: `_autoScrollIfNearEdge` now scales 0โ€“20 px/frame based on finger distance from edge (was fixed 15 px/frame)
      +- **`HyperTeardropHandlePainter` deduplicated**: renamed to `HyperTeardropHandlePainter`, made public, and exported from core; duplicate in the virtualized overlay deleted
      +
      +### ๐Ÿ› Bug Fixes
      +- **Edge-to-edge images**: `_kImageMargin` set to `0.0` โ€” `width: 100%` images now truly fill their container with no internal margin offset
      +
       ## [1.3.0] - 2026-05-03
       
       ### โœจ New Features
      diff --git a/packages/hyper_render_core/README.md b/packages/hyper_render_core/README.md
      index 9fa3ad1..3112221 100644
      --- a/packages/hyper_render_core/README.md
      +++ b/packages/hyper_render_core/README.md
      @@ -10,7 +10,7 @@ Most apps should depend on [`hyper_render`](https://pub.dev/packages/hyper_rende
       
       ```yaml
       dependencies:
      -  hyper_render_core: ^1.3.0
      +  hyper_render_core: ^1.3.1
       ```
       
       ---
      @@ -51,12 +51,12 @@ Register a `HyperNodePlugin` to render arbitrary HTML tags as Flutter widgets:
       
       ```dart
       class MyCardPlugin implements HyperNodePlugin {
      -  @override String get tagName => 'my-card';
      +  @override List get tagNames => ['my-card'];
         @override bool get isInline => false; // true = flows with text
       
         @override
      -  Widget? build(HyperPluginBuildContext ctx) {
      -    return Card(child: Text(ctx.node.textContent));
      +  Widget? buildWidget(UDTNode node, HyperPluginBuildContext ctx) {
      +    return Card(child: Text(node.textContent));
         }
       }
       
      @@ -97,6 +97,23 @@ Implement these to extend the engine:
       
       ---
       
      +## HyperRender Ecosystem
      +
      +| Package | Description |
      +|---------|-------------|
      +| [hyper_render](https://pub.dev/packages/hyper_render) | Main package โ€” `HyperViewer` widget, HTML + Markdown rendering |
      +| **[hyper_render_core](https://pub.dev/packages/hyper_render_core)** | **Core engine: UDT model, `RenderHyperBox`, plugin API** โ† you are here |
      +| [hyper_render_html](https://pub.dev/packages/hyper_render_html) | HTML + CSS โ†’ UDT parser |
      +| [hyper_render_markdown](https://pub.dev/packages/hyper_render_markdown) | Markdown (GFM) โ†’ UDT parser |
      +| [hyper_render_highlight](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `` / `
      ` blocks |
      +| [hyper_render_clipboard](https://pub.dev/packages/hyper_render_clipboard) | Image copy / save / share *(opt-in)* |
      +| [hyper_render_math](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML rendering *(opt-in)* |
      +| [hyper_render_devtools](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools inspector |
      +
      +[Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_core) ยท [Issues](https://github.com/brewkits/hyper_render/issues) ยท [Changelog](CHANGELOG.md)
      +
      +---
      +
       ## License
       
       MIT โ€” see [LICENSE](LICENSE).
      diff --git a/packages/hyper_render_core/lib/src/core/render_hyper_box.dart b/packages/hyper_render_core/lib/src/core/render_hyper_box.dart
      index ab26903..fe79b6b 100644
      --- a/packages/hyper_render_core/lib/src/core/render_hyper_box.dart
      +++ b/packages/hyper_render_core/lib/src/core/render_hyper_box.dart
      @@ -1576,4 +1576,59 @@ class RenderHyperBox extends RenderBox
               return BoxFit.cover;
           }
         }
      +
      +  ImageRepeat _getImageRepeat(String? cssValue) {
      +    switch (cssValue?.toLowerCase().trim()) {
      +      case 'repeat-x':
      +        return ImageRepeat.repeatX;
      +      case 'repeat-y':
      +        return ImageRepeat.repeatY;
      +      case 'no-repeat':
      +        return ImageRepeat.noRepeat;
      +      case 'repeat':
      +      case 'space':
      +      case 'round':
      +      default:
      +        return ImageRepeat.repeat;
      +    }
      +  }
      +
      +  Alignment _getBackgroundAlignment(String? cssValue) {
      +    if (cssValue == null) return Alignment.topLeft;
      +    final v = cssValue.toLowerCase().trim();
      +    switch (v) {
      +      case 'center':
      +        return Alignment.center;
      +      case 'top':
      +      case 'top center':
      +      case 'center top':
      +        return Alignment.topCenter;
      +      case 'bottom':
      +      case 'bottom center':
      +      case 'center bottom':
      +        return Alignment.bottomCenter;
      +      case 'left':
      +      case 'left center':
      +      case 'center left':
      +        return Alignment.centerLeft;
      +      case 'right':
      +      case 'right center':
      +      case 'center right':
      +        return Alignment.centerRight;
      +      case 'top left':
      +      case 'left top':
      +        return Alignment.topLeft;
      +      case 'top right':
      +      case 'right top':
      +        return Alignment.topRight;
      +      case 'bottom left':
      +      case 'left bottom':
      +        return Alignment.bottomLeft;
      +      case 'bottom right':
      +      case 'right bottom':
      +        return Alignment.bottomRight;
      +      default:
      +        return Alignment.topLeft;
      +    }
      +  }
       }
      diff --git a/packages/hyper_render_core/lib/src/core/render_hyper_box_layout.dart b/packages/hyper_render_core/lib/src/core/render_hyper_box_layout.dart
      index 7e5cee4..c368295 100644
      --- a/packages/hyper_render_core/lib/src/core/render_hyper_box_layout.dart
      +++ b/packages/hyper_render_core/lib/src/core/render_hyper_box_layout.dart
      @@ -7,7 +7,7 @@ const double _kDefaultFloatMargin = 8.0;
       
       const double _kMinFloatYStep = 1.0;
       const double _kImageMargin =
      -    32.0; // horizontal margin subtracted from maxWidth for images
      +    0.0; // horizontal margin subtracted from maxWidth for images
       const double _kDefaultFlexFallbackHeight = 50.0;
       const double _kDefaultTableFallbackHeight = 200.0;
       const double _kTableBottomMargin = 16.0;
      @@ -198,24 +198,30 @@ extension _RenderHyperBoxLayout on RenderHyperBox {
             final parentTag = parentBlock.tagName?.toLowerCase();
             final isOrdered = parentTag == 'ol';
       
      -      // Get or calculate list item index
      -      int index = 1;
      -      if (isOrdered) {
      -        // Count previous li siblings
      -        index = (_listItemIndices[parentBlock] ?? 0) + 1;
      -        _listItemIndices[parentBlock] = index;
      -      }
      +      // Resolve effective list-style-type: li overrides parent, then parent,
      +      // then default (disc for ul, decimal for ol).
      +      final effectiveType = style.listStyleType ??
      +          parentBlock.style.listStyleType ??
      +          (isOrdered ? 'decimal' : 'disc');
      +
      +      // list-style-type: none โ†’ suppress marker entirely.
      +      if (effectiveType != 'none') {
      +        int index = 1;
      +        if (isOrdered) {
      +          index = (_listItemIndices[parentBlock] ?? 0) + 1;
      +          _listItemIndices[parentBlock] = index;
      +        }
       
      -      // Create marker text
      -      final marker = isOrdered ? '$index. ' : 'โ€ข ';
      +        final marker = _buildListMarker(effectiveType, isOrdered, index);
       
      -      _fragments.add(_ListMarkerFragment(
      -        sourceNode: node,
      -        style: style,
      -        marker: marker,
      -        isOrdered: isOrdered,
      -        index: index,
      -      ));
      +        _fragments.add(_ListMarkerFragment(
      +          sourceNode: node,
      +          style: style,
      +          marker: marker,
      +          isOrdered: isOrdered,
      +          index: index,
      +        ));
      +      }
           }
       
           if (style.float != HyperFloat.none) {
      @@ -1576,6 +1582,75 @@ extension _RenderHyperBoxLayout on RenderHyperBox {
       
         /// Recursively extracts plain text content from a [UDTNode] subtree.
         /// Used by heading-anchor collection to provide human-readable TOC labels.
      +  /// Returns the marker string for a list item given its CSS list-style-type.
      +  String _buildListMarker(String type, bool isOrdered, int index) {
      +    switch (type) {
      +      case 'disc':
      +        return 'โ€ข ';
      +      case 'circle':
      +        return 'โ—ฆ ';
      +      case 'square':
      +        return 'โ–ช ';
      +      case 'decimal':
      +        return '$index. ';
      +      case 'decimal-leading-zero':
      +        return '${index.toString().padLeft(2, '0')}. ';
      +      case 'lower-alpha':
      +      case 'lower-latin':
      +        return '${_indexToAlpha(index, uppercase: false)}. ';
      +      case 'upper-alpha':
      +      case 'upper-latin':
      +        return '${_indexToAlpha(index, uppercase: true)}. ';
      +      case 'lower-roman':
      +        return '${_indexToRoman(index).toLowerCase()}. ';
      +      case 'upper-roman':
      +        return '${_indexToRoman(index)}. ';
      +      default:
      +        return isOrdered ? '$index. ' : 'โ€ข ';
      +    }
      +  }
      +
      +  String _indexToAlpha(int index, {required bool uppercase}) {
      +    // 1โ†’a, 2โ†’b, โ€ฆ26โ†’z, 27โ†’aa, etc.
      +    final base = uppercase ? 'A'.codeUnitAt(0) : 'a'.codeUnitAt(0);
      +    var n = index;
      +    final buf = StringBuffer();
      +    while (n > 0) {
      +      n--;
      +      buf.write(String.fromCharCode(base + n % 26));
      +      n ~/= 26;
      +    }
      +    return buf.toString().split('').reversed.join();
      +  }
      +
      +  String _indexToRoman(int n) {
      +    const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
      +    const syms = [
      +      'M',
      +      'CM',
      +      'D',
      +      'CD',
      +      'C',
      +      'XC',
      +      'L',
      +      'XL',
      +      'X',
      +      'IX',
      +      'V',
      +      'IV',
      +      'I'
      +    ];
      +    final buf = StringBuffer();
      +    var remaining = n;
      +    for (var i = 0; i < vals.length; i++) {
      +      while (remaining >= vals[i]) {
      +        buf.write(syms[i]);
      +        remaining -= vals[i];
      +      }
      +    }
      +    return buf.toString();
      +  }
      +
         String _extractNodeText(UDTNode node) {
           if (node is TextNode) return node.text;
           final buf = StringBuffer();
      diff --git a/packages/hyper_render_core/lib/src/core/render_hyper_box_paint.dart b/packages/hyper_render_core/lib/src/core/render_hyper_box_paint.dart
      index f677e75..3f9f66d 100644
      --- a/packages/hyper_render_core/lib/src/core/render_hyper_box_paint.dart
      +++ b/packages/hyper_render_core/lib/src/core/render_hyper_box_paint.dart
      @@ -707,8 +707,9 @@ extension _RenderHyperBoxPaint on RenderHyperBox {
               rect: rect,
               image: cached.image!,
               fit: _getBoxFit(fragment.style.backgroundSize),
      -        filterQuality:
      -            FilterQuality.medium, // Crisp rendering on retina displays
      +        repeat: _getImageRepeat(fragment.style.backgroundRepeat),
      +        alignment: _getBackgroundAlignment(fragment.style.backgroundPosition),
      +        filterQuality: FilterQuality.medium,
             );
       
             if (borderRadius != null) {
      diff --git a/packages/hyper_render_core/lib/src/model/computed_style.dart b/packages/hyper_render_core/lib/src/model/computed_style.dart
      index a32a181..cf8cdbd 100644
      --- a/packages/hyper_render_core/lib/src/model/computed_style.dart
      +++ b/packages/hyper_render_core/lib/src/model/computed_style.dart
      @@ -386,6 +386,13 @@ class ComputedStyle {
         /// CSS overflow-wrap
         String? overflowWrap;
       
      +  /// CSS list-style-type: disc | circle | square | decimal | lower-alpha |
      +  /// upper-alpha | lower-roman | upper-roman | none
      +  String? listStyleType;
      +
      +  /// CSS list-style-position: outside (default) | inside
      +  String? listStylePosition;
      +
         /// CSS text-shadow
         List? textShadow;
       
      @@ -414,6 +421,12 @@ class ComputedStyle {
         /// CSS background-size
         String? backgroundSize;
       
      +  /// CSS background-repeat: repeat | repeat-x | repeat-y | no-repeat | space | round
      +  String? backgroundRepeat;
      +
      +  /// CSS background-position: top | center | bottom | left | right | "x y"
      +  String? backgroundPosition;
      +
         // ============================================
         // Layout Properties
         // ============================================
      @@ -613,6 +626,8 @@ class ComputedStyle {
           this.textOverflow,
           this.wordBreak,
           this.overflowWrap,
      +    this.listStyleType,
      +    this.listStylePosition,
           this.textShadow,
           this.boxShadow,
           this.filter,
      @@ -621,6 +636,8 @@ class ComputedStyle {
           this.backgroundGradient,
           this.backgroundImage,
           this.backgroundSize,
      +    this.backgroundRepeat,
      +    this.backgroundPosition,
           this.display = DisplayType.inline,
           this.overflowX = HyperOverflow.visible,
           this.overflowY = HyperOverflow.visible,
      @@ -774,6 +791,8 @@ class ComputedStyle {
           TextOverflow? textOverflow,
           String? wordBreak,
           String? overflowWrap,
      +    String? listStyleType,
      +    String? listStylePosition,
           List? textShadow,
           List? boxShadow,
           ui.ImageFilter? filter,
      @@ -783,6 +802,8 @@ class ComputedStyle {
           Gradient? backgroundGradient,
           String? backgroundImage,
           String? backgroundSize,
      +    String? backgroundRepeat,
      +    String? backgroundPosition,
           // Layout
           DisplayType? display,
           HyperOverflow? overflowX,
      @@ -868,6 +889,8 @@ class ComputedStyle {
             textOverflow: textOverflow ?? this.textOverflow,
             wordBreak: wordBreak ?? this.wordBreak,
             overflowWrap: overflowWrap ?? this.overflowWrap,
      +      listStyleType: listStyleType ?? this.listStyleType,
      +      listStylePosition: listStylePosition ?? this.listStylePosition,
             textShadow: textShadow ?? this.textShadow,
             boxShadow: boxShadow ?? this.boxShadow,
             filter: filter ?? this.filter,
      @@ -876,6 +899,8 @@ class ComputedStyle {
             backgroundGradient: backgroundGradient ?? this.backgroundGradient,
             backgroundImage: backgroundImage ?? this.backgroundImage,
             backgroundSize: backgroundSize ?? this.backgroundSize,
      +      backgroundRepeat: backgroundRepeat ?? this.backgroundRepeat,
      +      backgroundPosition: backgroundPosition ?? this.backgroundPosition,
             display: display ?? this.display,
             overflowX: overflowX ?? this.overflowX,
             overflowY: overflowY ?? this.overflowY,
      diff --git a/packages/hyper_render_core/lib/src/style/resolver.dart b/packages/hyper_render_core/lib/src/style/resolver.dart
      index ce16959..fb1dda1 100644
      --- a/packages/hyper_render_core/lib/src/style/resolver.dart
      +++ b/packages/hyper_render_core/lib/src/style/resolver.dart
      @@ -1214,6 +1214,27 @@ class StyleResolver {
               style.markExplicitlySet('background-size');
               break;
       
      +      case 'background-repeat':
      +        final repeat = value.trim().toLowerCase();
      +        const validRepeats = {
      +          'repeat',
      +          'repeat-x',
      +          'repeat-y',
      +          'no-repeat',
      +          'space',
      +          'round',
      +        };
      +        if (validRepeats.contains(repeat)) {
      +          style.backgroundRepeat = repeat;
      +          style.markExplicitlySet('background-repeat');
      +        }
      +        break;
      +
      +      case 'background-position':
      +        style.backgroundPosition = value.trim().toLowerCase();
      +        style.markExplicitlySet('background-position');
      +        break;
      +
             case 'font-size':
               final size = _parseFontSize(value, parentFontSize: parentFontSize);
               if (size != null) {
      @@ -1565,6 +1586,64 @@ class StyleResolver {
               style.markExplicitlySet('overflow-wrap');
               break;
       
      +      case 'list-style-type':
      +        final lst = value.trim().toLowerCase();
      +        const validListStyleTypes = {
      +          'disc',
      +          'circle',
      +          'square',
      +          'decimal',
      +          'decimal-leading-zero',
      +          'lower-alpha',
      +          'lower-latin',
      +          'upper-alpha',
      +          'upper-latin',
      +          'lower-roman',
      +          'upper-roman',
      +          'none',
      +        };
      +        if (validListStyleTypes.contains(lst)) {
      +          style.listStyleType = lst;
      +          style.markExplicitlySet('list-style-type');
      +        }
      +        break;
      +
      +      case 'list-style-position':
      +        final lsp = value.trim().toLowerCase();
      +        if (lsp == 'inside' || lsp == 'outside') {
      +          style.listStylePosition = lsp;
      +          style.markExplicitlySet('list-style-position');
      +        }
      +        break;
      +
      +      case 'list-style':
      +        // Shorthand: parse each space-separated token
      +        for (final token in value.trim().toLowerCase().split(RegExp(r'\s+'))) {
      +          if (token == 'none') {
      +            style.listStyleType = 'none';
      +            style.markExplicitlySet('list-style-type');
      +          } else if (token == 'inside' || token == 'outside') {
      +            style.listStylePosition = token;
      +            style.markExplicitlySet('list-style-position');
      +          } else if (const {
      +            'disc',
      +            'circle',
      +            'square',
      +            'decimal',
      +            'decimal-leading-zero',
      +            'lower-alpha',
      +            'lower-latin',
      +            'upper-alpha',
      +            'upper-latin',
      +            'lower-roman',
      +            'upper-roman',
      +          }.contains(token)) {
      +            style.listStyleType = token;
      +            style.markExplicitlySet('list-style-type');
      +          }
      +        }
      +        break;
      +
             case 'text-shadow':
               final shadows = _parseTextShadow(value);
               if (shadows != null && shadows.isNotEmpty) {
      diff --git a/packages/hyper_render_core/lib/src/widgets/hyper_selection_overlay.dart b/packages/hyper_render_core/lib/src/widgets/hyper_selection_overlay.dart
      index 188f704..a54b842 100644
      --- a/packages/hyper_render_core/lib/src/widgets/hyper_selection_overlay.dart
      +++ b/packages/hyper_render_core/lib/src/widgets/hyper_selection_overlay.dart
      @@ -116,9 +116,10 @@ class HyperSelectionOverlayState extends State
         /// Whether context menu is showing
         bool _showContextMenu = false;
       
      -  /// Selection handles positions
      +  /// Selection handles positions and rects (cached to avoid redundant passes)
         Rect? _startHandleRect;
         Rect? _endHandleRect;
      +  List _selectionRects = const [];
       
         /// Which handle is being dragged (for potential future animation/haptic feedback)
         // ignore: unused_field
      @@ -294,9 +295,11 @@ class HyperSelectionOverlayState extends State
           final renderBox = _renderBox;
           if (renderBox == null) return;
       
      +    final rects = renderBox.getSelectionRects();
           setState(() {
      -      _startHandleRect = renderBox.getStartHandleRect();
      -      _endHandleRect = renderBox.getEndHandleRect();
      +      _selectionRects = rects;
      +      _startHandleRect = rects.isEmpty ? null : rects.first;
      +      _endHandleRect = rects.isEmpty ? null : rects.last;
           });
         }
       
      @@ -335,29 +338,18 @@ class HyperSelectionOverlayState extends State
           return KeyEventResult.ignored;
         }
       
      -  /// Calculate menu position above selection
      +  /// Calculate menu position above selection (uses cached rects โ€” no extra pass)
         Offset _calculateMenuPosition() {
      -    if (_startHandleRect == null) return Offset.zero;
      +    if (_selectionRects.isEmpty) return Offset.zero;
       
      -    final RenderBox? box =
      -        _renderKey.currentContext?.findRenderObject() as RenderBox?;
      -    if (box == null) return Offset.zero;
      -
      -    // Get all selection rects to find the topmost point
      -    final selectionRects = _renderBox?.getSelectionRects() ?? [];
      -    if (selectionRects.isEmpty) return Offset.zero;
      -
      -    // Find the topmost and leftmost point of selection
           double minY = double.infinity;
           double centerX = 0;
      -    for (final rect in selectionRects) {
      +    for (final rect in _selectionRects) {
             if (rect.top < minY) {
               minY = rect.top;
               centerX = rect.center.dx;
             }
           }
      -
      -    // Position menu above selection with some padding
           return Offset(centerX, minY - 8);
         }
       
      @@ -466,13 +458,15 @@ class HyperSelectionOverlayState extends State
           final localPosition = scrollableBox.globalToLocal(globalPosition);
           final size = scrollableBox.size;
       
      -    const threshold = 50.0;
      +    // Speed scales linearly with proximity: 0 px/event at threshold edge โ†’ 20 px/event at screen edge.
      +    const threshold = 60.0;
      +    const maxStep = 20.0;
           double dy = 0.0;
       
           if (localPosition.dy < threshold) {
      -      dy = -15.0;
      +      dy = -maxStep * (1.0 - localPosition.dy / threshold);
           } else if (localPosition.dy > size.height - threshold) {
      -      dy = 15.0;
      +      dy = maxStep * (1.0 - (size.height - localPosition.dy) / threshold);
           }
       
           if (dy != 0.0) {
      @@ -531,7 +525,7 @@ class HyperSelectionOverlayState extends State
               },
               child: CustomPaint(
                 size: const Size(22, 22),
      -          painter: _TeardropHandlePainter(
      +          painter: HyperTeardropHandlePainter(
                   color: widget.handleColor,
                   isStart: isStart,
                 ),
      @@ -623,11 +617,11 @@ class _ContextMenuButton extends StatelessWidget {
       }
       
       /// Custom painter for native-style teardrop selection handles
      -class _TeardropHandlePainter extends CustomPainter {
      +class HyperTeardropHandlePainter extends CustomPainter {
         final Color color;
         final bool isStart;
       
      -  _TeardropHandlePainter({
      +  HyperTeardropHandlePainter({
           required this.color,
           required this.isStart,
         });
      @@ -681,7 +675,7 @@ class _TeardropHandlePainter extends CustomPainter {
         }
       
         @override
      -  bool shouldRepaint(_TeardropHandlePainter oldDelegate) {
      +  bool shouldRepaint(HyperTeardropHandlePainter oldDelegate) {
           return color != oldDelegate.color || isStart != oldDelegate.isStart;
         }
       }
      diff --git a/packages/hyper_render_core/pubspec.yaml b/packages/hyper_render_core/pubspec.yaml
      index cd346fd..fdba64f 100644
      --- a/packages/hyper_render_core/pubspec.yaml
      +++ b/packages/hyper_render_core/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render_core
       description: Core engine for HyperRender. Universal Document Tree, single-RenderObject layout with CSS float, Flexbox, Grid, CJK typography, and crash-free text selection.
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_core
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      diff --git a/packages/hyper_render_devtools/CHANGELOG.md b/packages/hyper_render_devtools/CHANGELOG.md
      index 356b0f0..9e530cd 100644
      --- a/packages/hyper_render_devtools/CHANGELOG.md
      +++ b/packages/hyper_render_devtools/CHANGELOG.md
      @@ -1,5 +1,10 @@
       # Changelog โ€” hyper_render_devtools
       
      +## [1.3.1] - 2026-05-14
      +
      +### ๐Ÿ—๏ธ Maintenance
      +- Updated `hyper_render_core` dependency to `^1.3.1`
      +
       ## [1.3.0] - 2026-05-03
       
       ### โœจ New Features
      diff --git a/packages/hyper_render_devtools/README.md b/packages/hyper_render_devtools/README.md
      index b25b87d..4ce19f6 100644
      --- a/packages/hyper_render_devtools/README.md
      +++ b/packages/hyper_render_devtools/README.md
      @@ -17,14 +17,14 @@ Add the package to your app's `dev_dependencies` (debug-only usage):
       
       ```yaml
       dev_dependencies:
      -  hyper_render_devtools: ^1.3.0
      +  hyper_render_devtools: ^1.3.1
       ```
       
       Or add to `dependencies` if you want it available in profile builds:
       
       ```yaml
       dependencies:
      -  hyper_render_devtools: ^1.3.0
      +  hyper_render_devtools: ^1.3.1
       ```
       
       ## Usage
      @@ -108,4 +108,17 @@ The `devtools_ui` sub-directory contains the Flutter Web source for the panel UI
       
       ---
       
      -[hyper_render](https://pub.dev/packages/hyper_render) ยท [Issue tracker](https://github.com/brewkits/hyper_render/issues) ยท [Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_devtools)
      +## HyperRender Ecosystem
      +
      +| Package | Description |
      +|---------|-------------|
      +| [hyper_render](https://pub.dev/packages/hyper_render) | Main package โ€” `HyperViewer` widget, HTML + Markdown rendering |
      +| [hyper_render_core](https://pub.dev/packages/hyper_render_core) | Core engine: UDT model, `RenderHyperBox`, plugin API |
      +| [hyper_render_html](https://pub.dev/packages/hyper_render_html) | HTML + CSS โ†’ UDT parser |
      +| [hyper_render_markdown](https://pub.dev/packages/hyper_render_markdown) | Markdown (GFM) โ†’ UDT parser |
      +| [hyper_render_highlight](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `` / `
      ` blocks |
      +| [hyper_render_clipboard](https://pub.dev/packages/hyper_render_clipboard) | Image copy / save / share *(opt-in)* |
      +| [hyper_render_math](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML rendering *(opt-in)* |
      +| **[hyper_render_devtools](https://pub.dev/packages/hyper_render_devtools)** | **Flutter DevTools inspector** โ† you are here |
      +
      +[Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_devtools) ยท [Issues](https://github.com/brewkits/hyper_render/issues) ยท [Changelog](CHANGELOG.md)
      diff --git a/packages/hyper_render_devtools/devtools_ui/pubspec.yaml b/packages/hyper_render_devtools/devtools_ui/pubspec.yaml
      index 0d6414c..c1d85e7 100644
      --- a/packages/hyper_render_devtools/devtools_ui/pubspec.yaml
      +++ b/packages/hyper_render_devtools/devtools_ui/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render_devtools_ui
       description: "DevTools panel UI for HyperRender โ€” tree inspector, style viewer, layout debug."
      -version: 1.3.0
      +version: 1.3.1
       publish_to: 'none'
       
       environment:
      diff --git a/packages/hyper_render_devtools/pubspec.yaml b/packages/hyper_render_devtools/pubspec.yaml
      index cc67a64..038f2c9 100644
      --- a/packages/hyper_render_devtools/pubspec.yaml
      +++ b/packages/hyper_render_devtools/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render_devtools
       description: "DevTools extension for HyperRender โ€” inspect UDT trees, computed styles, fragments, and performance metrics."
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_devtools
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      diff --git a/packages/hyper_render_highlight/CHANGELOG.md b/packages/hyper_render_highlight/CHANGELOG.md
      index baa41f5..23a6362 100644
      --- a/packages/hyper_render_highlight/CHANGELOG.md
      +++ b/packages/hyper_render_highlight/CHANGELOG.md
      @@ -1,5 +1,10 @@
       # Changelog โ€” hyper_render_highlight
       
      +## [1.3.1] - 2026-05-14
      +
      +### ๐Ÿ—๏ธ Maintenance
      +- Updated `hyper_render_core` dependency to `^1.3.1`
      +
       ## [1.3.0] - 2026-05-03
       
       ### โœจ New Features
      diff --git a/packages/hyper_render_highlight/README.md b/packages/hyper_render_highlight/README.md
      index 77abace..cab0b0b 100644
      --- a/packages/hyper_render_highlight/README.md
      +++ b/packages/hyper_render_highlight/README.md
      @@ -8,8 +8,8 @@ Syntax highlighting plugin for [HyperRender](https://pub.dev/packages/hyper_rend
       
       ```yaml
       dependencies:
      -  hyper_render_core: ^1.3.0
      -  hyper_render_highlight: ^1.3.0
      +  hyper_render_core: ^1.3.1
      +  hyper_render_highlight: ^1.3.1
       ```
       
       ---
      @@ -75,6 +75,23 @@ print(FlutterHighlightCodeHighlighter().supportedLanguages);
       
       ---
       
      +## HyperRender Ecosystem
      +
      +| Package | Description |
      +|---------|-------------|
      +| [hyper_render](https://pub.dev/packages/hyper_render) | Main package โ€” `HyperViewer` widget, HTML + Markdown rendering |
      +| [hyper_render_core](https://pub.dev/packages/hyper_render_core) | Core engine: UDT model, `RenderHyperBox`, plugin API |
      +| [hyper_render_html](https://pub.dev/packages/hyper_render_html) | HTML + CSS โ†’ UDT parser |
      +| [hyper_render_markdown](https://pub.dev/packages/hyper_render_markdown) | Markdown (GFM) โ†’ UDT parser |
      +| **[hyper_render_highlight](https://pub.dev/packages/hyper_render_highlight)** | **Syntax highlighting for `` / `
      ` blocks** โ† you are here |
      +| [hyper_render_clipboard](https://pub.dev/packages/hyper_render_clipboard) | Image copy / save / share *(opt-in)* |
      +| [hyper_render_math](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML rendering *(opt-in)* |
      +| [hyper_render_devtools](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools inspector |
      +
      +[Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_highlight) ยท [Issues](https://github.com/brewkits/hyper_render/issues) ยท [Changelog](CHANGELOG.md)
      +
      +---
      +
       ## License
       
       MIT โ€” see [LICENSE](LICENSE).
      diff --git a/packages/hyper_render_highlight/pubspec.yaml b/packages/hyper_render_highlight/pubspec.yaml
      index 14defca..34840d7 100644
      --- a/packages/hyper_render_highlight/pubspec.yaml
      +++ b/packages/hyper_render_highlight/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render_highlight
       description: Syntax highlighting plugin for HyperRender using flutter_highlight. Supports 180+ programming languages with multiple themes.
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_highlight
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      diff --git a/packages/hyper_render_html/CHANGELOG.md b/packages/hyper_render_html/CHANGELOG.md
      index 989f04a..3f6af3c 100644
      --- a/packages/hyper_render_html/CHANGELOG.md
      +++ b/packages/hyper_render_html/CHANGELOG.md
      @@ -1,5 +1,10 @@
       # Changelog โ€” hyper_render_html
       
      +## [1.3.1] - 2026-05-14
      +
      +### ๐Ÿ—๏ธ Maintenance
      +- Updated `hyper_render_core` dependency to `^1.3.1`
      +
       ## [1.3.0] - 2026-05-03
       
       ### โœจ New Features
      diff --git a/packages/hyper_render_html/README.md b/packages/hyper_render_html/README.md
      index 26d5d25..9a667b8 100644
      --- a/packages/hyper_render_html/README.md
      +++ b/packages/hyper_render_html/README.md
      @@ -8,8 +8,8 @@ HTML parsing plugin for [HyperRender](https://pub.dev/packages/hyper_render). Co
       
       ```yaml
       dependencies:
      -  hyper_render_core: ^1.3.0
      -  hyper_render_html: ^1.3.0
      +  hyper_render_core: ^1.3.1
      +  hyper_render_html: ^1.3.1
       ```
       
       ---
      @@ -76,12 +76,31 @@ final document = HtmlContentParser().parseWithOptions(
       
       **Layout**: `display` (block, inline, inline-block, flex, grid, none), `float`, `clear`, `overflow`, `position` (static, relative)
       
      -**Visual**: `background-color`, `background-image`, `opacity`, `transform`, `filter`, `backdrop-filter`
      +**List**: `list-style-type` (disc/circle/square/decimal/lower|upper-alpha|latin|roman/none), `list-style-position`, `list-style` shorthand
      +
      +**Visual**: `background-color`, `background-image`, `background-repeat`, `background-position`, `opacity`, `transform`, `filter`, `backdrop-filter`
       
       **CSS features**: custom properties (`var()`), `calc()`, `@keyframes`, specificity cascade, inheritance
       
       ---
       
      +## HyperRender Ecosystem
      +
      +| Package | Description |
      +|---------|-------------|
      +| [hyper_render](https://pub.dev/packages/hyper_render) | Main package โ€” `HyperViewer` widget, HTML + Markdown rendering |
      +| [hyper_render_core](https://pub.dev/packages/hyper_render_core) | Core engine: UDT model, `RenderHyperBox`, plugin API |
      +| **[hyper_render_html](https://pub.dev/packages/hyper_render_html)** | **HTML + CSS โ†’ UDT parser** โ† you are here |
      +| [hyper_render_markdown](https://pub.dev/packages/hyper_render_markdown) | Markdown (GFM) โ†’ UDT parser |
      +| [hyper_render_highlight](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `` / `
      ` blocks |
      +| [hyper_render_clipboard](https://pub.dev/packages/hyper_render_clipboard) | Image copy / save / share *(opt-in)* |
      +| [hyper_render_math](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML rendering *(opt-in)* |
      +| [hyper_render_devtools](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools inspector |
      +
      +[Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_html) ยท [Issues](https://github.com/brewkits/hyper_render/issues) ยท [Changelog](CHANGELOG.md)
      +
      +---
      +
       ## License
       
       MIT โ€” see [LICENSE](LICENSE).
      diff --git a/packages/hyper_render_html/pubspec.yaml b/packages/hyper_render_html/pubspec.yaml
      index d46ada9..e69c9c1 100644
      --- a/packages/hyper_render_html/pubspec.yaml
      +++ b/packages/hyper_render_html/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render_html
       description: HTML parsing plugin for HyperRender. Converts HTML content to UDT with full CSS support.
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_html
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      diff --git a/packages/hyper_render_markdown/CHANGELOG.md b/packages/hyper_render_markdown/CHANGELOG.md
      index 593f6cd..0c8cb8e 100644
      --- a/packages/hyper_render_markdown/CHANGELOG.md
      +++ b/packages/hyper_render_markdown/CHANGELOG.md
      @@ -1,5 +1,10 @@
       # Changelog โ€” hyper_render_markdown
       
      +## [1.3.1] - 2026-05-14
      +
      +### ๐Ÿ—๏ธ Maintenance
      +- Updated `hyper_render_core` dependency to `^1.3.1`
      +
       ## [1.3.0] - 2026-05-03
       
       ### โœจ New Features
      diff --git a/packages/hyper_render_markdown/README.md b/packages/hyper_render_markdown/README.md
      index 28ad617..068e1fb 100644
      --- a/packages/hyper_render_markdown/README.md
      +++ b/packages/hyper_render_markdown/README.md
      @@ -8,8 +8,8 @@ Markdown parsing plugin for [HyperRender](https://pub.dev/packages/hyper_render)
       
       ```yaml
       dependencies:
      -  hyper_render_core: ^1.3.0
      -  hyper_render_markdown: ^1.3.0
      +  hyper_render_core: ^1.3.1
      +  hyper_render_markdown: ^1.3.1
       ```
       
       ---
      @@ -72,6 +72,23 @@ HyperRenderWidget(
       
       ---
       
      +## HyperRender Ecosystem
      +
      +| Package | Description |
      +|---------|-------------|
      +| [hyper_render](https://pub.dev/packages/hyper_render) | Main package โ€” `HyperViewer` widget, HTML + Markdown rendering |
      +| [hyper_render_core](https://pub.dev/packages/hyper_render_core) | Core engine: UDT model, `RenderHyperBox`, plugin API |
      +| [hyper_render_html](https://pub.dev/packages/hyper_render_html) | HTML + CSS โ†’ UDT parser |
      +| **[hyper_render_markdown](https://pub.dev/packages/hyper_render_markdown)** | **Markdown (GFM) โ†’ UDT parser** โ† you are here |
      +| [hyper_render_highlight](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `` / `
      ` blocks |
      +| [hyper_render_clipboard](https://pub.dev/packages/hyper_render_clipboard) | Image copy / save / share *(opt-in)* |
      +| [hyper_render_math](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML rendering *(opt-in)* |
      +| [hyper_render_devtools](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools inspector |
      +
      +[Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_markdown) ยท [Issues](https://github.com/brewkits/hyper_render/issues) ยท [Changelog](CHANGELOG.md)
      +
      +---
      +
       ## License
       
       MIT โ€” see [LICENSE](LICENSE).
      diff --git a/packages/hyper_render_markdown/pubspec.yaml b/packages/hyper_render_markdown/pubspec.yaml
      index 679e16d..701042d 100644
      --- a/packages/hyper_render_markdown/pubspec.yaml
      +++ b/packages/hyper_render_markdown/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render_markdown
       description: Markdown parser plugin for HyperRender - Converts Markdown to Universal Document Tree (UDT).
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_markdown
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      diff --git a/packages/hyper_render_markdown/test/markdown_adapter_test.dart b/packages/hyper_render_markdown/test/markdown_adapter_test.dart
      index 4be10ce..7d588ed 100644
      --- a/packages/hyper_render_markdown/test/markdown_adapter_test.dart
      +++ b/packages/hyper_render_markdown/test/markdown_adapter_test.dart
      @@ -76,7 +76,10 @@ void main() {
             final ul = result.document.children[0] as BlockNode;
             // md package generates 
    • ... expect(ul.children[0], isA()); - expect((ul.children[0] as BlockNode).attributes['data-task'], isNotNull); + final firstChild = (ul.children[0] as BlockNode).children.first; + expect(firstChild, isA()); + expect((firstChild as AtomicNode).tagName, 'input'); + expect(firstChild.attributes['type'], 'checkbox'); }); test('parse line break', () { diff --git a/packages/hyper_render_math/CHANGELOG.md b/packages/hyper_render_math/CHANGELOG.md index 0222dc7..b8a199d 100644 --- a/packages/hyper_render_math/CHANGELOG.md +++ b/packages/hyper_render_math/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog โ€” hyper_render_math +## [1.3.1] - 2026-05-14 + +### ๐Ÿ—๏ธ Packaging +- **Opt-in add-on**: `hyper_render_math` is no longer bundled with the root `hyper_render` package. Add it explicitly to your `pubspec.yaml` to use `MathNodePlugin` / `LatexNodePlugin`. +- Updated `hyper_render_core` dependency to `^1.3.1` + ## [1.3.0] - 2026-05-03 - Initial release: skeleton plugin for `` and `` tag rendering. diff --git a/packages/hyper_render_math/README.md b/packages/hyper_render_math/README.md index d7d1ff8..cdc5f2d 100644 --- a/packages/hyper_render_math/README.md +++ b/packages/hyper_render_math/README.md @@ -9,8 +9,7 @@ Renders `` and `` tags using [flutter_math_fork](https://pub.dev/pa ```yaml dependencies: - hyper_render_core: ^1.3.0 - hyper_render_math: ^1.3.0 + hyper_render_math: ^1.3.1 ``` --- @@ -64,7 +63,23 @@ so you can identify and fix the malformed formula. --- -## Contributing +## HyperRender Ecosystem -See [PLUGIN_DEVELOPMENT.md](../../doc/PLUGIN_DEVELOPMENT.md) for the full guide -on building, testing, and submitting plugins. +| Package | Description | +|---------|-------------| +| [hyper_render](https://pub.dev/packages/hyper_render) | Main package โ€” `HyperViewer` widget, HTML + Markdown rendering | +| [hyper_render_core](https://pub.dev/packages/hyper_render_core) | Core engine: UDT model, `RenderHyperBox`, plugin API | +| [hyper_render_html](https://pub.dev/packages/hyper_render_html) | HTML + CSS โ†’ UDT parser | +| [hyper_render_markdown](https://pub.dev/packages/hyper_render_markdown) | Markdown (GFM) โ†’ UDT parser | +| [hyper_render_highlight](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `` / `
      ` blocks |
      +| [hyper_render_clipboard](https://pub.dev/packages/hyper_render_clipboard) | Image copy / save / share *(opt-in)* |
      +| **[hyper_render_math](https://pub.dev/packages/hyper_render_math)** | **LaTeX / MathML rendering** โ† you are here |
      +| [hyper_render_devtools](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools inspector |
      +
      +[Source](https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_math) ยท [Issues](https://github.com/brewkits/hyper_render/issues) ยท [Changelog](CHANGELOG.md) ยท [Plugin Development Guide](https://github.com/brewkits/hyper_render/blob/main/doc/PLUGIN_DEVELOPMENT.md)
      +
      +---
      +
      +## License
      +
      +MIT โ€” see [LICENSE](LICENSE).
      diff --git a/packages/hyper_render_math/pubspec.yaml b/packages/hyper_render_math/pubspec.yaml
      index 8743aca..f48a71b 100644
      --- a/packages/hyper_render_math/pubspec.yaml
      +++ b/packages/hyper_render_math/pubspec.yaml
      @@ -2,7 +2,7 @@ name: hyper_render_math
       description: >
         Mathematical formula rendering plugin for HyperRender. Supports LaTeX and MathML
         using the flutter_math_fork engine.
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render/tree/main/packages/hyper_render_math
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      diff --git a/pubspec.yaml b/pubspec.yaml
      index 4fd75b3..6ade52e 100644
      --- a/pubspec.yaml
      +++ b/pubspec.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render
       description: "Render HTML/Markdown/Delta at 60 FPS. The only Flutter renderer with CSS float layout, crash-free text selection, and CJK Ruby typography. Drop-in flutter_html alternative."
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      @@ -23,21 +23,17 @@ dependencies:
       
         # ============================================
         # MODULAR ARCHITECTURE v1.0
      -  # This package is a convenience wrapper that includes all plugins
      -  # For minimal dependencies, use individual packages:
      -  #   - hyper_render_core: Zero-dep core
      -  #   - hyper_render_html: HTML parsing
      -  #   - hyper_render_markdown: Markdown parsing
      -  #   - hyper_render_highlight: Syntax highlighting
      +  # This package is a convenience wrapper that includes core plugins.
      +  # Optional add-ons (install separately if needed):
      +  #   - hyper_render_clipboard: Image copy/share (adds super_clipboard native dep)
      +  #   - hyper_render_math: LaTeX/MathML rendering (adds flutter_math_fork)
         # ============================================
       
         # NOTE: For publishing to pub.dev, replace path dependencies with versions:
      -  #   hyper_render_core: ^1.3.0
      -  #   hyper_render_html: ^1.3.0
      -  #   hyper_render_markdown: ^1.3.0
      -  #   hyper_render_highlight: ^1.3.0
      -  #   hyper_render_clipboard: ^1.3.0
      -  #   hyper_render_math: ^1.3.0
      +  #   hyper_render_core: ^1.3.1
      +  #   hyper_render_html: ^1.3.1
      +  #   hyper_render_markdown: ^1.3.1
      +  #   hyper_render_highlight: ^1.3.1
       
         hyper_render_core:
           path: packages/hyper_render_core
      @@ -51,12 +47,6 @@ dependencies:
         hyper_render_highlight:
           path: packages/hyper_render_highlight
       
      -  hyper_render_clipboard:
      -    path: packages/hyper_render_clipboard
      -
      -  hyper_render_math:
      -    path: packages/hyper_render_math
      -
         # Fixed version constraints for pub.dev compliance
         flutter_highlight: ^0.7.0
         highlight: ^0.7.0
      diff --git a/pubspec_publish_ready.yaml b/pubspec_publish_ready.yaml
      index a00b0c1..7245bdc 100644
      --- a/pubspec_publish_ready.yaml
      +++ b/pubspec_publish_ready.yaml
      @@ -1,6 +1,6 @@
       name: hyper_render
       description: "Render HTML/Markdown/Delta at 60 FPS. The only Flutter renderer with CSS float layout, crash-free text selection, and CJK Ruby typography. Drop-in flutter_html alternative."
      -version: 1.3.0
      +version: 1.3.1
       homepage: https://github.com/brewkits/hyper_render
       repository: https://github.com/brewkits/hyper_render
       issue_tracker: https://github.com/brewkits/hyper_render/issues
      @@ -34,12 +34,10 @@ dependencies:
         flutter:
           sdk: flutter
       
      -  hyper_render_core: ^1.3.0
      -  hyper_render_html: ^1.3.0
      -  hyper_render_markdown: ^1.3.0
      -  hyper_render_highlight: ^1.3.0
      -  hyper_render_clipboard: ^1.3.0
      -  hyper_render_math: ^1.3.0
      +  hyper_render_core: ^1.3.1
      +  hyper_render_html: ^1.3.1
      +  hyper_render_markdown: ^1.3.1
      +  hyper_render_highlight: ^1.3.1
       
         flutter_highlight: ^0.7.0
         highlight: ^0.7.0
      diff --git a/scripts/prepare_publish.sh b/scripts/prepare_publish.sh
      index 664c173..b0925ae 100755
      --- a/scripts/prepare_publish.sh
      +++ b/scripts/prepare_publish.sh
      @@ -77,7 +77,7 @@ else
         ok "pubspec.yaml already has version deps"
       fi
       
      -# Swap path: ../hyper_render_core โ†’ hyper_render_core: ^1.3.0 in sub-packages
      +# Swap path: ../hyper_render_core โ†’ hyper_render_core: ^1.3.1 in sub-packages
       # BSD sed (macOS) requires '' after -i; GNU sed does not
       _sed() { sed -i '' "$@" 2>/dev/null || sed -i "$@"; }
       
      @@ -89,7 +89,7 @@ for f in packages/hyper_render_html/pubspec.yaml \
                 packages/hyper_render_math/pubspec.yaml; do
         if [ -f "$f" ] && grep -q "path: \.\./hyper_render_core" "$f"; then
           _sed '/hyper_render_core:/{n;/path: \.\.\/hyper_render_core/d;}' "$f"
      -    _sed 's|hyper_render_core:$|hyper_render_core: ^1.3.0|' "$f"
      +    _sed 's|hyper_render_core:$|hyper_render_core: ^1.3.1|' "$f"
           ok "Swapped path dep โ†’ version dep in $f"
         fi
       done
      diff --git a/scripts/publish.sh b/scripts/publish.sh
      index 559aa0c..7b48152 100755
      --- a/scripts/publish.sh
      +++ b/scripts/publish.sh
      @@ -1,6 +1,6 @@
       #!/usr/bin/env bash
       # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      -# HyperRender publish helper โ€” v1.3.0
      +# HyperRender publish helper โ€” v1.3.1
       #
       # Usage:
       #   ./scripts/publish.sh dry-run   # verify all packages (no upload)
      @@ -21,7 +21,7 @@
       
       set -euo pipefail
       
      -VERSION="1.3.0"
      +VERSION="1.3.1"
       MODE="${1:-dry-run}"   # dry-run | publish
       ROOT="$(cd "$(dirname "$0")/.." && pwd)"
       
      diff --git a/test/golden/goldens/blockquote.png b/test/golden/goldens/blockquote.png
      index e6c312d..19b4db2 100644
      Binary files a/test/golden/goldens/blockquote.png and b/test/golden/goldens/blockquote.png differ
      diff --git a/test/golden/goldens/cjk_kinsoku.png b/test/golden/goldens/cjk_kinsoku.png
      index cc73778..86dbd66 100644
      Binary files a/test/golden/goldens/cjk_kinsoku.png and b/test/golden/goldens/cjk_kinsoku.png differ
      diff --git a/test/golden/goldens/cjk_ruby.png b/test/golden/goldens/cjk_ruby.png
      index b9358c3..2059191 100644
      Binary files a/test/golden/goldens/cjk_ruby.png and b/test/golden/goldens/cjk_ruby.png differ
      diff --git a/test/golden/goldens/code_block.png b/test/golden/goldens/code_block.png
      index 032f78d..6427612 100644
      Binary files a/test/golden/goldens/code_block.png and b/test/golden/goldens/code_block.png differ
      diff --git a/test/golden/goldens/definition_list.png b/test/golden/goldens/definition_list.png
      index 549ceee..a512127 100644
      Binary files a/test/golden/goldens/definition_list.png and b/test/golden/goldens/definition_list.png differ
      diff --git a/test/golden/goldens/float_cjk.png b/test/golden/goldens/float_cjk.png
      index 484422f..791f9e0 100644
      Binary files a/test/golden/goldens/float_cjk.png and b/test/golden/goldens/float_cjk.png differ
      diff --git a/test/golden/goldens/float_clear.png b/test/golden/goldens/float_clear.png
      index a676499..872fe7d 100644
      Binary files a/test/golden/goldens/float_clear.png and b/test/golden/goldens/float_clear.png differ
      diff --git a/test/golden/goldens/float_left.png b/test/golden/goldens/float_left.png
      index abdefaa..7b5efbc 100644
      Binary files a/test/golden/goldens/float_left.png and b/test/golden/goldens/float_left.png differ
      diff --git a/test/golden/goldens/float_right.png b/test/golden/goldens/float_right.png
      index 40b825e..4fe27f0 100644
      Binary files a/test/golden/goldens/float_right.png and b/test/golden/goldens/float_right.png differ
      diff --git a/test/golden/goldens/full_article.png b/test/golden/goldens/full_article.png
      index ad1f06b..6c07c27 100644
      Binary files a/test/golden/goldens/full_article.png and b/test/golden/goldens/full_article.png differ
      diff --git a/test/golden/goldens/headings.png b/test/golden/goldens/headings.png
      index 9d501bc..232a98e 100644
      Binary files a/test/golden/goldens/headings.png and b/test/golden/goldens/headings.png differ
      diff --git a/test/golden/goldens/image_placeholder.png b/test/golden/goldens/image_placeholder.png
      index 6ff8ba4..dd98331 100644
      Binary files a/test/golden/goldens/image_placeholder.png and b/test/golden/goldens/image_placeholder.png differ
      diff --git a/test/golden/goldens/links.png b/test/golden/goldens/links.png
      index 9a6fe8d..209c29a 100644
      Binary files a/test/golden/goldens/links.png and b/test/golden/goldens/links.png differ
      diff --git a/test/golden/goldens/mixed_inline.png b/test/golden/goldens/mixed_inline.png
      index 767bc53..de925a1 100644
      Binary files a/test/golden/goldens/mixed_inline.png and b/test/golden/goldens/mixed_inline.png differ
      diff --git a/test/golden/goldens/mixed_lists.png b/test/golden/goldens/mixed_lists.png
      index 22fc9b7..aa0ed93 100644
      Binary files a/test/golden/goldens/mixed_lists.png and b/test/golden/goldens/mixed_lists.png differ
      diff --git a/test/golden/goldens/ordered_list.png b/test/golden/goldens/ordered_list.png
      index d0f9475..33a9914 100644
      Binary files a/test/golden/goldens/ordered_list.png and b/test/golden/goldens/ordered_list.png differ
      diff --git a/test/golden/goldens/rtl_arabic.png b/test/golden/goldens/rtl_arabic.png
      index 9e32c15..7829608 100644
      Binary files a/test/golden/goldens/rtl_arabic.png and b/test/golden/goldens/rtl_arabic.png differ
      diff --git a/test/golden/goldens/rtl_hebrew.png b/test/golden/goldens/rtl_hebrew.png
      index c37f27f..971d1d5 100644
      Binary files a/test/golden/goldens/rtl_hebrew.png and b/test/golden/goldens/rtl_hebrew.png differ
      diff --git a/test/golden/goldens/rtl_mixed.png b/test/golden/goldens/rtl_mixed.png
      index ef08cde..d8ed84d 100644
      Binary files a/test/golden/goldens/rtl_mixed.png and b/test/golden/goldens/rtl_mixed.png differ
      diff --git a/test/golden/goldens/table.png b/test/golden/goldens/table.png
      index 8babef5..a118f8f 100644
      Binary files a/test/golden/goldens/table.png and b/test/golden/goldens/table.png differ
      diff --git a/test/golden/goldens/table_spans.png b/test/golden/goldens/table_spans.png
      index 7965e85..dd1c7c0 100644
      Binary files a/test/golden/goldens/table_spans.png and b/test/golden/goldens/table_spans.png differ
      diff --git a/test/golden/goldens/text_formatting.png b/test/golden/goldens/text_formatting.png
      index fbafe7f..d0079d5 100644
      Binary files a/test/golden/goldens/text_formatting.png and b/test/golden/goldens/text_formatting.png differ
      diff --git a/test/golden/goldens/unordered_list.png b/test/golden/goldens/unordered_list.png
      index e25aa0d..bdea670 100644
      Binary files a/test/golden/goldens/unordered_list.png and b/test/golden/goldens/unordered_list.png differ
      diff --git a/test/style/css_bug_fixes_test.dart b/test/style/css_bug_fixes_test.dart
      index e348d1c..cab7f57 100644
      --- a/test/style/css_bug_fixes_test.dart
      +++ b/test/style/css_bug_fixes_test.dart
      @@ -601,4 +601,94 @@ void main() {
             expect(linkNode!.style.color, const Color(0xFFFF0000));
           });
         });
      +
      +  // ---------------------------------------------------------------------------
      +  // list-style-type / list-style-position / list-style shorthand
      +  // ---------------------------------------------------------------------------
      +  group('list-style-type', () {
      +    test('default ul uses disc', () {
      +      final style = _styleOfTag('
      • item
      ', 'li'); + // No explicit list-style-type โ†’ null (resolved to disc in layout) + expect(style?.listStyleType, isNull); + }); + + test('list-style-type: none is parsed', () { + final style = _styleOfTag( + '
      • item
      ', + 'ul', + ); + expect(style?.listStyleType, 'none'); + }); + + test('list-style-type: circle on li', () { + final style = _styleOfTag( + '
      • item
      ', + 'li', + ); + expect(style?.listStyleType, 'circle'); + }); + + test('list-style-type: upper-roman', () { + final style = _styleOfTag( + '
      1. item
      ', + 'ol', + ); + expect(style?.listStyleType, 'upper-roman'); + }); + + test('list-style-position: inside', () { + final style = _styleOfTag( + '
      • item
      ', + 'ul', + ); + expect(style?.listStylePosition, 'inside'); + }); + + test('list-style shorthand: none sets type to none', () { + final style = _styleOfTag( + '
      • item
      ', + 'ul', + ); + expect(style?.listStyleType, 'none'); + }); + + test('list-style shorthand: square inside', () { + final style = _styleOfTag( + '
      • item
      ', + 'ul', + ); + expect(style?.listStyleType, 'square'); + expect(style?.listStylePosition, 'inside'); + }); + + test('invalid value is ignored', () { + final style = _styleOfTag( + '
      • item
      ', + 'ul', + ); + expect(style?.listStyleType, isNull); + }); + + testWidgets('list-style:none nav menu renders without crash', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: HyperViewer( + html: ''' +''', + ), + ), + ), + ); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + }); }