From f9c5508d4d456285aa800a6881b98d3779289e26 Mon Sep 17 00:00:00 2001 From: Sergey Drymchenko <2linreal@gmail.com> Date: Mon, 27 Jul 2026 11:48:35 +0300 Subject: [PATCH] T-31 empty placeholder support --- ARCHITECTURE.md | 20 ++- docs/ReadOnlyMode.md | 2 + docs/ThemingAndLocalization.md | 15 +- docs/iOsNativeSdk.md | 8 +- docs/recipes/EmptyDocumentPlaceholder.md | 72 ++++++++++ docs/recipes/ThemeCustomization.md | 6 +- editor-ios-sdk/api/editor-ios-sdk.klib.api | 15 +- .../controller/CascadeEditorConfiguration.kt | 49 ++----- .../controller/CascadeEditorViewController.kt | 2 + .../CascadeEditorStringsBridge.kt | 5 + .../ios/theme/CascadeEditorColorsBridge.kt | 4 + .../controller/CascadeEditorControllerTest.kt | 1 + .../localization/CascadeLocalizationTest.kt | 2 + .../theme/CascadeEditorColorsBridgeTest.kt | 3 + editor/api/android/editor.api | 31 +++-- editor/api/desktop/editor.api | 31 +++-- editor/api/editor.klib.api | 22 ++- .../editor/theme/CascadeEditorColors.kt | 4 + .../editor/theme/CascadeEditorStrings.kt | 4 + .../editor/ui/BackspaceAwareTextEdit.kt | 9 ++ .../cascade/editor/ui/CascadeEditorConfig.kt | 23 +-- .../editor/ui/renderers/TextBlockField.kt | 60 ++++++++ .../cascade/editor/CascadeEditorColorsTest.kt | 9 ++ .../cascade/editor/CascadeEditorConfigTest.kt | 2 + .../editor/CascadeEditorStringsTest.kt | 13 ++ .../editor/DocumentSerializationExtTest.kt | 11 ++ .../ui/SentinelGuardClassificationTest.kt | 28 ++++ .../renderers/EmptyDocumentPlaceholderTest.kt | 116 ++++++++++++++++ .../editor/ui/CascadeEditorPlaceholderTest.kt | 131 ++++++++++++++++++ .../App/SmokeViewController.swift | 4 +- .../iosNativeSample/App/AppTheme.swift | 6 +- .../Editor/EditorScreenModel.swift | 4 +- .../cascade/screens/EditorDemoScreen.kt | 5 +- 33 files changed, 600 insertions(+), 117 deletions(-) create mode 100644 docs/recipes/EmptyDocumentPlaceholder.md create mode 100644 editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/renderers/EmptyDocumentPlaceholderTest.kt create mode 100644 editor/src/desktopTest/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorPlaceholderTest.kt diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2ff6dea..a471cdb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -20,12 +20,13 @@ Block-based editor (Craft/Notion-like) for Compose Multiplatform. Unidirectional | Native iOS preview-grid integration | `iosNativeSample/iosNativeSample/Editor/CascadeDocumentPreviewHost.swift`, `iosNativeSample/iosNativeSample/Screens/PreviewGalleryScreen.swift`, `iosNativeSample/iosNativeSample/Screens/PreviewDocumentEditorScreen.swift`, `iosNativeSample/iosNativeSample/Screens/Preview/` | `CascadeDocumentPreviewHost`, `PreviewGalleryScreen`, `PreviewDocumentLibrary`, `PreviewCardModel`, `PreviewDocumentEditorModel` (seeded/persisted JSON, one owned editor session per destination, debounced autosave plus background/exit flush, preview refresh after successful saves) | | Native iOS SDK technical context | `docs/iOsNativeSdk.md` | Editable and preview Swift facades, bounded UIKit host lifecycle, native custom blocks/slash commands, runtime colors, public API, integration constraints | | Theme customization recipes | `docs/recipes/ThemeCustomization.md` | Agent-oriented Compose Multiplatform and native Swift tutorials for complete palettes and family-aware runtime appearance switching | +| Empty-document placeholder recipe | `docs/recipes/EmptyDocumentPlaceholder.md` | Short Compose Multiplatform and native Swift setup examples for enabling and customizing the opt-in placeholder | | Native iOS color bridge | `editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridge.kt` | Swift-visible `CascadeEditorColors` complete ARGB palette; controller snapshot mapping into core colors | | Native iOS publication runbook | `docs/iOsPublication.md` | Version/tag contract, GitHub/SwiftPM setup, immutable release sequence, recovery | | Native iOS release packaging | `scripts/package-ios-sdk.sh`, `distribution/swift-package/` | Serializes memory-heavy native test/release linking, verifies the dynamic Release XCFramework, and emits one-root ZIP, SHA-256, and external Swift package files | | Native iOS clean consumer validation | `scripts/validate-ios-consumer.sh`, `iosConsumerSmokeTest/` | Packaging installs an ignored ZIP-derived framework for direct Xcode use; validation removes it in a temporary copy, then performs generic-device Release and simulator UI/resource tests using only the supplied packaged binary | | Native iOS framework resources/privacy | `editor-ios-sdk/PrivacyInfo.xcprivacy`, `editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/resources/CascadeEditorResourceReader.kt` | Required-reason manifest and exact framework-bundle Compose resource lookup | -| Editor behavior config | `ui/CascadeEditorConfig.kt`, `ui/LocalCascadeEditorConfig.kt` | `CascadeEditorConfig` (incl. `crashPolicy`/`onInternalError`), `LocalCascadeEditorConfig` | +| Editor behavior config | `ui/CascadeEditorConfig.kt`, `ui/LocalCascadeEditorConfig.kt` | `CascadeEditorConfig` (incl. `emptyDocumentPlaceholderEnabled`, `crashPolicy`/`onInternalError`), `LocalCascadeEditorConfig` | | Crash handling | `CrashHandling.kt` | `CrashPolicy`, `CascadeError`, `CascadeErrorReporter`, `guarded()`/`reportContainedFailure()` (internal) | | Editor interaction policy | `ui/EditorInteractionPolicy.kt`, `ui/LocalEditorInteractionPolicy.kt` | `EditorInteractionPolicy` (internal) | | Text input | `ui/BackspaceAwareTextEdit.kt` | `BackspaceAwareTextField()`, `TextFieldState.selectedVisibleText()` | @@ -179,7 +180,7 @@ Block-based editor (Craft/Notion-like) for Compose Multiplatform. Unidirectional | Slash popup items local | `ui/LocalSlashPopupItems.kt` | `LocalSlashPopupItems` | | Slash command slot | `ui/SlashCommandSlot.kt` | `SlashCommandSlot` (`Default`, `None`) | | Slash enabled local | `ui/LocalSlashCommandsEnabled.kt` | `LocalSlashCommandsEnabled` (internal) | -| Theme colors | `theme/CascadeEditorColors.kt` | `CascadeEditorColors` (includes `toolbarBackground` for the floating-pill surface, defaults to `popupBackground`) | +| Theme colors | `theme/CascadeEditorColors.kt` | `CascadeEditorColors` (includes `placeholderText` for the empty-document hint and `toolbarBackground` for the floating-pill surface) | | Theme typography | `theme/CascadeEditorTypography.kt` | `CascadeEditorTypography` | | Theme dimensions | `theme/CascadeEditorDimensions.kt` | `CascadeEditorDimensions` | | Theme top-level | `theme/CascadeEditorTheme.kt` | `CascadeEditorTheme` | @@ -269,6 +270,8 @@ Static preview text is built by `buildPreviewAnnotatedString()` from exact visib **EditorStateHolder** — Compose-friendly mutable wrapper. Use `rememberEditorState(initialBlocks)` to create. Call `stateHolder.dispatch(action)` to modify state. Undo/redo history is owned internally by the holder in v1 and exposed via `canUndo`, `canRedo`, `undo()`, and `redo()`. The finalized v1 model is hybrid and linear: `BlockTextEntry` is used only for strict one-block text/span deltas, while `StructuralEntry` is used for semantic or multi-block document changes and replays through full checkpoints. Direct external `dispatch(action)` calls still bypass history unless they are routed through a history-aware integration point. `setState(...)` and `loadFromJson(...)` are hard replacement paths and clear history. When rendered through `CascadeEditor`, the holder is internally bound to the live `BlockTextStates` / `BlockSpanStates` instances so replay can choose the correct scope per entry type: `StructuralEntry` clears runtime holders, replaces snapshot state, rebuilds runtime text/span state for current text blocks, restores block selection when there is no focused block, and then restores focused text selection plus focused pending styles; `BlockTextEntry` patches only the target block snapshot/runtime text/runtime spans plus the same replayable UI state. Structural checkpoints include block selection because structural commands such as subtree drag are selection-defined; they still exclude slash UI and active drag state. Live typing capture currently happens in `TextBlockField` through `TextEditHistoryTracker`, which promotes committed one-block text edits into history after span maintenance and coalesces only caret-preserving insert/backspace/delete-forward batches within `500ms`; focus, selection, paste-like, programmatic, replay, and externally triggered formatting commands reset that local batch state through holder-registered tracker hooks. Structural edit sources now route through holder-owned transaction helpers that always push a forced `StructuralEntry`, break all open typing batches before capture, and re-anchor surviving trackers to the post-transaction checkpoint so later typing compares against the new structural baseline. Custom runtime-holder instances should remain stable for the lifetime of the bound holder; the default remembered instances already satisfy that contract. +**Empty-document placeholder** — `EditorStateHolder` still owns the existing synthetic trailing paragraph invariant; placeholder text is never inserted into a block. `TextBlockField` renders `CascadeEditorStrings.emptyDocumentPlaceholder` underneath the real field only when `emptyDocumentPlaceholderEnabled` is true, text editing is allowed, the document contains exactly one root `Paragraph` or `Heading`, and that block's live sentinel-aware visible text is empty. The flag defaults to false, making the hint opt-in. Focused and unfocused states share the same hint; typing hides it and clearing the field restores it. Read-only mode suppresses it. `CascadeDocumentPreview` never renders editor placeholders. + **BlockTextStates** — single source of truth for text content. One `TextFieldState` per block. Key methods: `getOrCreate()`, `getVisibleText()`, `getSelection()`, `mergeInto()`, `setText()`, `setSelection()`, `replaceVisibleRange()`, `consumeProgrammaticCommit()`, `extractAllText()`, `cleanup()`. Programmatic text mutations (`mergeInto` / `setText`) register per-block expected committed text so `SpanMaintenanceTextObserver` can skip/rebase non-user commits and avoid duplicate span adjustment. Programmatic selection restore uses visible-text coordinates through `getSelection()` / `setSelection()` so history replay does not need to reason about the internal ZWSP sentinel. Internal observers (like `SlashCommandTextObserver`) can also perform a non-destructive pending-commit peek when needed without consuming the authoritative span observer entry. Provided to renderers via `LocalBlockTextStates` CompositionLocal. **BlockSpanStates** — single source of truth for rich text spans during editing. One `MutableState>` per block plus snapshot-aware pending-style state. Key methods: `getOrCreate(..., textLength)`, `getSpans()`, `set(..., textLength)`, `adjustForUserEdit()`, `split()`, `mergeInto()`, `applyStyle()`, `removeStyle()`, `removeLinkSpans()`, `toggleStyle()`, `queryStyleStatus()`, `activeStylesAt()`, `resolveStylesForInsertion()`. Invariants are enforced at API ingress (`getOrCreate` / `set`) by normalizing and clamping spans with current visible text length. Created and remembered in `CascadeEditor`, cleaned up in `LaunchedEffect(state.blocks)` with text-only IDs (`collectTextBlockIds`) to prevent stale span state on non-text transitions, and provided to renderers via `LocalBlockSpanStates` CompositionLocal. Per-block span state is initialized in `TextBlockRenderer` from `BlockContent.Text.spans`. Rendering is applied through a stable per-block `BasicTextField` `outputTransformation` that reads latest spans at render time via `SpanMapper.applyStyles(...)` with defensive clamping in visible coordinates and theme-captured `inlineCodeBackground`, `highlight`, and `linkText` colors. Link spans render as `linkText` plus underline; click/open semantics are separate from rendering. User-edit span maintenance runs post-commit via `SpanMaintenanceTextObserver`, which consumes/rebases programmatic commit baselines from `BlockTextStates` before applying diff-based user edit maintenance. Programmatic split/merge runtime transfer is executed in `DefaultBlockCallbacks`, and `mergeInto(...)` clears pending styles on both source and target to avoid pending-style bleed after merge. External formatting operations (toolbar, keyboard shortcuts) should use `SpanActionDispatcher` (provided via `LocalSpanActionDispatcher`) which coordinates runtime `BlockSpanStates` update (immediate visual) with full snapshot sync via `UpdateBlockContent` (avoids stale-text-length mismatch). Link create/edit/remove operations use `LinkActionDispatcher`, exposed to custom chrome through gated `LocalLinkActions`; it validates URLs, mutates runtime text/spans against a captured `LinkTarget`, syncs snapshot through `UpdateBlockContent`, and captures one isolated history entry when an `EditorStateHolder` is supplied. Collapsed-cursor `toggleStyle` toggles pending styles instead of applying zero-width spans, but `resolveStylesForInsertion(...)` filters `SpanStyle.Link` from pending and inherited continuation styles so generic typing cannot create or extend links at boundaries. `ApplySpanStyle`/`RemoveSpanStyle` actions are snapshot-only and should not be dispatched directly during active editing. Snapshot span reducers use the same `SpanAlgorithms` normalization contract as runtime for canonical output. `SplitBlock` accepts `newBlockSpans` parameter for runtime-provided spans and always updates source block snapshot. `MergeBlocks` reducer merges snapshot spans alongside text. `DefaultBlockCallbacks` syncs merged text+spans to snapshot via `UpdateBlockContent` before `DeleteBlock` dispatch on merge paths. `UpdateBlockText` explicitly resets spans (callers needing span preservation use `UpdateBlockContent`). @@ -389,7 +392,8 @@ All state changes go through `EditorAction.reduce(state) → newState`. | Theming / styling API — data models | Done | `CascadeEditorTheme`, `CascadeEditorColors`, `CascadeEditorTypography`, `LocalCascadeTheme`; light/dark presets | | Theming / styling API — color migration | Done | All UI colors read from `LocalCascadeTheme.current.colors` | | Theming / styling API — typography migration | Done | All UI typography reads from `LocalCascadeTheme.current.typography` | -| Native iOS theming — color bridge | Done | Swift can seed a complete 24-slot `CascadeEditorColors` bag from either preset, apply a snapshotted palette at runtime with `setColors`, and restore preset selection with `clearCustomColors`; the native sample separates user-selected Violet/Forest families from OS-selected light/dark variants and reapplies both values when `colorScheme` changes | +| Native iOS theming — color bridge | Done | Swift can seed a complete 25-slot `CascadeEditorColors` bag from either preset, apply a snapshotted palette at runtime with `setColors`, and restore preset selection with `clearCustomColors`; the native sample separates user-selected Violet/Forest families from OS-selected light/dark variants and reapplies both values when `colorScheme` changes | +| Empty-document placeholder | Done | Localized/themed Box decoration for a sole empty root paragraph or heading; opt-in in KMP and native iOS, sentinel-aware, live-text-driven, read-only-suppressed, and excluded from document preview/persistence/history | | Localization — data models | Done | `CascadeEditorStrings`, `CascadeEditorBlockStrings`, `BlockLocalizedStrings`, `LocalCascadeStrings`, `LocalCascadeBlockStrings` | | Localization — UI string migration | Done | `SlashCommandPopup`, `UnknownBlockRenderer`, `RichTextToolbar` read from `LocalCascadeStrings`; link labels and validation-error labels live in `CascadeEditorStrings` | | Localization — slash command system | Done | `BuiltInSlashCommandFactory.generate()` accepts `CascadeEditorBlockStrings?` for localized titles/descriptions/keywords | @@ -443,7 +447,7 @@ All state changes go through `EditorAction.reduce(state) → newState`. | `UnknownBlockTypeTest.kt` | UnknownBlockType properties (supportsText, isConvertible, displayName, rawTypeJson), registry getRenderer unchanged | | `DocumentSchemaEncodeTest.kt` | Document encode: envelope/version, block attributes, all built-in types, content kinds, custom data, codec hooks, UnknownBlockType re-emit | | `DocumentSchemaDecodeTest.kt` | Document decode: round-trips, version guard, block attributes, heading/todo/numbered defaults, ID modes, malformed blocks, codecs, depth-aware renumbering, warnings | -| `DocumentSerializationExtTest.kt` | Editor integration: toJson runtime/snapshot resolution with attributes, loadFromJson state replacement with attributes, runtime clearing, codec pass-through | +| `DocumentSerializationExtTest.kt` | Editor integration: toJson runtime/snapshot resolution with attributes, presentation-only empty-document placeholder contract, loadFromJson state replacement with attributes, runtime clearing, codec pass-through | | `HtmlSerializationExtTest.kt` | HTML editor integration: toHtml runtime/snapshot resolution, non-spans runtime span stripping, loadFromHtml runtime clearing/state replacement/warning return, custom profile pass-through | | `RichTextSchemaTest.kt` | Span serialization round-trips, normalization, version handling | | `SpanAlgorithmsTest.kt` | Normalize, edit adjust, split/merge, apply/remove/toggle, style queries (~62 tests) | @@ -468,10 +472,12 @@ All state changes go through `EditorAction.reduce(state) → newState`. | `HistoryRegressionIntegrationTest.kt` | Cross-cutting hybrid undo/redo regressions: alternating `BlockTextEntry`/`StructuralEntry` flows, exact focused selection and pending-style restoration through `type -> split -> undo split -> undo typing`, and history clearing after document replacement | | `SlashPopupUtilsTest.kt` | Popup pure functions: estimatePopupHeightDp (compact/clamped), calculatePopupOffset (below/above/clamp), resolveNextHighlight (null/down/up/first/last/clamped/unknown) | | `CascadeEditorSlashIntegrationTest.kt` | Slash integration: registry coexistence (built-in + custom), custom override, custom execution alongside built-ins, session invalidation pure function (no session, healthy, drag, selection, anchor missing, different block deleted), full scenarios (drag start, anchor deletion) | -| `CascadeEditorColorsTest.kt` | Light/dark presets: non-transparent slots including `linkText`, known values, light vs dark differ on key slots, copy/equality semantics | -| `editor-ios-sdk/.../CascadeEditorColorsBridgeTest.kt` | Native iOS bridge: light/dark preset parity, all 24 Swift-facing ARGB slots map exactly, runtime values are snapshotted, custom palettes survive semantic dark-mode changes, and clearing restores preset selection | +| `CascadeEditorColorsTest.kt` | Light/dark presets: non-transparent slots including `linkText` and `placeholderText`, known values, light vs dark differ on key slots, copy/equality semantics | +| `editor-ios-sdk/.../CascadeEditorColorsBridgeTest.kt` | Native iOS bridge: light/dark preset parity, all 25 Swift-facing ARGB slots map exactly, runtime values are snapshotted, custom palettes survive semantic dark-mode changes, and clearing restores preset selection | | `CascadeEditorTypographyTest.kt` | Default preset: positive font sizes, monotonically decreasing headings, monospace code, medium-weight toolbar, copy/equality | -| `CascadeEditorStringsTest.kt` | Default preset: non-empty strings, unsupportedBlock interpolation, copy with custom values, known English defaults | +| `CascadeEditorStringsTest.kt` | Default preset: non-empty strings including the empty-document placeholder, unsupportedBlock interpolation, copy with custom values, known English defaults | +| `ui/renderers/EmptyDocumentPlaceholderTest.kt` | Empty-document structural eligibility: enabled/editable sole root paragraph or heading only; rejects read-only, disabled, multi-block, unsupported, indented, and mismatched targets | +| `ui/CascadeEditorPlaceholderTest.kt` (desktopTest) | Opt-in localized placeholder rendering for paragraphs and headings, live type/delete visibility, non-empty/trailing-scaffold exclusion, and default-disabled/read-only suppression | | `CascadeEditorBlockStringsTest.kt` | Default preset: all built-in typeIds present, non-empty displayName/description/keywords, forType null for unknown, BlockLocalizedStrings defaults | | `BuiltInSlashCommandFactoryLocalizationTest.kt` | Localized slash generation: title/description override, keyword merging + dedup, null blockStrings fallback, missing typeId fallback, mixed localized/unlocalized, English keywords always present | | `HtmlEscapingTest.kt` | `Html.escapeText` / `Html.escapeAttr`: ampersand-first escaping, escapable-set coverage, idempotence on plain ASCII, empty input, Unicode passthrough | diff --git a/docs/ReadOnlyMode.md b/docs/ReadOnlyMode.md index 6121bde..8795334 100644 --- a/docs/ReadOnlyMode.md +++ b/docs/ReadOnlyMode.md @@ -34,6 +34,8 @@ Read-only mode disables editor-owned mutations: When `readOnly` changes from `false` to `true`, `CascadeEditor` closes active slash sessions, cancels active drags, clears block selection, and dismisses editor-owned link editing popup sessions. This cleanup does not change document blocks, text focus, caret, or text selection. +The edit-oriented empty-document placeholder is also suppressed in read-only mode, even when `emptyDocumentPlaceholderEnabled` is `true`. + ## Toolbar Read-only mode does not add a separate toolbar visibility switch. diff --git a/docs/ThemingAndLocalization.md b/docs/ThemingAndLocalization.md index a6af97e..ed480f0 100644 --- a/docs/ThemingAndLocalization.md +++ b/docs/ThemingAndLocalization.md @@ -2,7 +2,7 @@ ## 1. Feature Overview -This feature introduces a **theme + strings** configuration layer that lets consumers control every visual and textual aspect of the editor via three top-level parameters on `CascadeEditor()`. All hardcoded `Color(0x...)` values, font sizes, and UI strings have been replaced with reads from `CompositionLocal`-provided config objects. The editor ships light and dark presets and English defaults, while remaining fully open to custom palettes, typography, and translations. Existing consumers are unaffected — all new parameters have defaults that reproduce the previous behavior exactly. +This feature introduces a **theme + strings** configuration layer that lets consumers control every visual and textual aspect of the editor via three top-level parameters on `CascadeEditor()`. All hardcoded `Color(0x...)` values, font sizes, and UI strings have been replaced with reads from `CompositionLocal`-provided config objects. The editor ships light and dark presets and English defaults, while remaining fully open to custom palettes, typography, and translations. The empty-document hint is available as an opt-in behavior. --- @@ -12,10 +12,10 @@ This feature introduces a **theme + strings** configuration layer that lets cons | Class | Role | |-------|------| -| `CascadeEditorColors` | `@Immutable data class` — 21 named color slots covering every hardcoded color in the editor plus rich-text rendering colors. Ships `light()` and `dark()` presets. | +| `CascadeEditorColors` | `@Immutable data class` — 25 named color slots covering every editor visual, including the empty-document placeholder and rich-text rendering colors. Ships `light()` and `dark()` presets. | | `CascadeEditorTypography` | `@Immutable data class` — 11 `TextStyle` slots (body, heading1–6, code, slash UI, toolbar). Ships `default()`. Styles carry font properties only — **no color**. | | `CascadeEditorTheme` | Facade combining `colors` + `typography`. Ships `light()` / `dark()`. | -| `CascadeEditorStrings` | `@Immutable data class` — UI chrome strings: toolbar accessibility labels, link popup labels, back button text, and error-message lambdas. Ships `default()` (English). | +| `CascadeEditorStrings` | `@Immutable data class` — UI chrome strings: toolbar accessibility labels, link popup labels, the empty-document placeholder, back button text, and error-message lambdas. Ships `default()` (English). | | `BlockLocalizedStrings` | Per-block-type display name, description, and additive search keywords. | | `CascadeEditorBlockStrings` | `Map` with a `forType()` lookup. Ships `default()` (English, mirrors `BlockRegistry`). | | `LocalCascadeTheme` | `internal staticCompositionLocalOf` | @@ -62,6 +62,7 @@ Consumer ├─ CompositionLocalProvider(LocalCascadeStrings provides strings, ...) │ ├─ RichTextToolbar: localizedLabel(spec, strings) for accessibility │ ├─ SlashCommandPopup: strings.back for back button + │ ├─ TextBlockField: strings.emptyDocumentPlaceholder for an empty editable document │ └─ UnknownBlockRenderer: strings.unsupportedBlock(typeId) │ └─ remember(blockStrings) { @@ -144,6 +145,9 @@ CascadeEditor( ```kotlin // Provide French strings CascadeEditor( + config = CascadeEditorConfig( + emptyDocumentPlaceholderEnabled = true, + ), strings = CascadeEditorStrings( back = "\u2039 Retour", unsupportedBlock = { "Type de bloc non supporté : $it" }, @@ -164,6 +168,7 @@ CascadeEditor( linkTitle = "Titre", linkUrl = "URL", linkValidationError = { "URL invalide : $it" }, + emptyDocumentPlaceholder = "Commencez ici…", ), blockStrings = CascadeEditorBlockStrings( blocks = mapOf( @@ -202,7 +207,7 @@ Theme/string `CompositionLocal` instances (`LocalCascadeTheme`, `LocalCascadeStr | `ui/DropIndicator.kt` | Resolves `colors.primary` from theme when no color override; removed `DropIndicatorDefaults.Color` | | `ui/BackspaceAwareTextEdit.kt` | Added `cursorBrush` parameter | | `ui/renderers/TextBlockRenderer.kt` | Reads typography for heading/body/code styles; applies `colors.text` | -| `ui/renderers/TextBlockField.kt` | Captures `colors.linkText`/`colors.inlineCodeBackground`/`colors.highlight`, passes them to SpanMapper via OutputTransformation | +| `ui/renderers/TextBlockField.kt` | Renders the localized/themed empty-document placeholder beneath the real field; captures `colors.linkText`/`colors.inlineCodeBackground`/`colors.highlight` for SpanMapper | | `ui/renderers/TodoBlockRenderer.kt` | Reads theme for body typography + text color | | `ui/renderers/DividerBlockRenderer.kt` | Reads `colors.contentDivider` | | `ui/renderers/UnknownBlockRenderer.kt` | Reads `colors` + `strings` for background/text/error message | @@ -228,6 +233,8 @@ Theme/string `CompositionLocal` instances (`LocalCascadeTheme`, `LocalCascadeStr **String lambda equality.** `CascadeEditorStrings` is a data class, but lambdas don't participate in structural equality. Default lambdas such as `unsupportedBlock` and `linkValidationError` are extracted as singletons to ensure `CascadeEditorStrings.default() == CascadeEditorStrings.default()` holds true. +**Placeholder is presentation, not content.** `emptyDocumentPlaceholder` is drawn beneath the existing text field only for a sole empty root paragraph or heading. Visibility uses the live text field's sentinel-aware empty check, so the ZWSP backspace sentinel never blocks the hint and the hint never enters blocks, serialization, history, selection, or preview mode. `CascadeEditorConfig.emptyDocumentPlaceholderEnabled` defaults to `false`; read-only mode suppresses the edit-oriented hint when explicitly enabled. + **Typography excludes color.** `TextStyle` slots in `CascadeEditorTypography` carry size, weight, and font family only. Color is always applied from `CascadeEditorColors` at use-site via `.copy(color = colors.text)`. Providing a `TextStyle` with color set will have that color overridden. **`UnknownBlockRenderer` font size stays hardcoded.** It renders `14.sp` for error text — intentional, as it's a debug/error display not considered user-facing content typography. diff --git a/docs/iOsNativeSdk.md b/docs/iOsNativeSdk.md index eeb5a6e..2c29da9 100644 --- a/docs/iOsNativeSdk.md +++ b/docs/iOsNativeSdk.md @@ -180,7 +180,7 @@ The built-in slash executor also distinguishes text and non-text conversion targ ### SDK identity and configuration - `CascadeEditorSdk.version: String` — currently `"1.0.0"`. -- `CascadeEditorConfiguration` — immutable configuration containing `readOnly`, `toolbarMode`, `slashCommandsEnabled`, `blockSelectionEnabled`, `blockDraggingEnabled`, `blockIndentationEnabled`, `isDark`, and `crashPolicy`. Setting `blockIndentationEnabled` to `false` disables indentation commands and keeps block drag/reorder at the payload's original indentation depth. +- `CascadeEditorConfiguration` — immutable configuration containing `readOnly`, `toolbarMode`, `slashCommandsEnabled`, `blockSelectionEnabled`, `blockDraggingEnabled`, `blockIndentationEnabled`, `emptyDocumentPlaceholderEnabled`, `isDark`, and `crashPolicy`. The placeholder flag defaults to `false`; when enabled, read-only mode still suppresses the edit-oriented hint. - `CascadeToolbarMode` — `builtIn` or `none`; `none` disables both the built-in formatting toolbar and its link popup. - `CascadeCrashPolicy` — `containAndReport` or `rethrow`. @@ -224,8 +224,8 @@ Runtime configuration, colors, and localization: - `updateConfiguration(value)`, `setReadOnly(value)`, `setDarkMode(value)`, `setToolbarMode(value)`, `setSlashCommandsEnabled(value)` - `setColors(CascadeEditorColors)`, `clearCustomColors()` - `setLocalization(CascadeEditorLocalization)` -- `CascadeEditorColors` is a mutable, complete 24-slot palette seeded from the built-in light (`init()`) or light/dark (`init(isDark:)`) preset. Every property is a Swift `Int64` containing `0xAARRGGBB`. -- `CascadeLocalizedStrings` supplies nullable overrides for built-in UI/accessibility strings. +- `CascadeEditorColors` is a mutable, complete 25-slot palette seeded from the built-in light (`init()`) or light/dark (`init(isDark:)`) preset. Every property is a Swift `Int64` containing `0xAARRGGBB`; `placeholderText` controls the empty-document hint. +- `CascadeLocalizedStrings` supplies nullable overrides for built-in UI/accessibility strings, including `emptyDocumentPlaceholder`. - `CascadeLocalizedBlockStrings` supplies slash-menu name, description, and additive keywords by block type ID. `setColors` snapshots the whole bag and can be called before or after @@ -368,7 +368,7 @@ Back-button flush keeps the editor open and surfaces the save failure. - **Registry system:** native blocks extend `BlockRegistry`; native commands extend `SlashCommandRegistry`. Observable revisions make runtime registration visible to an already-mounted editor. - **Serialization:** editable JSON uses `DocumentSchema` plus `NativeCustomBlockCodec`; HTML uses `HtmlProfile.Default`. Both share the editor's live text/span holders. The preview facade uses `DocumentSchema` directly to preserve generic unknown/custom data without creating runtime holders or resolving native editor types. - **Rich text and toolbar:** the bridge maps `FormattingState`, `IndentationState`, and `LinkState` from `CascadeEditorToolbarController` into Swift-friendly state and actions. -- **Localization and theme:** Swift string overrides resolve into core `CascadeEditorStrings` and `CascadeEditorBlockStrings`. `CascadeEditorColors` snapshots map all 24 ARGB slots into the core palette and recompose live. Without a custom palette, `isDark` selects the built-in colors; it remains exposed to native custom blocks in either mode. +- **Localization and theme:** Swift string overrides resolve into core `CascadeEditorStrings` and `CascadeEditorBlockStrings`. `CascadeEditorColors` snapshots map all 25 ARGB slots into the core palette and recompose live. Without a custom palette, `isDark` selects the built-in colors; it remains exposed to native custom blocks in either mode. - **Local build:** `scripts/build-xcframework.sh` assembles the dynamic debug XCFramework at `editor-ios-sdk/build/XCFrameworks/debug/CascadeEditor.xcframework`. The Xcode sample links and embeds this local artifact. - **External distribution:** `scripts/package-ios-sdk.sh` tests and assembles the release framework, validates resources/privacy/version/dSYMs, and emits a one-root ZIP plus SwiftPM checksum and manifest. `release-ios-sdk.yml` publishes immutable source-tag assets and updates `linreal/cascade-editor-ios`. - **Consumer validation:** `scripts/validate-ios-consumer.sh` stages the publication ZIP in `iosConsumerSmokeTest`, builds a generic-device Release app, executes its UI test on an arm64 simulator, and inspects the embedded framework. diff --git a/docs/recipes/EmptyDocumentPlaceholder.md b/docs/recipes/EmptyDocumentPlaceholder.md new file mode 100644 index 0000000..bc6251c --- /dev/null +++ b/docs/recipes/EmptyDocumentPlaceholder.md @@ -0,0 +1,72 @@ +# Empty Document Placeholder + +Use this recipe to show a hint such as `Start here…` in a new or empty +document. The feature is opt-in and appears only when the editable document +contains one empty root `Paragraph` or `Heading`. Read-only editors suppress it. + +## Compose Multiplatform + +Enable the behavior through `CascadeEditorConfig`. The built-in string and +theme color are used unless you override them: + +```kotlin +CascadeEditor( + // existing state and registries + config = CascadeEditorConfig( + emptyDocumentPlaceholderEnabled = true, + ), + strings = CascadeEditorStrings.default().copy( + emptyDocumentPlaceholder = "Add a title…", + ), + theme = CascadeEditorTheme.light().let { theme -> + theme.copy( + colors = theme.colors.copy( + placeholderText = Color(0xFF74747B), + ), + ) + }, +) +``` + +If the screen already owns a configuration, preserve its other settings with +`config.copy(emptyDocumentPlaceholderEnabled = true)`. + +## Native iOS (Swift) + +Pass `emptyDocumentPlaceholderEnabled: true` in the immutable native +configuration. Swift uses the complete primary initializer: + +```swift +let configuration = CascadeEditorConfiguration( + readOnly: false, + toolbarMode: .builtIn, + slashCommandsEnabled: true, + blockSelectionEnabled: true, + blockDraggingEnabled: true, + isDark: false, + crashPolicy: .containAndReport, + blockIndentationEnabled: true, + emptyDocumentPlaceholderEnabled: true +) + +let controller = CascadeEditorController( + initialJson: nil, + configuration: configuration +) + +let strings = CascadeLocalizedStrings() +strings.emptyDocumentPlaceholder = "Add a title…" +controller.setLocalization( + localization: CascadeEditorLocalization(strings: strings) +) + +let colors = CascadeEditorColors(isDark: false) +colors.placeholderText = 0xFF74747B +controller.setColors(colors: colors) +``` + +The localization and color calls are optional; omit them to use the built-in +`Start here…` string and the preset placeholder color. For an existing +controller, pass the updated full configuration to +`controller.updateConfiguration(value:)`. + diff --git a/docs/recipes/ThemeCustomization.md b/docs/recipes/ThemeCustomization.md index d0c1b2b..06574a9 100644 --- a/docs/recipes/ThemeCustomization.md +++ b/docs/recipes/ThemeCustomization.md @@ -68,7 +68,7 @@ Dark. If the app has its own appearance override, pass that Boolean instead. ## Native iOS (Swift/SwiftUI) The native SDK accepts colors as `0xAARRGGBB` values in a mutable -`CascadeEditorColors` bag. Seed the bag first so it always contains all 24 +`CascadeEditorColors` bag. Seed the bag first so it always contains all 25 slots: ```swift @@ -147,7 +147,7 @@ variant change. ## Color-slot checklist -Both APIs expose the same 24 semantic slots: +Both APIs expose the same 25 semantic slots: `primary`, `onPrimary`, `text`, `popupBackground`, `unknownBlockBackground`, `toolbarIcon`, `toolbarIconDisabled`, @@ -155,7 +155,7 @@ Both APIs expose the same 24 semantic slots: `contentDivider`, `slashSelectedItem`, `inlineCodeBackground`, `highlight`, `cursor`, `textSelectionBackground`, `quoteBorder`, `quoteBackground`, `selectionOverlay`, `linkText`, `error`, `codeBlockBackground`, and -`toolbarBackground`. +`toolbarBackground`, and `placeholderText`. Reference implementations: diff --git a/editor-ios-sdk/api/editor-ios-sdk.klib.api b/editor-ios-sdk/api/editor-ios-sdk.klib.api index 22d90a4..552c407 100644 --- a/editor-ios-sdk/api/editor-ios-sdk.klib.api +++ b/editor-ios-sdk/api/editor-ios-sdk.klib.api @@ -150,9 +150,7 @@ final class io.github.linreal.cascade.ios.controller/CascadeDocumentLoadResult { } final class io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration { // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration|null[0] - constructor () // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.|(){}[0] - constructor (kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeToolbarMode, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeCrashPolicy) // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.|(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy){}[0] - constructor (kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeToolbarMode, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, io.github.linreal.cascade.ios.controller/CascadeCrashPolicy, kotlin/Boolean) // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.|(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy;kotlin.Boolean){}[0] + constructor (kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeToolbarMode = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeCrashPolicy = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.|(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy;kotlin.Boolean;kotlin.Boolean){}[0] final val blockDraggingEnabled // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockDraggingEnabled|{}blockDraggingEnabled[0] final fun (): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockDraggingEnabled.|(){}[0] @@ -162,6 +160,8 @@ final class io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration final fun (): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.blockSelectionEnabled.|(){}[0] final val crashPolicy // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.crashPolicy|{}crashPolicy[0] final fun (): io.github.linreal.cascade.ios.controller/CascadeCrashPolicy // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.crashPolicy.|(){}[0] + final val emptyDocumentPlaceholderEnabled // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.emptyDocumentPlaceholderEnabled|{}emptyDocumentPlaceholderEnabled[0] + final fun (): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.emptyDocumentPlaceholderEnabled.|(){}[0] final val isDark // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.isDark|{}isDark[0] final fun (): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.isDark.|(){}[0] final val readOnly // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.readOnly|{}readOnly[0] @@ -179,7 +179,8 @@ final class io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration final fun component6(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component6|component6(){}[0] final fun component7(): io.github.linreal.cascade.ios.controller/CascadeCrashPolicy // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component7|component7(){}[0] final fun component8(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component8|component8(){}[0] - final fun copy(kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeToolbarMode = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeCrashPolicy = ..., kotlin/Boolean = ...): io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.copy|copy(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy;kotlin.Boolean){}[0] + final fun component9(): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.component9|component9(){}[0] + final fun copy(kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeToolbarMode = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.ios.controller/CascadeCrashPolicy = ..., kotlin/Boolean = ..., kotlin/Boolean = ...): io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.copy|copy(kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeToolbarMode;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.ios.controller.CascadeCrashPolicy;kotlin.Boolean;kotlin.Boolean){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // io.github.linreal.cascade.ios.controller/CascadeEditorConfiguration.toString|toString(){}[0] @@ -288,6 +289,9 @@ final class io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings { final var bold // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.bold|{}bold[0] final fun (): kotlin/String? // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.bold.|(){}[0] final fun (kotlin/String?) // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.bold.|(kotlin.String?){}[0] + final var emptyDocumentPlaceholder // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.emptyDocumentPlaceholder|{}emptyDocumentPlaceholder[0] + final fun (): kotlin/String? // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.emptyDocumentPlaceholder.|(){}[0] + final fun (kotlin/String?) // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.emptyDocumentPlaceholder.|(kotlin.String?){}[0] final var hideKeyboard // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.hideKeyboard|{}hideKeyboard[0] final fun (): kotlin/String? // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.hideKeyboard.|(){}[0] final fun (kotlin/String?) // io.github.linreal.cascade.ios.localization/CascadeLocalizedStrings.hideKeyboard.|(kotlin.String?){}[0] @@ -535,6 +539,9 @@ final class io.github.linreal.cascade.ios.theme/CascadeEditorColors { // io.gith final var onPrimary // io.github.linreal.cascade.ios.theme/CascadeEditorColors.onPrimary|{}onPrimary[0] final fun (): kotlin/Long // io.github.linreal.cascade.ios.theme/CascadeEditorColors.onPrimary.|(){}[0] final fun (kotlin/Long) // io.github.linreal.cascade.ios.theme/CascadeEditorColors.onPrimary.|(kotlin.Long){}[0] + final var placeholderText // io.github.linreal.cascade.ios.theme/CascadeEditorColors.placeholderText|{}placeholderText[0] + final fun (): kotlin/Long // io.github.linreal.cascade.ios.theme/CascadeEditorColors.placeholderText.|(){}[0] + final fun (kotlin/Long) // io.github.linreal.cascade.ios.theme/CascadeEditorColors.placeholderText.|(kotlin.Long){}[0] final var popupBackground // io.github.linreal.cascade.ios.theme/CascadeEditorColors.popupBackground|{}popupBackground[0] final fun (): kotlin/Long // io.github.linreal.cascade.ios.theme/CascadeEditorColors.popupBackground.|(){}[0] final fun (kotlin/Long) // io.github.linreal.cascade.ios.theme/CascadeEditorColors.popupBackground.|(kotlin.Long){}[0] diff --git a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorConfiguration.kt b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorConfiguration.kt index dfdcfea..3a257f0 100644 --- a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorConfiguration.kt +++ b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorConfiguration.kt @@ -24,45 +24,16 @@ public enum class CascadeCrashPolicy { @ObjCName("CascadeEditorConfiguration", exact = true) public data class CascadeEditorConfiguration( - public val readOnly: Boolean, - public val toolbarMode: CascadeToolbarMode, - public val slashCommandsEnabled: Boolean, - public val blockSelectionEnabled: Boolean, - public val blockDraggingEnabled: Boolean, - public val isDark: Boolean, - public val crashPolicy: CascadeCrashPolicy, - public val blockIndentationEnabled: Boolean, -) { - public constructor( - readOnly: Boolean, - toolbarMode: CascadeToolbarMode, - slashCommandsEnabled: Boolean, - blockSelectionEnabled: Boolean, - blockDraggingEnabled: Boolean, - isDark: Boolean, - crashPolicy: CascadeCrashPolicy, - ) : this( - readOnly = readOnly, - toolbarMode = toolbarMode, - slashCommandsEnabled = slashCommandsEnabled, - blockSelectionEnabled = blockSelectionEnabled, - blockDraggingEnabled = blockDraggingEnabled, - isDark = isDark, - crashPolicy = crashPolicy, - blockIndentationEnabled = true, - ) - - public constructor() : this( - readOnly = false, - toolbarMode = CascadeToolbarMode.builtIn, - slashCommandsEnabled = true, - blockSelectionEnabled = true, - blockDraggingEnabled = true, - isDark = false, - crashPolicy = CascadeCrashPolicy.containAndReport, - blockIndentationEnabled = true, - ) -} + public val readOnly: Boolean = false, + public val toolbarMode: CascadeToolbarMode = CascadeToolbarMode.builtIn, + public val slashCommandsEnabled: Boolean = true, + public val blockSelectionEnabled: Boolean = true, + public val blockDraggingEnabled: Boolean = true, + public val isDark: Boolean = false, + public val crashPolicy: CascadeCrashPolicy = CascadeCrashPolicy.containAndReport, + public val blockIndentationEnabled: Boolean = true, + public val emptyDocumentPlaceholderEnabled: Boolean = false, +) internal fun CascadeCrashPolicy.toCoreCrashPolicy(): CrashPolicy = when (this) { CascadeCrashPolicy.containAndReport -> CrashPolicy.ContainAndReport diff --git a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorViewController.kt b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorViewController.kt index 10d8c5b..7321fe6 100644 --- a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorViewController.kt +++ b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorViewController.kt @@ -63,6 +63,8 @@ public fun CascadeEditorController.makeViewController(): UIViewController = onMa (error.cause.message ?: error.cause.toString()) ) }, + emptyDocumentPlaceholderEnabled = + configurationState.emptyDocumentPlaceholderEnabled, ) } val theme = remember(configurationState.isDark, customColors) { diff --git a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/localization/CascadeEditorStringsBridge.kt b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/localization/CascadeEditorStringsBridge.kt index aac3bdd..ed70ccb 100644 --- a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/localization/CascadeEditorStringsBridge.kt +++ b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/localization/CascadeEditorStringsBridge.kt @@ -75,6 +75,9 @@ public class CascadeLocalizedStrings public constructor() { /** Validation message shown when the link URL field is left blank. */ public var linkValidationBlankUrl: String? = null + + /** Hint shown in the first block while the editable document is empty. */ + public var emptyDocumentPlaceholder: String? = null } /** @@ -147,6 +150,8 @@ internal fun CascadeEditorLocalization.toEditorStrings(): CascadeEditorStrings { linkValidationError = overrides.linkValidationBlankUrl?.let { message -> { _ -> message } } ?: defaults.linkValidationError, + emptyDocumentPlaceholder = + overrides.emptyDocumentPlaceholder ?: defaults.emptyDocumentPlaceholder, ) } diff --git a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridge.kt b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridge.kt index 2ef3170..fff7459 100644 --- a/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridge.kt +++ b/editor-ios-sdk/src/iosMain/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridge.kt @@ -99,6 +99,9 @@ public class CascadeEditorColors public constructor( /** Floating formatting-toolbar surface. */ public var toolbarBackground: Long = preset.toolbarBackground.toBridgeArgb() + /** Empty-document input hint text. */ + public var placeholderText: Long = preset.placeholderText.toBridgeArgb() + internal fun snapshot(): CoreCascadeEditorColors = CoreCascadeEditorColors( primary = primary.toComposeColor(), onPrimary = onPrimary.toComposeColor(), @@ -124,6 +127,7 @@ public class CascadeEditorColors public constructor( error = error.toComposeColor(), codeBlockBackground = codeBlockBackground.toComposeColor(), toolbarBackground = toolbarBackground.toComposeColor(), + placeholderText = placeholderText.toComposeColor(), ) } diff --git a/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorControllerTest.kt b/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorControllerTest.kt index 3d05519..4b7b4a5 100644 --- a/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorControllerTest.kt +++ b/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/controller/CascadeEditorControllerTest.kt @@ -46,6 +46,7 @@ class CascadeEditorControllerTest { assertEquals(CascadeToolbarMode.builtIn, configuration.toolbarMode) assertEquals(CascadeCrashPolicy.containAndReport, configuration.crashPolicy) assertTrue(configuration.blockIndentationEnabled) + assertFalse(configuration.emptyDocumentPlaceholderEnabled) assertEquals(configuration, emptyController.configuration) assertEquals("Seed", jsonController.exportPlainText()) } diff --git a/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/localization/CascadeLocalizationTest.kt b/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/localization/CascadeLocalizationTest.kt index 090f020..af9a2c9 100644 --- a/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/localization/CascadeLocalizationTest.kt +++ b/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/localization/CascadeLocalizationTest.kt @@ -42,6 +42,7 @@ class CascadeLocalizationTest { italic = "Kursiv" linkApply = "Link anwenden" linkValidationBlankUrl = "Bitte URL eingeben." + emptyDocumentPlaceholder = "Hier beginnen\u2026" unsupportedBlock = { typeId -> "Nicht unterstützter Block: $typeId" } } @@ -52,6 +53,7 @@ class CascadeLocalizationTest { assertEquals("Fett", resolved.bold) assertEquals("Kursiv", resolved.italic) assertEquals("Link anwenden", resolved.linkApply) + assertEquals("Hier beginnen\u2026", resolved.emptyDocumentPlaceholder) assertEquals("Bitte URL eingeben.", resolved.linkValidationError(LinkValidationError.Blank)) assertEquals("Nicht unterstützter Block: table", resolved.unsupportedBlock("table")) // Unset fields keep the built-in English defaults. diff --git a/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridgeTest.kt b/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridgeTest.kt index a6dd4bc..77440fc 100644 --- a/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridgeTest.kt +++ b/editor-ios-sdk/src/iosSimulatorArm64Test/kotlin/io/github/linreal/cascade/ios/theme/CascadeEditorColorsBridgeTest.kt @@ -45,6 +45,7 @@ class CascadeEditorColorsBridgeTest { colors.error = 0xFF000016L colors.codeBlockBackground = 0xFF000017L colors.toolbarBackground = 0xFF000018L + colors.placeholderText = 0xFF000019L assertEquals( listOf( @@ -72,6 +73,7 @@ class CascadeEditorColorsBridgeTest { 0xFF000016L, 0xFF000017L, 0xFF000018L, + 0xFF000019L, ), colors.snapshot().argbSlots(), ) @@ -134,6 +136,7 @@ private fun CoreCascadeEditorColors.argbSlots(): List = listOf( error.toArgbLong(), codeBlockBackground.toArgbLong(), toolbarBackground.toArgbLong(), + placeholderText.toArgbLong(), ) private fun Color.toArgbLong(): Long = toArgb().toLong() and 0xFFFF_FFFFL diff --git a/editor/api/android/editor.api b/editor/api/android/editor.api index 8723b75..f09400e 100644 --- a/editor/api/android/editor.api +++ b/editor/api/android/editor.api @@ -3449,8 +3449,8 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorBlockStri public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public static final field $stable I public static final field Companion Lio/github/linreal/cascade/editor/theme/CascadeEditorColors$Companion; - public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J @@ -3468,6 +3468,7 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public final fun component22-0d7_KjU ()J public final fun component23-0d7_KjU ()J public final fun component24-0d7_KjU ()J + public final fun component25-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J public final fun component5-0d7_KjU ()J @@ -3475,8 +3476,8 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-KKJ9vVU (JJJJJJJJJJJJJJJJJJJJJJJJ)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; - public static synthetic fun copy-KKJ9vVU$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorColors;JJJJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; + public final fun copy-krVq-78 (JJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; + public static synthetic fun copy-krVq-78$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorColors;JJJJJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; public fun equals (Ljava/lang/Object;)Z public final fun getCodeBlockBackground-0d7_KjU ()J public final fun getContentDivider-0d7_KjU ()J @@ -3486,6 +3487,7 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public final fun getInlineCodeBackground-0d7_KjU ()J public final fun getLinkText-0d7_KjU ()J public final fun getOnPrimary-0d7_KjU ()J + public final fun getPlaceholderText-0d7_KjU ()J public final fun getPopupBackground-0d7_KjU ()J public final fun getPrimary-0d7_KjU ()J public final fun getQuoteBackground-0d7_KjU ()J @@ -3533,8 +3535,8 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorDimension public final class io/github/linreal/cascade/editor/theme/CascadeEditorStrings { public static final field $stable I public static final field Companion Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings$Companion; - public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()Ljava/lang/String; public final fun component11 ()Ljava/lang/String; @@ -3547,6 +3549,7 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorStrings { public final fun component18 ()Ljava/lang/String; public final fun component19 ()Lkotlin/jvm/functions/Function1; public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun component20 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; @@ -3554,11 +3557,12 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorStrings { public final fun component7 ()Ljava/lang/String; public final fun component8 ()Ljava/lang/String; public final fun component9 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; - public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; + public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; + public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; public fun equals (Ljava/lang/Object;)Z public final fun getBack ()Ljava/lang/String; public final fun getBold ()Ljava/lang/String; + public final fun getEmptyDocumentPlaceholder ()Ljava/lang/String; public final fun getHideKeyboard ()Ljava/lang/String; public final fun getHighlight ()Ljava/lang/String; public final fun getIndentBackward ()Ljava/lang/String; @@ -3699,22 +3703,23 @@ public final class io/github/linreal/cascade/editor/ui/CascadeEditorConfig { public static final field $stable I public static final field Companion Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig$Companion; public fun ()V - public fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;)V - public fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;Z)V - public synthetic fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZ)V + public synthetic fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Z public final fun component2 ()Z public final fun component3 ()Z public final fun component4 ()Lio/github/linreal/cascade/editor/CrashPolicy; public final fun component5 ()Lkotlin/jvm/functions/Function1; public final fun component6 ()Z - public final fun copy (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;Z)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; - public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig;ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZILjava/lang/Object;)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; + public final fun component7 ()Z + public final fun copy (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZ)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; + public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig;ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZILjava/lang/Object;)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; public fun equals (Ljava/lang/Object;)Z public final fun getBlockDraggingEnabled ()Z public final fun getBlockIndentationEnabled ()Z public final fun getBlockSelectionEnabled ()Z public final fun getCrashPolicy ()Lio/github/linreal/cascade/editor/CrashPolicy; + public final fun getEmptyDocumentPlaceholderEnabled ()Z public final fun getOnInternalError ()Lkotlin/jvm/functions/Function1; public final fun getReadOnly ()Z public fun hashCode ()I diff --git a/editor/api/desktop/editor.api b/editor/api/desktop/editor.api index 8723b75..f09400e 100644 --- a/editor/api/desktop/editor.api +++ b/editor/api/desktop/editor.api @@ -3449,8 +3449,8 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorBlockStri public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public static final field $stable I public static final field Companion Lio/github/linreal/cascade/editor/theme/CascadeEditorColors$Companion; - public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J @@ -3468,6 +3468,7 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public final fun component22-0d7_KjU ()J public final fun component23-0d7_KjU ()J public final fun component24-0d7_KjU ()J + public final fun component25-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J public final fun component5-0d7_KjU ()J @@ -3475,8 +3476,8 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-KKJ9vVU (JJJJJJJJJJJJJJJJJJJJJJJJ)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; - public static synthetic fun copy-KKJ9vVU$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorColors;JJJJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; + public final fun copy-krVq-78 (JJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; + public static synthetic fun copy-krVq-78$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorColors;JJJJJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorColors; public fun equals (Ljava/lang/Object;)Z public final fun getCodeBlockBackground-0d7_KjU ()J public final fun getContentDivider-0d7_KjU ()J @@ -3486,6 +3487,7 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorColors { public final fun getInlineCodeBackground-0d7_KjU ()J public final fun getLinkText-0d7_KjU ()J public final fun getOnPrimary-0d7_KjU ()J + public final fun getPlaceholderText-0d7_KjU ()J public final fun getPopupBackground-0d7_KjU ()J public final fun getPrimary-0d7_KjU ()J public final fun getQuoteBackground-0d7_KjU ()J @@ -3533,8 +3535,8 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorDimension public final class io/github/linreal/cascade/editor/theme/CascadeEditorStrings { public static final field $stable I public static final field Companion Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings$Companion; - public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()Ljava/lang/String; public final fun component11 ()Ljava/lang/String; @@ -3547,6 +3549,7 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorStrings { public final fun component18 ()Ljava/lang/String; public final fun component19 ()Lkotlin/jvm/functions/Function1; public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun component20 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; @@ -3554,11 +3557,12 @@ public final class io/github/linreal/cascade/editor/theme/CascadeEditorStrings { public final fun component7 ()Ljava/lang/String; public final fun component8 ()Ljava/lang/String; public final fun component9 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; - public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; + public final fun copy (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; + public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILjava/lang/Object;)Lio/github/linreal/cascade/editor/theme/CascadeEditorStrings; public fun equals (Ljava/lang/Object;)Z public final fun getBack ()Ljava/lang/String; public final fun getBold ()Ljava/lang/String; + public final fun getEmptyDocumentPlaceholder ()Ljava/lang/String; public final fun getHideKeyboard ()Ljava/lang/String; public final fun getHighlight ()Ljava/lang/String; public final fun getIndentBackward ()Ljava/lang/String; @@ -3699,22 +3703,23 @@ public final class io/github/linreal/cascade/editor/ui/CascadeEditorConfig { public static final field $stable I public static final field Companion Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig$Companion; public fun ()V - public fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;)V - public fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;Z)V - public synthetic fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZ)V + public synthetic fun (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Z public final fun component2 ()Z public final fun component3 ()Z public final fun component4 ()Lio/github/linreal/cascade/editor/CrashPolicy; public final fun component5 ()Lkotlin/jvm/functions/Function1; public final fun component6 ()Z - public final fun copy (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;Z)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; - public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig;ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZILjava/lang/Object;)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; + public final fun component7 ()Z + public final fun copy (ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZ)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; + public static synthetic fun copy$default (Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig;ZZZLio/github/linreal/cascade/editor/CrashPolicy;Lkotlin/jvm/functions/Function1;ZZILjava/lang/Object;)Lio/github/linreal/cascade/editor/ui/CascadeEditorConfig; public fun equals (Ljava/lang/Object;)Z public final fun getBlockDraggingEnabled ()Z public final fun getBlockIndentationEnabled ()Z public final fun getBlockSelectionEnabled ()Z public final fun getCrashPolicy ()Lio/github/linreal/cascade/editor/CrashPolicy; + public final fun getEmptyDocumentPlaceholderEnabled ()Z public final fun getOnInternalError ()Lkotlin/jvm/functions/Function1; public final fun getReadOnly ()Z public fun hashCode ()I diff --git a/editor/api/editor.klib.api b/editor/api/editor.klib.api index c0dd4af..0d8333a 100644 --- a/editor/api/editor.klib.api +++ b/editor/api/editor.klib.api @@ -2717,7 +2717,7 @@ final class io.github.linreal.cascade.editor.theme/CascadeEditorBlockStrings { / } final class io.github.linreal.cascade.editor.theme/CascadeEditorColors { // io.github.linreal.cascade.editor.theme/CascadeEditorColors|null[0] - constructor (androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...) // io.github.linreal.cascade.editor.theme/CascadeEditorColors.|(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] + constructor (androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...) // io.github.linreal.cascade.editor.theme/CascadeEditorColors.|(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] final val codeBlockBackground // io.github.linreal.cascade.editor.theme/CascadeEditorColors.codeBlockBackground|{}codeBlockBackground[0] final fun (): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.codeBlockBackground.|(){}[0] @@ -2735,6 +2735,8 @@ final class io.github.linreal.cascade.editor.theme/CascadeEditorColors { // io.g final fun (): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.linkText.|(){}[0] final val onPrimary // io.github.linreal.cascade.editor.theme/CascadeEditorColors.onPrimary|{}onPrimary[0] final fun (): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.onPrimary.|(){}[0] + final val placeholderText // io.github.linreal.cascade.editor.theme/CascadeEditorColors.placeholderText|{}placeholderText[0] + final fun (): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.placeholderText.|(){}[0] final val popupBackground // io.github.linreal.cascade.editor.theme/CascadeEditorColors.popupBackground|{}popupBackground[0] final fun (): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.popupBackground.|(){}[0] final val primary // io.github.linreal.cascade.editor.theme/CascadeEditorColors.primary|{}primary[0] @@ -2785,6 +2787,7 @@ final class io.github.linreal.cascade.editor.theme/CascadeEditorColors { // io.g final fun component22(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component22|component22(){}[0] final fun component23(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component23|component23(){}[0] final fun component24(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component24|component24(){}[0] + final fun component25(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component25|component25(){}[0] final fun component3(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component3|component3(){}[0] final fun component4(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component4|component4(){}[0] final fun component5(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component5|component5(){}[0] @@ -2792,7 +2795,7 @@ final class io.github.linreal.cascade.editor.theme/CascadeEditorColors { // io.g final fun component7(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component7|component7(){}[0] final fun component8(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component8|component8(){}[0] final fun component9(): androidx.compose.ui.graphics/Color // io.github.linreal.cascade.editor.theme/CascadeEditorColors.component9|component9(){}[0] - final fun copy(androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...): io.github.linreal.cascade.editor.theme/CascadeEditorColors // io.github.linreal.cascade.editor.theme/CascadeEditorColors.copy|copy(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] + final fun copy(androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...): io.github.linreal.cascade.editor.theme/CascadeEditorColors // io.github.linreal.cascade.editor.theme/CascadeEditorColors.copy|copy(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // io.github.linreal.cascade.editor.theme/CascadeEditorColors.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // io.github.linreal.cascade.editor.theme/CascadeEditorColors.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorColors.toString|toString(){}[0] @@ -2824,12 +2827,14 @@ final class io.github.linreal.cascade.editor.theme/CascadeEditorDimensions { // } final class io.github.linreal.cascade.editor.theme/CascadeEditorStrings { // io.github.linreal.cascade.editor.theme/CascadeEditorStrings|null[0] - constructor (kotlin/String, kotlin/Function1, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/Function1 = ...) // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.|(kotlin.String;kotlin.Function1;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.Function1){}[0] + constructor (kotlin/String, kotlin/Function1, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/Function1 = ..., kotlin/String = ...) // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.|(kotlin.String;kotlin.Function1;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.Function1;kotlin.String){}[0] final val back // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.back|{}back[0] final fun (): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.back.|(){}[0] final val bold // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.bold|{}bold[0] final fun (): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.bold.|(){}[0] + final val emptyDocumentPlaceholder // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.emptyDocumentPlaceholder|{}emptyDocumentPlaceholder[0] + final fun (): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.emptyDocumentPlaceholder.|(){}[0] final val hideKeyboard // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.hideKeyboard|{}hideKeyboard[0] final fun (): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.hideKeyboard.|(){}[0] final val highlight // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.highlight|{}highlight[0] @@ -2877,6 +2882,7 @@ final class io.github.linreal.cascade.editor.theme/CascadeEditorStrings { // io. final fun component18(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component18|component18(){}[0] final fun component19(): kotlin/Function1 // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component19|component19(){}[0] final fun component2(): kotlin/Function1 // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component2|component2(){}[0] + final fun component20(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component20|component20(){}[0] final fun component3(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component3|component3(){}[0] final fun component4(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component4|component4(){}[0] final fun component5(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component5|component5(){}[0] @@ -2884,7 +2890,7 @@ final class io.github.linreal.cascade.editor.theme/CascadeEditorStrings { // io. final fun component7(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component7|component7(){}[0] final fun component8(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component8|component8(){}[0] final fun component9(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.component9|component9(){}[0] - final fun copy(kotlin/String = ..., kotlin/Function1 = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/Function1 = ...): io.github.linreal.cascade.editor.theme/CascadeEditorStrings // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.copy|copy(kotlin.String;kotlin.Function1;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.Function1){}[0] + final fun copy(kotlin/String = ..., kotlin/Function1 = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/Function1 = ..., kotlin/String = ...): io.github.linreal.cascade.editor.theme/CascadeEditorStrings // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.copy|copy(kotlin.String;kotlin.Function1;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.Function1;kotlin.String){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // io.github.linreal.cascade.editor.theme/CascadeEditorStrings.toString|toString(){}[0] @@ -3027,8 +3033,7 @@ final class io.github.linreal.cascade.editor.ui/CascadeDocumentPreviewConfig { / } final class io.github.linreal.cascade.editor.ui/CascadeEditorConfig { // io.github.linreal.cascade.editor.ui/CascadeEditorConfig|null[0] - constructor (kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.editor/CrashPolicy = ..., kotlin/Function1? = ..., kotlin/Boolean = ...) // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.|(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.editor.CrashPolicy;kotlin.Function1?;kotlin.Boolean){}[0] - constructor (kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, io.github.linreal.cascade.editor/CrashPolicy, kotlin/Function1?) // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.|(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.editor.CrashPolicy;kotlin.Function1?){}[0] + constructor (kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.editor/CrashPolicy = ..., kotlin/Function1? = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.|(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.editor.CrashPolicy;kotlin.Function1?;kotlin.Boolean;kotlin.Boolean){}[0] final val blockDraggingEnabled // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.blockDraggingEnabled|{}blockDraggingEnabled[0] final fun (): kotlin/Boolean // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.blockDraggingEnabled.|(){}[0] @@ -3038,6 +3043,8 @@ final class io.github.linreal.cascade.editor.ui/CascadeEditorConfig { // io.gith final fun (): kotlin/Boolean // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.blockSelectionEnabled.|(){}[0] final val crashPolicy // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.crashPolicy|{}crashPolicy[0] final fun (): io.github.linreal.cascade.editor/CrashPolicy // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.crashPolicy.|(){}[0] + final val emptyDocumentPlaceholderEnabled // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.emptyDocumentPlaceholderEnabled|{}emptyDocumentPlaceholderEnabled[0] + final fun (): kotlin/Boolean // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.emptyDocumentPlaceholderEnabled.|(){}[0] final val onInternalError // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.onInternalError|{}onInternalError[0] final fun (): kotlin/Function1? // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.onInternalError.|(){}[0] final val readOnly // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.readOnly|{}readOnly[0] @@ -3049,7 +3056,8 @@ final class io.github.linreal.cascade.editor.ui/CascadeEditorConfig { // io.gith final fun component4(): io.github.linreal.cascade.editor/CrashPolicy // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.component4|component4(){}[0] final fun component5(): kotlin/Function1? // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.component5|component5(){}[0] final fun component6(): kotlin/Boolean // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.component6|component6(){}[0] - final fun copy(kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.editor/CrashPolicy = ..., kotlin/Function1? = ..., kotlin/Boolean = ...): io.github.linreal.cascade.editor.ui/CascadeEditorConfig // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.copy|copy(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.editor.CrashPolicy;kotlin.Function1?;kotlin.Boolean){}[0] + final fun component7(): kotlin/Boolean // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.component7|component7(){}[0] + final fun copy(kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.linreal.cascade.editor/CrashPolicy = ..., kotlin/Function1? = ..., kotlin/Boolean = ..., kotlin/Boolean = ...): io.github.linreal.cascade.editor.ui/CascadeEditorConfig // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.copy|copy(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;io.github.linreal.cascade.editor.CrashPolicy;kotlin.Function1?;kotlin.Boolean;kotlin.Boolean){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // io.github.linreal.cascade.editor.ui/CascadeEditorConfig.toString|toString(){}[0] diff --git a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorColors.kt b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorColors.kt index 0c4ab4d..ff77b52 100644 --- a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorColors.kt +++ b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorColors.kt @@ -92,6 +92,8 @@ public data class CascadeEditorColors( * data-class [copy] retains the existing value unless set explicitly. */ val toolbarBackground: Color = popupBackground, + /** Empty-document input hint text. */ + val placeholderText: Color = text, ) { public companion object { /** @@ -125,6 +127,7 @@ public data class CascadeEditorColors( error = Color(0xFFB3261E), codeBlockBackground = Color(0x0F000000), toolbarBackground = Color.White, + placeholderText = Color(0xFF74747B), ) /** @@ -158,6 +161,7 @@ public data class CascadeEditorColors( error = Color(0xFFF2B8B5), codeBlockBackground = Color(0x1FFFFFFF), toolbarBackground = Color(0xFF26262E), + placeholderText = Color(0xFFABABB2), ) } } diff --git a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorStrings.kt b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorStrings.kt index f72b1cf..dffa87d 100644 --- a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorStrings.kt +++ b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/theme/CascadeEditorStrings.kt @@ -11,6 +11,7 @@ private const val DEFAULT_LINK_CANCEL_LABEL = "Cancel" private const val DEFAULT_LINK_REMOVE_LABEL = "Remove Link" private const val DEFAULT_LINK_TITLE_LABEL = "Title" private const val DEFAULT_LINK_URL_LABEL = "URL" +private const val DEFAULT_EMPTY_DOCUMENT_PLACEHOLDER = "Start here\u2026" private val DEFAULT_LINK_VALIDATION_ERROR_LABEL: (LinkValidationError) -> String = { error -> when (error) { @@ -66,6 +67,8 @@ public data class CascadeEditorStrings( val linkUrl: String = DEFAULT_LINK_URL_LABEL, /** Maps stable link validation errors to localized UI text. */ val linkValidationError: (LinkValidationError) -> String = DEFAULT_LINK_VALIDATION_ERROR_LABEL, + /** Hint shown in the first block while the editable document is empty. */ + val emptyDocumentPlaceholder: String = DEFAULT_EMPTY_DOCUMENT_PLACEHOLDER, ) { public companion object { /** Default [unsupportedBlock] lambda — extracted as a singleton for stable equality. */ @@ -94,6 +97,7 @@ public data class CascadeEditorStrings( linkTitle = DEFAULT_LINK_TITLE_LABEL, linkUrl = DEFAULT_LINK_URL_LABEL, linkValidationError = DEFAULT_LINK_VALIDATION_ERROR_LABEL, + emptyDocumentPlaceholder = DEFAULT_EMPTY_DOCUMENT_PLACEHOLDER, ) } } diff --git a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/BackspaceAwareTextEdit.kt b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/BackspaceAwareTextEdit.kt index 9190951..362b35c 100644 --- a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/BackspaceAwareTextEdit.kt +++ b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/BackspaceAwareTextEdit.kt @@ -238,6 +238,15 @@ public fun TextFieldState.visibleText(): String { return text.toString().removePrefix(ZWSP) } +/** + * Returns whether the field has no user-visible text without allocating a + * visible-text [String]. The raw field may still contain the ZWSP sentinel. + */ +internal fun TextFieldState.isVisibleTextEmpty(): Boolean { + val rawText = text + return rawText.isEmpty() || (rawText.length == 1 && rawText[0] == ZWSP_CHAR) +} + /** * Returns the cursor position relative to visible text. */ diff --git a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorConfig.kt b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorConfig.kt index 42cd0bb..638782d 100644 --- a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorConfig.kt +++ b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorConfig.kt @@ -24,6 +24,9 @@ import io.github.linreal.cascade.editor.CrashPolicy * are disabled and block dragging preserves the payload's original * indentation. Ignored when [readOnly] is `true`, because read-only mode * disables indentation changes regardless of this value. + * @param emptyDocumentPlaceholderEnabled When `true`, an editable document + * containing only an empty root paragraph or heading shows the localized + * empty-document placeholder. Read-only mode always suppresses it. */ @Immutable public data class CascadeEditorConfig( @@ -42,26 +45,8 @@ public data class CascadeEditorConfig( */ val onInternalError: CascadeErrorReporter? = null, val blockIndentationEnabled: Boolean = true, + val emptyDocumentPlaceholderEnabled: Boolean = false, ) { - /** - * Compatibility constructor preserving the pre-indentation-configuration - * parameter order for binary callers. - */ - public constructor( - readOnly: Boolean, - blockSelectionEnabled: Boolean, - blockDraggingEnabled: Boolean, - crashPolicy: CrashPolicy, - onInternalError: CascadeErrorReporter?, - ) : this( - readOnly = readOnly, - blockSelectionEnabled = blockSelectionEnabled, - blockDraggingEnabled = blockDraggingEnabled, - crashPolicy = crashPolicy, - onInternalError = onInternalError, - blockIndentationEnabled = true, - ) - public companion object { /** Editable default used when callers do not provide a config. */ public val Default: CascadeEditorConfig = CascadeEditorConfig() diff --git a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/renderers/TextBlockField.kt b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/renderers/TextBlockField.kt index 5404245..e58263f 100644 --- a/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/renderers/TextBlockField.kt +++ b/editor/src/commonMain/kotlin/io/github/linreal/cascade/editor/ui/renderers/TextBlockField.kt @@ -3,11 +3,13 @@ package io.github.linreal.cascade.editor.ui.renderers import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.input.OutputTransformation import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -33,6 +35,7 @@ import io.github.linreal.cascade.editor.action.CloseSlashCommand import io.github.linreal.cascade.editor.action.UpdateSlashCommandSession import io.github.linreal.cascade.editor.core.Block import io.github.linreal.cascade.editor.core.BlockContent +import io.github.linreal.cascade.editor.core.BlockId import io.github.linreal.cascade.editor.registry.BlockCallbacks import io.github.linreal.cascade.editor.action.ConvertBlockType import io.github.linreal.cascade.editor.action.UpdateBlockContent @@ -61,12 +64,14 @@ import io.github.linreal.cascade.editor.ui.LocalSlashPopupItems import io.github.linreal.cascade.editor.ui.LocalSlashSessionAnchorBlockId import io.github.linreal.cascade.editor.ui.visibleSelection import io.github.linreal.cascade.editor.ui.visibleText +import io.github.linreal.cascade.editor.ui.isVisibleTextEmpty import io.github.linreal.cascade.editor.guarded import io.github.linreal.cascade.editor.ui.LocalCascadeEditorConfig import io.github.linreal.cascade.editor.state.TextEditHistoryTracker import io.github.linreal.cascade.editor.state.captureCheckpoint import io.github.linreal.cascade.editor.state.captureFocusedEditingUiState import io.github.linreal.cascade.editor.theme.LocalCascadeTheme +import io.github.linreal.cascade.editor.theme.LocalCascadeStrings /** * Shared text editing composable used by renderers that need formattable text input. @@ -96,6 +101,7 @@ internal fun TextBlockField( ) { val textContent = block.content as? BlockContent.Text ?: return val colors = LocalCascadeTheme.current.colors + val strings = LocalCascadeStrings.current val focusRequester = remember { FocusRequester() } var hasComposeFocus by remember { mutableStateOf(false) } val stateHolder = LocalEditorStateHolder.current @@ -148,6 +154,12 @@ internal fun TextBlockField( val crashConfig = LocalCascadeEditorConfig.current val crashPolicy = crashConfig.crashPolicy val crashReporter = crashConfig.onInternalError + val placeholderEligible = isEmptyDocumentPlaceholderCandidate( + enabled = crashConfig.emptyDocumentPlaceholderEnabled, + canEditText = interactionPolicy.canEditText, + blocks = stateHolder.state.blocks, + blockId = block.id, + ) val outputTransformation: OutputTransformation? = remember( block.id, spanState, @@ -512,6 +524,13 @@ internal fun TextBlockField( } Box(modifier = modifier) { + EmptyDocumentPlaceholder( + eligible = placeholderEligible, + state = textFieldState, + text = strings.emptyDocumentPlaceholder, + style = textStyle.copy(color = colors.placeholderText), + ) + BackspaceAwareTextField( state = textFieldState, modifier = Modifier.fillMaxWidth() @@ -603,6 +622,47 @@ internal fun TextBlockField( } } +@Composable +private fun EmptyDocumentPlaceholder( + eligible: Boolean, + state: TextFieldState, + text: String, + style: TextStyle, +) { + val visible by remember(eligible, state) { + derivedStateOf { + eligible && state.isVisibleTextEmpty() + } + } + if (visible && text.isNotEmpty()) { + BasicText( + text = text, + style = style, + ) + } +} + +/** + * Resolves the structural half of empty-document placeholder visibility. + * + * Live text emptiness is intentionally checked in a small child composition so + * ordinary typing does not invalidate the full text-field integration scope. + */ +internal fun isEmptyDocumentPlaceholderCandidate( + enabled: Boolean, + canEditText: Boolean, + blocks: List, + blockId: BlockId, +): Boolean { + if (!enabled || !canEditText || blocks.size != 1) return false + val onlyBlock = blocks[0] + val supportsPlaceholder = + onlyBlock.type == BlockType.Paragraph || onlyBlock.type is BlockType.Heading + return onlyBlock.id == blockId && + supportsPlaceholder && + onlyBlock.attributes.indentationLevel == 0 +} + private const val ZWSP_SENTINEL: Char = '\u200B' /** diff --git a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorColorsTest.kt b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorColorsTest.kt index 2d15fc6..05d0c8b 100644 --- a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorColorsTest.kt +++ b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorColorsTest.kt @@ -37,6 +37,14 @@ class CascadeEditorColorsTest { assertNotEquals(Color.Unspecified, colors.error) } + @Test + fun `light and dark differ on placeholderText`() { + assertNotEquals( + CascadeEditorColors.light().placeholderText, + CascadeEditorColors.dark().placeholderText, + ) + } + @Test fun `light codeBlockBackground is non-transparent`() { val colors = CascadeEditorColors.light() @@ -224,5 +232,6 @@ class CascadeEditorColorsTest { "error" to c.error, "codeBlockBackground" to c.codeBlockBackground, "toolbarBackground" to c.toolbarBackground, + "placeholderText" to c.placeholderText, ) } diff --git a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorConfigTest.kt b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorConfigTest.kt index 27714e0..6d1cd1f 100644 --- a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorConfigTest.kt +++ b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorConfigTest.kt @@ -14,12 +14,14 @@ class CascadeEditorConfigTest { assertTrue(CascadeEditorConfig.Default.blockSelectionEnabled) assertTrue(CascadeEditorConfig.Default.blockDraggingEnabled) assertTrue(CascadeEditorConfig.Default.blockIndentationEnabled) + assertFalse(CascadeEditorConfig.Default.emptyDocumentPlaceholderEnabled) assertEquals( CascadeEditorConfig( readOnly = false, blockSelectionEnabled = true, blockDraggingEnabled = true, blockIndentationEnabled = true, + emptyDocumentPlaceholderEnabled = false, ), CascadeEditorConfig.Default, ) diff --git a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorStringsTest.kt b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorStringsTest.kt index db9c0ff..565eb81 100644 --- a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorStringsTest.kt +++ b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/CascadeEditorStringsTest.kt @@ -57,6 +57,11 @@ class CascadeEditorStringsTest { assertTrue(strings.hideKeyboard.isNotEmpty()) } + @Test + fun `empty document placeholder is non-empty`() { + assertTrue(strings.emptyDocumentPlaceholder.isNotEmpty()) + } + @Test fun `indentForward is non-empty`() { assertTrue(strings.indentForward.isNotEmpty()) @@ -110,6 +115,13 @@ class CascadeEditorStringsTest { assertEquals(strings.bold, custom.bold) } + @Test + fun `copy with custom empty document placeholder works`() { + val custom = strings.copy(emptyDocumentPlaceholder = "Commencez ici\u2026") + + assertEquals("Commencez ici\u2026", custom.emptyDocumentPlaceholder) + } + @Test fun `copy with custom unsupportedBlock lambda works`() { val custom = strings.copy(unsupportedBlock = { "Unknown: $it" }) @@ -188,5 +200,6 @@ class CascadeEditorStringsTest { assertEquals("Remove Link", strings.linkRemove) assertEquals("Title", strings.linkTitle) assertEquals("URL", strings.linkUrl) + assertEquals("Start here\u2026", strings.emptyDocumentPlaceholder) } } diff --git a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/DocumentSerializationExtTest.kt b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/DocumentSerializationExtTest.kt index a7e68f4..cf74962 100644 --- a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/DocumentSerializationExtTest.kt +++ b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/DocumentSerializationExtTest.kt @@ -49,6 +49,17 @@ class DocumentSerializationExtTest { assertEquals("World", (decoded[1].content as BlockContent.Text).text) } + @Test + fun `empty document placeholder remains presentation-only`() { + val holder = EditorStateHolder(EditorState.withBlocks(listOf(Block.paragraph()))) + + val json = holder.toJson(BlockTextStates(), BlockSpanStates()) + val decoded = DocumentSchema.decodeFromString(json) + val text = assertIs(decoded.single().content) + + assertEquals("", text.text) + } + @Test fun `toJson runtime text override - captures updated text`() { val blockId = BlockId("b1") diff --git a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/SentinelGuardClassificationTest.kt b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/SentinelGuardClassificationTest.kt index 00ffd0f..33b8deb 100644 --- a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/SentinelGuardClassificationTest.kt +++ b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/SentinelGuardClassificationTest.kt @@ -1,7 +1,10 @@ package io.github.linreal.cascade.editor.ui +import androidx.compose.foundation.text.input.TextFieldState import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue /** * Pure-logic coverage for the ZWSP-sentinel-guard classifier inside @@ -117,4 +120,29 @@ class SentinelGuardClassificationTest { classifySentinelChange(original, new), ) } + + @Test + fun `sentinel-only field has empty visible text without allocation`() { + assertTrue(TextFieldState(zwsp).isVisibleTextEmpty()) + } + + @Test + fun `defensive raw empty field has empty visible text`() { + assertTrue(TextFieldState("").isVisibleTextEmpty()) + } + + @Test + fun `visible character after sentinel is not empty`() { + assertFalse(TextFieldState("${zwsp}x").isVisibleTextEmpty()) + } + + @Test + fun `single visible character without sentinel is not empty`() { + assertFalse(TextFieldState("x").isVisibleTextEmpty()) + } + + @Test + fun `whitespace counts as visible document content`() { + assertFalse(TextFieldState("${zwsp} ").isVisibleTextEmpty()) + } } diff --git a/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/renderers/EmptyDocumentPlaceholderTest.kt b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/renderers/EmptyDocumentPlaceholderTest.kt new file mode 100644 index 0000000..d763c5c --- /dev/null +++ b/editor/src/commonTest/kotlin/io/github/linreal/cascade/editor/ui/renderers/EmptyDocumentPlaceholderTest.kt @@ -0,0 +1,116 @@ +package io.github.linreal.cascade.editor.ui.renderers + +import io.github.linreal.cascade.editor.core.Block +import io.github.linreal.cascade.editor.core.BlockAttributes +import io.github.linreal.cascade.editor.core.BlockId +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class EmptyDocumentPlaceholderTest { + + private val paragraph = Block.paragraph().copy(id = BlockId("paragraph")) + + @Test + fun `editable enabled root paragraph is eligible`() { + assertTrue( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = true, + blocks = listOf(paragraph), + blockId = paragraph.id, + ) + ) + } + + @Test + fun `editable enabled root heading is eligible`() { + val heading = Block.heading(level = 1).copy(id = BlockId("heading")) + + assertTrue( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = true, + blocks = listOf(heading), + blockId = heading.id, + ) + ) + } + + @Test + fun `disabled or read-only editor is not eligible`() { + assertFalse( + isEmptyDocumentPlaceholderCandidate( + enabled = false, + canEditText = true, + blocks = listOf(paragraph), + blockId = paragraph.id, + ) + ) + assertFalse( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = false, + blocks = listOf(paragraph), + blockId = paragraph.id, + ) + ) + } + + @Test + fun `non-empty document structure is not eligible`() { + assertFalse( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = true, + blocks = listOf(paragraph, Block.paragraph()), + blockId = paragraph.id, + ) + ) + assertFalse( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = true, + blocks = listOf(Block.divider(), paragraph), + blockId = paragraph.id, + ) + ) + } + + @Test + fun `unsupported text block or indented first block is not eligible`() { + val todo = Block.todo().copy(id = paragraph.id) + val indented = paragraph.copy( + attributes = BlockAttributes(indentationLevel = 1), + ) + + assertFalse( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = true, + blocks = listOf(todo), + blockId = todo.id, + ) + ) + assertFalse( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = true, + blocks = listOf(indented), + blockId = indented.id, + ) + ) + } + + @Test + fun `candidate must be the only block id`() { + assertFalse( + isEmptyDocumentPlaceholderCandidate( + enabled = true, + canEditText = true, + blocks = listOf(paragraph), + blockId = BlockId("other"), + ) + ) + } +} diff --git a/editor/src/desktopTest/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorPlaceholderTest.kt b/editor/src/desktopTest/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorPlaceholderTest.kt new file mode 100644 index 0000000..536f5d3 --- /dev/null +++ b/editor/src/desktopTest/kotlin/io/github/linreal/cascade/editor/ui/CascadeEditorPlaceholderTest.kt @@ -0,0 +1,131 @@ +package io.github.linreal.cascade.editor.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runComposeUiTest +import androidx.compose.ui.unit.dp +import io.github.linreal.cascade.editor.core.Block +import io.github.linreal.cascade.editor.core.BlockId +import io.github.linreal.cascade.editor.state.BlockTextStates +import io.github.linreal.cascade.editor.state.rememberEditorState +import io.github.linreal.cascade.editor.theme.CascadeEditorStrings +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class CascadeEditorPlaceholderTest { + + @Test + fun `new empty document shows localized placeholder and follows live text`() = + runComposeUiTest { + val block = Block.paragraph().copy(id = BlockId("empty")) + val textStates = BlockTextStates() + val placeholder = "Commencez ici\u2026" + + setContent { + val holder = rememberEditorState(listOf(block)) + CascadeEditor( + stateHolder = holder, + textStates = textStates, + strings = CascadeEditorStrings.default().copy( + emptyDocumentPlaceholder = placeholder, + ), + config = CascadeEditorConfig( + emptyDocumentPlaceholderEnabled = true, + ), + toolbar = ToolbarSlot.None, + modifier = Modifier.width(320.dp).height(200.dp), + ) + } + + waitForIdle() + onNodeWithText(placeholder).assertExists() + + runOnIdle { + textStates.setText(block.id, "A") + } + onNodeWithText(placeholder).assertDoesNotExist() + + runOnIdle { + textStates.setText(block.id, "") + } + onNodeWithText(placeholder).assertExists() + } + + @Test + fun `non-empty and trailing scaffold documents do not show placeholder`() = + runComposeUiTest { + setContent { + Column { + CascadeEditor( + stateHolder = rememberEditorState(listOf(Block.paragraph("Existing"))), + config = CascadeEditorConfig( + emptyDocumentPlaceholderEnabled = true, + ), + toolbar = ToolbarSlot.None, + modifier = Modifier.width(320.dp).height(160.dp), + ) + CascadeEditor( + stateHolder = rememberEditorState(listOf(Block.divider())), + config = CascadeEditorConfig( + emptyDocumentPlaceholderEnabled = true, + ), + toolbar = ToolbarSlot.None, + modifier = Modifier.width(320.dp).height(160.dp), + ) + } + } + + waitForIdle() + onAllNodesWithText("Start here\u2026").assertCountEquals(0) + } + + @Test + fun `empty heading shows placeholder when enabled`() = + runComposeUiTest { + setContent { + CascadeEditor( + stateHolder = rememberEditorState(listOf(Block.heading(level = 1))), + config = CascadeEditorConfig( + emptyDocumentPlaceholderEnabled = true, + ), + toolbar = ToolbarSlot.None, + modifier = Modifier.width(320.dp).height(160.dp), + ) + } + + waitForIdle() + onNodeWithText("Start here\u2026").assertExists() + } + + @Test + fun `default-disabled and read-only editors suppress placeholder`() = + runComposeUiTest { + setContent { + Column { + CascadeEditor( + stateHolder = rememberEditorState(), + toolbar = ToolbarSlot.None, + modifier = Modifier.width(320.dp).height(160.dp), + ) + CascadeEditor( + stateHolder = rememberEditorState(), + toolbar = ToolbarSlot.None, + config = CascadeEditorConfig( + readOnly = true, + emptyDocumentPlaceholderEnabled = true, + ), + modifier = Modifier.width(320.dp).height(160.dp), + ) + } + } + + waitForIdle() + onAllNodesWithText("Start here\u2026").assertCountEquals(0) + } +} diff --git a/iosConsumerSmokeTest/App/SmokeViewController.swift b/iosConsumerSmokeTest/App/SmokeViewController.swift index 0bbbc1e..cc31112 100644 --- a/iosConsumerSmokeTest/App/SmokeViewController.swift +++ b/iosConsumerSmokeTest/App/SmokeViewController.swift @@ -26,7 +26,9 @@ final class SmokeViewController: UIViewController { blockSelectionEnabled: true, blockDraggingEnabled: true, isDark: false, - crashPolicy: .rethrow + crashPolicy: .rethrow, + blockIndentationEnabled: true, + emptyDocumentPlaceholderEnabled: false ) return CascadeEditorController(initialJson: json, configuration: configuration) }() diff --git a/iosNativeSample/iosNativeSample/App/AppTheme.swift b/iosNativeSample/iosNativeSample/App/AppTheme.swift index 279a3f9..0545a83 100644 --- a/iosNativeSample/iosNativeSample/App/AppTheme.swift +++ b/iosNativeSample/iosNativeSample/App/AppTheme.swift @@ -211,7 +211,7 @@ struct AppTheme { ) } -/// Full 24-slot palettes demonstrate the native bridge without relying on the +/// Full 25-slot palettes demonstrate the native bridge without relying on the /// SDK's built-in light/dark colors. Values are standard 0xAARRGGBB Int64s. private func violetLightEditorColors() -> CascadeEditorColors { let colors = CascadeEditorColors(isDark: false) @@ -239,6 +239,7 @@ private func violetLightEditorColors() -> CascadeEditorColors { colors.error = 0xFFB3261E colors.codeBlockBackground = 0x0F6C3DE8 colors.toolbarBackground = 0xFFFFFFFF + colors.placeholderText = 0xFF746D85 return colors } @@ -268,6 +269,7 @@ private func violetDarkEditorColors() -> CascadeEditorColors { colors.error = 0xFFF2B8B5 colors.codeBlockBackground = 0x1F8B5CF6 colors.toolbarBackground = 0xFF26203A + colors.placeholderText = 0xFFAAA2BE return colors } @@ -297,6 +299,7 @@ private func forestLightEditorColors() -> CascadeEditorColors { colors.error = 0xFFB3261E colors.codeBlockBackground = 0x14147D64 colors.toolbarBackground = 0xFFFFFFFF + colors.placeholderText = 0xFF657A73 return colors } @@ -326,6 +329,7 @@ private func forestDarkEditorColors() -> CascadeEditorColors { colors.error = 0xFFF2B8B5 colors.codeBlockBackground = 0x1F67D4AF colors.toolbarBackground = 0xFF163229 + colors.placeholderText = 0xFFA4BDB4 return colors } diff --git a/iosNativeSample/iosNativeSample/Editor/EditorScreenModel.swift b/iosNativeSample/iosNativeSample/Editor/EditorScreenModel.swift index 9137806..d8ed78d 100644 --- a/iosNativeSample/iosNativeSample/Editor/EditorScreenModel.swift +++ b/iosNativeSample/iosNativeSample/Editor/EditorScreenModel.swift @@ -88,7 +88,9 @@ extension CascadeEditorConfiguration { blockSelectionEnabled: blockSelectionEnabled, blockDraggingEnabled: blockDraggingEnabled, isDark: isDark, - crashPolicy: .containAndReport + crashPolicy: .containAndReport, + blockIndentationEnabled: true, + emptyDocumentPlaceholderEnabled: true ) } } diff --git a/sample/src/commonMain/kotlin/io/github/linreal/cascade/screens/EditorDemoScreen.kt b/sample/src/commonMain/kotlin/io/github/linreal/cascade/screens/EditorDemoScreen.kt index 3b28b29..3a4d605 100644 --- a/sample/src/commonMain/kotlin/io/github/linreal/cascade/screens/EditorDemoScreen.kt +++ b/sample/src/commonMain/kotlin/io/github/linreal/cascade/screens/EditorDemoScreen.kt @@ -137,7 +137,10 @@ fun EditorDemoScreen( } runCatching { uriHandler.openUri(url) } }, - config = CascadeEditorConfig(readOnly = isReadOnly), + config = CascadeEditorConfig( + readOnly = isReadOnly, + emptyDocumentPlaceholderEnabled = true, + ), modifier = Modifier.fillMaxSize().imePadding(), ) }